n8n-nodes-aitable-unofficial
Version:
n8n community node for integration with AITable
1,019 lines • 52.6 kB
JavaScript
"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