n8n/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts

381 lines
11 KiB
TypeScript
Raw Normal View History

import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IBinaryData,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import {
apiRequest,
apiRequestAllItems,
downloadRecordAttachments,
} from './GenericFunctions';
import {
operationFields
} from './OperationDescription';
export class NocoDB implements INodeType {
description: INodeTypeDescription = {
displayName: 'NocoDB',
name: 'nocoDb',
icon: 'file:nocodb.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Read, update, write and delete data from NocoDB',
defaults: {
name: 'NocoDB',
color: '#0989ff',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'nocoDb',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Row',
value: 'row',
},
],
default: 'row',
description: 'The Resource to operate on',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'row',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a row',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a row',
},
{
name: 'Get All',
value: 'getAll',
description: 'Retrieve all rows',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve a row',
},
{
name: 'Update',
value: 'update',
description: 'Update a row',
},
],
default: 'get',
description: 'The operation to perform',
},
...operationFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
const projectId = this.getNodeParameter('projectId', 0) as string;
const table = this.getNodeParameter('table', 0) as string;
let returnAll = false;
let endpoint = '';
let requestMethod = '';
let qs: IDataObject = {};
if (resource === 'row') {
if (operation === 'create') {
requestMethod = 'POST';
endpoint = `/nc/${projectId}/api/v1/${table}/bulk`;
const body: IDataObject[] = [];
for (let i = 0; i < items.length; i++) {
const newItem: IDataObject = {};
const dataToSend = this.getNodeParameter('dataToSend', i) as 'defineBelow' | 'autoMapInputData';
if (dataToSend === 'autoMapInputData') {
const incomingKeys = Object.keys(items[i].json);
const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string;
const inputDataToIgnore = rawInputsToIgnore.split(',').map(c => c.trim());
for (const key of incomingKeys) {
if (inputDataToIgnore.includes(key)) continue;
newItem[key] = items[i].json[key];
}
} else {
const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as Array<{
fieldName: string;
binaryData: boolean;
fieldValue?: string;
binaryProperty?: string;
}>;
for (const field of fields) {
if (!field.binaryData) {
newItem[field.fieldName] = field.fieldValue;
} else if (field.binaryProperty) {
if (!items[i].binary) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
}
const binaryPropertyName = field.binaryProperty;
if (binaryPropertyName && !items[i].binary![binaryPropertyName]) {
throw new NodeOperationError(this.getNode(), `Binary property ${binaryPropertyName} does not exist on item!`);
}
const binaryData = items[i].binary![binaryPropertyName] as IBinaryData;
const formData = {
file: {
value: Buffer.from(binaryData.data, BINARY_ENCODING),
options: {
filename: binaryData.fileName,
contentType: binaryData.mimeType,
},
},
json: JSON.stringify({
api: 'xcAttachmentUpload',
project_id: projectId,
dbAlias: 'db',
args: {},
}),
};
const qs = { project_id: projectId };
responseData = await apiRequest.call(this, 'POST', '/dashboard', {}, qs, undefined, { formData });
newItem[field.fieldName] = JSON.stringify([responseData]);
}
}
}
body.push(newItem);
}
try {
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
// Calculate ID manually and add to return data
let id = responseData[0];
for (let i = body.length - 1; i >= 0; i--) {
body[i].id = id--;
}
returnData.push(...body);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.toString() });
}
throw new NodeApiError(this.getNode(), error);
}
} else if (operation === 'delete') {
requestMethod = 'DELETE';
endpoint = `/nc/${projectId}/api/v1/${table}/bulk`;
const body: IDataObject[] = [];
for (let i = 0; i < items.length; i++) {
const id = this.getNodeParameter('id', i) as string;
body.push({ id });
}
try {
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
returnData.push(...items.map(item => item.json));
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.toString() });
}
throw new NodeApiError(this.getNode(), error);
}
} else if (operation === 'getAll') {
const data = [];
const downloadAttachments = this.getNodeParameter('downloadAttachments', 0) as boolean;
try {
for (let i = 0; i < items.length; i++) {
requestMethod = 'GET';
endpoint = `/nc/${projectId}/api/v1/${table}`;
returnAll = this.getNodeParameter('returnAll', 0) as boolean;
qs = this.getNodeParameter('options', i, {}) as IDataObject;
if (qs.sort) {
const properties = (qs.sort as IDataObject).property as Array<{ field: string, direction: string }>;
qs.sort = properties.map(prop => `${prop.direction === 'asc' ? '' : '-'}${prop.field}`).join(',');
}
if (qs.fields) {
qs.fields = (qs.fields as IDataObject[]).join(',');
}
if (returnAll === true) {
responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', 0) as number;
responseData = await apiRequest.call(this, requestMethod, endpoint, {}, qs);
}
returnData.push.apply(returnData, responseData);
if (downloadAttachments === true) {
const downloadFieldNames = (this.getNodeParameter('downloadFieldNames', 0) as string).split(',');
const response = await downloadRecordAttachments.call(this, responseData, downloadFieldNames);
data.push(...response);
}
}
if (downloadAttachments) {
return [data];
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.toString() });
}
throw error;
}
} else if (operation === 'get') {
requestMethod = 'GET';
const newItems: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
try {
const id = this.getNodeParameter('id', i) as string;
endpoint = `/nc/${projectId}/api/v1/${table}/${id}`;
responseData = await apiRequest.call(this, requestMethod, endpoint, {}, qs);
const newItem: INodeExecutionData = { json: responseData };
const downloadAttachments = this.getNodeParameter('downloadAttachments', i) as boolean;
if (downloadAttachments === true) {
const downloadFieldNames = (this.getNodeParameter('downloadFieldNames', i) as string).split(',');
const data = await downloadRecordAttachments.call(this, [responseData], downloadFieldNames);
newItem.binary = data[0].binary;
}
newItems.push(newItem);
} catch (error) {
if (this.continueOnFail()) {
newItems.push({ json: { error: error.toString() } });
continue;
}
throw new NodeApiError(this.getNode(), error);
}
}
return this.prepareOutputData(newItems);
} else if (operation === 'update') {
requestMethod = 'PUT';
endpoint = `/nc/${projectId}/api/v1/${table}/bulk`;
const body: IDataObject[] = [];
for (let i = 0; i < items.length; i++) {
const id = this.getNodeParameter('id', i) as string;
const newItem: IDataObject = { id };
const dataToSend = this.getNodeParameter('dataToSend', i) as 'defineBelow' | 'autoMapInputData';
if (dataToSend === 'autoMapInputData') {
const incomingKeys = Object.keys(items[i].json);
const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string;
const inputDataToIgnore = rawInputsToIgnore.split(',').map(c => c.trim());
for (const key of incomingKeys) {
if (inputDataToIgnore.includes(key)) continue;
newItem[key] = items[i].json[key];
}
} else {
const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as Array<{
fieldName: string;
upload: boolean;
fieldValue?: string;
binaryProperty?: string;
}>;
for (const field of fields) {
if (!field.upload) {
newItem[field.fieldName] = field.fieldValue;
} else if (field.binaryProperty) {
if (!items[i].binary) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
}
const binaryPropertyName = field.binaryProperty;
if (binaryPropertyName && !items[i].binary![binaryPropertyName]) {
throw new NodeOperationError(this.getNode(), `Binary property ${binaryPropertyName} does not exist on item!`);
}
const binaryData = items[i].binary![binaryPropertyName] as IBinaryData;
const formData = {
file: {
value: Buffer.from(binaryData.data, BINARY_ENCODING),
options: {
filename: binaryData.fileName,
contentType: binaryData.mimeType,
},
},
json: JSON.stringify({
api: 'xcAttachmentUpload',
project_id: projectId,
dbAlias: 'db',
args: {},
}),
};
const qs = { project_id: projectId };
responseData = await apiRequest.call(this, 'POST', '/dashboard', {}, qs, undefined, { formData });
newItem[field.fieldName] = JSON.stringify([responseData]);
}
}
}
body.push(newItem);
}
try {
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
returnData.push(...body);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.toString() });
}
throw new NodeApiError(this.getNode(), error);
}
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}