Arbiter
NOTE: THIS IS IN BETA AND SHOULD NOT BE USED IN PRODUCTION YET
As good of a tool as Salesforce can be, it definitely has some downsides. Managing and building SOQL queries can be cumbersome. Also working directly with Salesforce fields is horrible, Custom__r.Another_Custom__r.Id
anyone?
Arbiter's mission is to ease the pain or using Salesforce as a software developer. It is not trying to be a tool to edit and set up your Salesforce objects and relations (you are still going to need developers to do that). While Arbiter is not a true ORM it does provide many of the awesome features that you lean on an ORM for. Including a awesome query building syntax, customized schema mappings, and ease of reading and writing to your Salesforce. Heavily inspired by Mongoosejs, Arbiter tries to keep its API as close to Mongoose as possible while matching the needs of working with Salesforce.
Usage
This is a basic rundown of Arbiter's usage. See below sections for full description of each part of the API.
const arbiter = const OpportunitySchema = 'Opportunity' id: 'Id' // this is actually done for you automatically even in nested schemas // simple key => value for mappings name: 'Name' // object syntax for customized definitions callRequested: sf: 'Call_Requested_By__c' // we want to be able to write to this field writable: true type: 'string' custom: 'Custom__c' // can handle nested Schemas project: 'Project__r' name: 'Name' createdDate: 'Created_Date__c' root: false const Opportunity = arbiter Opportunity // yeah grab all of project (id, name, createdDate)!
API
Schemas
const schemaObj = 'Salesforce Object' opts
It all starts with a Schema definintion. Schemas describe the mappings from a Salesforce value to how you want to reference it. They also can be nested. The first argument when creating a schema is is the mapping to a salesforce object. At the root schema level this would be the object you would save to. At nested levels it is the mapping of how the root references it. It's important to note that this nesting represents Salesforce Object relationships that have to be in place in order for Arbiter to query them. Arbiter cannot 'create' the connection between two Salesforce objects.
Imagine that in our Salesforce situation we have a Lead
object which has a reference to a Contact
which in turn has a reference to a Contract
. We could set up a schema like so.
const LeadSchema = 'Lead' // id: 'Id' is always written in at every level for you firstContactDate: 'First_Contact_Date__c' // we want to name our Contact relationship contact contact: 'Contact' firstName: 'First_Name__c' lastName: 'Last_Name__c' // and out Contact's Contract relationship as contract contract: 'Contract__r' signedDate: 'Contract_Signed_Date__c' // lead -> contact -> contract
We could now successfully query data off of the contract through our Lead Schema!
With Great Power
The nested schema power of Arbiter needs to be used with care. It is possible that in your instance you might have a trail of 5 connected objects and you would maybe link them all together. Or maybe you write a schema in one model that covers your entire salesforce setup. This will only lead to a bloated and slow Model. In our above example we are writing a Lead Schema if need be we could still write a contact and contract schema along with Models if those objects see a lot of activity. The idea in Arbiter is that a schema might extend slightly out of its bounds and into its relations only for querying power.
Models
const Model = arbiter
Creating a Model is easy all you need is a unique name (Arbiter makes sure you don't overwrite exising models) and a schema to bake into it. The Model and Schema work together to provide most of the functionality of Arbiter. To kick off a query start with one of the find
functions listed below. After that Arbiter's API is chainable, aside from exec()
, Model functions can be called in any order.
Model.find(opts)
opts
(Object) [optional] - If providedopts
work the same as passing an object toModel.where(opts)
. If nothing is passed then function becomes a noop and simply passes Model for chaining
Model
Model.findById(id)
id
(any) - id to include in query
Model // same as doingModel
Model.findByIds(ids)
ids
(Array) - ids to include in query
Model // same asModel
Model.fields(...fields)
The fields api allows you to select which fields you want returned back from a query.
field
(String) - The field to selectid
of toplevel schema is always included- If you pass the name of a nested schema all local fields on that schema will be selected (no relationships from it)
'.'
will select local fields on the top level schema (no relationships)'*'
will traverse the entire schema tree and give you all fields in the schema.
const simpleSchema = 'Simple' name: 'Name' other: 'Other' nested: 'Nested' something: 'Something__c' anotherNested: 'Another_Nested__r' anotherSomething: 'Another_Something__c' const SimpleModel = 'Simple' simpleSchema SimpleModel // selects id name, all of nested, and the anotherSomething field SimpleModel// selects id, name, and other SimpleModel // id, name, other, all of nested, all of anotherNested
In most cases Arbiters API is repeatable. fields(...fields)
is not. Successive calls to fields(...fields)
will overwrite previous calls.
Model.where(opts)
opts
(Object) - The where clauses to filter query by
Available where clause options
Model
Model.explain()
With Arbiter being a work in progress I wanted to keep it as open as possible so that developers can see what is going on behind the scenes in case bugs occur. Model.explain()
can be added to any part of the chain to reveal state of Model including fields selected, where clauses, and the query that has been built up at the point that explain()
is called. For now this function will simply log to the console. Later implementations will allow passing a custom logger. explain()
is chainable so it can be placed at any point in a Model's chain and not stop a query from executing.
Model //... // logs // { // SFObject: 'Whatever Object Model is linked to', // fields: ['id', 'name'], // where: { id: 1, status: { not: null } }, // query: 'SELECT Id, Name FROM ModelSFObject WHERE Id = \'1\' AND Status != null' // }
Model.exec()
Builds up and executes a query based off of state of Model. If at the the point of exec()
no fields have been selected then all fields in the schema will be selected for you. This function returns a promise that will resolve to grunt instances. It is important to note that if your query specifies a specific id then you will get back one grunt. No id, or a collection of ids will always return an array of grunts
Model Model
If the promise resolves on exec()
then you will get back what arbiter calls grunts. These objects are very similar to documents in Mongoosejs. Check their api for a full description.
Model.RAW(query)
query
(String) - query to execute
This is an option to have Arbiter query Salesforce and forget about all the mapping and features that Arbiter provides. Querying this way simply gives back untouched raw results
Model
Model.inject(query, params, quotes)
query
(String) - query to inject values intoparams
(Object) [default {}] - object with keys to replace with keys valuequotes
(Boolean) [default true] - whether to surround injected value with quotes
If you prefer to work with SOQL query strings directly then this function will help you write flexible query templates. NOTE: When the value of a key in params
is an array then the array is automatically stringified and surrouned in '()'
const query = 'Select Id from Lead Where Status Like @status'Model // Select Id from Lead where Status like \'Open\' const customReplacer = 'Select Id from Lead Where Status In @status'Model
Connection
It is possible to build and set up your Models and Schemas at any point. But in order to start querying and get results back a connection to salesforce must be established.
const config = // maxConnectionTime before reconnecting, default shown which is 6 hours maxConnectionTime: 21600000 username: 'some.user' password: 'somepassword' // this gets directly passed to jsforce.Connection() any of its options are valid connection: loginUrl: 'login.salesforce.com' // this is not required only if your situation requires one accessToken: 'some token' arbiter
arbiter.configure(config)
is mostly a passthrough to jsforce to create and maintain a connection.