const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const UserError = require('vn-loopback/util/user-error'); const utils = require('loopback/lib/utils'); module.exports = function(Self) { Self.ParameterizedSQL = ParameterizedSQL; require('../methods/vn-model/getSetValues')(Self); require('../methods/vn-model/getEnumValues')(Self); require('../methods/vn-model/printService')(Self); Object.assign(Self, { setup() { Self.super_.setup.call(this); /** * Setting a global transaction timeout to find out if the service * is blocked because the connection pool is empty. */ this.once('dataSourceAttached', () => { let orgBeginTransaction = this.beginTransaction; this.beginTransaction = function(options, cb) { options = options || {}; if (options.timeout === undefined) options.timeout = 120 * 1000; return orgBeginTransaction.call(this, options, cb); }; }); // this.beforeRemote('**', async ctx => { // if (!this.hasFilter(ctx)) return; // const defaultLimit = this.app.orm.selectLimit; // const filter = ctx.args.filter || {limit: defaultLimit}; // if (filter.limit > defaultLimit) { // filter.limit = defaultLimit; // ctx.args.filter = filter; // } // }); // this.afterRemote('**', async ctx => { // if (!this.hasFilter(ctx)) return; // const {result} = ctx; // const length = Array.isArray(result) ? result.length : result ? 1 : 0; // if (length >= this.app.orm.selectLimit) throw new UserError('Too many records'); // }); // Register field ACL validation /* this.beforeRemote('prototype.patchAttributes', ctx => this.checkUpdateAcls(ctx)); this.beforeRemote('updateAll', ctx => this.checkUpdateAcls(ctx)); this.beforeRemote('patchOrCreate', ctx => this.checkInsertAcls(ctx)); this.beforeRemote('create', ctx => this.checkInsertAcls(ctx)); this.beforeRemote('replaceById', ctx => this.checkInsertAcls(ctx)); this.beforeRemote('replaceOrCreate', ctx => this.checkInsertAcls(ctx)); */ this.remoteMethod('crud', { description: `Create, update or/and delete instances from model with a single request`, accessType: 'WRITE', accepts: [ { arg: 'deletes', description: `Identifiers of instances to delete`, type: ['Integer'] }, { arg: 'updates', description: `Instances to update with it's identifier {where, data}`, type: ['Object'] }, { arg: 'creates', description: `Instances to create`, type: ['Object'] } ], returns: { type: ['object'], root: true } }); }, async crud(deletes, updates, creates) { let tx = await this.beginTransaction({}); try { let options = {transaction: tx}; if (deletes) { let promises = []; for (let id of deletes) promises.push(this.destroyById(id, options)); await Promise.all(promises); } if (updates) { let promises = []; for (let update of updates) promises.push(this.upsertWithWhere(update.where, update.data, options)); await Promise.all(promises); } let created; if (creates && creates.length) { try { created = await this.create(creates, options); } catch (error) { throw error[error.length - 1]; } } await tx.commit(); return created; } catch (error) { await tx.rollback(); throw error; } }, /** * Wrapper for remoteMethod() but adding the context as * extra argument at the beginning of arguments list. * * @param {String} methodName The method name * @param {Object} options The method options */ remoteMethodCtx(methodName, options) { if (options.accepts === undefined) options.accepts = []; else if (!Array.isArray(options.accepts)) options.accepts = [options.accepts]; options.accepts.unshift({ arg: 'ctx', type: 'Object', http: {source: 'context'} }); this.remoteMethod(methodName, options); }, /** * Adds a validation, marking it as exportable to the browser. * Exportable validation functions should be synchronous and totally * independent from other code because they are parsed in the browser * using eval(). * * @param {String} propertyName The property name * @param {Function} validatorFn The validation function * @param {Object} options The validation options */ validateBinded(propertyName, validatorFn, options) { let customValidator = function(err) { if (!validatorFn(this[propertyName])) err(); }; options.isExportable = true; options.bindedFunction = validatorFn; this.validate(propertyName, customValidator, options); }, /** * Catches database errors overriding create() and upsert() methods. * * @param {Function} replaceErrFunc - Callback */ rewriteDbError(replaceErrFunc) { function replaceErr(err, replaceErrFunc) { if (Array.isArray(err)) { const errors = err.filter(error => { return error != undefined && error != null; }); let errs = []; for (let e of errors) { if (!(e instanceof UserError)) errs.push(replaceErrFunc(e)); else errs.push(e); } return errs; } return replaceErrFunc(err); } function rewriteMethod(methodName) { const realMethod = this[methodName]; return function(...args) { let cb; const lastArg = args[args.length - 1]; if (lastArg instanceof Function) { cb = lastArg; args.pop(); } else cb = utils.createPromiseCallback(); args.push(function(err, res) { if (err) err = replaceErr(err, replaceErrFunc); cb(err, res); }); realMethod.apply(this, args); return cb.promise; }; } this.once('attached', () => { this.remove = this.deleteAll = this.destroyAll = rewriteMethod.call(this, 'remove'); this.upsert = rewriteMethod.call(this, 'upsert'); this.create = rewriteMethod.call(this, 'create'); }); }, /* * Shortcut to VnMySQL.executeP() */ async rawSql(query, params, options) { const userId = options?.userId; const connector = this.dataSource.connector; let conn; let res; try { if (userId) { if (!options.transaction) { options = Object.assign({}, options); conn = await new Promise((resolve, reject) => { connector.client.getConnection(function(err, conn) { if (err) reject(err); else resolve(conn); }); }); options.transaction = { connection: conn, connector }; } await connector.executeP( 'CALL account.myUser_loginWithName((SELECT name FROM account.user WHERE id = ?))', [userId], options ); } res = await connector.executeP(query, params, options); if (userId) await connector.executeP('CALL account.myUser_logout()', null, options); } finally { if (conn) conn.release(); } return res; }, /* * Shortcut to VnMySQL.executeStmt() */ rawStmt(stmt, options) { return this.dataSource.connector.executeStmt(stmt, options); }, /* * Shortcut to VnMySQL.makeLimit() */ makeLimit(filter) { return this.dataSource.connector.makeLimit(filter); }, /* * Shortcut to VnMySQL.makeSuffix() */ makeSuffix(filter) { return this.dataSource.connector.makeSuffix(filter); }, /* * Shortcut to VnMySQL.buildModelSuffix() */ buildSuffix(filter, tableAlias) { return this.dataSource.connector.buildModelSuffix(this.modelName, filter, tableAlias); }, async checkAcls(ctx, actionType) { let userId = ctx.req.accessToken.userId; let models = this.app.models; let userRoles = await models.VnUser.getRoles(userId); let data = ctx.args.data; let modelAcls; function modifiedProperties(data) { let properties = []; for (property in data) properties.push(property); return properties; } modelAcls = await models.FieldAcl.find({ where: { and: [ {model: this.modelName}, {role: {inq: userRoles}}, {property: '*'}, {or: [{actionType: '*'}, {actionType: actionType}]} ] } }); let allowedAll = modelAcls.find(acl => { return acl.property == '*'; }); if (allowedAll) return; modelAcls = await models.FieldAcl.find({ where: { and: [ {model: this.modelName}, {role: {inq: userRoles}}, {property: {inq: modifiedProperties(data)}}, {or: [{actionType: '*'}, {actionType: actionType}]} ] } }); let propsHash = {}; for (let acl of modelAcls) propsHash[acl.property] = true; let allowedProperties = Object.keys(data).every(property => { return propsHash[property]; }); if (!allowedProperties) throw new UserError(`You don't have enough privileges`); }, checkUpdateAcls(ctx) { return this.checkAcls(ctx, 'update'); }, checkInsertAcls(ctx) { return this.checkAcls(ctx, 'insert'); }, hasFilter(ctx) { return ctx.req.method.toUpperCase() === 'GET' && ctx.method.accepts.some(x => x.arg === 'filter' && x.type.toLowerCase() === 'object'); } }); };