fix: Update operations to run per item (#8967)

Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Michael Kret 2024-05-22 15:28:09 +03:00 committed by GitHub
parent 870412f093
commit ef9d4aba90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 611 additions and 442 deletions

View file

@ -1,4 +1,4 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow'; import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
export class CodaApi implements ICredentialType { export class CodaApi implements ICredentialType {
name = 'codaApi'; name = 'codaApi';
@ -16,4 +16,13 @@ export class CodaApi implements ICredentialType {
default: '', default: '',
}, },
]; ];
test: ICredentialTestRequest = {
request: {
baseURL: 'https://coda.io/apis/v1/whoami',
headers: {
Authorization: '=Bearer {{$credentials.accessToken}}',
},
},
};
} }

View file

@ -12,12 +12,13 @@ export class Airtable extends VersionedNodeType {
icon: 'file:airtable.svg', icon: 'file:airtable.svg',
group: ['input'], group: ['input'],
description: 'Read, update, write and delete data from Airtable', description: 'Read, update, write and delete data from Airtable',
defaultVersion: 2, defaultVersion: 2.1,
}; };
const nodeVersions: IVersionedNodeType['nodeVersions'] = { const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new AirtableV1(baseDescription), 1: new AirtableV1(baseDescription),
2: new AirtableV2(baseDescription), 2: new AirtableV2(baseDescription),
2.1: new AirtableV2(baseDescription),
}; };
super(nodeVersions, baseDescription); super(nodeVersions, baseDescription);

View file

@ -149,74 +149,90 @@ export async function execute(
base: string, base: string,
table: string, table: string,
): Promise<INodeExecutionData[]> { ): Promise<INodeExecutionData[]> {
let returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];
const nodeVersion = this.getNode().typeVersion;
const body: IDataObject = {};
const qs: IDataObject = {};
const endpoint = `${base}/${table}`; const endpoint = `${base}/${table}`;
try { let itemsLength = items.length ? 1 : 0;
const returnAll = this.getNodeParameter('returnAll', 0); let fallbackPairedItems;
const options = this.getNodeParameter('options', 0, {});
const sort = this.getNodeParameter('sort', 0, {}) as IDataObject;
const filterByFormula = this.getNodeParameter('filterByFormula', 0) as string;
if (filterByFormula) { if (nodeVersion >= 2.1) {
qs.filterByFormula = filterByFormula; itemsLength = items.length;
} } else {
fallbackPairedItems = generatePairedItemData(items.length);
}
if (options.fields) { for (let i = 0; i < itemsLength; i++) {
if (typeof options.fields === 'string') { try {
qs.fields = options.fields.split(',').map((field) => field.trim()); const returnAll = this.getNodeParameter('returnAll', i);
} else { const options = this.getNodeParameter('options', i, {});
qs.fields = options.fields as string[]; 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) { if (options.fields) {
qs.sort = sort.property; if (typeof options.fields === 'string') {
} qs.fields = options.fields.split(',').map((field) => field.trim());
} else {
qs.fields = options.fields as string[];
}
}
if (options.view) { if (sort.property) {
qs.view = (options.view as IDataObject).value as string; qs.sort = sort.property;
} }
let responseData; if (options.view) {
qs.view = (options.view as IDataObject).value as string;
}
if (returnAll) { let responseData;
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);
}
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) { if (options.downloadFields) {
const pairedItem = generatePairedItemData(items.length); const itemWithAttachments = await downloadRecordAttachments.call(
return await downloadRecordAttachments.call( this,
this, responseData.records as IRecord[],
responseData.records as IRecord[], options.downloadFields as string[],
options.downloadFields as string[], fallbackPairedItems || [{ item: i }],
pairedItem, );
); returnData.push(...itemWithAttachments);
} continue;
}
returnData = returnData.map((record) => ({ let records = responseData.records;
json: flattenOutput(record as IDataObject),
}));
const itemData = generatePairedItemData(items.length); records = (records as IDataObject[]).map((record) => ({
json: flattenOutput(record),
})) as INodeExecutionData[];
returnData = this.helpers.constructExecutionMetaData(returnData, { const itemData = fallbackPairedItems || [{ item: i }];
itemData,
}); const executionData = this.helpers.constructExecutionMetaData(records, {
} catch (error) { itemData,
if (this.continueOnFail()) { });
returnData.push({ json: { message: error.message, error } });
} else { returnData.push(...executionData);
throw error; } catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { message: error.message, error }, pairedItem: { item: i } });
continue;
} else {
throw error;
}
} }
} }

View file

@ -9,7 +9,7 @@ export const versionDescription: INodeTypeDescription = {
name: 'airtable', name: 'airtable',
icon: 'file:airtable.svg', icon: 'file:airtable.svg',
group: ['input'], group: ['input'],
version: 2, version: [2, 2.1],
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}', subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
description: 'Read, update, write and delete data from Airtable', description: 'Read, update, write and delete data from Airtable',
defaults: { defaults: {

View file

@ -21,7 +21,7 @@ export class Coda implements INodeType {
name: 'coda', name: 'coda',
icon: 'file:coda.svg', icon: 'file:coda.svg',
group: ['output'], group: ['output'],
version: 1, version: [1, 1.1],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Coda API', description: 'Consume Coda API',
defaults: { defaults: {
@ -240,6 +240,7 @@ export class Coda implements INodeType {
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const nodeVersion = this.getNode().typeVersion;
const returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];
const items = this.getInputData(); const items = this.getInputData();
let responseData; let responseData;
@ -363,61 +364,83 @@ export class Coda implements INodeType {
} }
// https://coda.io/developers/apis/v1beta1#operation/listRows // https://coda.io/developers/apis/v1beta1#operation/listRows
if (operation === 'getAllRows') { if (operation === 'getAllRows') {
const docId = this.getNodeParameter('docId', 0) as string; let itemsLength = items.length ? 1 : 0;
const returnAll = this.getNodeParameter('returnAll', 0);
const tableId = this.getNodeParameter('tableId', 0) as string; if (nodeVersion >= 1.1) {
const options = this.getNodeParameter('options', 0); itemsLength = items.length;
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);
} }
if (options.rawData === true) { for (let i = 0; i < itemsLength; i++) {
return [this.helpers.returnJsonArray(responseData as IDataObject[])]; const docId = this.getNodeParameter('docId', i) as string;
} else { const returnAll = this.getNodeParameter('returnAll', i);
for (const item of responseData) { const tableId = this.getNodeParameter('tableId', i) as string;
returnData.push({ const options = this.getNodeParameter('options', i);
id: item.id, const endpoint = `/docs/${docId}/tables/${tableId}/rows`;
...item.values, 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 // https://coda.io/developers/apis/v1beta1#operation/deleteRows
if (operation === 'deleteRow') { if (operation === 'deleteRow') {
@ -630,15 +653,15 @@ export class Coda implements INodeType {
} }
//https://coda.io/developers/apis/v1beta1#operation/listControls //https://coda.io/developers/apis/v1beta1#operation/listControls
if (operation === 'getAll') { if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', 0);
qs.limit = this.getNodeParameter('limit', 0);
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
try { try {
const returnAll = this.getNodeParameter('returnAll', 0);
const docId = this.getNodeParameter('docId', i) as string; const docId = this.getNodeParameter('docId', i) as string;
const endpoint = `/docs/${docId}/controls`; const endpoint = `/docs/${docId}/controls`;
if (returnAll) { if (returnAll) {
responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}); responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {});
} else { } else {
qs.limit = this.getNodeParameter('limit', 0);
responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.items; responseData = responseData.items;
} }
@ -680,15 +703,15 @@ export class Coda implements INodeType {
} }
//https://coda.io/developers/apis/v1beta1#operation/listViews //https://coda.io/developers/apis/v1beta1#operation/listViews
if (operation === 'getAll') { if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', 0);
qs.limit = this.getNodeParameter('limit', 0);
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
try { try {
const returnAll = this.getNodeParameter('returnAll', 0);
const docId = this.getNodeParameter('docId', i) as string; const docId = this.getNodeParameter('docId', i) as string;
const endpoint = `/docs/${docId}/tables?tableTypes=view`; const endpoint = `/docs/${docId}/tables?tableTypes=view`;
if (returnAll) { if (returnAll) {
responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}); responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {});
} else { } else {
qs.limit = this.getNodeParameter('limit', 0);
responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.items; responseData = responseData.items;
} }
@ -712,58 +735,80 @@ export class Coda implements INodeType {
return [returnData]; return [returnData];
} }
if (operation === 'getAllViewRows') { if (operation === 'getAllViewRows') {
const docId = this.getNodeParameter('docId', 0) as string; let itemsLength = items.length ? 1 : 0;
const returnAll = this.getNodeParameter('returnAll', 0);
const viewId = this.getNodeParameter('viewId', 0) as string; if (nodeVersion >= 1.1) {
const options = this.getNodeParameter('options', 0); itemsLength = items.length;
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);
} }
if (options.rawData === true) { for (let i = 0; i < itemsLength; i++) {
return [this.helpers.returnJsonArray(responseData as IDataObject[])]; const docId = this.getNodeParameter('docId', i) as string;
} else { const returnAll = this.getNodeParameter('returnAll', i);
for (const item of responseData) { const viewId = this.getNodeParameter('viewId', i) as string;
returnData.push({ const options = this.getNodeParameter('options', i);
id: item.id, const endpoint = `/docs/${docId}/tables/${viewId}/rows`;
...item.values, 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 //https://coda.io/developers/apis/v1beta1#operation/deleteViewRow
if (operation === 'deleteViewRow') { if (operation === 'deleteViewRow') {
@ -823,16 +868,16 @@ export class Coda implements INodeType {
return [returnData]; return [returnData];
} }
if (operation === 'getAllViewColumns') { if (operation === 'getAllViewColumns') {
const returnAll = this.getNodeParameter('returnAll', 0);
qs.limit = this.getNodeParameter('limit', 0);
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
try { try {
const returnAll = this.getNodeParameter('returnAll', 0);
const docId = this.getNodeParameter('docId', i) as string; const docId = this.getNodeParameter('docId', i) as string;
const viewId = this.getNodeParameter('viewId', i) as string; const viewId = this.getNodeParameter('viewId', i) as string;
const endpoint = `/docs/${docId}/tables/${viewId}/columns`; const endpoint = `/docs/${docId}/tables/${viewId}/columns`;
if (returnAll) { if (returnAll) {
responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}); responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {});
} else { } else {
qs.limit = this.getNodeParameter('limit', 0);
responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs); responseData = await codaApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.items; responseData = responseData.items;
} }

View file

@ -27,12 +27,13 @@ export const tableOperations: INodeProperties[] = [
{ {
name: 'Get All Columns', name: 'Get All Columns',
value: 'getAllColumns', value: 'getAllColumns',
description: 'Get all columns in a table',
action: 'Get all columns', action: 'Get all columns',
}, },
{ {
name: 'Get All Rows', name: 'Get All Rows',
value: 'getAllRows', value: 'getAllRows',
description: 'Get all the rows', description: 'Get all rows in a table',
action: 'Get all rows', action: 'Get all rows',
}, },
{ {

View file

@ -28,7 +28,7 @@ export class GoogleFirebaseCloudFirestore implements INodeType {
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg // eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
icon: 'file:googleFirebaseCloudFirestore.png', icon: 'file:googleFirebaseCloudFirestore.png',
group: ['input'], group: ['input'],
version: 1, version: [1, 1.1],
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Interact with Google Firebase - Cloud Firestore API', description: 'Interact with Google Firebase - Cloud Firestore API',
defaults: { defaults: {
@ -94,15 +94,27 @@ export class GoogleFirebaseCloudFirestore implements INodeType {
const itemData = generatePairedItemData(items.length); const itemData = generatePairedItemData(items.length);
const returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];
let responseData; let responseData;
const resource = this.getNodeParameter('resource', 0); const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 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 (resource === 'document') {
if (operation === 'get') { if (operation === 'get') {
const projectId = this.getNodeParameter('projectId', 0) as string; const projectId = this.getNodeParameter('projectId', 0) as string;
const database = this.getNodeParameter('database', 0) as string; const database = this.getNodeParameter('database', 0) as string;
const simple = this.getNodeParameter('simple', 0) as boolean; 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 collection = this.getNodeParameter('collection', i) as string;
const documentId = this.getNodeParameter('documentId', i) as string; const documentId = this.getNodeParameter('documentId', i) as string;
return `projects/${projectId}/databases/${database}/documents/${collection}/${documentId}`; return `projects/${projectId}/databases/${database}/documents/${collection}/${documentId}`;
@ -179,49 +191,64 @@ export class GoogleFirebaseCloudFirestore implements INodeType {
}), }),
); );
} else if (operation === 'getAll') { } else if (operation === 'getAll') {
const projectId = this.getNodeParameter('projectId', 0) as string; for (let i = 0; i < itemsLength; i++) {
const database = this.getNodeParameter('database', 0) as string; try {
const collection = this.getNodeParameter('collection', 0) as string; const projectId = this.getNodeParameter('projectId', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0); const database = this.getNodeParameter('database', i) as string;
const simple = this.getNodeParameter('simple', 0) as boolean; const collection = this.getNodeParameter('collection', i) as string;
const returnAll = this.getNodeParameter('returnAll', i);
const simple = this.getNodeParameter('simple', i) as boolean;
if (returnAll) { if (returnAll) {
responseData = await googleApiRequestAllItems.call( responseData = await googleApiRequestAllItems.call(
this, this,
'documents', 'documents',
'GET', 'GET',
`/${projectId}/databases/${database}/documents/${collection}`, `/${projectId}/databases/${database}/documents/${collection}`,
); );
} else { } else {
const limit = this.getNodeParameter('limit', 0); const limit = this.getNodeParameter('limit', i);
const getAllResponse = (await googleApiRequest.call( const getAllResponse = (await googleApiRequest.call(
this, this,
'GET', 'GET',
`/${projectId}/databases/${database}/documents/${collection}`, `/${projectId}/databases/${database}/documents/${collection}`,
{}, {},
{ pageSize: limit }, { pageSize: limit },
)) as IDataObject; )) as IDataObject;
responseData = getAllResponse.documents; 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') { } else if (operation === 'delete') {
await Promise.all( 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 projectId = this.getNodeParameter('projectId', i) as string;
const database = this.getNodeParameter('database', i) as string; const database = this.getNodeParameter('database', i) as string;
const collection = this.getNodeParameter('collection', i) as string; const collection = this.getNodeParameter('collection', i) as string;
@ -295,44 +322,13 @@ export class GoogleFirebaseCloudFirestore implements INodeType {
returnData.push(...executionData); 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') { } else if (operation === 'query') {
const projectId = this.getNodeParameter('projectId', 0) as string; const projectId = this.getNodeParameter('projectId', 0) as string;
const database = this.getNodeParameter('database', 0) as string; const database = this.getNodeParameter('database', 0) as string;
const simple = this.getNodeParameter('simple', 0) as boolean; const simple = this.getNodeParameter('simple', 0) as boolean;
await Promise.all( await Promise.all(
items.map(async (item: IDataObject, i: number) => { items.map(async (_: IDataObject, i: number) => {
const query = this.getNodeParameter('query', i) as string; const query = this.getNodeParameter('query', i) as string;
responseData = await googleApiRequest.call( responseData = await googleApiRequest.call(
this, this,
@ -369,38 +365,51 @@ export class GoogleFirebaseCloudFirestore implements INodeType {
} }
} else if (resource === 'collection') { } else if (resource === 'collection') {
if (operation === 'getAll') { if (operation === 'getAll') {
const projectId = this.getNodeParameter('projectId', 0) as string; for (let i = 0; i < itemsLength; i++) {
const database = this.getNodeParameter('database', 0) as string; try {
const returnAll = this.getNodeParameter('returnAll', 0); const projectId = this.getNodeParameter('projectId', i) as string;
const database = this.getNodeParameter('database', i) as string;
const returnAll = this.getNodeParameter('returnAll', i);
if (returnAll) { if (returnAll) {
const getAllResponse = await googleApiRequestAllItems.call( const getAllResponse = await googleApiRequestAllItems.call(
this, this,
'collectionIds', 'collectionIds',
'POST', 'POST',
`/${projectId}/databases/${database}/documents:listCollectionIds`, `/${projectId}/databases/${database}/documents:listCollectionIds`,
); );
// @ts-ignore // @ts-ignore
responseData = getAllResponse.map((o) => ({ name: o })); responseData = getAllResponse.map((o) => ({ name: o }));
} else { } else {
const limit = this.getNodeParameter('limit', 0); const limit = this.getNodeParameter('limit', i);
const getAllResponse = (await googleApiRequest.call( const getAllResponse = (await googleApiRequest.call(
this, this,
'POST', 'POST',
`/${projectId}/databases/${database}/documents:listCollectionIds`, `/${projectId}/databases/${database}/documents:listCollectionIds`,
{}, {},
{ pageSize: limit }, { pageSize: limit },
)) as IDataObject; )) as IDataObject;
// @ts-ignore // @ts-ignore
responseData = getAllResponse.collectionIds.map((o) => ({ name: o })); 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);
} }
} }

View file

@ -69,7 +69,6 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
const returnData: INodeExecutionData[] = []; const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
// const spreadsheetId = this.getNodeParameter('spreadsheetId', i) as string;
const documentId = this.getNodeParameter('documentId', i, undefined, { const documentId = this.getNodeParameter('documentId', i, undefined, {
extractValue: true, extractValue: true,
}) as string; }) as string;

View file

@ -10,7 +10,6 @@ import type {
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { ITables } from './interfaces'; import type { ITables } from './interfaces';
@ -30,7 +29,7 @@ export class MicrosoftSql implements INodeType {
name: 'microsoftSql', name: 'microsoftSql',
icon: 'file:mssql.svg', icon: 'file:mssql.svg',
group: ['input'], group: ['input'],
version: 1, version: [1, 1.1],
description: 'Get, add and update data in Microsoft SQL', description: 'Get, add and update data in Microsoft SQL',
defaults: { defaults: {
name: 'Microsoft SQL', name: 'Microsoft SQL',
@ -250,10 +249,50 @@ export class MicrosoftSql implements INodeType {
await pool.connect(); await pool.connect();
let responseData: IDataObject | IDataObject[] = []; let responseData: IDataObject | IDataObject[] = [];
let returnData: INodeExecutionData[] = [];
const items = this.getInputData(); const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0); const operation = this.getNodeParameter('operation', 0);
const nodeVersion = this.getNode().typeVersion;
if (operation === 'executeQuery' && nodeVersion >= 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<any[]> = 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 { try {
if (operation === 'executeQuery') { if (operation === 'executeQuery') {
let rawQuery = this.getNodeParameter('query', 0) as string; 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]; const result = recordsets.length > 1 ? flatten(recordsets) : recordsets[0];
responseData = result; responseData = result;
} else if (operation === 'insert') { }
if (operation === 'insert') {
const tables = createTableStruct(this.getNodeParameter, items); const tables = createTableStruct(this.getNodeParameter, items);
await insertOperation(tables, pool); await insertOperation(tables, pool);
responseData = items; responseData = items;
} else if (operation === 'update') { }
if (operation === 'update') {
const updateKeys = items.map( const updateKeys = items.map(
(item, index) => this.getNodeParameter('updateKey', index) as string, (_, index) => this.getNodeParameter('updateKey', index) as string,
); );
const tables = createTableStruct( const tables = createTableStruct(
@ -288,7 +331,9 @@ export class MicrosoftSql implements INodeType {
await updateOperation(tables, pool); await updateOperation(tables, pool);
responseData = items; responseData = items;
} else if (operation === 'delete') { }
if (operation === 'delete') {
const tables = items.reduce((acc, item, index) => { const tables = items.reduce((acc, item, index) => {
const table = this.getNodeParameter('table', index) as string; const table = this.getNodeParameter('table', index) as string;
const deleteKey = this.getNodeParameter('deleteKey', index) as string; const deleteKey = this.getNodeParameter('deleteKey', index) as string;
@ -303,13 +348,14 @@ export class MicrosoftSql implements INodeType {
}, {} as ITables); }, {} as ITables);
responseData = await deleteOperation(tables, pool); 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) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
responseData = items; 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 // shuts down the connection pool associated with the db object to allow the process to finish
await pool.close(); await pool.close();
const itemData = generatePairedItemData(items.length); return [returnData];
const returnItems = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData),
{ itemData },
);
return [returnItems];
} }
} }

View file

@ -132,15 +132,17 @@ export function prepareFields(fields: string) {
.filter((field) => !!field); .filter((field) => !!field);
} }
export function stringifyObjectIDs(items: IDataObject[]) { export function stringifyObjectIDs(items: INodeExecutionData[]) {
items.forEach((item) => { items.forEach((item) => {
if (item._id instanceof ObjectId) { if (item._id instanceof ObjectId) {
item._id = item._id.toString(); item.json._id = item._id.toString();
} }
if (item.id instanceof ObjectId) { 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 = {}) { export async function connectMongoClient(connectionString: string, credentials: IDataObject = {}) {

View file

@ -9,7 +9,7 @@ import type {
INodeTypeDescription, INodeTypeDescription,
JsonObject, JsonObject,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { ApplicationError, NodeOperationError } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow';
import type { import type {
FindOneAndReplaceOptions, FindOneAndReplaceOptions,
@ -38,7 +38,7 @@ export class MongoDb implements INodeType {
name: 'mongoDb', name: 'mongoDb',
icon: 'file:mongodb.svg', icon: 'file:mongodb.svg',
group: ['input'], group: ['input'],
version: 1, version: [1, 1.1],
description: 'Find, insert and update documents in MongoDB', description: 'Find, insert and update documents in MongoDB',
defaults: { defaults: {
name: 'MongoDB', name: 'MongoDB',
@ -108,101 +108,126 @@ export class MongoDb implements INodeType {
const mdb = client.db(database); const mdb = client.db(database);
let responseData: IDataObject | IDataObject[] = []; let returnData: INodeExecutionData[] = [];
const items = this.getInputData(); const items = this.getInputData();
const operation = this.getNodeParameter('operation', 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 (operation === 'aggregate') { if (operation === 'aggregate') {
// ---------------------------------- for (let i = 0; i < itemsLength; i++) {
// aggregate try {
// ---------------------------------- const queryParameter = JSON.parse(
this.getNodeParameter('query', i) as string,
) as IDataObject;
try { if (queryParameter._id && typeof queryParameter._id === 'string') {
const queryParameter = JSON.parse( queryParameter._id = new ObjectId(queryParameter._id);
this.getNodeParameter('query', 0) as string, }
) as IDataObject;
if (queryParameter._id && typeof queryParameter._id === 'string') { const query = mdb
queryParameter._id = new ObjectId(queryParameter._id); .collection(this.getNodeParameter('collection', i) as string)
} .aggregate(queryParameter as unknown as Document[]);
const query = mdb for (const entry of await query.toArray()) {
.collection(this.getNodeParameter('collection', 0) as string) returnData.push({ json: entry, pairedItem: fallbackPairedItems ?? [{ item: i }] });
.aggregate(queryParameter as unknown as Document[]); }
} catch (error) {
responseData = await query.toArray(); if (this.continueOnFail()) {
} catch (error) { returnData.push({
if (this.continueOnFail()) { json: { error: (error as JsonObject).message },
responseData = [{ error: (error as JsonObject).message }]; pairedItem: fallbackPairedItems ?? [{ item: i }],
} else { });
continue;
}
throw error; throw error;
} }
} }
} else if (operation === 'delete') { }
// ----------------------------------
// delete
// ----------------------------------
try { if (operation === 'delete') {
const { deletedCount } = await mdb for (let i = 0; i < itemsLength; i++) {
.collection(this.getNodeParameter('collection', 0) as string) try {
.deleteMany(JSON.parse(this.getNodeParameter('query', 0) as string) as Document); const { deletedCount } = await mdb
.collection(this.getNodeParameter('collection', i) as string)
.deleteMany(JSON.parse(this.getNodeParameter('query', i) as string) as Document);
responseData = [{ deletedCount }]; returnData.push({
} catch (error) { json: { deletedCount },
if (this.continueOnFail()) { pairedItem: fallbackPairedItems ?? [{ item: i }],
responseData = [{ error: (error as JsonObject).message }]; });
} else { } catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: { error: (error as JsonObject).message },
pairedItem: fallbackPairedItems ?? [{ item: i }],
});
continue;
}
throw error; throw error;
} }
} }
} else if (operation === 'find') { }
// ----------------------------------
// find
// ----------------------------------
try { if (operation === 'find') {
const queryParameter = JSON.parse( for (let i = 0; i < itemsLength; i++) {
this.getNodeParameter('query', 0) as string, try {
) as IDataObject; const queryParameter = JSON.parse(
this.getNodeParameter('query', i) as string,
) as IDataObject;
if (queryParameter._id && typeof queryParameter._id === 'string') { if (queryParameter._id && typeof queryParameter._id === 'string') {
queryParameter._id = new ObjectId(queryParameter._id); queryParameter._id = new ObjectId(queryParameter._id);
} }
let query = mdb let query = mdb
.collection(this.getNodeParameter('collection', 0) as string) .collection(this.getNodeParameter('collection', i) as string)
.find(queryParameter as unknown as Document); .find(queryParameter as unknown as Document);
const options = this.getNodeParameter('options', 0); const options = this.getNodeParameter('options', i);
const limit = options.limit as number; const limit = options.limit as number;
const skip = options.skip as number; const skip = options.skip as number;
const sort = options.sort && (JSON.parse(options.sort as string) as Sort); 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();
responseData = queryResult; if (skip > 0) {
} catch (error) { query = query.skip(skip);
if (this.continueOnFail()) { }
responseData = [{ error: (error as JsonObject).message }]; if (limit > 0) {
} else { 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; 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 fields = prepareFields(this.getNodeParameter('fields', 0) as string);
const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean;
const dateFields = prepareFields( const dateFields = prepareFields(
@ -237,12 +262,14 @@ export class MongoDb implements INodeType {
} }
} }
responseData = updateItems; returnData = this.helpers.constructExecutionMetaData(
} else if (operation === 'findOneAndUpdate') { this.helpers.returnJsonArray(updateItems),
// ---------------------------------- { itemData: fallbackPairedItems },
// findOneAndUpdate );
// ---------------------------------- }
if (operation === 'findOneAndUpdate') {
fallbackPairedItems = fallbackPairedItems ?? generatePairedItemData(items.length);
const fields = prepareFields(this.getNodeParameter('fields', 0) as string); const fields = prepareFields(this.getNodeParameter('fields', 0) as string);
const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean;
const dateFields = prepareFields( const dateFields = prepareFields(
@ -277,11 +304,15 @@ export class MongoDb implements INodeType {
} }
} }
responseData = updateItems; returnData = this.helpers.constructExecutionMetaData(
} else if (operation === 'insert') { this.helpers.returnJsonArray(updateItems),
// ---------------------------------- { itemData: fallbackPairedItems },
// insert );
// ---------------------------------- }
if (operation === 'insert') {
fallbackPairedItems = fallbackPairedItems ?? generatePairedItemData(items.length);
let responseData: IDataObject[] = [];
try { try {
// Prepare the data to insert and copy it to be returned // Prepare the data to insert and copy it to be returned
const fields = prepareFields(this.getNodeParameter('fields', 0) as string); const fields = prepareFields(this.getNodeParameter('fields', 0) as string);
@ -310,11 +341,15 @@ export class MongoDb implements INodeType {
throw error; 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 fields = prepareFields(this.getNodeParameter('fields', 0) as string);
const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean;
const dateFields = prepareFields( const dateFields = prepareFields(
@ -349,30 +384,14 @@ export class MongoDb implements INodeType {
} }
} }
responseData = updateItems; returnData = this.helpers.constructExecutionMetaData(
} else { this.helpers.returnJsonArray(updateItems),
if (this.continueOnFail()) { { itemData: fallbackPairedItems },
responseData = [{ error: `The operation "${operation}" is not supported!` }]; );
} else {
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported!`,
{ itemIndex: 0 },
);
}
} }
await client.close(); await client.close();
stringifyObjectIDs(responseData); return [stringifyObjectIDs(returnData)];
const itemData = generatePairedItemData(items.length);
const returnItems = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData),
{ itemData },
);
return [returnItems];
} }
} }

View file

@ -28,7 +28,7 @@ export class RssFeedRead implements INodeType {
name: 'rssFeedRead', name: 'rssFeedRead',
icon: 'fa:rss', icon: 'fa:rss',
group: ['input'], group: ['input'],
version: 1, version: [1, 1.1],
description: 'Reads data from an RSS Feed', description: 'Reads data from an RSS Feed',
defaults: { defaults: {
name: 'RSS Read', name: 'RSS Read',
@ -65,59 +65,88 @@ export class RssFeedRead implements INodeType {
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const pairedItem = generatePairedItemData(this.getInputData().length); const returnData: INodeExecutionData[] = [];
const nodeVersion = this.getNode().typeVersion;
const items = this.getInputData();
try { let itemsLength = items.length ? 1 : 0;
const url = this.getNodeParameter('url', 0) as string; let fallbackPairedItems;
const options = this.getNodeParameter('options', 0);
const ignoreSSL = Boolean(options.ignoreSSL);
if (!url) { if (nodeVersion >= 1.1) {
throw new NodeOperationError(this.getNode(), 'The parameter "URL" has to be set!'); itemsLength = items.length;
} } else {
fallbackPairedItems = generatePairedItemData(items.length);
}
if (!validateURL(url)) { for (let i = 0; i < itemsLength; i++) {
throw new NodeOperationError(this.getNode(), 'The provided "URL" is not valid!');
}
const parser = new Parser({
requestOptions: {
rejectUnauthorized: !ignoreSSL,
},
});
let feed: Parser.Output<IDataObject>;
try { try {
feed = await parser.parseURL(url); const url = this.getNodeParameter('url', i) as string;
} catch (error) { const options = this.getNodeParameter('options', i);
if (error.code === 'ECONNREFUSED') { const ignoreSSL = Boolean(options.ignoreSSL);
throw new NodeOperationError(
this.getNode(), if (!url) {
`It was not possible to connect to the URL. Please make sure the URL "${url}" it is valid!`, throw new NodeOperationError(this.getNode(), 'The parameter "URL" has to be set!', {
); itemIndex: i,
});
} }
throw new NodeOperationError(this.getNode(), error as Error); if (!validateURL(url)) {
} throw new NodeOperationError(this.getNode(), 'The provided "URL" is not valid!', {
itemIndex: i,
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,
}); });
}); }
}
return [returnData]; const parser = new Parser({
} catch (error) { requestOptions: {
if (this.continueOnFail()) { rejectUnauthorized: !ignoreSSL,
return [[{ json: { error: error.message }, pairedItem }]]; },
});
let feed: Parser.Output<IDataObject>;
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];
} }
} }