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,342 @@
import type {
IDataObject,
IExecuteFunctions,
ILoadOptionsFunctions,
IPollFunctions,
IRequestOptions,
IHttpRequestMethods,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import type {
ICredential,
ICtx,
IDtableMetadataColumn,
IEndpointVariables,
IName,
IRow,
IRowObject,
} from './Interfaces';
import { schema } from './Schema';
import type { TDtableMetadataColumns, TDtableViewColumns, TEndpointVariableName } from './types';
const userBaseUri = (uri?: string) => {
if (uri === undefined) {
return uri;
}
if (uri.endsWith('/')) {
return uri.slice(0, -1);
}
return uri;
};
export function resolveBaseUri(ctx: ICtx) {
return ctx?.credentials?.environment === 'cloudHosted'
? 'https://cloud.seatable.io'
: userBaseUri(ctx?.credentials?.domain);
}
export async function getBaseAccessToken(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
ctx: ICtx,
) {
if (ctx?.base?.access_token !== undefined) {
return;
}
const options: IRequestOptions = {
headers: {
Authorization: `Token ${ctx?.credentials?.token}`,
},
uri: `${resolveBaseUri(ctx)}/api/v2.1/dtable/app-access-token/`,
json: true,
};
ctx.base = await this.helpers.request(options);
}
function endpointCtxExpr(ctx: ICtx, endpoint: string): string {
const endpointVariables: IEndpointVariables = {};
endpointVariables.access_token = ctx?.base?.access_token;
endpointVariables.dtable_uuid = ctx?.base?.dtable_uuid;
return endpoint.replace(
/{{ *(access_token|dtable_uuid|server) *}}/g,
(match: string, name: TEndpointVariableName) => {
return endpointVariables[name] || match;
},
);
}
export async function seaTableApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
ctx: ICtx,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
qs: IDataObject = {},
url: string | undefined = undefined,
option: IDataObject = {},
): Promise<any> {
const credentials = await this.getCredentials('seaTableApi');
ctx.credentials = credentials as unknown as ICredential;
await getBaseAccessToken.call(this, ctx);
const options: IRequestOptions = {
headers: {
Authorization: `Token ${ctx?.base?.access_token}`,
},
method,
qs,
body,
uri: url || `${resolveBaseUri(ctx)}${endpointCtxExpr(ctx, endpoint)}`,
json: true,
};
if (Object.keys(body as IDataObject).length === 0) {
delete options.body;
}
if (Object.keys(option).length !== 0) {
Object.assign(options, option);
}
try {
return await this.helpers.request(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function setableApiRequestAllItems(
this: IExecuteFunctions | IPollFunctions,
ctx: ICtx,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject,
query?: IDataObject,
): Promise<any> {
if (query === undefined) {
query = {};
}
const segment = schema.rowFetchSegmentLimit;
query.start = 0;
query.limit = segment;
const returnData: IDataObject[] = [];
let responseData;
do {
responseData = (await seaTableApiRequest.call(
this,
ctx,
method,
endpoint,
body,
query,
)) as unknown as IRow[];
//@ts-ignore
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
query.start = +query.start + segment;
} while (responseData && responseData.length > segment - 1);
return returnData;
}
export async function getTableColumns(
this: ILoadOptionsFunctions | IExecuteFunctions | IPollFunctions,
tableName: string,
ctx: ICtx = {},
): Promise<TDtableMetadataColumns> {
const {
metadata: { tables },
} = await seaTableApiRequest.call(
this,
ctx,
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
);
for (const table of tables) {
if (table.name === tableName) {
return table.columns;
}
}
return [];
}
export async function getTableViews(
this: ILoadOptionsFunctions | IExecuteFunctions,
tableName: string,
ctx: ICtx = {},
): Promise<TDtableViewColumns> {
const { views } = await seaTableApiRequest.call(
this,
ctx,
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/views',
{},
{ table_name: tableName },
);
return views;
}
export function simplify(data: { results: IRow[] }, metadata: IDataObject) {
return data.results.map((row: IDataObject) => {
for (const key of Object.keys(row)) {
if (!key.startsWith('_')) {
row[metadata[key] as string] = row[key];
delete row[key];
}
}
return row;
});
}
export function getColumns(data: { metadata: [{ key: string; name: string }] }) {
return data.metadata.reduce(
(obj, value) => Object.assign(obj, { [`${value.key}`]: value.name }),
{},
);
}
export function getDownloadableColumns(data: {
metadata: [{ key: string; name: string; type: string }];
}) {
return data.metadata.filter((row) => ['image', 'file'].includes(row.type)).map((row) => row.name);
}
const uniquePredicate = (current: string, index: number, all: string[]) =>
all.indexOf(current) === index;
const nonInternalPredicate = (name: string) => !Object.keys(schema.internalNames).includes(name);
const namePredicate = (name: string) => (named: IName) => named.name === name;
export const nameOfPredicate = (names: readonly IName[]) => (name: string) =>
names.find(namePredicate(name));
const normalize = (subject: string): string => (subject ? subject.normalize() : '');
export const split = (subject: string): string[] =>
normalize(subject)
.split(/\s*((?:[^\\,]*?(?:\\[\s\S])*)*?)\s*(?:,|$)/)
.filter((s) => s.length)
.map((s) => s.replace(/\\([\s\S])/gm, (_, $1) => $1));
export function columnNamesToArray(columnNames: string): string[] {
return columnNames ? split(columnNames).filter(nonInternalPredicate).filter(uniquePredicate) : [];
}
export function columnNamesGlob(
columnNames: string[],
dtableColumns: TDtableMetadataColumns,
): string[] {
const buffer: string[] = [];
const names: string[] = dtableColumns.map((c) => c.name).filter(nonInternalPredicate);
columnNames.forEach((columnName) => {
if (columnName !== '*') {
buffer.push(columnName);
return;
}
buffer.push(...names);
});
return buffer.filter(uniquePredicate);
}
/**
* sequence rows on _seq
*/
export function rowsSequence(rows: IRow[]) {
const l = rows.length;
if (l) {
const [first] = rows;
if (first?._seq !== undefined) {
return;
}
}
for (let i = 0; i < l; ) {
rows[i]._seq = ++i;
}
}
export function rowDeleteInternalColumns(row: IRow): IRow {
Object.keys(schema.internalNames).forEach((columnName) => delete row[columnName]);
return row;
}
function rowFormatColumn(input: unknown): boolean | number | string | string[] | null {
if (null === input || undefined === input) {
return null;
}
if (typeof input === 'boolean' || typeof input === 'number' || typeof input === 'string') {
return input;
}
if (Array.isArray(input) && input.every((i) => typeof i === 'string')) {
return input;
} else if (Array.isArray(input) && input.every((i) => typeof i === 'object')) {
const returnItems = [] as string[];
input.every((i) => returnItems.push(i.display_value as string));
return returnItems;
}
return null;
}
export function rowFormatColumns(row: IRow, columnNames: string[]): IRow {
const outRow = {} as IRow;
columnNames.forEach((c) => (outRow[c] = rowFormatColumn(row[c])));
return outRow;
}
export function rowsFormatColumns(rows: IRow[], columnNames: string[]) {
rows = rows.map((row) => rowFormatColumns(row, columnNames));
}
export function rowMapKeyToName(row: IRow, columns: TDtableMetadataColumns): IRow {
const mappedRow = {} as IRow;
// move internal columns first
Object.keys(schema.internalNames).forEach((key) => {
if (row[key]) {
mappedRow[key] = row[key];
delete row[key];
}
});
// pick each by its key for name
Object.keys(row).forEach((key) => {
const column = columns.find((c) => c.key === key);
if (column) {
mappedRow[column.name] = row[key];
}
});
return mappedRow;
}
export function rowExport(row: IRowObject, columns: TDtableMetadataColumns): IRowObject {
for (const columnName of Object.keys(columns)) {
if (!columns.find(namePredicate(columnName))) {
delete row[columnName];
}
}
return row;
}
export const dtableSchemaIsColumn = (column: IDtableMetadataColumn): boolean =>
!!schema.columnTypes[column.type];
const dtableSchemaIsUpdateAbleColumn = (column: IDtableMetadataColumn): boolean =>
!!schema.columnTypes[column.type] && !schema.nonUpdateAbleColumnTypes[column.type];
export const dtableSchemaColumns = (columns: TDtableMetadataColumns): TDtableMetadataColumns =>
columns.filter(dtableSchemaIsColumn);
export const updateAble = (columns: TDtableMetadataColumns): TDtableMetadataColumns =>
columns.filter(dtableSchemaIsUpdateAbleColumn);
@@ -0,0 +1,101 @@
import type {
TColumnType,
TColumnValue,
TDtableMetadataColumns,
TDtableMetadataTables,
TSeaTableServerEdition,
TSeaTableServerVersion,
} from './types';
export interface IApi {
server: string;
token: string;
appAccessToken?: IAppAccessToken;
info?: IServerInfo;
}
export interface IServerInfo {
version: TSeaTableServerVersion;
edition: TSeaTableServerEdition;
}
export interface IAppAccessToken {
app_name: string;
access_token: string;
dtable_uuid: string;
dtable_server: string;
dtable_socket: string;
workspace_id: number;
dtable_name: string;
}
export interface IDtableMetadataColumn {
key: string;
name: string;
type: TColumnType;
editable: boolean;
}
export interface TDtableViewColumn {
_id: string;
name: string;
}
export interface IDtableMetadataTable {
_id: string;
name: string;
columns: TDtableMetadataColumns;
}
export interface IDtableMetadata {
tables: TDtableMetadataTables;
version: string;
format_version: string;
}
export interface IEndpointVariables {
[name: string]: string | undefined;
}
export interface IRowObject {
[name: string]: TColumnValue;
}
export interface IRow extends IRowObject {
_id: string;
_ctime: string;
_mtime: string;
_seq?: number;
}
export interface IName {
name: string;
}
type TOperation = 'cloudHosted' | 'selfHosted';
export interface ICredential {
token: string;
domain: string;
environment: TOperation;
}
interface IBase {
dtable_uuid: string;
access_token: string;
}
export interface ICtx {
base?: IBase;
credentials?: ICredential;
}
export interface IRowResponse {
metadata: [
{
key: string;
name: string;
},
];
results: IRow[];
}
@@ -0,0 +1,335 @@
import type { INodeProperties } from 'n8n-workflow';
export const rowOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
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: 'Get a row',
action: 'Get a row',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many rows',
action: 'Get many rows',
},
{
name: 'Update',
value: 'update',
description: 'Update a row',
action: 'Update a row',
},
],
default: 'create',
description: 'The operation being performed',
},
];
export const rowFields: INodeProperties[] = [
// ----------------------------------
// shared
// ----------------------------------
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Name of the table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
hide: {
operation: ['get'],
},
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'The name of SeaTable table to access. Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table ID',
name: 'tableId',
type: 'options',
placeholder: 'ID of the table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableIds',
},
displayOptions: {
show: {
operation: ['get'],
},
},
default: '',
description:
'The name of SeaTable table to access. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
// ----------------------------------
// update
// ----------------------------------
{
displayName: 'Row ID',
name: 'rowId',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
},
// ----------------------------------
// create
// ----------------------------------
{
displayName: 'Data to Send',
name: 'fieldsToSend',
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'],
fieldsToSend: ['autoMapInputData'],
},
},
default: '',
description:
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
placeholder: 'Enter properties...',
},
{
displayName: 'Columns to Send',
name: 'columnsUi',
placeholder: 'Add Column',
type: 'fixedCollection',
typeOptions: {
multipleValueButtonText: 'Add Column to Send',
multipleValues: true,
},
options: [
{
displayName: 'Column',
name: 'columnValues',
values: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Column Name',
name: 'columnName',
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['table'],
loadOptionsMethod: 'getTableUpdateAbleColumns',
},
default: '',
},
{
displayName: 'Column Value',
name: 'columnValue',
type: 'string',
default: '',
},
],
},
],
displayOptions: {
show: {
operation: ['create', 'update'],
fieldsToSend: ['defineBelow'],
},
},
default: {},
description: 'Add destination column with its value',
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Row ID',
name: 'rowId',
type: 'string',
displayOptions: {
show: {
operation: ['delete'],
},
},
default: '',
},
// ----------------------------------
// get
// ----------------------------------
{
displayName: 'Row ID',
name: 'rowId',
type: 'string',
displayOptions: {
show: {
operation: ['get'],
},
},
default: '',
},
// ----------------------------------
// getAll
// ----------------------------------
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: ['getAll'],
},
},
default: true,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
},
default: 50,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
operation: ['getAll'],
},
},
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'View Name',
name: 'view_name',
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify an View Name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getViews',
},
default: '',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
operation: ['getAll'],
},
},
options: [
{
displayName: 'Convert Link ID',
name: 'convert_link_id',
type: 'boolean',
default: false,
description:
'Whether the ID of the linked row is returned in the link column (true). Otherwise, it return the name of the linked row (false).',
},
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'Ascending',
value: 'asc',
},
{
name: 'Descending',
value: 'desc',
},
],
default: 'asc',
description: 'The direction of the sort, ascending (asc) or descending (desc)',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Order By Column',
name: 'order_by',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAllSortableColumns',
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify a Column using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
},
],
},
];
@@ -0,0 +1,49 @@
import type { TColumnType, TDateTimeFormat, TInheritColumnKey } from './types';
export type ColumnType = keyof typeof schema.columnTypes;
export const schema = {
rowFetchSegmentLimit: 1000,
dateTimeFormat: 'YYYY-MM-DDTHH:mm:ss.SSSZ',
internalNames: {
_id: 'text',
_creator: 'creator',
_ctime: 'ctime',
_last_modifier: 'last-modifier',
_mtime: 'mtime',
_seq: 'auto-number',
},
columnTypes: {
text: 'Text',
'long-text': 'Long Text',
number: 'Number',
collaborator: 'Collaborator',
date: 'Date',
duration: 'Duration',
'single-select': 'Single Select',
'multiple-select': 'Multiple Select',
email: 'Email',
url: 'URL',
rate: 'Rating',
checkbox: 'Checkbox',
formula: 'Formula',
creator: 'Creator',
ctime: 'Created time',
'last-modifier': 'Last Modifier',
mtime: 'Last modified time',
'auto-number': 'Auto number',
},
nonUpdateAbleColumnTypes: {
creator: 'creator',
ctime: 'ctime',
'last-modifier': 'last-modifier',
mtime: 'mtime',
'auto-number': 'auto-number',
},
} as {
rowFetchSegmentLimit: number;
dateTimeFormat: TDateTimeFormat;
internalNames: { [key in TInheritColumnKey]: ColumnType };
columnTypes: { [key in TColumnType]: string };
nonUpdateAbleColumnTypes: { [key in ColumnType]: ColumnType };
};
@@ -0,0 +1,41 @@
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
import { rowFields, rowOperations } from './RowDescription';
export const versionDescription: INodeTypeDescription = {
displayName: 'SeaTable',
name: 'seaTable',
icon: 'file:seaTable.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Consume the SeaTable API',
defaults: {
name: 'SeaTable',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Row',
value: 'row',
},
],
default: 'row',
},
...rowOperations,
...rowFields,
],
};
@@ -0,0 +1,158 @@
import moment from 'moment-timezone';
import {
type IPollFunctions,
type ILoadOptionsFunctions,
type INodeExecutionData,
type INodePropertyOptions,
type INodeType,
type INodeTypeDescription,
NodeConnectionTypes,
type INodeTypeBaseDescription,
} from 'n8n-workflow';
import { getColumns, rowFormatColumns, seaTableApiRequest, simplify } from './GenericFunctions';
import type { ICtx, IRow, IRowResponse } from './Interfaces';
export class SeaTableTriggerV1 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
version: 1,
subtitle: '={{$parameter["event"]}}',
defaults: {
name: 'SeaTable Trigger',
},
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
polling: true,
inputs: [],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'Table Name or ID',
name: 'tableName',
type: 'options',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
default: '',
description:
'The name of SeaTable table to access. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Event',
name: 'event',
type: 'options',
options: [
{
name: 'Row Created',
value: 'rowCreated',
description: 'Trigger on newly created rows',
},
// {
// name: 'Row Modified',
// value: 'rowModified',
// description: 'Trigger has recently modified rows',
// },
],
default: 'rowCreated',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
description:
'Whether to return a simplified version of the response instead of the raw data',
},
],
};
}
methods = {
loadOptions: {
async getTableNames(this: ILoadOptionsFunctions) {
const returnData: INodePropertyOptions[] = [];
const {
metadata: { tables },
} = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
);
for (const table of tables) {
returnData.push({
name: table.name,
value: table.name,
});
}
return returnData;
},
},
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const webhookData = this.getWorkflowStaticData('node');
const tableName = this.getNodeParameter('tableName') as string;
const simple = this.getNodeParameter('simple') as boolean;
const event = this.getNodeParameter('event') as string;
const ctx: ICtx = {};
const credentials = await this.getCredentials('seaTableApi');
const timezone = (credentials.timezone as string) || 'Europe/Berlin';
const now = moment().utc().format();
const startDate = (webhookData.lastTimeChecked as string) || now;
const endDate = now;
webhookData.lastTimeChecked = endDate;
let rows;
const filterField = event === 'rowCreated' ? '_ctime' : '_mtime';
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/';
if (this.getMode() === 'manual') {
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
sql: `SELECT * FROM ${tableName} LIMIT 1`,
})) as IRowResponse;
} else {
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
sql: `SELECT * FROM ${tableName}
WHERE ${filterField} BETWEEN "${moment(startDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"
AND "${moment(endDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"`,
})) as IRowResponse;
}
let response;
if (rows.metadata && rows.results) {
const columns = getColumns(rows);
if (simple) {
response = simplify(rows, columns);
} else {
response = rows.results;
}
const allColumns = rows.metadata.map((meta) => meta.name);
response = response
//@ts-ignore
.map((row: IRow) => rowFormatColumns(row, allColumns))
.map((row: IRow) => ({ json: row }));
}
if (Array.isArray(response) && response.length) {
return [response];
}
return null;
}
}
@@ -0,0 +1,420 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
INodeTypeBaseDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import {
getTableColumns,
getTableViews,
rowExport,
rowFormatColumns,
rowMapKeyToName,
seaTableApiRequest,
setableApiRequestAllItems,
split,
updateAble,
} from './GenericFunctions';
import type { ICtx, IRow, IRowObject } from './Interfaces';
import { versionDescription } from './SeaTable.node';
import type { TColumnsUiValues, TColumnValue } from './types';
export class SeaTableV1 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = {
loadOptions: {
async getTableNames(this: ILoadOptionsFunctions) {
const returnData: INodePropertyOptions[] = [];
const {
metadata: { tables },
} = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
);
for (const table of tables) {
returnData.push({
name: table.name,
value: table.name,
});
}
return returnData;
},
async getTableIds(this: ILoadOptionsFunctions) {
const returnData: INodePropertyOptions[] = [];
const {
metadata: { tables },
} = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
);
for (const table of tables) {
returnData.push({
name: table.name,
value: table._id,
});
}
return returnData;
},
async getTableUpdateAbleColumns(this: ILoadOptionsFunctions) {
const tableName = this.getNodeParameter('tableName') as string;
const columns = await getTableColumns.call(this, tableName);
return columns
.filter((column) => column.editable)
.map((column) => ({ name: column.name, value: column.name }));
},
async getAllSortableColumns(this: ILoadOptionsFunctions) {
const tableName = this.getNodeParameter('tableName') as string;
const columns = await getTableColumns.call(this, tableName);
return columns
.filter(
(column) =>
!['file', 'image', 'url', 'collaborator', 'long-text'].includes(column.type),
)
.map((column) => ({ name: column.name, value: column.name }));
},
async getViews(this: ILoadOptionsFunctions) {
const tableName = this.getNodeParameter('tableName') as string;
const views = await getTableViews.call(this, tableName);
return views.map((view) => ({ name: view.name, value: view.name }));
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
const body: IDataObject = {};
const qs: IDataObject = {};
const ctx: ICtx = {};
if (resource === 'row') {
if (operation === 'create') {
// ----------------------------------
// row:create
// ----------------------------------
const tableName = this.getNodeParameter('tableName', 0) as string;
const tableColumns = await getTableColumns.call(this, tableName);
body.table_name = tableName;
const fieldsToSend = this.getNodeParameter('fieldsToSend', 0) as
| 'defineBelow'
| 'autoMapInputData';
let rowInput: IRowObject = {};
for (let i = 0; i < items.length; i++) {
rowInput = {} as IRowObject;
try {
if (fieldsToSend === 'autoMapInputData') {
const incomingKeys = Object.keys(items[i].json);
const inputDataToIgnore = split(
this.getNodeParameter('inputsToIgnore', i, '') as string,
);
for (const key of incomingKeys) {
if (inputDataToIgnore.includes(key)) continue;
rowInput[key] = items[i].json[key] as TColumnValue;
}
} else {
const columns = this.getNodeParameter(
'columnsUi.columnValues',
i,
[],
) as TColumnsUiValues;
for (const column of columns) {
rowInput[column.columnName] = column.columnValue;
}
}
body.row = rowExport(rowInput, updateAble(tableColumns));
responseData = await seaTableApiRequest.call(
this,
ctx,
'POST',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
body,
);
const { _id: insertId } = responseData;
if (insertId === undefined) {
throw new NodeOperationError(
this.getNode(),
'SeaTable: No identity after appending row.',
{ itemIndex: i },
);
}
const newRowInsertData = rowMapKeyToName(responseData as IRow, tableColumns);
qs.table_name = tableName;
qs.convert = true;
const newRow = await seaTableApiRequest.call(
this,
ctx,
'GET',
`/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/${encodeURIComponent(
insertId as string,
)}/`,
body,
qs,
);
if (newRow._id === undefined) {
throw new NodeOperationError(
this.getNode(),
'SeaTable: No identity for appended row.',
{ itemIndex: i },
);
}
const row = rowFormatColumns(
{ ...newRowInsertData, ...(newRow as IRow) },
tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']),
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(row),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
} else if (operation === 'get') {
for (let i = 0; i < items.length; i++) {
try {
const tableId = this.getNodeParameter('tableId', 0) as string;
const rowId = this.getNodeParameter('rowId', i) as string;
const response = (await seaTableApiRequest.call(
this,
ctx,
'GET',
`/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/${rowId}`,
{},
{ table_id: tableId, convert: true },
)) as IDataObject;
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
} else if (operation === 'getAll') {
// ----------------------------------
// row:getAll
// ----------------------------------
const tableName = this.getNodeParameter('tableName', 0) as string;
const tableColumns = await getTableColumns.call(this, tableName);
for (let i = 0; i < items.length; i++) {
try {
const endpoint = '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/';
qs.table_name = tableName;
const filters = this.getNodeParameter('filters', i);
const options = this.getNodeParameter('options', i);
const returnAll = this.getNodeParameter('returnAll', 0);
Object.assign(qs, filters, options);
if (qs.convert_link_id === false) {
delete qs.convert_link_id;
}
if (returnAll) {
responseData = await setableApiRequestAllItems.call(
this,
ctx,
'rows',
'GET',
endpoint,
body,
qs,
);
} else {
qs.limit = this.getNodeParameter('limit', 0);
responseData = await seaTableApiRequest.call(this, ctx, 'GET', endpoint, body, qs);
responseData = responseData.rows;
}
const rows = responseData.map((row: IRow) =>
rowFormatColumns(
{ ...row },
tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']),
),
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(rows as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
}
throw error;
}
}
} else if (operation === 'delete') {
for (let i = 0; i < items.length; i++) {
try {
const tableName = this.getNodeParameter('tableName', 0) as string;
const rowId = this.getNodeParameter('rowId', i) as string;
const requestBody: IDataObject = {
table_name: tableName,
row_id: rowId,
};
const response = (await seaTableApiRequest.call(
this,
ctx,
'DELETE',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
requestBody,
qs,
)) as IDataObject;
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
} else if (operation === 'update') {
// ----------------------------------
// row:update
// ----------------------------------
const tableName = this.getNodeParameter('tableName', 0) as string;
const tableColumns = await getTableColumns.call(this, tableName);
body.table_name = tableName;
const fieldsToSend = this.getNodeParameter('fieldsToSend', 0) as
| 'defineBelow'
| 'autoMapInputData';
let rowInput: IRowObject = {};
for (let i = 0; i < items.length; i++) {
const rowId = this.getNodeParameter('rowId', i) as string;
rowInput = {} as IRowObject;
try {
if (fieldsToSend === 'autoMapInputData') {
const incomingKeys = Object.keys(items[i].json);
const inputDataToIgnore = split(
this.getNodeParameter('inputsToIgnore', i, '') as string,
);
for (const key of incomingKeys) {
if (inputDataToIgnore.includes(key)) continue;
rowInput[key] = items[i].json[key] as TColumnValue;
}
} else {
const columns = this.getNodeParameter(
'columnsUi.columnValues',
i,
[],
) as TColumnsUiValues;
for (const column of columns) {
rowInput[column.columnName] = column.columnValue;
}
}
body.row = rowExport(rowInput, updateAble(tableColumns));
body.table_name = tableName;
body.row_id = rowId;
responseData = await seaTableApiRequest.call(
this,
ctx,
'PUT',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
body,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ _id: rowId, ...(responseData as IDataObject) }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
}
}
} else {
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`);
}
}
return [returnData];
}
}
@@ -0,0 +1,42 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
import { rowFields, rowOperations } from './RowDescription';
export const versionDescription: INodeTypeDescription = {
displayName: 'SeaTable',
name: 'seaTable',
icon: 'file:seaTable.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Consume the SeaTable API',
defaults: {
name: 'SeaTable',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Row',
value: 'row',
},
],
default: 'row',
},
...rowOperations,
...rowFields,
],
};
@@ -0,0 +1,85 @@
// ----------------------------------
// sea-table
// ----------------------------------
export type TSeaTableServerVersion = '2.0.6';
export type TSeaTableServerEdition = 'enterprise edition';
// ----------------------------------
// dtable
// ----------------------------------
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import type { IDtableMetadataColumn, IDtableMetadataTable, TDtableViewColumn } from './Interfaces';
export type TInheritColumnTypeTime = 'ctime' | 'mtime';
export type TInheritColumnTypeUser = 'creator' | 'last-modifier';
export type TColumnType =
| 'text'
| 'long-text'
| 'number'
| 'collaborator'
| 'date'
| 'duration'
| 'single-select'
| 'multiple-select'
| 'email'
| 'url'
| 'rate'
| 'checkbox'
| 'formula'
| TInheritColumnTypeTime
| TInheritColumnTypeUser
| 'auto-number';
type TImplementInheritColumnKey = '_seq';
export type TInheritColumnKey =
| '_id'
| '_creator'
| '_ctime'
| '_last_modifier'
| '_mtime'
| TImplementInheritColumnKey;
export type TColumnValue = undefined | boolean | number | string | string[] | null;
export type TColumnKey = TInheritColumnKey | string;
export type TDtableMetadataTables = readonly IDtableMetadataTable[];
export type TDtableMetadataColumns = readonly IDtableMetadataColumn[];
export type TDtableViewColumns = readonly TDtableViewColumn[];
// ----------------------------------
// api
// ----------------------------------
export type TEndpointVariableName = 'access_token' | 'dtable_uuid' | 'server';
// Template Literal Types requires-ts-4.1.5 -- deferred
export type TMethod = 'GET' | 'POST';
type TEndpoint =
| '/api/v2.1/dtable/app-access-token/'
| '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/';
export type TEndpointExpr = TEndpoint;
export type TEndpointResolvedExpr =
TEndpoint; /* deferred: but already in use for header values, e.g. authentication */
export type TDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZ' /* moment.js */;
// ----------------------------------
// node
// ----------------------------------
export type TCredentials = ICredentialDataDecryptedObject | undefined;
export type TTriggerOperation = 'create' | 'update';
export type TOperation = 'append' | 'list' | 'metadata';
export type TLoadedResource = {
name: string;
};
export type TColumnsUiValues = Array<{
columnName: string;
columnValue: string;
}>;