Archive for April, 2009

Adobe AIR Marketplace, from Idea to Product in 4 hours - April 30, 2009 at 3:58 pm

I recently listed the desktop version of CardLasso on Adobe’s AIR Marketplace.  If you haven’t heard of AIR, check it out on the Adobe site.  It is the “Adobe Integrated Runtime” which allows you to run RIA apps on your desktop and distribute them with the ease of a web app.  Our AIR version of CardLasso is targeted at the Tradeshow user, and allows you to quickly gather business cards when you have no internet connection and save them locally.  You can sync them up to Lasso2Go later back at Starbucks or the hotel.

It seems like not a lot of people know about the AIR Marketplace so I wanted to write a quick note to raise awareness and let you know about the listing process.
First off, I really like their approach.  You can use your existing Adobe ID to create a publishing account on the marketplace, this took 10 seconds.

Next you enter publisher information to tell everyone a little about your company and upload a company logo.  Very fast and easy.  I was immediately approved as a publisher.

Then I filled out the listing for our application which included all the standard info you would expect, description, logos, links to download the app, etc…  (See below)

airmarketplace-screenshot

The only hard part about the listing process was sizing down graphics and screen shots to fit the requirements, but not a big deal.

Then I clicked submit.

Literally less than an hour later I had an email from Adobe with a few questions and suggestions on my listing.  I made the suggested changes and our product was live.
It was great to list a product on an online store and have it live a four hours after starting the listing process.  Granted it doesn’t have the volume that the Apple AppStore has, or the security implications that the Salesforce AppExchange listings have, but both Apple and Salesforce could learn a few things from how Adobe approaches the listing process.

It’s been live for a few days, check it out here and add a review to let us know what you think.

 

To Secure or Not to Secure - April 28, 2009 at 11:04 am

For CRM users and strategists, the inherent value of centralizing customer data for the purposes of collaboration and presenting a single face to the customer is well understood. The goal is to break down functional and individual silos to bring value to the customer relationship and encourage an open means of communication, management, planning and support. Current and historical customer data represents tremendously valuable intellectual property and institutional memory that lives beyond the attrition of any individual in the organization.

Having said that, what happens when the vision becomes a reality? What happens when this valuable IP is readily accessible at the fingertips of your sales reps? These questions are often met with a feeling of gratitude and accomplishment at first, but that feeling is soon followed by a sinking feeling that resembles that of the first time you realized that you left your teenager home alone for the weekend. You want to trust that they will do the right thing, but there is that part of you that is not so sure.

How does an organization balance the benefits of deploying on enterprise CRM solution with the challenges protecting corporate IP? If you open up your data to be completely “collaborative,” are you being reckless? If you lock it down into silos, are you any better off than you were before?

Remember your objectives
Before you hit the panic button and lock down the system to the point where there is no possibility for collaboration, remember your core objectives. The security model can be no more restrictive than the business processes that have been established. For example, customer service cannot better service the customer without any visibility into the activity surrounding that customer. When designing your system, include thoughtful steps to ensure that users have enough information to service their customer base, but this doesn’t necessarily mean that they need the keys to the entire castle. Categorize the data needs of individual and groups of users to make sure you are striking the correct balance.

Separate real security issues from paranoia
There are legitimate security concerns that impact the competitive position of the organization. There may also be legal obligations imposed by regulation or commitments to customers that must be honored as part of your design. These issues need to be accounted for in your requirements, and your CRM system’s security model must be robust enough to accommodate them.

A separate class of concern arises out of paranoia of what might happen if customer information is viewed, shared, printed, downloaded, or stolen. Security breaches are much more likely to come from an internal source than an external one. The decisions made to address this type of concern need to weigh the risks against the opportunity costs of locking down the data. The answers are generally not black and white, so be prepared to compromise and live with at least some level of risk.

Leverage technology
The tools used to manage customer data will allow you some flexibility to be creative in the information you choose to share vs. protect. As part of your data categorization effort, identify whether it is necessary to secure entire classes of data, only certain records, or maybe just particularly sensitive data fields. The trade-off here is that, the more exceptions you make, the more complex the administrative effort is going to be maintain the system.

Trust, but verify
When addressing change management, we generally try to suppress the notion of a CRM system being used as “Big Brother” to the sales force. However, there is some validity to the need to monitor activity and look for behavior that may create cause for concern. The use of activity reports and logging of usage information can be helpful in identifying trends.

When security concerns still remain, some organizations have drafted non-disclosure agreements and usage policies for all of their CRM users to sign. As a last resort, this can provide your organization with some legal protection and also helps to reinforce the sensitivity of the data with the users.

Disruptive Technology Changes The Game, Again! - April 27, 2009 at 4:33 pm

Remember When Video Killed The Radio Star?

By now we should recognize the signs of a major shift caused by a new, “disruptive” technology. After all, we have seen it happen so many times before. The only difference is that today the shifts are quick and dramatic, catching off guard anybody that dares to stand flatfooted. So what is causing the tremors this time? Cloud computing! And it is already hitting with a force that is off the scale, especially for organizations that are not looking for it or prefer to live in the “legacy” world of on-premises business computing strategy and applications.

Perhaps “Video Killed The Radio Star” is not the appropriate analogy to communicate a dramatic shift in computing technology, but for some reason that tune keeps playing in my head whenever I think about this evolution in computing. MTV was a dramatic shift that stopped careers for those unwilling to accept it. It opened the door for a new breed of performers that were willing to adapt. Yet it was only the pinnacle of the iceberg for all the technology changes that followed – from CDs replacing cassettes and vinyl, to iTunes and single song downloads.

Cutting edge performers had been putting out music videos since the Beatles’ Yellow Submarine, not to mention the entire Monkeys TV show (which drove their albums to the top of the charts!). But it was the new technology of cable television and MTV that was the game changer. Cloud computing is the cable television of IT world. IT directors and departments will either have to grasp and ride the movement of computing to web service providers such as saleforce.com, Amazon Web Services and Google Apps, or find themselves and their employers on the clearance shelf of the business world because they have become irrelevant in the market, just like 8-track tapes.

The newspaper industry has experienced this shift as a result of technology change that helps to illustrate this point. The static newspaper has been driven down by the dynamic web page and email alerts for news. You don’t have to own, staff, and maintain huge printing plants running around the clock in order to sell and deliver the news today. Corporate size is not the advantage in distributing the news as it once had been thanks to the internet. Wireless and handheld access to the Internet has further made the hard copy delivery model a competitive disadvantage. The result has been a proliferation of news and information sites, and a long line of newspaper bankruptcies. The down economy has served to accelerate this phenomenon.  Think about this analogy as it relates to cloud computing. You can read other Model Metrics blogs from our technologists that explain in detail how we are leveraging salesforce.com, Amazon Web Services (the business unit) and Google Apps to create an elastic, powerful, and extremely cost effective new world order of computing.

Google “Amazon Web Services” if you want another perspective. My thoughts here are to sound the alarm. There is a change coming and you need to look up now to “the cloud” to take advantage of this opportunity to achieve competitive advantage, and in some cases survival, before it is too late.

Video killed the radio star…Cloud computing is going to kill traditional IT. Those that look to the cloud will be the new winners.

Your CRM Risk Mitigation Plan - April 23, 2009 at 8:05 am

CRM implementation strategies are often decorated with goals like establishing “Holistic view of the customer”, “360 degree view of the customer”, and “One-stop shop”. These are ambitious goals when you consider everything that is required to effectively identify, secure, service, and maintain a customer. Ambitions should not be squelched by the enormity of the effort, but keep in mind that it is always better to work smart than it is to work hard.

In anticipation of building the perfect system for your sales organization, it is important to manage some common risks.

People don’t like change
I admit that this is a generality, but chances are there are a number of salespeople in your organization that fall into this camp. There are very few sales people that are excited to change the way they work in favor of a new process or new software that is going to change their life, particularly if they have not been exposed to CRM before. Whether they embrace change or not, there is a practical limit to the amount of change that can be absorbed at one time, so maintain sensitivity to this with a solid communications plan, a prioritized roadmap, and lots of training.

An inch deep and mile wide
By trying to incorporate a lot of functionality into your CRM tool, there is always the risk of sacrificing richness of functionality just to get everything under one roof. As the saying goes, “If a job is worth doing, it is worth doing well.” This is not to say that you should spend time over-engineering your system, but rather invest the time to understand what features and functions are most important to your users and ensure that they are incorporated.

Artificial intelligence
There is a balance between building a system that helps your organization effectively manage their customer relationships and that which completely eliminates the possibility of human error. You customers cannot be managed solely by technology, so avoid the trap of trying to build a system that is laden with alerts, email blasts, and security that is targeted at preventing users from ever making a mistake. Trying to anticipate every possible user error is guaranteed to slow progress and introduce technical over-engineering. This time would be better spent on your change management and training plan.

Reinventing the wheel
Some systems and applications just work well and are not worth the effort to rebuild or re-engineer into a single tool. For example, most CRM tools come with some ability to send email through the application; this does not mean that you should make plans to rid your salespeople of their email system.  In the same way, spellchecking ability should not be a queue to get rid of your word processing application. Instead, look for ways to integrate your legacy applications to leverage what they do best while making for the most intuitive use-model as possible for your users.

Replicating data
In the world of the web, the term integration does not always mean replicating data. Sure, there are some valid reasons to use ETL tools to move data from one data store to another, but each integration case needs some analysis. In the SaaS world, it is faster, cheaper, and more effective to integrate at the user interface layer through mash-ups and dashboards. When possible, leave the data where in the data warehouse or the system of record, and leverage services to access the data and visual tools to represent the data in your CRM application.

You CRM system can be a powerful enabler for you sales people and your organization as a whole. A solid vision will be a valuable tool in guiding the path to long term success, but consideration of these stumbling points should help accelerate your progress.

CRM Success Rule #11 - Plain Old Conviction - April 16, 2009 at 4:02 pm

What do Siebel, RightNow, Oracle CRM, Microsoft Dynamics, and Salesforce.com all have in common? The answer is that none of them will change your business and further your CRM effectiveness all by themselves. If you are looking at any of these systems as being the savior for your organization, you are going to find yourself disappointed unless you augment your system with some conviction.

The benefits of CRM are well documented. Business of all sizes have, even in a down economy, made the investment in technologies and processes that will allow them to be more efficient, more effective, and more ROI focused in their sales and marketing activity. The recent economic slide has practically required that business leaders get serious about nurturing their existing customers and establish a sound plan for attracting new ones. However, the implementation of a CRM system does not, by itself, make for a CRM strategy.

The business problems (or opportunities) that drive the implementation of CRM foster deep-rooted convictions of the organization. These convictions represent the foundation from which the CRM strategy needs to be derived. Examples of common convictions include:

  • customer retention
  • margin retention
  • market penetration
  • growth rate
  • reduction in sales cycle time
  • response rate on new leads
  • improved service levels
  • competitive strategy
  • market intelligence
  • management of key sales activities
  • and on and on and on….

The decisions that you make as part of your CRM solution implementation need to be targeted at accomplishing your strategy as you have defined it. User adoption and overcoming technological hurdles are important, but must not overshadow the core business needs that drive you to this investment.

When establishing your roadmap, make sure to communicate, design, train, reinforce, and manage to your convictions. Make no mistake that your success or failure is going to be more a function of your strategy in accomplishing these goals than it will by features and functionality of your software purchase.

Model Metrics Opens Sixth salesforce.com Authorized Training Center in Seattle - - April 16, 2009 at 10:01 am

CHICAGO & SEATTLE – April 16, 2009 – Model Metrics, a leading Cloud Computing technology and services company and premier partner of salesforce.com, today announced the company has been selected to operate salesforce.com’s authorized training center (ATC) for the greater Seattle, Washington area.

<Link to press release>

Force.com Flex Toolkit AIRConnection updates - April 10, 2009 at 9:36 am

We’ve been working more and more with Adobe AIR and Salesforce.com recently, and as such we’ve been putting the AIRConnection class through its paces, fixing bugs and adding functionality along the way. Here are a few of the modifications we’ve made, and why:

SQL Reserved Words

One of the first things that we noticed was that we needed to amend the the SOQL_To_SQL() method in order to make sure SQLite queries didn’t bomb when querying Case objects. "Case", unfortunately, is a reserved word in SQL, but not in SOQL, so the query doesn’t translate perfectly. An easy fix for this is to always add back-ticks around the table name in the SQL statement, which is accomplished easily enough with a regular expression:

        private function SOQL_To_SQL(soql:String):String {
            //TG: We need to put back-ticks around the table names because

            //Case is an SQL reserved word, which causes bad things.
            //We should probably eventually back-tick column names as well,
            //but this is good enough until somebody names a
            //column with a reserved word, I guess.
            var sqlRegEx:RegExp = /FROM\s+(\S*)/gi;
            var sql:String = soql.replace( sqlRegEx, "FROM `$1`");

As noted, it would be a good idea to put back-ticks around the column names too, but we haven’t run into any reserved words in column names, and well, "if it ain’t broke…"

 Database Encryption

This is a pretty easy modification since the SQLConnection class allows you to encrypt the database by simply providing an encryption key to the open() method. We basically just added an encryptionKey:ByteArray parameter to the AIRConnection login() method, since this is where the SQLite database is first created or opened (if it already exists):

        public override function login(lreq:LoginRequest,encryptionKey:ByteArray=null):void {
 

This allows the parent application to create and provide an optional encryption key using whatever method is preferred by the developer, and the default of null ensures that we don’t break any existing applications using this library. Since this is an override function (AIRConnection extends Connection), you’ll want to add the same parameter to the login() method of the Connection class as well. That goes for any modification to an override method in the AIRConnection class. From there, we just have to pass the encryptionKey along to the openSQLiteDatabase method:

 

              if (openDatabases[lreq.username] == undefined)
              {
                if(encryptionKey==null)
                {
                    openSQLiteDatabase(lreq.username);
                }
                else
                {
                    //encrypt the db (or, if it exists, open it with the provided key)
                    openSQLiteDatabase(lreq.username,false,encryptionKey);
                }
              }

 Then, in the openSQLiteDatabase() method, we have to include the encryptionKey in the SQLConnection open() method:


                syncSQL.open(file, SQLMode.CREATE, false, 1024, encryptionKey);
 

From this point on, to open the SQLite database, you’ll need to use the encryption key, so it’s probably a good idea to either save it somewhere, or make sure you can reliably regenerate it, based on some user-provided information, like a username and password. If you’re looking for an SQLite DB admin tool that will allow you to provide an encryption key, I recommend Lita. It’s an AIR app, and all of the encryption stuff works.

openSQLiteConnection Mods

The openSQLiteConnection method has a few random bugs that cause things not to work properly with very large data sets. For instance, the answer to this question in the stock code…

            /* do we need this?
            if (openDatabases[dbName])
            {
              sql.close();
              openDatabases[dbName] = false;
            }
            */

 …is yes:

            /* do we need this?*/
            //TG: Yes we do. We want to close and re-open the database connection after we create the schema
            //    because the schema is created with the async connection, and after it is created, the sync
            //    connection won’t work anymore because the schema will have changed.  RefreshDatabase is the
            //    public method that does this.
            if (openDatabases[dbName])
            {
              sql.close();
              openDatabases[dbName] = false;
            }

           

Additionally, in the stock code opens up the asynchronous connection to the database before the synchronous connection, which can cause a crash because we can’t guarantee that the async open will be completed by the time the sync open is called, so the database may not exist yet. Reordering the two open statements is a simple fix:

            //TG: Note, we need to do the sync open before the async open because we can’t guarantee
            //    that the async will be completed by the time the sync open is called (i.e. the database
            //    won’t exist. This seems to mainly be a problem when using encryption. Alterately, we could
            //    use the openAsync responder, but re-ordering the two statements works fine.
           
            syncSQL = new SQLConnection();
            if(encryptionKey != null)
                syncSQL.open(file, SQLMode.CREATE, false, 1024, encryptionKey);
            else
                syncSQL.open(file);
           
           
            //open a second connection for asynchronous calls           
            if(encryptionKey != null)
                sql.openAsync(file, SQLMode.CREATE, null, false, 1024, encryptionKey);
            else
                sql.openAsync(file);

 

SQLite Serialization on OSX

One major problem we ran into recently is that there is a bug with AIR 1.5 on OSX that causes an application to crash when you try to deserialize an object (as opposed to a primitive data type) from an SQLite database. Serializing the object works fine, but trying to run a SELECT statement on a row that has a serialized object in it will cause SQLite to bomb. This means that  some of the fields in the _describe_sobject_cache table will crash an application, namely "childRelationships", "fields", and "recordTypeInfos". Until the AIR bug is fixed, a workaround is to do the serialization yourself, and store the serialized object as a blob. First, you’ll want to modify the CREATE statement in openSQLiteDatabase as follows:

 

            createDescribeSObjectCacheTable.text = "create table if not exists " + DESCRIBE_SOBJECT_TABLE +
              " (id integer primary key autoincrement, activateable boolean, childRelationships text, childRelationshipsBytes blob, createable boolean, custom boolean, " +
              " deletable boolean, fields text, fieldsBytes blob, keyPrefix text, layoutable boolean, label text, labelPlural text, mergeable boolean, name text, queryable boolean, " +
              " recordTypeInfos text, recordTypeInfosBytes blob, replicateable boolean, retrieveable boolean, searchable boolean, undeletable boolean, updateable boolean, urlDetail text, " +
              " urlEdit text, urlNew text)";

 

Notice the addition of these fields: "childRelationshipsBytes", "fieldsBytes", and "recordTypeInfosBytes". We’ll then want to add a method to serialize any given object:

        //serialize an object…
        //this is necessary because with the OSX version of AIR 1.5, apps will crash when you try to deserialize an object
        //from the SQLite database.
        public function serializeObject(someObject:Object):ByteArray
        {
            var objectBytes:ByteArray = new ByteArray();
              objectBytes.writeObject(someObject);
              return objectBytes;
        }

And then we need to use our new serializeObject() method in cacheDescribeSObjectResult(). First we need to add those fields to our INSERT and UPDATE statements:

              if (getCachedDescribeSObjectResultByType(d.name) != null) {
                // update
                dbStatement.text = "update " + DESCRIBE_SOBJECT_TABLE + " set activateable = :activateable, childRelationshipsBytes = :childRelationshipsBytes, " +
                      "createable = :createable, custom = :custom, deletable = :deletable, fieldsBytes = :fieldsBytes, keyPrefix = :keyPrefix, layoutable = :layoutable, " +
                      "label = :label, labelPlural = :labelPlural, mergeable = :mergeable, queryable = :queryable, recordTypeInfosBytes = :recordTypeInfosBytes, " +
                      "replicateable = :replicateable, retrieveable = :retrieveable, searchable = :searchable, undeletable = :undeletable, " +
                      "updateable = :updateable, urlDetail = :urlDetail, urlEdit = :urlEdit, urlNew = :urlNew  where name = :name";
              } else {
                // insert
                dbStatement.text = "insert into " + DESCRIBE_SOBJECT_TABLE + " (activateable, childRelationshipsBytes, createable, custom, deletable, fieldsBytes, " +
                      "keyPrefix, layoutable, label, labelPlural, mergeable, name, queryable, recordTypeInfosBytes, replicateable, retrieveable, " +
                      "searchable, undeletable, updateable, urlDetail, urlEdit, urlNew) values (:activateable, :childRelationshipsBytes, :createable, :custom, :deletable, :fieldsBytes, " +
                      ":keyPrefix, :layoutable, :label, :labelPlural, :mergeable, :name, :queryable, :recordTypeInfosBytes, :replicateable, :retrieveable, " +
                      ":searchable, :undeletable, :updateable, :urlDetail, :urlEdit, :urlNew)";
              }

 

And then instead of this line:

              dbStatement.parameters[":childRelationships"] = d.childRelationships;
 

We’ll want to do this:

              dbStatement.parameters[":childRelationshipsBytes"] = serializeObject(d.childRelationships);            
 

Same goes for "fields" and "recordTypeInfos". Ultimately, then, we’ll have to deserialize these objects ourselves too, so in getCachedDescribeSObjectResultByType(), we need to add a few lines to do that:

        private function getCachedDescribeSObjectResultByType(type:String):DescribeSObjectResult {
            var dbStatement:SQLStatementExt = new SQLStatementExt(syncSQL);
           
              dbStatement.text = "select * from " + DESCRIBE_SOBJECT_TABLE + " where name = :name";
              logger.debug(dbStatement.text);
              dbStatement.parameters[":name"] = type;
              dbStatement.itemClass = DescribeSObjectResult;
              dbStatement.execute();
         
              var result:SQLResult = dbStatement.getResult();
         
              if ((result != null) && (result.data != null) && (result.data.length > 0)) {
                  var dsr:DescribeSObjectResult = result.data[0];
                 
                  //TG deserialize:
                  dsr.fields = dsr.fieldsBytes.readObject();
                  dsr.childRelationships = dsr.childRelationshipsBytes.readObject();
                  dsr.recordTypeInfos = dsr.recordTypeInfosBytes.readObject();

                 
                if (dsr.fieldMap == null) {
                    dsr.fieldMap = {};
                    for each (var f:Object in dsr.fields)
                    {
                        dsr.fieldMap[f.name] = f;
                    }
                }                 
                return dsr;
              }
         
              return null;
        }

 

And then, since the dbStatement result is cast to a type of DescribeSObjectResult, we need to add these three new fields to that class:

     public dynamic class DescribeSObjectResult
    {
        public var activateable:Boolean;
        public var childRelationships:Array;
        public var childRelationshipsBytes:ByteArray;
        public var createable:Boolean;
        public var custom:Boolean;
        public var deletable:Boolean;
        public var fields:Array;
        public var fieldsBytes:ByteArray;
        public var keyPrefix:String;
        public var layoutable:Boolean;
        public var label:String;
        public var labelPlural:String;
        public var mergeable:Boolean;
        public var name:String;
        public var queryable:Boolean;
        public var recordTypeInfos:Array;
        public var recordTypeInfosBytes:ByteArray;

Online Caching

One of the biggest and most useful changes we made to AIRConnection was to make sure queries are reflected in the local database when remote SOQL insert and update statements are made ONline. In the stock AIRConnection class, local inserts and updates are only made when made offline, so if you are querying the local database directly using the public asynchronous or synchronous connection accessor methods, your queries won’t reflect the previous updates you’ve made remotely. This is solved easily for updates by simply doing the update both to SFDC and to the local SQLite database:

       override public function update(sobjects:Array, callback:IResponder):void
        {
             logger.debug(’updating ‘ + ObjectUtil.toString(sobjects));
             var apex:Connection=super;callback
       
             if (super.loginResult && connected)
             {
                 //online
                 super.update(sobjects,callback); 
               
                //TG: cache the data locally too
                //I thought of adding a switch to update so data isn’t always cached locally, but I can’t think of any reason to do so…
                //may as well keep the local data in sync with the remote data.
                if ( !syncSQL) { logger.debug("no open database to store into"); return }       
                if (sobjects.length > 0)
                {
                    for( var i:int=0; i<sobjects.length; i++ )
                    {
                        syncSQL.begin();
                        var dbCacheStatement:SQLStatementExt = new SQLStatementExt(syncSQL, sobjects[i]);
                        // create the update statement
                        dbCacheStatement.text = buildUpdateTableStatement(sobjects[i]);
                        // write record to table ( assumes table exists)
                        logger.debug(dbCacheStatement.text);
                        dbCacheStatement.execute(); 
                       
                       
                        syncSQL.commit();
                       
                    }
                }

             }
             else
             {
                 //offline
                // we are offline or not yet logged in
                if (!connected) { logger.debug( ‘create made while offline’); }
                else if ( ! super.loginResult ) { logger.debug(’user not yet logged in’); }
               
                if ( !syncSQL) { logger.debug("no open database to store into"); return }       
                if (sobjects.length > 0) {
                    for( var j:int=0; j<sobjects.length; j++ ) {
                        syncSQL.begin();
                        var dbStatement:SQLStatementExt = new SQLStatementExt(syncSQL, sobjects[j]);
                        // create the update statement
                        dbStatement.text = buildUpdateTableStatement(sobjects[j]);
                        // write record to table ( assumes table exists)
                        logger.debug(dbStatement.text);
                        dbStatement.execute();
       
                        // do not add this to the OFFLINE_UPDATES_TABLE if this is a new record
                        if ((sobjects[j].Id as String).indexOf(NEW_RECORD_TEMP_ID_PREFIX) != 0) {
                            var insertOfflineUpdate:SQLStatementExt = new SQLStatementExt(syncSQL);
                            insertOfflineUpdate.text = "insert into " + OFFLINE_UPDATES_TABLE + " (table_name, record_id) values (:table_name, :record_id)";
                            insertOfflineUpdate.parameters[":table_name"] = sobjects[j].type;
                            insertOfflineUpdate.parameters[":record_id"] = sobjects[j].Id;
                            logger.debug(insertOfflineUpdate.text);
                            insertOfflineUpdate.execute();
                        }
                       
                        syncSQL.commit();
                       
                        dispatchEvent(new Event("updateSyncingOfflineUpdates"));
                    }
                }
               
                // todo: return something more meaningful, but what?
                (callback as AsyncResponder).resultHandler({message: OFFLINE_UPDATE_SUCCEEDED });
            }
        }

Caching of inserts takes a bit more effort because if we’re online, an Id is automatically generated by SFDC, along with any auto-populated fields. So, an API call of create has to be followed by a query in order to make sure the local data is updated properly. This involves hijacking the callback passed in by the parent application:

        //TG: queryAfterCreate is used to automatically query the result from SFDC after the Create operation has
        //    completed. This is useful because not only do we pull down the Id of the newly created record, we also
        //    pull down any auto-filled fields.
        public override function create( sobjects:Array, callback:IResponder, queryAfterCreate:Boolean=false):void {
            var apex:Connection=super;
           
            if (super.loginResult && connected)
            {
                //TG: do we want to pull down the full record from SFDC after we create it? Default is false because that’s how it comes from SFDC
                if(!queryAfterCreate)
                {
                    //just do what the Flex lib normally does…
                    super.create(sobjects,callback);
                }
                else
                {
                    //TG: hijack the callback to grab the ID from SFDC, so we cache this record locally
                    super.create(sobjects, new AsyncResponder(
                        function(result:Object):void
                        {
                            for(var i:Number=0;i<result.length;i++)
                            {                        
                                if(result[i].success)
                                {
                                    //add the object type to the result object so the calling application knows what type of object to refresh
                                    result[i]['type']=sobjects[i].type;

                                   
                                    trace("cache this create result");
                                    //get the ID returned by SFDC, and create the local record
                                    sobjects[i].Id=result[i].id;
                                   
                                    //first, we need to get a list of fields to query from the _describe_sobject_cache table
                                    var queryStatement:SQLStatement = new SQLStatement();
                                    queryStatement.sqlConnection = syncSQL;
                                    queryStatement.text = "SELECT fieldsBytes FROM _describe_sobject_cache WHERE name = ‘"+sobjects[i].type+"’";
                                    queryStatement.execute();                               
                                   
                                    var fieldsResult:Array = queryStatement.getResult().data;
                                    if(fieldsResult.length > 0)
                                    {
                                        var fields:Object = fieldsResult[0].fieldsBytes.readObject();
                                   
                                        //build a list of fields to select from SFDC for this object
                                        var soqlFieldList:String = "";
                                        var soqlFieldList2:String = "";
                                        for(var aField:String in fields)
                                        {
                                            //is query length going to be more than 10000?
                                            if(("SELECT Id,"+soqlFieldList+aField+","+" FROM "+sobjects[i].type+" WHERE Id = ‘"+result[i].id+"’").length<10000)
                                            {
                                                if(aField.toLowerCase() != "id")
                                                    soqlFieldList+=aField + ",";
                                            }
                                            else
                                            {
                                                //query the rest…
                                                if(("SELECT Id,"+soqlFieldList2+aField+","+" FROM "+sobjects[i].type+" WHERE Id = ‘"+result[i].id+"’").length<10000)
                                                {
                                                    if(aField.toLowerCase() != "id")
                                                        soqlFieldList2+=aField + ",";
                                                }
                                                else
                                                {
                                                    //TODO: make this recursive, so it’s not limited to two queries…
                                                    break;
                                                }                                           
                                            }
                                        }
                                        //remove the last comma
                                        soqlFieldList = soqlFieldList.substr(0,soqlFieldList.length-1);
                                       
                                        //assemble the soql query string
                                        var soqlQueryStatement:String = "SELECT Id, "+soqlFieldList+" FROM "+sobjects[i].type+" WHERE Id = ‘"+result[i].id+"’";
                                       
                                        var soqlQueryStatement2:String;
                                        if(soqlFieldList2.length > 0)
                                        {
                                            soqlFieldList2 = soqlFieldList2.substr(0,soqlFieldList2.length-1);
                                            soqlQueryStatement2 = "SELECT Id, "+soqlFieldList2+" FROM "+sobjects[i].type+" WHERE Id = ‘"+result[i].id+"’";
                                        }
                                       
                                        //query SFDC for the complete record and then trigger the callback the user passed in
                                        query(soqlQueryStatement,new AsyncResponder(
                                            function(queryResult:Object):void
                                            {
                                                //any more to query?
                                                if(soqlFieldList2.length == 0)
                                                {
                                                    // call back the original method passed in
                                                    callback.result(result);
                                                }
                                                else
                                                {                                    
                                                    //more to do…
                                                    query(soqlQueryStatement2,new AsyncResponder(
                                                        function(queryResult2:Object):void
                                                        {
                                                            callback.result(result);
                                                        },
                                                        function(queryResult2:Object):void
                                                        {
                                                            //the create worked okay, but the query failed, so the local data will be out of sync…
                                                            //maybe not the biggest deal in the world, so just return the create result
                                                            trace("Create Query Error: "+queryResult2.faultstring);
                                                            callback.result(result);   
                                                        }
                                                    ),false);   
                                                }                
                                            },
                                            function(queryResult:Object):void
                                            {
                                                //the create worked okay, but the query failed, so the local data will be out of sync…
                                                //maybe not the biggest deal in the world, so just return the create result
                                                trace("Create Query Error: "+queryResult.faultstring);
                                                callback.result(result);   
                                            }
                                        ),false);
                                       
                                       
                                        //working insert code, but it’s being replaced with the query above
                                        /*syncSQL.begin();
                                   
                                        var dbStatement:SQLStatementExt = new SQLStatementExt(syncSQL, sobjects[i]);
                                   
                                        // create the insert statement
                                            dbStatement.text = buildInsertTableStatement(sobjects[i]);
                                   
                                        // write record to table ( assumes table exists)
                                        logger.debug(dbStatement.text);
                                   
                                        dbStatement.execute();
                                        syncSQL.commit();*/
                                    }
                                    else
                                    {
                                        //no fields to query from SFDC…dunno what happened, so just finish up and act like nothing happened.
                                        trace("no fields to query from SFDC");
                                        callback.result(result);   
                                    }
                                }
                                else
                                {
                                    callback.fault(result);   
                                }
                            }
                            //callback.result(result);         // call back the original method passed in
                        },
                        function (info:Object) :void {
                            callback.fault(info);     // incase this was passed also
                        } 
                    )); 
                   }
            }
            else
            {
                // we are offline or not yet logged in
                if (!connected) { logger.debug( ‘create made while offline’); }
                else if ( ! super.loginResult ) { logger.debug(’user not yet logged in’); }
               
                if ( !syncSQL) { logger.debug("no open database to store into"); return }       
                if (sobjects.length > 0) {
                            if (tableExists(sobjects[0].type)) {
                   
                        for( var j:int=0; j<sobjects.length; j++ ) {
                            // toss objects in
                            sobjects[j].Id = NEW_RECORD_TEMP_ID_PREFIX + UIDUtil.createUID();   // must be unique to the db
       
                            syncSQL.begin();
                            var dbStatement:SQLStatementExt = new SQLStatementExt(syncSQL, sobjects[j]);
                            // create the insert statement
                                dbStatement.text = buildInsertTableStatement(sobjects[j]);
                            // write record to table ( assumes table exists)
                            logger.debug(dbStatement.text);
                            //trace(dbStatement.text);
                            dbStatement.execute();
       
                            var insertOfflineCreate:SQLStatementExt = new SQLStatementExt(syncSQL);
                            insertOfflineCreate.text = "insert into " + OFFLINE_CREATES_TABLE + " (table_name, record_id) values (:table_name, :record_id)";
                            insertOfflineCreate.parameters[":table_name"] = sobjects[j].type;
                            insertOfflineCreate.parameters[":record_id"] = sobjects[j].Id;
                           
                            logger.debug(insertOfflineCreate.text);
                            insertOfflineCreate.execute();
                            syncSQL.commit();
                       
                            dispatchEvent(new Event("updateSyncingOfflineCreates"));
                                }
                            }
                }
               
                // todo: return something more meaningful, but what?
                (callback as AsyncResponder).resultHandler({message: OFFLINE_CREATE_SUCCEEDED});
            }
        }      

Anyway, these are just some handy updates that we’ve made recently. Many of them have been made in a bit of a hurry, so they could use some massaging, but hopefully they help somebody out.

Finally, a useful Salesforce Calendar on your iPhone - April 9, 2009 at 9:52 am

Calendar2GO Lite for Salesforce CRM on the iPhone

 

We just found out Calendar2GO Lite was approved by Apple.  This is a free little application that allows you to easily see your Salesforce calendar on your iPhone and shows all the events for the current week, broken out into days and is very similar to the home screen in Salesforce.  You can drill into an event to see the detail, whether it is recurring, it’s description, etc…

There is a full version that should be approved soon that adds several nice features such as a daily view, weekly view, linkable phone numbers, email addresses. 

Check it out and let us know what you think.  I’m curious how many other organizations use Salesforce as their company calendar.  Also, any interest in seeing this on a Blackberry?

 

 

 

Cloud Loader - “How To” - April 8, 2009 at 5:39 pm

We have had a lot of great interest in Cloud Loader, so the next question is "How do I start using it?"  Well, here is a simple 5 Step guide to get going with Cloud Loader to start doing integration for $0.10 an hour (or less with a reserved instance).

 

Step 1 – Sign Up for EC2
 aws-step1image

Step 2 – Sign into the EC2 Management Console

The Console is where you can go to create and manage your EC2 Instances.  Remember these are virtual servers and the console allows you to give them a static IP (Elastic IP), control the Security Group and stop and start your instances.  First off you need to create a new instance of the Cloud Loader AMI. 


Step 3 – Create a Cloud Loader Instance

Click on Instances→ Launch Instance→ Community AMIs and search on “Cloud_Loader” to find the Cloud Loader AMI.  Click the Select button.

 aws-step3image

After clicking Select you need to specify some specifics about your instance.  You will only need one instance and it can be a “Small” instance which will be fine to run the Cloud Loader.
 

aws_step3bimage
Next click on “Create” next to the Key Pair Name option, enter a name for your key pair and download the key pair.  This will be required to SSH into your Cloud Loader instance.

 aws_step3cimage
Next you need to pick a Security Group for the instance.  You can use the default one or optionally specify other settings (which ports will be open).  EC2 by default has all ports closed except 22 for SSH.

Last Click “Launch” to launch your instance.  This will take 3-4 minutes to boot and fully launch.

Step 4 – Connect to your Instance

If you are using a PC you will need to use an SSH client such as PUTTy to log into your instance.  Click here for Windows instructions.

If you are using a Mac you can use the native terminal to connect.  Click here for detailed instructions on how to set up your certs and bash profile.

Step 5 – Configure your Instance

(Remember this is built on Apex Data Loader, so look here to learn more about it)

•    After logging in go to the /usr/local/dataloader directory
•    Create an FTP user on the Cloud Loader instance to use for the inbound FTP of data (the .csv file you want to load into Salesforce).
•    Edit the config.properties file with your username and password for SFDC
      Note: You will need to include your security token at the end of your password if you don’t whitelist the Amazon Elastic IP within Salesforce
•    Edit the process-conf.xml file with the data loader processes you want to run
•    Edit the automate-dl script to configure the email address you would like to use to send reports to.
•    Setup a cron to run the automate-dl script (the way you schedule automated processes in UNIX)

Final Notes

Once you have tested this approach it is likely you will want to modify the script or approach to suit your own business need.  With EC2 you can use Cloud Loader as a base AMI and then configure your own AMI based upon it with your own unique additions. 

You may also like to utilize EBS (Elastic Block Storage) with Cloud Loader to have persistent storage of your configuration files, incoming integration files or log files.  We plan to add this functionality in an upcoming release.  If you have any further suggestions for Cloud Loader, please email us at:  support@modelmetrics.com

 

New Cloud Converter Feature: Import Your App from Excel (Beta) - April 6, 2009 at 8:47 am

Contact Reid by email (rcarlberg@modelmetrics.com) or Twitter (@ReidCarlberg).

I’ve been working on a new feature for Cloud Converter, the ability to import an app from an Excel spreadsheet. Here’s a walk through. You can use this for free, link code at the bottom.

You can add this to your org — DE, EE, UE — by creating a web tab with this URL:

 

http://ec2-75-101-163-49.compute-1.amazonaws.com:8080/mmimport/home.action?s={!$Api.Session_ID}&u={!$Api.Partner_Server_URL_150}

 

Note that this is a beta release — you should treat it as such. I recommend you try it out on a dev org or in a sandbox before doing something in a production org.

Also, you can see it’s hosted over on Amazon Web Services. It’s always possible that the URL will change to something more Model Metrics Esque when it goes GA at some unknown point in the future.