mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-31 08:46:58 +02:00
fix(Salesforce Node): Quote numeric string filter values in SOQL conditions (#31128)
This commit is contained in:
parent
1b9dfb20c4
commit
8dbcc8359a
|
|
@ -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 : ''}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user