import { IExecuteFunctions } from 'n8n-core'; import { ICredentialsDecrypted, ICredentialTestFunctions, IDataObject, ILoadOptionsFunctions, INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, } from 'n8n-workflow'; import { OptionsWithUri, } from 'request'; import { gristApiRequest, parseAutoMappedInputs, parseDefinedFields, parseFilterProperties, parseSortProperties, throwOnZeroDefinedFields, } from './GenericFunctions'; import { operationFields, } from './OperationDescription'; import { FieldsToSend, GristColumns, GristCreateRowPayload, GristCredentials, GristGetAllOptions, GristUpdateRowPayload, SendingOptions, } from './types'; export class Grist implements INodeType { description: INodeTypeDescription = { displayName: 'Grist', name: 'grist', icon: 'file:grist.svg', subtitle: '={{$parameter["operation"]}}', group: ['input'], version: 1, description: 'Consume the Grist API', defaults: { name: 'Grist', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'gristApi', required: true, testedBy: 'gristApiTest', }, ], properties: operationFields, }; methods = { loadOptions: { async getTableColumns(this: ILoadOptionsFunctions) { const docId = this.getNodeParameter('docId', 0) as string; const tableId = this.getNodeParameter('tableId', 0) as string; const endpoint = `/docs/${docId}/tables/${tableId}/columns`; const { columns } = await gristApiRequest.call(this, 'GET', endpoint) as GristColumns; return columns.map(({ id }) => ({ name: id, value: id })); }, }, credentialTest: { async gristApiTest( this: ICredentialTestFunctions, credential: ICredentialsDecrypted, ): Promise { const { apiKey, planType, customSubdomain, selfHostedUrl, } = credential.data as GristCredentials; const endpoint = '/orgs'; const gristapiurl = (planType === 'free') ? `https://docs.getgrist.com/api${endpoint}` : (planType === 'paid') ? `https://${customSubdomain}.getgrist.com/api${endpoint}` : `${selfHostedUrl}/api${endpoint}`; const options: OptionsWithUri = { headers: { Authorization: `Bearer ${apiKey}`, }, method: 'GET', uri: gristapiurl, qs: { limit: 1 }, json: true, }; try { await this.helpers.request(options); return { status: 'OK', message: 'Authentication successful', }; } catch (error) { return { status: 'Error', message: error.message, }; } }, }, }; async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); let responseData; const returnData: IDataObject[] = []; const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < items.length; i++) { try { if (operation === 'create') { // ---------------------------------- // create // ---------------------------------- // https://support.getgrist.com/api/#tag/records/paths/~1docs~1{docId}~1tables~1{tableId}~1records/post const body = { records: [] } as GristCreateRowPayload; const dataToSend = this.getNodeParameter('dataToSend', 0) as SendingOptions; if (dataToSend === 'autoMapInputs') { const incomingKeys = Object.keys(items[i].json); const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; const inputsToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); const fields = parseAutoMappedInputs(incomingKeys, inputsToIgnore, items[i].json); body.records.push({ fields }); } else if (dataToSend === 'defineInNode') { const { properties } = this.getNodeParameter('fieldsToSend', i, []) as FieldsToSend; throwOnZeroDefinedFields.call(this, properties); body.records.push({ fields: parseDefinedFields(properties) }); } const docId = this.getNodeParameter('docId', 0) as string; const tableId = this.getNodeParameter('tableId', 0) as string; const endpoint = `/docs/${docId}/tables/${tableId}/records`; responseData = await gristApiRequest.call(this, 'POST', endpoint, body); responseData = { id: responseData.records[0].id, ...body.records[0].fields, }; } else if (operation === 'delete') { // ---------------------------------- // delete // ---------------------------------- // https://support.getgrist.com/api/#tag/data/paths/~1docs~1{docId}~1tables~1{tableId}~1data~1delete/post const docId = this.getNodeParameter('docId', 0) as string; const tableId = this.getNodeParameter('tableId', 0) as string; const endpoint = `/docs/${docId}/tables/${tableId}/data/delete`; const rawRowIds = (this.getNodeParameter('rowId', i) as string).toString(); const body = rawRowIds.split(',').map(c => c.trim()).map(Number); await gristApiRequest.call(this, 'POST', endpoint, body); responseData = { success: true }; } else if (operation === 'update') { // ---------------------------------- // update // ---------------------------------- // https://support.getgrist.com/api/#tag/records/paths/~1docs~1{docId}~1tables~1{tableId}~1records/patch const body = { records: [] } as GristUpdateRowPayload; const rowId = this.getNodeParameter('rowId', i) as string; const dataToSend = this.getNodeParameter('dataToSend', 0) as SendingOptions; if (dataToSend === 'autoMapInputs') { const incomingKeys = Object.keys(items[i].json); const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; const inputsToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); const fields = parseAutoMappedInputs(incomingKeys, inputsToIgnore, items[i].json); body.records.push({ id: Number(rowId), fields }); } else if (dataToSend === 'defineInNode') { const { properties } = this.getNodeParameter('fieldsToSend', i, []) as FieldsToSend; throwOnZeroDefinedFields.call(this, properties); const fields = parseDefinedFields(properties); body.records.push({ id: Number(rowId), fields }); } const docId = this.getNodeParameter('docId', 0) as string; const tableId = this.getNodeParameter('tableId', 0) as string; const endpoint = `/docs/${docId}/tables/${tableId}/records`; await gristApiRequest.call(this, 'PATCH', endpoint, body); responseData = { id: rowId, ...body.records[0].fields, }; } else if (operation === 'getAll') { // ---------------------------------- // getAll // ---------------------------------- // https://support.getgrist.com/api/#tag/records const docId = this.getNodeParameter('docId', 0) as string; const tableId = this.getNodeParameter('tableId', 0) as string; const endpoint = `/docs/${docId}/tables/${tableId}/records`; const qs: IDataObject = {}; const returnAll = this.getNodeParameter('returnAll', i) as boolean; if (!returnAll) { qs.limit = this.getNodeParameter('limit', i) as number; } const { sort, filter } = this.getNodeParameter('additionalOptions', i) as GristGetAllOptions; if (sort?.sortProperties.length) { qs.sort = parseSortProperties(sort.sortProperties); } if (filter?.filterProperties.length) { const parsed = parseFilterProperties(filter.filterProperties); qs.filter = JSON.stringify(parsed); } responseData = await gristApiRequest.call(this, 'GET', endpoint, {}, qs); responseData = responseData.records.map((data: IDataObject) => { return { id: data.id, ...(data.fields as object) }; }); } } catch (error) { if (this.continueOnFail()) { returnData.push({ error: error.message }); continue; } throw error; } Array.isArray(responseData) ? returnData.push(...responseData) : returnData.push(responseData); } return [this.helpers.returnJsonArray(returnData)]; } }