first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { prepareItems } from './GenericFunctions';
|
||||
|
||||
describe('MongoDB Node: Generic Functions', () => {
|
||||
describe('prepareItems', () => {
|
||||
it('should select fields', () => {
|
||||
const items = [{ json: { name: 'John', age: 30 } }, { json: { name: 'Jane', age: 25 } }];
|
||||
const fields = ['name'];
|
||||
|
||||
const result = prepareItems({ items, fields });
|
||||
|
||||
expect(result).toEqual([{ name: 'John' }, { name: 'Jane' }]);
|
||||
});
|
||||
|
||||
it('should add updateKey to selected fields', () => {
|
||||
const items = [{ json: { name: 'John', age: 30 } }, { json: { name: 'Jane', age: 25 } }];
|
||||
const fields = ['age'];
|
||||
const updateKey = 'name';
|
||||
|
||||
const result = prepareItems({ items, fields, updateKey });
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'John', age: 30 },
|
||||
{ name: 'Jane', age: 25 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle dot notation', () => {
|
||||
const items = [{ json: { user: { name: 'John' } } }, { json: { user: { name: 'Jane' } } }];
|
||||
const fields = ['user.name'];
|
||||
const useDotNotation = true;
|
||||
|
||||
const result = prepareItems({ items, fields, updateKey: '', useDotNotation });
|
||||
|
||||
expect(result).toEqual([{ user: { name: 'John' } }, { user: { name: 'Jane' } }]);
|
||||
});
|
||||
|
||||
it('should parse dates', () => {
|
||||
const items = [
|
||||
{ json: { date: '2023-10-01T00:00:00Z' } },
|
||||
{ json: { date: '2023-10-02T00:00:00Z' } },
|
||||
];
|
||||
const fields = ['date'];
|
||||
const dateFields = ['date'];
|
||||
const useDotNotation = false;
|
||||
const isUpdate = false;
|
||||
const result = prepareItems({
|
||||
items,
|
||||
fields,
|
||||
updateKey: '',
|
||||
useDotNotation,
|
||||
dateFields,
|
||||
isUpdate,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{ date: new Date('2023-10-01T00:00:00Z') },
|
||||
{ date: new Date('2023-10-02T00:00:00Z') },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle updates', () => {
|
||||
// Should keep dot notation in result to not overwrite the original values
|
||||
const items = [
|
||||
{ json: { id: 1, user: { name: 'John', age: 30 } } },
|
||||
{ json: { id: 2, user: { name: 'Jane', age: 25 } } },
|
||||
];
|
||||
const fields = ['user.name'];
|
||||
const useDotNotation = true;
|
||||
const isUpdate = true;
|
||||
const result = prepareItems({
|
||||
items,
|
||||
fields,
|
||||
updateKey: '',
|
||||
useDotNotation,
|
||||
dateFields: [],
|
||||
isUpdate,
|
||||
});
|
||||
expect(result).toEqual([{ 'user.name': 'John' }, { 'user.name': 'Jane' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import { MongoClient, ObjectId } from 'mongodb';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IDataObject,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { createSecureContext } from 'tls';
|
||||
|
||||
import type {
|
||||
IMongoCredentials,
|
||||
IMongoCredentialsType,
|
||||
IMongoParametricCredentials,
|
||||
} from './mongoDb.types';
|
||||
import { formatPrivateKey } from '../../utils/utilities';
|
||||
|
||||
/**
|
||||
* Standard way of building the MongoDB connection string, unless overridden with a provided string
|
||||
*
|
||||
* @param {ICredentialDataDecryptedObject} credentials MongoDB credentials to use, unless conn string is overridden
|
||||
*/
|
||||
export function buildParameterizedConnString(credentials: IMongoParametricCredentials): string {
|
||||
if (credentials.port) {
|
||||
return `mongodb://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}`;
|
||||
} else {
|
||||
return `mongodb+srv://${credentials.user}:${credentials.password}@${credentials.host}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build mongoDb connection string and resolve database name.
|
||||
* If a connection string override value is provided, that will be used in place of individual args
|
||||
*
|
||||
* @param {ICredentialDataDecryptedObject} credentials raw/input MongoDB credentials to use
|
||||
*/
|
||||
export function buildMongoConnectionParams(
|
||||
node: INode,
|
||||
credentials: IMongoCredentialsType,
|
||||
): IMongoCredentials {
|
||||
const sanitizedDbName =
|
||||
credentials.database && credentials.database.trim().length > 0
|
||||
? credentials.database.trim()
|
||||
: '';
|
||||
if (credentials.configurationType === 'connectionString') {
|
||||
if (credentials.connectionString && credentials.connectionString.trim().length > 0) {
|
||||
return {
|
||||
connectionString: credentials.connectionString.trim(),
|
||||
database: sanitizedDbName,
|
||||
};
|
||||
} else {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
'Cannot override credentials: valid MongoDB connection string not provided ',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
connectionString: buildParameterizedConnString(credentials),
|
||||
database: sanitizedDbName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify credentials. If ok, build mongoDb connection string and resolve database name.
|
||||
*
|
||||
* @param {ICredentialDataDecryptedObject} credentials raw/input MongoDB credentials to use
|
||||
*/
|
||||
export function validateAndResolveMongoCredentials(
|
||||
node: INode,
|
||||
credentials?: ICredentialDataDecryptedObject,
|
||||
): IMongoCredentials {
|
||||
if (credentials === undefined) {
|
||||
throw new NodeOperationError(node, 'No credentials got returned!');
|
||||
} else {
|
||||
return buildMongoConnectionParams(node, credentials as unknown as IMongoCredentialsType);
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareItems({
|
||||
items,
|
||||
fields,
|
||||
updateKey = '',
|
||||
useDotNotation = false,
|
||||
dateFields = [],
|
||||
isUpdate = false,
|
||||
}: {
|
||||
items: INodeExecutionData[];
|
||||
fields: string[];
|
||||
updateKey?: string;
|
||||
useDotNotation?: boolean;
|
||||
dateFields?: string[];
|
||||
isUpdate?: boolean;
|
||||
}) {
|
||||
let data = items;
|
||||
|
||||
if (updateKey) {
|
||||
if (!fields.includes(updateKey)) {
|
||||
fields.push(updateKey);
|
||||
}
|
||||
data = items.filter((item) => item.json[updateKey] !== undefined);
|
||||
}
|
||||
|
||||
const preparedItems = data.map(({ json }) => {
|
||||
const updateItem: IDataObject = {};
|
||||
|
||||
for (const field of fields) {
|
||||
let fieldData;
|
||||
|
||||
if (useDotNotation) {
|
||||
fieldData = get(json, field, null);
|
||||
} else {
|
||||
fieldData = json[field] !== undefined ? json[field] : null;
|
||||
}
|
||||
|
||||
if (fieldData && dateFields.includes(field)) {
|
||||
fieldData = new Date(fieldData as string);
|
||||
}
|
||||
|
||||
if (useDotNotation && !isUpdate) {
|
||||
set(updateItem, field, fieldData);
|
||||
} else {
|
||||
updateItem[field] = fieldData;
|
||||
}
|
||||
}
|
||||
|
||||
return updateItem;
|
||||
});
|
||||
|
||||
return preparedItems;
|
||||
}
|
||||
|
||||
export function prepareFields(fields: string) {
|
||||
return fields
|
||||
.split(',')
|
||||
.map((field) => field.trim())
|
||||
.filter((field) => !!field);
|
||||
}
|
||||
|
||||
export function stringifyObjectIDs(items: INodeExecutionData[]) {
|
||||
items.forEach((item) => {
|
||||
if (item._id instanceof ObjectId) {
|
||||
item.json._id = item._id.toString();
|
||||
}
|
||||
if (item.id instanceof ObjectId) {
|
||||
item.json.id = item.id.toString();
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function connectMongoClient(
|
||||
connectionString: string,
|
||||
nodeVersion: number,
|
||||
credentials: IDataObject = {},
|
||||
) {
|
||||
let client: MongoClient;
|
||||
const driverInfo = {
|
||||
name: 'n8n_crud',
|
||||
version: nodeVersion > 0 ? nodeVersion.toString() : 'unknown',
|
||||
};
|
||||
|
||||
if (credentials.tls) {
|
||||
const ca = credentials.ca ? formatPrivateKey(credentials.ca as string) : undefined;
|
||||
const cert = credentials.cert ? formatPrivateKey(credentials.cert as string) : undefined;
|
||||
const key = credentials.key ? formatPrivateKey(credentials.key as string) : undefined;
|
||||
const passphrase = (credentials.passphrase as string) || undefined;
|
||||
|
||||
const secureContext = createSecureContext({
|
||||
ca,
|
||||
cert,
|
||||
key,
|
||||
passphrase,
|
||||
});
|
||||
|
||||
client = await MongoClient.connect(connectionString, {
|
||||
tls: true,
|
||||
secureContext,
|
||||
driverInfo,
|
||||
});
|
||||
} else {
|
||||
client = await MongoClient.connect(connectionString, { driverInfo });
|
||||
}
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.mongoDb",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Development", "Data & Storage"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/mongodb/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.mongodb/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "Why business process automation with n8n can change your daily life",
|
||||
"icon": "🧬",
|
||||
"url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/"
|
||||
},
|
||||
{
|
||||
"label": "Running n8n on ships: An interview with Maranics",
|
||||
"icon": "🛳",
|
||||
"url": "https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/"
|
||||
},
|
||||
{
|
||||
"label": "Automate your data processing pipeline in 9 steps",
|
||||
"icon": "⚙️",
|
||||
"url": "https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "How uProc scraped a multi-page website with a low-code workflow",
|
||||
"icon": " 🕸️",
|
||||
"url": "https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
import type {
|
||||
FindOneAndReplaceOptions,
|
||||
FindOneAndUpdateOptions,
|
||||
UpdateOptions,
|
||||
Sort,
|
||||
} from 'mongodb';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { ApplicationError, NodeConnectionTypes } from 'n8n-workflow';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
ICredentialsDecrypted,
|
||||
ICredentialTestFunctions,
|
||||
IDataObject,
|
||||
INodeCredentialTestResult,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
JsonObject,
|
||||
IPairedItemData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
buildParameterizedConnString,
|
||||
connectMongoClient,
|
||||
prepareFields,
|
||||
prepareItems,
|
||||
stringifyObjectIDs,
|
||||
validateAndResolveMongoCredentials,
|
||||
} from './GenericFunctions';
|
||||
import type { IMongoParametricCredentials } from './mongoDb.types';
|
||||
import { nodeProperties } from './MongoDbProperties';
|
||||
import { generatePairedItemData } from '../../utils/utilities';
|
||||
|
||||
export class MongoDb implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'MongoDB',
|
||||
name: 'mongoDb',
|
||||
icon: 'file:mongodb.svg',
|
||||
group: ['input'],
|
||||
version: [1, 1.1, 1.2],
|
||||
description: 'Find, insert and update documents in MongoDB',
|
||||
defaults: {
|
||||
name: 'MongoDB',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
usableAsTool: true,
|
||||
credentials: [
|
||||
{
|
||||
name: 'mongoDb',
|
||||
required: true,
|
||||
testedBy: 'mongoDbCredentialTest',
|
||||
},
|
||||
],
|
||||
properties: nodeProperties,
|
||||
};
|
||||
|
||||
methods = {
|
||||
credentialTest: {
|
||||
async mongoDbCredentialTest(
|
||||
this: ICredentialTestFunctions,
|
||||
credential: ICredentialsDecrypted,
|
||||
): Promise<INodeCredentialTestResult> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
// Note: ICredentialTestFunctions doesn't have a way to get the Node instance
|
||||
// so we set the version to 0
|
||||
const client = await connectMongoClient(connectionString, 0, credentials);
|
||||
|
||||
const { databases } = await client.db().admin().listDatabases();
|
||||
|
||||
if (!(databases as IDataObject[]).map((db) => db.name).includes(database)) {
|
||||
throw new ApplicationError(`Database "${database}" does not exist`, {
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
await client.close();
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'Error',
|
||||
message: (error as Error).message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'OK',
|
||||
message: 'Connection successful!',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const credentials = await this.getCredentials('mongoDb');
|
||||
const node = this.getNode();
|
||||
const { database, connectionString } = validateAndResolveMongoCredentials(node, credentials);
|
||||
const nodeVersion = node.typeVersion;
|
||||
const client = await connectMongoClient(connectionString, nodeVersion, credentials);
|
||||
let returnData: INodeExecutionData[] = [];
|
||||
|
||||
try {
|
||||
const mdb = client.db(database);
|
||||
|
||||
const items = this.getInputData();
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
let itemsLength = items.length ? 1 : 0;
|
||||
let fallbackPairedItems: IPairedItemData[] | null = null;
|
||||
|
||||
if (nodeVersion >= 1.1) {
|
||||
itemsLength = items.length;
|
||||
} else {
|
||||
fallbackPairedItems = generatePairedItemData(items.length);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 === '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,
|
||||
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)
|
||||
.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
|
||||
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({
|
||||
items,
|
||||
fields,
|
||||
updateKey: '',
|
||||
useDotNotation,
|
||||
dateFields,
|
||||
});
|
||||
|
||||
const { insertedIds } = await mdb
|
||||
.collection(this.getNodeParameter('collection', 0) as string)
|
||||
.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()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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 indexType = this.getNodeParameter('indexType', 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,
|
||||
type: indexType,
|
||||
});
|
||||
|
||||
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)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
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',
|
||||
value: 'aggregate',
|
||||
description: 'Aggregate documents',
|
||||
action: 'Aggregate documents',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete documents',
|
||||
action: 'Delete documents',
|
||||
},
|
||||
{
|
||||
name: 'Find',
|
||||
value: 'find',
|
||||
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',
|
||||
description: 'Insert documents',
|
||||
action: 'Insert documents',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update documents',
|
||||
action: 'Update documents',
|
||||
},
|
||||
],
|
||||
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',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'MongoDB Collection',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// aggregate
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['aggregate'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: '[{ "$match": { "$gt": "1950-01-01" }, ... }]',
|
||||
hint: 'Learn more about aggregation pipeline <a href="https://docs.mongodb.com/manual/core/aggregation-pipeline/">here</a>',
|
||||
required: true,
|
||||
description: 'MongoDB aggregation pipeline query in JSON format',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Delete Query (JSON Format)',
|
||||
name: 'query',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['delete'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
default: '{}',
|
||||
placeholder: '{ "birth": { "$gt": "1950-01-01" } }',
|
||||
required: true,
|
||||
description: 'MongoDB Delete query',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// find
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['find'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
placeholder: 'Add option',
|
||||
description: 'Add query options',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 0,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-limit
|
||||
description:
|
||||
'Use limit to specify the maximum number of documents or 0 for unlimited documents',
|
||||
},
|
||||
{
|
||||
displayName: 'Skip',
|
||||
name: 'skip',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The number of documents to skip in the results set',
|
||||
},
|
||||
{
|
||||
displayName: 'Sort (JSON Format)',
|
||||
name: 'sort',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
default: '{}',
|
||||
placeholder: '{ "field": -1 }',
|
||||
description: 'A JSON that defines the sort order of the result set',
|
||||
},
|
||||
{
|
||||
displayName: 'Projection (JSON Format)',
|
||||
name: 'projection',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
rows: 4,
|
||||
},
|
||||
default: '{}',
|
||||
placeholder: '{ "_id": 0, "field": 1 }',
|
||||
description:
|
||||
'A JSON that defines a selection of fields to retrieve or exclude from the result set',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Query (JSON Format)',
|
||||
name: 'query',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['find'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
default: '{}',
|
||||
placeholder: '{ "birth": { "$gt": "1950-01-01" } }',
|
||||
required: true,
|
||||
description: 'MongoDB Find query',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Fields',
|
||||
name: 'fields',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'name,description',
|
||||
description: 'Comma-separated list of the fields to be included into the new document',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Update Key',
|
||||
name: 'updateKey',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
default: 'id',
|
||||
required: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-id
|
||||
description:
|
||||
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
|
||||
},
|
||||
{
|
||||
displayName: 'Fields',
|
||||
name: 'fields',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'name,description',
|
||||
description: 'Comma-separated list of the fields to be included into the new document',
|
||||
},
|
||||
{
|
||||
displayName: 'Upsert',
|
||||
name: 'upsert',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update', 'findOneAndReplace', 'findOneAndUpdate'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to perform an insert if no documents match the update key',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update', 'insert', 'findOneAndReplace', 'findOneAndUpdate'],
|
||||
resource: ['document'],
|
||||
},
|
||||
},
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Date Fields',
|
||||
name: 'dateFields',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Comma-separated list of fields that will be parsed as Mongo Date type',
|
||||
},
|
||||
{
|
||||
displayName: 'Use Dot Notation',
|
||||
name: 'useDotNotation',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to use dot notation to access date fields',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Credentials object for Mongo, if using individual parameters
|
||||
*/
|
||||
export interface IMongoParametricCredentials {
|
||||
/**
|
||||
* Whether to allow overriding the parametric credentials with a connection string
|
||||
*/
|
||||
configurationType: 'values';
|
||||
|
||||
host: string;
|
||||
database: string;
|
||||
user: string;
|
||||
password: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credentials object for Mongo, if using override connection string
|
||||
*/
|
||||
export interface IMongoOverrideCredentials {
|
||||
/**
|
||||
* Whether to allow overriding the parametric credentials with a connection string
|
||||
*/
|
||||
configurationType: 'connectionString';
|
||||
/**
|
||||
* If using an override connection string, this is where it will be.
|
||||
*/
|
||||
connectionString: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified credential object type (whether params are overridden with a connection string or not)
|
||||
*/
|
||||
export type IMongoCredentialsType = IMongoParametricCredentials | IMongoOverrideCredentials;
|
||||
|
||||
/**
|
||||
* Resolve the database and connection string from input credentials
|
||||
*/
|
||||
export type IMongoCredentials = {
|
||||
/**
|
||||
* Database name (used to create the Mongo client)
|
||||
*/
|
||||
database: string;
|
||||
/**
|
||||
* Generated connection string (after validating and figuring out overrides)
|
||||
*/
|
||||
connectionString: string;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32"><path fill="#599636" d="m15.9.087.854 1.604c.192.296.4.558.645.802a22 22 0 0 1 2.004 2.266c1.447 1.9 2.423 4.01 3.12 6.292.418 1.394.645 2.824.662 4.27.07 4.323-1.412 8.035-4.4 11.12a13 13 0 0 1-1.57 1.342c-.296 0-.436-.227-.558-.436a3.6 3.6 0 0 1-.436-1.255c-.105-.523-.174-1.046-.14-1.586v-.244C16.057 24.21 15.796.21 15.9.087"/><path fill="#6cac48" d="M15.9.034c-.035-.07-.07-.017-.105.017.017.35-.105.662-.296.96-.21.296-.488.523-.767.767-1.55 1.342-2.77 2.963-3.747 4.776-1.3 2.44-1.97 5.055-2.16 7.808-.087.993.314 4.497.627 5.508.854 2.684 2.388 4.933 4.375 6.885.488.47 1.01.906 1.55 1.325.157 0 .174-.14.21-.244a5 5 0 0 0 .157-.68l.35-2.614z"/><path fill="#c2bfbf" d="M16.754 28.845c.035-.4.227-.732.436-1.063-.21-.087-.366-.26-.488-.453a3.2 3.2 0 0 1-.26-.575c-.244-.732-.296-1.5-.366-2.248v-.453c-.087.07-.105.662-.105.75a17 17 0 0 1-.314 2.353c-.052.314-.087.627-.28.906 0 .035 0 .07.017.122.314.924.4 1.865.453 2.824v.35c0 .418-.017.33.33.47.14.052.296.07.436.174.105 0 .122-.087.122-.157l-.052-.575v-1.604c-.017-.28.035-.558.07-.82z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,250 @@
|
||||
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 driverInfo = {
|
||||
name: 'n8n_crud',
|
||||
version: '1.2',
|
||||
};
|
||||
const client = new MongoClient('mongodb://localhost:27017', { driverInfo });
|
||||
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: {} },
|
||||
type: 'vectorSearch',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 } });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user