diff --git a/.changeset/ten-mayflies-nail.md b/.changeset/ten-mayflies-nail.md new file mode 100644 index 000000000..00e40c028 --- /dev/null +++ b/.changeset/ten-mayflies-nail.md @@ -0,0 +1,7 @@ +--- +'@openfn/language-salesforce': major +--- + +- add `query` option in `request` function +- remove `callback` option in `query` function +- update function examples and improve options documentation diff --git a/packages/salesforce/ast.json b/packages/salesforce/ast.json index 152355eca..e00212347 100644 --- a/packages/salesforce/ast.json +++ b/packages/salesforce/ast.json @@ -9,7 +9,7 @@ "options" ], "docs": { - "description": "Create and execute a bulk job.", + "description": "Create and execute a bulk job.\nThis function uses {@link https://sforce.co/4fDLJnk Bulk API},\nwhich is subject to {@link https://sforce.co/4b6kn6z rate limits}.", "tags": [ { "title": "public", @@ -26,6 +26,11 @@ "description": "bulk(\n \"vera__Beneficiary__c\",\n \"upsert\",\n [\n {\n vera__Reporting_Period__c: 2023,\n vera__Geographic_Area__c: \"Uganda\",\n \"vera__Indicator__r.vera__ExtId__c\": 1001,\n vera__Result_UID__c: \"1001_2023_Uganda\",\n },\n ],\n { extIdField: \"vera__Result_UID__c\" }\n);", "caption": "Bulk upsert" }, + { + "title": "example", + "description": "fn((state) => {\n state.accounts = state.data.map((a) => ({ Id: a.id, Name: a.name }));\n return state;\n});\nbulk(\"Account\", \"update\", $.accounts, { failOnError: true });", + "caption": "Bulk update Account records using a lazy state reference" + }, { "title": "function", "description": null, @@ -42,7 +47,7 @@ }, { "title": "param", - "description": "The bulk operation to be performed.Eg \"insert\" | \"update\" | \"upsert\"", + "description": "The bulk operation to be performed.Eg `insert`, `update` or `upsert`", "type": { "type": "NameExpression", "name": "string" @@ -60,76 +65,32 @@ }, { "title": "param", - "description": "Options passed to the bulk api.", + "description": "Options to configure the request. In addition to these, you can pass any of the options supported by the {@link https://bit.ly/41tyvVU jsforce API}.", "type": { "type": "NameExpression", - "name": "object" + "name": "Options" }, "name": "options" }, { - "title": "param", - "description": "External id field.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "string" - } - }, - "name": "options.extIdField" + "title": "state", + "description": "{SalesforceState}" }, { - "title": "param", - "description": "Skipping bulk operation if no records.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "boolean" - } - }, - "name": "options.allowNoOp", - "default": "false" + "title": "state", + "description": "{Object[]} data - An array of result objects." }, { - "title": "param", - "description": "Fail the operation on error.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "boolean" - } - }, - "name": "options.failOnError", - "default": "false" + "title": "state", + "description": "{string} data[].id - The unique identifier of the result." }, { - "title": "param", - "description": "Polling interval in milliseconds.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "integer" - } - }, - "name": "options.pollInterval", - "default": "6000" + "title": "state", + "description": "{boolean} data[].success - Indicates whether the operation was successful." }, { - "title": "param", - "description": "Polling timeout in milliseconds.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "integer" - } - }, - "name": "options.pollTimeout", - "default": "240000" + "title": "state", + "description": "{Array} data[].errors - An array of error messages, if any." }, { "title": "returns", @@ -141,16 +102,16 @@ } ] }, - "valid": false + "valid": true }, { "name": "bulkQuery", "params": [ - "qs", + "query", "options" ], "docs": { - "description": "Execute an SOQL Bulk Query.\nThis function uses bulk query to efficiently query large data sets and reduce the number of API requests.\n`bulkQuery()` uses {@link https://sforce.co/4azgczz Bulk API v.2.0 Query} which is available in API version 47.0 and later.\nThis API is subject to {@link https://sforce.co/4b6kn6z rate limits}.", + "description": "Execute an SOQL Bulk Query.\nThis function query large data sets and reduce the number of API requests.\n`bulkQuery()` uses {@link https://sforce.co/4azgczz Bulk API v2.0 Query} which is available in API version 47.0 and later.\nThis API is subject to {@link https://sforce.co/4b6kn6z rate limits}.", "tags": [ { "title": "public", @@ -159,12 +120,13 @@ }, { "title": "example", - "description": "bulkQuery(state=> `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`);", - "caption": "The results will be available on `state.data`" + "description": "bulkQuery(`SELECT Id FROM Patient__c WHERE Health_ID__c = '${$.data.healthId}'`);", + "caption": "Bulk query patient records where \"Health_ID__c\" is equal to the value in \"state.data.healthId\"" }, { "title": "example", - "description": "bulkQuery(\n (state) =>\n `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`,\n { pollTimeout: 10000, pollInterval: 6000 }\n);" + "description": "bulkQuery(\n (state) =>\n `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`,\n { pollTimeout: 10000, pollInterval: 6000 }\n);", + "caption": "Bulk query with custom polling options" }, { "title": "function", @@ -178,42 +140,20 @@ "type": "NameExpression", "name": "string" }, - "name": "qs" + "name": "query" }, { "title": "param", "description": "Options passed to the bulk api.", "type": { "type": "NameExpression", - "name": "object" + "name": "BulkQueryOptions" }, "name": "options" }, { - "title": "param", - "description": "Polling timeout in milliseconds.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "integer" - } - }, - "name": "options.pollTimeout", - "default": "90000" - }, - { - "title": "param", - "description": "Polling interval in milliseconds.", - "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "integer" - } - }, - "name": "options.pollInterval", - "default": "3000" + "title": "state", + "description": "{SalesforceState}" }, { "title": "returns", @@ -225,7 +165,7 @@ } ] }, - "valid": false + "valid": true }, { "name": "create", @@ -234,7 +174,7 @@ "records" ], "docs": { - "description": "Create a new sObject record(s).", + "description": "Create a new sObject record(s).\nThis function uses {@link https://jsforce.github.io/document/#create|jsforce create} under the hood.", "tags": [ { "title": "public", @@ -251,6 +191,11 @@ "description": "create(\"Account\",[{ Name: \"My Account #1\" }, { Name: \"My Account #2\" }]);", "caption": "Multiple records creation" }, + { + "title": "example", + "description": "fn((state) => {\n state.data = [{ Name: \"My Account #1\" }, { Name: \"My Account #2\" }];\n return state;\n});\ncreate(\"Account\", $.data);", + "caption": "Create a records from a state variable" + }, { "title": "function", "description": null, @@ -267,13 +212,35 @@ }, { "title": "param", - "description": "Field attributes for the new record.", + "description": "Field attributes for the new record, or an array of field attributes.", "type": { - "type": "NameExpression", - "name": "object" + "type": "UnionType", + "elements": [ + { + "type": "NameExpression", + "name": "Object" + }, + { + "type": "TypeApplication", + "expression": { + "type": "NameExpression", + "name": "Array" + }, + "applications": [ + { + "type": "NameExpression", + "name": "Object" + } + ] + } + ] }, "name": "records" }, + { + "title": "state", + "description": "{SalesforceState}" + }, { "title": "returns", "description": null, @@ -292,7 +259,7 @@ "sObjectName" ], "docs": { - "description": "Fetches and prints metadata for an sObject and pushes the result to `state.data`.\nIf `sObjectName` is not specified, it will print the total number of all available sObjects and push the result to `state.data`.", + "description": "Fetches and logs metadata for an sObject and pushes the result to `state.data`.\nIf `sObjectName` is not specified, it will print the total number of all available sObjects and push the result to `state.data`.", "tags": [ { "title": "public", @@ -318,14 +285,15 @@ "title": "param", "description": "The API name of the sObject. If omitted, fetches metadata for all sObjects.", "type": { - "type": "OptionalType", - "expression": { - "type": "NameExpression", - "name": "string" - } + "type": "NameExpression", + "name": "string" }, "name": "sObjectName" }, + { + "title": "state", + "description": "{SalesforceState}" + }, { "title": "returns", "description": null, @@ -346,7 +314,7 @@ "options" ], "docs": { - "description": "Delete records of an object.", + "description": "Delete records of an sObject.\nThis functions uses {@link https://jsforce.github.io/document/#delete|jsforce delete} under the hood.", "tags": [ { "title": "public", @@ -355,7 +323,13 @@ }, { "title": "example", - "description": "destroy('obj_name', [\n '0060n00000JQWHYAA5',\n '0090n00000JQEWHYAA5'\n], { failOnError: true })" + "description": "destroy(\"Account\", [\"001XXXXXXXXXXXXXXX\", \"001YYYYYYYYYYYYYYY\"], {\n failOnError: true,\n});", + "caption": "Allow operation to fail if any record fails to delete" + }, + { + "title": "example", + "description": "fn((state) => {\n state.data = [\"001XXXXXXXXXXXXXXX\", \"001YYYYYYYYYYYYYYY\"];\n return state;\n});\ndestroy(\"Account\", $.data);", + "caption": "Using a state variable" }, { "title": "function", @@ -375,8 +349,17 @@ "title": "param", "description": "Array of IDs of records to delete.", "type": { - "type": "NameExpression", - "name": "object" + "type": "TypeApplication", + "expression": { + "type": "NameExpression", + "name": "Array" + }, + "applications": [ + { + "type": "NameExpression", + "name": "string" + } + ] }, "name": "ids" }, @@ -389,6 +372,107 @@ }, "name": "options" }, + { + "title": "param", + "description": "If true, the operation will fail if any record fails to delete.", + "type": { + "type": "OptionalType", + "expression": { + "type": "NameExpression", + "name": "boolean" + } + }, + "name": "options.failOnError", + "default": "false" + }, + { + "title": "state", + "description": "{SalesforceState}" + }, + { + "title": "state", + "description": "{Object[]} data - An array of result objects." + }, + { + "title": "state", + "description": "{string} data[].id - The unique identifier of the result." + }, + { + "title": "state", + "description": "{boolean} data[].success - Indicates whether the operation was successful." + }, + { + "title": "state", + "description": "{Array} data[].errors - An array of error messages, if any." + }, + { + "title": "returns", + "description": null, + "type": { + "type": "NameExpression", + "name": "Operation" + } + } + ] + }, + "valid": false + }, + { + "name": "get", + "params": [ + "path", + "options" + ], + "docs": { + "description": "Send a GET request on salesforce server configured in `state.configuration`.", + "tags": [ + { + "title": "public", + "description": null, + "type": null + }, + { + "title": "example", + "description": "get('/actions/custom/flow/POC_OpenFN_Test_Flow');", + "caption": "Make a GET request to a custom Salesforce flow" + }, + { + "title": "example", + "description": "get('/actions/custom/flow/POC_OpenFN_Test_Flow', { query: { Status: 'Active' } });", + "caption": "Make a GET request to a custom Salesforce flow with query parameters" + }, + { + "title": "example", + "description": "get('/jobs/query/v1/jobs/001XXXXXXXXXXXXXXX/results', (state) => {\n // Mapping the response\n state.mapping = state.data.map(d => ({ name: d.name, id: d.extId }));\n return state;\n});", + "caption": "Make a GET request then map the response" + }, + { + "title": "function", + "description": null, + "name": null + }, + { + "title": "param", + "description": "The Salesforce API endpoint.", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "path" + }, + { + "title": "param", + "description": "Configure headers and query parameters for the request.", + "type": { + "type": "NameExpression", + "name": "RequestOptions" + }, + "name": "options" + }, + { + "title": "state", + "description": "{SalesforceState}" + }, { "title": "returns", "description": null, @@ -425,6 +509,11 @@ "description": "insert(\"Account\",[{ Name: \"My Account #1\" }, { Name: \"My Account #2\" }]);", "caption": "Multiple records creation" }, + { + "title": "example", + "description": "fn((state) => {\n state.data = [{ Name: \"My Account #1\" }, { Name: \"My Account #2\" }];\n return state;\n});\ninsert(\"Account\", $.data);", + "caption": "Using a state variable" + }, { "title": "function", "description": null, @@ -441,13 +530,35 @@ }, { "title": "param", - "description": "Field attributes for the new record.", + "description": "Field attributes for the new record, or an array of field attributes.", "type": { - "type": "NameExpression", - "name": "object" + "type": "UnionType", + "elements": [ + { + "type": "NameExpression", + "name": "Object" + }, + { + "type": "TypeApplication", + "expression": { + "type": "NameExpression", + "name": "Array" + }, + "applications": [ + { + "type": "NameExpression", + "name": "Object" + } + ] + } + ] }, "name": "records" }, + { + "title": "state", + "description": "{SalesforceState}" + }, { "title": "returns", "description": null, @@ -461,14 +572,14 @@ "valid": true }, { - "name": "query", + "name": "post", "params": [ - "qs", - "options", - "callback" + "path", + "data", + "options" ], "docs": { - "description": "Execute an SOQL query.\nNote that in an event of a query error,\nerror logs will be printed but the operation will not throw the error.\n\nThe Salesforce query API is subject to rate limits, {@link https://sforce.co/3W9zyaQ See for more details}.", + "description": "Send a POST request to salesforce server configured in `state.configuration`.", "tags": [ { "title": "public", @@ -477,12 +588,8 @@ }, { "title": "example", - "description": "query(state=> `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`);" - }, - { - "title": "example", - "description": "query(state=> `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`, { autoFetch: true });", - "caption": "Query more records if next records are available" + "description": "post(\"/actions/custom/flow/POC_OpenFN_Test_Flow\", {\n body: {\n inputs: [\n {\n CommentCount: 6,\n FeedItemId: \"0D5D0000000cfMY\",\n },\n ],\n },\n});", + "caption": "Make a POST request to a custom Salesforce flow" }, { "title": "function", @@ -491,43 +598,129 @@ }, { "title": "param", - "description": "A query string. Must be less than `4000` characters in WHERE clause", + "description": "The Salesforce API endpoint.", "type": { "type": "NameExpression", "name": "string" }, - "name": "qs" + "name": "path" }, { "title": "param", - "description": "Options passed to the bulk api.", + "description": "A JSON Object request body.", "type": { "type": "NameExpression", "name": "object" }, - "name": "options" + "name": "data" }, { "title": "param", - "description": "Fetch next records if available.", + "description": "Configure headers and query parameters for the request.", "type": { "type": "OptionalType", "expression": { "type": "NameExpression", - "name": "boolean" + "name": "RequestOptions" } }, - "name": "options.autoFetch", - "default": "false" + "name": "options" }, { - "title": "param", - "description": "A callback to execute once the record is retrieved", + "title": "state", + "description": "{SalesforceState}" + }, + { + "title": "returns", + "description": null, "type": { "type": "NameExpression", - "name": "function" + "name": "Operation" + } + } + ] + }, + "valid": true + }, + { + "name": "query", + "params": [ + "query", + "options" + ], + "docs": { + "description": "Executes an SOQL (Salesforce Object Query Language) query to retrieve records from Salesforce.\nThis operation uses {@link https://jsforce.github.io/document/#using-soql for querying salesforce records} using SOQL query and handles pagination.\nNote that in an event of a query error, error logs will be printed but the operation will not throw the error.\n\nThe Salesforce query API is subject to rate limits, {@link https://sforce.co/3W9zyaQ Learn more here}.", + "tags": [ + { + "title": "public", + "description": null, + "type": null + }, + { + "title": "example", + "description": "query('SELECT Id FROM Patient__c', { autoFetch: true });", + "caption": "Run a query and download all matching records" + }, + { + "title": "example", + "description": "query(state => `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.healthId}'`);", + "caption": "Query patients by Health ID" + }, + { + "title": "example", + "description": "query(`SELECT Id FROM Patient__c WHERE Health_ID__c = '${$.data.healthId}'`);", + "caption": "Query patients by Health ID using a lazy state reference" + }, + { + "title": "function", + "description": null, + "name": null + }, + { + "title": "param", + "description": "A SOQL query string or a function that returns a query string. Must be less than 4000 characters in WHERE clause", + "type": { + "type": "UnionType", + "elements": [ + { + "type": "NameExpression", + "name": "string" + }, + { + "type": "NameExpression", + "name": "function" + } + ] }, - "name": "callback" + "name": "query" + }, + { + "title": "param", + "description": "Optional configuration for the query operation", + "type": { + "type": "OptionalType", + "expression": { + "type": "NameExpression", + "name": "QueryOptions" + } + }, + "name": "options" + }, + { + "title": "state", + "description": "{SalesforceState}" + }, + { + "title": "state", + "description": "{boolean} data.done - Indicates whether the operation is complete." + }, + { + "title": "state", + "description": "{number} data.totalSize - The total number of items returned by the query." + }, + { + "title": "state", + "description": "{Object[]} data.records - The records returned by the query." }, { "title": "returns", @@ -539,7 +732,7 @@ } ] }, - "valid": false + "valid": true }, { "name": "upsert", @@ -549,7 +742,7 @@ "records" ], "docs": { - "description": "Create a new sObject record, or updates it if it already exists\nExternal ID field name must be specified in second argument.", + "description": "Create a new sObject record, or updates it if it already exists.\nThis function uses {@link https://jsforce.github.io/document/#upsert|jsforce upsert} under the hood.", "tags": [ { "title": "public", @@ -599,13 +792,13 @@ }, { "title": "param", - "description": "Field attributes for the new object.", + "description": "Field attributes for the records to upsert, or an array of field attributes.", "type": { "type": "UnionType", "elements": [ { "type": "NameExpression", - "name": "object" + "name": "Object" }, { "type": "TypeApplication", @@ -616,7 +809,7 @@ "applications": [ { "type": "NameExpression", - "name": "object" + "name": "Object" } ] } @@ -628,6 +821,10 @@ "title": "magic", "description": "records - $.children[?(@.name==\"{{args.sObject}}\")].children[?(!@.meta.externalId)]" }, + { + "title": "state", + "description": "{SalesforceState}" + }, { "title": "returns", "description": null, @@ -705,6 +902,10 @@ }, "name": "records" }, + { + "title": "state", + "description": "{SalesforceState}" + }, { "title": "returns", "description": null, @@ -732,7 +933,8 @@ }, { "title": "example", - "description": "fn((state) => {\n const s = toUTF8(\"άνθρωποι\");\n console.log(s); // anthropoi\n return state;\n});" + "description": "fn((state) => {\n const s = toUTF8(\"άνθρωποι\");\n console.log(s); // anthropoi\n return state;\n});", + "caption": "Transliterate `άνθρωποι` to `anthropoi`" }, { "title": "param", @@ -755,6 +957,67 @@ }, "valid": true }, + { + "name": "request", + "params": [ + "path", + "options" + ], + "docs": { + "description": "Send a request to salesforce server configured in `state.configuration`.", + "tags": [ + { + "title": "public", + "description": null, + "type": null + }, + { + "title": "example", + "description": "request(\"/actions/custom/flow/POC_OpenFN_Test_Flow\", {\n method: \"POST\",\n json: { inputs: [{}] },\n});", + "caption": "Make a POST request to a custom Salesforce flow" + }, + { + "title": "function", + "description": null, + "name": null + }, + { + "title": "param", + "description": "The Salesforce API endpoint.", + "type": { + "type": "NameExpression", + "name": "string" + }, + "name": "path" + }, + { + "title": "param", + "description": "Configure headers, query and body parameters for the request.", + "type": { + "type": "OptionalType", + "expression": { + "type": "NameExpression", + "name": "SalesforceRequestOptions" + } + }, + "name": "options" + }, + { + "title": "state", + "description": "{SalesforceState}" + }, + { + "title": "returns", + "description": null, + "type": { + "type": "NameExpression", + "name": "Operation" + } + } + ] + }, + "valid": true + }, { "name": "retrieve", "params": [ @@ -771,7 +1034,8 @@ }, { "title": "example", - "description": "retrieve('ContentVersion', '0684K0000020Au7QAE/VersionData');" + "description": "retrieve('ContentVersion', '0684K0000020Au7QAE/VersionData');", + "caption": "Retrieve a specific ContentVersion record" }, { "title": "function", @@ -796,6 +1060,10 @@ }, "name": "id" }, + { + "title": "state", + "description": "{SalesforceState}" + }, { "title": "returns", "description": null, diff --git a/packages/salesforce/src/Adaptor.js b/packages/salesforce/src/Adaptor.js index 9d96cbf63..5daab3309 100644 --- a/packages/salesforce/src/Adaptor.js +++ b/packages/salesforce/src/Adaptor.js @@ -11,6 +11,56 @@ * @ignore */ +/** + * State object + * @typedef {Object} SalesforceState + * @property data - Operation results + * @property references - History of all previous operations results + **/ + +/** + * Options provided to the Salesforce HTTP request + * @typedef {Object} SalesforceRequestOptions + * @public + * @property {string} [method=GET] - HTTP method to use. Defaults to GET + * @property {object} headers - Object of request headers + * @property {object} query - Object request query + * @property {object} json - Object request body + * @property {string} body - A string request body + */ + +/** + * @typedef {Object} RequestOptions + * @public + * @property {object} headers - Object of request headers + * @property {object} query - Object of request query + * */ + +/** + * Options provided to the Salesforce bulk API request + * @typedef {Object} Options + * @public + * @property {string} extIdField - External id field. Required for upsert. + * @property {boolean} [allowNoOp=false] - Skipping bulk operation if no records. Default: false + * @property {boolean} [failOnError=false] - Fail the operation on error. Default: false + * @property {integer} [pollTimeout=240000] - Polling timeout in milliseconds. + * @property {integer} [pollInterval=6000] - Polling interval in milliseconds. + */ + +/** + * Options provided to the Salesforce bulk query API request + * @typedef {Object} BulkQueryOptions + * @public + * @property {integer} [pollTimeout=90000] - Polling timeout in milliseconds. + * @property {integer} [pollInterval=3000] - Polling interval in milliseconds. + * */ + +/** + * @typedef {Object} QueryOptions + * @public + * @property {boolean} [autoFetch=false] - When true, automatically fetches next batch of records if available + * */ + import { execute as commonExecute, composeNextState, @@ -58,7 +108,10 @@ export function execute(...operations) { /** * Create and execute a bulk job. + * This function uses {@link https://sforce.co/4fDLJnk Bulk API}, + * which is subject to {@link https://sforce.co/4b6kn6z rate limits}. * @public + * * @example Bulk insert * bulk( * "Patient__c", @@ -80,16 +133,22 @@ export function execute(...operations) { * ], * { extIdField: "vera__Result_UID__c" } * ); + * @example Bulk update Account records using a lazy state reference + * fn((state) => { + * state.accounts = state.data.map((a) => ({ Id: a.id, Name: a.name })); + * return state; + * }); + * bulk("Account", "update", $.accounts, { failOnError: true }); * @function * @param {string} sObjectName - API name of the sObject. - * @param {string} operation - The bulk operation to be performed.Eg "insert" | "update" | "upsert" + * @param {string} operation - The bulk operation to be performed.Eg `insert`, `update` or `upsert` * @param {array} records - an array of records, or a function which returns an array. - * @param {object} options - Options passed to the bulk api. - * @param {string} [options.extIdField] - External id field. - * @param {boolean} [options.allowNoOp=false] - Skipping bulk operation if no records. - * @param {boolean} [options.failOnError=false] - Fail the operation on error. - * @param {integer} [options.pollInterval=6000] - Polling interval in milliseconds. - * @param {integer} [options.pollTimeout=240000] - Polling timeout in milliseconds. + * @param {Options} options - Options to configure the request. In addition to these, you can pass any of the options supported by the {@link https://bit.ly/41tyvVU jsforce API}. + * @state {SalesforceState} + * @state {Object[]} data - An array of result objects. + * @state {string} data[].id - The unique identifier of the result. + * @state {boolean} data[].success - Indicates whether the operation was successful. + * @state {Array} data[].errors - An array of error messages, if any. * @returns {Operation} */ export function bulk(sObjectName, operation, records, options = {}) { @@ -188,44 +247,46 @@ export function bulk(sObjectName, operation, records, options = {}) { } /** * Execute an SOQL Bulk Query. - * This function uses bulk query to efficiently query large data sets and reduce the number of API requests. - * `bulkQuery()` uses {@link https://sforce.co/4azgczz Bulk API v.2.0 Query} which is available in API version 47.0 and later. + * This function query large data sets and reduce the number of API requests. + * `bulkQuery()` uses {@link https://sforce.co/4azgczz Bulk API v2.0 Query} which is available in API version 47.0 and later. * This API is subject to {@link https://sforce.co/4b6kn6z rate limits}. * @public - * @example - * The results will be available on `state.data` - * bulkQuery(state=> `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`); - * @example + * @example Bulk query patient records where "Health_ID__c" is equal to the value in "state.data.healthId" + * bulkQuery(`SELECT Id FROM Patient__c WHERE Health_ID__c = '${$.data.healthId}'`); + * @example Bulk query with custom polling options * bulkQuery( * (state) => * `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`, * { pollTimeout: 10000, pollInterval: 6000 } * ); * @function - * @param {string} qs - A query string. - * @param {object} options - Options passed to the bulk api. - * @param {integer} [options.pollTimeout=90000] - Polling timeout in milliseconds. - * @param {integer} [options.pollInterval=3000] - Polling interval in milliseconds. + * @param {string} query - A query string. + * @param {BulkQueryOptions} options - Options passed to the bulk api. + * @state {SalesforceState} * @returns {Operation} */ -export function bulkQuery(qs, options = {}) { +export function bulkQuery(query, options = {}) { return async state => { const { connection } = state; - const [resolvedQs, resolvedOptions] = expandReferences(state, qs, options); + const [resolvedQuery, resolvedOptions] = expandReferences( + state, + query, + options + ); if (parseFloat(connection.version) < 47.0) throw new Error('bulkQuery requires API version 47.0 and later'); const { pollTimeout = 90000, pollInterval = 3000 } = resolvedOptions; - console.log(`Executing query: ${resolvedQs}`); + console.log(`Executing query: ${resolvedQuery}`); const queryJob = await connection.request({ method: 'POST', url: `/services/data/v${connection.version}/jobs/query`, body: JSON.stringify({ operation: 'query', - query: resolvedQs, + query: resolvedQuery, }), headers: { 'Content-Type': 'application/json', @@ -245,14 +306,22 @@ export function bulkQuery(qs, options = {}) { /** * Create a new sObject record(s). + * This function uses {@link https://jsforce.github.io/document/#create|jsforce create} under the hood. * @public * @example Single record creation * create("Account", { Name: "My Account #1" }); * @example Multiple records creation * create("Account",[{ Name: "My Account #1" }, { Name: "My Account #2" }]); + * @example Create a records from a state variable + * fn((state) => { + * state.data = [{ Name: "My Account #1" }, { Name: "My Account #2" }]; + * return state; + * }); + * create("Account", $.data); * @function * @param {string} sObjectName - API name of the sObject. - * @param {object} records - Field attributes for the new record. + * @param {(Object|Object[])} records - Field attributes for the new record, or an array of field attributes. + * @state {SalesforceState} * @returns {Operation} */ export function create(sObjectName, records) { @@ -275,7 +344,7 @@ export function create(sObjectName, records) { } /** - * Fetches and prints metadata for an sObject and pushes the result to `state.data`. + * Fetches and logs metadata for an sObject and pushes the result to `state.data`. * If `sObjectName` is not specified, it will print the total number of all available sObjects and push the result to `state.data`. * @public * @example Fetch metadata for all available sObjects @@ -283,7 +352,8 @@ export function create(sObjectName, records) { * @example Fetch metadata for Account sObject * describe('Account') * @function - * @param {string} [sObjectName] - The API name of the sObject. If omitted, fetches metadata for all sObjects. + * @param {string} sObjectName - The API name of the sObject. If omitted, fetches metadata for all sObjects. + * @state {SalesforceState} * @returns {Operation} */ export function describe(sObjectName) { @@ -311,17 +381,29 @@ export function describe(sObjectName) { } /** - * Delete records of an object. + * Delete records of an sObject. + * This functions uses {@link https://jsforce.github.io/document/#delete|jsforce delete} under the hood. * @public - * @example - * destroy('obj_name', [ - * '0060n00000JQWHYAA5', - * '0090n00000JQEWHYAA5' - * ], { failOnError: true }) + * @example Allow operation to fail if any record fails to delete + * destroy("Account", ["001XXXXXXXXXXXXXXX", "001YYYYYYYYYYYYYYY"], { + * failOnError: true, + * }); + * @example Using a state variable + * fn((state) => { + * state.data = ["001XXXXXXXXXXXXXXX", "001YYYYYYYYYYYYYYY"]; + * return state; + * }); + * destroy("Account", $.data); * @function * @param {string} sObjectName - API name of the sObject. - * @param {object} ids - Array of IDs of records to delete. + * @param {string[]} ids - Array of IDs of records to delete. * @param {object} options - Options for the destroy delete operation. + * @param {boolean} [options.failOnError=false] - If true, the operation will fail if any record fails to delete. + * @state {SalesforceState} + * @state {Object[]} data - An array of result objects. + * @state {string} data[].id - The unique identifier of the result. + * @state {boolean} data[].success - Indicates whether the operation was successful. + * @state {Array} data[].errors - An array of error messages, if any. * @returns {Operation} */ export function destroy(sObjectName, ids, options = {}) { @@ -359,11 +441,22 @@ export function destroy(sObjectName, ids, options = {}) { } /** - * Send a GET HTTP request using connected session information. - * @example + * Send a GET request on salesforce server configured in `state.configuration`. + * @public + * @example Make a GET request to a custom Salesforce flow * get('/actions/custom/flow/POC_OpenFN_Test_Flow'); - * @param {string} path - The Salesforce API endpoint, Relative to request from - * @param {object} options - Request query parameters and headers + * @example Make a GET request to a custom Salesforce flow with query parameters + * get('/actions/custom/flow/POC_OpenFN_Test_Flow', { query: { Status: 'Active' } }); + * @example Make a GET request then map the response + * get('/jobs/query/v1/jobs/001XXXXXXXXXXXXXXX/results', (state) => { + * // Mapping the response + * state.mapping = state.data.map(d => ({ name: d.name, id: d.extId })); + * return state; + * }); + * @function + * @param {string} path - The Salesforce API endpoint. + * @param {RequestOptions} options - Configure headers and query parameters for the request. + * @state {SalesforceState} * @returns {Operation} */ export function get(path, options = {}) { @@ -374,7 +467,7 @@ export function get(path, options = {}) { path, options ); - const { headers, ...query } = resolvedOptions; + const { headers, query } = resolvedOptions; console.log(`GET: ${resolvedPath}`); const requestOptions = { url: resolvedPath, @@ -395,9 +488,16 @@ export function get(path, options = {}) { * insert("Account", { Name: "My Account #1" }); * @example Multiple records creation * insert("Account",[{ Name: "My Account #1" }, { Name: "My Account #2" }]); + * @example Using a state variable + * fn((state) => { + * state.data = [{ Name: "My Account #1" }, { Name: "My Account #2" }]; + * return state; + * }); + * insert("Account", $.data); * @function * @param {string} sObjectName - API name of the sObject. - * @param {object} records - Field attributes for the new record. + * @param {(Object|Object[])} records - Field attributes for the new record, or an array of field attributes. + * @state {SalesforceState} * @returns {Operation} */ export function insert(sObjectName, records) { @@ -405,15 +505,24 @@ export function insert(sObjectName, records) { } /** - * Send a POST HTTP request using connected session information. - * - * @example - * post('/actions/custom/flow/POC_OpenFN_Test_Flow', { inputs: [{}] }); - * @param {string} path - The Salesforce API endpoint, Relative to request from - * @param {object} data - A JSON Object request body - * @param {object} options - Request options - * @param {object} [options.headers] - Object of request headers - * @param {object} [options.query] - A JSON Object request body + * Send a POST request to salesforce server configured in `state.configuration`. + * @public + * @example Make a POST request to a custom Salesforce flow + * post("/actions/custom/flow/POC_OpenFN_Test_Flow", { + * body: { + * inputs: [ + * { + * CommentCount: 6, + * FeedItemId: "0D5D0000000cfMY", + * }, + * ], + * }, + * }); + * @function + * @param {string} path - The Salesforce API endpoint. + * @param {object} data - A JSON Object request body. + * @param {RequestOptions} [options] - Configure headers and query parameters for the request. + * @state {SalesforceState} * @returns {Operation} */ export function post(path, data, options = {}) { @@ -444,28 +553,37 @@ export function post(path, data, options = {}) { } /** - * Execute an SOQL query. - * Note that in an event of a query error, - * error logs will be printed but the operation will not throw the error. + * Executes an SOQL (Salesforce Object Query Language) query to retrieve records from Salesforce. + * This operation uses {@link https://jsforce.github.io/document/#using-soql for querying salesforce records} using SOQL query and handles pagination. + * Note that in an event of a query error, error logs will be printed but the operation will not throw the error. + * + * The Salesforce query API is subject to rate limits, {@link https://sforce.co/3W9zyaQ Learn more here}. * - * The Salesforce query API is subject to rate limits, {@link https://sforce.co/3W9zyaQ See for more details}. * @public - * @example - * query(state=> `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`); - * @example Query more records if next records are available - * query(state=> `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.field1}'`, { autoFetch: true }); + * @example Run a query and download all matching records + * query('SELECT Id FROM Patient__c', { autoFetch: true }); + * @example Query patients by Health ID + * query(state => `SELECT Id FROM Patient__c WHERE Health_ID__c = '${state.data.healthId}'`); + * @example Query patients by Health ID using a lazy state reference + * query(`SELECT Id FROM Patient__c WHERE Health_ID__c = '${$.data.healthId}'`); * @function - * @param {string} qs - A query string. Must be less than `4000` characters in WHERE clause - * @param {object} options - Options passed to the bulk api. - * @param {boolean} [options.autoFetch=false] - Fetch next records if available. - * @param {function} callback - A callback to execute once the record is retrieved + * @param {(string|function)} query - A SOQL query string or a function that returns a query string. Must be less than 4000 characters in WHERE clause + * @param {QueryOptions} [options] - Optional configuration for the query operation + * @state {SalesforceState} + * @state {boolean} data.done - Indicates whether the operation is complete. + * @state {number} data.totalSize - The total number of items returned by the query. + * @state {Object[]} data.records - The records returned by the query. * @returns {Operation} */ -export function query(qs, options = {}, callback = s => s) { +export function query(query, options = {}) { return async state => { const { connection } = state; - const [resolvedQs, resolvedOptions] = expandReferences(state, qs, options); - console.log(`Executing query: ${resolvedQs}`); + const [resolvedQuery, resolvedOptions] = expandReferences( + state, + query, + options + ); + console.log(`Executing query: ${resolvedQuery}`); const autoFetch = resolvedOptions.autoFetch || resolvedOptions.autofetch; if (autoFetch) { @@ -504,7 +622,7 @@ export function query(qs, options = {}, callback = s => s) { }; try { - const qResult = await connection.query(resolvedQs); + const qResult = await connection.query(resolvedQuery); if (qResult.totalSize > 0) { console.log('Total records:', qResult.totalSize); await processRecords(qResult); @@ -522,17 +640,13 @@ export function query(qs, options = {}, callback = s => s) { 'Results retrieved and pushed to position [0] of the references array.' ); - const nextState = { - ...state, - references: [result, ...state.references], - }; - return callback(nextState); + return composeNextState(state, result); }; } /** - * Create a new sObject record, or updates it if it already exists - * External ID field name must be specified in second argument. + * Create a new sObject record, or updates it if it already exists. + * This function uses {@link https://jsforce.github.io/document/#upsert|jsforce upsert} under the hood. * @public * @example Single record upsert * upsert("UpsertTable__c", "ExtId__c", { Name: "Record #1", ExtId__c : 'ID-0000001' }); @@ -546,8 +660,9 @@ export function query(qs, options = {}, callback = s => s) { * @magic sObjectName - $.children[?(!@.meta.system)].name * @param {string} externalId - The external ID of the sObject. * @magic externalId - $.children[?(@.name=="{{args.sObject}}")].children[?(@.meta.externalId)].name - * @param {(object|object[])} records - Field attributes for the new object. + * @param {(Object|Object[])} records - Field attributes for the records to upsert, or an array of field attributes. * @magic records - $.children[?(@.name=="{{args.sObject}}")].children[?(!@.meta.externalId)] + * @state {SalesforceState} * @returns {Operation} */ export function upsert(sObjectName, externalId, records) { @@ -588,6 +703,7 @@ export function upsert(sObjectName, externalId, records) { * @function * @param {string} sObjectName - API name of the sObject. * @param {(object|object[])} records - Field attributes for the new object. + * @state {SalesforceState} * @returns {Operation} */ export function update(sObjectName, records) { @@ -612,7 +728,7 @@ export function update(sObjectName, records) { /** * Transliterates unicode characters to their best ASCII representation * @public - * @example + * @example Transliterate `άνθρωποι` to `anthropoi` * fn((state) => { * const s = toUTF8("άνθρωποι"); * console.log(s); // anthropoi @@ -626,19 +742,17 @@ export function toUTF8(input) { } /** - * Send a HTTP request using connected session information. - * - * @example - * request('/actions/custom/flow/POC_OpenFN_Test_Flow', { - * method: 'POST', + * Send a request to salesforce server configured in `state.configuration`. + * @public + * @example Make a POST request to a custom Salesforce flow + * request("/actions/custom/flow/POC_OpenFN_Test_Flow", { + * method: "POST", * json: { inputs: [{}] }, * }); - * @param {string} url - Relative to request from - * @param {object} options - The options for the request. - * @param {string} [options.method=GET] - HTTP method to use. Defaults to GET - * @param {object} [options.headers] - Object of request headers - * @param {object} [options.json] - A JSON object to send as the request body. - * @param {string} [options.body] - HTTP body (in POST/PUT/PATCH methods) + * @function + * @param {string} path - The Salesforce API endpoint. + * @param {SalesforceRequestOptions} [options] - Configure headers, query and body parameters for the request. + * @state {SalesforceState} * @returns {Operation} */ export function request(path, options = {}) { @@ -649,11 +763,12 @@ export function request(path, options = {}) { path, options ); - const { method = 'GET', json, body, headers } = resolvedOptions; + const { method = 'GET', json, body, headers, query } = resolvedOptions; const requestOptions = { url: resolvedPath, method, + query, headers: json ? { 'content-type': 'application/json', ...headers } : headers, @@ -669,11 +784,12 @@ export function request(path, options = {}) { /** * Retrieves a Salesforce sObject(s). * @public - * @example + * @example Retrieve a specific ContentVersion record * retrieve('ContentVersion', '0684K0000020Au7QAE/VersionData'); * @function * @param {string} sObjectName - The sObject to retrieve * @param {string} id - The id of the record + * @state {SalesforceState} * @returns {Operation} */ export function retrieve(sObjectName, id) { diff --git a/packages/salesforce/test/Adaptor.test.js b/packages/salesforce/test/Adaptor.test.js index 7274fb416..c764b80e0 100644 --- a/packages/salesforce/test/Adaptor.test.js +++ b/packages/salesforce/test/Adaptor.test.js @@ -1,10 +1,38 @@ import chai from 'chai'; import sinon from 'sinon'; -import { create, upsert, toUTF8, execute, query } from '../src/Adaptor'; +import { create, upsert, toUTF8, execute, query, get } from '../src/Adaptor'; const { expect } = chai; describe('Adaptor', () => { + describe('get', () => { + it('fetches an account record', done => { + const fakeConnection = { + request: function () { + return Promise.resolve({ Id: 10 }); + }, + }; + let state = { connection: fakeConnection, references: [] }; + + let spy = sinon.spy(fakeConnection, 'request'); + + get('/services/data/v58.0/sobjects/Account/10')(state) + .then(state => { + expect(spy.args[0]).to.eql([ + { + url: '/services/data/v58.0/sobjects/Account/10', + method: 'GET', + query: undefined, + headers: { 'content-type': 'application/json' }, + }, + ]); + expect(spy.called).to.eql(true); + expect(state.data).to.eql({ Id: 10 }); + }) + .then(done) + .catch(done); + }); + }); describe('create', () => { it('makes a new sObject', done => { const fakeConnection = { @@ -152,7 +180,7 @@ describe('Adaptor', () => { query('select Name from Account')(state) .then(state => { - expect(state.references[0]).to.eql({ + expect(state.data).to.eql({ done: true, totalSize: 0, records: [], @@ -177,7 +205,7 @@ describe('Adaptor', () => { query('select Name from Account')(state) .then(state => { expect(spy.called).to.eql(true); - expect(state.references[0]).to.eql({ + expect(state.data).to.eql({ done: true, totalSize: 1, records: [{ Name: 'OpenFn' }], @@ -212,7 +240,7 @@ describe('Adaptor', () => { .then(state => { expect(spy.called).to.eql(true); expect(spyReq.called).to.eql(true); - expect(state.references[0]).to.eql({ + expect(state.data).to.eql({ done: true, totalSize: 5713, records: [{ Name: 'Open' }, { Name: 'Fn' }], @@ -247,7 +275,7 @@ describe('Adaptor', () => { .then(state => { expect(spy.called).to.eql(true); expect(spyReq.called).to.eql(false); - expect(state.references[0]).to.eql({ + expect(state.data).to.eql({ done: false, totalSize: 5713, nextRecordsUrl: diff --git a/tools/parse-jsdoc/jsdoc/jsdoc.conf.json b/tools/parse-jsdoc/jsdoc/jsdoc.conf.json index 27679f88f..9c1925354 100644 --- a/tools/parse-jsdoc/jsdoc/jsdoc.conf.json +++ b/tools/parse-jsdoc/jsdoc/jsdoc.conf.json @@ -1,6 +1,6 @@ { "tags": { - "allowUnknownTags": false + "allowUnknownTags": true }, "plugins": ["./lookup-tags.cjs"] }