fix(Azure Cosmos DB Node): Preserve query parameter types instead of converting to strings (#25882)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com>
This commit is contained in:
Łukasz Strzelecki 2026-05-22 14:29:21 +02:00 committed by GitHub
parent 69c9e65e38
commit cb4db22b00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 271 additions and 5 deletions

View File

@ -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,
},
},
],
},
],

View File

@ -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<unknown>(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', {

View File

@ -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<IExecuteSingleFunctions>();
@ -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';

View File

@ -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'],
});
});

View File

@ -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
}
}
]
}
}