Build a Command Line App for Force.com with Ruby & Thor

May 1st, 2012

Over at CloudSpokes we write a lot of ruby code (CloudSpokes.com runs on Heroku in Ruby with Database.com as the backend) for debugging, data migration, utilities and more. We typically just write a quick ruby class and run it from the command line to execute the script. However, I thought it might be cooler to write a cli for Force.com that I could distribute to the team.

So if you know a little ruby, the process isn’t that difficult. All you need is the databasedoctom gem and thor, a simple tool for building self-documenting command line utilities in ruby. You then write your ruby code in the .thor file and execute if from the command line as you would any other cli.

So here are the available commands for the cli via thor’s help command. Feel free to fork the code at github.

This video walks you through the functionality, setup and code for the cli in case you want more detail. The main code is below after the jump.

Assuming you have ruby installed, you need to install the databasedotcom and thor gems. It’s very trivial and the instructions are available from their respective github repos. With thor, you can pass parameters to each task and setup method options. So for instance, in the thor file below, each task has an optional ‘-c’ switch where you specify the connection file to load thus making it easy to connect to different orgs. If you omit the switch, the default databasedotcom.yml file is loaded.

require 'databasedotcom'

class Utils < Thor
  
  desc "query SOQL", "runs a soql query and displays the value of each record's 'name' field"
  method_option :config_file, :type => :string, :default => "databasedotcom.yml",
    :aliases => "-c", :desc => "The name of the file containing the connection parameters."
  def query(soql)
    client = authenticate(options[:config_file])
    # execute the soql and iterate over the results to output the name
    client.query("#{soql}").each do |r|
      puts r.Name
    end
  end
  
  desc "export SOQL FIELDS FILE", "runs a soql query and exports the specified
comma separated list of fields to a comma separated file"
  method_option :config_file, :type => :string, :default => "databasedotcom.yml",
    :aliases => "-c", :desc => "The name of the file containing the connection parameters."
  def export(soql, fields, file)
    client = authenticate(options[:config_file])
    # query for records
    records = client.query("#{soql}")
    # open the file to write (probably local directory)
    File.open(file, 'w') do |f|
      # interate over the records
      records.each do |r|
        # create a single line with all field values specified
        line = ''
        fields.split(',').each do |field|
          line += "#{eval("r.#{field}")},"
        end
        # write each line to the csv file
        f.puts "#{line}\n"
      end
    end
  end
  
  desc "describe OBJECT", "displays the describe info for a particular object"
  method_option :config_file, :type => :string, :default => "databasedotcom.yml",
    :aliases => "-c", :desc => "The name of the file containing the connection parameters."
  def describe(object)
    client = authenticate(options[:config_file])
    # call describe on the object by name
    sobject = client.describe_sobject(object)
    # output the results -- not very useful (frowny face)
    puts sobject
  end
  
  desc "get_token", "retreives an access token"
  method_option :config_file, :type => :string, :default => "databasedotcom.yml",
    :aliases => "-c", :desc => "The name of the file containing the connection parameters."
  def get_token
    client = authenticate(options[:config_file]).oauth_token
    puts "Access token: #{client}"
  end

  desc "show_config", "display the salesforce connection properties"
  method_option :config_file, :type => :string, :default => "databasedotcom.yml",
    :aliases => "-c", :desc => "The name of the file containing the connection parameters."
  def show_config
    config = YAML.load_file(options[:config_file])
    puts config
  end
  
  private
  
    def authenticate(file_name)
      # load the configuration file with connection parameters
      config = YAML.load_file(file_name)
      # init the databasedotcom gem with the specified yml config file
      client = Databasedotcom::Client.new(file_name)
      # pass the credentials to authenticate
      client.authenticate :username => config['username'], :password => config['password']
      return client
    end
  
end
view raw gistfile1.rb This Gist brought to you by GitHub.

The connection file is a simple yml file containing the parameters specified by the databasedotcom gem. You can have as many connection files as you would like to for production, developer and sandbox orgs and easily load different files using the ‘-c’ switch. For instance, to run the query task against the org specified in the sandbox.yml file you would run:

thor utils:query ‘select id, name from account limie 10′ -c ‘sandbox.yml’
client_id: YOUR-CLIENT-ID
client_secret: YOUR-CLIENT-SECRET
host: test.salesforce.com
debugging: false
username: YOUR-USERNAME
password: YOUR-PASSWORD
view raw gistfile1.txt This Gist brought to you by GitHub.
VN:F [1.9.15_1155]
Rating: 10.0/10 (1 vote cast)
VN:F [1.9.15_1155]
Rating: 0 (from 0 votes)

Categories: Ruby, Salesforce

1 Comment

Node.js Demo with Force.com REST API, OAuth & Express

April 27th, 2012

We’ve been working with Node.js quite a bit at CloudSpokes but I hadn’t done anything with Force.com and Node.js using their REST API; we’ve mostly been using our own API. So I thought I would give it a spin and see what it would take to write a small demo app using Node.js, the Force.com REST API, OAuth, Express and Jade for templating.

As it turns out it wasn’t that difficult. Salesforce.com has done most of the work writing the REST and OAuth pieces.

So initially I started woking from Josh Birk‘s FDC-NODEJS-HEROKU repo (don’t use this btw). I should have known from the beginning not to use this as the code hadn’t been updated in 9 months. Anyway a spent some time over the weekend getting it to work with Express, adding some middleware and rewriting some stuff. Once I finally got it working, I emailed Josh with a question and he informed me that the code was outdated and not to you use it.

Being the cool guy that Josh is, he pointed me to Dave Carroll‘s rest4dbdotcom repo for the latest and greatest code. This is definitely the repo you want to watch.

So I put together a small demo that allows you to authorize access to an org, get a list of accounts, create new accounts and update existing ones.

Here’s the link to the live application on heroku and the github repo with the code for your forking pleasure.

Most of the logic for the application is in the main app.js file (below) and you can see that it’s pretty straight forward. We set some configuration parameters for OAuth, configure the middleware, create the server and then define the routes that we can call.


/**
* Module dependencies.
*/
var express = require('express')
  , routes = require('./routes')
  , rest = require('./rest.js')
  , oauth = require('./oauth.js')
  , url = require('url');

/**
* Setup some environment variables (heroku) with defaults if not present
*/
var port = process.env.PORT || 3001; // use heroku's dynamic port or 3001 if localhost
var cid = process.env.CLIENT_ID || "YOUR-REMOTE-ACCESS-CONSUMER-KEY";
var csecr = process.env.CLIENT_SECRET || "YOUR-REMOTE-ACCESS-CONSUMER-SECRET";
var lserv = process.env.LOGIN_SERVER || "https://login.salesforce.com";
var redir = process.env.REDIRECT_URI || "http://localhost:" + port + "/token";

/**
* Middleware to call identity service and attach result to session
*/
function idcheck() {
  return function(req, res, next) {
    // Invoke identity service if we haven't got one or access token has
    // changed since we got it
      if (!req.session || !req.session.identity || req.session.identity_check != req.oauth.access_token) {
        rest.api(req).identity(function(data) {
          console.log(data);
          req.session.identity = data;
          req.session.identity_check = req.oauth.access_token;
          next();
        });
    } else {
      next();
    }
  }
}

/**
* Create the server
*/
var app = express.createServer(
    express.cookieParser(),
    express.session({ secret: csecr }),
    express.query(),
    oauth.oauth({
        clientId: cid,
        clientSecret: csecr,
        loginServer: lserv,
        redirectUri: redir,
    }),
  idcheck()
);

/**
* Configuration the server
*/
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

/**
* Routes
*/

 // 'home' page
app.get('/', routes.index);

// list of accounts - see routes/index.js for more info
app.get('/accounts', routes.accounts);

// form to create a new account
app.get('/accounts/new', function(req, res) {
  // call describe to dynamically generate the form fields
  rest.api(req).describe('Account', function(data) {
    res.render('new', { title: 'New Account', data: data })
  });
});

// create the account in salesforce
app.post('/accounts/create', function(req, res) {
  rest.api(req).create("Account", req.body.account, function(results) {
    if (results.success == true) {
      res.redirect('/accounts/'+results.id);
      res.end();
    }
  });
});

// display the account
app.get('/accounts/:id', function(req, res) {
  rest.api(req).retrieve('Account', req.params.id, null, function(data) {
    res.render('show', { title: 'Account Details', data: data });
  });
});

// form to update an existing account
app.get('/accounts/:id/edit', function(req, res) {
  rest.api(req).retrieve('Account', req.params.id, null, function(data) {
    res.render('edit', { title: 'Edit Account', data: data });
  });
});

// update the account in salesforce
app.post('/accounts/:id/update', function(req, res) {
  rest.api(req).update("Account", req.params.id, req.body.account, function(results) {
    res.redirect('/accounts/'+req.params.id);
    res.end();
  });
});

app.listen(port, function(){
  console.log("Express server listening on port %d in %s mode",
    app.address().port, app.settings.env);
});

view raw gistfile1.js This Gist brought to you by GitHub.

I did make a small change for the “/accounts” route in app.js. You’ll notice that it delegates to routes/index.js and contains no actual code. When your node application start getting more complex and larger, you’ll want to refactor the code out of app.js and into their own routes to make life easier.

// only needed if calling the rest api from this file (accounts route)
var rest = rest = require('./../rest.js');

/*
* GET home page.
*/
exports.index = function(req, res){
  res.render('index', { title: 'Salesforce.com Node.js REST Demo' })
};

/*
* GET list of accounts - for larger apps, you may want to separate
* code into different routes for ease of maintenance and logic.
* Prevents app.js from becoming huge!
*/
exports.accounts = function(req, res){
  rest.api(req).query("select id, name from account limit 10", function(data) {
    res.render("accounts", { title: 'Accounts', data: data, user: req.session.identity } );
  });
};

view raw gistfile1.js This Gist brought to you by GitHub.
VN:F [1.9.15_1155]
Rating: 10.0/10 (2 votes cast)
VN:F [1.9.15_1155]
Rating: +1 (from 1 vote)

Categories: Node.js, Salesforce

No Comments

Guest Blogger on Amazon’s Web Services Blog

April 11th, 2012

So yesterday I was a guest blogger on Amazon’s Web Services blog highlighting the results of our CloudSpokes challenge Build an #Awesome Demo with Amazon DynamoDB. We featured 5 applications built by the CloudSpokes community with DynamoDB including videos, demo URLs and source code.

Check out the AWS post for the full details:

CloudSpokes Coding Challenge Winners – Build a DynamoDB Demo

VN:F [1.9.15_1155]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.15_1155]
Rating: 0 (from 0 votes)

Categories: AWS

No Comments

Convert a Related List to a Comma Separated List

April 10th, 2012

Sure, picklists and (sometimes) multi-select picklists are a great way to store data for Salesforce.com objects but related list are much more powerful and flexible. Here’s a great little trick to keep the power of related objects but still have the ease of use of a quasi-picklist for reporting, creating formulas, displaying values to users, external applications (iterating through a collection to display a comma separated list is tiresome!), etc.

The use case is that Accounts can operate in many regions so we need a way to tie Accounts and Regions together. Typically you would create a new custom object (i.e., a junction object) with a master-detail relationship to Account and a master-detail relationship to Region. This allows you to map multiple Accounts to multiple Regions.

However, for ease of use, let’s put a single textarea field on the Account object and show the names of the regions as a simple comma separated list of values. We do this by creating a trigger that updates the Account each time the junction object (Account_Region__c) records are inserted, updated or deleted.

The trigger below fires whenever an Account_Region__c records is inserted, updated or deleted and simply passes the IDs for the Accounts that are affected to the AccountRegionTriggerHandler class for processing.

trigger AccountRegionTrigger on Account_Region__c (after delete, after insert, after update) {
      
  // fires after both insert and update
  if((Trigger.isInsert || Trigger.isUpdate) && Trigger.isAfter){
    
    // find the ids of all accounts that were affected
    Set<Id> accountIds = new Set<Id>();
    for (Account_Region__c ar : [select Id, Account__c from Account_Region__c
      where Id IN :Trigger.newMap.keySet()])
      accountIds.add(ar.Account__c);
        
    // process the accounts
    AccountRegionTriggerHandler.ProcessRegionsAsync(accountIds);
    

  // fires when records are deleted. may want to do undelete also?
  } else if(Trigger.isDelete && Trigger.isAfter){
    
    // find the ids of all accounts that were affected
    Set<Id> accountIds = new Set<Id>();
    for (ID id : Trigger.oldMap.keySet())
      accountIds.add(Trigger.oldMap.get(id).Account__c);
    
    // process the accounts
    AccountRegionTriggerHandler.ProcessRegionsAsync(accountIds);

  }

}
view raw gistfile1.tex This Gist brought to you by GitHub.

The AccountRegionTriggerHandler does all of the heaving lifting. For the Accounts in context, it queries for all of the Regions for each account, builds a comma separated list of region names and then updates the accounts with this list of regions.

public with sharing class AccountRegionTriggerHandler {
  
  @future
  public static void ProcessRegionsAsync(Set<ID> accountIds){
    
    // holds a map of the account id and comma separated regions to build
    Map<Id, String> accountRegionMap = new Map<Id, String>();
      
    // get ALL of the regions for all affected accounts so we can build
    List<Account_Region__c> accountRegions = [select id, Account__c,
      Region__r.Name from Account_Region__c
      where Account__c IN :accountIds order by Region__r.Name];
      
    for (Account_Region__c ar : accountRegions) {
      if (!accountRegionMap.containsKey(ar.Account__c)) {
        // if the key (account) doesn't exist, add it with region name
        accountRegionMap.put(ar.Account__c,ar.Region__r.Name);
      } else {
        // if the key (account) already exist, add ", region-name"
        accountRegionMap.put(ar.Account__c,accountRegionMap.get(ar.Account__c) +
          ', ' + ar.Region__r.Name);
      }
    }
    
    // get the account that were affected
    List<Account> accounts = [select id from Account where Id IN :accountIds];
    
    // add the comma separated list of regions
    for (Account a : accounts)
      a.Regions__c = accountRegionMap.get(a.id);
    
    // update the accounts
    update accounts;
    
  }
  
}
view raw gistfile1.cls This Gist brought to you by GitHub.

And finally, here’s the unit tests for the trigger handler.

@isTest
private class Test_AccountRegionTriggerHandler {
  
  static List<Region__c> regions = new List<Region__c>();
  
  static {
  
    // insert some regions
    Region__c r1 = new Region__c(name='Region 1');
    Region__c r2 = new Region__c(name='Region 2');
    Region__c r3 = new Region__c(name='Region 3');
    Region__c r4 = new Region__c(name='Region 4');
    regions.add(r1);
    regions.add(r2);
    regions.add(r3);
    regions.add(r4);
    insert regions;
    
  }
  
  private static void testInsertRecords() {
    
    List<Account> accounts = new List<Account>();
    List<Account_Region__c> accountRegions = new List<Account_Region__c>();
    
    // insert some accounts
    Account a1 = new Account(name='Account 1');
    Account a2 = new Account(name='Account 2');
    accounts.add(a1);
    accounts.add(a2);
    insert accounts;
    
    Test.startTest();
    
      accountRegions.add(new Account_Region__c(Account__c=a1.Id, Region__c=regions.get(0).Id));
      accountRegions.add(new Account_Region__c(Account__c=a1.Id, Region__c=regions.get(1).Id));
      accountRegions.add(new Account_Region__c(Account__c=a2.Id, Region__c=regions.get(2).Id));
      accountRegions.add(new Account_Region__c(Account__c=a2.Id, Region__c=regions.get(3).Id));
      
      insert accountRegions;
    
    Test.stopTest();
    
    // since async, check for the accounts AFTER tests stop
    List<Account> updatedAccounts = [select id, name, regions__c from account where id IN :accounts];
    System.assertEquals('Region 1, Region 3',updatedAccounts.get(0).Regions__c);
    System.assertEquals('Region 2, Region 4',updatedAccounts.get(1).Regions__c);
    
  }
  
  private static void testDeleteRecords() {
    
    List<Account> accounts = new List<Account>();
    List<Account_Region__c> accountRegions = new List<Account_Region__c>();
    
    // insert an account
    Account a1 = new Account(name='Account 1');
    accounts.add(a1);
    insert accounts;
    
    Test.startTest();
    
      accountRegions.add(new Account_Region__c(Account__c=a1.Id, Region__c=regions.get(0).Id));
      accountRegions.add(new Account_Region__c(Account__c=a1.Id, Region__c=regions.get(1).Id));
      accountRegions.add(new Account_Region__c(Account__c=a1.Id, Region__c=regions.get(2).Id));
      accountRegions.add(new Account_Region__c(Account__c=a1.Id, Region__c=regions.get(3).Id));
      
      insert accountRegions;
    
      // now delete a record
      delete accountRegions.get(3);
    
    Test.stopTest();
    
    List<Account> updatedAccounts = [select id, name, regions__c from account where id IN :accounts];
    System.assertEquals('Region 1, Region 2, Region 3',updatedAccounts.get(0).Regions__c);
    
  }
  
}
view raw gistfile1.cls This Gist brought to you by GitHub.

A couple of caveats:

  1. Since textarea fields only hold 255 characters, this may not be the best approach for extremely long lists of values.
  2. You may want give profiles read-only access to the field holding the list of values so that they cannot edit it. The trigger runs in system mode so that it has read-write access to this field.
VN:F [1.9.15_1155]
Rating: 3.0/10 (2 votes cast)
VN:F [1.9.15_1155]
Rating: 0 (from 0 votes)

Categories: Salesforce

1 Comment

Build an API with Node.js, Express, MongoDB and Cloud Foundry

March 25th, 2012

I was finalist in the LinkedIn Hackday hackathon last November with my “Mobile Chow Finder” app using jQuery Mobile, Database.com and Ruby on Rails on Heroku. I really liked the Chow Finder use case but wanted to make it more of a finished application; not just something I threw together in a matter of hours. So I decided to start off by writing an API with Node.js and MongoDB and host it on Cloud Foundry. I’m going to build a website running off that API with something like Sinatra or Rails. I also want to eventually build HTML5, Android and iOS applications running off this API too.

So I put together a video of the initial process of building the API. It’s definitely a work in progress. You can clone the code from this repo if you find it useful. It assumes that you have Node.js, MongoDB, Express and the Cloud Foundry Command-Line Interface (vmc) installed.

The API has the following methods around two objects: locations and facilities.

GET /locations – returns a list of locations
GET /locations/:id — returns a location
POST /locations – creates a location
GET /locations/favorites – returns list of favorite locations for a user
POST /locations/favorites – creates a new favorite for a user
GET /locations/:id/facilities — returns a list of facilities for a location
POST /locations/:id/facilities — creates a new facility for a location
GET /locations/:id/facilities/:id — returns a facility
PUT /locations/:id/facilities/:id — updates a facility

You can check out code for app.js on github, but most of the interesting stuff is around the connection to MongoDB and the code for the actual methods.

Once you installed the MongoDB native Node.js driver, you just need to create your connection to either your localhost or Mongo running on Cloud Foundry in app.js.

if(process.env.VCAP_SERVICES){
  var env = JSON.parse(process.env.VCAP_SERVICES);
  var mongo = env['mongodb-1.8'][0]['credentials'];
}
else{
  var mongo = {
    "hostname":"localhost",
    "port":27017,
    "username":"",
    "password":"",
    "name":"",
    "db":"db"
  }
}

var generate_mongo_url = function(obj){
  obj.hostname = (obj.hostname || 'localhost');
  obj.port = (obj.port || 27017);
  obj.db = (obj.db || 'test');

  if(obj.username && obj.password){
    return "mongodb://" + obj.username + ":" + obj.password + "@" + obj.hostname + ":" + obj.port + "/" + obj.db;
  }
  else{
    return "mongodb://" + obj.hostname + ":" + obj.port + "/" + obj.db;
  }
}

var mongourl = generate_mongo_url(mongo);
view raw gistfile1.js This Gist brought to you by GitHub.

Now the API itself. What’s great about Node and Mongo are that they both talk JSON. So in this method, we POST some JSON and simply insert it into the ‘locations’ collection.

// creates a location in the 'locations' collection
app.post('/v.1/locations', function(req, res){
  require('mongodb').connect(mongourl, function(err, conn){
    conn.collection('locations', function(err, coll){
      coll.insert( req.body, {safe:true}, function(err){
      res.writeHead(200, {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*"
      });
      res.end(JSON.stringify(req.body));
      });
    });
  });
 });
view raw gistfile1.js This Gist brought to you by GitHub.

To return all of the locations in the collection, we issue the find() command which returns a cursor object to the callback which is passed to the responses as an array of documents.

// returns list of locations
app.get('/v.1/locations', function(req, res){

  require('mongodb').connect(mongourl, function(err, conn){
    conn.collection('locations', function(err, coll){
      coll.find(function(err, cursor) {
      cursor.toArray(function(err, items) {
        res.writeHead(200, {
          "Content-Type": "application/json",
          "Access-Control-Allow-Origin": "*"
        });
        res.end(JSON.stringify(items));
      });
      });
    });
  });

});
view raw gistfile1.js This Gist brought to you by GitHub.

To return a specific location (as a document) from Mongo, we use the findOne() command and pass in the location’s id from the URL.

// returns a specific location by id
app.get('/v.1/locations/:location_id', function(req, res){

  var ObjectID = require('mongodb').ObjectID;
  
  require('mongodb').connect(mongourl, function(err, conn){
    conn.collection('locations', function(err, coll){
      coll.findOne({'_id':new ObjectID(req.params.location_id)}, function(err, document) {
      res.writeHead(200, {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*"
      });
      res.end(JSON.stringify(document));
      });
    });
  });
  
});
view raw gistfile1.js This Gist brought to you by GitHub.

To create a facility for a location, we POST the entire JSON for the facility and then add the id for the parent location from the URL before inserting it into the collection.

// creates a new facility for a location
app.post('/v.1/locations/:location_id/facilities', function(req, res){
  
  // add the location id to the json
  var facility = req.body;
  facility['location'] = req.params.location_id;

  require('mongodb').connect(mongourl, function(err, conn){
    conn.collection('facilities', function(err, coll){
      coll.insert( facility, {safe:true}, function(err){
      res.writeHead(200, {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*"
      });
      res.end(JSON.stringify(facility));
      });
    });
  });

});
view raw gistfile1.js This Gist brought to you by GitHub.

The last method updates a facility document with the findAndModify() method. The method finds the facility by id and updates the data PUT in the JSON payload.

// updates a facility
app.put('/v.1/locations/:location_id/facilities/:facility_id', function(req, res){

  var ObjectID = require('mongodb').ObjectID;
  
  require('mongodb').connect(mongourl, function(err, conn){
    conn.collection('facilities', function(err, coll){
      coll.findAndModify({'_id':new ObjectID(req.params.facility_id)}, [['name','asc']], { $set: req.body }, {}, function(err, document) {
      res.writeHead(200, {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*"
      });
      res.end(JSON.stringify(document));
      });
    });
  });

});
view raw gistfile1.js This Gist brought to you by GitHub.
VN:F [1.9.15_1155]
Rating: 10.0/10 (1 vote cast)
VN:F [1.9.15_1155]
Rating: 0 (from 0 votes)

Categories: Cloud Foundry, MongoDB, Node.js

No Comments

Writing Unit Tests for v24 Apex REST Services

March 21st, 2012

With the Spring ’12 release, salesforce.com made some great enhancements to Apex REST services (v24):

  • Apex REST automatically provides the REST request and response in your Apex REST methods via a static RestContext object. You no longer need to declare a RestRequest or RestResponse parameter in your method.
  • User-defined types are now allowed as Apex REST parameter types.
  • Apex REST methods are now supported in managed and unmanaged packages.
  • The order of elements in the JSON or XML response data no longer has to match the Apex REST method parameter order.

My favorite is the user-defined types. My REST services can now pass back a wrapper class with error messages along with the actual data. Here’s what my new REST service looks like:

@RestResource(urlMapping='/v.9/member/*/results/*')
global with sharing class MemberRestSvc {
 
  @HttpGet
  global static ReturnClass doGet() {
  
    String[] uriKeys = RestContext.request.requestURI.split('/');
    // get the member name from the uri
    String memberName = uriKeys.get(uriKeys.size()-3);

    // do awesome programming stuff here & catch any exceptions
    try {
  
      List<Contact> contacts = [Select Id From Contact where member_name__c = :memberName];
      return new ReturnClass('true', 'Query executed successfully.', contacts);
    
    } catch (Exception e) {
      return new ReturnClass('false', e.getMessage(), null);
    }

  }

  global class ReturnClass {
    
    global String success;
    global String message;
    global List<Contact> records;
    
    global ReturnClass(String success, String message, List<Contact> records) {
      this.success = success;
      this.message = message;
      this.records = records;
    }
    
  }
    
}

So now that I have my service written and running like a champ, I just need to write my unit tests. If I was writing the unit test with the previous API (v23), I would write my unit test like:

@isTest
private class Test_MemberRestSvc {

  static {
    // setup test data
  }

  static testMethod void testDoGet() {
  
    RestRequest req = new RestRequest();
    RestResponse res = new RestResponse();

    // pass the req and resp objects to the method
    req.requestURI = 'https://cs9.salesforce.com/services/apexrest/v.9/member/me/results/today';
    req.httpMethod = 'GET';

    MemberRestSvc.ReturnClass results = MemberRestSvc.doGet(req,res);
    
    System.assertEquals('true', results.success);
    System.assertEquals(10, results.records.size());
    System.assertEquals('Query executed successfully.', results.message);
 
  }
  
}
view raw gistfile1.txt This Gist brought to you by GitHub.

Since v24 now includes a static RestContext object, testing is a little different as you no longer need to pass a Request and Response object to the method. I searched the Apex docs but there was no mention of writing unit tests. Pat Patterson has a good blog post for Apex REST but no mention of unit testing either.

So I tried a few routes for an hour or so to no avail. I finally IM’d Pat and begged for help. I posted the question on the Force.com Discussion Boards and Pat went to work. However, before Pat could finish his investigation and provide a solution, Kartik beat him to it (thanks!!).

So here’s what the unit test looks like for a v24 Apex REST service. Notice that you pass a request and response object to the RestContext but that’s it. Doesn’t seem very intuitive?

@isTest
private class Test_MemberRestSvc {

  static {
    // setup test data
  }

  static testMethod void testDoGet() {
  
    RestRequest req = new RestRequest();
    RestResponse res = new RestResponse();

    req.requestURI = 'https://cs9.salesforce.com/services/apexrest/v.9/member/me/results/today';
    req.httpMethod = 'GET';
    RestContext.request = req;
    RestContext.response = res;

    MemberRestSvc.ReturnClass results = MemberRestSvc.doGet();
    
    System.assertEquals('true', results.success);
    System.assertEquals(10, results.records.size());
    System.assertEquals('Query executed successfully.', results.message);
 
  }
  
}
VN:F [1.9.15_1155]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.15_1155]
Rating: +1 (from 1 vote)

Categories: Apex, Code Sample, Salesforce

No Comments

Synchronizing Asynchronous Events in Salesforce.com

March 20th, 2012

So the title is slightly dubious as you really can’t synchronize asynchronous events in salesforce.com. However, here’s a strategy to make asynchronous events more manageable.

Don’t get me wrong, asynchronous events (Apex methods with the @future annotation) are awesome. You get higher limits, they fire when the platform has spare cycles and users don’t have to wait for processing to finish. However, if you use them liberally in your application, orchestrating them may become difficult.

So here’s the situation we ran into at CloudSpokes and how I finally solved the issue. I may be a bit slow but it took a few iterations.

So if you haven’t been to CloudSpokes (shame on you if you haven’t!!) we run development challenges for cash, prizes, badges and bragging right. A good majority of our challenges are Force.com related. So we run a challenge like Multiple Campaign Picker and a number of developers submit solutions for the challenge. After the challenge closes for submissions, multiple judges score each developer’s submission. Here’s the problem: all of our scoring is done asynchronously. So when a judge submits a scorecard for a developer it is scored asynchronously. When all of the judges have submitted all of their scorecards for a developer, the developer is given a final score, again asynchronously. Then when all of the developers have been scored by all of the judges, the entire challenge is scored…. you guessed it, asynchronously.

The issue we were running into was that because of the asynchronous nature of the event processing the final scoring for the challenge was being done (occasionally) before that last developer was scored. At first I thought a simple sleep() would work at the beginning of the final challenging scoring. This would give the platform some time to score that final developer. However, as you know, there is no sleep() on Force.com as it would tie up valuable shared resources.

I then wrote batch job that I kicked off when the final developer was scored. The job was schedule to run in two minutes. That quickly hit the limit of scheduled tasks available and I didn’t really like the solution anyway.

Note: a queue in Force.com would be awesome in this circumstance and Taggart Matthiesen mentioned in a recent webinar that it is on the roadmap. (Be still my beating heart.)

So here’s the solution that I can up with. I went back to good ‘ole workflow. Never do programmatically what you can do declaratively, I always say. So I wrote a workflow that fires on the challenge object when the number of scored developers equals the number of developers participating. The workflow runs a time-dependent action that fires “0 Hours After Rule Trigger Date” and changes a picklist value. My trigger catches this change and fires the Apex code to score the entire challenge. Viola! Problem solved with point-and-click programming!

There are some limitations with this workflow method but they are minor. Time-based triggers don’t support minutes or seconds. I think the documentation states that they fire within an hour but I read on the developerforce boards once that it’s generally 15 minutes or so. I’ve noticed that most of ours fire around 5 minutes or so.

VN:F [1.9.15_1155]
Rating: 10.0/10 (1 vote cast)
VN:F [1.9.15_1155]
Rating: 0 (from 0 votes)

Categories: Salesforce

No Comments

Apex REST Service Not Found?

February 28th, 2012

I just deployed an Apex REST service to production and noticed something strange when trying to access it. When calling the URL with my Rails app, I received the error: “Could not find a match for URL /v.9/quickquiz/results/today”. Strange because the URL worked in my sandbox and there was no difference in the code. I tried with the Apigee Console with the same results. Hmmm….

I was only able to access the URL after I “Ran All Tests” in the org. Check out the debug below.

VN:F [1.9.15_1155]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.15_1155]
Rating: 0 (from 0 votes)

Categories: Salesforce

1 Comment

Using Node.js to Host Development Web Services

February 2nd, 2012

One of the cool things about Node.js, besides the fact that it’s fast and scalable, is that it’s extremely easy to setup and get running. In a couple of minutes you can get a fully operational web server running with minimal code. I’ve been playing around with the Express library and it makes developing Node.js app sooo much easier. It’s early but I’m really impressed with it so far. Check out this screencast for a 5 minute overview.

One use case that we’ve come across is to use Node.js to mock up web services for development and testing. It’s easy to put together a small app the stubs out a return structure to get your POC up and running quickly. How many times have you wanted to test your Apex callouts but the web service wasn’t finished or stable yet? Now you can setup a quick Node.js app on Heroku that returns dummy data, develop your callouts and then switch to the production endpoint when available. This use case is extremely useful when developing mobile apps as you can run the Node.js server locally while developing with your simulator! (Another great idea from Romin).

We’ve found a really cool use case for Node.js at CloudSpokes. Most of our challenges use some sort of backend datastore or API and this can become tedious when working on jQuery, Rails or HTML challenges where the developer really doesn’t care where the data comes from. Typically their first step in development is authenticating to the API which is a pain when all you really care about is the UI. Now with Node.js we’ve decoupled the frontend and backend requirements. We’ve made available a little Node.js app with resources that simply return JSON in the structure that they’d receive from actual calls to Database.com. They can modify the Node.js app to return different hashes or extend the app to simulate different calls if needed. Now the developers don’t have to worry about authenticating to Database.com and we don’t have to setup an org, enter dummy data and configurations and provide them access. Birds are chirping, the sun is shining and everyone is happy.

The source code for the CloudSpokes Node.js devserver is available at github and you can run the app on heroku.

James Ward has a great article about getting a Node.js app up and running on heroku. It’s a little long if you are familiar with Heroku but it’s well worth the read. Make sure you don’t skip the section about configuring your app to listen on the port defined by Heroku’s environment variables.

VN:F [1.9.15_1155]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.15_1155]
Rating: 0 (from 0 votes)

Categories: CloudSpokes, Heroku

1 Comment

Welcome to CloudSpokes Rob Cheng!

February 2nd, 2012

We just announced on the CloudSpokes blog that Rob Cheng has joined the team and will be heading up our CloudSpokes strategy. Rob came from salesforce.com where he was the Director of Force.com Platform Product Marketing and has also had stints at CollabNet and Borland. He actually has a really cool blog as well that you should check out.

Personally, this is a great addition to the team and since Rob is focusing on partners, sponsors and strategy, this allows me to have more time to work with the community. A few less tasks on my plate.

The CloudSpokes platform and community is growing wickedly fast and Rob was badly needed. We will be adding more people to the team shortly. Are you interested? Drop me a line.

VN:F [1.9.15_1155]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.15_1155]
Rating: 0 (from 0 votes)

Categories: Salesforce

No Comments

Feed

http://blog.jeffdouglas.com /

WordPress Appliance - Powered by TurnKey Linux