From 8dbcc8359aed981fc9778ccc69a1e159cb560bde Mon Sep 17 00:00:00 2001 From: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com> Date: Wed, 27 May 2026 09:52:28 +0300 Subject: [PATCH] fix(Salesforce Node): Quote numeric string filter values in SOQL conditions (#31128) --- .../nodes/Salesforce/GenericFunctions.ts | 34 +++- .../nodes/Salesforce/Salesforce.node.ts | 39 ++--- .../Salesforce/SalesforceTrigger.node.ts | 7 +- .../__test__/GenericFunctions.test.ts | 75 +++++++-- .../__test__/Salesforce.node.test.ts | 147 +++++++++++++++++- .../__test__/SalesforceTrigger.node.test.ts | 7 +- 6 files changed, 264 insertions(+), 45 deletions(-) diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index bf25d2561c6..c409422d5b3 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -305,7 +305,13 @@ const SALESFORCE_DATE_LITERALS = new Set([ 'NEXT_FISCAL_YEAR', ]); -export function getValue(value: any): string | number | boolean { +// Salesforce node typeVersion at which numeric-looking strings stopped being +// auto-coerced to unquoted SOQL numbers. See NODE-5116: string-typed Salesforce +// fields (e.g. external IDs) need quoted literals regardless of content. Older +// typeVersions keep the legacy coercion for backwards compatibility. +const NUMERIC_STRING_QUOTING_VERSION = 1.1; + +export function getValue(value: any, nodeVersion = 1): string | number | boolean { if (value === null || value === undefined) { return 'null'; } @@ -379,22 +385,28 @@ export function getValue(value: any): string | number | boolean { } } - // Detect numeric strings and return them unquoted (leading zeros are preserved as strings) - if (/^-?(0|[1-9]\d*)(\.\d+)?$/.test(value)) { + // Legacy behavior (typeVersion < 1.1): auto-coerce numeric strings to unquoted + // SOQL numbers. Kept for existing workflows that rely on it for numeric SF fields + // (e.g. `AnnualRevenue > '0'`). Fixed in typeVersion 1.1 — see NODE-5116. + if (nodeVersion < NUMERIC_STRING_QUOTING_VERSION && /^-?(0|[1-9]\d*)(\.\d+)?$/.test(value)) { const numericValue = Number(value); if (Number.isFinite(numericValue)) { return numericValue; } } - // All other strings are escaped and quoted + // All other strings are escaped and quoted. From typeVersion 1.1 onwards this + // includes numeric-looking strings — the value input has no field-type info, + // and string-typed Salesforce fields (e.g. external IDs) require quoted literals + // regardless of content. Users wanting a numeric comparison must pass a number + // via an expression. return `'${escapeSoqlString(value)}'`; } throw new Error(`Unsupported value type: ${typeof value}`); } -export function getConditions(options: IDataObject): string | undefined { +export function getConditions(options: IDataObject, nodeVersion = 1): string | undefined { const conditions = (options.conditionsUi as IDataObject)?.conditionValues as IDataObject[]; if (!Array.isArray(conditions) || conditions.length === 0) { @@ -404,7 +416,7 @@ export function getConditions(options: IDataObject): string | undefined { const conditionStrings = conditions.map((condition: IDataObject) => { const field = validateSoqlFieldName(condition.field as string); const operator = validateSoqlOperator(condition.operation as string); - const value = getValue(condition.value); + const value = getValue(condition.value, nodeVersion); return `${field} ${operator} ${value}`; }); @@ -427,7 +439,13 @@ export function getDefaultFields(sobject: string) { )[sobject]; } -export function getQuery(options: IDataObject, sobject: string, returnAll: boolean, limit = 0) { +export function getQuery( + options: IDataObject, + sobject: string, + returnAll: boolean, + limit = 0, + nodeVersion = 1, +) { const validSobject = validateSoqlObjectName(sobject); const fields: string[] = []; @@ -451,7 +469,7 @@ export function getQuery(options: IDataObject, sobject: string, returnAll: boole ((getDefaultFields(validSobject) as string) || 'id,LastModifiedDate').split(','), ); } - const conditions = getConditions(options); + const conditions = getConditions(options, nodeVersion); let query = `SELECT ${fields.join(',')} FROM ${validSobject} ${conditions ? conditions : ''}`; diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index d0e43f35e4b..7470dbdcb55 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -46,7 +46,7 @@ export class Salesforce implements INodeType { name: 'salesforce', icon: 'file:salesforce.svg', group: ['output'], - version: 1, + version: [1, 1.1], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Consume Salesforce API', defaults: { @@ -1049,6 +1049,7 @@ export class Salesforce implements INodeType { const qs: IDataObject = {}; const resource = this.getNodeParameter('resource', 0); const operation = this.getNodeParameter('operation', 0); + const nodeVersion = this.getNode().typeVersion; this.logger.debug( `Running "Salesforce" node named "${this.getNode.name}" resource "${resource}" operation "${operation}"`, @@ -1289,7 +1290,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, 'Lead', returnAll, 0); + qs.q = getQuery(options, 'Lead', returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -1300,7 +1301,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, 'Lead', returnAll, limit); + qs.q = getQuery(options, 'Lead', returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -1663,7 +1664,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, 'Contact', returnAll, 0); + qs.q = getQuery(options, 'Contact', returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -1674,7 +1675,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, 'Contact', returnAll, limit); + qs.q = getQuery(options, 'Contact', returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -1815,7 +1816,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, customObject, returnAll, 0); + qs.q = getQuery(options, customObject, returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -1826,7 +1827,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, customObject, returnAll, limit); + qs.q = getQuery(options, customObject, returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2049,7 +2050,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, 'Opportunity', returnAll, 0); + qs.q = getQuery(options, 'Opportunity', returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2060,7 +2061,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, 'Opportunity', returnAll, limit); + qs.q = getQuery(options, 'Opportunity', returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2337,7 +2338,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, 'Account', returnAll, 0); + qs.q = getQuery(options, 'Account', returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2348,7 +2349,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, 'Account', returnAll, limit); + qs.q = getQuery(options, 'Account', returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2552,7 +2553,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, 'Case', returnAll, 0); + qs.q = getQuery(options, 'Case', returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2563,7 +2564,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, 'Case', returnAll, limit); + qs.q = getQuery(options, 'Case', returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2815,7 +2816,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, 'Task', returnAll, 0); + qs.q = getQuery(options, 'Task', returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2826,7 +2827,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, 'Task', returnAll, limit); + qs.q = getQuery(options, 'Task', returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2947,7 +2948,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, 'Attachment', returnAll, 0); + qs.q = getQuery(options, 'Attachment', returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -2958,7 +2959,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, 'Attachment', returnAll, limit); + qs.q = getQuery(options, 'Attachment', returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -3002,7 +3003,7 @@ export class Salesforce implements INodeType { const options = this.getNodeParameter('options', i); try { if (returnAll) { - qs.q = getQuery(options, 'User', returnAll, 0); + qs.q = getQuery(options, 'User', returnAll, 0, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', @@ -3013,7 +3014,7 @@ export class Salesforce implements INodeType { ); } else { const limit = this.getNodeParameter('limit', i); - qs.q = getQuery(options, 'User', returnAll, limit); + qs.q = getQuery(options, 'User', returnAll, limit, nodeVersion); responseData = await salesforceApiRequestAllItems.call( this, 'records', diff --git a/packages/nodes-base/nodes/Salesforce/SalesforceTrigger.node.ts b/packages/nodes-base/nodes/Salesforce/SalesforceTrigger.node.ts index 739c81c511a..40527d56fda 100644 --- a/packages/nodes-base/nodes/Salesforce/SalesforceTrigger.node.ts +++ b/packages/nodes-base/nodes/Salesforce/SalesforceTrigger.node.ts @@ -26,7 +26,7 @@ export class SalesforceTrigger implements INodeType { name: 'salesforceTrigger', icon: 'file:salesforce.svg', group: ['trigger'], - version: 1, + version: [1, 1.1], description: 'Fetches data from Salesforce and starts the workflow on specified polling intervals.', subtitle: '={{($parameter["triggerOn"])}}', @@ -194,6 +194,7 @@ export class SalesforceTrigger implements INodeType { const triggerOn = this.getNodeParameter('triggerOn') as string; let triggerResource = triggerOn.slice(0, 1).toUpperCase() + triggerOn.slice(1, -7); const changeType = triggerOn.slice(-7); + const nodeVersion = this.getNode().typeVersion; if (triggerResource === 'CustomObject') { triggerResource = this.getNodeParameter('customObject') as string; @@ -249,9 +250,9 @@ export class SalesforceTrigger implements INodeType { try { if (this.getMode() === 'manual') { - qs.q = getQuery(options, triggerResource, false, 1); + qs.q = getQuery(options, triggerResource, false, 1, nodeVersion); } else { - qs.q = getQuery(options, triggerResource, true, 0); + qs.q = getQuery(options, triggerResource, true, 0, nodeVersion); } responseData = await salesforceApiRequestAllItems.call( this, diff --git a/packages/nodes-base/nodes/Salesforce/__test__/GenericFunctions.test.ts b/packages/nodes-base/nodes/Salesforce/__test__/GenericFunctions.test.ts index a018df97184..44cba077711 100644 --- a/packages/nodes-base/nodes/Salesforce/__test__/GenericFunctions.test.ts +++ b/packages/nodes-base/nodes/Salesforce/__test__/GenericFunctions.test.ts @@ -1371,15 +1371,50 @@ describe('Salesforce -> GenericFunctions', () => { expect(result).toBe("'Bob\\' OR \\'1\\'=\\'1'"); }); - it('should return numeric strings as numbers', () => { - expect(getValue('0')).toBe(0); - expect(getValue('123')).toBe(123); - expect(getValue('123.45')).toBe(123.45); - expect(getValue('-5')).toBe(-5); + describe('typeVersion 1 (legacy: numeric strings are coerced to unquoted SOQL numbers)', () => { + it('should return numeric strings as unquoted numbers', () => { + expect(getValue('0', 1)).toBe(0); + expect(getValue('123', 1)).toBe(123); + expect(getValue('123.45', 1)).toBe(123.45); + expect(getValue('-5', 1)).toBe(-5); + }); + + it('should preserve leading zeros as quoted strings', () => { + expect(getValue('00123', 1)).toBe("'00123'"); + }); + + it('should default to legacy behavior when nodeVersion is omitted', () => { + // Safety net: callers that haven't passed nodeVersion must keep + // behaving as v1 so existing workflows are not impacted. + expect(getValue('123')).toBe(123); + }); }); - it('should preserve leading zeros as quoted strings', () => { - expect(getValue('00123')).toBe("'00123'"); + describe('typeVersion 1.1 (numeric strings are quoted — NODE-5116 fix)', () => { + it('should quote numeric-looking string values', () => { + expect(getValue('0', 1.1)).toBe("'0'"); + expect(getValue('123', 1.1)).toBe("'123'"); + expect(getValue('123.45', 1.1)).toBe("'123.45'"); + expect(getValue('-5', 1.1)).toBe("'-5'"); + }); + + it('should quote numeric strings without leading zero (regression: NODE-5116)', () => { + // String-typed Salesforce fields (e.g. external IDs) reject unquoted + // numeric literals: "must be of type string and should be enclosed in quotes". + expect(getValue('307795203', 1.1)).toBe("'307795203'"); + }); + + it('should preserve leading zeros as quoted strings', () => { + expect(getValue('00123', 1.1)).toBe("'00123'"); + expect(getValue('039381512', 1.1)).toBe("'039381512'"); + }); + + it('should keep number-typed values unquoted', () => { + // Users wanting numeric SOQL comparisons pass numbers via expressions. + expect(getValue(307795203, 1.1)).toBe(307795203); + expect(getValue(0, 1.1)).toBe(0); + expect(getValue(-5, 1.1)).toBe(-5); + }); }); it('should return ISO datetime strings as-is', () => { @@ -1558,14 +1593,36 @@ describe('Salesforce -> GenericFunctions', () => { expect(result).toBe("WHERE Name = 'Bob\\'s' AND Email LIKE '%test%'"); }); - it('should return numeric string values unquoted', () => { + it('should keep numeric string values unquoted on typeVersion 1 (legacy)', () => { const options: IDataObject = { conditionsUi: { conditionValues: [{ field: 'AnnualRevenue', operation: '>', value: '0' }], }, }; - const result = getConditions(options); + const result = getConditions(options, 1); + expect(result).toBe('WHERE AnnualRevenue > 0'); + }); + + it('should quote numeric string values on typeVersion 1.1 (NODE-5116 fix)', () => { + const options: IDataObject = { + conditionsUi: { + conditionValues: [{ field: 'IdNumber__c', operation: 'equal', value: '307795203' }], + }, + }; + + const result = getConditions(options, 1.1); + expect(result).toBe("WHERE IdNumber__c = '307795203'"); + }); + + it('should keep number-typed values unquoted on typeVersion 1.1', () => { + const options: IDataObject = { + conditionsUi: { + conditionValues: [{ field: 'AnnualRevenue', operation: '>', value: 0 }], + }, + }; + + const result = getConditions(options, 1.1); expect(result).toBe('WHERE AnnualRevenue > 0'); }); diff --git a/packages/nodes-base/nodes/Salesforce/__test__/Salesforce.node.test.ts b/packages/nodes-base/nodes/Salesforce/__test__/Salesforce.node.test.ts index 899fffa9247..cebaf1d2814 100644 --- a/packages/nodes-base/nodes/Salesforce/__test__/Salesforce.node.test.ts +++ b/packages/nodes-base/nodes/Salesforce/__test__/Salesforce.node.test.ts @@ -1479,6 +1479,7 @@ describe('Salesforce', () => { 'Lead', true, 0, + 1, ); expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith( 'records', @@ -1507,7 +1508,7 @@ describe('Salesforce', () => { await node.execute.call(mockExecuteFunctions); - expect(getQuerySpy).toHaveBeenCalledWith({}, 'Lead', false, 50); + expect(getQuerySpy).toHaveBeenCalledWith({}, 'Lead', false, 50, 1); }); it('should handle lead delete operation', async () => { @@ -1858,7 +1859,7 @@ describe('Salesforce', () => { await node.execute.call(mockExecuteFunctions); - expect(getQuerySpy).toHaveBeenCalledWith({ fields: 'Id,Subject,Type' }, 'Case', true, 0); + expect(getQuerySpy).toHaveBeenCalledWith({ fields: 'Id,Subject,Type' }, 'Case', true, 0, 1); expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith( 'records', 'GET', @@ -1886,7 +1887,7 @@ describe('Salesforce', () => { await node.execute.call(mockExecuteFunctions); - expect(getQuerySpy).toHaveBeenCalledWith({}, 'Case', false, 25); + expect(getQuerySpy).toHaveBeenCalledWith({}, 'Case', false, 25, 1); }); it('should handle case getAll operation error handling', async () => { @@ -2355,6 +2356,7 @@ describe('Salesforce', () => { 'Contact', true, 0, + 1, ); expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith( 'records', @@ -2634,7 +2636,7 @@ describe('Salesforce', () => { await node.execute.call(mockExecuteFunctions); - expect(getQuerySpy).toHaveBeenCalledWith({}, 'CustomObject__c', true, 0); + expect(getQuerySpy).toHaveBeenCalledWith({}, 'CustomObject__c', true, 0, 1); expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith( 'records', 'GET', @@ -3577,6 +3579,7 @@ describe('Salesforce', () => { 'Task', false, 10, + 1, ); expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith( 'records', @@ -3898,6 +3901,7 @@ describe('Salesforce', () => { 'Attachment', true, 0, + 1, ); expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith( 'records', @@ -3936,7 +3940,7 @@ describe('Salesforce', () => { await node.execute.call(mockExecuteFunctions); - expect(getQuerySpy).toHaveBeenCalledWith({}, 'Attachment', false, 5); + expect(getQuerySpy).toHaveBeenCalledWith({}, 'Attachment', false, 5, 1); }); it('should handle attachment getAll operation error handling', async () => { @@ -4031,4 +4035,137 @@ describe('Salesforce', () => { }); }); }); + + // Coverage for the 9 getAll call sites that previously had no test (Contact limit, + // CustomObject limit, Opportunity returnAll/limit, Account returnAll/limit, Task + // returnAll, User returnAll/limit) and end-to-end verification that the SF node's + // typeVersion is threaded into getQuery (NODE-5116 version-gate wiring). + describe('Execute Method - GetAll Query Wiring', () => { + const mockGetAll = (resource: string, returnAll: boolean, limit?: number, options = {}) => { + mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => { + const params: Record = { + resource, + operation: 'getAll', + returnAll, + ...(returnAll ? {} : { limit }), + options, + customObject: 'CustomObject__c', + }; + return params[param]; + }); + }; + + it('should call getQuery for contact getAll with limit', async () => { + mockGetAll('contact', false, 25); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT * FROM Contact LIMIT 25'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({}, 'Contact', false, 25, 1); + }); + + it('should call getQuery for customObject getAll with limit', async () => { + mockGetAll('customObject', false, 10); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT * FROM CustomObject__c LIMIT 10'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({}, 'CustomObject__c', false, 10, 1); + }); + + it('should call getQuery for opportunity getAll with returnAll', async () => { + mockGetAll('opportunity', true, undefined, { fields: 'Id,Name' }); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT Id,Name FROM Opportunity'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({ fields: 'Id,Name' }, 'Opportunity', true, 0, 1); + }); + + it('should call getQuery for opportunity getAll with limit', async () => { + mockGetAll('opportunity', false, 5); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT * FROM Opportunity LIMIT 5'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({}, 'Opportunity', false, 5, 1); + }); + + it('should call getQuery for account getAll with returnAll', async () => { + mockGetAll('account', true, undefined, { fields: 'Id,Name,Type' }); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT Id,Name,Type FROM Account'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({ fields: 'Id,Name,Type' }, 'Account', true, 0, 1); + }); + + it('should call getQuery for account getAll with limit', async () => { + mockGetAll('account', false, 15); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT * FROM Account LIMIT 15'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({}, 'Account', false, 15, 1); + }); + + it('should call getQuery for task getAll with returnAll', async () => { + mockGetAll('task', true, undefined, { fields: 'Id,Subject' }); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT Id,Subject FROM Task'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({ fields: 'Id,Subject' }, 'Task', true, 0, 1); + }); + + it('should call getQuery for user getAll with returnAll', async () => { + mockGetAll('user', true, undefined, { fields: 'Id,Name,Email' }); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT Id,Name,Email FROM User'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({ fields: 'Id,Name,Email' }, 'User', true, 0, 1); + }); + + it('should call getQuery for user getAll with limit', async () => { + mockGetAll('user', false, 20); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT * FROM User LIMIT 20'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({}, 'User', false, 20, 1); + }); + + it('should pass typeVersion 1.1 to getQuery when the node is on the new version', async () => { + // End-to-end proof that getNode().typeVersion is threaded into getQuery, + // so a workflow created on v1.1 actually gets the NODE-5116 fix at runtime. + mockNode.typeVersion = 1.1; + mockGetAll('lead', false, 10); + const getQuerySpy = jest.spyOn(GenericFunctions, 'getQuery'); + getQuerySpy.mockReturnValue('SELECT * FROM Lead LIMIT 10'); + salesforceApiRequestAllItemsSpy.mockResolvedValue([]); + + await node.execute.call(mockExecuteFunctions); + + expect(getQuerySpy).toHaveBeenCalledWith({}, 'Lead', false, 10, 1.1); + }); + }); }); diff --git a/packages/nodes-base/nodes/Salesforce/__test__/SalesforceTrigger.node.test.ts b/packages/nodes-base/nodes/Salesforce/__test__/SalesforceTrigger.node.test.ts index a25a52b0117..e3e3820ae8a 100644 --- a/packages/nodes-base/nodes/Salesforce/__test__/SalesforceTrigger.node.test.ts +++ b/packages/nodes-base/nodes/Salesforce/__test__/SalesforceTrigger.node.test.ts @@ -176,6 +176,7 @@ describe('SalesforceTrigger', () => { 'Account', true, 0, + 1, ); expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith( 'records', @@ -236,6 +237,7 @@ describe('SalesforceTrigger', () => { 'Account', true, 0, + 1, ); expect(result).toBeDefined(); @@ -266,7 +268,7 @@ describe('SalesforceTrigger', () => { const result = await trigger.poll.call(mockPollFunctions); - expect(getQuerySpy).toHaveBeenCalledWith(expect.any(Object), 'CustomObject__c', true, 0); + expect(getQuerySpy).toHaveBeenCalledWith(expect.any(Object), 'CustomObject__c', true, 0, 1); expect(result).toBeDefined(); expect(result![0]).toHaveLength(1); @@ -341,6 +343,7 @@ describe('SalesforceTrigger', () => { 'Account', false, 1, + 1, ); expect(result).toBeDefined(); @@ -369,6 +372,7 @@ describe('SalesforceTrigger', () => { 'Account', false, 1, + 1, ); }); }); @@ -534,6 +538,7 @@ describe('SalesforceTrigger', () => { '', // Empty resource name true, 0, + 1, ); }); });