feat(MongoDB Node): Add search index CRUD API to MongoDB CRUD Node (#16490)

This commit is contained in:
Bailey Pearson
2025-08-04 00:23:40 -06:00
committed by GitHub
parent 557e261d67
commit 1554e76500
3 changed files with 765 additions and 267 deletions

View File

@@ -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<INodeExecutionData[][]> {
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<string, unknown>;
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<string, unknown>;
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)];
}

View File

@@ -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 <a href="https://www.mongodb.com/docs/atlas/atlas-search/index-definitions/">here</a>',
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',
},
];

View File

@@ -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 } });
});
});
});