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
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:
@@ -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;
|
||||
}>;
|
||||
Reference in New Issue
Block a user