Introduction
Arbiter is a Salesforce ORM for Nodejs with the goal of making modeling and querying Salesforce as painless as possible. With powerful schemas for validation and field remappings, Arbiter is the perfect layer to have between you and Salesforce.
Arbiter is built on top of the popular JSforce query library. People familiar with its api will feel right at home in Arbiter since most of the api is mirrored. Also, JSForce connection objects are exposed providing escape hatches when needed, or an eased migration to Arbiter.
What you get with Arbiter:
- A Declarative way of defining models and associations between them
- Extended query building API. JSForce with more helpers
- Field remappings. No longer are you tied to your Salesforce field names. Create your own that are easier to reason about or matches your problem domain. Or just to strip out those darn underscores...
- Smart query results. Makes updating and creating new objects quick and easy
- Optional field validation and defaults
What Arbiter doesn't give you:
- A way to modify Salesforce objects and schemas. You will still need Salesforce devs to do that. The objects and relations have to be in place in order for Arbiter to query them.
- Automatic loading of Salesforce schemas. Arbiter schemas must be writen by hand. This means that you don't have to follow your Salesforce setup exactly and allows you to map fields to completely different names to align with your problem domain. This could be seen as a downside but hopefully after trying the library you will appreciate the flexibility.
Arbiter does everything it can to get you down the happy path of making queries and getting access to your Salesforce data. Many of the helpers and additions are to help handle edge that arise when querying nested relations from Salesforce.
Getting Started
Installation
-S is a shorthand for --save
npm install -S arbiter
Install using either npm or yarn.
Define
const arbiter = require('arbiter')
const { Schema } = arbiter
arbiter.configure({
username: 'Johnny English',
password: 'Secret_Agent_Man',
connection: {
loginUrl: 'http://login.salesforce.com',
}
})
// create schema
const opportunitySchema = new Schema('Opportunity', {
oppName: 'Name',
contract: new Schema('Contract', {
status: {
sf: 'Status',
writable: true,
enum: [
'Active',
'Inactive',
'Suspended'
]
}
})
})
// create model with schema
const Opportunity = arbiter.model('Opportunity', opportunitySchema)
// Give it a go!
Opportunity.findOne({ 'contract.status': 'Active' })
.select(['oppName', 'contract.*'])
.throwIfNotFound()
.exec()
.then(opp => {
// you have an opportunity!
})
To use Arbiter you will need to configure a connection to Salesforce. This only needs to happen once somewhere in your application. Arbiter will then maintain and monitor the connection and make it available to your models and their queries.
After that you can build up a model by first creating a schema, which defines field mappings and validations, then adding it to the model.
Then you are ready to start writing queries.
Schemas
const schema = new arbiter.Schema('Opportunity', {
// key: value for simple mapping of fields
name: 'Name',
// field definition when you
// need more than just mapping for a field
description: {
// mapping to salesforce
sf: 'Description',
// if we want to be able to update field
writable: true,
// type validation when updating
// 'date', 'boolean', 'number', or 'string'
type: 'string'
},
status: {
sf: 'Status',
writable: true,
// when only certain values are allowed
enum: [
'Active',
'Inactive'
],
// when creating a new object and we don't specify value
// default to value, can also be a function!
default: 'Active'
},
customField: {
sf: 'Custom_Field__c',
// when creating a new object you have to supply this field
// or save will reject
required: true,
writable: true
}
})
const schema = new arbiter.Schema(SFObject, mappings)
Schemas are the driving force behind how Arbiter queries work so they deserve to be talked about first. Simply put, they are mappings of fields from how you want to reference them to the actual name of the field in Salesforce. Beyond that you can configure them to allow updates and validations on fields.
The first argument when creating a schema is the name of the Salesforce object that will be queried. At nested levels the name is how the parent schema references that object. The second argument is the mappings for the fields you want to be able to query. Once you have defined the mapping, you never reference the field by its Salesforce name again. Arbiter takes care of mapping to the real value behind the scenes for you. You can also only query for fields that you have defined in your schema.
Based on schema to the right we would be able to query for a name
field which we cannot update or change. A description
which is updateable, but must be a string. A status
which is updateable but only to certain options, and customField
which if we are creating a new Opportunity cannot be null.
Nested Schemas
Using nested definition
const schema = new arbiter.Schema('Opportunity', {
name: 'Name',
project: new arbiter.Schema('Project__c', {
status: 'Status',
proposal: new arbiter.Schema('Proposal__c', {
proposalStarted: 'Proposal_Started__c',
propsoalCompleted: 'Proposal_Completed__c'
})
})
})
Usage with
schema.addChildSchema(key, schema)
// create all schemas first
const oppSchema = new arbiter.Schema('Opportunity', {
name: 'Name'
})
const projSchema = new arbiter.Schema('Project__c', {
status: 'Status'
})
const propSchema = new arbiter.Schema('Proposal__c', {
proposalStarted: 'Proposal_Started__c',
proposalCompleted: 'Proposal_Completed__c'
})
// then add them to each other
oppSchema.addChildSchema('project', projSchema)
projSchema.addChildSchema('proposal', propSchema)
// or add schemas at a path
// the parts of the path have to exist
// setting this before setting the project schema woudldn't work
oppSchema.addChildSchema('project.proposal', propSchema)
Arbiter allows schemas to be nested in each other. This nesting represents the relationships that you have defined in your company's Salesforce schema. By nesting schemas you are making it possible to query through relations just like you can in SOQL queries. As with regular fields the keys become the mapping to how we want to reference the relation.
In the example we name all the nested schema names based off of how parent schema references them. In the code to the right we named the project Project__c
because thats how our Opportunity references it. Even if really the Project__c
is a My_Company_Project__c
object in our Salesforce.
There are two apis for defining nested schemas as shown on the right. Both styles outcomes are the exact same.
Feature
Lets say we do a query based on the schema to the right to get the proposalCompleted
field off of the proposal
. What if in our query the project doesn't actually exist for our Opportunity? This would normally lead to errors when you tried to read opportunity.project.proposal.proposalCompleted
since the project doesn't exist.
Arbiter will always scaffold out all the objects you request and place null
in any fields you ask for. Then all the fields that come back from query will get filled in. This means you never need to check if objects exist along the path making defensive code a thing of the past. If you ever need to check to see if an object is not there then just look at its id
property. If you see null
you know that it does not exist.
Models
const model = arbiter.model(NameForModel, schemaInstance)
const aShema = new arbiter.Schema('SalesforceObject', {
createdDate: 'CreatedDate',
status: 'Staus'
})
const MyModel = arbiter.model('MyModel', aSchema)
MyModel.find({ status: 'Active' })
// query instance methods
.limit(10)
.sort('-createdDate')
.exec()
// stash complicated queries
MyModel.getByStatus = function (status) {
return model.find({ status }).exec()
}
Models are the glue that bring schemas and queries together. They are just objects making them a great place to stash complicated queries.
A query instance gets returned by calling any one of the model's find functions
Associations
const accountSchema = new arbiter.Schema('Account', {
description: 'Description',
rating: 'Rating'
})
const Account = arbiter.model('Account', accountSchema)
const oppSchema = new arbiter.Schema('Opportunity', {
accountId: 'AccountId',
status: 'Status'
})
const Opportunity = arbiter.model('Opportunity', oppSchema)
// normally we wouldn't be able to get fields off of
// account through the Opportunity
// but with Arbiter we can
Opportunity.setAssocitions({
// the name we want to give the association
// this comes into play later when we ask to get it
account: {
// what field on the current model do you join on
from: 'accountId',
// what field on the other object do you join on
// IMPORTANT this still using the mapping of the field
to: 'id',
// what model to use for second query
model: Account
// hasOne for when theres only one
// hasMany when it should be an array
relation: Opportunity.relations.hasOne
}
})
Opportunity.findOne({ status: 'Open' })
.with('account', accountQuery => {
accountQuery.select('rating')
})
.exec()
// or
Opportunity.findOne({ status: 'Open' })
.with({
account: accountQuery => {
accountQuery.select('rating')
}
})
The above will return an result shaped like this:
{
"id": "some id...",
"account": {
"rating": "some rating"
}
}
Nested Schemas are powerful because they allow you to query through relations in one query and get the results you need. What do you do when you have a reference to another object but you can't query through it? Thats were associations come in. They allow you to describe to Arbiter how to do the joins and then Arbiter will make the queries and handle zipping the results together. Multiple queries still need to happen to get the data, but it can be a lifesaver to not have to write the manual code to do this yourself.
Just showing off
Case.find()
.limit(10)
// grab the comments for the cases we find
// for every comment also go fetch the owner of the comment
.with({
comments: commentsQuery => {
commentsQuery.select('*')
.with('owner', ownerQuery => {
ownerQuery.select('firstName')
})
},
recordType: recordTypeQuery => {
recordTypeQuery.select('name')
}
})
.exec()
That would result in this, only showing one result
[
{
"id": "some case Id",
"recordType": {
"name": "A RecordType name"
},
"comments": [
{
"id": "some comment id",
"comment": "The guy was talking....",
"owner": {
"firstName": "Peter"
}
}
]
}
]
Whats even more impressive is that you can call off multiple associations at once and/or call associations on your assocations. This all is kicked off by calling query.with()
Grunts
Assuming that status is a writable field and passes validation defined in schema
model.findOne({ status: 'Open' })
.select('status')
.exec()
.then(grunt => {
// we can add other fields to the grunt that are not in the schema
// these will not get sent with any saves
grunt.extraField = 'Some extra field on grunt'
grunt.status = 'Closed'
return grunt.save()
})
.then(savedGrunt => {...})
Updating many grunts
model.find({ isActive: true })
.limit(10)
.then(grunts => {
const updates = grunts.map(grunt => {
grunt.isActive = false
return grunt.save()
})
return Promise.all(updates)
})
.then(updatedGrunts => {...})
Grunts are objects returned by queries. For the most part you can use them just like regular objects, but they do have some tricks up their sleeves.
Grunts are Proxied objects. The handler for a grunt is aware of what fields are writable
in the schema and will run validations and mappings as you mutate writable fields on a grunt. Grunts are not limited to only fields that came back from a query or are in the schema. Feel free to add whatever additional fields you want and use grunts as transfer objects. The handler will make sure to keep track of only the updates that are writable
and that have passed your validations as defined in the schema. The benefit of the approach is that you don't pay a cost for having these smart objects. If you only want to use them for their query power and not do mutations there is no slow down in performance. It also means you can query for 100's of grunts and it won't affect your query performance either.
You won't interface directly with the handler, but knowing it's there and what it is doing will make it easier to reason about what is happening. To update Salesforce with the writable changes on a grunt simply call grunt.save()
. Any errors in your mutations will show up in the catch of the returned promise. The resolved value of the promise is the grunt itself.
Nested mutations
Updating nested values
model.findOne()
.select('account.contract.createdDate')
.then(grunt => {
// we want to update the createdDate which is nested
// since an id is present, calling save will do an update
const contractGrunt = contractModel.new(grunt.contract)
contractGrunt.createdDate = new Date()
return contractGrunt.save()
})
While models are able to query into nested relations the grunts they produce are not responsible to update the nested values. The grunts will have all the fields on them that you asked for, they just aren't responsible to save all of them. In order to update nested fields you will need to make a new grunt out of the data you want through the model.new()
api and then mutate and save the changes. If grunts have an id
property they will call update, if no id
is present on a grunt it will call a create query.
This is in part to make error handling and ordering of the saves explicit. If Arbiter allowed grunts to save every nested level on them it would be hard to imagine a clean api when an error occurred while saving.
Raw JSForce Queries
Raw connection object
const arbiter = require('arbiter')
arbiter.getConnection()
.then(conn => {
// raw jsforce connection object
})
model.sobject() call
const Opportunity = arbiter.model('Opportunity', aSchema)
Opportunity.sobject()
.then(sobject => {
// same as conn.sobject('whatever schema points to').then(...)
})
Arbiter is built using JSForce behind the scenes. After it maps your values to what they really are it builds up a JSForce query. If for whatever reason you need to use a regular JSForce query, they are available to you. Keep in mind that when using raw queries you are not going through the schema and will not receive back prettied results, or even grunts. This is a direct call into JSForce api and what they return.
Recipe Book
Composable queries
Returning the query instance for additional modification
const arbiter = require('arbiter')
// leaving out schema details for brevity
const oppSchema = new arbiter.Schema('Case', {})
const Case = arbiter.model('Case', oppSchema)
// Notice that by calling find and where
// that we are adding up the state of the two
// one doesn't overwrite the other unless they
// contain the same field
Case.byStatus = function (status, whereClauses = {}) {
return Case.find({ status })
.select('*')
.where(whereClauses)
}
A lot of flexibility is available by leveraging the composabilty of queries. Since their state is cumulative you can accept outside input and add it to the query. We have also found that it is advantageous to not execute the query inside a function attached to the model. By returning the query you allow the outside world to attach more fields and other query state to shape the query different for their needs. Doing this will make a lot more of your queries reusable in other places in your app and trims down on code.
At a minimum consider having all of your query functions accept a where clause argument. This is generally the part that makes one query unique over another or helps make queries more composable.
This is not always possible and sometimes you have to execute queries in order to supply the right data or maybe you don't want the abstraction of the query to show up outside of the model. But if you find yourself seeing an existing query that is very close to what you need consider changing your apis to this style and being more extensible.
Reusable Schemas
Extract the field config into a seperate file
// opportunity.js
module.exports = {
name: 'Name',
status: {
sf: 'Status',
enum: ['Active', 'Suspended', 'Inactive'],
writable: true,
default: 'Active'
}
}
// service.js
module.exports = {
name: 'Name',
assignedTo: {
sf: 'Assigned_To__c',
writable: true,
required: true
}
}
// building the schema
const arbiter = require('arbiter')
const opportunityConfig = require('./opportunity')
const serviceConfig = require('./service')
// notice how we don't have to define the field configs here now
const opportunitySchema = new arbiter.Schema('Opporunity', opportunityConfig)
const serviceSchema = new arbiter.Schema('Service__c', serviceConfig)
opportunitySchema.addChildSchema('service', serviceSchema)
There might come a point when working with Arbiter when you have to use the same schema object in multiple schema trees. You could just redefine the schema fields and deal with the possiblilty of the two getting out of sync. Or what you can do is pull out the basic structure of each of your schemas into seperate files. Then require those files when you are building up your schema trees.
This makes it so that you have a lot less schemas to write over and over and you can move much faster. It also means that you can no longer do a nested schema definition and you will need to the the addChildSchema
api for building up the objects.
This is optional but highly recommended since writing schema configs can be cumbersome and its nice to only have to do it once per object you will interface with.
API Reference
Arbiter
The main import of the library houses the manager of the connection to Salesforce as well as a place to define Schemas and models. It is also where you go to kick off a JSForce query.
configure
const arbiter = require('arbiter')
// showing required fields
arbiter.configure({
username: 'Johnny English',
password: 'Secret_Agent_Man',
connection: {
loginUrl: 'https://login.salesforce.com'
}
})
Sets up connection to Salesforce. It needs to happen somewhere in your application one time. You are free to define models and Schemas before it is configured, but it most be done before you attempt a query.
Arguments
opts
(object) - The options to configure connectionusername
(string) - username for connectionpassword
(string) - password for connectionmaxConnectionTime
(number) [defaut:21600000] - number of milliseconds before refreshing connectionconnection
(object) - Anything valid passed to new jsforce.Connection()loginUrl
(string) - Url to login. Only required field onconnection
.
Return Value
undefined
getConnection
const arbiter = require('arbiter')
arbiter.getConnection()
.then(conn => {
/*
You have a jsforce connection
*/
})
Gets connection to salesforce. This is a JSForce connection and any queries done through this connection will not have any mappings or validations from the schema. Use this as an escape hatch or while migrating to Arbiter. Arbiter uses the same connection throughout all of its queries so any changes to the connection here will show up everywhere until the connection expires as defined in the call to arbiter.configure()
Arguments
None
Return Value
JSForce Connection instance
model
const arbiter = require('arbiter')
const schemaInstance = new arbiter.Schema('Opportunity', {...})
const model = arbiter.model('SomeName', schemaInstance)
Creates a new model instance
Arguments
name
(string) - name for the model, this will show up in debugging and errors.schema
(Schema) - a Schema instance to query against
Return Value
model
Schema
const arbiter = require('arbiter')
const schema = new arbiter.Schema('Opportunity', {
status: {
sf: 'Status',
writable: true,
enum: [
'Active',
'Inactive',
'Suspended'
],
default: 'Active'
},
accountId: 'AccountId'
})
Creates a schema instance defining mapping and validation for fields.
Arguments
salesforceObject
(string) - name of salesforce object the schema is a reference to.config
(object) - configuration for fields of schema
The fields of the config can either be simple key: value
pairings which sets the key as the way you want to reference the field. The value can also be an object in order to extend the configuration. All the possible values are shown in example to right.
Expanded config for a field in schema config
{
// only required field
sf: 'Some_Salesforce_Field__c',
// boolean, date, string, number
type: 'string',
// must be one of these
enum: [ null, 'value', 'value2' ],
// we want to be able to have grunts update this field
writable: true,
// when saving a new object this field cannot be null/undefined
required: true,
// defaults, this makes having required redundant since it will always
// be there because of this having a default
default: 'A Default',
// can also be a function
default() { return 'A default' }
}
Model
An object that can get connections from pool and kick off queries.
sobject
model.sobject()
.then(sobject => {
return sobject.find(...)
})
Same as calling
arbiter.getConnection()
.then(conn => conn.sobject('SomeSObject'))
Gets a JSForce connection and sets that connection to the sobject that the model is linked to. This is found out by looking at the schema that was added to the model.
Argumens
None
Return Value
JSForce SObject instance
setAssociations
model.setAssocations({
singeAssociation: {
from: 'a field on me',
to: 'a field on them',
model: SomeOtherModel,
// this field should be a single value
relation: model.relations.hasOne
},
pluralAssocation: {
from: 'a field on me',
to: 'a field on them',
model: SomeModel,
// this field should be an array of things
relation: model.relations.hasMany
}
})
Assocations are a way to have Arbiter be able to execute multiple queries and join the results together. In your Salesforce you will most likely have many relationships that allow you to do queries across relations. Associations are for the cases where you cannot traverse the relationship and instead would have to do multiple queries to get the data for associated objects. Using this api allows you to show models how to do the "joins" in order to get all the data.
The keys of the object become the name of the association you can ask for later when you call query.with()
Arguments
associations
(object) - Associations to add to the modelfrom
(string) - Field on current model to start fromto
(string) - Field on associated model to join onmodel
(model) - Model to execute next queryrelation
(string) - [hasOne| hasMany] - Signify whether association is single entity or many things.
For convenience models have a relations.hasMany
and relations.hasOne
property on them that point to the respective strings.
Return Value
undefined
new
const newObject = model.new()
newObject.status = 'Active'
newObject.save()
.then(() => /* created grunt! */)
Example of passing in some starting fields
// these fields will still need to pass validation of the schema
const newObject = model.new({
status: 'Active'
})
newObject.save().then(...)
Creates an instance of a grunt. This is one way of creating salesforce objects on the fly or doing a direct update of an object without querying for it first. When calling save()
on a grunt if it's given an id
field it will do an update. Otherwise it will attempt to create the object.
Arguments
fields
(object) [optional] - fields to add to grunt after creating it
Return Value
grunt instance
find
model.find()
.select('*')
.exec()
Passing in where clause
model.find({ 'account.lastName': 'Rollins' })
Same as doing:
model.find().where({ 'account.lastName': 'Rollins' })
Kicks off a query. Optional where clause options can be passed to find.
Arguments
whereClauses
(object) [optional] - passes object into query as where clauses
Return Value
query instance
findOne
model.findOne({ isActive: true })
Same as doing
model.find({ isActive: true }).first()
Shorthand that exists for calling a query that is only looking for an individual record back
Arguments
whereClauses
(object) [optional] - passes object into query as where clauses
Return Value
query instance
findById
model.findById('1')
Same as doing:
model.find().where({ id: '1' }).first()
Kicks off a query. Returns a single result, if one is found.
Arguments
id
(string) - id of model to be searched for
Return Value
query instance
findByIds
model.findByIds([ '1', '2', '3' ])
Same as doing:
model.find().where({ id: [ '1', '2', '3' ]})
Kicks off a query searching for objects from a collection of ids.
Arguments
ids
[array] - ids to search for
Return Value
query instance
Query
Queries are created by calling any of the find methods on a model find, findById, findByIds
and have many methods to shape your request. Besides any of the methods that execute a query (then, exec, execute
) query methods can be called in any order. Also most query functions state is cumulative. Calling select, where, or with keeps adding to the same state instead of wiping it out everytime. You can use this to your advantage to build highly composable queries.
select
All call signatures
model.find()
// as positional arguments
.select('id', 'isDeleted', 'status', 'contract.created')
model.find()
// as array
.select([ 'id', 'isDeleted', 'status', 'contract.created' ])
model.find()
// as single string of comma delimited fields
.select('id, isDeleted, status, contract.created')
Selects the fields you want returned in a query.
Arguments
fields
(array | string) - fields you would like returned
Return Value
query instance
fields
alias for select
where
model.find()
.where({
status: 'Active',
isDeleted: false
})
Also accepts a RAW SOQL String
// keep in mind this needs to be the Salesforce naming of fields
// this is good for some edge case queries like one below
model.find()
.where("DISTANCE(Location__c, GEOLOCATION(37.775,-122.418), 'mi') < 20")
Adds where clauses to the query.
Arguments
clauses
(object | string) - clauses to add to query
Return Value
query instance
limit
model.find()
.limit(5)
Sets a limit on the query.
Arguments
amount
(number) - The number of results to limit the query to
Return Value
query instance
skip
model.find()
.skip(10)
Sets up a skip on query.
Arguments
amount
(number) - Amount to skip in query
Return Value
query instance
offset
alias to skip
sort
minus in front of field signifys descending order
model.find()
.sort('-createdDate status')
Other signatures
model.find()
.sort({ createdData: -1, status: 1 })
Sets up sorting of query.
Arguments
sort
(object | string) - opts for how to sort
Return Value
query instance
first
model.find()
.first()
Executing the query does something similar to
model.find()
.exec()
.then(results => results[0])
Sets the query state that you only want the first result that comes back. This makes the return a single value instead of an array.
Arguments
None
Return Value
query instance
with
As positional arguments
model.find({ status: 'Suspended' })
.select('*')
// with this syntax you would have to call `with` multiple times
// to fetch multiple associations
.with('association', associationQuery => {
// notice we don't call any of the functions to start query
// Arbiter does this for you when it is ready to do query
associationQuery.select('field, anotherField, yetAnotherField')
})
As an object to with
model.find({ status: 'Active' })
.select('*')
.with({
// you can pass multiple associations here too if you need to
association (associationQuery) {
associationQuery.select('field, anotherField, yetAnotherField')
}
})
Sets up an association query to be called once outer query has executed. The associations that you can fetch go hand in hand with the associations that have been configured on the models with setAssociations()
.
These are very powerful for joining together objects that you normally can't query through in a regular SOQL query. Also they can be nested, since the query function for an association is just creating a query instance the entire query api is available to you. The only things that aren't would be the functions to execute the inner queries since Arbiter will need to do that once it is ready.
Two apis exist for calling with
. The with(associationName, queryCB)
style and the with(asocciationObject)
style. The associationObject
style might be better if you need to fetch multiple associations but that is always possible with the other style as long as you call with
multiple times.
Arguments
2 argument version
associationName
(string) - the name of the association to fetch as configured on modelqueryCB
(function) [optional] - the query to filter and modify the association query. Not specifying filters or fields will simply return ids for nested queries
Object style
associationsConfig
(object) - the associations to fetch and functions to call for associationsassociation
(string) - name of association to fetch as configured on modelqueryCB
(function) - the query to filter and modify the association query. Not specifying filters or fields will simply return ids for nested queries
throwIfNotFound
model.find()
.throwIfNotFound()
Handles when query is on a single value or an array of values
// when query would maybe return one thing
model.findOne()
.throwIfNotFound(new Error('whoops'))
// or when query returns multiple things
model.find()
.throwIfNotFound('error message')
Helper for handling when query might return an empty array or null for a query. Doing so will cause the query to return a rejected promise. This covers some edge cases that can happen when you are operating on data in your .then()
not realizing that the query returned no results.
Arguments
error
(error | string | undefined) - What error to throw. Ifundefined
is passed the default message for the thrown error is[Model.name] not found
Return Value
query instance
allowMutations
model.find()
// assuming that status is a writable field
.select('status')
.allowMuations(['status'])
.then(grunt => {
grunt.status = 'something new'
// any other updates that are writable in schema would fail
return grunt.save()
})
Configures the returned grunts to allow mutations on only certain fields. The fields that are passed must be listed as writable in the schema. This is handy for sanitizing queries and restricting writes to certain values that are writable some of the time but you need to protect at other times
Arguments
mutations
(array) - fields to allow mutations on
Return Value
query instance
rejectMuations
model.find()
.rejectMutations(['status'])
.then(grunt => {
// cannot change status even if listed as writable in schema
})
The inverse of allowMuations use which ever one is easier to describe the fields you want to allow or reject field mutations in the query
Arguments
mutations
(array) - fields to not allow modification of on grunt
execute
model.find()
.execute()
Triggers a query to get executed. All of the state built up to this point gets turned into a query and sent over connection
Arguments
None
Return Value
exec
alias for execute
then
Make query promise like
model.find()
.then(onSuccessFn, onRejectFn)
alias for execute
except for that it allows passing of success and reject handlers for the query. This makes the query conform to a promise interface
Arguments
successHandler
(function) - function to call on successful queryerrorHandler
(function) [optional] - function to call on unsuccessful query