n8n/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

289 lines
8.2 KiB
TypeScript
Raw Normal View History

import type {
IPollFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
2023-10-25 17:20:43 -07:00
import { seaTableApiRequest, simplify_new, enrichColumns } from './v2/GenericFunctions';
2023-10-25 17:20:43 -07:00
import type {
ICtx,
IRow,
IRowResponse,
IGetMetadataResult,
IGetRowsResult,
IDtableMetadataColumn,
ICollaborator,
ICollaboratorsResult,
IColumnDigitalSignature,
} from './v2/actions/Interfaces';
import moment from 'moment';
2023-10-25 17:20:43 -07:00
import { loadOptions } from './v2/methods';
export class SeaTableTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'SeaTable Trigger',
name: 'seaTableTrigger',
2023-10-25 17:20:43 -07:00
icon: 'file:seatable.svg',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when SeaTable events occur',
subtitle: '={{$parameter["event"]}}',
defaults: {
name: 'SeaTable Trigger',
},
credentials: [
{
name: 'seaTableApi',
required: true,
},
],
polling: true,
inputs: [],
outputs: ['main'],
properties: [
2023-10-25 17:20:43 -07:00
{
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',
},
{
refactor: Apply more `eslint-plugin-n8n-nodes-base` autofixable rules (#3432) * :zap: Update `lintfix` script * :shirt: Remove unneeded lint exceptions * :shirt: Run baseline `lintfix` * :shirt: Apply `node-param-description-miscased-url` (#3441) * :shirt: Apply `rule node-param-placeholder-miscased-id` (#3443) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-option-name-wrong-for-upsert` (#3446) * :shirt: Apply `node-param-min-value-wrong-for-limit` (#3442) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * Apply `node-param-display-name-wrong-for-dynamic-options` (#3454) * :hammer: fix * :zap: Fix `Assigned To` fields Co-authored-by: Michael Kret <michael.k@radency.com> * :shirt: Apply `rule node-param-default-wrong-for-number` (#3453) * :shirt: Apply `node-param-default-wrong-for-string` (#3452) Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * Apply `node-param-display-name-miscased` (#3449) * :hammer: fix * :hammer: exceptions * :zap: review fixes * :shirt: Apply `node-param-description-lowercase-first-char` (#3451) * :zap: fix * :zap: review fixes * :zap: fix Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Apply `node-param-description-wrong-for-dynamic-options` (#3456) * Rule working as intended * Add rule * :fire: Remove repetitions * :shirt: Add exceptions Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :shirt: Small fix for `node-param-description-wrong-for-dynamic-options` * :shirt: Apply `node-param-default-wrong-for-fixed-collection` (#3460) * :shirt: Apply `node-param-description-line-break-html-tag` (#3462) * :shirt: Run baseline `lintfix` * :shirt: Apply `node-param-options-type-unsorted-items` (#3459) * :zap: fix * :hammer: exceptions * Add exception for Salesmate and Zoom Co-authored-by: Michael Kret <michael.k@radency.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com> * :zap: Restore `lintfix` command Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: agobrech <45268029+agobrech@users.noreply.github.com> Co-authored-by: Michael Kret <michael.k@radency.com> Co-authored-by: brianinoa <54530642+brianinoa@users.noreply.github.com>
2022-06-03 10:23:49 -07:00
displayName: 'Table Name or ID',
name: 'tableName',
type: 'options',
required: true,
typeOptions: {
loadOptionsMethod: 'getTableNames',
},
default: '',
description:
'The name of SeaTable table to access. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
},
{
2023-10-25 17:20:43 -07:00
displayName: 'View Name or ID (optional)',
name: 'viewName',
type: 'options',
2023-10-25 17:20:43 -07:00
required: false,
displayOptions: {
show: {
event: ['newRow', 'updatedRow'],
},
2023-10-25 17:20:43 -07:00
},
typeOptions: {
loadOptionsDependsOn: ['tableName'],
loadOptionsMethod: 'getTableViews',
},
default: '',
description: 'The name of SeaTable view to access. Choose from the list, or specify ...',
},
{
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.',
},
{
2023-10-25 17:20:43 -07:00
displayName: 'Simplify output',
name: 'simple',
type: 'boolean',
default: true,
description:
2023-10-25 17:20:43 -07:00
'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: '',
},
],
};
2023-10-25 17:20:43 -07:00
methods = { loadOptions };
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const webhookData = this.getWorkflowStaticData('node');
2023-10-25 17:20:43 -07:00
const event = this.getNodeParameter('event') as string;
const tableName = this.getNodeParameter('tableName') as string;
2023-10-25 17:20:43 -07:00
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;
2023-10-25 17:20:43 -07:00
const ctx: ICtx = {};
2023-10-25 17:20:43 -07:00
const startDate =
this.getMode() === 'manual'
? moment().utc().subtract(1, 'h').format()
: (webhookData.lastTimeChecked as string);
const endDate = (webhookData.lastTimeChecked = moment().utc().format());
// this is working, even if the columns _mtime and _ctime have other names. Only relevant for newRow / updatedRow.
const filterField = event === 'newRow' ? '_ctime' : '_mtime';
// Difference between getRows and SqlQuery:
// ====================
// 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!
// SqlQuery (if no view is selected)
// SqlQuery returns up to 1.000. WHERE by time and ORDER BY _ctime or _mtime is possible.
// SqlQuery returns rows and metadata
let requestMeta: IGetMetadataResult;
let requestRows: IGetRowsResult;
let metadata: IDtableMetadataColumn[] = [];
let rows: IRow[];
let sqlResult: 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);
}
}
}
}
}
2023-10-25 17:20:43 -07:00
// 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,
},
);
// I need only metadata of the selected table.
metadata =
requestMeta.metadata.tables.find((table) => table.name === tableName)?.columns ?? [];
2023-10-25 17:20:43 -07:00
// remove unwanted rows that are too old (compare startDate with _ctime or _mtime)
if (this.getMode() === 'manual') {
rows = requestRows.rows as IRow[];
} else {
2023-10-25 17:20:43 -07:00
rows = requestRows.rows.filter(
(obj) => new Date(obj[filterField]) > new Date(startDate),
) as IRow[];
}
2023-10-25 17:20:43 -07:00
}
2023-10-25 17:20:43 -07:00
// 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[];
}
2023-10-25 17:20:43 -07:00
// =========================================
// => 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));
}
// enrich column types like {collaborator, creator, last_modifier}, {image, file}
// remove button column from rows
rows = rows.map((row) => enrichColumns(row, metadata, collaborators));
// prepare for final output
return [this.helpers.returnJsonArray(rows)];
}
return null;
}
}