From da2d2a83bbfb05db3a10aef99bfde3ccaf160d60 Mon Sep 17 00:00:00 2001 From: mertmit Date: Wed, 8 Nov 2023 13:04:35 +0300 Subject: [PATCH] feat(NocoDB Node): Add new data apis and workspace support (#7329) - Changed old NocoDB logo with new one To support new release of [NocoDB (v0.202.4)](https://github.com/nocodb/nocodb/releases/tag/0.202.4): - Added a new version(3) to support new data apis and workspaces - Renamed `project` to `base` for new version (kept labels for old versions for backwards compatibility) --------- Signed-off-by: mertmit Co-authored-by: Jonathan Bennetts --- .../nodes-base/nodes/NocoDB/NocoDB.node.ts | 216 ++++++--- .../nodes/NocoDB/OperationDescription.ts | 150 +++++- packages/nodes-base/nodes/NocoDB/nocodb.svg | 427 +----------------- 3 files changed, 296 insertions(+), 497 deletions(-) diff --git a/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts b/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts index b1eddae550..fd010508f0 100644 --- a/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts +++ b/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts @@ -20,7 +20,7 @@ export class NocoDB implements INodeType { name: 'nocoDb', icon: 'file:nocodb.svg', group: ['input'], - version: [1, 2], + version: [1, 2, 3], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Read, update, write and delete data from NocoDB', defaults: { @@ -54,14 +54,14 @@ export class NocoDB implements INodeType { name: 'authentication', type: 'options', options: [ - { - name: 'User Token', - value: 'nocoDb', - }, { name: 'API Token', value: 'nocoDbApiToken', }, + { + name: 'User Token', + value: 'nocoDb', + }, ], default: 'nocoDb', }, @@ -69,11 +69,6 @@ export class NocoDB implements INodeType { displayName: 'API Version', name: 'version', type: 'options', - displayOptions: { - show: { - '@version': [1], - }, - }, isNodeSetting: true, options: [ { @@ -84,18 +79,22 @@ export class NocoDB implements INodeType { name: 'v0.90.0 Onwards', value: 2, }, + { + name: 'v0.200.0 Onwards', + value: 3, + }, ], + displayOptions: { + show: { + '@version': [1], + }, + }, default: 1, }, { displayName: 'API Version', name: 'version', type: 'options', - displayOptions: { - show: { - '@version': [2], - }, - }, isNodeSetting: true, options: [ { @@ -106,9 +105,44 @@ export class NocoDB implements INodeType { name: 'v0.90.0 Onwards', value: 2, }, + { + name: 'v0.200.0 Onwards', + value: 3, + }, ], + displayOptions: { + show: { + '@version': [2], + }, + }, default: 2, }, + { + displayName: 'API Version', + name: 'version', + type: 'options', + isNodeSetting: true, + options: [ + { + name: 'Before v0.90.0', + value: 1, + }, + { + name: 'v0.90.0 Onwards', + value: 2, + }, + { + name: 'v0.200.0 Onwards', + value: 3, + }, + ], + displayOptions: { + show: { + '@version': [3], + }, + }, + default: 3, + }, { displayName: 'Resource', name: 'resource', @@ -172,26 +206,51 @@ export class NocoDB implements INodeType { methods = { loadOptions: { - async getProjects(this: ILoadOptionsFunctions) { + async getWorkspaces(this: ILoadOptionsFunctions) { try { const requestMethod = 'GET'; - const endpoint = '/api/v1/db/meta/projects/'; + const endpoint = '/api/v1/workspaces/'; const responseData = await apiRequest.call(this, requestMethod, endpoint, {}, {}); return responseData.list.map((i: IDataObject) => ({ name: i.title, value: i.id })); + } catch (e) { + return [{ name: 'No Workspace', value: 'none' }]; + } + }, + async getBases(this: ILoadOptionsFunctions) { + const version = this.getNodeParameter('version', 0) as number; + const workspaceId = this.getNodeParameter('workspaceId', 0) as string; + try { + if (workspaceId && workspaceId !== 'none') { + const requestMethod = 'GET'; + const endpoint = `/api/v1/workspaces/${workspaceId}/bases/`; + const responseData = await apiRequest.call(this, requestMethod, endpoint, {}, {}); + return responseData.list.map((i: IDataObject) => ({ name: i.title, value: i.id })); + } else { + const requestMethod = 'GET'; + const endpoint = version === 3 ? '/api/v2/meta/bases/' : '/api/v1/db/meta/projects/'; + const responseData = await apiRequest.call(this, requestMethod, endpoint, {}, {}); + return responseData.list.map((i: IDataObject) => ({ name: i.title, value: i.id })); + } } catch (e) { throw new NodeOperationError( this.getNode(), - new Error('Error while fetching projects!', { cause: e }), + new Error(`Error while fetching ${version === 3 ? 'bases' : 'projects'}!`, { + cause: e, + }), ); } }, - // This only supports using the Project ID + // This only supports using the Base ID async getTables(this: ILoadOptionsFunctions) { - const projectId = this.getNodeParameter('projectId', 0) as string; - if (projectId) { + const version = this.getNodeParameter('version', 0) as number; + const baseId = this.getNodeParameter('projectId', 0) as string; + if (baseId) { try { const requestMethod = 'GET'; - const endpoint = `/api/v1/db/meta/projects/${projectId}/tables`; + const endpoint = + version === 3 + ? `/api/v2/meta/bases/${baseId}/tables` + : `/api/v1/db/meta/projects/${baseId}/tables`; const responseData = await apiRequest.call(this, requestMethod, endpoint, {}, {}); return responseData.list.map((i: IDataObject) => ({ name: i.title, value: i.id })); } catch (e) { @@ -201,7 +260,10 @@ export class NocoDB implements INodeType { ); } } else { - throw new NodeOperationError(this.getNode(), 'No project selected!'); + throw new NodeOperationError( + this.getNode(), + `No ${version === 3 ? 'base' : 'project'} selected!`, + ); } }, }, @@ -223,7 +285,7 @@ export class NocoDB implements INodeType { let endPoint = ''; - const projectId = this.getNodeParameter('projectId', 0) as string; + const baseId = this.getNodeParameter('projectId', 0) as string; const table = this.getNodeParameter('table', 0) as string; if (resource === 'row') { @@ -231,9 +293,11 @@ export class NocoDB implements INodeType { requestMethod = 'POST'; if (version === 1) { - endPoint = `/nc/${projectId}/api/v1/${table}/bulk`; + endPoint = `/nc/${baseId}/api/v1/${table}/bulk`; } else if (version === 2) { - endPoint = `/api/v1/db/data/bulk/noco/${projectId}/${table}`; + endPoint = `/api/v1/db/data/bulk/noco/${baseId}/${table}`; + } else if (version === 3) { + endPoint = `/api/v2/tables/${table}/records`; } const body: IDataObject[] = []; @@ -278,7 +342,7 @@ export class NocoDB implements INodeType { }, json: JSON.stringify({ api: 'xcAttachmentUpload', - project_id: projectId, + project_id: baseId, dbAlias: 'db', args: {}, }), @@ -289,6 +353,8 @@ export class NocoDB implements INodeType { postUrl = '/dashboard'; } else if (version === 2) { postUrl = '/api/v1/db/storage/upload'; + } else if (version === 3) { + postUrl = '/api/v2/storage/upload'; } responseData = await apiRequest.call( @@ -296,13 +362,15 @@ export class NocoDB implements INodeType { 'POST', postUrl, {}, - { project_id: projectId }, + version === 3 ? { base_id: baseId } : { project_id: baseId }, undefined, { formData, }, ); - newItem[field.fieldName] = JSON.stringify([responseData]); + newItem[field.fieldName] = JSON.stringify( + Array.isArray(responseData) ? responseData : [responseData], + ); } } } @@ -311,13 +379,21 @@ export class NocoDB implements INodeType { 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--; - } + if (version === 3) { + for (let i = body.length - 1; i >= 0; i--) { + body[i] = { ...body[i], ...responseData[i] }; + } - returnData.push(...body); + returnData.push(...body); + } else { + // 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() }); @@ -331,9 +407,16 @@ export class NocoDB implements INodeType { let primaryKey = 'id'; if (version === 1) { - endPoint = `/nc/${projectId}/api/v1/${table}/bulk`; + endPoint = `/nc/${baseId}/api/v1/${table}/bulk`; } else if (version === 2) { - endPoint = `/api/v1/db/data/bulk/noco/${projectId}/${table}`; + endPoint = `/api/v1/db/data/bulk/noco/${baseId}/${table}`; + + primaryKey = this.getNodeParameter('primaryKey', 0) as string; + if (primaryKey === 'custom') { + primaryKey = this.getNodeParameter('customPrimaryKey', 0) as string; + } + } else if (version === 3) { + endPoint = `/api/v2/tables/${table}/records`; primaryKey = this.getNodeParameter('primaryKey', 0) as string; if (primaryKey === 'custom') { @@ -349,13 +432,7 @@ export class NocoDB implements INodeType { } try { - responseData = (await apiRequest.call( - this, - requestMethod, - endPoint, - body, - qs, - )) as number[]; + responseData = (await apiRequest.call(this, requestMethod, endPoint, body, qs)) as any[]; if (version === 1) { returnData.push(...items.map((item) => item.json)); } else if (version === 2) { @@ -377,6 +454,8 @@ export class NocoDB implements INodeType { }; }), ); + } else if (version === 3) { + returnData.push(...responseData); } } catch (error) { if (this.continueOnFail()) { @@ -394,9 +473,11 @@ export class NocoDB implements INodeType { requestMethod = 'GET'; if (version === 1) { - endPoint = `/nc/${projectId}/api/v1/${table}`; + endPoint = `/nc/${baseId}/api/v1/${table}`; } else if (version === 2) { - endPoint = `/api/v1/db/data/noco/${projectId}/${table}`; + endPoint = `/api/v1/db/data/noco/${baseId}/${table}`; + } else if (version === 3) { + endPoint = `/api/v2/tables/${table}/records`; } returnAll = this.getNodeParameter('returnAll', 0); @@ -421,7 +502,7 @@ export class NocoDB implements INodeType { } else { qs.limit = this.getNodeParameter('limit', 0); responseData = await apiRequest.call(this, requestMethod, endPoint, {}, qs); - if (version === 2) { + if (version === 2 || version === 3) { responseData = responseData.list; } } @@ -468,9 +549,11 @@ export class NocoDB implements INodeType { const id = this.getNodeParameter('id', i) as string; if (version === 1) { - endPoint = `/nc/${projectId}/api/v1/${table}/${id}`; + endPoint = `/nc/${baseId}/api/v1/${table}/${id}`; } else if (version === 2) { - endPoint = `/api/v1/db/data/noco/${projectId}/${table}/${id}`; + endPoint = `/api/v1/db/data/noco/${baseId}/${table}/${id}`; + } else if (version === 3) { + endPoint = `/api/v2/tables/${table}/records/${id}`; } responseData = await apiRequest.call(this, requestMethod, endPoint, {}, qs); @@ -542,22 +625,24 @@ export class NocoDB implements INodeType { let primaryKey = 'id'; if (version === 1) { - endPoint = `/nc/${projectId}/api/v1/${table}/bulk`; + endPoint = `/nc/${baseId}/api/v1/${table}/bulk`; requestMethod = 'PUT'; } else if (version === 2) { - endPoint = `/api/v1/db/data/bulk/noco/${projectId}/${table}`; + endPoint = `/api/v1/db/data/bulk/noco/${baseId}/${table}`; primaryKey = this.getNodeParameter('primaryKey', 0) as string; if (primaryKey === 'custom') { primaryKey = this.getNodeParameter('customPrimaryKey', 0) as string; } + } else if (version === 3) { + endPoint = `/api/v2/tables/${table}/records`; } const body: IDataObject[] = []; for (let i = 0; i < items.length; i++) { - const id = this.getNodeParameter('id', i) as string; - const newItem: IDataObject = { [primaryKey]: id }; + const id = version === 3 ? null : (this.getNodeParameter('id', i) as string); + const newItem: IDataObject = version === 3 ? {} : { [primaryKey]: id }; const dataToSend = this.getNodeParameter('dataToSend', i) as | 'defineBelow' | 'autoMapInputData'; @@ -596,7 +681,7 @@ export class NocoDB implements INodeType { }, json: JSON.stringify({ api: 'xcAttachmentUpload', - project_id: projectId, + project_id: baseId, dbAlias: 'db', args: {}, }), @@ -606,19 +691,24 @@ export class NocoDB implements INodeType { postUrl = '/dashboard'; } else if (version === 2) { postUrl = '/api/v1/db/storage/upload'; + } else if (version === 3) { + postUrl = '/api/v2/storage/upload'; } + responseData = await apiRequest.call( this, 'POST', postUrl, {}, - { project_id: projectId }, + version === 3 ? { base_id: baseId } : { project_id: baseId }, undefined, { formData, }, ); - newItem[field.fieldName] = JSON.stringify([responseData]); + newItem[field.fieldName] = JSON.stringify( + Array.isArray(responseData) ? responseData : [responseData], + ); } } } @@ -626,13 +716,7 @@ export class NocoDB implements INodeType { } try { - responseData = (await apiRequest.call( - this, - requestMethod, - endPoint, - body, - qs, - )) as number[]; + responseData = (await apiRequest.call(this, requestMethod, endPoint, body, qs)) as any[]; if (version === 1) { returnData.push(...body); @@ -655,6 +739,12 @@ export class NocoDB implements INodeType { }; }), ); + } else if (version === 3) { + for (let i = body.length - 1; i >= 0; i--) { + body[i] = { ...body[i], ...responseData[i] }; + } + + returnData.push(...body); } } catch (error) { if (this.continueOnFail()) { diff --git a/packages/nodes-base/nodes/NocoDB/OperationDescription.ts b/packages/nodes-base/nodes/NocoDB/OperationDescription.ts index be2be114ed..aaf43206c8 100644 --- a/packages/nodes-base/nodes/NocoDB/OperationDescription.ts +++ b/packages/nodes-base/nodes/NocoDB/OperationDescription.ts @@ -4,6 +4,40 @@ export const operationFields: INodeProperties[] = [ // ---------------------------------- // Shared // ---------------------------------- + { + displayName: 'Workspace Name or ID', + name: 'workspaceId', + type: 'options', + default: 'none', + displayOptions: { + show: { + version: [3], + }, + }, + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + }, + { + displayName: 'Base Name or ID', + name: 'projectId', + type: 'options', + default: '', + displayOptions: { + show: { + version: [3], + }, + }, + required: true, + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsDependsOn: ['workspaceId'], + loadOptionsMethod: 'getBases', + }, + }, { displayName: 'Project ID', name: 'projectId', @@ -31,7 +65,7 @@ export const operationFields: INodeProperties[] = [ description: 'Choose from the list, or specify an ID using an expression', typeOptions: { - loadOptionsMethod: 'getProjects', + loadOptionsMethod: 'getBases', }, }, { @@ -41,7 +75,7 @@ export const operationFields: INodeProperties[] = [ default: '', displayOptions: { show: { - version: [2], + version: [2, 3], }, }, required: true, @@ -91,10 +125,42 @@ export const operationFields: INodeProperties[] = [ ], displayOptions: { show: { + version: [1, 2], operation: ['delete', 'update'], }, }, }, + { + displayName: 'Primary Key Type', + name: 'primaryKey', + type: 'options', + default: 'id', + options: [ + { + name: 'Default', + value: 'id', + description: + 'Default, added when table was created from UI by those options: Create new table / Import from Excel / Import from CSV', + }, + { + name: 'Imported From Airtable', + value: 'ncRecordId', + description: 'Select if table was imported from Airtable', + }, + { + name: 'Custom', + value: 'custom', + description: + 'When connecting to existing external database as existing primary key field is retained as is, enter the name of the primary key field below', + }, + ], + displayOptions: { + show: { + version: [3], + operation: ['delete'], + }, + }, + }, { displayName: 'Field Name', name: 'customPrimaryKey', @@ -102,11 +168,25 @@ export const operationFields: INodeProperties[] = [ default: '', displayOptions: { show: { + version: [1, 2], operation: ['delete', 'update'], primaryKey: ['custom'], }, }, }, + { + displayName: 'Field Name', + name: 'customPrimaryKey', + type: 'string', + default: '', + displayOptions: { + show: { + version: [3], + operation: ['delete'], + primaryKey: ['custom'], + }, + }, + }, { displayName: 'Row ID Value', name: 'id', @@ -116,10 +196,25 @@ export const operationFields: INodeProperties[] = [ description: 'The value of the ID field', displayOptions: { show: { + version: [1, 2], operation: ['delete', 'get', 'update'], }, }, }, + { + displayName: 'Row ID Value', + name: 'id', + type: 'string', + default: '', + required: true, + description: 'The value of the ID field', + displayOptions: { + show: { + version: [3], + operation: ['delete', 'get'], + }, + }, + }, // ---------------------------------- // delete // ---------------------------------- @@ -195,6 +290,17 @@ export const operationFields: INodeProperties[] = [ default: {}, placeholder: 'Add Option', options: [ + { + displayName: 'View ID', + name: 'viewId', + type: 'string', + typeOptions: { + multipleValues: false, + }, + default: '', + placeholder: 'View ID', + description: 'The select fields of the returned rows', + }, { displayName: 'Fields', name: 'fields', @@ -207,14 +313,6 @@ export const operationFields: INodeProperties[] = [ placeholder: 'Name', description: 'The select fields of the returned rows', }, - { - displayName: 'Filter By Formula', - name: 'where', - type: 'string', - default: '', - placeholder: '(name,like,example%)~or(name,eq,test)', - description: 'A formula used to filter rows', - }, { displayName: 'Sort', name: 'sort', @@ -260,6 +358,14 @@ export const operationFields: INodeProperties[] = [ }, ], }, + { + displayName: 'Filter By Formula', + name: 'where', + type: 'string', + default: '', + placeholder: '(name,like,example%)~or(name,eq,test)', + description: 'A formula used to filter rows', + }, ], }, // ---------------------------------- @@ -322,6 +428,30 @@ export const operationFields: INodeProperties[] = [ default: 'defineBelow', description: 'Whether to insert the input data this node receives in the new row', }, + { + displayName: + "In this mode, make sure the incoming data fields are named the same as the columns in NocoDB. (Use a 'set' node before this node to change them if required.)", + name: 'info', + type: 'notice', + default: '', + displayOptions: { + show: { + dataToSend: ['autoMapInputData'], + }, + }, + }, + { + displayName: 'This operation requires the primary key to be included for each row.', + name: 'info', + type: 'notice', + default: '', + displayOptions: { + show: { + operation: ['update'], + version: [3], + }, + }, + }, { displayName: 'Inputs to Ignore', name: 'inputsToIgnore', diff --git a/packages/nodes-base/nodes/NocoDB/nocodb.svg b/packages/nodes-base/nodes/NocoDB/nocodb.svg index 42a90146ba..d6a0515eb6 100644 --- a/packages/nodes-base/nodes/NocoDB/nocodb.svg +++ b/packages/nodes-base/nodes/NocoDB/nocodb.svg @@ -1,425 +1,4 @@ - - - + + +