From ef9d4aba90c92f9b72a17de242a4ffeb7c034802 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Wed, 22 May 2024 15:28:09 +0300 Subject: [PATCH] fix: Update operations to run per item (#8967) Co-authored-by: Elias Meire --- .../credentials/CodaApi.credentials.ts | 11 +- .../nodes/Airtable/Airtable.node.ts | 3 +- .../v2/actions/record/search.operation.ts | 122 +++++---- .../Airtable/v2/actions/versionDescription.ts | 2 +- packages/nodes-base/nodes/Coda/Coda.node.ts | 257 ++++++++++-------- .../nodes-base/nodes/Coda/TableDescription.ts | 3 +- .../GoogleFirebaseCloudFirestore.node.ts | 215 ++++++++------- .../actions/spreadsheet/delete.operation.ts | 1 - .../nodes/Microsoft/Sql/MicrosoftSql.node.ts | 79 ++++-- .../nodes/MongoDb/GenericFunctions.ts | 8 +- .../nodes-base/nodes/MongoDb/MongoDb.node.ts | 233 ++++++++-------- .../nodes/RssFeedRead/RssFeedRead.node.ts | 119 +++++--- 12 files changed, 611 insertions(+), 442 deletions(-) diff --git a/packages/nodes-base/credentials/CodaApi.credentials.ts b/packages/nodes-base/credentials/CodaApi.credentials.ts index 2639c660b7..06a96a7cbc 100644 --- a/packages/nodes-base/credentials/CodaApi.credentials.ts +++ b/packages/nodes-base/credentials/CodaApi.credentials.ts @@ -1,4 +1,4 @@ -import type { ICredentialType, INodeProperties } from 'n8n-workflow'; +import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow'; export class CodaApi implements ICredentialType { name = 'codaApi'; @@ -16,4 +16,13 @@ export class CodaApi implements ICredentialType { default: '', }, ]; + + test: ICredentialTestRequest = { + request: { + baseURL: 'https://coda.io/apis/v1/whoami', + headers: { + Authorization: '=Bearer {{$credentials.accessToken}}', + }, + }, + }; } diff --git a/packages/nodes-base/nodes/Airtable/Airtable.node.ts b/packages/nodes-base/nodes/Airtable/Airtable.node.ts index 5c3d15d0d9..8478bc3ff4 100644 --- a/packages/nodes-base/nodes/Airtable/Airtable.node.ts +++ b/packages/nodes-base/nodes/Airtable/Airtable.node.ts @@ -12,12 +12,13 @@ export class Airtable extends VersionedNodeType { icon: 'file:airtable.svg', group: ['input'], description: 'Read, update, write and delete data from Airtable', - defaultVersion: 2, + defaultVersion: 2.1, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new AirtableV1(baseDescription), 2: new AirtableV2(baseDescription), + 2.1: new AirtableV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/record/search.operation.ts b/packages/nodes-base/nodes/Airtable/v2/actions/record/search.operation.ts index e731f8b170..05a26e29a8 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/record/search.operation.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/record/search.operation.ts @@ -149,74 +149,90 @@ export async function execute( base: string, table: string, ): Promise { - let returnData: INodeExecutionData[] = []; - - const body: IDataObject = {}; - const qs: IDataObject = {}; + const returnData: INodeExecutionData[] = []; + const nodeVersion = this.getNode().typeVersion; const endpoint = `${base}/${table}`; - try { - const returnAll = this.getNodeParameter('returnAll', 0); - const options = this.getNodeParameter('options', 0, {}); - const sort = this.getNodeParameter('sort', 0, {}) as IDataObject; - const filterByFormula = this.getNodeParameter('filterByFormula', 0) as string; + let itemsLength = items.length ? 1 : 0; + let fallbackPairedItems; - if (filterByFormula) { - qs.filterByFormula = filterByFormula; - } + if (nodeVersion >= 2.1) { + itemsLength = items.length; + } else { + fallbackPairedItems = generatePairedItemData(items.length); + } - if (options.fields) { - if (typeof options.fields === 'string') { - qs.fields = options.fields.split(',').map((field) => field.trim()); - } else { - qs.fields = options.fields as string[]; + for (let i = 0; i < itemsLength; i++) { + try { + const returnAll = this.getNodeParameter('returnAll', i); + const options = this.getNodeParameter('options', i, {}); + const sort = this.getNodeParameter('sort', i, {}) as IDataObject; + const filterByFormula = this.getNodeParameter('filterByFormula', i) as string; + + const body: IDataObject = {}; + const qs: IDataObject = {}; + + if (filterByFormula) { + qs.filterByFormula = filterByFormula; } - } - if (sort.property) { - qs.sort = sort.property; - } + if (options.fields) { + if (typeof options.fields === 'string') { + qs.fields = options.fields.split(',').map((field) => field.trim()); + } else { + qs.fields = options.fields as string[]; + } + } - if (options.view) { - qs.view = (options.view as IDataObject).value as string; - } + if (sort.property) { + qs.sort = sort.property; + } - let responseData; + if (options.view) { + qs.view = (options.view as IDataObject).value as string; + } - if (returnAll) { - responseData = await apiRequestAllItems.call(this, 'GET', endpoint, body, qs); - } else { - qs.maxRecords = this.getNodeParameter('limit', 0); - responseData = await apiRequest.call(this, 'GET', endpoint, body, qs); - } + let responseData; - returnData = responseData.records as INodeExecutionData[]; + if (returnAll) { + responseData = await apiRequestAllItems.call(this, 'GET', endpoint, body, qs); + } else { + qs.maxRecords = this.getNodeParameter('limit', i); + responseData = await apiRequest.call(this, 'GET', endpoint, body, qs); + } - if (options.downloadFields) { - const pairedItem = generatePairedItemData(items.length); - return await downloadRecordAttachments.call( - this, - responseData.records as IRecord[], - options.downloadFields as string[], - pairedItem, - ); - } + if (options.downloadFields) { + const itemWithAttachments = await downloadRecordAttachments.call( + this, + responseData.records as IRecord[], + options.downloadFields as string[], + fallbackPairedItems || [{ item: i }], + ); + returnData.push(...itemWithAttachments); + continue; + } - returnData = returnData.map((record) => ({ - json: flattenOutput(record as IDataObject), - })); + let records = responseData.records; - const itemData = generatePairedItemData(items.length); + records = (records as IDataObject[]).map((record) => ({ + json: flattenOutput(record), + })) as INodeExecutionData[]; - returnData = this.helpers.constructExecutionMetaData(returnData, { - itemData, - }); - } catch (error) { - if (this.continueOnFail()) { - returnData.push({ json: { message: error.message, error } }); - } else { - throw error; + const itemData = fallbackPairedItems || [{ item: i }]; + + const executionData = this.helpers.constructExecutionMetaData(records, { + itemData, + }); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { message: error.message, error }, pairedItem: { item: i } }); + continue; + } else { + throw error; + } } } diff --git a/packages/nodes-base/nodes/Airtable/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Airtable/v2/actions/versionDescription.ts index 3665469456..176d4b3ae9 100644 --- a/packages/nodes-base/nodes/Airtable/v2/actions/versionDescription.ts +++ b/packages/nodes-base/nodes/Airtable/v2/actions/versionDescription.ts @@ -9,7 +9,7 @@ export const versionDescription: INodeTypeDescription = { name: 'airtable', icon: 'file:airtable.svg', group: ['input'], - version: 2, + version: [2, 2.1], subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}', description: 'Read, update, write and delete data from Airtable', defaults: { diff --git a/packages/nodes-base/nodes/Coda/Coda.node.ts b/packages/nodes-base/nodes/Coda/Coda.node.ts index 3a9644bb44..49dd337e4e 100644 --- a/packages/nodes-base/nodes/Coda/Coda.node.ts +++ b/packages/nodes-base/nodes/Coda/Coda.node.ts @@ -21,7 +21,7 @@ export class Coda implements INodeType { name: 'coda', icon: 'file:coda.svg', group: ['output'], - version: 1, + version: [1, 1.1], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Consume Coda API', defaults: { @@ -240,6 +240,7 @@ export class Coda implements INodeType { }; async execute(this: IExecuteFunctions): Promise { + const nodeVersion = this.getNode().typeVersion; const returnData: INodeExecutionData[] = []; const items = this.getInputData(); let responseData; @@ -363,61 +364,83 @@ export class Coda implements INodeType { } // 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); - const tableId = this.getNodeParameter('tableId', 0) as string; - const options = this.getNodeParameter('options', 0); - const endpoint = `/docs/${docId}/tables/${tableId}/rows`; - if (options.useColumnNames === false) { - qs.useColumnNames = options.useColumnNames as boolean; - } else { - qs.useColumnNames = true; - } - if (options.valueFormat) { - qs.valueFormat = options.valueFormat as string; - } - if (options.sortBy) { - qs.sortBy = options.sortBy as string; - } - if (options.visibleOnly) { - qs.visibleOnly = options.visibleOnly as boolean; - } - if (options.query) { - qs.query = options.query as string; - } - try { - if (returnAll) { - responseData = await codaApiRequestAllItems.call( - this, - 'items', - 'GET', - endpoint, - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', 0); - responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); - responseData = responseData.items; - } - } catch (error) { - if (this.continueOnFail()) { - return [this.helpers.returnJsonArray({ error: error.message })]; - } - throw new NodeApiError(this.getNode(), error as JsonObject); + let itemsLength = items.length ? 1 : 0; + + if (nodeVersion >= 1.1) { + itemsLength = items.length; } - if (options.rawData === true) { - return [this.helpers.returnJsonArray(responseData as IDataObject[])]; - } else { - for (const item of responseData) { - returnData.push({ - id: item.id, - ...item.values, - }); + for (let i = 0; i < itemsLength; i++) { + const docId = this.getNodeParameter('docId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const tableId = this.getNodeParameter('tableId', i) as string; + const options = this.getNodeParameter('options', i); + const endpoint = `/docs/${docId}/tables/${tableId}/rows`; + if (options.useColumnNames === false) { + qs.useColumnNames = options.useColumnNames as boolean; + } else { + qs.useColumnNames = true; + } + if (options.valueFormat) { + qs.valueFormat = options.valueFormat as string; + } + if (options.sortBy) { + qs.sortBy = options.sortBy as string; + } + if (options.visibleOnly) { + qs.visibleOnly = options.visibleOnly as boolean; + } + if (options.query) { + qs.query = options.query as string; + } + try { + if (returnAll) { + responseData = await codaApiRequestAllItems.call( + this, + 'items', + 'GET', + endpoint, + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); + responseData = responseData.items; + } + + if (options.rawData === true) { + for (const item of responseData) { + returnData.push({ + json: item, + pairedItem: [{ item: i }], + }); + } + } else { + for (const item of responseData) { + returnData.push({ + json: { + id: item.id, + ...item.values, + }, + pairedItem: [{ item: i }], + }); + } + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: error.message }, + pairedItem: [{ item: i }], + }); + continue; + } + if (error instanceof NodeApiError) throw error; + throw new NodeApiError(this.getNode(), error as JsonObject); } - return [this.helpers.returnJsonArray(returnData)]; } + + return [returnData]; } // https://coda.io/developers/apis/v1beta1#operation/deleteRows if (operation === 'deleteRow') { @@ -630,15 +653,15 @@ export class Coda implements INodeType { } //https://coda.io/developers/apis/v1beta1#operation/listControls if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', 0); + qs.limit = this.getNodeParameter('limit', 0); for (let i = 0; i < items.length; i++) { try { - const returnAll = this.getNodeParameter('returnAll', 0); const docId = this.getNodeParameter('docId', i) as string; const endpoint = `/docs/${docId}/controls`; if (returnAll) { responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}); } else { - qs.limit = this.getNodeParameter('limit', 0); responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); responseData = responseData.items; } @@ -680,15 +703,15 @@ export class Coda implements INodeType { } //https://coda.io/developers/apis/v1beta1#operation/listViews if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', 0); + qs.limit = this.getNodeParameter('limit', 0); for (let i = 0; i < items.length; i++) { try { - const returnAll = this.getNodeParameter('returnAll', 0); const docId = this.getNodeParameter('docId', i) as string; const endpoint = `/docs/${docId}/tables?tableTypes=view`; if (returnAll) { responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}); } else { - qs.limit = this.getNodeParameter('limit', 0); responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); responseData = responseData.items; } @@ -712,58 +735,80 @@ export class Coda implements INodeType { return [returnData]; } if (operation === 'getAllViewRows') { - const docId = this.getNodeParameter('docId', 0) as string; - const returnAll = this.getNodeParameter('returnAll', 0); - const viewId = this.getNodeParameter('viewId', 0) as string; - const options = this.getNodeParameter('options', 0); - const endpoint = `/docs/${docId}/tables/${viewId}/rows`; - if (options.useColumnNames === false) { - qs.useColumnNames = options.useColumnNames as boolean; - } else { - qs.useColumnNames = true; - } - if (options.valueFormat) { - qs.valueFormat = options.valueFormat as string; - } - if (options.sortBy) { - qs.sortBy = options.sortBy as string; - } - if (options.query) { - qs.query = options.query as string; - } - try { - if (returnAll) { - responseData = await codaApiRequestAllItems.call( - this, - 'items', - 'GET', - endpoint, - {}, - qs, - ); - } else { - qs.limit = this.getNodeParameter('limit', 0); - responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); - responseData = responseData.items; - } - } catch (error) { - if (this.continueOnFail()) { - return [this.helpers.returnJsonArray({ error: error.message })]; - } - throw new NodeApiError(this.getNode(), error as JsonObject); + let itemsLength = items.length ? 1 : 0; + + if (nodeVersion >= 1.1) { + itemsLength = items.length; } - if (options.rawData === true) { - return [this.helpers.returnJsonArray(responseData as IDataObject[])]; - } else { - for (const item of responseData) { - returnData.push({ - id: item.id, - ...item.values, - }); + for (let i = 0; i < itemsLength; i++) { + const docId = this.getNodeParameter('docId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const viewId = this.getNodeParameter('viewId', i) as string; + const options = this.getNodeParameter('options', i); + const endpoint = `/docs/${docId}/tables/${viewId}/rows`; + if (options.useColumnNames === false) { + qs.useColumnNames = options.useColumnNames as boolean; + } else { + qs.useColumnNames = true; + } + if (options.valueFormat) { + qs.valueFormat = options.valueFormat as string; + } + if (options.sortBy) { + qs.sortBy = options.sortBy as string; + } + if (options.query) { + qs.query = options.query as string; + } + try { + if (returnAll) { + responseData = await codaApiRequestAllItems.call( + this, + 'items', + 'GET', + endpoint, + {}, + qs, + ); + } else { + qs.limit = this.getNodeParameter('limit', i); + responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); + responseData = responseData.items; + } + + if (options.rawData === true) { + for (const item of responseData) { + returnData.push({ + json: item, + pairedItem: [{ item: i }], + }); + } + } else { + for (const item of responseData) { + returnData.push({ + json: { + id: item.id, + ...item.values, + }, + pairedItem: [{ item: i }], + }); + } + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: error.message }, + pairedItem: [{ item: i }], + }); + continue; + } + if (error instanceof NodeApiError) throw error; + throw new NodeApiError(this.getNode(), error as JsonObject); } - return [this.helpers.returnJsonArray(returnData)]; } + + return [returnData]; } //https://coda.io/developers/apis/v1beta1#operation/deleteViewRow if (operation === 'deleteViewRow') { @@ -823,16 +868,16 @@ export class Coda implements INodeType { return [returnData]; } if (operation === 'getAllViewColumns') { + const returnAll = this.getNodeParameter('returnAll', 0); + qs.limit = this.getNodeParameter('limit', 0); for (let i = 0; i < items.length; i++) { try { - const returnAll = this.getNodeParameter('returnAll', 0); const docId = this.getNodeParameter('docId', i) as string; const viewId = this.getNodeParameter('viewId', i) as string; const endpoint = `/docs/${docId}/tables/${viewId}/columns`; if (returnAll) { responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}); } else { - qs.limit = this.getNodeParameter('limit', 0); responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); responseData = responseData.items; } diff --git a/packages/nodes-base/nodes/Coda/TableDescription.ts b/packages/nodes-base/nodes/Coda/TableDescription.ts index ec48fb15be..6edea4a2e5 100644 --- a/packages/nodes-base/nodes/Coda/TableDescription.ts +++ b/packages/nodes-base/nodes/Coda/TableDescription.ts @@ -27,12 +27,13 @@ export const tableOperations: INodeProperties[] = [ { name: 'Get All Columns', value: 'getAllColumns', + description: 'Get all columns in a table', action: 'Get all columns', }, { name: 'Get All Rows', value: 'getAllRows', - description: 'Get all the rows', + description: 'Get all rows in a table', action: 'Get all rows', }, { diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/GoogleFirebaseCloudFirestore.node.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/GoogleFirebaseCloudFirestore.node.ts index 49c76afbb4..edd70cc48b 100644 --- a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/GoogleFirebaseCloudFirestore.node.ts +++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/GoogleFirebaseCloudFirestore.node.ts @@ -28,7 +28,7 @@ export class GoogleFirebaseCloudFirestore implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg icon: 'file:googleFirebaseCloudFirestore.png', group: ['input'], - version: 1, + version: [1, 1.1], subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', description: 'Interact with Google Firebase - Cloud Firestore API', defaults: { @@ -94,15 +94,27 @@ export class GoogleFirebaseCloudFirestore implements INodeType { const itemData = generatePairedItemData(items.length); const returnData: INodeExecutionData[] = []; let responseData; + const resource = this.getNodeParameter('resource', 0); const operation = this.getNodeParameter('operation', 0); + const nodeVersion = this.getNode().typeVersion; + + let itemsLength = items.length ? 1 : 0; + let fallbackPairedItems; + + if (nodeVersion >= 1.1) { + itemsLength = items.length; + } else { + fallbackPairedItems = generatePairedItemData(items.length); + } + if (resource === 'document') { if (operation === 'get') { const projectId = this.getNodeParameter('projectId', 0) as string; const database = this.getNodeParameter('database', 0) as string; const simple = this.getNodeParameter('simple', 0) as boolean; - const documentList = items.map((item: IDataObject, i: number) => { + const documentList = items.map((_: IDataObject, i: number) => { const collection = this.getNodeParameter('collection', i) as string; const documentId = this.getNodeParameter('documentId', i) as string; return `projects/${projectId}/databases/${database}/documents/${collection}/${documentId}`; @@ -179,49 +191,64 @@ export class GoogleFirebaseCloudFirestore implements INodeType { }), ); } else if (operation === 'getAll') { - const projectId = this.getNodeParameter('projectId', 0) as string; - const database = this.getNodeParameter('database', 0) as string; - const collection = this.getNodeParameter('collection', 0) as string; - const returnAll = this.getNodeParameter('returnAll', 0); - const simple = this.getNodeParameter('simple', 0) as boolean; + for (let i = 0; i < itemsLength; i++) { + try { + const projectId = this.getNodeParameter('projectId', i) as string; + const database = this.getNodeParameter('database', i) as string; + const collection = this.getNodeParameter('collection', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); + const simple = this.getNodeParameter('simple', i) as boolean; - if (returnAll) { - responseData = await googleApiRequestAllItems.call( - this, - 'documents', - 'GET', - `/${projectId}/databases/${database}/documents/${collection}`, - ); - } else { - const limit = this.getNodeParameter('limit', 0); - const getAllResponse = (await googleApiRequest.call( - this, - 'GET', - `/${projectId}/databases/${database}/documents/${collection}`, - {}, - { pageSize: limit }, - )) as IDataObject; - responseData = getAllResponse.documents; + if (returnAll) { + responseData = await googleApiRequestAllItems.call( + this, + 'documents', + 'GET', + `/${projectId}/databases/${database}/documents/${collection}`, + ); + } else { + const limit = this.getNodeParameter('limit', i); + const getAllResponse = (await googleApiRequest.call( + this, + 'GET', + `/${projectId}/databases/${database}/documents/${collection}`, + {}, + { pageSize: limit }, + )) as IDataObject; + responseData = getAllResponse.documents; + } + + responseData = responseData.map((element: IDataObject) => { + element.id = (element.name as string).split('/').pop(); + return element; + }); + + if (simple) { + responseData = responseData.map((element: IDataObject) => + fullDocumentToJson(element), + ); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: fallbackPairedItems ?? [{ item: i }] }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: error.message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } + throw error; + } } - - responseData = responseData.map((element: IDataObject) => { - element.id = (element.name as string).split('/').pop(); - return element; - }); - - if (simple) { - responseData = responseData.map((element: IDataObject) => fullDocumentToJson(element)); - } - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData }, - ); - - returnData.push(...executionData); } else if (operation === 'delete') { await Promise.all( - items.map(async (item: IDataObject, i: number) => { + items.map(async (_: IDataObject, i: number) => { const projectId = this.getNodeParameter('projectId', i) as string; const database = this.getNodeParameter('database', i) as string; const collection = this.getNodeParameter('collection', i) as string; @@ -295,44 +322,13 @@ export class GoogleFirebaseCloudFirestore implements INodeType { returnData.push(...executionData); } - - // } else if (operation === 'update') { - // const projectId = this.getNodeParameter('projectId', 0) as string; - // const database = this.getNodeParameter('database', 0) as string; - // const simple = this.getNodeParameter('simple', 0) as boolean; - - // await Promise.all(items.map(async (item: IDataObject, i: number) => { - // const collection = this.getNodeParameter('collection', i) as string; - // const updateKey = this.getNodeParameter('updateKey', i) as string; - // // @ts-ignore - // const documentId = item['json'][updateKey] as string; - // const columns = this.getNodeParameter('columns', i) as string; - // const columnList = columns.split(',').map(column => column.trim()) as string[]; - // const document = {}; - // columnList.map(column => { - // // @ts-ignore - // document[column] = item['json'].hasOwnProperty(column) ? jsonToDocument(item['json'][column]) : jsonToDocument(null); - // }); - // responseData = await googleApiRequest.call( - // this, - // 'PATCH', - // `/${projectId}/databases/${database}/documents/${collection}/${documentId}`, - // { fields: document }, - // { [`updateMask.fieldPaths`]: columnList }, - // ); - // if (simple === false) { - // returnData.push(responseData); - // } else { - // returnData.push(fullDocumentToJson(responseData as IDataObject)); - // } - // })); } else if (operation === 'query') { const projectId = this.getNodeParameter('projectId', 0) as string; const database = this.getNodeParameter('database', 0) as string; const simple = this.getNodeParameter('simple', 0) as boolean; await Promise.all( - items.map(async (item: IDataObject, i: number) => { + items.map(async (_: IDataObject, i: number) => { const query = this.getNodeParameter('query', i) as string; responseData = await googleApiRequest.call( this, @@ -369,38 +365,51 @@ export class GoogleFirebaseCloudFirestore implements INodeType { } } else if (resource === 'collection') { if (operation === 'getAll') { - const projectId = this.getNodeParameter('projectId', 0) as string; - const database = this.getNodeParameter('database', 0) as string; - const returnAll = this.getNodeParameter('returnAll', 0); + for (let i = 0; i < itemsLength; i++) { + try { + const projectId = this.getNodeParameter('projectId', i) as string; + const database = this.getNodeParameter('database', i) as string; + const returnAll = this.getNodeParameter('returnAll', i); - if (returnAll) { - const getAllResponse = await googleApiRequestAllItems.call( - this, - 'collectionIds', - 'POST', - `/${projectId}/databases/${database}/documents:listCollectionIds`, - ); - // @ts-ignore - responseData = getAllResponse.map((o) => ({ name: o })); - } else { - const limit = this.getNodeParameter('limit', 0); - const getAllResponse = (await googleApiRequest.call( - this, - 'POST', - `/${projectId}/databases/${database}/documents:listCollectionIds`, - {}, - { pageSize: limit }, - )) as IDataObject; - // @ts-ignore - responseData = getAllResponse.collectionIds.map((o) => ({ name: o })); + if (returnAll) { + const getAllResponse = await googleApiRequestAllItems.call( + this, + 'collectionIds', + 'POST', + `/${projectId}/databases/${database}/documents:listCollectionIds`, + ); + // @ts-ignore + responseData = getAllResponse.map((o) => ({ name: o })); + } else { + const limit = this.getNodeParameter('limit', i); + const getAllResponse = (await googleApiRequest.call( + this, + 'POST', + `/${projectId}/databases/${database}/documents:listCollectionIds`, + {}, + { pageSize: limit }, + )) as IDataObject; + // @ts-ignore + responseData = getAllResponse.collectionIds.map((o) => ({ name: o })); + } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: fallbackPairedItems ?? [{ item: i }] }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: error.message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } + throw error; + } } - - const executionData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData as IDataObject[]), - { itemData }, - ); - - returnData.push(...executionData); } } diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/delete.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/delete.operation.ts index 1991b0c17d..310efc8de7 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/delete.operation.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/spreadsheet/delete.operation.ts @@ -69,7 +69,6 @@ export async function execute(this: IExecuteFunctions): Promise= 1.1) { + for (let i = 0; i < items.length; i++) { + try { + let rawQuery = this.getNodeParameter('query', i) as string; + + for (const resolvable of getResolvables(rawQuery)) { + rawQuery = rawQuery.replace( + resolvable, + this.evaluateExpression(resolvable, i) as string, + ); + } + + const { recordsets }: IResult = await pool.request().query(rawQuery); + + const result: IDataObject[] = recordsets.length > 1 ? flatten(recordsets) : recordsets[0]; + + for (const entry of result) { + returnData.push({ + json: entry, + pairedItem: [{ item: i }], + }); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: error.message }, + pairedItem: [{ item: i }], + }); + continue; + } + await pool.close(); + throw error; + } + } + + await pool.close(); + return [returnData]; + } try { if (operation === 'executeQuery') { let rawQuery = this.getNodeParameter('query', 0) as string; @@ -267,15 +306,19 @@ export class MicrosoftSql implements INodeType { const result = recordsets.length > 1 ? flatten(recordsets) : recordsets[0]; responseData = result; - } else if (operation === 'insert') { + } + + if (operation === 'insert') { const tables = createTableStruct(this.getNodeParameter, items); await insertOperation(tables, pool); responseData = items; - } else if (operation === 'update') { + } + + if (operation === 'update') { const updateKeys = items.map( - (item, index) => this.getNodeParameter('updateKey', index) as string, + (_, index) => this.getNodeParameter('updateKey', index) as string, ); const tables = createTableStruct( @@ -288,7 +331,9 @@ export class MicrosoftSql implements INodeType { await updateOperation(tables, pool); responseData = items; - } else if (operation === 'delete') { + } + + if (operation === 'delete') { const tables = items.reduce((acc, item, index) => { const table = this.getNodeParameter('table', index) as string; const deleteKey = this.getNodeParameter('deleteKey', index) as string; @@ -303,13 +348,14 @@ export class MicrosoftSql implements INodeType { }, {} as ITables); responseData = await deleteOperation(tables, pool); - } else { - await pool.close(); - throw new NodeOperationError( - this.getNode(), - `The operation "${operation}" is not supported!`, - ); } + + const itemData = generatePairedItemData(items.length); + + returnData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData }, + ); } catch (error) { if (this.continueOnFail()) { responseData = items; @@ -322,13 +368,6 @@ export class MicrosoftSql implements INodeType { // shuts down the connection pool associated with the db object to allow the process to finish await pool.close(); - const itemData = generatePairedItemData(items.length); - - const returnItems = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData), - { itemData }, - ); - - return [returnItems]; + return [returnData]; } } diff --git a/packages/nodes-base/nodes/MongoDb/GenericFunctions.ts b/packages/nodes-base/nodes/MongoDb/GenericFunctions.ts index 552ea52a38..32adcf0b79 100644 --- a/packages/nodes-base/nodes/MongoDb/GenericFunctions.ts +++ b/packages/nodes-base/nodes/MongoDb/GenericFunctions.ts @@ -132,15 +132,17 @@ export function prepareFields(fields: string) { .filter((field) => !!field); } -export function stringifyObjectIDs(items: IDataObject[]) { +export function stringifyObjectIDs(items: INodeExecutionData[]) { items.forEach((item) => { if (item._id instanceof ObjectId) { - item._id = item._id.toString(); + item.json._id = item._id.toString(); } if (item.id instanceof ObjectId) { - item.id = item.id.toString(); + item.json.id = item.id.toString(); } }); + + return items; } export async function connectMongoClient(connectionString: string, credentials: IDataObject = {}) { diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index 89e5e1ef0a..32f55d4fbf 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -9,7 +9,7 @@ import type { INodeTypeDescription, JsonObject, } from 'n8n-workflow'; -import { ApplicationError, NodeOperationError } from 'n8n-workflow'; +import { ApplicationError } from 'n8n-workflow'; import type { FindOneAndReplaceOptions, @@ -38,7 +38,7 @@ export class MongoDb implements INodeType { name: 'mongoDb', icon: 'file:mongodb.svg', group: ['input'], - version: 1, + version: [1, 1.1], description: 'Find, insert and update documents in MongoDB', defaults: { name: 'MongoDB', @@ -108,101 +108,126 @@ export class MongoDb implements INodeType { const mdb = client.db(database); - let responseData: IDataObject | IDataObject[] = []; + let returnData: INodeExecutionData[] = []; const items = this.getInputData(); const operation = this.getNodeParameter('operation', 0); + const nodeVersion = this.getNode().typeVersion; + + let itemsLength = items.length ? 1 : 0; + let fallbackPairedItems; + + if (nodeVersion >= 1.1) { + itemsLength = items.length; + } else { + fallbackPairedItems = generatePairedItemData(items.length); + } if (operation === 'aggregate') { - // ---------------------------------- - // aggregate - // ---------------------------------- + for (let i = 0; i < itemsLength; i++) { + try { + const queryParameter = JSON.parse( + this.getNodeParameter('query', i) as string, + ) as IDataObject; - try { - const queryParameter = JSON.parse( - this.getNodeParameter('query', 0) as string, - ) as IDataObject; + if (queryParameter._id && typeof queryParameter._id === 'string') { + queryParameter._id = new ObjectId(queryParameter._id); + } - if (queryParameter._id && typeof queryParameter._id === 'string') { - queryParameter._id = new ObjectId(queryParameter._id); - } + const query = mdb + .collection(this.getNodeParameter('collection', i) as string) + .aggregate(queryParameter as unknown as Document[]); - const query = mdb - .collection(this.getNodeParameter('collection', 0) as string) - .aggregate(queryParameter as unknown as Document[]); - - responseData = await query.toArray(); - } catch (error) { - if (this.continueOnFail()) { - responseData = [{ error: (error as JsonObject).message }]; - } else { + for (const entry of await query.toArray()) { + returnData.push({ json: entry, pairedItem: fallbackPairedItems ?? [{ item: i }] }); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: (error as JsonObject).message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } throw error; } } - } else if (operation === 'delete') { - // ---------------------------------- - // delete - // ---------------------------------- + } - try { - const { deletedCount } = await mdb - .collection(this.getNodeParameter('collection', 0) as string) - .deleteMany(JSON.parse(this.getNodeParameter('query', 0) as string) as Document); + if (operation === 'delete') { + for (let i = 0; i < itemsLength; i++) { + try { + const { deletedCount } = await mdb + .collection(this.getNodeParameter('collection', i) as string) + .deleteMany(JSON.parse(this.getNodeParameter('query', i) as string) as Document); - responseData = [{ deletedCount }]; - } catch (error) { - if (this.continueOnFail()) { - responseData = [{ error: (error as JsonObject).message }]; - } else { + returnData.push({ + json: { deletedCount }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: (error as JsonObject).message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } throw error; } } - } else if (operation === 'find') { - // ---------------------------------- - // find - // ---------------------------------- + } - try { - const queryParameter = JSON.parse( - this.getNodeParameter('query', 0) as string, - ) as IDataObject; + if (operation === 'find') { + for (let i = 0; i < itemsLength; i++) { + try { + const queryParameter = JSON.parse( + this.getNodeParameter('query', i) as string, + ) as IDataObject; - if (queryParameter._id && typeof queryParameter._id === 'string') { - queryParameter._id = new ObjectId(queryParameter._id); - } + if (queryParameter._id && typeof queryParameter._id === 'string') { + queryParameter._id = new ObjectId(queryParameter._id); + } - let query = mdb - .collection(this.getNodeParameter('collection', 0) as string) - .find(queryParameter as unknown as Document); + let query = mdb + .collection(this.getNodeParameter('collection', i) as string) + .find(queryParameter as unknown as Document); - const options = this.getNodeParameter('options', 0); - const limit = options.limit as number; - const skip = options.skip as number; - const sort = options.sort && (JSON.parse(options.sort as string) as Sort); - if (skip > 0) { - query = query.skip(skip); - } - if (limit > 0) { - query = query.limit(limit); - } - if (sort && Object.keys(sort).length !== 0 && sort.constructor === Object) { - query = query.sort(sort); - } - const queryResult = await query.toArray(); + const options = this.getNodeParameter('options', i); + const limit = options.limit as number; + const skip = options.skip as number; + const sort = options.sort && (JSON.parse(options.sort as string) as Sort); - responseData = queryResult; - } catch (error) { - if (this.continueOnFail()) { - responseData = [{ error: (error as JsonObject).message }]; - } else { + if (skip > 0) { + query = query.skip(skip); + } + if (limit > 0) { + query = query.limit(limit); + } + if (sort && Object.keys(sort).length !== 0 && sort.constructor === Object) { + query = query.sort(sort); + } + + const queryResult = await query.toArray(); + + for (const entry of queryResult) { + returnData.push({ json: entry, pairedItem: fallbackPairedItems ?? [{ item: i }] }); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: (error as JsonObject).message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } throw error; } } - } else if (operation === 'findOneAndReplace') { - // ---------------------------------- - // findOneAndReplace - // ---------------------------------- + } + if (operation === 'findOneAndReplace') { + fallbackPairedItems = fallbackPairedItems ?? generatePairedItemData(items.length); const fields = prepareFields(this.getNodeParameter('fields', 0) as string); const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; const dateFields = prepareFields( @@ -237,12 +262,14 @@ export class MongoDb implements INodeType { } } - responseData = updateItems; - } else if (operation === 'findOneAndUpdate') { - // ---------------------------------- - // findOneAndUpdate - // ---------------------------------- + returnData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(updateItems), + { itemData: fallbackPairedItems }, + ); + } + if (operation === 'findOneAndUpdate') { + fallbackPairedItems = fallbackPairedItems ?? generatePairedItemData(items.length); const fields = prepareFields(this.getNodeParameter('fields', 0) as string); const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; const dateFields = prepareFields( @@ -277,11 +304,15 @@ export class MongoDb implements INodeType { } } - responseData = updateItems; - } else if (operation === 'insert') { - // ---------------------------------- - // insert - // ---------------------------------- + returnData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(updateItems), + { itemData: fallbackPairedItems }, + ); + } + + if (operation === 'insert') { + fallbackPairedItems = fallbackPairedItems ?? generatePairedItemData(items.length); + let responseData: IDataObject[] = []; try { // Prepare the data to insert and copy it to be returned const fields = prepareFields(this.getNodeParameter('fields', 0) as string); @@ -310,11 +341,15 @@ export class MongoDb implements INodeType { throw error; } } - } else if (operation === 'update') { - // ---------------------------------- - // update - // ---------------------------------- + returnData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData), + { itemData: fallbackPairedItems }, + ); + } + + if (operation === 'update') { + fallbackPairedItems = fallbackPairedItems ?? generatePairedItemData(items.length); const fields = prepareFields(this.getNodeParameter('fields', 0) as string); const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; const dateFields = prepareFields( @@ -349,30 +384,14 @@ export class MongoDb implements INodeType { } } - responseData = updateItems; - } else { - if (this.continueOnFail()) { - responseData = [{ error: `The operation "${operation}" is not supported!` }]; - } else { - throw new NodeOperationError( - this.getNode(), - `The operation "${operation}" is not supported!`, - { itemIndex: 0 }, - ); - } + returnData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(updateItems), + { itemData: fallbackPairedItems }, + ); } await client.close(); - stringifyObjectIDs(responseData); - - const itemData = generatePairedItemData(items.length); - - const returnItems = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(responseData), - { itemData }, - ); - - return [returnItems]; + return [stringifyObjectIDs(returnData)]; } } diff --git a/packages/nodes-base/nodes/RssFeedRead/RssFeedRead.node.ts b/packages/nodes-base/nodes/RssFeedRead/RssFeedRead.node.ts index 615e21e4ee..e16db2fb1c 100644 --- a/packages/nodes-base/nodes/RssFeedRead/RssFeedRead.node.ts +++ b/packages/nodes-base/nodes/RssFeedRead/RssFeedRead.node.ts @@ -28,7 +28,7 @@ export class RssFeedRead implements INodeType { name: 'rssFeedRead', icon: 'fa:rss', group: ['input'], - version: 1, + version: [1, 1.1], description: 'Reads data from an RSS Feed', defaults: { name: 'RSS Read', @@ -65,59 +65,88 @@ export class RssFeedRead implements INodeType { }; async execute(this: IExecuteFunctions): Promise { - const pairedItem = generatePairedItemData(this.getInputData().length); + const returnData: INodeExecutionData[] = []; + const nodeVersion = this.getNode().typeVersion; + const items = this.getInputData(); - try { - const url = this.getNodeParameter('url', 0) as string; - const options = this.getNodeParameter('options', 0); - const ignoreSSL = Boolean(options.ignoreSSL); + let itemsLength = items.length ? 1 : 0; + let fallbackPairedItems; - if (!url) { - throw new NodeOperationError(this.getNode(), 'The parameter "URL" has to be set!'); - } + if (nodeVersion >= 1.1) { + itemsLength = items.length; + } else { + fallbackPairedItems = generatePairedItemData(items.length); + } - if (!validateURL(url)) { - throw new NodeOperationError(this.getNode(), 'The provided "URL" is not valid!'); - } - - const parser = new Parser({ - requestOptions: { - rejectUnauthorized: !ignoreSSL, - }, - }); - - let feed: Parser.Output; + for (let i = 0; i < itemsLength; i++) { try { - feed = await parser.parseURL(url); - } catch (error) { - if (error.code === 'ECONNREFUSED') { - throw new NodeOperationError( - this.getNode(), - `It was not possible to connect to the URL. Please make sure the URL "${url}" it is valid!`, - ); + const url = this.getNodeParameter('url', i) as string; + const options = this.getNodeParameter('options', i); + const ignoreSSL = Boolean(options.ignoreSSL); + + if (!url) { + throw new NodeOperationError(this.getNode(), 'The parameter "URL" has to be set!', { + itemIndex: i, + }); } - throw new NodeOperationError(this.getNode(), error as Error); - } - - const returnData: INodeExecutionData[] = []; - - // For now we just take the items and ignore everything else - if (feed.items) { - feed.items.forEach((item) => { - returnData.push({ - json: item, - pairedItem, + if (!validateURL(url)) { + throw new NodeOperationError(this.getNode(), 'The provided "URL" is not valid!', { + itemIndex: i, }); - }); - } + } - return [returnData]; - } catch (error) { - if (this.continueOnFail()) { - return [[{ json: { error: error.message }, pairedItem }]]; + const parser = new Parser({ + requestOptions: { + rejectUnauthorized: !ignoreSSL, + }, + }); + + let feed: Parser.Output; + try { + feed = await parser.parseURL(url); + } catch (error) { + if (error.code === 'ECONNREFUSED') { + throw new NodeOperationError( + this.getNode(), + `It was not possible to connect to the URL. Please make sure the URL "${url}" it is valid!`, + { + itemIndex: i, + }, + ); + } + + throw new NodeOperationError(this.getNode(), error as Error, { + itemIndex: i, + }); + } + + // For now we just take the items and ignore everything else + if (feed.items) { + const feedItems = (feed.items as IDataObject[]).map((item) => ({ + json: item, + })) as INodeExecutionData[]; + + const itemData = fallbackPairedItems || [{ item: i }]; + + const executionData = this.helpers.constructExecutionMetaData(feedItems, { + itemData, + }); + + returnData.push(...executionData); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: error.message }, + pairedItem: fallbackPairedItems || [{ item: i }], + }); + continue; + } + throw error; } - throw error; } + + return [returnData]; } }