diff --git a/packages/nodes-base/nodes/Coda/Coda.node.ts b/packages/nodes-base/nodes/Coda/Coda.node.ts index 4a54bf41c0..5c6e475da6 100644 --- a/packages/nodes-base/nodes/Coda/Coda.node.ts +++ b/packages/nodes-base/nodes/Coda/Coda.node.ts @@ -14,9 +14,9 @@ import { codaApiRequestAllItems, } from './GenericFunctions'; import { - rowOpeations, - rowFields -} from './RowDescription'; + tableFields, + tableOperations, +} from './TableDescription'; export class Coda implements INodeType { description: INodeTypeDescription = { @@ -46,18 +46,16 @@ export class Coda implements INodeType { 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.`, + name: 'Table', + value: 'table', + description: `Access data of tables in documents.`, }, ], - default: 'row', + default: 'table', description: 'Resource to consume.', }, - ...rowOpeations, - ...rowFields, + ...tableOperations, + ...tableFields, ], }; @@ -84,6 +82,29 @@ export class Coda implements INodeType { } return returnData; }, + // 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[] = []; + let tables; + + const docId = this.getCurrentNodeParameter('docId'); + + try { + tables = await codaApiRequestAllItems.call(this, 'items', 'GET', `/docs/${docId}/tables`, {}); + } 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; + }, }, }; @@ -91,66 +112,110 @@ export class Coda implements INodeType { const returnData: IDataObject[] = []; const items = this.getInputData(); let responseData; - const qs: IDataObject = {}; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; - if (resource === 'row') { - //https://coda.io/developers/apis/v1beta1#operation/upsertRows - if (operation === 'create') { - const docId = this.getNodeParameter('docId', 0) as string; - const tableId = this.getNodeParameter('tableId', 0) as string; - const additionalFields = this.getNodeParameter('additionalFields', 0) as IDataObject; - const endpoint = `/docs/${docId}/tables/${tableId}/rows`; - if (additionalFields.keyColumns) { - // @ts-ignore - items[0].json['keyColumns'] = additionalFields.keyColumns.split(',') as string[]; + + let qs: IDataObject = {}; + + if (resource === 'table') { + // https://coda.io/developers/apis/v1beta1#operation/upsertRows + if (operation === 'createRow') { + const sendData = {} as IDataObject; + for (let i = 0; i < items.length; i++) { + qs = {}; + const docId = this.getNodeParameter('docId', i) as string; + const tableId = this.getNodeParameter('tableId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const endpoint = `/docs/${docId}/tables/${tableId}/rows`; + + if (additionalFields.keyColumns) { + // @ts-ignore + items[i].json['keyColumns'] = additionalFields.keyColumns.split(',') as string[]; + } + if (additionalFields.disableParsing) { + qs.disableParsing = additionalFields.disableParsing as boolean; + } + + const cells = []; + cells.length = 0; + for (const key of Object.keys(items[i].json)) { + cells.push({ + column: key, + value: items[i].json[key], + }); + } + + // Collect all the data for the different docs/tables + if (sendData[endpoint] === undefined) { + sendData[endpoint] = { + rows: [], + // TODO: This is not perfect as it ignores if qs changes between + // different items but should be OK for now + qs, + }; + } + ((sendData[endpoint]! as IDataObject).rows! as IDataObject[]).push({ cells }); } - if (additionalFields.disableParsing) { - qs.disableParsing = additionalFields.disableParsing as boolean; - } - try { - responseData = await codaApiRequest.call(this, 'POST', endpoint, items[0].json, qs); - } catch (err) { - throw new Error(`Coda Error: ${err.message}`); + + // Now that all data got collected make all the requests + for (const endpoint of Object.keys(sendData)) { + await codaApiRequest.call(this, 'POST', endpoint, sendData[endpoint], (sendData[endpoint]! as IDataObject).qs! as IDataObject); } + + // Return the incoming data + return [items]; } - //https://coda.io/developers/apis/v1beta1#operation/getRow - if (operation === 'get') { - const docId = this.getNodeParameter('docId', 0) as string; - const tableId = this.getNodeParameter('tableId', 0) as string; - const rowId = this.getNodeParameter('rowId', 0) as string; - const filters = this.getNodeParameter('filters', 0) as IDataObject; - const endpoint = `/docs/${docId}/tables/${tableId}/rows/${rowId}`; - if (filters.useColumnNames) { - qs.useColumnNames = filters.useColumnNames as boolean; - } - if (filters.valueFormat) { - qs.valueFormat = filters.valueFormat as string; - } - try { + // https://coda.io/developers/apis/v1beta1#operation/getRow + if (operation === 'getRow') { + for (let i = 0; i < items.length; i++) { + const docId = this.getNodeParameter('docId', i) as string; + const tableId = this.getNodeParameter('tableId', i) as string; + const rowId = this.getNodeParameter('rowId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + const endpoint = `/docs/${docId}/tables/${tableId}/rows/${rowId}`; + if (options.useColumnNames === false) { + qs.useColumnNames = options.useColumnNames as boolean; + } else { + qs.useColumnNames = true; + } + if (options.valueFormat) { + qs.valueFormat = options.valueFormat as string; + } + responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); - } catch (err) { - throw new Error(`Coda Error: ${err.message}`); + if (options.rawData === true) { + returnData.push(responseData); + } else { + returnData.push({ + id: responseData.id, + ...responseData.values + }); + } } + + return [this.helpers.returnJsonArray(returnData)]; } - //https://coda.io/developers/apis/v1beta1#operation/listRows - if (operation === 'getAll') { + // https://coda.io/developers/apis/v1beta1#operation/listRows + if (operation === 'getAllRows') { const docId = this.getNodeParameter('docId', 0) as string; const returnAll = this.getNodeParameter('returnAll', 0) as boolean; const tableId = this.getNodeParameter('tableId', 0) as string; - const filters = this.getNodeParameter('filters', 0) as IDataObject; + const options = this.getNodeParameter('options', 0) as IDataObject; const endpoint = `/docs/${docId}/tables/${tableId}/rows`; - if (filters.useColumnNames) { - qs.useColumnNames = filters.useColumnNames as boolean; + if (options.useColumnNames === false) { + qs.useColumnNames = options.useColumnNames as boolean; + } else { + qs.useColumnNames = true; } - if (filters.valueFormat) { - qs.valueFormat = filters.valueFormat as string; + if (options.valueFormat) { + qs.valueFormat = options.valueFormat as string; } - if (filters.sortBy) { - qs.sortBy = filters.sortBy as string; + if (options.sortBy) { + qs.sortBy = options.sortBy as string; } - if (filters.visibleOnly) { - qs.visibleOnly = filters.visibleOnly as boolean; + if (options.visibleOnly) { + qs.visibleOnly = options.visibleOnly as boolean; } try { if (returnAll === true) { @@ -163,33 +228,46 @@ export class Coda implements INodeType { } catch (err) { throw new Error(`Flow Error: ${err.message}`); } + + if (options.rawData === true) { + return [this.helpers.returnJsonArray(responseData)]; + } else { + for (const item of responseData) { + returnData.push({ + id: item.id, + ...item.values + }); + } + return [this.helpers.returnJsonArray(returnData)]; + } } - //https://coda.io/developers/apis/v1beta1#operation/deleteRows - if (operation === 'delete') { - const docId = this.getNodeParameter('docId', 0) as string; - const tableId = this.getNodeParameter('tableId', 0) as string; - const body = {}; - let rowIds = ''; - const endpoint = `/docs/${docId}/tables/${tableId}/rows`; + // https://coda.io/developers/apis/v1beta1#operation/deleteRows + if (operation === 'deleteRow') { + const sendData = {} as IDataObject; for (let i = 0; i < items.length; i++) { + const docId = this.getNodeParameter('docId', i) as string; + const tableId = this.getNodeParameter('tableId', i) as string; const rowId = this.getNodeParameter('rowId', i) as string; - rowIds += rowId; + const endpoint = `/docs/${docId}/tables/${tableId}/rows`; + + // Collect all the data for the different docs/tables + if (sendData[endpoint] === undefined) { + sendData[endpoint] = []; + } + + (sendData[endpoint] as string[]).push(rowId); } - // @ts-ignore - body['rowIds'] = rowIds.split(',') as string[]; - try { - // @ts-ignore - responseData = await codaApiRequest.call(this, 'DELETE', endpoint, body, qs); - } catch (err) { - throw new Error(`Coda Error: ${err.message}`); + + // Now that all data got collected make all the requests + for (const endpoint of Object.keys(sendData)) { + await codaApiRequest.call(this, 'DELETE', endpoint, { rowIds: sendData[endpoint]}, qs); } - } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else { - returnData.push(responseData as IDataObject); + + // Return the incoming data + return [items]; } } - return [this.helpers.returnJsonArray(responseData)]; + + return []; } } diff --git a/packages/nodes-base/nodes/Coda/RowDescription.ts b/packages/nodes-base/nodes/Coda/TableDescription.ts similarity index 75% rename from packages/nodes-base/nodes/Coda/RowDescription.ts rename to packages/nodes-base/nodes/Coda/TableDescription.ts index 70529412ea..f7f6752f50 100644 --- a/packages/nodes-base/nodes/Coda/RowDescription.ts +++ b/packages/nodes-base/nodes/Coda/TableDescription.ts @@ -1,6 +1,6 @@ -import { INodeProperties } from "n8n-workflow"; +import { INodeProperties } from 'n8n-workflow'; -export const rowOpeations = [ +export const tableOperations = [ { displayName: 'Operation', name: 'operation', @@ -8,41 +8,41 @@ export const rowOpeations = [ displayOptions: { show: { resource: [ - 'row', + 'table', ], }, }, options: [ { - name: 'Create', - value: 'create', + name: 'Create Row', + value: 'createRow', description: 'Create/Upsert a row', }, { - name: 'Get', - value: 'get', + name: 'Get Row', + value: 'getRow', description: 'Get row', }, { - name: 'Get All', - value: 'getAll', + name: 'Get All Rows', + value: 'getAllRows', description: 'Get all the rows', }, { - name: 'Delete', - value: 'delete', + name: 'Delete Row', + value: 'deleteRow', description: 'Delete one or multiple rows', }, ], - default: 'create', + default: 'createRow', description: 'The operation to perform.', }, ] as INodeProperties[]; -export const rowFields = [ +export const tableFields = [ /* -------------------------------------------------------------------------- */ -/* row:create */ +/* table:createRow */ /* -------------------------------------------------------------------------- */ { displayName: 'Doc', @@ -52,38 +52,39 @@ export const rowFields = [ typeOptions: { loadOptionsMethod: 'getDocs', }, - default: [], + default: '', displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'create' + 'createRow', ] }, }, description: 'ID of the doc.', }, { - displayName: 'Table ID', + displayName: 'Table', name: 'tableId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, required: true, default: [], displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'create' + 'createRow', ] }, }, - description: `ID or name of the table. Names are discouraged because
- they're easily prone to being changed by users.
- If you're using a name, be sure to URI-encode it.`, + description: 'The table to create the row in.', }, { displayName: 'Additional Fields', @@ -94,10 +95,10 @@ export const rowFields = [ displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'create', + 'createRow', ], }, }, @@ -121,7 +122,7 @@ export const rowFields = [ }, /* -------------------------------------------------------------------------- */ -/* row:get */ +/* table:get */ /* -------------------------------------------------------------------------- */ { displayName: 'Doc', @@ -131,38 +132,39 @@ export const rowFields = [ typeOptions: { loadOptionsMethod: 'getDocs', }, - default: [], + default: '', displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'get' + 'getRow', ] }, }, description: 'ID of the doc.', }, { - displayName: 'Table ID', + displayName: 'Table', name: 'tableId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, required: true, default: [], displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'get' + 'getRow', ] }, }, - description: `ID or name of the table. Names are discouraged because
- they're easily prone to being changed by users.
- If you're using a name, be sure to URI-encode it.`, + description: 'The table to get the row from.', }, { displayName: 'Row ID', @@ -173,10 +175,10 @@ export const rowFields = [ displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'get' + 'getRow', ] }, }, @@ -185,18 +187,18 @@ export const rowFields = [ If there are multiple rows with the same value in the identifying column, an arbitrary one will be selected`, }, { - displayName: 'Filters', - name: 'filters', + displayName: 'Options', + name: 'options', type: 'collection', - placeholder: 'Add Filter', + placeholder: 'Add Option', default: {}, displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'get', + 'getRow', ], }, }, @@ -210,6 +212,13 @@ export const rowFields = [ This is generally discouraged as it is fragile. If columns are renamed,
code using original names may throw errors.`, }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: `Returns the data exactly in the way it got received from the API.`, + }, { displayName: 'ValueFormat', name: 'valueFormat', @@ -234,7 +243,7 @@ export const rowFields = [ ] }, /* -------------------------------------------------------------------------- */ -/* get:all */ +/* table:getAll */ /* -------------------------------------------------------------------------- */ { displayName: 'Doc', @@ -244,38 +253,39 @@ export const rowFields = [ typeOptions: { loadOptionsMethod: 'getDocs', }, - default: [], + default: '', displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'getAll' + 'getAllRows', ] }, }, description: 'ID of the doc.', }, { - displayName: 'Table ID', + displayName: 'Table', name: 'tableId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, required: true, default: [], displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'getAll' + 'getAllRows', ] }, }, - description: `ID or name of the table. Names are discouraged because
- they're easily prone to being changed by users.
- If you're using a name, be sure to URI-encode it.`, + description: 'The table to get the rows from.', }, { displayName: 'Return All', @@ -284,10 +294,10 @@ export const rowFields = [ displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'getAll' + 'getAllRows', ] }, }, @@ -301,10 +311,10 @@ export const rowFields = [ displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'getAll' + 'getAllRows', ], returnAll: [ false, @@ -319,18 +329,18 @@ export const rowFields = [ description: 'How many results to return.', }, { - displayName: 'Filters', - name: 'filters', + displayName: 'Options', + name: 'options', type: 'collection', - placeholder: 'Add Filter', + placeholder: 'Add Option', default: {}, displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'getAll', + 'getAllRows', ], }, }, @@ -365,6 +375,13 @@ export const rowFields = [ ], description: `The format that cell values are returned as.`, }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: `Returns the data exactly in the way it got received from the API.`, + }, { displayName: 'Sort By', name: 'sortBy', @@ -403,38 +420,39 @@ export const rowFields = [ typeOptions: { loadOptionsMethod: 'getDocs', }, - default: [], + default: '', displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'delete' + 'deleteRow', ] }, }, description: 'ID of the doc.', }, { - displayName: 'Table ID', + displayName: 'Table', name: 'tableId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTables', + }, required: true, default: [], displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'delete' + 'deleteRow', ] }, }, - description: `ID or name of the table. Names are discouraged because
- they're easily prone to being changed by users.
- If you're using a name, be sure to URI-encode it.`, + description: 'The table to delete the row in.', }, { displayName: 'Row ID', @@ -445,14 +463,14 @@ export const rowFields = [ displayOptions: { show: { resource: [ - 'row', + 'table', ], operation: [ - 'delete' + 'deleteRow', ] }, }, - description: `Row IDs to delete separated by ,.`, + description: 'Row IDs to delete.', }, ] as INodeProperties[];