diff --git a/packages/nodes-base/credentials/NocoDb.credentials.ts b/packages/nodes-base/credentials/NocoDb.credentials.ts index 4977393a70..85d8141bab 100644 --- a/packages/nodes-base/credentials/NocoDb.credentials.ts +++ b/packages/nodes-base/credentials/NocoDb.credentials.ts @@ -11,7 +11,7 @@ export class NocoDb implements ICredentialType { documentationUrl = 'nocoDb'; properties: INodeProperties[] = [ { - displayName: 'API Token', + displayName: 'User Token', name: 'apiToken', type: 'string', default: '', diff --git a/packages/nodes-base/credentials/NocoDbApiToken.credentials.ts b/packages/nodes-base/credentials/NocoDbApiToken.credentials.ts new file mode 100644 index 0000000000..62455a9d5a --- /dev/null +++ b/packages/nodes-base/credentials/NocoDbApiToken.credentials.ts @@ -0,0 +1,37 @@ +import { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + + +export class NocoDbApiToken implements ICredentialType { + name = 'nocoDbApiToken'; + displayName = 'NocoDB API Token'; + documentationUrl = 'nocoDb'; + properties: INodeProperties[] = [ + { + displayName: 'API Token', + name: 'apiToken', + type: 'string', + default: '', + }, + { + displayName: 'Host', + name: 'host', + type: 'string', + default: '', + placeholder: 'http(s)://localhost:8080', + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + 'xc-token': '={{$credentials.apiToken}}', + }, + }, + }; +} diff --git a/packages/nodes-base/nodes/NocoDB/GenericFunctions.ts b/packages/nodes-base/nodes/NocoDB/GenericFunctions.ts index 053bf2dc46..f2e64b3910 100644 --- a/packages/nodes-base/nodes/NocoDB/GenericFunctions.ts +++ b/packages/nodes-base/nodes/NocoDB/GenericFunctions.ts @@ -35,14 +35,22 @@ interface IAttachment { * @returns {Promise} */ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, endpoint: string, body: object, query?: IDataObject, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = await this.getCredentials('nocoDb'); + const authenticationMethod = this.getNodeParameter('authentication', 0) as string; + const credentials = await this.getCredentials(authenticationMethod); + + if (credentials === undefined) { + throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); + } + + const baseUrl = credentials.host as string; + query = query || {}; const options: OptionsWithUri = { method, body, qs: query, - uri: uri || `${credentials.host}${endpoint}`, + uri: uri || baseUrl.endsWith('/') ? `${baseUrl.slice(0, -1)}${endpoint}` : `${baseUrl}${endpoint}`, json: true, }; @@ -56,7 +64,7 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa } try { - return await this.helpers.requestWithAuthentication.call(this, 'nocoDb', options); + return await this.helpers.requestWithAuthentication.call(this, authenticationMethod, options); } catch (error) { throw new NodeApiError(this.getNode(), error); } @@ -76,6 +84,7 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa * @returns {Promise} */ export async function apiRequestAllItems(this: IHookFunctions | IExecuteFunctions | IPollFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise { // tslint:disable-line:no-any + const version = this.getNode().typeVersion as number; if (query === undefined) { query = {}; @@ -88,12 +97,12 @@ export async function apiRequestAllItems(this: IHookFunctions | IExecuteFunction do { responseData = await apiRequest.call(this, method, endpoint, body, query); - returnData.push(...responseData); + version === 1 ? returnData.push(...responseData) : returnData.push(...responseData.list); query.offset += query.limit; } while ( - responseData.length !== 0 + version === 1 ? responseData.length !== 0 : responseData.pageInfo.isLastPage !== true ); return returnData; diff --git a/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts b/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts index df9c78784a..35c0f60d38 100644 --- a/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts +++ b/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts @@ -6,6 +6,7 @@ import { import { IBinaryData, IDataObject, + ILoadOptionsFunctions, INodeExecutionData, INodeType, INodeTypeDescription, @@ -29,7 +30,7 @@ export class NocoDB implements INodeType { name: 'nocoDb', icon: 'file:nocodb.svg', group: ['input'], - version: 1, + version: [1, 2], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Read, update, write and delete data from NocoDB', defaults: { @@ -41,9 +42,91 @@ export class NocoDB implements INodeType { { name: 'nocoDb', required: true, + displayOptions: { + show: { + authentication: [ + 'nocoDb', + ], + }, + }, + }, + { + name: 'nocoDbApiToken', + required: true, + displayOptions: { + show: { + authentication: [ + 'nocoDbApiToken', + ], + }, + }, }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'User Token', + value: 'nocoDb', + }, + { + name: 'API Token', + value: 'nocoDbApiToken', + }, + ], + default: 'nocoDb', + }, + { + displayName: 'API Version', + name: 'version', + type: 'options', + displayOptions: { + show: { + '@version': [ + 1, + ], + }, + }, + isNodeSetting: true, + options: [ + { + name: 'Before v0.90.0', + value: 1, + }, + { + name: 'v0.90.0 Onwards', + value: 2, + }, + ], + default: 1, + }, + { + displayName: 'API Version', + name: 'version', + type: 'options', + displayOptions: { + show: { + '@version': [ + 2, + ], + }, + }, + isNodeSetting: true, + options: [ + { + name: 'Before v0.90.0', + value: 1, + }, + { + name: 'v0.90.0 Onwards', + value: 2, + }, + ], + default: 2, + }, { displayName: 'Resource', name: 'resource', @@ -107,28 +190,66 @@ export class NocoDB implements INodeType { ], }; + methods = { + loadOptions: { + async getProjects(this: ILoadOptionsFunctions) { + try { + const requestMethod = 'GET'; + const endpoint = '/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(), `Error while fetching projects! (${e})`); + } + + }, + // This only supports using the Project ID + async getTables(this: ILoadOptionsFunctions) { + const projectId = this.getNodeParameter('projectId', 0) as string; + if (projectId) { + try { + const requestMethod = 'GET'; + const endpoint = `/api/v1/db/meta/projects/${projectId}/tables`; + 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(), `Error while fetching tables! (${e})`); + } + } else { + throw new NodeOperationError(this.getNode(), `No project selected!`); + } + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; let responseData; + const version = this.getNodeParameter('version', 0) as number; 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 = {}; + let endPoint = ''; + + const projectId = this.getNodeParameter('projectId', 0) as string; + const table = this.getNodeParameter('table', 0) as string; + if (resource === 'row') { - if (operation === 'create') { - requestMethod = 'POST'; - endpoint = `/nc/${projectId}/api/v1/${table}/bulk`; + + if (version === 1) { + endPoint = `/nc/${projectId}/api/v1/${table}/bulk`; + } else if (version === 2) { + endPoint = `/api/v1/db/data/bulk/noco/${projectId}/${table}`; + } const body: IDataObject[] = []; @@ -183,7 +304,14 @@ export class NocoDB implements INodeType { }; const qs = { project_id: projectId }; - responseData = await apiRequest.call(this, 'POST', '/dashboard', {}, qs, undefined, { formData }); + let postUrl = ''; + if (version === 1) { + postUrl = '/dashboard'; + } else if (version === 2) { + postUrl = '/api/v1/db/storage/upload'; + } + + responseData = await apiRequest.call(this, 'POST', postUrl, {}, qs, undefined, { formData }); newItem[field.fieldName] = JSON.stringify([responseData]); } } @@ -191,7 +319,7 @@ export class NocoDB implements INodeType { body.push(newItem); } try { - responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = await apiRequest.call(this, requestMethod, endPoint, body, qs); // Calculate ID manually and add to return data let id = responseData[0]; @@ -206,32 +334,62 @@ export class NocoDB implements INodeType { } throw new NodeApiError(this.getNode(), error); } - } else if (operation === 'delete') { + } + if (operation === 'delete') { requestMethod = 'DELETE'; - endpoint = `/nc/${projectId}/api/v1/${table}/bulk`; + if (version === 1) { + endPoint = `/nc/${projectId}/api/v1/${table}/bulk`; + } else if (version === 2) { + endPoint = `/api/v1/db/data/bulk/noco/${projectId}/${table}`; + } + 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)); + responseData = await apiRequest.call(this, requestMethod, endPoint, body, qs); + if (version === 1) { + returnData.push(...items.map(item => item.json)); + } else if (version === 2 ) { + + returnData.push(...responseData.map((result: number, index: number) => { + if (result === 0) { + const errorMessage = `The row with the ID "${body[index].id}" could not be deleted. It probably doesn't exist.`; + if (this.continueOnFail()) { + return { error: errorMessage }; + } + throw new NodeApiError(this.getNode(), { message: errorMessage }, { message: errorMessage, itemIndex: index }); + } + return { + success: true, + }; + })); + } } catch (error) { if (this.continueOnFail()) { returnData.push({ error: error.toString() }); } throw new NodeApiError(this.getNode(), error); } - } else if (operation === 'getAll') { + } + + 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}`; + + if (version === 1) { + endPoint = `/nc/${projectId}/api/v1/${table}`; + } else if (version === 2 ) { + endPoint = `/api/v1/db/data/noco/${projectId}/${table}`; + } returnAll = this.getNodeParameter('returnAll', 0) as boolean; qs = this.getNodeParameter('options', i, {}) as IDataObject; @@ -246,10 +404,13 @@ export class NocoDB implements INodeType { } if (returnAll === true) { - responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, {}, qs); + 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); + responseData = await apiRequest.call(this, requestMethod, endPoint, {}, qs); + if (version === 2) { + responseData = responseData.list; + } } returnData.push.apply(returnData, responseData); @@ -265,24 +426,51 @@ export class NocoDB implements INodeType { return [data]; } - } catch (error) { + } catch (error) { if (this.continueOnFail()) { returnData.push({ error: error.toString() }); } throw error; } - } else if (operation === 'get') { + } + 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 }; + if (version === 1) { + endPoint = `/nc/${projectId}/api/v1/${table}/${id}`; + } else if (version === 2) { + endPoint = `/api/v1/db/data/noco/${projectId}/${table}/${id}`; + } + + responseData = await apiRequest.call(this, requestMethod, endPoint, {}, qs); + + let newItem: INodeExecutionData = { json: {} }; + + if (version === 1) { + newItem = { json: responseData }; + } else if (version === 2 ) { + if (Object.keys(responseData).length === 0) { + // Get did fail + const errorMessage = `The row with the ID "${id}" could not be queried. It probably doesn't exist.`; + if (this.continueOnFail()) { + newItem = { + json: { error: errorMessage }, + }; + } + throw new NodeApiError(this.getNode(), { message: errorMessage }, { message: errorMessage, itemIndex: i }); + } else { + // Get did work + newItem = { json: responseData }; + } + } + + // const newItem: INodeExecutionData = { json: responseData }; const downloadAttachments = this.getNodeParameter('downloadAttachments', i) as boolean; if (downloadAttachments === true) { @@ -301,12 +489,18 @@ export class NocoDB implements INodeType { } } return this.prepareOutputData(newItems); + } - } else if (operation === 'update') { + if (operation === 'update') { - requestMethod = 'PUT'; - endpoint = `/nc/${projectId}/api/v1/${table}/bulk`; + let requestMethod = 'PATCH'; + if (version === 1) { + endPoint = `/nc/${projectId}/api/v1/${table}/bulk`; + requestMethod = 'PUT'; + } else if (version === 2) { + endPoint = `/api/v1/db/data/bulk/noco/${projectId}/${table}`; + } const body: IDataObject[] = []; for (let i = 0; i < items.length; i++) { @@ -326,13 +520,13 @@ export class NocoDB implements INodeType { } else { const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as Array<{ fieldName: string; - upload: boolean; + binaryData: boolean; fieldValue?: string; binaryProperty?: string; }>; for (const field of fields) { - if (!field.upload) { + if (!field.binaryData) { newItem[field.fieldName] = field.fieldValue; } else if (field.binaryProperty) { if (!items[i].binary) { @@ -361,8 +555,13 @@ export class NocoDB implements INodeType { }), }; const qs = { project_id: projectId }; - - responseData = await apiRequest.call(this, 'POST', '/dashboard', {}, qs, undefined, { formData }); + let postUrl = ''; + if (version === 1) { + postUrl = '/dashboard'; + } else if (version === 2) { + postUrl = '/api/v1/db/storage/upload'; + } + responseData = await apiRequest.call(this, 'POST', postUrl, {}, qs, undefined, { formData }); newItem[field.fieldName] = JSON.stringify([responseData]); } } @@ -371,8 +570,24 @@ export class NocoDB implements INodeType { } try { - responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); - returnData.push(...body); + responseData = await apiRequest.call(this, requestMethod, endPoint, body, qs); + + if (version === 1) { + returnData.push(...body); + } else if (version === 2 ) { + returnData.push(...responseData.map((result: number, index: number) => { + if (result === 0) { + const errorMessage = `The row with the ID "${body[index].id}" could not be updated. It probably doesn't exist.`; + if (this.continueOnFail()) { + return { error: errorMessage }; + } + throw new NodeApiError(this.getNode(), { message: errorMessage }, { message: errorMessage, itemIndex: index }); + } + return { + success: true, + }; + })); + } } catch (error) { if (this.continueOnFail()) { returnData.push({ error: error.toString() }); diff --git a/packages/nodes-base/nodes/NocoDB/OperationDescription.ts b/packages/nodes-base/nodes/NocoDB/OperationDescription.ts index 9f7b8d23ce..9e4d4f4c00 100644 --- a/packages/nodes-base/nodes/NocoDB/OperationDescription.ts +++ b/packages/nodes-base/nodes/NocoDB/OperationDescription.ts @@ -11,14 +11,67 @@ export const operationFields: INodeProperties[] = [ name: 'projectId', type: 'string', default: '', + displayOptions: { + show: { + version: [ + 1, + ], + }, + }, required: true, description: 'The ID of the project', }, + { + displayName: 'Project Name or ID', + name: 'projectId', + type: 'options', + default: '', + displayOptions: { + show: { + version: [ + 2, + ], + }, + }, + required: true, + description: 'Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an expression.', + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + }, + { + displayName: 'Table Name or ID', + name: 'table', + type: 'options', + default: '', + displayOptions: { + show: { + version: [ + 2, + ], + }, + }, + required: true, + description: 'The table to operate on. Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an expression.', + typeOptions: { + loadOptionsDependsOn: [ + 'projectId', + ], + loadOptionsMethod: 'getTables', + }, + }, { displayName: 'Table', name: 'table', type: 'string', default: '', + displayOptions: { + show: { + version: [ + 1, + ], + }, + }, required: true, description: 'The name of the table', }, @@ -75,7 +128,7 @@ export const operationFields: INodeProperties[] = [ minValue: 1, maxValue: 100, }, - default: 100, + default: 50, description: 'Max number of results to return', }, { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7dc371cd5a..b84a21f9c3 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -210,6 +210,7 @@ "dist/credentials/NextCloudApi.credentials.js", "dist/credentials/NextCloudOAuth2Api.credentials.js", "dist/credentials/NocoDb.credentials.js", + "dist/credentials/NocoDbApiToken.credentials.js", "dist/credentials/NotionApi.credentials.js", "dist/credentials/NotionOAuth2Api.credentials.js", "dist/credentials/OAuth1Api.credentials.js",