UNPKG

n8n-nodes-aitable-unofficial

Version:

n8n community node for integration with AITable

1,019 lines 52.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Aitable = void 0; const n8n_workflow_1 = require("n8n-workflow"); class Aitable { constructor() { this.description = { displayName: 'Aitable Unofficial', name: 'aitable', icon: 'file:aitable.svg', group: ['transform'], version: 1, description: 'Interact with Aitable.ai API (Unofficial Integration)', defaults: { name: 'Aitable Unofficial', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'aitableApi', required: true, }, ], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Create Record', value: 'createRecord', description: 'Create a new record in a datasheet with support for all field types including linked records', action: 'Create a new record in a datasheet with support for all field types including linked records', }, { name: 'Delete Record', value: 'deleteRecord', description: 'Delete an existing record from a datasheet', action: 'Delete an existing record from a datasheet', }, { name: 'Edit Record', value: 'editRecord', description: 'Update an existing record in a datasheet', action: 'Update an existing record in a datasheet', }, { name: 'Search Nodes', value: 'searchNodes', description: 'Search for nodes (datasheets, folders, etc.) in a space', action: 'Search for nodes datasheets folders etc in a space', }, { name: 'Search Records in Datasheet', value: 'searchNodesInDatasheet', description: 'Search and retrieve records from a specific datasheet (supports both simple search and formula filtering)', action: 'Search and retrieve records from a specific datasheet supports both simple search and formula filtering', }, ], default: 'searchNodes', }, { displayName: 'Space ID', name: 'spaceId', type: 'string', default: '', required: true, description: 'ID of the space (e.g., spcX9P2xUcKst)', }, { displayName: 'Node Type', name: 'nodeType', type: 'options', options: [ { name: 'Dashboard', value: 'Dashboard', }, { name: 'Datasheet', value: 'Datasheet', }, { name: 'Folder', value: 'Folder', }, { name: 'Form', value: 'Form', }, { name: 'Mirror', value: 'Mirror', }, ], default: 'Datasheet', description: 'Type of node to search for', displayOptions: { show: { operation: [ 'searchNodes', ], }, }, }, { displayName: 'Query', name: 'query', type: 'string', default: '', description: 'Search nodes by name - partial matches supported', displayOptions: { show: { operation: [ 'searchNodes', ], }, }, }, { displayName: 'Permissions', name: 'permissions', type: 'multiOptions', options: [ { name: 'Manager (0)', value: '0', description: 'Full management permissions', }, { name: 'Editor (1)', value: '1', description: 'Can edit but not manage', }, { name: 'Update-only (2)', value: '2', description: 'Can add and edit records but not delete', }, { name: 'Read-only (3)', value: '3', description: 'Can only view data', }, ], default: ['0', '1', '2', '3'], description: 'Filter nodes by permission levels', displayOptions: { show: { operation: [ 'searchNodes', ], }, }, }, { displayName: 'Datasheet ID', name: 'datasheetId', type: 'string', default: '', required: true, description: 'ID of the datasheet to search records in (e.g., dstXXXXXXXXXXX)', displayOptions: { show: { operation: [ 'searchNodesInDatasheet', ], }, }, placeholder: 'dstXXXXXXXXXXX', hint: 'You can find this in the URL when viewing a datasheet: https://aitable.ai/space/spcXXXX/datasheet/dst{DATASHEET_ID}/...' }, { displayName: 'Simple Search', name: 'searchTerm', type: 'string', default: '', description: 'Simple term to search for within the datasheet records (leave empty to retrieve all records)', displayOptions: { show: { operation: [ 'searchNodesInDatasheet', ], }, }, placeholder: 'Example: project name', }, { displayName: 'Filter By Formula', name: 'filterByFormula', type: 'string', default: '', description: 'Use Aitable formula syntax for advanced filtering (e.g., {Name}="Project X")', displayOptions: { show: { operation: [ 'searchNodesInDatasheet', ], }, }, placeholder: 'Example: {Name}="Project X" or OR(find("Keyword", {Description}) > 0)', }, { displayName: 'Columns to Search', name: 'columnsToSearch', type: 'string', default: '', description: 'Comma-separated list of column IDs to search within. Leave empty to search all columns.', displayOptions: { show: { operation: [ 'searchNodesInDatasheet', ], }, }, }, { displayName: 'Maximum Results', name: 'maxResults', type: 'number', default: 100, description: 'Maximum number of results to return (1-1000)', displayOptions: { show: { operation: [ 'searchNodesInDatasheet', ], }, }, }, { displayName: 'Datasheet ID', name: 'datasheetId', type: 'string', default: '', required: true, description: 'ID of the datasheet to create a record in (e.g., dstXXXXXXXXXXX)', displayOptions: { show: { operation: [ 'createRecord', ], }, }, placeholder: 'dstXXXXXXXXXXX', hint: 'You can find this in the URL when viewing a datasheet: https://aitable.ai/space/spcXXXX/datasheet/dst{DATASHEET_ID}/...' }, { displayName: 'Use Field Names', name: 'useFieldNames', type: 'boolean', default: true, description: 'Whether to use field names instead of field IDs when creating a record (recommended)', displayOptions: { show: { operation: [ 'createRecord', ], }, }, }, { displayName: 'Fields', name: 'fieldsUi', placeholder: 'Add Field', type: 'fixedCollection', typeOptions: { multipleValues: true, }, displayOptions: { show: { operation: [ 'createRecord', ], }, }, options: [ { name: 'fieldValues', displayName: 'Field', values: [ { displayName: 'Field Name or ID', name: 'fieldName', type: 'string', default: '', description: 'The name or ID of the field (e.g., "Title" or fldXXXXXXXXXXX)', placeholder: 'Title', }, { displayName: 'Field Type', name: 'fieldType', type: 'options', options: [ { name: 'Checkbox', value: 'checkbox', }, { name: 'Date', value: 'date', }, { name: 'Link (One-Way)', value: 'link', }, { name: 'Link (Two-Way)', value: 'twoWayLink', }, { name: 'MultiSelect', value: 'multiSelect', }, { name: 'Number', value: 'number', }, { name: 'Select', value: 'select', }, { name: 'Text', value: 'text', }, ], default: 'text', description: 'The type of the field', }, { displayName: 'Field Value', name: 'fieldValue', type: 'string', default: '', description: 'The value to set for the field. For linked fields, enter the record ID(s) separated by commas. To explicitly clear a linked field, type "null" (this will set an actual null value in the API call). Empty values will be ignored for security reasons.', }, ], }, ], default: {}, description: 'Fields to set on the record', }, { displayName: 'Fetch Fields', name: 'fetchFields', type: 'boolean', default: false, description: 'Whether to fetch the datasheet fields first to help with record creation', displayOptions: { show: { operation: [ 'createRecord', ], }, }, }, { displayName: 'Datasheet ID', name: 'datasheetId', type: 'string', default: '', required: true, description: 'ID of the datasheet containing the record to edit (e.g., dstXXXXXXXXXXX)', displayOptions: { show: { operation: [ 'editRecord', ], }, }, placeholder: 'dstXXXXXXXXXXX', hint: 'You can find this in the URL when viewing a datasheet: https://aitable.ai/space/spcXXXX/datasheet/dst{DATASHEET_ID}/...' }, { displayName: 'Record ID', name: 'recordId', type: 'string', default: '', required: true, description: 'ID of the record to edit (e.g., recXXXXXXXXXXX)', displayOptions: { show: { operation: [ 'editRecord', ], }, }, placeholder: 'recXXXXXXXXXXX', }, { displayName: 'Use Field Names', name: 'useFieldNames', type: 'boolean', default: true, description: 'Whether to use field names instead of field IDs when updating a record (recommended)', displayOptions: { show: { operation: [ 'editRecord', ], }, }, }, { displayName: 'Fields', name: 'fieldsUi', placeholder: 'Add Field', type: 'fixedCollection', typeOptions: { multipleValues: true, }, displayOptions: { show: { operation: [ 'editRecord', ], }, }, options: [ { name: 'fieldValues', displayName: 'Field', values: [ { displayName: 'Field Name or ID', name: 'fieldName', type: 'string', default: '', description: 'The name or ID of the field (e.g., "Title" or fldXXXXXXXXXXX)', placeholder: 'Title', }, { displayName: 'Field Type', name: 'fieldType', type: 'options', options: [ { name: 'Checkbox', value: 'checkbox', }, { name: 'Date', value: 'date', }, { name: 'Link (One-Way)', value: 'link', }, { name: 'Link (Two-Way)', value: 'twoWayLink', }, { name: 'MultiSelect', value: 'multiSelect', }, { name: 'Number', value: 'number', }, { name: 'Select', value: 'select', }, { name: 'Text', value: 'text', }, ], default: 'text', description: 'The type of the field', }, { displayName: 'Field Value', name: 'fieldValue', type: 'string', default: '', description: 'The value to set for the field. For linked fields, enter the record ID(s) separated by commas. To explicitly clear a linked field, type "null" (this will set an actual null value in the API call). Empty values will be ignored for security reasons.', }, ], }, ], default: {}, description: 'Fields to update on the record', }, { displayName: 'Fetch Fields', name: 'fetchFields', type: 'boolean', default: false, description: 'Whether to fetch the datasheet fields first to help with record updates', displayOptions: { show: { operation: [ 'editRecord', ], }, }, }, { displayName: 'Datasheet ID', name: 'datasheetId', type: 'string', default: '', required: true, description: 'ID of the datasheet to delete the record from (e.g., dstXXXXXXXXXXXX)', displayOptions: { show: { operation: [ 'deleteRecord', ], }, }, }, { displayName: 'Record ID', name: 'recordId', type: 'string', default: '', required: true, description: 'ID of the record to delete (e.g., recXXXXXXXXXXXX)', displayOptions: { show: { operation: [ 'deleteRecord', ], }, }, }, ], }; } async execute() { var _a; const items = this.getInputData(); const returnData = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { try { const operation = this.getNodeParameter('operation', itemIndex); const spaceId = this.getNodeParameter('spaceId', itemIndex); if (operation === 'searchNodes') { const nodeType = this.getNodeParameter('nodeType', itemIndex); const query = this.getNodeParameter('query', itemIndex, ''); const permissionsArray = this.getNodeParameter('permissions', itemIndex, []); let permissionsParam = ''; if (permissionsArray.length > 0) { permissionsParam = permissionsArray.join(','); } const queryParams = { type: nodeType, }; if (query) { queryParams.query = query; } if (permissionsParam) { queryParams.permissions = permissionsParam; } const endpoint = `/fusion/v1/spaces/${spaceId}/nodes`; const response = await this.helpers.httpRequestWithAuthentication.call(this, 'aitableApi', { method: 'GET', url: endpoint, baseURL: 'https://aitable.ai', qs: queryParams, headers: { 'Content-Type': 'application/json', }, }); if (response.success === true && response.data && response.data.nodes) { const responseData = response.data.nodes; for (const node of responseData) { returnData.push({ json: node, pairedItem: itemIndex, }); } } else { returnData.push({ json: { success: false, message: 'No data returned or error occurred', response, }, pairedItem: itemIndex, }); } } else if (operation === 'searchNodesInDatasheet') { const datasheetId = this.getNodeParameter('datasheetId', itemIndex); if (!datasheetId) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Datasheet ID is required', { itemIndex }); } const searchTerm = this.getNodeParameter('searchTerm', itemIndex); const filterByFormula = this.getNodeParameter('filterByFormula', itemIndex); const columnsToSearch = this.getNodeParameter('columnsToSearch', itemIndex, ''); const maxResults = this.getNodeParameter('maxResults', itemIndex, 100); try { const recordsEndpoint = `/fusion/v1/datasheets/${datasheetId}/records`; const recordsParams = {}; if (columnsToSearch) { recordsParams.fieldIds = columnsToSearch; } if (maxResults) { recordsParams.pageSize = maxResults; } if (filterByFormula) { recordsParams.filterByFormula = filterByFormula; } const recordsResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'aitableApi', { method: 'GET', url: recordsEndpoint, baseURL: 'https://aitable.ai', qs: recordsParams, headers: { 'Content-Type': 'application/json', }, }); if (recordsResponse.success === true && recordsResponse.data && recordsResponse.data.records) { let records = recordsResponse.data.records; if (searchTerm) { records = records.filter((record) => { for (const fieldKey in record.fields) { const fieldValue = record.fields[fieldKey]; if (typeof fieldValue === 'string' && fieldValue.toLowerCase().includes(searchTerm.toLowerCase())) { return true; } } return false; }); } for (const record of records) { returnData.push({ json: { ...record, datasheetId, }, pairedItem: itemIndex, }); } } else { returnData.push({ json: { success: false, message: 'No records found in datasheet or error occurred', response: recordsResponse, }, pairedItem: itemIndex, }); } } catch (error) { this.logger.error('Error fetching records: ' + error.message); if (this.continueOnFail()) { returnData.push({ json: { success: false, message: `Error fetching records: ${error.message}`, datasheetId, }, pairedItem: itemIndex, }); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error fetching records from datasheet ${datasheetId}: ${error.message}`, { itemIndex }); } } } else if (operation === 'createRecord') { const datasheetId = this.getNodeParameter('datasheetId', itemIndex); if (!datasheetId) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Datasheet ID is required', { itemIndex }); } const useFieldNames = this.getNodeParameter('useFieldNames', itemIndex, true); const fetchFields = this.getNodeParameter('fetchFields', itemIndex, false); const fieldsUi = this.getNodeParameter('fieldsUi', itemIndex, {}); const fields = {}; let fieldMetadata = {}; let nameToIdMap = {}; if (fetchFields) { try { const metadataEndpoint = `/fusion/v1/datasheets/${datasheetId}/fields`; this.logger.info('Fetching field metadata from Aitable API'); const metadataResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'aitableApi', { method: 'GET', url: metadataEndpoint, baseURL: 'https://aitable.ai', headers: { 'Content-Type': 'application/json', }, }); if (metadataResponse.success === true && metadataResponse.data && metadataResponse.data.fields) { fieldMetadata = metadataResponse.data.fields.reduce((acc, field) => { acc[field.id] = field; return acc; }, {}); nameToIdMap = metadataResponse.data.fields.reduce((acc, field) => { acc[field.name.toLowerCase()] = field.id; return acc; }, {}); this.logger.info(`Successfully fetched metadata for ${Object.keys(fieldMetadata).length} fields`); } } catch (error) { this.logger.error('Error fetching field metadata: ' + error.message); throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to fetch field metadata: ${error.message}. Disable "Fetch Fields" or check your connection to Aitable.`, { itemIndex }); } } if (fieldsUi.fieldValues && fieldsUi.fieldValues.length > 0) { for (const field of fieldsUi.fieldValues) { const { fieldName, fieldType, fieldValue } = field; if (!fieldName) { continue; } let fieldKey = fieldName; if (!useFieldNames || (fetchFields && nameToIdMap[fieldName.toLowerCase()])) { if (fetchFields && nameToIdMap[fieldName.toLowerCase()]) { fieldKey = nameToIdMap[fieldName.toLowerCase()]; } } let processedValue; switch (fieldType) { case 'text': processedValue = fieldValue; break; case 'number': processedValue = fieldValue === '' ? null : Number(fieldValue); break; case 'checkbox': processedValue = fieldValue.toLowerCase() === 'true'; break; case 'select': processedValue = fieldValue; break; case 'multiSelect': processedValue = fieldValue.split(',').map(option => option.trim()).filter(Boolean); break; case 'date': if (/^\d+$/.test(fieldValue)) { processedValue = Number(fieldValue); } else { processedValue = fieldValue; } break; case 'link': case 'twoWayLink': if (fieldValue.toLowerCase() === 'null') { processedValue = null; } else if (fieldValue === '') { continue; } else { processedValue = fieldValue.split(',').map(id => id.trim()).filter(Boolean); } break; default: processedValue = fieldValue; } fields[fieldKey] = processedValue; } } try { const recordsEndpoint = `/fusion/v1/datasheets/${datasheetId}/records`; const recordsData = { records: [ { fields, }, ], }; if (useFieldNames) { recordsData.fieldKey = 'name'; } const recordsResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'aitableApi', { method: 'POST', url: recordsEndpoint, baseURL: 'https://aitable.ai', headers: { 'Content-Type': 'application/json', }, body: recordsData, }); if (recordsResponse.success === true && recordsResponse.data && recordsResponse.data.records) { const createdRecord = recordsResponse.data.records[0]; returnData.push({ json: { ...createdRecord, datasheetId, }, pairedItem: itemIndex, }); } else { returnData.push({ json: { success: false, message: 'Record creation failed or error occurred', response: recordsResponse, }, pairedItem: itemIndex, }); } } catch (error) { this.logger.error('Error creating record: ' + error.message); if (this.continueOnFail()) { returnData.push({ json: { success: false, message: `Error creating record: ${error.message}`, datasheetId, }, pairedItem: itemIndex, }); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error creating record in datasheet ${datasheetId}: ${error.message}`, { itemIndex }); } } } else if (operation === 'editRecord') { const datasheetId = this.getNodeParameter('datasheetId', itemIndex); const recordId = this.getNodeParameter('recordId', itemIndex); const useFieldNames = this.getNodeParameter('useFieldNames', itemIndex, true); const fetchFields = this.getNodeParameter('fetchFields', itemIndex, false); const fieldsUi = this.getNodeParameter('fieldsUi', itemIndex, {}); const fields = {}; let fieldMetadata = {}; let nameToIdMap = {}; if (fetchFields) { try { const metadataEndpoint = `/fusion/v1/datasheets/${datasheetId}/fields`; this.logger.info('Fetching field metadata from Aitable API'); const metadataResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'aitableApi', { method: 'GET', url: metadataEndpoint, baseURL: 'https://aitable.ai', headers: { 'Content-Type': 'application/json', }, }); if (metadataResponse.success === true && metadataResponse.data && metadataResponse.data.fields) { fieldMetadata = metadataResponse.data.fields.reduce((acc, field) => { acc[field.id] = field; return acc; }, {}); nameToIdMap = metadataResponse.data.fields.reduce((acc, field) => { acc[field.name.toLowerCase()] = field.id; return acc; }, {}); this.logger.info(`Successfully fetched metadata for ${Object.keys(fieldMetadata).length} fields`); } } catch (error) { this.logger.error('Error fetching field metadata: ' + error.message); throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to fetch field metadata: ${error.message}. Disable "Fetch Fields" or check your connection to Aitable.`, { itemIndex }); } } if (fieldsUi.fieldValues && fieldsUi.fieldValues.length > 0) { for (const field of fieldsUi.fieldValues) { const { fieldName, fieldType, fieldValue } = field; if (!fieldName) { continue; } let fieldKey = fieldName; if (!useFieldNames || (fetchFields && nameToIdMap[fieldName.toLowerCase()])) { if (fetchFields && nameToIdMap[fieldName.toLowerCase()]) { fieldKey = nameToIdMap[fieldName.toLowerCase()]; } } let processedValue; switch (fieldType) { case 'text': processedValue = fieldValue; break; case 'number': processedValue = fieldValue === '' ? null : Number(fieldValue); break; case 'checkbox': processedValue = fieldValue.toLowerCase() === 'true'; break; case 'select': processedValue = fieldValue; break; case 'multiSelect': processedValue = fieldValue.split(',').map(option => option.trim()).filter(Boolean); break; case 'date': if (/^\d+$/.test(fieldValue)) { processedValue = Number(fieldValue); } else { processedValue = fieldValue; } break; case 'link': case 'twoWayLink': if (fieldValue.toLowerCase() === 'null') { processedValue = null; } else if (fieldValue === '') { continue; } else { processedValue = fieldValue.split(',').map(id => id.trim()).filter(Boolean); } break; default: processedValue = fieldValue; } fields[fieldKey] = processedValue; } } try { const recordsEndpoint = `/fusion/v1/datasheets/${datasheetId}/records`; const recordsData = { records: [ { recordId, fields, }, ], }; if (useFieldNames) { recordsData.fieldKey = 'name'; } const recordsResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'aitableApi', { method: 'PATCH', url: recordsEndpoint, baseURL: 'https://aitable.ai', headers: { 'Content-Type': 'application/json', }, body: recordsData, }); if (recordsResponse.success === true && recordsResponse.data && recordsResponse.data.records) { const updatedRecord = recordsResponse.data.records[0]; returnData.push({ json: { ...updatedRecord, datasheetId, }, pairedItem: itemIndex, }); } else { returnData.push({ json: { success: false, message: 'Record update failed or error occurred', response: recordsResponse, }, pairedItem: itemIndex, }); } } catch (error) { this.logger.error('Error updating record: ' + error.message); if (this.continueOnFail()) { returnData.push({ json: { success: false, message: `Error updating record: ${error.message}`, datasheetId, }, pairedItem: itemIndex, }); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error updating record in datasheet ${datasheetId}: ${error.message}`, { itemIndex }); } } } else if (operation === 'deleteRecord') { const datasheetId = this.getNodeParameter('datasheetId', itemIndex); const recordId = this.getNodeParameter('recordId', itemIndex); if (!datasheetId) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Datasheet ID is required', { itemIndex }); } if (!recordId) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Record ID is required', { itemIndex }); } try { const recordsEndpoint = `/fusion/v1/datasheets/${datasheetId}/records`; const recordsResponse = await this.helpers.httpRequestWithAuthentication.call(this, 'aitableApi', { method: 'DELETE', url: recordsEndpoint, baseURL: 'https://aitable.ai', headers: { 'Content-Type': 'application/json', }, qs: { recordIds: recordId, }, }); if (recordsResponse.success === true) { returnData.push({ json: { success: true, message: 'Record deleted successfully', recordId, datasheetId, response: recordsResponse, }, pairedItem: itemIndex, }); } else { returnData.push({ json: { success: false, message: 'Record deletion failed or