From e791055fcd3615f8232dc7f1f11a15e73fdd8a2a Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Thu, 26 Oct 2023 02:20:43 +0200 Subject: [PATCH 01/14] rework of seatable-n8n-node --- .../credentials/SeaTableApi.credentials.ts | 42 +- .../nodes/SeaTable/SeaTable.node.ts | 465 +----------------- .../nodes/SeaTable/SeaTableTrigger.node.ts | 288 ++++++++--- .../SeaTable/{seaTable.svg => seatable.svg} | 0 .../SeaTable/{ => v1}/GenericFunctions.ts | 0 .../nodes/SeaTable/{ => v1}/Interfaces.ts | 0 .../nodes/SeaTable/{ => v1}/RowDescription.ts | 0 .../nodes/SeaTable/{ => v1}/Schema.ts | 0 .../nodes/SeaTable/v1/SeaTableV1.node.ts | 451 +++++++++++++++++ .../nodes/SeaTable/{ => v1}/types.ts | 0 .../nodes/SeaTable/v2/GenericFunctions.ts | 338 +++++++++++++ .../nodes-base/nodes/SeaTable/v2/Schema.ts | 61 +++ .../nodes/SeaTable/v2/SeaTableV2.node.ts | 27 + .../nodes/SeaTable/v2/actions/Interfaces.ts | 195 ++++++++ .../actions/asset/getPublicURL/description.ts | 19 + .../v2/actions/asset/getPublicURL/execute.ts | 21 + .../v2/actions/asset/getPublicURL/index.ts | 4 + .../nodes/SeaTable/v2/actions/asset/index.ts | 36 ++ .../v2/actions/asset/upload/description.ts | 104 ++++ .../v2/actions/asset/upload/execute.ts | 89 ++++ .../SeaTable/v2/actions/asset/upload/index.ts | 4 + .../v2/actions/base/apiCall/description.ts | 136 +++++ .../v2/actions/base/apiCall/execute.ts | 15 + .../SeaTable/v2/actions/base/apiCall/index.ts | 4 + .../actions/base/collaborator/description.ts | 20 + .../v2/actions/base/collaborator/execute.ts | 25 + .../v2/actions/base/collaborator/index.ts | 4 + .../nodes/SeaTable/v2/actions/base/index.ts | 53 ++ .../v2/actions/base/metadata/description.ts | 3 + .../v2/actions/base/metadata/execute.ts | 15 + .../v2/actions/base/metadata/index.ts | 4 + .../v2/actions/base/snapshot/description.ts | 3 + .../v2/actions/base/snapshot/execute.ts | 17 + .../v2/actions/base/snapshot/index.ts | 4 + .../v2/actions/link/add/description.ts | 69 +++ .../SeaTable/v2/actions/link/add/execute.ts | 25 + .../SeaTable/v2/actions/link/add/index.ts | 4 + .../nodes/SeaTable/v2/actions/link/index.ts | 36 ++ .../v2/actions/link/remove/description.ts | 69 +++ .../v2/actions/link/remove/execute.ts | 28 ++ .../SeaTable/v2/actions/link/remove/index.ts | 4 + .../nodes/SeaTable/v2/actions/router.ts | 56 +++ .../v2/actions/row/create/description.ts | 121 +++++ .../SeaTable/v2/actions/row/create/execute.ts | 62 +++ .../SeaTable/v2/actions/row/create/index.ts | 4 + .../v2/actions/row/get/description.ts | 54 ++ .../SeaTable/v2/actions/row/get/execute.ts | 42 ++ .../SeaTable/v2/actions/row/get/index.ts | 4 + .../nodes/SeaTable/v2/actions/row/index.ts | 76 +++ .../v2/actions/row/lock/description.ts | 40 ++ .../SeaTable/v2/actions/row/lock/execute.ts | 20 + .../SeaTable/v2/actions/row/lock/index.ts | 4 + .../v2/actions/row/remove/description.ts | 40 ++ .../SeaTable/v2/actions/row/remove/execute.ts | 25 + .../SeaTable/v2/actions/row/remove/index.ts | 4 + .../v2/actions/row/search/description.ts | 83 ++++ .../SeaTable/v2/actions/row/search/execute.ts | 67 +++ .../SeaTable/v2/actions/row/search/index.ts | 4 + .../v2/actions/row/unlock/description.ts | 40 ++ .../SeaTable/v2/actions/row/unlock/execute.ts | 23 + .../SeaTable/v2/actions/row/unlock/index.ts | 4 + .../v2/actions/row/update/description.ts | 137 ++++++ .../SeaTable/v2/actions/row/update/execute.ts | 63 +++ .../SeaTable/v2/actions/row/update/index.ts | 4 + .../SeaTable/v2/actions/versionDescription.ts | 57 +++ .../nodes/SeaTable/v2/methods/index.ts | 1 + .../nodes/SeaTable/v2/methods/loadOptions.ts | 227 +++++++++ .../nodes-base/nodes/SeaTable/v2/types.ts | 97 ++++ 68 files changed, 3491 insertions(+), 550 deletions(-) rename packages/nodes-base/nodes/SeaTable/{seaTable.svg => seatable.svg} (100%) rename packages/nodes-base/nodes/SeaTable/{ => v1}/GenericFunctions.ts (100%) rename packages/nodes-base/nodes/SeaTable/{ => v1}/Interfaces.ts (100%) rename packages/nodes-base/nodes/SeaTable/{ => v1}/RowDescription.ts (100%) rename packages/nodes-base/nodes/SeaTable/{ => v1}/Schema.ts (100%) create mode 100644 packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts rename packages/nodes-base/nodes/SeaTable/{ => v1}/types.ts (100%) create mode 100644 packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/Schema.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/add/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/add/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/router.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/create/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/get/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/get/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/get/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/search/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/update/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/update/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/versionDescription.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/methods/index.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/types.ts diff --git a/packages/nodes-base/credentials/SeaTableApi.credentials.ts b/packages/nodes-base/credentials/SeaTableApi.credentials.ts index 1481548510..2aad821fb0 100644 --- a/packages/nodes-base/credentials/SeaTableApi.credentials.ts +++ b/packages/nodes-base/credentials/SeaTableApi.credentials.ts @@ -1,24 +1,10 @@ -import type { ICredentialType, INodeProperties, INodePropertyOptions } from 'n8n-workflow'; - -import moment from 'moment-timezone'; - -// Get options for timezones -const timezones: INodePropertyOptions[] = moment.tz - .countries() - .reduce((tz: INodePropertyOptions[], country: string) => { - const zonesForCountry = moment.tz - .zonesForCountry(country) - .map((zone) => ({ value: zone, name: zone })); - return tz.concat(zonesForCountry); - }, []); +import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow'; export class SeaTableApi implements ICredentialType { name = 'seaTableApi'; - displayName = 'SeaTable API'; - - documentationUrl = 'seaTable'; - + documentationUrl = + 'https://seatable.io/docs/n8n-integration/erstellen-eines-api-tokens-fuer-n8n/?lang=auto'; properties: INodeProperties[] = [ { displayName: 'Environment', @@ -41,7 +27,7 @@ export class SeaTableApi implements ICredentialType { name: 'domain', type: 'string', default: '', - placeholder: 'https://www.mydomain.com', + placeholder: 'https://seatable.example.com', displayOptions: { show: { environment: ['selfHosted'], @@ -52,16 +38,20 @@ export class SeaTableApi implements ICredentialType { displayName: 'API Token (of a Base)', name: 'token', type: 'string', + description: + 'The API-Token of the SeaTable base you would like to use with n8n. n8n can only connect to one base a at a time.', typeOptions: { password: true }, default: '', }, - { - displayName: 'Timezone', - name: 'timezone', - type: 'options', - default: '', - description: "Seatable server's timezone", - options: [...timezones], - }, ]; + + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials?.domain || "https://cloud.seatable.io" }}', + url: '/api/v2.1/dtable/app-access-token/', + headers: { + Authorization: '={{"Token " + $credentials.token}}', + }, + }, + }; } diff --git a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts index 4f8f55b856..51c9723fa2 100644 --- a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts +++ b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts @@ -1,451 +1,26 @@ -import type { - IExecuteFunctions, - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, -} from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -import { - getTableColumns, - getTableViews, - rowExport, - rowFormatColumns, - rowMapKeyToName, - seaTableApiRequest, - setableApiRequestAllItems, - split, - updateAble, -} from './GenericFunctions'; +import { SeaTableV1 } from './v1/SeaTableV1.node'; +import { SeaTableV2 } from './v2/SeaTableV2.node'; -import { rowFields, rowOperations } from './RowDescription'; +export class SeaTable extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'SeaTable', + name: 'seatable', + icon: 'file:seatable.svg', + group: ['output'], + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Read, update, write and delete data from SeaTable', + defaultVersion: 2, + }; -import type { TColumnsUiValues, TColumnValue } from './types'; + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new SeaTableV1(), + 2: new SeaTableV2(baseDescription), + }; -import type { ICtx, IRow, IRowObject } from './Interfaces'; - -export class SeaTable implements INodeType { - description: INodeTypeDescription = { - displayName: 'SeaTable', - name: 'seaTable', - icon: 'file:seaTable.svg', - group: ['input'], - version: 1, - subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', - description: 'Consume the SeaTable API', - defaults: { - name: 'SeaTable', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'seaTableApi', - required: true, - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Row', - value: 'row', - }, - ], - default: 'row', - }, - ...rowOperations, - ...rowFields, - ], - }; - - methods = { - loadOptions: { - async getTableNames(this: ILoadOptionsFunctions) { - const returnData: INodePropertyOptions[] = []; - const { - metadata: { tables }, - } = await seaTableApiRequest.call( - this, - {}, - 'GET', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata', - ); - for (const table of tables) { - returnData.push({ - name: table.name, - value: table.name, - }); - } - return returnData; - }, - async getTableIds(this: ILoadOptionsFunctions) { - const returnData: INodePropertyOptions[] = []; - const { - metadata: { tables }, - } = await seaTableApiRequest.call( - this, - {}, - 'GET', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata', - ); - for (const table of tables) { - returnData.push({ - name: table.name, - value: table._id, - }); - } - return returnData; - }, - - async getTableUpdateAbleColumns(this: ILoadOptionsFunctions) { - const tableName = this.getNodeParameter('tableName') as string; - const columns = await getTableColumns.call(this, tableName); - return columns - .filter((column) => column.editable) - .map((column) => ({ name: column.name, value: column.name })); - }, - async getAllSortableColumns(this: ILoadOptionsFunctions) { - const tableName = this.getNodeParameter('tableName') as string; - const columns = await getTableColumns.call(this, tableName); - return columns - .filter( - (column) => - !['file', 'image', 'url', 'collaborator', 'long-text'].includes(column.type), - ) - .map((column) => ({ name: column.name, value: column.name })); - }, - async getViews(this: ILoadOptionsFunctions) { - const tableName = this.getNodeParameter('tableName') as string; - const views = await getTableViews.call(this, tableName); - return views.map((view) => ({ name: view.name, value: view.name })); - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - 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); } } diff --git a/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts b/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts index 9ede573334..d27e55144a 100644 --- a/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts +++ b/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts @@ -1,23 +1,33 @@ import type { IPollFunctions, - ILoadOptionsFunctions, INodeExecutionData, - INodePropertyOptions, INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { getColumns, rowFormatColumns, seaTableApiRequest, simplify } from './GenericFunctions'; +import { seaTableApiRequest, simplify_new, enrichColumns } from './v2/GenericFunctions'; -import type { ICtx, IRow, IRowResponse } from './Interfaces'; +import type { + ICtx, + IRow, + IRowResponse, + IGetMetadataResult, + IGetRowsResult, + IDtableMetadataColumn, + ICollaborator, + ICollaboratorsResult, + IColumnDigitalSignature, +} from './v2/actions/Interfaces'; import moment from 'moment'; +import { loadOptions } from './v2/methods'; + export class SeaTableTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'SeaTable Trigger', name: 'seaTableTrigger', - icon: 'file:seaTable.svg', + icon: 'file:seatable.svg', group: ['trigger'], version: 1, description: 'Starts the workflow when SeaTable events occur', @@ -35,6 +45,29 @@ export class SeaTableTrigger implements INodeType { inputs: [], outputs: ['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', + }, { displayName: 'Table Name or ID', name: 'tableName', @@ -48,109 +81,206 @@ export class SeaTableTrigger implements INodeType { 'The name of SeaTable table to access. Choose from the list, or specify an ID using an expression.', }, { - displayName: 'Event', - name: 'event', + displayName: 'View Name or ID (optional)', + name: 'viewName', type: 'options', - options: [ - { - name: 'Row Created', - value: 'rowCreated', - description: 'Trigger on newly created rows', + required: false, + displayOptions: { + show: { + event: ['newRow', 'updatedRow'], }, - // { - // name: 'Row Modified', - // value: 'rowModified', - // description: 'Trigger has recently modified rows', - // }, - ], - default: 'rowCreated', + }, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getTableViews', + }, + default: '', + description: 'The name of SeaTable view to access. Choose from the list, or specify ...', }, { - displayName: 'Simplify', + displayName: 'Signature column', + name: 'assetColumn', + type: 'options', + required: true, + displayOptions: { + show: { + event: ['newAsset'], + }, + }, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getSignatureColumns', + }, + default: '', + description: 'Select the digital-signature column that should be tracked.', + }, + { + displayName: 'Simplify output', name: 'simple', type: 'boolean', default: true, description: - 'Whether to return a simplified version of the response instead of the raw data', + 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.', + }, + { + displayName: '"Fetch Test Event" returns max. three items of the last hour.', + name: 'notice', + type: 'notice', + default: '', }, ], }; - methods = { - loadOptions: { - async getTableNames(this: ILoadOptionsFunctions) { - const returnData: INodePropertyOptions[] = []; - const { - metadata: { tables }, - } = await seaTableApiRequest.call( - this, - {}, - 'GET', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata', - ); - for (const table of tables) { - returnData.push({ - name: table.name, - value: table.name, - }); - } - return returnData; - }, - }, - }; + methods = { loadOptions }; async poll(this: IPollFunctions): Promise { const webhookData = this.getWorkflowStaticData('node'); - const tableName = this.getNodeParameter('tableName') as string; - const simple = this.getNodeParameter('simple') as boolean; const event = this.getNodeParameter('event') as string; + const tableName = this.getNodeParameter('tableName') as string; + const viewName = (event != 'newAsset' ? this.getNodeParameter('viewName') : '') as string; + const assetColumn = (event == 'newAsset' ? this.getNodeParameter('assetColumn') : '') as string; + const simple = this.getNodeParameter('simple') as boolean; + const ctx: ICtx = {}; - const credentials = await this.getCredentials('seaTableApi'); - const timezone = (credentials.timezone as string) || 'Europe/Berlin'; - const now = moment().utc().format(); - const startDate = (webhookData.lastTimeChecked as string) || now; - const endDate = now; - webhookData.lastTimeChecked = endDate; + const startDate = + this.getMode() === 'manual' + ? moment().utc().subtract(1, 'h').format() + : (webhookData.lastTimeChecked as string); + const endDate = (webhookData.lastTimeChecked = moment().utc().format()); - let rows; + // this is working, even if the columns _mtime and _ctime have other names. Only relevant for newRow / updatedRow. + const filterField = event === 'newRow' ? '_ctime' : '_mtime'; - const filterField = event === 'rowCreated' ? '_ctime' : '_mtime'; + // Difference between getRows and SqlQuery: + // ==================== - const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/'; + // getRows (if view is selected) + // getRows always gets up to 1.000 rows of the selected view. + // getRows delivers only the rows, not the metadata + // no possibility to filter for _ctime or _mtime with the API call. + // Problems, not yet solved: + // if a column is empty, the column is not returned! + // view with more than 1.000 rows will not work! - if (this.getMode() === 'manual') { - 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; + // SqlQuery (if no view is selected) + // SqlQuery returns up to 1.000. WHERE by time and ORDER BY _ctime or _mtime is possible. + // SqlQuery returns rows and metadata + + let requestMeta: IGetMetadataResult; + let requestRows: IGetRowsResult; + let metadata: IDtableMetadataColumn[] = []; + let rows: IRow[]; + let sqlResult: IRowResponse; + + const limit = this.getMode() === 'manual' ? 3 : 1000; + + // New Signature + if (event == 'newAsset') { + const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/'; + sqlResult = await seaTableApiRequest.call(this, ctx, 'POST', endpoint, { + sql: `SELECT _id, _ctime, _mtime, \`${assetColumn}\` FROM ${tableName} WHERE \`${assetColumn}\` IS NOT NULL ORDER BY _mtime DESC LIMIT ${limit}`, + convert_keys: true, + }); + + metadata = sqlResult.metadata as IDtableMetadataColumn[]; + const columnType = metadata.find((obj) => obj.name == assetColumn); + const assetColumnType = columnType?.type || null; + + // remove unwanted entries + rows = sqlResult.results.filter( + (obj) => new Date(obj['_mtime']) > new Date(startDate), + ) as IRow[]; + + // split the objects into new lines (not necessary for digital-sign) + const newRows: any = []; + for (const row of rows) { + if (assetColumnType === 'digital-sign') { + let signature = (row[assetColumn] as IColumnDigitalSignature) || []; + if (signature.sign_time) { + if (new Date(signature.sign_time) > new Date(startDate)) { + newRows.push(signature); + } + } + } + } } - let response; + // View => use getRows. + else if (viewName) { + requestMeta = await seaTableApiRequest.call( + this, + ctx, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/', + ); + requestRows = await seaTableApiRequest.call( + this, + ctx, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + {}, + { + table_name: tableName, + view_name: viewName, + limit: limit, + }, + ); - if (rows.metadata && rows.results) { - const columns = getColumns(rows); - if (simple) { - response = simplify(rows, columns); + // I need only metadata of the selected table. + 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 as IRow[]; } else { - response = rows.results; + rows = requestRows.rows.filter( + (obj) => new Date(obj[filterField]) > new Date(startDate), + ) as IRow[]; + } + } + + // No view => use SQL-Query + else { + const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/'; + const sqlQuery = `SELECT * FROM \`${tableName}\` WHERE ${filterField} BETWEEN "${moment( + startDate, + ).format('YYYY-MM-D HH:mm:ss')}" AND "${moment(endDate).format( + 'YYYY-MM-D HH:mm:ss', + )}" ORDER BY ${filterField} DESC LIMIT ${limit}`; + sqlResult = await seaTableApiRequest.call(this, ctx, 'POST', endpoint, { + sql: sqlQuery, + convert_keys: true, + }); + metadata = sqlResult.metadata as IDtableMetadataColumn[]; + rows = sqlResult.results as IRow[]; + } + + // ========================================= + // => now I have rows and metadata. + + // lets get the collaborators + let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call( + this, + ctx, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/', + ); + let collaborators: ICollaborator[] = collaboratorsResult.user_list || []; + + if (Array.isArray(rows) && rows.length > 0) { + // remove columns starting with _ if simple; + if (simple) { + rows = rows.map((row) => simplify_new(row)); } - const allColumns = rows.metadata.map((meta) => meta.name); + // enrich column types like {collaborator, creator, last_modifier}, {image, file} + // remove button column from rows + rows = rows.map((row) => enrichColumns(row, metadata, collaborators)); - response = response - //@ts-ignore - .map((row: IRow) => rowFormatColumns(row, allColumns)) - .map((row: IRow) => ({ json: row })); - } - - if (Array.isArray(response) && response.length) { - return [response]; + // prepare for final output + return [this.helpers.returnJsonArray(rows)]; } return null; diff --git a/packages/nodes-base/nodes/SeaTable/seaTable.svg b/packages/nodes-base/nodes/SeaTable/seatable.svg similarity index 100% rename from packages/nodes-base/nodes/SeaTable/seaTable.svg rename to packages/nodes-base/nodes/SeaTable/seatable.svg diff --git a/packages/nodes-base/nodes/SeaTable/GenericFunctions.ts b/packages/nodes-base/nodes/SeaTable/v1/GenericFunctions.ts similarity index 100% rename from packages/nodes-base/nodes/SeaTable/GenericFunctions.ts rename to packages/nodes-base/nodes/SeaTable/v1/GenericFunctions.ts diff --git a/packages/nodes-base/nodes/SeaTable/Interfaces.ts b/packages/nodes-base/nodes/SeaTable/v1/Interfaces.ts similarity index 100% rename from packages/nodes-base/nodes/SeaTable/Interfaces.ts rename to packages/nodes-base/nodes/SeaTable/v1/Interfaces.ts diff --git a/packages/nodes-base/nodes/SeaTable/RowDescription.ts b/packages/nodes-base/nodes/SeaTable/v1/RowDescription.ts similarity index 100% rename from packages/nodes-base/nodes/SeaTable/RowDescription.ts rename to packages/nodes-base/nodes/SeaTable/v1/RowDescription.ts diff --git a/packages/nodes-base/nodes/SeaTable/Schema.ts b/packages/nodes-base/nodes/SeaTable/v1/Schema.ts similarity index 100% rename from packages/nodes-base/nodes/SeaTable/Schema.ts rename to packages/nodes-base/nodes/SeaTable/v1/Schema.ts diff --git a/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts b/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts new file mode 100644 index 0000000000..058ae00eca --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts @@ -0,0 +1,451 @@ +import type { + IExecuteFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +import { + getTableColumns, + getTableViews, + rowExport, + rowFormatColumns, + rowMapKeyToName, + seaTableApiRequest, + setableApiRequestAllItems, + split, + updateAble, +} from './GenericFunctions'; + +import { rowFields, rowOperations } from './RowDescription'; + +import type { TColumnsUiValues, TColumnValue } from './types'; + +import type { ICtx, IRow, IRowObject } from './Interfaces'; + +export class SeaTableV1 implements INodeType { + description: INodeTypeDescription = { + displayName: 'SeaTable', + name: 'seaTable', + icon: 'file:seaTable.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the SeaTable API', + defaults: { + name: 'SeaTable', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'seaTableApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Row', + value: 'row', + }, + ], + default: 'row', + }, + ...rowOperations, + ...rowFields, + ], + }; + + methods = { + loadOptions: { + async getTableNames(this: ILoadOptionsFunctions) { + const returnData: INodePropertyOptions[] = []; + const { + metadata: { tables }, + } = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata', + ); + for (const table of tables) { + returnData.push({ + name: table.name, + value: table.name, + }); + } + return returnData; + }, + async getTableIds(this: ILoadOptionsFunctions) { + const returnData: INodePropertyOptions[] = []; + const { + metadata: { tables }, + } = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata', + ); + for (const table of tables) { + returnData.push({ + name: table.name, + value: table._id, + }); + } + return returnData; + }, + + async getTableUpdateAbleColumns(this: ILoadOptionsFunctions) { + const tableName = this.getNodeParameter('tableName') as string; + const columns = await getTableColumns.call(this, tableName); + return columns + .filter((column) => column.editable) + .map((column) => ({ name: column.name, value: column.name })); + }, + async getAllSortableColumns(this: ILoadOptionsFunctions) { + const tableName = this.getNodeParameter('tableName') as string; + const columns = await getTableColumns.call(this, tableName); + return columns + .filter( + (column) => + !['file', 'image', 'url', 'collaborator', 'long-text'].includes(column.type), + ) + .map((column) => ({ name: column.name, value: column.name })); + }, + async getViews(this: ILoadOptionsFunctions) { + const tableName = this.getNodeParameter('tableName') as string; + const views = await getTableViews.call(this, tableName); + return views.map((view) => ({ name: view.name, value: view.name })); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + 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]; + } +} diff --git a/packages/nodes-base/nodes/SeaTable/types.ts b/packages/nodes-base/nodes/SeaTable/v1/types.ts similarity index 100% rename from packages/nodes-base/nodes/SeaTable/types.ts rename to packages/nodes-base/nodes/SeaTable/v1/types.ts diff --git a/packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts b/packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts new file mode 100644 index 0000000000..6f57e50efe --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts @@ -0,0 +1,338 @@ +import type { OptionsWithUri } from 'request'; +import FormData from 'form-data'; + +import type { + IDataObject, + IExecuteFunctions, + ILoadOptionsFunctions, + IPollFunctions, + JsonObject, + IHttpRequestMethods, +} from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; + +import type { TDtableMetadataColumns, TEndpointVariableName } from './types'; + +import { schema } from './Schema'; + +import type { + ICollaborator, + ICollaboratorsResult, + ICredential, + ICtx, + IDtableMetadataColumn, + IEndpointVariables, + IName, + IRow, + IRowObject, + IColumnDigitalSignature, + IFile, +} from './actions/Interfaces'; + +// remove last backslash +const userBaseUri = (uri?: string) => { + if (uri === undefined) return uri; + if (uri.endsWith('/')) return uri.slice(0, -1); + return uri; +}; + +export function resolveBaseUri(ctx: ICtx) { + return ctx?.credentials?.environment === 'cloudHosted' + ? 'https://cloud.seatable.io' + : userBaseUri(ctx?.credentials?.domain); +} + +export async function getBaseAccessToken( + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, + ctx: ICtx, +) { + if (ctx?.base?.access_token !== undefined) return; + + const options: OptionsWithUri = { + headers: { + Authorization: `Token ${ctx?.credentials?.token}`, + }, + uri: `${resolveBaseUri(ctx)}/api/v2.1/dtable/app-access-token/`, + json: true, + }; + ctx.base = await this.helpers.request(options); +} + +function endpointCtxExpr(ctx: ICtx, endpoint: string): string { + const endpointVariables: IEndpointVariables = {}; + endpointVariables.access_token = ctx?.base?.access_token; + endpointVariables.dtable_uuid = ctx?.base?.dtable_uuid; + + return endpoint.replace( + /({{ *(access_token|dtable_uuid|server) *}})/g, + (match: string, expr: string, name: TEndpointVariableName) => { + // I need expr. Why? + return (endpointVariables[name] as string) || match; + }, + ); +} + +export async function seaTableApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, + ctx: ICtx, + method: IHttpRequestMethods, + endpoint: string, + body: IDataObject | FormData | string | Buffer = {}, + qs: IDataObject = {}, + url: string | undefined = undefined, + option: IDataObject = {}, +): Promise { + const credentials = await this.getCredentials('seaTableApi'); + + ctx.credentials = credentials as unknown as ICredential; + + await getBaseAccessToken.call(this, ctx); + + // some API endpoints require the api_token instead of base_access_token. + const token = + endpoint.indexOf('/api/v2.1/dtable/app-download-link/') === 0 || + endpoint == '/api/v2.1/dtable/app-upload-link/' || + endpoint.indexOf('/seafhttp/upload-api') === 0 + ? `${ctx?.credentials?.token}` + : `${ctx?.base?.access_token}`; + + let options: OptionsWithUri = { + uri: url || `${resolveBaseUri(ctx)}${endpointCtxExpr(ctx, endpoint)}`, + headers: { + Authorization: `Token ${token}`, + }, + method, + qs, + body, + json: true, + }; + + if (Object.keys(option).length !== 0) { + options = Object.assign({}, options, option); + } + + // remove header from download request. + if (endpoint.indexOf('/seafhttp/files/') === 0) { + delete options.headers; + } + + // enhance header for upload request + if (endpoint.indexOf('/seafhttp/upload-api') === 0) { + options.json = true; + options.headers = { + ...options.headers, + 'Content-Type': 'multipart/form-data', + }; + } + + // DEBUG-MODE OR API-REQUESTS + // console.log(options); + + if (Object.keys(body).length === 0) { + delete options.body; + } + + try { + return this.helpers.requestWithAuthentication.call(this, 'seaTableApi', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error as JsonObject); + } +} + +export async function getBaseCollaborators( + this: ILoadOptionsFunctions | IExecuteFunctions | IPollFunctions, +): Promise { + let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/', + ); + let collaborators: ICollaborator[] = collaboratorsResult.user_list || []; + return collaborators; +} + +export async function getTableColumns( + this: ILoadOptionsFunctions | IExecuteFunctions | IPollFunctions, + tableName: string, + ctx: ICtx = {}, +): Promise { + const { + metadata: { tables }, + } = await seaTableApiRequest.call( + this, + ctx, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata', + ); + for (const table of tables) { + if (table.name === tableName) { + return table.columns; + } + } + return []; +} + +export function simplify_new(row: IRow) { + for (const key of Object.keys(row)) { + if (key.startsWith('_')) delete row[key]; + } + return row; +} + +/*const uniquePredicate = (current: string, index: number, all: string[]) => + all.indexOf(current) === index; +const nonInternalPredicate = (name: string) => !Object.keys(schema.internalNames).includes(name);*/ +const namePredicate = (name: string) => (named: IName) => named.name === name; +export const nameOfPredicate = (names: readonly IName[]) => (name: string) => + names.find(namePredicate(name)); + +const normalize = (subject: string): string => (subject ? subject.normalize() : ''); + +/* will ich diesen call ? */ +export const split = (subject: string): string[] => + normalize(subject) + .split(/\s*((?:[^\\,]*?(?:\\[\s\S])*)*?)\s*(?:,|$)/) + .filter((s) => s.length) + .map((s) => s.replace(/\\([\s\S])/gm, ($0, $1) => $1)); + +// INTERNAL: get collaborator info from @auth.local address +function getCollaboratorInfo( + authLocal: string | null | undefined, + collaboratorList: ICollaborator[], +) { + let collaboratorDetails: ICollaborator; + collaboratorDetails = collaboratorList.find( + (singleCollaborator) => singleCollaborator['email'] === authLocal, + ) || { contact_email: 'unknown', name: 'unkown', email: 'unknown' }; + return collaboratorDetails; +} + +// INTERNAL: split asset path. +function getAssetPath(type: string, url: string) { + const parts = url.split(`/${type}/`); + if (parts[1]) { + return '/' + type + '/' + parts[1]; + } + return url; +} + +// CDB: neu von mir +export function enrichColumns( + row: IRow, + metadata: IDtableMetadataColumn[], + collaboratorList: ICollaborator[], +): IRow { + Object.keys(row).forEach((key) => { + let columnDef = metadata.find((obj) => obj.name === key || obj.key === key); + //console.log(key + " is from type " + columnDef?.type); + + if (columnDef?.type === 'collaborator') { + // collaborator is an array of strings. + let collaborators = (row[key] as string[]) || []; + if (collaborators.length > 0) { + let newArray = collaborators.map((email) => { + let collaboratorDetails = getCollaboratorInfo(email, collaboratorList); + let newColl = { + email: email, + contact_email: collaboratorDetails['contact_email'], + name: collaboratorDetails['name'], + }; + return newColl; + }); + row[key] = newArray; + } + } + + if ( + columnDef?.type === 'last-modifier' || + columnDef?.type === 'creator' || + columnDef?.key === '_creator' || + columnDef?.key === '_last_modifier' + ) { + // creator or last-modifier are always a single string. + let collaboratorDetails = getCollaboratorInfo(row[key] as string, collaboratorList); + row[key] = { + email: row[key], + contact_email: collaboratorDetails['contact_email'], + name: collaboratorDetails['name'], + }; + } + + if (columnDef?.type === 'image') { + let pictures = (row[key] as string[]) || []; + if (pictures.length > 0) { + let newArray = pictures.map((url) => ({ + name: url.split('/').pop(), + size: 0, + type: 'image', + url: url, + path: getAssetPath('images', url), + })); + row[key] = newArray; + } + } + + if (columnDef?.type === 'file') { + let files = (row[key] as IFile[]) || []; + files.forEach((file) => { + file.path = getAssetPath('files', file.url); + }); + } + + if (columnDef?.type === 'digital-sign') { + let digitalSignature: IColumnDigitalSignature | any = row[key]; + let collaboratorDetails = getCollaboratorInfo(digitalSignature?.username, collaboratorList); + if (digitalSignature?.username) { + digitalSignature.contact_email = collaboratorDetails['contact_email']; + digitalSignature.name = collaboratorDetails['name']; + } + } + + if (columnDef?.type === 'button') { + delete row[key]; + } + }); + + return row; +} + +// using create, I input a string like a5adebe279e04415a28b2c7e256e9e8d@auth.local and it should be transformed to an array. +// same with multi-select. +export function splitStringColumnsToArrays( + row: IRowObject, + columns: TDtableMetadataColumns, +): IRowObject { + columns.map((column) => { + if (column.type == 'collaborator' || column.type == 'multiple-select') { + if (typeof row[column.name] === 'string') { + const input = row[column.name] as string; + row[column.name] = input.split(',').map((item) => item.trim()); + } + } + }); + return row; +} + +// sollte eher heißen: remove nonUpdateColumnTypes and only use allowed columns! +export function rowExport(row: IRowObject, columns: TDtableMetadataColumns): IRowObject { + let rowAllowed = {} as IRowObject; + columns.map((column) => { + if (row[column.name]) { + rowAllowed[column.name] = row[column.name]; + } + }); + return rowAllowed; +} + +export const dtableSchemaIsColumn = (column: IDtableMetadataColumn): boolean => + !!schema.columnTypes[column.type]; + +const dtableSchemaIsUpdateAbleColumn = (column: IDtableMetadataColumn): boolean => + !!schema.columnTypes[column.type] && !schema.nonUpdateAbleColumnTypes[column.type]; + +export const dtableSchemaColumns = (columns: TDtableMetadataColumns): TDtableMetadataColumns => + columns.filter(dtableSchemaIsColumn); + +export const updateAble = (columns: TDtableMetadataColumns): TDtableMetadataColumns => + columns.filter(dtableSchemaIsUpdateAbleColumn); diff --git a/packages/nodes-base/nodes/SeaTable/v2/Schema.ts b/packages/nodes-base/nodes/SeaTable/v2/Schema.ts new file mode 100644 index 0000000000..d64ad3bbe6 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/Schema.ts @@ -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 }; +}; diff --git a/packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts b/packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts new file mode 100644 index 0000000000..2acbeee4d1 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts @@ -0,0 +1,27 @@ +import type { + IExecuteFunctions, + INodeType, + INodeTypeDescription, + INodeTypeBaseDescription, +} from 'n8n-workflow'; + +import { versionDescription } from './actions/versionDescription'; +import { loadOptions } from './methods'; +import { router } from './actions/router'; + +export class SeaTableV2 implements INodeType { + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } + + methods = { loadOptions }; + + async execute(this: IExecuteFunctions) { + return router.call(this); + } +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts new file mode 100644 index 0000000000..50af0f7b6f --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts @@ -0,0 +1,195 @@ +import type { AllEntities, Entity, PropertiesOf } from 'n8n-workflow'; + +type SeaTableMap = { + row: 'create' | 'get' | 'search' | 'update' | 'remove' | 'lock' | 'unlock'; + base: 'snapshot' | 'metadata' | 'apiCall' | 'collaborator'; + link: 'add' | 'remove'; + asset: 'upload' | 'getPublicURL'; +}; + +export type SeaTable = AllEntities; + +export type SeaTableRow = Entity; +export type SeaTableBase = Entity; +export type SeaTableLink = Entity; +export type SeaTableAsset = Entity; + +export type RowProperties = PropertiesOf; +export type BaseProperties = PropertiesOf; +export type LinkProperties = PropertiesOf; +export type AssetProperties = PropertiesOf; + +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; +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/description.ts new file mode 100644 index 0000000000..da37de8e69 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/description.ts @@ -0,0 +1,19 @@ +import type { AssetProperties } from '../../Interfaces'; + +export const assetGetPublicURLDescription: AssetProperties = [ + { + displayName: 'Asset path', + name: 'assetPath', + type: 'string', + placeholder: '/images/2023-09/logo.png', + required: true, + displayOptions: { + show: { + resource: ['asset'], + operation: ['getPublicURL'], + }, + }, + default: '', + description: '', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/execute.ts new file mode 100644 index 0000000000..d980a2cc21 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/execute.ts @@ -0,0 +1,21 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function getPublicURL( + this: IExecuteFunctions, + index: number, +): Promise { + const assetPath = this.getNodeParameter('assetPath', index) as string; + + let responseData = [] as IDataObject[]; + if (assetPath) { + responseData = await seaTableApiRequest.call( + this, + {}, + 'GET', + `/api/v2.1/dtable/app-download-link/?path=${assetPath}`, + ); + } + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/index.ts new file mode 100644 index 0000000000..067493a968 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/index.ts @@ -0,0 +1,4 @@ +import { getPublicURL as execute } from './execute'; +import { assetGetPublicURLDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts new file mode 100644 index 0000000000..661a110d16 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts @@ -0,0 +1,36 @@ +import * as upload from './upload'; +import * as getPublicURL from './getPublicURL'; +import type { INodeProperties } from 'n8n-workflow'; + +export { upload, getPublicURL }; + +export const descriptions: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['asset'], + }, + }, + options: [ + { + name: 'Public URL', + value: 'getPublicURL', + description: 'Get the public URL from asset path', + action: 'Get the public URL from asset path', + }, + { + name: 'Upload', + value: 'upload', + description: 'Add a file/image to an existing row', + action: 'Upload a file/image', + }, + ], + default: 'upload', + }, + ...upload.description, + ...getPublicURL.description, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts new file mode 100644 index 0000000000..7304cf159f --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts @@ -0,0 +1,104 @@ +import type { AssetProperties } from '../../Interfaces'; + +export const assetUploadDescription: AssetProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['asset'], + operation: ['upload'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Column', + name: 'uploadColumn', + type: 'options', + displayOptions: { + show: { + resource: ['asset'], + operation: ['upload'], + }, + }, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getAssetColumns', + }, + required: true, + default: '', + description: 'Select the column for the upload.', + }, + { + displayName: 'Row ID', + name: 'rowId', + type: 'options', + required: true, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getRowIds', + }, + displayOptions: { + show: { + resource: ['asset'], + operation: ['upload'], + }, + }, + default: '', + }, + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'number', + typeOptions: { + minValue: 1, + numberStepSize: 1, + }, + required: true, + displayOptions: { + show: { + resource: ['asset'], + operation: ['upload'], + }, + }, + default: '', + description: + 'How to get the workspace ID: https://seatable.io/docs/arbeiten-mit-gruppen/workspace-id-einer-gruppe-ermitteln/?lang=auto', + }, + { + displayName: 'Property Name', + name: 'dataPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + resource: ['asset'], + operation: ['upload'], + }, + }, + description: 'Name of the binary property which contains the data for the file to be written', + }, + /*{ + displayName: 'Replace', + name: 'replace', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: ['asset'], + operation: ['upload'], + }, + }, + description: 'Replace existing file if the file/image already exists with same name.', + },*/ +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts new file mode 100644 index 0000000000..2f3db9677a --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts @@ -0,0 +1,89 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; +import type { IUploadLink, IRowObject } from '../../Interfaces'; + +export async function upload( + this: IExecuteFunctions, + index: number, +): Promise { + // step 1: upload file to base + const uploadColumn = this.getNodeParameter('uploadColumn', index) as any; + const uploadColumnType = uploadColumn.split(':::')[1]; + const dataPropertyName = this.getNodeParameter('dataPropertyName', index) as string; + const tableName = this.getNodeParameter('tableName', index) as string; + const rowId = this.getNodeParameter('rowId', index) as string; + const workspaceId = this.getNodeParameter('workspaceId', index) as string; + const uploadLink = (await seaTableApiRequest.call( + this, + {}, + 'GET', + '/api/v2.1/dtable/app-upload-link/', + )) as IUploadLink; + + // Get the binary data + const fileBufferData = await this.helpers.getBinaryDataBuffer(index, dataPropertyName); + const binaryData = this.helpers.assertBinaryData(index, dataPropertyName); + // Create our request option + const options = { + formData: { + file: { + value: fileBufferData, + options: { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }, + }, + parent_dir: uploadLink.parent_path, + replace: '0', + relative_path: + uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path, + }, + }; + + // Send the request + let uploadAsset = await seaTableApiRequest.call( + this, + {}, + 'POST', + `/seafhttp/upload-api/${uploadLink.upload_link.split('seafhttp/upload-api/')[1]}?ret-json=true`, + {}, + {}, + '', + options, + ); + + // now step 2 (attaching the file to a column in a base) + for (let c = 0; c < uploadAsset.length; c++) { + const body = { + table_name: tableName, + row_id: rowId, + row: {}, + } as IDataObject; + let rowInput = {} as IRowObject; + + const filePath = [ + `/workspace/${workspaceId}${uploadLink.parent_path}/${uploadLink.img_relative_path}/${uploadAsset[c].name}`, + ]; + + if (uploadColumnType === 'image') { + rowInput[uploadColumn.split(':::')[0]] = filePath; + } else if (uploadColumnType === 'file') { + rowInput[uploadColumn.split(':::')[0]] = uploadAsset; + uploadAsset[c].type = 'file'; + uploadAsset[c].url = filePath; + } + body.row = rowInput; + + const responseData = await seaTableApiRequest.call( + this, + {}, + 'PUT', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + body, + ); + + uploadAsset[c]['upload_successful'] = responseData.success; + } + + return this.helpers.returnJsonArray(uploadAsset as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/index.ts new file mode 100644 index 0000000000..3f8b338788 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/index.ts @@ -0,0 +1,4 @@ +import { upload as execute } from './execute'; +import { assetUploadDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts new file mode 100644 index 0000000000..8e8f36a669 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts @@ -0,0 +1,136 @@ +import type { BaseProperties } from '../../Interfaces'; + +export const baseApiCallDescription: BaseProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['base'], + operation: ['apiCall'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'HTTP Method', + name: 'apiMethod', + type: 'options', + options: [ + { + name: 'POST', + value: 'POST', + }, + { + name: 'GET', + value: 'GET', + }, + { + name: 'POST', + value: 'POST', + }, + { + name: 'DELETE', + value: 'DELETE', + }, + ], + displayOptions: { + show: { + resource: ['base'], + operation: ['apiCall'], + }, + }, + required: true, + default: '', + }, + { + displayName: 'Hint: The Authentication header is included automatically.', + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { + resource: ['base'], + operation: ['apiCall'], + }, + }, + }, + { + displayName: 'URL', + name: 'apiEndpoint', + type: 'string', + displayOptions: { + show: { + operation: ['apiCall'], + }, + }, + required: true, + default: '', + placeholder: '/dtable-server/...', + description: + 'The URL has to start with /dtable-server/ or /dtable-db/. All possible requests can be found at the SeaTable API Reference at https://api.seatable.io \ + Please be aware that only request from the section Base Operations that use an Base-Token for the authentication are allowed to use.', + }, + { + displayName: 'Query String Parameters', + name: 'apiParams', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + description: + 'These params will be URL-encoded and appended to the URL when making the request.', + options: [ + { + name: 'apiParamsValues', + displayName: 'Parameters', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + displayOptions: { + show: { + resource: ['base'], + operation: ['apiCall'], + }, + }, + }, + { + displayName: 'Body', + name: 'apiBody', + type: 'json', + displayOptions: { + show: { + resource: ['base'], + operation: ['apiCall'], + }, + }, + typeOptions: { + rows: 4, + }, + default: '', + description: + 'Only valid JSON is accepted. n8n will pass anything you enter as raw input. For example, {"foo", "bar"} is perfectly valid. Of cause you can use variables from n8n inside your JSON.', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/execute.ts new file mode 100644 index 0000000000..8b212b1d87 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/execute.ts @@ -0,0 +1,15 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function apiCall( + this: IExecuteFunctions, + index: number, +): Promise { + const responseData = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/', + ); + return this.helpers.returnJsonArray(responseData.metadata as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/index.ts new file mode 100644 index 0000000000..599ffa9511 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/index.ts @@ -0,0 +1,4 @@ +import { apiCall as execute } from './execute'; +import { baseApiCallDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/description.ts new file mode 100644 index 0000000000..492df944f1 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/description.ts @@ -0,0 +1,20 @@ +import type { BaseProperties } from '../../Interfaces'; + +export const baseCollaboratorDescription: BaseProperties = [ + { + displayName: 'Name or email of the collaborator', + name: 'searchString', + type: 'string', + placeholder: 'Enter the name or the email or the collaborator', + required: true, + displayOptions: { + show: { + resource: ['base'], + operation: ['collaborator'], + }, + }, + default: '', + description: + 'SeaTable identifies users with a unique username like 244b43hr6fy54bb4afa2c2cb7369d244@auth.local. Get this username from an email or the name of a collaborator.', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/execute.ts new file mode 100644 index 0000000000..57878dda9e --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/execute.ts @@ -0,0 +1,25 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; +import { ICollaborator } from '../../Interfaces'; + +export async function collaborator( + this: IExecuteFunctions, + index: number, +): Promise { + const searchString = this.getNodeParameter('searchString', index) as string; + + const collaboratorsResult = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/', + ); + const collaborators = collaboratorsResult.user_list || []; + + const collaborator = collaborators.filter( + (col: ICollaborator) => + col.contact_email.includes(searchString) || col.name.includes(searchString), + ); + + return this.helpers.returnJsonArray(collaborator as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/index.ts new file mode 100644 index 0000000000..ca007fb424 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/collaborator/index.ts @@ -0,0 +1,4 @@ +import { collaborator as execute } from './execute'; +import { baseCollaboratorDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts new file mode 100644 index 0000000000..9e83556515 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts @@ -0,0 +1,53 @@ +import * as snapshot from './snapshot'; +import * as metadata from './metadata'; +import * as apiCall from './apiCall'; +import * as collaborator from './collaborator'; + +import type { INodeProperties } from 'n8n-workflow'; + +export { snapshot, metadata, apiCall, collaborator }; + +export const descriptions: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['base'], + }, + }, + options: [ + { + name: 'Snapshot', + value: 'snapshot', + description: 'Create a snapshot of the base', + action: 'Create a Snapshot', + }, + { + name: 'Metadata', + value: 'metadata', + description: 'Get the complete metadata of the base', + action: 'Get metadata of a base', + }, + { + name: 'API Call', + value: 'apiCall', + description: 'Perform an authorized API call (Base Operation)', + action: 'Make an API Call', + }, + { + name: 'Collaborator', + value: 'collaborator', + description: 'Get this username from the email or name of a collaborator.', + action: 'Get username from email or name', + }, + ], + default: '', + }, + ...snapshot.description, + ...metadata.description, + ...apiCall.description, + ...collaborator.description, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/description.ts new file mode 100644 index 0000000000..7ecc84ee01 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/description.ts @@ -0,0 +1,3 @@ +import type { BaseProperties } from '../../Interfaces'; + +export const baseMetadataDescription: BaseProperties = []; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/execute.ts new file mode 100644 index 0000000000..5ee8904e92 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/execute.ts @@ -0,0 +1,15 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function metadata( + this: IExecuteFunctions, + index: number, +): Promise { + const responseData = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/', + ); + return this.helpers.returnJsonArray(responseData.metadata as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/index.ts new file mode 100644 index 0000000000..5c1fc8a545 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/metadata/index.ts @@ -0,0 +1,4 @@ +import { metadata as execute } from './execute'; +import { baseMetadataDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/description.ts new file mode 100644 index 0000000000..4a414cc8ed --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/description.ts @@ -0,0 +1,3 @@ +import type { BaseProperties } from '../../Interfaces'; + +export const baseSnapshotDescription: BaseProperties = []; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/execute.ts new file mode 100644 index 0000000000..9cea3c2374 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/execute.ts @@ -0,0 +1,17 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function snapshot( + this: IExecuteFunctions, + index: number, +): Promise { + const responseData = await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/snapshot/', + { dtable_name: 'snapshot' }, + ); + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/index.ts new file mode 100644 index 0000000000..085a4731a5 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/snapshot/index.ts @@ -0,0 +1,4 @@ +import { snapshot as execute } from './execute'; +import { baseSnapshotDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts new file mode 100644 index 0000000000..e82c1f6a46 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts @@ -0,0 +1,69 @@ +import type { LinkProperties } from '../../Interfaces'; + +export const linkAddDescription: LinkProperties = [ + { + displayName: 'Table Name (Source)', + name: 'tableName', + type: 'options', + placeholder: 'Name of table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNameAndId', + }, + displayOptions: { + show: { + resource: ['link'], + operation: ['add'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Link column', + name: 'linkColumn', + type: 'options', + displayOptions: { + show: { + resource: ['link'], + operation: ['add'], + }, + }, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getLinkColumns', + }, + required: true, + default: '', + description: 'Select the column to create a link.', + }, + { + displayName: 'Row ID from the source table', + name: 'linkColumnSourceId', + type: 'string', + displayOptions: { + show: { + resource: ['link'], + operation: ['add'], + }, + }, + required: true, + default: '', + description: 'Provide the row ID of table you selected.', + }, + { + displayName: 'Row ID from the target', + name: 'linkColumnTargetId', + type: 'string', + displayOptions: { + show: { + resource: ['link'], + operation: ['add'], + }, + }, + required: true, + default: '', + description: 'Provide the row ID of table you want to link.', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/execute.ts new file mode 100644 index 0000000000..34fe83a8f0 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/execute.ts @@ -0,0 +1,25 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function add(this: IExecuteFunctions, index: number): Promise { + const tableName = this.getNodeParameter('tableName', index) as string; + const linkColumn = this.getNodeParameter('linkColumn', index) as any; + const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string; + const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string; + + const responseData = await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/links/', + { + link_id: linkColumn.split(':::')[1], + table_id: tableName.split(':::')[1], + table_row_id: linkColumnSourceId, + other_table_id: linkColumn.split(':::')[2], + other_table_row_id: linkColumnTargetId, + }, + ); + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/index.ts new file mode 100644 index 0000000000..bda9cb5f0e --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/index.ts @@ -0,0 +1,4 @@ +import { add as execute } from './execute'; +import { linkAddDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts new file mode 100644 index 0000000000..56d3c58257 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts @@ -0,0 +1,36 @@ +import * as add from './add'; +import * as remove from './remove'; +import type { INodeProperties } from 'n8n-workflow'; + +export { add, remove }; + +export const descriptions: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['link'], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Create a link between two rows in a link column', + action: 'Add a row link', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a link between two rows from a link column', + action: 'Remove a row link', + }, + ], + default: 'add', + }, + ...add.description, + ...remove.description, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/description.ts new file mode 100644 index 0000000000..a7f4fc35c0 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/description.ts @@ -0,0 +1,69 @@ +import type { LinkProperties } from '../../Interfaces'; + +export const linkRemoveDescription: LinkProperties = [ + { + displayName: 'Table Name (Source)', + name: 'tableName', + type: 'options', + placeholder: 'Name of table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNameAndId', + }, + displayOptions: { + show: { + resource: ['link'], + operation: ['remove'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Link column', + name: 'linkColumn', + type: 'options', + displayOptions: { + show: { + resource: ['link'], + operation: ['remove'], + }, + }, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getLinkColumns', + }, + required: true, + default: '', + description: 'Select the column to create a link.', + }, + { + displayName: 'Row ID from the source table', + name: 'linkColumnSourceId', + type: 'string', + displayOptions: { + show: { + resource: ['link'], + operation: ['remove'], + }, + }, + required: true, + default: '', + description: 'Provide the row ID of table you selected.', + }, + { + displayName: 'Row ID from the target', + name: 'linkColumnTargetId', + type: 'string', + displayOptions: { + show: { + resource: ['link'], + operation: ['remove'], + }, + }, + required: true, + default: '', + description: 'Provide the row ID of table you want to link.', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/execute.ts new file mode 100644 index 0000000000..0c9b7c9dfc --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/execute.ts @@ -0,0 +1,28 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function remove( + this: IExecuteFunctions, + index: number, +): Promise { + const tableName = this.getNodeParameter('tableName', index) as string; + const linkColumn = this.getNodeParameter('linkColumn', index) as any; + const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string; + const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string; + + const responseData = await seaTableApiRequest.call( + this, + {}, + 'DELETE', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/links/', + { + link_id: linkColumn.split(':::')[1], + table_id: tableName.split(':::')[1], + table_row_id: linkColumnSourceId, + other_table_id: linkColumn.split(':::')[2], + other_table_row_id: linkColumnTargetId, + }, + ); + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/index.ts new file mode 100644 index 0000000000..f22fce125a --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/index.ts @@ -0,0 +1,4 @@ +import { remove as execute } from './execute'; +import { linkRemoveDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/router.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/router.ts new file mode 100644 index 0000000000..c9e7a58c45 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/router.ts @@ -0,0 +1,56 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; + +import * as row from './row'; +import * as base from './base'; +import * as link from './link'; +import * as asset from './asset'; + +import type { SeaTable } from './Interfaces'; + +export async function router(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const operationResult: INodeExecutionData[] = []; + let responseData: IDataObject | IDataObject[] = []; + + //console.log('ITEM LENGTH ' + items.length); + + for (let i = 0; i < items.length; i++) { + const resource = this.getNodeParameter('resource', i); + const operation = this.getNodeParameter('operation', i); + + //console.log(operation); + //console.log(resource); + + const seatable = { + resource, + operation, + } as SeaTable; + + try { + if (seatable.resource === 'row') { + responseData = await row[seatable.operation].execute.call(this, i); + } else if (seatable.resource === 'base') { + responseData = await base[seatable.operation].execute.call(this, i); + } else if (seatable.resource === 'link') { + responseData = await link[seatable.operation].execute.call(this, i); + } else if (seatable.resource === 'asset') { + responseData = await asset[seatable.operation].execute.call(this, i); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: { item: i } }, + ); + operationResult.push(...executionData); + } catch (err) { + if (this.continueOnFail()) { + operationResult.push({ json: this.getInputData(i)[0].json, error: err }); + } else { + if (err.context) err.context.itemIndex = i; + throw err; + } + } + } + + return [operationResult]; +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts new file mode 100644 index 0000000000..895b3b9b8f --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts @@ -0,0 +1,121 @@ +import type { RowProperties } from '../../Interfaces'; + +export const rowCreateDescription: RowProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['create'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Data to Send', + name: 'fieldsToSend', + type: 'options', + options: [ + { + name: 'Auto-Map Input Data to Columns', + value: 'autoMapInputData', + description: 'Use when node input properties match destination column names', + }, + { + name: 'Define Below for Each Column', + value: 'defineBelow', + description: 'Set the value for each destination column', + }, + ], + displayOptions: { + show: { + resource: ['row'], + operation: ['create'], + }, + }, + default: 'defineBelow', + description: 'Whether to insert the input data this node receives in the new row', + }, + { + displayName: 'Inputs to Ignore', + name: 'inputsToIgnore', + type: 'string', + displayOptions: { + show: { + resource: ['row'], + operation: ['create'], + fieldsToSend: ['autoMapInputData'], + }, + }, + default: '', + description: + 'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.', + placeholder: 'Enter properties...', + }, + { + displayName: 'Columns to Send', + name: 'columnsUi', + placeholder: 'Add Column', + type: 'fixedCollection', + typeOptions: { + multipleValueButtonText: 'Add Column to Send', + multipleValues: true, + }, + options: [ + { + displayName: 'Column', + name: 'columnValues', + values: [ + { + displayName: 'Column Name', + name: 'columnName', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getTableUpdateAbleColumns', + }, + default: '', + }, + { + displayName: 'Column Value', + name: 'columnValue', + type: 'string', + default: '', + }, + ], + }, + ], + displayOptions: { + show: { + resource: ['row'], + operation: ['create'], + fieldsToSend: ['defineBelow'], + }, + }, + default: {}, + description: 'Add destination column with its value', + }, + { + displayName: 'Hint: Link, files, images or digital signatures have to be added separately.', + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { + resource: ['row'], + operation: ['create'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts new file mode 100644 index 0000000000..7ffef50455 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts @@ -0,0 +1,62 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { + seaTableApiRequest, + getTableColumns, + split, + rowExport, + updateAble, + splitStringColumnsToArrays, +} from '../../../GenericFunctions'; +import type { IRowObject } from '../../Interfaces'; +import type { TColumnValue, TColumnsUiValues } from '../../../types'; + +export async function create( + this: IExecuteFunctions, + index: number, +): Promise { + const tableName = this.getNodeParameter('tableName', index) as string; + const tableColumns = await getTableColumns.call(this, tableName); + const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as + | 'defineBelow' + | 'autoMapInputData'; + + const body = { + table_name: tableName, + row: {}, + } as IDataObject; + let rowInput = {} as IRowObject; + + // get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }. + if (fieldsToSend === 'autoMapInputData') { + const items = this.getInputData(); + const incomingKeys = Object.keys(items[index].json); + const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string); + for (const key of incomingKeys) { + if (inputDataToIgnore.includes(key)) continue; + rowInput[key] = items[index].json[key] as TColumnValue; + } + } else { + const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues; + for (const column of columns) { + rowInput[column.columnName] = column.columnValue; + } + } + + // only keep key:value pairs for columns that are allowed to update. + rowInput = rowExport(rowInput, updateAble(tableColumns)); + + // string to array: multi-select and collaborators + rowInput = splitStringColumnsToArrays(rowInput, tableColumns); + + body.row = rowInput; + + const responseData = await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + body, + ); + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/index.ts new file mode 100644 index 0000000000..220e8afb00 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/index.ts @@ -0,0 +1,4 @@ +import { create as execute } from './execute'; +import { rowCreateDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/description.ts new file mode 100644 index 0000000000..301653d1b1 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/description.ts @@ -0,0 +1,54 @@ +import type { RowProperties } from '../../Interfaces'; + +export const rowGetDescription: RowProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['get'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Row ID', + name: 'rowId', + type: 'options', + required: true, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getRowIds', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['get'], + }, + }, + default: '', + }, + { + displayName: 'Simplify output', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: ['row'], + operation: ['get'], + }, + }, + default: true, + description: + 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/execute.ts new file mode 100644 index 0000000000..de548a1bcd --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/execute.ts @@ -0,0 +1,42 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import type { IRow, IRowResponse, IDtableMetadataColumn } from './../../Interfaces'; +import { + seaTableApiRequest, + enrichColumns, + simplify_new, + getBaseCollaborators, +} from '../../../GenericFunctions'; + +export async function get(this: IExecuteFunctions, index: number): Promise { + // get parameters + const tableName = this.getNodeParameter('tableName', index) as string; + const rowId = this.getNodeParameter('rowId', index) as string; + const simple = this.getNodeParameter('simple', index) as boolean; + + // get collaborators + const collaborators = await getBaseCollaborators.call(this); + + // get rows + let sqlResult = (await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-db/api/v1/query/{{dtable_uuid}}/', + { + sql: `SELECT * FROM \`${tableName}\` WHERE _id = '${rowId}'`, + convert_keys: true, + }, + )) as IRowResponse; + let metadata = sqlResult.metadata as IDtableMetadataColumn[]; + let rows = sqlResult.results as IRow[]; + + // hide columns like button + rows.map((row) => enrichColumns(row, metadata, collaborators)); + + // remove columns starting with _ if simple; + if (simple) { + rows.map((row) => simplify_new(row)); + } + + return this.helpers.returnJsonArray(rows as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/index.ts new file mode 100644 index 0000000000..7f9c1e982f --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/index.ts @@ -0,0 +1,4 @@ +import { get as execute } from './execute'; +import { rowGetDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts new file mode 100644 index 0000000000..b6453601f9 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts @@ -0,0 +1,76 @@ +import * as create from './create'; +import * as get from './get'; +import * as search from './search'; +import * as update from './update'; +import * as remove from './remove'; +import * as lock from './lock'; +import * as unlock from './unlock'; +import type { INodeProperties } from 'n8n-workflow'; + +export { create, get, search, update, remove, lock, unlock }; + +export const descriptions: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['row'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new row', + action: 'Create a row', + }, + { + name: 'Get', + value: 'get', + description: 'Get the content of a row', + action: 'Get a row', + }, + { + name: 'Search', + value: 'search', + description: 'Search one or multiple rows', + action: 'Search a row by keyword', + }, + { + name: 'Update', + value: 'update', + description: 'Update the content of a row', + action: 'Update a row', + }, + { + name: 'Delete', + value: 'remove', + description: 'Delete a row', + action: 'Delete a row', + }, + { + name: 'Lock', + value: 'lock', + description: 'Lock a row to prevent further changes.', + action: 'Add a row lock', + }, + { + name: 'Unlock', + value: 'unlock', + description: 'Remove the lock from a row', + action: 'Remove a row lock', + }, + ], + default: 'create', + }, + ...create.description, + ...get.description, + ...search.description, + ...update.description, + ...remove.description, + ...lock.description, + ...unlock.description, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/description.ts new file mode 100644 index 0000000000..e1ed6e1194 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/description.ts @@ -0,0 +1,40 @@ +import type { RowProperties } from '../../Interfaces'; + +export const rowLockDescription: RowProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['lock'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Row ID', + name: 'rowId', + type: 'options', + required: true, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getRowIds', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['lock'], + }, + }, + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/execute.ts new file mode 100644 index 0000000000..0a6b06fe7f --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/execute.ts @@ -0,0 +1,20 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function lock(this: IExecuteFunctions, index: number): Promise { + const tableName = this.getNodeParameter('tableName', index) as string; + const rowId = this.getNodeParameter('rowId', index) as string; + + const responseData = await seaTableApiRequest.call( + this, + {}, + 'PUT', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/lock-rows/', + { + table_name: tableName, + row_ids: [rowId], + }, + ); + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/index.ts new file mode 100644 index 0000000000..96cea685b2 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/index.ts @@ -0,0 +1,4 @@ +import { lock as execute } from './execute'; +import { rowLockDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts new file mode 100644 index 0000000000..fca64f49dc --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts @@ -0,0 +1,40 @@ +import type { RowProperties } from '../../Interfaces'; + +export const rowRemoveDescription: RowProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['remove'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Row ID', + name: 'rowId', + type: 'options', + required: true, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getRowIds', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['remove'], + }, + }, + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/execute.ts new file mode 100644 index 0000000000..62e5d0b684 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/execute.ts @@ -0,0 +1,25 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function remove( + this: IExecuteFunctions, + index: number, +): Promise { + const tableName = this.getNodeParameter('tableName', index) as string; + const rowId = this.getNodeParameter('rowId', index) as string; + + const requestBody: IDataObject = { + table_name: tableName, + row_id: rowId, + }; + + const responseData = await seaTableApiRequest.call( + this, + {}, + 'DELETE', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + requestBody, + ); + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/index.ts new file mode 100644 index 0000000000..38e5a10043 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/index.ts @@ -0,0 +1,4 @@ +import { remove as execute } from './execute'; +import { rowRemoveDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts new file mode 100644 index 0000000000..ce94fa6ca8 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts @@ -0,0 +1,83 @@ +import type { RowProperties } from '../../Interfaces'; + +export const rowSearchDescription: RowProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['search'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Column', + name: 'searchColumn', + type: 'options', + displayOptions: { + show: { + resource: ['row'], + operation: ['search'], + }, + }, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getSearchableColumns', + }, + required: true, + default: '', + description: 'Select the column to be searched. Not all column types are supported for search.', + }, + { + displayName: 'Search term', + name: 'searchTerm', + type: 'string', + displayOptions: { + show: { + resource: ['row'], + operation: ['search'], + }, + }, + required: true, + default: '', + description: 'What to look for?', + }, + { + displayName: 'Activate wildcard search', + name: 'wildcard', + type: 'boolean', + displayOptions: { + show: { + resource: ['row'], + operation: ['search'], + }, + }, + default: false, + description: + 'FALSE: The search only results perfect matches. TRUE: Finds a row even if the search value is part of a string.', + }, + { + displayName: 'Simplify output', + name: 'simple', + type: 'boolean', + default: true, + displayOptions: { + show: { + resource: ['row'], + operation: ['search'], + }, + }, + description: + 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts new file mode 100644 index 0000000000..816d9c5107 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts @@ -0,0 +1,67 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { + seaTableApiRequest, + enrichColumns, + simplify_new, + getBaseCollaborators, +} from '../../../GenericFunctions'; +import { IDtableMetadataColumn, IRow, IRowResponse } from '../../Interfaces'; + +export async function search( + this: IExecuteFunctions, + index: number, +): Promise { + const tableName = this.getNodeParameter('tableName', index) as string; + const searchColumn = this.getNodeParameter('searchColumn', index) as string; + const searchTerm = this.getNodeParameter('searchTerm', index) as any; // string or integer + const wildcard = this.getNodeParameter('wildcard', index) as boolean; + const simple = this.getNodeParameter('simple', index) as boolean; + + // get collaborators + const collaborators = await getBaseCollaborators.call(this); + + //let metadata: IDtableMetadataColumn[] = []; + //let rows: IRow[]; + //let sqlResult: IRowResponse; + + // get the collaborators (avoid executing this multiple times !!!!) + /*let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/', + ); + let collaborators: ICollaborator[] = collaboratorsResult.user_list || []; + */ + + // this is the base query. The WHERE has to be finalized... + let sqlQuery = `SELECT * FROM \`${tableName}\` WHERE \`${searchColumn}\``; + + if (wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE "%' + searchTerm + '%"'; + else if (!wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' = "' + searchTerm + '"'; + else if (wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE %' + searchTerm + '%'; + else if (!wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' = ' + searchTerm; + + const sqlResult = (await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-db/api/v1/query/{{dtable_uuid}}/', + { + sql: sqlQuery, + convert_keys: true, + }, + )) as IRowResponse; + const metadata = sqlResult.metadata as IDtableMetadataColumn[]; + let rows = sqlResult.results as IRow[]; + + // hide columns like button + rows.map((row) => enrichColumns(row, metadata, collaborators)); + + // remove columns starting with _; + if (simple) { + rows.map((row) => simplify_new(row)); + } + + return this.helpers.returnJsonArray(rows as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/index.ts new file mode 100644 index 0000000000..faaac81429 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/index.ts @@ -0,0 +1,4 @@ +import { search as execute } from './execute'; +import { rowSearchDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/description.ts new file mode 100644 index 0000000000..dcea346302 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/description.ts @@ -0,0 +1,40 @@ +import type { RowProperties } from '../../Interfaces'; + +export const rowUnlockDescription: RowProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['unlock'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Row ID', + name: 'rowId', + type: 'options', + required: true, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getRowIds', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['unlock'], + }, + }, + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/execute.ts new file mode 100644 index 0000000000..3db5e1c50c --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/execute.ts @@ -0,0 +1,23 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function unlock( + this: IExecuteFunctions, + index: number, +): Promise { + const tableName = this.getNodeParameter('tableName', index) as string; + const rowId = this.getNodeParameter('rowId', index) as string; + + const responseData = await seaTableApiRequest.call( + this, + {}, + 'PUT', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/unlock-rows/', + { + table_name: tableName, + row_ids: [rowId], + }, + ); + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/index.ts new file mode 100644 index 0000000000..d904cf0bca --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/index.ts @@ -0,0 +1,4 @@ +import { unlock as execute } from './execute'; +import { rowUnlockDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts new file mode 100644 index 0000000000..473323d4b7 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts @@ -0,0 +1,137 @@ +import type { RowProperties } from '../../Interfaces'; + +export const rowUpdateDescription: RowProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['update'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'Row ID', + name: 'rowId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getRowIds', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['update'], + }, + }, + default: '', + }, + { + displayName: 'Data to Send', + name: 'fieldsToSend', + type: 'options', + options: [ + { + name: 'Auto-Map Input Data to Columns', + value: 'autoMapInputData', + description: 'Use when node input properties match destination column names', + }, + { + name: 'Define Below for Each Column', + value: 'defineBelow', + description: 'Set the value for each destination column', + }, + ], + displayOptions: { + show: { + resource: ['row'], + operation: ['update'], + }, + }, + default: 'defineBelow', + description: 'Whether to insert the input data this node receives in the new row', + }, + { + displayName: 'Inputs to Ignore', + name: 'inputsToIgnore', + type: 'string', + displayOptions: { + show: { + resource: ['row'], + operation: ['update'], + fieldsToSend: ['autoMapInputData'], + }, + }, + default: '', + description: + 'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.', + placeholder: 'Enter properties...', + }, + { + displayName: 'Columns to Send', + name: 'columnsUi', + placeholder: 'Add Column', + type: 'fixedCollection', + typeOptions: { + multipleValueButtonText: 'Add Column to Send', + multipleValues: true, + }, + options: [ + { + displayName: 'Column', + name: 'columnValues', + values: [ + { + displayName: 'Column Name', + name: 'columnName', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getTableUpdateAbleColumns', + }, + default: '', + }, + { + displayName: 'Column Value', + name: 'columnValue', + type: 'string', + default: '', + }, + ], + }, + ], + displayOptions: { + show: { + resource: ['row'], + operation: ['update'], + fieldsToSend: ['defineBelow'], + }, + }, + default: {}, + description: 'Add destination column with its value', + }, + { + displayName: 'Hint: Link, files, images or digital signatures have to be added separately.', + name: 'notice', + type: 'notice', + default: '', + displayOptions: { + show: { + resource: ['row'], + operation: ['update'], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/execute.ts new file mode 100644 index 0000000000..37a7435789 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/execute.ts @@ -0,0 +1,63 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { + seaTableApiRequest, + getTableColumns, + split, + rowExport, + updateAble, + splitStringColumnsToArrays, +} from '../../../GenericFunctions'; +import type { IRowObject } from '../../Interfaces'; +import type { TColumnsUiValues, TColumnValue } from '../../../types'; + +export async function update( + this: IExecuteFunctions, + index: number, +): Promise { + const tableName = this.getNodeParameter('tableName', index) as string; + const tableColumns = await getTableColumns.call(this, tableName); + const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as + | 'defineBelow' + | 'autoMapInputData'; + const rowId = this.getNodeParameter('rowId', index) as string; + + const body = { + table_name: tableName, + row_id: rowId, + } as IDataObject; + let rowInput = {} as IRowObject; + + // get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }. + if (fieldsToSend === 'autoMapInputData') { + const items = this.getInputData(); + const incomingKeys = Object.keys(items[index].json); + const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string); + for (const key of incomingKeys) { + if (inputDataToIgnore.includes(key)) continue; + rowInput[key] = items[index].json[key] as TColumnValue; + } + } else { + const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues; + for (const column of columns) { + rowInput[column.columnName] = column.columnValue; + } + } + + // only keep key:value pairs for columns that are allowed to update. + rowInput = rowExport(rowInput, updateAble(tableColumns)); + + // string to array: multi-select and collaborators + rowInput = splitStringColumnsToArrays(rowInput, tableColumns); + + body.row = rowInput; + + const responseData = await seaTableApiRequest.call( + this, + {}, + 'PUT', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + body, + ); + + return this.helpers.returnJsonArray(responseData as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/index.ts new file mode 100644 index 0000000000..512ce6a286 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/index.ts @@ -0,0 +1,4 @@ +import { update as execute } from './execute'; +import { rowUpdateDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/versionDescription.ts new file mode 100644 index 0000000000..c9b37d41da --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/versionDescription.ts @@ -0,0 +1,57 @@ +import type { INodeTypeDescription } from 'n8n-workflow'; +import * as row from './row'; +import * as base from './base'; +import * as link from './link'; +import * as asset from './asset'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'SeaTable', + name: 'seaTable', + icon: 'file:seatable.svg', + group: ['output'], + version: 2, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the SeaTable API', + defaults: { + name: 'SeaTable', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'seaTableApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Row', + value: 'row', + }, + { + name: 'Base', + value: 'base', + }, + { + name: 'Link', + value: 'link', + }, + { + name: 'Asset', + value: 'asset', + }, + ], + default: 'row', + }, + ...row.descriptions, + ...base.descriptions, + ...link.descriptions, + ...asset.descriptions, + ], +}; diff --git a/packages/nodes-base/nodes/SeaTable/v2/methods/index.ts b/packages/nodes-base/nodes/SeaTable/v2/methods/index.ts new file mode 100644 index 0000000000..65ff6192a3 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/methods/index.ts @@ -0,0 +1 @@ +export * as loadOptions from './loadOptions'; diff --git a/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts new file mode 100644 index 0000000000..9f6f0518f0 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts @@ -0,0 +1,227 @@ +import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; +import { getTableColumns, seaTableApiRequest, updateAble } from '../GenericFunctions'; +import type { IRow } from '../actions/Interfaces'; + +export async function getTableNames(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + // this.getCurrentNodeParameter('viewName'); // das kommt vom trigger. Brauche ich das??? + const { + metadata: { tables }, + } = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata', + ); + for (const table of tables) { + returnData.push({ + name: table.name, + value: table.name, + }); + } + return returnData; +} + +export async function getTableNameAndId( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const { + metadata: { tables }, + } = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata', + ); + for (const table of tables) { + returnData.push({ + name: table.name, + value: table.name + ':::' + table._id, + }); + } + return returnData; +} + +export async function getSearchableColumns( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const tableName = this.getCurrentNodeParameter('tableName') as string; + if (tableName) { + const columns = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns', + {}, + { table_name: tableName }, + ); + for (const col of columns.columns) { + if ( + col.type === 'text' || + col.type === 'long-text' || + col.type === 'number' || + col.type === 'single-select' || + col.type === 'email' || + col.type === 'url' || + col.type === 'rate' || + col.type === 'formula' + ) { + returnData.push({ + name: col.name, + value: col.name, + }); + } + } + } + return returnData; +} + +export async function getLinkColumns(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let tableName = this.getCurrentNodeParameter('tableName') as string; + tableName = tableName.split(':::')[0]; + //const tableId = (tableName.split(':::')[1] ? tableName.split(':::')[1] : ""); + if (tableName) { + const columns = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns', + {}, + { table_name: tableName }, + ); + for (const col of columns.columns) { + if (col.type === 'link') { + returnData.push({ + name: col.name, + value: col.name + ':::' + col.data.link_id + ':::' + col.data.other_table_id, + }); + } + } + } + return returnData; +} + +export async function getAssetColumns( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const tableName = this.getCurrentNodeParameter('tableName') as string; + if (tableName) { + const columns = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns', + {}, + { table_name: tableName }, + ); + for (const col of columns.columns) { + if (col.type === 'image' || col.type === 'file') { + returnData.push({ + name: col.name, + value: col.name + ':::' + col.type, + }); + } + } + } + return returnData; +} + +export async function getSignatureColumns( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const tableName = this.getCurrentNodeParameter('tableName') as string; + if (tableName) { + // only execute if table is selected + const columns = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns', + {}, + { table_name: tableName }, + ); + for (const col of columns.columns) { + if (col.type === 'digital-sign') { + // file+image are difficult: every time the row changes, all files trigger. + returnData.push({ + name: col.name, + value: col.name, + }); + } + } + } + return returnData; +} + +export async function getTableUpdateAbleColumns( + this: ILoadOptionsFunctions, +): Promise { + const tableName = this.getNodeParameter('tableName') as string; + let columns = await getTableColumns.call(this, tableName); + + // remove columns that could not be filled + columns = updateAble(columns); + + return columns + .filter((column) => column.editable) + .map((column) => ({ name: column.name, value: column.name })); +} + +export async function getRowIds(this: ILoadOptionsFunctions): Promise { + const tableName = this.getNodeParameter('tableName') as string; + const returnData: INodePropertyOptions[] = []; + + if (tableName) { + const sqlResult = await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-db/api/v1/query/{{dtable_uuid}}/', + { + sql: `SELECT * FROM \`${tableName}\` LIMIT 1000`, + convert_keys: false, + }, + ); + let rows = sqlResult.results as IRow[]; + + for (const row of rows) { + returnData.push({ + name: row['0000'] + ' (' + row._id + ')', + value: row._id, + }); + } + } + return returnData; +} + +export async function getTableViews(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tableName = this.getCurrentNodeParameter('tableName') as string; + if (tableName) { + // only execute if table is selected, to avoid unnecessary API requests + const { views } = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/views', + {}, + { table_name: tableName }, + ); + returnData.push({ + name: '', + value: '', + }); + for (const view of views) { + returnData.push({ + name: view.name, + value: view.name, + }); + } + } + return returnData; +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/types.ts b/packages/nodes-base/nodes/SeaTable/v2/types.ts new file mode 100644 index 0000000000..db0078d9c5 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/types.ts @@ -0,0 +1,97 @@ +// ---------------------------------- +// SeaTable +// ---------------------------------- + +export type TSeaTableServerVersion = '2.0.6'; +export type TSeaTableServerEdition = 'enterprise edition'; + +// ---------------------------------- +// dtable +// ---------------------------------- + +import type { + IDtableMetadataColumn, + IDtableMetadataTable, + TDtableViewColumn, +} from './actions/Interfaces'; +import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; + +export type TColumnType = + | 'text' + | 'long-text' + | 'number' + | 'collaborator' + | 'date' + | 'duration' + | 'single-select' + | 'multiple-select' + | 'image' + | 'file' + | 'email' + | 'url' + | 'checkbox' + | 'rate' + | 'formula' + | 'link-formula' + | 'geolocation' + | 'link' + | 'creator' + | 'ctime' + | 'last-modifier' + | 'mtime' + | 'auto-number' + | 'button' + | 'digital-sign'; + +export type TInheritColumnKey = + | '_id' + | '_creator' + | '_ctime' + | '_last_modifier' + | '_mtime' + | '_seq' + | '_archived' + | '_locked' + | '_locked_by'; + +export type TColumnValue = undefined | boolean | number | string | string[] | null; +export type TColumnKey = TInheritColumnKey | string; + +export type TDtableMetadataTables = readonly IDtableMetadataTable[]; +export type TDtableMetadataColumns = IDtableMetadataColumn[]; +export type TDtableViewColumns = readonly TDtableViewColumn[]; + +// ---------------------------------- +// api +// ---------------------------------- + +export type TEndpointVariableName = 'access_token' | 'dtable_uuid' | 'server'; + +// Template Literal Types requires-ts-4.1.5 -- deferred +export type TMethod = 'GET' | 'POST'; +type TEndpoint = + | '/api/v2.1/dtable/app-access-token/' + | '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/'; +export type TEndpointExpr = TEndpoint; +export type TEndpointResolvedExpr = + TEndpoint; /* deferred: but already in use for header values, e.g. authentication */ + +export type TDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZ' /* moment.js */; + +// ---------------------------------- +// node +// ---------------------------------- + +export type TCredentials = ICredentialDataDecryptedObject | undefined; + +export type TTriggerOperation = 'create' | 'update'; + +export type TOperation = 'append' | 'list' | 'metadata'; + +export type TLoadedResource = { + name: string; +}; +export type TColumnsUiValues = Array<{ + columnName: string; + columnValue: string; +}>; From f4e999521cbaf78979fb1a0da253a66a526655ff Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Fri, 1 Dec 2023 13:52:26 +0100 Subject: [PATCH 02/14] bugfix wrong uploadurl for images/files --- .../nodes/SeaTable/v2/actions/asset/upload/execute.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts index 2f3db9677a..51b2e0e554 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts @@ -19,6 +19,8 @@ export async function upload( 'GET', '/api/v2.1/dtable/app-upload-link/', )) as IUploadLink; + const relativePath = + uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path; // Get the binary data const fileBufferData = await this.helpers.getBinaryDataBuffer(index, dataPropertyName); @@ -35,8 +37,7 @@ export async function upload( }, parent_dir: uploadLink.parent_path, replace: '0', - relative_path: - uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path, + relative_path: relativePath, }, }; @@ -61,12 +62,10 @@ export async function upload( } as IDataObject; let rowInput = {} as IRowObject; - const filePath = [ - `/workspace/${workspaceId}${uploadLink.parent_path}/${uploadLink.img_relative_path}/${uploadAsset[c].name}`, - ]; + const filePath = `/workspace/${workspaceId}${uploadLink.parent_path}/${relativePath}/${uploadAsset[c].name}`; if (uploadColumnType === 'image') { - rowInput[uploadColumn.split(':::')[0]] = filePath; + rowInput[uploadColumn.split(':::')[0]] = [filePath]; } else if (uploadColumnType === 'file') { rowInput[uploadColumn.split(':::')[0]] = uploadAsset; uploadAsset[c].type = 'file'; From bda3edbfa9f1364e2c648523688889d56adca070 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 5 Dec 2023 09:24:03 +0100 Subject: [PATCH 03/14] added missing loadOptionsDependsOn for update a row action --- .../nodes/SeaTable/v2/actions/row/update/description.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts index 473323d4b7..f61b64d5ff 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts @@ -26,6 +26,7 @@ export const rowUpdateDescription: RowProperties = [ type: 'options', required: true, typeOptions: { + loadOptionsDependsOn: ['tableName'], loadOptionsMethod: 'getRowIds', }, displayOptions: { From 4f8be47b435d72eefcc8ff50c535cd5c9878a7d0 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Fri, 15 Dec 2023 15:03:22 +0100 Subject: [PATCH 04/14] add new action "get many rows" --- .../nodes/SeaTable/v2/actions/Interfaces.ts | 2 +- .../nodes/SeaTable/v2/actions/row/index.ts | 10 +++- .../v2/actions/row/list/description.ts | 55 +++++++++++++++++++ .../SeaTable/v2/actions/row/list/execute.ts | 54 ++++++++++++++++++ .../SeaTable/v2/actions/row/list/index.ts | 4 ++ 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/list/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/list/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/row/list/index.ts diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts index 50af0f7b6f..35c44eec0c 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts @@ -1,7 +1,7 @@ import type { AllEntities, Entity, PropertiesOf } from 'n8n-workflow'; type SeaTableMap = { - row: 'create' | 'get' | 'search' | 'update' | 'remove' | 'lock' | 'unlock'; + row: 'create' | 'get' | 'search' | 'update' | 'remove' | 'lock' | 'unlock' | 'list'; base: 'snapshot' | 'metadata' | 'apiCall' | 'collaborator'; link: 'add' | 'remove'; asset: 'upload' | 'getPublicURL'; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts index b6453601f9..a78e54f528 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts @@ -1,5 +1,6 @@ import * as create from './create'; import * as get from './get'; +import * as list from './list'; import * as search from './search'; import * as update from './update'; import * as remove from './remove'; @@ -7,7 +8,7 @@ import * as lock from './lock'; import * as unlock from './unlock'; import type { INodeProperties } from 'n8n-workflow'; -export { create, get, search, update, remove, lock, unlock }; +export { create, get, search, update, remove, lock, unlock, list }; export const descriptions: INodeProperties[] = [ { @@ -33,6 +34,12 @@ export const descriptions: INodeProperties[] = [ description: 'Get the content of a row', action: 'Get a row', }, + { + name: 'Get Many', + value: 'list', + description: 'Get many rows from a table of view', + action: 'Get many rows', + }, { name: 'Search', value: 'search', @@ -68,6 +75,7 @@ export const descriptions: INodeProperties[] = [ }, ...create.description, ...get.description, + ...list.description, ...search.description, ...update.description, ...remove.description, diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/description.ts new file mode 100644 index 0000000000..3d1360cf0f --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/description.ts @@ -0,0 +1,55 @@ +import type { RowProperties } from '../../Interfaces'; + +export const rowListDescription: RowProperties = [ + { + displayName: 'Table Name', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNames', + }, + displayOptions: { + show: { + resource: ['row'], + operation: ['list'], + }, + }, + default: '', + description: + 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + }, + { + displayName: 'View Name or ID (optional)', + name: 'viewName', + type: 'options', + required: false, + displayOptions: { + show: { + resource: ['row'], + operation: ['list'], + }, + }, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getTableViews', + }, + default: '', + description: 'The name of SeaTable view to access. Choose from the list, or specify ...', + }, + { + displayName: 'Simplify output', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: ['row'], + operation: ['list'], + }, + }, + default: true, + description: + 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/execute.ts new file mode 100644 index 0000000000..c46cd4639e --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/execute.ts @@ -0,0 +1,54 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import type { IRow } from './../../Interfaces'; +import { + seaTableApiRequest, + enrichColumns, + simplify_new, + getBaseCollaborators, +} from '../../../GenericFunctions'; + +export async function list(this: IExecuteFunctions, index: number): Promise { + // get parameters + const tableName = this.getNodeParameter('tableName', index) as string; + const viewName = this.getNodeParameter('viewName', index) as string; + const simple = this.getNodeParameter('simple', index) as boolean; + + // get collaborators + const collaborators = await getBaseCollaborators.call(this); + + // get rows + let requestMeta = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/', + ); + + let requestRows = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + {}, + { + table_name: tableName, + view_name: viewName, + limit: 1000, + }, + ); + + let metadata = + requestMeta.metadata.tables.find((table: { name: string }) => table.name === tableName) + ?.columns ?? []; + let rows = requestRows.rows as IRow[]; + + // hide columns like button + rows.map((row) => enrichColumns(row, metadata, collaborators)); + + // remove columns starting with _ if simple; + if (simple) { + rows.map((row) => simplify_new(row)); + } + + return this.helpers.returnJsonArray(rows as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/index.ts new file mode 100644 index 0000000000..ab055196fd --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/index.ts @@ -0,0 +1,4 @@ +import { list as execute } from './execute'; +import { rowListDescription as description } from './description'; + +export { description, execute }; From 03594050f1fe625f6bc16ed7a04188ea7aaeec4f Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Fri, 15 Dec 2023 15:55:19 +0100 Subject: [PATCH 05/14] add case insensitive search to "search" action --- .../v2/actions/row/search/description.ts | 14 ++++++++++++ .../SeaTable/v2/actions/row/search/execute.ts | 22 ++++++------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts index ce94fa6ca8..666197f14a 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts @@ -52,6 +52,20 @@ export const rowSearchDescription: RowProperties = [ default: '', description: 'What to look for?', }, + { + displayName: 'Case Insensitive Search', + name: 'insensitive', + type: 'boolean', + displayOptions: { + show: { + resource: ['row'], + operation: ['search'], + }, + }, + default: false, + description: + 'FALSE: The search distinguish between uppercase and lowercase characters. TRUE: Search ignores case sensitivity.', + }, { displayName: 'Activate wildcard search', name: 'wildcard', diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts index 816d9c5107..f9de6940b1 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts @@ -13,30 +13,22 @@ export async function search( ): Promise { const tableName = this.getNodeParameter('tableName', index) as string; const searchColumn = this.getNodeParameter('searchColumn', index) as string; - const searchTerm = this.getNodeParameter('searchTerm', index) as any; // string or integer + let searchTerm = this.getNodeParameter('searchTerm', index) as any; // string or integer + const insensitive = this.getNodeParameter('insensitive', index) as boolean; const wildcard = this.getNodeParameter('wildcard', index) as boolean; const simple = this.getNodeParameter('simple', index) as boolean; // get collaborators const collaborators = await getBaseCollaborators.call(this); - //let metadata: IDtableMetadataColumn[] = []; - //let rows: IRow[]; - //let sqlResult: IRowResponse; - - // get the collaborators (avoid executing this multiple times !!!!) - /*let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call( - this, - {}, - 'GET', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/', - ); - let collaborators: ICollaborator[] = collaboratorsResult.user_list || []; - */ - // this is the base query. The WHERE has to be finalized... let sqlQuery = `SELECT * FROM \`${tableName}\` WHERE \`${searchColumn}\``; + if (insensitive) { + searchTerm = searchTerm.toLowerCase(); + sqlQuery = `SELECT * FROM \`${tableName}\` WHERE lower(\`${searchColumn}\`)`; + } + if (wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE "%' + searchTerm + '%"'; else if (!wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' = "' + searchTerm + '"'; else if (wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE %' + searchTerm + '%'; From 976521900213f23a7c32ae108a9ff20f86bda172 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Thu, 21 Dec 2023 22:44:07 +0100 Subject: [PATCH 06/14] fix "Make an API call" --- .../v2/actions/base/apiCall/description.ts | 39 +++++++++---------- .../v2/actions/base/apiCall/execute.ts | 31 +++++++++++++-- .../nodes-base/nodes/SeaTable/v2/types.ts | 2 + 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts index 8e8f36a669..da4f8f0210 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts @@ -1,25 +1,6 @@ import type { BaseProperties } from '../../Interfaces'; export const baseApiCallDescription: BaseProperties = [ - { - displayName: 'Table Name', - name: 'tableName', - type: 'options', - placeholder: 'Select a table', - required: true, - typeOptions: { - loadOptionsMethod: 'getTableNames', - }, - displayOptions: { - show: { - resource: ['base'], - operation: ['apiCall'], - }, - }, - default: '', - description: - 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', - }, { displayName: 'HTTP Method', name: 'apiMethod', @@ -34,8 +15,8 @@ export const baseApiCallDescription: BaseProperties = [ value: 'GET', }, { - name: 'POST', - value: 'POST', + name: 'PUT', + value: 'PUT', }, { name: 'DELETE', @@ -133,4 +114,20 @@ export const baseApiCallDescription: BaseProperties = [ description: 'Only valid JSON is accepted. n8n will pass anything you enter as raw input. For example, {"foo", "bar"} is perfectly valid. Of cause you can use variables from n8n inside your JSON.', }, + { + displayName: 'Response object parameter name', + name: 'responseObjectName', + type: 'string', + placeholder: 'Leave it empty or use a value like "rows", "metadata", "views" etc.', + required: false, + displayOptions: { + show: { + resource: ['base'], + operation: ['apiCall'], + }, + }, + default: '', + description: + 'When using the SeaTable API, you can specify a parameter to retrieve either the entire array of objects or a specific object within it. This allows you to choose whether to fetch the complete output or only the object related to the provided parameter.', + }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/execute.ts index 8b212b1d87..1a66d6e9dc 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/execute.ts @@ -1,15 +1,40 @@ import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; import { seaTableApiRequest } from '../../../GenericFunctions'; +import { APITypes } from '../../../types'; export async function apiCall( this: IExecuteFunctions, index: number, ): Promise { + const apiMethod = this.getNodeParameter('apiMethod', index) as APITypes; + const apiEndpoint = this.getNodeParameter('apiEndpoint', index) as APITypes; + const responseObjectName = this.getNodeParameter('responseObjectName', index) as string; + + // body params + const apiBody = this.getNodeParameter('apiBody', index) as any; + + // query params + const apiParams: IDataObject = {}; + const params = this.getNodeParameter('apiParams.apiParamsValues', index, []) as any; + for (const param of params) { + apiParams[`${param.key}`] = param.value; + } + console.log(apiParams); + const responseData = await seaTableApiRequest.call( this, {}, - 'GET', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/', + apiMethod, + apiEndpoint, + apiBody, + apiParams, ); - return this.helpers.returnJsonArray(responseData.metadata as IDataObject[]); + console.log(responseData); + + // output + if (responseObjectName) { + return this.helpers.returnJsonArray(responseData[responseObjectName] as IDataObject[]); + } else { + return this.helpers.returnJsonArray(responseData as IDataObject[]); + } } diff --git a/packages/nodes-base/nodes/SeaTable/v2/types.ts b/packages/nodes-base/nodes/SeaTable/v2/types.ts index db0078d9c5..a61ef2d1e0 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/types.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/types.ts @@ -95,3 +95,5 @@ export type TColumnsUiValues = Array<{ columnName: string; columnValue: string; }>; + +export type APITypes = 'GET' | 'POST' | 'DELETE' | 'PUT'; From 95b66a02263cfd8829e7101a8813aefb63de2d90 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Mon, 8 Jan 2024 23:17:21 +0100 Subject: [PATCH 07/14] improved "upload asset": - input workspace_id is not required anymore - two new options: `Replace existing file` and `Append to column` --- .../v2/actions/asset/upload/description.ts | 44 +++++++------- .../v2/actions/asset/upload/execute.ts | 58 +++++++++++++++---- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts index 7304cf159f..4cc3ca337d 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts @@ -55,25 +55,6 @@ export const assetUploadDescription: AssetProperties = [ }, default: '', }, - { - displayName: 'Workspace ID', - name: 'workspaceId', - type: 'number', - typeOptions: { - minValue: 1, - numberStepSize: 1, - }, - required: true, - displayOptions: { - show: { - resource: ['asset'], - operation: ['upload'], - }, - }, - default: '', - description: - 'How to get the workspace ID: https://seatable.io/docs/arbeiten-mit-gruppen/workspace-id-einer-gruppe-ermitteln/?lang=auto', - }, { displayName: 'Property Name', name: 'dataPropertyName', @@ -88,17 +69,32 @@ export const assetUploadDescription: AssetProperties = [ }, description: 'Name of the binary property which contains the data for the file to be written', }, - /*{ - displayName: 'Replace', + { + displayName: 'Replace existing file', name: 'replace', type: 'boolean', - default: false, + default: true, displayOptions: { show: { resource: ['asset'], operation: ['upload'], }, }, - description: 'Replace existing file if the file/image already exists with same name.', - },*/ + description: + 'Replace existing asset with the same name. Otherwise a new version with another name (numeral in parentheses) will be uploaded.', + }, + { + displayName: 'Append to column', + name: 'append', + type: 'boolean', + default: true, + displayOptions: { + show: { + resource: ['asset'], + operation: ['upload'], + }, + }, + description: + 'Keep existing files/images in the column and append the new asset. Otherwise the existing files/images are remove from the column.', + }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts index 51b2e0e554..d96f458e69 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts @@ -6,13 +6,12 @@ export async function upload( this: IExecuteFunctions, index: number, ): Promise { - // step 1: upload file to base const uploadColumn = this.getNodeParameter('uploadColumn', index) as any; const uploadColumnType = uploadColumn.split(':::')[1]; + const uploadColumnName = uploadColumn.split(':::')[0]; const dataPropertyName = this.getNodeParameter('dataPropertyName', index) as string; const tableName = this.getNodeParameter('tableName', index) as string; const rowId = this.getNodeParameter('rowId', index) as string; - const workspaceId = this.getNodeParameter('workspaceId', index) as string; const uploadLink = (await seaTableApiRequest.call( this, {}, @@ -21,11 +20,43 @@ export async function upload( )) as IUploadLink; const relativePath = uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path; + const replace = this.getNodeParameter('replace', index) as string; + const append = this.getNodeParameter('append', index) as string; - // Get the binary data + // get server url + const credentials = await this.getCredentials('seaTableApi'); + const serverURL = credentials.domain ?? 'https://cloud.seatable.io'; + + // get workspaceId + const workspaceId = ( + await this.helpers.request({ + headers: { + Authorization: `Token ${credentials.token}`, + }, + uri: `${serverURL}/api/v2.1/dtable/app-access-token/`, + json: true, + }) + ).workspace_id; + + // if there are already assets attached to the column + let existingAssetArray = []; + if (append) { + let rowToUpdate = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/' + rowId, + {}, + { + table_name: tableName, + }, + ); + existingAssetArray = rowToUpdate[uploadColumnName]; + } + + // Get the binary data and prepare asset for upload const fileBufferData = await this.helpers.getBinaryDataBuffer(index, dataPropertyName); const binaryData = this.helpers.assertBinaryData(index, dataPropertyName); - // Create our request option const options = { formData: { file: { @@ -36,12 +67,12 @@ export async function upload( }, }, parent_dir: uploadLink.parent_path, - replace: '0', + replace: replace ? '1' : '0', relative_path: relativePath, }, }; - // Send the request + // Send the upload request let uploadAsset = await seaTableApiRequest.call( this, {}, @@ -53,7 +84,7 @@ export async function upload( options, ); - // now step 2 (attaching the file to a column in a base) + // now step 2 (attaching the asset to a column in a base) for (let c = 0; c < uploadAsset.length; c++) { const body = { table_name: tableName, @@ -62,17 +93,24 @@ export async function upload( } as IDataObject; let rowInput = {} as IRowObject; - const filePath = `/workspace/${workspaceId}${uploadLink.parent_path}/${relativePath}/${uploadAsset[c].name}`; + const filePath = `${serverURL}/workspace/${workspaceId}${uploadLink.parent_path}/${relativePath}/${uploadAsset[c].name}`; if (uploadColumnType === 'image') { - rowInput[uploadColumn.split(':::')[0]] = [filePath]; + rowInput[uploadColumnName] = [filePath]; } else if (uploadColumnType === 'file') { - rowInput[uploadColumn.split(':::')[0]] = uploadAsset; + rowInput[uploadColumnName] = uploadAsset; uploadAsset[c].type = 'file'; uploadAsset[c].url = filePath; } + + // merge with existing assets in this column or with [] and remove duplicates + rowInput[uploadColumnName] = [ + // @ts-ignore: + ...new Set([...rowInput[uploadColumnName], ...existingAssetArray]), + ]; body.row = rowInput; + // attach assets to table row const responseData = await seaTableApiRequest.call( this, {}, From 5f4ada49b945a0493eca1840b32bb9ba00861cf6 Mon Sep 17 00:00:00 2001 From: Jonathan Bennetts Date: Tue, 16 Jan 2024 10:35:16 +0000 Subject: [PATCH 08/14] Lint fix on trigger node --- .../nodes/SeaTable/SeaTableTrigger.node.ts | 69 ++++++------------- 1 file changed, 22 insertions(+), 47 deletions(-) diff --git a/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts b/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts index 41f497497f..b368dab462 100644 --- a/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts +++ b/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts @@ -81,10 +81,9 @@ export class SeaTableTrigger implements INodeType { 'The name of SeaTable table to access. Choose from the list, or specify an ID using an expression.', }, { - displayName: 'View Name or ID (optional)', + displayName: 'View Name or ID', name: 'viewName', type: 'options', - required: false, displayOptions: { show: { event: ['newRow', 'updatedRow'], @@ -95,10 +94,11 @@ export class SeaTableTrigger implements INodeType { loadOptionsMethod: 'getTableViews', }, default: '', - description: 'The name of SeaTable view to access. Choose from the list, or specify ...', + description: + 'The name of SeaTable view to access. Choose from the list, or specify an ID using an expression.', }, { - displayName: 'Signature column', + displayName: 'Signature Column Name or ID', name: 'assetColumn', type: 'options', required: true, @@ -112,15 +112,16 @@ export class SeaTableTrigger implements INodeType { loadOptionsMethod: 'getSignatureColumns', }, default: '', - description: 'Select the digital-signature column that should be tracked.', + description: + 'Select the digital-signature column that should be tracked. Choose from the list, or specify an ID using an expression.', }, { - displayName: 'Simplify output', + displayName: 'Simplify', name: 'simple', type: 'boolean', default: true, description: - 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.', + 'Whether to return a simplified version of the response instead of the raw data', }, { displayName: '"Fetch Test Event" returns max. three items of the last hour.', @@ -137,8 +138,10 @@ export class SeaTableTrigger implements INodeType { 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 viewName = (event !== 'newAsset' ? this.getNodeParameter('viewName') : '') as string; + const assetColumn = ( + event === 'newAsset' ? this.getNodeParameter('assetColumn') : '' + ) as string; const simple = this.getNodeParameter('simple') as boolean; const ctx: ICtx = {}; @@ -149,24 +152,8 @@ export class SeaTableTrigger implements INodeType { : (webhookData.lastTimeChecked as string); const endDate = (webhookData.lastTimeChecked = moment().utc().format()); - // this is working, even if the columns _mtime and _ctime have other names. Only relevant for newRow / updatedRow. const filterField = event === 'newRow' ? '_ctime' : '_mtime'; - // Difference between getRows and SqlQuery: - // ==================== - - // getRows (if view is selected) - // getRows always gets up to 1.000 rows of the selected view. - // getRows delivers only the rows, not the metadata - // no possibility to filter for _ctime or _mtime with the API call. - // Problems, not yet solved: - // if a column is empty, the column is not returned! - // view with more than 1.000 rows will not work! - - // SqlQuery (if no view is selected) - // SqlQuery returns up to 1.000. WHERE by time and ORDER BY _ctime or _mtime is possible. - // SqlQuery returns rows and metadata - let requestMeta: IGetMetadataResult; let requestRows: IGetRowsResult; let metadata: IDtableMetadataColumn[] = []; @@ -176,7 +163,7 @@ export class SeaTableTrigger implements INodeType { const limit = this.getMode() === 'manual' ? 3 : 1000; // New Signature - if (event == 'newAsset') { + if (event === 'newAsset') { const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/'; sqlResult = await seaTableApiRequest.call(this, ctx, 'POST', endpoint, { sql: `SELECT _id, _ctime, _mtime, \`${assetColumn}\` FROM ${tableName} WHERE \`${assetColumn}\` IS NOT NULL ORDER BY _mtime DESC LIMIT ${limit}`, @@ -188,15 +175,13 @@ export class SeaTableTrigger implements INodeType { const assetColumnType = columnType?.type || null; // remove unwanted entries - rows = sqlResult.results.filter( - (obj) => new Date(obj['_mtime']) > new Date(startDate), - ) as IRow[]; + 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') { - let signature = (row[assetColumn] as IColumnDigitalSignature) || []; + const signature = (row[assetColumn] as IColumnDigitalSignature) || []; if (signature.sign_time) { if (new Date(signature.sign_time) > new Date(startDate)) { newRows.push(signature); @@ -223,26 +208,20 @@ export class SeaTableTrigger implements INodeType { { table_name: tableName, view_name: viewName, - limit: limit, + limit, }, ); - // I need only metadata of the selected table. 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 as IRow[]; + rows = requestRows.rows; } else { - rows = requestRows.rows.filter( - (obj) => new Date(obj[filterField]) > new Date(startDate), - ) as IRow[]; + rows = requestRows.rows.filter((obj) => new Date(obj[filterField]) > new Date(startDate)); } - } - - // No view => use SQL-Query - else { + } else { const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/'; const sqlQuery = `SELECT * FROM \`${tableName}\` WHERE ${filterField} BETWEEN "${moment( startDate, @@ -254,20 +233,16 @@ export class SeaTableTrigger implements INodeType { convert_keys: true, }); metadata = sqlResult.metadata as IDtableMetadataColumn[]; - rows = sqlResult.results as IRow[]; + rows = sqlResult.results; } - // ========================================= - // => now I have rows and metadata. - - // lets get the collaborators - let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call( + const collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call( this, ctx, 'GET', '/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/', ); - let collaborators: ICollaborator[] = collaboratorsResult.user_list || []; + const collaborators: ICollaborator[] = collaboratorsResult.user_list || []; if (Array.isArray(rows) && rows.length > 0) { // remove columns starting with _ if simple; From 787f33de4e8f9019ec14fc24ab25b097f44ff68a Mon Sep 17 00:00:00 2001 From: Jonathan Bennetts Date: Tue, 16 Jan 2024 11:54:44 +0000 Subject: [PATCH 09/14] Fix breaking change that resulted in the old v1 node from appearing --- .../nodes/SeaTable/SeaTable.node.ts | 4 +- .../nodes/SeaTable/v1/SeaTableV1.node.ts | 49 ++++--------------- .../nodes/SeaTable/v1/VersionDescription.ts | 40 +++++++++++++++ 3 files changed, 52 insertions(+), 41 deletions(-) create mode 100644 packages/nodes-base/nodes/SeaTable/v1/VersionDescription.ts diff --git a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts index 51c9723fa2..c8d066736b 100644 --- a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts +++ b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts @@ -8,7 +8,7 @@ export class SeaTable extends VersionedNodeType { constructor() { const baseDescription: INodeTypeBaseDescription = { displayName: 'SeaTable', - name: 'seatable', + name: 'seaTable', icon: 'file:seatable.svg', group: ['output'], subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', @@ -17,7 +17,7 @@ export class SeaTable extends VersionedNodeType { }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { - 1: new SeaTableV1(), + 1: new SeaTableV1(baseDescription), 2: new SeaTableV2(baseDescription), }; diff --git a/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts b/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts index 058ae00eca..d12620ed57 100644 --- a/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts +++ b/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts @@ -6,6 +6,7 @@ import type { INodePropertyOptions, INodeType, INodeTypeDescription, + INodeTypeBaseDescription, } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; @@ -21,50 +22,20 @@ import { updateAble, } from './GenericFunctions'; -import { rowFields, rowOperations } from './RowDescription'; - import type { TColumnsUiValues, TColumnValue } from './types'; import type { ICtx, IRow, IRowObject } from './Interfaces'; +import { versionDescription } from './VersionDescription'; export class SeaTableV1 implements INodeType { - description: INodeTypeDescription = { - displayName: 'SeaTable', - name: 'seaTable', - icon: 'file:seaTable.svg', - group: ['input'], - version: 1, - subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', - description: 'Consume the SeaTable API', - defaults: { - name: 'SeaTable', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'seaTableApi', - required: true, - }, - ], - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Row', - value: 'row', - }, - ], - default: 'row', - }, - ...rowOperations, - ...rowFields, - ], - }; + description: INodeTypeDescription; + + constructor(baseDescription: INodeTypeBaseDescription) { + this.description = { + ...baseDescription, + ...versionDescription, + }; + } methods = { loadOptions: { diff --git a/packages/nodes-base/nodes/SeaTable/v1/VersionDescription.ts b/packages/nodes-base/nodes/SeaTable/v1/VersionDescription.ts new file mode 100644 index 0000000000..554cd349d4 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v1/VersionDescription.ts @@ -0,0 +1,40 @@ +import type { INodeTypeDescription } from 'n8n-workflow'; +import { rowFields, rowOperations } from './RowDescription'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'SeaTable', + name: 'seaTable', + icon: 'file:seaTable.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the SeaTable API', + defaults: { + name: 'SeaTable', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'seaTableApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Row', + value: 'row', + }, + ], + default: 'row', + }, + ...rowOperations, + ...rowFields, + ], +}; From 5198fdad55783045c1a7f05f749d33931a06ca33 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Mon, 29 Jan 2024 10:08:13 +0100 Subject: [PATCH 10/14] supporting big data backend with links and row create, update and delete --- .../nodes/SeaTable/SeaTable.node.ts | 2 +- .../nodes/SeaTable/v2/GenericFunctions.ts | 30 ++++++++++++++++ .../v2/actions/link/add/description.ts | 6 ++-- .../SeaTable/v2/actions/link/add/execute.ts | 19 +++++----- .../v2/actions/link/remove/description.ts | 5 ++- .../v2/actions/link/remove/execute.ts | 19 +++++----- .../v2/actions/row/create/description.ts | 18 ++++++++-- .../SeaTable/v2/actions/row/create/execute.ts | 35 +++++++++++++------ .../v2/actions/row/remove/description.ts | 1 + .../SeaTable/v2/actions/row/remove/execute.ts | 4 +-- .../v2/actions/row/update/description.ts | 7 ++-- .../SeaTable/v2/actions/row/update/execute.ts | 16 +++++---- .../nodes/SeaTable/v2/methods/loadOptions.ts | 16 ++++++--- 13 files changed, 127 insertions(+), 51 deletions(-) diff --git a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts index c8d066736b..20985d3dc5 100644 --- a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts +++ b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts @@ -8,7 +8,7 @@ export class SeaTable extends VersionedNodeType { constructor() { const baseDescription: INodeTypeBaseDescription = { displayName: 'SeaTable', - name: 'seaTable', + name: 'seatable', icon: 'file:seatable.svg', group: ['output'], subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', diff --git a/packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts b/packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts index 6f57e50efe..b473f1ba20 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts @@ -29,6 +29,9 @@ import type { IFile, } from './actions/Interfaces'; +// for date transformations +import moment from 'moment'; + // remove last backslash const userBaseUri = (uri?: string) => { if (uri === undefined) return uri; @@ -310,6 +313,33 @@ export function splitStringColumnsToArrays( row[column.name] = input.split(',').map((item) => item.trim()); } } + if (column.type == 'number') { + if (typeof row[column.name] === 'string') { + const input = row[column.name] as string; + row[column.name] = parseFloat(input); + } + } + if (column.type == 'rate' || column.type == 'duration') { + if (typeof row[column.name] === 'string') { + const input = row[column.name] as string; + row[column.name] = parseInt(input); + } + } + if (column.type == 'checkbox') { + if (typeof row[column.name] === 'string') { + const input = row[column.name] as string; + row[column.name] = false; + if (input === 'true' || input === 'on' || input === '1') { + row[column.name] = true; + } + } + } + if (column.type == 'date') { + if (typeof row[column.name] === 'string') { + const input = row[column.name] as string; + row[column.name] = moment(input, 'YYYY-mm-dd', true); + } + } }); return row; } diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts index e82c1f6a46..e82e58cc7a 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts @@ -17,8 +17,7 @@ export const linkAddDescription: LinkProperties = [ }, }, default: '', - description: - 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + description: 'If you use an expression, provide it in the way ":::".', }, { displayName: 'Link column', @@ -36,7 +35,8 @@ export const linkAddDescription: LinkProperties = [ }, required: true, default: '', - description: 'Select the column to create a link.', + description: + 'If you use an expression, provide it in the way "::::::".', }, { displayName: 'Row ID from the source table', diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/execute.ts index 34fe83a8f0..d7c5286e38 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/execute.ts @@ -7,18 +7,21 @@ export async function add(this: IExecuteFunctions, index: number): Promiseexpression.', + description: 'If you use an expression, provide it in the way ":::".', }, { displayName: 'Link column', @@ -36,7 +35,7 @@ export const linkRemoveDescription: LinkProperties = [ }, required: true, default: '', - description: 'Select the column to create a link.', + description: 'If you use an expression, provide it in the way "::::::".', }, { displayName: 'Row ID from the source table', diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/execute.ts index 0c9b7c9dfc..4f336dfca2 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/execute.ts @@ -10,18 +10,21 @@ export async function remove( const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string; const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string; + const body = { + link_id: linkColumn.split(':::')[1], + table_id: tableName.split(':::')[1], + other_table_id: linkColumn.split(':::')[2], + other_rows_ids_map: { + [linkColumnSourceId]: [linkColumnTargetId], + }, + }; + const responseData = await seaTableApiRequest.call( this, {}, 'DELETE', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/links/', - { - link_id: linkColumn.split(':::')[1], - table_id: tableName.split(':::')[1], - table_row_id: linkColumnSourceId, - other_table_id: linkColumn.split(':::')[2], - other_table_row_id: linkColumnTargetId, - }, + '/dtable-db/api/v1/base/{{dtable_uuid}}/links/', + body, ); return this.helpers.returnJsonArray(responseData as IDataObject[]); diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts index 895b3b9b8f..702676f638 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts @@ -80,7 +80,7 @@ export const rowCreateDescription: RowProperties = [ name: 'columnName', type: 'options', description: - 'Choose from the list, or specify an ID using an expression', + 'Choose from the list, or specify the column name using an expression', typeOptions: { loadOptionsDependsOn: ['tableName'], loadOptionsMethod: 'getTableUpdateAbleColumns', @@ -104,7 +104,21 @@ export const rowCreateDescription: RowProperties = [ }, }, default: {}, - description: 'Add destination column with its value', + description: + 'Add destination column with its value. Provide the value in this way:
Date: YYYY-MM-DD or YYYY-MM-DD hh:mm
Duration: time in seconds
Checkbox: true, on or 1
Multi-Select: comma separated list', + }, + { + displayName: 'Save to "Big Data" backend', + name: 'bigdata', + type: 'boolean', + displayOptions: { + show: { + resource: ['row'], + operation: ['create'], + }, + }, + default: false, + description: 'This requires the activation of the Big Data backend in the base.', }, { displayName: 'Hint: Link, files, images or digital signatures have to be added separately.', diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts index 7ffef50455..756b3d203a 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts @@ -19,6 +19,7 @@ export async function create( const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as | 'defineBelow' | 'autoMapInputData'; + const bigdata = this.getNodeParameter('bigdata', index) as string; const body = { table_name: tableName, @@ -48,15 +49,29 @@ export async function create( // string to array: multi-select and collaborators rowInput = splitStringColumnsToArrays(rowInput, tableColumns); - body.row = rowInput; + // save to big data backend + if (bigdata) { + body.rows = [rowInput]; + const responseData = await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-db/api/v1/insert-rows/{{dtable_uuid}}/', + body, + ); + return this.helpers.returnJsonArray(responseData as IDataObject[]); + } + // save to normal backend + else { + body.row = rowInput; - const responseData = await seaTableApiRequest.call( - this, - {}, - 'POST', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', - body, - ); - - return this.helpers.returnJsonArray(responseData as IDataObject[]); + const responseData = await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + body, + ); + return this.helpers.returnJsonArray(responseData as IDataObject[]); + } } diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts index fca64f49dc..1a253ef2a0 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts @@ -36,5 +36,6 @@ export const rowRemoveDescription: RowProperties = [ }, }, default: '', + description: 'Remove any row from the normal or big data backend based on its unique row ID.', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/execute.ts index 62e5d0b684..d958b0e5ef 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/execute.ts @@ -10,14 +10,14 @@ export async function remove( const requestBody: IDataObject = { table_name: tableName, - row_id: rowId, + row_ids: [rowId], }; const responseData = await seaTableApiRequest.call( this, {}, 'DELETE', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + '/dtable-db/api/v1/delete-rows/{{dtable_uuid}}/', requestBody, ); diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts index f61b64d5ff..29e0659cac 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts @@ -26,7 +26,7 @@ export const rowUpdateDescription: RowProperties = [ type: 'options', required: true, typeOptions: { - loadOptionsDependsOn: ['tableName'], + loadOptionsDependsOn: ['tableName'], loadOptionsMethod: 'getRowIds', }, displayOptions: { @@ -97,7 +97,7 @@ export const rowUpdateDescription: RowProperties = [ name: 'columnName', type: 'options', description: - 'Choose from the list, or specify an ID using an expression', + 'Choose from the list, or specify the column name using an expression', typeOptions: { loadOptionsDependsOn: ['tableName'], loadOptionsMethod: 'getTableUpdateAbleColumns', @@ -121,7 +121,8 @@ export const rowUpdateDescription: RowProperties = [ }, }, default: {}, - description: 'Add destination column with its value', + 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: 'Hint: Link, files, images or digital signatures have to be added separately.', diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/execute.ts index 37a7435789..1d19faffdb 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/execute.ts @@ -21,10 +21,6 @@ export async function update( | 'autoMapInputData'; const rowId = this.getNodeParameter('rowId', index) as string; - const body = { - table_name: tableName, - row_id: rowId, - } as IDataObject; let rowInput = {} as IRowObject; // get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }. @@ -49,13 +45,21 @@ export async function update( // string to array: multi-select and collaborators rowInput = splitStringColumnsToArrays(rowInput, tableColumns); - body.row = rowInput; + const body = { + table_name: tableName, + updates: [ + { + row_id: rowId, + row: rowInput, + }, + ], + } as IDataObject; const responseData = await seaTableApiRequest.call( this, {}, 'PUT', - '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', + '/dtable-db/api/v1/update-rows/{{dtable_uuid}}/', body, ); diff --git a/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts index 9f6f0518f0..c9c12387fc 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts @@ -37,7 +37,7 @@ export async function getTableNameAndId( for (const table of tables) { returnData.push({ name: table.name, - value: table.name + ':::' + table._id, + value: table.name + ':::' + table['_id'], }); } return returnData; @@ -80,9 +80,11 @@ export async function getSearchableColumns( export async function getLinkColumns(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - let tableName = this.getCurrentNodeParameter('tableName') as string; - tableName = tableName.split(':::')[0]; - //const tableId = (tableName.split(':::')[1] ? tableName.split(':::')[1] : ""); + 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, @@ -94,9 +96,13 @@ export async function getLinkColumns(this: ILoadOptionsFunctions): Promise Date: Mon, 29 Jan 2024 10:11:52 +0100 Subject: [PATCH 11/14] take over change from Jeffcom --- packages/nodes-base/nodes/SeaTable/SeaTable.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts index 20985d3dc5..c8d066736b 100644 --- a/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts +++ b/packages/nodes-base/nodes/SeaTable/SeaTable.node.ts @@ -8,7 +8,7 @@ export class SeaTable extends VersionedNodeType { constructor() { const baseDescription: INodeTypeBaseDescription = { displayName: 'SeaTable', - name: 'seatable', + name: 'seaTable', icon: 'file:seatable.svg', group: ['output'], subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', From 871c24abe1800a7334954f6b164809ffd12a4a11 Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Tue, 28 May 2024 17:09:00 +0200 Subject: [PATCH 12/14] Fix error in upload action, if server domain ends with / --- .../nodes/SeaTable/v2/actions/asset/upload/execute.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts index d96f458e69..bdd74545e3 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts @@ -24,8 +24,10 @@ export async function upload( const append = this.getNodeParameter('append', index) as string; // get server url - const credentials = await this.getCredentials('seaTableApi'); - const serverURL = credentials.domain ?? 'https://cloud.seatable.io'; + const credentials: any = await this.getCredentials('seaTableApi'); + const serverURL: string = credentials.domain + ? credentials.domain.replace(/\/$/, '') + : 'https://cloud.seatable.io'; // get workspaceId const workspaceId = ( From 6f41c759eb9e7fce5f9dbd8bd8151e890a13414e Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Thu, 13 Jun 2024 14:05:21 +0200 Subject: [PATCH 13/14] fixing asset upload --- .../v2/actions/asset/upload/execute.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts index bdd74545e3..5d98cda205 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/execute.ts @@ -53,7 +53,7 @@ export async function upload( table_name: tableName, }, ); - existingAssetArray = rowToUpdate[uploadColumnName]; + existingAssetArray = rowToUpdate[uploadColumnName] ?? []; } // Get the binary data and prepare asset for upload @@ -85,6 +85,7 @@ export async function upload( '', options, ); + //console.log('uploadAsset: ' + uploadAsset); // now step 2 (attaching the asset to a column in a base) for (let c = 0; c < uploadAsset.length; c++) { @@ -106,11 +107,21 @@ export async function upload( } // merge with existing assets in this column or with [] and remove duplicates - rowInput[uploadColumnName] = [ - // @ts-ignore: - ...new Set([...rowInput[uploadColumnName], ...existingAssetArray]), - ]; + //console.log('existingAssetArray: ' + existingAssetArray); + //console.log('rowInput_uploadedColumnName: ' + rowInput[uploadColumnName]); + + const mergedArray = existingAssetArray.concat(rowInput[uploadColumnName]); + + // Remove duplicates based on "url", keeping the last one + const uniqueAssets = Array.from( + // @ts-ignore + mergedArray.reduce((map, asset) => map.set(asset.url, asset), new Map()).values(), + ); + + // Update the rowInput with the unique assets and store into body.row. + rowInput[uploadColumnName] = uniqueAssets; body.row = rowInput; + //console.log(body.row); // attach assets to table row const responseData = await seaTableApiRequest.call( @@ -120,6 +131,7 @@ export async function upload( '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/', body, ); + //console.log('responseData: ' + responseData); uploadAsset[c]['upload_successful'] = responseData.success; } From 3b0325a7e8cf949e0cd311f52b66e12b13c3a1ff Mon Sep 17 00:00:00 2001 From: Christoph Dyllick-Brenzinger Date: Fri, 14 Jun 2024 15:25:15 +0200 Subject: [PATCH 14/14] fixed linter errors and new action get row links --- .../nodes/SeaTable/SeaTableTrigger.node.ts | 18 +++-- .../nodes-base/nodes/SeaTable/seatable.svg | 2 +- .../nodes/SeaTable/v1/RowDescription.ts | 34 ++++++---- .../nodes/SeaTable/v1/SeaTable.node.ts | 40 +++++++++++ .../nodes/SeaTable/v1/SeaTableV1.node.ts | 2 +- .../nodes-base/nodes/SeaTable/v1/types.ts | 2 +- .../nodes/SeaTable/v2/SeaTableV2.node.ts | 2 +- .../nodes/SeaTable/v2/actions/Interfaces.ts | 2 +- .../SeaTable/v2/actions/SeaTable.node.ts | 57 ++++++++++++++++ .../actions/asset/getPublicURL/description.ts | 3 +- .../nodes/SeaTable/v2/actions/asset/index.ts | 2 +- .../v2/actions/asset/upload/description.ts | 20 ++++-- .../v2/actions/base/apiCall/description.ts | 14 ++-- .../nodes/SeaTable/v2/actions/base/index.ts | 8 +-- .../v2/actions/link/add/description.ts | 19 ++++-- .../nodes/SeaTable/v2/actions/link/index.ts | 10 ++- .../v2/actions/link/list/description.ts | 66 +++++++++++++++++++ .../SeaTable/v2/actions/link/list/execute.ts | 29 ++++++++ .../SeaTable/v2/actions/link/list/index.ts | 4 ++ .../v2/actions/link/remove/description.ts | 20 ++++-- .../v2/actions/row/create/description.ts | 11 +++- .../SeaTable/v2/actions/row/create/execute.ts | 1 - .../v2/actions/row/get/description.ts | 10 ++- .../nodes/SeaTable/v2/actions/row/index.ts | 38 +++++------ .../v2/actions/row/list/description.ts | 19 +++--- .../v2/actions/row/lock/description.ts | 6 ++ .../v2/actions/row/remove/description.ts | 7 +- .../v2/actions/row/search/description.ts | 22 ++++--- .../SeaTable/v2/actions/row/search/execute.ts | 11 ++-- .../v2/actions/row/unlock/description.ts | 5 ++ .../v2/actions/row/update/description.ts | 10 ++- .../nodes/SeaTable/v2/methods/loadOptions.ts | 43 +++++++++++- 32 files changed, 423 insertions(+), 114 deletions(-) create mode 100644 packages/nodes-base/nodes/SeaTable/v1/SeaTable.node.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/SeaTable.node.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/list/description.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/list/execute.ts create mode 100644 packages/nodes-base/nodes/SeaTable/v2/actions/link/list/index.ts diff --git a/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts b/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts index b368dab462..dfe9a7f0ac 100644 --- a/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts +++ b/packages/nodes-base/nodes/SeaTable/SeaTableTrigger.node.ts @@ -69,7 +69,8 @@ export class SeaTableTrigger implements INodeType { default: 'newRow', }, { - 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', required: true, @@ -77,11 +78,13 @@ export class SeaTableTrigger implements INodeType { loadOptionsMethod: 'getTableNames', }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: - 'The name of SeaTable table to access. Choose from the list, or specify an ID using an expression.', + 'The name of SeaTable table to access. Choose from the list, or specify the name using an expression.', }, { - displayName: 'View Name or ID', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'View Name', name: 'viewName', type: 'options', displayOptions: { @@ -94,11 +97,13 @@ export class SeaTableTrigger implements INodeType { 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 an ID using an expression.', + 'The name of SeaTable view to access. Choose from the list, or specify the name using an expression.', }, { - displayName: 'Signature Column Name or ID', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Signature Column', name: 'assetColumn', type: 'options', required: true, @@ -112,8 +117,9 @@ export class SeaTableTrigger implements INodeType { 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 an ID using an expression.', + 'Select the digital-signature column that should be tracked. Choose from the list, or specify the name using an expression.', }, { displayName: 'Simplify', diff --git a/packages/nodes-base/nodes/SeaTable/seatable.svg b/packages/nodes-base/nodes/SeaTable/seatable.svg index 437e33c87a..472598576d 100644 --- a/packages/nodes-base/nodes/SeaTable/seatable.svg +++ b/packages/nodes-base/nodes/SeaTable/seatable.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/nodes-base/nodes/SeaTable/v1/RowDescription.ts b/packages/nodes-base/nodes/SeaTable/v1/RowDescription.ts index 2d6e42a079..29e4c19637 100644 --- a/packages/nodes-base/nodes/SeaTable/v1/RowDescription.ts +++ b/packages/nodes-base/nodes/SeaTable/v1/RowDescription.ts @@ -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 expression.', + 'The name of SeaTable table to access. Choose from the list, or specify the name using an expression.', }, { - 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 expression.', }, @@ -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 expression', + 'Choose from the list, or specify the name using an expression', 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 expression', + 'Choose from the list, or specify an View Name using an expression', 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 expression.', + 'Choose from the list, or specify a Column using an expression', }, ], }, diff --git a/packages/nodes-base/nodes/SeaTable/v1/SeaTable.node.ts b/packages/nodes-base/nodes/SeaTable/v1/SeaTable.node.ts new file mode 100644 index 0000000000..554cd349d4 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v1/SeaTable.node.ts @@ -0,0 +1,40 @@ +import type { INodeTypeDescription } from 'n8n-workflow'; +import { rowFields, rowOperations } from './RowDescription'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'SeaTable', + name: 'seaTable', + icon: 'file:seaTable.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the SeaTable API', + defaults: { + name: 'SeaTable', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'seaTableApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Row', + value: 'row', + }, + ], + default: 'row', + }, + ...rowOperations, + ...rowFields, + ], +}; diff --git a/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts b/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts index d12620ed57..b6d2c78f1e 100644 --- a/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts +++ b/packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts @@ -25,7 +25,7 @@ import { import type { TColumnsUiValues, TColumnValue } from './types'; import type { ICtx, IRow, IRowObject } from './Interfaces'; -import { versionDescription } from './VersionDescription'; +import { versionDescription } from './SeaTable.node'; export class SeaTableV1 implements INodeType { description: INodeTypeDescription; diff --git a/packages/nodes-base/nodes/SeaTable/v1/types.ts b/packages/nodes-base/nodes/SeaTable/v1/types.ts index 876342b368..cc8ee249fe 100644 --- a/packages/nodes-base/nodes/SeaTable/v1/types.ts +++ b/packages/nodes-base/nodes/SeaTable/v1/types.ts @@ -9,8 +9,8 @@ export type TSeaTableServerEdition = 'enterprise edition'; // dtable // ---------------------------------- -import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; import type { IDtableMetadataColumn, IDtableMetadataTable, TDtableViewColumn } from './Interfaces'; +import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; export type TInheritColumnTypeTime = 'ctime' | 'mtime'; export type TInheritColumnTypeUser = 'creator' | 'last-modifier'; diff --git a/packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts b/packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts index 2acbeee4d1..a5bfc8e4e8 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts @@ -5,7 +5,7 @@ import type { INodeTypeBaseDescription, } from 'n8n-workflow'; -import { versionDescription } from './actions/versionDescription'; +import { versionDescription } from './actions/SeaTable.node'; import { loadOptions } from './methods'; import { router } from './actions/router'; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts index 35c44eec0c..13c46822fd 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts @@ -3,7 +3,7 @@ import type { AllEntities, Entity, PropertiesOf } from 'n8n-workflow'; type SeaTableMap = { row: 'create' | 'get' | 'search' | 'update' | 'remove' | 'lock' | 'unlock' | 'list'; base: 'snapshot' | 'metadata' | 'apiCall' | 'collaborator'; - link: 'add' | 'remove'; + link: 'add' | 'list' | 'remove'; asset: 'upload' | 'getPublicURL'; }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/SeaTable.node.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/SeaTable.node.ts new file mode 100644 index 0000000000..c9b37d41da --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/SeaTable.node.ts @@ -0,0 +1,57 @@ +import type { INodeTypeDescription } from 'n8n-workflow'; +import * as row from './row'; +import * as base from './base'; +import * as link from './link'; +import * as asset from './asset'; + +export const versionDescription: INodeTypeDescription = { + displayName: 'SeaTable', + name: 'seaTable', + icon: 'file:seatable.svg', + group: ['output'], + version: 2, + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Consume the SeaTable API', + defaults: { + name: 'SeaTable', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'seaTableApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Row', + value: 'row', + }, + { + name: 'Base', + value: 'base', + }, + { + name: 'Link', + value: 'link', + }, + { + name: 'Asset', + value: 'asset', + }, + ], + default: 'row', + }, + ...row.descriptions, + ...base.descriptions, + ...link.descriptions, + ...asset.descriptions, + ], +}; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/description.ts index da37de8e69..58a4652193 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/getPublicURL/description.ts @@ -2,7 +2,7 @@ import type { AssetProperties } from '../../Interfaces'; export const assetGetPublicURLDescription: AssetProperties = [ { - displayName: 'Asset path', + displayName: 'Asset Path', name: 'assetPath', type: 'string', placeholder: '/images/2023-09/logo.png', @@ -14,6 +14,5 @@ export const assetGetPublicURLDescription: AssetProperties = [ }, }, default: '', - description: '', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts index 661a110d16..95a87716d0 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts @@ -26,7 +26,7 @@ export const descriptions: INodeProperties[] = [ name: 'Upload', value: 'upload', description: 'Add a file/image to an existing row', - action: 'Upload a file/image', + action: 'Upload a file or image', }, ], default: 'upload', diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts index 4cc3ca337d..2b739cc0ba 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/asset/upload/description.ts @@ -2,6 +2,7 @@ import type { AssetProperties } from '../../Interfaces'; export const assetUploadDescription: AssetProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', @@ -17,11 +18,13 @@ export const assetUploadDescription: AssetProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', }, { - displayName: 'Column', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column Name', name: 'uploadColumn', type: 'options', displayOptions: { @@ -36,12 +39,17 @@ export const assetUploadDescription: AssetProperties = [ }, required: true, default: '', - description: 'Select the column for the upload.', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Select the column for the upload. Choose from the list, or specify the name using an expression.', }, { + // 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 expression', required: true, typeOptions: { loadOptionsDependsOn: ['tableName'], @@ -70,7 +78,7 @@ export const assetUploadDescription: AssetProperties = [ description: 'Name of the binary property which contains the data for the file to be written', }, { - displayName: 'Replace existing file', + displayName: 'Replace Existing File', name: 'replace', type: 'boolean', default: true, @@ -81,10 +89,10 @@ export const assetUploadDescription: AssetProperties = [ }, }, description: - 'Replace existing asset with the same name. Otherwise a new version with another name (numeral in parentheses) will be uploaded.', + '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', + displayName: 'Append to Column', name: 'append', type: 'boolean', default: true, @@ -95,6 +103,6 @@ export const assetUploadDescription: AssetProperties = [ }, }, description: - 'Keep existing files/images in the column and append the new asset. Otherwise the existing files/images are remove from the column.', + '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).', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts index da4f8f0210..6725b470d0 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/apiCall/description.ts @@ -30,7 +30,7 @@ export const baseApiCallDescription: BaseProperties = [ }, }, required: true, - default: '', + default: 'POST', }, { displayName: 'Hint: The Authentication header is included automatically.', @@ -56,20 +56,17 @@ export const baseApiCallDescription: BaseProperties = [ required: true, default: '', placeholder: '/dtable-server/...', - description: - 'The URL has to start with /dtable-server/ or /dtable-db/. All possible requests can be found at the SeaTable API Reference at https://api.seatable.io \ - Please be aware that only request from the section Base Operations that use an Base-Token for the authentication are allowed to use.', + description: 'The URL has to start with /dtable-server/ or /dtable-db/. All possible requests can be found at the SeaTable API Reference at https://api.seatable.io Please be aware that only request from the section Base Operations that use an Base-Token for the authentication are allowed to use.', }, { displayName: 'Query String Parameters', name: 'apiParams', type: 'fixedCollection', - default: '', + default: {}, typeOptions: { multipleValues: true, }, - description: - 'These params will be URL-encoded and appended to the URL when making the request.', + description: 'These params will be URL-encoded and appended to the URL when making the request', options: [ { name: 'apiParamsValues', @@ -115,11 +112,10 @@ export const baseApiCallDescription: BaseProperties = [ 'Only valid JSON is accepted. n8n will pass anything you enter as raw input. For example, {"foo", "bar"} is perfectly valid. Of cause you can use variables from n8n inside your JSON.', }, { - displayName: 'Response object parameter name', + displayName: 'Response Object Parameter Name', name: 'responseObjectName', type: 'string', placeholder: 'Leave it empty or use a value like "rows", "metadata", "views" etc.', - required: false, displayOptions: { show: { resource: ['base'], diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts index 9e83556515..0349c100a0 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts @@ -23,7 +23,7 @@ export const descriptions: INodeProperties[] = [ name: 'Snapshot', value: 'snapshot', description: 'Create a snapshot of the base', - action: 'Create a Snapshot', + action: 'Create a snapshot', }, { name: 'Metadata', @@ -35,16 +35,16 @@ export const descriptions: INodeProperties[] = [ name: 'API Call', value: 'apiCall', description: 'Perform an authorized API call (Base Operation)', - action: 'Make an API Call', + action: 'Make an api call', }, { name: 'Collaborator', value: 'collaborator', - description: 'Get this username from the email or name of a collaborator.', + description: 'Get this username from the email or name of a collaborator', action: 'Get username from email or name', }, ], - default: '', + default: 'snapshot', }, ...snapshot.description, ...metadata.description, diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts index e82e58cc7a..5e0a35abf6 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/add/description.ts @@ -2,6 +2,7 @@ import type { LinkProperties } from '../../Interfaces'; export const linkAddDescription: LinkProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name (Source)', name: 'tableName', type: 'options', @@ -17,10 +18,13 @@ export const linkAddDescription: LinkProperties = [ }, }, default: '', - description: 'If you use an expression, provide it in the way ":::".', + // 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".', }, { - displayName: 'Link column', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Link Column', name: 'linkColumn', type: 'options', displayOptions: { @@ -35,11 +39,12 @@ export const linkAddDescription: LinkProperties = [ }, required: true, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: - 'If you use an expression, provide it in the way "::::::".', + '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', + displayName: 'Row ID From the Source Table', name: 'linkColumnSourceId', type: 'string', displayOptions: { @@ -50,10 +55,10 @@ export const linkAddDescription: LinkProperties = [ }, required: true, default: '', - description: 'Provide the row ID of table you selected.', + description: 'Provide the row ID of table you selected', }, { - displayName: 'Row ID from the target', + displayName: 'Row ID From the Target', name: 'linkColumnTargetId', type: 'string', displayOptions: { @@ -64,6 +69,6 @@ export const linkAddDescription: LinkProperties = [ }, required: true, default: '', - description: 'Provide the row ID of table you want to link.', + description: 'Provide the row ID of table you want to link', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts index 56d3c58257..38837e7aa0 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts @@ -1,8 +1,9 @@ import * as add from './add'; +import * as list from './list'; import * as remove from './remove'; import type { INodeProperties } from 'n8n-workflow'; -export { add, remove }; +export { add, list, remove }; export const descriptions: INodeProperties[] = [ { @@ -22,6 +23,12 @@ export const descriptions: INodeProperties[] = [ 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', @@ -32,5 +39,6 @@ export const descriptions: INodeProperties[] = [ default: 'add', }, ...add.description, + ...list.description, ...remove.description, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/description.ts new file mode 100644 index 0000000000..8767139f24 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/description.ts @@ -0,0 +1,66 @@ +import type { LinkProperties } from '../../Interfaces'; + +export const listLinkDescription: LinkProperties = [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Table Name 32', + name: 'tableName', + type: 'options', + placeholder: 'Select a table', + required: true, + typeOptions: { + loadOptionsMethod: 'getTableNameAndId', + }, + displayOptions: { + show: { + resource: ['link'], + operation: ['list'], + }, + }, + default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Choose from the list, of specify by using an expression. Provide it in the way "table_name:::table_id".', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Link Column', + name: 'linkColumn', + type: 'options', + displayOptions: { + show: { + resource: ['link'], + operation: ['list'], + }, + }, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getLinkColumnsWithColumnKey', + }, + required: true, + default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Choose from the list of specify the Link Column by using an expression. You have to provide it in the way "column_name:::link_id:::other_table_id:::column_key".', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Row ID', + name: 'rowId', + type: 'options', + description: + 'Choose from the list, or specify an ID using an expression', + required: true, + typeOptions: { + loadOptionsDependsOn: ['tableName'], + loadOptionsMethod: 'getRowIds', + }, + displayOptions: { + show: { + resource: ['link'], + operation: ['list'], + }, + }, + default: '', + }, +]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/execute.ts new file mode 100644 index 0000000000..87d51cbce2 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/execute.ts @@ -0,0 +1,29 @@ +import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow'; +import { seaTableApiRequest } from '../../../GenericFunctions'; + +export async function list(this: IExecuteFunctions, index: number): Promise { + // get parameters + const tableName = this.getNodeParameter('tableName', index) as string; + const linkColumn = this.getNodeParameter('linkColumn', index) as string; + const rowId = this.getNodeParameter('rowId', index) as string; + + // get rows + let responseData = await seaTableApiRequest.call( + this, + {}, + 'POST', + '/dtable-db/api/v1/linked-records/{{dtable_uuid}}/', + { + table_id: tableName.split(':::')[1], + link_column: linkColumn.split(':::')[3], + rows: [ + { + row_id: rowId, + offset: 0, + limit: 100, + }, + ], + }, + ); + return this.helpers.returnJsonArray(responseData[rowId] as IDataObject[]); +} diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/index.ts new file mode 100644 index 0000000000..42478a6ce3 --- /dev/null +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/list/index.ts @@ -0,0 +1,4 @@ +import { list as execute } from './execute'; +import { listLinkDescription as description } from './description'; + +export { description, execute }; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/description.ts index 4fa7d22e5d..288bcefd0b 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/link/remove/description.ts @@ -2,6 +2,7 @@ import type { LinkProperties } from '../../Interfaces'; export const linkRemoveDescription: LinkProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name (Source)', name: 'tableName', type: 'options', @@ -17,10 +18,13 @@ export const linkRemoveDescription: LinkProperties = [ }, }, default: '', - description: 'If you use an expression, provide it in the way ":::".', + // 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".', }, { - displayName: 'Link column', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Link Column', name: 'linkColumn', type: 'options', displayOptions: { @@ -35,10 +39,12 @@ export const linkRemoveDescription: LinkProperties = [ }, required: true, default: '', - description: 'If you use an expression, provide it in the way "::::::".', + // 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', + displayName: 'Row ID From the Source Table', name: 'linkColumnSourceId', type: 'string', displayOptions: { @@ -49,10 +55,10 @@ export const linkRemoveDescription: LinkProperties = [ }, required: true, default: '', - description: 'Provide the row ID of table you selected.', + description: 'Provide the row ID of table you selected', }, { - displayName: 'Row ID from the target', + displayName: 'Row ID From the Target Table', name: 'linkColumnTargetId', type: 'string', displayOptions: { @@ -63,6 +69,6 @@ export const linkRemoveDescription: LinkProperties = [ }, required: true, default: '', - description: 'Provide the row ID of table you want to link.', + description: 'Provide the row ID of table you want to link', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts index 702676f638..69589e9982 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/description.ts @@ -2,6 +2,7 @@ import type { RowProperties } from '../../Interfaces'; export const rowCreateDescription: RowProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', @@ -17,6 +18,7 @@ export const rowCreateDescription: RowProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', }, @@ -76,9 +78,11 @@ export const rowCreateDescription: RowProperties = [ name: 'columnValues', values: [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Column Name', name: 'columnName', type: 'options', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'Choose from the list, or specify the column name using an expression', typeOptions: { @@ -105,10 +109,10 @@ export const rowCreateDescription: RowProperties = [ }, 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', + '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', + displayName: 'Save to "Big Data" Backend', name: 'bigdata', type: 'boolean', displayOptions: { @@ -118,7 +122,8 @@ export const rowCreateDescription: RowProperties = [ }, }, default: false, - description: 'This requires the activation of the Big Data backend in the base.', + 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.', diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts index 756b3d203a..71f539d01a 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/create/execute.ts @@ -64,7 +64,6 @@ export async function create( // save to normal backend else { body.row = rowInput; - const responseData = await seaTableApiRequest.call( this, {}, diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/description.ts index 301653d1b1..ebcb716e0a 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/get/description.ts @@ -2,6 +2,7 @@ import type { RowProperties } from '../../Interfaces'; export const rowGetDescription: RowProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', @@ -17,13 +18,17 @@ export const rowGetDescription: RowProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', }, { + // 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 expression', required: true, typeOptions: { loadOptionsDependsOn: ['tableName'], @@ -38,7 +43,7 @@ export const rowGetDescription: RowProperties = [ default: '', }, { - displayName: 'Simplify output', + displayName: 'Simplify', name: 'simple', type: 'boolean', displayOptions: { @@ -48,7 +53,6 @@ export const rowGetDescription: RowProperties = [ }, }, default: true, - description: - 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.', + description: 'Whether to return a simplified version of the response instead of the raw data', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts index a78e54f528..14f10c17f5 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts @@ -28,6 +28,12 @@ export const descriptions: INodeProperties[] = [ 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', @@ -37,39 +43,33 @@ export const descriptions: INodeProperties[] = [ { name: 'Get Many', value: 'list', - description: 'Get many rows from a table of view', + 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: 'Update', - value: 'update', - description: 'Update the content of a row', - action: 'Update a row', - }, - { - name: 'Delete', - value: 'remove', - description: 'Delete a row', - action: 'Delete a row', - }, - { - name: 'Lock', - value: 'lock', - description: 'Lock a row to prevent further changes.', - action: 'Add a row lock', - }, { name: 'Unlock', value: 'unlock', description: 'Remove the lock from a row', action: 'Remove a row lock', }, + { + name: 'Update', + value: 'update', + description: 'Update the content of a row', + action: 'Update a row', + }, ], default: 'create', }, diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/description.ts index 3d1360cf0f..e0af69a689 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/list/description.ts @@ -2,13 +2,14 @@ import type { RowProperties } from '../../Interfaces'; export const rowListDescription: RowProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', placeholder: 'Select a table', required: true, typeOptions: { - loadOptionsMethod: 'getTableNames', + loadOptionsMethod: 'getTableNameAndId', }, displayOptions: { show: { @@ -17,14 +18,15 @@ export const rowListDescription: RowProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: - 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', + 'Choose from the list, or specify by using an expression. Provide it in the way "table_name:::table_id".', }, { - displayName: 'View Name or ID (optional)', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'View Name', name: 'viewName', type: 'options', - required: false, displayOptions: { show: { resource: ['row'], @@ -36,10 +38,12 @@ export const rowListDescription: RowProperties = [ loadOptionsMethod: 'getTableViews', }, default: '', - description: 'The name of SeaTable view to access. Choose from the list, or specify ...', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'The name of SeaTable view to access, or specify by using an expression. Provide it in the way "col.name:::col.type".', }, { - displayName: 'Simplify output', + displayName: 'Simplify', name: 'simple', type: 'boolean', displayOptions: { @@ -49,7 +53,6 @@ export const rowListDescription: RowProperties = [ }, }, default: true, - description: - 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.', + description: 'Whether to return a simplified version of the response instead of the raw data', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/description.ts index e1ed6e1194..e3cfeee332 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/lock/description.ts @@ -2,6 +2,7 @@ import type { RowProperties } from '../../Interfaces'; export const rowLockDescription: RowProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', @@ -17,13 +18,18 @@ export const rowLockDescription: RowProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', }, { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Row ID', name: 'rowId', type: 'options', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Choose from the list, or specify an ID using an expression', required: true, typeOptions: { loadOptionsDependsOn: ['tableName'], diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts index 1a253ef2a0..77093c2cb0 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/remove/description.ts @@ -2,6 +2,7 @@ import type { RowProperties } from '../../Interfaces'; export const rowRemoveDescription: RowProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', @@ -17,10 +18,12 @@ export const rowRemoveDescription: RowProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', }, { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Row ID', name: 'rowId', type: 'options', @@ -36,6 +39,8 @@ export const rowRemoveDescription: RowProperties = [ }, }, default: '', - description: 'Remove any row from the normal or big data backend based on its unique row ID.', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Remove any row from the normal or big data backend based on its unique row ID. Choose from the list, or specify an ID using an expression.', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts index 666197f14a..c89fbe3647 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/description.ts @@ -2,6 +2,7 @@ import type { RowProperties } from '../../Interfaces'; export const rowSearchDescription: RowProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', @@ -17,11 +18,13 @@ export const rowSearchDescription: RowProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', }, { - displayName: 'Column', + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Column Name', name: 'searchColumn', type: 'options', displayOptions: { @@ -36,10 +39,12 @@ export const rowSearchDescription: RowProperties = [ }, required: true, default: '', - description: 'Select the column to be searched. Not all column types are supported for search.', + // 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 expression.', }, { - displayName: 'Search term', + displayName: 'Search Term', name: 'searchTerm', type: 'string', displayOptions: { @@ -64,10 +69,10 @@ export const rowSearchDescription: RowProperties = [ }, default: false, description: - 'FALSE: The search distinguish between uppercase and lowercase characters. TRUE: Search ignores case sensitivity.', + 'Whether the search ignores case sensitivity (true). Otherwise, it distinguishes between uppercase and lowercase characters.', }, { - displayName: 'Activate wildcard search', + displayName: 'Activate Wildcard Search', name: 'wildcard', type: 'boolean', displayOptions: { @@ -78,10 +83,10 @@ export const rowSearchDescription: RowProperties = [ }, default: false, description: - 'FALSE: The search only results perfect matches. TRUE: Finds a row even if the search value is part of a string.', + '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 output', + displayName: 'Simplify', name: 'simple', type: 'boolean', default: true, @@ -91,7 +96,6 @@ export const rowSearchDescription: RowProperties = [ operation: ['search'], }, }, - description: - 'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.', + description: 'Whether to return a simplified version of the response instead of the raw data', }, ]; diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts index f9de6940b1..ee4e445593 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/search/execute.ts @@ -13,7 +13,8 @@ export async function search( ): Promise { const tableName = this.getNodeParameter('tableName', index) as string; const searchColumn = this.getNodeParameter('searchColumn', index) as string; - let searchTerm = this.getNodeParameter('searchTerm', index) as any; // string or integer + const searchTerm = this.getNodeParameter('searchTerm', index) as string | number; + let searchTermString = String(searchTerm) as string; const insensitive = this.getNodeParameter('insensitive', index) as boolean; const wildcard = this.getNodeParameter('wildcard', index) as boolean; const simple = this.getNodeParameter('simple', index) as boolean; @@ -25,14 +26,12 @@ export async function search( let sqlQuery = `SELECT * FROM \`${tableName}\` WHERE \`${searchColumn}\``; if (insensitive) { - searchTerm = searchTerm.toLowerCase(); + searchTermString = searchTermString.toLowerCase(); sqlQuery = `SELECT * FROM \`${tableName}\` WHERE lower(\`${searchColumn}\`)`; } - if (wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE "%' + searchTerm + '%"'; - else if (!wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' = "' + searchTerm + '"'; - else if (wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE %' + searchTerm + '%'; - else if (!wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' = ' + searchTerm; + if (wildcard) sqlQuery = sqlQuery + ' LIKE "%' + searchTermString + '%"'; + else if (!wildcard) sqlQuery = sqlQuery + ' = "' + searchTermString + '"'; const sqlResult = (await seaTableApiRequest.call( this, diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/description.ts index dcea346302..19f4d3355f 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/unlock/description.ts @@ -2,6 +2,7 @@ import type { RowProperties } from '../../Interfaces'; export const rowUnlockDescription: RowProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', @@ -17,13 +18,17 @@ export const rowUnlockDescription: RowProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', }, { + // 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 expression', required: true, typeOptions: { loadOptionsDependsOn: ['tableName'], diff --git a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts index 29e0659cac..cf61b83ad3 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/actions/row/update/description.ts @@ -2,6 +2,7 @@ import type { RowProperties } from '../../Interfaces'; export const rowUpdateDescription: RowProperties = [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Table Name', name: 'tableName', type: 'options', @@ -17,13 +18,18 @@ export const rowUpdateDescription: RowProperties = [ }, }, default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'The name of SeaTable table to access. Choose from the list, or specify a name using an expression.', }, { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Row ID', name: 'rowId', type: 'options', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Choose from the list, or specify an ID using an expression', required: true, typeOptions: { loadOptionsDependsOn: ['tableName'], @@ -93,9 +99,11 @@ export const rowUpdateDescription: RowProperties = [ name: 'columnValues', values: [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options displayName: 'Column Name', name: 'columnName', type: 'options', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options description: 'Choose from the list, or specify the column name using an expression', typeOptions: { @@ -122,7 +130,7 @@ export const rowUpdateDescription: RowProperties = [ }, 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', + '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.', diff --git a/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts b/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts index c9c12387fc..e0e0a795c5 100644 --- a/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts +++ b/packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts @@ -110,6 +110,40 @@ export async function getLinkColumns(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const table = this.getCurrentNodeParameter('tableName') as string; + + const tableName = table.split(':::')[0]; + const tableId = table.split(':::')[1]; + + if (tableName) { + const columns = await seaTableApiRequest.call( + this, + {}, + 'GET', + '/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns', + {}, + { table_name: tableName }, + ); + for (const col of columns.columns) { + if (col.type === 'link') { + // make sure that the "other table id" is returned and not the same table id again. + const otid = + tableId !== col.data.other_table_id ? col.data.other_table_id : col.data.table_id; + + returnData.push({ + name: col.name, + value: col.name + ':::' + col.data.link_id + ':::' + otid + ':::' + col.key, + }); + } + } + } + return returnData; +} + export async function getAssetColumns( this: ILoadOptionsFunctions, ): Promise { @@ -179,9 +213,14 @@ export async function getTableUpdateAbleColumns( } export async function getRowIds(this: ILoadOptionsFunctions): Promise { - const tableName = this.getNodeParameter('tableName') as string; - const returnData: INodePropertyOptions[] = []; + const table = this.getCurrentNodeParameter('tableName') as string; + let tableName = table; + if (table.indexOf(':::') !== -1) { + tableName = table.split(':::')[0]; + } + + const returnData: INodePropertyOptions[] = []; if (tableName) { const sqlResult = await seaTableApiRequest.call( this,