From 096e535f1ededcf6d21b42c10fee68d142a7da7c Mon Sep 17 00:00:00 2001 From: Nikolay Shebanov Date: Tue, 29 Jul 2025 09:53:12 +0200 Subject: [PATCH] feat(Google BigQuery Node): Add parameterized query support (#14302) Co-authored-by: Elias Meire --- .../node/executeQuery.queryParameters.test.ts | 61 ++++++++++++++ ...executeQuery.queryParameters.workflow.json | 79 +++++++++++++++++++ .../database/executeQuery.operation.ts | 74 +++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.queryParameters.test.ts create mode 100644 packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.queryParameters.workflow.json diff --git a/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.queryParameters.test.ts b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.queryParameters.test.ts new file mode 100644 index 0000000000..a1ef61b0e2 --- /dev/null +++ b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.queryParameters.test.ts @@ -0,0 +1,61 @@ +import { NodeTestHarness } from '@nodes-testing/node-test-harness'; +import nock from 'nock'; + +jest.mock('jsonwebtoken', () => ({ + sign: jest.fn().mockReturnValue('signature'), +})); + +describe('Test Google BigQuery V2, executeQuery with named parameters', () => { + nock('https://oauth2.googleapis.com') + .persist() + .post( + '/token', + 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=signature', + ) + .reply(200, { access_token: 'token' }); + + nock('https://bigquery.googleapis.com/bigquery') + .post('/v2/projects/test-project/jobs', { + configuration: { + query: { + queryParameters: [ + { + name: 'email', + parameterType: { type: 'STRING' }, + parameterValue: { value: 'test@n8n.io' }, + }, + { + name: 'name', + parameterType: { type: 'STRING' }, + parameterValue: { value: 'Test Testerson' }, + }, + { + name: 'n8n_variable', + parameterType: { type: 'STRING' }, + parameterValue: { value: 42 }, + }, + ], + query: + 'SELECT * FROM bigquery_node_dev_test_dataset.test_json WHERE email = @email AND name = @name AND n8n_variable = @n8n_variable;', + useLegacySql: false, + parameterMode: 'NAMED', + }, + }, + }) + .reply(200, { + jobReference: { + jobId: 'job_123', + }, + status: { + state: 'DONE', + }, + }) + .get('/v2/projects/test-project/queries/job_123') + .reply(200) + .get('/v2/projects/test-project/queries/job_123?maxResults=1000&timeoutMs=10000') + .reply(200, { rows: [], schema: {} }); + + new NodeTestHarness().setupTests({ + workflowFiles: ['executeQuery.queryParameters.workflow.json'], + }); +}); diff --git a/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.queryParameters.workflow.json b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.queryParameters.workflow.json new file mode 100644 index 0000000000..967051db92 --- /dev/null +++ b/packages/nodes-base/nodes/Google/BigQuery/test/v2/node/executeQuery.queryParameters.workflow.json @@ -0,0 +1,79 @@ +{ + "name": "My workflow 12", + "nodes": [ + { + "parameters": {}, + "id": "7db7d51a-83c2-4aa0-a736-9c3d1c031b60", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [360, 340] + }, + { + "parameters": { + "authentication": "serviceAccount", + "projectId": { + "__rl": true, + "value": "test-project", + "mode": "list", + "cachedResultName": "test-project", + "cachedResultUrl": "https://console.cloud.google.com/bigquery?project=test-project" + }, + "sqlQuery": "SELECT * FROM bigquery_node_dev_test_dataset.test_json WHERE email = @email AND name = @name AND n8n_variable = @n8n_variable;", + "options": { + "queryParameters": { + "namedParameters": [ + { + "name": "email", + "value": "test@n8n.io" + }, + { + "name": "name", + "value": "Test Testerson" + }, + { + "name": "n8n_variable", + "value": "={{ 40 + 2 }}" + } + ] + } + } + }, + "id": "83d00275-0f98-4d5e-a3d6-bbca940ff8ac", + "name": "Google BigQuery", + "type": "n8n-nodes-base.googleBigQuery", + "typeVersion": 2, + "position": [620, 340], + "credentials": { + "googleApi": { + "id": "66", + "name": "Google account 5" + } + } + } + ], + "pinData": { + "Google BigQuery": [] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Google BigQuery", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "be2fc126-5d71-4e86-9a4e-eb62ad266860", + "id": "156", + "meta": { + "instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts index 38dcb91f3a..a847395382 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts @@ -12,6 +12,13 @@ import type { ResponseWithJobReference } from '../../helpers/interfaces'; import { prepareOutput } from '../../helpers/utils'; import { googleBigQueryApiRequestAllItems, googleBigQueryApiRequest } from '../../transport'; +interface IQueryParameterOptions { + namedParameters: Array<{ + name: string; + value: string; + }>; +} + const properties: INodeProperties[] = [ { displayName: 'SQL Query', @@ -151,6 +158,53 @@ const properties: INodeProperties[] = [ description: 'Whether all integer values will be returned as numbers. If set to false, all integer values will be returned as strings.', }, + { + displayName: 'Query Parameters (Named)', + name: 'queryParameters', + type: 'fixedCollection', + description: + 'Use parameterized queries to prevent SQL injections. Positional arguments are not supported at the moment. This feature won\'t be available when using legacy SQL.', + displayOptions: { + hide: { + '/options.useLegacySql': [true], + }, + }, + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Parameter', + default: { + namedParameters: [ + { + name: '', + value: '', + }, + ], + }, + options: [ + { + name: 'namedParameters', + displayName: 'Named Parameter', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the parameter', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: + 'The substitute value. It must be a string. Arrays, dates and struct types mentioned in the official documentation are not yet supported.', + }, + ], + }, + ], + }, ], }, ]; @@ -189,6 +243,7 @@ export async function execute(this: IExecuteFunctions): Promise { + // BigQuery type descriptors are very involved, and it would be hard to support all possible + // options, that's why the only supported type here is "STRING". + // + // If we switch this node to the official JS SDK from Google, we should be able to use `getTypeDescriptorFromValue` + // at runtime, which would infer BQ type descriptors of any valid JS value automatically: + // + // https://github.com/googleapis/nodejs-bigquery/blob/22021957f697ce67491bd50535f6fb43a99feea0/src/bigquery.ts#L1111 + // + // Another, less user-friendly option, would be to allow users to specify the types manually. + return { name, parameterType: { type: 'STRING' }, parameterValue: { value } }; + }); + } + //https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/insert const response: ResponseWithJobReference = await googleBigQueryApiRequest.call( this,