rework of seatable-n8n-node

This commit is contained in:
Christoph Dyllick-Brenzinger 2023-10-26 02:20:43 +02:00
parent 742c8a8534
commit e791055fcd
68 changed files with 3491 additions and 550 deletions

View file

@ -1,24 +1,10 @@
import type { ICredentialType, INodeProperties, INodePropertyOptions } from 'n8n-workflow'; import type { ICredentialTestRequest, ICredentialType, INodeProperties } 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);
}, []);
export class SeaTableApi implements ICredentialType { export class SeaTableApi implements ICredentialType {
name = 'seaTableApi'; name = 'seaTableApi';
displayName = 'SeaTable API'; displayName = 'SeaTable API';
documentationUrl =
documentationUrl = 'seaTable'; 'https://seatable.io/docs/n8n-integration/erstellen-eines-api-tokens-fuer-n8n/?lang=auto';
properties: INodeProperties[] = [ properties: INodeProperties[] = [
{ {
displayName: 'Environment', displayName: 'Environment',
@ -41,7 +27,7 @@ export class SeaTableApi implements ICredentialType {
name: 'domain', name: 'domain',
type: 'string', type: 'string',
default: '', default: '',
placeholder: 'https://www.mydomain.com', placeholder: 'https://seatable.example.com',
displayOptions: { displayOptions: {
show: { show: {
environment: ['selfHosted'], environment: ['selfHosted'],
@ -52,16 +38,20 @@ export class SeaTableApi implements ICredentialType {
displayName: 'API Token (of a Base)', displayName: 'API Token (of a Base)',
name: 'token', name: 'token',
type: 'string', 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 }, typeOptions: { password: true },
default: '', 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 { import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
IExecuteFunctions, import { VersionedNodeType } from 'n8n-workflow';
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { import { SeaTableV1 } from './v1/SeaTableV1.node';
getTableColumns, import { SeaTableV2 } from './v2/SeaTableV2.node';
getTableViews,
rowExport,
rowFormatColumns,
rowMapKeyToName,
seaTableApiRequest,
setableApiRequestAllItems,
split,
updateAble,
} from './GenericFunctions';
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(),
2: new SeaTableV2(baseDescription),
};
import type { ICtx, IRow, IRowObject } from './Interfaces'; super(nodeVersions, baseDescription);
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()) {
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

@ -1,23 +1,33 @@
import type { import type {
IPollFunctions, IPollFunctions,
ILoadOptionsFunctions,
INodeExecutionData, INodeExecutionData,
INodePropertyOptions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { getColumns, rowFormatColumns, seaTableApiRequest, simplify } from './GenericFunctions'; import { seaTableApiRequest, simplify_new, enrichColumns } from './v2/GenericFunctions';
import type { ICtx, IRow, IRowResponse } from './Interfaces'; import type {
ICtx,
IRow,
IRowResponse,
IGetMetadataResult,
IGetRowsResult,
IDtableMetadataColumn,
ICollaborator,
ICollaboratorsResult,
IColumnDigitalSignature,
} from './v2/actions/Interfaces';
import moment from 'moment'; import moment from 'moment';
import { loadOptions } from './v2/methods';
export class SeaTableTrigger implements INodeType { export class SeaTableTrigger implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'SeaTable Trigger', displayName: 'SeaTable Trigger',
name: 'seaTableTrigger', name: 'seaTableTrigger',
icon: 'file:seaTable.svg', icon: 'file:seatable.svg',
group: ['trigger'], group: ['trigger'],
version: 1, version: 1,
description: 'Starts the workflow when SeaTable events occur', description: 'Starts the workflow when SeaTable events occur',
@ -35,6 +45,29 @@ export class SeaTableTrigger implements INodeType {
inputs: [], inputs: [],
outputs: ['main'], outputs: ['main'],
properties: [ properties: [
{
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',
},
{ {
displayName: 'Table Name or ID', displayName: 'Table Name or ID',
name: 'tableName', name: 'tableName',
@ -48,109 +81,206 @@ export class SeaTableTrigger implements INodeType {
'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 an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
}, },
{ {
displayName: 'Event', displayName: 'View Name or ID (optional)',
name: 'event', name: 'viewName',
type: 'options', type: 'options',
options: [ required: false,
{ displayOptions: {
name: 'Row Created', show: {
value: 'rowCreated', event: ['newRow', 'updatedRow'],
description: 'Trigger on newly created rows',
}, },
// { },
// name: 'Row Modified', typeOptions: {
// value: 'rowModified', loadOptionsDependsOn: ['tableName'],
// description: 'Trigger has recently modified rows', loadOptionsMethod: 'getTableViews',
// }, },
], default: '',
default: 'rowCreated', description: 'The name of SeaTable view to access. Choose from the list, or specify ...',
}, },
{ {
displayName: 'Simplify', displayName: 'Signature column',
name: 'assetColumn',
type: 'options',
required: true,
displayOptions: {
show: {
event: ['newAsset'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getSignatureColumns',
},
default: '',
description: 'Select the digital-signature column that should be tracked.',
},
{
displayName: 'Simplify output',
name: 'simple', name: 'simple',
type: 'boolean', type: 'boolean',
default: true, default: true,
description: description:
'Whether to return a simplified version of the response instead of the raw data', 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.',
},
{
displayName: '"Fetch Test Event" returns max. three items of the last hour.',
name: 'notice',
type: 'notice',
default: '',
}, },
], ],
}; };
methods = { methods = { loadOptions };
loadOptions: {
async getTableNames(this: ILoadOptionsFunctions) {
const returnData: INodePropertyOptions[] = [];
const {
metadata: { tables },
} = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
);
for (const table of tables) {
returnData.push({
name: table.name,
value: table.name,
});
}
return returnData;
},
},
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> { async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const webhookData = this.getWorkflowStaticData('node'); 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 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 ctx: ICtx = {};
const credentials = await this.getCredentials('seaTableApi');
const timezone = (credentials.timezone as string) || 'Europe/Berlin'; const startDate =
const now = moment().utc().format(); this.getMode() === 'manual'
const startDate = (webhookData.lastTimeChecked as string) || now; ? moment().utc().subtract(1, 'h').format()
const endDate = now; : (webhookData.lastTimeChecked as string);
webhookData.lastTimeChecked = endDate; const endDate = (webhookData.lastTimeChecked = moment().utc().format());
let rows; // this is working, even if the columns _mtime and _ctime have other names. Only relevant for newRow / updatedRow.
const filterField = event === 'newRow' ? '_ctime' : '_mtime';
const filterField = event === 'rowCreated' ? '_ctime' : '_mtime'; // Difference between getRows and SqlQuery:
// ====================
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/'; // getRows (if view is selected)
// getRows always gets up to 1.000 rows of the selected view.
// getRows delivers only the rows, not the metadata
// no possibility to filter for _ctime or _mtime with the API call.
// Problems, not yet solved:
// if a column is empty, the column is not returned!
// view with more than 1.000 rows will not work!
if (this.getMode() === 'manual') { // SqlQuery (if no view is selected)
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, { // SqlQuery returns up to 1.000. WHERE by time and ORDER BY _ctime or _mtime is possible.
sql: `SELECT * FROM ${tableName} LIMIT 1`, // SqlQuery returns rows and metadata
})) as IRowResponse;
} else { let requestMeta: IGetMetadataResult;
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, { let requestRows: IGetRowsResult;
sql: `SELECT * FROM ${tableName} let metadata: IDtableMetadataColumn[] = [];
WHERE ${filterField} BETWEEN "${moment(startDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}" let rows: IRow[];
AND "${moment(endDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"`, let sqlResult: IRowResponse;
})) as IRowResponse;
const limit = this.getMode() === 'manual' ? 3 : 1000;
// 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),
) as IRow[];
// split the objects into new lines (not necessary for digital-sign)
const newRows: any = [];
for (const row of rows) {
if (assetColumnType === 'digital-sign') {
let 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: limit,
},
);
if (rows.metadata && rows.results) { // I need only metadata of the selected table.
const columns = getColumns(rows); metadata =
if (simple) { requestMeta.metadata.tables.find((table) => table.name === tableName)?.columns ?? [];
response = simplify(rows, columns);
// remove unwanted rows that are too old (compare startDate with _ctime or _mtime)
if (this.getMode() === 'manual') {
rows = requestRows.rows as IRow[];
} else { } else {
response = rows.results; rows = requestRows.rows.filter(
(obj) => new Date(obj[filterField]) > new Date(startDate),
) as IRow[];
}
}
// No view => use SQL-Query
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 as IRow[];
}
// =========================================
// => now I have rows and metadata.
// lets get the collaborators
let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
this,
ctx,
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
);
let 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 // prepare for final output
//@ts-ignore return [this.helpers.returnJsonArray(rows)];
.map((row: IRow) => rowFormatColumns(row, allColumns))
.map((row: IRow) => ({ json: row }));
}
if (Array.isArray(response) && response.length) {
return [response];
} }
return null; return null;

View file

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

View file

@ -0,0 +1,451 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import {
getTableColumns,
getTableViews,
rowExport,
rowFormatColumns,
rowMapKeyToName,
seaTableApiRequest,
setableApiRequestAllItems,
split,
updateAble,
} from './GenericFunctions';
import { rowFields, rowOperations } from './RowDescription';
import type { TColumnsUiValues, TColumnValue } from './types';
import type { ICtx, IRow, IRowObject } from './Interfaces';
export class SeaTableV1 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()) {
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,338 @@
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';
// 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());
}
}
});
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/versionDescription';
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';
base: 'snapshot' | 'metadata' | 'apiCall' | 'collaborator';
link: 'add' | '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,19 @@
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: '',
description: '',
},
];

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/image',
},
],
default: 'upload',
},
...upload.description,
...getPublicURL.description,
];

View file

@ -0,0 +1,104 @@
import type { AssetProperties } from '../../Interfaces';
export const assetUploadDescription: AssetProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
default: '',
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: 'Column',
name: 'uploadColumn',
type: 'options',
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getAssetColumns',
},
required: true,
default: '',
description: 'Select the column for the upload.',
},
{
displayName: 'Row ID',
name: 'rowId',
type: 'options',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
default: '',
},
{
displayName: 'Workspace ID',
name: 'workspaceId',
type: 'number',
typeOptions: {
minValue: 1,
numberStepSize: 1,
},
required: true,
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
default: '',
description:
'How to get the workspace ID: https://seatable.io/docs/arbeiten-mit-gruppen/workspace-id-einer-gruppe-ermitteln/?lang=auto',
},
{
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',
name: 'replace',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['asset'],
operation: ['upload'],
},
},
description: 'Replace existing file if the file/image already exists with same name.',
},*/
];

View file

@ -0,0 +1,89 @@
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[]> {
// step 1: upload file to base
const uploadColumn = this.getNodeParameter('uploadColumn', index) as any;
const uploadColumnType = uploadColumn.split(':::')[1];
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 workspaceId = this.getNodeParameter('workspaceId', index) as string;
const uploadLink = (await seaTableApiRequest.call(
this,
{},
'GET',
'/api/v2.1/dtable/app-upload-link/',
)) as IUploadLink;
// Get the binary data
const fileBufferData = await this.helpers.getBinaryDataBuffer(index, dataPropertyName);
const binaryData = this.helpers.assertBinaryData(index, dataPropertyName);
// Create our request option
const options = {
formData: {
file: {
value: fileBufferData,
options: {
filename: binaryData.fileName,
contentType: binaryData.mimeType,
},
},
parent_dir: uploadLink.parent_path,
replace: '0',
relative_path:
uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path,
},
};
// Send the request
let uploadAsset = await seaTableApiRequest.call(
this,
{},
'POST',
`/seafhttp/upload-api/${uploadLink.upload_link.split('seafhttp/upload-api/')[1]}?ret-json=true`,
{},
{},
'',
options,
);
// now step 2 (attaching the file 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 = [
`/workspace/${workspaceId}${uploadLink.parent_path}/${uploadLink.img_relative_path}/${uploadAsset[c].name}`,
];
if (uploadColumnType === 'image') {
rowInput[uploadColumn.split(':::')[0]] = filePath;
} else if (uploadColumnType === 'file') {
rowInput[uploadColumn.split(':::')[0]] = uploadAsset;
uploadAsset[c].type = 'file';
uploadAsset[c].url = filePath;
}
body.row = rowInput;
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
body,
);
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,136 @@
import type { BaseProperties } from '../../Interfaces';
export const baseApiCallDescription: BaseProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['base'],
operation: ['apiCall'],
},
},
default: '',
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: 'HTTP Method',
name: 'apiMethod',
type: 'options',
options: [
{
name: 'POST',
value: 'POST',
},
{
name: 'GET',
value: 'GET',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'DELETE',
value: 'DELETE',
},
],
displayOptions: {
show: {
resource: ['base'],
operation: ['apiCall'],
},
},
required: true,
default: '',
},
{
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.',
},
];

View file

@ -0,0 +1,15 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { seaTableApiRequest } from '../../../GenericFunctions';
export async function apiCall(
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 { 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.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,69 @@
import type { LinkProperties } from '../../Interfaces';
export const linkAddDescription: LinkProperties = [
{
displayName: 'Table Name (Source)',
name: 'tableName',
type: 'options',
placeholder: 'Name of table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
displayOptions: {
show: {
resource: ['link'],
operation: ['add'],
},
},
default: '',
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: 'Link column',
name: 'linkColumn',
type: 'options',
displayOptions: {
show: {
resource: ['link'],
operation: ['add'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getLinkColumns',
},
required: true,
default: '',
description: 'Select the column to create a link.',
},
{
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,25 @@
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 responseData = await seaTableApiRequest.call(
this,
{},
'POST',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/links/',
{
link_id: linkColumn.split(':::')[1],
table_id: tableName.split(':::')[1],
table_row_id: linkColumnSourceId,
other_table_id: linkColumn.split(':::')[2],
other_table_row_id: linkColumnTargetId,
},
);
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,36 @@
import * as add from './add';
import * as remove from './remove';
import type { INodeProperties } from 'n8n-workflow';
export { add, 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: 'Remove',
value: 'remove',
description: 'Remove a link between two rows from a link column',
action: 'Remove a row link',
},
],
default: 'add',
},
...add.description,
...remove.description,
];

View file

@ -0,0 +1,69 @@
import type { LinkProperties } from '../../Interfaces';
export const linkRemoveDescription: LinkProperties = [
{
displayName: 'Table Name (Source)',
name: 'tableName',
type: 'options',
placeholder: 'Name of table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNameAndId',
},
displayOptions: {
show: {
resource: ['link'],
operation: ['remove'],
},
},
default: '',
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: 'Link column',
name: 'linkColumn',
type: 'options',
displayOptions: {
show: {
resource: ['link'],
operation: ['remove'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getLinkColumns',
},
required: true,
default: '',
description: 'Select the column to create a link.',
},
{
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',
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,28 @@
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 responseData = await seaTableApiRequest.call(
this,
{},
'DELETE',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/links/',
{
link_id: linkColumn.split(':::')[1],
table_id: tableName.split(':::')[1],
table_row_id: linkColumnSourceId,
other_table_id: linkColumn.split(':::')[2],
other_table_row_id: linkColumnTargetId,
},
);
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,121 @@
import type { RowProperties } from '../../Interfaces';
export const rowCreateDescription: RowProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['create'],
},
},
default: '',
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: [
{
displayName: 'Column Name',
name: 'columnName',
type: 'options',
description:
'Choose from the list, or specify an ID 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',
},
{
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,62 @@
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 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);
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,54 @@
import type { RowProperties } from '../../Interfaces';
export const rowGetDescription: RowProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['get'],
},
},
default: '',
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: 'Row ID',
name: 'rowId',
type: 'options',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['get'],
},
},
default: '',
},
{
displayName: 'Simplify output',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: ['row'],
operation: ['get'],
},
},
default: true,
description:
'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.',
},
];

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,76 @@
import * as create from './create';
import * as get from './get';
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 };
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: 'Get',
value: 'get',
description: 'Get the content of a row',
action: 'Get a row',
},
{
name: 'Search',
value: 'search',
description: 'Search one or multiple rows',
action: 'Search a row by keyword',
},
{
name: 'Update',
value: 'update',
description: 'Update the content of a row',
action: 'Update a row',
},
{
name: 'Delete',
value: 'remove',
description: 'Delete a row',
action: 'Delete a row',
},
{
name: 'Lock',
value: 'lock',
description: 'Lock a row to prevent further changes.',
action: 'Add a row lock',
},
{
name: 'Unlock',
value: 'unlock',
description: 'Remove the lock from a row',
action: 'Remove a row lock',
},
],
default: 'create',
},
...create.description,
...get.description,
...search.description,
...update.description,
...remove.description,
...lock.description,
...unlock.description,
];

View file

@ -0,0 +1,40 @@
import type { RowProperties } from '../../Interfaces';
export const rowLockDescription: RowProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['lock'],
},
},
default: '',
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: 'Row ID',
name: 'rowId',
type: 'options',
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,40 @@
import type { RowProperties } from '../../Interfaces';
export const rowRemoveDescription: RowProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['remove'],
},
},
default: '',
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: 'Row ID',
name: 'rowId',
type: 'options',
required: true,
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getRowIds',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['remove'],
},
},
default: '',
},
];

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_id: rowId,
};
const responseData = await seaTableApiRequest.call(
this,
{},
'DELETE',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
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,83 @@
import type { RowProperties } from '../../Interfaces';
export const rowSearchDescription: RowProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
default: '',
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: 'Column',
name: 'searchColumn',
type: 'options',
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getSearchableColumns',
},
required: true,
default: '',
description: 'Select the column to be searched. Not all column types are supported for search.',
},
{
displayName: 'Search term',
name: 'searchTerm',
type: 'string',
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
required: true,
default: '',
description: 'What to look for?',
},
{
displayName: 'Activate wildcard search',
name: 'wildcard',
type: 'boolean',
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
default: false,
description:
'FALSE: The search only results perfect matches. TRUE: Finds a row even if the search value is part of a string.',
},
{
displayName: 'Simplify output',
name: 'simple',
type: 'boolean',
default: true,
displayOptions: {
show: {
resource: ['row'],
operation: ['search'],
},
},
description:
'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.',
},
];

View file

@ -0,0 +1,67 @@
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 any; // string or integer
const wildcard = this.getNodeParameter('wildcard', index) as boolean;
const simple = this.getNodeParameter('simple', index) as boolean;
// get collaborators
const collaborators = await getBaseCollaborators.call(this);
//let metadata: IDtableMetadataColumn[] = [];
//let rows: IRow[];
//let sqlResult: IRowResponse;
// get the collaborators (avoid executing this multiple times !!!!)
/*let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
);
let collaborators: ICollaborator[] = collaboratorsResult.user_list || [];
*/
// this is the base query. The WHERE has to be finalized...
let sqlQuery = `SELECT * FROM \`${tableName}\` WHERE \`${searchColumn}\``;
if (wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE "%' + searchTerm + '%"';
else if (!wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' = "' + searchTerm + '"';
else if (wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE %' + searchTerm + '%';
else if (!wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' = ' + searchTerm;
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,40 @@
import type { RowProperties } from '../../Interfaces';
export const rowUnlockDescription: RowProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['unlock'],
},
},
default: '',
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: 'Row ID',
name: 'rowId',
type: 'options',
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,137 @@
import type { RowProperties } from '../../Interfaces';
export const rowUpdateDescription: RowProperties = [
{
displayName: 'Table Name',
name: 'tableName',
type: 'options',
placeholder: 'Select a table',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
displayOptions: {
show: {
resource: ['row'],
operation: ['update'],
},
},
default: '',
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: 'Row ID',
name: 'rowId',
type: 'options',
required: true,
typeOptions: {
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: [
{
displayName: 'Column Name',
name: 'columnName',
type: 'options',
description:
'Choose from the list, or specify an ID 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',
},
{
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,63 @@
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;
const body = {
table_name: tableName,
row_id: rowId,
} 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);
body.row = rowInput;
const responseData = await seaTableApiRequest.call(
this,
{},
'PUT',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
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,227 @@
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[] = [];
let tableName = this.getCurrentNodeParameter('tableName') as string;
tableName = tableName.split(':::')[0];
//const tableId = (tableName.split(':::')[1] ? tableName.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') {
returnData.push({
name: col.name,
value: col.name + ':::' + col.data.link_id + ':::' + col.data.other_table_id,
});
}
}
}
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 tableName = this.getNodeParameter('tableName') as string;
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,97 @@
// ----------------------------------
// 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;
}>;