salix/services/loopback/common/models/vn-model.js

272 lines
9.1 KiB
JavaScript

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');
}
});
};