diff --git a/.editorconfig b/.editorconfig index bec755324..5d02a5688 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,3 +6,10 @@ indent_style = tab end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + +[package.json] +indent_style = space +indent_size = 2 + +[*.ts] +quote_type = single \ No newline at end of file diff --git a/package.json b/package.json index b045830f5..0c7d8dac1 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "bootstrap": "lerna bootstrap --hoist --no-ci", "build": "lerna exec npm run build", "dev": "lerna exec npm run dev --parallel", + "clean:dist": "lerna exec -- rimraf ./dist", "start": "run-script-os", "start:default": "cd packages/cli/bin && ./n8n", "start:windows": "cd packages/cli/bin && n8n", @@ -14,6 +15,7 @@ }, "devDependencies": { "lerna": "^3.13.1", + "rimraf": "^3.0.2", "run-script-os": "^1.0.7" }, "postcss": {} diff --git a/packages/cli/package.json b/packages/cli/package.json index 11a69c245..913ea2341 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -93,7 +93,7 @@ "jwks-rsa": "^1.6.0", "localtunnel": "^2.0.0", "lodash.get": "^4.4.2", - "mongodb": "^3.2.3", + "mongodb": "^3.5.5", "mysql2": "^2.0.1", "n8n-core": "~0.32.0", "n8n-editor-ui": "~0.43.0", diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index f9a7f879a..e1b30cd16 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -179,7 +179,7 @@ if (process.env.NODE_ENV !== 'production') { // not do anything about it anyway return; } - console.error('error cought in main.ts'); // eslint-disable-line no-console + console.error('error caught in main.ts'); // eslint-disable-line no-console console.error(message); // eslint-disable-line no-console console.error(error); // eslint-disable-line no-console }; diff --git a/packages/nodes-base/credentials/MongoDb.credentials.ts b/packages/nodes-base/credentials/MongoDb.credentials.ts index b39c7016d..436e9f2e4 100644 --- a/packages/nodes-base/credentials/MongoDb.credentials.ts +++ b/packages/nodes-base/credentials/MongoDb.credentials.ts @@ -1,8 +1,4 @@ -import { - ICredentialType, - NodePropertyTypes, -} from 'n8n-workflow'; - +import { ICredentialType, NodePropertyTypes } from 'n8n-workflow'; export class MongoDb implements ICredentialType { name = 'mongoDb'; @@ -12,34 +8,57 @@ export class MongoDb implements ICredentialType { displayName: 'Host', name: 'host', type: 'string' as NodePropertyTypes, - default: 'localhost', + default: 'localhost' }, { displayName: 'Database', name: 'database', type: 'string' as NodePropertyTypes, default: '', + description: + 'Note: the database should still be provided even if using an override connection string' }, { displayName: 'User', name: 'user', type: 'string' as NodePropertyTypes, - default: '', + default: '' }, { displayName: 'Password', name: 'password', type: 'string' as NodePropertyTypes, typeOptions: { - password: true, + password: true }, - default: '', + default: '' }, { displayName: 'Port', name: 'port', type: 'number' as NodePropertyTypes, - default: 27017, + default: 27017 }, + { + displayName: 'Override conn string', + name: 'shouldOverrideConnString', + type: 'boolean' as NodePropertyTypes, + default: false, + required: false, + description: + 'Whether to override the generated connection string. Credentials will also be ignored in this case.' + }, + { + displayName: 'Conn string override', + name: 'connStringOverrideVal', + type: 'string' as NodePropertyTypes, + typeOptions: { + rows: 1 + }, + default: '', + placeholder: `mongodb://USERNAMEHERE:PASSWORDHERE@localhost:27017/?authSource=admin&readPreference=primary&appname=n8n&ssl=false`, + required: false, + description: `If provided, the value here will be used as a MongoDB connection string, and the MongoDB credentials will be ignored` + } ]; } diff --git a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts index 0aa6d603a..dbd7ab292 100644 --- a/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts +++ b/packages/nodes-base/nodes/MongoDb/MongoDb.node.ts @@ -3,192 +3,29 @@ import { IDataObject, INodeExecutionData, INodeType, - INodeTypeDescription, + INodeTypeDescription } from 'n8n-workflow'; - +import { nodeDescription } from './mongo.node.options'; import { MongoClient } from 'mongodb'; - - -/** - * Returns of copy of the items which only contains the json data and - * of that only the define properties - * - * @param {INodeExecutionData[]} items The items to copy - * @param {string[]} properties The properties it should include - * @returns - */ -function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] { - // Prepare the data to insert and copy it to be returned - let newItem: IDataObject; - return items.map((item) => { - newItem = {}; - for (const property of properties) { - if (item.json[property] === undefined) { - newItem[property] = null; - } else { - newItem[property] = JSON.parse(JSON.stringify(item.json[property])); - } - } - return newItem; - }); -} - +import { + getItemCopy, + validateAndResolveMongoCredentials +} from './mongo.node.utils'; export class MongoDb implements INodeType { - description: INodeTypeDescription = { - displayName: 'MongoDB', - name: 'mongoDb', - icon: 'file:mongoDb.png', - group: ['input'], - version: 1, - description: 'Find, insert and update documents in MongoDB.', - defaults: { - name: 'MongoDB', - color: '#13AA52', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'mongoDb', - required: true, - } - ], - properties: [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Find', - value: 'find', - description: 'Find documents.', - }, - { - name: 'Insert', - value: 'insert', - description: 'Insert documents.', - }, - { - name: 'Update', - value: 'update', - description: 'Updates documents.', - }, - ], - default: 'find', - description: 'The operation to perform.', - }, - - { - displayName: 'Collection', - name: 'collection', - type: 'string', - required: true, - default: '', - description: 'MongoDB Collection' - }, - - // ---------------------------------- - // find - // ---------------------------------- - { - displayName: 'Query (JSON format)', - name: 'query', - type: 'string', - typeOptions: { - rows: 5, - }, - displayOptions: { - show: { - operation: [ - 'find' - ], - }, - }, - default: '{}', - placeholder: `{ "birth": { "$gt": "1950-01-01" } }`, - required: true, - description: 'MongoDB Find query.', - }, - - - // ---------------------------------- - // insert - // ---------------------------------- - { - displayName: 'Fields', - name: 'fields', - type: 'string', - displayOptions: { - show: { - operation: [ - 'insert' - ], - }, - }, - 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' - ], - }, - }, - default: 'id', - required: true, - 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' - ], - }, - }, - default: '', - placeholder: 'name,description', - description: 'Comma separated list of the fields to be included into the new document.', - }, - - ] - }; - + description: INodeTypeDescription = nodeDescription; async execute(this: IExecuteFunctions): Promise { + const { database, connectionString } = validateAndResolveMongoCredentials( + this.getCredentials('mongoDb') + ); - const credentials = this.getCredentials('mongoDb'); + const client: MongoClient = await MongoClient.connect(connectionString, { + useNewUrlParser: true, + useUnifiedTopology: true + }); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - let connectionUri = ''; - - if (credentials.port) { - connectionUri = `mongodb://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}`; - } else { - connectionUri = `mongodb+srv://${credentials.user}:${credentials.password}@${credentials.host}`; - } - - const client = await MongoClient.connect(connectionUri, { useNewUrlParser: true, useUnifiedTopology: true }); - const mdb = client.db(credentials.database as string); + const mdb = client.db(database as string); let returnItems = []; @@ -206,7 +43,6 @@ export class MongoDb implements INodeType { .toArray(); returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); - } else if (operation === 'insert') { // ---------------------------------- // insert @@ -229,7 +65,7 @@ export class MongoDb implements INodeType { returnItems.push({ json: { ...insertItems[parseInt(i, 10)], - id: insertedIds[parseInt(i, 10)] as string, + id: insertedIds[parseInt(i, 10)] as string } }); } @@ -258,7 +94,7 @@ export class MongoDb implements INodeType { continue; } - const filter: { [key: string] :string } = {}; + const filter: { [key: string]: string } = {}; filter[updateKey] = item[updateKey] as string; await mdb @@ -267,7 +103,6 @@ export class MongoDb implements INodeType { } returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]); - } else { throw new Error(`The operation "${operation}" is not supported!`); } diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts new file mode 100644 index 000000000..190ff3de8 --- /dev/null +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.options.ts @@ -0,0 +1,131 @@ +import { INodeTypeDescription } from 'n8n-workflow'; + +/** + * Options to be displayed + */ +export const nodeDescription: INodeTypeDescription = { + displayName: 'MongoDB', + name: 'mongoDb', + icon: 'file:mongoDb.png', + group: ['input'], + version: 1, + description: 'Find, insert and update documents in MongoDB.', + defaults: { + name: 'MongoDB', + color: '#13AA52' + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'mongoDb', + required: true + } + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Find', + value: 'find', + description: 'Find documents.' + }, + { + name: 'Insert', + value: 'insert', + description: 'Insert documents.' + }, + { + name: 'Update', + value: 'update', + description: 'Updates documents.' + } + ], + default: 'find', + description: 'The operation to perform.' + }, + + { + displayName: 'Collection', + name: 'collection', + type: 'string', + required: true, + default: '', + description: 'MongoDB Collection' + }, + + // ---------------------------------- + // find + // ---------------------------------- + { + displayName: 'Query (JSON format)', + name: 'query', + type: 'string', + typeOptions: { + rows: 5 + }, + displayOptions: { + show: { + operation: ['find'] + } + }, + default: '{}', + placeholder: `{ "birth": { "$gt": "1950-01-01" } }`, + required: true, + description: 'MongoDB Find query.' + }, + + // ---------------------------------- + // insert + // ---------------------------------- + { + displayName: 'Fields', + name: 'fields', + type: 'string', + displayOptions: { + show: { + operation: ['insert'] + } + }, + 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'] + } + }, + default: 'id', + required: true, + 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'] + } + }, + default: '', + placeholder: 'name,description', + description: + 'Comma separated list of the fields to be included into the new document.' + } + ] +}; diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts new file mode 100644 index 000000000..2995fdd2a --- /dev/null +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.types.ts @@ -0,0 +1,52 @@ +import { CredentialInformation } from 'n8n-workflow'; + +/** + * Credentials object for Mongo, if using individual parameters + */ +export interface IMongoParametricCredentials { + /** + * Whether to allow overriding the parametric credentials with a connection string + */ + shouldOverrideConnString: false | undefined | null; + 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 + */ + shouldOverrideConnString: true; + /** + * If using an override connection string, this is where it will be. + */ + connStringOverrideVal: 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; +}; diff --git a/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts new file mode 100644 index 000000000..1d682a450 --- /dev/null +++ b/packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts @@ -0,0 +1,104 @@ +import { + IDataObject, + INodeExecutionData, + ICredentialDataDecryptedObject +} from 'n8n-workflow'; +import { + IMongoCredentialsType, + IMongoParametricCredentials, + IMongoCredentials +} from './mongo.node.types'; + +/** + * 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 + */ +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 + */ +function buildMongoConnectionParams( + credentials: IMongoCredentialsType +): IMongoCredentials { + const sanitizedDbName = + credentials.database && credentials.database.trim().length > 0 + ? credentials.database.trim() + : ''; + if (credentials.shouldOverrideConnString) { + if ( + credentials.connStringOverrideVal && + credentials.connStringOverrideVal.trim().length > 0 + ) { + return { + connectionString: credentials.connStringOverrideVal.trim(), + database: sanitizedDbName + }; + } else { + throw new Error( + '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( + credentials?: ICredentialDataDecryptedObject +): IMongoCredentials { + if (credentials == undefined) { + throw new Error('No credentials got returned!'); + } else { + return buildMongoConnectionParams( + (credentials as any) as IMongoCredentialsType + ); + } +} + +/** + * Returns of copy of the items which only contains the json data and + * of that only the define properties + * + * @param {INodeExecutionData[]} items The items to copy + * @param {string[]} properties The properties it should include + * @returns + */ +export function getItemCopy( + items: INodeExecutionData[], + properties: string[] +): IDataObject[] { + // Prepare the data to insert and copy it to be returned + let newItem: IDataObject; + return items.map(item => { + newItem = {}; + for (const property of properties) { + if (item.json[property] === undefined) { + newItem[property] = null; + } else { + newItem[property] = JSON.parse(JSON.stringify(item.json[property])); + } + } + return newItem; + }); +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 537223969..51062fbe2 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -261,7 +261,7 @@ "@types/jest": "^24.0.18", "@types/lodash.set": "^4.3.6", "@types/moment-timezone": "^0.5.12", - "@types/mongodb": "^3.3.6", + "@types/mongodb": "^3.5.4", "@types/node": "^10.10.1", "@types/nodemailer": "^4.6.5", "@types/redis": "^2.8.11", @@ -292,7 +292,7 @@ "lodash.unset": "^4.5.2", "moment": "2.24.0", "moment-timezone": "^0.5.28", - "mongodb": "^3.3.2", + "mongodb": "^3.5.5", "mysql2": "^2.0.1", "n8n-core": "~0.32.0", "nodemailer": "^5.1.1",