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