diff --git a/packages/nodes-base/credentials/CodaApi.credentials.ts b/packages/nodes-base/credentials/CodaApi.credentials.ts new file mode 100644 index 0000000000..c7e8befe4c --- /dev/null +++ b/packages/nodes-base/credentials/CodaApi.credentials.ts @@ -0,0 +1,23 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class CodaApi implements ICredentialType { + name = 'codaApi'; + displayName = 'Coda API'; + properties = [ + { + displayName: 'Doc ID', + name: 'docId', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Coda/Coda.node.ts b/packages/nodes-base/nodes/Coda/Coda.node.ts new file mode 100644 index 0000000000..605db290f6 --- /dev/null +++ b/packages/nodes-base/nodes/Coda/Coda.node.ts @@ -0,0 +1,299 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + codaApiRequest, + codaApiRequestAllItems, +} from './GenericFunctions'; +import { + rowOpeations, + rowFields +} from './RowDescription'; + +export class Coda implements INodeType { + description: INodeTypeDescription = { + displayName: 'Coda', + name: 'Coda', + icon: 'file:coda.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Coda Beta API', + defaults: { + name: 'Coda', + color: '#c02428', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'codaApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Rows', + value: 'row', + description: `You'll likely use this part of the API the most. + These endpoints let you retrieve row data from tables in Coda as well + as create, upsert, update, and delete them.`, + }, + ], + default: 'row', + description: 'Resource to consume.', + }, + ...rowOpeations, + ...rowFields, + ], + }; + + methods = { + loadOptions: { + // Get all the available tables to display them to user so that he can + // select them easily + async getTables(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const credentials = this.getCredentials('codaApi'); + const qs = {}; + let tables; + try { + tables = await codaApiRequestAllItems.call(this,'items', 'GET', `/docs/${credentials!.docId}/tables`, {}, qs); + } catch (err) { + throw new Error(`Coda Error: ${err}`); + } + for (const table of tables) { + const tableName = table.name; + const tableId = table.id; + returnData.push({ + name: tableName, + value: tableId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const qs: IDataObject = {}; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + // if (resource === 'task') { + // //https://developer.getflow.com/api/#tasks_create-task + // if (operation === 'create') { + // const workspaceId = this.getNodeParameter('workspaceId', i) as string; + // const name = this.getNodeParameter('name', i) as string; + // const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + // const body: ITask = { + // organization_id: credentials.organizationId as number, + // }; + // const task: TaskInfo = { + // name, + // workspace_id: parseInt(workspaceId, 10) + // }; + // if (additionalFields.ownerId) { + // task.owner_id = parseInt(additionalFields.ownerId as string, 10); + // } + // if (additionalFields.listId) { + // task.list_id = parseInt(additionalFields.listId as string, 10); + // } + // if (additionalFields.startsOn) { + // task.starts_on = additionalFields.startsOn as string; + // } + // if (additionalFields.dueOn) { + // task.due_on = additionalFields.dueOn as string; + // } + // if (additionalFields.mirrorParentSubscribers) { + // task.mirror_parent_subscribers = additionalFields.mirrorParentSubscribers as boolean; + // } + // if (additionalFields.mirrorParentTags) { + // task.mirror_parent_tags = additionalFields.mirrorParentTags as boolean; + // } + // if (additionalFields.noteContent) { + // task.note_content = additionalFields.noteContent as string; + // } + // if (additionalFields.noteMimeType) { + // task.note_mime_type = additionalFields.noteMimeType as string; + // } + // if (additionalFields.parentId) { + // task.parent_id = parseInt(additionalFields.parentId as string, 10); + // } + // if (additionalFields.positionList) { + // task.position_list = additionalFields.positionList as number; + // } + // if (additionalFields.positionUpcoming) { + // task.position_upcoming = additionalFields.positionUpcoming as number; + // } + // if (additionalFields.position) { + // task.position = additionalFields.position as number; + // } + // if (additionalFields.sectionId) { + // task.section_id = additionalFields.sectionId as number; + // } + // if (additionalFields.tags) { + // task.tags = (additionalFields.tags as string).split(','); + // } + // body.task = task; + // try { + // responseData = await flowApiRequest.call(this, 'POST', '/tasks', body); + // responseData = responseData.task; + // } catch (err) { + // throw new Error(`Flow Error: ${err.message}`); + // } + // } + // //https://developer.getflow.com/api/#tasks_update-a-task + // if (operation === 'update') { + // const workspaceId = this.getNodeParameter('workspaceId', i) as string; + // const taskId = this.getNodeParameter('taskId', i) as string; + // const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + // const body: ITask = { + // organization_id: credentials.organizationId as number, + // }; + // const task: TaskInfo = { + // workspace_id: parseInt(workspaceId, 10), + // id: parseInt(taskId, 10), + // }; + // if (updateFields.name) { + // task.name = updateFields.name as string; + // } + // if (updateFields.ownerId) { + // task.owner_id = parseInt(updateFields.ownerId as string, 10); + // } + // if (updateFields.listId) { + // task.list_id = parseInt(updateFields.listId as string, 10); + // } + // if (updateFields.startsOn) { + // task.starts_on = updateFields.startsOn as string; + // } + // if (updateFields.dueOn) { + // task.due_on = updateFields.dueOn as string; + // } + // if (updateFields.mirrorParentSubscribers) { + // task.mirror_parent_subscribers = updateFields.mirrorParentSubscribers as boolean; + // } + // if (updateFields.mirrorParentTags) { + // task.mirror_parent_tags = updateFields.mirrorParentTags as boolean; + // } + // if (updateFields.noteContent) { + // task.note_content = updateFields.noteContent as string; + // } + // if (updateFields.noteMimeType) { + // task.note_mime_type = updateFields.noteMimeType as string; + // } + // if (updateFields.parentId) { + // task.parent_id = parseInt(updateFields.parentId as string, 10); + // } + // if (updateFields.positionList) { + // task.position_list = updateFields.positionList as number; + // } + // if (updateFields.positionUpcoming) { + // task.position_upcoming = updateFields.positionUpcoming as number; + // } + // if (updateFields.position) { + // task.position = updateFields.position as number; + // } + // if (updateFields.sectionId) { + // task.section_id = updateFields.sectionId as number; + // } + // if (updateFields.tags) { + // task.tags = (updateFields.tags as string).split(','); + // } + // if (updateFields.completed) { + // task.completed = updateFields.completed as boolean; + // } + // body.task = task; + // try { + // responseData = await flowApiRequest.call(this, 'PUT', `/tasks/${taskId}`, body); + // responseData = responseData.task; + // } catch (err) { + // throw new Error(`Flow Error: ${err.message}`); + // } + // } + // //https://developer.getflow.com/api/#tasks_get-task + // if (operation === 'get') { + // const taskId = this.getNodeParameter('taskId', i) as string; + // const filters = this.getNodeParameter('filters', i) as IDataObject; + // qs.organization_id = credentials.organizationId as number; + // if (filters.include) { + // qs.include = (filters.include as string[]).join(','); + // } + // try { + // responseData = await flowApiRequest.call(this,'GET', `/tasks/${taskId}`, {}, qs); + // } catch (err) { + // throw new Error(`Flow Error: ${err.message}`); + // } + // } + // //https://developer.getflow.com/api/#tasks_get-tasks + // if (operation === 'getAll') { + // const returnAll = this.getNodeParameter('returnAll', i) as boolean; + // const filters = this.getNodeParameter('filters', i) as IDataObject; + // qs.organization_id = credentials.organizationId as number; + // if (filters.include) { + // qs.include = (filters.include as string[]).join(','); + // } + // if (filters.order) { + // qs.order = filters.order as string; + // } + // if (filters.workspaceId) { + // qs.workspace_id = filters.workspaceId as string; + // } + // if (filters.createdBefore) { + // qs.created_before = filters.createdBefore as string; + // } + // if (filters.createdAfter) { + // qs.created_after = filters.createdAfter as string; + // } + // if (filters.updateBefore) { + // qs.updated_before = filters.updateBefore as string; + // } + // if (filters.updateAfter) { + // qs.updated_after = filters.updateAfter as string; + // } + // if (filters.deleted) { + // qs.deleted = filters.deleted as boolean; + // } + // if (filters.cleared) { + // qs.cleared = filters.cleared as boolean; + // } + // try { + // if (returnAll === true) { + // responseData = await FlowApiRequestAllItems.call(this, 'tasks', 'GET', '/tasks', {}, qs); + // } else { + // qs.limit = this.getNodeParameter('limit', i) as number; + // responseData = await flowApiRequest.call(this, 'GET', '/tasks', {}, qs); + // responseData = responseData.tasks; + // } + // } catch (err) { + // throw new Error(`Flow Error: ${err.message}`); + // } + // } + // } + // if (Array.isArray(responseData)) { + // returnData.push.apply(returnData, responseData as IDataObject[]); + // } else { + // returnData.push(responseData as IDataObject); + // } + } + return [this.helpers.returnJsonArray({})]; + } +} diff --git a/packages/nodes-base/nodes/Coda/GenericFunctions.ts b/packages/nodes-base/nodes/Coda/GenericFunctions.ts new file mode 100644 index 0000000000..ecd6bcf64b --- /dev/null +++ b/packages/nodes-base/nodes/Coda/GenericFunctions.ts @@ -0,0 +1,66 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; +import { response } from 'express'; + +export async function codaApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('codaApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { 'Authorization': `Bearer ${credentials.accessToken}`}, + method, + qs, + body, + uri: uri ||`https://coda.io/apis/v1beta1${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + return await this.helpers.request!(options); + } catch (error) { + let errorMessage = error.message; + if (error.response.body) { + errorMessage = error.response.body.message || error.response.body.Message || error.message; + } + + throw new Error(errorMessage); + } +} + +/** + * Make an API request to paginated coda endpoint + * and return all results + */ +export async function codaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.limit = 100; + + let uri: string | undefined; + + do { + responseData = await codaApiRequest.call(this, method, resource, body, query, uri); + uri = responseData.nextPageLink; + // @ts-ignore + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.nextPageLink !== undefined && + responseData.nextPageLink !== '' + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Coda/RowDescription.ts b/packages/nodes-base/nodes/Coda/RowDescription.ts new file mode 100644 index 0000000000..831d381426 --- /dev/null +++ b/packages/nodes-base/nodes/Coda/RowDescription.ts @@ -0,0 +1,95 @@ +import { INodeProperties } from "n8n-workflow"; + +export const rowOpeations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'row', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create/Upsert a row', + }, + { + name: 'Update', + value: 'update', + description: 'Update row', + }, + { + name: 'Get', + value: 'get', + description: 'Get row', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all the rows', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const rowFields = [ + +/* -------------------------------------------------------------------------- */ +/* row:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Table', + name: 'table', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getTables', + }, + default: [], + displayOptions: { + show: { + resource: [ + 'row', + ], + operation: [ + 'create' + ] + }, + }, + description: 'The title of the task.', + }, + { + displayName: 'Additional Fields', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'row', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Key Columns', + name: 'keyColumns', + type: 'string', + default: '', + description: `Optional column IDs, URLs, or names (fragile and discouraged), + specifying columns to be used as upsert keys. If more than one separate by ,`, + }, + ] + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Coda/coda.png b/packages/nodes-base/nodes/Coda/coda.png new file mode 100644 index 0000000000..cd74b6330f Binary files /dev/null and b/packages/nodes-base/nodes/Coda/coda.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index dad780ab23..66a57b7cf4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -32,6 +32,7 @@ "dist/credentials/AsanaApi.credentials.js", "dist/credentials/Aws.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", + "dist/credentials/CodaApi.credentials.js", "dist/credentials/DropboxApi.credentials.js", "dist/credentials/FreshdeskApi.credentials.js", "dist/credentials/FileMaker.credentials.js", @@ -85,6 +86,7 @@ "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", "dist/nodes/Cron.node.js", + "dist/nodes/Coda/Coda.node.js", "dist/nodes/Dropbox/Dropbox.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/EditImage.node.js",