Merge branch 'dev' of https://git.verdnatura.es/salix into dev

This commit is contained in:
Gerard 2018-10-30 15:04:04 +01:00
commit a2db6201aa
24 changed files with 772 additions and 643 deletions

View File

@ -26,5 +26,7 @@ rules:
bracketSpacing: 0 bracketSpacing: 0
space-infix-ops: 1 space-infix-ops: 1
prefer-const: 0 prefer-const: 0
curly: ["error", "multi-or-nest"] curly: [error, multi]
indent: [error, 4] indent: [error, 4]
arrow-parens: [error, as-needed]
no-focused-tests: 0

View File

@ -203,7 +203,7 @@ export default class CrudModel extends ModelProxy {
this.canceler = this.$q.defer(); this.canceler = this.$q.defer();
let params = Object.assign( let params = Object.assign(
{filter: filter}, {filter},
this.buildParams() this.buildParams()
); );
let options = { let options = {

View File

@ -1,6 +1,7 @@
import ngModule from '../../module'; import ngModule from '../../module';
import Component from '../../lib/component'; import Component from '../../lib/component';
import './style.scss'; import './style.scss';
import {buildFilter} from 'vn-loopback/common/filter.js';
/** /**
* An input specialized to perform searches, it allows to use a panel * An input specialized to perform searches, it allows to use a panel
@ -84,41 +85,28 @@ export default class Controller extends Component {
this.pushFilterToState(this.filter); this.pushFilterToState(this.filter);
if (this.onSearch) if (this.onSearch)
this.onSearch({filter: this.filter}); this.onSearch({$params: this.filter});
if (this.model) { if (this.model) {
let and = []; let where = buildFilter(this.filter,
(param, value) => this.exprBuilder({param, value}));
let userParams = {}; let userParams = {};
let hasParams = false; let hasParams = false;
if (this.paramBuilder)
for (let param in this.filter) { for (let param in this.filter) {
let value = this.filter[param]; let value = this.filter[param];
if (value == null) continue; if (value == null) continue;
let expr = this.exprBuilder({param, value});
if (expr)
and.push(expr);
if (this.paramBuilder) {
let expr = this.paramBuilder({param, value}); let expr = this.paramBuilder({param, value});
if (expr) { if (expr) {
Object.assign(userParams, expr); Object.assign(userParams, expr);
hasParams = true; hasParams = true;
} }
} }
}
let where;
if (and.length == 1)
where = and[0];
else if (and.length > 1)
where = {and};
else
where = null;
this.model.applyFilter( this.model.applyFilter(
and.length > 0 ? {where: where} : null, where ? {where} : null,
hasParams ? userParams : null hasParams ? userParams : null
); );
} }
@ -235,7 +223,7 @@ ngModule.component('vnSearchbar', {
template: require('./searchbar.html'), template: require('./searchbar.html'),
bindings: { bindings: {
filter: '<?', filter: '<?',
onSearch: '&', onSearch: '&?',
panel: '@', panel: '@',
model: '<?', model: '<?',
exprBuilder: '&?', exprBuilder: '&?',

View File

@ -1,9 +1,9 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="/ticket/api/Tickets/filter" url="/ticket/api/Tickets/filter"
filter="::$ctrl.filter"
limit="20" limit="20"
data="tickets" data="tickets"
order="shipped DESC"
auto-load="false"> auto-load="false">
</vn-crud-model> </vn-crud-model>
<div margin-medium> <div margin-medium>
@ -11,8 +11,7 @@
<vn-card pad-medium-h> <vn-card pad-medium-h>
<vn-searchbar <vn-searchbar
panel="vn-ticket-search-panel" panel="vn-ticket-search-panel"
model="model" on-search="model.applyFilter(null, $params);"
expr-builder="$ctrl.exprBuilder(param, value)"
vn-focus> vn-focus>
</vn-searchbar> </vn-searchbar>
</vn-card> </vn-card>
@ -85,10 +84,12 @@
scroll-selector="ui-view"> scroll-selector="ui-view">
</vn-pagination> </vn-pagination>
</div> </div>
<vn-dialog class="dialog-summary" <vn-dialog
vn-id="dialog-summary-ticket"> vn-id="summary"
class="dialog-summary">
<tpl-body> <tpl-body>
<vn-ticket-summary ticket="$ctrl.ticketSelected"></vn-ticket-summary> <vn-ticket-summary ticket="$ctrl.selectedTicket"></vn-ticket-summary>
</tpl-body> </tpl-body>
</vn-dialog> </vn-dialog>
<vn-client-descriptor-popover vn-id="descriptor"></vn-client-descriptor-popover> <vn-client-descriptor-popover vn-id="descriptor">
</vn-client-descriptor-popover>

View File

@ -2,32 +2,8 @@ import ngModule from '../module';
export default class Controller { export default class Controller {
constructor($scope) { constructor($scope) {
this.$scope = $scope; this.$ = $scope;
this.ticketSelected = null; this.selectedTicket = null;
this.filter = {
order: 'shipped DESC'
};
}
exprBuilder(param, value) {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {id: value}
: {nickname: {like: value}};
case 'from':
return {shipped: {gte: value}};
case 'to':
return {shipped: {lte: value}};
case 'nickname':
return {[param]: {like: value}};
case 'id':
case 'clientFk':
case 'agencyModeFk':
case 'warehouseFk':
return {[param]: value};
}
} }
compareDate(date) { compareDate(date) {
@ -40,28 +16,23 @@ export default class Controller {
if (comparation == 0) if (comparation == 0)
return 'warning'; return 'warning';
if (comparation < 0) if (comparation < 0)
return 'success'; return 'success';
} }
showDescriptor(event, clientFk) { showDescriptor(event, clientFk) {
this.$scope.descriptor.clientFk = clientFk;
this.$scope.descriptor.parent = event.target;
this.$scope.descriptor.show();
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
} this.$.descriptor.clientFk = clientFk;
this.$.descriptor.parent = event.target;
onDescriptorLoad() { this.$.descriptor.show();
this.$scope.popover.relocate();
} }
preview(event, ticket) { preview(event, ticket) {
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
this.$scope.dialogSummaryTicket.show(); this.selectedTicket = ticket;
this.ticketSelected = ticket; this.$.summary.show();
} }
} }

View File

@ -1,98 +1,89 @@
import './index.js'; import './index.js';
describe('ticket', () => {
describe('Component vnTicketIndex', () => { describe('Component vnTicketIndex', () => {
let $componentController; let $element;
let controller; let $ctrl;
let $window;
let tickets = [{
id: 1,
clientFk: 1,
salesPersonFk: 9,
shipped: new Date(),
nickname: 'Test',
total: 10.5
}];
beforeEach(() => { beforeEach(() => {
angular.mock.module('ticket'); ngModule('client');
ngModule('item');
ngModule('ticket');
}); });
beforeEach(angular.mock.inject(_$componentController_ => { beforeEach(inject(($compile, $rootScope, $httpBackend, _$window_) => {
$componentController = _$componentController_; $window = _$window_;
controller = $componentController('vnTicketIndex'); $element = $compile('<vn-ticket-index></vn-ticket-index>')($rootScope);
$ctrl = $element.controller('vnTicketIndex');
$httpBackend.whenGET(/\/ticket\/api\/Tickets\/filter.*/).respond(tickets);
$httpBackend.flush();
})); }));
describe('exprBuilder()', () => { afterEach(() => {
it('should return a formated object with the id in case of search', () => { $element.remove();
let param = 'search';
let value = 1;
let result = controller.exprBuilder(param, value);
expect(result).toEqual({id: 1});
});
it('should return a formated object with the nickname in case of search', () => {
let param = 'search';
let value = 'Bruce';
let result = controller.exprBuilder(param, value);
expect(result).toEqual({nickname: {like: 'Bruce'}});
});
it('should return a formated object with the date in case of from', () => {
let param = 'from';
let value = 'Fri Aug 10 2018 11:39:21 GMT+0200';
let result = controller.exprBuilder(param, value);
expect(result).toEqual({shipped: {gte: 'Fri Aug 10 2018 11:39:21 GMT+0200'}});
});
it('should return a formated object with the date in case of to', () => {
let param = 'to';
let value = 'Fri Aug 10 2018 11:39:21 GMT+0200';
let result = controller.exprBuilder(param, value);
expect(result).toEqual({shipped: {lte: 'Fri Aug 10 2018 11:39:21 GMT+0200'}});
});
it('should return a formated object with the nickname in case of nickname', () => {
let param = 'nickname';
let value = 'Bruce';
let result = controller.exprBuilder(param, value);
expect(result).toEqual({nickname: {like: 'Bruce'}});
});
it('should return a formated object with the warehouseFk in case of warehouseFk', () => {
let param = 'warehouseFk';
let value = 'Silla';
let result = controller.exprBuilder(param, value);
expect(result).toEqual({warehouseFk: 'Silla'});
});
}); });
describe('compareDate()', () => { describe('compareDate()', () => {
it('should return warning when the date is the present', () => { it('should return warning when the date is the present', () => {
let date = new Date(); let curDate = new Date();
let result = controller.compareDate(date); let result = $ctrl.compareDate(curDate);
expect(result).toEqual('warning'); expect(result).toEqual('warning');
}); });
it('should return sucess when the date is in the future', () => { it('should return sucess when the date is in the future', () => {
let futureDate = '2518-05-19T00:00:00.000Z'; let futureDate = new Date();
let result = controller.compareDate(futureDate); futureDate = futureDate.setDate(futureDate.getDate() + 10);
let result = $ctrl.compareDate(futureDate);
expect(result).toEqual('success'); expect(result).toEqual('success');
}); });
it('should return undefined when the date is in the past', () => {
let pastDate = new Date();
pastDate = pastDate.setDate(pastDate.getDate() - 10);
let result = $ctrl.compareDate(pastDate);
expect(result).toEqual(undefined);
});
});
describe('showDescriptor()', () => {
it('should show the descriptor popover', () => {
spyOn($ctrl.$.descriptor, 'show');
let event = new MouseEvent('click', {
view: $window,
bubbles: true,
cancelable: true
});
$ctrl.showDescriptor(event, tickets[0].clientFk);
expect($ctrl.$.descriptor.show).toHaveBeenCalledWith();
});
}); });
describe('preview()', () => { describe('preview()', () => {
it('should call preventDefault and stopImmediatePropagation from event and show', () => { it('should show the dialog summary', () => {
let event = jasmine.createSpyObj('event', ['preventDefault', 'stopImmediatePropagation']); spyOn($ctrl.$.summary, 'show');
controller.$scope = {dialogSummaryTicket: {show: () => {}}}; let event = new MouseEvent('click', {
spyOn(controller.$scope.dialogSummaryTicket, 'show'); view: $window,
let ticket = {}; bubbles: true,
controller.preview(event, ticket); cancelable: true
});
$ctrl.preview(event, tickets[0]);
expect(event.preventDefault).toHaveBeenCalledWith(); expect($ctrl.$.summary.show).toHaveBeenCalledWith();
expect(event.stopImmediatePropagation).toHaveBeenCalledWith();
expect(controller.$scope.dialogSummaryTicket.show).toHaveBeenCalledWith();
});
}); });
}); });
}); });

View File

@ -37,20 +37,42 @@
vn-one vn-one
label="Agency" label="Agency"
field="filter.agencyModeFk" field="filter.agencyModeFk"
url="/api/AgencyModes" url="/api/AgencyModes">
show-field="name"
value-field="id">
<tpl-item>{{name}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete
vn-one vn-one
label="Warehouse" label="Warehouse"
field="filter.warehouseFk" field="filter.warehouseFk"
url="/api/Warehouses" url="/api/Warehouses">
show-field="name"
value-field="id">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
label="Sales person"
field="filter.salesPersonFk"
url="/api/Workers">
</vn-autocomplete>
<vn-autocomplete
vn-one
label="Province"
field="filter.provinceFk"
url="/api/Provinces">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
label="State"
field="filter.stateFk"
url="/api/States">
</vn-autocomplete>
<vn-check
vn-one
label="My tickets"
field="filter.myTeam">
</vn-check>
</vn-horizontal>
<vn-horizontal margin-large-top> <vn-horizontal margin-large-top>
<vn-submit label="Search"></vn-submit> <vn-submit label="Search"></vn-submit>
</vn-horizontal> </vn-horizontal>

View File

@ -5,3 +5,7 @@ From: Desde
To: Hasta To: Hasta
Agency: Agencia Agency: Agencia
Warehouse: Almacén Warehouse: Almacén
Sales person: Comercial
Province: Provincia
My team: Mi equipo
My tickets: Mis tickets

View File

@ -9,128 +9,108 @@ describe('Ticket Create packages path', () => {
.waitForLogin('employee'); .waitForLogin('employee');
}); });
it('should click on the Tickets button of the top bar menu', (done) => { it('should click on the Tickets button of the top bar menu', async () => {
return nightmare let url = await nightmare
.waitToClick(selectors.globalItems.applicationsMenuButton) .waitToClick(selectors.globalItems.applicationsMenuButton)
.wait(selectors.globalItems.applicationsMenuVisible) .wait(selectors.globalItems.applicationsMenuVisible)
.waitToClick(selectors.globalItems.ticketsButton) .waitToClick(selectors.globalItems.ticketsButton)
.wait(selectors.ticketsIndex.searchResult) .wait(selectors.ticketsIndex.searchResult)
.parsedUrl() .parsedUrl();
.then((url) => {
expect(url.hash).toEqual('#!/ticket/index'); expect(url.hash).toEqual('#!/ticket/index');
done();
}).catch(done.fail);
}); });
it('should search for the ticket 1', (done) => { it('should search for the ticket 1', async () => {
return nightmare let result = await nightmare
.wait(selectors.ticketsIndex.searchResult) .wait(selectors.ticketsIndex.searchResult)
.type(selectors.ticketsIndex.searchTicketInput, 'id:1') .type(selectors.ticketsIndex.searchTicketInput, 'id:1')
.click(selectors.ticketsIndex.searchButton) .click(selectors.ticketsIndex.searchButton)
.waitForNumberOfElements(selectors.ticketsIndex.searchResult, 1) .waitForNumberOfElements(selectors.ticketsIndex.searchResult, 1)
.countElement(selectors.ticketsIndex.searchResult) .countElement(selectors.ticketsIndex.searchResult);
.then((result) => {
expect(result).toEqual(1); expect(result).toEqual(1);
done();
}).catch(done.fail);
}); });
it(`should click on the search result to access to the ticket packages`, (done) => { it(`should click on the search result to access to the ticket packages`, async () => {
return nightmare let url = await nightmare
.waitForTextInElement(selectors.ticketsIndex.searchResultAddress, 'address 21') .waitForTextInElement(selectors.ticketsIndex.searchResultAddress, 'address 21')
.waitToClick(selectors.ticketsIndex.searchResult) .waitToClick(selectors.ticketsIndex.searchResult)
.waitToClick(selectors.ticketPackages.packagesButton) .waitToClick(selectors.ticketPackages.packagesButton)
.waitForURL('package/index') .waitForURL('package/index')
.url() .url();
.then((url) => {
expect(url).toContain('package/index'); expect(url).toContain('package/index');
done();
}).catch(done.fail);
}); });
it(`should delete the first package and receive and error to save a new one with blank quantity`, (done) => { it(`should delete the first package and receive and error to save a new one with blank quantity`, async () => {
return nightmare let result = await nightmare
.waitToClick(selectors.ticketPackages.firstRemovePackageButton) .waitToClick(selectors.ticketPackages.firstRemovePackageButton)
.waitToClick(selectors.ticketPackages.addPackageButton) .waitToClick(selectors.ticketPackages.addPackageButton)
.waitToClick(selectors.ticketPackages.firstPackageSelect) .waitToClick(selectors.ticketPackages.firstPackageSelect)
.waitToClick(selectors.ticketPackages.firstPackageSelectOptionTwo) .waitToClick(selectors.ticketPackages.firstPackageSelectOptionTwo)
.click(selectors.ticketPackages.savePackagesButton) .click(selectors.ticketPackages.savePackagesButton)
.waitForLastSnackbar() .waitForLastSnackbar();
.then((result) => {
expect(result).toEqual('Some fields are invalid'); expect(result).toEqual('Some fields are invalid');
done();
}).catch(done.fail);
}); });
it(`should attempt create a new package but receive an error if quantity is a string`, (done) => { it(`should attempt create a new package but receive an error if quantity is a string`, async () => {
return nightmare let result = await nightmare
.type(selectors.ticketPackages.firstQuantityInput, 'ninety 9') .type(selectors.ticketPackages.firstQuantityInput, 'ninety 9')
.click(selectors.ticketPackages.savePackagesButton) .click(selectors.ticketPackages.savePackagesButton)
.waitForLastSnackbar() .waitForLastSnackbar();
.then((result) => {
expect(result).toEqual('Some fields are invalid'); expect(result).toEqual('Some fields are invalid');
done();
}).catch(done.fail);
}); });
it(`should attempt create a new package but receive an error if quantity is 0`, (done) => { it(`should attempt create a new package but receive an error if quantity is 0`, async () => {
return nightmare let result = await nightmare
.clearInput(selectors.ticketPackages.firstQuantityInput) .clearInput(selectors.ticketPackages.firstQuantityInput)
.type(selectors.ticketPackages.firstQuantityInput, 0) .type(selectors.ticketPackages.firstQuantityInput, 0)
.click(selectors.ticketPackages.savePackagesButton) .click(selectors.ticketPackages.savePackagesButton)
.waitForLastSnackbar() .waitForLastSnackbar();
.then((result) => {
expect(result).toEqual('Some fields are invalid'); expect(result).toEqual('Some fields are invalid');
done();
}).catch(done.fail);
}); });
it(`should attempt create a new package but receive an error if package is blank`, (done) => { it(`should attempt create a new package but receive an error if package is blank`, async () => {
return nightmare let result = await nightmare
.clearInput(selectors.ticketPackages.firstQuantityInput) .clearInput(selectors.ticketPackages.firstQuantityInput)
.type(selectors.ticketPackages.firstQuantityInput, 99) .type(selectors.ticketPackages.firstQuantityInput, 99)
.click(selectors.ticketPackages.clearPackageSelectButton) .click(selectors.ticketPackages.clearPackageSelectButton)
.click(selectors.ticketPackages.savePackagesButton) .click(selectors.ticketPackages.savePackagesButton)
.waitForLastSnackbar() .waitForLastSnackbar();
.then((result) => {
expect(result).toEqual('Package cannot be blank'); expect(result).toEqual('Package cannot be blank');
done();
}).catch(done.fail);
}); });
it(`should create a new package with correct data`, (done) => { it(`should create a new package with correct data`, async () => {
return nightmare let result = await nightmare
.waitToClick(selectors.ticketPackages.firstPackageSelect) .waitToClick(selectors.ticketPackages.firstPackageSelect)
.waitToClick(selectors.ticketPackages.firstPackageSelectOptionTwo) .waitToClick(selectors.ticketPackages.firstPackageSelectOptionTwo)
.waitForTextInInput(selectors.ticketPackages.firstPackageSelect, 'Legendary Box') .waitForTextInInput(selectors.ticketPackages.firstPackageSelect, 'Legendary Box')
.click(selectors.ticketPackages.savePackagesButton) .click(selectors.ticketPackages.savePackagesButton)
.waitForLastSnackbar() .waitForLastSnackbar();
.then((result) => {
expect(result).toEqual('Data saved!'); expect(result).toEqual('Data saved!');
done();
}).catch(done.fail);
}); });
it(`should confirm the first select is the expected one`, (done) => { it(`should confirm the first select is the expected one`, async () => {
return nightmare let result = await nightmare
.click(selectors.ticketSales.saleButton) .click(selectors.ticketSales.saleButton)
.wait(selectors.ticketSales.firstPackageSelect) .wait(selectors.ticketSales.firstPackageSelect)
.click(selectors.ticketPackages.packagesButton) .click(selectors.ticketPackages.packagesButton)
.waitForTextInInput(selectors.ticketPackages.firstPackageSelect, 'Legendary Box') .waitForTextInInput(selectors.ticketPackages.firstPackageSelect, 'Legendary Box')
.getInputValue(selectors.ticketPackages.firstPackageSelect) .getInputValue(selectors.ticketPackages.firstPackageSelect);
.then((result) => {
expect(result).toEqual('Legendary Box'); expect(result).toEqual('Legendary Box');
done();
}).catch(done.fail);
}); });
it(`should confirm the first quantity is the expected one`, (done) => { it(`should confirm the first quantity is the expected one`, async () => {
return nightmare let result = await nightmare
.waitForTextInInput(selectors.ticketPackages.firstQuantityInput, '99') .waitForTextInInput(selectors.ticketPackages.firstQuantityInput, '99')
.getInputValue(selectors.ticketPackages.firstQuantityInput) .getInputValue(selectors.ticketPackages.firstQuantityInput);
.then((result) => {
expect(result).toEqual('99'); expect(result).toEqual('99');
done();
}).catch(done.fail);
}); });
}); });

60
package-lock.json generated
View File

@ -250,7 +250,7 @@
"string-width": { "string-width": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=",
"dev": true, "dev": true,
"requires": { "requires": {
"is-fullwidth-code-point": "^2.0.0", "is-fullwidth-code-point": "^2.0.0",
@ -1482,7 +1482,7 @@
"bluebird": { "bluebird": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk=", "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
"dev": true "dev": true
}, },
"bn.js": { "bn.js": {
@ -1677,7 +1677,7 @@
"string-width": { "string-width": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=",
"dev": true, "dev": true,
"requires": { "requires": {
"is-fullwidth-code-point": "^2.0.0", "is-fullwidth-code-point": "^2.0.0",
@ -4340,7 +4340,7 @@
"dot-prop": { "dot-prop": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", "integrity": "sha1-HxngwuGqDjJ5fEl5nyg3rGr2nFc=",
"dev": true, "dev": true,
"requires": { "requires": {
"is-obj": "^1.0.0" "is-obj": "^1.0.0"
@ -4447,7 +4447,7 @@
}, },
"jsonfile": { "jsonfile": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
"integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -5545,7 +5545,7 @@
}, },
"readable-stream": { "readable-stream": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true, "dev": true,
"requires": { "requires": {
@ -10019,7 +10019,7 @@
"jasmine-spec-reporter": { "jasmine-spec-reporter": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",
"integrity": "sha1-HWMq7ANBZwrTJPkrqEtLMrNeniI=", "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==",
"dev": true, "dev": true,
"requires": { "requires": {
"colors": "1.1.2" "colors": "1.1.2"
@ -10141,7 +10141,7 @@
"karma": { "karma": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
"integrity": "sha1-hcwI6eCiLXzpzKN8ShvoJPaisa4=", "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==",
"dev": true, "dev": true,
"requires": { "requires": {
"bluebird": "^3.3.0", "bluebird": "^3.3.0",
@ -11724,12 +11724,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -11749,7 +11751,8 @@
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
@ -11897,6 +11900,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -19187,7 +19191,7 @@
"split2": { "split2": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz",
"integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", "integrity": "sha1-GGsldbz4PoW30YRldWI47k7kJJM=",
"dev": true, "dev": true,
"requires": { "requires": {
"through2": "^2.0.2" "through2": "^2.0.2"
@ -19789,7 +19793,7 @@
"touch": { "touch": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
"integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", "integrity": "sha1-/jZfX3XsntTlaCXgu3bSSrdK+Ds=",
"dev": true, "dev": true,
"requires": { "requires": {
"nopt": "~1.0.10" "nopt": "~1.0.10"
@ -20286,7 +20290,7 @@
"useragent": { "useragent": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz",
"integrity": "sha1-IX+UOtVAyyEoZYqyP8lg9qiMmXI=", "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "4.1.x", "lru-cache": "4.1.x",
@ -20754,12 +20758,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -20774,17 +20780,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -20901,7 +20910,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -20913,6 +20923,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -20927,6 +20938,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -20934,12 +20946,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -20958,6 +20972,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -21038,7 +21053,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -21050,6 +21066,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -21171,6 +21188,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -22930,7 +22948,7 @@
"write-file-atomic": { "write-file-atomic": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
"integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", "integrity": "sha1-H/YVdcLipOjlENb6TiQ8zhg5mas=",
"dev": true, "dev": true,
"requires": { "requires": {
"graceful-fs": "^4.1.11", "graceful-fs": "^4.1.11",

View File

@ -29,7 +29,8 @@ module.exports = Self => {
Self.filter = async (filter, params) => { Self.filter = async (filter, params) => {
let stmt = new ParameterizedSQL( let stmt = new ParameterizedSQL(
`SELECT `SELECT * FROM (
SELECT
r.id, r.id,
r.isConciliate, r.isConciliate,
r.payed, r.payed,
@ -51,24 +52,26 @@ module.exports = Self => {
i.id, i.id,
TRUE, TRUE,
i.dued, i.dued,
c.code AS company, c.code,
i.created, i.created,
CONCAT(' N/FRA ', i.ref) description, CONCAT(' N/FRA ', i.ref),
i.amount AS debit, i.amount,
0 credit, 0 credit,
NULL bank, NULL,
NULL firstName, NULL,
NULL name, NULL,
i.clientFk i.clientFk
FROM vn.invoiceOut i FROM vn.invoiceOut i
JOIN vn.company c ON c.id = i.companyFk JOIN vn.company c ON c.id = i.companyFk
WHERE clientFk = ? WHERE clientFk = ?
) t
ORDER BY payed DESC, created DESC`, [ ORDER BY payed DESC, created DESC`, [
params.clientFk, params.clientFk,
params.clientFk params.clientFk
]); ]
);
stmt.merge(Self.buildPagination(filter)); stmt.merge(Self.makeLimit(filter));
return await Self.rawStmt(stmt); return await Self.rawStmt(stmt);
}; };
}; };

View File

@ -1 +1,2 @@
INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES (118, 'WorkerTeam', '*', '*', 'ALLOW', 'role', 'salesPerson'); INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES (118, 'WorkerTeam', '*', '*', 'ALLOW', 'role', 'salesPerson');
INSERT INTO `salix`.`ACL` (`id`, `model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES (119, 'TicketRequest', '*', '*', 'ALLOW', 'role', 'salesPerson');

View File

@ -0,0 +1,18 @@
USE `vn`;
CREATE
OR REPLACE ALGORITHM = UNDEFINED
DEFINER = `root`@`%`
SQL SECURITY DEFINER
VIEW `ticketRequest` AS
SELECT
`t`.`Id_ORDEN` AS `id`,
`t`.`ORDEN` AS `description`,
`t`.`CodVENDEDOR` AS `requestFk`,
`t`.`CodCOMPRADOR` AS `atenderFk`,
`t`.`CANTIDAD` AS `quantity`,
`t`.`PRECIOMAX` AS `price`,
`t`.`KO` AS `isOk`,
`t`.`Id_Movimiento` AS `saleFk`,
`t`.`odbc_date` AS `created`
FROM
`vn2008`.`Ordenes` `t`;

View File

@ -1018,3 +1018,10 @@ INSERT INTO `vn2008`.`workerTeam`(`id`, `team`, `user`)
(4, 2, 102), (4, 2, 102),
(5, 3, 103), (5, 3, 103),
(6, 3, 104); (6, 3, 104);
INSERT INTO `vn`.`ticketRequest`(`id`, `description`, `requestFk`, `atenderFk`, `quantity`, `price`, `isOk`, `saleFk`, `created`)
VALUES
(1, 'Gem of Time', '018', '035', 5, 9.10, 0, 1, DATE_ADD(CURDATE(), INTERVAL -15 DAY)),
(2, 'Gem of Mind', '018', '035', 10, 1.07, 0, 2, DATE_ADD(CURDATE(), INTERVAL -15 DAY)),
(3, 'Mark I', '018', '035', 20, 3.06, 0, 4, DATE_ADD(CURDATE(), INTERVAL -15 DAY)),
(4, 'Gem of Mind', '018', '035', 15, 1.30, 0, 7, CURDATE());

View File

@ -46,15 +46,7 @@ function mergeWhere(src, dst) {
let and = []; let and = [];
if (src) and.push(src); if (src) and.push(src);
if (dst) and.push(dst); if (dst) and.push(dst);
return simplifyOperation(and, 'and');
switch (and.length) {
case 0:
return undefined;
case 1:
return and[0];
default:
return {and};
}
} }
/** /**
@ -80,13 +72,40 @@ function mergeFilters(src, dst) {
res.order = src.order; res.order = src.order;
if (src.limit) if (src.limit)
res.limit = src.limit; res.limit = src.limit;
if (src.offset)
res.offset = src.offset;
if (src.skip)
res.skip = src.skip;
return res; return res;
} }
function simplifyOperation(operation, operator) {
switch(operation.length) {
case 0:
return undefined;
case 1:
return operation[0];
default:
return {[operator]: operation};
}
}
function buildFilter(params, builderFunc) {
let and = [];
for (let param in params) {
let value = params[param];
if (value == null) continue;
let expr = builderFunc(param, value);
if (expr) and.push(expr);
}
return simplifyOperation(and, 'and');
}
module.exports = { module.exports = {
fieldsToObject: fieldsToObject, fieldsToObject: fieldsToObject,
mergeFields: mergeFields, mergeFields: mergeFields,
mergeWhere: mergeWhere, mergeWhere: mergeWhere,
mergeFilters: mergeFilters mergeFilters: mergeFilters,
buildFilter: buildFilter
}; };

View File

@ -37,5 +37,6 @@
"You don't have enough privileges to do that": "You don't have enough privileges to do that", "You don't have enough privileges to do that": "You don't have enough privileges to do that",
"You don't have enough privileges to change that field": "You don't have enough privileges to change that field", "You don't have enough privileges to change that field": "You don't have enough privileges to change that field",
"You don't have enough privileges": "You don't have enough privileges", "You don't have enough privileges": "You don't have enough privileges",
"You can't make changes on a client with verified data": "You can't make changes on a client with verified data" "You can't make changes on a client with verified data": "You can't make changes on a client with verified data",
"That payment method requires a BIC": "That payment method requires a BIC"
} }

View File

@ -1,16 +1,68 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('../../filter.js').buildFilter;
const mergeFilters = require('../../filter.js').mergeFilters;
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('filter', { Self.remoteMethod('filter', {
description: 'Find all instances of the model matched by filter from the data source.', description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [ accepts: [
{ {
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
}, {
arg: 'filter', arg: 'filter',
type: 'Object', type: 'Object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
http: {source: 'query'} }, {
arg: 'search',
type: 'String',
description: `If it's and integer searchs by id, otherwise it searchs by nickname`
}, {
arg: 'from',
type: 'Date',
description: `The from date filter`
}, {
arg: 'to',
type: 'Date',
description: `The to date filter`
}, {
arg: 'nickname',
type: 'String',
description: `The nickname filter`
}, {
arg: 'id',
type: 'Integer',
description: `The ticket id filter`
}, {
arg: 'clientFk',
type: 'Integer',
description: `The client id filter`
}, {
arg: 'agencyModeFk',
type: 'Integer',
description: `The agency mode id filter`
}, {
arg: 'warehouseFk',
type: 'Integer',
description: `The warehouse id filter`
}, {
arg: 'salesPersonFk',
type: 'Integer',
description: `The salesperson id filter`
}, {
arg: 'provinceFk',
type: 'Integer',
description: `The province id filter`
}, {
arg: 'stateFk',
type: 'Number',
description: `The state id filter`
}, {
arg: 'myTeam',
type: 'Boolean',
description: `Whether to show only tickets for the current logged user team (Ignored until implemented)`
} }
], ],
returns: { returns: {
@ -18,12 +70,52 @@ module.exports = Self => {
root: true root: true
}, },
http: { http: {
path: `/filter`, path: '/filter',
verb: 'GET' verb: 'GET'
} }
}); });
Self.filter = async filter => { Self.filter = async (ctx, filter) => {
let conn = Self.dataSource.connector;
// TODO: Using the current worker id until WorkerTeam model is created
let worker = await Self.app.models.Worker.findOne({
fields: ['id'],
where: {userFk: ctx.req.accessToken.userId}
});
let teamIds = [worker && worker.id];
let where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'t.id': value}
: {'t.nickname': {like: `%${value}%`}};
case 'from':
return {'t.shipped': {gte: value}};
case 'to':
return {'t.shipped': {lte: value}};
case 'nickname':
return {'t.nickname': {like: `%${value}%`}};
case 'salesPersonFk':
return {'c.salesPersonFk': value};
case 'provinceFk':
return {'a.provinceFk': value};
case 'stateFk':
return {'ts.stateFk': value};
case 'myTeam':
return {'c.salesPersonFk': {inq: teamIds}};
case 'id':
case 'clientFk':
case 'agencyModeFk':
case 'warehouseFk':
param = `t.${param}`;
return {[param]: value};
}
});
filter = mergeFilters(filter, {where});
let stmts = []; let stmts = [];
let stmt; let stmt;
@ -31,7 +123,8 @@ module.exports = Self => {
stmt = new ParameterizedSQL( stmt = new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.filter `CREATE TEMPORARY TABLE tmp.filter
(INDEX (id)) ENGINE = MEMORY (INDEX (id))
ENGINE = MEMORY
SELECT SELECT
t.id, t.id,
t.shipped, t.shipped,
@ -58,20 +151,22 @@ module.exports = Self => {
LEFT JOIN state st ON st.id = ts.stateFk LEFT JOIN state st ON st.id = ts.stateFk
LEFT JOIN client c ON c.id = t.clientFk LEFT JOIN client c ON c.id = t.clientFk
LEFT JOIN worker wk ON wk.id = c.salesPersonFk`); LEFT JOIN worker wk ON wk.id = c.salesPersonFk`);
stmt.merge(Self.buildSuffix(filter, 't')); stmt.merge(conn.makeSuffix(filter));
stmts.push(stmt); stmts.push(stmt);
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticket'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticket');
stmts.push(` stmts.push(`
CREATE TEMPORARY TABLE tmp.ticket CREATE TEMPORARY TABLE tmp.ticket
(INDEX (ticketFk)) ENGINE = MEMORY (INDEX (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk FROM tmp.filter`); SELECT id ticketFk FROM tmp.filter`);
stmts.push('CALL ticketGetTotal()'); stmts.push('CALL ticketGetTotal()');
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticketGetProblems'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticketGetProblems');
stmts.push(` stmts.push(`
CREATE TEMPORARY TABLE tmp.ticketGetProblems CREATE TEMPORARY TABLE tmp.ticketGetProblems
(INDEX (ticketFk)) ENGINE = MEMORY (INDEX (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk, clientFk, warehouseFk, shipped SELECT id ticketFk, clientFk, warehouseFk, shipped
FROM tmp.filter`); FROM tmp.filter`);
stmts.push('CALL ticketGetProblems()'); stmts.push('CALL ticketGetProblems()');
@ -84,7 +179,7 @@ module.exports = Self => {
FROM tmp.filter f FROM tmp.filter f
LEFT JOIN tmp.ticketProblems tp ON tp.ticketFk = f.id LEFT JOIN tmp.ticketProblems tp ON tp.ticketFk = f.id
LEFT JOIN tmp.ticketTotal tt ON tt.ticketFk = f.id`); LEFT JOIN tmp.ticketTotal tt ON tt.ticketFk = f.id`);
stmt.merge(Self.buildOrderBy(filter)); stmt.merge(conn.makeOrderBy(filter.order));
let ticketsIndex = stmts.push(stmt) - 1; let ticketsIndex = stmts.push(stmt) - 1;
stmts.push( stmts.push(
@ -95,7 +190,7 @@ module.exports = Self => {
tmp.ticketGetProblems`); tmp.ticketGetProblems`);
let sql = ParameterizedSQL.join(stmts, ';'); let sql = ParameterizedSQL.join(stmts, ';');
let result = await Self.rawStmt(sql); let result = await conn.executeStmt(sql);
return result[ticketsIndex]; return result[ticketsIndex];
}; };

View File

@ -2,8 +2,10 @@ const app = require(`${servicesDir}/ticket/server/server`);
describe('ticket filter()', () => { describe('ticket filter()', () => {
it('should call the filter method', async () => { it('should call the filter method', async () => {
let ctx = {req: {accessToken: {userId: 9}}};
let filter = {order: 'shipped DESC'}; let filter = {order: 'shipped DESC'};
let result = await app.models.Ticket.filter(filter); let result = await app.models.Ticket.filter(ctx, filter);
let ticketId = result[0].id; let ticketId = result[0].id;
expect(ticketId).toEqual(15); expect(ticketId).toEqual(15);

View File

@ -0,0 +1,46 @@
{
"name": "TicketRequest",
"base": "VnModel",
"options": {
"mysql": {
"table": "ticketRequest"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"description": {
"type": "String"
},
"created": {
"type": "Date"
},
"quantity": {
"type": "Number"
},
"price": {
"type": "Number"
},
"isOk": {
"type": "Boolean"
},
"atender": {
"type": "String",
"columnName": "atenderFk"
},
"requester": {
"type": "String",
"columnName": "requesterFk"
}
},
"relations": {
"sale": {
"type": "belongsTo",
"model": "Sale",
"foreignKey": "saleFk"
}
}
}

View File

@ -5,6 +5,10 @@ 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);
Self.setup = function() { Self.setup = function() {
Self.super_.setup.call(this); Self.super_.setup.call(this);
@ -93,7 +97,7 @@ module.exports = function(Self) {
}; };
Self.remoteMethodCtx = function(methodName, args) { Self.remoteMethodCtx = function(methodName, args) {
var ctx = { let ctx = {
arg: 'context', arg: 'context',
type: 'object', type: 'object',
http: function(ctx) { http: function(ctx) {
@ -131,9 +135,9 @@ module.exports = function(Self) {
let options = {transaction: transaction}; let options = {transaction: transaction};
try { try {
if (actions.delete && actions.delete.length) { if (actions.delete && actions.delete.length)
await this.destroyAll({id: {inq: actions.delete}}, options); await this.destroyAll({id: {inq: actions.delete}}, options);
}
if (actions.update) { if (actions.update) {
try { try {
let promises = []; let promises = [];
@ -159,131 +163,6 @@ module.exports = function(Self) {
} }
}; };
/**
* 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) { Self.checkAcls = async function(ctx, actionType) {
let userId = ctx.req.accessToken.userId; let userId = ctx.req.accessToken.userId;
let models = this.app.models; let models = this.app.models;
@ -294,9 +173,8 @@ module.exports = function(Self) {
function modifiedProperties(data) { function modifiedProperties(data) {
let properties = []; let properties = [];
for (property in data) { for (property in data)
properties.push(property); properties.push(property);
}
return properties; return properties;
} }
@ -350,10 +228,38 @@ module.exports = function(Self) {
return this.checkAcls(ctx, 'insert'); return this.checkAcls(ctx, 'insert');
}; };
// Action bindings /*
require('../methods/vn-model/validateBinded')(Self); * Shortcut to VnMySQL.executeP()
// Handle MySql errors */
require('../methods/vn-model/rewriteDbError')(Self); Self.rawSql = function(query, params, options, cb) {
// Get table set of values return this.dataSource.connector.executeP(query, params, options, cb);
require('../methods/vn-model/getSetValues')(Self); };
/*
* 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,47 +1,19 @@
var mysql = require('mysql'); const mysql = require('mysql');
const loopbackConnector = require('loopback-connector'); const loopbackConnector = require('loopback-connector');
const SqlConnector = loopbackConnector.SqlConnector; const SqlConnector = loopbackConnector.SqlConnector;
const ParameterizedSQL = loopbackConnector.ParameterizedSQL; const ParameterizedSQL = loopbackConnector.ParameterizedSQL;
var MySQL = require('loopback-connector-mysql').MySQL; const MySQL = require('loopback-connector-mysql').MySQL;
var EnumFactory = require('loopback-connector-mysql').EnumFactory; const EnumFactory = require('loopback-connector-mysql').EnumFactory;
var debug = require('debug')('loopback-connector-sql'); const fs = require('fs');
exports.initialize = function(dataSource, callback) { class VnMySQL extends MySQL {
dataSource.driver = mysql; constructor(settings) {
dataSource.connector = new VnMySQL(dataSource.settings); super();
dataSource.connector.dataSource = dataSource;
var modelBuilder = dataSource.modelBuilder;
var defineType = modelBuilder.defineValueType ?
modelBuilder.defineValueType.bind(modelBuilder) :
modelBuilder.constructor.registerType.bind(modelBuilder.constructor);
defineType(function Point() {});
dataSource.EnumFactory = EnumFactory;
if (callback) {
if (dataSource.settings.lazyConnect) {
process.nextTick(function() {
callback();
});
} else {
dataSource.connector.connect(callback);
}
}
};
exports.VnMySQL = VnMySQL;
function VnMySQL(settings) {
SqlConnector.call(this, 'mysql', settings); SqlConnector.call(this, 'mysql', settings);
} }
VnMySQL.prototype = Object.create(MySQL.prototype); toColumnValue(prop, val) {
VnMySQL.constructor = VnMySQL;
VnMySQL.prototype.toColumnValue = function(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);
@ -57,139 +29,220 @@ VnMySQL.prototype.toColumnValue = function(prop, val) {
function fillZeros(v) { function fillZeros(v) {
return v < 10 ? '0' + v : v; return v < 10 ? '0' + v : v;
} }
}; }
/** /**
* Private make method * Promisified version of execute().
* @param {Object} model Model instance *
* @param {Object} where Where filter * @param {String} query The SQL query string
* @return {ParameterizedSQL} Parametized object * @param {Array} params The query parameters
* @param {Object} options The loopback options
* @param {Function} cb The callback
* @return {Promise} The operation promise
*/ */
VnMySQL.prototype._makeWhere = function(model, where) { executeP(query, params, options = {}, cb) {
let columnValue; return new Promise((resolve, reject) => {
let sqlExp; this.execute(query, params, options, (error, response) => {
if (cb)
if (!where) { cb(error, response);
return new ParameterizedSQL(''); if (error)
} reject(error);
if (typeof where !== 'object' || Array.isArray(where)) { else
debug('Invalid value for where: %j', where); resolve(response);
return new ParameterizedSQL('');
}
var self = this;
var props = self.getModelDefinition(model).properties;
var whereStmts = [];
for (var key in where) {
var stmt = new ParameterizedSQL('', []);
// Handle and/or operators
if (key === 'and' || key === 'or') {
var branches = [];
var branchParams = [];
var clauses = where[key];
if (Array.isArray(clauses)) {
for (var i = 0, n = clauses.length; i < n; i++) {
var stmtForClause = self._makeWhere(model, clauses[i]);
if (stmtForClause.sql) {
stmtForClause.sql = '(' + stmtForClause.sql + ')';
branchParams = branchParams.concat(stmtForClause.params);
branches.push(stmtForClause.sql);
}
}
stmt.merge({
sql: branches.join(' ' + key.toUpperCase() + ' '),
params: branchParams
}); });
whereStmts.push(stmt);
continue;
}
// The value is not an array, fall back to regular fields
}
var p = props[key];
// eslint-disable one-var
var expression = where[key];
var columnName = self.columnEscaped(model, key);
// eslint-enable one-var
if (expression === null || expression === undefined) {
stmt.merge(columnName + ' IS NULL');
} else if (expression && expression.constructor === Object) {
var operator = Object.keys(expression)[0];
// Get the expression without the operator
expression = expression[operator];
if (operator === 'inq' || operator === 'nin' || operator === 'between') {
columnValue = [];
if (Array.isArray(expression)) {
// Column value is a list
for (var j = 0, m = expression.length; j < m; j++) {
columnValue.push(this.toColumnValue(p, expression[j]));
}
} else {
columnValue.push(this.toColumnValue(p, expression));
}
if (operator === 'between') {
// BETWEEN v1 AND v2
var v1 = columnValue[0] === undefined ? null : columnValue[0];
var v2 = columnValue[1] === undefined ? null : columnValue[1];
columnValue = [v1, v2];
} else {
// IN (v1,v2,v3) or NOT IN (v1,v2,v3)
if (columnValue.length === 0) {
if (operator === 'inq') {
columnValue = [null];
} else {
// nin () is true
continue;
}
}
}
} else if (operator === 'regexp' && expression instanceof RegExp) {
// do not coerce RegExp based on property definitions
columnValue = expression;
} else {
columnValue = this.toColumnValue(p, expression);
}
sqlExp = self.buildExpression(columnName, operator, columnValue, p);
stmt.merge(sqlExp);
} else {
// The expression is the field value, not a condition
columnValue = self.toColumnValue(p, expression);
if (columnValue === null) {
stmt.merge(columnName + ' IS NULL');
} else if (columnValue instanceof ParameterizedSQL) {
stmt.merge(columnName + '=').merge(columnValue);
} else {
stmt.merge({
sql: columnName + '=?',
params: [columnValue]
}); });
} }
}
whereStmts.push(stmt);
}
var params = [];
var sqls = [];
for (var k = 0, s = whereStmts.length; k < s; k++) {
sqls.push(whereStmts[k].sql);
params = params.concat(whereStmts[k].params);
}
var whereStmt = new ParameterizedSQL({
sql: sqls.join(' AND '),
params: params
});
return whereStmt;
};
/** /**
* Build the SQL WHERE clause for the where object * Executes an SQL query from an Stmt.
* @param {string} model Model name *
* @param {ParameterizedSql} stmt - Stmt object
* @param {Object} options Query options (Ex: {transaction})
* @return {Object} Connector promise
*/
executeStmt(stmt, options) {
return this.executeP(stmt.sql, stmt.params, options);
}
/**
* Executes a query from an SQL script.
*
* @param {String} sqlScript The sql script file
* @param {Array} params The query parameters
* @param {Object} options Query options (Ex: {transaction})
* @return {Object} Connector promise
*/
executeScript(sqlScript, params, options) {
return new Promise((resolve, reject) => {
fs.readFile(sqlScript, 'utf8', (err, contents) => {
if (err) return reject(err);
this.execute(contents, params, options)
.then(resolve, reject);
});
});
}
/**
* Build the SQL WHERE clause for the where object without checking that
* properties exists in the model.
*
* @param {object} where An object for the where conditions * @param {object} where An object for the where conditions
* @return {ParameterizedSQL} The SQL WHERE clause * @return {ParameterizedSQL} The SQL WHERE clause
*/ */
VnMySQL.prototype.makeWhere = function(model, where) { makeWhere(where) {
var whereClause = this._makeWhere(model, where); let wrappedConnector = Object.create(this);
if (whereClause.sql) { Object.assign(wrappedConnector, {
whereClause.sql = 'WHERE ' + whereClause.sql; getModelDefinition() {
} return {
return whereClause; properties: new Proxy({}, {
get: () => true
})
};
},
toColumnValue(_, val) {
return val;
},
columnEscaped(_, property) {
return this.escapeName(property);
}
});
return wrappedConnector.buildWhere(null, where);
}
/**
* Constructs SQL order clause from Loopback filter.
*
* @param {Object} order The order definition
* @return {String} Built SQL order
*/
makeOrderBy(order) {
if (!order)
return '';
if (typeof order === 'string')
order = [order];
let clauses = [];
for (let clause of order) {
let sqlOrder = '';
let t = clause.split(/[\s,]+/);
sqlOrder += this.escapeName(t[0]);
if (t.length > 1)
sqlOrder += ' ' + (t[1].toUpperCase() == 'ASC' ? 'ASC' : 'DESC');
clauses.push(sqlOrder);
}
return `ORDER BY ${clauses.join(', ')}`;
}
/**
* Constructs SQL limit clause from Loopback filter.
*
* @param {Object} filter The loopback filter
* @return {String} Built SQL limit
*/
makeLimit(filter) {
let limit = parseInt(filter.limit);
let offset = parseInt(filter.offset || filter.skip);
return this._buildLimit(null, limit, offset);
}
/**
* Constructs SQL pagination from Loopback filter.
*
* @param {Object} filter The loopback filter
* @return {String} Built SQL pagination
*/
makePagination(filter) {
return ParameterizedSQL.join([
this.makeOrderBy(filter.order),
this.makeLimit(filter)
]);
}
/**
* Constructs SQL filter including where, order and limit
* clauses from Loopback filter.
*
* @param {Object} filter The loopback filter
* @return {String} Built SQL filter
*/
makeSuffix(filter) {
return ParameterizedSQL.join([
this.makeWhere(filter.where),
this.makePagination(filter)
]);
}
/**
* Constructs SQL where clause from Loopback filter discarding
* properties that not pertain to the model. If defined, appends
* the table alias to each field.
*
* @param {String} model The model name
* @param {Object} where The loopback where filter
* @param {String} tableAlias Query main table alias
* @return {String} Built SQL where
*/
buildModelWhere(model, where, tableAlias) {
let parent = this;
let wrappedConnector = Object.create(this);
Object.assign(wrappedConnector, {
columnEscaped(model, property) {
let sql = tableAlias
? this.escapeName(tableAlias) + '.'
: '';
return sql + parent.columnEscaped(model, property);
}
});
return wrappedConnector.buildWhere(model, where);
}
/**
* Constructs SQL where clause from Loopback filter discarding
* properties that not pertain to the model. If defined, appends
* the table alias to each field.
*
* @param {String} model The model name
* @param {Object} filter The loopback filter
* @param {String} tableAlias Query main table alias
* @return {String} Built SQL suffix
*/
buildModelSuffix(model, filter, tableAlias) {
return ParameterizedSQL.join([
this.buildModelWhere(model, filter.where, tableAlias),
this.makePagination(filter)
]);
}
}
exports.VnMySQL = VnMySQL;
exports.initialize = function initialize(dataSource, callback) {
dataSource.driver = mysql;
dataSource.connector = new VnMySQL(dataSource.settings);
dataSource.connector.dataSource = dataSource;
const modelBuilder = dataSource.modelBuilder;
const defineType = modelBuilder.defineValueType ?
modelBuilder.defineValueType.bind(modelBuilder) :
modelBuilder.constructor.registerType.bind(modelBuilder.constructor);
defineType(function Point() {});
dataSource.EnumFactory = EnumFactory;
if (callback) {
if (dataSource.settings.lazyConnect) {
process.nextTick(function() {
callback();
});
} else
dataSource.connector.connect(callback);
}
}; };

View File

@ -143,5 +143,8 @@
}, },
"WorkerTeam": { "WorkerTeam": {
"dataSource": "vn" "dataSource": "vn"
},
"TicketRequest": {
"dataSource": "vn"
} }
} }

View File

@ -8,12 +8,10 @@ module.exports = Self => {
accepts: [{ accepts: [{
arg: 'filter', arg: 'filter',
type: 'Object', type: 'Object',
required: false, description: 'Filter defining where and paginated data'
description: 'Filter defining where and paginated data',
http: {source: 'query'}
}], }],
returns: { returns: {
type: ["Object"], type: ['Object'],
root: true root: true
}, },
http: { http: {
@ -41,7 +39,7 @@ module.exports = Self => {
JOIN worker w ON w.id = st.workerFk JOIN worker w ON w.id = st.workerFk
JOIN state ste ON ste.id = st.stateFk`); JOIN state ste ON ste.id = st.stateFk`);
stmt.merge(Self.buildSuffix(filter)); stmt.merge(Self.makeSuffix(filter));
let trackings = await Self.rawStmt(stmt); let trackings = await Self.rawStmt(stmt);