fix(Google BigQuery Node): Better error messages, transform timestamps (#9255)

Co-authored-by: Jonathan Bennetts <jonathan.bennetts@gmail.com>
This commit is contained in:
Elias Meire 2024-05-02 12:11:41 +02:00 committed by GitHub
parent e896889394
commit 7ff24f134b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 39 additions and 10 deletions

View file

@ -13,12 +13,13 @@ export class GoogleBigQuery extends VersionedNodeType {
group: ['input'], group: ['input'],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Google BigQuery API', description: 'Consume Google BigQuery API',
defaultVersion: 2, defaultVersion: 2.1,
}; };
const nodeVersions: IVersionedNodeType['nodeVersions'] = { const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new GoogleBigQueryV1(baseDescription), 1: new GoogleBigQueryV1(baseDescription),
2: new GoogleBigQueryV2(baseDescription), 2: new GoogleBigQueryV2(baseDescription),
2.1: new GoogleBigQueryV2(baseDescription),
}; };
super(nodeVersions, baseDescription); super(nodeVersions, baseDescription);

View file

@ -1,11 +1,14 @@
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; import type { IDataObject, IExecuteFunctions, INode } from 'n8n-workflow';
import { constructExecutionMetaData } from 'n8n-core'; import { constructExecutionMetaData } from 'n8n-core';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { prepareOutput } from '../../../v2/helpers/utils'; import { prepareOutput } from '../../../v2/helpers/utils';
describe('Google BigQuery v2 Utils', () => { describe('Google BigQuery v2 Utils', () => {
it('should prepareOutput', () => { it('should prepareOutput', () => {
const thisArg = mock<IExecuteFunctions>({ helpers: mock({ constructExecutionMetaData }) }); const thisArg = mock<IExecuteFunctions>({
getNode: () => ({ typeVersion: 2.1 }) as INode,
helpers: mock({ constructExecutionMetaData }),
});
const response: IDataObject = { const response: IDataObject = {
kind: 'bigquery#getQueryResultsResponse', kind: 'bigquery#getQueryResultsResponse',
etag: 'e_tag', etag: 'e_tag',

View file

@ -249,10 +249,16 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
returnData.push(...executionErrorData); returnData.push(...executionErrorData);
continue; continue;
} }
if ((error.message as string).includes('location')) { if ((error.message as string).includes('location') || error.httpCode === '404') {
error.description = error.description =
"Are you sure your table is in that region? You can specify the region using the 'Location' parameter from options."; "Are you sure your table is in that region? You can specify the region using the 'Location' parameter from options.";
} }
if (error.httpCode === '403' && error.message.includes('Drive')) {
error.description =
'If your table(s) pull from a document in Google Drive, make sure that document is shared with your user';
}
throw new NodeOperationError(this.getNode(), error as Error, { throw new NodeOperationError(this.getNode(), error as Error, {
itemIndex: i, itemIndex: i,
description: error.description, description: error.description,

View file

@ -7,7 +7,7 @@ export const versionDescription: INodeTypeDescription = {
name: 'googleBigQuery', name: 'googleBigQuery',
icon: 'file:googleBigQuery.svg', icon: 'file:googleBigQuery.svg',
group: ['input'], group: ['input'],
version: 2, version: [2, 2.1],
subtitle: '={{$parameter["operation"]}}', subtitle: '={{$parameter["operation"]}}',
description: 'Consume Google BigQuery API', description: 'Consume Google BigQuery API',
defaults: { defaults: {

View file

@ -1,8 +1,9 @@
import { DateTime } from 'luxon';
import type { IDataObject, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; import type { IDataObject, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { jsonParse, NodeOperationError } from 'n8n-workflow'; import { jsonParse, NodeOperationError } from 'n8n-workflow';
import type { SchemaField, TableRawData, TableSchema } from './interfaces'; import type { SchemaField, TableRawData, TableSchema } from './interfaces';
function getFieldValue(schemaField: SchemaField, field: IDataObject) { function getFieldValue(schemaField: SchemaField, field: IDataObject, parseTimestamps = false) {
if (schemaField.type === 'RECORD') { if (schemaField.type === 'RECORD') {
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
return simplify([field.v as TableRawData], schemaField.fields as unknown as SchemaField[]); return simplify([field.v as TableRawData], schemaField.fields as unknown as SchemaField[]);
@ -12,6 +13,9 @@ function getFieldValue(schemaField: SchemaField, field: IDataObject) {
try { try {
value = jsonParse(value as string); value = jsonParse(value as string);
} catch (error) {} } catch (error) {}
} else if (schemaField.type === 'TIMESTAMP' && parseTimestamps) {
const dt = DateTime.fromSeconds(Number(value));
value = dt.isValid ? dt.toISO() : value;
} }
return value; return value;
} }
@ -26,18 +30,27 @@ export function wrapData(data: IDataObject | IDataObject[]): INodeExecutionData[
})); }));
} }
export function simplify(data: TableRawData[], schema: SchemaField[], includeSchema = false) { export function simplify(
data: TableRawData[],
schema: SchemaField[],
includeSchema = false,
parseTimestamps = false,
) {
const returnData: IDataObject[] = []; const returnData: IDataObject[] = [];
for (const entry of data) { for (const entry of data) {
const record: IDataObject = {}; const record: IDataObject = {};
for (const [index, field] of entry.f.entries()) { for (const [index, field] of entry.f.entries()) {
if (schema[index].mode !== 'REPEATED') { if (schema[index].mode !== 'REPEATED') {
record[schema[index].name] = getFieldValue(schema[index], field); record[schema[index].name] = getFieldValue(schema[index], field, parseTimestamps);
} else { } else {
record[schema[index].name] = (field.v as unknown as IDataObject[]).flatMap( record[schema[index].name] = (field.v as unknown as IDataObject[]).flatMap(
(repeatedField) => { (repeatedField) => {
return getFieldValue(schema[index], repeatedField as unknown as IDataObject); return getFieldValue(
schema[index],
repeatedField as unknown as IDataObject,
parseTimestamps,
);
}, },
); );
} }
@ -68,12 +81,18 @@ export function prepareOutput(
responseData = response; responseData = response;
} else { } else {
const { rows, schema } = response; const { rows, schema } = response;
const parseTimestamps = this.getNode().typeVersion >= 2.1;
if (rows !== undefined && schema !== undefined) { if (rows !== undefined && schema !== undefined) {
const fields = (schema as TableSchema).fields; const fields = (schema as TableSchema).fields;
responseData = rows; responseData = rows;
responseData = simplify(responseData as TableRawData[], fields, includeSchema); responseData = simplify(
responseData as TableRawData[],
fields,
includeSchema,
parseTimestamps,
);
} else if (schema && includeSchema) { } else if (schema && includeSchema) {
responseData = { success: true, _schema: schema }; responseData = { success: true, _schema: schema };
} else { } else {