This commit is contained in:
Cedric Ziel 2025-03-05 17:13:38 +01:00 committed by GitHub
commit 386c324903
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1765 additions and 179 deletions

View file

@ -10,18 +10,22 @@ import {
import {
baserowApiRequest,
baserowFileUploadRequest,
baserowApiRequestAllItems,
getJwtToken,
TableFieldMapper,
toOptions,
getTableFields,
} from './GenericFunctions';
import { operationFields } from './OperationDescription';
import type {
BaserowCredentials,
FieldsUiValues,
FileOperation,
GetAllAdditionalOptions,
LoadedResource,
Operation,
Resource,
RowOperation,
Row,
} from './types';
@ -53,6 +57,10 @@ export class Baserow implements INodeType {
type: 'options',
noDataExpression: true,
options: [
{
name: 'File',
value: 'file',
},
{
name: 'Row',
value: 'row',
@ -104,6 +112,48 @@ 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',
},
{
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.',
},
...operationFields,
],
};
@ -155,182 +205,250 @@ export class Baserow implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const mapper = new TableFieldMapper();
const returnData: INodeExecutionData[] = [];
const operation = this.getNodeParameter('operation', 0) as Operation;
const tableId = this.getNodeParameter('tableId', 0) as string;
const returnData: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0) as Resource;
const operation = this.getNodeParameter('operation', 0) as RowOperation | FileOperation;
const credentials = await this.getCredentials<BaserowCredentials>('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 (operation === 'getAll') {
// ----------------------------------
// getAll
// ----------------------------------
if (resource === 'row') {
const tableId = this.getNodeParameter('tableId', 0) as string;
const fields = await getTableFields.call(this, tableId, jwtToken);
// https://api.baserow.io/api/redoc/#operation/list_database_table_rows
const mapper = new TableFieldMapper();
mapper.createMappings(fields);
const { order, filters, filterType, search } = this.getNodeParameter(
'additionalOptions',
i,
) as GetAllAdditionalOptions;
if (operation === 'getAll') {
// ----------------------------------
// getAll
// ----------------------------------
const qs: IDataObject = {};
// https://api.baserow.io/api/redoc/#operation/list_database_table_rows
if (order?.fields) {
qs.order_by = order.fields
.map(({ field, direction }) => `${direction}${mapper.setField(field)}`)
.join(',');
}
const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean;
const limit = this.getNodeParameter('limit', 0, 0);
if (filters?.fields) {
filters.fields.forEach(({ field, operator, value }) => {
qs[`filter__field_${mapper.setField(field)}__${operator}`] = value;
});
}
const { order, filters, filterType, search } = this.getNodeParameter(
'additionalOptions',
i,
) as GetAllAdditionalOptions;
if (filterType) {
qs.filter_type = filterType;
}
const qs: IDataObject = {};
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);
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,
returnAll,
limit,
)) 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 (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 (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);
}
} catch (error) {
if (this.continueOnFail()) {

View file

@ -1,20 +1,57 @@
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
IRequestOptions,
IPollFunctions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import type { Accumulator, BaserowCredentials, LoadedResource } from './types';
export async function baserowFileUploadRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
jwtToken: string,
file: Buffer,
fileName: string,
mimeType: string,
) {
const credentials = await this.getCredentials<BaserowCredentials>('baserowApi');
const options: IHttpRequestOptions = {
headers: {
Authorization: `JWT ${jwtToken}`,
'Content-Type': 'multipart/form-data',
},
method: 'POST',
url: `${credentials.host}/api/user-files/upload-file/`,
json: false,
body: {
file: {
value: file,
options: {
filename: fileName,
contentType: mimeType,
},
},
},
};
try {
return await this.helpers.httpRequest(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
/**
* Make a request to Baserow API.
*/
export async function baserowApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
endpoint: string,
jwtToken: string,
@ -23,17 +60,26 @@ export async function baserowApiRequest(
) {
const credentials = await this.getCredentials<BaserowCredentials>('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;
options.headers = {
...options.headers,
'Content-Type': 'multipart/form-data',
};
options.returnFullResponse = true;
}
if (Object.keys(qs).length === 0) {
delete options.qs;
}
@ -43,7 +89,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);
}
@ -53,27 +99,28 @@ 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,
body: IDataObject,
qs: IDataObject = {},
returnAll: boolean = false,
limit: number = 0,
): Promise<IDataObject[]> {
const returnData: IDataObject[] = [];
let responseData;
qs.page = 1;
qs.size = 100;
const returnAll = this.getNodeParameter('returnAll', 0, false);
const limit = this.getNodeParameter('limit', 0, 0);
if (!qs.size) {
qs.size = 100;
}
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);
}
@ -87,21 +134,21 @@ 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: IRequestOptions = {
const options: IHttpRequestOptions = {
method: 'POST',
body: {
username,
password,
},
uri: `${host}/api/user/token-auth/`,
url: `${host}/api/user/token-auth/`,
json: true,
};
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);
@ -127,6 +174,15 @@ export async function getFieldNamesAndIds(
};
}
export async function getTableFields(
this: IExecuteFunctions | IPollFunctions,
table: string,
jwtToken: string,
): Promise<LoadedResource[]> {
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 }));
@ -140,15 +196,6 @@ export class TableFieldMapper {
mapIds = true;
async getTableFields(
this: IExecuteFunctions,
table: string,
jwtToken: string,
): Promise<LoadedResource[]> {
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);
@ -181,6 +228,10 @@ export class TableFieldMapper {
});
}
idToName(id: string) {
return this.idToNameMapping[id] ?? id;
}
namesToIds(obj: Record<string, unknown>) {
Object.entries(obj).forEach(([key, value]) => {
if (this.nameToIdMapping[key] !== undefined) {
@ -189,4 +240,8 @@ export class TableFieldMapper {
}
});
}
nameToId(name: string) {
return this.nameToIdMapping[name] ?? name;
}
}

View file

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

View file

@ -0,0 +1,173 @@
import type {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IN8nHttpFullResponse,
IN8nHttpResponse,
INode,
} from 'n8n-workflow';
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',
name: 'Baserow Upload',
type: 'n8n-nodes-base.Baserow',
typeVersion: 1,
position: [0, 0],
parameters: {
operation: 'upload',
domain: 'file',
},
};
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: {
requestWithAuthentication: jest.fn().mockResolvedValue({ statusCode: 200 }),
httpRequest: jest.fn(),
},
getNode() {
return node;
},
getCredentials: jest.fn(),
getNodeParameter: jest.fn(),
} as unknown as 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: {
requestWithAuthentication: jest.fn().mockResolvedValue({ statusCode: 200 }),
httpRequest: jest.fn(),
},
getNode() {
return node;
},
getCredentials: jest.fn(),
getNodeParameter: jest.fn(),
} as unknown as 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/',
});
});
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,232 @@
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,
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',
},
},
],
]);
});
});
});

View file

@ -35,7 +35,9 @@ export type Row = Record<string, string>;
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';