Skip to content

Commit e16f04c

Browse files
authored
Refactor to use functions instead of weird syntax (#7)
* Refactor to use apis instead of weird syntax * Update dependencies * Update README and add scripts Updated Readme. Added composite api script. Fixed bugs with GetSessionId. Updated old scripts to use the new api * Change DML Api * Update readme * Update readme * Minor syntax update * Update readme * Update packages * Update contact add file script * Update JS button scripts in Account and Contact cmdt * Update readme
1 parent 1d1c3eb commit e16f04c

38 files changed

+23028
-7145
lines changed

.prettierrc

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
{
22
"trailingComma": "none",
3+
"printWidth": 120,
34
"overrides": [
45
{
56
"files": "**/lwc/**/*.html",
67
"options": { "parser": "lwc" }
78
},
89
{
910
"files": "*.{cmp,page,component}",
10-
"options": { "parser": "html","printWidth":120 }
11+
"options": { "parser": "html"}
1112
},
1213
{
1314
"files": "*.{cls,trigger,apex}",
14-
"options": { "parser": "apex","printWidth":120 }
15+
"options": { "parser": "apex"}
1516
}
1617
]
1718
}

README.md

+97-36
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,125 @@
11
# Pure JS Buttons in Lightning
22

3-
JS buttons are back in Lightning! (For now, at least) And they are even more powerful than JS buttons in classic. Run SOQL and DML statements seamlessly. Make callouts to APIs, including Salesforce APIs using named credentials directly from JavaScript! This would allow you to build buttons that do amazing things, just using JavaScript. Check out the `scripts` folder for examples. Feel free to raise a PR to contribute your own scripts.
3+
JS buttons are back in Lightning! (For now, at least) And they are even more
4+
powerful than JS buttons in classic. Run SOQL and DML statements seamlessly.
5+
Make callouts to APIs, including Salesforce APIs directly from JavaScript!
6+
This would allow you to build buttons that do amazing things, just using
7+
JavaScript. Check out the [scripts](./scripts/jsButton) folder for examples.
8+
Feel free to raise a PR to contribute your own scripts.
49

510
### The Setup
611

7-
The button can be made available to users via a quick action powered by the `jsButtonQuickAction` component. The actual JavaScript should be entered into a `JS_Button__mdt` custom metadata record, into the `Script__c` field with the same name as the name of the SObject. The repo contains a couple of samples for Account and Contact. The corollary is that, out of the box, only one button per SObjectType may be supported. Note that the Contact js button intentionally throws an error (by attempting to create a File) to showcase error handling capabilities.
12+
The button can be made available to users via a quick action powered by the
13+
`jsButtonQuickAction` component. The actual JavaScript should be entered into a
14+
`JS_Button__mdt` custom metadata record, into the `Script__c` field with the
15+
same name as the name of the SObject. The repo contains a couple of samples
16+
for `Account` and `Contact`. The corollary is that, out of the box, only one
17+
button per SObjectType may be supported, for quick actions. You can add any
18+
number of buttons on the flexipage, with the underlying JS added using the
19+
flexipage builder.
20+
21+
### APIs
22+
23+
The library supports the following apis
24+
25+
- soql
26+
- dml (dml.insert, dml.update, dml.upsert and dml.del ) // `delete` is a resrved keyword :(
27+
- callout ( used for calling external services through Apex. Named credentials are supported! )
28+
- sfapi ( used for calling Salesforce APIs from the same org. Requires CORS and
29+
CSP Trusted Sites setup. Details below)
30+
- toast ( show a platform toast message )
831

932
### The Syntax
1033

11-
This is the fun part. The syntax is quite permissive, with some restrictions, which I will cover below. I haven't, obviously, explored all possible scenarios and the information may still be incomplete. Please raise an issue if you come across something I haven't covered.
34+
This is the fun part. I haven't, obviously, explored all possible scenarios and
35+
the information may still be incomplete. Please raise an issue if you come
36+
across something I haven't covered.
1237

13-
* Simple examples (no soql/dml)
38+
- Simple examples (no soql/dml)
1439

15-
```javascript
16-
alert('hello,world');
40+
```js
41+
alert("hello,world");
1742
```
1843

19-
```javascript
20-
alert(Array(5).fill(0).map((e,i)=>'Hello, '+i));
44+
```js
45+
toast(
46+
Array(5)
47+
.fill(0)
48+
.map((e, i) => "Hello, " + i)
49+
); /* `toast` service to show message toasts */
2150
```
2251

23-
* Fetch 10 of the 100 latest Accounts without a Contact and add a Contact to each of them
24-
25-
```javascript
26-
let accts=|| Select Name,(Select Id from Contacts) from Account order by createddate desc limit 100 ||;
27-
let contacts = accts.filter((a)=>!a.Contacts || a.Contacts.length===0)
28-
.slice(0,10)
29-
.map((a)=>({LastName: a.Name+'-Contact', AccountId: a.Id}));
30-
let contactIds = || insert Contact(contacts) ||; /*Note how the SObjectType has been specified. This is required for insert and upsert*/
31-
$A.get('e.force:refreshView').fire(); /* $A is supported!*/
52+
- Fetch 100 of the latest Accounts and for upto 10 of the ones without a Contact, add a Contact
53+
54+
```js
55+
let accts = await soql(
56+
`Select Name,(Select Id from Contacts) from Account order by createddate desc
57+
limit 100`
58+
); /* Querying child records is supported */
59+
let contacts = accts
60+
.filter((a) => !a.Contacts || a.Contacts.length === 0)
61+
.slice(0, 10)
62+
.map((a) => ({ LastName: a.Name + "-Contact", AccountId: a.Id }));
63+
let contactIds = await dml.insert(
64+
contacts,
65+
"Contact"
66+
); /*Note how the SObjectType has been specified. This is required for insert and upsert*/
67+
$A.get("e.force:refreshView").fire(); /* $A is supported!*/
3268
```
3369

34-
* Act in the context of the current record
70+
- Act in the context of the current record
3571

36-
```javascript
37-
let acct = || Select NumberOfEmployees from Account where Id='${recordId}' ||;
72+
```js
73+
let acct = await soql(
74+
`Select NumberOfEmployees from Account where Id='${recordId}'`
75+
); /* Note the use of template literal syntax to resolve
76+
variable values in the query */
3877
acct[0].NumberOfEmployees = (acct[0].NumberOfEmployees || 0) + 10;
39-
let acctId = || update acct ||;
40-
acct = || Select NumberOfEmployees from Account where Id='${acctId}' ||;
41-
alert(acct[0].NumberOfEmployees);
42-
$A.get('e.force:refreshView').fire();
78+
let acctId = await dml.update(acct);
79+
acct = await soql(`Select NumberOfEmployees from Account where Id='${acctId}'`);
80+
toast(acct[0].NumberOfEmployees, "success");
81+
$A.get("e.force:refreshView").fire();
82+
```
83+
84+
- Upload files to ContentVersion(ContentDocument) records
85+
86+
```js
87+
let fileContent = btoa("Hello World");
88+
/* convert your file content to base64 data before uploading */
89+
let cv = {
90+
VersionData: fileContent,
91+
Title: "My Awesome File",
92+
PathOnClient: "MyFile.txt"
93+
};
94+
let cvId = await dml.insert(cv, "ContentVersion");
4395
```
4496

4597
### About the Syntax
4698

47-
* Note how the syntax is linear for SOQL and DML. Coupled with JavaScript's support for manipulating arrays, this makes it easier to manipulate data, even compared to Apex in several instances.
48-
* SOQL and DML statements must be enclosed in `||`. Semi-colon can be inside or outside the `||`
49-
* Upsert and Update statements must be qualified with the SObjectType thus `|| insert Account(accts) ||;`
50-
* SOQL statements are parsed using template literals. Any arguments should follow the appropriate syntax `${argument}`
51-
* SOQL and DML statements may not be wrapped in a function.
52-
* All statements must be strictly terminated by a semicolon.
99+
- Note how the syntax is linear for SOQL and DML. Coupled with JavaScript's
100+
support for manipulating arrays, this makes it easier to manipulate data,
101+
even compared to Apex in several instances.
102+
- `dml.insert` and `dml.upsert` expect the SObjectType as the second argument.
103+
Thus `dml.insert(acct,"Account")`
104+
- Statements with contextual arguments such as `recordId`
105+
are best expressed using [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals).
106+
- All statements must be strictly terminated by a semicolon.
53107

54108
### Known Limitations
55109

56-
* Support for delete has been intentionally withheld.
57-
* Single-line comments are not supported.
58-
* Haven't tested DML with date, datetime, boolean, geolocation and other compound fields. I will update this section as I do so.
59-
* SOQL and DML statements should be enclosed in async functions, if they are required to be contained in functions. The program automatically adds `await` to SOQL and DML statements
60-
* DML on Files, Attachments, Documents, etc. is not supported
110+
- Single-line comments are not supported.
111+
- Haven't tested DML with date, datetime, boolean, geolocation and other
112+
compound fields. I will update this section as I do so.
113+
- To insert `ContentVersion` make sure to set `VersionData` to base64 data.
114+
Refer to the example [here](./scripts/jsButton/createContactFiles.js) for details.
61115

62116
### Using Salesforce (and other) APIs in your script
63117

64-
You can use any of Salesforce's APIs (REST, Tooling, Metadata) by setting up a named credential for your own Salesforce instance. This allows you to write scripts for admins to perform tasks like [deleting inactive versions of flows](scripts/jsButton/deleteInactiveFlowVersions.js), or [creating new JS Buttons](scripts/jsButton/createNewJSButton.js)! You can also use named credentials to interact with other APIs as well, of course. Although, for Public APIs, you can just use `fetch` directly. The Salesforce named credential set up would need to have the following scopes (api refresh_token offline_access web). You would need to set up your own Connected App and a Salesforce Auth. Provider that uses this connected app.
118+
To use Salesforce APIs from your org, using the `sfapi` method, take the following steps:
119+
120+
- Add your lightning domain (ends with `lightning.force.com`) to the `CORS` list under `Setup`.
121+
- Add your classic domain to `CSP Trusted Sites` list under `Setup`.
122+
123+
This allows you to write scripts for admins to perform tasks like [deleting inactive versions of flows](./scripts/jsButton/deleteInactiveFlowVersions.js) or [use composite api](./scripts/jsButton/compositeApiExample.js)
124+
for creating parent and child records.
125+
To access protected APIs such as those from other Salesforce orgs, use a named credential and the `callout` api. For Public APIs, you can use `fetch` directly.

force-app/main/default/aura/jsButton/jsButtonController.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
.catch((err) => {
1313
reject(err);
1414
});
15-
} else {
15+
} else if (js) {
1616
helper.runJS(component, resolve, reject);
1717
}
1818
})

force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickActionController.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
.invoke()
66
.then(
77
$A.getCallback((resp) => {
8-
console.log('>> resp '+JSON.stringify(resp));
8+
console.log(">> resp " + JSON.stringify(resp));
99
$A.get("e.force:closeQuickAction").fire();
1010
})
1111
)

force-app/main/default/classes/APICallController.cls

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
/**
2-
* description: Controller for making api calls and sending the response back
3-
**/
1+
/************************************************************
42
3+
*** @author Suraj Pillai
4+
*** @group Controller
5+
*** @description Controller for making API calls and sending the response back
6+
***
7+
**/
58
public with sharing class APICallController {
69
/**
7-
* description Given an endpoint, request params and headers, callout the api and return the response
8-
* @param endpoint The endpoint to callout to
10+
* @description Given an endpoint, request params and headers, callout the api and return the response
11+
* @param endPoint The endpoint to callout to
912
* @param method The http method to use
1013
* @param bodyStr The request body string.
1114
* @param headers Map of string key and value for request headers
12-
* @return HttpResponseWrapper
15+
* @return The response for the http request
1316
*
1417
*/
1518
@AuraEnabled

force-app/main/default/classes/DynamicSOQLDMLController.cls

+31-10
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,48 @@ public with sharing class DynamicSOQLDMLController {
3636
return recordId.getSObjectType().getDescribe().getName();
3737
}
3838

39+
private static List<ContentVersion> deserializeContentVersion(String strData) {
40+
List<Object> deserializedRecords = (List<Object>) JSON.deserializeUntyped(strData);
41+
List<ContentVersion> recordsList = new List<ContentVersion>();
42+
for (Object objRec : deserializedRecords) {
43+
Map<String, Object> record = (Map<String, Object>) objRec;
44+
ContentVersion cv = new ContentVersion();
45+
String vData = String.valueOf(record.remove('VersionData'));
46+
cv = (ContentVersion) JSON.deserialize(JSON.serialize(record), ContentVersion.class);
47+
cv.put('VersionData', EncodingUtil.base64Decode(vData));
48+
recordsList.add(cv);
49+
}
50+
return recordsList;
51+
}
52+
3953
/**
40-
* Execute a DML statement
54+
* @description Execute a DML statement
4155
* @param operation 'Insert','Update' or 'Upsert'
42-
* @param strdata The records to update, stringified
43-
* @param sobjectType The SObject type to perform the DML on
44-
*
56+
* @param strData The records to update, stringified
57+
* @param sObjectType The SObject type to perform the DML on
4558
* @return Id[]
4659
**/
4760
@AuraEnabled
48-
public static Id[] executeDml(String operation, String strData, String sObjectType) {
49-
SObject[] records = (SObject[]) JSON.deserialize(strData, Type.forName('List<' + sObjectType + '>'));
61+
public static List<Id> executeDml(String operation, String strData, String sObjectType) {
62+
List<SObject> records = null;
63+
64+
if (sObjectType.equalsIgnoreCase('ContentVersion')) {
65+
records = deserializeContentVersion(strData);
66+
} else {
67+
records = (SObject[]) JSON.deserialize(strData, Type.forName('List<' + sObjectType + '>'));
68+
}
69+
5070
if (operation == 'insert') {
5171
insert records;
52-
return new List<Id>(new Map<Id, SObject>(records).keySet());
5372
} else if (operation == 'update') {
5473
update records;
55-
return new List<Id>(new Map<Id, SObject>(records).keySet());
5674
} else if (operation == 'upsert') {
5775
upsert records;
58-
return new List<Id>(new Map<Id, SObject>(records).keySet());
76+
} else if (operation == 'delete') {
77+
delete records;
78+
} else {
79+
return null;
5980
}
60-
return null;
81+
return new List<Id>(new Map<Id, SObject>(records).keySet());
6182
}
6283
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/************************************************************
2+
3+
*** Copyright (c) Vertex Computer Systems Inc. All rights reserved.
4+
5+
*** @author Suraj Pillai
6+
*** @group Controller
7+
*** @date 01/2021
8+
*** @description Get API-ready session id of the current users
9+
***
10+
**/
11+
public with sharing class GetSessionIdController {
12+
/****
13+
** @description Returns the current user's session id that may be used for calling Salesforce APIs
14+
** @return the current user's api-ready session id
15+
**/
16+
@AuraEnabled(cacheable=true)
17+
public static String getSessionId() {
18+
String content = Page.GetSessionId.getContent().toString();
19+
return getSessionIdFromPage(content);
20+
}
21+
22+
private static String getSessionIdFromPage(String content) {
23+
Integer s = content.indexOf('Start_Of_Session_Id') + 'Start_Of_Session_Id'.length(),
24+
e = content.indexOf('End_Of_Session_Id');
25+
return content.substring(s, e);
26+
}
27+
28+
@AuraEnabled(cacheable=true)
29+
public static String getRestAPIBaseUrl() {
30+
return URL.getSalesforceBaseUrl().toExternalForm() + '/services/data/v51.0';
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>49.0</apiVersion>
4+
<status>Active</status>
5+
</ApexClass>

force-app/main/default/customMetadata/JS_Button.Account.md-meta.xml

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<protected>false</protected>
55
<values>
66
<field>Script__c</field>
7-
<value xsi:type="xsd:string">let acct = || Select NumberOfEmployees from Account where Id=&apos;${recordId}&apos; ||;
7+
<value xsi:type="xsd:string">let acct = await soql(`Select NumberOfEmployees from Account where Id=&apos;${recordId}&apos;`);
88

99
acct[0].NumberOfEmployees = (acct[0].NumberOfEmployees || 0) + 10;
1010

11-
let acctId = || update acct ||;
11+
let acctId = await dml.update(acct);
1212

13-
acct = || Select NumberOfEmployees from Account where Id=&apos;${acctId}&apos; ||;
13+
acct = await soql(`Select NumberOfEmployees from Account where Id=&apos;${acctId}&apos;`);
1414

15-
alert(acct[0].NumberOfEmployees);
15+
toast(`Number of employees updated to ${acct[0].NumberOfEmployees}`,"success");
1616

1717
$A.get(&apos;e.force:refreshView&apos;).fire();</value>
1818
</values>

force-app/main/default/customMetadata/JS_Button.Contact.md-meta.xml

+13-13
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44
<protected>false</protected>
55
<values>
66
<field>Script__c</field>
7-
<value xsi:type="xsd:string">let con = || select LastName from Contact where Id=&apos;${recordId}&apos; ||;
8-
9-
let files = Array(5).fill(0).map((e,i)=&gt;({ VersionData: btoa(con[0].LastName+&apos;-&apos;+i)), PathOnClient: &apos;file.txt&apos;, Title: con[0].LastName+&apos;-File-&apos;+i }));
10-
11-
let fileIds = || insert ContentVersion(files) ||;
12-
13-
let docIds = || select ContentDocumentId from ContentVersion where Id in (&apos;${fileIds.join(&quot;&apos;,&apos;&quot;)}&apos;) ||;
14-
15-
let linkedEntities = docIds.map((e,i)=&gt;({LinkedEnityId: acct[0].Id, ContentDocumentId: e.Id});
16-
17-
|| insert linkedEntities ||;
18-
19-
alert(&apos;done&apos;);</value>
7+
<value xsi:type="xsd:string">/* Creates 5 files related to the current Contact record */
8+
let con = await soql(`select LastName from Contact where Id=&apos;${recordId}&apos;`);
9+
let files = Array(5)
10+
.fill(0)
11+
.map((e, i) =&gt; ({
12+
VersionData: btoa(con[0].LastName + &quot;-&quot; + i),
13+
PathOnClient: &quot;file.txt&quot;,
14+
Title: con[0].LastName + &quot;-File-&quot; + i,
15+
FirstPublishLocationId: recordId
16+
}));
17+
let fileIds = await dml.insert(files, &quot;ContentVersion&quot;);
18+
toast(&quot;done&quot;, &quot;success&quot;);
19+
$A.get(&apos;e.force:refreshView&apos;).fire();</value>
2020
</values>
2121
</CustomMetadata>

0 commit comments

Comments
 (0)