diff --git a/packages/nodes-base/nodes/DataStore/DataStore.node.json b/packages/nodes-base/nodes/DataStore/DataStore.node.json new file mode 100644 index 00000000000..ff13b636ecd --- /dev/null +++ b/packages/nodes-base/nodes/DataStore/DataStore.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.dataStore", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "details": "Data Store", + "categories": ["Core Nodes"], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.dataStore/" + } + ] + }, + "alias": ["data", "table", "knowledge"], + "subcategories": { + "Core Nodes": ["Data Transformation"] + } +} diff --git a/packages/nodes-base/nodes/DataStore/DataStore.node.ts b/packages/nodes-base/nodes/DataStore/DataStore.node.ts new file mode 100644 index 00000000000..f1e5324b0ac --- /dev/null +++ b/packages/nodes-base/nodes/DataStore/DataStore.node.ts @@ -0,0 +1,164 @@ +import type { + IDataObject, + IExecuteFunctions, + ILoadOptionsFunctions, + INodeExecutionData, + INodeListSearchResult, + INodeType, + INodeTypeDescription, + JsonObject, + ResourceMapperField, + ResourceMapperFields, +} from 'n8n-workflow'; +import { NodeConnectionTypes } from 'n8n-workflow'; + +import { rowOperations, rowFields } from './RowDescription'; + +// TODO: hide this node +export class DataStore implements INodeType { + description: INodeTypeDescription = { + displayName: 'Data Store', + name: 'dataStore', + icon: 'fa:table', + iconColor: 'orange', + group: ['transform'], // ? + version: 1, + subtitle: '={{$parameter["action"]}}', + description: 'Save data across workflow executions in a table', + defaults: { + name: 'Data Store', + }, + usableAsTool: true, + inputs: [NodeConnectionTypes.Main], + outputs: [NodeConnectionTypes.Main], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Row', + value: 'row', + }, + ], + default: 'row', + }, + ...rowOperations, + ...rowFields, + ], + }; + + methods = { + listSearch: { + async tableSearch(this: ILoadOptionsFunctions): Promise { + if (this.helpers.getDataStoreAggregateProxy === undefined) return { results: [] }; + + const proxy = await this.helpers.getDataStoreAggregateProxy(); + const result = await proxy.getManyAndCount({ take: 10000 }); + + const results = result.data.map((row) => { + return { + name: row.name, + value: row.id, + }; + }); + + return { + results, + }; + }, + }, + resourceMapping: { + async getColumns(this: ILoadOptionsFunctions): Promise { + if (this.helpers.getDataStoreProxy === undefined) return { fields: [] }; + + const dataStoreId = this.getNodeParameter('tableId', '', { extractValue: true }) as string; + const proxy = await this.helpers.getDataStoreProxy(dataStoreId); + const result = await proxy.getColumns(); + + const fields: ResourceMapperField[] = []; + + for (const field of result) { + const type = field.type === 'date' ? 'dateTime' : field.type; + + fields.push({ + id: field.name, + displayName: field.name, + required: false, + defaultMatch: false, + display: true, + type, + readOnly: false, + removed: false, + }); + } + + return { fields }; + }, + }, + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + if (this.helpers.getDataStoreProxy === undefined) return [items]; + + const returnData: INodeExecutionData[] = []; + const length = items.length; + const resource = this.getNodeParameter('resource', 0, 'table'); + const operation = this.getNodeParameter('operation', 0, 'create'); + + for (let i = 0; i < length; i++) { + try { + if (resource === 'row') { + const tableId = this.getNodeParameter('tableId', i, '', { + extractValue: true, + }) as string; + const dataStoreProxy = await this.helpers.getDataStoreProxy(tableId); + if (operation === 'insert') { + const dataMode = this.getNodeParameter('columns.mappingMode', 0) as string; + + let data: IDataObject; + + if (dataMode === 'autoMapInputData') { + data = items[i].json; + } else { + const fields = this.getNodeParameter('columns.value', i, {}) as IDataObject; + + data = fields; + } + const response = await dataStoreProxy.insertRows([data as never]); + if (response) + returnData.push({ + json: data, + }); + } + if (operation === 'get') { + const response = await dataStoreProxy.getManyRowsAndCount({}); + + (response?.data ?? []).forEach((item) => { + returnData.push({ + json: item, + }); + }); + } + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: (error as JsonObject).message, + }, + pairedItem: { + item: i, + }, + }); + continue; + } + throw error; + } + } + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/DataStore/RowDescription.ts b/packages/nodes-base/nodes/DataStore/RowDescription.ts new file mode 100644 index 00000000000..c9984e0825a --- /dev/null +++ b/packages/nodes-base/nodes/DataStore/RowDescription.ts @@ -0,0 +1,100 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const rowOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['row'], + }, + }, + options: [ + { + name: 'Create or Update', + value: 'upsert', + description: 'Create a new record, or update the current one if it already exists (upsert)', + action: 'Create or update a row', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a row', + action: 'Delete a row', + }, + { + name: 'Get', + value: 'get', + description: 'Get a row', + action: 'Get a row', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many rows', + action: 'Get many rows', + }, + { + name: 'Insert', + value: 'insert', + description: 'Insert a new row', + action: 'Insert a row', + }, + ], + default: 'upsert', + }, +]; + +export const rowFields: INodeProperties[] = [ + { + displayName: 'Table', + name: 'tableId', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'tableSearch', + searchable: true, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + }, + ], + displayOptions: { show: { resource: ['row'] } }, + }, + { + displayName: 'Columns', + name: 'columns', + type: 'resourceMapper', + default: { + mappingMode: 'defineBelow', + value: null, + }, + noDataExpression: true, + required: true, + typeOptions: { + loadOptionsDependsOn: ['tableId.value'], + resourceMapper: { + resourceMapperMethod: 'getColumns', + mode: 'add', + fieldWords: { + singular: 'column', + plural: 'columns', + }, + addAllFields: true, + multiKeyMatch: true, + }, + }, + displayOptions: { show: { resource: ['row'], operation: ['upsert', 'insert'] } }, + }, +]; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0734d4234c7..ea85b7e2163 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -488,6 +488,7 @@ "dist/nodes/Crypto/Crypto.node.js", "dist/nodes/CustomerIo/CustomerIo.node.js", "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", + "dist/nodes/DataStore/DataStore.node.js", "dist/nodes/DateTime/DateTime.node.js", "dist/nodes/DebugHelper/DebugHelper.node.js", "dist/nodes/DeepL/DeepL.node.js",