diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index 86afe14690..0589496310 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -16,6 +16,7 @@ import type { INodeType, INodeTypeDescription, JsonObject, + IPairedItemData, } from 'n8n-workflow'; import { @@ -101,324 +102,445 @@ export class MongoDb implements INodeType { async execute(this: IExecuteFunctions): Promise { const credentials = await this.getCredentials('mongoDb'); const { database, connectionString } = validateAndResolveMongoCredentials(this, credentials); - const client = await connectMongoClient(connectionString, credentials); - - const mdb = client.db(database); - let returnData: INodeExecutionData[] = []; - const items = this.getInputData(); - const operation = this.getNodeParameter('operation', 0); - const nodeVersion = this.getNode().typeVersion; + try { + const mdb = client.db(database); - let itemsLength = items.length ? 1 : 0; - let fallbackPairedItems; + const items = this.getInputData(); + const operation = this.getNodeParameter('operation', 0); + const nodeVersion = this.getNode().typeVersion; - if (nodeVersion >= 1.1) { - itemsLength = items.length; - } else { - fallbackPairedItems = generatePairedItemData(items.length); - } + let itemsLength = items.length ? 1 : 0; + let fallbackPairedItems: IPairedItemData[] | null = null; - if (operation === 'aggregate') { - for (let i = 0; i < itemsLength; i++) { - try { - const queryParameter = JSON.parse( - this.getNodeParameter('query', i) as string, - ) as IDataObject; + if (nodeVersion >= 1.1) { + itemsLength = items.length; + } else { + fallbackPairedItems = generatePairedItemData(items.length); + } - if (queryParameter._id && typeof queryParameter._id === 'string') { - queryParameter._id = new ObjectId(queryParameter._id); + if (operation === 'aggregate') { + 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); + } + + const query = mdb + .collection(this.getNodeParameter('collection', i) as string) + .aggregate(queryParameter as unknown as Document[]); + + 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; } + } + } - const query = mdb - .collection(this.getNodeParameter('collection', i) as string) - .aggregate(queryParameter as unknown 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); - 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 }, + json: { deletedCount }, pairedItem: fallbackPairedItems ?? [{ item: i }], }); - continue; + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: (error as JsonObject).message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } + throw error; } - throw error; - } - } - } - - 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); - - 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; - } - } - } - - 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); - } - - let query = mdb - .collection(this.getNodeParameter('collection', i) as string) - .find(queryParameter as unknown as Document); - - const options = this.getNodeParameter('options', i); - const limit = options.limit as number; - const skip = options.skip as number; - const projection = - options.projection && (JSON.parse(options.projection as string) as Document); - 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); - } - - if ( - projection && - Object.keys(projection).length !== 0 && - projection.constructor === Object - ) { - query = query.project(projection); - } - - 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; - } - } - } - - 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( - 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 as FindOneAndReplaceOptions); - } catch (error) { - if (this.continueOnFail()) { - item.json = { error: (error as JsonObject).message }; - continue; - } - throw error; } } - returnData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(updateItems), - { itemData: fallbackPairedItems }, - ); - } + if (operation === 'find') { + for (let i = 0; i < itemsLength; i++) { + try { + const queryParameter = JSON.parse( + this.getNodeParameter('query', i) as string, + ) as IDataObject; - 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( - this.getNodeParameter('options.dateFields', 0, '') as string, - ); + if (queryParameter._id && typeof queryParameter._id === 'string') { + queryParameter._id = new ObjectId(queryParameter._id); + } - const updateKey = ((this.getNodeParameter('updateKey', 0) as string) || '').trim(); + let query = mdb + .collection(this.getNodeParameter('collection', i) as string) + .find(queryParameter as unknown as Document); - const updateOptions = (this.getNodeParameter('upsert', 0) as boolean) - ? { upsert: true } - : undefined; + const options = this.getNodeParameter('options', i); + const limit = options.limit as number; + const skip = options.skip as number; + const projection = + options.projection && (JSON.parse(options.projection as string) as Document); + const sort = options.sort && (JSON.parse(options.sort as string) as Sort); - const updateItems = prepareItems({ - items, - fields, - updateKey, - useDotNotation, - dateFields, - isUpdate: nodeVersion >= 1.2, - }); + 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); + } - 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; + if ( + projection && + Object.keys(projection).length !== 0 && + projection.constructor === Object + ) { + query = query.project(projection); + } + + 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; } - - await mdb - .collection(this.getNodeParameter('collection', 0) as string) - .findOneAndUpdate(filter, { $set: item }, updateOptions as FindOneAndUpdateOptions); - } catch (error) { - if (this.continueOnFail()) { - item.json = { error: (error as JsonObject).message }; - continue; - } - throw error; } } - 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 + 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( this.getNodeParameter('options.dateFields', 0, '') as string, ); - const insertItems = prepareItems({ + 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 as FindOneAndReplaceOptions); + } catch (error) { + if (this.continueOnFail()) { + item.json = { error: (error as JsonObject).message }; + continue; + } + throw error; + } + } + + 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( + 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: '', + updateKey, useDotNotation, dateFields, + isUpdate: nodeVersion >= 1.2, }); - const { insertedIds } = await mdb - .collection(this.getNodeParameter('collection', 0) as string) - .insertMany(insertItems); + 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; + } - // Add the id to the data - for (const i of Object.keys(insertedIds)) { - responseData.push({ - ...insertItems[parseInt(i, 10)], - id: insertedIds[parseInt(i, 10)] as unknown as string, - }); - } - } catch (error) { - if (this.continueOnFail()) { - responseData = [{ error: (error as JsonObject).message }]; - } else { - throw error; + await mdb + .collection(this.getNodeParameter('collection', 0) as string) + .findOneAndUpdate(filter, { $set: item }, updateOptions as FindOneAndUpdateOptions); + } catch (error) { + if (this.continueOnFail()) { + item.json = { error: (error as JsonObject).message }; + continue; + } + throw error; + } } + + returnData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(updateItems), + { itemData: fallbackPairedItems }, + ); } - 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( - 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, - isUpdate: nodeVersion >= 1.2, - }); - - for (const item of updateItems) { + if (operation === 'insert') { + fallbackPairedItems = fallbackPairedItems ?? generatePairedItemData(items.length); + let responseData: IDataObject[] = []; try { - const filter = { [updateKey]: item[updateKey] }; - if (updateKey === '_id') { - filter[updateKey] = new ObjectId(item[updateKey] as string); - delete item._id; - } + // Prepare the data to insert and copy it to be returned + 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, + ); - await mdb + const insertItems = prepareItems({ + items, + fields, + updateKey: '', + useDotNotation, + dateFields, + }); + + const { insertedIds } = await mdb .collection(this.getNodeParameter('collection', 0) as string) - .updateOne(filter, { $set: item }, updateOptions as UpdateOptions); + .insertMany(insertItems); + + // Add the id to the data + for (const i of Object.keys(insertedIds)) { + responseData.push({ + ...insertItems[parseInt(i, 10)], + id: insertedIds[parseInt(i, 10)] as unknown as string, + }); + } } catch (error) { if (this.continueOnFail()) { - item.json = { error: (error as JsonObject).message }; - continue; + responseData = [{ error: (error as JsonObject).message }]; + } else { + throw error; + } + } + + 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( + 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, + isUpdate: nodeVersion >= 1.2, + }); + + 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) + .updateOne(filter, { $set: item }, updateOptions as UpdateOptions); + } catch (error) { + if (this.continueOnFail()) { + item.json = { error: (error as JsonObject).message }; + continue; + } + throw error; + } + } + + returnData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(updateItems), + { itemData: fallbackPairedItems }, + ); + } + + if (operation === 'listSearchIndexes') { + for (let i = 0; i < itemsLength; i++) { + try { + const collection = this.getNodeParameter('collection', i) as string; + const indexName = (() => { + const name = this.getNodeParameter('indexName', i) as string; + return name.length === 0 ? undefined : name; + })(); + + const cursor = indexName + ? mdb.collection(collection).listSearchIndexes(indexName) + : mdb.collection(collection).listSearchIndexes(); + + const query = await cursor.toArray(); + const result = query.map((json) => ({ + json, + pairedItem: fallbackPairedItems ?? [{ item: i }], + })); + returnData.push(...result); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: (error as JsonObject).message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } + throw error; } - throw error; } } - returnData = this.helpers.constructExecutionMetaData( - this.helpers.returnJsonArray(updateItems), - { itemData: fallbackPairedItems }, - ); - } + if (operation === 'dropSearchIndex') { + for (let i = 0; i < itemsLength; i++) { + try { + const collection = this.getNodeParameter('collection', i) as string; + const indexName = this.getNodeParameter('indexNameRequired', i) as string; - await client.close(); + await mdb.collection(collection).dropSearchIndex(indexName); + returnData.push({ + json: { + [indexName]: true, + }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: (error as JsonObject).message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } + throw error; + } + } + } + + if (operation === 'createSearchIndex') { + for (let i = 0; i < itemsLength; i++) { + try { + const collection = this.getNodeParameter('collection', i) as string; + const indexName = this.getNodeParameter('indexNameRequired', i) as string; + const definition = JSON.parse( + this.getNodeParameter('indexDefinition', i) as string, + ) as Record; + + await mdb.collection(collection).createSearchIndex({ + name: indexName, + definition, + }); + + returnData.push({ + json: { indexName }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: (error as JsonObject).message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } + throw error; + } + } + } + + if (operation === 'updateSearchIndex') { + for (let i = 0; i < itemsLength; i++) { + try { + const collection = this.getNodeParameter('collection', i) as string; + const indexName = this.getNodeParameter('indexNameRequired', i) as string; + const definition = JSON.parse( + this.getNodeParameter('indexDefinition', i) as string, + ) as Record; + + await mdb.collection(collection).updateSearchIndex(indexName, definition); + + returnData.push({ + json: { [indexName]: true }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { error: (error as JsonObject).message }, + pairedItem: fallbackPairedItems ?? [{ item: i }], + }); + continue; + } + throw error; + } + } + } + } finally { + await client.close().catch(() => {}); + } return [stringifyObjectIDs(returnData)]; } diff --git a/packages/nodes-base/nodes/MongoDb/MongoDbProperties.ts b/packages/nodes-base/nodes/MongoDb/MongoDbProperties.ts index 95b7fb93a5..98dcda3e15 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDbProperties.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDbProperties.ts @@ -1,11 +1,33 @@ import type { INodeProperties } from 'n8n-workflow'; export const nodeProperties: INodeProperties[] = [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Search Index', + value: 'searchIndexes', + }, + { + name: 'Document', + value: 'document', + }, + ], + default: 'document', + }, { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, + displayOptions: { + show: { + resource: ['document'], + }, + }, options: [ { name: 'Aggregate', @@ -52,7 +74,40 @@ export const nodeProperties: INodeProperties[] = [ ], default: 'find', }, - + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['searchIndexes'], + }, + }, + options: [ + { + name: 'Create', + value: 'createSearchIndex', + action: 'Create Search Index', + }, + { + name: 'Drop', + value: 'dropSearchIndex', + action: 'Drop Search Index', + }, + { + name: 'List', + value: 'listSearchIndexes', + action: 'List Search Indexes', + }, + { + name: 'Update', + value: 'updateSearchIndex', + action: 'Update Search Index', + }, + ], + default: 'createSearchIndex', + }, { displayName: 'Collection', name: 'collection', @@ -75,6 +130,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['aggregate'], + resource: ['document'], }, }, default: '', @@ -97,6 +153,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['delete'], + resource: ['document'], }, }, default: '{}', @@ -115,6 +172,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['find'], + resource: ['document'], }, }, default: {}, @@ -175,6 +233,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['find'], + resource: ['document'], }, }, default: '{}', @@ -193,6 +252,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['insert'], + resource: ['document'], }, }, default: '', @@ -210,6 +270,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'], + resource: ['document'], }, }, default: 'id', @@ -225,6 +286,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'], + resource: ['document'], }, }, default: '', @@ -238,6 +300,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'], + resource: ['document'], }, }, default: false, @@ -250,6 +313,7 @@ export const nodeProperties: INodeProperties[] = [ displayOptions: { show: { operation: ['update', 'insert', 'findOneAndReplace', 'findOneAndUpdate'], + resource: ['document'], }, }, placeholder: 'Add option', @@ -271,4 +335,74 @@ export const nodeProperties: INodeProperties[] = [ }, ], }, + { + displayName: 'Index Name', + name: 'indexName', + type: 'string', + displayOptions: { + show: { + operation: ['listSearchIndexes'], + resource: ['searchIndexes'], + }, + }, + default: '', + description: 'If provided, only lists indexes with the specified name', + }, + { + displayName: 'Index Name', + name: 'indexNameRequired', + type: 'string', + displayOptions: { + show: { + operation: ['createSearchIndex', 'dropSearchIndex', 'updateSearchIndex'], + resource: ['searchIndexes'], + }, + }, + default: '', + required: true, + description: 'The name of the search index', + }, + { + displayName: 'Index Definition', + name: 'indexDefinition', + type: 'json', + displayOptions: { + show: { + operation: ['createSearchIndex', 'updateSearchIndex'], + resource: ['searchIndexes'], + }, + }, + typeOptions: { + alwaysOpenEditWindow: true, + }, + placeholder: '{ "type": "vectorSearch", "definition": {} }', + hint: 'Learn more about search index definitions here', + default: '{}', + required: true, + description: 'The search index definition', + }, + { + displayName: 'Index Type', + name: 'indexType', + type: 'options', + displayOptions: { + show: { + operation: ['createSearchIndex'], + resource: ['searchIndexes'], + }, + }, + options: [ + { + value: 'vectorSearch', + name: 'Vector Search', + }, + { + name: 'Search', + value: 'search', + }, + ], + default: 'vectorSearch', + required: true, + description: 'The search index index type', + }, ]; diff --git a/packages/nodes-base/nodes/MongoDb/test/MongoDB.test.ts b/packages/nodes-base/nodes/MongoDb/test/MongoDB.test.ts new file mode 100644 index 0000000000..726e6eaa0b --- /dev/null +++ b/packages/nodes-base/nodes/MongoDb/test/MongoDB.test.ts @@ -0,0 +1,242 @@ +import { NodeTestHarness } from '@nodes-testing/node-test-harness'; +import { Collection, MongoClient } from 'mongodb'; +import type { INodeParameters, WorkflowTestData } from 'n8n-workflow'; + +MongoClient.connect = async function () { + const client = new MongoClient('mongodb://localhost:27017'); + return client; +}; + +function buildWorkflow({ + parameters, + expectedResult, +}: { parameters: INodeParameters; expectedResult: unknown[] }) { + const test: WorkflowTestData = { + description: 'should pass test', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '8b7bb389-e4ef-424a-bca1-e7ead60e43eb', + name: 'When clicking "Execute Workflow"', + type: 'n8n-nodes-base.manualTrigger', + typeVersion: 1, + position: [740, 380], + }, + { + parameters, + id: '8b7bb389-e4ef-424a-bca1-e7ead60e43ec', + name: 'mongoDb', + type: 'n8n-nodes-base.mongoDb', + typeVersion: 1.2, + position: [1260, 360], + credentials: { + mongoDb: { + id: 'mongodb://localhost:27017', + name: 'Connection String', + }, + }, + }, + ], + connections: { + 'When clicking "Execute Workflow"': { + main: [ + [ + { + node: 'mongoDb', + type: 'main', + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + assertBinaryData: true, + nodeData: { + mongoDb: [expectedResult], + }, + }, + }; + + return test; +} + +describe('MongoDB CRUD Node', () => { + const testHarness = new NodeTestHarness(); + + describe('createSearchIndex operation', () => { + const spy: jest.SpyInstance = jest.spyOn(Collection.prototype, 'createSearchIndex'); + afterAll(() => jest.restoreAllMocks()); + beforeAll(() => { + spy.mockResolvedValueOnce('my-index'); + }); + + testHarness.setupTest( + buildWorkflow({ + parameters: { + operation: 'createSearchIndex', + resource: 'searchIndexes', + collection: 'foo', + indexType: 'vectorSearch', + indexDefinition: JSON.stringify({ mappings: {} }), + indexNameRequired: 'my-index', + }, + expectedResult: [{ json: { indexName: 'my-index' } }], + }), + ); + + it('calls the spy with the expected arguments', function () { + expect(spy).toBeCalledWith({ name: 'my-index', definition: { mappings: {} } }); + }); + }); + + describe('listSearchIndexes operation', () => { + describe('no index name provided', function () { + let spy: jest.SpyInstance; + beforeAll(() => { + spy = jest.spyOn(Collection.prototype, 'listSearchIndexes'); + const mockCursor = { + toArray: async () => [], + }; + spy.mockReturnValue(mockCursor); + }); + + afterAll(() => jest.restoreAllMocks()); + + testHarness.setupTest( + buildWorkflow({ + parameters: { + resource: 'searchIndexes', + operation: 'listSearchIndexes', + collection: 'foo', + }, + expectedResult: [], + }), + ); + + it('calls the spy with the expected arguments', function () { + expect(spy).toHaveBeenCalledWith(); + }); + }); + + describe('index name provided', function () { + let spy: jest.SpyInstance; + beforeAll(() => { + spy = jest.spyOn(Collection.prototype, 'listSearchIndexes'); + const mockCursor = { + toArray: async () => [], + }; + spy.mockReturnValue(mockCursor); + }); + + afterAll(() => jest.restoreAllMocks()); + + testHarness.setupTest( + buildWorkflow({ + parameters: { + resource: 'searchIndexes', + operation: 'listSearchIndexes', + collection: 'foo', + indexName: 'my-index', + }, + expectedResult: [], + }), + ); + + it('calls the spy with the expected arguments', function () { + expect(spy).toHaveBeenCalledWith('my-index'); + }); + }); + + describe('return values are transformed into the expected return type', function () { + let spy: jest.SpyInstance; + beforeAll(() => { + spy = jest.spyOn(Collection.prototype, 'listSearchIndexes'); + const mockCursor = { + toArray: async () => [{ name: 'my-index' }, { name: 'my-index-2' }], + }; + spy.mockReturnValue(mockCursor); + }); + + afterAll(() => jest.restoreAllMocks()); + + testHarness.setupTest( + buildWorkflow({ + parameters: { + operation: 'listSearchIndexes', + resource: 'searchIndexes', + collection: 'foo', + indexName: 'my-index', + }, + expectedResult: [ + { + json: { name: 'my-index' }, + }, + { + json: { name: 'my-index-2' }, + }, + ], + }), + ); + }); + }); + + describe('dropSearchIndex operation', () => { + let spy: jest.SpyInstance; + afterAll(() => jest.restoreAllMocks()); + beforeAll(() => { + spy = jest.spyOn(Collection.prototype, 'dropSearchIndex'); + spy.mockResolvedValueOnce(undefined); + }); + + testHarness.setupTest( + buildWorkflow({ + parameters: { + operation: 'dropSearchIndex', + resource: 'searchIndexes', + collection: 'foo', + indexNameRequired: 'my-index', + }, + expectedResult: [{ json: { 'my-index': true } }], + }), + ); + + it('calls the spy with the expected arguments', function () { + expect(spy).toBeCalledWith('my-index'); + }); + }); + + describe('updateSearchIndex operation', () => { + let spy: jest.SpyInstance; + afterAll(() => jest.restoreAllMocks()); + beforeAll(() => { + spy = jest.spyOn(Collection.prototype, 'updateSearchIndex'); + spy.mockResolvedValueOnce(undefined); + }); + + testHarness.setupTest( + buildWorkflow({ + parameters: { + operation: 'updateSearchIndex', + resource: 'searchIndexes', + collection: 'foo', + indexNameRequired: 'my-index', + indexDefinition: JSON.stringify({ + mappings: { + dynamic: true, + }, + }), + }, + expectedResult: [{ json: { 'my-index': true } }], + }), + ); + + it('calls the spy with the expected arguments', function () { + expect(spy).toBeCalledWith('my-index', { mappings: { dynamic: true } }); + }); + }); +});