Merge branch 'seatable_node_rework'

This commit is contained in:
Christoph Dyllick-Brenzinger 2024-06-25 11:01:27 +02:00
commit 8e0770485f
78 changed files with 4077 additions and 565 deletions

View file

@ -1,24 +1,10 @@
import type { ICredentialType, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
import moment from 'moment-timezone';
// Get options for timezones
const timezones: INodePropertyOptions[] = moment.tz
.countries()
.reduce((tz: INodePropertyOptions[], country: string) => {
const zonesForCountry = moment.tz
.zonesForCountry(country)
.map((zone) => ({ value: zone, name: zone }));
return tz.concat(zonesForCountry);
}, []);
import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
export class SeaTableApi implements ICredentialType {
name = 'seaTableApi';
displayName = 'SeaTable API';
documentationUrl = 'seaTable';
documentationUrl =
'https://seatable.io/docs/n8n-integration/erstellen-eines-api-tokens-fuer-n8n/?lang=auto';
properties: INodeProperties[] = [
{
displayName: 'Environment',
@ -41,7 +27,7 @@ export class SeaTableApi implements ICredentialType {
name: 'domain',
type: 'string',
default: '',
placeholder: 'https://www.mydomain.com',
placeholder: 'https://seatable.example.com',
displayOptions: {
show: {
environment: ['selfHosted'],
@ -52,16 +38,20 @@ export class SeaTableApi implements ICredentialType {
displayName: 'API Token (of a Base)',
name: 'token',
type: 'string',
description:
'The API-Token of the SeaTable base you would like to use with n8n. n8n can only connect to one base a at a time.',
typeOptions: { password: true },
default: '',
},
{
displayName: 'Timezone',
name: 'timezone',
type: 'options',
default: '',
description: "Seatable server's timezone",
options: [...timezones],
},
];
test: ICredentialTestRequest = {
request: {
baseURL: '={{$credentials?.domain || "https://cloud.seatable.io" }}',
url: '/api/v2.1/dtable/app-access-token/',
headers: {
Authorization: '={{"Token " + $credentials.token}}',
},
},
};
}

View file

@ -1,451 +1,26 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
import { VersionedNodeType } from 'n8n-workflow';
import {
getTableColumns,
getTableViews,
rowExport,
rowFormatColumns,
rowMapKeyToName,
seaTableApiRequest,
setableApiRequestAllItems,
split,
updateAble,
} from './GenericFunctions';
import { SeaTableV1 } from './v1/SeaTableV1.node';
import { SeaTableV2 } from './v2/SeaTableV2.node';
import { rowFields, rowOperations } from './RowDescription';
export class SeaTable extends VersionedNodeType {
constructor() {
const baseDescription: INodeTypeBaseDescription = {
displayName: 'SeaTable',
name: 'seaTable',
icon: 'file:seatable.svg',
group: ['output'],
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Read, update, write and delete data from SeaTable',
defaultVersion: 2,
};
import type { TColumnsUiValues, TColumnValue } from './types';
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new SeaTableV1(baseDescription),
2: new SeaTableV2(baseDescription),
};
import type { 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',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Row',
value: 'row',
},
],
default: 'row',
},
...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: 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(error)) {
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(error)) {
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(error)) {
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(error)) {
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(error)) {
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];
super(nodeVersions, baseDescription);
}
}

View file

@ -1,22 +1,33 @@
import type {
IPollFunctions,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import moment from 'moment-timezone';
import { getColumns, rowFormatColumns, seaTableApiRequest, simplify } from './GenericFunctions';
import type { ICtx, IRow, IRowResponse } from './Interfaces';
import { seaTableApiRequest, simplify_new, enrichColumns } from './v2/GenericFunctions';
import type {
ICtx,
IRow,
IRowResponse,
IGetMetadataResult,
IGetRowsResult,
IDtableMetadataColumn,
ICollaborator,
ICollaboratorsResult,
IColumnDigitalSignature,
} from './v2/actions/Interfaces';
import { loadOptions } from './v2/methods';
export class SeaTableTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'SeaTable Trigger',
name: 'seaTableTrigger',
icon: 'file:seaTable.svg',
icon: 'file:seatable.svg',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when SeaTable events occur',
@ -35,7 +46,31 @@ export class SeaTableTrigger implements INodeType {
outputs: ['main'],
properties: [
{
displayName: 'Table Name or ID',
displayName: 'Event',
name: 'event',
type: 'options',
options: [
{
name: 'New Row',
value: 'newRow',
description: 'Trigger on newly created rows',
},
{
name: 'New or Updated Row',
value: 'updatedRow',
description: 'Trigger has recently created or modified rows',
},
{
name: 'New Signature',
value: 'newAsset',
description: 'Trigger on new signatures',
},
],
default: 'newRow',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
required: true,
@ -43,26 +78,48 @@ export class SeaTableTrigger implements INodeType {
loadOptionsMethod: 'getTableNames',
},
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 an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
'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>.',
},
{
displayName: 'Event',
name: 'event',
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'View Name',
name: 'viewName',
type: 'options',
options: [
{
name: 'Row Created',
value: 'rowCreated',
description: 'Trigger on newly created rows',
displayOptions: {
show: {
event: ['newRow', 'updatedRow'],
},
// {
// name: 'Row Modified',
// value: 'rowModified',
// description: 'Trigger has recently modified rows',
// },
],
default: 'rowCreated',
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getTableViews',
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'The name of SeaTable view to access. 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: 'Signature Column',
name: 'assetColumn',
type: 'options',
required: true,
displayOptions: {
show: {
event: ['newAsset'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getSignatureColumns',
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Select the digital-signature column that should be tracked. Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
displayName: 'Simplify',
@ -72,84 +129,139 @@ export class SeaTableTrigger implements INodeType {
description:
'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: '"Fetch Test Event" returns max. three items of the last hour.',
name: 'notice',
type: 'notice',
default: '',
},
],
};
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;
},
},
};
methods = { loadOptions };
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 tableName = this.getNodeParameter('tableName') as string;
const viewName = (event !== 'newAsset' ? this.getNodeParameter('viewName') : '') as string;
const assetColumn = (
event === 'newAsset' ? this.getNodeParameter('assetColumn') : ''
) as string;
const simple = this.getNodeParameter('simple') as boolean;
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;
const startDate =
this.getMode() === 'manual'
? moment().utc().subtract(1, 'h').format()
: (webhookData.lastTimeChecked as string);
const endDate = (webhookData.lastTimeChecked = moment().utc().format());
let rows;
const filterField = event === 'newRow' ? '_ctime' : '_mtime';
const filterField = event === 'rowCreated' ? '_ctime' : '_mtime';
let requestMeta: IGetMetadataResult;
let requestRows: IGetRowsResult;
let metadata: IDtableMetadataColumn[] = [];
let rows: IRow[];
let sqlResult: IRowResponse;
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/';
const limit = this.getMode() === 'manual' ? 3 : 1000;
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;
// New Signature
if (event === 'newAsset') {
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/';
sqlResult = await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
sql: `SELECT _id, _ctime, _mtime, \`${assetColumn}\` FROM ${tableName} WHERE \`${assetColumn}\` IS NOT NULL ORDER BY _mtime DESC LIMIT ${limit}`,
convert_keys: true,
});
metadata = sqlResult.metadata as IDtableMetadataColumn[];
const columnType = metadata.find((obj) => obj.name == assetColumn);
const assetColumnType = columnType?.type || null;
// remove unwanted entries
rows = sqlResult.results.filter((obj) => new Date(obj._mtime) > new Date(startDate));
// split the objects into new lines (not necessary for digital-sign)
const newRows: any = [];
for (const row of rows) {
if (assetColumnType === 'digital-sign') {
const signature = (row[assetColumn] as IColumnDigitalSignature) || [];
if (signature.sign_time) {
if (new Date(signature.sign_time) > new Date(startDate)) {
newRows.push(signature);
}
}
}
}
}
let response;
// View => use getRows.
else if (viewName) {
requestMeta = await seaTableApiRequest.call(
this,
ctx,
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
);
requestRows = await seaTableApiRequest.call(
this,
ctx,
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
{},
{
table_name: tableName,
view_name: viewName,
limit,
},
);
if (rows.metadata && rows.results) {
const columns = getColumns(rows);
if (simple) {
response = simplify(rows, columns);
metadata =
requestMeta.metadata.tables.find((table) => table.name === tableName)?.columns ?? [];
// remove unwanted rows that are too old (compare startDate with _ctime or _mtime)
if (this.getMode() === 'manual') {
rows = requestRows.rows;
} else {
response = rows.results;
rows = requestRows.rows.filter((obj) => new Date(obj[filterField]) > new Date(startDate));
}
} else {
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/';
const sqlQuery = `SELECT * FROM \`${tableName}\` WHERE ${filterField} BETWEEN "${moment(
startDate,
).format('YYYY-MM-D HH:mm:ss')}" AND "${moment(endDate).format(
'YYYY-MM-D HH:mm:ss',
)}" ORDER BY ${filterField} DESC LIMIT ${limit}`;
sqlResult = await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
sql: sqlQuery,
convert_keys: true,
});
metadata = sqlResult.metadata as IDtableMetadataColumn[];
rows = sqlResult.results;
}
const collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
this,
ctx,
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
);
const collaborators: ICollaborator[] = collaboratorsResult.user_list || [];
if (Array.isArray(rows) && rows.length > 0) {
// remove columns starting with _ if simple;
if (simple) {
rows = rows.map((row) => simplify_new(row));
}
const allColumns = rows.metadata.map((meta) => meta.name);
// enrich column types like {collaborator, creator, last_modifier}, {image, file}
// remove button column from rows
rows = rows.map((row) => enrichColumns(row, metadata, collaborators));
response = response
//@ts-ignore
.map((row: IRow) => rowFormatColumns(row, allColumns))
.map((row: IRow) => ({ json: row }));
}
if (Array.isArray(response) && response.length) {
return [response];
// prepare for final output
return [this.helpers.returnJsonArray(rows)];
}
return null;

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><path fill="url(#a)" d="M16.787 43.213 28.574 55l16.943-16.942a7.87 7.87 0 0 0 0-11.132l-6.22-6.221-.112-.111-18.611 18.57.13.131z"/><path fill="#ff8000" d="m20.704 39.295 22.51-22.507L31.425 5 14.483 21.942a7.87 7.87 0 0 0 0 11.133z"/><defs><linearGradient id="a" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-109.048 29.213 6.813)scale(10.08407)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff8000"/><stop offset="1" stop-color="#ec2837"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 560 B

View 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

View file

@ -49,10 +49,11 @@ export const rowFields: INodeProperties[] = [
// ----------------------------------
{
displayName: 'Table Name or ID',
// 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 table',
placeholder: 'Name of the table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
@ -63,14 +64,16 @@ export const rowFields: INodeProperties[] = [
},
},
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 an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
'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>.',
},
{
displayName: 'Table Name or ID',
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table ID',
name: 'tableId',
type: 'options',
placeholder: 'Name of table',
placeholder: 'ID of the table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableIds',
@ -81,6 +84,7 @@ export const rowFields: INodeProperties[] = [
},
},
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 an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
@ -157,11 +161,13 @@ export const rowFields: INodeProperties[] = [
name: 'columnValues',
values: [
{
displayName: 'Column Name or ID',
// 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 an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
'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',
@ -243,7 +249,6 @@ export const rowFields: INodeProperties[] = [
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'Max number of results to return',
@ -261,11 +266,13 @@ export const rowFields: INodeProperties[] = [
},
options: [
{
displayName: 'View Name or ID',
// 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 ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
'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',
},
@ -291,7 +298,7 @@ export const rowFields: INodeProperties[] = [
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',
'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',
@ -312,15 +319,16 @@ export const rowFields: INodeProperties[] = [
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Order By',
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:
'A column\'s name or ID, use this column to sort the rows. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
'Choose from the list, or specify a Column using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
},
],
},

View file

@ -0,0 +1,40 @@
import 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: ['main'],
outputs: ['main'],
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Row',
value: 'row',
},
],
default: 'row',
},
...rowOperations,
...rowFields,
],
};

View file

@ -0,0 +1,422 @@
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 { TColumnsUiValues, TColumnValue } from './types';
import type { ICtx, IRow, IRowObject } from './Interfaces';
import { versionDescription } from './SeaTable.node';
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];
}
}

View file

@ -0,0 +1,40 @@
import 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: ['main'],
outputs: ['main'],
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Row',
value: 'row',
},
],
default: 'row',
},
...rowOperations,
...rowFields,
],
};

View file

@ -9,8 +9,8 @@ export type TSeaTableServerEdition = 'enterprise edition';
// dtable
// ----------------------------------
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import type { IDtableMetadataColumn, IDtableMetadataTable, TDtableViewColumn } from './Interfaces';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
export type TInheritColumnTypeTime = 'ctime' | 'mtime';
export type TInheritColumnTypeUser = 'creator' | 'last-modifier';

View file

@ -0,0 +1,368 @@
import type { OptionsWithUri } from 'request';
import FormData from 'form-data';
import type {
IDataObject,
IExecuteFunctions,
ILoadOptionsFunctions,
IPollFunctions,
JsonObject,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import type { TDtableMetadataColumns, TEndpointVariableName } from './types';
import { schema } from './Schema';
import type {
ICollaborator,
ICollaboratorsResult,
ICredential,
ICtx,
IDtableMetadataColumn,
IEndpointVariables,
IName,
IRow,
IRowObject,
IColumnDigitalSignature,
IFile,
} from './actions/Interfaces';
// for date transformations
import moment from 'moment';
// remove last backslash
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: 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);
}
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, expr: string, name: TEndpointVariableName) => {
// I need expr. Why?
return (endpointVariables[name] as string) || match;
},
);
}
export async function seaTableApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
ctx: ICtx,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject | FormData | string | Buffer = {},
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);
// some API endpoints require the api_token instead of base_access_token.
const token =
endpoint.indexOf('/api/v2.1/dtable/app-download-link/') === 0 ||
endpoint == '/api/v2.1/dtable/app-upload-link/' ||
endpoint.indexOf('/seafhttp/upload-api') === 0
? `${ctx?.credentials?.token}`
: `${ctx?.base?.access_token}`;
let options: OptionsWithUri = {
uri: url || `${resolveBaseUri(ctx)}${endpointCtxExpr(ctx, endpoint)}`,
headers: {
Authorization: `Token ${token}`,
},
method,
qs,
body,
json: true,
};
if (Object.keys(option).length !== 0) {
options = Object.assign({}, options, option);
}
// remove header from download request.
if (endpoint.indexOf('/seafhttp/files/') === 0) {
delete options.headers;
}
// enhance header for upload request
if (endpoint.indexOf('/seafhttp/upload-api') === 0) {
options.json = true;
options.headers = {
...options.headers,
'Content-Type': 'multipart/form-data',
};
}
// DEBUG-MODE OR API-REQUESTS
// console.log(options);
if (Object.keys(body).length === 0) {
delete options.body;
}
try {
return this.helpers.requestWithAuthentication.call(this, 'seaTableApi', options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function getBaseCollaborators(
this: ILoadOptionsFunctions | IExecuteFunctions | IPollFunctions,
): Promise<any> {
let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
);
let collaborators: ICollaborator[] = collaboratorsResult.user_list || [];
return collaborators;
}
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 function simplify_new(row: IRow) {
for (const key of Object.keys(row)) {
if (key.startsWith('_')) delete row[key];
}
return row;
}
/*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() : '');
/* will ich diesen call ? */
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));
// INTERNAL: get collaborator info from @auth.local address
function getCollaboratorInfo(
authLocal: string | null | undefined,
collaboratorList: ICollaborator[],
) {
let collaboratorDetails: ICollaborator;
collaboratorDetails = collaboratorList.find(
(singleCollaborator) => singleCollaborator['email'] === authLocal,
) || { contact_email: 'unknown', name: 'unkown', email: 'unknown' };
return collaboratorDetails;
}
// INTERNAL: split asset path.
function getAssetPath(type: string, url: string) {
const parts = url.split(`/${type}/`);
if (parts[1]) {
return '/' + type + '/' + parts[1];
}
return url;
}
// CDB: neu von mir
export function enrichColumns(
row: IRow,
metadata: IDtableMetadataColumn[],
collaboratorList: ICollaborator[],
): IRow {
Object.keys(row).forEach((key) => {
let columnDef = metadata.find((obj) => obj.name === key || obj.key === key);
//console.log(key + " is from type " + columnDef?.type);
if (columnDef?.type === 'collaborator') {
// collaborator is an array of strings.
let collaborators = (row[key] as string[]) || [];
if (collaborators.length > 0) {
let newArray = collaborators.map((email) => {
let collaboratorDetails = getCollaboratorInfo(email, collaboratorList);
let newColl = {
email: email,
contact_email: collaboratorDetails['contact_email'],
name: collaboratorDetails['name'],
};
return newColl;
});
row[key] = newArray;
}
}
if (
columnDef?.type === 'last-modifier' ||
columnDef?.type === 'creator' ||
columnDef?.key === '_creator' ||
columnDef?.key === '_last_modifier'
) {
// creator or last-modifier are always a single string.
let collaboratorDetails = getCollaboratorInfo(row[key] as string, collaboratorList);
row[key] = {
email: row[key],
contact_email: collaboratorDetails['contact_email'],
name: collaboratorDetails['name'],
};
}
if (columnDef?.type === 'image') {
let pictures = (row[key] as string[]) || [];
if (pictures.length > 0) {
let newArray = pictures.map((url) => ({
name: url.split('/').pop(),
size: 0,
type: 'image',
url: url,
path: getAssetPath('images', url),
}));
row[key] = newArray;
}
}
if (columnDef?.type === 'file') {
let files = (row[key] as IFile[]) || [];
files.forEach((file) => {
file.path = getAssetPath('files', file.url);
});
}
if (columnDef?.type === 'digital-sign') {
let digitalSignature: IColumnDigitalSignature | any = row[key];
let collaboratorDetails = getCollaboratorInfo(digitalSignature?.username, collaboratorList);
if (digitalSignature?.username) {
digitalSignature.contact_email = collaboratorDetails['contact_email'];
digitalSignature.name = collaboratorDetails['name'];
}
}
if (columnDef?.type === 'button') {
delete row[key];
}
});
return row;
}
// using create, I input a string like a5adebe279e04415a28b2c7e256e9e8d@auth.local and it should be transformed to an array.
// same with multi-select.
export function splitStringColumnsToArrays(
row: IRowObject,
columns: TDtableMetadataColumns,
): IRowObject {
columns.map((column) => {
if (column.type == 'collaborator' || column.type == 'multiple-select') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = input.split(',').map((item) => item.trim());
}
}
if (column.type == 'number') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = parseFloat(input);
}
}
if (column.type == 'rate' || column.type == 'duration') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = parseInt(input);
}
}
if (column.type == 'checkbox') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = false;
if (input === 'true' || input === 'on' || input === '1') {
row[column.name] = true;
}
}
}
if (column.type == 'date') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = moment(input, 'YYYY-mm-dd', true);
}
}
});
return row;
}
// sollte eher heißen: remove nonUpdateColumnTypes and only use allowed columns!
export function rowExport(row: IRowObject, columns: TDtableMetadataColumns): IRowObject {
let rowAllowed = {} as IRowObject;
columns.map((column) => {
if (row[column.name]) {
rowAllowed[column.name] = row[column.name];
}
});
return rowAllowed;
}
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);

View file

@ -0,0 +1,61 @@
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',
image: 'Image',
file: 'File',
email: 'Email',
url: 'URL',
checkbox: 'Checkbox',
rate: 'Rating',
formula: 'Formula',
'link-formula': 'Link-Formula',
geolocation: 'Geolocation',
link: 'Link',
creator: 'Creator',
ctime: 'Created time',
'last-modifier': 'Last Modifier',
mtime: 'Last modified time',
'auto-number': 'Auto number',
button: 'Button',
'digital-sign': 'Digital Signature',
},
nonUpdateAbleColumnTypes: {
creator: 'creator',
ctime: 'ctime',
'last-modifier': 'last-modifier',
mtime: 'mtime',
'auto-number': 'auto-number',
button: 'button',
formula: 'formula',
'link-formula': 'link-formula',
link: 'link',
'digital-sign': 'digital-sign',
},
} as {
rowFetchSegmentLimit: number;
dateTimeFormat: TDateTimeFormat;
internalNames: { [key in TInheritColumnKey]: ColumnType };
columnTypes: { [key in TColumnType]: string };
nonUpdateAbleColumnTypes: { [key in ColumnType]: ColumnType };
};

View file

@ -0,0 +1,27 @@
import type {
IExecuteFunctions,
INodeType,
INodeTypeDescription,
INodeTypeBaseDescription,
} from 'n8n-workflow';
import { versionDescription } from './actions/SeaTable.node';
import { loadOptions } from './methods';
import { router } from './actions/router';
export class SeaTableV2 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = { loadOptions };
async execute(this: IExecuteFunctions) {
return router.call(this);
}
}

View file

@ -0,0 +1,195 @@
import type { AllEntities, Entity, PropertiesOf } from 'n8n-workflow';
type SeaTableMap = {
row: 'create' | 'get' | 'search' | 'update' | 'remove' | 'lock' | 'unlock' | 'list';
base: 'snapshot' | 'metadata' | 'apiCall' | 'collaborator';
link: 'add' | 'list' | 'remove';
asset: 'upload' | 'getPublicURL';
};
export type SeaTable = AllEntities<SeaTableMap>;
export type SeaTableRow = Entity<SeaTableMap, 'row'>;
export type SeaTableBase = Entity<SeaTableMap, 'base'>;
export type SeaTableLink = Entity<SeaTableMap, 'link'>;
export type SeaTableAsset = Entity<SeaTableMap, 'asset'>;
export type RowProperties = PropertiesOf<SeaTableRow>;
export type BaseProperties = PropertiesOf<SeaTableBase>;
export type LinkProperties = PropertiesOf<SeaTableLink>;
export type AssetProperties = PropertiesOf<SeaTableAsset>;
import type {
TColumnType,
TColumnValue,
TDtableMetadataColumns,
TDtableMetadataTables,
TSeaTableServerEdition,
TSeaTableServerVersion,
} from '../types';
export interface IApi {
server: string;
token: string;
appAccessToken?: IAppAccessToken;
info?: IServerInfo;
}
export interface IServerInfo {
version: TSeaTableServerVersion;
edition: TSeaTableServerEdition;
}
export interface IAppAccessToken {
app_name: string;
access_token: string;
dtable_uuid: string;
dtable_server: string;
dtable_socket: string;
workspace_id: number;
dtable_name: string;
}
export interface IDtableMetadataColumn {
key: string;
name: string;
type: TColumnType;
editable?: boolean;
}
export interface TDtableViewColumn {
_id: string;
name: string;
}
export interface IDtableMetadataTable {
_id: string;
name: string;
columns: TDtableMetadataColumns;
}
export interface IDtableMetadata {
tables: TDtableMetadataTables;
version: string;
format_version: string;
}
export interface IEndpointVariables {
[name: string]: string | number | undefined;
}
export interface IRowObject {
[name: string]: TColumnValue | object;
}
export interface IRow extends IRowObject {
_id: string;
_ctime: string;
_mtime: string;
_seq?: number;
}
export interface IName {
name: string;
}
type TOperation = 'cloudHosted' | 'selfHosted';
export interface ICredential {
token: string;
domain: string;
environment: TOperation;
}
interface IBase {
dtable_uuid: string;
access_token: string;
workspace_id: number;
dtable_name: string;
}
export interface ICtx {
base?: IBase;
credentials?: ICredential;
}
// response object of SQL-Query!
export interface IRowResponse {
metadata: [
{
key: string;
name: string;
type: string;
},
];
results: IRow[];
}
// das ist bad
export interface IRowResponse2 {
rows: IRow[];
}
/** neu von mir **/
// response object of SQL-Query!
export interface ISqlQueryResult {
metadata: [
{
key: string;
name: string;
},
];
results: IRow[];
}
// response object of GetMetadata
export interface IGetMetadataResult {
metadata: IDtableMetadata;
}
// response object of GetRows
export interface IGetRowsResult {
rows: IRow[];
}
export interface ICollaboratorsResult {
user_list: ICollaborator[];
}
export interface ICollaborator {
email: string;
name: string;
contact_email: string;
avatar_url?: string;
id_in_org?: string;
}
export interface IColumnDigitalSignature {
username: string;
sign_image_url: string;
sign_time: string;
contact_email?: string;
name: string;
}
export interface IFile {
name: string;
size: number;
type: 'file';
url: string;
path?: string;
}
export interface ILinkData {
table_id: string;
other_table_id: string;
link_id: string;
}
export interface IUploadLink {
upload_link: string;
parent_path: string;
img_relative_path: string;
file_relative_path: string;
}

View file

@ -0,0 +1,57 @@
import type { INodeTypeDescription } from 'n8n-workflow';
import * as row from './row';
import * as base from './base';
import * as link from './link';
import * as asset from './asset';
export const versionDescription: INodeTypeDescription = {
displayName: 'SeaTable',
name: 'seaTable',
icon: 'file:seatable.svg',
group: ['output'],
version: 2,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Consume the SeaTable API',
defaults: {
name: 'SeaTable',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Row',
value: 'row',
},
{
name: 'Base',
value: 'base',
},
{
name: 'Link',
value: 'link',
},
{
name: 'Asset',
value: 'asset',
},
],
default: 'row',
},
...row.descriptions,
...base.descriptions,
...link.descriptions,
...asset.descriptions,
],
};

View file

@ -0,0 +1,18 @@
import type { AssetProperties } from '../../Interfaces';
export const assetGetPublicURLDescription: AssetProperties = [
{
displayName: 'Asset Path',
name: 'assetPath',
type: 'string',
placeholder: '/images/2023-09/logo.png',
required: true,
displayOptions: {
show: {
resource: ['asset'],
operation: ['getPublicURL'],
},
},
default: '',
},
];

View file

@ -0,0 +1,21 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function getPublicURL(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const assetPath = this.getNodeParameter('assetPath', index) as string;
let responseData = [] as IDataObject[];
if (assetPath) {
responseData = await seaTableApiRequest.call(
this,
{},
'GET',
`/api/v2.1/dtable/app-download-link/?path=${assetPath}`,
);
}
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { getPublicURL as execute } from './execute';
import { assetGetPublicURLDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,36 @@
import * as upload from './upload';
import * as getPublicURL from './getPublicURL';
import type { INodeProperties } from 'n8n-workflow';
export { upload, getPublicURL };
export const descriptions: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['asset'],
},
},
options: [
{
name: 'Public URL',
value: 'getPublicURL',
description: 'Get the public URL from asset path',
action: 'Get the public URL from asset path',
},
{
name: 'Upload',
value: 'upload',
description: 'Add a file/image to an existing row',
action: 'Upload a file or image',
},
],
default: 'upload',
},
...upload.description,
...getPublicURL.description,
];

View file

@ -0,0 +1,108 @@
import type { AssetProperties } from '../../Interfaces';
export const assetUploadDescription: AssetProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
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 a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Column Name',
name: 'uploadColumn',
type: 'options',
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getAssetColumns',
},
required: true,
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Select the column for the upload. 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: 'Row ID',
name: 'rowId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
default: '',
},
{
displayName: 'Property Name',
name: 'dataPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
description: 'Name of the binary property which contains the data for the file to be written',
},
{
displayName: 'Replace Existing File',
name: 'replace',
type: 'boolean',
default: true,
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
description:
'Whether to replace the existing asset with the same name (true). Otherwise, a new version with a different name (numeral in parentheses) will be uploaded (false).',
},
{
displayName: 'Append to Column',
name: 'append',
type: 'boolean',
default: true,
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
description:
'Whether to keep existing files/images in the column and append the new asset (true). Otherwise, the existing files/images are removed from the column (false).',
},
];

View file

@ -0,0 +1,140 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
import type { IUploadLink, IRowObject } from '../../Interfaces';
export async function upload(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const uploadColumn = this.getNodeParameter('uploadColumn', index) as any;
const uploadColumnType = uploadColumn.split(':::')[1];
const uploadColumnName = uploadColumn.split(':::')[0];
const dataPropertyName = this.getNodeParameter('dataPropertyName', index) as string;
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const uploadLink = (await seaTableApiRequest.call(
this,
{},
'GET',
'/api/v2.1/dtable/app-upload-link/',
)) as IUploadLink;
const relativePath =
uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path;
const replace = this.getNodeParameter('replace', index) as string;
const append = this.getNodeParameter('append', index) as string;
// get server url
const credentials: any = await this.getCredentials('seaTableApi');
const serverURL: string = credentials.domain
? credentials.domain.replace(/\/$/, '')
: 'https://cloud.seatable.io';
// get workspaceId
const workspaceId = (
await this.helpers.request({
headers: {
Authorization: `Token ${credentials.token}`,
},
uri: `${serverURL}/api/v2.1/dtable/app-access-token/`,
json: true,
})
).workspace_id;
// if there are already assets attached to the column
let existingAssetArray = [];
if (append) {
let rowToUpdate = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/' + rowId,
{},
{
table_name: tableName,
},
);
existingAssetArray = rowToUpdate[uploadColumnName] ?? [];
}
// Get the binary data and prepare asset for upload
const fileBufferData = await this.helpers.getBinaryDataBuffer(index, dataPropertyName);
const binaryData = this.helpers.assertBinaryData(index, dataPropertyName);
const options = {
formData: {
file: {
value: fileBufferData,
options: {
filename: binaryData.fileName,
contentType: binaryData.mimeType,
},
},
parent_dir: uploadLink.parent_path,
replace: replace ? '1' : '0',
relative_path: relativePath,
},
};
// Send the upload request
let uploadAsset = await seaTableApiRequest.call(
this,
{},
'POST',
`/seafhttp/upload-api/${uploadLink.upload_link.split('seafhttp/upload-api/')[1]}?ret-json=true`,
{},
{},
'',
options,
);
//console.log('uploadAsset: ' + uploadAsset);
// now step 2 (attaching the asset to a column in a base)
for (let c = 0; c < uploadAsset.length; c++) {
const body = {
table_name: tableName,
row_id: rowId,
row: {},
} as IDataObject;
let rowInput = {} as IRowObject;
const filePath = `${serverURL}/workspace/${workspaceId}${uploadLink.parent_path}/${relativePath}/${uploadAsset[c].name}`;
if (uploadColumnType === 'image') {
rowInput[uploadColumnName] = [filePath];
} else if (uploadColumnType === 'file') {
rowInput[uploadColumnName] = uploadAsset;
uploadAsset[c].type = 'file';
uploadAsset[c].url = filePath;
}
// merge with existing assets in this column or with [] and remove duplicates
//console.log('existingAssetArray: ' + existingAssetArray);
//console.log('rowInput_uploadedColumnName: ' + rowInput[uploadColumnName]);
const mergedArray = existingAssetArray.concat(rowInput[uploadColumnName]);
// Remove duplicates based on "url", keeping the last one
const uniqueAssets = Array.from(
// @ts-ignore
mergedArray.reduce((map, asset) => map.set(asset.url, asset), new Map()).values(),
);
// Update the rowInput with the unique assets and store into body.row.
rowInput[uploadColumnName] = uniqueAssets;
body.row = rowInput;
//console.log(body.row);
// attach assets to table row
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
body,
);
//console.log('responseData: ' + responseData);
uploadAsset[c]['upload_successful'] = responseData.success;
}
return this.helpers.returnJsonArray(uploadAsset as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { upload as execute } from './execute';
import { assetUploadDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,129 @@
import type { BaseProperties } from '../../Interfaces';
export const baseApiCallDescription: BaseProperties = [
{
displayName: 'HTTP Method',
name: 'apiMethod',
type: 'options',
options: [
{
name: 'POST',
value: 'POST',
},
{
name: 'GET',
value: 'GET',
},
{
name: 'PUT',
value: 'PUT',
},
{
name: 'DELETE',
value: 'DELETE',
},
],
displayOptions: {
show: {
resource: ['base'],
operation: ['apiCall'],
},
},
required: true,
default: 'POST',
},
{
displayName: 'Hint: The Authentication header is included automatically.',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
resource: ['base'],
operation: ['apiCall'],
},
},
},
{
displayName: 'URL',
name: 'apiEndpoint',
type: 'string',
displayOptions: {
show: {
operation: ['apiCall'],
},
},
required: true,
default: '',
placeholder: '/dtable-server/...',
description: 'The URL has to start with /dtable-server/ or /dtable-db/. All possible requests can be found at the SeaTable API Reference at https://api.seatable.io Please be aware that only request from the section Base Operations that use an Base-Token for the authentication are allowed to use.',
},
{
displayName: 'Query String Parameters',
name: 'apiParams',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
description: 'These params will be URL-encoded and appended to the URL when making the request',
options: [
{
name: 'apiParamsValues',
displayName: 'Parameters',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
displayOptions: {
show: {
resource: ['base'],
operation: ['apiCall'],
},
},
},
{
displayName: 'Body',
name: 'apiBody',
type: 'json',
displayOptions: {
show: {
resource: ['base'],
operation: ['apiCall'],
},
},
typeOptions: {
rows: 4,
},
default: '',
description:
'Only valid JSON is accepted. n8n will pass anything you enter as raw input. For example, {"foo", "bar"} is perfectly valid. Of cause you can use variables from n8n inside your JSON.',
},
{
displayName: 'Response Object Parameter Name',
name: 'responseObjectName',
type: 'string',
placeholder: 'Leave it empty or use a value like "rows", "metadata", "views" etc.',
displayOptions: {
show: {
resource: ['base'],
operation: ['apiCall'],
},
},
default: '',
description:
'When using the SeaTable API, you can specify a parameter to retrieve either the entire array of objects or a specific object within it. This allows you to choose whether to fetch the complete output or only the object related to the provided parameter.',
},
];

View file

@ -0,0 +1,40 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
import { APITypes } from '../../../types';
export async function apiCall(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const apiMethod = this.getNodeParameter('apiMethod', index) as APITypes;
const apiEndpoint = this.getNodeParameter('apiEndpoint', index) as APITypes;
const responseObjectName = this.getNodeParameter('responseObjectName', index) as string;
// body params
const apiBody = this.getNodeParameter('apiBody', index) as any;
// query params
const apiParams: IDataObject = {};
const params = this.getNodeParameter('apiParams.apiParamsValues', index, []) as any;
for (const param of params) {
apiParams[`${param.key}`] = param.value;
}
console.log(apiParams);
const responseData = await seaTableApiRequest.call(
this,
{},
apiMethod,
apiEndpoint,
apiBody,
apiParams,
);
console.log(responseData);
// output
if (responseObjectName) {
return this.helpers.returnJsonArray(responseData[responseObjectName] as IDataObject[]);
} else {
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
}

View file

@ -0,0 +1,4 @@
import { apiCall as execute } from './execute';
import { baseApiCallDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,20 @@
import type { BaseProperties } from '../../Interfaces';
export const baseCollaboratorDescription: BaseProperties = [
{
displayName: 'Name or email of the collaborator',
name: 'searchString',
type: 'string',
placeholder: 'Enter the name or the email or the collaborator',
required: true,
displayOptions: {
show: {
resource: ['base'],
operation: ['collaborator'],
},
},
default: '',
description:
'SeaTable identifies users with a unique username like 244b43hr6fy54bb4afa2c2cb7369d244@auth.local. Get this username from an email or the name of a collaborator.',
},
];

View file

@ -0,0 +1,25 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
import { ICollaborator } from '../../Interfaces';
export async function collaborator(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const searchString = this.getNodeParameter('searchString', index) as string;
const collaboratorsResult = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
);
const collaborators = collaboratorsResult.user_list || [];
const collaborator = collaborators.filter(
(col: ICollaborator) =>
col.contact_email.includes(searchString) || col.name.includes(searchString),
);
return this.helpers.returnJsonArray(collaborator as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { collaborator as execute } from './execute';
import { baseCollaboratorDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,53 @@
import * as snapshot from './snapshot';
import * as metadata from './metadata';
import * as apiCall from './apiCall';
import * as collaborator from './collaborator';
import type { INodeProperties } from 'n8n-workflow';
export { snapshot, metadata, apiCall, collaborator };
export const descriptions: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['base'],
},
},
options: [
{
name: 'Snapshot',
value: 'snapshot',
description: 'Create a snapshot of the base',
action: 'Create a snapshot',
},
{
name: 'Metadata',
value: 'metadata',
description: 'Get the complete metadata of the base',
action: 'Get metadata of a base',
},
{
name: 'API Call',
value: 'apiCall',
description: 'Perform an authorized API call (Base Operation)',
action: 'Make an api call',
},
{
name: 'Collaborator',
value: 'collaborator',
description: 'Get this username from the email or name of a collaborator',
action: 'Get username from email or name',
},
],
default: 'snapshot',
},
...snapshot.description,
...metadata.description,
...apiCall.description,
...collaborator.description,
];

View file

@ -0,0 +1,3 @@
import type { BaseProperties } from '../../Interfaces';
export const baseMetadataDescription: BaseProperties = [];

View file

@ -0,0 +1,15 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function metadata(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const responseData = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
);
return this.helpers.returnJsonArray(responseData.metadata as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { metadata as execute } from './execute';
import { baseMetadataDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,3 @@
import type { BaseProperties } from '../../Interfaces';
export const baseSnapshotDescription: BaseProperties = [];

View file

@ -0,0 +1,17 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function snapshot(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/snapshot/',
{ dtable_name: 'snapshot' },
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { snapshot as execute } from './execute';
import { baseSnapshotDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,74 @@
import type { LinkProperties } from '../../Interfaces';
export const linkAddDescription: LinkProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name (Source)',
name: 'tableName',
type: 'options',
placeholder: 'Name of table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
displayOptions: {
show: {
resource: ['link'],
operation: ['add'],
},
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Link Column',
name: 'linkColumn',
type: 'options',
displayOptions: {
show: {
resource: ['link'],
operation: ['add'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getLinkColumns',
},
required: true,
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id".',
},
{
displayName: 'Row ID From the Source Table',
name: 'linkColumnSourceId',
type: 'string',
displayOptions: {
show: {
resource: ['link'],
operation: ['add'],
},
},
required: true,
default: '',
description: 'Provide the row ID of table you selected',
},
{
displayName: 'Row ID From the Target',
name: 'linkColumnTargetId',
type: 'string',
displayOptions: {
show: {
resource: ['link'],
operation: ['add'],
},
},
required: true,
default: '',
description: 'Provide the row ID of table you want to link',
},
];

View file

@ -0,0 +1,28 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function add(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const linkColumn = this.getNodeParameter('linkColumn', index) as any;
const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string;
const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string;
const body = {
link_id: linkColumn.split(':::')[1],
table_id: tableName.split(':::')[1],
other_table_id: linkColumn.split(':::')[2],
other_rows_ids_map: {
[linkColumnSourceId]: [linkColumnTargetId],
},
};
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-db/api/v1/base/{{dtable_uuid}}/links/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { add as execute } from './execute';
import { linkAddDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,44 @@
import * as add from './add';
import * as list from './list';
import * as remove from './remove';
import type { INodeProperties } from 'n8n-workflow';
export { add, list, remove };
export const descriptions: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['link'],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Create a link between two rows in a link column',
action: 'Add a row link',
},
{
name: 'List',
value: 'list',
description: 'List all links of a specific row',
action: 'List row links',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a link between two rows from a link column',
action: 'Remove a row link',
},
],
default: 'add',
},
...add.description,
...list.description,
...remove.description,
];

View file

@ -0,0 +1,66 @@
import type { LinkProperties } from '../../Interfaces';
export const listLinkDescription: LinkProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name 32',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
displayOptions: {
show: {
resource: ['link'],
operation: ['list'],
},
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Link Column',
name: 'linkColumn',
type: 'options',
displayOptions: {
show: {
resource: ['link'],
operation: ['list'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getLinkColumnsWithColumnKey',
},
required: true,
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id:::column_key".',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Row ID',
name: 'rowId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['link'],
operation: ['list'],
},
},
default: '',
},
];

View file

@ -0,0 +1,29 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function list(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
// get parameters
const tableName = this.getNodeParameter('tableName', index) as string;
const linkColumn = this.getNodeParameter('linkColumn', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
// get rows
let responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-db/api/v1/linked-records/{{dtable_uuid}}/',
{
table_id: tableName.split(':::')[1],
link_column: linkColumn.split(':::')[3],
rows: [
{
row_id: rowId,
offset: 0,
limit: 100,
},
],
},
);
return this.helpers.returnJsonArray(responseData[rowId] as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { list as execute } from './execute';
import { listLinkDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,74 @@
import type { LinkProperties } from '../../Interfaces';
export const linkRemoveDescription: LinkProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name (Source)',
name: 'tableName',
type: 'options',
placeholder: 'Name of table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
displayOptions: {
show: {
resource: ['link'],
operation: ['remove'],
},
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Link Column',
name: 'linkColumn',
type: 'options',
displayOptions: {
show: {
resource: ['link'],
operation: ['remove'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getLinkColumns',
},
required: true,
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id".',
},
{
displayName: 'Row ID From the Source Table',
name: 'linkColumnSourceId',
type: 'string',
displayOptions: {
show: {
resource: ['link'],
operation: ['remove'],
},
},
required: true,
default: '',
description: 'Provide the row ID of table you selected',
},
{
displayName: 'Row ID From the Target Table',
name: 'linkColumnTargetId',
type: 'string',
displayOptions: {
show: {
resource: ['link'],
operation: ['remove'],
},
},
required: true,
default: '',
description: 'Provide the row ID of table you want to link',
},
];

View file

@ -0,0 +1,31 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function remove(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const linkColumn = this.getNodeParameter('linkColumn', index) as any;
const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string;
const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string;
const body = {
link_id: linkColumn.split(':::')[1],
table_id: tableName.split(':::')[1],
other_table_id: linkColumn.split(':::')[2],
other_rows_ids_map: {
[linkColumnSourceId]: [linkColumnTargetId],
},
};
const responseData = await seaTableApiRequest.call(
this,
{},
'DELETE',
'/dtable-db/api/v1/base/{{dtable_uuid}}/links/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { remove as execute } from './execute';
import { linkRemoveDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,56 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import * as row from './row';
import * as base from './base';
import * as link from './link';
import * as asset from './asset';
import type { SeaTable } from './Interfaces';
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const operationResult: INodeExecutionData[] = [];
let responseData: IDataObject | IDataObject[] = [];
//console.log('ITEM LENGTH ' + items.length);
for (let i = 0; i < items.length; i++) {
const resource = this.getNodeParameter<SeaTable>('resource', i);
const operation = this.getNodeParameter('operation', i);
//console.log(operation);
//console.log(resource);
const seatable = {
resource,
operation,
} as SeaTable;
try {
if (seatable.resource === 'row') {
responseData = await row[seatable.operation].execute.call(this, i);
} else if (seatable.resource === 'base') {
responseData = await base[seatable.operation].execute.call(this, i);
} else if (seatable.resource === 'link') {
responseData = await link[seatable.operation].execute.call(this, i);
} else if (seatable.resource === 'asset') {
responseData = await asset[seatable.operation].execute.call(this, i);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData),
{ itemData: { item: i } },
);
operationResult.push(...executionData);
} catch (err) {
if (this.continueOnFail()) {
operationResult.push({ json: this.getInputData(i)[0].json, error: err });
} else {
if (err.context) err.context.itemIndex = i;
throw err;
}
}
}
return [operationResult];
}

View file

@ -0,0 +1,140 @@
import type { RowProperties } from '../../Interfaces';
export const rowCreateDescription: RowProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['create'],
},
},
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 a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
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: {
resource: ['row'],
operation: ['create'],
},
},
default: 'defineBelow',
description: 'Whether to insert the input data this node receives in the new row',
},
{
displayName: 'Inputs to Ignore',
name: 'inputsToIgnore',
type: 'string',
displayOptions: {
show: {
resource: ['row'],
operation: ['create'],
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 column name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getTableUpdateAbleColumns',
},
default: '',
},
{
displayName: 'Column Value',
name: 'columnValue',
type: 'string',
default: '',
},
],
},
],
displayOptions: {
show: {
resource: ['row'],
operation: ['create'],
fieldsToSend: ['defineBelow'],
},
},
default: {},
description:
'Add destination column with its value. Provide the value in this way. Date: YYYY-MM-DD or YYYY-MM-DD hh:mm. Duration: time in seconds. Checkbox: true, on or 1. Multi-Select: comma-separated list.',
},
{
displayName: 'Save to "Big Data" Backend',
name: 'bigdata',
type: 'boolean',
displayOptions: {
show: {
resource: ['row'],
operation: ['create'],
},
},
default: false,
description:
'Whether write to Big Data backend (true) or not (false). True requires the activation of the Big Data backend in the base.',
},
{
displayName: 'Hint: Link, files, images or digital signatures have to be added separately.',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
resource: ['row'],
operation: ['create'],
},
},
},
];

View file

@ -0,0 +1,76 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import {
seaTableApiRequest,
getTableColumns,
split,
rowExport,
updateAble,
splitStringColumnsToArrays,
} from '../../../GenericFunctions';
import type { IRowObject } from '../../Interfaces';
import type { TColumnValue, TColumnsUiValues } from '../../../types';
export async function create(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const tableColumns = await getTableColumns.call(this, tableName);
const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as
| 'defineBelow'
| 'autoMapInputData';
const bigdata = this.getNodeParameter('bigdata', index) as string;
const body = {
table_name: tableName,
row: {},
} as IDataObject;
let rowInput = {} as IRowObject;
// get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }.
if (fieldsToSend === 'autoMapInputData') {
const items = this.getInputData();
const incomingKeys = Object.keys(items[index].json);
const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string);
for (const key of incomingKeys) {
if (inputDataToIgnore.includes(key)) continue;
rowInput[key] = items[index].json[key] as TColumnValue;
}
} else {
const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues;
for (const column of columns) {
rowInput[column.columnName] = column.columnValue;
}
}
// only keep key:value pairs for columns that are allowed to update.
rowInput = rowExport(rowInput, updateAble(tableColumns));
// string to array: multi-select and collaborators
rowInput = splitStringColumnsToArrays(rowInput, tableColumns);
// save to big data backend
if (bigdata) {
body.rows = [rowInput];
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-db/api/v1/insert-rows/{{dtable_uuid}}/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
// save to normal backend
else {
body.row = rowInput;
const responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}
}

View file

@ -0,0 +1,4 @@
import { create as execute } from './execute';
import { rowCreateDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,58 @@
import type { RowProperties } from '../../Interfaces';
export const rowGetDescription: RowProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
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 a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Row ID',
name: 'rowId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['get'],
},
},
default: '',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['row'],
operation: ['get'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];

View file

@ -0,0 +1,42 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import type { IRow, IRowResponse, IDtableMetadataColumn } from './../../Interfaces';
import {
seaTableApiRequest,
enrichColumns,
simplify_new,
getBaseCollaborators,
} from '../../../GenericFunctions';
export async function get(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
// get parameters
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const simple = this.getNodeParameter('simple', index) as boolean;
// get collaborators
const collaborators = await getBaseCollaborators.call(this);
// get rows
let sqlResult = (await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-db/api/v1/query/{{dtable_uuid}}/',
{
sql: `SELECT * FROM \`${tableName}\` WHERE _id = '${rowId}'`,
convert_keys: true,
},
)) as IRowResponse;
let metadata = sqlResult.metadata as IDtableMetadataColumn[];
let rows = sqlResult.results as IRow[];
// hide columns like button
rows.map((row) => enrichColumns(row, metadata, collaborators));
// remove columns starting with _ if simple;
if (simple) {
rows.map((row) => simplify_new(row));
}
return this.helpers.returnJsonArray(rows as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { get as execute } from './execute';
import { rowGetDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,84 @@
import * as create from './create';
import * as get from './get';
import * as list from './list';
import * as search from './search';
import * as update from './update';
import * as remove from './remove';
import * as lock from './lock';
import * as unlock from './unlock';
import type { INodeProperties } from 'n8n-workflow';
export { create, get, search, update, remove, lock, unlock, list };
export const descriptions: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['row'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new row',
action: 'Create a row',
},
{
name: 'Delete',
value: 'remove',
description: 'Delete a row',
action: 'Delete a row',
},
{
name: 'Get',
value: 'get',
description: 'Get the content of a row',
action: 'Get a row',
},
{
name: 'Get Many',
value: 'list',
description: 'Get many rows from a table or a table view',
action: 'Get many rows',
},
{
name: 'Lock',
value: 'lock',
description: 'Lock a row to prevent further changes',
action: 'Add a row lock',
},
{
name: 'Search',
value: 'search',
description: 'Search one or multiple rows',
action: 'Search a row by keyword',
},
{
name: 'Unlock',
value: 'unlock',
description: 'Remove the lock from a row',
action: 'Remove a row lock',
},
{
name: 'Update',
value: 'update',
description: 'Update the content of a row',
action: 'Update a row',
},
],
default: 'create',
},
...create.description,
...get.description,
...list.description,
...search.description,
...update.description,
...remove.description,
...lock.description,
...unlock.description,
];

View file

@ -0,0 +1,58 @@
import type { RowProperties } from '../../Interfaces';
export const rowListDescription: RowProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['list'],
},
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify by using an expression. Provide it in the way "table_name:::table_id".',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'View Name',
name: 'viewName',
type: 'options',
displayOptions: {
show: {
resource: ['row'],
operation: ['list'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getTableViews',
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'The name of SeaTable view to access, or specify by using an expression. Provide it in the way "col.name:::col.type".',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['row'],
operation: ['list'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];

View file

@ -0,0 +1,54 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import type { IRow } from './../../Interfaces';
import {
seaTableApiRequest,
enrichColumns,
simplify_new,
getBaseCollaborators,
} from '../../../GenericFunctions';
export async function list(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
// get parameters
const tableName = this.getNodeParameter('tableName', index) as string;
const viewName = this.getNodeParameter('viewName', index) as string;
const simple = this.getNodeParameter('simple', index) as boolean;
// get collaborators
const collaborators = await getBaseCollaborators.call(this);
// get rows
let requestMeta = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
);
let requestRows = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
{},
{
table_name: tableName,
view_name: viewName,
limit: 1000,
},
);
let metadata =
requestMeta.metadata.tables.find((table: { name: string }) => table.name === tableName)
?.columns ?? [];
let rows = requestRows.rows as IRow[];
// hide columns like button
rows.map((row) => enrichColumns(row, metadata, collaborators));
// remove columns starting with _ if simple;
if (simple) {
rows.map((row) => simplify_new(row));
}
return this.helpers.returnJsonArray(rows as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { list as execute } from './execute';
import { rowListDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,46 @@
import type { RowProperties } from '../../Interfaces';
export const rowLockDescription: RowProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['lock'],
},
},
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 a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Row ID',
name: 'rowId',
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['lock'],
},
},
default: '',
},
];

View file

@ -0,0 +1,20 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function lock(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/lock-rows/',
{
table_name: tableName,
row_ids: [rowId],
},
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { lock as execute } from './execute';
import { rowLockDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,46 @@
import type { RowProperties } from '../../Interfaces';
export const rowRemoveDescription: RowProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['remove'],
},
},
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 a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Row ID',
name: 'rowId',
type: 'options',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['remove'],
},
},
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Remove any row from the normal or big data backend based on its unique row ID. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
];

View file

@ -0,0 +1,25 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function remove(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const requestBody: IDataObject = {
table_name: tableName,
row_ids: [rowId],
};
const responseData = await seaTableApiRequest.call(
this,
{},
'DELETE',
'/dtable-db/api/v1/delete-rows/{{dtable_uuid}}/',
requestBody,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { remove as execute } from './execute';
import { rowRemoveDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,101 @@
import type { RowProperties } from '../../Interfaces';
export const rowSearchDescription: RowProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
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 a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Column Name',
name: 'searchColumn',
type: 'options',
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getSearchableColumns',
},
required: true,
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Select the column to be searched. Not all column types are supported for search. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
displayName: 'Search Term',
name: 'searchTerm',
type: 'string',
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
required: true,
default: '',
description: 'What to look for?',
},
{
displayName: 'Case Insensitive Search',
name: 'insensitive',
type: 'boolean',
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
default: false,
description:
'Whether the search ignores case sensitivity (true). Otherwise, it distinguishes between uppercase and lowercase characters.',
},
{
displayName: 'Activate Wildcard Search',
name: 'wildcard',
type: 'boolean',
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
default: false,
description:
'Whether the search only results perfect matches (true). Otherwise, it finds a row even if the search value is part of a string (false).',
},
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];

View file

@ -0,0 +1,58 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import {
seaTableApiRequest,
enrichColumns,
simplify_new,
getBaseCollaborators,
} from '../../../GenericFunctions';
import { IDtableMetadataColumn, IRow, IRowResponse } from '../../Interfaces';
export async function search(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const searchColumn = this.getNodeParameter('searchColumn', index) as string;
const searchTerm = this.getNodeParameter('searchTerm', index) as string | number;
let searchTermString = String(searchTerm) as string;
const insensitive = this.getNodeParameter('insensitive', index) as boolean;
const wildcard = this.getNodeParameter('wildcard', index) as boolean;
const simple = this.getNodeParameter('simple', index) as boolean;
// get collaborators
const collaborators = await getBaseCollaborators.call(this);
// this is the base query. The WHERE has to be finalized...
let sqlQuery = `SELECT * FROM \`${tableName}\` WHERE \`${searchColumn}\``;
if (insensitive) {
searchTermString = searchTermString.toLowerCase();
sqlQuery = `SELECT * FROM \`${tableName}\` WHERE lower(\`${searchColumn}\`)`;
}
if (wildcard) sqlQuery = sqlQuery + ' LIKE "%' + searchTermString + '%"';
else if (!wildcard) sqlQuery = sqlQuery + ' = "' + searchTermString + '"';
const sqlResult = (await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-db/api/v1/query/{{dtable_uuid}}/',
{
sql: sqlQuery,
convert_keys: true,
},
)) as IRowResponse;
const metadata = sqlResult.metadata as IDtableMetadataColumn[];
let rows = sqlResult.results as IRow[];
// hide columns like button
rows.map((row) => enrichColumns(row, metadata, collaborators));
// remove columns starting with _;
if (simple) {
rows.map((row) => simplify_new(row));
}
return this.helpers.returnJsonArray(rows as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { search as execute } from './execute';
import { rowSearchDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,45 @@
import type { RowProperties } from '../../Interfaces';
export const rowUnlockDescription: RowProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['unlock'],
},
},
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 a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Row ID',
name: 'rowId',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['unlock'],
},
},
default: '',
},
];

View file

@ -0,0 +1,23 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function unlock(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const rowId = this.getNodeParameter('rowId', index) as string;
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/unlock-rows/',
{
table_name: tableName,
row_ids: [rowId],
},
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { unlock as execute } from './execute';
import { rowUnlockDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,147 @@
import type { RowProperties } from '../../Interfaces';
export const rowUpdateDescription: RowProperties = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['update'],
},
},
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 a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Row ID',
name: 'rowId',
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['update'],
},
},
default: '',
},
{
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: {
resource: ['row'],
operation: ['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: {
resource: ['row'],
operation: ['update'],
fieldsToSend: ['autoMapInputData'],
},
},
default: '',
description:
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
placeholder: 'Enter properties...',
},
{
displayName: 'Columns to Send',
name: 'columnsUi',
placeholder: 'Add Column',
type: 'fixedCollection',
typeOptions: {
multipleValueButtonText: 'Add Column to Send',
multipleValues: true,
},
options: [
{
displayName: 'Column',
name: 'columnValues',
values: [
{
// 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 column name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getTableUpdateAbleColumns',
},
default: '',
},
{
displayName: 'Column Value',
name: 'columnValue',
type: 'string',
default: '',
},
],
},
],
displayOptions: {
show: {
resource: ['row'],
operation: ['update'],
fieldsToSend: ['defineBelow'],
},
},
default: {},
description:
'Add destination column with its value. Provide the value in this way:Date: YYYY-MM-DD or YYYY-MM-DD hh:mmDuration: time in secondsCheckbox: true, on or 1Multi-Select: comma-separated list.',
},
{
displayName: 'Hint: Link, files, images or digital signatures have to be added separately.',
name: 'notice',
type: 'notice',
default: '',
displayOptions: {
show: {
resource: ['row'],
operation: ['update'],
},
},
},
];

View file

@ -0,0 +1,67 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import {
seaTableApiRequest,
getTableColumns,
split,
rowExport,
updateAble,
splitStringColumnsToArrays,
} from '../../../GenericFunctions';
import type { IRowObject } from '../../Interfaces';
import type { TColumnsUiValues, TColumnValue } from '../../../types';
export async function update(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const tableName = this.getNodeParameter('tableName', index) as string;
const tableColumns = await getTableColumns.call(this, tableName);
const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as
| 'defineBelow'
| 'autoMapInputData';
const rowId = this.getNodeParameter('rowId', index) as string;
let rowInput = {} as IRowObject;
// get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }.
if (fieldsToSend === 'autoMapInputData') {
const items = this.getInputData();
const incomingKeys = Object.keys(items[index].json);
const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string);
for (const key of incomingKeys) {
if (inputDataToIgnore.includes(key)) continue;
rowInput[key] = items[index].json[key] as TColumnValue;
}
} else {
const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues;
for (const column of columns) {
rowInput[column.columnName] = column.columnValue;
}
}
// only keep key:value pairs for columns that are allowed to update.
rowInput = rowExport(rowInput, updateAble(tableColumns));
// string to array: multi-select and collaborators
rowInput = splitStringColumnsToArrays(rowInput, tableColumns);
const body = {
table_name: tableName,
updates: [
{
row_id: rowId,
row: rowInput,
},
],
} as IDataObject;
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/dtable-db/api/v1/update-rows/{{dtable_uuid}}/',
body,
);
return this.helpers.returnJsonArray(responseData as IDataObject[]);
}

View file

@ -0,0 +1,4 @@
import { update as execute } from './execute';
import { rowUpdateDescription as description } from './description';
export { description, execute };

View file

@ -0,0 +1,57 @@
import type { INodeTypeDescription } from 'n8n-workflow';
import * as row from './row';
import * as base from './base';
import * as link from './link';
import * as asset from './asset';
export const versionDescription: INodeTypeDescription = {
displayName: 'SeaTable',
name: 'seaTable',
icon: 'file:seatable.svg',
group: ['output'],
version: 2,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Consume the SeaTable API',
defaults: {
name: 'SeaTable',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Row',
value: 'row',
},
{
name: 'Base',
value: 'base',
},
{
name: 'Link',
value: 'link',
},
{
name: 'Asset',
value: 'asset',
},
],
default: 'row',
},
...row.descriptions,
...base.descriptions,
...link.descriptions,
...asset.descriptions,
],
};

View file

@ -0,0 +1 @@
export * as loadOptions from './loadOptions';

View file

@ -0,0 +1,272 @@
import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
import { getTableColumns, seaTableApiRequest, updateAble } from '../GenericFunctions';
import type { IRow } from '../actions/Interfaces';
export async function getTableNames(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
// this.getCurrentNodeParameter('viewName'); // das kommt vom trigger. Brauche ich das???
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;
}
export async function getTableNameAndId(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
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 + ':::' + table['_id'],
});
}
return returnData;
}
export async function getSearchableColumns(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tableName = this.getCurrentNodeParameter('tableName') as string;
if (tableName) {
const columns = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
{},
{ table_name: tableName },
);
for (const col of columns.columns) {
if (
col.type === 'text' ||
col.type === 'long-text' ||
col.type === 'number' ||
col.type === 'single-select' ||
col.type === 'email' ||
col.type === 'url' ||
col.type === 'rate' ||
col.type === 'formula'
) {
returnData.push({
name: col.name,
value: col.name,
});
}
}
}
return returnData;
}
export async function getLinkColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const table = this.getCurrentNodeParameter('tableName') as string;
const tableName = table.split(':::')[0];
const tableId = table.split(':::')[1];
if (tableName) {
const columns = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
{},
{ table_name: tableName },
);
for (const col of columns.columns) {
if (col.type === 'link') {
// make sure that the "other table id" is returned and not the same table id again.
const otid =
tableId !== col.data.other_table_id ? col.data.other_table_id : col.data.table_id;
returnData.push({
name: col.name,
value: col.name + ':::' + col.data.link_id + ':::' + otid,
});
}
}
}
return returnData;
}
export async function getLinkColumnsWithColumnKey(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const table = this.getCurrentNodeParameter('tableName') as string;
const tableName = table.split(':::')[0];
const tableId = table.split(':::')[1];
if (tableName) {
const columns = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
{},
{ table_name: tableName },
);
for (const col of columns.columns) {
if (col.type === 'link') {
// make sure that the "other table id" is returned and not the same table id again.
const otid =
tableId !== col.data.other_table_id ? col.data.other_table_id : col.data.table_id;
returnData.push({
name: col.name,
value: col.name + ':::' + col.data.link_id + ':::' + otid + ':::' + col.key,
});
}
}
}
return returnData;
}
export async function getAssetColumns(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tableName = this.getCurrentNodeParameter('tableName') as string;
if (tableName) {
const columns = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
{},
{ table_name: tableName },
);
for (const col of columns.columns) {
if (col.type === 'image' || col.type === 'file') {
returnData.push({
name: col.name,
value: col.name + ':::' + col.type,
});
}
}
}
return returnData;
}
export async function getSignatureColumns(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tableName = this.getCurrentNodeParameter('tableName') as string;
if (tableName) {
// only execute if table is selected
const columns = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
{},
{ table_name: tableName },
);
for (const col of columns.columns) {
if (col.type === 'digital-sign') {
// file+image are difficult: every time the row changes, all files trigger.
returnData.push({
name: col.name,
value: col.name,
});
}
}
}
return returnData;
}
export async function getTableUpdateAbleColumns(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const tableName = this.getNodeParameter('tableName') as string;
let columns = await getTableColumns.call(this, tableName);
// remove columns that could not be filled
columns = updateAble(columns);
return columns
.filter((column) => column.editable)
.map((column) => ({ name: column.name, value: column.name }));
}
export async function getRowIds(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const table = this.getCurrentNodeParameter('tableName') as string;
let tableName = table;
if (table.indexOf(':::') !== -1) {
tableName = table.split(':::')[0];
}
const returnData: INodePropertyOptions[] = [];
if (tableName) {
const sqlResult = await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-db/api/v1/query/{{dtable_uuid}}/',
{
sql: `SELECT * FROM \`${tableName}\` LIMIT 1000`,
convert_keys: false,
},
);
let rows = sqlResult.results as IRow[];
for (const row of rows) {
returnData.push({
name: row['0000'] + ' (' + row._id + ')',
value: row._id,
});
}
}
return returnData;
}
export async function getTableViews(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tableName = this.getCurrentNodeParameter('tableName') as string;
if (tableName) {
// only execute if table is selected, to avoid unnecessary API requests
const { views } = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/views',
{},
{ table_name: tableName },
);
returnData.push({
name: '<Do not limit to a view>',
value: '',
});
for (const view of views) {
returnData.push({
name: view.name,
value: view.name,
});
}
}
return returnData;
}

View file

@ -0,0 +1,99 @@
// ----------------------------------
// SeaTable
// ----------------------------------
export type TSeaTableServerVersion = '2.0.6';
export type TSeaTableServerEdition = 'enterprise edition';
// ----------------------------------
// dtable
// ----------------------------------
import type {
IDtableMetadataColumn,
IDtableMetadataTable,
TDtableViewColumn,
} from './actions/Interfaces';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
export type TColumnType =
| 'text'
| 'long-text'
| 'number'
| 'collaborator'
| 'date'
| 'duration'
| 'single-select'
| 'multiple-select'
| 'image'
| 'file'
| 'email'
| 'url'
| 'checkbox'
| 'rate'
| 'formula'
| 'link-formula'
| 'geolocation'
| 'link'
| 'creator'
| 'ctime'
| 'last-modifier'
| 'mtime'
| 'auto-number'
| 'button'
| 'digital-sign';
export type TInheritColumnKey =
| '_id'
| '_creator'
| '_ctime'
| '_last_modifier'
| '_mtime'
| '_seq'
| '_archived'
| '_locked'
| '_locked_by';
export type TColumnValue = undefined | boolean | number | string | string[] | null;
export type TColumnKey = TInheritColumnKey | string;
export type TDtableMetadataTables = readonly IDtableMetadataTable[];
export type TDtableMetadataColumns = 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;
}>;
export type APITypes = 'GET' | 'POST' | 'DELETE' | 'PUT';