mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(SeaTable Node): Update node with new options (#11431)
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import moment from 'moment-timezone';
|
||||
import type { ICredentialType, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
|
||||
import type {
|
||||
ICredentialTestRequest,
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
INodePropertyOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
// Get options for timezones
|
||||
const timezones: INodePropertyOptions[] = moment.tz
|
||||
@@ -40,7 +45,7 @@ export class SeaTableApi implements ICredentialType {
|
||||
name: 'domain',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'https://www.mydomain.com',
|
||||
placeholder: 'https://seatable.example.com',
|
||||
displayOptions: {
|
||||
show: {
|
||||
environment: ['selfHosted'],
|
||||
@@ -51,6 +56,8 @@ export class SeaTableApi implements ICredentialType {
|
||||
displayName: 'API Token (of a Base)',
|
||||
name: 'token',
|
||||
type: 'string',
|
||||
description:
|
||||
'The API-Token of the SeaTable base you would like to use with n8n. n8n can only connect to one base at a time.',
|
||||
typeOptions: { password: true },
|
||||
default: '',
|
||||
},
|
||||
@@ -63,4 +70,14 @@ export class SeaTableApi implements ICredentialType {
|
||||
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}}',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,449 +1,27 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
getTableColumns,
|
||||
getTableViews,
|
||||
rowExport,
|
||||
rowFormatColumns,
|
||||
rowMapKeyToName,
|
||||
seaTableApiRequest,
|
||||
setableApiRequestAllItems,
|
||||
split,
|
||||
updateAble,
|
||||
} from './GenericFunctions';
|
||||
import type { ICtx, IRow, IRowObject } from './Interfaces';
|
||||
import { rowFields, rowOperations } from './RowDescription';
|
||||
import type { TColumnsUiValues, TColumnValue } from './types';
|
||||
import { SeaTableV1 } from './v1/SeaTableV1.node';
|
||||
import { SeaTableV2 } from './v2/SeaTableV2.node';
|
||||
|
||||
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',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Row',
|
||||
value: 'row',
|
||||
},
|
||||
],
|
||||
default: 'row',
|
||||
},
|
||||
...rowOperations,
|
||||
...rowFields,
|
||||
],
|
||||
};
|
||||
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,
|
||||
usableAsTool: true,
|
||||
};
|
||||
|
||||
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;
|
||||
},
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new SeaTableV1(baseDescription),
|
||||
2: new SeaTableV2(baseDescription),
|
||||
};
|
||||
|
||||
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];
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,157 +1,25 @@
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
type IPollFunctions,
|
||||
type ILoadOptionsFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodePropertyOptions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
NodeConnectionType,
|
||||
} from 'n8n-workflow';
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { getColumns, rowFormatColumns, seaTableApiRequest, simplify } from './GenericFunctions';
|
||||
import type { ICtx, IRow, IRowResponse } from './Interfaces';
|
||||
import { SeaTableTriggerV1 } from './v1/SeaTableTriggerV1.node';
|
||||
import { SeaTableTriggerV2 } from './v2/SeaTableTriggerV2.node';
|
||||
|
||||
export class SeaTableTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'SeaTable Trigger',
|
||||
name: 'seaTableTrigger',
|
||||
icon: 'file:seaTable.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when SeaTable events occur',
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
defaults: {
|
||||
name: 'SeaTable Trigger',
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
polling: true,
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Table Name or ID',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Row Created',
|
||||
value: 'rowCreated',
|
||||
description: 'Trigger on newly created rows',
|
||||
},
|
||||
// {
|
||||
// name: 'Row Modified',
|
||||
// value: 'rowModified',
|
||||
// description: 'Trigger has recently modified rows',
|
||||
// },
|
||||
],
|
||||
default: 'rowCreated',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
],
|
||||
};
|
||||
export class SeaTableTrigger extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'SeaTable Trigger',
|
||||
name: 'seaTableTrigger',
|
||||
icon: 'file:seaTable.svg',
|
||||
group: ['trigger'],
|
||||
defaultVersion: 2,
|
||||
description: 'Starts the workflow when SeaTable events occur',
|
||||
};
|
||||
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new SeaTableTriggerV1(baseDescription),
|
||||
2: new SeaTableTriggerV2(baseDescription),
|
||||
};
|
||||
|
||||
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const simple = this.getNodeParameter('simple') as boolean;
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const ctx: ICtx = {};
|
||||
const credentials = await this.getCredentials('seaTableApi');
|
||||
|
||||
const timezone = (credentials.timezone as string) || 'Europe/Berlin';
|
||||
const now = moment().utc().format();
|
||||
const startDate = (webhookData.lastTimeChecked as string) || now;
|
||||
const endDate = now;
|
||||
webhookData.lastTimeChecked = endDate;
|
||||
|
||||
let rows;
|
||||
|
||||
const filterField = event === 'rowCreated' ? '_ctime' : '_mtime';
|
||||
|
||||
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/';
|
||||
|
||||
if (this.getMode() === 'manual') {
|
||||
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
|
||||
sql: `SELECT * FROM ${tableName} LIMIT 1`,
|
||||
})) as IRowResponse;
|
||||
} else {
|
||||
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
|
||||
sql: `SELECT * FROM ${tableName}
|
||||
WHERE ${filterField} BETWEEN "${moment(startDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"
|
||||
AND "${moment(endDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"`,
|
||||
})) as IRowResponse;
|
||||
}
|
||||
|
||||
let response;
|
||||
|
||||
if (rows.metadata && rows.results) {
|
||||
const columns = getColumns(rows);
|
||||
if (simple) {
|
||||
response = simplify(rows, columns);
|
||||
} else {
|
||||
response = rows.results;
|
||||
}
|
||||
|
||||
const allColumns = rows.metadata.map((meta) => meta.name);
|
||||
|
||||
response = response
|
||||
//@ts-ignore
|
||||
.map((row: IRow) => rowFormatColumns(row, allColumns))
|
||||
.map((row: IRow) => ({ json: row }));
|
||||
}
|
||||
|
||||
if (Array.isArray(response) && response.length) {
|
||||
return [response];
|
||||
}
|
||||
|
||||
return null;
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import type {
|
||||
ICollaborator,
|
||||
IDtableMetadataColumn,
|
||||
IRow,
|
||||
IRowObject,
|
||||
IColumnDigitalSignature,
|
||||
} from '../../v2/actions/Interfaces';
|
||||
import {
|
||||
enrichColumns,
|
||||
rowExport,
|
||||
simplify_new,
|
||||
splitStringColumnsToArrays,
|
||||
} from '../../v2/GenericFunctions';
|
||||
import type { TDtableMetadataColumns } from '../../v2/types';
|
||||
|
||||
describe('Seatable > v2 > GenericFunctions', () => {
|
||||
describe('rowExport', () => {
|
||||
const mockColumns: TDtableMetadataColumns = [
|
||||
{ key: 'a', name: 'id', type: 'text' },
|
||||
{ key: 'b', name: 'name', type: 'text' },
|
||||
{ key: 'c', name: 'age', type: 'number' },
|
||||
];
|
||||
|
||||
it('should export only allowed columns from row', () => {
|
||||
const row: IRowObject = {
|
||||
id: '1',
|
||||
name: 'John',
|
||||
age: 30,
|
||||
extraField: 'should not be included',
|
||||
};
|
||||
|
||||
const expected: IRowObject = {
|
||||
id: '1',
|
||||
name: 'John',
|
||||
age: 30,
|
||||
};
|
||||
|
||||
expect(rowExport(row, mockColumns)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle empty row', () => {
|
||||
const row: IRowObject = {};
|
||||
expect(rowExport(row, mockColumns)).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle row with missing fields', () => {
|
||||
const row: IRowObject = {
|
||||
id: '1',
|
||||
// name is missing
|
||||
age: 30,
|
||||
};
|
||||
|
||||
const expected: IRowObject = {
|
||||
id: '1',
|
||||
age: 30,
|
||||
};
|
||||
|
||||
expect(rowExport(row, mockColumns)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitStringColumnsToArrays', () => {
|
||||
it('should convert collaborator strings to arrays', () => {
|
||||
const columns: TDtableMetadataColumns = [
|
||||
{ key: 'a', name: 'collaborators', type: 'collaborator' },
|
||||
];
|
||||
const row: IRowObject = {
|
||||
collaborators: 'john@example.com, jane@example.com',
|
||||
};
|
||||
|
||||
const result = splitStringColumnsToArrays(row, columns);
|
||||
expect(result.collaborators).toEqual(['john@example.com', 'jane@example.com']);
|
||||
});
|
||||
|
||||
it('should convert multiple-select strings to arrays', () => {
|
||||
const columns: TDtableMetadataColumns = [{ key: 'a', name: 'tags', type: 'multiple-select' }];
|
||||
const row: IRowObject = {
|
||||
tags: 'urgent, important',
|
||||
};
|
||||
|
||||
const result = splitStringColumnsToArrays(row, columns);
|
||||
expect(result.tags).toEqual(['urgent', 'important']);
|
||||
});
|
||||
|
||||
it('should convert number strings to numbers', () => {
|
||||
const columns: TDtableMetadataColumns = [{ key: 'a', name: 'amount', type: 'number' }];
|
||||
const row: IRowObject = {
|
||||
amount: '123.45',
|
||||
};
|
||||
|
||||
const result = splitStringColumnsToArrays(row, columns);
|
||||
expect(result.amount).toBe(123.45);
|
||||
});
|
||||
|
||||
it('should convert rate and duration strings to integers', () => {
|
||||
const columns: TDtableMetadataColumns = [
|
||||
{ key: 'a', name: 'rating', type: 'rate' },
|
||||
{ key: 'b', name: 'duration', type: 'duration' },
|
||||
];
|
||||
const row: IRowObject = {
|
||||
rating: '4',
|
||||
duration: '60',
|
||||
};
|
||||
|
||||
const result = splitStringColumnsToArrays(row, columns);
|
||||
expect(result.rating).toBe(4);
|
||||
expect(result.duration).toBe(60);
|
||||
});
|
||||
|
||||
it('should convert checkbox strings to booleans', () => {
|
||||
const columns: TDtableMetadataColumns = [{ key: 'a', name: 'isActive', type: 'checkbox' }];
|
||||
const testCases = [
|
||||
{ input: 'true', expected: true },
|
||||
{ input: 'on', expected: true },
|
||||
{ input: '1', expected: true },
|
||||
{ input: 'false', expected: false },
|
||||
{ input: 'off', expected: false },
|
||||
{ input: '0', expected: false },
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
const row: IRowObject = { isActive: input };
|
||||
const result = splitStringColumnsToArrays(row, columns);
|
||||
expect(result.isActive).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple column types in one row', () => {
|
||||
const columns: TDtableMetadataColumns = [
|
||||
{ key: 'a', name: 'tags', type: 'multiple-select' },
|
||||
{ key: 'b', name: 'amount', type: 'number' },
|
||||
{ key: 'c', name: 'isActive', type: 'checkbox' },
|
||||
];
|
||||
const row: IRowObject = {
|
||||
tags: 'tag1, tag2',
|
||||
amount: '123.45',
|
||||
isActive: 'true',
|
||||
};
|
||||
|
||||
const result = splitStringColumnsToArrays(row, columns);
|
||||
expect(result).toEqual({
|
||||
tags: ['tag1', 'tag2'],
|
||||
amount: 123.45,
|
||||
isActive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty/invalid inputs', () => {
|
||||
const columns: TDtableMetadataColumns = [
|
||||
{ key: 'a', name: 'empty', type: 'multiple-select' },
|
||||
{ key: 'b', name: 'invalid', type: 'number' },
|
||||
];
|
||||
const row: IRowObject = {
|
||||
empty: '',
|
||||
invalid: 'not-a-number',
|
||||
};
|
||||
|
||||
const result = splitStringColumnsToArrays(row, columns);
|
||||
expect(result.empty).toEqual(['']);
|
||||
expect(result.invalid).toBeNaN();
|
||||
});
|
||||
});
|
||||
|
||||
describe('enrichColumns', () => {
|
||||
const baseRow = {
|
||||
_id: '1234',
|
||||
_ctime: '2024-01-01T00:00:00Z',
|
||||
_mtime: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
const mockCollaborators: ICollaborator[] = [
|
||||
{
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
contact_email: 'john@example.com',
|
||||
},
|
||||
{
|
||||
name: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
contact_email: 'jane@example.com',
|
||||
},
|
||||
];
|
||||
|
||||
const mockMetadata: IDtableMetadataColumn[] = [
|
||||
{ name: 'assignee', type: 'collaborator', key: 'assignee' },
|
||||
{ name: 'creator', type: 'creator', key: '_creator' },
|
||||
{ name: 'lastModifier', type: 'last-modifier', key: '_last_modifier' },
|
||||
{ name: 'images', type: 'image', key: 'images' },
|
||||
{ name: 'files', type: 'file', key: 'files' },
|
||||
{ name: 'signature', type: 'digital-sign', key: 'signature' },
|
||||
{ name: 'action', type: 'button', key: 'action' },
|
||||
];
|
||||
|
||||
it('should preserve base IRow properties', () => {
|
||||
const row: IRow = {
|
||||
...baseRow,
|
||||
assignee: ['john@example.com'],
|
||||
};
|
||||
|
||||
const result = enrichColumns(row, mockMetadata, mockCollaborators);
|
||||
expect(result._id).toBe(baseRow._id);
|
||||
expect(result._ctime).toBe(baseRow._ctime);
|
||||
expect(result._mtime).toBe(baseRow._mtime);
|
||||
});
|
||||
|
||||
it('should enrich collaborator columns', () => {
|
||||
const row: IRow = {
|
||||
...baseRow,
|
||||
assignee: ['john@example.com'],
|
||||
};
|
||||
|
||||
const result = enrichColumns(row, mockMetadata, mockCollaborators);
|
||||
expect(result.assignee).toEqual([
|
||||
{
|
||||
email: 'john@example.com',
|
||||
contact_email: 'john@example.com',
|
||||
name: 'John Doe',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should enrich creator and last-modifier columns', () => {
|
||||
const row: IRow = {
|
||||
...baseRow,
|
||||
creator: 'john@example.com',
|
||||
lastModifier: 'jane@example.com',
|
||||
};
|
||||
|
||||
const result = enrichColumns(row, mockMetadata, mockCollaborators);
|
||||
expect(result.creator).toEqual({
|
||||
email: 'john@example.com',
|
||||
contact_email: 'john@example.com',
|
||||
name: 'John Doe',
|
||||
});
|
||||
});
|
||||
|
||||
it('should enrich image columns', () => {
|
||||
const row: IRow = {
|
||||
...baseRow,
|
||||
images: ['https://example.com/image.jpg'],
|
||||
};
|
||||
|
||||
const result = enrichColumns(row, mockMetadata, mockCollaborators);
|
||||
expect(result.images).toEqual([
|
||||
{
|
||||
name: 'image.jpg',
|
||||
size: 0,
|
||||
type: 'image',
|
||||
url: 'https://example.com/image.jpg',
|
||||
path: 'https://example.com/image.jpg',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty/missing data gracefully', () => {
|
||||
const row: IRow = {
|
||||
...baseRow,
|
||||
assignee: [],
|
||||
images: [],
|
||||
files: [],
|
||||
signature: {} as IColumnDigitalSignature,
|
||||
};
|
||||
|
||||
const result = enrichColumns(row, mockMetadata, mockCollaborators);
|
||||
expect(result.assignee).toEqual([]);
|
||||
expect(result.images).toEqual([]);
|
||||
expect(result.files).toEqual([]);
|
||||
expect(result.signature).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('simplify_new', () => {
|
||||
it('should remove keys starting with underscore', () => {
|
||||
const input: IRow = {
|
||||
_id: '123',
|
||||
_ctime: '2024-01-01',
|
||||
_mtime: '2024-01-01',
|
||||
name: 'Test',
|
||||
value: 42,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
name: 'Test',
|
||||
value: 42,
|
||||
};
|
||||
|
||||
expect(simplify_new(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle empty object', () => {
|
||||
const input: IRow = {
|
||||
_id: '123',
|
||||
_ctime: '2024-01-01',
|
||||
_mtime: '2024-01-01',
|
||||
};
|
||||
|
||||
expect(simplify_new(input)).toEqual({});
|
||||
});
|
||||
|
||||
it('should preserve non-underscore keys', () => {
|
||||
const input: IRow = {
|
||||
_id: '123',
|
||||
_ctime: '2024-01-01',
|
||||
_mtime: '2024-01-01',
|
||||
normal_key: 'value',
|
||||
dash_key: 'value',
|
||||
};
|
||||
|
||||
const expected = {
|
||||
normal_key: 'value',
|
||||
dash_key: 'value',
|
||||
};
|
||||
|
||||
expect(simplify_new(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><path fill="url(#a)" d="M16.787 43.213 28.574 55l16.943-16.942a7.87 7.87 0 0 0 0-11.132l-6.22-6.221-.112-.111-18.611 18.57.13.131z"/><path fill="#ff8000" d="m20.704 39.295 22.51-22.507L31.425 5 14.483 21.942a7.87 7.87 0 0 0 0 11.133z"/><defs><linearGradient id="a" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-109.048 29.213 6.813)scale(10.08407)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff8000"/><stop offset="1" stop-color="#ec2837"/></linearGradient></defs></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><path d="M16.787 43.213L28.574 55l16.943-16.942a7.872 7.872 0 000-11.132l-6.22-6.221-.112-.111-18.611 18.57.13.131z" fill="url(#g1)"/><path d="M20.704 39.295l22.51-22.507L31.425 5 14.483 21.942a7.872 7.872 0 000 11.133z" fill="#ff8000"/><defs id="d1"><linearGradient id="g1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="scale(-10.08407) rotate(70.952 .948 -4.065)"><stop offset="0" id="stop905" stop-color="#ff8000" stop-opacity="1"/><stop offset="1" id="stop907" stop-color="#ec2837" stop-opacity="1"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 629 B |
@@ -3,9 +3,9 @@ import type {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IPollFunctions,
|
||||
JsonObject,
|
||||
IHttpRequestMethods,
|
||||
IRequestOptions,
|
||||
IHttpRequestMethods,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
@@ -64,8 +64,8 @@ function endpointCtxExpr(ctx: ICtx, endpoint: string): string {
|
||||
endpointVariables.dtable_uuid = ctx?.base?.dtable_uuid;
|
||||
|
||||
return endpoint.replace(
|
||||
/({{ *(access_token|dtable_uuid|server) *}})/g,
|
||||
(match: string, _: string, name: TEndpointVariableName) => {
|
||||
/{{ *(access_token|dtable_uuid|server) *}}/g,
|
||||
(match: string, name: TEndpointVariableName) => {
|
||||
return endpointVariables[name] || match;
|
||||
},
|
||||
);
|
||||
@@ -76,7 +76,6 @@ export async function seaTableApiRequest(
|
||||
ctx: ICtx,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
|
||||
body: any = {},
|
||||
qs: IDataObject = {},
|
||||
url: string | undefined = undefined,
|
||||
@@ -49,10 +49,11 @@ export const rowFields: INodeProperties[] = [
|
||||
// ----------------------------------
|
||||
|
||||
{
|
||||
displayName: 'Table Name or ID',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Name of table',
|
||||
placeholder: 'Name of the table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
@@ -63,14 +64,16 @@ export const rowFields: INodeProperties[] = [
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
'The name of SeaTable table to access. Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Table Name or ID',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Table ID',
|
||||
name: 'tableId',
|
||||
type: 'options',
|
||||
placeholder: 'Name of table',
|
||||
placeholder: 'ID of the table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableIds',
|
||||
@@ -81,6 +84,7 @@ export const rowFields: INodeProperties[] = [
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
@@ -157,11 +161,13 @@ export const rowFields: INodeProperties[] = [
|
||||
name: 'columnValues',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Column Name or ID',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Column Name',
|
||||
name: 'columnName',
|
||||
type: 'options',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
'Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['table'],
|
||||
loadOptionsMethod: 'getTableUpdateAbleColumns',
|
||||
@@ -243,7 +249,6 @@ export const rowFields: INodeProperties[] = [
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
@@ -261,11 +266,13 @@ export const rowFields: INodeProperties[] = [
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'View Name or ID',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'View Name',
|
||||
name: 'view_name',
|
||||
type: 'options',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
'Choose from the list, or specify an View Name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getViews',
|
||||
},
|
||||
@@ -291,7 +298,7 @@ export const rowFields: INodeProperties[] = [
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the link column in the returned row is the ID of the linked row or the name of the linked row',
|
||||
'Whether the ID of the linked row is returned in the link column (true). Otherwise, it return the name of the linked row (false).',
|
||||
},
|
||||
{
|
||||
displayName: 'Direction',
|
||||
@@ -312,15 +319,16 @@ export const rowFields: INodeProperties[] = [
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Order By',
|
||||
displayName: 'Order By Column',
|
||||
name: 'order_by',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAllSortableColumns',
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'A column\'s name or ID, use this column to sort the rows. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
'Choose from the list, or specify a Column using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
},
|
||||
],
|
||||
},
|
||||
41
packages/nodes-base/nodes/SeaTable/v1/SeaTable.node.ts
Normal file
41
packages/nodes-base/nodes/SeaTable/v1/SeaTable.node.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NodeConnectionType, type INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import { rowFields, rowOperations } from './RowDescription';
|
||||
|
||||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'SeaTable',
|
||||
name: 'seaTable',
|
||||
icon: 'file:seaTable.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
||||
description: 'Consume the SeaTable API',
|
||||
defaults: {
|
||||
name: 'SeaTable',
|
||||
},
|
||||
inputs: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Row',
|
||||
value: 'row',
|
||||
},
|
||||
],
|
||||
default: 'row',
|
||||
},
|
||||
...rowOperations,
|
||||
...rowFields,
|
||||
],
|
||||
};
|
||||
158
packages/nodes-base/nodes/SeaTable/v1/SeaTableTriggerV1.node.ts
Normal file
158
packages/nodes-base/nodes/SeaTable/v1/SeaTableTriggerV1.node.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
type IPollFunctions,
|
||||
type ILoadOptionsFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodePropertyOptions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
NodeConnectionType,
|
||||
type INodeTypeBaseDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getColumns, rowFormatColumns, seaTableApiRequest, simplify } from './GenericFunctions';
|
||||
import type { ICtx, IRow, IRowResponse } from './Interfaces';
|
||||
|
||||
export class SeaTableTriggerV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
defaults: {
|
||||
name: 'SeaTable Trigger',
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
polling: true,
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Table Name or ID',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Row Created',
|
||||
value: 'rowCreated',
|
||||
description: 'Trigger on newly created rows',
|
||||
},
|
||||
// {
|
||||
// name: 'Row Modified',
|
||||
// value: 'rowModified',
|
||||
// description: 'Trigger has recently modified rows',
|
||||
// },
|
||||
],
|
||||
default: 'rowCreated',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getTableNames(this: ILoadOptionsFunctions) {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table.name,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const simple = this.getNodeParameter('simple') as boolean;
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const ctx: ICtx = {};
|
||||
const credentials = await this.getCredentials('seaTableApi');
|
||||
|
||||
const timezone = (credentials.timezone as string) || 'Europe/Berlin';
|
||||
const now = moment().utc().format();
|
||||
const startDate = (webhookData.lastTimeChecked as string) || now;
|
||||
const endDate = now;
|
||||
webhookData.lastTimeChecked = endDate;
|
||||
|
||||
let rows;
|
||||
|
||||
const filterField = event === 'rowCreated' ? '_ctime' : '_mtime';
|
||||
|
||||
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/';
|
||||
|
||||
if (this.getMode() === 'manual') {
|
||||
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
|
||||
sql: `SELECT * FROM ${tableName} LIMIT 1`,
|
||||
})) as IRowResponse;
|
||||
} else {
|
||||
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
|
||||
sql: `SELECT * FROM ${tableName}
|
||||
WHERE ${filterField} BETWEEN "${moment(startDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"
|
||||
AND "${moment(endDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"`,
|
||||
})) as IRowResponse;
|
||||
}
|
||||
|
||||
let response;
|
||||
|
||||
if (rows.metadata && rows.results) {
|
||||
const columns = getColumns(rows);
|
||||
if (simple) {
|
||||
response = simplify(rows, columns);
|
||||
} else {
|
||||
response = rows.results;
|
||||
}
|
||||
|
||||
const allColumns = rows.metadata.map((meta) => meta.name);
|
||||
|
||||
response = response
|
||||
//@ts-ignore
|
||||
.map((row: IRow) => rowFormatColumns(row, allColumns))
|
||||
.map((row: IRow) => ({ json: row }));
|
||||
}
|
||||
|
||||
if (Array.isArray(response) && response.length) {
|
||||
return [response];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
420
packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts
Normal file
420
packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeTypeBaseDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
getTableColumns,
|
||||
getTableViews,
|
||||
rowExport,
|
||||
rowFormatColumns,
|
||||
rowMapKeyToName,
|
||||
seaTableApiRequest,
|
||||
setableApiRequestAllItems,
|
||||
split,
|
||||
updateAble,
|
||||
} from './GenericFunctions';
|
||||
import type { ICtx, IRow, IRowObject } from './Interfaces';
|
||||
import { versionDescription } from './SeaTable.node';
|
||||
import type { TColumnsUiValues, TColumnValue } from './types';
|
||||
|
||||
export class SeaTableV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getTableNames(this: ILoadOptionsFunctions) {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table.name,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getTableIds(this: ILoadOptionsFunctions) {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table._id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
|
||||
async getTableUpdateAbleColumns(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const columns = await getTableColumns.call(this, tableName);
|
||||
return columns
|
||||
.filter((column) => column.editable)
|
||||
.map((column) => ({ name: column.name, value: column.name }));
|
||||
},
|
||||
async getAllSortableColumns(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const columns = await getTableColumns.call(this, tableName);
|
||||
return columns
|
||||
.filter(
|
||||
(column) =>
|
||||
!['file', 'image', 'url', 'collaborator', 'long-text'].includes(column.type),
|
||||
)
|
||||
.map((column) => ({ name: column.name, value: column.name }));
|
||||
},
|
||||
async getViews(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const views = await getTableViews.call(this, tableName);
|
||||
return views.map((view) => ({ name: view.name, value: view.name }));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
let responseData;
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
const body: IDataObject = {};
|
||||
const qs: IDataObject = {};
|
||||
const ctx: ICtx = {};
|
||||
|
||||
if (resource === 'row') {
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// row:create
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
body.table_name = tableName;
|
||||
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', 0) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
let rowInput: IRowObject = {};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
rowInput = {} as IRowObject;
|
||||
try {
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const incomingKeys = Object.keys(items[i].json);
|
||||
const inputDataToIgnore = split(
|
||||
this.getNodeParameter('inputsToIgnore', i, '') as string,
|
||||
);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[i].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter(
|
||||
'columnsUi.columnValues',
|
||||
i,
|
||||
[],
|
||||
) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
body.row = rowExport(rowInput, updateAble(tableColumns));
|
||||
|
||||
responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'POST',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
const { _id: insertId } = responseData;
|
||||
if (insertId === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'SeaTable: No identity after appending row.',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
const newRowInsertData = rowMapKeyToName(responseData as IRow, tableColumns);
|
||||
|
||||
qs.table_name = tableName;
|
||||
qs.convert = true;
|
||||
const newRow = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
`/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/${encodeURIComponent(
|
||||
insertId as string,
|
||||
)}/`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
if (newRow._id === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'SeaTable: No identity for appended row.',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
const row = rowFormatColumns(
|
||||
{ ...newRowInsertData, ...(newRow as IRow) },
|
||||
tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']),
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(row),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'get') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const tableId = this.getNodeParameter('tableId', 0) as string;
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
const response = (await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
`/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/${rowId}`,
|
||||
{},
|
||||
{ table_id: tableId, convert: true },
|
||||
)) as IDataObject;
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'getAll') {
|
||||
// ----------------------------------
|
||||
// row:getAll
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const endpoint = '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/';
|
||||
qs.table_name = tableName;
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
const options = this.getNodeParameter('options', i);
|
||||
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||
|
||||
Object.assign(qs, filters, options);
|
||||
|
||||
if (qs.convert_link_id === false) {
|
||||
delete qs.convert_link_id;
|
||||
}
|
||||
|
||||
if (returnAll) {
|
||||
responseData = await setableApiRequestAllItems.call(
|
||||
this,
|
||||
ctx,
|
||||
'rows',
|
||||
'GET',
|
||||
endpoint,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.limit = this.getNodeParameter('limit', 0);
|
||||
responseData = await seaTableApiRequest.call(this, ctx, 'GET', endpoint, body, qs);
|
||||
responseData = responseData.rows;
|
||||
}
|
||||
|
||||
const rows = responseData.map((row: IRow) =>
|
||||
rowFormatColumns(
|
||||
{ ...row },
|
||||
tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']),
|
||||
),
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(rows as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'delete') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
const requestBody: IDataObject = {
|
||||
table_name: tableName,
|
||||
row_id: rowId,
|
||||
};
|
||||
const response = (await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'DELETE',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
requestBody,
|
||||
qs,
|
||||
)) as IDataObject;
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// row:update
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
body.table_name = tableName;
|
||||
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', 0) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
let rowInput: IRowObject = {};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
rowInput = {} as IRowObject;
|
||||
try {
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const incomingKeys = Object.keys(items[i].json);
|
||||
const inputDataToIgnore = split(
|
||||
this.getNodeParameter('inputsToIgnore', i, '') as string,
|
||||
);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[i].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter(
|
||||
'columnsUi.columnValues',
|
||||
i,
|
||||
[],
|
||||
) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
body.row = rowExport(rowInput, updateAble(tableColumns));
|
||||
body.table_name = tableName;
|
||||
body.row_id = rowId;
|
||||
responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'PUT',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ _id: rowId, ...(responseData as IDataObject) }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`);
|
||||
}
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
42
packages/nodes-base/nodes/SeaTable/v1/VersionDescription.ts
Normal file
42
packages/nodes-base/nodes/SeaTable/v1/VersionDescription.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import { NodeConnectionType, type INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import { rowFields, rowOperations } from './RowDescription';
|
||||
|
||||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'SeaTable',
|
||||
name: 'seaTable',
|
||||
icon: 'file:seaTable.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
||||
description: 'Consume the SeaTable API',
|
||||
defaults: {
|
||||
name: 'SeaTable',
|
||||
},
|
||||
inputs: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Row',
|
||||
value: 'row',
|
||||
},
|
||||
],
|
||||
default: 'row',
|
||||
},
|
||||
...rowOperations,
|
||||
...rowFields,
|
||||
],
|
||||
};
|
||||
343
packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts
Normal file
343
packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
import type FormData from 'form-data';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IPollFunctions,
|
||||
JsonObject,
|
||||
IHttpRequestMethods,
|
||||
IHttpRequestOptions,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import type {
|
||||
ICollaborator,
|
||||
ICollaboratorsResult,
|
||||
ICredential,
|
||||
ICtx,
|
||||
IDtableMetadataColumn,
|
||||
IEndpointVariables,
|
||||
IName,
|
||||
IRow,
|
||||
IRowObject,
|
||||
IColumnDigitalSignature,
|
||||
IFile,
|
||||
} from './actions/Interfaces';
|
||||
import { schema } from './Schema';
|
||||
import type { TDtableMetadataColumns, TEndpointVariableName } from './types';
|
||||
|
||||
const userBaseUri = (uri?: string) => {
|
||||
if (uri === undefined) return uri;
|
||||
if (uri.endsWith('/')) return uri.slice(0, -1);
|
||||
return uri;
|
||||
};
|
||||
|
||||
export function resolveBaseUri(ctx: ICtx) {
|
||||
return ctx?.credentials?.environment === 'cloudHosted'
|
||||
? 'https://cloud.seatable.io'
|
||||
: userBaseUri(ctx?.credentials?.domain);
|
||||
}
|
||||
|
||||
export async function getBaseAccessToken(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
ctx: ICtx,
|
||||
) {
|
||||
if (ctx?.base?.access_token !== undefined) return;
|
||||
|
||||
const options: IHttpRequestOptions = {
|
||||
headers: {
|
||||
Authorization: `Token ${ctx?.credentials?.token}`,
|
||||
},
|
||||
url: `${resolveBaseUri(ctx)}/api/v2.1/dtable/app-access-token/`,
|
||||
json: true,
|
||||
};
|
||||
ctx.base = await this.helpers.httpRequest(options);
|
||||
}
|
||||
|
||||
function endpointCtxExpr(ctx: ICtx, endpoint: string): string {
|
||||
const endpointVariables: IEndpointVariables = {};
|
||||
endpointVariables.access_token = ctx?.base?.access_token;
|
||||
endpointVariables.dtable_uuid = ctx?.base?.dtable_uuid;
|
||||
|
||||
return endpoint.replace(
|
||||
/{{ *(access_token|dtable_uuid|server) *}}/g,
|
||||
(match: string, name: TEndpointVariableName) => {
|
||||
return (endpointVariables[name] 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 = '',
|
||||
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: IRequestOptions = {
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
try {
|
||||
return await 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> {
|
||||
const collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/related-users/',
|
||||
);
|
||||
const 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',
|
||||
'/api-gateway/api/v2/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 namePredicate = (name: string) => (named: IName) => named.name === name;
|
||||
export const nameOfPredicate = (names: readonly IName[]) => (name: string) =>
|
||||
names.find(namePredicate(name));
|
||||
|
||||
const normalize = (subject: string): string => (subject ? subject.normalize() : '');
|
||||
|
||||
export const split = (subject: string): string[] =>
|
||||
normalize(subject)
|
||||
.split(/\s*((?:[^\\,]*?(?:\\[\s\S])*)*?)\s*(?:,|$)/)
|
||||
.filter((s) => s.length)
|
||||
.map((s) => s.replace(/\\([\s\S])/gm, (_, $1) => $1));
|
||||
|
||||
function getCollaboratorInfo(
|
||||
authLocal: string | null | undefined,
|
||||
collaboratorList: ICollaborator[],
|
||||
): ICollaborator {
|
||||
return (
|
||||
collaboratorList.find((singleCollaborator) => singleCollaborator.email === authLocal) || {
|
||||
contact_email: 'unknown',
|
||||
name: 'unknown',
|
||||
email: 'unknown',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getAssetPath(type: string, url: string) {
|
||||
const parts = url.split(`/${type}/`);
|
||||
if (parts[1]) {
|
||||
return '/' + type + '/' + parts[1];
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function enrichColumns(
|
||||
row: IRow,
|
||||
metadata: IDtableMetadataColumn[],
|
||||
collaboratorList: ICollaborator[],
|
||||
): IRow {
|
||||
Object.keys(row).forEach((key) => {
|
||||
const columnDef = metadata.find((obj) => obj.name === key || obj.key === key);
|
||||
|
||||
if (columnDef?.type === 'collaborator') {
|
||||
// collaborator is an array of strings.
|
||||
const collaborators = (row[key] as string[]) || [];
|
||||
if (collaborators.length > 0) {
|
||||
const newArray = collaborators.map((email) => {
|
||||
const collaboratorDetails = getCollaboratorInfo(email, collaboratorList);
|
||||
const newColl = {
|
||||
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.
|
||||
const collaboratorDetails = getCollaboratorInfo(row[key] as string, collaboratorList);
|
||||
row[key] = {
|
||||
email: row[key],
|
||||
contact_email: collaboratorDetails.contact_email,
|
||||
name: collaboratorDetails.name,
|
||||
};
|
||||
}
|
||||
|
||||
if (columnDef?.type === 'image') {
|
||||
const pictures = (row[key] as string[]) || [];
|
||||
if (pictures.length > 0) {
|
||||
const newArray = pictures.map((url) => ({
|
||||
name: url.split('/').pop(),
|
||||
size: 0,
|
||||
type: 'image',
|
||||
url,
|
||||
path: getAssetPath('images', url),
|
||||
}));
|
||||
row[key] = newArray;
|
||||
}
|
||||
}
|
||||
|
||||
if (columnDef?.type === 'file') {
|
||||
const files = (row[key] as IFile[]) || [];
|
||||
files.forEach((file) => {
|
||||
file.path = getAssetPath('files', file.url);
|
||||
});
|
||||
}
|
||||
|
||||
if (columnDef?.type === 'digital-sign') {
|
||||
const digitalSignature: IColumnDigitalSignature | any = row[key];
|
||||
const 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;
|
||||
}
|
||||
|
||||
export function splitStringColumnsToArrays(
|
||||
row: IRowObject,
|
||||
columns: TDtableMetadataColumns,
|
||||
): IRowObject {
|
||||
columns.map((column) => {
|
||||
if (column.type === 'collaborator' || column.type === 'multiple-select') {
|
||||
if (typeof row[column.name] === 'string') {
|
||||
const input = row[column.name] as string;
|
||||
row[column.name] = input.split(',').map((item) => item.trim());
|
||||
}
|
||||
}
|
||||
if (column.type === 'number') {
|
||||
if (typeof row[column.name] === 'string') {
|
||||
const input = row[column.name] as string;
|
||||
row[column.name] = parseFloat(input);
|
||||
}
|
||||
}
|
||||
if (column.type === 'rate' || column.type === 'duration') {
|
||||
if (typeof row[column.name] === 'string') {
|
||||
const input = row[column.name] as string;
|
||||
row[column.name] = parseInt(input);
|
||||
}
|
||||
}
|
||||
if (column.type === 'checkbox') {
|
||||
if (typeof row[column.name] === 'string') {
|
||||
const input = row[column.name] as string;
|
||||
row[column.name] = false;
|
||||
if (input === 'true' || input === 'on' || input === '1') {
|
||||
row[column.name] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
export function rowExport(row: IRowObject, columns: TDtableMetadataColumns): IRowObject {
|
||||
const 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);
|
||||
61
packages/nodes-base/nodes/SeaTable/v2/Schema.ts
Normal file
61
packages/nodes-base/nodes/SeaTable/v2/Schema.ts
Normal 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 };
|
||||
};
|
||||
292
packages/nodes-base/nodes/SeaTable/v2/SeaTableTriggerV2.node.ts
Normal file
292
packages/nodes-base/nodes/SeaTable/v2/SeaTableTriggerV2.node.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
type IPollFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
NodeConnectionType,
|
||||
type INodeTypeBaseDescription,
|
||||
type IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type {
|
||||
ICtx,
|
||||
IRow,
|
||||
IRowResponse,
|
||||
IGetMetadataResult,
|
||||
IGetRowsResult,
|
||||
IDtableMetadataColumn,
|
||||
ICollaborator,
|
||||
ICollaboratorsResult,
|
||||
IColumnDigitalSignature,
|
||||
} from './actions/Interfaces';
|
||||
import { seaTableApiRequest, simplify_new, enrichColumns } from './GenericFunctions';
|
||||
import { loadOptions } from './methods';
|
||||
|
||||
export class SeaTableTriggerV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: 2,
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
defaults: {
|
||||
name: 'SeaTable Trigger',
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
polling: true,
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
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',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'View Name',
|
||||
name: 'viewName',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: ['newRow', 'updatedRow'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getTableViews',
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'The name of SeaTable view to access. Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Signature Column',
|
||||
name: 'assetColumn',
|
||||
type: 'options',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: ['newAsset'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getSignatureColumns',
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Select the digital-signature column that should be tracked. Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Column Names',
|
||||
name: 'convert',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return the column keys (false) or the column names (true)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/event': ['newRow', 'updatedRow'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: '"Fetch Test Event" returns max. three items of the last hour.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
methods = { loadOptions };
|
||||
|
||||
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
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 options = this.getNodeParameter('options') as IDataObject;
|
||||
|
||||
const ctx: ICtx = {};
|
||||
|
||||
const startDate =
|
||||
this.getMode() === 'manual'
|
||||
? moment().utc().subtract(1, 'h').format()
|
||||
: (webhookData.lastTimeChecked as string);
|
||||
const endDate = (webhookData.lastTimeChecked = moment().utc().format());
|
||||
|
||||
const filterField = event === 'newRow' ? '_ctime' : '_mtime';
|
||||
|
||||
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 = '/api-gateway/api/v2/dtables/{{dtable_uuid}}/sql';
|
||||
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: options.convert ?? true,
|
||||
});
|
||||
|
||||
metadata = sqlResult.metadata as IDtableMetadataColumn[];
|
||||
const columnType = metadata.find((obj) => obj.name == assetColumn);
|
||||
const assetColumnType = columnType?.type || null;
|
||||
|
||||
// remove unwanted entries
|
||||
rows = sqlResult.results.filter((obj) => new Date(obj._mtime) > new Date(startDate));
|
||||
|
||||
// split the objects into new lines (not necessary for digital-sign)
|
||||
const newRows: any = [];
|
||||
for (const row of rows) {
|
||||
if (assetColumnType === 'digital-sign') {
|
||||
const signature = (row[assetColumn] as IColumnDigitalSignature) || [];
|
||||
if (signature.sign_time) {
|
||||
if (new Date(signature.sign_time) > new Date(startDate)) {
|
||||
newRows.push(signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// View => use getRows.
|
||||
else if (viewName || viewName === '') {
|
||||
requestMeta = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/metadata/',
|
||||
);
|
||||
requestRows = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
|
||||
{},
|
||||
{
|
||||
table_name: tableName,
|
||||
view_name: viewName,
|
||||
limit,
|
||||
convert_keys: options.convert ?? true,
|
||||
},
|
||||
);
|
||||
|
||||
metadata =
|
||||
requestMeta.metadata.tables.find((table) => table.name === tableName)?.columns ?? [];
|
||||
|
||||
// remove unwanted rows that are too old (compare startDate with _ctime or _mtime)
|
||||
if (this.getMode() === 'manual') {
|
||||
rows = requestRows.rows;
|
||||
} else {
|
||||
rows = requestRows.rows.filter((obj) => new Date(obj[filterField]) > new Date(startDate));
|
||||
}
|
||||
} else {
|
||||
const endpoint = '/api-gateway/api/v2/dtables/{{dtable_uuid}}/sql';
|
||||
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: options.convert ?? true,
|
||||
});
|
||||
metadata = sqlResult.metadata as IDtableMetadataColumn[];
|
||||
rows = sqlResult.results;
|
||||
}
|
||||
|
||||
const collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/related-users/',
|
||||
);
|
||||
const collaborators: ICollaborator[] = collaboratorsResult.user_list || [];
|
||||
|
||||
if (Array.isArray(rows) && rows.length > 0) {
|
||||
const simple = options.simple ?? true;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
27
packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts
Normal file
27
packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeTypeBaseDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { router } from './actions/router';
|
||||
import { versionDescription } from './actions/SeaTable.node';
|
||||
import { loadOptions } from './methods';
|
||||
|
||||
export class SeaTableV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = { loadOptions };
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
return await router.call(this);
|
||||
}
|
||||
}
|
||||
195
packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts
Normal file
195
packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { AllEntities, Entity, PropertiesOf } from 'n8n-workflow';
|
||||
|
||||
type SeaTableMap = {
|
||||
row: 'create' | 'get' | 'search' | 'update' | 'remove' | 'lock' | 'unlock' | 'list';
|
||||
base: 'snapshot' | 'metadata' | 'collaborator';
|
||||
link: 'add' | 'list' | 'remove';
|
||||
asset: 'upload' | 'getPublicURL';
|
||||
};
|
||||
|
||||
export type SeaTable = AllEntities<SeaTableMap>;
|
||||
|
||||
export type SeaTableRow = Entity<SeaTableMap, 'row'>;
|
||||
export type SeaTableBase = Entity<SeaTableMap, 'base'>;
|
||||
export type SeaTableLink = Entity<SeaTableMap, 'link'>;
|
||||
export type SeaTableAsset = Entity<SeaTableMap, 'asset'>;
|
||||
|
||||
export type RowProperties = PropertiesOf<SeaTableRow>;
|
||||
export type BaseProperties = PropertiesOf<SeaTableBase>;
|
||||
export type LinkProperties = PropertiesOf<SeaTableLink>;
|
||||
export type AssetProperties = PropertiesOf<SeaTableAsset>;
|
||||
|
||||
import type {
|
||||
TColumnType,
|
||||
TColumnValue,
|
||||
TDtableMetadataColumns,
|
||||
TDtableMetadataTables,
|
||||
TSeaTableServerEdition,
|
||||
TSeaTableServerVersion,
|
||||
} from '../types';
|
||||
|
||||
export interface IApi {
|
||||
server: string;
|
||||
token: string;
|
||||
appAccessToken?: IAppAccessToken;
|
||||
info?: IServerInfo;
|
||||
}
|
||||
|
||||
export interface IServerInfo {
|
||||
version: TSeaTableServerVersion;
|
||||
edition: TSeaTableServerEdition;
|
||||
}
|
||||
|
||||
export interface IAppAccessToken {
|
||||
app_name: string;
|
||||
access_token: string;
|
||||
dtable_uuid: string;
|
||||
dtable_server: string;
|
||||
dtable_socket: string;
|
||||
workspace_id: number;
|
||||
dtable_name: string;
|
||||
}
|
||||
|
||||
export interface IDtableMetadataColumn {
|
||||
key: string;
|
||||
name: string;
|
||||
type: TColumnType;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
export interface TDtableViewColumn {
|
||||
_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IDtableMetadataTable {
|
||||
_id: string;
|
||||
name: string;
|
||||
columns: TDtableMetadataColumns;
|
||||
}
|
||||
|
||||
export interface IDtableMetadata {
|
||||
tables: TDtableMetadataTables;
|
||||
version: string;
|
||||
format_version: string;
|
||||
}
|
||||
|
||||
export interface IEndpointVariables {
|
||||
[name: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
export interface IRowObject {
|
||||
[name: string]: TColumnValue | object;
|
||||
}
|
||||
|
||||
export interface IRow extends IRowObject {
|
||||
_id: string;
|
||||
_ctime: string;
|
||||
_mtime: string;
|
||||
_seq?: number;
|
||||
}
|
||||
|
||||
export interface IName {
|
||||
name: string;
|
||||
}
|
||||
|
||||
type TOperation = 'cloudHosted' | 'selfHosted';
|
||||
|
||||
export interface ICredential {
|
||||
token: string;
|
||||
domain: string;
|
||||
environment: TOperation;
|
||||
}
|
||||
|
||||
interface IBase {
|
||||
dtable_uuid: string;
|
||||
access_token: string;
|
||||
workspace_id: number;
|
||||
dtable_name: string;
|
||||
}
|
||||
|
||||
export interface ICtx {
|
||||
base?: IBase;
|
||||
credentials?: ICredential;
|
||||
}
|
||||
|
||||
// response object of SQL-Query!
|
||||
export interface IRowResponse {
|
||||
metadata: [
|
||||
{
|
||||
key: string;
|
||||
name: string;
|
||||
type: string;
|
||||
},
|
||||
];
|
||||
results: IRow[];
|
||||
}
|
||||
|
||||
// das ist bad
|
||||
export interface IRowResponse2 {
|
||||
rows: IRow[];
|
||||
}
|
||||
|
||||
/** neu von mir **/
|
||||
|
||||
// response object of SQL-Query!
|
||||
export interface ISqlQueryResult {
|
||||
metadata: [
|
||||
{
|
||||
key: string;
|
||||
name: string;
|
||||
},
|
||||
];
|
||||
results: IRow[];
|
||||
}
|
||||
|
||||
// response object of GetMetadata
|
||||
export interface IGetMetadataResult {
|
||||
metadata: IDtableMetadata;
|
||||
}
|
||||
|
||||
// response object of GetRows
|
||||
export interface IGetRowsResult {
|
||||
rows: IRow[];
|
||||
}
|
||||
|
||||
export interface ICollaboratorsResult {
|
||||
user_list: ICollaborator[];
|
||||
}
|
||||
|
||||
export interface ICollaborator {
|
||||
email: string;
|
||||
name: string;
|
||||
contact_email: string;
|
||||
avatar_url?: string;
|
||||
id_in_org?: string;
|
||||
}
|
||||
|
||||
export interface IColumnDigitalSignature {
|
||||
username: string;
|
||||
sign_image_url: string;
|
||||
sign_time: string;
|
||||
contact_email?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IFile {
|
||||
name: string;
|
||||
size: number;
|
||||
type: 'file';
|
||||
url: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface ILinkData {
|
||||
table_id: string;
|
||||
other_table_id: string;
|
||||
link_id: string;
|
||||
}
|
||||
|
||||
export interface IUploadLink {
|
||||
upload_link: string;
|
||||
parent_path: string;
|
||||
img_relative_path: string;
|
||||
file_relative_path: string;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { NodeConnectionType, type INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import * as asset from './asset';
|
||||
import * as base from './base';
|
||||
import * as link from './link';
|
||||
import * as row from './row';
|
||||
|
||||
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: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.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,
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Asset Path',
|
||||
name: 'assetPath',
|
||||
type: 'string',
|
||||
placeholder: '/images/2023-09/logo.png',
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['getPublicURL'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
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);
|
||||
}
|
||||
37
packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts
Normal file
37
packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as getPublicURL from './getPublicURL.operation';
|
||||
import * as upload from './upload.operation';
|
||||
|
||||
export { upload, getPublicURL };
|
||||
|
||||
export const descriptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Public URL',
|
||||
value: 'getPublicURL',
|
||||
description: 'Get the public URL from asset path',
|
||||
action: 'Get the public URL from asset path',
|
||||
},
|
||||
{
|
||||
name: 'Upload',
|
||||
value: 'upload',
|
||||
description: 'Add a file/image to an existing row',
|
||||
action: 'Upload a file or image',
|
||||
},
|
||||
],
|
||||
default: 'upload',
|
||||
},
|
||||
...upload.description,
|
||||
...getPublicURL.description,
|
||||
];
|
||||
@@ -0,0 +1,232 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
import type { IUploadLink, IRowObject } from '../Interfaces';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Column Name',
|
||||
name: 'uploadColumn',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getAssetColumns',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose from the list, or specify the name using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
description: 'Name of the binary property which contains the data for the file to be written',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Replace Existing File',
|
||||
name: 'replace',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to replace the existing asset with the same name (true). Otherwise, a new version with a different name (numeral in parentheses) will be uploaded (false).',
|
||||
},
|
||||
{
|
||||
displayName: 'Append to Column',
|
||||
name: 'append',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to keep existing files/images in the column and append the new asset (true). Otherwise, the existing files/images are removed from the column (false).',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['upload'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const uploadColumn = this.getNodeParameter('uploadColumn', index) as string;
|
||||
const uploadColumnType = uploadColumn.split(':::')[1];
|
||||
const uploadColumnName = uploadColumn.split(':::')[0];
|
||||
const dataPropertyName = this.getNodeParameter('dataPropertyName', index);
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
const uploadLink = (await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api/v2.1/dtable/app-upload-link/',
|
||||
)) as IUploadLink;
|
||||
const relativePath =
|
||||
uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path;
|
||||
|
||||
const options = this.getNodeParameter('options', index) as IDataObject;
|
||||
|
||||
// get server url
|
||||
const credentials: any = await this.getCredentials('seaTableApi');
|
||||
const serverURL: string = credentials.domain
|
||||
? credentials.domain.replace(/\/$/, '')
|
||||
: 'https://cloud.seatable.io';
|
||||
|
||||
// get workspaceId
|
||||
const workspaceId = (
|
||||
await this.helpers.httpRequest({
|
||||
headers: {
|
||||
Authorization: `Token ${credentials.token}`,
|
||||
},
|
||||
url: `${serverURL}/api/v2.1/dtable/app-access-token/`,
|
||||
json: true,
|
||||
})
|
||||
).workspace_id;
|
||||
|
||||
// if there are already assets attached to the column
|
||||
let existingAssetArray = [];
|
||||
const append = options.append ?? true;
|
||||
if (append) {
|
||||
const rowToUpdate = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/' + rowId,
|
||||
{},
|
||||
{
|
||||
table_name: tableName,
|
||||
convert_keys: true,
|
||||
},
|
||||
);
|
||||
existingAssetArray = rowToUpdate[uploadColumnName] ?? [];
|
||||
}
|
||||
|
||||
// Get the binary data and prepare asset for upload
|
||||
const fileBufferData = await this.helpers.getBinaryDataBuffer(index, dataPropertyName);
|
||||
const binaryData = this.helpers.assertBinaryData(index, dataPropertyName);
|
||||
const requestOptions = {
|
||||
formData: {
|
||||
file: {
|
||||
value: fileBufferData,
|
||||
options: {
|
||||
filename: binaryData.fileName,
|
||||
contentType: binaryData.mimeType,
|
||||
},
|
||||
},
|
||||
parent_dir: uploadLink.parent_path,
|
||||
replace: options.replace ? '1' : '0',
|
||||
relative_path: relativePath,
|
||||
},
|
||||
};
|
||||
|
||||
// Send the upload request
|
||||
const uploadAsset = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
`/seafhttp/upload-api/${uploadLink.upload_link.split('seafhttp/upload-api/')[1]}?ret-json=true`,
|
||||
{},
|
||||
{},
|
||||
'',
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
// attach the asset to a column in a base
|
||||
for (let c = 0; c < uploadAsset.length; c++) {
|
||||
const rowInput = {} as IRowObject;
|
||||
|
||||
const filePath = `${serverURL}/workspace/${workspaceId}${uploadLink.parent_path}/${relativePath}/${uploadAsset[c].name}`;
|
||||
|
||||
if (uploadColumnType === 'image') {
|
||||
rowInput[uploadColumnName] = [filePath];
|
||||
} else if (uploadColumnType === 'file') {
|
||||
rowInput[uploadColumnName] = uploadAsset;
|
||||
uploadAsset[c].type = 'file';
|
||||
uploadAsset[c].url = filePath;
|
||||
}
|
||||
|
||||
// merge with existing assets in this column or with [] and remove duplicates
|
||||
const mergedArray = existingAssetArray.concat(rowInput[uploadColumnName]);
|
||||
|
||||
// Remove duplicates from input, keeping the last one
|
||||
const uniqueAssets = Array.from(new Set(mergedArray));
|
||||
|
||||
// Update the rowInput with the unique assets and store into body.row.
|
||||
rowInput[uploadColumnName] = uniqueAssets;
|
||||
const body = {
|
||||
table_name: tableName,
|
||||
updates: [
|
||||
{
|
||||
row_id: rowId,
|
||||
row: rowInput,
|
||||
},
|
||||
],
|
||||
} as IDataObject;
|
||||
|
||||
// attach assets to table row
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'PUT',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
uploadAsset[c].upload_successful = responseData.success;
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(uploadAsset as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
import type { ICollaborator } from '../Interfaces';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Name or email of the collaborator',
|
||||
name: 'searchString',
|
||||
type: 'string',
|
||||
placeholder: 'Enter the name or the email or the collaborator',
|
||||
required: true,
|
||||
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.',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['collaborator'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const searchString = this.getNodeParameter('searchString', index) as string;
|
||||
|
||||
const collaboratorsResult = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/related-users/',
|
||||
);
|
||||
const collaborators = collaboratorsResult.user_list || [];
|
||||
|
||||
const data = collaborators.filter(
|
||||
(col: ICollaborator) =>
|
||||
col.contact_email.includes(searchString) || col.name.includes(searchString),
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(data as IDataObject[]);
|
||||
}
|
||||
45
packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts
Normal file
45
packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as collaborator from './collaborator.operation';
|
||||
import * as metadata from './metadata.operation';
|
||||
import * as snapshot from './snapshot.operation';
|
||||
|
||||
export { snapshot, metadata, 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: 'Collaborator',
|
||||
value: 'collaborator',
|
||||
description: 'Get the username from the email or name of a collaborator',
|
||||
action: 'Get username from email or name',
|
||||
},
|
||||
],
|
||||
default: 'snapshot',
|
||||
},
|
||||
...snapshot.description,
|
||||
...metadata.description,
|
||||
...collaborator.description,
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export const properties: INodeProperties[] = [];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['metadata'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/metadata/',
|
||||
);
|
||||
return this.helpers.returnJsonArray(responseData.metadata as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export const properties: INodeProperties[] = [];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['snapshot'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/snapshot/',
|
||||
{ dtable_name: 'snapshot' },
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Table Name (Source)',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Name of table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNameAndId',
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Link Column',
|
||||
name: 'linkColumn',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getLinkColumns',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id".',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID From the Source Table',
|
||||
name: 'linkColumnSourceId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Provide the row ID of table you selected',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID From the Target',
|
||||
name: 'linkColumnTargetId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Provide the row ID of table you want to link',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['add'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const linkColumn = this.getNodeParameter('linkColumn', index) as any;
|
||||
const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string;
|
||||
const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string;
|
||||
|
||||
const body = {
|
||||
link_id: linkColumn.split(':::')[1],
|
||||
table_id: tableName.split(':::')[1],
|
||||
other_table_id: linkColumn.split(':::')[2],
|
||||
other_rows_ids_map: {
|
||||
[linkColumnSourceId]: [linkColumnTargetId],
|
||||
},
|
||||
};
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/links/',
|
||||
body,
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
45
packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts
Normal file
45
packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as add from './add.operation';
|
||||
import * as list from './list.operation';
|
||||
import * as remove from './remove.operation';
|
||||
|
||||
export { add, list, remove };
|
||||
|
||||
export const descriptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add',
|
||||
value: 'add',
|
||||
description: 'Create a link between two rows in a link column',
|
||||
action: 'Add a row link',
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
value: 'list',
|
||||
description: 'List all links of a specific row',
|
||||
action: 'List row links',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
value: 'remove',
|
||||
description: 'Remove a link between two rows from a link column',
|
||||
action: 'Remove a row link',
|
||||
},
|
||||
],
|
||||
default: 'add',
|
||||
},
|
||||
...add.description,
|
||||
...list.description,
|
||||
...remove.description,
|
||||
];
|
||||
@@ -0,0 +1,92 @@
|
||||
/* eslint-disable n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options */
|
||||
/* eslint-disable n8n-nodes-base/node-param-description-wrong-for-dynamic-options */
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNameAndId',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
|
||||
},
|
||||
{
|
||||
displayName: 'Link Column',
|
||||
name: 'linkColumn',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getLinkColumnsWithColumnKey',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id:::column_key".',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['list'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
// get parameters
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const linkColumn = this.getNodeParameter('linkColumn', index) as string;
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
|
||||
// get rows
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/query-links/',
|
||||
{
|
||||
table_id: tableName.split(':::')[1],
|
||||
link_column_key: linkColumn.split(':::')[3],
|
||||
rows: [
|
||||
{
|
||||
row_id: rowId,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/* eslint-disable n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options */
|
||||
/* eslint-disable n8n-nodes-base/node-param-description-wrong-for-dynamic-options */
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Table Name (Source)',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Name of table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNameAndId',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".',
|
||||
},
|
||||
{
|
||||
displayName: 'Link Column',
|
||||
name: 'linkColumn',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getLinkColumns',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id".',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID From the Source Table',
|
||||
name: 'linkColumnSourceId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Provide the row ID of table you selected',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID From the Target Table',
|
||||
name: 'linkColumnTargetId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Provide the row ID of table you want to link',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const linkColumn = this.getNodeParameter('linkColumn', index) as any;
|
||||
const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string;
|
||||
const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string;
|
||||
|
||||
const body = {
|
||||
link_id: linkColumn.split(':::')[1],
|
||||
table_id: tableName.split(':::')[1],
|
||||
other_table_id: linkColumn.split(':::')[2],
|
||||
other_rows_ids_map: {
|
||||
[linkColumnSourceId]: [linkColumnTargetId],
|
||||
},
|
||||
};
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'DELETE',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/links/',
|
||||
body,
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
53
packages/nodes-base/nodes/SeaTable/v2/actions/router.ts
Normal file
53
packages/nodes-base/nodes/SeaTable/v2/actions/router.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import * as asset from './asset';
|
||||
import * as base from './base';
|
||||
import type { SeaTable } from './Interfaces';
|
||||
import * as link from './link';
|
||||
import * as row from './row';
|
||||
|
||||
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const operationResult: INodeExecutionData[] = [];
|
||||
let responseData: IDataObject | IDataObject[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const resource = this.getNodeParameter<SeaTable>('resource', i);
|
||||
const operation = this.getNodeParameter('operation', i);
|
||||
|
||||
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(
|
||||
responseData as INodeExecutionData[],
|
||||
{
|
||||
itemData: { item: i },
|
||||
},
|
||||
);
|
||||
|
||||
operationResult.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
operationResult.push({ json: this.getInputData(i)[0].json, error });
|
||||
} else {
|
||||
if (error.context) error.context.itemIndex = i;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [operationResult];
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
getTableColumns,
|
||||
split,
|
||||
rowExport,
|
||||
updateAble,
|
||||
splitStringColumnsToArrays,
|
||||
} from '../../GenericFunctions';
|
||||
import type { TColumnValue, TColumnsUiValues } from '../../types';
|
||||
import type { IRowObject } from '../Interfaces';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
default: 'defineBelow',
|
||||
description: 'Whether to insert the input data this node receives in the new row',
|
||||
},
|
||||
{
|
||||
displayName: 'Apply Column Default Values',
|
||||
name: 'apply_default',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to use the column default values to populate new rows during creation (only available for normal backend)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
bigdata: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'In this mode, make sure the incoming data fields are named the same as the columns in SeaTable. (Use an "Edit Fields" node before this node to change them if required.)',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/fieldsToSend': ['autoMapInputData'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Inputs to Ignore',
|
||||
name: 'inputsToIgnore',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
|
||||
placeholder: 'Enter properties...',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/fieldsToSend': ['autoMapInputData'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Columns to Send',
|
||||
name: 'columnsUi',
|
||||
placeholder: 'Add Column',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValueButtonText: 'Add Column to Send',
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/fieldsToSend': ['defineBelow'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Column',
|
||||
name: 'columnValues',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Column Name or ID',
|
||||
name: 'columnName',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getTableUpdateAbleColumns',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Column Value',
|
||||
name: 'columnValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
default: {},
|
||||
description:
|
||||
'Add destination column with its value. Provide the value in this way. Date: YYYY-MM-DD or YYYY-MM-DD hh:mm. Duration: time in seconds. Checkbox: true, on or 1. Multi-Select: comma-separated list.',
|
||||
},
|
||||
{
|
||||
displayName: 'Save to "Big Data" Backend',
|
||||
name: 'bigdata',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether write to Big Data backend (true) or not (false). True requires the activation of the Big Data backend in the base.',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Hint: Link, files, images or digital signatures have to be added separately. These column types cannot be set with this node.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['create'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
const bigdata = this.getNodeParameter('bigdata', index) as boolean;
|
||||
const apply_default = this.getNodeParameter('apply_default', index, false) as boolean;
|
||||
|
||||
const body = {
|
||||
table_name: tableName,
|
||||
rows: {},
|
||||
} as IDataObject;
|
||||
let rowInput = {} as IRowObject;
|
||||
|
||||
// get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }.
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const items = this.getInputData();
|
||||
const incomingKeys = Object.keys(items[index].json);
|
||||
const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[index].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
|
||||
// only keep key:value pairs for columns that are allowed to update.
|
||||
rowInput = rowExport(rowInput, updateAble(tableColumns));
|
||||
|
||||
// string to array: multi-select and collaborators
|
||||
rowInput = splitStringColumnsToArrays(rowInput, tableColumns);
|
||||
|
||||
// save to big data backend
|
||||
if (bigdata) {
|
||||
body.rows = [rowInput];
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/add-archived-rows/',
|
||||
body,
|
||||
);
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
// save to normal backend
|
||||
else {
|
||||
body.rows = [rowInput];
|
||||
if (apply_default) {
|
||||
body.apply_default = true;
|
||||
}
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
if (responseData.first_row) {
|
||||
return this.helpers.returnJsonArray(responseData.first_row as IDataObject[]);
|
||||
}
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
enrichColumns,
|
||||
simplify_new,
|
||||
getBaseCollaborators,
|
||||
} from '../../GenericFunctions';
|
||||
import type { IRowResponse, IDtableMetadataColumn } from '../Interfaces';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Column Names',
|
||||
name: 'convert',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return the column keys (false) or the column names (true)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['get'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
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 options = this.getNodeParameter('options', index) as IDataObject;
|
||||
|
||||
// get collaborators
|
||||
const collaborators = await getBaseCollaborators.call(this);
|
||||
|
||||
// get rows
|
||||
const sqlResult = (await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/sql/',
|
||||
{
|
||||
sql: `SELECT * FROM \`${tableName}\` WHERE _id = '${rowId}'`,
|
||||
convert_keys: options.convert ?? true,
|
||||
},
|
||||
)) as IRowResponse;
|
||||
const metadata = sqlResult.metadata as IDtableMetadataColumn[];
|
||||
const rows = sqlResult.results;
|
||||
|
||||
// hide columns like button
|
||||
rows.map((row) => enrichColumns(row, metadata, collaborators));
|
||||
const simple = options.simple ?? true;
|
||||
// remove columns starting with _ if simple;
|
||||
if (simple) {
|
||||
rows.map((row) => simplify_new(row));
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(rows as IDataObject[]);
|
||||
}
|
||||
84
packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts
Normal file
84
packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as create from './create.operation';
|
||||
import * as get from './get.operation';
|
||||
import * as list from './list.operation';
|
||||
import * as lock from './lock.operation';
|
||||
import * as remove from './remove.operation';
|
||||
import * as search from './search.operation';
|
||||
import { sharedProperties } from './sharedProperties';
|
||||
import * as unlock from './unlock.operation';
|
||||
import * as update from './update.operation';
|
||||
|
||||
export { create, get, search, update, remove, lock, unlock, list };
|
||||
|
||||
export const descriptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a new row',
|
||||
action: 'Create a row',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'remove',
|
||||
description: 'Delete a row',
|
||||
action: 'Delete a row',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get the content of a row',
|
||||
action: 'Get a row',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'list',
|
||||
description: 'Get many rows from a table or a table view',
|
||||
action: 'Get many rows',
|
||||
},
|
||||
{
|
||||
name: 'Lock',
|
||||
value: 'lock',
|
||||
description: 'Lock a row to prevent further changes',
|
||||
action: 'Add a row lock',
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
description: 'Search one or multiple rows',
|
||||
action: 'Search a row by keyword',
|
||||
},
|
||||
{
|
||||
name: 'Unlock',
|
||||
value: 'unlock',
|
||||
description: 'Remove the lock from a row',
|
||||
action: 'Remove a row lock',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update the content of a row',
|
||||
action: 'Update a row',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
...sharedProperties,
|
||||
...create.description,
|
||||
...get.description,
|
||||
...list.description,
|
||||
...search.description,
|
||||
...update.description,
|
||||
];
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
enrichColumns,
|
||||
simplify_new,
|
||||
getBaseCollaborators,
|
||||
} from '../../GenericFunctions';
|
||||
import type { IRow } from '../Interfaces';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'View Name',
|
||||
name: 'viewName',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getTableViews',
|
||||
},
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'The name of SeaTable view to access, or specify by using an expression. Provide it in the way "col.name:::col.type".',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Column Names',
|
||||
name: 'convert',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return the column keys (false) or the column names (true)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['list'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
// get parameters
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const viewName = this.getNodeParameter('viewName', index) as string;
|
||||
const options = this.getNodeParameter('options', index) as IDataObject;
|
||||
|
||||
// get collaborators
|
||||
const collaborators = await getBaseCollaborators.call(this);
|
||||
|
||||
// get rows
|
||||
const requestMeta = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/metadata/',
|
||||
);
|
||||
|
||||
const requestRows = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
|
||||
{},
|
||||
{
|
||||
table_name: tableName,
|
||||
view_name: viewName,
|
||||
limit: 1000,
|
||||
convert_keys: options.convert ?? true,
|
||||
},
|
||||
);
|
||||
|
||||
const metadata =
|
||||
requestMeta.metadata.tables.find((table: { name: string }) => table.name === tableName)
|
||||
?.columns ?? [];
|
||||
const rows = requestRows.rows as IRow[];
|
||||
|
||||
// hide columns like button
|
||||
rows.map((row) => enrichColumns(row, metadata, collaborators));
|
||||
|
||||
const simple = options.simple ?? true;
|
||||
// remove columns starting with _ if simple;
|
||||
if (simple) {
|
||||
rows.map((row) => simplify_new(row));
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(rows as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { IDataObject, INodeExecutionData, IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export async function execute(
|
||||
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',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/lock-rows/',
|
||||
{
|
||||
table_name: tableName,
|
||||
row_ids: [rowId],
|
||||
},
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { IDataObject, INodeExecutionData, IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
|
||||
const requestBody: IDataObject = {
|
||||
table_name: tableName,
|
||||
row_ids: [rowId],
|
||||
};
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'DELETE',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
|
||||
requestBody,
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
enrichColumns,
|
||||
simplify_new,
|
||||
getBaseCollaborators,
|
||||
} from '../../GenericFunctions';
|
||||
import type { IDtableMetadataColumn, IRowResponse } from '../Interfaces';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Column Name or ID',
|
||||
name: 'searchColumn',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getSearchableColumns',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Select the column to be searched. Not all column types are supported for search. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Search Term',
|
||||
name: 'searchTerm',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'What to look for?',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Case Insensitive Search',
|
||||
name: 'insensitive',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the search ignores case sensitivity (true). Otherwise, it distinguishes between uppercase and lowercase characters.',
|
||||
},
|
||||
{
|
||||
displayName: 'Activate Wildcard Search',
|
||||
name: 'wildcard',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether the search only results perfect matches (true). Otherwise, it finds a row even if the search value is part of a string (false).',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Column Names',
|
||||
name: 'convert',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return the column keys (false) or the column names (true)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['search'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const searchColumn = this.getNodeParameter('searchColumn', index) as string;
|
||||
const searchTerm = this.getNodeParameter('searchTerm', index) as string | number;
|
||||
let searchTermString = String(searchTerm);
|
||||
const options = this.getNodeParameter('options', index) as IDataObject;
|
||||
|
||||
// get collaborators
|
||||
const collaborators = await getBaseCollaborators.call(this);
|
||||
|
||||
// this is the base query. The WHERE has to be finalized...
|
||||
let sqlQuery = `SELECT * FROM \`${tableName}\` WHERE \`${searchColumn}\``;
|
||||
|
||||
if (options.insensitive) {
|
||||
searchTermString = searchTermString.toLowerCase();
|
||||
sqlQuery = `SELECT * FROM \`${tableName}\` WHERE lower(\`${searchColumn}\`)`;
|
||||
}
|
||||
|
||||
const wildcard = options.wildcard ?? true;
|
||||
|
||||
if (wildcard) sqlQuery = sqlQuery + ' LIKE "%' + searchTermString + '%"';
|
||||
else if (!wildcard) sqlQuery = sqlQuery + ' = "' + searchTermString + '"';
|
||||
|
||||
const sqlResult = (await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/sql',
|
||||
{
|
||||
sql: sqlQuery,
|
||||
convert_keys: options.convert ?? true,
|
||||
},
|
||||
)) as IRowResponse;
|
||||
const metadata = sqlResult.metadata as IDtableMetadataColumn[];
|
||||
const rows = sqlResult.results;
|
||||
|
||||
// hide columns like button
|
||||
rows.map((row) => enrichColumns(row, metadata, collaborators));
|
||||
|
||||
// remove columns starting with _;
|
||||
if (options.simple) {
|
||||
rows.map((row) => simplify_new(row));
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(rows as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const sharedProperties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
},
|
||||
hide: {
|
||||
operation: ['create', 'list', 'search'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { IDataObject, INodeExecutionData, IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { seaTableApiRequest } from '../../GenericFunctions';
|
||||
|
||||
export async function execute(
|
||||
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',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/unlock-rows/',
|
||||
{
|
||||
table_name: tableName,
|
||||
row_ids: [rowId],
|
||||
},
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
type IDataObject,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type IExecuteFunctions,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
getTableColumns,
|
||||
split,
|
||||
rowExport,
|
||||
updateAble,
|
||||
splitStringColumnsToArrays,
|
||||
} from '../../GenericFunctions';
|
||||
import type { TColumnsUiValues, TColumnValue } from '../../types';
|
||||
import type { IRowObject } from '../Interfaces';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
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 or ID',
|
||||
name: 'columnName',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getTableUpdateAbleColumns',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Column Value',
|
||||
name: 'columnValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['update'],
|
||||
fieldsToSend: ['defineBelow'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
description:
|
||||
'Add destination column with its value. Provide the value in this way:Date: YYYY-MM-DD or YYYY-MM-DD hh:mmDuration: time in secondsCheckbox: true, on or 1Multi-Select: comma-separated list.',
|
||||
},
|
||||
{
|
||||
displayName: 'Hint: Link, files, images or digital signatures have to be added separately.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['update'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
|
||||
let rowInput = {} as IRowObject;
|
||||
|
||||
// get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }.
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const items = this.getInputData();
|
||||
const incomingKeys = Object.keys(items[index].json);
|
||||
const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[index].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
|
||||
// only keep key:value pairs for columns that are allowed to update.
|
||||
rowInput = rowExport(rowInput, updateAble(tableColumns));
|
||||
|
||||
// string to array: multi-select and collaborators
|
||||
rowInput = splitStringColumnsToArrays(rowInput, tableColumns);
|
||||
|
||||
const body = {
|
||||
table_name: tableName,
|
||||
updates: [
|
||||
{
|
||||
row_id: rowId,
|
||||
row: rowInput,
|
||||
},
|
||||
],
|
||||
} as IDataObject;
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'PUT',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import { NodeConnectionType, type INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import * as asset from './asset';
|
||||
import * as base from './base';
|
||||
import * as link from './link';
|
||||
import * as row from './row';
|
||||
|
||||
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: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.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,
|
||||
],
|
||||
};
|
||||
1
packages/nodes-base/nodes/SeaTable/v2/methods/index.ts
Normal file
1
packages/nodes-base/nodes/SeaTable/v2/methods/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as loadOptions from './loadOptions';
|
||||
280
packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts
Normal file
280
packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
|
||||
|
||||
import type { IRow } from '../actions/Interfaces';
|
||||
import { getTableColumns, seaTableApiRequest, updateAble } from '../GenericFunctions';
|
||||
|
||||
export async function getTableNames(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/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',
|
||||
'/api-gateway/api/v2/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',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/columns',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
for (const col of columns.columns) {
|
||||
if (
|
||||
col.type === 'text' ||
|
||||
col.type === 'long-text' ||
|
||||
col.type === 'number' ||
|
||||
col.type === 'single-select' ||
|
||||
col.type === 'email' ||
|
||||
col.type === 'url' ||
|
||||
col.type === 'rate' ||
|
||||
col.type === 'formula'
|
||||
) {
|
||||
returnData.push({
|
||||
name: col.name,
|
||||
value: col.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getLinkColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const table = this.getCurrentNodeParameter('tableName') as string;
|
||||
|
||||
const tableName = table.split(':::')[0];
|
||||
const tableId = table.split(':::')[1];
|
||||
|
||||
if (tableName) {
|
||||
const columns = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/columns',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
for (const col of columns.columns) {
|
||||
if (col.type === 'link') {
|
||||
// make sure that the "other table id" is returned and not the same table id again.
|
||||
const otid =
|
||||
tableId !== col.data.other_table_id ? col.data.other_table_id : col.data.table_id;
|
||||
|
||||
returnData.push({
|
||||
name: col.name,
|
||||
value: col.name + ':::' + col.data.link_id + ':::' + otid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getLinkColumnsWithColumnKey(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const table = this.getCurrentNodeParameter('tableName') as string;
|
||||
|
||||
const tableName = table.split(':::')[0];
|
||||
const tableId = table.split(':::')[1];
|
||||
|
||||
if (tableName) {
|
||||
const columns = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/columns',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
for (const col of columns.columns) {
|
||||
if (col.type === 'link') {
|
||||
// make sure that the "other table id" is returned and not the same table id again.
|
||||
const otid =
|
||||
tableId !== col.data.other_table_id ? col.data.other_table_id : col.data.table_id;
|
||||
|
||||
returnData.push({
|
||||
name: col.name,
|
||||
value: col.name + ':::' + col.data.link_id + ':::' + otid + ':::' + col.key,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getAssetColumns(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const tableName = this.getCurrentNodeParameter('tableName') as string;
|
||||
if (tableName) {
|
||||
const columns = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/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',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/columns',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
for (const col of columns.columns) {
|
||||
if (col.type === 'digital-sign') {
|
||||
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);
|
||||
|
||||
columns = updateAble(columns);
|
||||
|
||||
return columns
|
||||
.filter((column) => column.editable)
|
||||
.map((column) => ({ name: column.name, value: column.name }));
|
||||
}
|
||||
|
||||
export async function getRowIds(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const table = this.getCurrentNodeParameter('tableName') as string;
|
||||
const operation = this.getCurrentNodeParameter('operation') as string;
|
||||
let tableName = table;
|
||||
|
||||
if (table.indexOf(':::') !== -1) {
|
||||
tableName = table.split(':::')[0];
|
||||
}
|
||||
|
||||
let lockQuery = '';
|
||||
|
||||
if (operation === 'lock') {
|
||||
lockQuery = 'WHERE _locked is null';
|
||||
}
|
||||
|
||||
if (operation === 'unlock') {
|
||||
lockQuery = 'WHERE _locked = true';
|
||||
}
|
||||
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
if (tableName) {
|
||||
const sqlResult = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/api-gateway/api/v2/dtables/{{dtable_uuid}}/sql',
|
||||
{
|
||||
sql: `SELECT * FROM \`${tableName}\` ${lockQuery} LIMIT 1000`,
|
||||
convert_keys: false,
|
||||
},
|
||||
);
|
||||
const rows = sqlResult.results as IRow[];
|
||||
|
||||
for (const row of rows) {
|
||||
returnData.push({
|
||||
name: `${row['0000'] as string} (${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) {
|
||||
const { views } = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api-gateway/api/v2/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;
|
||||
}
|
||||
100
packages/nodes-base/nodes/SeaTable/v2/types.ts
Normal file
100
packages/nodes-base/nodes/SeaTable/v2/types.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// ----------------------------------
|
||||
// SeaTable
|
||||
// ----------------------------------
|
||||
|
||||
export type TSeaTableServerVersion = '2.0.6';
|
||||
export type TSeaTableServerEdition = 'enterprise edition';
|
||||
|
||||
// ----------------------------------
|
||||
// dtable
|
||||
// ----------------------------------
|
||||
|
||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||
|
||||
import type {
|
||||
IDtableMetadataColumn,
|
||||
IDtableMetadataTable,
|
||||
TDtableViewColumn,
|
||||
} from './actions/Interfaces';
|
||||
|
||||
export type TColumnType =
|
||||
| 'text'
|
||||
| 'long-text'
|
||||
| 'number'
|
||||
| 'collaborator'
|
||||
| 'date'
|
||||
| 'duration'
|
||||
| 'single-select'
|
||||
| 'multiple-select'
|
||||
| 'image'
|
||||
| 'file'
|
||||
| 'email'
|
||||
| 'url'
|
||||
| 'checkbox'
|
||||
| 'rate'
|
||||
| 'formula'
|
||||
| 'link-formula'
|
||||
| 'geolocation'
|
||||
| 'link'
|
||||
| 'creator'
|
||||
| 'ctime'
|
||||
| 'last-modifier'
|
||||
| 'mtime'
|
||||
| 'auto-number'
|
||||
| 'button'
|
||||
| 'digital-sign';
|
||||
|
||||
export type TInheritColumnKey =
|
||||
| '_id'
|
||||
| '_creator'
|
||||
| '_ctime'
|
||||
| '_last_modifier'
|
||||
| '_mtime'
|
||||
| '_seq'
|
||||
| '_archived'
|
||||
| '_locked'
|
||||
| '_locked_by';
|
||||
|
||||
export type TColumnValue = undefined | boolean | number | string | string[] | null;
|
||||
export type TColumnKey = TInheritColumnKey | string;
|
||||
|
||||
export type TDtableMetadataTables = readonly IDtableMetadataTable[];
|
||||
export type TDtableMetadataColumns = IDtableMetadataColumn[];
|
||||
export type TDtableViewColumns = readonly TDtableViewColumn[];
|
||||
|
||||
// ----------------------------------
|
||||
// api
|
||||
// ----------------------------------
|
||||
|
||||
export type TEndpointVariableName = 'access_token' | 'dtable_uuid' | 'server';
|
||||
|
||||
// Template Literal Types requires-ts-4.1.5 -- deferred
|
||||
export type TMethod = 'GET' | 'POST';
|
||||
type TEndpoint =
|
||||
| '/api/v2.1/dtable/app-access-token/'
|
||||
| '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/';
|
||||
export type TEndpointExpr = TEndpoint;
|
||||
export type TEndpointResolvedExpr =
|
||||
TEndpoint; /* deferred: but already in use for header values, e.g. authentication */
|
||||
|
||||
export type TDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZ' /* moment.js */;
|
||||
|
||||
// ----------------------------------
|
||||
// node
|
||||
// ----------------------------------
|
||||
|
||||
export type TCredentials = ICredentialDataDecryptedObject | undefined;
|
||||
|
||||
export type TTriggerOperation = 'create' | 'update';
|
||||
|
||||
export type TOperation = 'append' | 'list' | 'metadata';
|
||||
|
||||
export type TLoadedResource = {
|
||||
name: string;
|
||||
};
|
||||
export type TColumnsUiValues = Array<{
|
||||
columnName: string;
|
||||
columnValue: string;
|
||||
}>;
|
||||
|
||||
export type APITypes = 'GET' | 'POST' | 'DELETE' | 'PUT';
|
||||
Reference in New Issue
Block a user