diff --git a/packages/nodes-base/credentials/TableFlowApi.credentials.ts b/packages/nodes-base/credentials/TableFlowApi.credentials.ts new file mode 100644 index 00000000000..80ffc451f2e --- /dev/null +++ b/packages/nodes-base/credentials/TableFlowApi.credentials.ts @@ -0,0 +1,39 @@ +import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow'; + +export class TableFlowApi implements ICredentialType { + name = 'tableflowApi'; + displayName = 'TableFlow API'; + documentationUrl = ''; + + properties: INodeProperties[] = [ + // User access token + { + displayName: 'Access Token', + name: 'token', + type: 'string', + default: '', + typeOptions: { + password: true, + }, + required: true, + description: 'Your TableFlow API access token (JWT)', + }, + ]; + + authenticate = { + type: 'generic' as const, + properties: { + headers: { + Authorization: '={{"Bearer " + $credentials.token}}', + }, + }, + }; + + // Method for checking credentials + test: ICredentialTestRequest = { + request: { + method: 'GET', + url: 'https://example.com', + }, + }; +} diff --git a/packages/nodes-base/nodes/TableFlow/README.md b/packages/nodes-base/nodes/TableFlow/README.md new file mode 100644 index 00000000000..5168655e187 --- /dev/null +++ b/packages/nodes-base/nodes/TableFlow/README.md @@ -0,0 +1,86 @@ +# 🧩 TableFlow + +> **Category:** Database / Data Management +> **Node Type:** Integration +> **Version:** 1.0 +> **Author:** Your Name +> **License:** MIT + +--- + +## 🧠 Overview + +The **TableFlow** node allows you to connect n8n with the [TableFlow API](https://www.tableflow.tech). +It enables you to automate data import, synchronization, and table management workflows directly within n8n. + +Use it to create, update, or fetch data from TableFlow tables β€” or trigger and transformations programmatically. + +--- + +## πŸ” Credentials + +### **TableFlow API** + +This node requires a **TableFlow Access Token**. + +1. Log in to your [TableFlow Dashboard](https://www.tableflow.tech). +2. Go to **Settings β†’ Access Token**. +3. Generate and copy a new token. +4. In n8n, add new credentials: + - **Name:** TableFlow API + - **Access Token:** your_token_here + +Credentials are stored securely and shared across TableFlow operations. + +--- + +## βš™οΈ Node Configuration + +| Property | Type | Description | +|-----------|------|-------------| +| **Resource** | Dropdown | Choose which TableFlow resource to interact with (e.g., Table, Row, Import). | +| **Operation** | Dropdown | Choose the specific action to perform (e.g., Get, Create, Update, Delete). | +| **Table ID** | String | The unique identifier for the TableFlow table. | +| **Data** | JSON | JSON-formatted payload for create/update operations. | +| **Additional Fields** | Object | Optional parameters such as pagination, filtering, or sorting. | + +--- + +## 🧩 Supported Resources and Operations + +| Resource | Operation | Description | +|-----------|------------|-------------| +| **Table** | *Get All* | Retrieve all tables from your TableFlow workspace. | +| **Table** | *Get* | Retrieve details of a specific table. | +| **Row** | *Create* | Insert new rows into a table. | +| **Row** | *Update* | Modify existing table rows. | +| **Row** | *Delete* | Delete rows by ID. | + +--- + +## πŸ§ͺ Example Workflows + +### **1️⃣ List All Tables** + +Fetch a list of all tables in your TableFlow workspace. + +**Configuration:** +- Resource: `Table` +- Operation: `Get All` + +**Example Output:** +```json +[ + { + "id": "tbl_12345", + "name": "Customer Data", + "rows": 120, + "createdAt": "2025-08-10T12:00:00Z" + }, + { + "id": "tbl_67890", + "name": "Product Catalog", + "rows": 45, + "createdAt": "2025-09-22T08:15:00Z" + } +] \ No newline at end of file diff --git a/packages/nodes-base/nodes/TableFlow/TableFlow.node.ts b/packages/nodes-base/nodes/TableFlow/TableFlow.node.ts new file mode 100644 index 00000000000..9b725d44397 --- /dev/null +++ b/packages/nodes-base/nodes/TableFlow/TableFlow.node.ts @@ -0,0 +1,467 @@ +import type { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow'; + +export class TableFlow implements INodeType { + // πŸ”Ή Node type description + description: INodeTypeDescription = { + displayName: 'TableFlow', + name: 'tableflow', + icon: { light: 'file:icon.svg', dark: 'file:icon-dark.svg' }, + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Interact with TableFlow APIs using Access token', + defaults: { + name: 'TableFlow', + }, + usableAsTool: true, + inputs: [NodeConnectionTypes.Main], + outputs: [NodeConnectionTypes.Main], + credentials: [ + { + name: 'tableflowApi', + required: true, + }, + ], + + properties: [ + // Resource selector + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [{ name: 'Record', value: 'record' }], + default: 'record', + }, + + // βœ… Record operations (CRUD β†’ shows under Actions) + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['record'] } }, + options: [ + { name: 'Create', value: 'create', action: 'Create a record' }, + { + name: 'Create or Update', + value: 'create or update', + action: 'Create or update a record', + }, + { name: 'Get', value: 'get', action: 'Get a record' }, + ], + default: 'create', + }, + + // Table selection + { + displayName: 'Table', + name: 'tableId', + type: 'options', + typeOptions: { loadOptionsMethod: 'getTables' }, + default: '', + required: true, + displayOptions: { + show: { operation: ['create', 'create or update', 'get'] }, + }, + }, + + // Record ID (for update mode) + { + displayName: 'Record ID', + name: 'recordId', + type: 'string', + default: '', + description: 'If provided, the record with this ID will be updated instead of created', + displayOptions: { + show: { operation: ['create or update'], resource: ['record'] }, + }, + }, + + // Mapping mode + { + displayName: 'Mapping Column Mode', + name: 'mappingMode', + type: 'options', + noDataExpression: true, + options: [ + { name: 'Map Each Column Manually', value: 'manual' }, + { name: 'Map Automatically', value: 'auto' }, + ], + default: 'manual', + required: true, + displayOptions: { + show: { operation: ['create', 'create or update'], resource: ['record'] }, + }, + }, + + { + displayName: + 'In this mode, make sure the incoming data fields are named the same as the columns in TableFlow. (Use an "Edit Fields" node before this node to change them if required.)', + name: 'autoMappingNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + mappingMode: ['auto'], + operation: ['create', 'create or update'], + resource: ['record'], + }, + }, + }, + + // Manual mapping + { + displayName: 'Values to Send', + name: 'valuesToSend', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + placeholder: 'Add Column', + default: {}, + displayOptions: { + show: { mappingMode: ['manual'], operation: ['create', 'create or update'] }, + }, + options: [ + { + name: 'columns', + displayName: 'Columns', + values: [ + { + displayName: 'Column Name', + name: 'columnName', + type: 'options', + typeOptions: { loadOptionsMethod: 'getTableColumns' }, + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + + // βœ… Extra fields for GET records + { + displayName: 'Search Query', + name: 'search', + type: 'string', + default: '', + description: 'Search string to filter records', + displayOptions: { + show: { operation: ['get'], resource: ['record'] }, + }, + }, + + { + displayName: 'Offset', + name: 'offset', + type: 'number', + default: 0, + description: 'Number of records to skip', + displayOptions: { + show: { operation: ['get'], resource: ['record'] }, + }, + }, + + { + displayName: 'Count', + name: 'count', + type: 'number', + default: 50, + description: 'Number of records to fetch', + displayOptions: { + show: { operation: ['get'], resource: ['record'] }, + }, + }, + + { + displayName: 'Include Count', + name: 'includeCount', + type: 'boolean', + default: true, + description: 'Whether to include total count in the response', + displayOptions: { + show: { operation: ['get'], resource: ['record'] }, + }, + }, + ], + }; + + // πŸ”Ή Load options for dynamic dropdowns + methods = { + loadOptions: { + async getTables(this: ILoadOptionsFunctions): Promise { + try { + const credentials = await this.getCredentials('tableflowApi'); + const accessToken = credentials?.token; + + const response = await this.helpers.request({ + method: 'GET', + url: 'https://api.tableflow.tech/app', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + json: true, + }); + + const options: INodePropertyOptions[] = []; + if (response.Tablees) { + for (const [id, table] of Object.entries(response.Tablees)) { + options.push({ name: (table as any).DbName, value: id }); + } + } + return options; + } catch (error: any) { + throw new NodeOperationError(this.getNode(), `Failed to fetch tables: ${error.message}`); + } + }, + + async getTableColumns(this: ILoadOptionsFunctions): Promise { + const tableId = this.getCurrentNodeParameter('tableId') as string; + if (!tableId) return []; + + const credentials = await this.getCredentials('tableflowApi'); + const accessToken = credentials?.token; + + const response = await this.helpers.request({ + method: 'GET', + url: `https://api.tableflow.tech/app`, + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + json: true, + }); + + const table = response.Tablees?.[tableId]; + if (!table || !table.Fields) { + return []; + } + + return table.Fields.map((field: any) => ({ + name: field.Label || field.Name, + value: field.Name, + })); + }, + + async getDynamicColumns(this: ILoadOptionsFunctions) { + const tableId = this.getCurrentNodeParameter('tableId') as string; + if (!tableId) return []; + + const credentials = await this.getCredentials('tableflowApi'); + const accessToken = credentials?.token; + + const response = await this.helpers.request({ + method: 'GET', + url: 'https://api.tableflow.tech/app', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + json: true, + }); + + const table = response.Tablees?.[tableId]; + if (!table?.Fields) return []; + + return table.Fields.map((field: any) => ({ + displayName: field.Label || field.Name, + name: field.Name, + type: 'string', + default: '', + })); + }, + }, + }; + + // πŸ”Ή Execute handler for record CRUD + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = await this.getCredentials('tableflowApi'); + const accessToken = credentials?.token; + + // Cache schema so we don’t call /app multiple times + let schemaCache: any = null; + + for (let i = 0; i < items.length; i++) { + try { + const resource = this.getNodeParameter('resource', i) as string; + const operation = this.getNodeParameter('operation', i) as string; + + // βœ… Create / Create or Update + if (resource === 'record' && (operation === 'create' || operation === 'create or update')) { + const tableId = this.getNodeParameter('tableId', i) as string; + const mappingMode = this.getNodeParameter('mappingMode', i) as string; + const recordId = this.getNodeParameter('recordId', i, '') as string; + + let fields: any[] = []; + + // πŸ”Ή Collect fields from input + if (mappingMode === 'manual') { + const valuesToSend = this.getNodeParameter('valuesToSend', i, {}) as { + columns?: Array<{ columnName: string; value: string }>; + }; + + if (valuesToSend.columns) { + fields = valuesToSend.columns.map((col) => ({ + Name: col.columnName, + Type: 1, + Value: col.value, + })); + } + } else if (mappingMode === 'auto') { + const itemData = items[i].json; + fields = Object.entries(itemData).map(([key, value]) => ({ + Name: key, + Type: 1, + Value: value, + })); + } + + let response; + + // βœ… Case 1: Update (only selected fields) + if (recordId && operation === 'create or update') { + const body = [ + { + TableId: tableId, + Locked: false, + Fields: fields, + Id: recordId, + }, + ]; + + response = await this.helpers.request({ + method: 'PUT', + url: 'https://api.tableflow.tech/record', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, + json: true, + }); + } + // βœ… Case 2: Create (or CreateOrUpdate without recordId) + else { + // Load schema once & cache + if (!schemaCache) { + schemaCache = await this.helpers.request({ + method: 'GET', + url: `https://api.tableflow.tech/app`, + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + json: true, + }); + } + + const table = schemaCache.Tablees?.[tableId]; + if (!table) { + throw new Error(`Table with id ${tableId} not found in schema`); + } + + const allColumns = table.Fields || []; + const providedMap = new Map(fields.map((f) => [f.Name, f])); + + const fullFields = allColumns.map((col: any) => { + if (providedMap.has(col.Name)) { + return providedMap.get(col.Name); + } + return { + Name: col.Name, + Type: col.Type ?? 1, + Value: null, + }; + }); + + const body = { + TableId: tableId, + Locked: false, + Fields: fullFields, + }; + + response = await this.helpers.request({ + method: 'POST', + url: 'https://api.tableflow.tech/record', + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, + json: true, + }); + } + + returnData.push({ json: response }); + } + + // βœ… Get Records + if (resource === 'record' && operation === 'get') { + const tableId = this.getNodeParameter('tableId', i) as string; + const searchQuery = this.getNodeParameter('search', i, '') as string; + const offset = this.getNodeParameter('offset', i, 0) as number; + const count = this.getNodeParameter('count', i, 50) as number; + const includeCount = this.getNodeParameter('includeCount', i, true) as boolean; + + const response = await this.helpers.request({ + method: 'GET', + url: `https://api.tableflow.tech/record/search/${tableId}`, + qs: { + search: searchQuery, + offset, + count, + includecount: includeCount, + }, + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + }, + json: true, + }); + + // returnData.push({ json: response }); + const headers = response.Headers.map((h: any) => h.Name); + const rows = response.Rows.map((row: any) => { + const obj: any = {}; + headers.forEach((col: string, idx: number) => { + obj[col] = row.Values[idx]; + }); + return obj; + }); + + // πŸ”Ή Push each row as a separate item + for (const row of rows) { + returnData.push({ json: row }); + } + } + } catch (error: any) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message }, pairedItem: { item: i } }); + continue; + } + throw new NodeOperationError(this.getNode(), error, { itemIndex: i }); + } + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/TableFlow/icon-dark.svg b/packages/nodes-base/nodes/TableFlow/icon-dark.svg new file mode 100644 index 00000000000..6358b676c11 --- /dev/null +++ b/packages/nodes-base/nodes/TableFlow/icon-dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/TableFlow/icon.svg b/packages/nodes-base/nodes/TableFlow/icon.svg new file mode 100644 index 00000000000..d46392a074a --- /dev/null +++ b/packages/nodes-base/nodes/TableFlow/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b4358bd1af7..bb2d451dffe 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -353,6 +353,7 @@ "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", "dist/credentials/SyncroMspApi.credentials.js", "dist/credentials/SysdigApi.credentials.js", + "dist/credentials/TableFlowApi.credentials.js", "dist/credentials/TaigaApi.credentials.js", "dist/credentials/TapfiliateApi.credentials.js", "dist/credentials/TelegramApi.credentials.js", @@ -785,6 +786,7 @@ "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js", "dist/nodes/Switch/Switch.node.js", "dist/nodes/SyncroMSP/SyncroMsp.node.js", + "dist/nodes/TableFlow/TableFlow.node.js", "dist/nodes/Taiga/Taiga.node.js", "dist/nodes/Taiga/TaigaTrigger.node.js", "dist/nodes/Tapfiliate/Tapfiliate.node.js",