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

359 lines
12 KiB
JavaScript

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 || 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) {
const {method, req} = ctx;
if (method.noLimit) return false;
return req.method.toUpperCase() === 'GET' &&
method.accepts.some(x => x.arg === 'filter' && x.type.toLowerCase() === 'object');
},
});
};