first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,19 @@
{
"node": "n8n-nodes-base.baserow",
"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.baserow/"
}
],
"generic": []
}
}
@@ -0,0 +1,348 @@
import {
type IExecuteFunctions,
type IDataObject,
type ILoadOptionsFunctions,
type INodeExecutionData,
type INodeType,
type INodeTypeDescription,
NodeConnectionTypes,
} from 'n8n-workflow';
import {
baserowApiRequest,
baserowApiRequestAllItems,
getJwtToken,
TableFieldMapper,
toOptions,
} from './GenericFunctions';
import { operationFields } from './OperationDescription';
import type {
BaserowCredentials,
FieldsUiValues,
GetAllAdditionalOptions,
LoadedResource,
Operation,
Row,
} from './types';
export class Baserow implements INodeType {
description: INodeTypeDescription = {
displayName: 'Baserow',
name: 'baserow',
icon: 'file:baserow.svg',
group: ['output'],
version: 1,
description: 'Consume the Baserow API',
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
defaults: {
name: 'Baserow',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
usableAsTool: true,
credentials: [
{
name: 'baserowApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Row',
value: 'row',
},
],
default: 'row',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['row'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a row',
action: 'Create a row',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a row',
action: 'Delete a row',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve a row',
action: 'Get a row',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve many rows',
action: 'Get many rows',
},
{
name: 'Update',
value: 'update',
description: 'Update a row',
action: 'Update a row',
},
],
default: 'getAll',
},
...operationFields,
],
};
methods = {
loadOptions: {
async getDatabaseIds(this: ILoadOptionsFunctions) {
const credentials = await this.getCredentials<BaserowCredentials>('baserowApi');
const jwtToken = await getJwtToken.call(this, credentials);
const endpoint = '/api/applications/';
const databases = (await baserowApiRequest.call(
this,
'GET',
endpoint,
jwtToken,
)) as LoadedResource[];
// Baserow has different types of applications, we only want the databases
// https://api.baserow.io/api/redoc/#tag/Applications/operation/list_all_applications
return toOptions(databases.filter((database) => database.type === 'database'));
},
async getTableIds(this: ILoadOptionsFunctions) {
const credentials = await this.getCredentials<BaserowCredentials>('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<BaserowCredentials>('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 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 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
// ----------------------------------
// https://api.baserow.io/api/redoc/#operation/list_database_table_rows
const { order, filters, filterType, search } = this.getNodeParameter(
'additionalOptions',
i,
) as GetAllAdditionalOptions;
const qs: IDataObject = {};
if (order?.fields) {
qs.order_by = order.fields
.map(({ field, direction }) => `${direction}${mapper.setField(field)}`)
.join(',');
}
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 (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()) {
returnData.push({ error: error.message, json: {}, itemIndex: i });
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,192 @@
import type {
IDataObject,
IExecuteFunctions,
IHttpRequestMethods,
ILoadOptionsFunctions,
IRequestOptions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import type { Accumulator, BaserowCredentials, LoadedResource } from './types';
/**
* Make a request to Baserow API.
*/
export async function baserowApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
jwtToken: string,
body: IDataObject = {},
qs: IDataObject = {},
) {
const credentials = await this.getCredentials<BaserowCredentials>('baserowApi');
const options: IRequestOptions = {
headers: {
Authorization: `JWT ${jwtToken}`,
},
method,
body,
qs,
uri: `${credentials.host}${endpoint}`,
json: true,
};
if (Object.keys(qs).length === 0) {
delete options.qs;
}
if (Object.keys(body).length === 0) {
delete options.body;
}
try {
return await this.helpers.request(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
/**
* Get all results from a paginated query to Baserow API.
*/
export async function baserowApiRequestAllItems(
this: IExecuteFunctions,
method: IHttpRequestMethods,
endpoint: string,
jwtToken: string,
body: IDataObject,
qs: IDataObject = {},
): 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);
do {
responseData = await baserowApiRequest.call(this, method, endpoint, jwtToken, body, qs);
returnData.push(...(responseData.results as IDataObject[]));
if (!returnAll && returnData.length > limit) {
return returnData.slice(0, limit);
}
qs.page += 1;
} while (responseData.next !== null);
return returnData;
}
/**
* Get a JWT token based on Baserow account username and password.
*/
export async function getJwtToken(
this: IExecuteFunctions | ILoadOptionsFunctions,
{ username, password, host }: BaserowCredentials,
) {
const options: IRequestOptions = {
method: 'POST',
body: {
username,
password,
},
uri: `${host}/api/user/token-auth/`,
json: true,
};
try {
const { token } = (await this.helpers.request(options)) as { token: string };
return token;
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function getFieldNamesAndIds(
this: IExecuteFunctions,
tableId: string,
jwtToken: string,
) {
const endpoint = `/api/database/fields/table/${tableId}/`;
const response = (await baserowApiRequest.call(
this,
'GET',
endpoint,
jwtToken,
)) as LoadedResource[];
return {
names: response.map((field) => field.name),
ids: response.map((field) => `field_${field.id}`),
};
}
export const toOptions = (items: LoadedResource[]) =>
items.map(({ name, id }) => ({ name, value: id }));
/**
* Responsible for mapping field IDs `field_n` to names and vice versa.
*/
export class TableFieldMapper {
nameToIdMapping: Record<string, string> = {};
idToNameMapping: Record<string, string> = {};
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);
}
private createIdToNameMapping(responseData: LoadedResource[]) {
return responseData.reduce<Accumulator>((acc, cur) => {
acc[`field_${cur.id}`] = cur.name;
return acc;
}, {});
}
private createNameToIdMapping(responseData: LoadedResource[]) {
return responseData.reduce<Accumulator>((acc, cur) => {
acc[cur.name] = `field_${cur.id}`;
return acc;
}, {});
}
setField(field: string) {
return this.mapIds ? field : (this.nameToIdMapping[field] ?? field);
}
idsToNames(obj: Record<string, unknown>) {
Object.entries(obj).forEach(([key, value]) => {
if (this.idToNameMapping[key] !== undefined) {
delete obj[key];
obj[this.idToNameMapping[key]] = value;
}
});
}
namesToIds(obj: Record<string, unknown>) {
Object.entries(obj).forEach(([key, value]) => {
if (this.nameToIdMapping[key] !== undefined) {
delete obj[key];
obj[this.nameToIdMapping[key]] = value;
}
});
}
}
@@ -0,0 +1,445 @@
import type { INodeProperties } from 'n8n-workflow';
export const operationFields: INodeProperties[] = [
// ----------------------------------
// shared
// ----------------------------------
{
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 <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
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 <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
typeOptions: {
loadOptionsDependsOn: ['databaseId'],
loadOptionsMethod: 'getTableIds',
},
},
// ----------------------------------
// get
// ----------------------------------
{
displayName: 'Row ID',
name: 'rowId',
type: 'string',
displayOptions: {
show: {
operation: ['get'],
},
},
default: '',
required: true,
description: 'ID of the row to return',
},
// ----------------------------------
// update
// ----------------------------------
{
displayName: 'Row ID',
name: 'rowId',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
required: true,
description: 'ID of the row to update',
},
// ----------------------------------
// create/update
// ----------------------------------
{
displayName: 'Data to Send',
name: 'dataToSend',
type: 'options',
options: [
{
name: 'Auto-Map Input Data to Columns',
value: 'autoMapInputData',
description: 'Use when node input properties match destination column names',
},
{
name: 'Define Below for Each Column',
value: 'defineBelow',
description: 'Set the value for each destination column',
},
],
displayOptions: {
show: {
operation: ['create', 'update'],
},
},
default: 'defineBelow',
description: 'Whether to insert the input data this node receives in the new row',
},
{
displayName: 'Inputs to Ignore',
name: 'inputsToIgnore',
type: 'string',
displayOptions: {
show: {
operation: ['create', 'update'],
dataToSend: ['autoMapInputData'],
},
},
default: '',
description:
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
placeholder: 'Enter properties...',
},
{
displayName: 'Fields to Send',
name: 'fieldsUi',
placeholder: 'Add Field',
type: 'fixedCollection',
typeOptions: {
multipleValueButtonText: 'Add Field to Send',
multipleValues: true,
},
displayOptions: {
show: {
operation: ['create', 'update'],
dataToSend: ['defineBelow'],
},
},
default: {},
options: [
{
displayName: 'Field',
name: 'fieldValues',
values: [
{
displayName: 'Field Name or ID',
name: 'fieldId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['tableId'],
loadOptionsMethod: 'getTableFields',
},
default: '',
},
{
displayName: 'Field Value',
name: 'fieldValue',
type: 'string',
default: '',
},
],
},
],
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Row ID',
name: 'rowId',
type: 'string',
displayOptions: {
show: {
operation: ['delete'],
},
},
default: '',
required: true,
description: 'ID of the row to delete',
},
// ----------------------------------
// getAll
// ----------------------------------
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
operation: ['getAll'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
description: 'Max number of results to return',
typeOptions: {
minValue: 1,
maxValue: 100,
},
displayOptions: {
show: {
operation: ['getAll'],
returnAll: [false],
},
},
},
{
displayName: 'Options',
name: 'additionalOptions',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
operation: ['getAll'],
},
},
options: [
{
displayName: 'Filters',
name: 'filters',
placeholder: 'Add Filter',
description: 'Filter rows based on comparison operators',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'fields',
displayName: 'Field',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
default: '',
description:
'Field to compare. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
typeOptions: {
loadOptionsDependsOn: ['tableId'],
loadOptionsMethod: 'getTableFields',
},
},
{
displayName: 'Filter',
name: 'operator',
description: 'Operator to compare field and value with',
type: 'options',
options: [
{
name: 'Contains',
value: 'contains',
description: 'Field contains value',
},
{
name: 'Contains Not',
value: 'contains_not',
description: 'Field does not contain value',
},
{
name: 'Date After Date',
value: 'date_after',
description: "Field after this date. Format: 'YYYY-MM-DD'.",
},
{
name: 'Date Before Date',
value: 'date_before',
description: "Field before this date. Format: 'YYYY-MM-DD'.",
},
{
name: 'Date Equal',
value: 'date_equal',
description: "Field is date. Format: 'YYYY-MM-DD'.",
},
{
name: 'Date Equals Month',
value: 'date_equals_month',
description: 'Field in this month. Format: string.',
},
{
name: 'Date Equals Today',
value: 'date_equals_today',
description: 'Field is today. Format: string.',
},
{
name: 'Date Equals Year',
value: 'date_equals_year',
description: 'Field in this year. Format: string.',
},
{
name: 'Date Not Equal',
value: 'date_not_equal',
description: "Field is not date. Format: 'YYYY-MM-DD'.",
},
{
name: 'Equal',
value: 'equal',
description: 'Field is equal to value',
},
{
name: 'Filename Contains',
value: 'filename_contains',
description: 'Field filename contains value',
},
{
name: 'Higher Than',
value: 'higher_than',
description: 'Field is higher than value',
},
{
name: 'Is Empty',
value: 'empty',
description: 'Field is empty',
},
{
name: 'Is Not Empty',
value: 'not_empty',
description: 'Field is not empty',
},
{
name: 'Is True',
value: 'boolean',
description: 'Boolean field is true',
},
{
name: 'Link Row Does Not Have',
value: 'link_row_has_not',
description: 'Field does not have link ID',
},
{
name: 'Link Row Has',
value: 'link_row_has',
description: 'Field has link ID',
},
{
name: 'Lower Than',
value: 'lower_than',
description: 'Field is lower than value',
},
{
name: 'Not Equal',
value: 'not_equal',
description: 'Field is not equal to value',
},
{
name: 'Single Select Equal',
value: 'single_select_equal',
description: 'Field selected option is value',
},
{
name: 'Single Select Not Equal',
value: 'single_select_not_equal',
description: 'Field selected option is not value',
},
],
default: 'equal',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to compare to',
},
],
},
],
},
{
displayName: 'Filter Type',
name: 'filterType',
type: 'options',
options: [
{
name: 'AND',
value: 'AND',
description: 'Indicates that the rows must match all the provided filters',
},
{
name: 'OR',
value: 'OR',
description: 'Indicates that the rows only have to match one of the filters',
},
],
default: 'AND',
description:
'This works only if two or more filters are provided. Defaults to <code>AND</code>',
},
{
displayName: 'Search Term',
name: 'search',
type: 'string',
default: '',
description: 'Text to match (can be in any column)',
},
{
displayName: 'Sorting',
name: 'order',
placeholder: 'Add Sort Order',
description: 'Set the sort order of the result rows',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'fields',
displayName: 'Field',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
default: '',
description:
'Field name to sort by. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
typeOptions: {
loadOptionsDependsOn: ['tableId'],
loadOptionsMethod: 'getTableFields',
},
},
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'ASC',
value: '',
description: 'Sort in ascending order',
},
{
name: 'DESC',
value: '-',
description: 'Sort in descending order',
},
],
default: '',
description: 'Sort direction, either ascending or descending',
},
],
},
],
},
],
},
];
@@ -0,0 +1,141 @@
/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */
import { NodeApiError } from 'n8n-workflow';
import {
baserowApiRequest,
baserowApiRequestAllItems,
getJwtToken,
getFieldNamesAndIds,
toOptions,
TableFieldMapper,
} from '../GenericFunctions';
describe('Baserow > GenericFunctions', () => {
const mockExecuteFunctions: any = {
helpers: {
request: jest.fn(),
},
getCredentials: jest.fn().mockResolvedValue({
username: 'nathan@n8n.io',
password: 'this-is-a-fake-password',
host: 'https://api.baserow.io',
}),
getNodeParameter: jest.fn(),
getNode: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('baserowApiRequest', () => {
it('should return data on success', async () => {
mockExecuteFunctions.helpers.request.mockResolvedValue({ success: true });
const result = await baserowApiRequest.call(
mockExecuteFunctions,
'GET',
'/endpoint',
'testJwt',
);
expect(result).toEqual({ success: true });
expect(mockExecuteFunctions.helpers.request).toHaveBeenCalled();
});
it('should throw NodeApiError on failure', async () => {
mockExecuteFunctions.helpers.request.mockRejectedValue({ error: 'fail' });
await expect(
baserowApiRequest.call(mockExecuteFunctions, 'GET', '/endpoint', 'testJwt'),
).rejects.toThrow(NodeApiError);
});
});
describe('baserowApiRequestAllItems', () => {
it('should accumulate all pages', async () => {
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce(true) // returnAll
.mockReturnValue(1000); // limit
mockExecuteFunctions.helpers.request
.mockResolvedValueOnce({ results: [{ data: 1 }], next: 'page2' })
.mockResolvedValueOnce({ results: [{ data: 2 }], next: null });
const result = await baserowApiRequestAllItems.call(
mockExecuteFunctions,
'GET',
'/endpoint',
'testJwt',
{},
{},
);
expect(result).toEqual([{ data: 1 }, { data: 2 }]);
});
});
describe('getJwtToken', () => {
it('should return a token', async () => {
mockExecuteFunctions.helpers.request.mockResolvedValue({ token: 'mockToken' });
const result = await getJwtToken.call(mockExecuteFunctions, {
username: 'nathan@n8n.io',
password: 'this-is-a-fake-password',
host: 'https://api.baserow.io',
});
expect(result).toBe('mockToken');
});
it('should throw NodeApiError if request fails', async () => {
mockExecuteFunctions.helpers.request.mockRejectedValue({ error: 'fail' });
await expect(
getJwtToken.call(mockExecuteFunctions, {
username: 'nathan@n8n.io',
password: 'this-is-a-fake-password',
host: 'https://api.baserow.io',
}),
).rejects.toThrow(NodeApiError);
});
});
describe('getFieldNamesAndIds', () => {
it('should return field names and ids', async () => {
mockExecuteFunctions.helpers.request.mockResolvedValue([
{ id: 1, name: 'field1' },
{ id: 2, name: 'field2' },
]);
const result = await getFieldNamesAndIds.call(mockExecuteFunctions, '1', 'testJwt');
expect(result).toEqual({
names: ['field1', 'field2'],
ids: ['field_1', 'field_2'],
});
});
});
describe('toOptions', () => {
it('should map items to options', () => {
const result = toOptions([
{ id: 1, name: 'field1' },
{ id: 2, name: 'field2' },
]);
expect(result).toEqual([
{ name: 'field1', value: 1 },
{ name: 'field2', value: 2 },
]);
});
});
describe('TableFieldMapper', () => {
it('should create name-to-id and id-to-name mappings', () => {
const mapper = new TableFieldMapper();
mapper.createMappings([
{ id: 1, name: 'field1' },
{ id: 2, name: 'field2' },
]);
expect(mapper.nameToIdMapping).toEqual({
field1: 'field_1',
field2: 'field_2',
});
expect(mapper.idToNameMapping).toEqual({
field_1: 'field1',
field_2: 'field2',
});
});
});
});
@@ -0,0 +1,85 @@
export const fieldsResponse = [
{
id: 3799030,
table_id: 482710,
name: 'Name',
order: 0,
type: 'text',
primary: true,
read_only: false,
immutable_type: false,
immutable_properties: false,
description: null,
text_default: '',
},
{
id: 3799031,
table_id: 482710,
name: 'Notes',
order: 1,
type: 'long_text',
primary: false,
read_only: false,
immutable_type: false,
immutable_properties: false,
description: null,
long_text_enable_rich_text: false,
},
{
id: 3799032,
table_id: 482710,
name: 'Active',
order: 2,
type: 'boolean',
primary: false,
read_only: false,
immutable_type: false,
immutable_properties: false,
description: null,
},
];
export const getResponse = {
id: 1,
order: '1.00000000000000000000',
field_3799030: 'Foo',
field_3799031: 'bar',
field_3799032: false,
};
export const getAllResponse = {
count: 2,
next: null,
previous: null,
results: [
{
id: 1,
order: '1.00000000000000000000',
field_3799030: 'Foo',
field_3799031: 'bar',
field_3799032: false,
},
{
id: 2,
order: '2.00000000000000000000',
field_3799030: 'Bar',
field_3799031: 'foo',
field_3799032: true,
},
],
};
export const createResponse = {
id: 3,
order: '3.00000000000000000000',
field_3799030: 'Nathan',
field_3799031: 'testing',
field_3799032: false,
};
export const updateResponse = {
id: 3,
order: '3.00000000000000000000',
field_3799030: 'Nathan',
field_3799031: 'testing',
field_3799032: true,
};
@@ -0,0 +1,328 @@
{
"name": "Baserow Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-20, 400],
"id": "fccdbab1-aa37-4606-8744-520e19a90a01",
"name": "When clicking Execute workflow"
},
{
"parameters": {
"operation": "get",
"databaseId": 199364,
"tableId": 482710,
"rowId": "1"
},
"type": "n8n-nodes-base.baserow",
"typeVersion": 1,
"position": [200, 0],
"id": "56b90399-9400-4fff-84b5-75430102119d",
"name": "Baserow > Get",
"credentials": {
"baserowApi": {
"id": "SWSFqWDWdnC74WMJ",
"name": "NodeQA"
}
}
},
{
"parameters": {
"databaseId": 199364,
"tableId": 482710,
"limit": 2,
"additionalOptions": {}
},
"type": "n8n-nodes-base.baserow",
"typeVersion": 1,
"position": [200, 200],
"id": "8a183257-3297-4ec9-bcae-aefb1f563355",
"name": "Baserow > Get Many",
"credentials": {
"baserowApi": {
"id": "SWSFqWDWdnC74WMJ",
"name": "NodeQA"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [420, 0],
"id": "5afa3206-a796-41dc-a2c5-54b5b9f2fbf4",
"name": "GetResponse"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [420, 200],
"id": "d61f9e61-856a-4590-aad1-d7ef88e7185b",
"name": "GetMany Response"
},
{
"parameters": {
"operation": "create",
"databaseId": 199364,
"tableId": 482710,
"fieldsUi": {
"fieldValues": [
{
"fieldId": 3799030,
"fieldValue": "Nathan"
},
{
"fieldId": 3799031,
"fieldValue": "testing"
}
]
}
},
"type": "n8n-nodes-base.baserow",
"typeVersion": 1,
"position": [200, 400],
"id": "e3c5b6c4-b4b3-40ac-8d12-2a240c174e81",
"name": "Baserow > Create",
"credentials": {
"baserowApi": {
"id": "SWSFqWDWdnC74WMJ",
"name": "NodeQA"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [420, 400],
"id": "bb7648a4-1d81-421f-927d-0942325db4c9",
"name": "Create Response"
},
{
"parameters": {
"operation": "update",
"databaseId": 199364,
"tableId": 482710,
"rowId": "3",
"fieldsUi": {
"fieldValues": [
{
"fieldId": 3799032,
"fieldValue": "true"
}
]
}
},
"type": "n8n-nodes-base.baserow",
"typeVersion": 1,
"position": [200, 600],
"id": "557a163a-0d60-4dbf-b4e0-342533b56ad5",
"name": "Baserow > Update",
"credentials": {
"baserowApi": {
"id": "SWSFqWDWdnC74WMJ",
"name": "NodeQA"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [420, 600],
"id": "34992c77-25d1-4af4-a667-ca452049908d",
"name": "Update Response"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [420, 800],
"id": "76e75c2a-2c6e-4307-ad67-fbc3d442ea53",
"name": "Delete Response"
},
{
"parameters": {
"operation": "delete",
"databaseId": 199364,
"tableId": 482710,
"rowId": "3"
},
"type": "n8n-nodes-base.baserow",
"typeVersion": 1,
"position": [200, 800],
"id": "f1c84a01-f514-49c2-a54e-548b15dc91fd",
"name": "Baserow > Delete",
"credentials": {
"baserowApi": {
"id": "SWSFqWDWdnC74WMJ",
"name": "NodeQA"
}
}
}
],
"pinData": {
"GetResponse": [
{
"json": {
"id": 1,
"order": "1.00000000000000000000",
"Name": "Foo",
"Notes": "bar",
"Active": false
}
}
],
"GetMany Response": [
{
"json": {
"id": 1,
"order": "1.00000000000000000000",
"Name": "Foo",
"Notes": "bar",
"Active": false
}
},
{
"json": {
"id": 2,
"order": "2.00000000000000000000",
"Name": "Bar",
"Notes": "foo",
"Active": true
}
}
],
"Create Response": [
{
"json": {
"id": 3,
"order": "3.00000000000000000000",
"Name": "Nathan",
"Notes": "testing",
"Active": false
}
}
],
"Update Response": [
{
"json": {
"id": 3,
"order": "3.00000000000000000000",
"Name": "Nathan",
"Notes": "testing",
"Active": true
}
}
],
"Delete Response": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Baserow > Get",
"type": "main",
"index": 0
},
{
"node": "Baserow > Get Many",
"type": "main",
"index": 0
},
{
"node": "Baserow > Create",
"type": "main",
"index": 0
},
{
"node": "Baserow > Update",
"type": "main",
"index": 0
},
{
"node": "Baserow > Delete",
"type": "main",
"index": 0
}
]
]
},
"Baserow > Get": {
"main": [
[
{
"node": "GetResponse",
"type": "main",
"index": 0
}
]
]
},
"Baserow > Get Many": {
"main": [
[
{
"node": "GetMany Response",
"type": "main",
"index": 0
}
]
]
},
"Baserow > Create": {
"main": [
[
{
"node": "Create Response",
"type": "main",
"index": 0
}
]
]
},
"Baserow > Update": {
"main": [
[
{
"node": "Update Response",
"type": "main",
"index": 0
}
]
]
},
"Baserow > Delete": {
"main": [
[
{
"node": "Delete Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "72dbd4c1-80b9-4a22-a298-7bcd577e2f0c",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "0fa937d34dcabeff4bd6480d3b42cc95edf3bc20e6810819086ef1ce2623639d"
},
"id": "2IrLMcqSSFfSyj76",
"tags": []
}
@@ -0,0 +1,59 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import {
createResponse,
fieldsResponse,
getAllResponse,
getResponse,
updateResponse,
} from './apiResponses';
describe('Baserow > Workflows', () => {
const credentials = {
baserowApi: {
host: 'https://api.baserow.io',
username: 'nathan@n8n.io',
password: 'fake-password',
},
};
describe('Run workflow', () => {
beforeAll(() => {
const mock = nock('https://api.baserow.io');
// Baserow > Get Token
mock
.persist()
.post('/api/user/token-auth/', { username: 'nathan@n8n.io', password: 'fake-password' })
.reply(200, {
token: 'fake-jwt-token',
});
// Baserow > Get Fields
mock.get('/api/database/fields/table/482710/').reply(200, fieldsResponse);
// Baserow > Get Row
mock.get('/api/database/rows/table/482710/1/').reply(200, getResponse);
// Baserow > Get all rows
mock
.get('/api/database/rows/table/482710/')
.query({ page: 1, size: 100 })
.reply(200, getAllResponse);
// Baserow > Create Row
mock
.post('/api/database/rows/table/482710/', {
field_3799030: 'Nathan',
field_3799031: 'testing',
})
.reply(200, createResponse);
// Baserow > Update Row
mock
.patch('/api/database/rows/table/482710/3/', {
field_3799032: 'true',
})
.reply(200, updateResponse);
// Baserow > Delete Row
mock.delete('/api/database/rows/table/482710/3/').reply(200, {});
});
new NodeTestHarness().setupTests({ credentials });
});
});
@@ -0,0 +1 @@
<svg id="Warstwa_1" data-name="Warstwa 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600"><defs><style>.cls-1{fill:#4d68c4;}.cls-2{fill:#5190ef;}.cls-3{fill:#2bc3f1;}</style></defs><title>baserow_logo_glyph</title><rect class="cls-1" y="451.65233" width="374.17383" height="148.34767" rx="32.25562" transform="translate(374.17383 1051.65233) rotate(180)"/><path class="cls-2" d="M32.256,225.82617H567.744A32.256,32.256,0,0,1,600,258.08217v83.83567a32.256,32.256,0,0,1-32.256,32.256H32.25562A32.25562,32.25562,0,0,1,0,341.91821v-83.836A32.256,32.256,0,0,1,32.256,225.82617Z" transform="translate(600 600) rotate(180)"/><rect class="cls-3" x="225.82617" width="374.17383" height="148.34767" rx="32.256" transform="translate(825.82617 148.34767) rotate(180)"/><rect class="cls-1" x="451.65233" y="451.65233" width="148.34767" height="148.34767" rx="32.256" transform="translate(1051.65233 1051.65234) rotate(-180)"/><rect class="cls-3" width="148.34767" height="148.34766" rx="32.256"/></svg>

After

Width:  |  Height:  |  Size: 999 B

@@ -0,0 +1,42 @@
export type BaserowCredentials = {
username: string;
password: string;
host: string;
};
export type GetAllAdditionalOptions = {
order?: {
fields: Array<{
field: string;
direction: string;
}>;
};
filters?: {
fields: Array<{
field: string;
operator: string;
value: string;
}>;
};
filterType: string;
search: string;
};
export type LoadedResource = {
id: number;
name: string;
type?: string;
};
export type Accumulator = {
[key: string]: string;
};
export type Row = Record<string, string>;
export type FieldsUiValues = Array<{
fieldId: string;
fieldValue: string;
}>;
export type Operation = 'create' | 'delete' | 'update' | 'get' | 'getAll';