const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const UserError = require('../helpers').UserError; module.exports = function(Self) { Self.ParameterizedSQL = ParameterizedSQL; Self.setup = function() { 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: 'actions', type: 'Object', require: true, description: 'Instances to update, example: {create: [instances], update: [instances], delete: [ids]}', http: {source: 'body'} } ], http: { path: `/crud`, verb: 'POST' } }); }; Self.defineScope = function(serverFilter) { this.remoteMethodCtx('list', { accepts: [ { arg: 'filter', type: 'object', description: 'Filter defining where' } ], returns: { type: [this.modelName], root: true }, http: { verb: 'get', path: '/list' } }); this.list = function(ctx, clientFilter, cb) { let clientFields = (clientFilter && clientFilter.fields) ? clientFilter.fields : []; let serverFields = (serverFilter && serverFilter.fields) ? serverFilter.fields : []; let fields = clientFields.filter(itemC => { return serverFields.some(itemS => itemS === itemC); }); let and = []; let order; let limit; let filter = {order: order, limit: limit}; if (clientFilter && clientFilter.where) and.push(clientFilter.where); if (serverFilter && serverFilter.where) and.push(serverFilter.where); if (clientFilter && clientFilter.order) order = clientFilter.order; else if (serverFilter && serverFilter.order) order = serverFilter.order; if (serverFilter && serverFilter.limit) limit = serverFilter.limit; else if (clientFilter && clientFilter.limit) limit = clientFilter.limit; filter.where = (and.length > 0) && {and: and}; filter.fields = fields; this.find(filter, function(err, states) { if (err) cb(err, null); else cb(null, states); }); }; }; Self.remoteMethodCtx = function(methodName, args) { var ctx = { arg: 'context', type: 'object', http: function(ctx) { return ctx; } }; if (args.accepts === undefined) args.accepts = []; else if (!Array.isArray(args.accepts)) args.accepts = [args.accepts]; args.accepts.unshift(ctx); this.remoteMethod(methodName, args); }; Self.getConnection = function(cb) { this.dataSource.connector.client.getConnection(cb); }; Self.connectToService = function(ctx, dataSource) { this.app.dataSources[dataSource].connector.remotes.auth = { bearer: new Buffer(ctx.req.accessToken.id).toString('base64'), sendImmediately: true }; }; Self.disconnectFromService = function(dataSource) { this.app.dataSources[dataSource].connector.remotes.auth = { bearer: new Buffer('').toString('base64'), sendImmediately: true }; }; Self.crud = async function(actions) { let transaction = await this.beginTransaction({}); let options = {transaction: transaction}; try { if (actions.delete && actions.delete.length) { await this.destroyAll({id: {inq: actions.delete}}, options); } if (actions.update) { try { let promises = []; actions.update.forEach(toUpdate => { promises.push(this.upsertWithWhere(toUpdate.where, toUpdate.data, options)); }); await Promise.all(promises); } catch (error) { throw error; } } if (actions.create && actions.create.length) { try { await this.create(actions.create, options); } catch (error) { throw error[error.length - 1]; } } await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } }; /** * Executes an SQL query * @param {String} query - SQL query * @param {Object} params - Query data params * @param {Object} options - Query options (Ex: {transaction}) * @param {Object} cb - Callback * @return {Object} Connector promise */ Self.rawSql = function(query, params, options = {}, cb) { var connector = this.dataSource.connector; return new Promise(function(resolve, reject) { connector.execute(query, params, options, function(error, response) { if (cb) cb(error, response); if (error) reject(error); else resolve(response); }); }); }; /** * Executes an SQL query from an Stmt * @param {ParameterizedSql} stmt - Stmt object * @param {Object} options - Query options (Ex: {transaction}) * @return {Object} Connector promise */ Self.rawStmt = function(stmt, options = {}) { return this.rawSql(stmt.sql, stmt.params, options); }; Self.escapeName = function(name) { return this.dataSource.connector.escapeName(name); }; /** * Constructs SQL where clause from Loopback filter * @param {Object} filter - filter * @param {String} tableAlias - Query main table alias * @return {String} Builded SQL where */ Self.buildWhere = function(filter, tableAlias) { let connector = this.dataSource.connector; let wrappedConnector = Object.create(connector); wrappedConnector.columnEscaped = function(model, property) { let sql = tableAlias ? connector.escapeName(tableAlias) + '.' : ''; return sql + connector.columnEscaped(model, property); }; return wrappedConnector.makeWhere(this.modelName, filter.where); }; /** * Constructs SQL limit clause from Loopback filter * @param {Object} filter - filter * @return {String} Builded SQL limit */ Self.buildLimit = function(filter) { let sql = new ParameterizedSQL(''); this.dataSource.connector.applyPagination(this.modelName, sql, filter); return sql; }; /** * Constructs SQL order clause from Loopback filter * @param {Object} filter - filter * @return {String} Builded SQL order */ Self.buildOrderBy = function(filter) { let order = filter.order; if (!order) return ''; if (typeof order === 'string') order = [order]; let clauses = []; for (let clause of order) { let sqlOrder = ''; let t = clause.split(/[\s,]+/); let names = t[0].split('.'); if (names.length > 1) sqlOrder += this.escapeName(names[0]) + '.'; sqlOrder += this.escapeName(names[names.length - 1]); if (t.length > 1) sqlOrder += ' ' + (t[1].toUpperCase() == 'ASC' ? 'ASC' : 'DESC'); clauses.push(sqlOrder); } return `ORDER BY ${clauses.join(', ')}`; }; /** * Constructs SQL pagination from Loopback filter * @param {Object} filter - filter * @return {String} Builded SQL pagination */ Self.buildPagination = function(filter) { return ParameterizedSQL.join([ this.buildOrderBy(filter), this.buildLimit(filter) ]); }; /** * Constructs SQL filter including where, order and limit * clauses from Loopback filter * @param {Object} filter - filter * @param {String} tableAlias - Query main table alias * @return {String} Builded SQL limit */ Self.buildSuffix = function(filter, tableAlias) { return ParameterizedSQL.join([ this.buildWhere(filter, tableAlias), this.buildPagination(filter) ]); }; Self.checkAcls = async function(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`); }; Self.checkUpdateAcls = function(ctx) { return this.checkAcls(ctx, 'update'); }; Self.checkInsertAcls = function(ctx) { return this.checkAcls(ctx, 'insert'); }; // Action bindings require('../methods/vn-model/validateBinded')(Self); // Handle MySql errors require('../methods/vn-model/rewriteDbError')(Self); // Get table set of values require('../methods/vn-model/getSetValues')(Self); };