From b9d5a270a741e5d165039a8c20692c317a448915 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Sun, 28 Jul 2024 10:27:15 +0200 Subject: [PATCH 01/17] feat(baserow): upload files via url --- .../nodes/Baserow/Baserow.node.json | 2 +- .../nodes-base/nodes/Baserow/Baserow.node.ts | 64 +++++++++++++++++-- .../nodes/Baserow/OperationDescription.ts | 38 +++++++++++ packages/nodes-base/nodes/Baserow/types.ts | 6 +- 4 files changed, 100 insertions(+), 10 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.json b/packages/nodes-base/nodes/Baserow/Baserow.node.json index 176ec1664e..512c8f1612 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.json +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.json @@ -1,6 +1,6 @@ { "node": "n8n-nodes-base.baserow", - "nodeVersion": "1.0", + "nodeVersion": "1.1", "codexVersion": "1.0", "categories": ["Data & Storage"], "resources": { diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.ts b/packages/nodes-base/nodes/Baserow/Baserow.node.ts index bb378a97da..98746e5374 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.ts +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.ts @@ -21,9 +21,11 @@ import { operationFields } from './OperationDescription'; import type { BaserowCredentials, FieldsUiValues, + FileOperation, GetAllAdditionalOptions, LoadedResource, - Operation, + Resource, + RowOperation, Row, } from './types'; @@ -55,6 +57,10 @@ export class Baserow implements INodeType { type: 'options', noDataExpression: true, options: [ + { + name: 'File', + value: 'file', + }, { name: 'Row', value: 'row', @@ -106,6 +112,32 @@ export class Baserow implements INodeType { ], default: 'getAll', }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['file'], + }, + }, + options: [ + { + name: 'Upload', + value: 'upload', + description: 'Upload a file', + action: 'Upload a file', + }, + { + name: 'Upload via URL', + value: 'upload-via-url', + description: 'Upload a file via URL', + action: 'Upload a file via URL', + }, + ], + default: 'upload', + }, ...operationFields, ], }; @@ -159,7 +191,8 @@ export class Baserow implements INodeType { const items = this.getInputData(); const mapper = new TableFieldMapper(); const returnData: INodeExecutionData[] = []; - const operation = this.getNodeParameter('operation', 0) as Operation; + const resource = this.getNodeParameter('resource', 0) as Resource; + const operation = this.getNodeParameter('operation', 0) as RowOperation | FileOperation; const tableId = this.getNodeParameter('tableId', 0) as string; const credentials = await this.getCredentials('baserowApi'); @@ -169,7 +202,7 @@ export class Baserow implements INodeType { for (let i = 0; i < items.length; i++) { try { - if (operation === 'getAll') { + if (resource === 'row' && operation === 'getAll') { // ---------------------------------- // getAll // ---------------------------------- @@ -219,7 +252,7 @@ export class Baserow implements INodeType { { itemData: { item: i } }, ); returnData.push(...executionData); - } else if (operation === 'get') { + } else if (resource === 'row' && operation === 'get') { // ---------------------------------- // get // ---------------------------------- @@ -236,7 +269,7 @@ export class Baserow implements INodeType { { itemData: { item: i } }, ); returnData.push(...executionData); - } else if (operation === 'create') { + } else if (resource === 'row' && operation === 'create') { // ---------------------------------- // create // ---------------------------------- @@ -275,7 +308,7 @@ export class Baserow implements INodeType { { itemData: { item: i } }, ); returnData.push(...executionData); - } else if (operation === 'update') { + } else if (resource === 'row' && operation === 'update') { // ---------------------------------- // update // ---------------------------------- @@ -316,7 +349,7 @@ export class Baserow implements INodeType { { itemData: { item: i } }, ); returnData.push(...executionData); - } else if (operation === 'delete') { + } else if (resource === 'row' && operation === 'delete') { // ---------------------------------- // delete // ---------------------------------- @@ -333,6 +366,23 @@ export class Baserow implements INodeType { { itemData: { item: i } }, ); returnData.push(...executionData); + } else if (resource === 'file' && operation === 'upload-via-url') { + // ---------------------------------- + // upload-via-url + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#tag/User-files/operation/upload_via_url + + const url = this.getNodeParameter('url', i) as string; + const endpoint = '/api/user-files/upload-via-url/'; + const body = { url }; + const file = await baserowApiRequest.call(this, 'POST', endpoint, jwtToken, body); + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(file), + { itemData: { item: i } }, + ); + returnData.push(...executionData); } } catch (error) { if (this.continueOnFail()) { diff --git a/packages/nodes-base/nodes/Baserow/OperationDescription.ts b/packages/nodes-base/nodes/Baserow/OperationDescription.ts index 36332d1423..55e7f7f267 100644 --- a/packages/nodes-base/nodes/Baserow/OperationDescription.ts +++ b/packages/nodes-base/nodes/Baserow/OperationDescription.ts @@ -6,6 +6,11 @@ export const operationFields: INodeProperties[] = [ // ---------------------------------- { displayName: 'Database Name or ID', + displayOptions: { + show: { + resource: ['row'], + }, + }, name: 'databaseId', type: 'options', default: '', @@ -18,6 +23,11 @@ export const operationFields: INodeProperties[] = [ }, { displayName: 'Table Name or ID', + displayOptions: { + show: { + resource: ['row'], + }, + }, name: 'tableId', type: 'options', default: '', @@ -442,4 +452,32 @@ export const operationFields: INodeProperties[] = [ }, ], }, + { + displayName: 'File', + displayOptions: { + show: { + resource: ['file'], + operation: ['upload'], + }, + }, + name: 'file', + type: 'resourceLocator', + default: '', + required: true, + description: 'The file to upload', + }, + { + displayName: 'File URL', + displayOptions: { + show: { + resource: ['file'], + operation: ['upload-via-url'], + }, + }, + name: 'url', + type: 'string', + default: '', + required: true, + description: 'The URL of the file to upload', + }, ]; diff --git a/packages/nodes-base/nodes/Baserow/types.ts b/packages/nodes-base/nodes/Baserow/types.ts index bb340d3231..a4ea3a6af9 100644 --- a/packages/nodes-base/nodes/Baserow/types.ts +++ b/packages/nodes-base/nodes/Baserow/types.ts @@ -35,7 +35,9 @@ export type Row = Record; export type FieldsUiValues = Array<{ fieldId: string; - fieldValue: string; + fieldValue: string | string[]; }>; -export type Operation = 'create' | 'delete' | 'update' | 'get' | 'getAll'; +export type Resource = 'file' | 'row' | 'table' | 'database'; +export type RowOperation = 'create' | 'delete' | 'update' | 'get' | 'getAll'; +export type FileOperation = 'upload' | 'upload-via-url'; From 84133eeace0b4cb8a9d4e3ad310da486a9d941b9 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 4 Dec 2024 21:24:25 +0100 Subject: [PATCH 02/17] feat: upload from file --- .../nodes-base/nodes/Baserow/Baserow.node.ts | 414 +++++++++++------- .../nodes/Baserow/GenericFunctions.ts | 45 ++ 2 files changed, 289 insertions(+), 170 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.ts b/packages/nodes-base/nodes/Baserow/Baserow.node.ts index 98746e5374..f0a6b58df9 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.ts +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.ts @@ -10,6 +10,7 @@ import { import { baserowApiRequest, + baserowFileUploadRequest, baserowApiRequestAllItems, getJwtToken, TableFieldMapper, @@ -138,7 +139,36 @@ export class Baserow implements INodeType { ], default: 'upload', }, - ...operationFields, + { + displayName: 'Input Data Field Name', + name: 'binaryPropertyName', + + type: 'string', + default: 'data', + displayOptions: { + show: { + operation: ['upload'], + resource: ['file'], + }, + }, + required: true, + description: + 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG.', + }, + { + displayName: 'File URL', + displayOptions: { + show: { + resource: ['file'], + operation: ['upload-via-url'], + }, + }, + name: 'url', + type: 'string', + default: '', + required: true, + description: 'The URL of the file to upload', + }, ], }; @@ -189,200 +219,244 @@ export class Baserow implements INodeType { async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); - const mapper = new TableFieldMapper(); + const returnData: INodeExecutionData[] = []; const resource = this.getNodeParameter('resource', 0) as Resource; const operation = this.getNodeParameter('operation', 0) as RowOperation | FileOperation; - const tableId = this.getNodeParameter('tableId', 0) as string; const credentials = await this.getCredentials('baserowApi'); const jwtToken = await getJwtToken.call(this, credentials); - const fields = await mapper.getTableFields.call(this, tableId, jwtToken); - mapper.createMappings(fields); for (let i = 0; i < items.length; i++) { try { - if (resource === 'row' && operation === 'getAll') { - // ---------------------------------- - // getAll - // ---------------------------------- + if (resource === 'row') { + const mapper = new TableFieldMapper(); + const tableId = this.getNodeParameter('tableId', 0) as string; + const fields = await mapper.getTableFields.call(this, tableId, jwtToken); + mapper.createMappings(fields); - // https://api.baserow.io/api/redoc/#operation/list_database_table_rows + if (operation === 'getAll') { + // ---------------------------------- + // getAll + // ---------------------------------- - const { order, filters, filterType, search } = this.getNodeParameter( - 'additionalOptions', - i, - ) as GetAllAdditionalOptions; + // https://api.baserow.io/api/redoc/#operation/list_database_table_rows - const qs: IDataObject = {}; + const { order, filters, filterType, search } = this.getNodeParameter( + 'additionalOptions', + i, + ) as GetAllAdditionalOptions; - if (order?.fields) { - qs.order_by = order.fields - .map(({ field, direction }) => `${direction}${mapper.setField(field)}`) - .join(','); - } + const qs: IDataObject = {}; - if (filters?.fields) { - filters.fields.forEach(({ field, operator, value }) => { - qs[`filter__field_${mapper.setField(field)}__${operator}`] = value; - }); - } - - if (filterType) { - qs.filter_type = filterType; - } - - if (search) { - qs.search = search; - } - - const endpoint = `/api/database/rows/table/${tableId}/`; - const rows = (await baserowApiRequestAllItems.call( - this, - 'GET', - endpoint, - jwtToken, - {}, - qs, - )) as Row[]; - - rows.forEach((row) => mapper.idsToNames(row)); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(rows), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } else if (resource === 'row' && operation === 'get') { - // ---------------------------------- - // get - // ---------------------------------- - - // https://api.baserow.io/api/redoc/#operation/get_database_table_row - - const rowId = this.getNodeParameter('rowId', i) as string; - const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; - const row = await baserowApiRequest.call(this, 'GET', endpoint, jwtToken); - - mapper.idsToNames(row as Row); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(row as Row), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } else if (resource === 'row' && operation === 'create') { - // ---------------------------------- - // create - // ---------------------------------- - - // https://api.baserow.io/api/redoc/#operation/create_database_table_row - - const body: IDataObject = {}; - - const dataToSend = this.getNodeParameter('dataToSend', 0) as - | 'defineBelow' - | 'autoMapInputData'; - - if (dataToSend === 'autoMapInputData') { - const incomingKeys = Object.keys(items[i].json); - const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; - const inputDataToIgnore = rawInputsToIgnore.split(',').map((c) => c.trim()); - - for (const key of incomingKeys) { - if (inputDataToIgnore.includes(key)) continue; - body[key] = items[i].json[key]; - mapper.namesToIds(body); + if (order?.fields) { + qs.order_by = order.fields + .map(({ field, direction }) => `${direction}${mapper.setField(field)}`) + .join(','); } - } else { - const fieldsUi = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues; - for (const field of fieldsUi) { - body[`field_${field.fieldId}`] = field.fieldValue; + + if (filters?.fields) { + filters.fields.forEach(({ field, operator, value }) => { + qs[`filter__field_${mapper.setField(field)}__${operator}`] = value; + }); } + + if (filterType) { + qs.filter_type = filterType; + } + + if (search) { + qs.search = search; + } + + const endpoint = `/api/database/rows/table/${tableId}/`; + const rows = (await baserowApiRequestAllItems.call( + this, + 'GET', + endpoint, + jwtToken, + {}, + qs, + )) as Row[]; + + rows.forEach((row) => mapper.idsToNames(row)); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(rows), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } else if (operation === 'get') { + // ---------------------------------- + // get + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/get_database_table_row + + const rowId = this.getNodeParameter('rowId', i) as string; + const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; + const row = await baserowApiRequest.call(this, 'GET', endpoint, jwtToken); + + mapper.idsToNames(row as Row); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(row as Row), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } else if (operation === 'create') { + // ---------------------------------- + // create + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/create_database_table_row + + const body: IDataObject = {}; + + const dataToSend = this.getNodeParameter('dataToSend', 0) as + | 'defineBelow' + | 'autoMapInputData'; + + if (dataToSend === 'autoMapInputData') { + const incomingKeys = Object.keys(items[i].json); + const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; + const inputDataToIgnore = rawInputsToIgnore.split(',').map((c) => c.trim()); + + for (const key of incomingKeys) { + if (inputDataToIgnore.includes(key)) continue; + body[key] = items[i].json[key]; + mapper.namesToIds(body); + } + } else { + const fieldsUi = this.getNodeParameter( + 'fieldsUi.fieldValues', + i, + [], + ) as FieldsUiValues; + for (const field of fieldsUi) { + body[`field_${field.fieldId}`] = field.fieldValue; + } + } + + const endpoint = `/api/database/rows/table/${tableId}/`; + const createdRow = await baserowApiRequest.call(this, 'POST', endpoint, jwtToken, body); + + mapper.idsToNames(createdRow as Row); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(createdRow as Row), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/update_database_table_row + + const rowId = this.getNodeParameter('rowId', i) as string; + + const body: IDataObject = {}; + + const dataToSend = this.getNodeParameter('dataToSend', 0) as + | 'defineBelow' + | 'autoMapInputData'; + + if (dataToSend === 'autoMapInputData') { + const incomingKeys = Object.keys(items[i].json); + const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; + const inputsToIgnore = rawInputsToIgnore.split(',').map((c) => c.trim()); + + for (const key of incomingKeys) { + if (inputsToIgnore.includes(key)) continue; + body[key] = items[i].json[key]; + mapper.namesToIds(body); + } + } else { + const fieldsUi = this.getNodeParameter( + 'fieldsUi.fieldValues', + i, + [], + ) as FieldsUiValues; + for (const field of fieldsUi) { + body[`field_${field.fieldId}`] = field.fieldValue; + } + } + + const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; + const updatedRow = await baserowApiRequest.call( + this, + 'PATCH', + endpoint, + jwtToken, + body, + ); + + mapper.idsToNames(updatedRow as Row); + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(updatedRow as Row), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } else if (resource === 'row' && operation === 'delete') { + // ---------------------------------- + // delete + // ---------------------------------- + + // https://api.baserow.io/api/redoc/#operation/delete_database_table_row + + const rowId = this.getNodeParameter('rowId', i) as string; + + const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; + await baserowApiRequest.call(this, 'DELETE', endpoint, jwtToken); + + const executionData = this.helpers.constructExecutionMetaData( + [{ json: { success: true } }], + { itemData: { item: i } }, + ); + returnData.push(...executionData); } + } else if (resource === 'file') { + if (operation === 'upload-via-url') { + // ---------------------------------- + // upload-via-url + // ---------------------------------- - const endpoint = `/api/database/rows/table/${tableId}/`; - const createdRow = await baserowApiRequest.call(this, 'POST', endpoint, jwtToken, body); + // https://api.baserow.io/api/redoc/#tag/User-files/operation/upload_via_url - mapper.idsToNames(createdRow as Row); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(createdRow as Row), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } else if (resource === 'row' && operation === 'update') { - // ---------------------------------- - // update - // ---------------------------------- + const url = this.getNodeParameter('url', i) as string; + const endpoint = '/api/user-files/upload-via-url/'; + const body = { url }; + const file = await baserowApiRequest.call(this, 'POST', endpoint, jwtToken, body); - // https://api.baserow.io/api/redoc/#operation/update_database_table_row + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(file), + { itemData: { item: i } }, + ); + returnData.push(...executionData); + } else if (operation === 'upload') { + // ---------------------------------- + // upload + // ---------------------------------- - const rowId = this.getNodeParameter('rowId', i) as string; + // https://api.baserow.io/api/redoc/#tag/User-files/operation/upload_file - const body: IDataObject = {}; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i); + const { fileName, mimeType } = this.helpers.assertBinaryData(i, binaryPropertyName); + const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); - const dataToSend = this.getNodeParameter('dataToSend', 0) as - | 'defineBelow' - | 'autoMapInputData'; + const file = await baserowFileUploadRequest.call( + this, + jwtToken, + binaryDataBuffer, + fileName as string, + mimeType, + ); - if (dataToSend === 'autoMapInputData') { - const incomingKeys = Object.keys(items[i].json); - const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; - const inputsToIgnore = rawInputsToIgnore.split(',').map((c) => c.trim()); - - for (const key of incomingKeys) { - if (inputsToIgnore.includes(key)) continue; - body[key] = items[i].json[key]; - mapper.namesToIds(body); - } - } else { - const fieldsUi = this.getNodeParameter('fieldsUi.fieldValues', i, []) as FieldsUiValues; - for (const field of fieldsUi) { - body[`field_${field.fieldId}`] = field.fieldValue; - } + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(file), + { itemData: { item: i } }, + ); + returnData.push(...executionData); } - - const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; - const updatedRow = await baserowApiRequest.call(this, 'PATCH', endpoint, jwtToken, body); - - mapper.idsToNames(updatedRow as Row); - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(updatedRow as Row), - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } else if (resource === 'row' && operation === 'delete') { - // ---------------------------------- - // delete - // ---------------------------------- - - // https://api.baserow.io/api/redoc/#operation/delete_database_table_row - - const rowId = this.getNodeParameter('rowId', i) as string; - - const endpoint = `/api/database/rows/table/${tableId}/${rowId}/`; - await baserowApiRequest.call(this, 'DELETE', endpoint, jwtToken); - - const executionData = this.helpers.constructExecutionMetaData( - [{ json: { success: true } }], - { itemData: { item: i } }, - ); - returnData.push(...executionData); - } else if (resource === 'file' && operation === 'upload-via-url') { - // ---------------------------------- - // upload-via-url - // ---------------------------------- - - // https://api.baserow.io/api/redoc/#tag/User-files/operation/upload_via_url - - const url = this.getNodeParameter('url', i) as string; - const endpoint = '/api/user-files/upload-via-url/'; - const body = { url }; - const file = await baserowApiRequest.call(this, 'POST', endpoint, jwtToken, body); - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(file), - { itemData: { item: i } }, - ); - returnData.push(...executionData); } } catch (error) { if (this.continueOnFail()) { diff --git a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts index ba33755e2f..5f4f75141c 100644 --- a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts @@ -10,6 +10,41 @@ import { NodeApiError } from 'n8n-workflow'; import type { Accumulator, BaserowCredentials, LoadedResource } from './types'; +export async function baserowFileUploadRequest( + this: IExecuteFunctions, + jwtToken: string, + file: Buffer, + fileName: string, + mimeType: string, +) { + const credentials = (await this.getCredentials('baserowApi')) as BaserowCredentials; + + const options: OptionsWithUri = { + headers: { + Authorization: `JWT ${jwtToken}`, + 'Content-Type': 'multipart/form-data', + }, + method: 'POST', + uri: `${credentials.host}/api/user-files/upload-file/`, + json: false, + body: { + file: { + value: file, + options: { + filename: fileName, + contentType: mimeType, + }, + }, + }, + }; + + try { + return await this.helpers.request(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } +} + /** * Make a request to Baserow API. */ @@ -34,6 +69,16 @@ export async function baserowApiRequest( json: true, }; + if (body.formData) { + options.json = false; + // set content type to multipart/form-data + options.headers = { + ...options.headers, + 'Content-Type': 'multipart/form-data', + }; + options.body.resolveWithFullResponse = true; + } + if (Object.keys(qs).length === 0) { delete options.qs; } From 350eb81078a0f2f804437047f6c1534b77b261fe Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Fri, 6 Dec 2024 08:30:38 +0000 Subject: [PATCH 03/17] chore: test file upload for baserow node --- .../nodes-base/nodes/Baserow/Baserow.node.ts | 2 - .../nodes/Baserow/GenericFunctions.ts | 28 +++--- .../Baserow/test/GenericFunctions.test.ts | 87 +++++++++++++++++++ 3 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.ts b/packages/nodes-base/nodes/Baserow/Baserow.node.ts index f0a6b58df9..b3ea7a8f04 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.ts +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.ts @@ -17,8 +17,6 @@ import { toOptions, } from './GenericFunctions'; -import { operationFields } from './OperationDescription'; - import type { BaserowCredentials, FieldsUiValues, diff --git a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts index 5f4f75141c..f71058a8be 100644 --- a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts @@ -1,9 +1,10 @@ import type { IDataObject, IExecuteFunctions, + IHookFunctions, IHttpRequestMethods, + IHttpRequestOptions, ILoadOptionsFunctions, - IRequestOptions, JsonObject, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; @@ -11,21 +12,21 @@ import { NodeApiError } from 'n8n-workflow'; import type { Accumulator, BaserowCredentials, LoadedResource } from './types'; export async function baserowFileUploadRequest( - this: IExecuteFunctions, + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, jwtToken: string, file: Buffer, fileName: string, mimeType: string, ) { - const credentials = (await this.getCredentials('baserowApi')) as BaserowCredentials; + const credentials = await this.getCredentials('baserowApi'); - const options: OptionsWithUri = { + const options: IHttpRequestOptions = { headers: { Authorization: `JWT ${jwtToken}`, 'Content-Type': 'multipart/form-data', }, method: 'POST', - uri: `${credentials.host}/api/user-files/upload-file/`, + url: `${credentials.host}/api/user-files/upload-file/`, json: false, body: { file: { @@ -39,7 +40,7 @@ export async function baserowFileUploadRequest( }; try { - return await this.helpers.request(options); + return await this.helpers.httpRequest(options); } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); } @@ -49,7 +50,7 @@ export async function baserowFileUploadRequest( * Make a request to Baserow API. */ export async function baserowApiRequest( - this: IExecuteFunctions | ILoadOptionsFunctions, + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: IHttpRequestMethods, endpoint: string, jwtToken: string, @@ -58,25 +59,24 @@ export async function baserowApiRequest( ) { const credentials = await this.getCredentials('baserowApi'); - const options: IRequestOptions = { + const options: IHttpRequestOptions = { headers: { Authorization: `JWT ${jwtToken}`, }, method, body, qs, - uri: `${credentials.host}${endpoint}`, + url: `${credentials.host}${endpoint}`, json: true, }; if (body.formData) { options.json = false; - // set content type to multipart/form-data options.headers = { ...options.headers, 'Content-Type': 'multipart/form-data', }; - options.body.resolveWithFullResponse = true; + options.returnFullResponse = true; } if (Object.keys(qs).length === 0) { @@ -88,7 +88,7 @@ export async function baserowApiRequest( } try { - return await this.helpers.request(options); + return await this.helpers.httpRequest(options); } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); } @@ -135,13 +135,13 @@ export async function getJwtToken( this: IExecuteFunctions | ILoadOptionsFunctions, { username, password, host }: BaserowCredentials, ) { - const options: IRequestOptions = { + const options: IHttpRequestOptions = { method: 'POST', body: { username, password, }, - uri: `${host}/api/user/token-auth/`, + url: `${host}/api/user/token-auth/`, json: true, }; diff --git a/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts new file mode 100644 index 0000000000..e71c0c7f3e --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts @@ -0,0 +1,87 @@ +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IN8nHttpFullResponse, + IN8nHttpResponse, + INode, +} from 'n8n-workflow'; + +import { baserowFileUploadRequest } from '../GenericFunctions'; +import { mock } from 'node:test'; + +export const node: INode = { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow Upload', + type: 'n8n-nodes-base.Baserow', + typeVersion: 1, + position: [0, 0], + parameters: { + operation: 'upload', + domain: 'file', + }, +}; + +describe('Baserow', () => { + describe('baserowFileUploadRequest', () => { + const mockThis = { + helpers: { + requestWithAuthentication: jest.fn().mockResolvedValue({ statusCode: 200 }), + httpRequest: jest.fn(), + }, + getNode() { + return node; + }, + getCredentials: jest.fn(), + getNodeParameter: jest.fn(), + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + it('should make an authenticated API file-upload request to Baserow', async () => { + mockThis.getCredentials.mockResolvedValue({ + host: 'https://my-host.com', + }); + + // see https://api.baserow.io/api/redoc/#tag/User-files/operation/upload_file + mockThis.helpers.httpRequest.mockResolvedValue({ + statusCode: 200, + body: { + size: 2147483647, + mime_type: 'string', + is_image: true, + image_width: 32767, + image_height: 32767, + uploaded_at: '2019-08-24T14:15:22Z', + url: 'http://example.com', + thumbnails: { + property1: null, + property2: null, + }, + name: 'string', + original_name: 'string', + }, + } as IN8nHttpFullResponse | IN8nHttpResponse); + + const jwtToken = 'jwt'; + const file = Buffer.from('file'); + const filename = 'filename.txt'; + const mimeType = 'text/plain'; + + await baserowFileUploadRequest.call(mockThis, jwtToken, file, filename, mimeType); + + expect(mockThis.getCredentials).toHaveBeenCalledWith('baserowApi'); + + expect(mockThis.helpers.httpRequest).toHaveBeenCalledWith({ + body: { + file: { + options: { contentType: 'text/plain', filename: 'filename.txt' }, + value: file, + }, + }, + headers: { Authorization: 'JWT jwt', 'Content-Type': 'multipart/form-data' }, + json: false, + method: 'POST', + url: 'https://my-host.com/api/user-files/upload-file/', + }); + }); + }); +}); From 2e056e37ead6c9637c4d3c9f74c1ddc9f7db01cc Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Fri, 6 Dec 2024 09:37:35 +0100 Subject: [PATCH 04/17] fix: reset node version --- packages/nodes-base/nodes/Baserow/Baserow.node.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.json b/packages/nodes-base/nodes/Baserow/Baserow.node.json index 512c8f1612..176ec1664e 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.json +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.json @@ -1,6 +1,6 @@ { "node": "n8n-nodes-base.baserow", - "nodeVersion": "1.1", + "nodeVersion": "1.0", "codexVersion": "1.0", "categories": ["Data & Storage"], "resources": { From dae1349138503c3d388be12b35535bb266f59996 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Fri, 6 Dec 2024 08:58:17 +0000 Subject: [PATCH 05/17] chore: test baserowApiRequest --- .../Baserow/test/GenericFunctions.test.ts | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts index e71c0c7f3e..e41dac19d3 100644 --- a/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts +++ b/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts @@ -7,8 +7,7 @@ import type { INode, } from 'n8n-workflow'; -import { baserowFileUploadRequest } from '../GenericFunctions'; -import { mock } from 'node:test'; +import { baserowApiRequest, baserowFileUploadRequest } from '../GenericFunctions'; export const node: INode = { id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', @@ -23,6 +22,71 @@ export const node: INode = { }; describe('Baserow', () => { + describe('baserowApiRequest', () => { + const mockThis = { + helpers: { + requestWithAuthentication: jest.fn().mockResolvedValue({ statusCode: 200 }), + httpRequest: jest.fn(), + }, + getNode() { + return node; + }, + getCredentials: jest.fn(), + getNodeParameter: jest.fn(), + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + it('should upload a file via url', async () => { + mockThis.getCredentials.mockResolvedValue({ + host: 'https://my-host.com', + }); + + mockThis.helpers.httpRequest.mockResolvedValue({ + statusCode: 200, + body: { + size: 2147483647, + mime_type: 'string', + is_image: true, + image_width: 32767, + image_height: 32767, + uploaded_at: '2019-08-24T14:15:22Z', + url: 'http://example.com', + thumbnails: { + property1: null, + property2: null, + }, + name: 'string', + original_name: 'string', + }, + } as IN8nHttpFullResponse | IN8nHttpResponse); + + const jwtToken = 'jwt'; + const fileUrl = 'http://example.com/image.png'; + + const body = { + file_url: fileUrl, + }; + + await baserowApiRequest.call( + mockThis, + 'POST', + '/api/user-files/upload-via-url/', + jwtToken, + body, + ); + + expect(mockThis.getCredentials).toHaveBeenCalledWith('baserowApi'); + expect(mockThis.helpers.httpRequest).toHaveBeenCalledWith({ + headers: { + Authorization: `JWT ${jwtToken}`, + }, + method: 'POST', + url: 'https://my-host.com/api/user-files/upload-via-url/', + body, + json: true, + }); + }); + }); + describe('baserowFileUploadRequest', () => { const mockThis = { helpers: { From 78303b587a67f4907f7d445851a3fc16de9b2b5e Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Fri, 6 Dec 2024 09:54:01 +0000 Subject: [PATCH 06/17] chore: test getJWT --- .../nodes/Baserow/GenericFunctions.ts | 2 +- .../Baserow/test/GenericFunctions.test.ts | 30 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts index f71058a8be..1bc7caa752 100644 --- a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts @@ -146,7 +146,7 @@ export async function getJwtToken( }; try { - const { token } = (await this.helpers.request(options)) as { token: string }; + const { token } = (await this.helpers.httpRequest(options)) as { token: string }; return token; } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); diff --git a/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts index e41dac19d3..a0fbff8fdb 100644 --- a/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts +++ b/packages/nodes-base/nodes/Baserow/test/GenericFunctions.test.ts @@ -7,7 +7,9 @@ import type { INode, } from 'n8n-workflow'; -import { baserowApiRequest, baserowFileUploadRequest } from '../GenericFunctions'; +import { baserowApiRequest, baserowFileUploadRequest, getJwtToken } from '../GenericFunctions'; +import { username } from 'minifaker'; +import { httpRequest } from 'n8n-core'; export const node: INode = { id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', @@ -21,7 +23,27 @@ export const node: INode = { }, }; -describe('Baserow', () => { +describe('Baserow GenericFunctions', () => { + describe('getJwtToken', () => { + const mockThis = { + helpers: { + httpRequest: jest.fn().mockResolvedValue({ + token: 'jwt', + }), + }, + } as unknown as IExecuteFunctions | ILoadOptionsFunctions; + + it('should return the JWT token', async () => { + const jwtToken = await getJwtToken.call(mockThis, { + username: 'user', + password: 'password', + host: 'https://my-host.com', + }); + + expect(jwtToken).toBe('jwt'); + }); + }); + describe('baserowApiRequest', () => { const mockThis = { helpers: { @@ -33,7 +55,7 @@ describe('Baserow', () => { }, getCredentials: jest.fn(), getNodeParameter: jest.fn(), - } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + } as unknown as IExecuteFunctions | ILoadOptionsFunctions; it('should upload a file via url', async () => { mockThis.getCredentials.mockResolvedValue({ @@ -98,7 +120,7 @@ describe('Baserow', () => { }, getCredentials: jest.fn(), getNodeParameter: jest.fn(), - } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + } as unknown as IExecuteFunctions | ILoadOptionsFunctions; it('should make an authenticated API file-upload request to Baserow', async () => { mockThis.getCredentials.mockResolvedValue({ From efb5307f6367859a0453fb4b5df74f571f3c1b1d Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Fri, 6 Dec 2024 13:39:04 +0000 Subject: [PATCH 07/17] chore: Bring testing for row:getAll into place --- .../nodes-base/nodes/Baserow/Baserow.node.ts | 6 +- .../nodes/Baserow/GenericFunctions.ts | 18 +-- .../Baserow/test/node/rows/getAll.test.ts | 123 ++++++++++++++++++ 3 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 packages/nodes-base/nodes/Baserow/test/node/rows/getAll.test.ts diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.ts b/packages/nodes-base/nodes/Baserow/Baserow.node.ts index b3ea7a8f04..0bc8e18e3d 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.ts +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.ts @@ -15,6 +15,7 @@ import { getJwtToken, TableFieldMapper, toOptions, + getTableFields, } from './GenericFunctions'; import type { @@ -228,9 +229,10 @@ export class Baserow implements INodeType { for (let i = 0; i < items.length; i++) { try { if (resource === 'row') { - const mapper = new TableFieldMapper(); const tableId = this.getNodeParameter('tableId', 0) as string; - const fields = await mapper.getTableFields.call(this, tableId, jwtToken); + const fields = await getTableFields.call(this, tableId, jwtToken); + + const mapper = new TableFieldMapper(); mapper.createMappings(fields); if (operation === 'getAll') { diff --git a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts index 1bc7caa752..e4b2d52aed 100644 --- a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts @@ -172,6 +172,15 @@ export async function getFieldNamesAndIds( }; } +export async function getTableFields( + this: IExecuteFunctions, + table: string, + jwtToken: string, +): Promise { + const endpoint = `/api/database/fields/table/${table}/`; + return await baserowApiRequest.call(this, 'GET', endpoint, jwtToken); +} + export const toOptions = (items: LoadedResource[]) => items.map(({ name, id }) => ({ name, value: id })); @@ -185,15 +194,6 @@ export class TableFieldMapper { mapIds = true; - async getTableFields( - this: IExecuteFunctions, - table: string, - jwtToken: string, - ): Promise { - const endpoint = `/api/database/fields/table/${table}/`; - return await baserowApiRequest.call(this, 'GET', endpoint, jwtToken); - } - createMappings(tableFields: LoadedResource[]) { this.nameToIdMapping = this.createNameToIdMapping(tableFields); this.idToNameMapping = this.createIdToNameMapping(tableFields); diff --git a/packages/nodes-base/nodes/Baserow/test/node/rows/getAll.test.ts b/packages/nodes-base/nodes/Baserow/test/node/rows/getAll.test.ts new file mode 100644 index 0000000000..eca205d0f0 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/node/rows/getAll.test.ts @@ -0,0 +1,123 @@ +import { constructExecutionMetaData, returnJsonArray } from 'n8n-core'; +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INode, + INodeExecutionData, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { Baserow } from '../../../Baserow.node'; +import { getTableFields } from '../../../GenericFunctions'; +import type { GetAllAdditionalOptions } from '../../../types'; + +jest.mock('../../../GenericFunctions', () => { + const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); + return { + ...originalModule, + getJwtToken: jest.fn().mockResolvedValue('jwt'), + getTableFields: jest.fn().mockResolvedValue([ + { + id: '1', + name: 'my_field_name', + }, + ]), + }; +}); + +describe('Baserow Node', () => { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../GenericFunctions'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('resource: row', () => { + it('getAll should fetch all records', async () => { + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + httpRequest: jest.fn().mockResolvedValue({ + count: 1, + next: null, + previous: null, + results: [ + { + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }, + ], + }), + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow Upload', + type: 'n8n-nodes-base.Baserow', + typeVersion: 1, + position: [0, 0], + parameters: { + operation: 'getAll', + resource: 'row', + tableId: 1, + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: {}, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'resource': + return 'row'; + case 'operation': + return 'getAll'; + case 'tableId': + return 1; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + const node = new Baserow(); + const response: INodeExecutionData[][] = await node.execute.call(mockThis); + + expect(getTableFields).toHaveBeenCalledTimes(1); + expect(response).toEqual([ + [ + { + json: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + my_field_name: 'baz', + }, + pairedItem: { + item: 0, + }, + }, + ], + ]); + }); + }); +}); From 1e00961f7b65699170796ba5220b76a485f44247 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Mon, 9 Dec 2024 23:54:47 +0100 Subject: [PATCH 08/17] chore: test crud actions --- .../test/node/file/upload-via-url.test.ts | 143 ++++++++++++++++ .../Baserow/test/node/file/upload.test.ts | 156 ++++++++++++++++++ .../Baserow/test/node/rows/create.test.ts | 140 ++++++++++++++++ .../Baserow/test/node/rows/delete.test.ts | 125 ++++++++++++++ .../nodes/Baserow/test/node/rows/get.test.ts | 137 +++++++++++++++ .../Baserow/test/node/rows/getAll.test.ts | 2 +- .../Baserow/test/node/rows/update.test.ts | 144 ++++++++++++++++ 7 files changed, 846 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/nodes/Baserow/test/node/file/upload-via-url.test.ts create mode 100644 packages/nodes-base/nodes/Baserow/test/node/file/upload.test.ts create mode 100644 packages/nodes-base/nodes/Baserow/test/node/rows/create.test.ts create mode 100644 packages/nodes-base/nodes/Baserow/test/node/rows/delete.test.ts create mode 100644 packages/nodes-base/nodes/Baserow/test/node/rows/get.test.ts create mode 100644 packages/nodes-base/nodes/Baserow/test/node/rows/update.test.ts diff --git a/packages/nodes-base/nodes/Baserow/test/node/file/upload-via-url.test.ts b/packages/nodes-base/nodes/Baserow/test/node/file/upload-via-url.test.ts new file mode 100644 index 0000000000..3c454f1606 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/node/file/upload-via-url.test.ts @@ -0,0 +1,143 @@ +import { constructExecutionMetaData, returnJsonArray } from 'n8n-core'; +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INode, + INodeExecutionData, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { Baserow } from '../../../Baserow.node'; +import { baserowApiRequest, getTableFields } from '../../../GenericFunctions'; +import type { GetAllAdditionalOptions } from '../../../types'; + +jest.mock('../../../GenericFunctions', () => { + const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); + return { + ...originalModule, + baserowApiRequest: jest.fn().mockResolvedValue({ + size: 2147483647, + mime_type: 'string', + is_image: true, + image_width: 32767, + image_height: 32767, + uploaded_at: '2019-08-24T14:15:22Z', + url: 'http://example.com', + thumbnails: { + property1: null, + property2: null, + }, + name: 'string', + original_name: 'string', + }), + getJwtToken: jest.fn().mockResolvedValue('jwt'), + }; +}); + +describe('Baserow Node', () => { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../GenericFunctions'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('resource: file', () => { + it('upload-via-url should upload a file via url', async () => { + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + httpRequest: jest.fn().mockResolvedValue({ + count: 1, + next: null, + previous: null, + results: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }, + }), + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow upload-via-url', + type: 'n8n-nodes-base.Baserow', + typeVersion: 1, + position: [0, 0], + parameters: { + operation: 'upload-via-url', + resource: 'file', + tableId: 1, + rowId: 1, + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: {}, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'resource': + return 'file'; + case 'operation': + return 'upload-via-url'; + case 'url': + return 'https://example.com/image.jpg'; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + const node = new Baserow(); + const response: INodeExecutionData[][] = await node.execute.call(mockThis); + expect(baserowApiRequest).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenNthCalledWith( + 1, + 'POST', + '/api/user-files/upload-via-url/', + 'jwt', + { url: 'https://example.com/image.jpg' }, + ); + + expect(response).toEqual([ + [ + { + json: { + image_height: 32767, + image_width: 32767, + is_image: true, + mime_type: 'string', + name: 'string', + original_name: 'string', + size: 2147483647, + thumbnails: { property1: null, property2: null }, + uploaded_at: '2019-08-24T14:15:22Z', + url: 'http://example.com', + }, + pairedItem: { item: 0 }, + }, + ], + ]); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Baserow/test/node/file/upload.test.ts b/packages/nodes-base/nodes/Baserow/test/node/file/upload.test.ts new file mode 100644 index 0000000000..9a5a832b46 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/node/file/upload.test.ts @@ -0,0 +1,156 @@ +import { constructExecutionMetaData, returnJsonArray } from 'n8n-core'; +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INode, + INodeExecutionData, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { Baserow } from '../../../Baserow.node'; +import { baserowFileUploadRequest } from '../../../GenericFunctions'; +import type { GetAllAdditionalOptions } from '../../../types'; + +jest.mock('../../../GenericFunctions', () => { + const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); + return { + ...originalModule, + baserowFileUploadRequest: jest.fn().mockResolvedValue({ + size: 2147483647, + mime_type: 'string', + is_image: true, + image_width: 32767, + image_height: 32767, + uploaded_at: '2019-08-24T14:15:22Z', + url: 'http://example.com', + thumbnails: { + property1: null, + property2: null, + }, + name: 'string', + original_name: 'string', + }), + getJwtToken: jest.fn().mockResolvedValue('jwt'), + }; +}); + +describe('Baserow Node', () => { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../GenericFunctions'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('resource: file', () => { + it('upload should upload a file', async () => { + const buffer = Buffer.from('test'); + + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + assertBinaryData: jest + .fn() + .mockReturnValue({ fileName: 'test.png', mimeType: 'image/png' }), + getBinaryDataBuffer: jest.fn().mockResolvedValue(buffer), + httpRequest: jest.fn().mockResolvedValue({ + count: 1, + next: null, + previous: null, + results: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }, + }), + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow upload', + type: 'n8n-nodes-base.Baserow', + typeVersion: 1, + position: [0, 0], + parameters: { + operation: 'upload', + resource: 'file', + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: {}, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'resource': + return 'file'; + case 'operation': + return 'upload'; + case 'url': + return 'https://example.com/image.jpg'; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + case 'binaryPropertyName': + return 'data'; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + const node = new Baserow(); + const response: INodeExecutionData[][] = await node.execute.call(mockThis); + + expect(mockThis.helpers.assertBinaryData).toHaveBeenCalledTimes(1); + expect(mockThis.helpers.getBinaryDataBuffer).toHaveBeenCalledTimes(1); + + expect(baserowFileUploadRequest).toHaveBeenCalledTimes(1); + expect(baserowFileUploadRequest).toHaveBeenNthCalledWith( + 1, + 'jwt', + buffer, + 'test.png', + 'image/png', + ); + + expect(response).toEqual([ + [ + { + json: { + size: 2147483647, + mime_type: 'string', + is_image: true, + image_width: 32767, + image_height: 32767, + uploaded_at: '2019-08-24T14:15:22Z', + url: 'http://example.com', + thumbnails: { + property1: null, + property2: null, + }, + name: 'string', + original_name: 'string', + }, + pairedItem: { item: 0 }, + }, + ], + ]); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Baserow/test/node/rows/create.test.ts b/packages/nodes-base/nodes/Baserow/test/node/rows/create.test.ts new file mode 100644 index 0000000000..dfb2e211a4 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/node/rows/create.test.ts @@ -0,0 +1,140 @@ +import { constructExecutionMetaData, returnJsonArray } from 'n8n-core'; +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INode, + INodeExecutionData, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { Baserow } from '../../../Baserow.node'; +import { baserowApiRequest, getTableFields } from '../../../GenericFunctions'; +import type { GetAllAdditionalOptions } from '../../../types'; + +jest.mock('../../../GenericFunctions', () => { + const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); + return { + ...originalModule, + baserowApiRequest: jest.fn().mockResolvedValue({ + id: 1, + field_1: 'baz', + }), + getJwtToken: jest.fn().mockResolvedValue('jwt'), + getTableFields: jest.fn().mockResolvedValue([ + { + id: '1', + name: 'my_field_name', + }, + ]), + }; +}); + +describe('Baserow Node', () => { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../GenericFunctions'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('resource: row', () => { + it('create should create a record', async () => { + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + httpRequest: jest.fn().mockResolvedValue({ + count: 1, + next: null, + previous: null, + results: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }, + }), + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow get', + type: 'n8n-nodes-base.Baserow', + typeVersion: 1, + position: [0, 0], + parameters: { + operation: 'create', + resource: 'row', + tableId: 1, + rowId: 1, + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: { + my_field_name: 'new value', + }, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'resource': + return 'row'; + case 'operation': + return 'create'; + case 'tableId': + return 1; + case 'dataToSend': + return 'autoMapInputData'; + case 'inputsToIgnore': + return ''; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + const node = new Baserow(); + const response: INodeExecutionData[][] = await node.execute.call(mockThis); + + expect(getTableFields).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenNthCalledWith( + 1, + 'POST', + '/api/database/rows/table/1/', + 'jwt', + { field_1: 'new value' }, + ); + + expect(response).toEqual([ + [ + { + json: { + id: 1, + my_field_name: 'baz', + }, + pairedItem: { + item: 0, + }, + }, + ], + ]); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Baserow/test/node/rows/delete.test.ts b/packages/nodes-base/nodes/Baserow/test/node/rows/delete.test.ts new file mode 100644 index 0000000000..e8d3fd5064 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/node/rows/delete.test.ts @@ -0,0 +1,125 @@ +import { constructExecutionMetaData, returnJsonArray } from 'n8n-core'; +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INode, + INodeExecutionData, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { Baserow } from '../../../Baserow.node'; +import { baserowApiRequest, getTableFields } from '../../../GenericFunctions'; +import type { GetAllAdditionalOptions } from '../../../types'; + +jest.mock('../../../GenericFunctions', () => { + const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); + return { + ...originalModule, + baserowApiRequest: jest.fn().mockResolvedValue({ + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }), + getJwtToken: jest.fn().mockResolvedValue('jwt'), + getTableFields: jest.fn().mockResolvedValue([ + { + id: '1', + name: 'my_field_name', + }, + ]), + }; +}); + +describe('Baserow Node', () => { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../GenericFunctions'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('resource: row', () => { + it('delete should delete a record', async () => { + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow delete', + type: 'n8n-nodes-base.Baserow', + typeVersion: 1, + position: [0, 0], + parameters: { + operation: 'delete', + resource: 'row', + tableId: 1, + rowId: 1, + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: {}, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'resource': + return 'row'; + case 'operation': + return 'delete'; + case 'tableId': + return 1; + case 'rowId': + return 1; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + const node = new Baserow(); + const response: INodeExecutionData[][] = await node.execute.call(mockThis); + + expect(getTableFields).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenNthCalledWith( + 1, + 'DELETE', + '/api/database/rows/table/1/1/', + 'jwt', + ); + + expect(response).toEqual([ + [ + { + json: { + success: true, + }, + pairedItem: { + item: 0, + }, + }, + ], + ]); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Baserow/test/node/rows/get.test.ts b/packages/nodes-base/nodes/Baserow/test/node/rows/get.test.ts new file mode 100644 index 0000000000..495700a28e --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/node/rows/get.test.ts @@ -0,0 +1,137 @@ +import { constructExecutionMetaData, returnJsonArray } from 'n8n-core'; +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INode, + INodeExecutionData, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { Baserow } from '../../../Baserow.node'; +import { baserowApiRequest, getTableFields } from '../../../GenericFunctions'; +import type { GetAllAdditionalOptions } from '../../../types'; + +jest.mock('../../../GenericFunctions', () => { + const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); + return { + ...originalModule, + baserowApiRequest: jest.fn().mockResolvedValue({ + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }), + getJwtToken: jest.fn().mockResolvedValue('jwt'), + getTableFields: jest.fn().mockResolvedValue([ + { + id: '1', + name: 'my_field_name', + }, + ]), + }; +}); + +describe('Baserow Node', () => { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../GenericFunctions'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('resource: row', () => { + it('get should fetch a record', async () => { + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + httpRequest: jest.fn().mockResolvedValue({ + count: 1, + next: null, + previous: null, + results: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }, + }), + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow get', + type: 'n8n-nodes-base.Baserow', + typeVersion: 1, + position: [0, 0], + parameters: { + operation: 'get', + resource: 'row', + tableId: 1, + rowId: 1, + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: {}, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'resource': + return 'row'; + case 'operation': + return 'get'; + case 'tableId': + return 1; + case 'rowId': + return 1; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + const node = new Baserow(); + const response: INodeExecutionData[][] = await node.execute.call(mockThis); + + expect(getTableFields).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenNthCalledWith( + 1, + 'GET', + '/api/database/rows/table/1/1/', + 'jwt', + ); + + expect(response).toEqual([ + [ + { + json: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + my_field_name: 'baz', + }, + pairedItem: { + item: 0, + }, + }, + ], + ]); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Baserow/test/node/rows/getAll.test.ts b/packages/nodes-base/nodes/Baserow/test/node/rows/getAll.test.ts index eca205d0f0..81dcb4057e 100644 --- a/packages/nodes-base/nodes/Baserow/test/node/rows/getAll.test.ts +++ b/packages/nodes-base/nodes/Baserow/test/node/rows/getAll.test.ts @@ -62,7 +62,7 @@ describe('Baserow Node', () => { getNode() { return { id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', - name: 'Baserow Upload', + name: 'Baserow getAll', type: 'n8n-nodes-base.Baserow', typeVersion: 1, position: [0, 0], diff --git a/packages/nodes-base/nodes/Baserow/test/node/rows/update.test.ts b/packages/nodes-base/nodes/Baserow/test/node/rows/update.test.ts new file mode 100644 index 0000000000..85ce78cc78 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/node/rows/update.test.ts @@ -0,0 +1,144 @@ +import { constructExecutionMetaData, returnJsonArray } from 'n8n-core'; +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INode, + INodeExecutionData, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { Baserow } from '../../../Baserow.node'; +import { baserowApiRequest, getTableFields } from '../../../GenericFunctions'; +import type { GetAllAdditionalOptions } from '../../../types'; + +jest.mock('../../../GenericFunctions', () => { + const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); + return { + ...originalModule, + baserowApiRequest: jest.fn().mockResolvedValue({ + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'changed', + }), + getJwtToken: jest.fn().mockResolvedValue('jwt'), + getTableFields: jest.fn().mockResolvedValue([ + { + id: '1', + name: 'my_field_name', + }, + ]), + }; +}); + +describe('Baserow Node', () => { + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../GenericFunctions'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('resource: row', () => { + it('update should update a record', async () => { + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + httpRequest: jest.fn().mockResolvedValue({ + count: 1, + next: null, + previous: null, + results: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }, + }), + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow get', + type: 'n8n-nodes-base.Baserow', + typeVersion: 1, + position: [0, 0], + parameters: { + operation: 'update', + resource: 'row', + tableId: 1, + rowId: 1, + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: { + my_field_name: 'changed', + }, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'resource': + return 'row'; + case 'operation': + return 'update'; + case 'tableId': + return 1; + case 'rowId': + return 1; + case 'dataToSend': + return 'autoMapInputData'; + case 'inputsToIgnore': + return ''; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + const node = new Baserow(); + const response: INodeExecutionData[][] = await node.execute.call(mockThis); + + expect(getTableFields).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenCalledTimes(1); + expect(baserowApiRequest).toHaveBeenNthCalledWith( + 1, + 'PATCH', + '/api/database/rows/table/1/1/', + 'jwt', + { field_1: 'changed' }, + ); + + expect(response).toEqual([ + [ + { + json: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + my_field_name: 'changed', + }, + pairedItem: { + item: 0, + }, + }, + ], + ]); + }); + }); +}); From 991e06b83c16ad9c3dad83592054c2d41b518b30 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 11 Dec 2024 08:54:48 +0100 Subject: [PATCH 09/17] feat: add trigger node --- .../nodes-base/nodes/Baserow/Baserow.node.ts | 5 +- .../nodes/Baserow/BaserowTrigger.node.json | 18 ++ .../nodes/Baserow/BaserowTrigger.node.ts | 227 ++++++++++++++++ .../nodes/Baserow/GenericFunctions.ts | 23 +- .../Baserow/test/node/trigger/trigger.test.ts | 247 ++++++++++++++++++ packages/nodes-base/package.json | 1 + 6 files changed, 514 insertions(+), 7 deletions(-) create mode 100644 packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json create mode 100644 packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Baserow/test/node/trigger/trigger.test.ts diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.ts b/packages/nodes-base/nodes/Baserow/Baserow.node.ts index 0bc8e18e3d..8aedad533e 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.ts +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.ts @@ -8,6 +8,7 @@ import { NodeConnectionType, } from 'n8n-workflow'; +import { returnAllOrLimit } from '@utils/descriptions'; import { baserowApiRequest, baserowFileUploadRequest, @@ -17,7 +18,7 @@ import { toOptions, getTableFields, } from './GenericFunctions'; - +import { operationFields } from './OperationDescription'; import type { BaserowCredentials, FieldsUiValues, @@ -168,6 +169,8 @@ export class Baserow implements INodeType { required: true, description: 'The URL of the file to upload', }, + ...operationFields, + ...returnAllOrLimit, ], }; diff --git a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json new file mode 100644 index 0000000000..37bfc38fa0 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.baserowTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Data & Storage"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/credentials/baserow/" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.baserowtrigger/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts new file mode 100644 index 0000000000..c545022563 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts @@ -0,0 +1,227 @@ +import moment from 'moment-timezone'; +import type { + IPollFunctions, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + ILoadOptionsFunctions, +} from 'n8n-workflow'; +import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; + +import { returnAllOrLimit } from '@utils/descriptions'; + +import { + baserowApiRequest, + baserowApiRequestAllItems, + getJwtToken, + getTableFields, + TableFieldMapper, + toOptions, +} from './GenericFunctions'; +import type { BaserowCredentials, LoadedResource, Row } from './types'; + +export class BaserowTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Baserow Trigger', + name: 'baserowTrigger', + icon: 'file:baserow.svg', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Baserow events occur', + subtitle: '={{$parameter["event"]}}', + defaults: { + name: 'Baserow Trigger', + }, + credentials: [ + { + name: 'baserowApi', + required: true, + }, + ], + polling: true, + inputs: [], + outputs: [NodeConnectionType.Main], + properties: [ + { + displayName: 'Database Name or ID', + name: 'databaseId', + type: 'options', + default: '', + required: true, + description: + 'Database to operate on. Choose from the list, or specify an ID using an expression.', + typeOptions: { + loadOptionsMethod: 'getDatabaseIds', + }, + }, + { + displayName: 'Table Name or ID', + name: 'tableId', + type: 'options', + default: '', + required: true, + description: + 'Table to operate on. Choose from the list, or specify an ID using an expression.', + typeOptions: { + loadOptionsDependsOn: ['databaseId'], + loadOptionsMethod: 'getTableIds', + }, + }, + { + displayName: 'Trigger Field', + name: 'triggerField', + type: 'string', + default: '', + description: + 'A Created Time or Last Modified Time field that will be used to sort records. If you do not have a Created Time or Last Modified Time field in your schema, please create one, because without this field trigger will not work correctly.', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + requiresDataPath: 'multiple', + default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-id + description: + 'Fields to be included in the response. Multiple ones can be set separated by comma. Example: name, id. By default all fields will be included.', + }, + { + displayName: 'View ID', + name: 'viewId', + type: 'string', + default: '', + description: + 'The name or ID of a view in the table. If set, only the records in that view will be returned.', + }, + ], + }, + ...returnAllOrLimit, + ], + }; + + methods = { + loadOptions: { + async getDatabaseIds(this: ILoadOptionsFunctions) { + const credentials = await this.getCredentials('baserowApi'); + const jwtToken = await getJwtToken.call(this, credentials); + const endpoint = '/api/applications/'; + const databases = (await baserowApiRequest.call( + this, + 'GET', + endpoint, + jwtToken, + )) as LoadedResource[]; + return toOptions(databases); + }, + + async getTableIds(this: ILoadOptionsFunctions) { + const credentials = await this.getCredentials('baserowApi'); + const jwtToken = await getJwtToken.call(this, credentials); + const databaseId = this.getNodeParameter('databaseId', 0) as string; + const endpoint = `/api/database/tables/database/${databaseId}/`; + const tables = (await baserowApiRequest.call( + this, + 'GET', + endpoint, + jwtToken, + )) as LoadedResource[]; + return toOptions(tables); + }, + + async getTableFields(this: ILoadOptionsFunctions) { + const credentials = await this.getCredentials('baserowApi'); + const jwtToken = await getJwtToken.call(this, credentials); + const tableId = this.getNodeParameter('tableId', 0) as string; + const endpoint = `/api/database/fields/table/${tableId}/`; + const fields = (await baserowApiRequest.call( + this, + 'GET', + endpoint, + jwtToken, + )) as LoadedResource[]; + return toOptions(fields); + }, + }, + }; + + async poll(this: IPollFunctions): Promise { + const credentials = await this.getCredentials('baserowApi'); + const jwtToken = await getJwtToken.call(this, credentials); + const additionalFields = this.getNodeParameter('additionalFields') as IDataObject; + const webhookData = this.getWorkflowStaticData('node'); + const tableId = this.getNodeParameter('tableId') as string; + const triggerField = this.getNodeParameter('triggerField') as string; + + const fields = await getTableFields.call(this, tableId, jwtToken); + const tableMapper = new TableFieldMapper(); + tableMapper.createMappings(fields); + + const qs: IDataObject = {}; + + const endpoint = `/api/database/rows/table/${tableId}/`; + + const now = moment().utc().format(); + + const startDate = (webhookData.lastTimeChecked as string) || now; + + const endDate = now; + + if (additionalFields.viewId) { + qs.view_id = additionalFields.viewId; + } + + if (additionalFields.fields) { + const include_fields = (additionalFields.fields as string) + .split(',') + .map((field) => tableMapper.setField(field)) + .join(','); + qs.include = include_fields; + } + + // Constructing datetime filters is unintuitive.. + // First, the date_after filter is deprecated, but still works. + // Second, the datetime needs to be prefixed with the timezone. + // + // Example: "Europe/Amsterdam?2024-10-11 12:13:14" + // see: https://community.baserow.io/t/filtering-on-datetime-fields-does-not-seem-possible/6515/5 + // Note: the real zone does not matter, as long as it is consistent. + const timezone = moment.tz.guess(); + qs[`filter__${tableMapper.nameToId(triggerField)}__date_after`] = `${timezone}?${startDate}`; + + if (this.getMode() === 'manual') { + qs.size = 1; + } + + const rows = (await baserowApiRequestAllItems.call( + this, + 'GET', + endpoint, + jwtToken, + {}, + qs, + )) as Row[]; + + webhookData.lastTimeChecked = endDate; + + if (Array.isArray(rows) && rows.length) { + rows.forEach((row) => tableMapper.idsToNames(row)); + + if (this.getMode() === 'manual' && rows[0][triggerField] === undefined) { + throw new NodeOperationError(this.getNode(), `The Field "${triggerField}" does not exist.`); + } + + return [this.helpers.returnJsonArray(rows)]; + } + + return null; + } +} diff --git a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts index e4b2d52aed..213a09304f 100644 --- a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts @@ -5,6 +5,7 @@ import type { IHttpRequestMethods, IHttpRequestOptions, ILoadOptionsFunctions, + IPollFunctions, JsonObject, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; @@ -50,7 +51,7 @@ export async function baserowFileUploadRequest( * Make a request to Baserow API. */ export async function baserowApiRequest( - this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, method: IHttpRequestMethods, endpoint: string, jwtToken: string, @@ -98,7 +99,7 @@ export async function baserowApiRequest( * Get all results from a paginated query to Baserow API. */ export async function baserowApiRequestAllItems( - this: IExecuteFunctions, + this: IExecuteFunctions | IPollFunctions, method: IHttpRequestMethods, endpoint: string, jwtToken: string, @@ -109,10 +110,12 @@ export async function baserowApiRequestAllItems( let responseData; qs.page = 1; - qs.size = 100; + if (!qs.size) { + qs.size = 100; + } const returnAll = this.getNodeParameter('returnAll', 0, false); - const limit = this.getNodeParameter('limit', 0, 0); + const limit = this.getNodeParameter('limit', 0, 0) as number; do { responseData = await baserowApiRequest.call(this, method, endpoint, jwtToken, body, qs); @@ -132,7 +135,7 @@ export async function baserowApiRequestAllItems( * Get a JWT token based on Baserow account username and password. */ export async function getJwtToken( - this: IExecuteFunctions | ILoadOptionsFunctions, + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, { username, password, host }: BaserowCredentials, ) { const options: IHttpRequestOptions = { @@ -173,7 +176,7 @@ export async function getFieldNamesAndIds( } export async function getTableFields( - this: IExecuteFunctions, + this: IExecuteFunctions | IPollFunctions, table: string, jwtToken: string, ): Promise { @@ -226,6 +229,10 @@ export class TableFieldMapper { }); } + idToName(id: string) { + return this.idToNameMapping[id] ?? id; + } + namesToIds(obj: Record) { Object.entries(obj).forEach(([key, value]) => { if (this.nameToIdMapping[key] !== undefined) { @@ -234,4 +241,8 @@ export class TableFieldMapper { } }); } + + nameToId(name: string) { + return this.nameToIdMapping[name] ?? name; + } } diff --git a/packages/nodes-base/nodes/Baserow/test/node/trigger/trigger.test.ts b/packages/nodes-base/nodes/Baserow/test/node/trigger/trigger.test.ts new file mode 100644 index 0000000000..06dd243262 --- /dev/null +++ b/packages/nodes-base/nodes/Baserow/test/node/trigger/trigger.test.ts @@ -0,0 +1,247 @@ +import { constructExecutionMetaData, returnJsonArray } from 'n8n-core'; +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + INode, + INodeExecutionData, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { BaserowTrigger } from '../../../BaserowTrigger.node'; +import { baserowApiRequestAllItems, getTableFields } from '../../../GenericFunctions'; +import type { GetAllAdditionalOptions } from '../../../types'; + +jest.mock('../../../GenericFunctions', () => { + const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); + return { + ...originalModule, + baserowFileUploadRequest: jest.fn().mockResolvedValue({ + size: 2147483647, + mime_type: 'string', + is_image: true, + image_width: 32767, + image_height: 32767, + uploaded_at: '2019-08-24T14:15:22Z', + url: 'http://example.com', + thumbnails: { + property1: null, + property2: null, + }, + name: 'string', + original_name: 'string', + }), + baserowApiRequestAllItems: jest.fn(), + getJwtToken: jest.fn().mockResolvedValue('jwt'), + getTableFields: jest.fn(), + }; +}); + +describe('Baserow Trigger Node', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-12-10T12:57:41Z')); + }); + + beforeAll(() => { + nock.disableNetConnect(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + afterAll(() => { + nock.restore(); + jest.unmock('../../../GenericFunctions'); + }); + + describe('trigger', () => { + it('should not trigger when no new record', async () => { + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + httpRequest: jest.fn().mockResolvedValue({ + count: 0, + next: null, + previous: null, + results: [], + }), + }, + getWorkflowStaticData() { + return { + node: {}, + }; + }, + getMode() { + return 'auto'; + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow Trigger', + type: 'n8n-nodes-base.baserowTrigger', + typeVersion: 1, + position: [0, 0], + parameters: { + databaseId: 1, + tableId: 1, + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: {}, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'databaseId': + return 1; + case 'tableId': + return 1; + case 'additionalFields': + return { viewId: 1 }; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + getTableFields.mockResolvedValue([ + { + id: '1', + name: 'my_field_name', + }, + ]); + + baserowApiRequestAllItems.mockResolvedValue([]); + + const node = new BaserowTrigger(); + const response: INodeExecutionData[][] | null = await node.poll.call(mockThis); + + expect(baserowApiRequestAllItems).toHaveBeenCalledTimes(1); + expect(getTableFields).toHaveBeenCalled(); + + expect(response).toBeNull(); + }); + it('should trigger when new record', async () => { + const mockThis = { + helpers: { + returnJsonArray, + constructExecutionMetaData, + httpRequest: jest.fn().mockResolvedValue({ + count: 1, + next: null, + previous: null, + results: [ + { + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }, + ], + }), + }, + getMode() { + return 'auto'; + }, + getWorkflowStaticData() { + return { + node: {}, + }; + }, + getNode() { + return { + id: 'c4a5ca75-18c7-4cc8-bf7d-5d57bb7d84da', + name: 'Baserow Trigger', + type: 'n8n-nodes-base.baserowTrigger', + typeVersion: 1, + position: [0, 0], + parameters: { + databaseId: 1, + tableId: 1, + }, + } as INode; + }, + getCredentials: jest.fn().mockResolvedValue({ + username: 'user', + password: 'password', + host: 'https://my-host.com', + }), + getInputData: () => [ + { + json: {}, + }, + ], + getNodeParameter: (parameter: string) => { + switch (parameter) { + case 'databaseId': + return 1; + case 'tableId': + return 1; + case 'triggerField': + return 'changed_at'; + case 'additionalOptions': + return {} as GetAllAdditionalOptions; + case 'additionalFields': + return { viewId: 1 }; + default: + return undefined; + } + }, + continueOnFail: () => false, + } as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + + baserowApiRequestAllItems.mockResolvedValue([ + { + id: 1, + order: '^-?\\(?:\\.\\)?$', + field_1: 'baz', + }, + ]); + + getTableFields.mockResolvedValue([ + { + id: '1', + name: 'my_field_name', + }, + ]); + + const node = new BaserowTrigger(); + const response: INodeExecutionData[][] | null = await node.poll.call(mockThis); + + expect(getTableFields).toHaveBeenCalledTimes(1); + expect(baserowApiRequestAllItems).toHaveBeenCalledTimes(1); + expect(baserowApiRequestAllItems).toHaveBeenNthCalledWith( + 1, + 'GET', + '/api/database/rows/table/1/', + undefined, + {}, + { filter__changed_at__date_after: 'Africa/Abidjan?2024-12-10T12:57:41Z', view_id: 1 }, + ); + + expect(response).toEqual([ + [ + { + json: { + id: 1, + order: '^-?\\(?:\\.\\)?$', + my_field_name: 'baz', + }, + }, + ], + ]); + }); + }); +}); diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e70bac2fc4..27fb4b65c0 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -427,6 +427,7 @@ "dist/nodes/BambooHr/BambooHr.node.js", "dist/nodes/Bannerbear/Bannerbear.node.js", "dist/nodes/Baserow/Baserow.node.js", + "dist/nodes/Baserow/BaserowTrigger.node.js", "dist/nodes/Beeminder/Beeminder.node.js", "dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Bitly/Bitly.node.js", From eac36f0d94d887204a1012c7292e6558b65de9c0 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 11 Dec 2024 08:11:29 +0000 Subject: [PATCH 10/17] trigger: fix node docs link --- packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json index 37bfc38fa0..3ea6f63829 100644 --- a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json +++ b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json @@ -11,7 +11,7 @@ ], "primaryDocumentation": [ { - "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.baserowtrigger/" + "url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.baserowtrigger/" } ] } From d50b1f6e02a3ee0bbb74ade547db7dbaf3c6ce20 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 11 Dec 2024 08:11:46 +0000 Subject: [PATCH 11/17] chore: cleanup test --- .../Baserow/test/node/trigger/trigger.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/test/node/trigger/trigger.test.ts b/packages/nodes-base/nodes/Baserow/test/node/trigger/trigger.test.ts index 06dd243262..ad48dfa8ca 100644 --- a/packages/nodes-base/nodes/Baserow/test/node/trigger/trigger.test.ts +++ b/packages/nodes-base/nodes/Baserow/test/node/trigger/trigger.test.ts @@ -16,21 +16,6 @@ jest.mock('../../../GenericFunctions', () => { const originalModule: { [key: string]: any } = jest.requireActual('../../../GenericFunctions'); return { ...originalModule, - baserowFileUploadRequest: jest.fn().mockResolvedValue({ - size: 2147483647, - mime_type: 'string', - is_image: true, - image_width: 32767, - image_height: 32767, - uploaded_at: '2019-08-24T14:15:22Z', - url: 'http://example.com', - thumbnails: { - property1: null, - property2: null, - }, - name: 'string', - original_name: 'string', - }), baserowApiRequestAllItems: jest.fn(), getJwtToken: jest.fn().mockResolvedValue('jwt'), getTableFields: jest.fn(), From f3655db0e61289bf2b87aef1a4962e1d8af0b625 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 11 Dec 2024 08:45:17 +0000 Subject: [PATCH 12/17] chore: remove additional fields --- packages/nodes-base/nodes/Baserow/Baserow.node.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.ts b/packages/nodes-base/nodes/Baserow/Baserow.node.ts index 8aedad533e..6b2c0cec21 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.ts +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.ts @@ -8,7 +8,6 @@ import { NodeConnectionType, } from 'n8n-workflow'; -import { returnAllOrLimit } from '@utils/descriptions'; import { baserowApiRequest, baserowFileUploadRequest, @@ -170,7 +169,6 @@ export class Baserow implements INodeType { description: 'The URL of the file to upload', }, ...operationFields, - ...returnAllOrLimit, ], }; @@ -245,6 +243,9 @@ export class Baserow implements INodeType { // https://api.baserow.io/api/redoc/#operation/list_database_table_rows + const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean; + const limit = this.getNodeParameter('limit', 0, 0); + const { order, filters, filterType, search } = this.getNodeParameter( 'additionalOptions', i, @@ -280,6 +281,8 @@ export class Baserow implements INodeType { jwtToken, {}, qs, + returnAll, + limit, )) as Row[]; rows.forEach((row) => mapper.idsToNames(row)); From 092373f87e94826ce24996967437bc66da7376a1 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 11 Dec 2024 08:45:29 +0000 Subject: [PATCH 13/17] chore: remove additional fields --- packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts index c545022563..7ec502ad49 100644 --- a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts +++ b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts @@ -9,8 +9,6 @@ import type { } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; -import { returnAllOrLimit } from '@utils/descriptions'; - import { baserowApiRequest, baserowApiRequestAllItems, @@ -104,7 +102,6 @@ export class BaserowTrigger implements INodeType { }, ], }, - ...returnAllOrLimit, ], }; From 731fbae4673852cfe804de33f3c4f3ce89ebaedd Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 11 Dec 2024 08:46:17 +0000 Subject: [PATCH 14/17] chore: add default parameter values and fix getAll retrieval --- packages/nodes-base/nodes/Baserow/GenericFunctions.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts index 213a09304f..765bcbc7a8 100644 --- a/packages/nodes-base/nodes/Baserow/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Baserow/GenericFunctions.ts @@ -105,6 +105,8 @@ export async function baserowApiRequestAllItems( jwtToken: string, body: IDataObject, qs: IDataObject = {}, + returnAll: boolean = false, + limit: number = 0, ): Promise { const returnData: IDataObject[] = []; let responseData; @@ -114,14 +116,11 @@ export async function baserowApiRequestAllItems( qs.size = 100; } - const returnAll = this.getNodeParameter('returnAll', 0, false); - const limit = this.getNodeParameter('limit', 0, 0) as number; - do { responseData = await baserowApiRequest.call(this, method, endpoint, jwtToken, body, qs); returnData.push(...(responseData.results as IDataObject[])); - if (!returnAll && returnData.length > limit) { + if (!returnAll && limit > 0 && returnData.length > limit) { return returnData.slice(0, limit); } From 98253b7d9d05fa24875de3ac5d8c800325b84c5c Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 11 Dec 2024 17:32:59 +0000 Subject: [PATCH 15/17] fix: allow test events --- packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts index 7ec502ad49..88469a76b5 100644 --- a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts +++ b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts @@ -194,7 +194,9 @@ export class BaserowTrigger implements INodeType { const timezone = moment.tz.guess(); qs[`filter__${tableMapper.nameToId(triggerField)}__date_after`] = `${timezone}?${startDate}`; + // unset filters to allow fetching a test event if (this.getMode() === 'manual') { + delete qs[`filter__${tableMapper.nameToId(triggerField)}__date_after`]; qs.size = 1; } @@ -205,6 +207,8 @@ export class BaserowTrigger implements INodeType { jwtToken, {}, qs, + this.getMode() === 'manual' ? false : undefined, + this.getMode() === 'manual' ? 1 : undefined, )) as Row[]; webhookData.lastTimeChecked = endDate; From 06665ae44b36c3b8846ddf206d929667464e24c2 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Wed, 11 Dec 2024 17:33:11 +0000 Subject: [PATCH 16/17] fix: remove duplicate option --- packages/nodes-base/nodes/Baserow/Baserow.node.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/nodes-base/nodes/Baserow/Baserow.node.ts b/packages/nodes-base/nodes/Baserow/Baserow.node.ts index 6b2c0cec21..d87ff7b959 100644 --- a/packages/nodes-base/nodes/Baserow/Baserow.node.ts +++ b/packages/nodes-base/nodes/Baserow/Baserow.node.ts @@ -154,20 +154,6 @@ export class Baserow implements INodeType { description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG.', }, - { - displayName: 'File URL', - displayOptions: { - show: { - resource: ['file'], - operation: ['upload-via-url'], - }, - }, - name: 'url', - type: 'string', - default: '', - required: true, - description: 'The URL of the file to upload', - }, ...operationFields, ], }; From 426177bf87e05ad46932230c335e2ad9d02ce499 Mon Sep 17 00:00:00 2001 From: Cedric Ziel Date: Tue, 17 Dec 2024 08:55:01 +0100 Subject: [PATCH 17/17] fix: remove trigger node --- .../nodes/Baserow/BaserowTrigger.node.json | 18 -- .../nodes/Baserow/BaserowTrigger.node.ts | 228 ------------------ packages/nodes-base/package.json | 1 - 3 files changed, 247 deletions(-) delete mode 100644 packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json delete mode 100644 packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts diff --git a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json deleted file mode 100644 index 3ea6f63829..0000000000 --- a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "node": "n8n-nodes-base.baserowTrigger", - "nodeVersion": "1.0", - "codexVersion": "1.0", - "categories": ["Data & Storage"], - "resources": { - "credentialDocumentation": [ - { - "url": "https://docs.n8n.io/integrations/builtin/credentials/baserow/" - } - ], - "primaryDocumentation": [ - { - "url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.baserowtrigger/" - } - ] - } -} diff --git a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts b/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts deleted file mode 100644 index 88469a76b5..0000000000 --- a/packages/nodes-base/nodes/Baserow/BaserowTrigger.node.ts +++ /dev/null @@ -1,228 +0,0 @@ -import moment from 'moment-timezone'; -import type { - IPollFunctions, - IDataObject, - INodeExecutionData, - INodeType, - INodeTypeDescription, - ILoadOptionsFunctions, -} from 'n8n-workflow'; -import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; - -import { - baserowApiRequest, - baserowApiRequestAllItems, - getJwtToken, - getTableFields, - TableFieldMapper, - toOptions, -} from './GenericFunctions'; -import type { BaserowCredentials, LoadedResource, Row } from './types'; - -export class BaserowTrigger implements INodeType { - description: INodeTypeDescription = { - displayName: 'Baserow Trigger', - name: 'baserowTrigger', - icon: 'file:baserow.svg', - group: ['trigger'], - version: 1, - description: 'Starts the workflow when Baserow events occur', - subtitle: '={{$parameter["event"]}}', - defaults: { - name: 'Baserow Trigger', - }, - credentials: [ - { - name: 'baserowApi', - required: true, - }, - ], - polling: true, - inputs: [], - outputs: [NodeConnectionType.Main], - properties: [ - { - displayName: 'Database Name or ID', - name: 'databaseId', - type: 'options', - default: '', - required: true, - description: - 'Database to operate on. Choose from the list, or specify an ID using an expression.', - typeOptions: { - loadOptionsMethod: 'getDatabaseIds', - }, - }, - { - displayName: 'Table Name or ID', - name: 'tableId', - type: 'options', - default: '', - required: true, - description: - 'Table to operate on. Choose from the list, or specify an ID using an expression.', - typeOptions: { - loadOptionsDependsOn: ['databaseId'], - loadOptionsMethod: 'getTableIds', - }, - }, - { - displayName: 'Trigger Field', - name: 'triggerField', - type: 'string', - default: '', - description: - 'A Created Time or Last Modified Time field that will be used to sort records. If you do not have a Created Time or Last Modified Time field in your schema, please create one, because without this field trigger will not work correctly.', - required: true, - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - options: [ - { - displayName: 'Fields', - name: 'fields', - type: 'string', - requiresDataPath: 'multiple', - default: '', - // eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-id - description: - 'Fields to be included in the response. Multiple ones can be set separated by comma. Example: name, id. By default all fields will be included.', - }, - { - displayName: 'View ID', - name: 'viewId', - type: 'string', - default: '', - description: - 'The name or ID of a view in the table. If set, only the records in that view will be returned.', - }, - ], - }, - ], - }; - - methods = { - loadOptions: { - async getDatabaseIds(this: ILoadOptionsFunctions) { - const credentials = await this.getCredentials('baserowApi'); - const jwtToken = await getJwtToken.call(this, credentials); - const endpoint = '/api/applications/'; - const databases = (await baserowApiRequest.call( - this, - 'GET', - endpoint, - jwtToken, - )) as LoadedResource[]; - return toOptions(databases); - }, - - async getTableIds(this: ILoadOptionsFunctions) { - const credentials = await this.getCredentials('baserowApi'); - const jwtToken = await getJwtToken.call(this, credentials); - const databaseId = this.getNodeParameter('databaseId', 0) as string; - const endpoint = `/api/database/tables/database/${databaseId}/`; - const tables = (await baserowApiRequest.call( - this, - 'GET', - endpoint, - jwtToken, - )) as LoadedResource[]; - return toOptions(tables); - }, - - async getTableFields(this: ILoadOptionsFunctions) { - const credentials = await this.getCredentials('baserowApi'); - const jwtToken = await getJwtToken.call(this, credentials); - const tableId = this.getNodeParameter('tableId', 0) as string; - const endpoint = `/api/database/fields/table/${tableId}/`; - const fields = (await baserowApiRequest.call( - this, - 'GET', - endpoint, - jwtToken, - )) as LoadedResource[]; - return toOptions(fields); - }, - }, - }; - - async poll(this: IPollFunctions): Promise { - const credentials = await this.getCredentials('baserowApi'); - const jwtToken = await getJwtToken.call(this, credentials); - const additionalFields = this.getNodeParameter('additionalFields') as IDataObject; - const webhookData = this.getWorkflowStaticData('node'); - const tableId = this.getNodeParameter('tableId') as string; - const triggerField = this.getNodeParameter('triggerField') as string; - - const fields = await getTableFields.call(this, tableId, jwtToken); - const tableMapper = new TableFieldMapper(); - tableMapper.createMappings(fields); - - const qs: IDataObject = {}; - - const endpoint = `/api/database/rows/table/${tableId}/`; - - const now = moment().utc().format(); - - const startDate = (webhookData.lastTimeChecked as string) || now; - - const endDate = now; - - if (additionalFields.viewId) { - qs.view_id = additionalFields.viewId; - } - - if (additionalFields.fields) { - const include_fields = (additionalFields.fields as string) - .split(',') - .map((field) => tableMapper.setField(field)) - .join(','); - qs.include = include_fields; - } - - // Constructing datetime filters is unintuitive.. - // First, the date_after filter is deprecated, but still works. - // Second, the datetime needs to be prefixed with the timezone. - // - // Example: "Europe/Amsterdam?2024-10-11 12:13:14" - // see: https://community.baserow.io/t/filtering-on-datetime-fields-does-not-seem-possible/6515/5 - // Note: the real zone does not matter, as long as it is consistent. - const timezone = moment.tz.guess(); - qs[`filter__${tableMapper.nameToId(triggerField)}__date_after`] = `${timezone}?${startDate}`; - - // unset filters to allow fetching a test event - if (this.getMode() === 'manual') { - delete qs[`filter__${tableMapper.nameToId(triggerField)}__date_after`]; - qs.size = 1; - } - - const rows = (await baserowApiRequestAllItems.call( - this, - 'GET', - endpoint, - jwtToken, - {}, - qs, - this.getMode() === 'manual' ? false : undefined, - this.getMode() === 'manual' ? 1 : undefined, - )) as Row[]; - - webhookData.lastTimeChecked = endDate; - - if (Array.isArray(rows) && rows.length) { - rows.forEach((row) => tableMapper.idsToNames(row)); - - if (this.getMode() === 'manual' && rows[0][triggerField] === undefined) { - throw new NodeOperationError(this.getNode(), `The Field "${triggerField}" does not exist.`); - } - - return [this.helpers.returnJsonArray(rows)]; - } - - return null; - } -} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 27fb4b65c0..e70bac2fc4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -427,7 +427,6 @@ "dist/nodes/BambooHr/BambooHr.node.js", "dist/nodes/Bannerbear/Bannerbear.node.js", "dist/nodes/Baserow/Baserow.node.js", - "dist/nodes/Baserow/BaserowTrigger.node.js", "dist/nodes/Beeminder/Beeminder.node.js", "dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Bitly/Bitly.node.js",