358 lines
12 KiB
JavaScript
358 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 > 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');
|
|
}
|
|
|
|
});
|
|
};
|