const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const UserError = require('../helpers').UserError; module.exports = function(Self) { Self.ParameterizedSQL = ParameterizedSQL; require('../methods/vn-model/getSetValues')(Self); Object.assign(Self, { setup() { Self.super_.setup.call(this); // 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'] } ] }); }, async crud(deletes, updates, creates) { let transaction = await this.beginTransaction({}); let options = {transaction}; try { 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); } if (creates && creates.length) try { await this.create(creates, options); } catch (error) { throw error[error.length - 1]; } await transaction.commit(); } catch (error) { await transaction.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)) { let errs = []; for (let e of err) errs.push(replaceErrFunc(e)); return errs; } return replaceErrFunc(err); } this.once('attached', () => { let realUpsert = this.upsert; this.upsert = async (data, options, cb) => { if (options instanceof Function) { cb = options; options = null; } try { await realUpsert.call(this, data, options); if (cb) cb(); } catch (err) { let myErr = replaceErr(err, replaceErrFunc); if (cb) cb(myErr); else throw myErr; } }; let realCreate = this.create; this.create = async (data, options, cb) => { if (options instanceof Function) { cb = options; options = null; } try { await realCreate.call(this, data, options); if (cb) cb(); } catch (err) { let myErr = replaceErr(err, replaceErrFunc); if (cb) cb(myErr); else throw myErr; } }; }); }, /* * Shortcut to VnMySQL.executeP() */ rawSql(query, params, options, cb) { return this.dataSource.connector.executeP(query, params, options, cb); }, /* * 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.Account.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'); } }); };