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,195 @@
import type { AllEntities, Entity, PropertiesOf } from 'n8n-workflow';
type SeaTableMap = {
row: 'create' | 'get' | 'search' | 'update' | 'remove' | 'lock' | 'unlock' | 'list';
base: 'snapshot' | 'metadata' | 'collaborator';
link: 'add' | 'list' | 'remove';
asset: 'upload' | 'getPublicURL';
};
export type SeaTable = AllEntities<SeaTableMap>;
export type SeaTableRow = Entity<SeaTableMap, 'row'>;
export type SeaTableBase = Entity<SeaTableMap, 'base'>;
export type SeaTableLink = Entity<SeaTableMap, 'link'>;
export type SeaTableAsset = Entity<SeaTableMap, 'asset'>;
export type RowProperties = PropertiesOf<SeaTableRow>;
export type BaseProperties = PropertiesOf<SeaTableBase>;
export type LinkProperties = PropertiesOf<SeaTableLink>;
export type AssetProperties = PropertiesOf<SeaTableAsset>;
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 | number | undefined;
}
export interface IRowObject {
[name: string]: TColumnValue | object;
}
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;
workspace_id: number;
dtable_name: string;
}
export interface ICtx {
base?: IBase;
credentials?: ICredential;
}
// response object of SQL-Query!
export interface IRowResponse {
metadata: [
{
key: string;
name: string;
type: string;
},
];
results: IRow[];
}
// das ist bad
export interface IRowResponse2 {
rows: IRow[];
}
/** neu von mir **/
// response object of SQL-Query!
export interface ISqlQueryResult {
metadata: [
{
key: string;
name: string;
},
];
results: IRow[];
}
// response object of GetMetadata
export interface IGetMetadataResult {
metadata: IDtableMetadata;
}
// response object of GetRows
export interface IGetRowsResult {
rows: IRow[];
}
export interface ICollaboratorsResult {
user_list: ICollaborator[];
}
export interface ICollaborator {
email: string;
name: string;
contact_email: string;
avatar_url?: string;
id_in_org?: string;
}
export interface IColumnDigitalSignature {
username: string;
sign_image_url: string;
sign_time: string;
contact_email?: string;
name: string;
}
export interface IFile {
name: string;
size: number;
type: 'file';
url: string;
path?: string;
}
export interface ILinkData {
table_id: string;
other_table_id: string;
link_id: string;
}
export interface IUploadLink {
upload_link: string;
parent_path: string;
img_relative_path: string;
file_relative_path: string;
}
@@ -0,0 +1,58 @@
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
import * as asset from './asset';
import * as base from './base';
import * as link from './link';
import * as row from './row';
export const versionDescription: INodeTypeDescription = {
displayName: 'SeaTable',
name: 'seaTable',
icon: 'file:seaTable.svg',
group: ['output'],
version: 2,
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',
},
{
name: 'Base',
value: 'base',
},
{
name: 'Link',
value: 'link',
},
{
name: 'Asset',
value: 'asset',
},
],
default: 'row',
},
...row.descriptions,
...base.descriptions,
...link.descriptions,
...asset.descriptions,
],
};
@@ -0,0 +1,48 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
const properties: INodeProperties[] = [
{
displayName: 'Asset Path',
name: 'assetPath',
type: 'string',
placeholder: '/images/2023-09/logo.png',
required: true,
default: '',
},
];
const displayOptions = {
show: {
resource: ['asset'],
operation: ['getPublicURL'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const assetPath = this.getNodeParameter('assetPath', index) as string;
let responseData = [] as IDataObject[];
if (assetPath) {
responseData = await seaTableApiRequest.call(
this,
{},
'GET',
`/api/v2.1/dtable/app-download-link/?path=${assetPath}`,
);
}
return this.helpers.returnJsonArray(responseData);
}
@@ -0,0 +1,37 @@
import type { INodeProperties } from 'n8n-workflow';
import * as getPublicURL from './getPublicURL.operation';
import * as upload from './upload.operation';
export { upload, getPublicURL };
export const descriptions: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['asset'],
},
},
options: [
{
name: 'Public URL',
value: 'getPublicURL',
description: 'Get the public URL from asset path',
action: 'Get the public URL from asset path',
},
{
name: 'Upload',
value: 'upload',
description: 'Add a file/image to an existing row',
action: 'Upload a file or image',
},
],
default: 'upload',
},
...upload.description,
...getPublicURL.description,
];
@@ -0,0 +1,232 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
import type { IUploadLink, IRowObject } from '../Interfaces';
const properties: INodeProperties[] = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify a 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: 'Column Name',
name: 'uploadColumn',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getAssetColumns',
},
required: true,
default: '',
// 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/expressions/">expression</a>',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Row ID',
name: 'rowId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
default: '',
},
{
displayName: 'Property Name',
name: 'dataPropertyName',
type: 'string',
default: 'data',
required: true,
description: 'Name of the binary property which contains the data for the file to be written',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Replace Existing File',
name: 'replace',
type: 'boolean',
default: true,
description:
'Whether to replace the existing asset with the same name (true). Otherwise, a new version with a different name (numeral in parentheses) will be uploaded (false).',
},
{
displayName: 'Append to Column',
name: 'append',
type: 'boolean',
default: true,
description:
'Whether to keep existing files/images in the column and append the new asset (true). Otherwise, the existing files/images are removed from the column (false).',
},
],
},
];
const displayOptions = {
show: {
resource: ['asset'],
operation: ['upload'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const uploadColumn = this.getNodeParameter('uploadColumn', index) as string;
const uploadColumnType = uploadColumn.split(':::')[1];
const uploadColumnName = uploadColumn.split(':::')[0];
const dataPropertyName = this.getNodeParameter('dataPropertyName', index);
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const uploadLink = (await seaTableApiRequest.call(
this,
{},
'GET',
'/api/v2.1/dtable/app-upload-link/',
)) as IUploadLink;
const relativePath =
uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path;
const options = this.getNodeParameter('options', index);
// get server url
const credentials: any = await this.getCredentials('seaTableApi');
const serverURL: string = credentials.domain
? credentials.domain.replace(/\/$/, '')
: 'https://cloud.seatable.io';
// get workspaceId
const workspaceId = (
await this.helpers.httpRequest({
headers: {
Authorization: `Token ${credentials.token}`,
},
url: `${serverURL}/api/v2.1/dtable/app-access-token/`,
json: true,
})
).workspace_id;
// if there are already assets attached to the column
let existingAssetArray = [];
const append = options.append ?? true;
if (append) {
const rowToUpdate = await seaTableApiRequest.call(
this,
{},
'GET',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/' + rowId,
{},
{
table_name: tableName,
convert_keys: true,
},
);
existingAssetArray = rowToUpdate[uploadColumnName] ?? [];
}
// Get the binary data and prepare asset for upload
const fileBufferData = await this.helpers.getBinaryDataBuffer(index, dataPropertyName);
const binaryData = this.helpers.assertBinaryData(index, dataPropertyName);
const requestOptions = {
formData: {
file: {
value: fileBufferData,
options: {
filename: binaryData.fileName,
contentType: binaryData.mimeType,
},
},
parent_dir: uploadLink.parent_path,
replace: options.replace ? '1' : '0',
relative_path: relativePath,
},
};
// Send the upload request
const uploadAsset = await seaTableApiRequest.call(
this,
{},
'POST',
`/seafhttp/upload-api/${uploadLink.upload_link.split('seafhttp/upload-api/')[1]}?ret-json=true`,
{},
{},
'',
requestOptions,
);
// attach the asset to a column in a base
for (let c = 0; c < uploadAsset.length; c++) {
const rowInput = {} as IRowObject;
const filePath = `${serverURL}/workspace/${workspaceId}${uploadLink.parent_path}/${relativePath}/${uploadAsset[c].name}`;
if (uploadColumnType === 'image') {
rowInput[uploadColumnName] = [filePath];
} else if (uploadColumnType === 'file') {
rowInput[uploadColumnName] = uploadAsset;
uploadAsset[c].type = 'file';
uploadAsset[c].url = filePath;
}
// merge with existing assets in this column or with [] and remove duplicates
const mergedArray = existingAssetArray.concat(rowInput[uploadColumnName]);
// Remove duplicates from input, keeping the last one
const uniqueAssets = Array.from(new Set(mergedArray));
// Update the rowInput with the unique assets and store into body.row.
rowInput[uploadColumnName] = uniqueAssets;
const body = {
table_name: tableName,
updates: [
{
row_id: rowId,
row: rowInput,
},
],
} as IDataObject;
// attach assets to table row
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
body,
);
uploadAsset[c].upload_successful = responseData.success;
}
return this.helpers.returnJsonArray(uploadAsset as IDataObject[]);
}
@@ -0,0 +1,54 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
import type { ICollaborator } from '../Interfaces';
export const properties: INodeProperties[] = [
{
displayName: 'Name or email of the collaborator',
name: 'searchString',
type: 'string',
placeholder: 'Enter the name or the email or the collaborator',
required: true,
default: '',
description:
'SeaTable identifies users with a unique username like 244b43hr6fy54bb4afa2c2cb7369d244@auth.local. Get this username from an email or the name of a collaborator.',
},
];
const displayOptions = {
show: {
resource: ['base'],
operation: ['collaborator'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const searchString = this.getNodeParameter('searchString', index) as string;
const collaboratorsResult = await seaTableApiRequest.call(
this,
{},
'GET',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/related-users/',
);
const collaborators = collaboratorsResult.user_list || [];
const data = collaborators.filter(
(col: ICollaborator) =>
col.contact_email.includes(searchString) || col.name.includes(searchString),
);
return this.helpers.returnJsonArray(data as IDataObject[]);
}
@@ -0,0 +1,45 @@
import type { INodeProperties } from 'n8n-workflow';
import * as collaborator from './collaborator.operation';
import * as metadata from './metadata.operation';
import * as snapshot from './snapshot.operation';
export { snapshot, metadata, collaborator };
export const descriptions: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['base'],
},
},
options: [
{
name: 'Snapshot',
value: 'snapshot',
description: 'Create a snapshot of the base',
action: 'Create a snapshot',
},
{
name: 'Metadata',
value: 'metadata',
description: 'Get the complete metadata of the base',
action: 'Get metadata of a base',
},
{
name: 'Collaborator',
value: 'collaborator',
description: 'Get the username from the email or name of a collaborator',
action: 'Get username from email or name',
},
],
default: 'snapshot',
},
...snapshot.description,
...metadata.description,
...collaborator.description,
];
@@ -0,0 +1,30 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
export const properties: INodeProperties[] = [];
const displayOptions = {
show: {
resource: ['base'],
operation: ['metadata'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
const responseData = await seaTableApiRequest.call(
this,
{},
'GET',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/metadata/',
);
return this.helpers.returnJsonArray(responseData.metadata as IDataObject[]);
}
@@ -0,0 +1,32 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
export const properties: INodeProperties[] = [];
const displayOptions = {
show: {
resource: ['base'],
operation: ['snapshot'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/snapshot/',
{ dtable_name: 'snapshot' },
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -0,0 +1,96 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
export const properties: INodeProperties[] = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name (Source)',
name: 'tableName',
type: 'options',
placeholder: 'Name of table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Link Column',
name: 'linkColumn',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getLinkColumns',
},
required: true,
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id".',
},
{
displayName: 'Row ID From the Source Table',
name: 'linkColumnSourceId',
type: 'string',
required: true,
default: '',
description: 'Provide the row ID of table you selected',
},
{
displayName: 'Row ID From the Target',
name: 'linkColumnTargetId',
type: 'string',
required: true,
default: '',
description: 'Provide the row ID of table you want to link',
},
];
const displayOptions = {
show: {
resource: ['link'],
operation: ['add'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const linkColumn = this.getNodeParameter('linkColumn', index) as any;
const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string;
const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string;
const body = {
link_id: linkColumn.split(':::')[1],
table_id: tableName.split(':::')[1],
other_table_id: linkColumn.split(':::')[2],
other_rows_ids_map: {
[linkColumnSourceId]: [linkColumnTargetId],
},
};
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/links/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -0,0 +1,45 @@
import type { INodeProperties } from 'n8n-workflow';
import * as add from './add.operation';
import * as list from './list.operation';
import * as remove from './remove.operation';
export { add, list, remove };
export const descriptions: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['link'],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Create a link between two rows in a link column',
action: 'Add a row link',
},
{
name: 'List',
value: 'list',
description: 'List all links of a specific row',
action: 'List row links',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a link between two rows from a link column',
action: 'Remove a row link',
},
],
default: 'add',
},
...add.description,
...list.description,
...remove.description,
];
@@ -0,0 +1,92 @@
/* eslint-disable n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options */
/* eslint-disable n8n-nodes-base/node-param-description-wrong-for-dynamic-options */
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
export const properties: INodeProperties[] = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
default: '',
description:
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
},
{
displayName: 'Link Column',
name: 'linkColumn',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getLinkColumnsWithColumnKey',
},
required: true,
default: '',
description:
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id:::column_key".',
},
{
displayName: 'Row ID',
name: 'rowId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
default: '',
},
];
const displayOptions = {
show: {
resource: ['link'],
operation: ['list'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
// get parameters
const tableName = this.getNodeParameter('tableName', index) as string;
const linkColumn = this.getNodeParameter('linkColumn', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
// get rows
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/query-links/',
{
table_id: tableName.split(':::')[1],
link_column_key: linkColumn.split(':::')[3],
rows: [
{
row_id: rowId,
offset: 0,
limit: 100,
},
],
},
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -0,0 +1,94 @@
/* eslint-disable n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options */
/* eslint-disable n8n-nodes-base/node-param-description-wrong-for-dynamic-options */
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
export const properties: INodeProperties[] = [
{
displayName: 'Table Name (Source)',
name: 'tableName',
type: 'options',
placeholder: 'Name of table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
default: '',
description:
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
},
{
displayName: 'Link Column',
name: 'linkColumn',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getLinkColumns',
},
required: true,
default: '',
description:
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id".',
},
{
displayName: 'Row ID From the Source Table',
name: 'linkColumnSourceId',
type: 'string',
required: true,
default: '',
description: 'Provide the row ID of table you selected',
},
{
displayName: 'Row ID From the Target Table',
name: 'linkColumnTargetId',
type: 'string',
required: true,
default: '',
description: 'Provide the row ID of table you want to link',
},
];
const displayOptions = {
show: {
resource: ['link'],
operation: ['remove'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const linkColumn = this.getNodeParameter('linkColumn', index) as any;
const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string;
const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string;
const body = {
link_id: linkColumn.split(':::')[1],
table_id: tableName.split(':::')[1],
other_table_id: linkColumn.split(':::')[2],
other_rows_ids_map: {
[linkColumnSourceId]: [linkColumnTargetId],
},
};
const responseData = await seaTableApiRequest.call(
this,
{},
'DELETE',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/links/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -0,0 +1,53 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import * as asset from './asset';
import * as base from './base';
import type { SeaTable } from './Interfaces';
import * as link from './link';
import * as row from './row';
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const operationResult: INodeExecutionData[] = [];
let responseData: IDataObject | IDataObject[] = [];
for (let i = 0; i < items.length; i++) {
const resource = this.getNodeParameter<SeaTable>('resource', i);
const operation = this.getNodeParameter('operation', i);
const seatable = {
resource,
operation,
} as SeaTable;
try {
if (seatable.resource === 'row') {
responseData = await row[seatable.operation].execute.call(this, i);
} else if (seatable.resource === 'base') {
responseData = await base[seatable.operation].execute.call(this, i);
} else if (seatable.resource === 'link') {
responseData = await link[seatable.operation].execute.call(this, i);
} else if (seatable.resource === 'asset') {
responseData = await asset[seatable.operation].execute.call(this, i);
}
const executionData = this.helpers.constructExecutionMetaData(
responseData as INodeExecutionData[],
{
itemData: { item: i },
},
);
operationResult.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
operationResult.push({ json: this.getInputData(i)[0].json, error });
} else {
if (error.context) error.context.itemIndex = i;
throw error;
}
}
}
return [operationResult];
}
@@ -0,0 +1,219 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import {
seaTableApiRequest,
getTableColumns,
split,
rowExport,
updateAble,
splitStringColumnsToArrays,
} from '../../GenericFunctions';
import type { TColumnValue, TColumnsUiValues } from '../../types';
import type { IRowObject } from '../Interfaces';
export const properties: INodeProperties[] = [
{
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',
},
],
default: 'defineBelow',
description: 'Whether to insert the input data this node receives in the new row',
},
{
displayName: 'Apply Column Default Values',
name: 'apply_default',
type: 'boolean',
default: false,
description:
'Whether to use the column default values to populate new rows during creation (only available for normal backend)',
displayOptions: {
show: {
bigdata: [false],
},
},
},
{
displayName:
'In this mode, make sure the incoming data fields are named the same as the columns in SeaTable. (Use an "Edit Fields" node before this node to change them if required.)',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
'/fieldsToSend': ['autoMapInputData'],
},
},
},
{
displayName: 'Inputs to Ignore',
name: 'inputsToIgnore',
type: 'string',
default: '',
description:
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
placeholder: 'Enter properties...',
displayOptions: {
show: {
'/fieldsToSend': ['autoMapInputData'],
},
},
},
{
displayName: 'Columns to Send',
name: 'columnsUi',
placeholder: 'Add Column',
type: 'fixedCollection',
typeOptions: {
multipleValueButtonText: 'Add Column to Send',
multipleValues: true,
},
displayOptions: {
show: {
'/fieldsToSend': ['defineBelow'],
},
},
options: [
{
displayName: 'Column',
name: 'columnValues',
values: [
{
displayName: 'Column Name or ID',
name: 'columnName',
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: ['tableName'],
loadOptionsMethod: 'getTableUpdateAbleColumns',
},
default: '',
},
{
displayName: 'Column Value',
name: 'columnValue',
type: 'string',
default: '',
},
],
},
],
default: {},
description:
'Add destination column with its value. Provide the value in this way. Date: YYYY-MM-DD or YYYY-MM-DD hh:mm. Duration: time in seconds. Checkbox: true, on or 1. Multi-Select: comma-separated list.',
},
{
displayName: 'Save to "Big Data" Backend',
name: 'bigdata',
type: 'boolean',
default: false,
description:
'Whether write to Big Data backend (true) or not (false). True requires the activation of the Big Data backend in the base.',
},
{
displayName:
'Hint: Link, files, images or digital signatures have to be added separately. These column types cannot be set with this node.',
name: 'notice',
type: 'notice',
default: '',
},
];
const displayOptions = {
show: {
resource: ['row'],
operation: ['create'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const tableColumns = await getTableColumns.call(this, tableName);
const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as
| 'defineBelow'
| 'autoMapInputData';
const bigdata = this.getNodeParameter('bigdata', index) as boolean;
const apply_default = this.getNodeParameter('apply_default', index, false) as boolean;
const body = {
table_name: tableName,
rows: {},
} as IDataObject;
let rowInput = {} as IRowObject;
// get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }.
if (fieldsToSend === 'autoMapInputData') {
const items = this.getInputData();
const incomingKeys = Object.keys(items[index].json);
const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string);
for (const key of incomingKeys) {
if (inputDataToIgnore.includes(key)) continue;
rowInput[key] = items[index].json[key] as TColumnValue;
}
} else {
const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues;
for (const column of columns) {
rowInput[column.columnName] = column.columnValue;
}
}
// only keep key:value pairs for columns that are allowed to update.
rowInput = rowExport(rowInput, updateAble(tableColumns));
// string to array: multi-select and collaborators
rowInput = splitStringColumnsToArrays(rowInput, tableColumns);
// save to big data backend
if (bigdata) {
body.rows = [rowInput];
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/add-archived-rows/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
// save to normal backend
else {
body.rows = [rowInput];
if (apply_default) {
body.apply_default = true;
}
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
body,
);
if (responseData.first_row) {
return this.helpers.returnJsonArray(responseData.first_row as IDataObject[]);
}
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
}
@@ -0,0 +1,88 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import {
seaTableApiRequest,
enrichColumns,
simplify_new,
getBaseCollaborators,
} from '../../GenericFunctions';
import type { IRowResponse, IDtableMetadataColumn } from '../Interfaces';
export const properties: INodeProperties[] = [
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
description:
'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Return Column Names',
name: 'convert',
type: 'boolean',
default: true,
description: 'Whether to return the column keys (false) or the column names (true)',
},
],
},
];
const displayOptions = {
show: {
resource: ['row'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
// get parameters
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const options = this.getNodeParameter('options', index);
// get collaborators
const collaborators = await getBaseCollaborators.call(this);
// get rows
const sqlResult = (await seaTableApiRequest.call(
this,
{},
'POST',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/sql/',
{
sql: `SELECT * FROM \`${tableName}\` WHERE _id = '${rowId}'`,
convert_keys: options.convert ?? true,
},
)) as IRowResponse;
const metadata = sqlResult.metadata as IDtableMetadataColumn[];
const rows = sqlResult.results;
// hide columns like button
rows.map((row) => enrichColumns(row, metadata, collaborators));
const simple = options.simple ?? true;
// remove columns starting with _ if simple;
if (simple) {
rows.map((row) => simplify_new(row));
}
return this.helpers.returnJsonArray(rows as IDataObject[]);
}
@@ -0,0 +1,84 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as get from './get.operation';
import * as list from './list.operation';
import * as lock from './lock.operation';
import * as remove from './remove.operation';
import * as search from './search.operation';
import { sharedProperties } from './sharedProperties';
import * as unlock from './unlock.operation';
import * as update from './update.operation';
export { create, get, search, update, remove, lock, unlock, list };
export const descriptions: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['row'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new row',
action: 'Create a row',
},
{
name: 'Delete',
value: 'remove',
description: 'Delete a row',
action: 'Delete a row',
},
{
name: 'Get',
value: 'get',
description: 'Get the content of a row',
action: 'Get a row',
},
{
name: 'Get Many',
value: 'list',
description: 'Get many rows from a table or a table view',
action: 'Get many rows',
},
{
name: 'Lock',
value: 'lock',
description: 'Lock a row to prevent further changes',
action: 'Add a row lock',
},
{
name: 'Search',
value: 'search',
description: 'Search one or multiple rows',
action: 'Search a row by keyword',
},
{
name: 'Unlock',
value: 'unlock',
description: 'Remove the lock from a row',
action: 'Remove a row lock',
},
{
name: 'Update',
value: 'update',
description: 'Update the content of a row',
action: 'Update a row',
},
],
default: 'create',
},
...sharedProperties,
...create.description,
...get.description,
...list.description,
...search.description,
...update.description,
];
@@ -0,0 +1,116 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import {
seaTableApiRequest,
enrichColumns,
simplify_new,
getBaseCollaborators,
} from '../../GenericFunctions';
import type { IRow } from '../Interfaces';
export const properties: INodeProperties[] = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'View Name',
name: 'viewName',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getTableViews',
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'The name of SeaTable view to access, or specify by using an expression. Provide it in the way "col.name:::col.type".',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
description:
'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Return Column Names',
name: 'convert',
type: 'boolean',
default: true,
description: 'Whether to return the column keys (false) or the column names (true)',
},
],
},
];
const displayOptions = {
show: {
resource: ['row'],
operation: ['list'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
// get parameters
const tableName = this.getNodeParameter('tableName', index) as string;
const viewName = this.getNodeParameter('viewName', index) as string;
const options = this.getNodeParameter('options', index);
// get collaborators
const collaborators = await getBaseCollaborators.call(this);
// get rows
const requestMeta = await seaTableApiRequest.call(
this,
{},
'GET',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/metadata/',
);
const requestRows = await seaTableApiRequest.call(
this,
{},
'GET',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
{},
{
table_name: tableName,
view_name: viewName,
limit: 1000,
convert_keys: options.convert ?? true,
},
);
const metadata =
requestMeta.metadata.tables.find((table: { name: string }) => table.name === tableName)
?.columns ?? [];
const rows = requestRows.rows as IRow[];
// hide columns like button
rows.map((row) => enrichColumns(row, metadata, collaborators));
const simple = options.simple ?? true;
// remove columns starting with _ if simple;
if (simple) {
rows.map((row) => simplify_new(row));
}
return this.helpers.returnJsonArray(rows as IDataObject[]);
}
@@ -0,0 +1,24 @@
import type { IDataObject, INodeExecutionData, IExecuteFunctions } from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/lock-rows/',
{
table_name: tableName,
row_ids: [rowId],
},
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -0,0 +1,26 @@
import type { IDataObject, INodeExecutionData, IExecuteFunctions } from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const requestBody: IDataObject = {
table_name: tableName,
row_ids: [rowId],
};
const responseData = await seaTableApiRequest.call(
this,
{},
'DELETE',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
requestBody,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -0,0 +1,139 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import {
seaTableApiRequest,
enrichColumns,
simplify_new,
getBaseCollaborators,
} from '../../GenericFunctions';
import type { IDtableMetadataColumn, IRowResponse } from '../Interfaces';
export const properties: INodeProperties[] = [
{
displayName: 'Column Name or ID',
name: 'searchColumn',
type: 'options',
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getSearchableColumns',
},
required: true,
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Select the column to be searched. Not all column types are supported for search. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
displayName: 'Search Term',
name: 'searchTerm',
type: 'string',
required: true,
default: '',
description: 'What to look for?',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Case Insensitive Search',
name: 'insensitive',
type: 'boolean',
default: false,
description:
'Whether the search ignores case sensitivity (true). Otherwise, it distinguishes between uppercase and lowercase characters.',
},
{
displayName: 'Activate Wildcard Search',
name: 'wildcard',
type: 'boolean',
default: true,
description:
'Whether the search only results perfect matches (true). Otherwise, it finds a row even if the search value is part of a string (false).',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
description:
'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Return Column Names',
name: 'convert',
type: 'boolean',
default: true,
description: 'Whether to return the column keys (false) or the column names (true)',
},
],
},
];
const displayOptions = {
show: {
resource: ['row'],
operation: ['search'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const searchColumn = this.getNodeParameter('searchColumn', index) as string;
const searchTerm = this.getNodeParameter('searchTerm', index) as string | number;
let searchTermString = String(searchTerm);
const options = this.getNodeParameter('options', index);
// get collaborators
const collaborators = await getBaseCollaborators.call(this);
// this is the base query. The WHERE has to be finalized...
let sqlQuery = `SELECT * FROM \`${tableName}\` WHERE \`${searchColumn}\``;
if (options.insensitive) {
searchTermString = searchTermString.toLowerCase();
sqlQuery = `SELECT * FROM \`${tableName}\` WHERE lower(\`${searchColumn}\`)`;
}
const wildcard = options.wildcard ?? true;
if (wildcard) sqlQuery = sqlQuery + ' LIKE "%' + searchTermString + '%"';
else if (!wildcard) sqlQuery = sqlQuery + ' = "' + searchTermString + '"';
const sqlResult = (await seaTableApiRequest.call(
this,
{},
'POST',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/sql',
{
sql: sqlQuery,
convert_keys: options.convert ?? true,
},
)) as IRowResponse;
const metadata = sqlResult.metadata as IDtableMetadataColumn[];
const rows = sqlResult.results;
// hide columns like button
rows.map((row) => enrichColumns(row, metadata, collaborators));
// remove columns starting with _;
if (options.simple) {
rows.map((row) => simplify_new(row));
}
return this.helpers.returnJsonArray(rows as IDataObject[]);
}
@@ -0,0 +1,45 @@
import type { INodeProperties } from 'n8n-workflow';
export const sharedProperties: INodeProperties[] = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
default: '',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
displayOptions: {
show: {
resource: ['row'],
},
},
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Row ID',
name: 'rowId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
default: '',
displayOptions: {
show: {
resource: ['row'],
},
hide: {
operation: ['create', 'list', 'search'],
},
},
},
];
@@ -0,0 +1,24 @@
import type { IDataObject, INodeExecutionData, IExecuteFunctions } from 'n8n-workflow';
import { seaTableApiRequest } from '../../GenericFunctions';
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/unlock-rows/',
{
table_name: tableName,
row_ids: [rowId],
},
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -0,0 +1,173 @@
import {
type IDataObject,
type INodeExecutionData,
type INodeProperties,
type IExecuteFunctions,
updateDisplayOptions,
} from 'n8n-workflow';
import {
seaTableApiRequest,
getTableColumns,
split,
rowExport,
updateAble,
splitStringColumnsToArrays,
} from '../../GenericFunctions';
import type { TColumnsUiValues, TColumnValue } from '../../types';
import type { IRowObject } from '../Interfaces';
export const properties: INodeProperties[] = [
{
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',
},
],
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: {
resource: ['row'],
operation: ['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: [
{
displayName: 'Column Name or ID',
name: 'columnName',
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: ['tableName'],
loadOptionsMethod: 'getTableUpdateAbleColumns',
},
default: '',
},
{
displayName: 'Column Value',
name: 'columnValue',
type: 'string',
default: '',
},
],
},
],
displayOptions: {
show: {
resource: ['row'],
operation: ['update'],
fieldsToSend: ['defineBelow'],
},
},
default: {},
description:
'Add destination column with its value. Provide the value in this way:Date: YYYY-MM-DD or YYYY-MM-DD hh:mmDuration: time in secondsCheckbox: true, on or 1Multi-Select: comma-separated list.',
},
{
displayName: 'Hint: Link, files, images or digital signatures have to be added separately.',
name: 'notice',
type: 'notice',
default: '',
},
];
const displayOptions = {
show: {
resource: ['row'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const tableColumns = await getTableColumns.call(this, tableName);
const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as
| 'defineBelow'
| 'autoMapInputData';
const rowId = this.getNodeParameter('rowId', index) as string;
let rowInput = {} as IRowObject;
// get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }.
if (fieldsToSend === 'autoMapInputData') {
const items = this.getInputData();
const incomingKeys = Object.keys(items[index].json);
const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string);
for (const key of incomingKeys) {
if (inputDataToIgnore.includes(key)) continue;
rowInput[key] = items[index].json[key] as TColumnValue;
}
} else {
const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues;
for (const column of columns) {
rowInput[column.columnName] = column.columnValue;
}
}
// only keep key:value pairs for columns that are allowed to update.
rowInput = rowExport(rowInput, updateAble(tableColumns));
// string to array: multi-select and collaborators
rowInput = splitStringColumnsToArrays(rowInput, tableColumns);
const body = {
table_name: tableName,
updates: [
{
row_id: rowId,
row: rowInput,
},
],
} as IDataObject;
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
@@ -0,0 +1,59 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import { NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
import * as asset from './asset';
import * as base from './base';
import * as link from './link';
import * as row from './row';
export const versionDescription: INodeTypeDescription = {
displayName: 'SeaTable',
name: 'seaTable',
icon: 'file:seaTable.svg',
group: ['output'],
version: 2,
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',
},
{
name: 'Base',
value: 'base',
},
{
name: 'Link',
value: 'link',
},
{
name: 'Asset',
value: 'asset',
},
],
default: 'row',
},
...row.descriptions,
...base.descriptions,
...link.descriptions,
...asset.descriptions,
],
};