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 and 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 in your ts-force-config.json replacing 'Account' in the sObjects
array with the following JSON:
{
"apiName": "Account",
"fieldMappings": [
{
"apiName" : "Name__c",
"propName": "nameCustom"
}
]
}
- Run
ts-force-gen -j ts-force-config.json
again and open./src/generated/sobs.ts
. Note thatName__c
now maps tonameCustom
. - Go back to
Account.Name__c
in your dev org and update it's field level security so that it's not visible to any profiles. - Run
ts-force-gen -j ts-force-config.json
again and open./src/generated/sobs.ts
. Note that thenameCustom
property has disappeared.
When creating the interfaces in sobs.ts
the generator will only show fields visible to your user. For this reason you'll want to make sure you run the generator with an example end user. Otherwise they may get errors due to a field not being visible to them.
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 './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 authenticates 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.
The generateSelect()
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).
- Create a contact with the email
[email protected]
so will get a response from the query - 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 queryRecords() { //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);
}
queryRecords().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();
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
}
Thanks for the feedback!
That was the original vision but I was struggling to come up with anything meaningful. I think the point you made of just referencing the readme instead and having them do something similar in parallel is a great idea.
Good point.
If you cloned down from ts-scratch-paper you should have had source control setup. But it might be good to just create a separate project as the starting point... Could include the ts-force dependencies, but I kinda like that the user has to install them
This is definitely a complicated & confusing part of this library that I should have covered more in depth.
You only need to be logged end as the "end user" when running the
ts-force-gen
command (EG in ts-force-config.json). How you would do this depends on the project. If everyone has full access to the objects your working with, then you can typically just get by using the Admin User. I typically will just create a new user in dev stand box and assign them to one of the target profiles. Technically a permission set with all the permission your app uses, assigned to a standard user is the most fool-proof way. Then just use sfdx to auth as that user and update ts-force-config.json.It's worth noting there are no actually security concerns, but because it determines which fields are sent in a request, it could cause unexpected failures.
Example:
If the end user has write access to
Name
but readonly access toMy_Custom_Field__c
, this code would fail if you generated it with an admin profile. The reason being we are sendingMy_Custom_Field__c
in the update request (since we queried it) and Salesforce will throw an "invalid field access". If you generate it using the "End user" then the field gets marked as readonly and we know not to send it.This is a less than perfect solution as to be 100% safe from these types for failures, you really need to keep a permission set in sync with every field you are referencing (worth noting I've never actually had this error happen in a production app, and I've always just generated it using a profile; really depends on the target audience of the app).
The proper solution would be to add tracking to mark which fields have actually been changed, and then ONLY include those in the request. This would also prevent someone from overwriting changes unintentionally with an in memory object (which is perhaps a bigger issue with this library).
Ya, I feel a seperate debugging challenge using the ts-scratch-paper git repo would be cool.