Skip to content

Instantly share code, notes, and snippets.

@ChuckJonas
Last active October 27, 2018 04:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ChuckJonas/723c1e4f7ab9de67c88f48e2a627043f to your computer and use it in GitHub Desktop.
Save ChuckJonas/723c1e4f7ab9de67c88f48e2a627043f to your computer and use it in GitHub Desktop.
ts-force tutorial

ts-force tutorial

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.

install & configuration

Before starting, make sure you have the sfdx-cli install and a developer throw away org authenticated.

  1. git clone https://github.com/ChuckJonas/ts-scratch-paper.git ts-force-tutorial. cd into dir
  2. npm install
  3. npm install ts-force -S

generation

While some aspects of ts-force can be used without it, the real value of the libraries is in the generated classes.

  1. npm install ts-force-gen -g. This is a dev tool that allow class generation
  2. create a file in the root call ts-force-config.json
  3. 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.

  1. run ts-force-gen -j ts-force-config.json
  2. 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.
  3. Open your dev org. On Account, create a new text field with API name of Name (EG: Name__c once created)
  4. run ts-force-gen -j ts-force-config.json again
  5. 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"
    }
  ]
}
  1. Now when you generate and open the sobs.ts, you'll see that Name__c maps to nameCustom.

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.

setup/authenication ts-force

Now lets actually start writing some code...

  1. create a new file ./src/index.ts
  2. add imports:
import * as child_process from 'child_process'
import {setDefaultConfig, generateSelect} from 'ts-force'
import { Account, Contact } from '@src/generated/sobs'
  1. 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.

queries

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.

  1. 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.

  1. Add import generateSelectValues to ts-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} = 'test@example.com'`;
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).

  1. To actually execute the query, it just needs to be passed into the static retrieve() method of the respective SObject. This method returns a Promise<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!');
});
  1. 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 test@example.com so a result is returned

Working with objects

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!

BULK

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.

  1. Add import CompositeCollection to ts-force
  2. 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);
    }
}

Error Handling

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
}
@ralphcallaway
Copy link

glad to see this was already here ...!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment