This tutorial will walk will cover the basics of using ts-force
. While quite a bit of functionality is not covered, I've tried to include the most common use cases.
Before starting, make sure you have the sfdx-cli install and a developer throw away org authenticated.
git clone https://github.com/ChuckJonas/ts-scratch-paper.git ts-force-tutorial
. cd into dirnpm install
npm install ts-force -S
While some aspects of ts-force can be used without it, the real value of the libraries is in the generated classes.
npm install ts-force-gen -g
. This is a dev tool that allow class generation- create a file in the root call
ts-force-config.json
- Add the following:
{
"auth": {
"username": "SET_THIS"
},
"sObjects": [
"Account",
"Contact"
],
"outPath": "./src/generated/sobs.ts"
}
For username
, you will need to user a sfdx-cli authorized user of a developer or scratch org which you can muck up.
- run
ts-force-gen -j ts-force-config.json
- Look at
./src/generated/sobs.ts
and scan over what has been created. You'll see an interface and class for each SObject. Notice that the properties have all been 'prettified' to javascript standard naming conventions. - Open your dev org. On Account, create a new text field with API name of
Name
(EG:Name__c
once created) - run
ts-force-gen -j ts-force-config.json
again - Open
./src/generated/sobs.ts
. Note the error due to duplicate identifier
In this case, the auto-mapping is in conflict with the standard name field. You can override the auto-mapping by doing something replacing 'Account' with :
{
"apiName": "Account",
"fieldMappings": [
{
"apiName" : "Name__c",
"propName": "nameCustom"
}
]
}
- Now when you generate and open the sobs.ts, you'll see that
Name__c
maps tonameCustom
.
NOTE: for production use, you always want to generate your classes using your END USER. This will ensure that the generated classes are created properly.
Now lets actually start writing some code...
- create a new file
./src/index.ts
- add imports:
import * as child_process from 'child_process'
import {setDefaultConfig, generateSelect} from 'ts-force'
import { Account, Contact } from '@src/generated/sobs'
- add the following code:
// MAKE SURE TO UPDATE 'SET_THIS' to your dev org user
let orgInfo: {result: {accessToken: string, instanceUrl: string}} = JSON.parse(child_process.execSync("sfdx force:org:display -u 'SET_THIS' --json").toString('utf8'));
setDefaultConfig({
accessToken: orgInfo.result.accessToken,
instanceUrl: orgInfo.result.instanceUrl,
});
The above snippet uses sfdx-cli
to get the user token & instanceUrl for your dev org user (something you'd never do in a production app). Then is passes it to setDefaultConfig
, which authinicates ts-force in the global context.
It's generally best practice to defined "Models" for each of your objects in the query. That way, you can pull the same fields in different context (EG if you directly FROM Account
or you wanted to get the related account when selecting FROM CONTACT
). This can be done by first creating an array of any fields for the given object.
- Add the following models:
const accountModel = [
Account.FIELDS.id,
Account.FIELDS.name,
Account.FIELDS.type,
Account.FIELDS.nameCustom
];
const contactModel = [
Contact.FIELDS.id,
Contact.FIELDS.name,
Contact.FIELDS.phone,
];
The toString()
method on each of these FIELDS
properties has been overridden to return the API name.
- Add import
generateSelectValues
tots-force
The generateSelectValues()
method that makes it easy to use these models in your SOQL
query. The first param is the list of fields you want to query. There is an optional second parameter which can be used to append relationships. You can use the relationship FIELD property to access this value.
let qry1 = `SELECT ${generateSelect(contactModel)},
${generateSelect(accountModel, Contact.FIELDS.account)}
FROM ${Contact.API_NAME}
WHERE ${Contact.FIELDS.email} = '[email protected]'`;
console.log('qry1:', qry1);
You can also use the same functionality for inner queries on child relationships:
let qry2 = `SELECT ${generateSelect(accountModel)},
(SELECT ${generateSelect(contactModel)} FROM ${Account.FIELDS.contacts})
FROM ${Account.API_NAME}`;
console.log('query2:', qry2);
Add the above code and hit f5
to see the result (it will be a little slow due to the sfdx cli authentication).
- To actually execute the query, it just needs to be passed into the static
retrieve()
method of the respective SObject. This method returns aPromise<SObject[]>
.
Try the following code:
async function doAsyncStuff() { //from here out, all code should be appended to this method!
let contacts = await Contact.retrieve(qry1);
console.log(contacts);
let accounts = await Account.retrieve(qry2);
console.log(accounts);
}
doAsyncStuff().then(() => {
console.log('done!');
});
- Note that you can reference all the relationships (parent and child) from the results.
//add code to end of queryRecords()
console.log(contacts[0].account.name);
for(let acc of accounts){
for(let contact of acc.contacts){
console.log(contact.email);
}
}
NOTE: You'll need to modify the qry1
or add a contact with email of [email protected]
so a result is returned
Any SObject can be created via the constructor. The constructor takes a single param which allows you to initialize the fields:
let account = new Account({
name: 'abc',
accountNumber: '123',
website: 'example.com'
});
Each SObject
also standard DML operations on it's instance. insert(), update(), delete()
await account.insert();
console.log(account.id);
account.name = 'abc123';
await account.update();
You can specify parent relationships via the corresponding Id
field or via external id
let contact1 = new Contact({
firstName: 'john',
lastName: 'doe',
accountId: account.id
});
await contact1.insert();
console.log('contact1:',contact1.id);
let contact2 = new Contact({
firstName: 'jimmy',
lastName: 'smalls',
account: new Account({myExternalId:'123'}) //add an My_External_Id__c field to account to test this
});
await contact2.insert();
console.log('contact2:',contact2.id);
NOTE: When executing DML on a record which children, the children ARE NOT included in the request!
A frequent use-case you will encounter is that you will want to insert/update/delete many records. Obviously making each callout one at a time is extremely inefficient. In these cases you will want to use the "CompositeCollection" api.
- Add import
CompositeCollection
tots-force
- Add the following code:
let bulk = new CompositeCollection();
let contacts = await Contact.retrieve(qry1 + ' LIMIT 1');
for(let c of contacts){
c.description = 'updated by ts-force';
}
let results = await bulk.update(contacts, false); //allow partial update
//results returned in same order as request
for(let i = 0; i < results.length; i++){
let result = results[i];
let c = contacts[i];
if(result.success){
console.log('updated contact:', c.id)
}else{
let errs = result.errors.map(e=>`${e.message}: ${e.fields.join(',')}`).join('\n');
console.log('Failed to update contact:', c.id, errs);
}
}
If a request fails, an Axios error can be caught. Typically you'll want to handle this error something like this:
try{
//bad request
await Account.retrieve('SELECT Id, Foo FROM Account');
}catch(e){
if(e.response){
console.log(e.response.status);
console.log(JSON.stringify(e.response.data));
}else{
console.log(e.toString());
}
//do something meaningful
}
glad to see this was already here ...!