Skip to content

Commit a99dd03

Browse files
authored
59 add selected fields (#60)
Fixed issue with 431 and 414 errors in Get Updated Objects Polling trigger: new configuration field Selected Fields added
1 parent 0cb429c commit a99dd03

File tree

6 files changed

+147
-19
lines changed

6 files changed

+147
-19
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 2.5.1 (January 31, 2023)
2+
* Fixed [issue](https://github.com/elasticio/salesforce-component-v2/issues/59) with 431 and 414 errors in `Get Updated Objects Polling` trigger: new configuration field `Selected Fields` added
3+
14
## 2.5.0 (January 13, 2023)
25
* Fixed issue with attachments in `Bulk Create/Update/Delete/Upsert` action
36
* Added ability to directly provide url to csv file in `Bulk Create/Update/Delete/Upsert` action

README.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@
3838
The component uses Salesforce - API Version 46.0 by defaults but can be overwritten by the environment variable `SALESFORCE_API_VERSION`
3939

4040
### Environment variables
41-
Name|Mandatory|Description|Values|
42-
|----|---------|-----------|------|
43-
|SALESFORCE_API_VERSION| false | Determines API version of Salesforce to use | Default: `46.0` |
44-
|REFRESH_TOKEN_RETRIES| false | Determines how many retries to refresh token should be done before throwing an error | Default: `10` |
45-
|HASH_LIMIT_TIME| false | Hash expiration time in ms | Default: `600000` |
46-
|HASH_LIMIT_ELEMENTS| false | Hash size number limit | Default: `10` |
47-
|UPSERT_TIME_OUT| false | Time out for `Upsert Object` action in ms | Default: `120000` (2min) |
41+
| Name | Mandatory | Description | Values |
42+
|------------------------|-----------|--------------------------------------------------------------------------------------|--------------------------|
43+
| SALESFORCE_API_VERSION | false | Determines API version of Salesforce to use | Default: `46.0` |
44+
| REFRESH_TOKEN_RETRIES | false | Determines how many retries to refresh token should be done before throwing an error | Default: `10` |
45+
| HASH_LIMIT_TIME | false | Hash expiration time in ms | Default: `600000` |
46+
| HASH_LIMIT_ELEMENTS | false | Hash size number limit | Default: `10` |
47+
| UPSERT_TIME_OUT | false | Time out for `Upsert Object` action in ms | Default: `120000` (2min) |
4848

4949
## Credentials
5050
Authentication occurs via OAuth 2.0.
@@ -56,13 +56,13 @@ During credentials creation you would need to:
5656
- select existing Auth Client from drop-down list ``Choose Auth Client`` or create the new one.
5757
For creating Auth Client you should specify following fields:
5858

59-
Field name|Mandatory|Description|
60-
|----|---------|-----------|
61-
|Name| true | your Auth Client's name |
62-
|Client ID| true | your OAuth client key |
63-
|Client Secret| true | your OAuth client secret |
64-
|Authorization Endpoint| true | your OAuth authorization endpoint. For production use `https://login.salesforce.com/services/oauth2/authorize`, for sandbox - `https://test.salesforce.com/services/oauth2/authorize`|
65-
|Token Endpoint| true | your OAuth Token endpoint for refreshing access token. For production use `https://login.salesforce.com/services/oauth2/token`, for sandbox - `https://test.salesforce.com/services/oauth2/token`|
59+
| Field name | Mandatory | Description |
60+
|------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
61+
| Name | true | your Auth Client's name |
62+
| Client ID | true | your OAuth client key |
63+
| Client Secret | true | your OAuth client secret |
64+
| Authorization Endpoint | true | your OAuth authorization endpoint. For production use `https://login.salesforce.com/services/oauth2/authorize`, for sandbox - `https://test.salesforce.com/services/oauth2/authorize` |
65+
| Token Endpoint | true | your OAuth Token endpoint for refreshing access token. For production use `https://login.salesforce.com/services/oauth2/token`, for sandbox - `https://test.salesforce.com/services/oauth2/token` |
6666

6767
- fill field ``Name Your Credential``
6868
- click on ``Authenticate`` button - if you have not logged in Salesforce before then log in by entering data in the login window that appears
@@ -74,6 +74,7 @@ Field name|Mandatory|Description|
7474
### Get Updated Objects Polling
7575
### Config Fields
7676
* **Object Type** Dropdown: Indicates Object Type to be fetched
77+
* **Selected Fields** Multiselect dropdown: list with all Object Fields. Select fields, which will be returned in response. That can prevent [431 and 414 Errors](https://developer.salesforce.com/docs/atlas.en-us.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_api.htm).
7778
* **Include linked objects** Multiselect dropdown: list with all the related child and parent objects of the selected object type. List entries are given as `Object Name/Reference To (Relationship Name)`. Select one or more related objects, which will be join queried and included in the response from your Salesforce Organization. Please see the **Limitations** section below for use case advisories.
7879
* **Emit behavior** Dropdown: Indicates emit objects individually or emit by page
7980
* **Start Time** - TextField (string, optional): Indicates the beginning time to start retrieving events from in ISO 8601 Date time utc format - YYYY-MM-DDThh:mm:ssZ

component.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Customer relationship management (CRM) software & cloud computing from the leader in CRM solutions for businesses large & small.",
44
"docsUrl": "https://github.com/elasticio/salesforce-component-v2",
55
"url": "http://www.salesforce.com/",
6-
"version": "2.5.0",
6+
"version": "2.5.1",
77
"authClientTypes": [
88
"oauth2"
99
],
@@ -48,6 +48,19 @@
4848
"model": "objectTypes",
4949
"prompt": "Please select a Salesforce Object"
5050
},
51+
"selectedFields": {
52+
"label": "Selected Fields",
53+
"viewClass": "MultiSelectView",
54+
"order": 17,
55+
"required": false,
56+
"require": [
57+
"sobject"
58+
],
59+
"help": {
60+
"description": "Select fields that will be returned"
61+
},
62+
"model": "getObjectFields"
63+
},
5164
"linkedObjects": {
5265
"label": "Include linked objects",
5366
"viewClass": "MultiSelectView",

lib/triggers/getUpdatedObjectsPolling.js

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,28 @@ const isDateValid = (date) => new Date(date).toString() !== 'Invalid Date';
99
const isNumberNaN = (num) => Number(num).toString() === 'NaN';
1010
const MAX_FETCH = 10000;
1111

12+
function getSelectedFields(cfg) {
13+
const { selectedFields = [] } = cfg;
14+
if (selectedFields.length === 0) {
15+
return '*';
16+
}
17+
if (!selectedFields.includes('LastModifiedDate')) {
18+
selectedFields.push('LastModifiedDate');
19+
}
20+
return selectedFields.toString();
21+
}
22+
1223
exports.process = async function processTrigger(_msg, cfg, snapshot) {
1324
this.logger.info('Start processing "Get Updated Objects Polling" trigger');
1425
const currentTime = new Date();
1526

1627
const {
1728
sobject,
18-
pageSize = MAX_FETCH,
1929
linkedObjects = [],
30+
pageSize = MAX_FETCH,
2031
emitBehavior = 'emitIndividually',
32+
} = cfg;
33+
let {
2134
singlePagePerInterval,
2235
} = cfg;
2336

@@ -39,12 +52,20 @@ exports.process = async function processTrigger(_msg, cfg, snapshot) {
3952

4053
this.logger.info(`Filter "${sobject}" updated from ${timeToString(from)} to ${timeToString(to)}, page limit ${pageSize}`);
4154

55+
const selectedFields = getSelectedFields(cfg);
4256
const options = {
4357
sobject,
44-
selectedObjects: linkedObjects.reduce((query, obj) => (obj.startsWith('!') ? query : `${query}, ${obj}.*`), '*'),
58+
selectedObjects: linkedObjects.reduce((query, obj) => (obj.startsWith('!') ? query : `${query}, ${obj}.*`), selectedFields),
4559
linkedObjects,
4660
};
4761

62+
const isDebugFlow = process.env.ELASTICIO_FLOW_TYPE === 'debug';
63+
if (isDebugFlow) {
64+
options.maxFetch = 10;
65+
singlePagePerInterval = true;
66+
this.logger.info('Debug flow detected, set maxFetch to 10');
67+
}
68+
4869
let proceed = true;
4970
let hasNextPage = false;
5071
let iteration = 1;
@@ -53,53 +74,69 @@ exports.process = async function processTrigger(_msg, cfg, snapshot) {
5374
do {
5475
if (!hasNextPage) {
5576
options.whereCondition = `LastModifiedDate >= ${timeToString(from)} AND LastModifiedDate < ${timeToString(to)}`;
77+
this.logger.debug('Start poll object with options: %j', options);
5678
results = await callJSForceMethod.call(this, cfg, 'pollingSelectQuery', options);
5779
this.logger.info(`Polling iteration ${iteration} - ${results.length} results found`);
5880
iteration++;
5981
}
6082
if (results.length !== 0) {
83+
this.logger.debug('New records found, check other options...');
6184
nextStartTime = currentTime;
6285
if (singlePagePerInterval && snapshot.lastElementId) {
86+
this.logger.debug('Snapshot contain lastElementId, going to delete records that have already been emitted');
6387
const lastElement = results.filter((item) => item.Id === snapshot.lastElementId)[0];
6488
const lastElementIndex = results.indexOf(lastElement);
6589
results = results.slice(lastElementIndex + 1);
90+
this.logger.debug('Emitted records deleted. Current results length is %s', results.length);
6691
}
6792
if (results.length === MAX_FETCH) {
93+
this.logger.debug('The size of the resulting array is equal to MAX_FETCH, so all entries that have the same LastModifiedDate as the last entry will be deleted from the resulting array to prevent emitting duplicates');
6894
nextStartTime = results[results.length - 1].LastModifiedDate;
6995
const filteredResults = results.filter((item) => item.LastModifiedDate === nextStartTime);
7096
results = results.slice(0, MAX_FETCH - filteredResults.length);
97+
this.logger.debug('Entries that have the same LastModifiedDate as the last entry deleted. Current size of the resulting array is %s', results.length);
7198
}
7299
if (results.length >= Number(pageSize)) {
100+
this.logger.debug('The size of the resulting array >= pageSize. Going to process one page...');
73101
hasNextPage = true;
74102
const pageResults = results.slice(0, pageSize);
75103
results = results.slice(pageSize);
76104
const lastElementLastModifiedDate = pageResults[pageSize - 1].LastModifiedDate;
77105
const lastElementId = pageResults[pageSize - 1].Id;
78106
if (emitBehavior === 'fetchPage') {
107+
this.logger.debug('Emit Behavior set as Fetch Page, going to emit one page...');
79108
await this.emit('data', messages.newMessageWithBody({ results: pageResults }));
80109
} else if (emitBehavior === 'emitIndividually') {
110+
this.logger.debug('Emit Behavior set as Emit Individually, going to emit records one by one');
81111
for (const record of pageResults) {
82112
await this.emit('data', messages.newMessageWithBody(record));
83113
}
84114
}
115+
this.logger.debug('Page processing is finished.');
85116

86117
if (singlePagePerInterval) {
118+
this.logger.debug('Single Page Per Interval option is set. Going to emit snapshot %j', { nextStartTime: lastElementLastModifiedDate, lastElementId });
87119
await this.emit('snapshot', { nextStartTime: lastElementLastModifiedDate, lastElementId });
88120
proceed = false;
89121
}
90122
} else {
123+
this.logger.debug('The size of the resulting array < pageSize. Going to process all found results');
91124
hasNextPage = false;
92125
if (emitBehavior === 'fetchPage') {
126+
this.logger.debug('Emit Behavior set as Fetch Page, going to emit one page...');
93127
await this.emit('data', messages.newMessageWithBody({ results }));
94128
} else if (emitBehavior === 'emitIndividually') {
129+
this.logger.debug('Emit Behavior set as Emit Individually, going to emit records one by one');
95130
for (const record of results) {
96131
await this.emit('data', messages.newMessageWithBody(record));
97132
}
98133
}
134+
this.logger.debug('All results processed. going to emit snapshot: %j', { nextStartTime });
99135
await this.emit('snapshot', { nextStartTime });
100136
proceed = false;
101137
}
102138
} else {
139+
this.logger.debug('The size of the resulting array is 0, going to emit snapshot: %j', { nextStartTime });
103140
await this.emit('snapshot', { nextStartTime });
104141
await this.emit('end');
105142
proceed = false;
@@ -118,6 +155,13 @@ module.exports.objectTypes = async function getObjectTypes(configuration) {
118155
return callJSForceMethod.call(this, configuration, 'getObjectTypes');
119156
};
120157

158+
module.exports.getObjectFields = async function getObjectFields(configuration) {
159+
const fields = await callJSForceMethod.call(this, configuration, 'getObjectFieldsMetaData');
160+
const result = {};
161+
fields.forEach((field) => { result[field.name] = field.label; });
162+
return result;
163+
};
164+
121165
module.exports.linkedObjectTypes = async function linkedObjectTypes(configuration) {
122166
const meta = await callJSForceMethod.call(this, configuration, 'describe');
123167
return getLinkedObjectTypes(meta);
@@ -126,7 +170,16 @@ module.exports.linkedObjectTypes = async function linkedObjectTypes(configuratio
126170
module.exports.getMetaModel = async function getMetaModel(configuration) {
127171
const describeObject = await callJSForceMethod.call(this, configuration, 'describe');
128172
const metadata = await processMeta(describeObject, 'polling');
129-
const { outputMethod } = configuration;
173+
const { outputMethod, selectedFields = [] } = configuration;
174+
if (selectedFields.length > 0) {
175+
const filteredOutMetadata = {};
176+
for (const outProperty of Object.keys(metadata.out.properties)) {
177+
if (selectedFields.includes(outProperty)) {
178+
filteredOutMetadata[outProperty] = metadata.out.properties[outProperty];
179+
}
180+
}
181+
metadata.out.properties = filteredOutMetadata;
182+
}
130183
if (outputMethod !== 'emitIndividually') {
131184
const outputArray = {
132185
type: 'object',

spec-integration/triggers/polling.spec.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable no-return-assign */
2+
process.env.ELASTICIO_FLOW_TYPE = 'debug';
23
const { expect } = require('chai');
34
const polling = require('../../lib/triggers/getUpdatedObjectsPolling');
45
const { getContext, testsCommon } = require('../../spec/common');
@@ -40,10 +41,46 @@ describe('polling', () => {
4041
await polling.process.call(getContext(), msg, testCfg, snapshot);
4142
});
4243

44+
it('Custom Object 300+ custom fields polling', async () => {
45+
const testCfg = {
46+
oauth: getOauth(),
47+
sobject: 'TestBook__c',
48+
emitBehavior: 'fetchPage',
49+
pageSize: 10,
50+
linkedObjects: ['CreatedBy'],
51+
selectedFields: ['Book_Description__c', 'Test_Book_Custom_Field_1__c'],
52+
};
53+
const snapshot = {};
54+
const msg = { body: {} };
55+
const context = getContext();
56+
await polling.process.call(context, msg, testCfg, snapshot);
57+
expect(context.emit.firstCall.args[0]).to.deep.equal('data');
58+
expect(context.emit.firstCall.args[1].body.results[0].Book_Description__c).to.deep.equal('First book');
59+
});
60+
4361
it('Get metadata', async () => {
44-
const testCfg = { oauth: getOauth(), sobject: 'Contact', outputMethod: 'emitAll' };
62+
const testCfg = {
63+
oauth: getOauth(),
64+
sobject: 'Contact',
65+
outputMethod: 'emitAll',
66+
selectedFields: ['Id', 'LastName'],
67+
};
4568

4669
const result = await polling.getMetaModel.call(getContext(), testCfg);
4770
expect(result).to.be.equal(true);
4871
});
72+
73+
it('Get object Fields', async () => {
74+
const testCfg = { oauth: getOauth(), sobject: 'TestBook__c' };
75+
76+
const result = await polling.getObjectFields.call(getContext(), testCfg);
77+
expect(result.Id).to.be.equal('Record ID');
78+
});
79+
80+
it('Get linkedObjectTypes', async () => {
81+
const testCfg = { oauth: getOauth(), sobject: 'TestBook__c' };
82+
83+
const result = await polling.linkedObjectTypes.call(getContext(), testCfg);
84+
expect(result.CreatedBy).to.be.equal('User (CreatedBy)');
85+
});
4986
});

spec/triggers/getUpdatedObjectsPolling.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@ describe('getUpdatedObjectsPolling trigger', () => {
3131
expect(context.emit.getCall(0).lastArg.body).to.deep.equal(duplicateRecords[0]);
3232
});
3333

34+
it('emitIndividually with selected fields', async () => {
35+
execRequest = sinon.stub(callJSForceMethod, 'call').returns(duplicateRecords);
36+
const cfg = {
37+
sobject: 'Document',
38+
pageSize: 10,
39+
linkedObjects: [],
40+
selectedFields: ['Id', 'Name'],
41+
emitBehavior: 'emitIndividually',
42+
singlePagePerInterval: false,
43+
};
44+
const snapshot = {};
45+
const msg = {};
46+
const context = getContext();
47+
await process.call(context, msg, cfg, snapshot);
48+
expect(execRequest.firstCall.args[3].selectedObjects).to.be.equal('Id,Name,LastModifiedDate');
49+
expect(context.emit.callCount).to.be.equal(28);
50+
expect(context.emit.getCall(27).firstArg).to.be.equal('snapshot');
51+
expect(execRequest.callCount).to.be.equal(1);
52+
expect(context.emit.getCall(0).lastArg.body).to.deep.equal(duplicateRecords[0]);
53+
});
54+
3455
it('fetchPage sizePage = 10, singlePagePerInterval = false', async () => {
3556
execRequest = sinon.stub(callJSForceMethod, 'call').returns(duplicateRecords);
3657
const cfg = {

0 commit comments

Comments
 (0)