🔀 Merge branch 'clonardo-mongodb_updates'

This commit is contained in:
Jan Oberhauser 2020-05-01 20:25:26 +02:00
commit 069228f00a
11 changed files with 412 additions and 208 deletions

View file

@ -6,3 +6,10 @@ indent_style = tab
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[package.json]
indent_style = space
indent_size = 2
[*.ts]
quote_type = single

View file

@ -6,6 +6,7 @@
"bootstrap": "lerna bootstrap --hoist --no-ci", "bootstrap": "lerna bootstrap --hoist --no-ci",
"build": "lerna exec npm run build", "build": "lerna exec npm run build",
"dev": "lerna exec npm run dev --parallel", "dev": "lerna exec npm run dev --parallel",
"clean:dist": "lerna exec -- rimraf ./dist",
"start": "run-script-os", "start": "run-script-os",
"start:default": "cd packages/cli/bin && ./n8n", "start:default": "cd packages/cli/bin && ./n8n",
"start:windows": "cd packages/cli/bin && n8n", "start:windows": "cd packages/cli/bin && n8n",
@ -14,6 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"lerna": "^3.13.1", "lerna": "^3.13.1",
"rimraf": "^3.0.2",
"run-script-os": "^1.0.7" "run-script-os": "^1.0.7"
}, },
"postcss": {} "postcss": {}

View file

@ -93,7 +93,7 @@
"jwks-rsa": "^1.6.0", "jwks-rsa": "^1.6.0",
"localtunnel": "^2.0.0", "localtunnel": "^2.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mongodb": "^3.2.3", "mongodb": "^3.5.5",
"mysql2": "^2.0.1", "mysql2": "^2.0.1",
"n8n-core": "~0.32.0", "n8n-core": "~0.32.0",
"n8n-editor-ui": "~0.43.0", "n8n-editor-ui": "~0.43.0",

View file

@ -21,7 +21,8 @@
<font-awesome-icon icon="question-circle" /> <font-awesome-icon icon="question-circle" />
</el-tooltip> </el-tooltip>
</div> </div>
<el-row v-for="parameter in credentialTypeData.properties" :key="parameter.name" class="parameter-wrapper"> <span v-for="parameter in credentialTypeData.properties" :key="parameter.name">
<el-row v-if="displayCredentialParameter(parameter)" class="parameter-wrapper">
<el-col :span="6" class="parameter-name"> <el-col :span="6" class="parameter-name">
{{parameter.displayName}}: {{parameter.displayName}}:
<el-tooltip placement="top" class="parameter-info" v-if="parameter.description" effect="light"> <el-tooltip placement="top" class="parameter-info" v-if="parameter.description" effect="light">
@ -33,6 +34,7 @@
<parameter-input :parameter="parameter" :value="propertyValue[parameter.name]" :path="parameter.name" :isCredential="true" @valueChanged="valueChanged" /> <parameter-input :parameter="parameter" :value="propertyValue[parameter.name]" :path="parameter.name" :isCredential="true" @valueChanged="valueChanged" />
</el-col> </el-col>
</el-row> </el-row>
</span>
<el-row class="nodes-access-wrapper"> <el-row class="nodes-access-wrapper">
<el-col :span="6" class="headline"> <el-col :span="6" class="headline">
@ -85,6 +87,7 @@ import {
ICredentialType, ICredentialType,
ICredentialNodeAccess, ICredentialNodeAccess,
INodeCredentialDescription, INodeCredentialDescription,
INodeProperties,
INodeTypeDescription, INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -162,6 +165,14 @@ export default mixins(
tempValue[name] = parameterData.value; tempValue[name] = parameterData.value;
Vue.set(this, 'propertyValue', tempValue); Vue.set(this, 'propertyValue', tempValue);
}, },
displayCredentialParameter (parameter: INodeProperties): boolean {
if (parameter.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return this.displayParameter(this.propertyValue, parameter, '');
},
async createCredentials (): Promise<void> { async createCredentials (): Promise<void> {
const nodesAccess = this.nodesAccess.map((nodeType) => { const nodesAccess = this.nodesAccess.map((nodeType) => {
return { return {

View file

@ -179,7 +179,7 @@ if (process.env.NODE_ENV !== 'production') {
// not do anything about it anyway // not do anything about it anyway
return; 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(message); // eslint-disable-line no-console
console.error(error); // eslint-disable-line no-console console.error(error); // eslint-disable-line no-console
}; };

View file

@ -1,45 +1,106 @@
import { import { ICredentialType, NodePropertyTypes } from 'n8n-workflow';
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MongoDb implements ICredentialType { export class MongoDb implements ICredentialType {
name = 'mongoDb'; name = 'mongoDb';
displayName = 'MongoDB'; displayName = 'MongoDB';
properties = [ properties = [
{
displayName: 'Configuration Type',
name: 'configurationType',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'Connection String',
value: 'connectionString',
description: 'Provide connection data via string',
},
{
name: 'Values',
value: 'values',
description: 'Provide connection data via values',
},
],
default: 'values',
description: 'The operation to perform.',
},
{
displayName: 'Connection String',
name: 'connectionString',
type: 'string' as NodePropertyTypes,
displayOptions: {
show: {
configurationType: [
'connectionString',
],
},
},
default: '',
placeholder: 'mongodb://<USERNAME>:<PASSWORD>@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,<br />
and the MongoDB credentials will be ignored`
},
{ {
displayName: 'Host', displayName: 'Host',
name: 'host', name: 'host',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: 'localhost', displayOptions: {
show: {
configurationType: [
'values',
],
},
},
default: 'localhost'
}, },
{ {
displayName: 'Database', displayName: 'Database',
name: 'database', name: 'database',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', default: '',
description: 'Note: the database should still be provided even if using an override connection string'
}, },
{ {
displayName: 'User', displayName: 'User',
name: 'user', name: 'user',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', displayOptions: {
show: {
configurationType: [
'values',
],
},
},
default: ''
}, },
{ {
displayName: 'Password', displayName: 'Password',
name: 'password', name: 'password',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
typeOptions: { typeOptions: {
password: true, password: true
}, },
default: '', displayOptions: {
show: {
configurationType: [
'values',
],
},
},
default: ''
}, },
{ {
displayName: 'Port', displayName: 'Port',
name: 'port', name: 'port',
type: 'number' as NodePropertyTypes, type: 'number' as NodePropertyTypes,
default: 27017, displayOptions: {
show: {
configurationType: [
'values',
],
},
},
default: 27017
}, },
]; ];
} }

View file

@ -3,192 +3,29 @@ import {
IDataObject, IDataObject,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription
} from 'n8n-workflow'; } from 'n8n-workflow';
import { nodeDescription } from './mongo.node.options';
import { MongoClient } from 'mongodb'; import { MongoClient } from 'mongodb';
import {
getItemCopy,
/** validateAndResolveMongoCredentials
* Returns of copy of the items which only contains the json data and } from './mongo.node.utils';
* 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;
});
}
export class MongoDb implements INodeType { export class MongoDb implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = nodeDescription;
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.',
},
]
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
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) { const mdb = client.db(database as string);
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);
let returnItems = []; let returnItems = [];
@ -206,7 +43,6 @@ export class MongoDb implements INodeType {
.toArray(); .toArray();
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
} else if (operation === 'insert') { } else if (operation === 'insert') {
// ---------------------------------- // ----------------------------------
// insert // insert
@ -229,7 +65,7 @@ export class MongoDb implements INodeType {
returnItems.push({ returnItems.push({
json: { json: {
...insertItems[parseInt(i, 10)], ...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; continue;
} }
const filter: { [key: string] :string } = {}; const filter: { [key: string]: string } = {};
filter[updateKey] = item[updateKey] as string; filter[updateKey] = item[updateKey] as string;
await mdb await mdb
@ -267,7 +103,6 @@ export class MongoDb implements INodeType {
} }
returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]); returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]);
} else { } else {
throw new Error(`The operation "${operation}" is not supported!`); throw new Error(`The operation "${operation}" is not supported!`);
} }

View file

@ -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.'
}
]
};

View file

@ -0,0 +1,53 @@
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
*/
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;
};

View file

@ -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.configurationType === 'connectionString') {
if (
credentials.connectionString &&
credentials.connectionString.trim().length > 0
) {
return {
connectionString: credentials.connectionString.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 unknown 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;
});
}

View file

@ -261,7 +261,7 @@
"@types/jest": "^24.0.18", "@types/jest": "^24.0.18",
"@types/lodash.set": "^4.3.6", "@types/lodash.set": "^4.3.6",
"@types/moment-timezone": "^0.5.12", "@types/moment-timezone": "^0.5.12",
"@types/mongodb": "^3.3.6", "@types/mongodb": "^3.5.4",
"@types/node": "^10.10.1", "@types/node": "^10.10.1",
"@types/nodemailer": "^4.6.5", "@types/nodemailer": "^4.6.5",
"@types/redis": "^2.8.11", "@types/redis": "^2.8.11",
@ -292,7 +292,7 @@
"lodash.unset": "^4.5.2", "lodash.unset": "^4.5.2",
"moment": "2.24.0", "moment": "2.24.0",
"moment-timezone": "^0.5.28", "moment-timezone": "^0.5.28",
"mongodb": "^3.3.2", "mongodb": "^3.5.5",
"mysql2": "^2.0.1", "mysql2": "^2.0.1",
"n8n-core": "~0.32.0", "n8n-core": "~0.32.0",
"nodemailer": "^5.1.1", "nodemailer": "^5.1.1",