nforce 0.7.0's main goal is to introduce a new plugin system. In addition, I am proposing some breaking API changes that I believe will make nforce more flexible and make plugin authoring far easier.
I'm looking for feedback so please comment if you have anything to add.
nforce 0.7.0 will introduce a plugin system. This will enables developers to easily extend nforce with custom functionality and let nforce be more modular.
So why is this a good idea?
This will help prevent nforce from becoming a monolithic beast that implements all current and future API's provided by Salesforce. Also, breaking up functionality into modules is sort of the node way. Modules in node tend to be small and applications are built by combining only the functionality that you need using the vast module ecosystem. Today, nforce handles the base REST API very well (auth, crud, queries, etc). When you start looking at adding support for the Tooling API, Chatter, and future apis, cramming all of this functionality into nforce (core) would turn nforce into a huge project. I believe Chatter itself might be bigger than the core nforce package!
A plugin system will allow you to easily extend nforce with just the functionality you need. I already have repos for nforce-tooling, nforce-chatter, nforce-canvas, and nforce-express. They're just placeholders, but Jeff Douglas is already hacking on the Tooling API plugin and I know Chris Bland has done some work here that he could contribute.
Here is an example of the api for building a module. As you can see, it's pretty simple.
Like most plugin authors, I have my regrets about the initial implementation. In particular, there are two things that I don't like.
- API methods - variable arguments: Single-user mode let's you omit the oauth argument in API method calls because it's cached locally in the connection object. This forces you to juggle the arguments passed in and each function has to do this.
- sObject (Record.js) class: This was a bad implementation all around. Javascript getters/setters lead to tough inspection in console.log and that object functions are not on the prototype.
The initial implementation should have implemented all api methods the same way...the node way.
- Limit the methods to two arguments
- Accept a hash as the first argument
- Second argument is the callback, unless no hash is provided meaning the only argument is the callback
So updating nforce to leverage this means breaking most of the api. These changes would likely be pretty trivial though. This would mean changing a query from this...
org.query('SELECT Id FROM Account', oauth, function(err, records) {
// code
});
to this...
org.query({ query: 'SELECT Id FROM Account', oauth: oauth }, function(err, records {
//code
});
or single user mode...
org.query({ query: 'SELECT Id FROM Account' }, function(err, records {
//code
});
Internally, this makes things much easier to reason about. We could easilty create a utility function that could parse the options and find the oauth object. Currently, each method with variable arguments needs to implement something like this...
Connection.protoype.insert = function(data, oauth, cb) {
if(this.mode === 'single') {
var args = Array.prototype.slice.call(arguments);
oauth = this.oauth;
if(args.length == 2) callback = args[1];
}
if(!callback) callback = function(){}
// rest of implementation
});
Knowing that the arguments will be either 1 or 2 arguments make it so that we can create a utility method that handles it all for you...
Connection.protoype.insert = function(data, oauth, cb) {
var opts = this.getOpts(arguments)
// {
// "sobject": [Object],
// "oauth": [Object],
// "callback": [Function]
// }
});
This will make plugin authoring much easier because we can provide the utility functions to handle most of what you need.
I'd like to update the sObject (Record.js) class to implement explicit set()
and get()
functions to set properties on an sObject. The reason why we do this is so that the class can track changes to sObjects and send only the changed attributes in an update call. Today, this is done with javascript getters and setters...which work fine...but make it difficult to console.log
since the values show as [Getter/Setter]
.
I'd like the API to feel very similar to how you interact with a model in Backbone.js. They're essentially doing the same thing. I plan on adding a bunch of utility methods to make working with the sObjects easier as well.
In addition, there are currently some methods that should be moved to the prototype. These also show up in console.log statements.
Here are some example on how this API would look.
var acc = nforce.createSObject('Account', { Name: 'Salesforce.com' });
acc.set({
Industry: 'Technology',
Employees: 25
});
console.log(acc.get('Name')); // => "Salesforce.com"
console.log(acc.get('Industry')); // => "Technology"
console.log(acc.toJSON()); // => { "Name": "Salesforce.com", "Industry": "Cloud Stuff", "Employees": 25 }
example showing change caching...
var query = 'SELECT Id, Industry, Employees FROM Account WHERE Name = "Salesforce.com" LIMIT 1';
org.query({ query: query }, function(err, recs) {
var acc = recs[0];
console.log(acc.changed()); // => {}
console.log(acc.hasChanged()); // => false
acc.set('Industry', 'Cloud Stuff');
console.log(acc.hasChanged()); // => true
console.log(acc.hasChanged('Industry')); // => true
console.log(acc.hasChanged('Name')); // => false
console.log(acc.changed()); // => { "Industry": "Cloud Stuff" }
console.log(acc.previous()); // => { "Industry": "Technology" }
console.log(acc.previous('Industry'); // => "Technology"
console.log(account.toJSON()); // => { "Name": "Salesforce.com", "Industry": "Cloud Stuff", "Employees": 25 }
});
If we implement these changes, I plan on keeping 0.6 around for a while and releasing on that as well. That way, we can give users plenty of time to update to >= 0.7.0. I would also plan on creating an upgrade guide.
I feel like this is the right time to make some of these changes. I want to get this to 1.0 in the near future but before that happens, I feel like these API changes are necessary.
I'd like your feedback. Please comment with thoughts, suggestions, concerns, or even other api changes that you'd like to see made.
Thanks, KO
Sounds like a good approach. I never met a plugin system I didn't like. OK, I have - but never one I liked less than simply not having one at all.