From cb4db22b00ef50f41f88d95fff3c387ed8886cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Strzelecki?= <58707956+strzeluk@users.noreply.github.com> Date: Fri, 22 May 2026 14:29:21 +0200 Subject: [PATCH] fix(Azure Cosmos DB Node): Preserve query parameter types instead of converting to strings (#25882) Co-authored-by: Claude Opus 4.6 Co-authored-by: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com> --- .../descriptions/item/query.operation.ts | 15 +- .../Microsoft/AzureCosmosDb/helpers/utils.ts | 22 ++- .../AzureCosmosDb/test/helpers/utils.test.ts | 134 +++++++++++++++++- .../AzureCosmosDb/test/item/query.test.ts | 37 +++++ .../item/queryNumericParams.workflow.json | 68 +++++++++ 5 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/item/queryNumericParams.workflow.json diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/descriptions/item/query.operation.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/descriptions/item/query.operation.ts index c64e4f60f2a..2c4ac17664c 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/descriptions/item/query.operation.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/descriptions/item/query.operation.ts @@ -49,7 +49,7 @@ const properties: INodeProperties[] = [ name: 'queryParameters', default: '', description: - 'Comma-separated list of values used as query parameters. Use $1, $2, $3, etc., in your query.', + 'Comma-separated list of string values used as query parameters. Use $1, $2, $3, etc., in your query. All values are treated as strings — use "Query Parameters (JSON)" for typed values.', hint: 'Reference them in your query as $1, $2, $3…', placeholder: 'e.g. value1,value2,value3', routing: { @@ -59,6 +59,19 @@ const properties: INodeProperties[] = [ }, type: 'string', }, + { + displayName: 'Query Parameters (JSON)', + name: 'queryParametersJson', + default: '', + description: + 'JSON array of values used as query parameters. Preserves types (numbers, booleans, null, strings with leading zeros). Use $1, $2, $3, etc., in your query.', + hint: 'E.g. [1737062400000, "01234", true, null]. Use this instead of "Query Parameters" when type precision matters.', + placeholder: 'e.g. [value1, value2, value3]', + type: 'string', + typeOptions: { + rows: 1, + }, + }, ], }, ], diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/helpers/utils.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/helpers/utils.ts index 80f6e99475f..07107dfb8fe 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/helpers/utils.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/helpers/utils.ts @@ -81,10 +81,26 @@ export async function validateQueryParameters( const parameterNames = query.replace(/\$(\d+)/g, '@Param$1').match(/@\w+/g) ?? []; + const queryParamsJson = queryOptions?.queryParametersJson as string; const queryParamsString = queryOptions?.queryParameters as string; - const parameterValues = queryParamsString - ? queryParamsString.split(',').map((param) => param.trim()) - : []; + + let parameterValues: unknown[]; + + if (queryParamsJson) { + const parsed = jsonParse(queryParamsJson, { + errorMessage: 'Query Parameters (JSON) must be a valid JSON array', + }); + if (!Array.isArray(parsed)) { + throw new NodeOperationError(this.getNode(), 'Query Parameters (JSON) must be a JSON array', { + description: 'Provide values as a JSON array, e.g. [1737062400000, "01234", true, null]', + }); + } + parameterValues = parsed; + } else { + parameterValues = queryParamsString + ? queryParamsString.split(',').map((param) => param.trim()) + : []; + } if (parameterNames.length !== parameterValues.length) { throw new NodeOperationError(this.getNode(), 'Empty parameter value provided', { diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/helpers/utils.test.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/helpers/utils.test.ts index 8c92f9431a5..80f8c8db028 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/helpers/utils.test.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/helpers/utils.test.ts @@ -22,7 +22,7 @@ import { } from '../../helpers/utils'; interface RequestBodyWithParameters extends IDataObject { - parameters: Array<{ name: string; value: string }>; + parameters: Array<{ name: string; value: string | number | boolean | null }>; } const mockExecuteSingleFunctions = mock(); @@ -277,6 +277,138 @@ describe('validateQueryParameters', () => { } }); + test('should treat all comma-separated values as strings (regression: no numeric heuristic)', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1, $2') + .mockReturnValueOnce({ queryParameters: 'P12223, 1737062400000' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([ + { name: '@Param1', value: 'P12223' }, + { name: '@Param2', value: '1737062400000' }, + ]); + }); + + describe('queryParametersJson', () => { + test('should preserve string with leading zeros', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1') + .mockReturnValueOnce({ queryParametersJson: '["012345"]' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([ + { name: '@Param1', value: '012345' }, + ]); + }); + + test('should preserve integer value', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1') + .mockReturnValueOnce({ queryParametersJson: '[1737062400000]' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([ + { name: '@Param1', value: 1737062400000 }, + ]); + }); + + test('should lose precision for integers larger than Number.MAX_SAFE_INTEGER', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1') + .mockReturnValueOnce({ queryParametersJson: '[9007199254740993]' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([ + { name: '@Param1', value: 9007199254740992 }, + ]); + }); + + test('should preserve boolean values', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1, $2') + .mockReturnValueOnce({ queryParametersJson: '[true, false]' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([ + { name: '@Param1', value: true }, + { name: '@Param2', value: false }, + ]); + }); + + test('should preserve null value', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1') + .mockReturnValueOnce({ queryParametersJson: '[null]' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([ + { name: '@Param1', value: null }, + ]); + }); + + test('should handle empty string value', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1') + .mockReturnValueOnce({ queryParametersJson: '[""]' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([ + { name: '@Param1', value: '' }, + ]); + }); + + test('should handle empty parameters array', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('') + .mockReturnValueOnce({ queryParametersJson: '[]' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([]); + }); + + test('should handle mixed types', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1, $2, $3, $4') + .mockReturnValueOnce({ queryParametersJson: '[1737062400000, "01234", true, null]' }); + + const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions); + + expect((result.body as RequestBodyWithParameters).parameters).toEqual([ + { name: '@Param1', value: 1737062400000 }, + { name: '@Param2', value: '01234' }, + { name: '@Param3', value: true }, + { name: '@Param4', value: null }, + ]); + }); + + test('should throw NodeOperationError when value is not a JSON array', async () => { + mockExecuteSingleFunctions.getNodeParameter + .mockReturnValueOnce('$1') + .mockReturnValueOnce({ queryParametersJson: '{"a": 1}' }); + + await expect( + validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions), + ).rejects.toThrowError( + new NodeOperationError( + mockExecuteSingleFunctions.getNode(), + 'Query Parameters (JSON) must be a JSON array', + { + description: + 'Provide values as a JSON array, e.g. [1737062400000, "01234", true, null]', + }, + ), + ); + }); + }); + test('should extract and map parameter names correctly using regex', async () => { const query = '$1, $2, $3'; const queryParamsString = 'value1, value2, value3'; diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/item/query.test.ts b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/item/query.test.ts index bb860beec9e..b516a169459 100644 --- a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/item/query.test.ts +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/item/query.test.ts @@ -34,3 +34,40 @@ describe('Azure Cosmos DB - Query Items', () => { workflowFiles: ['query.workflow.json'], }); }); + +describe('Azure Cosmos DB - Query Items with Numeric Parameters', () => { + beforeEach(() => { + const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi; + + nock(baseUrl) + .persist() + .defaultReplyHeaders({ 'Content-Type': 'application/json' }) + .post('/colls/newId/docs', { + query: 'SELECT * FROM c WHERE c.projectId = @Param1 AND c.startDate = @Param2', + parameters: [ + { + name: '@Param1', + value: 'P12223', + }, + { + name: '@Param2', + value: 1737062400000, + }, + ], + }) + .reply(200, { + Documents: [ + { + id: 'User1', + projectId: 'P12223', + startDate: 1737062400000, + }, + ], + }); + }); + + new NodeTestHarness().setupTests({ + credentials, + workflowFiles: ['queryNumericParams.workflow.json'], + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/item/queryNumericParams.workflow.json b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/item/queryNumericParams.workflow.json new file mode 100644 index 00000000000..4a93c51dc9f --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/AzureCosmosDb/test/item/queryNumericParams.workflow.json @@ -0,0 +1,68 @@ +{ + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-80, -100], + "id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1", + "name": "When clicking 'Execute workflow'" + }, + { + "parameters": { + "resource": "item", + "operation": "query", + "container": { + "__rl": true, + "value": "newId", + "mode": "list", + "cachedResultName": "newId" + }, + "query": "SELECT * FROM c WHERE c.projectId = $1 AND c.startDate = $2", + "options": { + "queryOptions": { + "queryParametersJson": "[\"P12223\", 1737062400000]" + } + }, + "requestOptions": {} + }, + "type": "n8n-nodes-base.azureCosmosDb", + "typeVersion": 1, + "position": [160, -100], + "id": "0dc90797-8211-457c-a673-b7df28139de8", + "name": "queryItemsNumericParam", + "retryOnFail": false, + "alwaysOutputData": true, + "credentials": { + "microsoftAzureCosmosDbSharedKeyApi": { + "id": "exampleId", + "name": "Azure Cosmos DB account " + } + } + } + ], + "connections": { + "When clicking 'Execute workflow'": { + "main": [ + [ + { + "node": "queryItemsNumericParam", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "queryItemsNumericParam": [ + { + "json": { + "id": "User1", + "projectId": "P12223", + "startDate": 1737062400000 + } + } + ] + } +}