From 260f548019af221eb6dd920e492df764ef7273b5 Mon Sep 17 00:00:00 2001 From: Juan Date: Tue, 30 Oct 2018 13:58:02 +0100 Subject: [PATCH] #599 Advanced ticket search, eslint rules fixed, e2e ticket_04 refactor --- .eslintrc.yml | 6 +- .../src/components/crud-model/crud-model.js | 2 +- .../src/components/searchbar/searchbar.js | 34 +- client/ticket/src/index/index.html | 15 +- client/ticket/src/index/index.js | 43 +- client/ticket/src/index/index.spec.js | 155 ++++--- client/ticket/src/search-panel/index.html | 36 +- client/ticket/src/search-panel/locale/es.yml | 6 +- .../04_create_ticket_packages.spec.js | 120 +++--- package-lock.json | 60 ++- .../client/common/methods/receipt/filter.js | 79 ++-- services/loopback/common/filter.js | 39 +- services/loopback/common/locale/en.json | 3 +- .../loopback/common/methods/ticket/filter.js | 149 +++++-- .../methods/ticket/specs/filter.spec.js | 6 +- services/loopback/common/models/vn-model.js | 178 ++------ .../loopback/server/connectors/vn-mysql.js | 395 ++++++++++-------- .../methods/sale-tracking/listSaleTracking.js | 8 +- .../specs/listSaleTracking.spec.js | 4 +- 19 files changed, 696 insertions(+), 642 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index f20cefcb8..e7121aa4a 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -26,5 +26,7 @@ rules: bracketSpacing: 0 space-infix-ops: 1 prefer-const: 0 - curly: ["error", "multi-or-nest"] - indent: [error, 4] \ No newline at end of file + curly: [error, multi] + indent: [error, 4] + arrow-parens: [error, as-needed] + no-focused-tests: 0 \ No newline at end of file diff --git a/client/core/src/components/crud-model/crud-model.js b/client/core/src/components/crud-model/crud-model.js index 5ddf54653..251dbcfc4 100644 --- a/client/core/src/components/crud-model/crud-model.js +++ b/client/core/src/components/crud-model/crud-model.js @@ -203,7 +203,7 @@ export default class CrudModel extends ModelProxy { this.canceler = this.$q.defer(); let params = Object.assign( - {filter: filter}, + {filter}, this.buildParams() ); let options = { diff --git a/client/core/src/components/searchbar/searchbar.js b/client/core/src/components/searchbar/searchbar.js index 6bb019b91..40a023d5a 100644 --- a/client/core/src/components/searchbar/searchbar.js +++ b/client/core/src/components/searchbar/searchbar.js @@ -1,6 +1,7 @@ import ngModule from '../../module'; import Component from '../../lib/component'; import './style.scss'; +import {buildFilter} from 'vn-loopback/common/filter.js'; /** * 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); if (this.onSearch) - this.onSearch({filter: this.filter}); + this.onSearch({$params: this.filter}); if (this.model) { - let and = []; + let where = buildFilter(this.filter, + (param, value) => this.exprBuilder({param, value})); + let userParams = {}; let hasParams = false; - for (let param in this.filter) { - let value = this.filter[param]; - if (value == null) continue; - - let expr = this.exprBuilder({param, value}); - if (expr) - and.push(expr); - - if (this.paramBuilder) { + if (this.paramBuilder) + for (let param in this.filter) { + let value = this.filter[param]; + if (value == null) continue; let expr = this.paramBuilder({param, value}); if (expr) { Object.assign(userParams, expr); hasParams = true; } } - } - - let where; - - if (and.length == 1) - where = and[0]; - else if (and.length > 1) - where = {and}; - else - where = null; this.model.applyFilter( - and.length > 0 ? {where: where} : null, + where ? {where} : null, hasParams ? userParams : null ); } @@ -235,7 +223,7 @@ ngModule.component('vnSearchbar', { template: require('./searchbar.html'), bindings: { filter: '
@@ -11,8 +11,7 @@ @@ -85,10 +84,12 @@ scroll-selector="ui-view">
- + - + - \ No newline at end of file + + \ No newline at end of file diff --git a/client/ticket/src/index/index.js b/client/ticket/src/index/index.js index 1fa53b5ce..04c54638e 100644 --- a/client/ticket/src/index/index.js +++ b/client/ticket/src/index/index.js @@ -2,32 +2,8 @@ import ngModule from '../module'; export default class Controller { constructor($scope) { - this.$scope = $scope; - this.ticketSelected = 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}; - } + this.$ = $scope; + this.selectedTicket = null; } compareDate(date) { @@ -40,28 +16,23 @@ export default class Controller { if (comparation == 0) return 'warning'; - if (comparation < 0) return 'success'; } showDescriptor(event, clientFk) { - this.$scope.descriptor.clientFk = clientFk; - this.$scope.descriptor.parent = event.target; - this.$scope.descriptor.show(); event.preventDefault(); event.stopImmediatePropagation(); - } - - onDescriptorLoad() { - this.$scope.popover.relocate(); + this.$.descriptor.clientFk = clientFk; + this.$.descriptor.parent = event.target; + this.$.descriptor.show(); } preview(event, ticket) { event.preventDefault(); event.stopImmediatePropagation(); - this.$scope.dialogSummaryTicket.show(); - this.ticketSelected = ticket; + this.selectedTicket = ticket; + this.$.summary.show(); } } diff --git a/client/ticket/src/index/index.spec.js b/client/ticket/src/index/index.spec.js index 6d60996a6..d334cb7c9 100644 --- a/client/ticket/src/index/index.spec.js +++ b/client/ticket/src/index/index.spec.js @@ -1,98 +1,89 @@ import './index.js'; -describe('ticket', () => { - describe('Component vnTicketIndex', () => { - let $componentController; - let controller; +describe('Component vnTicketIndex', () => { + let $element; + let $ctrl; + let $window; + let tickets = [{ + id: 1, + clientFk: 1, + salesPersonFk: 9, + shipped: new Date(), + nickname: 'Test', + total: 10.5 + }]; - beforeEach(() => { - angular.mock.module('ticket'); + beforeEach(() => { + ngModule('client'); + ngModule('item'); + ngModule('ticket'); + }); + + beforeEach(inject(($compile, $rootScope, $httpBackend, _$window_) => { + $window = _$window_; + $element = $compile('')($rootScope); + $ctrl = $element.controller('vnTicketIndex'); + + $httpBackend.whenGET(/\/ticket\/api\/Tickets\/filter.*/).respond(tickets); + $httpBackend.flush(); + })); + + afterEach(() => { + $element.remove(); + }); + + describe('compareDate()', () => { + it('should return warning when the date is the present', () => { + let curDate = new Date(); + let result = $ctrl.compareDate(curDate); + + expect(result).toEqual('warning'); }); - beforeEach(angular.mock.inject(_$componentController_ => { - $componentController = _$componentController_; - controller = $componentController('vnTicketIndex'); - })); + it('should return sucess when the date is in the future', () => { + let futureDate = new Date(); + futureDate = futureDate.setDate(futureDate.getDate() + 10); + let result = $ctrl.compareDate(futureDate); - describe('exprBuilder()', () => { - it('should return a formated object with the id in case of search', () => { - 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'}); - }); + expect(result).toEqual('success'); }); - describe('compareDate()', () => { - it('should return warning when the date is the present', () => { - let date = new Date(); - let result = controller.compareDate(date); + 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('warning'); - }); - - it('should return sucess when the date is in the future', () => { - let futureDate = '2518-05-19T00:00:00.000Z'; - let result = controller.compareDate(futureDate); - - expect(result).toEqual('success'); - }); + expect(result).toEqual(undefined); }); + }); - describe('preview()', () => { - it('should call preventDefault and stopImmediatePropagation from event and show', () => { - let event = jasmine.createSpyObj('event', ['preventDefault', 'stopImmediatePropagation']); + describe('showDescriptor()', () => { + it('should show the descriptor popover', () => { + spyOn($ctrl.$.descriptor, 'show'); - controller.$scope = {dialogSummaryTicket: {show: () => {}}}; - spyOn(controller.$scope.dialogSummaryTicket, 'show'); - let ticket = {}; - controller.preview(event, ticket); - - expect(event.preventDefault).toHaveBeenCalledWith(); - expect(event.stopImmediatePropagation).toHaveBeenCalledWith(); - expect(controller.$scope.dialogSummaryTicket.show).toHaveBeenCalledWith(); + let event = new MouseEvent('click', { + view: $window, + bubbles: true, + cancelable: true }); + $ctrl.showDescriptor(event, tickets[0].clientFk); + + expect($ctrl.$.descriptor.show).toHaveBeenCalledWith(); + }); + }); + + describe('preview()', () => { + it('should show the dialog summary', () => { + spyOn($ctrl.$.summary, 'show'); + + let event = new MouseEvent('click', { + view: $window, + bubbles: true, + cancelable: true + }); + $ctrl.preview(event, tickets[0]); + + expect($ctrl.$.summary.show).toHaveBeenCalledWith(); }); }); }); diff --git a/client/ticket/src/search-panel/index.html b/client/ticket/src/search-panel/index.html index e8ddd290e..b53e3e739 100644 --- a/client/ticket/src/search-panel/index.html +++ b/client/ticket/src/search-panel/index.html @@ -37,20 +37,42 @@ vn-one label="Agency" field="filter.agencyModeFk" - url="/api/AgencyModes" - show-field="name" - value-field="id"> - {{name}} + url="/api/AgencyModes"> + url="/api/Warehouses"> + + + + + + + + + + + + diff --git a/client/ticket/src/search-panel/locale/es.yml b/client/ticket/src/search-panel/locale/es.yml index 1f892a742..20ef1ee23 100644 --- a/client/ticket/src/search-panel/locale/es.yml +++ b/client/ticket/src/search-panel/locale/es.yml @@ -4,4 +4,8 @@ Nickname: Alias From: Desde To: Hasta Agency: Agencia -Warehouse: Almacén \ No newline at end of file +Warehouse: Almacén +Sales person: Comercial +Province: Provincia +My team: Mi equipo +My tickets: Mis tickets \ No newline at end of file diff --git a/e2e/paths/ticket-module/04_create_ticket_packages.spec.js b/e2e/paths/ticket-module/04_create_ticket_packages.spec.js index ad9a52830..a5a266489 100644 --- a/e2e/paths/ticket-module/04_create_ticket_packages.spec.js +++ b/e2e/paths/ticket-module/04_create_ticket_packages.spec.js @@ -9,128 +9,108 @@ describe('Ticket Create packages path', () => { .waitForLogin('employee'); }); - it('should click on the Tickets button of the top bar menu', (done) => { - return nightmare + it('should click on the Tickets button of the top bar menu', async () => { + let url = await nightmare .waitToClick(selectors.globalItems.applicationsMenuButton) .wait(selectors.globalItems.applicationsMenuVisible) .waitToClick(selectors.globalItems.ticketsButton) .wait(selectors.ticketsIndex.searchResult) - .parsedUrl() - .then((url) => { - expect(url.hash).toEqual('#!/ticket/index'); - done(); - }).catch(done.fail); + .parsedUrl(); + + expect(url.hash).toEqual('#!/ticket/index'); }); - it('should search for the ticket 1', (done) => { - return nightmare + it('should search for the ticket 1', async () => { + let result = await nightmare .wait(selectors.ticketsIndex.searchResult) .type(selectors.ticketsIndex.searchTicketInput, 'id:1') .click(selectors.ticketsIndex.searchButton) .waitForNumberOfElements(selectors.ticketsIndex.searchResult, 1) - .countElement(selectors.ticketsIndex.searchResult) - .then((result) => { - expect(result).toEqual(1); - done(); - }).catch(done.fail); + .countElement(selectors.ticketsIndex.searchResult); + + expect(result).toEqual(1); }); - it(`should click on the search result to access to the ticket packages`, (done) => { - return nightmare + it(`should click on the search result to access to the ticket packages`, async () => { + let url = await nightmare .waitForTextInElement(selectors.ticketsIndex.searchResultAddress, 'address 21') .waitToClick(selectors.ticketsIndex.searchResult) .waitToClick(selectors.ticketPackages.packagesButton) .waitForURL('package/index') - .url() - .then((url) => { - expect(url).toContain('package/index'); - done(); - }).catch(done.fail); + .url(); + + expect(url).toContain('package/index'); }); - it(`should delete the first package and receive and error to save a new one with blank quantity`, (done) => { - return nightmare + it(`should delete the first package and receive and error to save a new one with blank quantity`, async () => { + let result = await nightmare .waitToClick(selectors.ticketPackages.firstRemovePackageButton) .waitToClick(selectors.ticketPackages.addPackageButton) .waitToClick(selectors.ticketPackages.firstPackageSelect) .waitToClick(selectors.ticketPackages.firstPackageSelectOptionTwo) .click(selectors.ticketPackages.savePackagesButton) - .waitForLastSnackbar() - .then((result) => { - expect(result).toEqual('Some fields are invalid'); - done(); - }).catch(done.fail); + .waitForLastSnackbar(); + + expect(result).toEqual('Some fields are invalid'); }); - it(`should attempt create a new package but receive an error if quantity is a string`, (done) => { - return nightmare + it(`should attempt create a new package but receive an error if quantity is a string`, async () => { + let result = await nightmare .type(selectors.ticketPackages.firstQuantityInput, 'ninety 9') .click(selectors.ticketPackages.savePackagesButton) - .waitForLastSnackbar() - .then((result) => { - expect(result).toEqual('Some fields are invalid'); - done(); - }).catch(done.fail); + .waitForLastSnackbar(); + + expect(result).toEqual('Some fields are invalid'); }); - it(`should attempt create a new package but receive an error if quantity is 0`, (done) => { - return nightmare + it(`should attempt create a new package but receive an error if quantity is 0`, async () => { + let result = await nightmare .clearInput(selectors.ticketPackages.firstQuantityInput) .type(selectors.ticketPackages.firstQuantityInput, 0) .click(selectors.ticketPackages.savePackagesButton) - .waitForLastSnackbar() - .then((result) => { - expect(result).toEqual('Some fields are invalid'); - done(); - }).catch(done.fail); + .waitForLastSnackbar(); + + expect(result).toEqual('Some fields are invalid'); }); - it(`should attempt create a new package but receive an error if package is blank`, (done) => { - return nightmare + it(`should attempt create a new package but receive an error if package is blank`, async () => { + let result = await nightmare .clearInput(selectors.ticketPackages.firstQuantityInput) .type(selectors.ticketPackages.firstQuantityInput, 99) .click(selectors.ticketPackages.clearPackageSelectButton) .click(selectors.ticketPackages.savePackagesButton) - .waitForLastSnackbar() - .then((result) => { - expect(result).toEqual('Package cannot be blank'); - done(); - }).catch(done.fail); + .waitForLastSnackbar(); + + expect(result).toEqual('Package cannot be blank'); }); - it(`should create a new package with correct data`, (done) => { - return nightmare + it(`should create a new package with correct data`, async () => { + let result = await nightmare .waitToClick(selectors.ticketPackages.firstPackageSelect) .waitToClick(selectors.ticketPackages.firstPackageSelectOptionTwo) .waitForTextInInput(selectors.ticketPackages.firstPackageSelect, 'Legendary Box') .click(selectors.ticketPackages.savePackagesButton) - .waitForLastSnackbar() - .then((result) => { - expect(result).toEqual('Data saved!'); - done(); - }).catch(done.fail); + .waitForLastSnackbar(); + + expect(result).toEqual('Data saved!'); }); - it(`should confirm the first select is the expected one`, (done) => { - return nightmare + it(`should confirm the first select is the expected one`, async () => { + let result = await nightmare .click(selectors.ticketSales.saleButton) .wait(selectors.ticketSales.firstPackageSelect) .click(selectors.ticketPackages.packagesButton) .waitForTextInInput(selectors.ticketPackages.firstPackageSelect, 'Legendary Box') - .getInputValue(selectors.ticketPackages.firstPackageSelect) - .then((result) => { - expect(result).toEqual('Legendary Box'); - done(); - }).catch(done.fail); + .getInputValue(selectors.ticketPackages.firstPackageSelect); + + expect(result).toEqual('Legendary Box'); }); - it(`should confirm the first quantity is the expected one`, (done) => { - return nightmare + it(`should confirm the first quantity is the expected one`, async () => { + let result = await nightmare .waitForTextInInput(selectors.ticketPackages.firstQuantityInput, '99') - .getInputValue(selectors.ticketPackages.firstQuantityInput) - .then((result) => { - expect(result).toEqual('99'); - done(); - }).catch(done.fail); + .getInputValue(selectors.ticketPackages.firstQuantityInput); + + expect(result).toEqual('99'); }); }); diff --git a/package-lock.json b/package-lock.json index 48fe0ed94..5ea74cf5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -250,7 +250,7 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", @@ -1482,7 +1482,7 @@ "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk=", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", "dev": true }, "bn.js": { @@ -1677,7 +1677,7 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", @@ -4340,7 +4340,7 @@ "dot-prop": { "version": "4.2.0", "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, "requires": { "is-obj": "^1.0.0" @@ -4447,7 +4447,7 @@ }, "jsonfile": { "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=", "dev": true, "requires": { @@ -5545,7 +5545,7 @@ }, "readable-stream": { "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==", "dev": true, "requires": { @@ -10019,7 +10019,7 @@ "jasmine-spec-reporter": { "version": "4.2.1", "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, "requires": { "colors": "1.1.2" @@ -10141,7 +10141,7 @@ "karma": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz", - "integrity": "sha1-hcwI6eCiLXzpzKN8ShvoJPaisa4=", + "integrity": "sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg==", "dev": true, "requires": { "bluebird": "^3.3.0", @@ -11724,12 +11724,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11749,7 +11751,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", @@ -11897,6 +11900,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -19187,7 +19191,7 @@ "split2": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "integrity": "sha1-GGsldbz4PoW30YRldWI47k7kJJM=", "dev": true, "requires": { "through2": "^2.0.2" @@ -19789,7 +19793,7 @@ "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "integrity": "sha1-/jZfX3XsntTlaCXgu3bSSrdK+Ds=", "dev": true, "requires": { "nopt": "~1.0.10" @@ -20286,7 +20290,7 @@ "useragent": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", - "integrity": "sha1-IX+UOtVAyyEoZYqyP8lg9qiMmXI=", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", "dev": true, "requires": { "lru-cache": "4.1.x", @@ -20754,12 +20758,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -20774,17 +20780,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -20901,7 +20910,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -20913,6 +20923,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -20927,6 +20938,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -20934,12 +20946,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -20958,6 +20972,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -21038,7 +21053,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -21050,6 +21066,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -21171,6 +21188,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -22930,7 +22948,7 @@ "write-file-atomic": { "version": "2.3.0", "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, "requires": { "graceful-fs": "^4.1.11", diff --git a/services/client/common/methods/receipt/filter.js b/services/client/common/methods/receipt/filter.js index 12474240a..f63bac66d 100644 --- a/services/client/common/methods/receipt/filter.js +++ b/services/client/common/methods/receipt/filter.js @@ -29,46 +29,49 @@ module.exports = Self => { Self.filter = async (filter, params) => { let stmt = new ParameterizedSQL( - `SELECT - r.id, - r.isConciliate, - r.payed, - c.code AS company, - r.created, - '' description, - 0 AS debit, - r.amountPaid AS credit, - r.bankFk, - firstName, - name, - r.clientFk - FROM vn.receipt r - LEFT JOIN vn.worker w ON w.id = r.workerFk - JOIN vn.company c ON c.id = r.companyFk - WHERE clientFk = ? - UNION ALL - SELECT - i.id, - TRUE, - i.dued, - c.code AS company, - i.created, - CONCAT(' N/FRA ', i.ref) description, - i.amount AS debit, - 0 credit, - NULL bank, - NULL firstName, - NULL name, - i.clientFk - FROM vn.invoiceOut i - JOIN vn.company c ON c.id = i.companyFk - WHERE clientFk = ? + `SELECT * FROM ( + SELECT + r.id, + r.isConciliate, + r.payed, + c.code AS company, + r.created, + '' description, + 0 AS debit, + r.amountPaid AS credit, + r.bankFk, + firstName, + name, + r.clientFk + FROM vn.receipt r + LEFT JOIN vn.worker w ON w.id = r.workerFk + JOIN vn.company c ON c.id = r.companyFk + WHERE clientFk = ? + UNION ALL + SELECT + i.id, + TRUE, + i.dued, + c.code, + i.created, + CONCAT(' N/FRA ', i.ref), + i.amount, + 0 credit, + NULL, + NULL, + NULL, + i.clientFk + FROM vn.invoiceOut i + JOIN vn.company c ON c.id = i.companyFk + WHERE clientFk = ? + ) t 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); }; }; diff --git a/services/loopback/common/filter.js b/services/loopback/common/filter.js index 9f23b5330..73833116f 100644 --- a/services/loopback/common/filter.js +++ b/services/loopback/common/filter.js @@ -46,15 +46,7 @@ function mergeWhere(src, dst) { let and = []; if (src) and.push(src); if (dst) and.push(dst); - - switch (and.length) { - case 0: - return undefined; - case 1: - return and[0]; - default: - return {and}; - } + return simplifyOperation(and, 'and'); } /** @@ -80,13 +72,40 @@ function mergeFilters(src, dst) { res.order = src.order; if (src.limit) res.limit = src.limit; + if (src.offset) + res.offset = src.offset; + if (src.skip) + res.skip = src.skip; 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 = { fieldsToObject: fieldsToObject, mergeFields: mergeFields, mergeWhere: mergeWhere, - mergeFilters: mergeFilters + mergeFilters: mergeFilters, + buildFilter: buildFilter }; diff --git a/services/loopback/common/locale/en.json b/services/loopback/common/locale/en.json index d870087d1..b188713ed 100644 --- a/services/loopback/common/locale/en.json +++ b/services/loopback/common/locale/en.json @@ -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 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 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" } \ No newline at end of file diff --git a/services/loopback/common/methods/ticket/filter.js b/services/loopback/common/methods/ticket/filter.js index 0a7edf1f4..ec2da5895 100644 --- a/services/loopback/common/methods/ticket/filter.js +++ b/services/loopback/common/methods/ticket/filter.js @@ -1,16 +1,68 @@ const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; +const buildFilter = require('../../filter.js').buildFilter; +const mergeFilters = require('../../filter.js').mergeFilters; module.exports = Self => { Self.remoteMethod('filter', { description: 'Find all instances of the model matched by filter from the data source.', - accessType: 'READ', accepts: [ { + arg: 'ctx', + type: 'Object', + http: {source: 'context'} + }, { arg: 'filter', type: 'Object', - description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', - http: {source: 'query'} + description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string` + }, { + 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: { @@ -18,12 +70,52 @@ module.exports = Self => { root: true }, http: { - path: `/filter`, + path: '/filter', 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 stmt; @@ -31,12 +123,13 @@ module.exports = Self => { stmt = new ParameterizedSQL( `CREATE TEMPORARY TABLE tmp.filter - (INDEX (id)) ENGINE = MEMORY + (INDEX (id)) + ENGINE = MEMORY SELECT t.id, - t.shipped, - t.nickname, - t.refFk, + t.shipped, + t.nickname, + t.refFk, t.routeFk, t.agencyModeFk, t.warehouseFk, @@ -44,34 +137,36 @@ module.exports = Self => { c.salesPersonFk, a.provinceFk, ts.stateFk, - p.name AS province, - w.name AS warehouse, - am.name AS agencyMode, - st.name AS state, + p.name AS province, + w.name AS warehouse, + am.name AS agencyMode, + st.name AS state, wk.name AS salesPerson - FROM ticket t - LEFT JOIN address a ON a.id = t.addressFk - LEFT JOIN province p ON p.id = a.provinceFk - LEFT JOIN warehouse w ON w.id = t.warehouseFk - LEFT JOIN agencyMode am ON am.id = t.agencyModeFk - LEFT JOIN ticketState ts ON ts.ticketFk = t.id - LEFT JOIN state st ON st.id = ts.stateFk - LEFT JOIN client c ON c.id = t.clientFk - LEFT JOIN worker wk ON wk.id = c.salesPersonFk`); - stmt.merge(Self.buildSuffix(filter, 't')); + FROM ticket t + LEFT JOIN address a ON a.id = t.addressFk + LEFT JOIN province p ON p.id = a.provinceFk + LEFT JOIN warehouse w ON w.id = t.warehouseFk + LEFT JOIN agencyMode am ON am.id = t.agencyModeFk + LEFT JOIN ticketState ts ON ts.ticketFk = t.id + LEFT JOIN state st ON st.id = ts.stateFk + LEFT JOIN client c ON c.id = t.clientFk + LEFT JOIN worker wk ON wk.id = c.salesPersonFk`); + stmt.merge(conn.makeSuffix(filter)); stmts.push(stmt); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticket'); stmts.push(` CREATE TEMPORARY TABLE tmp.ticket - (INDEX (ticketFk)) ENGINE = MEMORY + (INDEX (ticketFk)) + ENGINE = MEMORY SELECT id ticketFk FROM tmp.filter`); stmts.push('CALL ticketGetTotal()'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticketGetProblems'); stmts.push(` CREATE TEMPORARY TABLE tmp.ticketGetProblems - (INDEX (ticketFk)) ENGINE = MEMORY + (INDEX (ticketFk)) + ENGINE = MEMORY SELECT id ticketFk, clientFk, warehouseFk, shipped FROM tmp.filter`); stmts.push('CALL ticketGetProblems()'); @@ -84,7 +179,7 @@ module.exports = Self => { FROM tmp.filter f LEFT JOIN tmp.ticketProblems tp ON tp.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; stmts.push( @@ -95,7 +190,7 @@ module.exports = Self => { tmp.ticketGetProblems`); let sql = ParameterizedSQL.join(stmts, ';'); - let result = await Self.rawStmt(sql); + let result = await conn.executeStmt(sql); return result[ticketsIndex]; }; diff --git a/services/loopback/common/methods/ticket/specs/filter.spec.js b/services/loopback/common/methods/ticket/specs/filter.spec.js index 38de4fabe..e1d790b2e 100644 --- a/services/loopback/common/methods/ticket/specs/filter.spec.js +++ b/services/loopback/common/methods/ticket/specs/filter.spec.js @@ -1,9 +1,11 @@ const app = require(`${servicesDir}/ticket/server/server`); 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 result = await app.models.Ticket.filter(filter); + let result = await app.models.Ticket.filter(ctx, filter); let ticketId = result[0].id; expect(ticketId).toEqual(15); diff --git a/services/loopback/common/models/vn-model.js b/services/loopback/common/models/vn-model.js index 65b6a139c..e5c15be8a 100644 --- a/services/loopback/common/models/vn-model.js +++ b/services/loopback/common/models/vn-model.js @@ -5,6 +5,10 @@ const UserError = require('../helpers').UserError; module.exports = function(Self) { 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.super_.setup.call(this); @@ -93,7 +97,7 @@ module.exports = function(Self) { }; Self.remoteMethodCtx = function(methodName, args) { - var ctx = { + let ctx = { arg: 'context', type: 'object', http: function(ctx) { @@ -131,9 +135,9 @@ module.exports = function(Self) { let options = {transaction: transaction}; try { - if (actions.delete && actions.delete.length) { + if (actions.delete && actions.delete.length) await this.destroyAll({id: {inq: actions.delete}}, options); - } + if (actions.update) { try { 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) { let userId = ctx.req.accessToken.userId; let models = this.app.models; @@ -294,9 +173,8 @@ module.exports = function(Self) { function modifiedProperties(data) { let properties = []; - for (property in data) { + for (property in data) properties.push(property); - } return properties; } @@ -350,10 +228,38 @@ module.exports = function(Self) { return this.checkAcls(ctx, 'insert'); }; - // Action bindings - require('../methods/vn-model/validateBinded')(Self); - // Handle MySql errors - require('../methods/vn-model/rewriteDbError')(Self); - // Get table set of values - require('../methods/vn-model/getSetValues')(Self); + /* + * 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); + }; }; diff --git a/services/loopback/server/connectors/vn-mysql.js b/services/loopback/server/connectors/vn-mysql.js index ead44653c..046866ede 100644 --- a/services/loopback/server/connectors/vn-mysql.js +++ b/services/loopback/server/connectors/vn-mysql.js @@ -1,19 +1,235 @@ -var mysql = require('mysql'); +const mysql = require('mysql'); const loopbackConnector = require('loopback-connector'); const SqlConnector = loopbackConnector.SqlConnector; const ParameterizedSQL = loopbackConnector.ParameterizedSQL; -var MySQL = require('loopback-connector-mysql').MySQL; -var EnumFactory = require('loopback-connector-mysql').EnumFactory; -var debug = require('debug')('loopback-connector-sql'); +const MySQL = require('loopback-connector-mysql').MySQL; +const EnumFactory = require('loopback-connector-mysql').EnumFactory; +const fs = require('fs'); -exports.initialize = function(dataSource, callback) { +class VnMySQL extends MySQL { + constructor(settings) { + super(); + SqlConnector.call(this, 'mysql', settings); + } + + toColumnValue(prop, val) { + if (val == null || !prop || prop.type !== Date) + return MySQL.prototype.toColumnValue.call(this, prop, val); + + val = new Date(val); + + return val.getFullYear() + '-' + + fillZeros(val.getMonth() + 1) + '-' + + fillZeros(val.getDate()) + ' ' + + fillZeros(val.getHours()) + ':' + + fillZeros(val.getMinutes()) + ':' + + fillZeros(val.getSeconds()); + + function fillZeros(v) { + return v < 10 ? '0' + v : v; + } + } + + /** + * Promisified version of execute(). + * + * @param {String} query The SQL query string + * @param {Array} params The query parameters + * @param {Object} options The loopback options + * @param {Function} cb The callback + * @return {Promise} The operation promise + */ + executeP(query, params, options = {}, cb) { + return new Promise((resolve, reject) => { + this.execute(query, params, options, (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 + */ + 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 + * @return {ParameterizedSQL} The SQL WHERE clause + */ + makeWhere(where) { + let wrappedConnector = Object.create(this); + Object.assign(wrappedConnector, { + getModelDefinition() { + return { + 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; - var modelBuilder = dataSource.modelBuilder; - var defineType = modelBuilder.defineValueType ? + const modelBuilder = dataSource.modelBuilder; + const defineType = modelBuilder.defineValueType ? modelBuilder.defineValueType.bind(modelBuilder) : modelBuilder.constructor.registerType.bind(modelBuilder.constructor); @@ -26,170 +242,7 @@ exports.initialize = function(dataSource, callback) { process.nextTick(function() { callback(); }); - } else { + } else dataSource.connector.connect(callback); - } } }; - -exports.VnMySQL = VnMySQL; - -function VnMySQL(settings) { - SqlConnector.call(this, 'mysql', settings); -} - -VnMySQL.prototype = Object.create(MySQL.prototype); -VnMySQL.constructor = VnMySQL; - -VnMySQL.prototype.toColumnValue = function(prop, val) { - if (val == null || !prop || prop.type !== Date) - return MySQL.prototype.toColumnValue.call(this, prop, val); - - val = new Date(val); - - return val.getFullYear() + '-' + - fillZeros(val.getMonth() + 1) + '-' + - fillZeros(val.getDate()) + ' ' + - fillZeros(val.getHours()) + ':' + - fillZeros(val.getMinutes()) + ':' + - fillZeros(val.getSeconds()); - - function fillZeros(v) { - return v < 10 ? '0' + v : v; - } -}; - -/** - * Private make method - * @param {Object} model Model instance - * @param {Object} where Where filter - * @return {ParameterizedSQL} Parametized object - */ -VnMySQL.prototype._makeWhere = function(model, where) { - let columnValue; - let sqlExp; - - if (!where) { - return new ParameterizedSQL(''); - } - if (typeof where !== 'object' || Array.isArray(where)) { - debug('Invalid value for where: %j', where); - 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 - * @param {string} model Model name - * @param {object} where An object for the where conditions - * @return {ParameterizedSQL} The SQL WHERE clause - */ -VnMySQL.prototype.makeWhere = function(model, where) { - var whereClause = this._makeWhere(model, where); - if (whereClause.sql) { - whereClause.sql = 'WHERE ' + whereClause.sql; - } - return whereClause; -}; diff --git a/services/ticket/common/methods/sale-tracking/listSaleTracking.js b/services/ticket/common/methods/sale-tracking/listSaleTracking.js index ff198f3ad..9a6b024e1 100644 --- a/services/ticket/common/methods/sale-tracking/listSaleTracking.js +++ b/services/ticket/common/methods/sale-tracking/listSaleTracking.js @@ -8,12 +8,10 @@ module.exports = Self => { accepts: [{ arg: 'filter', type: 'Object', - required: false, - description: 'Filter defining where and paginated data', - http: {source: 'query'} + description: 'Filter defining where and paginated data' }], returns: { - type: ["Object"], + type: ['Object'], root: true }, http: { @@ -41,7 +39,7 @@ module.exports = Self => { JOIN worker w ON w.id = st.workerFk JOIN state ste ON ste.id = st.stateFk`); - stmt.merge(Self.buildSuffix(filter)); + stmt.merge(Self.makeSuffix(filter)); let trackings = await Self.rawStmt(stmt); diff --git a/services/ticket/common/methods/sale-tracking/specs/listSaleTracking.spec.js b/services/ticket/common/methods/sale-tracking/specs/listSaleTracking.spec.js index d2179d2cc..a40f5b7b5 100644 --- a/services/ticket/common/methods/sale-tracking/specs/listSaleTracking.spec.js +++ b/services/ticket/common/methods/sale-tracking/specs/listSaleTracking.spec.js @@ -1,14 +1,14 @@ const app = require(`${servicesDir}/ticket/server/server`); describe('ticket listSaleTracking()', () => { - it('should call the listSaleTracking method and return the response', async() => { + it('should call the listSaleTracking method and return the response', async () => { let filter = {where: {ticketFk: 1}}; let result = await app.models.SaleTracking.listSaleTracking(filter); expect(result[0].concept).toEqual('Gem of Time'); }); - it(`should call the listSaleTracking method and return zero if doesn't have lines`, async() => { + it(`should call the listSaleTracking method and return zero if doesn't have lines`, async () => { let filter = {where: {ticketFk: 2}}; let result = await app.models.SaleTracking.listSaleTracking(filter);