VnModel.crud() bugs fixed (#784) and code refactor, VnMysql refactor

This commit is contained in:
Juan 2018-11-02 15:44:27 +01:00
parent c3401a3d9a
commit a50a98668c
11 changed files with 301 additions and 390 deletions

View File

@ -135,14 +135,14 @@ export default class CrudModel extends ModelProxy {
if (!this.isChanged) if (!this.isChanged)
return null; return null;
let create = []; let deletes = [];
let update = []; let updates = [];
let remove = []; let creates = [];
let pk = this.primaryKey; let pk = this.primaryKey;
for (let row of this.removed) for (let row of this.removed)
remove.push(row.$orgRow[pk]); deletes.push(row.$orgRow[pk]);
for (let row of this._data) for (let row of this._data)
if (row.$isNew) { if (row.$isNew) {
@ -150,22 +150,22 @@ export default class CrudModel extends ModelProxy {
for (let prop in row) for (let prop in row)
if (prop.charAt(0) !== '$') if (prop.charAt(0) !== '$')
data[prop] = row[prop]; data[prop] = row[prop];
create.push(data); creates.push(data);
} else if (row.$oldData) { } else if (row.$oldData) {
let data = {}; let data = {};
for (let prop in row.$oldData) for (let prop in row.$oldData)
data[prop] = row[prop]; data[prop] = row[prop];
update.push({ updates.push({
data, data,
where: {[pk]: row.$orgRow[pk]} where: {[pk]: row.$orgRow[pk]}
}); });
} }
let changes = { let changes = {deletes, updates, creates};
create: create,
update: update, for (let prop in changes)
delete: remove if (changes[prop].length === 0)
}; changes[prop] = undefined;
return changes; return changes;
} }

View File

@ -8,11 +8,6 @@
auto-save="true" auto-save="true"
on-save="$ctrl.onSave()"> on-save="$ctrl.onSave()">
</vn-crud-model> </vn-crud-model>
<vn-watcher
vn-id="watcher"
data="weeklies"
form="form">
</vn-watcher>
<form name="form"> <form name="form">
<div margin-medium> <div margin-medium>
<vn-card margin-medium-v pad-medium> <vn-card margin-medium-v pad-medium>

View File

@ -2,30 +2,36 @@ import ngModule from '../module';
import './style.scss'; import './style.scss';
export default class Controller { export default class Controller {
constructor($scope) { constructor($scope, vnApp, $translate) {
this.$scope = $scope; this.$scope = $scope;
this.vnApp = vnApp;
this._ = $translate;
this.ticketSelected = null; this.ticketSelected = null;
this.filter = { this.filter = {
include: [ include: {
{relation: 'ticket', relation: 'ticket',
scope: { scope: {
fields: ['id', 'clientFk', 'companyFk', 'warehouseFk'], fields: ['id', 'clientFk', 'companyFk', 'warehouseFk'],
include: [ include: [
{relation: 'client', {
scope: { relation: 'client',
fields: ['salesPersonFk', 'name'], scope: {
include: { fields: ['salesPersonFk', 'name'],
relation: 'salesPerson', include: {
fields: ['firstName', 'name'] relation: 'salesPerson',
scope: {
fields: ['id', 'firstName', 'name']
} }
} }
}, }
{relation: 'warehouse'} },
] {relation: 'warehouse'}
} ]
} }
] }
}; };
this.weekdays = [ this.weekdays = [
{id: 0, name: 'Monday'}, {id: 0, name: 'Monday'},
{id: 1, name: 'Tuesday'}, {id: 1, name: 'Tuesday'},
@ -38,7 +44,7 @@ export default class Controller {
} }
onSave() { onSave() {
this.$scope.watcher.notifySaved(); this.vnApp.showSuccess(this._.instant('Data saved!'));
} }
showClientDescriptor(event, clientFk) { showClientDescriptor(event, clientFk) {
@ -65,17 +71,9 @@ export default class Controller {
this.expeditionId = expedition.id; this.expeditionId = expedition.id;
this.$scope.deleteWeekly.show(); this.$scope.deleteWeekly.show();
} }
onSubmit() {
this.$scope.watcher.check();
this.$scope.model.save().then(() => {
this.$scope.watcher.notifySaved();
this.$scope.model.refresh();
});
}
} }
Controller.$inject = ['$scope']; Controller.$inject = ['$scope', 'vnApp', '$translate'];
ngModule.component('vnTicketWeekly', { ngModule.component('vnTicketWeekly', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -1,4 +0,0 @@
module.exports = State => {
var serverFilter = {where: {order: {gt: 0}}, order: 'order, name'};
State.defineScope(serverFilter);
};

View File

@ -1,58 +0,0 @@
module.exports = Self => {
/**
* Catches database errors overriding
* model.create() and model.upsert() methods
* @param {Object} replaceErrFunc - Callback
*/
Self.rewriteDbError = function(replaceErrFunc) {
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;
}
};
});
};
function replaceErr(err, replaceErrFunc) {
if (Array.isArray(err)) {
let errs = [];
for (let e of err)
errs.push(replaceErrFunc(e));
return errs;
}
return replaceErrFunc(err);
}
};

View File

@ -8,13 +8,11 @@ describe('Model crud()', () => {
expect(ItemBarcode.crud).toBeDefined(); expect(ItemBarcode.crud).toBeDefined();
}); });
it('should create a new instance', async() => { it('should create a new instance', async () => {
let data = {code: '500', itemFk: '1'}; let data = {code: '500', itemFk: '1'};
let creates = [data];
crudObject = { await ItemBarcode.crud(null, null, creates);
create: [data]
};
await ItemBarcode.crud(crudObject);
let instance = await ItemBarcode.findOne({where: data}); let instance = await ItemBarcode.findOne({where: data});
insertId = instance.id; insertId = instance.id;
@ -22,24 +20,21 @@ describe('Model crud()', () => {
expect(instance.code).toEqual('500'); expect(instance.code).toEqual('500');
}); });
it('should update the instance', async() => { it('should update the instance', async () => {
crudObject = { let updates = [{
update: [{ where: {id: insertId},
where: {id: insertId}, data: {code: '501', itemFk: 1}
data: {code: '501', itemFk: 1} }];
}]
}; await ItemBarcode.crud(null, updates);
await ItemBarcode.crud(crudObject);
let instance = await ItemBarcode.findById(insertId); let instance = await ItemBarcode.findById(insertId);
expect(instance.code).toEqual('501'); expect(instance.code).toEqual('501');
}); });
it('should delete the created instance', async() => { it('should delete the created instance', async () => {
crudObject = { let deletes = [insertId];
delete: [insertId] await ItemBarcode.crud(deletes);
};
await ItemBarcode.crud(crudObject);
let instance = await ItemBarcode.findById(insertId); let instance = await ItemBarcode.findById(insertId);
expect(instance).toEqual(null); expect(instance).toEqual(null);

View File

@ -1,10 +0,0 @@
module.exports = function(Self) {
Self.validateBinded = function(propertyName, validatorFn, options) {
var customValidator = function(err) {
if (!validatorFn(this[propertyName])) err();
};
options.isExportable = true;
options.bindedFunction = validatorFn;
this.validate(propertyName, customValidator, options);
};
};

View File

@ -1,5 +1,4 @@
module.exports = Self => { module.exports = Self => {
Self.defineScope({where: {isManaged: {neq: 0}}});
require('../methods/agency/landsThatDay')(Self); require('../methods/agency/landsThatDay')(Self);
require('../methods/agency/getFirstShipped')(Self); require('../methods/agency/getFirstShipped')(Self);
}; };

View File

@ -1,6 +1,4 @@
module.exports = Self => { module.exports = Self => {
require('../methods/state/list')(Self);
/** /**
* Checks if the alertLevel of a state is 0. * Checks if the alertLevel of a state is 0.
* *

View File

@ -5,261 +5,267 @@ const UserError = require('../helpers').UserError;
module.exports = function(Self) { module.exports = function(Self) {
Self.ParameterizedSQL = ParameterizedSQL; Self.ParameterizedSQL = ParameterizedSQL;
require('../methods/vn-model/validateBinded')(Self);
require('../methods/vn-model/rewriteDbError')(Self);
require('../methods/vn-model/getSetValues')(Self); require('../methods/vn-model/getSetValues')(Self);
Self.setup = function() { Object.assign(Self, {
Self.super_.setup.call(this); setup() {
Self.super_.setup.call(this);
// Register field ACL validation // Register field ACL validation
this.beforeRemote('prototype.patchAttributes', ctx => this.checkUpdateAcls(ctx)); this.beforeRemote('prototype.patchAttributes', ctx => this.checkUpdateAcls(ctx));
this.beforeRemote('updateAll', ctx => this.checkUpdateAcls(ctx)); this.beforeRemote('updateAll', ctx => this.checkUpdateAcls(ctx));
this.beforeRemote('patchOrCreate', ctx => this.checkInsertAcls(ctx)); this.beforeRemote('patchOrCreate', ctx => this.checkInsertAcls(ctx));
this.beforeRemote('create', ctx => this.checkInsertAcls(ctx)); this.beforeRemote('create', ctx => this.checkInsertAcls(ctx));
this.beforeRemote('replaceById', ctx => this.checkInsertAcls(ctx)); this.beforeRemote('replaceById', ctx => this.checkInsertAcls(ctx));
this.beforeRemote('replaceOrCreate', ctx => this.checkInsertAcls(ctx)); this.beforeRemote('replaceOrCreate', ctx => this.checkInsertAcls(ctx));
this.remoteMethod('crud', { this.remoteMethod('crud', {
description: 'Create, update or/and delete instances from model with a single request', description: `Create, update or/and delete instances from model with a single request`,
accessType: 'WRITE', accessType: 'WRITE',
accepts: [ accepts: [
{ {
arg: 'actions', arg: 'deletes',
type: 'Object', description: `Identifiers of instances to delete`,
require: true, type: ['Integer']
description: 'Instances to update, example: {create: [instances], update: [instances], delete: [ids]}', }, {
http: {source: 'body'} arg: 'updates',
} description: `Instances to update with it's identifier {where, data}`,
], type: ['Object']
http: { }, {
path: `/crud`, arg: 'creates',
verb: 'POST' description: `Instances to create`,
} type: ['Object']
}); }
}; ]
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) async crud(deletes, updates, creates) {
and.push(clientFilter.where); let transaction = await this.beginTransaction({});
if (serverFilter && serverFilter.where) let options = {transaction};
and.push(serverFilter.where);
if (clientFilter && clientFilter.order) try {
order = clientFilter.order; if (deletes) {
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) {
let 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 = []; let promises = [];
actions.update.forEach(toUpdate => { for (let id of deletes)
promises.push(this.upsertWithWhere(toUpdate.where, toUpdate.data, options)); promises.push(this.destroyById(id, options));
});
await Promise.all(promises); await Promise.all(promises);
} catch (error) {
throw error;
} }
} if (updates) {
if (actions.create && actions.create.length) { let promises = [];
try { for (let update of updates)
await this.create(actions.create, options); promises.push(this.upsertWithWhere(update.where, update.data, options));
} catch (error) { await Promise.all(promises);
throw error[error.length - 1];
} }
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;
} }
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');
} }
}; });
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');
};
/*
* Shortcut to VnMySQL.executeP()
*/
Self.rawSql = function(query, params, options, cb) {
return this.dataSource.connector.executeP(query, params, options, cb);
};
/*
* Shortcut to VnMySQL.executeStmt()
*/
Self.rawStmt = function(stmt, options) {
return this.dataSource.connector.executeStmt(stmt, options);
};
/*
* Shortcut to VnMySQL.makeLimit()
*/
Self.makeLimit = function(filter) {
return this.dataSource.connector.makeLimit(filter);
};
/*
* Shortcut to VnMySQL.makeSuffix()
*/
Self.makeSuffix = function(filter) {
return this.dataSource.connector.makeSuffix(filter);
};
/*
* Shortcut to VnMySQL.buildModelSuffix()
*/
Self.buildSuffix = function(filter, tableAlias) {
return this.dataSource.connector.buildModelSuffix(this.modelName, filter, tableAlias);
};
}; };

View File

@ -1,18 +1,11 @@
const mysql = require('mysql'); const mysql = require('mysql');
const loopbackConnector = require('loopback-connector'); const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const SqlConnector = loopbackConnector.SqlConnector;
const ParameterizedSQL = loopbackConnector.ParameterizedSQL;
const MySQL = require('loopback-connector-mysql').MySQL; const MySQL = require('loopback-connector-mysql').MySQL;
const EnumFactory = require('loopback-connector-mysql').EnumFactory; const EnumFactory = require('loopback-connector-mysql').EnumFactory;
const fs = require('fs'); const fs = require('fs');
class VnMySQL extends MySQL { class VnMySQL extends MySQL {
constructor(settings) {
super();
SqlConnector.call(this, 'mysql', settings);
}
toColumnValue(prop, val) { toColumnValue(prop, val) {
if (val == null || !prop || prop.type !== Date) if (val == null || !prop || prop.type !== Date)
return MySQL.prototype.toColumnValue.call(this, prop, val); return MySQL.prototype.toColumnValue.call(this, prop, val);
@ -237,12 +230,11 @@ exports.initialize = function initialize(dataSource, callback) {
dataSource.EnumFactory = EnumFactory; dataSource.EnumFactory = EnumFactory;
if (callback) { if (callback)
if (dataSource.settings.lazyConnect) { if (dataSource.settings.lazyConnect) {
process.nextTick(function() { process.nextTick(function() {
callback(); callback();
}); });
} else } else
dataSource.connector.connect(callback); dataSource.connector.connect(callback);
}
}; };