├── .eslintignore
├── .eslintrc.js
├── .forceignore
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── config
└── project-scratch-def.json
├── force-app
└── main
│ └── default
│ ├── aura
│ ├── jsButton
│ │ ├── jsButton.cmp
│ │ ├── jsButton.cmp-meta.xml
│ │ ├── jsButton.design
│ │ ├── jsButtonController.js
│ │ └── jsButtonHelper.js
│ └── jsButtonQuickAction
│ │ ├── jsButtonQuickAction.cmp
│ │ ├── jsButtonQuickAction.cmp-meta.xml
│ │ └── jsButtonQuickActionController.js
│ ├── classes
│ ├── APICallController.cls
│ ├── APICallController.cls-meta.xml
│ ├── APICallControllerTest.cls
│ ├── APICallControllerTest.cls-meta.xml
│ ├── DynamicSOQLDMLController.cls
│ ├── DynamicSOQLDMLController.cls-meta.xml
│ ├── DynamicSOQLDMLControllerTest.cls
│ ├── DynamicSOQLDMLControllerTest.cls-meta.xml
│ ├── GetSessionIdController.cls
│ ├── GetSessionIdController.cls-meta.xml
│ ├── HttpResponseWrapper.cls
│ └── HttpResponseWrapper.cls-meta.xml
│ ├── customMetadata
│ ├── JS_Button.Account.md-meta.xml
│ └── JS_Button.Contact.md-meta.xml
│ ├── layouts
│ └── JS_Button__mdt-JS Button Layout.layout-meta.xml
│ ├── lwc
│ ├── .eslintrc.json
│ ├── apiService
│ │ ├── apiService.js
│ │ └── apiService.js-meta.xml
│ ├── dmlService
│ │ ├── dmlService.js
│ │ └── dmlService.js-meta.xml
│ ├── httpRequest
│ │ ├── httpRequest.js
│ │ └── httpRequest.js-meta.xml
│ ├── jsButtonLwc
│ │ ├── jsButtonLwc.html
│ │ ├── jsButtonLwc.js
│ │ ├── jsButtonLwc.js-meta.xml
│ │ └── jsButtonLwc.svg
│ ├── soqlService
│ │ ├── soqlService.js
│ │ └── soqlService.js-meta.xml
│ └── toastService
│ │ ├── toastService.js
│ │ └── toastService.js-meta.xml
│ ├── objects
│ └── JS_Button__mdt
│ │ ├── JS_Button__mdt.object-meta.xml
│ │ └── fields
│ │ └── Script__c.field-meta.xml
│ ├── pages
│ ├── GetSessionId.page
│ └── GetSessionId.page-meta.xml
│ └── quickActions
│ ├── Account.Add_Employees.quickAction-meta.xml
│ └── Contact.Add_Files.quickAction-meta.xml
├── package-lock.json
├── package.json
├── scripts
└── jsButton
│ ├── README.md
│ ├── addDefaultOpportunityLineItems.js
│ ├── compositeApiExample.js
│ ├── copyData.js
│ ├── createContactFiles.js
│ ├── createContactsUsingCallout.js
│ ├── createNewJSButton.js
│ ├── deleteInactiveFlowVersions.js
│ ├── fetchAndAddFile.js
│ └── fetchAndCreateContacts.js
└── sfdx-project.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/lwc/**/*.css
2 | **/lwc/**/*.html
3 | **/lwc/**/*.json
4 | **/lwc/**/*.svg
5 | **/lwc/**/*.xml
6 | .sfdx
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true
5 | },
6 | extends: "eslint:recommended",
7 | globals: {
8 | Atomics: "readonly",
9 | SharedArrayBuffer: "readonly"
10 | },
11 | parserOptions: {
12 | ecmaVersion: 2018,
13 | sourceType: "module"
14 | },
15 | rules: {}
16 | };
17 |
--------------------------------------------------------------------------------
/.forceignore:
--------------------------------------------------------------------------------
1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
3 | #
4 |
5 | package.xml
6 |
7 | # LWC configuration files
8 | **/jsconfig.json
9 | **/.eslintrc.json
10 |
11 | # LWC Jest
12 | **/__tests__/**
13 | **/profiles/**
14 | **/flexipages/**
15 | **/layouts/Account*
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore.
2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore
3 | # For useful gitignore templates see: https://github.com/github/gitignore
4 |
5 | # Salesforce cache
6 | .sfdx/
7 | .localdevserver/
8 |
9 | # LWC VSCode autocomplete
10 | **/lwc/jsconfig.json
11 |
12 | # LWC Jest coverage reports
13 | coverage/
14 |
15 | # Logs
16 | logs
17 | *.log
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | # Dependency directories
23 | node_modules/
24 |
25 | # Eslint cache
26 | .eslintcache
27 |
28 | # MacOS system files
29 | .DS_Store
30 |
31 | # Windows system files
32 | Thumbs.db
33 | ehthumbs.db
34 | [Dd]esktop.ini
35 | $RECYCLE.BIN/
36 | flexipages/
37 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # List files or directories below to ignore them when running prettier
2 | # More information: https://prettier.io/docs/en/ignore.html
3 | #
4 |
5 | **/staticresources/**
6 | .localdevserver
7 | .sfdx
8 |
9 | coverage/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "printWidth": 120,
4 | "overrides": [
5 | {
6 | "files": "**/lwc/**/*.html",
7 | "options": { "parser": "lwc" }
8 | },
9 | {
10 | "files": "*.{cmp,page,component}",
11 | "options": { "parser": "html"}
12 | },
13 | {
14 | "files": "*.{cls,trigger,apex}",
15 | "options": { "parser": "apex"}
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "salesforce.salesforcedx-vscode",
4 | "redhat.vscode-xml",
5 | "dbaeumer.vscode-eslint",
6 | "esbenp.prettier-vscode"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Launch Apex Replay Debugger",
9 | "type": "apex-replay",
10 | "request": "launch",
11 | "logFile": "${command:AskForLogFileName}",
12 | "stopOnEntry": true,
13 | "trace": true
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "search.exclude": {
3 | "**/node_modules": true,
4 | "**/bower_components": true,
5 | "**/.sfdx": true
6 | }
7 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Suraj Pillai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pure JS Buttons in Lightning
2 |
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.
9 |
10 | ### The Setup
11 |
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 )
31 |
32 | ### The Syntax
33 |
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.
37 |
38 | - Simple examples (no soql/dml)
39 |
40 | ```js
41 | alert("hello,world");
42 | ```
43 |
44 | ```js
45 | toast(
46 | Array(5)
47 | .fill(0)
48 | .map((e, i) => "Hello, " + i)
49 | ); /* `toast` service to show message toasts */
50 | ```
51 |
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!*/
68 | ```
69 |
70 | - Act in the context of the current record
71 |
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 */
77 | acct[0].NumberOfEmployees = (acct[0].NumberOfEmployees || 0) + 10;
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 | - Add a 'File' to the current record
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 | FirstPublishLocationId: recordId
94 | };
95 | let cvId = await dml.insert(cv, "ContentVersion");
96 | toast("New file Added", "success");
97 | $A.get("e.force:refreshView").fire();
98 | ```
99 |
100 | ### About the Syntax
101 |
102 | - Note how the syntax is linear for SOQL and DML. Coupled with JavaScript's
103 | support for manipulating arrays, this makes it easier to manipulate data,
104 | even compared to Apex in several instances.
105 | - `dml.insert` and `dml.upsert` expect the SObjectType as the second argument.
106 | Thus `dml.insert(acct,"Account")`
107 | - Statements with contextual arguments such as `recordId`
108 | are best expressed using [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals).
109 | - All statements must be strictly terminated by a semicolon.
110 |
111 | ### Known Limitations
112 |
113 | - Single-line comments are not supported.
114 | - Haven't tested DML with date, datetime, boolean, geolocation and other
115 | compound fields. I will update this section as I do so.
116 | - To insert `ContentVersion` make sure to set `VersionData` to base64 data.
117 | Refer to the example [here](./scripts/jsButton/createContactFiles.js) for details.
118 | - The maximum size of files I was able to upload was around 2 MB.
119 | Anything larger will fail silently due to heap size limits in Apex
120 |
121 | ### Using Salesforce (and other) APIs in your script
122 |
123 | To use Salesforce APIs from your org, using the `sfapi` method, take the following steps:
124 |
125 | - Add your lightning domain (ends with `lightning.force.com`) to the `CORS` list under `Setup`.
126 | - Add your classic domain to `CSP Trusted Sites` list under `Setup`.
127 |
128 | 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)
129 | for creating parent and child records.
130 | 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.
131 |
--------------------------------------------------------------------------------
/config/project-scratch-def.json:
--------------------------------------------------------------------------------
1 | {
2 | "orgName": "Demo company",
3 | "edition": "Developer",
4 | "features": [],
5 | "settings": {
6 | "lightningExperienceSettings": {
7 | "enableS1DesktopEnabled": true
8 | },
9 | "securitySettings": {
10 | "passwordPolicies": {
11 | "enableSetPasswordInApi": true
12 | }
13 | },
14 | "mobileSettings": {
15 | "enableS1EncryptedStoragePref2": false
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/jsButton/jsButton.cmp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/jsButton/jsButton.cmp-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 48.0
4 | A Lightning Component Bundle
5 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/jsButton/jsButton.design:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/jsButton/jsButtonController.js:
--------------------------------------------------------------------------------
1 | ({
2 | invoke: function (component, event, helper) {
3 | return new Promise(
4 | $A.getCallback((resolve, reject) => {
5 | let js = component.get("v.js");
6 | if (!js && component.get("v.cmdtName")) {
7 | helper
8 | .fetchJSFromCmdt(component)
9 | .then(() => {
10 | helper.runJS(component, resolve, reject);
11 | })
12 | .catch((err) => {
13 | reject(err);
14 | });
15 | } else if (js) {
16 | helper.runJS(component, resolve, reject);
17 | }
18 | })
19 | );
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/jsButton/jsButtonHelper.js:
--------------------------------------------------------------------------------
1 | ({
2 | /*
3 | regexp: {
4 | SOQL: new RegExp(/\|\|\s?(select\s+[^|]+)\s?\|\|/, "gi")
5 | },
6 | */
7 | REGEX_SOQL: "\\|\\|\\s?(select\\s+[^|]+)\\s?\\|\\|",
8 | REGEX_UPDATE: "\\|\\|\\s?update\\s([^|;]+);?\\s*\\|\\|",
9 | REGEX_INSERT_UPSERT:
10 | "\\|\\|\\s?(insert|upsert)\\s([\\w\\d_]+)\\s?\\(\\s?(\\w+).*\\|\\|",
11 |
12 | callApexAction: function (component, action) {
13 | return new Promise(
14 | $A.getCallback((resolve, reject) => {
15 | action.setCallback(this, (result) => {
16 | if (component.isValid() && result.getState() === "SUCCESS") {
17 | resolve(result.getReturnValue());
18 | } else {
19 | reject(
20 | result.getError() ? result.getError()[0].message : "Unknown error"
21 | );
22 | }
23 | });
24 | $A.enqueueAction(action);
25 | })
26 | );
27 | },
28 |
29 | runJS: function (component, resolve, reject) {
30 | let helper = this;
31 | let recordId = component.get("v.recordId");
32 | let js = component.get("v.js");
33 | //replace consecutive spaces
34 | js = js.replace(/\s+/g, " ");
35 |
36 | //parse soql
37 | js = js.replace(
38 | new RegExp(helper.REGEX_SOQL, "gi"),
39 | "await helper.executeSoql(cmp,`$1`);"
40 | );
41 |
42 | //parse updates
43 | js = js.replace(
44 | new RegExp(helper.REGEX_UPDATE, "gi"),
45 | "await helper.executeDml(cmp,'update',$1);"
46 | );
47 |
48 | //parse inserts
49 | js = js.replace(
50 | new RegExp(helper.REGEX_INSERT_UPSERT, "gi"),
51 | "await helper.executeDml(cmp,'$1',$3,'$2');"
52 | );
53 |
54 | Function(
55 | "recordId",
56 | "cmp",
57 | "helper",
58 | `return (async (recordId,cmp,helper)=>{${js}})(recordId,cmp,helper)`
59 | )(recordId, component, helper)
60 | .then((op) => {
61 | if (Array.isArray(op)) {
62 | if (op[0] && typeof op[0] !== "string")
63 | op = op.map((row) => JSON.stringify(row));
64 | component.set("v.outputArray", op);
65 | } else component.set("v.output", op);
66 | resolve();
67 | })
68 | .catch((err) => {
69 | reject(err);
70 | });
71 | },
72 |
73 | fetchJSFromCmdt: function (cmp) {
74 | let helper = this;
75 | return new Promise(
76 | $A.getCallback((resolve, reject) => {
77 | let cmdtAction = cmp.get("c.getJSFromCmdt");
78 | cmdtAction.setParams({ cmdtName: cmp.get("v.cmdtName") });
79 | helper
80 | .callApexAction(cmp, cmdtAction)
81 | .then((js) => {
82 | cmp.set("v.js", js);
83 | resolve();
84 | })
85 | .catch((err) => {
86 | reject(err);
87 | });
88 | })
89 | );
90 | },
91 |
92 | executeSoql: function (cmp, soql) {
93 | return new Promise(
94 | $A.getCallback((resolve, reject) => {
95 | let soqlAction = cmp.get("c.executeSoqlQuery");
96 | soqlAction.setParams({ query: soql });
97 | soqlAction.setCallback(this, (result) => {
98 | resolve(result.getReturnValue());
99 | });
100 | $A.enqueueAction(soqlAction);
101 | })
102 | );
103 | },
104 |
105 | getSObjectType: function (cmp, record, sobjectType) {
106 | let helper = this;
107 | return new Promise(
108 | $A.getCallback((resolve, reject) => {
109 | if (!sobjectType) {
110 | let getSobjectTypeAction = cmp.get("c.getSObjectTypeFromId");
111 | getSobjectTypeAction.setParams({ recordId: record.Id });
112 | helper
113 | .callApexAction(cmp, getSobjectTypeAction)
114 | .then((type) => resolve(type))
115 | .catch((err) => reject(err));
116 | } else resolve(sobjectType);
117 | })
118 | );
119 | },
120 |
121 | executeDml: function (cmp, dmlType, records, sobjectType) {
122 | let helper = this;
123 | return new Promise(
124 | $A.getCallback((resolve, reject) => {
125 | if (!Array.isArray(records)) records = [records];
126 | helper
127 | .getSObjectType(cmp, records[0], sobjectType)
128 | .then((sobjectType) => {
129 | records.forEach((rec) =>
130 | Object.assign(rec, { attributes: { type: sobjectType } })
131 | );
132 | let dmlAction = cmp.get("c.executeDml");
133 | dmlAction.setParams({
134 | operation: dmlType,
135 | strData: sobjectType
136 | ? JSON.stringify(records, (k, v) =>
137 | typeof v === "number" ? "" + v : v
138 | )
139 | : null,
140 | sObjectType: sobjectType
141 | });
142 | helper
143 | .callApexAction(cmp, dmlAction)
144 | .then((res) => resolve(res))
145 | .catch((err) => reject(err));
146 | })
147 | .catch((err) => reject(err));
148 | })
149 | );
150 | }
151 | });
152 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickAction.cmp:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickAction.cmp-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 48.0
4 | A Lightning Component Bundle
5 |
6 |
--------------------------------------------------------------------------------
/force-app/main/default/aura/jsButtonQuickAction/jsButtonQuickActionController.js:
--------------------------------------------------------------------------------
1 | ({
2 | doInit: function (component) {
3 | component
4 | .find("jsbutton")
5 | .invoke()
6 | .then(
7 | $A.getCallback((resp) => {
8 | console.log(">> resp " + JSON.stringify(resp));
9 | $A.get("e.force:closeQuickAction").fire();
10 | })
11 | )
12 | .catch(
13 | $A.getCallback((err) => {
14 | $A.get("e.force:closeQuickAction").fire();
15 | })
16 | );
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/force-app/main/default/classes/APICallController.cls:
--------------------------------------------------------------------------------
1 | /************************************************************
2 |
3 | *** @author Suraj Pillai
4 | *** @group Controller
5 | *** @description Controller for making API calls and sending the response back
6 | ***
7 | **/
8 | public with sharing class APICallController {
9 | /**
10 | * @description Given an endpoint, request params and headers, callout the api and return the response
11 | * @param endPoint The endpoint to callout to
12 | * @param method The http method to use
13 | * @param bodyStr The request body string.
14 | * @param headers Map of string key and value for request headers
15 | * @return The response for the http request
16 | *
17 | */
18 | @AuraEnabled
19 | public static HttpResponseWrapper makeApiCall(
20 | String endPoint,
21 | String method,
22 | String bodyStr,
23 | Map headers
24 | ) {
25 | HttpRequest req = new HttpRequest();
26 | req.setEndpoint(endPoint);
27 | req.setMethod(method);
28 | if (method != 'GET') {
29 | req.setBody(bodyStr);
30 | }
31 | if (headers != null) {
32 | for (String key : headers.keySet()) {
33 | req.setHeader(key, headers.get(key));
34 | }
35 | }
36 | HttpResponse resp = new Http().send(req);
37 | Map respHeaders = new Map();
38 | for (String key : resp.getHeaderKeys()) {
39 | respHeaders.put(key, String.valueOf(resp.getHeader(key)));
40 | }
41 | return new HttpResponseWrapper(resp.getBody(), resp.getStatusCode(), respHeaders);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/force-app/main/default/classes/APICallController.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 48.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/main/default/classes/APICallControllerTest.cls:
--------------------------------------------------------------------------------
1 | @isTest
2 | public with sharing class APICallControllerTest {
3 | @isTest
4 | public static void testAPICall() {
5 | Test.setMock(HttpCalloutMock.class, new APICallMock());
6 | HttpResponseWrapper resp = APICallController.makeApiCall(
7 | 'https://api.example.com',
8 | 'POST',
9 | '{"message":"sample_request"}',
10 | new Map{ 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
11 | );
12 | system.assertEquals('{"message": "sample response"}', resp.body, 'Unexpected Response');
13 | system.assertEquals(200, resp.statusCode, 'Incorrect value for status code');
14 | system.assertEquals(2, resp.headers.size(), 'Mismatch in the number of response headers expected');
15 | system.assertEquals('sample_value1', resp.headers.get('custom_header1'), 'Incorrect value for first header');
16 | system.assertEquals('sample_value2', resp.headers.get('custom_header2'), 'Incorrect value for second header');
17 | }
18 |
19 | class APICallMock implements HttpCalloutMock {
20 | public HttpResponse respond(HttpRequest req) {
21 | HttpResponse resp = new HttpResponse();
22 | if (
23 | req.getBody() == '{"message":"sample_request"}' &&
24 | req.getHeader('Accept') == 'application/json' &&
25 | req.getHeader('Content-Type') == 'application/json'
26 | ) {
27 | resp.setBody('{"message": "sample response"}');
28 | resp.setHeader('custom_header1', 'sample_value1');
29 | resp.setHeader('custom_header2', 'sample_value2');
30 | resp.setStatusCode(200);
31 | } else {
32 | resp.setStatusCode(400);
33 | resp.setBody('{"message":"Bad Request"}');
34 | }
35 | return resp;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/force-app/main/default/classes/APICallControllerTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 48.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/main/default/classes/DynamicSOQLDMLController.cls:
--------------------------------------------------------------------------------
1 | public with sharing class DynamicSOQLDMLController {
2 | /**
3 | * Execute a Soql Query
4 | * @param query The soql query to execute
5 | *
6 | * @return SObject[]
7 | **/
8 | @AuraEnabled
9 | public static SObject[] executeSoqlQuery(String query) {
10 | return Database.query(query);
11 | }
12 |
13 | /**
14 | * Get JavaScript from a Custom Metadata with a given name
15 | * @param cmdtName The name of Custom Metadata record to fetch the script from
16 | *
17 | * @return String
18 | **/
19 | @AuraEnabled(cacheable=true)
20 | public static String getJSFromCmdt(String cmdtName) {
21 | JS_Button__mdt[] jsButton = [SELECT Script__c FROM JS_Button__mdt WHERE DeveloperName = :cmdtName LIMIT 1];
22 | if (jsButton.size() == 1)
23 | return jsButton[0].Script__c;
24 | else
25 | return '';
26 | }
27 |
28 | /**
29 | * Short Description
30 | * @param recordId Get the SObject Type given a record Id
31 | *
32 | * @return String
33 | **/
34 | @AuraEnabled(cacheable=true)
35 | public static String getSObjectTypeFromId(Id recordId) {
36 | return recordId.getSObjectType().getDescribe().getName();
37 | }
38 |
39 | private static List deserializeContentVersion(String strData) {
40 | List