feat(Google BigQuery Node): Add parameterized query support (#14302)

Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Nikolay Shebanov
2025-07-29 09:53:12 +02:00
committed by GitHub
parent 8abc1d5e90
commit 096e535f1e
3 changed files with 214 additions and 0 deletions

View File

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

View File

@@ -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": []
}

View File

@@ -12,6 +12,13 @@ import type { ResponseWithJobReference } from '../../helpers/interfaces';
import { prepareOutput } from '../../helpers/utils'; import { prepareOutput } from '../../helpers/utils';
import { googleBigQueryApiRequestAllItems, googleBigQueryApiRequest } from '../../transport'; import { googleBigQueryApiRequestAllItems, googleBigQueryApiRequest } from '../../transport';
interface IQueryParameterOptions {
namedParameters: Array<{
name: string;
value: string;
}>;
}
const properties: INodeProperties[] = [ const properties: INodeProperties[] = [
{ {
displayName: 'SQL Query', displayName: 'SQL Query',
@@ -151,6 +158,53 @@ const properties: INodeProperties[] = [
description: description:
'Whether all integer values will be returned as numbers. If set to false, all integer values will be returned as strings.', '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 <a href="https://cloud.google.com/bigquery/docs/parameterized-queries#using_structs_in_parameterized_queries" target="_blank">parameterized queries</a> 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 <a href="https://cloud.google.com/bigquery/docs/parameterized-queries#using_structs_in_parameterized_queries" target="_blank">the official documentation</a> are not yet supported.',
},
],
},
],
},
], ],
}, },
]; ];
@@ -189,6 +243,7 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
rawOutput?: boolean; rawOutput?: boolean;
useLegacySql?: boolean; useLegacySql?: boolean;
returnAsNumbers?: boolean; returnAsNumbers?: boolean;
queryParameters?: IQueryParameterOptions;
}; };
const projectId = this.getNodeParameter('projectId', i, undefined, { const projectId = this.getNodeParameter('projectId', i, undefined, {
@@ -237,6 +292,25 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
body.useLegacySql = false; body.useLegacySql = false;
} }
if (typeof body.queryParameters === 'object') {
const { namedParameters } = body.queryParameters as IQueryParameterOptions;
body.parameterMode = 'NAMED';
body.queryParameters = namedParameters.map(({ name, value }) => {
// 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 //https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/insert
const response: ResponseWithJobReference = await googleBigQueryApiRequest.call( const response: ResponseWithJobReference = await googleBigQueryApiRequest.call(
this, this,