From b5511e5ac7745c6b4a20aa49d2633f2f91bdbb7b Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Thu, 1 Sep 2022 11:23:15 +0300 Subject: [PATCH] feature: add MongoDB credential testing and two operations: findOneAndReplace and findOneAndUpdate (#3901) * feature: add MongoDB credential testing and two operations: findOneAndReplace and findOneAndUpdate Co-authored-by: Anas Naim --- .../nodes-base/nodes/MongoDb/MongoDb.node.ts | 203 +++++++++++++----- .../nodes/MongoDb/mongo.node.options.ts | 21 +- .../nodes/MongoDb/mongo.node.utils.ts | 90 ++++---- 3 files changed, 219 insertions(+), 95 deletions(-) diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index 7bb4c4ea83..49eb031acc 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -1,7 +1,10 @@ import { IExecuteFunctions } from 'n8n-core'; import { + ICredentialsDecrypted, + ICredentialTestFunctions, IDataObject, + INodeCredentialTestResult, INodeExecutionData, INodeType, INodeTypeDescription, @@ -11,18 +14,62 @@ import { import { nodeDescription } from './mongo.node.options'; +import { buildParameterizedConnString, prepareFields, prepareItems } from './mongo.node.utils'; + import { MongoClient, ObjectID } from 'mongodb'; -import { - getItemCopy, - handleDateFields, - handleDateFieldsWithDotNotation, - validateAndResolveMongoCredentials, -} from './mongo.node.utils'; +import { validateAndResolveMongoCredentials } from './mongo.node.utils'; + +import { IMongoParametricCredentials } from './mongo.node.types'; export class MongoDb implements INodeType { description: INodeTypeDescription = nodeDescription; + methods = { + credentialTest: { + async mongoDbCredentialTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const credentials = credential.data as IDataObject; + try { + const database = ((credentials.database as string) || '').trim(); + let connectionString = ''; + + if (credentials.configurationType === 'connectionString') { + connectionString = ((credentials.connectionString as string) || '').trim(); + } else { + connectionString = buildParameterizedConnString( + credentials as unknown as IMongoParametricCredentials, + ); + } + + const client: MongoClient = await MongoClient.connect(connectionString, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + const { databases } = await client.db().admin().listDatabases(); + + if (!(databases as IDataObject[]).map((db) => db.name).includes(database)) { + // eslint-disable-next-line n8n-nodes-base/node-execute-block-wrong-error-thrown + throw new Error(`Database "${database}" does not exist`); + } + client.close(); + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + return { + status: 'OK', + message: 'Connection successful!', + }; + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { const { database, connectionString } = validateAndResolveMongoCredentials( this, @@ -59,10 +106,9 @@ export class MongoDb implements INodeType { .aggregate(queryParameter); responseData = await query.toArray(); - } catch (error) { if (this.continueOnFail()) { - responseData = [ { error: (error as JsonObject).message } ]; + responseData = [{ error: (error as JsonObject).message }]; } else { throw error; } @@ -124,25 +170,99 @@ export class MongoDb implements INodeType { throw error; } } + } else if (operation === 'findOneAndReplace') { + // ---------------------------------- + // findOneAndReplace + // ---------------------------------- + + const fields = prepareFields(this.getNodeParameter('fields', 0) as string); + const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; + const dateFields = prepareFields( + this.getNodeParameter('options.dateFields', 0, '') as string, + ); + + const updateKey = ((this.getNodeParameter('updateKey', 0) as string) || '').trim(); + + const updateOptions = (this.getNodeParameter('upsert', 0) as boolean) + ? { upsert: true } + : undefined; + + const updateItems = prepareItems(items, fields, updateKey, useDotNotation, dateFields); + + for (const item of updateItems) { + try { + const filter = { [updateKey]: item[updateKey] }; + if (updateKey === '_id') { + filter[updateKey] = new ObjectID(item[updateKey] as string); + delete item['_id']; + } + + await mdb + .collection(this.getNodeParameter('collection', 0) as string) + .findOneAndReplace(filter, item, updateOptions); + } catch (error) { + if (this.continueOnFail()) { + item.json = { error: (error as JsonObject).message }; + continue; + } + throw error; + } + } + + responseData = updateItems; + } else if (operation === 'findOneAndUpdate') { + // ---------------------------------- + // findOneAndUpdate + // ---------------------------------- + + const fields = prepareFields(this.getNodeParameter('fields', 0) as string); + const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; + const dateFields = prepareFields( + this.getNodeParameter('options.dateFields', 0, '') as string, + ); + + const updateKey = ((this.getNodeParameter('updateKey', 0) as string) || '').trim(); + + const updateOptions = (this.getNodeParameter('upsert', 0) as boolean) + ? { upsert: true } + : undefined; + + const updateItems = prepareItems(items, fields, updateKey, useDotNotation, dateFields); + + for (const item of updateItems) { + try { + const filter = { [updateKey]: item[updateKey] }; + if (updateKey === '_id') { + filter[updateKey] = new ObjectID(item[updateKey] as string); + delete item['_id']; + } + + await mdb + .collection(this.getNodeParameter('collection', 0) as string) + .findOneAndUpdate(filter, { $set: item }, updateOptions); + } catch (error) { + if (this.continueOnFail()) { + item.json = { error: (error as JsonObject).message }; + continue; + } + throw error; + } + } + + responseData = updateItems; } else if (operation === 'insert') { // ---------------------------------- // insert // ---------------------------------- try { // Prepare the data to insert and copy it to be returned - const fields = (this.getNodeParameter('fields', 0) as string) - .split(',') - .map((f) => f.trim()) - .filter((f) => !!f); + const fields = prepareFields(this.getNodeParameter('fields', 0) as string); + const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; + const dateFields = prepareFields( + this.getNodeParameter('options.dateFields', 0, '') as string, + ); - const options = this.getNodeParameter('options', 0) as IDataObject; - const insertItems = getItemCopy(items, fields); - - if (options.dateFields && !options.useDotNotation) { - handleDateFields(insertItems, options.dateFields as string); - } else if (options.dateFields && options.useDotNotation) { - handleDateFieldsWithDotNotation(insertItems, options.dateFields as string); - } + const insertItems = prepareItems(items, fields, '', useDotNotation, dateFields); const { insertedIds } = await mdb .collection(this.getNodeParameter('collection', 0) as string) @@ -167,45 +287,28 @@ export class MongoDb implements INodeType { // update // ---------------------------------- - const fields = (this.getNodeParameter('fields', 0) as string) - .split(',') - .map((f) => f.trim()) - .filter((f) => !!f); + const fields = prepareFields(this.getNodeParameter('fields', 0) as string); + const useDotNotation = this.getNodeParameter('options.useDotNotation', 0, false) as boolean; + const dateFields = prepareFields( + this.getNodeParameter('options.dateFields', 0, '') as string, + ); - const options = this.getNodeParameter('options', 0) as IDataObject; - - let updateKey = this.getNodeParameter('updateKey', 0) as string; - updateKey = updateKey.trim(); + const updateKey = ((this.getNodeParameter('updateKey', 0) as string) || '').trim(); const updateOptions = (this.getNodeParameter('upsert', 0) as boolean) ? { upsert: true } : undefined; - if (!fields.includes(updateKey)) { - fields.push(updateKey); - } - - // Prepare the data to update and copy it to be returned - const updateItems = getItemCopy(items, fields); - - if (options.dateFields && !options.useDotNotation) { - handleDateFields(updateItems, options.dateFields as string); - } else if (options.dateFields && options.useDotNotation) { - handleDateFieldsWithDotNotation(updateItems, options.dateFields as string); - } + const updateItems = prepareItems(items, fields, updateKey, useDotNotation, dateFields); for (const item of updateItems) { try { - if (item[updateKey] === undefined) { - continue; - } - - const filter: { [key: string]: string | ObjectID } = {}; - filter[updateKey] = item[updateKey] as string; + const filter = { [updateKey]: item[updateKey] }; if (updateKey === '_id') { - filter[updateKey] = new ObjectID(filter[updateKey]); + filter[updateKey] = new ObjectID(item[updateKey] as string); delete item['_id']; } + await mdb .collection(this.getNodeParameter('collection', 0) as string) .updateOne(filter, { $set: item }, updateOptions); @@ -223,7 +326,11 @@ export class MongoDb implements INodeType { 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}); + throw new NodeOperationError( + this.getNode(), + `The operation "${operation}" is not supported!`, + { itemIndex: 0 }, + ); } } diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts index d97e40ae27..130a747a44 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts @@ -20,6 +20,7 @@ export const nodeDescription: INodeTypeDescription = { { name: 'mongoDb', required: true, + testedBy: 'mongoDbCredentialTest', }, ], properties: [ @@ -47,6 +48,18 @@ export const nodeDescription: INodeTypeDescription = { description: 'Find documents', action: 'Find documents', }, + { + name: 'Find And Replace', + value: 'findOneAndReplace', + description: 'Find and replace documents', + action: 'Find and replace documents', + }, + { + name: 'Find And Update', + value: 'findOneAndUpdate', + description: 'Find and update documents', + action: 'Find and update documents', + }, { name: 'Insert', value: 'insert', @@ -207,7 +220,7 @@ export const nodeDescription: INodeTypeDescription = { type: 'string', displayOptions: { show: { - operation: ['update'], + operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'], }, }, default: 'id', @@ -222,7 +235,7 @@ export const nodeDescription: INodeTypeDescription = { type: 'string', displayOptions: { show: { - operation: ['update'], + operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'], }, }, default: '', @@ -235,7 +248,7 @@ export const nodeDescription: INodeTypeDescription = { type: 'boolean', displayOptions: { show: { - operation: ['update'], + operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'], }, }, default: false, @@ -247,7 +260,7 @@ export const nodeDescription: INodeTypeDescription = { type: 'collection', displayOptions: { show: { - operation: ['update', 'insert'], + operation: ['update', 'insert', 'findOneAndReplace', 'findOneAndUpdate'], }, }, placeholder: 'Add Option', diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts index 60828d9ab6..3edbafeef4 100644 --- a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts @@ -1,10 +1,12 @@ import { IExecuteFunctions } from 'n8n-core'; + import { ICredentialDataDecryptedObject, IDataObject, INodeExecutionData, NodeOperationError, } from 'n8n-workflow'; + import { IMongoCredentials, IMongoCredentialsType, @@ -18,7 +20,7 @@ import { get, set } from 'lodash'; * * @param {ICredentialDataDecryptedObject} credentials MongoDB credentials to use, unless conn string is overridden */ -function buildParameterizedConnString(credentials: IMongoParametricCredentials): string { +export function buildParameterizedConnString(credentials: IMongoParametricCredentials): string { if (credentials.port) { return `mongodb://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}`; } else { @@ -78,52 +80,54 @@ export function validateAndResolveMongoCredentials( } } -/** - * Returns of copy of the items which only contains the json data and - * of that only the define properties - * - * @param {INodeExecutionData[]} items The items to copy - * @param {string[]} properties The properties it should include - * @returns - */ -export function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] { - // Prepare the data to insert and copy it to be returned - let newItem: IDataObject; - return items.map((item) => { - newItem = {}; - for (const property of properties) { - if (item.json[property] === undefined) { - newItem[property] = null; +export function prepareItems( + items: INodeExecutionData[], + fields: string[], + updateKey = '', + useDotNotation = false, + dateFields: string[] = [], +) { + let data = items; + + if (updateKey) { + if (!fields.includes(updateKey)) { + fields.push(updateKey); + } + data = items.filter((item) => item.json[updateKey] !== undefined); + } + + const preperedItems = data.map(({ json }) => { + const updateItem: IDataObject = {}; + + for (const field of fields) { + let fieldData; + + if (useDotNotation) { + fieldData = get(json, field, null); } else { - newItem[property] = JSON.parse(JSON.stringify(item.json[property])); + fieldData = json[field] !== undefined ? json[field] : null; + } + + if (fieldData && dateFields.includes(field)) { + fieldData = new Date(fieldData as string); + } + + if (useDotNotation) { + set(updateItem, field, fieldData); + } else { + updateItem[field] = fieldData; } } - return newItem; + + return updateItem; }); + + return preperedItems; } -export function handleDateFields(insertItems: IDataObject[], fields: string) { - const dateFields = (fields as string).split(','); - for (let i = 0; i < insertItems.length; i++) { - for (const key of Object.keys(insertItems[i])) { - if (dateFields.includes(key)) { - insertItems[i][key] = new Date(insertItems[i][key] as string); - } - } - } -} - -export function handleDateFieldsWithDotNotation(insertItems: IDataObject[], fields: string) { - const dateFields = fields.split(',').map((field) => field.trim()); - - for (let i = 0; i < insertItems.length; i++) { - for (const field of dateFields) { - const fieldValue = get(insertItems[i], field) as string; - const date = new Date(fieldValue); - - if (fieldValue && !isNaN(date.valueOf())) { - set(insertItems[i], field, date); - } - } - } +export function prepareFields(fields: string) { + return fields + .split(',') + .map((field) => field.trim()) + .filter((field) => !!field); }