From 4eed7bb9fbf0d9ce26f1acc51db485a71461b447 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 25 Nov 2020 07:08:59 -0500 Subject: [PATCH] :sparkles: Add Quick Base Node (#1161) * introduce quick base node * fix quick base api request headers * refine quick base configuration pages * fix race condition detection * :zap: improvements Co-authored-by: Tristian Flanagan --- .../credentials/QuickBaseApi.credentials.ts | 25 + .../nodes/QuickBase/FieldDescription.ts | 118 ++++ .../nodes/QuickBase/FileDescription.ts | 132 ++++ .../nodes/QuickBase/GenericFunctions.ts | 132 ++++ .../nodes/QuickBase/QuickBase.node.ts | 561 ++++++++++++++++ .../nodes/QuickBase/RecordDescription.ts | 614 ++++++++++++++++++ .../nodes/QuickBase/ReportDescription.ts | 156 +++++ .../nodes-base/nodes/QuickBase/quickbase.png | Bin 0 -> 5062 bytes packages/nodes-base/package.json | 2 + 9 files changed, 1740 insertions(+) create mode 100644 packages/nodes-base/credentials/QuickBaseApi.credentials.ts create mode 100644 packages/nodes-base/nodes/QuickBase/FieldDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBase/FileDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBase/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/QuickBase/QuickBase.node.ts create mode 100644 packages/nodes-base/nodes/QuickBase/RecordDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBase/ReportDescription.ts create mode 100644 packages/nodes-base/nodes/QuickBase/quickbase.png diff --git a/packages/nodes-base/credentials/QuickBaseApi.credentials.ts b/packages/nodes-base/credentials/QuickBaseApi.credentials.ts new file mode 100644 index 0000000000..f0f67ef83a --- /dev/null +++ b/packages/nodes-base/credentials/QuickBaseApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class QuickBaseApi implements ICredentialType { + name = 'quickbaseApi'; + displayName = 'Quick Base API'; + documentationUrl = 'quickbase'; + properties = [ + { + displayName: 'Hostname', + name: 'hostname', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'demo.quickbase.com', + }, + { + displayName: 'User Token', + name: 'userToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/QuickBase/FieldDescription.ts b/packages/nodes-base/nodes/QuickBase/FieldDescription.ts new file mode 100644 index 0000000000..81b6f5056e --- /dev/null +++ b/packages/nodes-base/nodes/QuickBase/FieldDescription.ts @@ -0,0 +1,118 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const fieldOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'field', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all fields', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fieldFields = [ + /* -------------------------------------------------------------------------- */ + /* field:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'field', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'field', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your user contacts.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'field', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'field', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include Field Perms', + name: 'includeFieldPerms', + type: 'boolean', + default: false, + description: `Set to 'true' if you'd like to get back the custom permissions for the field(s)`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBase/FileDescription.ts b/packages/nodes-base/nodes/QuickBase/FileDescription.ts new file mode 100644 index 0000000000..dc88602ab5 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBase/FileDescription.ts @@ -0,0 +1,132 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const fileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'file', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a file', + }, + { + name: 'Download', + value: 'download', + description: 'Download a file', + }, + ], + default: 'download', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fileFields = [ + /* -------------------------------------------------------------------------- */ + /* file:download */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'download', + 'delete', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Record ID', + name: 'recordId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'download', + 'delete', + ], + }, + }, + description: 'The unique identifier of the record', + }, + { + displayName: 'Field ID', + name: 'fieldId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'download', + 'delete', + ], + }, + }, + description: 'The unique identifier of the field.', + }, + { + displayName: 'Version Number', + name: 'versionNumber', + type: 'number', + default: 1, + required: true, + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'download', + 'delete', + ], + }, + }, + description: 'The file attachment version number.', + }, + { + displayName: 'Binary Property', + displayOptions: { + show: { + resource: [ + 'file', + ], + operation: [ + 'download', + ], + }, + }, + name: 'binaryPropertyName', + type: 'string', + default: 'data', + description: 'Object property name which holds binary data.', + required: true, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts b/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts new file mode 100644 index 0000000000..aa6fd07e4d --- /dev/null +++ b/packages/nodes-base/nodes/QuickBase/GenericFunctions.ts @@ -0,0 +1,132 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function quickbaseApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('quickbaseApi') as IDataObject; + + if (credentials.hostname === '') { + throw new Error('Hostname must be defined'); + } + + if (credentials.userKey === '') { + throw new Error('User Token must be defined'); + } + + try { + const options: OptionsWithUri = { + headers: { + 'QB-Realm-Hostname': credentials.hostname, + 'User-Agent': 'n8n', + 'Authorization': `QB-USER-TOKEN ${credentials.userToken}`, + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: `https://api.quickbase.com/v1${resource}`, + json: true, + }; + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (Object.keys(qs).length === 0) { + delete options.qs; + } + + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + //@ts-ignore + return await this.helpers?.request(options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.description) { + + const message = error.response.body.description; + + // Try to return the error prettier + throw new Error( + `Quickbase error response [${error.statusCode}]: ${message} (qb-api-ray=${error.response.headers['qb-api-ray']})`, + ); + } + throw error; + } +} + +//@ts-ignore +export async function getFieldsObject(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, tableId: string): any { + const fieldsLabelKey: { [key: string]: number } = {}; + const fieldsIdKey: { [key: number]: string } = {}; + const data = await quickbaseApiRequest.call(this, 'GET', '/fields', {}, { tableId }); + for (const field of data) { + fieldsLabelKey[field.label] = field.id; + fieldsIdKey[field.id] = field.label; + } + return { fieldsLabelKey, fieldsIdKey }; +} + +export async function quickbaseApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData = []; + + if (method === 'POST') { + body.options = { + skip: 0, + top: 100, + }; + } else { + query.skip = 0; + query.top = 100; + } + + let metadata; + + do { + const { data, fields, metadata: meta } = await quickbaseApiRequest.call(this, method, resource, body, query); + + metadata = meta; + + const fieldsIdKey: { [key: string]: string } = {}; + + for (const field of fields) { + fieldsIdKey[field.id] = field.label; + } + + for (const record of data) { + const data: IDataObject = {}; + for (const [key, value] of Object.entries(record)) { + data[fieldsIdKey[key]] = (value as IDataObject).value; + } + responseData.push(data); + } + + if (method === 'POST') { + body.options.skip += body.options.top; + } else { + //@ts-ignore + query.skip += query.top; + } + returnData.push.apply(returnData, responseData); + responseData = []; + } while ( + returnData.length < metadata.totalRecords + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts b/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts new file mode 100644 index 0000000000..4d7b94b4e4 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBase/QuickBase.node.ts @@ -0,0 +1,561 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + getFieldsObject, + quickbaseApiRequest, + quickbaseApiRequestAllItems, +} from './GenericFunctions'; + +import { + fieldFields, + fieldOperations, +} from './FieldDescription'; + +import { + fileFields, + fileOperations, +} from './FileDescription'; + +import { + recordFields, + recordOperations, +} from './RecordDescription'; + +import { + reportFields, + reportOperations, +} from './ReportDescription'; + +export class QuickBase implements INodeType { + description: INodeTypeDescription = { + displayName: 'Quick Base', + name: 'quickbase', + icon: 'file:quickbase.png', + group: [ 'input' ], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Integrate with the Quick Base RESTful API.', + defaults: { + name: 'Quick Base', + color: '#73489d', + }, + inputs: [ 'main' ], + outputs: [ 'main' ], + credentials: [ + { + name: 'quickbaseApi', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Field', + value: 'field', + }, + { + name: 'File', + value: 'file', + }, + { + name: 'Record', + value: 'record', + }, + { + name: 'Report', + value: 'report', + }, + ], + default: 'record', + description: 'The resource to operate on.', + }, + ...fieldOperations, + ...fieldFields, + ...fileOperations, + ...fileFields, + ...recordOperations, + ...recordFields, + ...reportOperations, + ...reportFields, + ], + }; + + methods = { + loadOptions: { + async getTableFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tableId = this.getCurrentNodeParameter('tableId') as string; + const fields = await quickbaseApiRequest.call(this, 'GET', '/fields', {}, { tableId }); + for (const field of fields) { + returnData.push({ + name: field.label, + value: field.id, + }); + } + return returnData; + }, + async getUniqueTableFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tableId = this.getCurrentNodeParameter('tableId') as string; + const fields = await quickbaseApiRequest.call(this, 'GET', '/fields', {}, { tableId }); + for (const field of fields) { + //upsert can be achived just with fields that are set as unique and are no the primary key + if (field.unique === true && field.properties.primaryKey === false) { + returnData.push({ + name: field.label, + value: field.id, + }); + } + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + const headers: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + if (resource === 'field') { + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const tableId = this.getNodeParameter('tableId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + const qs: IDataObject = { + tableId, + }; + + Object.assign(qs, options); + + responseData = await quickbaseApiRequest.call(this, 'GET', '/fields', {}, qs); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.splice(0, limit); + } + + returnData.push.apply(returnData, responseData); + } + } + } + + if (resource === 'file') { + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + const tableId = this.getNodeParameter('tableId', i) as string; + + const recordId = this.getNodeParameter('recordId', i) as string; + + const fieldId = this.getNodeParameter('fieldId', i) as string; + + const versionNumber = this.getNodeParameter('versionNumber', i) as string; + + responseData = await quickbaseApiRequest.call(this,'DELETE', `/files/${tableId}/${recordId}/${fieldId}/${versionNumber}`); + + returnData.push(responseData); + } + } + + if (operation === 'download') { + + for (let i = 0; i < length; i++) { + + const tableId = this.getNodeParameter('tableId', i) as string; + + const recordId = this.getNodeParameter('recordId', i) as string; + + const fieldId = this.getNodeParameter('fieldId', i) as string; + + const versionNumber = this.getNodeParameter('versionNumber', i) as string; + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + if (items[i].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, items[i].binary); + } + + items[i] = newItem; + + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string; + + responseData = await quickbaseApiRequest.call(this,'GET', `/files/${tableId}/${recordId}/${fieldId}/${versionNumber}`, {}, {}, { json: false, resolveWithFullResponse: true }); + + //content-disposition': 'attachment; filename="dog-puppy-on-garden-royalty-free-image-1586966191.jpg"', + const contentDisposition = responseData.headers['content-disposition']; + + const data = Buffer.from(responseData.body as string, 'base64'); + + items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, contentDisposition.split('=')[1]); + } + + return this.prepareOutputData(items); + } + } + + if (resource === 'record') { + if (operation === 'create') { + const tableId = this.getNodeParameter('tableId', 0) as string; + + const { fieldsLabelKey, fieldsIdKey } = await getFieldsObject.call(this, tableId); + + const simple = this.getNodeParameter('simple', 0) as boolean; + + const data: IDataObject[] = []; + + const options = this.getNodeParameter('options', 0) as IDataObject; + + for (let i = 0; i < length; i++) { + const record: IDataObject = {}; + + const columns = this.getNodeParameter('columns', i) as string; + + const columnList = columns.split(',').map(column => column.trim()); + + for (const key of Object.keys(items[i].json)) { + if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { + record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + } + } + + data.push(record); + } + + const body: IDataObject = { + data, + to: tableId, + }; + + // If not fields are set return at least the record id + body.fieldsToReturn = [fieldsLabelKey['Record ID#']]; + + if (options.fields) { + body.fieldsToReturn = options.fields as string[]; + } + + responseData = await quickbaseApiRequest.call(this, 'POST', '/records', body); + + if (simple === true) { + const { data: records } = responseData; + responseData = []; + + for (const record of records) { + const data: IDataObject = {}; + for (const [key, value] of Object.entries(record)) { + data[fieldsIdKey[key]] = (value as IDataObject).value; + } + responseData.push(data); + } + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData); + } else { + returnData.push(responseData); + } + } + + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + const tableId = this.getNodeParameter('tableId', i) as string; + + const where = this.getNodeParameter('where', i) as string; + + const body: IDataObject = { + from: tableId, + where, + }; + + responseData = await quickbaseApiRequest.call(this, 'DELETE', '/records', body); + + returnData.push(responseData); + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const tableId = this.getNodeParameter('tableId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + const body: IDataObject = { + from: tableId, + }; + + Object.assign(body, options); + + if (options.sortByUi) { + const sort = (options.sortByUi as IDataObject).sortByValues as IDataObject[]; + body.sortBy = sort; + delete body.sortByUi; + } + + // if (options.groupByUi) { + // const group = (options.groupByUi as IDataObject).groupByValues as IDataObject[]; + // body.groupBy = group; + // delete body.groupByUi; + // } + + if (returnAll) { + responseData = await quickbaseApiRequestAllItems.call(this, 'POST', '/records/query', body, qs); + } else { + body.options = { top: this.getNodeParameter('limit', i) as number }; + + responseData = await quickbaseApiRequest.call(this, 'POST', '/records/query', body, qs); + + const { data: records, fields } = responseData; + responseData = []; + + const fieldsIdKey: { [key: string]: string } = {}; + + for (const field of fields) { + fieldsIdKey[field.id] = field.label; + } + + for (const record of records) { + const data: IDataObject = {}; + for (const [key, value] of Object.entries(record)) { + data[fieldsIdKey[key]] = (value as IDataObject).value; + } + responseData.push(data); + } + } + returnData.push.apply(returnData, responseData); + } + } + + if (operation === 'update') { + const tableId = this.getNodeParameter('tableId', 0) as string; + + const { fieldsLabelKey, fieldsIdKey } = await getFieldsObject.call(this, tableId); + + const simple = this.getNodeParameter('simple', 0) as boolean; + + const updateKey = this.getNodeParameter('updateKey', 0) as string; + + const data: IDataObject[] = []; + + const options = this.getNodeParameter('options', 0) as IDataObject; + + for (let i = 0; i < length; i++) { + const record: IDataObject = {}; + + const columns = this.getNodeParameter('columns', i) as string; + + const columnList = columns.split(',').map(column => column.trim()); + + for (const key of Object.keys(items[i].json)) { + if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { + record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + } + } + + if (items[i].json[updateKey] === undefined) { + throw new Error(`The update key ${updateKey} could not be found in the input`); + } + + record[fieldsLabelKey['Record ID#']] = { value: items[i].json[updateKey] }; + + data.push(record); + } + + const body: IDataObject = { + data, + to: tableId, + }; + + // If not fields are set return at least the record id + body.fieldsToReturn = [fieldsLabelKey['Record ID#']]; + + if (options.fields) { + body.fieldsToReturn = options.fields as string[]; + } + + responseData = await quickbaseApiRequest.call(this, 'POST', '/records', body); + + if (simple === true) { + const { data: records } = responseData; + responseData = []; + + for (const record of records) { + const data: IDataObject = {}; + for (const [key, value] of Object.entries(record)) { + data[fieldsIdKey[key]] = (value as IDataObject).value; + } + responseData.push(data); + } + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData); + } else { + returnData.push(responseData); + } + } + + if (operation === 'upsert') { + const tableId = this.getNodeParameter('tableId', 0) as string; + + const { fieldsLabelKey, fieldsIdKey } = await getFieldsObject.call(this, tableId); + + const simple = this.getNodeParameter('simple', 0) as boolean; + + const updateKey = this.getNodeParameter('updateKey', 0) as string; + + const mergeFieldId = this.getNodeParameter('mergeFieldId', 0) as string; + + const data: IDataObject[] = []; + + const options = this.getNodeParameter('options', 0) as IDataObject; + + for (let i = 0; i < length; i++) { + const record: IDataObject = {}; + + const columns = this.getNodeParameter('columns', i) as string; + + const columnList = columns.split(',').map(column => column.trim()); + + for (const key of Object.keys(items[i].json)) { + if (fieldsLabelKey.hasOwnProperty(key) && columnList.includes(key)) { + record[fieldsLabelKey[key].toString()] = { value: items[i].json[key] }; + } + } + + if (items[i].json[updateKey] === undefined) { + throw new Error(`The update key ${updateKey} could not be found in the input`); + } + + record[mergeFieldId] = { value: items[i].json[updateKey] }; + + data.push(record); + } + + const body: IDataObject = { + data, + to: tableId, + mergeFieldId, + }; + + // If not fields are set return at least the record id + body.fieldsToReturn = [fieldsLabelKey['Record ID#']]; + + if (options.fields) { + body.fieldsToReturn = options.fields as string[]; + } + + responseData = await quickbaseApiRequest.call(this, 'POST', '/records', body); + + if (simple === true) { + const { data: records } = responseData; + responseData = []; + + for (const record of records) { + const data: IDataObject = {}; + for (const [key, value] of Object.entries(record)) { + data[fieldsIdKey[key]] = (value as IDataObject).value; + } + responseData.push(data); + } + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData); + } else { + returnData.push(responseData); + } + } + } + + if (resource === 'report') { + + if (operation === 'run') { + for (let i = 0; i < length; i++) { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const tableId = this.getNodeParameter('tableId', i) as string; + + const reportId = this.getNodeParameter('reportId', i) as string; + + qs.tableId = tableId; + + if (returnAll) { + responseData = await quickbaseApiRequestAllItems.call(this, 'POST', `/reports/${reportId}/run`, {}, qs); + } else { + qs.top = this.getNodeParameter('limit', i) as number; + + responseData = await quickbaseApiRequest.call(this, 'POST', `/reports/${reportId}/run`, {}, qs); + + const { data: records, fields } = responseData; + responseData = []; + + const fieldsIdKey: { [key: string]: string } = {}; + + for (const field of fields) { + fieldsIdKey[field.id] = field.label; + } + + for (const record of records) { + const data: IDataObject = {}; + for (const [key, value] of Object.entries(record)) { + data[fieldsIdKey[key]] = (value as IDataObject).value; + } + responseData.push(data); + } + } + returnData.push.apply(returnData, responseData); + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + + const reportId = this.getNodeParameter('reportId', i) as string; + + const tableId = this.getNodeParameter('tableId', i) as string; + + qs.tableId = tableId; + + responseData = await quickbaseApiRequest.call(this, 'GET', `/reports/${reportId}`, {}, qs); + + returnData.push(responseData); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/QuickBase/RecordDescription.ts b/packages/nodes-base/nodes/QuickBase/RecordDescription.ts new file mode 100644 index 0000000000..e1f7a43e34 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBase/RecordDescription.ts @@ -0,0 +1,614 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const recordOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'record', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a record', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a record', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all records', + }, + { + name: 'Update', + value: 'update', + description: 'Update a record', + }, + { + name: 'Upsert', + value: 'upsert', + description: 'Upsert a record', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const recordFields = [ + /* -------------------------------------------------------------------------- */ + /* record:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: 'Comma separated list of the properties which should used as columns for the new rows.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'create', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTableFields', + loadOptionsDependsOn: [ + 'tableId', + ], + }, + default: [], + description: `Specify an array of field ids that will return data for any updates or added record. Record ID (FID 3) is always returned if any field ID is requested.`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* record:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Where', + name: 'where', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'delete', + ], + }, + }, + description: `The filter to delete records. To delete all records specify a filter that will include all records,
+ for example {3.GT.0} where 3 is the ID of the Record ID field.`, + }, + /* -------------------------------------------------------------------------- */ + /* record:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your user contacts.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + // { + // displayName: 'Group By', + // name: 'groupByUi', + // placeholder: 'Add Group By', + // type: 'fixedCollection', + // typeOptions: { + // multipleValues: true, + // }, + // default: {}, + // options: [ + // { + // name: 'groupByValues', + // displayName: 'Group By', + // values: [ + // { + // displayName: 'Field ID', + // name: 'fieldId', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getTableFields', + // }, + // default: '', + // description: 'The unique identifier of a field in a table.', + // }, + // { + // displayName: 'Grouping', + // name: 'grouping', + // type: 'options', + // options: [ + // { + // name: 'ASC', + // value: 'ASC', + // }, + // { + // name: 'DESC', + // value: 'DESC', + // }, + // { + // name: 'Equal Values', + // value: 'equal-values', + // }, + // ], + // default: 'ASC', + // }, + // ], + // }, + // ], + // }, + { + displayName: 'Select', + name: 'select', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTableFields', + }, + default: [], + description: 'An array of field ids for the fields that should be returned in the response. If empty, the default columns on the table will be returned.', + }, + { + displayName: 'Sort By', + name: 'sortByUi', + placeholder: 'Add Sort By', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'sortByValues', + displayName: 'Sort By', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTableFields', + }, + default: '', + description: 'The unique identifier of a field in a table.', + }, + { + displayName: 'Order', + name: 'order', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: 'ASC', + }, + ], + }, + ], + description: `By default, queries will be sorted by the given sort fields or the default sort if the query does not provide any.
+ Set to false to avoid sorting when the order of the data returned is not important. Returning data without sorting can improve performance.`, + }, + { + displayName: 'Where', + name: 'where', + type: 'string', + default: '', + description: 'The filter, using the Quick Base query language, which determines the records to return.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* record:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'update', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: 'Comma separated list of the properties which should used as columns for the new rows.', + }, + { + displayName: 'Update Key', + name: 'updateKey', + type: 'string', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'update can use the key field on the table, or any other supported unique field.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'update', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTableFields', + loadOptionsDependsOn: [ + 'tableId', + ], + }, + default: [], + description: `Specify an array of field ids that will return data for any updates or added record. Record ID (FID 3) is always returned if any field ID is requested.`, + }, + // { + // displayName: 'Merge Field ID', + // name: 'mergeFieldId', + // type: 'options', + // typeOptions: { + // loadOptionsMethod: 'getUniqueTableFields', + // }, + // default: '', + // description: `You're updating records in a Quick Base table with data from an external file. In order for a merge like this to work,
+ // Quick Base needs a way to match records in the source data with corresponding records in the destination table. You make this possible by
+ // choosing the field in the app table that holds unique matching values. This is called a merge field.`, + // }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* record:upsert */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'upsert', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'upsert', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: 'Comma separated list of the properties which should used as columns for the new rows.', + }, + { + displayName: 'Update Key', + name: 'updateKey', + type: 'string', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'upsert', + ], + }, + }, + default: '', + description: 'update can use the key field on the table, or any other supported unique field.', + }, + { + displayName: 'Merge Field ID', + name: 'mergeFieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUniqueTableFields', + }, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'upsert', + ], + }, + }, + default: '', + description: `You're updating records in a Quick Base table with data from an external file. In order for a merge like this to work,
+ Quick Base needs a way to match records in the source data with corresponding records in the destination table. You make this possible by
+ choosing the field in the app table that holds unique matching values. This is called a merge field.`, + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'upsert', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'record', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTableFields', + loadOptionsDependsOn: [ + 'tableId', + ], + }, + default: [], + description: `Specify an array of field ids that will return data for any updates or added record. Record ID (FID 3) is always returned if any field ID is requested.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBase/ReportDescription.ts b/packages/nodes-base/nodes/QuickBase/ReportDescription.ts new file mode 100644 index 0000000000..ab9f2f3a15 --- /dev/null +++ b/packages/nodes-base/nodes/QuickBase/ReportDescription.ts @@ -0,0 +1,156 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const reportOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'report', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a report', + }, + { + name: 'Run', + value: 'run', + description: 'Run a report', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const reportFields = [ + /* -------------------------------------------------------------------------- */ + /* report:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Report ID', + name: 'reportId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The identifier of the report, unique to the table', + }, + /* -------------------------------------------------------------------------- */ + /* report:run */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Table ID', + name: 'tableId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'run', + ], + }, + }, + description: 'The table identifier', + }, + { + displayName: 'Report ID', + name: 'reportId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'run', + ], + }, + }, + description: 'The identifier of the report, unique to the table', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'run', + ], + }, + }, + default: true, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'run', + ], + }, + hide: { + returnAll: [ + true, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'Number of results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/QuickBase/quickbase.png b/packages/nodes-base/nodes/QuickBase/quickbase.png new file mode 100644 index 0000000000000000000000000000000000000000..0d362fd7e1571d53b9780906dec5022fe948ed03 GIT binary patch literal 5062 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 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 090f15f47f..7e1a79db65 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -162,6 +162,7 @@ "dist/credentials/PushbulletOAuth2Api.credentials.js", "dist/credentials/PushoverApi.credentials.js", "dist/credentials/QuestDb.credentials.js", + "dist/credentials/QuickBaseApi.credentials.js", "dist/credentials/Redis.credentials.js", "dist/credentials/RocketchatApi.credentials.js", "dist/credentials/RundeckApi.credentials.js", @@ -381,6 +382,7 @@ "dist/nodes/Pushbullet/Pushbullet.node.js", "dist/nodes/Pushover/Pushover.node.js", "dist/nodes/QuestDb/QuestDb.node.js", + "dist/nodes/QuickBase/QuickBase.node.js", "dist/nodes/ReadBinaryFile.node.js", "dist/nodes/ReadBinaryFiles.node.js", "dist/nodes/ReadPdf.node.js",