mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 16:26:59 +02:00
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:
parent
69c9e65e38
commit
cb4db22b00
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user