Skip to content

Instantly share code, notes, and snippets.

@ralphcallaway
Forked from ChuckJonas/tutorial.md
Last active October 29, 2018 16:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ralphcallaway/a42d3cb5792ddbe0ff345a08fa0ea2b8 to your computer and use it in GitHub Desktop.
Save ralphcallaway/a42d3cb5792ddbe0ff345a08fa0ea2b8 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 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"
    }
  ]
}
  1. Run ts-force-gen -j ts-force-config.json again and open ./src/generated/sobs.ts. Note that Name__c now maps to nameCustom.
  2. 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.
  3. Run ts-force-gen -j ts-force-config.json again and open ./src/generated/sobs.ts. Note that the nameCustom 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.

setup/authentication 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 './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 authenticates 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.

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} = '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. Create a contact with the email test@example.com so will get a response from the query
  2. 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 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!');
});
  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();
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
}
@ChuckJonas
Copy link

ChuckJonas commented Oct 29, 2018

Thanks for the feedback!

adding an actual challenge that required typing instead of copy and paste would add a lot more interactivity and i'd expect retention

  • seems like most of this content should be in the ts-force-repo, and/or just a reference to a section in the docs, and/or an example/tutorial in the repo?
  • putting everything in a gist makes it hard to collaborate since you can't edit the gist (although i guess you could just fork and update the link in the skill challenge)

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.

  • for the authentication, could be de-emphasized if we're not going to do this in production, and maybe a comment of how you would normally use it (i.e. run in a vf page with session id passed in)

Good point.

  • might help to add source control as you go so you can easily see the diffs as things change with the config.

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

  • add an example of how field level security impacts generator (see ralph's tutorial gist) - ?? so how exactly am I supposed to authenticate with a specific end user? guessing i could url hack my way in there, but seems kind of tricky for something that has to be done everytime # ts-force

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:

let acc = (await Account.retrieve('SELECT My_Custom_Field__c FROM Account LIMIT 1'))[0];
acc.name = acc.myCustomField + ' foo';
acc.update();

If the end user has write access to Name but readonly access to My_Custom_Field__c, this code would fail if you generated it with an admin profile. The reason being we are sending My_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).

  • incorporate breakpoints and inspector to explore the structure of the query responses in place of some of the console.log print debugging, would help train people on debugging ts-force - add a challenge at the end that incorporates the techniques learned and cements knowledge

Ya, I feel a seperate debugging challenge using the ts-scratch-paper git repo would be cool.

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