├── .gitignore ├── src ├── fixtures │ ├── database_template │ │ ├── database.config.js │ │ ├── models │ │ │ ├── index.js │ │ │ ├── TokenModel.js │ │ │ ├── Table2Model.js │ │ │ ├── Table1Model.js │ │ │ └── UserModel.js │ │ └── Database.ServiceTemplate.js │ └── error.codes.js ├── services │ ├── greeter.service.js │ ├── auth.service.js │ ├── api.service.js │ ├── serviceB.service.js │ ├── serviceA.service.js │ └── users.service.js ├── mixins │ └── request.mixin.js └── adapters │ └── Database.js ├── index.js ├── moleculer.config.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | moleculer-mysql-template.wiki/ 4 | -------------------------------------------------------------------------------- /src/fixtures/database_template/database.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | host: "mysql.example.host", 4 | port: "3306", // Default for mysql => 3306 5 | database: "db_example", 6 | username: "db_user", 7 | password: "db_password" 8 | } 9 | -------------------------------------------------------------------------------- /src/fixtures/database_template/models/index.js: -------------------------------------------------------------------------------- 1 | const UserModel = require("./UserModel"); 2 | const TokenModel = require("./TokenModel"); 3 | const Table1Model = require("./Table1Model"); 4 | const Table2Model = require("./Table2Model"); 5 | 6 | 7 | 8 | module.exports = { 9 | User: UserModel, 10 | Token: TokenModel, 11 | Table1: Table1Model, 12 | Table2: Table2Model 13 | }; 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ServiceBroker } = require("moleculer"); 4 | const DatabaseServices = require("./src/fixtures/database_template/Database.ServiceTemplate"); 5 | 6 | const broker = new ServiceBroker({ logger: console }); 7 | 8 | 9 | 10 | broker.loadServices("./src/services"); 11 | 12 | DatabaseServices.forEach( (service) => { 13 | broker.createService(service); 14 | }); 15 | 16 | 17 | 18 | broker.start() 19 | .then( () => { 20 | broker.repl(); 21 | broker.call("users.createAdminIfNotExists") 22 | .then( () => console.log("Server started")); 23 | }); 24 | -------------------------------------------------------------------------------- /src/fixtures/database_template/models/TokenModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Sequelize = require("sequelize"); 4 | 5 | // For more information about Sequelize Data Types : 6 | // http://docs.sequelizejs.com/manual/tutorial/models-definition.html#data-types 7 | 8 | 9 | 10 | module.exports = { 11 | name: "token", 12 | define: { 13 | id: { // id must always exist 14 | type: Sequelize.UUID, // Uses uuidv4 by default (default value is recommended) 15 | primaryKey: true, 16 | defaultValue: Sequelize.UUIDV4 17 | }, 18 | 19 | token: { 20 | type: Sequelize.TEXT, 21 | allowNull: false 22 | }, 23 | 24 | userId: { 25 | type: Sequelize.UUID, 26 | allowNull: false 27 | } 28 | }, 29 | options: { 30 | timestamps: false 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/fixtures/database_template/models/Table2Model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Sequelize = require("sequelize"); 4 | 5 | // For more information about Sequelize Data Types : 6 | // http://docs.sequelizejs.com/manual/tutorial/models-definition.html#data-types 7 | 8 | 9 | 10 | module.exports = { 11 | name: "table2_elt", 12 | define: { 13 | id: { // id must always exist 14 | type: Sequelize.UUID, // Uses uuidv4 by default (default value is recommended) 15 | primaryKey: true, 16 | defaultValue: Sequelize.UUIDV4 17 | }, 18 | 19 | first: { 20 | type: Sequelize.TEXT, 21 | allowNull: false 22 | }, 23 | 24 | second: { 25 | type: Sequelize.BIGINT, 26 | allowNull: true 27 | } 28 | }, 29 | options: { 30 | timestamps: false 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /moleculer.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | namespace: "dev", 5 | nodeID: null, 6 | 7 | logger: true, 8 | logLevel: "info", 9 | logFormatter: "default", 10 | 11 | serializer: null, 12 | 13 | requestTimeout: 0 * 1000, 14 | requestRetry: 0, 15 | maxCallLevel: 0, 16 | heartbeatInterval: 5, 17 | heartbeatTimeout: 15, 18 | 19 | disableBalancer: false, 20 | 21 | registry: { 22 | strategy: "RoundRobin", 23 | preferLocal: true 24 | }, 25 | 26 | circuitBreaker: { 27 | enabled: false, 28 | maxFailures: 3, 29 | halfOpenTime: 10 * 1000, 30 | failureOnTimeout: true, 31 | failureOnReject: true 32 | }, 33 | 34 | validation: true, 35 | validator: null, 36 | metrics: false, 37 | metricsRate: 1, 38 | statistics: false, 39 | internalActions: true, 40 | 41 | hotReload: false 42 | }; -------------------------------------------------------------------------------- /src/services/greeter.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | name: "greeter", 5 | 6 | settings: { 7 | 8 | }, 9 | 10 | metadata: { 11 | 12 | }, 13 | 14 | actions: { 15 | 16 | /** 17 | * Say a 'Hello' 18 | * 19 | * @returns 20 | */ 21 | hello() { 22 | return "Hello Moleculer"; 23 | }, 24 | 25 | /** 26 | * Welcome a username 27 | * 28 | * @param {String} name - User name 29 | */ 30 | welcome: { 31 | params: { 32 | name: "string" 33 | }, 34 | handler(ctx) { 35 | return `Welcome, ${ctx.params.name}`; 36 | } 37 | } 38 | }, 39 | 40 | events: { 41 | 42 | }, 43 | 44 | methods: { 45 | 46 | }, 47 | 48 | created() { 49 | 50 | }, 51 | 52 | started() { 53 | console.log(`Service ${this.name} up and running`); 54 | }, 55 | 56 | stopped() { 57 | console.log(`Service ${this.name} down`); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/fixtures/database_template/models/Table1Model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Sequelize = require("sequelize"); 4 | 5 | // For more information about Sequelize Data Types : 6 | // http://docs.sequelizejs.com/manual/tutorial/models-definition.html#data-types 7 | 8 | 9 | 10 | module.exports = { 11 | name: "table1_elt", 12 | define: { 13 | id: { // id must always exist 14 | type: Sequelize.UUID, // Uses uuidv4 by default (default value is recommended) 15 | primaryKey: true, 16 | defaultValue: Sequelize.UUIDV4 17 | }, 18 | 19 | first: { 20 | type: Sequelize.STRING(255), 21 | allowNull: false, 22 | unique: true, 23 | defaultValue: "Default" 24 | }, 25 | 26 | second: { 27 | type: Sequelize.TEXT, 28 | allowNull: false 29 | }, 30 | 31 | third: { 32 | type: Sequelize.DOUBLE, 33 | allowNull: false 34 | } 35 | }, 36 | options: { 37 | timestamps: false 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/fixtures/database_template/models/UserModel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Sequelize = require("sequelize"); 4 | 5 | // For more information about Sequelize Data Types : 6 | // http://docs.sequelizejs.com/manual/tutorial/models-definition.html#data-types 7 | 8 | 9 | 10 | module.exports = { 11 | name: "user", 12 | define: { 13 | id: { // id must always exist 14 | type: Sequelize.UUID, // Uses uuidv4 by default (default value is recommended) 15 | primaryKey: true, 16 | defaultValue: Sequelize.UUIDV4 17 | }, 18 | 19 | username: { 20 | type: Sequelize.STRING(20), 21 | allowNull: false, 22 | unique: true 23 | }, 24 | 25 | password: { 26 | type: Sequelize.TEXT, 27 | allowNull: false 28 | }, 29 | 30 | role: { 31 | type: Sequelize.STRING(10), 32 | allowNull: false, 33 | defaultValue: "USER" 34 | }, 35 | 36 | age: { 37 | type: Sequelize.INTEGER, 38 | allowNull: true 39 | } 40 | }, 41 | options: { 42 | timestamps: false 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/fixtures/error.codes.js: -------------------------------------------------------------------------------- 1 | // All strings must be different 2 | module.exports = { 3 | // Errors on Table1 4 | T1_NOTHING_FOUND: "Table1: Nothing Found", 5 | T1_FIRST_CONSTRAINT: "Invalid First", 6 | T1_THIRD_CONSTRAINT: "Invalid Third", 7 | 8 | // Errors on Table2 9 | T2_NOTHING_FOUND: "Table2: Nothing Found", 10 | T1_SECOND_CONSTRAINT: "Invalid Second", 11 | 12 | // Errors on Table1 & Table2 13 | T1_T2_NOTHING_FOUND: "Nothing Found", 14 | 15 | // Errors on Users 16 | USERS_NOT_LOGGED_ERROR: "User Not Logged", 17 | USERS_NOTHING_FOUND: "Unknown Username", 18 | USERS_USERNAME_CONSTRAINT: "Invalid Username", 19 | USERS_FORBIDDEN_REMOVE: "Forbidden Remove", 20 | USERS_INVALID_ROLE: "Invalid Role", 21 | 22 | // Errors on Auth 23 | AUTH_INVALID_CREDENTIALS: "Invalid Crendentials", 24 | AUTH_ADMIN_RESTRICTION: "Restricted Action", 25 | AUTH_ACCESS_DENIED: "Access Denied", 26 | AUTH_INVALID_TOKEN: "Invalid Token", 27 | AUTH_NO_TOKEN: "Token Required", 28 | 29 | // Unknown Error 30 | UNKOWN_ERROR: "Unknown Error" 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moleculer-mysql-template", 3 | "version": "2.0.0", 4 | "description": "Moleculer-MySQL Adapter with authentication", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ./index.js" 8 | }, 9 | "keywords": [ 10 | "microservices", 11 | "moleculer", 12 | "moleculer-web", 13 | "mysql", 14 | "jsonwebtoken", 15 | "authentication" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:AGenson/moleculer-mysql-template.git" 20 | }, 21 | "homepage": "https://github.com/AGenson/moleculer-mysql-template#README", 22 | "author": "Anthony Genson", 23 | "devDependencies": { 24 | "moleculer-repl": "^0.3.0" 25 | }, 26 | "dependencies": { 27 | "jsonwebtoken": "^8.2.1", 28 | "moleculer": "^0.11.0", 29 | "moleculer-db": "^0.7.0", 30 | "moleculer-db-adapter-sequelize": "^0.1.5", 31 | "moleculer-web": "^0.6.0", 32 | "mysql2": "^1.5.2", 33 | "password-hash": "^1.2.2" 34 | }, 35 | "engines": { 36 | "node": ">= 6.x.x" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/fixtures/database_template/Database.ServiceTemplate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const DbService = require("moleculer-db"); 4 | const SqlAdapter = require("moleculer-db-adapter-sequelize"); 5 | const Sequelize = require("sequelize"); 6 | const Config = require("./database.config"); 7 | const Models = require("./models/index"); 8 | 9 | 10 | 11 | const serviceActions = { 12 | actions: { 13 | 14 | insertMany(ctx) { 15 | return this.adapter.insertMany(ctx.params.entities); 16 | }, 17 | 18 | updateById(ctx) { 19 | return this.adapter.updateById(ctx.params.id, { $set: ctx.params.update }); 20 | }, 21 | 22 | removeById(ctx) { 23 | return this.adapter.removeById(ctx.params.id); 24 | }, 25 | 26 | removeMany(ctx) { 27 | return this.adapter.removeMany(ctx.params.query); 28 | }, 29 | 30 | removeAll(ctx) { 31 | return this.adapter.clear(); 32 | } 33 | 34 | } 35 | } 36 | 37 | 38 | 39 | function CreateDBService(table) { 40 | if (Models[table] !== undefined) 41 | return { 42 | name: `DB_${table}s`, 43 | 44 | mixins: [ 45 | DbService, 46 | serviceActions 47 | ], 48 | adapter: new SqlAdapter(`mysql://${Config.username}:${Config.password}@${Config.host}:${Config.port}/${Config.database}`), 49 | model: Models[table] 50 | 51 | }; 52 | else 53 | return undefined; 54 | }; 55 | 56 | 57 | 58 | var DatabaseServices = []; 59 | 60 | for (var item in Models){ 61 | if (!Models[item].define.id) 62 | throw new Error(`Error: model of table '${item}' needs to have a field 'id' as a Primary Key.`); 63 | if (Models[item].define.id.primaryKey !== true) 64 | throw new Error(`Error: field 'id' of table '${item}' needs to be set as a Primary Key.`); 65 | 66 | DatabaseServices.push( CreateDBService(item) ); 67 | } 68 | 69 | 70 | 71 | module.exports = DatabaseServices; 72 | -------------------------------------------------------------------------------- /src/mixins/request.mixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Promise = require("bluebird"); 4 | const { MoleculerError } = require("moleculer").Errors; 5 | const CodeTypes = require("../fixtures/error.codes"); 6 | 7 | 8 | // Common methods for request answer to different services 9 | module.exports = { 10 | methods: { 11 | 12 | requestSuccess(name, data) { 13 | return Promise.resolve({ 14 | name: name, 15 | data: data, 16 | code: 200 17 | }); 18 | }, 19 | 20 | requestError(codeError) { 21 | var message, code; 22 | 23 | switch (codeError) { 24 | 25 | // Errors on Table1 26 | 27 | case CodeTypes.T1_NOTHING_FOUND: 28 | message = "No entity found in Table1 with the given parameters"; 29 | code = 404; 30 | break; 31 | 32 | case CodeTypes.T1_FIRST_CONSTRAINT: 33 | message = "First must be unique: not all entities have been inserted or updated"; 34 | code = 417; 35 | break; 36 | 37 | case CodeTypes.T1_THIRD_CONSTRAINT: 38 | message = "Third must be a number"; 39 | code = 417; 40 | break; 41 | 42 | // Errors on Table2 43 | 44 | case CodeTypes.T2_NOTHING_FOUND: 45 | message = "No entity found in Table2 with the given parameters"; 46 | code = 404; 47 | break; 48 | 49 | case CodeTypes.T2_SECOND_CONSTRAINT: 50 | message = "Second must be a number"; 51 | code = 417; 52 | break; 53 | 54 | // Errors on Table1 & Table2 55 | 56 | case CodeTypes.T1_T2_NOTHING_FOUND: 57 | message = "Table1 or Table2 is empty"; 58 | code = 404; 59 | break; 60 | 61 | // Errors on Users 62 | 63 | case CodeTypes.USERS_NOT_LOGGED_ERROR: 64 | message = "Action need a logged user"; 65 | code = 401; 66 | break; 67 | 68 | case CodeTypes.USERS_NOTHING_FOUND: 69 | message = "Username does not exist"; 70 | code = 404; 71 | break; 72 | 73 | case CodeTypes.USERS_USERNAME_CONSTRAINT: 74 | message = "Username already used"; 75 | code = 417; 76 | break; 77 | 78 | case CodeTypes.USERS_FORBIDDEN_REMOVE: 79 | message = "ADMIN User cannot be removed"; 80 | code = 403; 81 | break; 82 | 83 | case CodeTypes.USERS_INVALID_ROLE: 84 | message = "Role Unknown"; 85 | code = 417; 86 | break; 87 | 88 | // Errors on Auth 89 | 90 | case CodeTypes.AUTH_INVALID_CREDENTIALS: 91 | message = "Wrong password"; 92 | code = 417; 93 | break; 94 | 95 | case CodeTypes.AUTH_ADMIN_RESTRICTION: 96 | message = "Action need ADMIN permission"; 97 | code = 401; 98 | break; 99 | 100 | case CodeTypes.AUTH_ACCESS_DENIED: 101 | message = "Role invalid for this action"; 102 | code = 401; 103 | break; 104 | 105 | case CodeTypes.AUTH_INVALID_TOKEN: 106 | message = "Invalid Token: verification of authentification failed"; 107 | code = 401; 108 | break; 109 | 110 | case CodeTypes.AUTH_NO_TOKEN: 111 | message = "Missing Token: a logged User is required for this kind of request"; 112 | code = 401; 113 | break; 114 | 115 | // Unknown Error 116 | 117 | default: 118 | message = "Operation failed internally: unknown details"; 119 | code = 500; 120 | break; 121 | } 122 | 123 | return this.Promise.reject(new MoleculerError(codeError, code, "ERR_CRITIAL", { code: code, message: message })); 124 | } 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /src/services/auth.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const jwt = require("jsonwebtoken"); 4 | const passwordHash = require('password-hash'); 5 | const { pick } = require("lodash"); 6 | const Promise = require("bluebird"); 7 | const { MoleculerError } = require("moleculer").Errors; 8 | const Database = require("../adapters/Database"); 9 | const Request = require("../mixins/request.mixin"); 10 | const CodeTypes = require("../fixtures/error.codes"); 11 | 12 | // Filters applied when searching for entities 13 | // Elements correspond to the columns of the table 14 | const Filters_Users = { 15 | security: ["id", "username", "password", "role"], 16 | encode: ["id", "username", "role"] 17 | }; 18 | const Filters_Tokens = { 19 | empty: ["id"] 20 | }; 21 | 22 | const JWT_SECRET = "TOP SECRET!!!"; 23 | 24 | 25 | 26 | module.exports = { 27 | name: "auth", 28 | 29 | mixins: [ Request ], 30 | 31 | actions: { 32 | 33 | login: { 34 | params: { 35 | username: "string", 36 | password: "string" 37 | }, 38 | handler(ctx) { 39 | return ctx.call("auth.verifyPassword", { username: ctx.params.username, password: ctx.params.password }) 40 | .then( (res) => { 41 | return this.generateToken(res.data) 42 | .then( (res2) => { 43 | return this.DB_Tokens.insert(ctx, { 44 | userId: res.data.id, 45 | token: res2 46 | }) 47 | .then( () => this.requestSuccess("Login Success", res2) ); 48 | }) 49 | }) 50 | .catch( (err) => { 51 | if (err instanceof MoleculerError) 52 | return Promise.reject(err); 53 | else 54 | return this.requestError(CodeTypes.UNKOWN_ERROR); 55 | }); 56 | } 57 | }, 58 | 59 | verifyPassword: { 60 | params: { 61 | username: "string", 62 | password: "string" 63 | }, 64 | handler(ctx) { 65 | return this.DB_Users.findOne(ctx, { 66 | query: { 67 | username: ctx.params.username 68 | }, 69 | filter: Filters_Users.security 70 | }) 71 | .then( (res) => { 72 | if (passwordHash.verify(ctx.params.password, res.data.password)) 73 | return this.requestSuccess("Valid Password", pick(res.data, Filters_Users.encode)); 74 | else 75 | return this.requestError(CodeTypes.AUTH_INVALID_CREDENTIALS); 76 | }) 77 | .catch( (err) => { 78 | if (err instanceof MoleculerError) 79 | return Promise.reject(err); 80 | else if (err.name === 'Nothing Found') 81 | return this.requestError(CodeTypes.USERS_NOTHING_FOUND); 82 | else 83 | return this.requestError(CodeTypes.UNKOWN_ERROR); 84 | }); 85 | } 86 | }, 87 | 88 | verifyToken: { 89 | params: { 90 | token: "string" 91 | }, 92 | handler(ctx) { 93 | return this.DB_Tokens.findOne(ctx, { 94 | query: { 95 | token: ctx.params.token 96 | } 97 | }) 98 | .then( () => this.verify(ctx.params.token, JWT_SECRET)) 99 | .catch( () => undefined ); 100 | } 101 | }, 102 | 103 | countSessions: { 104 | params: { 105 | 106 | }, 107 | handler(ctx) { 108 | return this.verifyIfLogged(ctx) 109 | .then( () => this.DB_Tokens.count(ctx, { 110 | userId: ctx.meta.user.id 111 | })) 112 | .then( (res) => this.requestSuccess("Count Complete", res.data) ) 113 | .catch( (err) => { 114 | if (err instanceof MoleculerError) 115 | return Promise.reject(err); 116 | else 117 | return this.requestError(CodeTypes.UNKOWN_ERROR); 118 | }); 119 | } 120 | }, 121 | 122 | closeAllSessions: { 123 | params: { 124 | 125 | }, 126 | handler(ctx) { 127 | return this.verifyIfLogged(ctx) 128 | .then( () => this.DB_Tokens.removeMany(ctx, { 129 | userId: ctx.meta.user.id 130 | })) 131 | .then( () => this.requestSuccess("All existing sessions closed", true) ) 132 | .catch( (err) => { 133 | if (err instanceof MoleculerError) 134 | return Promise.reject(err); 135 | else 136 | return this.requestError(CodeTypes.UNKOWN_ERROR); 137 | }); 138 | } 139 | }, 140 | 141 | logout: { 142 | params: { 143 | 144 | }, 145 | handler(ctx) { 146 | return this.verifyIfLogged(ctx) 147 | .then( () => this.DB_Tokens.removeMany(ctx, { 148 | token: ctx.meta.user.token 149 | })) 150 | .then( () => this.requestSuccess("Logout Success", true) ) 151 | .catch( (err) => { 152 | if (err instanceof MoleculerError) 153 | return Promise.reject(err); 154 | else 155 | return this.requestError(CodeTypes.UNKOWN_ERROR); 156 | }); 157 | } 158 | } 159 | 160 | }, 161 | 162 | methods: { 163 | 164 | generateToken(user) { 165 | return this.encode(user, JWT_SECRET); 166 | }, 167 | 168 | verifyIfLogged(ctx){ 169 | if (ctx.meta.user !== undefined) 170 | return this.requestSuccess("User Logged", true); 171 | else 172 | return this.requestError(CodeTypes.USERS_NOT_LOGGED_ERROR); 173 | } 174 | 175 | }, 176 | 177 | created() { 178 | // Create Promisify encode & verify methods 179 | this.encode = Promise.promisify(jwt.sign); 180 | this.verify = Promise.promisify(jwt.verify); 181 | 182 | this.DB_Users = new Database("User", Filters_Users.token); 183 | this.DB_Tokens = new Database("Token", Filters_Tokens.empty); 184 | } 185 | }; 186 | -------------------------------------------------------------------------------- /src/services/api.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Promise = require("bluebird"); 4 | const ApiGateway = require("moleculer-web"); 5 | const { MoleculerError } = require("moleculer").Errors; 6 | const Request = require("../mixins/request.mixin"); 7 | const CodeTypes = require("../fixtures/error.codes"); 8 | 9 | 10 | module.exports = { 11 | name: "api", 12 | 13 | mixins: [ 14 | ApiGateway, 15 | Request 16 | ], 17 | 18 | settings: { 19 | port: process.env.PORT || 9000, 20 | 21 | cors: { 22 | origin: "*", 23 | methods: ["GET", /*"PATCH", "OPTIONS",*/ "POST", "PUT", "DELETE"], 24 | allowedHeaders: ["Content-Type"], 25 | exposedHeaders: [], 26 | credentials: false, 27 | maxAge: 3600 28 | }, 29 | 30 | //path: "/api", 31 | 32 | routes: [ 33 | 34 | { 35 | bodyParsers: { 36 | json: true, 37 | }, 38 | path: "/public/", 39 | authorization: false, 40 | whitelist: [ 41 | "auth.login", 42 | "users.create" 43 | ], 44 | aliases: { 45 | // Auth: login only 46 | "POST login": "auth.login", 47 | 48 | // Users: create User account only 49 | "POST user": "users.create" 50 | } 51 | }, 52 | { 53 | bodyParsers: { 54 | json: true, 55 | }, 56 | path: "/admin/", 57 | roles: ["ADMIN"], 58 | authorization: true, 59 | whitelist: [ 60 | "users.*" 61 | ], 62 | aliases: { 63 | // Users: Actions on Users that needs priviledges 64 | "GET users/count": "users.count", 65 | "PUT user/change/role/:username/:role": "users.changeRole", 66 | "DELETE banish/:username": "users.banish", 67 | "DELETE users": "users.removeAll", 68 | } 69 | }, 70 | { 71 | bodyParsers: { 72 | json: true, 73 | }, 74 | path: "/common/", 75 | roles: ["ADMIN", "USER"], 76 | authorization: true, 77 | whitelist: [ 78 | "auth.countSessions", 79 | "auth.logout", 80 | "auth.closeAllSessions", 81 | "users.getAll", 82 | "users.get", 83 | "users.changeInfo", 84 | "users.changePassword", 85 | "users.remove" 86 | ], 87 | aliases: { 88 | // Auth: Session Controls only 89 | "GET sessions": "auth.countSessions", 90 | "DELETE logout": "auth.logout", 91 | "DELETE sessions": "auth.closeAllSessions", 92 | 93 | // Users: Actions on Users that does not need priviledges 94 | "GET users": "users.getAll", 95 | "GET user/:username": "users.get", 96 | "PUT user/change/infos": "users.changeInfo", 97 | "PUT user/change/password": "users.changePassword", 98 | "DELETE user": "users.remove", 99 | } 100 | }, 101 | { 102 | path: "/greeter/", 103 | authorization: false, 104 | whitelist: [ 105 | "greeter.*" 106 | ], 107 | aliases: { 108 | "GET hello": "greeter.hello", 109 | "POST welcome/:name": "greeter.welcome" 110 | } 111 | }, 112 | { 113 | bodyParsers: { 114 | json: true, 115 | }, 116 | path: "/services/", 117 | roles: ["ADMIN", "USER"], 118 | authorization: true, 119 | whitelist: [ 120 | "serviceA.*", 121 | "serviceB.*" 122 | ], 123 | aliases: { 124 | // ServiceA: Table1 only 125 | "POST A/one": "serviceA.create", 126 | "POST A/many": "serviceA.createMany", 127 | "GET A/all": "serviceA.getAll", 128 | "GET A/ids": "serviceA.getAllIds", 129 | "GET A/first/:first": "serviceA.getByFirst", 130 | "GET A/id/:id": "serviceA.getById", 131 | "GET A/count": "serviceA.count", 132 | "PUT A/one": "serviceA.update", 133 | "PUT A/many": "serviceA.updateMany", 134 | "DELETE A/byId/:id": "serviceA.remove", 135 | "DELETE A/byFirst/:first": "serviceA.removeByFirst", 136 | "DELETE A/byThird/:third": "serviceA.removeByThird", 137 | "DELETE A/all": "serviceA.removeAll", 138 | 139 | // ServiceB: Table2 only 140 | "POST B/one": "serviceB.create", 141 | "POST B/many": "serviceB.createMany", 142 | "GET B/all": "serviceB.getAll", 143 | "GET B/ids": "serviceB.getAllIds", 144 | "GET B/first/:first": "serviceB.getByFirst", 145 | "GET B/id/:id": "serviceB.getById", 146 | "GET B/count": "serviceB.count", 147 | "PUT B/one": "serviceB.update", 148 | "PUT B/many": "serviceB.updateMany", 149 | "DELETE B/byId/:id": "serviceB.remove", 150 | "DELETE B/byFirst/:first": "serviceB.removeByFirst", 151 | "DELETE B/byThird/:third": "serviceB.removeByThird", 152 | "DELETE B/all": "serviceB.removeAll", 153 | 154 | // ServiceB: Table1 & Table2 155 | "GET B/all/tables": "serviceB.getAll_1_2" 156 | } 157 | } 158 | 159 | ] 160 | 161 | }, 162 | 163 | methods: { 164 | 165 | authorize(ctx, route, req) { 166 | var authValue = req.headers["authorization"]; 167 | 168 | if (authValue && authValue.startsWith("Bearer ")) { 169 | var token = authValue.slice(7); 170 | 171 | return ctx.call("auth.verifyToken", { token }) 172 | .then( (decoded) => { 173 | if (route.opts.roles.indexOf(decoded.role) === -1) 174 | return this.requestError(CodeTypes.AUTH_ACCESS_DENIED); 175 | 176 | ctx.meta.user = decoded; 177 | ctx.meta.user.token = token; 178 | }) 179 | .catch( (err) => { 180 | if (err instanceof MoleculerError) 181 | return Promise.reject(err); 182 | 183 | return this.requestError(CodeTypes.AUTH_INVALID_TOKEN); 184 | }); 185 | 186 | } else 187 | return this.requestError(CodeTypes.AUTH_NO_TOKEN); 188 | } 189 | 190 | } 191 | }; 192 | -------------------------------------------------------------------------------- /src/services/serviceB.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Database = require("../adapters/Database"); 4 | const Request = require("../mixins/request.mixin"); 5 | const CodeTypes = require("../fixtures/error.codes"); 6 | 7 | // Filters applied when searching for entities 8 | // Elements correspond to the columns of wanted table 9 | const Filters_T1 = { 10 | full: ["id", "first", "second", "third"], 11 | two: ["id", "first", "second"], 12 | one: ["id", "first"], 13 | id: ["id"] 14 | }; 15 | const Filters_T2 = { 16 | full: ["id", "first", "second"], 17 | simple: ["first", "second"], 18 | id: ["id"] 19 | }; 20 | 21 | 22 | 23 | module.exports = { 24 | name: "serviceB", 25 | 26 | mixins: [ Request ], 27 | 28 | actions: { 29 | 30 | create: { 31 | params: { 32 | first: "string", 33 | second: { type: "number", optional: true } // Means that the value can be 'null' 34 | }, 35 | handler(ctx) { 36 | return this.DB_Table2.insert(ctx, { 37 | first: ctx.params.first, 38 | second: ctx.params.second 39 | }) 40 | //.then( (res) => res ) 41 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 42 | } // A 'notNull' error can also occur if a field is defined by 'allowNull: false' 43 | }, 44 | 45 | createMany: { 46 | params: { 47 | entities: { type: "array", optional: false, items: "object" } 48 | }, 49 | handler(ctx) { 50 | return this.DB_Table2.insertMany(ctx, ctx.params.entities) 51 | //.then( (res) => res ) 52 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 53 | } // A 'notNull' error can also occur if a field is defined by 'allowNull: false' 54 | }, 55 | 56 | getAll: { 57 | params: { 58 | 59 | }, 60 | handler(ctx) { 61 | return this.DB_Table2.find(ctx) 62 | //.then( (res) => res ) 63 | .catch( (err) => { 64 | if (err.name === 'Nothing Found') 65 | return this.requestError(CodeTypes.T2_NOTHING_FOUND); 66 | else 67 | return this.requestError(CodeTypes.UNKOWN_ERROR); 68 | }); 69 | } 70 | }, 71 | 72 | getAllIds: { 73 | params: { 74 | 75 | }, 76 | handler(ctx) { 77 | return this.DB_Table2.find(ctx, { 78 | //query: { }, 79 | filter: Filters_T2.id 80 | }) 81 | .then( (res) => { 82 | var result = res; 83 | 84 | result.data = res.data.map( (item) => item.id ); 85 | 86 | return res; 87 | }) 88 | .catch( (err) => { 89 | if (err.name === 'Nothing Found') 90 | return this.requestError(CodeTypes.T2_NOTHING_FOUND); 91 | else 92 | return this.requestError(CodeTypes.UNKOWN_ERROR); 93 | }); 94 | } 95 | }, 96 | 97 | getByFirst: { 98 | params: { 99 | first: "string" 100 | }, 101 | handler(ctx) { 102 | return this.DB_Table2.findOne(ctx, { 103 | query: { 104 | first: ctx.params.first 105 | }, 106 | //filter: Filters_T2.full 107 | }) 108 | //.then( (res) => res ) 109 | .catch( (err) => { 110 | if (err.name === 'Nothing Found') 111 | return this.requestError(CodeTypes.T2_NOTHING_FOUND); 112 | else 113 | return this.requestError(CodeTypes.UNKOWN_ERROR); 114 | }); 115 | } 116 | }, 117 | 118 | getById: { 119 | params: { 120 | id: "string" 121 | }, 122 | handler(ctx) { 123 | return this.DB_Table2.findById(ctx, { 124 | id: ctx.params.id, 125 | //filter: Filters_T2.full 126 | }) 127 | //.then( (res) => res ) 128 | .catch( (err) => { 129 | if (err.name === 'Nothing Found') 130 | return this.requestError(CodeTypes.T2_NOTHING_FOUND); 131 | else 132 | return this.requestError(CodeTypes.UNKOWN_ERROR); 133 | }); 134 | } 135 | }, 136 | 137 | getAll_1_2: { 138 | params: { 139 | 140 | }, 141 | handler(ctx) { 142 | var first_search = {}; 143 | 144 | return this.Promise.all( [ this.DB_Table1.find(ctx), this.DB_Table2.find(ctx)] ) 145 | .then( (res) => { 146 | var result = res; 147 | 148 | result[0].message += " in Table1"; 149 | result[1].message += " in Table2"; 150 | 151 | return result; 152 | }) 153 | .catch( (err) => { 154 | if (err.name === 'Nothing Found') 155 | return this.requestError(CodeTypes.T1_T2_NOTHING_FOUND); 156 | else 157 | return this.requestError(CodeTypes.UNKOWN_ERROR); 158 | }); 159 | } 160 | }, 161 | 162 | count: { 163 | params: { 164 | 165 | }, 166 | handler(ctx) { 167 | return this.DB_Table2.count(ctx, { }) 168 | //.then( (res) => res ) 169 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 170 | } 171 | }, 172 | 173 | update: { 174 | params: { 175 | id: "string", 176 | first: "string", 177 | second: { type: "number", optional: true } // Means that the value can be 'null' 178 | }, 179 | handler(ctx) { 180 | return this.DB_Table2.updateById(ctx, ctx.params.id, { 181 | first: ctx.params.first, 182 | second: ctx.params.second 183 | }) 184 | //.then( (res) => res ) 185 | .catch( (err) => { 186 | if (err.name === 'Nothing Found') 187 | return this.requestError(CodeTypes.T2_NOTHING_FOUND); 188 | else 189 | return this.requestError(CodeTypes.UNKOWN_ERROR); 190 | }); 191 | } 192 | }, 193 | 194 | updateMany: { 195 | params: { 196 | minimum: "number", 197 | second: { type: "number", optional: true } // Means that the value can be 'null' 198 | }, 199 | handler(ctx) { 200 | return this.DB_Table2.updateMany(ctx, { 201 | second: { $gt: ctx.params.minimum } // $gt -> all entities with 'third' field greater than 202 | }, { 203 | second: ctx.params.second 204 | }) 205 | //.then( (res) => res ) 206 | .catch( (err) => { 207 | if (err.name === 'Nothing Found') 208 | return this.requestError(CodeTypes.T2_NOTHING_FOUND); 209 | else 210 | return this.requestError(CodeTypes.UNKOWN_ERROR); 211 | }); 212 | } 213 | }, 214 | 215 | remove: { 216 | params: { 217 | id: "string" 218 | }, 219 | handler(ctx) { 220 | return this.DB_Table2.removeById(ctx, ctx.params.id) 221 | //.then( (res) => res ) 222 | .catch( (err) => { 223 | if (err.name === 'Nothing Found') 224 | return this.requestError(CodeTypes.T2_NOTHING_FOUND); 225 | else 226 | return this.requestError(CodeTypes.UNKOWN_ERROR); 227 | }); 228 | } 229 | }, 230 | 231 | removeByFirst: { 232 | params: { 233 | first: "string" 234 | }, 235 | handler(ctx) { 236 | return this.DB_Table2.removeMany(ctx, { 237 | first: ctx.params.first 238 | }) 239 | //.then( (res) => res ) 240 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 241 | } 242 | }, 243 | 244 | removeBySecond: { 245 | params: { 246 | second: "string" // This time, 'second' is required not null 247 | }, 248 | handler(ctx) { 249 | var second = parseInt(ctx.params.second); 250 | 251 | if (!isNaN(second)) 252 | return this.DB_Table2.removeMany(ctx, { 253 | second: { $lt: ctx.params.second } // $lt -> all entities with 'second' field less than 254 | }) 255 | //.then( (res) => res ) 256 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 257 | else 258 | return this.requestError(CodeTypes.T1_SECOND_CONSTRAINT); 259 | } 260 | }, 261 | 262 | removeAll: { 263 | params: { 264 | 265 | }, 266 | handler(ctx) { 267 | return this.DB_Table2.removeAll(ctx) 268 | //.then( (res) => res ) 269 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 270 | } 271 | } 272 | }, 273 | 274 | methods: { 275 | 276 | }, 277 | 278 | created() { 279 | this.DB_Table1 = new Database("Table1", Filters_T1.two); 280 | this.DB_Table2 = new Database("Table2"); // Default: Filters_T2.full 281 | } 282 | }; 283 | -------------------------------------------------------------------------------- /src/services/serviceA.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Database = require("../adapters/Database"); 4 | const Request = require("../mixins/request.mixin"); 5 | const CodeTypes = require("../fixtures/error.codes"); 6 | 7 | // Filters applied when searching for entities 8 | // Elements correspond to the columns of the table 9 | const Filters_T1 = { 10 | full: ["id", "first", "second", "third"], 11 | two: ["id", "first", "second"], 12 | one: ["id", "first"], 13 | id: ["id"] 14 | }; 15 | 16 | 17 | 18 | module.exports = { 19 | name: "serviceA", 20 | 21 | mixins: [ Request ], 22 | 23 | actions: { 24 | 25 | create: { 26 | params: { 27 | first: "string", 28 | second: "string", 29 | third: "number" 30 | }, 31 | handler(ctx) { 32 | return this.DB_Table1.insert(ctx, { 33 | first: ctx.params.first, 34 | second: ctx.params.second, 35 | third: ctx.params.third 36 | }) 37 | //.then( (res) => res ) 38 | .catch( (err) => { // A 'notNull' error can also occur if a field is defined by 'allowNull: false' 39 | if (err.name === 'Database Error' && Array.isArray(err.data)){ 40 | if (err.data[0].type === 'unique' && err.data[0].field === 'first') 41 | return this.requestError(CodeTypes.T1_FIRST_CONSTRAINT); 42 | } 43 | 44 | return this.requestError(CodeTypes.UNKOWN_ERROR); 45 | }); 46 | } 47 | }, 48 | 49 | createMany: { 50 | params: { 51 | entities: { type: "array", optional: false, items: "object" } 52 | }, 53 | handler(ctx) { 54 | return this.DB_Table1.insertMany(ctx, ctx.params.entities) 55 | //.then( (res) => res ) 56 | .catch( (err) => { // A 'notNull' error can also occur if a field is defined by 'allowNull: false' 57 | if (err.name === 'Database Error' && Array.isArray(err.data)){ 58 | if (err.data[0].type === 'unique' && err.data[0].field === 'first') 59 | return this.requestError(CodeTypes.T1_FIRST_CONSTRAINT); 60 | } 61 | 62 | return this.requestError(CodeTypes.UNKOWN_ERROR); 63 | }); 64 | } 65 | }, 66 | 67 | getAll: { 68 | params: { 69 | 70 | }, 71 | handler(ctx) { 72 | return this.DB_Table1.find(ctx) 73 | //.then( (res) => res ) 74 | .catch( (err) => { 75 | if (err.name === 'Nothing Found') 76 | return this.requestError(CodeTypes.T1_NOTHING_FOUND); 77 | else 78 | return this.requestError(CodeTypes.UNKOWN_ERROR); 79 | }); 80 | } 81 | }, 82 | 83 | getAllIds: { 84 | params: { 85 | 86 | }, 87 | handler(ctx) { 88 | return this.DB_Table1.find(ctx, { 89 | //query: { }, 90 | filter: Filters_T1.id 91 | }) 92 | .then( (res) => { 93 | var result = res; 94 | 95 | result.data = res.data.map( (item) => item.id ); 96 | 97 | return res; 98 | }) 99 | .catch( (err) => { 100 | if (err.name === 'Nothing Found') 101 | return this.requestError(CodeTypes.T1_NOTHING_FOUND); 102 | else 103 | return this.requestError(CodeTypes.UNKOWN_ERROR); 104 | }); 105 | } 106 | }, 107 | 108 | getByFirst: { 109 | params: { 110 | first: "string" 111 | }, 112 | handler(ctx) { 113 | return this.DB_Table1.findOne(ctx, { 114 | query: { 115 | first: ctx.params.first 116 | }, 117 | //filter: Filters_T1.full 118 | }) 119 | //.then( (res) => res ) 120 | .catch( (err) => { 121 | if (err.name === 'Nothing Found') 122 | return this.requestError(CodeTypes.T1_NOTHING_FOUND); 123 | else 124 | return this.requestError(CodeTypes.UNKOWN_ERROR); 125 | }); 126 | } 127 | }, 128 | 129 | getById: { 130 | params: { 131 | id: "string" 132 | }, 133 | handler(ctx) { 134 | return this.DB_Table1.findById(ctx, { 135 | id: ctx.params.id, 136 | //filter: Filters_T1.full 137 | }) 138 | //.then( (res) => res ) 139 | .catch( (err) => { 140 | if (err.name === 'Nothing Found') 141 | return this.requestError(CodeTypes.T1_NOTHING_FOUND); 142 | else 143 | return this.requestError(CodeTypes.UNKOWN_ERROR); 144 | }); 145 | } 146 | }, 147 | 148 | count: { 149 | params: { 150 | 151 | }, 152 | handler(ctx) { 153 | return this.DB_Table1.count(ctx, { }) 154 | //.then( (res) => res ) 155 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 156 | } 157 | }, 158 | 159 | update: { 160 | params: { 161 | id: "string", 162 | first: "string", 163 | second: "string", 164 | third: "number" 165 | }, 166 | handler(ctx) { 167 | return this.DB_Table1.updateById(ctx, ctx.params.id, { 168 | first: ctx.params.first, 169 | second: ctx.params.second, 170 | third: ctx.params.third 171 | }) 172 | //.then( (res) => res ) 173 | .catch( (err) => { 174 | if (err.name === 'Database Error' && Array.isArray(err.data)){ 175 | if (err.data[0].type === 'unique' && err.data[0].field === 'first') 176 | return this.requestError(CodeTypes.T1_FIRST_CONSTRAINT); 177 | } 178 | else if (err.name === 'Nothing Found') 179 | return this.requestError(CodeTypes.T1_NOTHING_FOUND); 180 | else 181 | return this.requestError(CodeTypes.UNKOWN_ERROR); 182 | }); 183 | } 184 | }, 185 | 186 | updateMany: { 187 | params: { 188 | minimum: "number", 189 | second: "string", 190 | third: "number" 191 | }, 192 | handler(ctx) { 193 | return this.DB_Table1.updateMany(ctx, { 194 | third: { $gt: ctx.params.minimum } // $gt -> all entities with 'third' field greater than 195 | }, { // Cannot put 'first field' -> unique constraint 196 | second: ctx.params.second, 197 | third: ctx.params.third 198 | }) 199 | //.then( (res) => res ) 200 | .catch( (err) => { 201 | if (err.name === 'Database Error' && Array.isArray(err.data)){ 202 | if (err.data[0].type === 'unique' && err.data[0].field === 'first') 203 | return this.requestError(CodeTypes.T1_FIRST_CONSTRAINT); 204 | } 205 | else if (err.name === 'Nothing Found') 206 | return this.requestError(CodeTypes.T1_NOTHING_FOUND); 207 | else 208 | return this.requestError(CodeTypes.UNKOWN_ERROR); 209 | }); 210 | } 211 | }, 212 | 213 | remove: { 214 | params: { 215 | id: "string" 216 | }, 217 | handler(ctx) { 218 | return this.DB_Table1.removeById(ctx, ctx.params.id) 219 | //.then( (res) => res ) 220 | .catch( (err) => { 221 | if (err.name === 'Nothing Found') 222 | return this.requestError(CodeTypes.T1_NOTHING_FOUND); 223 | else 224 | return this.requestError(CodeTypes.UNKOWN_ERROR); 225 | }); 226 | } 227 | }, 228 | 229 | removeByFirst: { 230 | params: { 231 | first: "string" 232 | }, 233 | handler(ctx) { 234 | return this.DB_Table1.removeMany(ctx, { 235 | first: ctx.params.first 236 | }) 237 | //.then( (res) => res ) 238 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 239 | } 240 | }, 241 | 242 | removeByThird: { 243 | params: { 244 | third: "string" 245 | }, 246 | handler(ctx) { 247 | var third = parseInt(ctx.params.third); 248 | 249 | if (!isNaN(third)) 250 | return this.DB_Table1.removeMany(ctx, { 251 | third: { $lt: ctx.params.third } // $lt -> all entities with 'third' field less than 252 | }) 253 | //.then( (res) => res ) 254 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 255 | else 256 | return this.requestError(CodeTypes.T1_THIRD_CONSTRAINT); 257 | } 258 | }, 259 | 260 | removeAll: { 261 | params: { 262 | 263 | }, 264 | handler(ctx) { 265 | return this.DB_Table1.removeAll(ctx) 266 | //.then( (res) => res ) 267 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 268 | } 269 | } 270 | 271 | }, 272 | 273 | methods: { 274 | 275 | }, 276 | 277 | created() { 278 | this.DB_Table1 = new Database("Table1", Filters_T1.full); 279 | } 280 | }; 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Moleculer logo](http://moleculer.services/images/banner.png) 2 | 3 | # moleculer-mysql-template 4 | 5 | Moleculer template for creating a secure web api, with a remote MySQL database, and a default account management. 6 | 7 | **This template is based on [moleculer](https://github.com/moleculerjs/moleculer), using:** 8 | - [moleculer-web](https://github.com/moleculerjs/moleculer-web) 9 | - [moleculer-db](https://github.com/moleculerjs/moleculer-db) 10 | - [sequelize](https://github.com/sequelize/sequelize) 11 | - [mysql2](https://github.com/sidorares/node-mysql2) 12 | - [password-hash](https://github.com/davidwood/node-password-hash) 13 | - [JSON Web Token](https://github.com/auth0/node-jsonwebtoken) (JWT) 14 | 15 | # Description 16 | 17 | **This adapter overwrites the one from moleculer-db:** 18 | - So less functionalities 19 | - But some are added (like multi-table management per service). 20 | 21 | For now the actions are very limited, but when understanding the adapter, you can change it to add your own features. 22 | 23 | **It is more an example of usage than a template, but you can :** 24 | - Change as you want the tables' model 25 | - Create your own services (just be sure to keep the configuration described in [Usage](https://github.com/AGenson/moleculer-mysql-template/wiki/Usage)) 26 | - Change API routes to your own purpose (*cf - [moleculer-web](https://github.com/moleculerjs/moleculer-web)* for more details) 27 | 28 | **New** 29 | - Securing the API with an authentification process (password / tokens) 30 | - Create, manage or delete user accounts 31 | - ADMIN priviledge management 32 | 33 | --- 34 | 35 | # Features 36 | - Remote MySQL connection 37 | - Simple CRUD actions 38 | - Fields filtering 39 | - Multi-table management (one service can do operations on several tables of the database) 40 | - Formatting answers from requests ( Responses / Errors ) 41 | 42 | **New Features** 43 | - Authentification of http request 44 | - Default user account management 45 | - Securing of accounts with hashed password and tokens management 46 | 47 | --- 48 | 49 | # Install 50 | ``` bash 51 | # Clone repository 52 | git clone https://github.com/AGenson/moleculer-mysql-template 53 | 54 | # Install dependencies 55 | npm install 56 | ``` 57 | 58 | --- 59 | --- 60 | # The following is to resume functionalities 61 | ## [-> See the detailed version here](https://github.com/AGenson/moleculer-mysql-template/wiki) 62 | --- 63 | --- 64 | 65 | # Usage 66 | 67 | ## Configure database informations: 68 | All the following configuration will be in this folder : **./src/fixtures/database_template/** 69 | ### Database Connection: 70 | database.config.js 71 | ```js 72 | module.exports = { 73 | host: "mysql.example.host", 74 | port: "3306", // Default for mysql => 3306 75 | database: "db_example", 76 | username: "db_user", 77 | password: "db_password" 78 | } 79 | ``` 80 | 81 | ### Database Schema Examples: models/ 82 | index.js 83 | ```js 84 | const Table1Model = require("./Table1Model"); 85 | const Table2Model = require("./Table2Model"); 86 | 87 | 88 | module.exports = { 89 | Table1: Table1Model, 90 | Table2: Table2Model 91 | }; 92 | ``` 93 | 94 | Table1Model.js 95 | ```js 96 | module.exports = { 97 | name: "table1_elt", 98 | define: { 99 | id: { // id must always exist 100 | type: Sequelize.UUID, // Uses uuidv4 by default (default value is recommended) 101 | primaryKey: true, 102 | defaultValue: Sequelize.UUIDV4 103 | }, 104 | 105 | first: { 106 | type: Sequelize.STRING(255), 107 | allowNull: false, 108 | defaultValue: "Default" 109 | }, 110 | 111 | ... 112 | }, 113 | options: { 114 | timestamps: false 115 | } 116 | }; 117 | ``` 118 | 119 | ## Give service access to database 120 | - Change Filters to your need 121 | - And add the tables you want for your service 122 | ```js 123 | "use strict"; 124 | const Database = require("../adapters/Database"); 125 | 126 | // Filters applied when searching for entities 127 | // Elements correspond to the columns of the table 128 | const Filters_T1 = { 129 | full: ["id", "first", "second", "third"] 130 | }; 131 | const Filters_T2 = { 132 | full: ["id", "first", "second"] 133 | }; 134 | 135 | 136 | module.exports = { 137 | name: "service", 138 | 139 | actions: { ... }, 140 | 141 | created() { 142 | this.DB_Table1 = new Database("Table1", Filters_T1.full); 143 | this.DB_Table2 = new Database("Table2"); // Default: Filters_T2.full 144 | } 145 | 146 | ``` 147 | 148 | ## Call action on wanted table 149 | ```js 150 | getAll: { 151 | params: { }, 152 | handler(ctx) { 153 | return this.DB_Table1.find(ctx) 154 | } 155 | } 156 | ``` 157 | 158 | --- 159 | 160 | # Database Management Functions 161 | Functions are all detailed [HERE](https://github.com/AGenson/moleculer-mysql-template/wiki/Functions) 162 | ## Constructor 163 | | Property | Type | Default | Description | 164 | | :------: | :--------------: | :----------: | ------------------------------------------------ | 165 | | `table` | `String` | **required** | Name of the wanted table (defined in ./src/fixtures/database_template/models/index.js) | 166 | | `filter` | `Array.` | all columns | Default filter for search (columns of the table) | 167 | 168 | ## Operations 169 | All operations on a table 170 | * [***find***](https://github.com/AGenson/moleculer-mysql-template/wiki/find) : Find all entities by query, and filter the fileds of results 171 | * [***findOne***](https://github.com/AGenson/moleculer-mysql-template/wiki/findOne) : Find only one entity by query, and filter the fileds of the result 172 | * [***findById***](https://github.com/AGenson/moleculer-mysql-template/wiki/findById) : Find the entity with the given id, and filter the fileds of the result 173 | * [***count***](https://github.com/AGenson/moleculer-mysql-template/wiki/count) : Count the entities found corresponding to the given querry 174 | * [***insert***](https://github.com/AGenson/moleculer-mysql-template/wiki/insert) : Insert a new entity into the table of the database 175 | * [***insertMany***](https://github.com/AGenson/moleculer-mysql-template/wiki/insertMany) : Insert several entities into the table of the database 176 | * [***updateById***](https://github.com/AGenson/moleculer-mysql-template/wiki/updateById) : Update the entity with the given id 177 | * [***updateMany***](https://github.com/AGenson/moleculer-mysql-template/wiki/updateMany) : Update all entity corresponding to the given query 178 | * [***removeById***](https://github.com/AGenson/moleculer-mysql-template/wiki/removeById) : Remove the entity with the given id 179 | * [***removeMany***](https://github.com/AGenson/moleculer-mysql-template/wiki/removeMany) : Remove several entities with the given query 180 | * [***removeAll***](https://github.com/AGenson/moleculer-mysql-template/wiki/removeAll) : Remove all entities from the table 181 | 182 | --- 183 | 184 | # Database Errors 185 | 186 | ## Errors handling 187 | Each operation functions return the wanted information, with a **specific format** (name, message, data). 188 | 189 | But they may encounters errors. And the error format is the same as for normal answers: **name, message(, data)** 190 | 191 | Here's a little description of how they are handled. 192 | 193 | See details [HERE](https://github.com/AGenson/moleculer-mysql-template/wiki/Errors-handling) 194 | 195 | ## Errors handling (from service) 196 | The adapter will manage the format of the response, as described in functions or errors handling. 197 | 198 | But you do not especially want your client to see all those formatted responses. So here is an implementation of what could be a modulable solution. 199 | 200 | See details [HERE](https://github.com/AGenson/moleculer-mysql-template/wiki/Errors-handling-(from-service)) 201 | 202 | --- 203 | 204 | # User account & Priviledge Management 205 | Detailed description of authentification processes and user account functions. 206 | 207 | * [API Gateway](https://github.com/AGenson/moleculer-mysql-template/wiki/API-Gateway) 208 | * [Auth Service](https://github.com/AGenson/moleculer-mysql-template/wiki/Auth-Service) 209 | * [User Service](https://github.com/AGenson/moleculer-mysql-template/wiki/User-Service) 210 | -------------------------------------------------------------------------------- /src/services/users.service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const passwordHash = require('password-hash'); 4 | const { MoleculerError } = require("moleculer").Errors; 5 | const Database = require("../adapters/Database"); 6 | const Request = require("../mixins/request.mixin"); 7 | const CodeTypes = require("../fixtures/error.codes"); 8 | 9 | // Filters applied when searching for entities 10 | // Elements correspond to the columns of the table 11 | const Filters_Users = { 12 | full: ["id", "username", "password", "role"], 13 | role: ["id", "role"], 14 | restricted: ["username"], 15 | unrestricted: ["username", "age"] 16 | }; 17 | const Filters_Tokens = { 18 | empty: ["id"] 19 | }; 20 | 21 | const Roles = ["ADMIN", "USER"]; 22 | 23 | 24 | 25 | module.exports = { 26 | name: "users", 27 | 28 | mixins: [ Request ], 29 | 30 | actions: { 31 | 32 | create: { 33 | params: { 34 | username: "string", 35 | password: "string" 36 | }, 37 | handler(ctx) { 38 | return this.generateHash(ctx.params.password) 39 | .then( (res) => this.DB_Users.insert(ctx, { 40 | username: ctx.params.username, 41 | password: res.data 42 | })) 43 | .then( () => this.requestSuccess("User Account Created", ctx.params.username) ) 44 | .catch( (err) => { 45 | if (err.name === 'Database Error' && Array.isArray(err.data)){ 46 | if (err.data[0].type === 'unique' && err.data[0].field === 'username') 47 | return this.requestError(CodeTypes.USERS_USERNAME_CONSTRAINT); 48 | } 49 | 50 | return this.requestError(CodeTypes.UNKOWN_ERROR); 51 | }); 52 | } 53 | }, 54 | 55 | getAll: { 56 | params: { 57 | 58 | }, 59 | handler(ctx) { 60 | return this.verifyIfLogged(ctx) 61 | .then( () => this.DB_Users.find(ctx, { }) ) 62 | .then( (res) => this.requestSuccess("Search Complete", res.data) ) 63 | .catch( (err) => { 64 | if (err.name === 'Nothing Found') 65 | return this.requestError(CodeTypes.USERS_NOTHING_FOUND); 66 | else 67 | return this.requestError(CodeTypes.UNKOWN_ERROR); 68 | }); 69 | } 70 | }, 71 | 72 | get: { 73 | params: { 74 | username: "string" 75 | }, 76 | handler(ctx) { 77 | return this.verifyIfLogged(ctx) 78 | .then( () => this.DB_Users.findOne(ctx, { 79 | query: { 80 | username: ctx.params.username 81 | }, 82 | filter: (ctx.params.username === ctx.meta.user.username) ? Filters_Users.unrestricted : Filters_Users.restricted 83 | })) 84 | .then( (res) => this.requestSuccess("Search Complete", res.data) ) 85 | .catch( (err) => { 86 | if (err.name === 'Nothing Found') 87 | return this.requestError(CodeTypes.USERS_NOTHING_FOUND); 88 | else 89 | return this.requestError(CodeTypes.UNKOWN_ERROR); 90 | }); 91 | } 92 | }, 93 | 94 | count: { 95 | params: { 96 | 97 | }, 98 | handler(ctx) { 99 | return this.DB_Users.count(ctx, { }) 100 | .then( (res) => this.requestSuccess("Count Complete", res.data) ) 101 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 102 | } 103 | }, 104 | 105 | changeInfo: { 106 | params: { 107 | age: "number" 108 | }, 109 | handler(ctx) { 110 | return this.verifyIfLogged(ctx) 111 | .then( () => this.DB_Users.updateById(ctx, ctx.meta.user.id, { 112 | age: ctx.params.age 113 | })) 114 | .then( (res) => this.requestSuccess("Changes Saved", true) ) 115 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 116 | } 117 | }, 118 | 119 | changePassword: { 120 | params: { 121 | oldPassword: "string", 122 | newPassword: "string" 123 | }, 124 | handler(ctx) { 125 | return this.verifyIfLogged(ctx) 126 | .then( () => ctx.call("auth.verifyPassword", { username: ctx.meta.user.username, password: ctx.params.oldPassword})) 127 | .then( () => this.generateHash(ctx.params.newPassword) ) 128 | .then( (res) => this.DB_Users.updateById(ctx, ctx.meta.user.id, { 129 | password: res.data 130 | })) 131 | .then( () => ctx.call("auth.closeAllSessions")) 132 | .then( () => this.requestSuccess("Changes Saved", true) ) 133 | .catch( (err) => { 134 | if (err instanceof MoleculerError) 135 | return Promise.reject(err); 136 | else 137 | return this.requestError(CodeTypes.UNKOWN_ERROR); 138 | }); 139 | } 140 | }, 141 | 142 | changeRole: { 143 | params: { 144 | username: "string", 145 | role: "string" 146 | }, 147 | handler(ctx) { 148 | return this.verifyIfAdmin(ctx) 149 | .then( () => this.verifyRole(ctx.params.role) ) 150 | .then( () => { 151 | if ((ctx.meta.user.username === ctx.params.username) && (ctx.params.role !== "ADMIN")) 152 | return this.isLastAdmin(ctx) 153 | .then( (res) => { 154 | if (res.data === false) 155 | return Promise.resolve(true); 156 | else 157 | return this.requestError(CodeTypes.USERS_FORBIDDEN_REMOVE); 158 | }); 159 | else 160 | return Promise.resolve(true); 161 | }) 162 | .then( () => this.DB_Users.findOne(ctx, { 163 | query: { 164 | username: ctx.params.username 165 | }, 166 | filter: Filters_Users.role 167 | })) 168 | .then( (res) => this.DB_Tokens.removeMany(ctx, { 169 | userId: res.data.id 170 | })) 171 | .then( () => this.DB_Users.updateMany(ctx, { 172 | username: ctx.params.username 173 | }, { 174 | role: ctx.params.role 175 | })) 176 | .then( () => this.requestSuccess("Changes Saved", true) ) 177 | .catch( (err) => { 178 | if (err instanceof MoleculerError) 179 | return Promise.reject(err); 180 | else 181 | return this.requestError(CodeTypes.UNKOWN_ERROR); 182 | }); 183 | } 184 | }, 185 | 186 | remove: { 187 | params: { 188 | password: "string" 189 | }, 190 | handler(ctx) { 191 | return this.verifyIfLogged(ctx) 192 | .then( () => this.isLastAdmin(ctx) ) 193 | .then( (res) => { 194 | if (res.data === false) 195 | return Promise.resolve(true); 196 | else 197 | return this.requestError(CodeTypes.USERS_FORBIDDEN_REMOVE); 198 | }) 199 | .then( () => ctx.call("auth.verifyPassword", { username: ctx.meta.user.username, password: ctx.params.password})) 200 | .then( () => ctx.call("auth.closeAllSessions") ) 201 | .then( () => this.DB_Users.removeById(ctx, ctx.meta.user.id)) 202 | .then( () => this.requestSuccess("Delete Complete", true) ) 203 | .catch( (err) => { 204 | if (err instanceof MoleculerError) 205 | return Promise.reject(err); 206 | else 207 | return this.requestError(CodeTypes.UNKOWN_ERROR); 208 | }); 209 | } 210 | }, 211 | 212 | banish: { 213 | params: { 214 | username: "string" 215 | }, 216 | handler(ctx) { 217 | return this.verifyIfAdmin(ctx) 218 | .then( () => this.DB_Users.findOne(ctx, { 219 | query: { 220 | username: ctx.params.username 221 | }, 222 | filter: Filters_Users.role 223 | })) 224 | .then( (res) => { 225 | if (res.data.role !== "ADMIN") 226 | return this.DB_Tokens.removeMany(ctx, { 227 | userId: res.data.id 228 | }) 229 | .then( () => this.DB_Users.removeMany(ctx, { 230 | username: ctx.params.username 231 | })); 232 | else 233 | return this.requestError(CodeTypes.USERS_FORBIDDEN_REMOVE); 234 | }) 235 | .then( () => this.requestSuccess("Delete Complete", true) ) 236 | .catch( (err) => { 237 | if (err instanceof MoleculerError) 238 | return Promise.reject(err); 239 | else if (err.name === 'Nothing Found') 240 | return this.requestError(CodeTypes.USERS_NOTHING_FOUND); 241 | else 242 | return this.requestError(CodeTypes.UNKOWN_ERROR); 243 | }); 244 | } 245 | }, 246 | 247 | removeAll: { 248 | params: { 249 | password: "string" 250 | }, 251 | handler(ctx) { 252 | return this.verifyIfAdmin(ctx) 253 | .then( () => ctx.call("auth.verifyPassword", { username: ctx.meta.user.username, password: ctx.params.password})) 254 | .then( () => this.DB_Tokens.removeAll(ctx) ) 255 | .then( () => this.DB_Users.removeAll(ctx) ) 256 | .then( () => ctx.call("users.createAdminIfNotExists")) 257 | .then( () => this.requestSuccess("Delete Complete", true) ) 258 | .catch( (err) => { 259 | if (err instanceof MoleculerError) 260 | return Promise.reject(err); 261 | else 262 | return this.requestError(CodeTypes.UNKOWN_ERROR); 263 | }); 264 | } 265 | }, 266 | 267 | createAdminIfNotExists: { 268 | params: { 269 | 270 | }, 271 | handler(ctx) { 272 | return this.DB_Users.count(ctx, { 273 | role: "ADMIN" 274 | }) 275 | .then( (res) => { 276 | if (res.data === 0) 277 | return this.generateHash("admin") 278 | .then( (res) => this.DB_Users.insert(ctx, { 279 | username: "admin", 280 | password: res.data, 281 | role: "ADMIN" 282 | })); 283 | else 284 | return Promise.resolve(true); 285 | }) 286 | .then( () => this.requestSuccess("Admin Exists", true) ) 287 | .catch( (err) => this.requestError(CodeTypes.UNKOWN_ERROR) ); 288 | } 289 | } 290 | 291 | }, 292 | 293 | methods: { 294 | 295 | generateHash(value){ 296 | return Promise.resolve(passwordHash.generate(value, {algorithm: 'sha256'})) 297 | .then( (res) => this.requestSuccess("Password Encrypted", res) ); 298 | }, 299 | 300 | verifyIfLogged(ctx){ 301 | if (ctx.meta.user !== undefined) 302 | return this.requestSuccess("User Logged", true); 303 | else 304 | return this.requestError(CodeTypes.USERS_NOT_LOGGED_ERROR); 305 | }, 306 | 307 | verifyIfAdmin(ctx){ 308 | return this.verifyIfLogged(ctx) 309 | .then( () => { 310 | if (ctx.meta.user.role === "ADMIN") 311 | return this.requestSuccess("User is ADMIN", true); 312 | else 313 | return this.requestError(CodeTypes.AUTH_ADMIN_RESTRICTION); 314 | }); 315 | }, 316 | 317 | verifyRole(role){ 318 | if (Roles.indexOf(role) !== -1) 319 | return this.requestSuccess("Role Exists", true); 320 | else 321 | return this.requestError(CodeTypes.USERS_INVALID_ROLE); 322 | }, 323 | 324 | isLastAdmin(ctx){ 325 | return this.verifyIfAdmin(ctx) 326 | .then( () => this.DB_Users.count(ctx, { 327 | role: "ADMIN" 328 | })) 329 | .then( (res) => { 330 | if (res.data === 1) 331 | return this.requestSuccess("Last Admin", true); 332 | else 333 | return this.requestSuccess("Last Admin", false); 334 | }) 335 | .catch( (err) => { 336 | if (err.message === CodeTypes.AUTH_ADMIN_RESTRICTION) 337 | return this.requestSuccess("Last Admin", false); 338 | else 339 | return Promise.reject(err); 340 | }); 341 | } 342 | 343 | }, 344 | 345 | created() { 346 | this.DB_Users = new Database("User", Filters_Users.restricted); 347 | this.DB_Tokens = new Database("Token", Filters_Tokens.empty); 348 | } 349 | }; 350 | -------------------------------------------------------------------------------- /src/adapters/Database.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { pick } = require("lodash"); 4 | const Promise = require("bluebird"); 5 | const Models = require("../fixtures/database_template/models/index"); 6 | 7 | 8 | 9 | class Database { 10 | 11 | /** 12 | * Create an instance of Database for a given table 13 | * 14 | * @param {String} table - name of the table (defined in ../fixtures/database_template/models/index.js) 15 | * @param {String[]} filter - default filter for search (columns of the table) 16 | */ 17 | constructor(table, filter) { 18 | if (!table) 19 | throw new Error("Missing table name !"); 20 | else if (!Models[table]) 21 | throw new Error("Invalid table name !"); 22 | 23 | this.table = `DB_${table}s`; 24 | this.fields = []; 25 | 26 | for (var field in Models[table].define) { 27 | this.fields.push(field); 28 | } 29 | 30 | if (Array.isArray(filter)){ 31 | filter.forEach( (item) => { 32 | if (!this.fields.includes(item)) 33 | throw new Error(`Invalid filter field ${item}. The table ${table} do not contain this column.`); 34 | }) 35 | 36 | if (filter.length === 0) 37 | throw new Error(`Invalid filter. No fields found.`); 38 | } else if (filter){ 39 | throw new Error(`Invalid filter. The filter needs to be an array of Strings.`); 40 | } 41 | 42 | this.default_filter = filter || this.fields; 43 | } 44 | 45 | /** 46 | * Verify the existance of all the parameters given 47 | * 48 | * @param {Object} obj 49 | * 50 | * @returns {Promise} 51 | */ 52 | parameterValidator(obj){ 53 | for (var item in obj) { 54 | if (!obj[item]) 55 | return Promise.reject({ 56 | name: "Missing parameter", 57 | message: `Missing parameter ${item}` 58 | }); 59 | 60 | switch(item){ 61 | case "ctx": 62 | if (typeof obj[item].call !== "function") 63 | return Promise.reject({ 64 | name: "Invalid parameter", 65 | message: `Invalid parameter ${item}.` 66 | }); 67 | break; 68 | 69 | case "query": 70 | case "entity": 71 | case "update": 72 | if (typeof obj[item] !== "object") 73 | return Promise.reject({ 74 | name: "Invalid parameter", 75 | message: `Invalid parameter ${item}, it needs to be an Object, and its fields must exist in the table (columns)` 76 | }); 77 | 78 | var test = this.verifyFields(obj[item], false); 79 | 80 | if (test !== true) 81 | return Promise.reject(test); 82 | break; 83 | 84 | case "entities": 85 | if (!Array.isArray(obj[item])) 86 | return Promise.reject({ 87 | name: "Invalid parameter", 88 | message: `Invalid parameter ${item}.` 89 | }); 90 | 91 | var test = true; 92 | 93 | obj[item].forEach( (entity) => { 94 | if (typeof entity !== "object") 95 | return Promise.reject({ 96 | name: "Invalid parameter", 97 | message: `Invalid parameter ${item}, it needs to be an Object, and its fields must exist in the table (columns)` 98 | }); 99 | 100 | if (test === true) 101 | test = this.verifyFields(entity, false); 102 | }); 103 | 104 | if (test !== true) 105 | return Promise.reject(test); 106 | break; 107 | } 108 | } 109 | 110 | return Promise.resolve(true); 111 | } 112 | 113 | /** 114 | * Verify that each field of the object correspond to a column of the table 115 | * 116 | * @param {(Object|String[])} fields 117 | * @param {Boolean} [isPromise=true] - (Optional) 118 | * 119 | * @returns {Promise} 120 | */ 121 | verifyFields(fields, isPromise){ 122 | var resultType = (isPromise === false) ? isPromise : true; 123 | var result = { 124 | error: false, 125 | type: '', 126 | field: '', 127 | value: '' 128 | }; 129 | 130 | if (Array.isArray(fields)){ 131 | fields.forEach( (item, i) => { 132 | if (!this.fields.includes(item)) 133 | result = { 134 | error: true, 135 | type: 'filter', 136 | field: `n°${i}`, 137 | value: `: ${item}` 138 | }; 139 | }) 140 | } else if (typeof fields === "object") { 141 | for (var item in fields) { 142 | if (!this.fields.includes(item)) 143 | result = { 144 | error: true, 145 | type: 'query', 146 | field: `${item}`, 147 | value: `` 148 | }; 149 | } 150 | } 151 | else if (resultType) 152 | return Promise.reject({ 153 | name: "Invalid data type", 154 | message: `Data type must be (Object|String[])` 155 | }); 156 | else 157 | return { 158 | name: "Invalid data type", 159 | message: `Data type must be (Object|String[])` 160 | }; 161 | 162 | if (resultType){ 163 | if (result.error) 164 | return Promise.reject({ 165 | name: "Invalid field", 166 | message: `Invalid field ${result.field} for ${result.type}${result.value}` 167 | }); 168 | else 169 | return Promise.resolve(true); 170 | } else 171 | if (result.error) 172 | return { 173 | name: "Invalid field", 174 | message: `Invalid field ${result.field} for ${result.type}${result.value}` 175 | }; 176 | else 177 | return true; 178 | } 179 | 180 | /** 181 | * Format an Error Message from Sequelize (database) Error 182 | * 183 | * @param {Object} err - SequelizeError 184 | * 185 | * @returns {Object} 186 | */ 187 | sequelizeErrorHandler(err){ 188 | var message = "Details:"; 189 | var errorTypes = { 190 | null: false, 191 | unique: false, 192 | unkown: false 193 | }; 194 | 195 | if (err.name === 'SequelizeDatabaseError') 196 | return { 197 | name: "Database Error", 198 | message: err.parent.sqlMessage 199 | }; 200 | else if (err.errors) { 201 | var errors = err.errors.map( (error) => { 202 | var type = "Unknown"; 203 | 204 | switch (error.type){ 205 | case 'notNull Violation': 206 | errorTypes.null = true; 207 | type = "notNull"; 208 | break; 209 | 210 | case 'unique violation': 211 | errorTypes.unique = true; 212 | type = "unique"; 213 | break; 214 | 215 | default: 216 | errorTypes.unkown = true; 217 | type = error.type; 218 | break; 219 | } 220 | 221 | return { 222 | field: error.path, 223 | type: type 224 | } 225 | }); 226 | 227 | if (errorTypes.null === true) 228 | message += " NOT NULL contraint not respected;"; 229 | if (errorTypes.unique === true) 230 | message += " UNIQUE contraint not respected;"; 231 | if (errorTypes.unkown === true) 232 | message += " Unknown error;"; 233 | 234 | return { 235 | name: "Database Error", 236 | message: message, 237 | data: errors 238 | }; 239 | } 240 | else { 241 | return { 242 | name: "Database Error", 243 | message: "Unknown Error" 244 | }; 245 | } 246 | } 247 | 248 | /** 249 | * Find all entities by query, and filter the fields of results 250 | * 251 | * Search Fields: 252 | * - query: {type: "Object", optional: true} --> ex: { username: "username", age: { $lt: 5 } } 253 | * - filter: {type: "Array", optional: true, item: "String"} --> ex: ["id", "username"] 254 | * - limit: {type: "Number", optional: true} --> ex: 10 255 | * 256 | * @param {Object} ctx - Will serve to call a service action: ctx.call 257 | * @param {Object} search 258 | * 259 | * @returns {Promise} 260 | */ 261 | find(ctx, search) { 262 | var query = (search) ? search.query || { } : { }; 263 | var filter = (search) ? search.filter || this.default_filter : this.default_filter; 264 | var limit = (search) ? ((typeof search.limit === "number") ? search.limit : undefined) : undefined; 265 | 266 | return this.parameterValidator({ctx: ctx}) 267 | .then( () => { 268 | if (typeof query === "object" && Array.isArray(filter)) 269 | return Promise.resolve(true); 270 | else 271 | return Promise.reject({ 272 | name: "Invalid 'search' parameter", 273 | message: "Details: 'search.query' needs to be an Object, and 'search.filter' an Array" 274 | }); 275 | }) 276 | .then( () => this.verifyFields(query)) 277 | .then( () => this.verifyFields(filter)) 278 | .then( () => ctx.call(`${this.table}.find`, { 279 | query: query, 280 | limit: limit 281 | })) 282 | .then( (res) => res.map( (item) => pick(item, filter) ) ) 283 | .then( (res) => { 284 | if (res.length !== 0) 285 | return Promise.resolve({ 286 | name: "Operation Successful", 287 | message: `Search Complete: ${res.length} element(s) found`, 288 | data: res 289 | }); 290 | else 291 | return Promise.reject({ 292 | name: "Nothing Found", 293 | message: `Search Complete: 0 element found` 294 | }); 295 | }) 296 | .catch( (err) => { 297 | if (err.name && err.message && !err.type && !err.code && !err.ctx) 298 | return Promise.reject(err); 299 | else 300 | return Promise.reject({ 301 | name: "Unkown Error", 302 | message: "Internal Error: something went wrong while searching for entities" 303 | }); 304 | }); 305 | } 306 | 307 | /** 308 | * Find only one entity by query, and filter the fields of the result 309 | * 310 | * Obj Fields: 311 | * - query: {type: "Object", optional: true} --> ex: { username: "username", age: { $lt: 5 } } 312 | * - filter: {type: "Array", optional: true, item: "String"} --> ex: ["id", "username"] 313 | * 314 | * @param {Object} ctx - Will serve to call a service action: ctx.call 315 | * @param {Object} search 316 | * 317 | * @returns {Promise} 318 | */ 319 | findOne(ctx, search) { 320 | var new_search = (search) ? search : { }; 321 | 322 | new_search.limit = 1; 323 | 324 | return this.find(ctx, search) 325 | .then( (res) => { 326 | var new_res = res; 327 | 328 | new_res.data = res.data[0]; 329 | new_res.message = `Search Complete: element found`; 330 | 331 | return new_res; 332 | }); 333 | } 334 | 335 | /** 336 | * Find the entity with the given id, and filter the fields of the result 337 | * 338 | * Search Fields: 339 | * - id: {type: "any", optional: false} --> ex: { id: "G-123456" } 340 | * - filter: {type: "Array", optional: true, item: "String"} --> ex: ["id", "username"] 341 | * 342 | * @param {Object} ctx - Will serve to call a service action: ctx.call 343 | * @param {Object} search 344 | * 345 | * @returns {Promise} 346 | */ 347 | findById(ctx, search) { 348 | return this.parameterValidator({search: search}) 349 | .then( () => this.parameterValidator({"search.id": search.id})) 350 | .then(() => this.findOne(ctx, { 351 | query: { id : search.id }, 352 | filter: search.filter 353 | })); 354 | } 355 | 356 | /** 357 | * Count the entities found corresponding to the given query 358 | * 359 | * @param {Object} ctx - Will serve to call a service action: ctx.call 360 | * @param {Object} query - Clause WHERE --> ex: { username: "username", age: { $lt: 5 } } 361 | * 362 | * @returns {Promise} 363 | */ 364 | count(ctx, query) { 365 | return this.parameterValidator({ctx: ctx, query: query}) 366 | .then( () => ctx.call(`${this.table}.count`, { 367 | query: query 368 | })) 369 | .then( (res) => Promise.resolve({ 370 | name: "Operation Successful", 371 | message: `Count Complete: ${res} element(s) found`, 372 | data: res 373 | })) 374 | .catch( (err) => { 375 | if (err.name && err.message && !err.type && !err.code && !err.ctx) 376 | return Promise.reject(err); 377 | else 378 | return Promise.reject({ 379 | name: "Unkown Error", 380 | message: "Internal Error: something went wrong while searching for entities" 381 | }); 382 | }); 383 | } 384 | 385 | /** 386 | * Insert a new entity into the table of the database 387 | * 388 | * @param {Object} ctx - Will serve to call a service action: ctx.call 389 | * @param {Object} entity 390 | * 391 | * @returns {Promise} 392 | */ 393 | insert(ctx, entity) { 394 | return this.parameterValidator({ctx: ctx, entity: entity}) 395 | .then( () => ctx.call(`${this.table}.insert`, { 396 | entity: entity 397 | }) 398 | ) 399 | .then( (res) => Promise.resolve({ 400 | name: "Operation Successful", 401 | message: `Insert Done: element (id: ${res.id}) inserted in table`, 402 | data: res.id 403 | })) 404 | .catch( (err) => { 405 | if (err.name.indexOf("Sequelize") !== -1) 406 | return Promise.reject(this.sequelizeErrorHandler(err)); 407 | else if (err.name && err.message && !err.type && !err.code && !err.ctx) 408 | return Promise.reject(err); 409 | else 410 | return Promise.reject({ 411 | name: "Unkown Error", 412 | message: "Internal Error: something went wrong while searching for entities" 413 | }); 414 | }); 415 | } 416 | 417 | /** 418 | * Insert several entities into the table of the database 419 | * 420 | * @param {Object} ctx - Will serve to call a service action: ctx.call 421 | * @param {Object[]} entities - Array of entities 422 | * 423 | * @returns {Promise} 424 | */ 425 | insertMany(ctx, entities) { 426 | var entities_id = []; 427 | 428 | return this.parameterValidator({ctx: ctx, entities: entities}) 429 | .then( () => entities.map( (entity) => entity ) ) 430 | .then( (final_entities) => ctx.call(`${this.table}.insertMany`, { 431 | entities: final_entities 432 | })) 433 | .then( (res) => res.map( (item) => item.dataValues.id ) ) 434 | .then( (res) => { 435 | return Promise.resolve({ 436 | name: "Operation Successful", 437 | message: `Inserts Done: ${res.length} element(s) inserted in table`, 438 | data: res 439 | }); 440 | }) 441 | .catch( (err) => { 442 | if (err.name.indexOf("Sequelize") !== -1) 443 | return Promise.reject(this.sequelizeErrorHandler(err)); 444 | else if (err.name && err.message && !err.type && !err.code && !err.ctx) 445 | return Promise.reject(err); 446 | else 447 | return Promise.reject({ 448 | name: "Unkown Error", 449 | message: "Internal Error: something went wrong while searching for entities" 450 | }); 451 | }); 452 | } 453 | 454 | /** 455 | * Update the entity with the given id 456 | * 457 | * @param {Object} ctx - Will serve to call a service action: ctx.call 458 | * @param {String} id 459 | * @param {Object} update - Object with fields to update --> ex: { username: "user_2" } 460 | * 461 | * @returns {Promise} 462 | */ 463 | updateById(ctx, id, update) { 464 | return this.parameterValidator({ctx: ctx, id: id, update: update}) 465 | .then( () => this.findById(ctx, { 466 | id: id, 467 | filter: ["id"] 468 | })) 469 | .then( () => ctx.call(`${this.table}.updateById`, { 470 | id: id, 471 | update: update 472 | })) 473 | .then( (res) => Promise.resolve({ 474 | name: "Operation Successful", 475 | message: `Update Done: element (id: ${res.id}) updated in table`, 476 | data: res.id 477 | })) 478 | .catch( (err) => { 479 | if (err.name.indexOf("Sequelize") !== -1) 480 | return Promise.reject(this.sequelizeErrorHandler(err)); 481 | else if (err.name === 'Nothing Found') 482 | return Promise.reject({ 483 | name: 'Nothing Found', 484 | message: "Nothing was deleted" 485 | }); 486 | else if (err.name && err.message && !err.type && !err.code && !err.ctx) 487 | return Promise.reject(err); 488 | else 489 | return Promise.reject({ 490 | name: "Unkown Error", 491 | message: "Internal Error: something went wrong while updating the entities" 492 | }); 493 | }); 494 | } 495 | 496 | /** 497 | * Update all entity corresponding to the given query 498 | * 499 | * @param {Object} ctx - Will serve to call a service action: ctx.call 500 | * @param {Object} query - Clause WHERE --> ex: { username: "username", age: { $lt: 5 } } 501 | * @param {Object} update - Object with fields to update --> ex: { username: "user_2" } 502 | * 503 | * @returns {Promise} 504 | */ 505 | updateMany(ctx, query, update) { 506 | var entities_id = []; 507 | 508 | return this.parameterValidator({ctx: ctx, query: query, update: update}) 509 | .then( () => this.find(ctx, { 510 | query: query, 511 | filter: ["id"] 512 | })) 513 | .then( (res) => Promise.all( res.data.map( (item) => this.updateById(ctx, item.id, update) ) ) ) 514 | .then( (res) => res.map( (item) => item.data ) ) 515 | .then( (res) => Promise.resolve({ 516 | name: "Operation Successful", 517 | message: `Updates Done: ${res.length} element(s) updated`, 518 | data: res 519 | })) 520 | .catch( (err) => { 521 | if (err.name.indexOf("Sequelize") !== -1) 522 | return Promise.reject(this.sequelizeErrorHandler(err)); 523 | else if (err.name === 'Nothing Found') 524 | return Promise.reject({ 525 | name: 'Nothing Found', 526 | message: "Nothing was deleted" 527 | }); 528 | else if (err.name && err.message && !err.type && !err.code && !err.ctx) 529 | return Promise.reject(err); 530 | else 531 | return Promise.reject({ 532 | name: "Unkown Error", 533 | message: "Internal Error: something went wrong while updating the entities" 534 | }); 535 | }); 536 | } 537 | 538 | /** 539 | * Remove the entity with the given id 540 | * 541 | * @param {Object} ctx - Will serve to call a service action: ctx.call 542 | * @param {Object} id 543 | * 544 | * @returns {Promise} 545 | */ 546 | removeById(ctx, id) { 547 | return this.parameterValidator({ctx: ctx, id: id}) 548 | .then( () => this.findById(ctx, { 549 | id: id, 550 | filter: ["id"] 551 | })) 552 | .then( () => ctx.call(`${this.table}.removeById`, { 553 | id: id 554 | })) 555 | .then( () => Promise.resolve({ 556 | name: "Operation Successful", 557 | message: `Delete Complete: element (id: ${id}) deleted`, 558 | data: id 559 | })) 560 | .catch( (err) => { 561 | if (err.name === 'Nothing Found') 562 | return Promise.reject({ 563 | name: 'Nothing Found', 564 | message: "Wrong id: nothing was deleted" 565 | }); 566 | else if (err.name && err.message && !err.type && !err.code && !err.ctx) 567 | return Promise.reject(err); 568 | else 569 | return Promise.reject({ 570 | name: "Unkown Error", 571 | message: "Internal Error: something went wrong while deleting the entity" 572 | }); 573 | }); 574 | } 575 | 576 | /** 577 | * Remove several entities with the given query 578 | * 579 | * @param {Object} ctx - Will serve to call a service action: ctx.call 580 | * @param {Object} query - Clause WHERE --> ex: { username: "username", age: { $lt: 5 } } 581 | * 582 | * @returns {Promise} 583 | */ 584 | removeMany(ctx, query) { 585 | return this.parameterValidator({ctx: ctx, query: query}) 586 | .then( () => ctx.call(`${this.table}.removeMany`, { 587 | query: query 588 | })) 589 | .then( (res) => Promise.resolve({ 590 | name: "Operation Successful", 591 | message: `Delete Complete: ${res} element(s) deleted`, 592 | data: res 593 | })) 594 | .catch( (err) => { 595 | if (err.name && err.message && !err.type && !err.code && !err.ctx) 596 | return Promise.reject(err); 597 | else 598 | return Promise.reject({ 599 | name: "Unkown Error", 600 | message: "Internal Error: something went wrong while deleting the entities" 601 | }); 602 | }); 603 | } 604 | 605 | /** 606 | * Remove all entities from the table 607 | * 608 | * @param {Object} ctx - Will serve to call a service action: ctx.call 609 | * 610 | * @returns {Promise} 611 | */ 612 | removeAll(ctx) { 613 | return this.parameterValidator({ctx: ctx}) 614 | .then( () => ctx.call(`${this.table}.removeAll`) ) 615 | .then( () => Promise.resolve({ 616 | name: "Operation Successful", 617 | message: "Delete Complete" 618 | })) 619 | .catch( (err) => { 620 | if (err.name && err.message && !err.type && !err.code && !err.ctx) 621 | return Promise.reject(err); 622 | else 623 | return Promise.reject({ 624 | name: "Unkown Error", 625 | message: "Internal Error: something went wrong while emptying the table" 626 | }); 627 | }); 628 | } 629 | 630 | } 631 | 632 | 633 | 634 | module.exports = Database; 635 | --------------------------------------------------------------------------------