mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
✨ Add SeaTable node and trigger (#2240)
* Add SeaTable node Node for SeaTable, initial credentials, trigger- and standard-node. Contribution-by: SeaTable GmbH <https://seatable.io> Signed-off-by: Tom Klingenberg <tkl@seatable.io> * ⚡ Improvements * ⚡ Improvements * ⚡ Fix node and method names and table parameter * ⚡ Change display name for now again Co-authored-by: Tom Klingenberg <tkl@seatable.io> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
4db91d55dd
commit
a144a8e315
48
packages/nodes-base/credentials/SeaTableApi.credentials.ts
Normal file
48
packages/nodes-base/credentials/SeaTableApi.credentials.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import {
|
||||||
|
ICredentialType,
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class SeaTableApi implements ICredentialType {
|
||||||
|
name = 'seaTableApi';
|
||||||
|
displayName = 'SeaTable API';
|
||||||
|
documentationUrl = 'seaTable';
|
||||||
|
properties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Environment',
|
||||||
|
name: 'environment',
|
||||||
|
type: 'options',
|
||||||
|
default: 'cloudHosted',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Cloud-hosted',
|
||||||
|
value: 'cloudHosted',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Self-hosted',
|
||||||
|
value: 'selfHosted',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Self-hosted domain',
|
||||||
|
name: 'domain',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'https://www.mydomain.com',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
environment: [
|
||||||
|
'selfHosted',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'API Token (of a Base)',
|
||||||
|
name: 'token',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
294
packages/nodes-base/nodes/SeaTable/GenericFunctions.ts
Normal file
294
packages/nodes-base/nodes/SeaTable/GenericFunctions.ts
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
OptionsWithUri,
|
||||||
|
} from 'request';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
IPollFunctions,
|
||||||
|
NodeApiError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TDtableMetadataColumns,
|
||||||
|
TDtableViewColumns,
|
||||||
|
TEndpointResolvedExpr,
|
||||||
|
TEndpointVariableName,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
schema,
|
||||||
|
} from './Schema';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ICredential,
|
||||||
|
ICtx,
|
||||||
|
IDtableMetadataColumn,
|
||||||
|
IEndpointVariables,
|
||||||
|
IName,
|
||||||
|
IRow,
|
||||||
|
IRowObject,
|
||||||
|
} from './Interfaces';
|
||||||
|
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
export async function seaTableApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, ctx: ICtx, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, url: string | undefined = undefined, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||||
|
|
||||||
|
const credentials = await this.getCredentials('seaTableApi');
|
||||||
|
|
||||||
|
ctx.credentials = credentials as unknown as ICredential;
|
||||||
|
|
||||||
|
await getBaseAccessToken.call(this, ctx);
|
||||||
|
|
||||||
|
const options: OptionsWithUri = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Token ${ctx?.base?.access_token}`,
|
||||||
|
},
|
||||||
|
method,
|
||||||
|
qs,
|
||||||
|
body,
|
||||||
|
uri: url || `${resolveBaseUri(ctx)}${endpointCtxExpr(ctx, endpoint)}`,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(body).length === 0) {
|
||||||
|
delete options.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(option).length !== 0) {
|
||||||
|
Object.assign(options, option);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
//@ts-ignore
|
||||||
|
return await this.helpers.request!(options);
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(this.getNode(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setableApiRequestAllItems(this: IExecuteFunctions | IPollFunctions, ctx: ICtx, propertyName: string, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise<any> { // tslint:disable-line:no-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]);
|
||||||
|
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 async function getBaseAccessToken(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, ctx: ICtx) {
|
||||||
|
|
||||||
|
if (ctx?.base?.access_token !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: OptionsWithUri = {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBaseUri(ctx: ICtx) {
|
||||||
|
return (ctx?.credentials?.environment === 'cloudHosted')
|
||||||
|
? 'https://cloud.seatable.io' : ctx?.credentials?.domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: ReadonlyArray<IName>) => (name: string) => names.find(namePredicate(name));
|
||||||
|
|
||||||
|
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 && 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rowsDeleteInternalColumns(rows: IRow[]) {
|
||||||
|
rows = rows.map(rowDeleteInternalColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
function endpointCtxExpr(this: void, 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, expr: string, name: TEndpointVariableName) => {
|
||||||
|
return endpointVariables[name] || match;
|
||||||
|
}) as TEndpointResolvedExpr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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, ($0, $1) => $1))
|
||||||
|
;
|
102
packages/nodes-base/nodes/SeaTable/Interfaces.ts
Normal file
102
packages/nodes-base/nodes/SeaTable/Interfaces.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import {
|
||||||
|
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[];
|
||||||
|
}
|
348
packages/nodes-base/nodes/SeaTable/RowDescription.ts
Normal file
348
packages/nodes-base/nodes/SeaTable/RowDescription.ts
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
import {
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const rowOperations = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Create a row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
value: 'delete',
|
||||||
|
description: 'Delete a row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Get a row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get All',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Get all rows',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 'update',
|
||||||
|
description: 'Update a row',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
description: 'The operation being performed',
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
||||||
|
|
||||||
|
export const rowFields = [
|
||||||
|
// ----------------------------------
|
||||||
|
// shared
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Table Name/ID',
|
||||||
|
name: 'tableName',
|
||||||
|
type: 'options',
|
||||||
|
placeholder: 'Name of table',
|
||||||
|
required: true,
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getTableNames',
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
hide: {
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The name of SeaTable table to access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Table Name/ID',
|
||||||
|
name: 'tableId',
|
||||||
|
type: 'options',
|
||||||
|
placeholder: 'Name of table',
|
||||||
|
required: true,
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getTableIds',
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'get',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The name of SeaTable table to access',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// 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: [
|
||||||
|
{
|
||||||
|
displayName: 'Column Name',
|
||||||
|
name: 'columnName',
|
||||||
|
type: 'options',
|
||||||
|
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,
|
||||||
|
maxValue: 100,
|
||||||
|
},
|
||||||
|
default: 50,
|
||||||
|
description: 'How many results to return',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Filters',
|
||||||
|
name: 'filters',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Filter',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'View Name',
|
||||||
|
name: 'view_name',
|
||||||
|
type: 'options',
|
||||||
|
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 link column in the returned row is the ID of the linked row or the name of the linked row`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Order By',
|
||||||
|
name: 'order_by',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getAllSortableColumns',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `A column's name or ID, use this column to sort the rows`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as INodeProperties[];
|
49
packages/nodes-base/nodes/SeaTable/Schema.ts
Normal file
49
packages/nodes-base/nodes/SeaTable/Schema.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import {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 }
|
||||||
|
};
|
72
packages/nodes-base/nodes/SeaTable/SeaTable.node.json
Normal file
72
packages/nodes-base/nodes/SeaTable/SeaTable.node.json
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.seaTable",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": [
|
||||||
|
"Data & Storage"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"credentialDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/credentials/seaTable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.seaTable/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generic": [
|
||||||
|
{
|
||||||
|
"label": "2021 Goals: Level Up Your Vocabulary With Vonage and n8n",
|
||||||
|
"icon": "🎯",
|
||||||
|
"url": "https://n8n.io/blog/2021-goals-level-up-your-vocabulary-with-vonage-and-n8n/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "2021: The Year to Automate the New You with n8n",
|
||||||
|
"icon": "☀️",
|
||||||
|
"url": "https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "15 Google apps you can combine and automate to increase productivity",
|
||||||
|
"icon": "💡",
|
||||||
|
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Building an expense tracking app in 10 minutes",
|
||||||
|
"icon": "📱",
|
||||||
|
"url": "https://n8n.io/blog/building-an-expense-tracking-app-in-10-minutes/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Why this Product Manager loves workflow automation with n8n",
|
||||||
|
"icon": "🧠",
|
||||||
|
"url": "https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Learn to Build Powerful API Endpoints Using Webhooks",
|
||||||
|
"icon": "🧰",
|
||||||
|
"url": "https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Sending SMS the Low-Code Way with SeaTable, Twilio Programmable SMS, and n8n",
|
||||||
|
"icon": "📱",
|
||||||
|
"url": "https://n8n.io/blog/sending-sms-the-low-code-way-with-seatable-twilio-programmable-sms-and-n8n/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Automating Conference Organization Processes with n8n",
|
||||||
|
"icon": "🙋♀️",
|
||||||
|
"url": "https://n8n.io/blog/automating-conference-organization-processes-with-n8n/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin",
|
||||||
|
"icon": "🎖",
|
||||||
|
"url": "https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "How Goomer automated their operations with over 200 n8n workflows",
|
||||||
|
"icon": "🛵",
|
||||||
|
"url": "https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
319
packages/nodes-base/nodes/SeaTable/SeaTable.node.ts
Normal file
319
packages/nodes-base/nodes/SeaTable/SeaTable.node.ts
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodePropertyOptions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
NodeOperationError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTableColumns,
|
||||||
|
getTableViews,
|
||||||
|
rowExport,
|
||||||
|
rowFormatColumns,
|
||||||
|
rowMapKeyToName,
|
||||||
|
seaTableApiRequest,
|
||||||
|
setableApiRequestAllItems,
|
||||||
|
split,
|
||||||
|
updateAble,
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
rowFields,
|
||||||
|
rowOperations,
|
||||||
|
} from './RowDescription';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TColumnsUiValues,
|
||||||
|
TColumnValue,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ICtx,
|
||||||
|
IRow,
|
||||||
|
IRowObject,
|
||||||
|
} from './Interfaces';
|
||||||
|
|
||||||
|
export class SeaTable implements INodeType {
|
||||||
|
description: 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',
|
||||||
|
color: '#FF8000',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'seaTableApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Row',
|
||||||
|
value: 'row',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'row',
|
||||||
|
description: 'The resource to operate on',
|
||||||
|
},
|
||||||
|
...rowOperations,
|
||||||
|
...rowFields,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
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: IDataObject[] = [];
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
const resource = this.getNodeParameter('resource', 0) as string;
|
||||||
|
const operation = this.getNodeParameter('operation', 0) as string;
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRowInsertData = rowMapKeyToName(responseData, 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)}/`, body, qs);
|
||||||
|
|
||||||
|
if (newRow._id === undefined) {
|
||||||
|
throw new NodeOperationError(this.getNode(), 'SeaTable: No identity for appended row.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = rowFormatColumns({ ...newRowInsertData, ...newRow }, tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']));
|
||||||
|
|
||||||
|
returnData.push(row);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData.push({ error: error.message });
|
||||||
|
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;
|
||||||
|
returnData.push(response);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData.push({ error: error.message });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (operation === 'getAll') {
|
||||||
|
// ----------------------------------
|
||||||
|
// row:getAll
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||||
|
const tableColumns = await getTableColumns.call(this, tableName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const endpoint = `/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/`;
|
||||||
|
qs.table_name = tableName;
|
||||||
|
const filters = this.getNodeParameter('filters', i) as IDataObject;
|
||||||
|
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
|
||||||
|
|
||||||
|
Object.assign(qs, filters, options);
|
||||||
|
|
||||||
|
if (returnAll) {
|
||||||
|
responseData = await setableApiRequestAllItems.call(this, ctx, 'rows', 'GET', endpoint, body, qs);
|
||||||
|
} else {
|
||||||
|
qs.limit = this.getNodeParameter('limit', 0) as number;
|
||||||
|
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'])));
|
||||||
|
|
||||||
|
returnData.push(...rows);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData.push({ error: error.message });
|
||||||
|
}
|
||||||
|
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 body: IDataObject = {
|
||||||
|
table_name: tableName,
|
||||||
|
row_id: rowId,
|
||||||
|
};
|
||||||
|
const response = await seaTableApiRequest.call(this, ctx, 'DELETE', `/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/`, body, qs) as IDataObject;
|
||||||
|
returnData.push(response);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData.push({ error: error.message });
|
||||||
|
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);
|
||||||
|
|
||||||
|
returnData.push(responseData);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData.push({ error: error.message });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [this.helpers.returnJsonArray(returnData)];
|
||||||
|
}
|
||||||
|
}
|
20
packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.json
Normal file
20
packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.seaTableTrigger",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": [
|
||||||
|
"Data & Storage"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"credentialDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/credentials/seaTable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.seaTableTrigger/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
158
packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts
Normal file
158
packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import {
|
||||||
|
IPollFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodePropertyOptions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getColumns,
|
||||||
|
rowFormatColumns,
|
||||||
|
seaTableApiRequest,
|
||||||
|
simplify,
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ICtx,
|
||||||
|
IRow,
|
||||||
|
IRowResponse,
|
||||||
|
} from './Interfaces';
|
||||||
|
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
export class SeaTableTrigger implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'SeaTable Trigger',
|
||||||
|
name: 'seaTableTrigger',
|
||||||
|
icon: 'file:seaTable.svg',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Starts the workflow when SeaTable events occur',
|
||||||
|
subtitle: '={{$parameter["event"]}}',
|
||||||
|
defaults: {
|
||||||
|
name: 'SeaTable Trigger',
|
||||||
|
color: '#FF8000',
|
||||||
|
},
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'seaTableApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
polling: true,
|
||||||
|
inputs: [],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Table',
|
||||||
|
name: 'tableName',
|
||||||
|
type: 'options',
|
||||||
|
required: true,
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getTableNames',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The name of SeaTable table to access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 Response',
|
||||||
|
name: 'simple',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: '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 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).utc().format('YYYY-MM-D HH:mm:ss')}" AND "${moment(endDate).utc().format('YYYY-MM-D HH:mm:ss')}"` }) as IRowResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (rows.metadata && rows.results) {
|
||||||
|
const columns = getColumns(rows);
|
||||||
|
if (simple === true) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
1
packages/nodes-base/nodes/SeaTable/seaTable.svg
Normal file
1
packages/nodes-base/nodes/SeaTable/seaTable.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><path d="M16.787 43.213L28.574 55l16.943-16.942a7.872 7.872 0 000-11.132l-6.22-6.221-.112-.111-18.611 18.57.13.131z" fill="url(#g1)"/><path d="M20.704 39.295l22.51-22.507L31.425 5 14.483 21.942a7.872 7.872 0 000 11.133z" fill="#ff8000"/><defs id="d1"><linearGradient id="g1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="scale(-10.08407) rotate(70.952 .948 -4.065)"><stop offset="0" id="stop905" stop-color="#ff8000" stop-opacity="1"/><stop offset="1" id="stop907" stop-color="#ec2837" stop-opacity="1"/></linearGradient></defs></svg>
|
After Width: | Height: | Size: 629 B |
69
packages/nodes-base/nodes/SeaTable/types.d.ts
vendored
Normal file
69
packages/nodes-base/nodes/SeaTable/types.d.ts
vendored
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// ----------------------------------
|
||||||
|
// sea-table
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
type TSeaTableServerVersion = '2.0.6';
|
||||||
|
type TSeaTableServerEdition = 'enterprise edition';
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dtable
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
import {IDtableMetadataColumn, IDtableMetadataTable, TDtableViewColumn} from './Interfaces';
|
||||||
|
import {ICredentialDataDecryptedObject} from 'n8n-workflow';
|
||||||
|
|
||||||
|
type TInheritColumnTypeTime = 'ctime' | 'mtime';
|
||||||
|
type TInheritColumnTypeUser = 'creator' | 'last-modifier';
|
||||||
|
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';
|
||||||
|
type TInheritColumnKey = '_id' | '_creator' | '_ctime' | '_last_modifier' | '_mtime' | TImplementInheritColumnKey;
|
||||||
|
|
||||||
|
type TColumnValue = undefined | boolean | number | string | string[] | null;
|
||||||
|
type TColumnKey = TInheritColumnKey | string;
|
||||||
|
|
||||||
|
export type TDtableMetadataTables = ReadonlyArray<IDtableMetadataTable>;
|
||||||
|
export type TDtableMetadataColumns = ReadonlyArray<IDtableMetadataColumn>;
|
||||||
|
export type TDtableViewColumns = ReadonlyArray<TDtableViewColumn>;
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// api
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
type TEndpointVariableName = 'access_token' | 'dtable_uuid' | 'server';
|
||||||
|
|
||||||
|
// Template Literal Types requires-ts-4.1.5 -- deferred
|
||||||
|
type TMethod = 'GET' | 'POST';
|
||||||
|
type TDeferredEndpoint = string;
|
||||||
|
type TDeferredEndpointExpr = string;
|
||||||
|
type TEndpoint =
|
||||||
|
'/api/v2.1/dtable/app-access-token/'
|
||||||
|
| '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/'
|
||||||
|
| TDeferredEndpoint;
|
||||||
|
type TEndpointExpr = TEndpoint | TDeferredEndpointExpr;
|
||||||
|
type TEndpointResolvedExpr = TEndpoint | string; /* deferred: but already in use for header values, e.g. authentication */
|
||||||
|
|
||||||
|
type TDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZ' /* moment.js */;
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// node
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
type TCredentials = ICredentialDataDecryptedObject | undefined;
|
||||||
|
|
||||||
|
type TTriggerOperation = 'create' | 'update';
|
||||||
|
|
||||||
|
type TOperation = 'append' | 'list' | 'metadata';
|
||||||
|
|
||||||
|
type TLoadedResource = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
export type TColumnsUiValues = Array<{
|
||||||
|
columnName: string;
|
||||||
|
columnValue: string;
|
||||||
|
}>;
|
|
@ -230,6 +230,7 @@
|
||||||
"dist/credentials/SalesforceJwtApi.credentials.js",
|
"dist/credentials/SalesforceJwtApi.credentials.js",
|
||||||
"dist/credentials/SalesforceOAuth2Api.credentials.js",
|
"dist/credentials/SalesforceOAuth2Api.credentials.js",
|
||||||
"dist/credentials/SalesmateApi.credentials.js",
|
"dist/credentials/SalesmateApi.credentials.js",
|
||||||
|
"dist/credentials/SeaTableApi.credentials.js",
|
||||||
"dist/credentials/SecurityScorecardApi.credentials.js",
|
"dist/credentials/SecurityScorecardApi.credentials.js",
|
||||||
"dist/credentials/SegmentApi.credentials.js",
|
"dist/credentials/SegmentApi.credentials.js",
|
||||||
"dist/credentials/SendGridApi.credentials.js",
|
"dist/credentials/SendGridApi.credentials.js",
|
||||||
|
@ -548,6 +549,8 @@
|
||||||
"dist/nodes/Rundeck/Rundeck.node.js",
|
"dist/nodes/Rundeck/Rundeck.node.js",
|
||||||
"dist/nodes/S3/S3.node.js",
|
"dist/nodes/S3/S3.node.js",
|
||||||
"dist/nodes/Salesforce/Salesforce.node.js",
|
"dist/nodes/Salesforce/Salesforce.node.js",
|
||||||
|
"dist/nodes/SeaTable/SeaTable.node.js",
|
||||||
|
"dist/nodes/SeaTable/SeaTableTrigger.node.js",
|
||||||
"dist/nodes/SecurityScorecard/SecurityScorecard.node.js",
|
"dist/nodes/SecurityScorecard/SecurityScorecard.node.js",
|
||||||
"dist/nodes/Set.node.js",
|
"dist/nodes/Set.node.js",
|
||||||
"dist/nodes/SentryIo/SentryIo.node.js",
|
"dist/nodes/SentryIo/SentryIo.node.js",
|
||||||
|
|
Loading…
Reference in a new issue