diff --git a/.gitignore b/.gitignore index 73ebd01968826de5bb4291d1bcd9d54c64e1d6e8..560276d02d4a8d66d92a8cbdfae9b049dc7314b6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ .env docker-compose.yaml .editorconfig +package-lock.json +*~ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fdf4292066895a8eb033a0321249efb3e7869930..610933e85ae52dcb4a6c1a89fc063043fd2d0ec7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,23 +24,30 @@ stages: run_test: stage: test - before_script: - - apt-get update -q -y - - apt-get install wget gnupg -y - - echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" > /etc/apt/sources.list.d/pgdg.list - - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - - - apt-get update -q -y - - apt-get install -y postgresql-client-10 - - git clone --recurse-submodules https://gitlab.c3sl.ufpr.br/simmctic/form-creator/form-creator-database.git form-creator-database - - cd form-creator-database - - psql-manager/manager.sh create workspace - - psql-manager/manager.sh fixture workspace - - cd .. - - rm -rf form-creator-database script: + - apt-get update -q -y + - apt-get install wget gnupg -y + - echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" > /etc/apt/sources.list.d/pgdg.list + - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - + - apt-get update -q -y + - apt-get install -y postgresql-client-10 + - git clone --recurse-submodules https://gitlab.c3sl.ufpr.br/simmctic/form-creator/form-creator-database.git form-creator-database + - cd form-creator-database + - psql-manager/manager.sh create workspace + - psql-manager/manager.sh fixture workspace + - cd .. + - rm -rf form-creator-database - yarn install --frozen-lockfile --silent --non-interactive - ln -s config.env.example config/test.env - yarn test + tags: + - node + +run_lint: + stage: test + script: + - yarn install --frozen-lockfile --silent --non-interactive + - ln -s config.env.example config/test.env - yarn run lint tags: - node diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f04653bcfc2a735badcc78f03337f20f3ddbc6..d16cd2784fcb55e9dcb8b1c5e24fc019982655de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,77 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.1.0 - 11-10-2019 +### Changed +- Create a stable version + +## 1.0.10 - 10-10-2019 +### Added +- Validation type DEPENDENCY #41 (Gianfranco) +### Changed +- ValidateInput method to receive a vector of inputs + + +## 1.0.9 - 09-10-2019 +### Added +- Validation type MAXANSWERS #38 (Gianfranco) + + +## 1.0.8 - 08-10-2019 +### Added +- Validation type SOMECHECKBOX #39 (Gianfranco) +### Changed +- ValidateInput method to receive a vector of input answers + + +## 1.0.7 - 01-10-2019 +### Added +- Validation type TYPEOF #37 (Gianfranco) + + +## 1.0.6 - 30-09-2019 +### Added +- QueryBuilder Class #47 (Gianfranco) +- FormQueryBuilder Class #47 +- AnswerQueryBuilder Class #47 +### Changed +- DbHandler to only have database connections + + +## 1.0.5 - 27-09-2019 +### Added +- Input type Select #36 (Gianfranco) + + +## 1.0.4 - 26-09-2019 +### Added +- Input type Radio #35 (Gianfranco) + + +## 1.0.3 - 24-09-2019 +### Changed +- Refactor DbHandler #46 (Gianfranco) +- Fix api routes +- Set false to max-classes-per-file on tslint + + +## 1.0.2 - 27-08-2019 +### Added +- Input type Checkbox #34 (Gianfranco) +- Sugestions for input answers +### Changed +- OptHandler to validate Sugestions +- DbHandler to insert Sugestions on database + + +## 1.0.1 - 20-08-2019 +### Added +- DbHandler methods to update form table #42 (Gianfranco) +### Changed +- FormUpdate to receive a options changed +- DiffHandler to recognize changes on forms + + ## 1.0.0 - 19-08-2019 ### Changed - Create a stable version diff --git a/package.json b/package.json index eef8fd87e5eea05c61f639a7607921047210ea5e..a7c3995a0af590232a2143781af3171f85b0c8b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "form-creator-api", - "version": "1.0.0", + "version": "1.1.0", "description": "RESTful API used to manage and answer forms.", "main": "index.js", "scripts": { diff --git a/src/api/controllers/form.spec.ts b/src/api/controllers/form.spec.ts index ef1e8a001543d3c174f087d78409ea5ab57b582a..99653d75a22c50f720d5d15960f35595b6a685b8 100644 --- a/src/api/controllers/form.spec.ts +++ b/src/api/controllers/form.spec.ts @@ -31,7 +31,7 @@ import { Form, FormOptions } from "../../core/form"; import { FormUpdate, FormUpdateOptions } from "../../core/formUpdate"; import { Input, InputOptions, Validation } from "../../core/input"; import { InputUpdate, InputUpdateOptions } from "../../core/inputUpdate"; -import { DbHandler, QueryOptions } from "../../utils/dbHandler"; +import { DbHandler } from "../../utils/dbHandler"; import { configs } from "../../utils/config"; describe("API data controller", () => { @@ -316,8 +316,12 @@ describe("API data controller", () => { , description: 'Description Question 4 Form 3' , question: 'Question 4 Form 3' , enabled: true - , type: InputType.TEXT + , type: InputType.CHECKBOX , validation: [] + , sugestions: [ + { value: "Sugestion 1", placement: 0 } + , { value: "Sugestion 2", placement: 1 } + ] , id: undefined } , { @@ -415,6 +419,116 @@ describe("API data controller", () => { .end(done); }); + it("should respond 200 when putting valid form update changing the title", (done) => { + request(server) + .put("/form/4") + .send({ + id: 4 + , title: 'Title 4' + , description: 'Description 4' + , inputs: [ + { + placement: 0 + , description: 'Description Question 1 Form 4' + , question: 'Question 1 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 8 + } + , { + placement: 1 + , description: 'Description Question 2 Form 4' + , question: 'Question 2 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 9 + } + , { + placement: 2 + , description: 'Description Question 3 Form 4' + , question: 'Question 3 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 10 + } + ] + }) + + .expect(200) + .expect((res: any) => { + const message = "Updated" + expect(res.body.message).to.be.equal(message); + }) + + .end(done); + }); + + it("should respond 200 when putting valid form update undo changes", (done) => { + request(server) + .put("/form/4") + .send({ + id: 4 + , title: 'Form Title 4' + , description: 'Form Description 4' + , inputs: [ + { + placement: 0 + , description: 'Description Question 1 Form 4' + , question: 'Question 1 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 8 + } + , { + placement: 1 + , description: 'Description Question 2 Form 4' + , question: 'Question 2 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 9 + } + , { + placement: 2 + , description: 'Description Question 3 Form 4' + , question: 'Question 3 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 10 + } + ] + }) + + .expect(200) + .expect((res: any) => { + const message = "Updated" + expect(res.body.message).to.be.equal(message); + }) + + .end(done); + }); + it("should respond 500 when putting a valid form update for an inexistent form", (done) => { request(server) .put("/form/10") diff --git a/src/api/controllers/form.ts b/src/api/controllers/form.ts index df60bc1fcbbac3d04d17f08aad90c19b79f504a4..6e40357c03237def4bb4171ead70132834bb10be 100644 --- a/src/api/controllers/form.ts +++ b/src/api/controllers/form.ts @@ -31,7 +31,7 @@ export class FormCtrl { public static list(req: Request, res: Response, next: NextFunction) { - req.db.listForms((err: Error, forms?: Form[]) => { + req.db.form.list((err: Error, forms?: Form[]) => { if (err){ res.status(500).json({ message: "Could not list forms. Some error has occurred. Check error property for details.", @@ -39,22 +39,20 @@ export class FormCtrl { }); return; } - else{ - const mappedForms = forms.map(form => ({ - id: form.id, - title: form.title, - description: form.description - })); - res.json(mappedForms); - return; - } + const mappedForms = forms.map(form => ({ + id: form.id + , title: form.title + , description: form.description + })); + res.json(mappedForms); + return; }); - } public static read(req: Request, res: Response, next: NextFunction) { - req.db.readForm(req.params.id, (err: Error, form?: Form) => { + + req.db.form.read(req.params.id, (err: Error, form?: Form) => { if (err){ res.status(500).json({ message: "Form with id: '" + req.params.id + "' not found. Some error has occurred. Check error property for details.", @@ -62,10 +60,9 @@ export class FormCtrl { }); return; } - else{ - res.json(form); - return; - } + + res.json(form); + return; }); } @@ -83,43 +80,43 @@ export class FormCtrl { } waterfall ([ (callback: (err: Error, result?: FormUpdate) => void) => { - req.db.writeForm(form, (err: Error, formResult: Form) => { + req.db.form.write(form, (err: Error, formResult: Form) => { if (err) { callback(err); + return; } - else { - const formOpts: FormOptions = { - id: formResult.id - , title: formResult.title - , description: formResult.description - , inputs: [] - }; - const formUpdate: FormUpdate = DiffHandler.diff(formResult, new Form (formOpts)); - - callback(null, formUpdate); - } + const formOpts: FormOptions = { + id: formResult.id + , title: formResult.title + , description: formResult.description + , inputs: [] + }; + const formUpdate: FormUpdate = DiffHandler.diff(formResult, new Form (formOpts)); + + callback(null, formUpdate); }); }, (formUpdate: FormUpdate, callback: (err: Error, formId: number) => void) => { - req.db.updateForm(formUpdate, (err: Error) => { + req.db.form.update(formUpdate, (err: Error) => { callback(err, formUpdate.form.id); }); } ], (err, resultId) => { if (err) { res.status(500).json({ - message: "Could not insert form. Some error has occurred. Check error property for details.", - error: err.message - }); - } - else { - res.json({ - id: resultId - , message: "Form added. Id on key 'id'" + message: "Could not insert form. Some error has occurred. Check error property for details." + , error: err.message }); + return; } + + res.json({ + id: resultId + , message: "Form added. Id on key 'id'" + }); return; }); + } public static update(req: Request, res: Response, next: NextFunction) { @@ -129,48 +126,36 @@ export class FormCtrl { newForm = new Form(OptHandler.form(req.body)); } catch(e) { res.status(500).json({ - message: "Invalid Form. Check error property for details.", - error: e.message + message: "Invalid Form. Check error property for details." + , error: e.message }); return; } waterfall([ (callback: (err: Error, result?: FormUpdate) => void) => { - req.db.readForm(req.params.id, (err: Error, oldForm: Form) => { + req.db.form.read(req.params.id, (err: Error, oldForm: Form) => { if (err) { callback(err); + return; } - else { - const formUpdate: FormUpdate = DiffHandler.diff(newForm, oldForm); - callback(null, formUpdate); - } + + const formUpdate: FormUpdate = DiffHandler.diff(newForm, oldForm); + + callback(null, formUpdate); }); }, (formUpdate: FormUpdate, callback: (err: Error, formUpdateResult?: FormUpdate) => void) => { - req.db.updateDatabase(formUpdate, (err: Error, formUpdateResult: FormUpdate) => { - if (err) { - callback(err); - } - callback(null,formUpdateResult); - }); - }, - (formUpdate: FormUpdate, callback: (err: Error) => void) => { - req.db.updateForm(formUpdate, (err: Error) => { - callback(err); - }); + req.db.form.update(formUpdate, callback); } ], (err) => { if (err) { res.status(500).json({ - message: "Could not update Form. Some error has ocurred. Check error property for details.", - error: err.message - }); - } - else { - res.json({ - message: "Updated" + message: "Could not update Form. Some error has ocurred. Check error property for details." + , error: err.message }); + return; } + res.json({ message: "Updated" }); return; }); } diff --git a/src/api/controllers/formAnswer.spec.ts b/src/api/controllers/formAnswer.spec.ts index c88b2f0e621e89745952d51bb65dd9737ff83b56..cda55f53579940805607b369325fa8954da09416 100644 --- a/src/api/controllers/formAnswer.spec.ts +++ b/src/api/controllers/formAnswer.spec.ts @@ -19,54 +19,72 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - import * as request from "supertest"; - import { expect } from "chai"; - import * as server from "../../main"; - import { EnumHandler,InputType, ValidationType } from "../../utils/enumHandler"; - import { TestHandler } from "../../utils/testHandler"; - import { OptHandler } from "../../utils/optHandler"; - import { Form, FormOptions } from "../../core/form"; - import { Input, InputOptions, Validation } from "../../core/input"; +import * as request from "supertest"; +import { expect } from "chai"; +import * as server from "../../main"; +import { EnumHandler,InputType, ValidationType } from "../../utils/enumHandler"; +import { TestHandler } from "../../utils/testHandler"; +import { OptHandler } from "../../utils/optHandler"; +import { Form, FormOptions } from "../../core/form"; +import { Input, InputOptions, Validation } from "../../core/input"; - describe("API data controller", () => { +describe("API data controller", () => { - it("should respond 200 when posting valid form Answer", (done) => { + it("should respond 200 when posting valid form Answer", (done) => { - request(server) - .post("/answer/1") - .send({ - 1:["Answer to Question 1 Form 1"] - , 2:["12345-000"] - , 3:["MAXCHAR 10"] - }) - .expect(200) - .expect((res: any) => { - expect(res.body.id).to.be.equal(7); - expect(res.body.message).to.be.equal("Answered"); - }) - .end(done); - }); + request(server) + .post("/answer/1") + .send({ + 1:["Answer to Question 1 Form 1"] + , 2:["12345-000"] + , 3:["MAXCHAR 10"] + }) + .expect(200) + .expect((res: any) => { + expect(res.body.id).to.be.equal(7); + expect(res.body.message).to.be.equal("Answered"); + }) + .end(done); + }); - it("should respond 500 when posting invalid form Answer", (done) => { + it("should respond 500 when posting invalid form Answer", (done) => { - request(server) - .post("/answer/1") - .send({ - 1:["Answer to Question 1 Form 1"] - , 2:["12a345-000"] - , 3:["MAXCHAR 10 AND MORE"] - }) - .expect(500) - .expect((res: any) => { - const message = "Could not Create form Answer. Some error has occurred. Check error property for details."; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("error"); - expect(res.body.message).to.be.equal(message); - expect(res.body.error["2"]).to.be.equal("RegEx do not match"); - expect(res.body.error["3"]).to.be.equal("Input answer must be lower than 10"); - }) - .end(done); - }); + request(server) + .post("/answer/1") + .send({ + 1:["Answer to Question 1 Form 1"] + , 2:["12a345-000"] + , 3:["MAXCHAR 10 AND MORE"] + }) + .expect(500) + .expect((res: any) => { + const message = "Could not Create form Answer. Some error has occurred. Check error property for details."; + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("error"); + expect(res.body.message).to.be.equal(message); + expect(res.body.error["2"]).to.be.equal("RegEx do not match"); + expect(res.body.error["3"]).to.be.equal("Input answer must be lower than 10"); + }) + .end(done); + }); - }); + it("should respond 500 when posting valid form Answer for a invalid Form", (done) => { + + request(server) + .post("/answer/10") + .send({ + 1:["Answer to Question 1 Form 1"] + , 2:["12a345-000"] + , 3:["MAXCHAR 10 AND MORE"] + }) + .expect(500) + .expect((res: any) => { + const message = "Form with id: '10' not found. Some error has occurred. Check error property for details."; + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("error"); + expect(res.body.message).to.be.equal(message); + }) + .end(done); + }); +}); diff --git a/src/api/controllers/formAnswer.ts b/src/api/controllers/formAnswer.ts index 9549a66dd1c643f57f29b9e6e0f636c6b89658e1..226068460d301a8519e42ded9deab3460bbfacd5 100644 --- a/src/api/controllers/formAnswer.ts +++ b/src/api/controllers/formAnswer.ts @@ -31,72 +31,64 @@ export class AnswerCtrl { public static write(req: Request, res: Response, next: NextFunction) { - req.db.readForm(req.params.id, (err: Error, form?: Form) => { + req.db.form.read(req.params.id, (err: Error, form?: Form) => { if (err) { res.status(500).json({ - message: "Form with id: '" + req.params.id + "' not found. Some error has occurred. Check error property for details.", - error: err + message: "Form with id: '" + req.params.id + "' not found. Some error has occurred. Check error property for details." + , error: err }); return; } - else { - let inputAnswerOptionsDict: InputAnswerOptionsDict = {} + + let inputAnswerOptionsDict: InputAnswerOptionsDict = {} - for (const key of Object.keys(req.body)) { - inputAnswerOptionsDict[parseInt(key, 10)] = []; - for (const i in req.body[key]) { - const tmpInputAnswerOption: InputAnswerOptions = { - idInput: parseInt(key, 10) - , placement: parseInt(i, 10) - , value: req.body[key][i] - } - inputAnswerOptionsDict[parseInt(key, 10)].push(tmpInputAnswerOption); + for (const key of Object.keys(req.body)) { + inputAnswerOptionsDict[parseInt(key, 10)] = []; + for (const i in req.body[key]) { + const tmpInputAnswerOption: InputAnswerOptions = { + idInput: parseInt(key, 10) + , placement: parseInt(i, 10) + , value: req.body[key][i] } + inputAnswerOptionsDict[parseInt(key, 10)].push(tmpInputAnswerOption); } + } + let formAnswerOpt: FormAnswerOptions = { + form: form + , timestamp: new Date(Date.now()) + , inputsAnswerOptions: inputAnswerOptionsDict + } - let formAnswerOpt: FormAnswerOptions = { - form: form - , timestamp: new Date(Date.now()) - , inputsAnswerOptions: inputAnswerOptionsDict - } - - try{ - const formAnswer: FormAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOpt)); - ValidationHandler.validateFormAnswer(formAnswer); - req.db.writeFormAnswer(formAnswer, (err: Error, formAnswerResult: FormAnswer) => { - if (err){ - throw err; - } - else{ - res.json({ - id: formAnswerResult.id - , message: "Answered" - }); - return; - } + try { + const formAnswer: FormAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOpt)); + ValidationHandler.validateFormAnswer(formAnswer); + req.db.answer.write(formAnswer, (err: Error, formAnswerResult: FormAnswer) => { + if (err){ + throw err; + return; + } + res.json({ + id: formAnswerResult.id + , message: "Answered" }); - } - catch (e) { - - if( e.validationDict !== undefined){ - res.status(500).json({ - message: "Could not Create form Answer. Some error has occurred. Check error property for details." - , error: e.validationDict - }); - }else{ - res.status(500).json({ - message: "Could not Create form Answer. Some error has occurred. Check error property for details." - , error: e.message - }); - } - return - } + return; + }); + } catch (e) { + if (e.validationDict !== undefined) { + res.status(500).json({ + message: "Could not Create form Answer. Some error has occurred. Check error property for details." + , error: e.validationDict + }); + return; + } + res.status(500).json({ + message: "Could not Create form Answer. Some error has occurred. Check error property for details." + , error: e.message + }); + return; } }); - } - - } diff --git a/src/core/formUpdate.ts b/src/core/formUpdate.ts index 5ee927dce8b5cbe52e8052a7df0c84742653477f..4091c307dcf511c3ba73e0f741d351effbcf0ab7 100644 --- a/src/core/formUpdate.ts +++ b/src/core/formUpdate.ts @@ -26,10 +26,12 @@ import { InputUpdate, InputUpdateOptions } from "./inputUpdate"; export interface FormUpdateOptions { /** Unique identifier of a FormUpdate instance. */ id?: number; - /** Changed Form. */ + /** Changed Form. */ form: FormOptions; /** Date which form was updated. */ updateDate: Date; + /** True when Form title or description has changed. */ + changed?: boolean; /** Array of InputUpdate containing changes on inputs. */ inputUpdates: InputUpdateOptions[]; } @@ -44,6 +46,8 @@ export class FormUpdate { public readonly form: Form; /** Date which form was updated. */ public readonly updateDate: Date; + /** True when Form title or description has changed. */ + public readonly changed?: boolean; /** Array of InputUpdate containing changes on inputs. */ public readonly inputUpdates: InputUpdate[]; /** @@ -54,6 +58,7 @@ export class FormUpdate { this.id = options.id ? options.id : null; this.form = new Form (options.form); this.updateDate = options.updateDate; + this.changed = options.changed ? options.changed : false; this.inputUpdates = options.inputUpdates.map((i: any) => { return new InputUpdate(i); }); diff --git a/src/core/input.ts b/src/core/input.ts index f7097aa46aede4916908aa5a513db39bb8b9c5fa..c199c378fe37c9c3d11af2780f71fd861b3de3b1 100644 --- a/src/core/input.ts +++ b/src/core/input.ts @@ -24,19 +24,21 @@ import { InputType, ValidationType } from "../utils/enumHandler"; /** Parameters used to create a input object. */ export interface InputOptions { /** Unique identifier of a Input instance. */ - id?: number; + id?: number; /** Place where input should be in the form. */ - placement: number; + placement: number; /** Input's Description */ - description: string; + description: string; /** Question of input */ - question: string; + question: string; /** Enabled/Disable input */ - enabled?: boolean; + enabled?: boolean; /** Type of input */ - type: InputType; + type: InputType; /** Array contain all input's validation */ - validation: Validation[]; + validation: Validation[]; + /** Question answer sugestion */ + sugestions?: Sugestion[]; } /** Validation contains the type of it, and n arguments to validate if necessary */ export interface Validation { @@ -47,10 +49,17 @@ export interface Validation { arguments: string[]; } +/** Sugestion for answers */ +export interface Sugestion { + /** Answer's sugestion for a input */ + value: string; + /** Place where sugestion should be in the input */ + placement: number; +} + /** * Input Class to manage project's inputs forms */ - export class Input { /** Unique identifier of a Input instance. */ public readonly id: number; @@ -66,6 +75,8 @@ export class Input { public readonly type: InputType; /** Array contain all input's validation */ public readonly validation: Validation[]; + /** Question answer sugestions */ + public readonly sugestions?: Sugestion[]; /** * Creates a new instance of Input Class @@ -83,7 +94,12 @@ export class Input { this.enabled = options.enabled; } this.type = options.type; + if (options.sugestions) { + this.sugestions = options.sugestions.map((i: any) => i); + } + else { + this.sugestions = []; + } this.validation = options.validation; } - } diff --git a/src/utils/answerQueryBuilder.ts b/src/utils/answerQueryBuilder.ts new file mode 100644 index 0000000000000000000000000000000000000000..8363e93f4723ba35cdabcd37e51fcb2d985881ce --- /dev/null +++ b/src/utils/answerQueryBuilder.ts @@ -0,0 +1,357 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { eachSeries, map, waterfall } from "async"; +import { Pool, PoolConfig, QueryResult } from "pg"; +import { Form, FormOptions } from "../core/form"; +import { FormAnswer, FormAnswerOptions } from "../core/formAnswer"; +import { Input, InputOptions } from "../core/input"; +import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; +import { EnumHandler, InputType } from "./enumHandler"; +import { ErrorHandler} from "./errorHandler"; +import { FormQueryBuilder } from "./formQueryBuilder"; +import { OptHandler } from "./optHandler"; +import { QueryBuilder, QueryOptions } from "./queryBuilder"; +import { Sorter } from "./sorter"; +/** + * Class used to manage all the Answer operations into the database. + * This operations include read and write data. + */ +export class AnswerQueryBuilder extends QueryBuilder { + + private formQueryBuilder: FormQueryBuilder; + + constructor(builder: FormQueryBuilder, pool: Pool) { + super(pool); + this.formQueryBuilder = builder; + } + + /** + * Asynchronously write a Answer on database. + * @param formAnswer - FormAnswer to be inserted. + * @param cb - Callback function which contains the inserted data. + * @param cb.err - Error information when the method fails. + * @param cb.form - FormAnswer or null if any error occurs. + */ + public write(formAnswer: FormAnswer, cb: (err: Error, result?: FormAnswer) => void) { + + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin((error: Error, results?: QueryResult) => { + callback(error); + }); + }, + + (callback: (err: Error, result?: number) => void) => { + this.writeController(formAnswer, (error: Error, resultAnswerId?: number) => { + callback(error, resultAnswerId); + }); + }, + + (formAnswerId: number, callback: (err: Error, result?: number) => void) => { + this.commit((error: Error, results?: QueryResult) => { + callback(error, formAnswerId); + }); + }, + (formAnswerId: number, callback: (err: Error, result?: FormAnswer) => void) => { + this.read(formAnswerId, (error: Error, formAnswerResult?: FormAnswer) => { + callback(error, formAnswerResult); + }); + } + + ], (err, formAnswerResult?: FormAnswer) => { + if (err) { + this.rollback((error: Error, results?: QueryResult) => { + cb(err); + }); + return; + } + cb(null, formAnswerResult); + }); + } + + /** + * Asynchronously write a Answer on database without transactions. + * @param formAnswer - FormAnswer identifier to be inserted. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - FormAnswer identifier or null if any error occurs. + */ + private writeController(formAnswer: FormAnswer, cb: (err: Error, result?: number) => void) { + waterfall([ + (callback: (err: Error, result?: any) => void) => { + this.executeWriteForm(formAnswer, (error: Error, resultId?: number) => { + if (error) { + callback(error); + return; + } + callback(null, resultId); + }); + }, + (formAnswerId: number, callback: (err: Error, result?: any) => void) => { + eachSeries(Object.keys(formAnswer.inputAnswers), (key, outerCallback) => { + eachSeries(formAnswer.inputAnswers[parseInt(key, 10)], (inputsAnswer, innerCallback) => { + this.executeWriteInput(formAnswerId, inputsAnswer, innerCallback); + }, (error) => { + outerCallback(error); + }); + }, (err, id?: number) => { + callback(err, formAnswerId); + }); + } + ], (err, id?: number) => { + cb(err, id); + }); + } + + /** + * Asynchronously insert a FormAnswer on database. + * @param formAnswer - FormAnswer to be inserted. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + * @param cb.result - FormAnswer identifier or null if any error occurs. + */ + private executeWriteForm(formAnswer: FormAnswer, cb: (err: Error, result?: number) => void) { + const queryString: string = "INSERT INTO form_answer (id_form, answered_at) \ + VALUES( $1, $2 ) \ + RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + formAnswer.form.id + , formAnswer.timestamp + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err){ + cb(err); + return; + } + + if (result.rowCount !== 1){ + cb(ErrorHandler.notInserted("FormAnswer")); + return; + } + cb(null, result.rows[0]["id"]); + }); + } + + /** + * Asynchronously insert a inputAnswer on database. + * @param formAnswerId - Indentifier to relate with InputAnswer. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private executeWriteInput(formAnswerId: number, inputAnswer: InputAnswer, cb: (err: Error) => void) { + const queryString: string = "INSERT INTO input_answer (id_form_answer, id_input, value, placement) \ + VALUES ( $1, $2, $3, $4) \ + RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + formAnswerId + , inputAnswer.idInput + , inputAnswer.value + , inputAnswer.placement + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err){ + cb(err); + return; + } + + if (result.rowCount !== 1){ + cb(ErrorHandler.notInserted("InputsAnswer")); + return; + } + cb(null); + }); + } + + /** + * Asynchronously read a FormAnswer from database. + * @param formAnswerId - FormAnswer identifier to be founded. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.formAnswers - FormAnswer object or null if form not exists. + */ + public read(formAnswerId: number, cb: (err: Error, formAnswers?: FormAnswer) => void) { + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin((error: Error, results?: QueryResult) => { + callback(error); + }); + }, + (callback: (err: Error, result?: FormAnswer) => void) => { + this.readController(formAnswerId, (error: Error, resultAnswer?: FormAnswer) => { + callback(error, resultAnswer); + }); + }, + (formAnswer: FormAnswer, callback: (err: Error, result?: FormAnswer) => void) => { + this.commit((error: Error, results?: QueryResult) => { + callback(error, formAnswer); + }); + }, + (formAnswer: FormAnswer, callback: (err: Error, result?: FormAnswer) => void) => { + this.formQueryBuilder.read(formAnswer.form.id, (error: Error, resultForm?: Form) => { + const formAnswerObj: FormAnswer = { + id: formAnswer.id + , form: resultForm + , timestamp: formAnswer.timestamp + , inputAnswers: formAnswer.inputAnswers + }; + callback(error, formAnswerObj); + }); + } + ], (err, formAnswer?: FormAnswer) => { + if (err) { + this.rollback((error: Error, results?: QueryResult) => { + cb(err); + }); + return; + } + cb(null, formAnswer); + }); + } + + /** + * Asynchronously read a FormAnswer from database without transaction. + * @param formAnswerId - FormAnswer identifier to be founded. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.result - FormAnswer object or null if FormAnswer not exists. + */ + private readController(formAnswerId: number, cb: (err: Error, result?: FormAnswer) => void) { + waterfall([ + (callback: (err: Error, result?: InputAnswerOptionsDict) => void) => { + this.executeReadInput(formAnswerId, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + const inputAnswersOpts: InputAnswerOptions[] = result.rows.map((inputsAnswerResult) => { + const inputAnswersOpt: InputAnswerOptions = { + id: inputsAnswerResult["id"] + , idInput: inputsAnswerResult["id_input"] + , value: inputsAnswerResult["value"] + , placement: inputsAnswerResult["placement"] + }; + return OptHandler.inputAnswer(inputAnswersOpt); + }); + + const inputsAnswerResults: InputAnswerOptionsDict = {}; + for (const i of inputAnswersOpts){ + if (inputsAnswerResults[i["idInput"]]) { + inputsAnswerResults[i["idInput"]].push(i); + inputsAnswerResults[i["idInput"]] = Sorter.sortByPlacement(inputsAnswerResults[i["idInput"]]); + } else { + inputsAnswerResults[i["idInput"]] = [i]; + } + } + + callback(err, inputsAnswerResults); + }); + }, + (inputsAnswerResults: InputAnswerOptionsDict, callback: (err: Error, result?: FormAnswer) => void) => { + this.executeReadForm(formAnswerId, (error: Error, answerResult?: any) => { + if (error) { + callback(error); + return; + } + const formTmp: Form = { + id: answerResult.id_form + , title: undefined + , description: undefined + , inputs: [] + }; + const formAnswerTmp: FormAnswerOptions = { + id: answerResult.id + , form: formTmp + , timestamp: answerResult.answered_at + , inputsAnswerOptions: inputsAnswerResults + }; + + callback(null, new FormAnswer(formAnswerTmp)); + }); + } + ], (err, formAnswer: FormAnswer) => { + if (err) { + cb(err); + return; + } + cb(null, formAnswer); + }); + } + + /** + * Asynchronously read a formAnswer from database. + * @param formUpdate - FormAnswer identifier to be founded. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + * @param cb.result - The read FormAnswer result query row. + */ + private executeReadForm(formAnswerId: number, cb: (err: Error, result?: QueryResult) => void) { + const queryString: string = "SELECT id, id_form, answered_at \ + FROM form_answer \ + WHERE id=$1;"; + const query: QueryOptions = { + query: queryString + , parameters: [formAnswerId] + }; + + this.executeQuery(query, (error: Error, result?: QueryResult) => { + if (result.rowCount !== 1) { + cb(ErrorHandler.badIdAmount(result.rowCount)); + return; + } + + cb(error, result.rows[0]); + }); + } + + /** + * Asynchronously read a InputAnswer from database. + * @param formAnswerId - Identifier to read InputAnswers. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + * @param cb.result - The read InputAnswer result query. + */ + private executeReadInput(formAnswerId: number, cb: (err: Error, result?: QueryResult) => void) { + const queryString: string = "SELECT id, id_form_answer, id_input, value, placement \ + FROM input_answer \ + WHERE id_form_answer=$1;"; + const query: QueryOptions = { + query: queryString + , parameters: [formAnswerId] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null, result); + }); + } +} diff --git a/src/utils/dbHandler.spec.ts b/src/utils/dbHandler.spec.ts index 441b866e4f6187196bf6cbde6d578f0a12a32486..d9301a62bde28b94dd7fe579d1bd3e272f3d9d52 100644 --- a/src/utils/dbHandler.spec.ts +++ b/src/utils/dbHandler.spec.ts @@ -28,9 +28,10 @@ import { Input, InputOptions, Validation } from "../core/input"; import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; import { configs } from "./config"; -import { DbHandler, QueryOptions } from "./dbHandler"; +import { DbHandler } from "./dbHandler"; import { InputType, UpdateType, ValidationType } from "./enumHandler"; import { OptHandler } from "./optHandler"; +import { QueryBuilder, QueryOptions } from "./queryBuilder"; import { TestHandler } from "./testHandler"; describe("Database Handler", () => { @@ -38,9 +39,9 @@ describe("Database Handler", () => { it("should insert a form", (done) => { const queryString: string = "INSERT INTO form(id, title, description)\ - VALUES (5, 'Form Title 5', 'Form Description 5');"; + VALUES (5, 'Form Title 5', 'Form Description 5');"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("INSERT"); expect(result.rowCount).to.be.equal(1); @@ -52,17 +53,17 @@ describe("Database Handler", () => { it("should insert a form and then rollback", (done) => { series([ (cb: (err: Error, result?: QueryResult) => void) => { - dbhandler.begin(cb); + dbhandler.form.begin(cb); }, (callback: (err: Error, result?: QueryResult) => void) => { const queryString: string = "INSERT INTO form(id, title, description)\ VALUES (6, 'Form Title 6', 'Form Description 6');"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, callback); + dbhandler.form.executeQuery(query, callback); }, (cb: (err: Error, result?: QueryResult) => void) => { - dbhandler.rollback(cb); + dbhandler.form.rollback(cb); } ], (err, results) => { @@ -79,7 +80,7 @@ describe("Database Handler", () => { it("should select all forms", (done) => { const queryString: string = "SELECT * FROM form;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("SELECT"); expect(result.rowCount).to.be.equal(5); @@ -93,7 +94,7 @@ describe("Database Handler", () => { const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(0); @@ -105,7 +106,7 @@ describe("Database Handler", () => { it("should remove existent form", (done) => { const queryString: string = "DELETE FROM form WHERE id=5;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(1); @@ -120,7 +121,7 @@ describe("Database Handler", () => { (2, 3,'TEXT', TRUE, 'Question 3 Form 2', 'Description Question 3 Form 2'),\ (2, 4,'TEXT', TRUE, 'Question 4 Form 2', 'Description Question 4 Form 2');"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("INSERT"); expect(result.rowCount).to.be.equal(2); @@ -132,7 +133,7 @@ describe("Database Handler", () => { const queryString: string = "SELECT * FROM input;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("SELECT"); expect(result.rowCount).to.be.equal(15); @@ -145,7 +146,7 @@ describe("Database Handler", () => { const queryString: string = "DELETE FROM input WHERE id=20;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(0); @@ -154,13 +155,13 @@ describe("Database Handler", () => { }); it("should remove existent input", (done) => { - const queryString: string = "DELETE FROM input WHERE id=12 OR id=13 OR id=14 OR id=15;"; + const queryString: string = "DELETE FROM input WHERE id=9 OR id=14 OR id=15;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); - expect(result.rowCount).to.be.equal(4); + expect(result.rowCount).to.be.equal(3); done(); }); @@ -173,7 +174,7 @@ describe("Database Handler", () => { (5, 'MANDATORY');"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("INSERT"); expect(result.rowCount).to.be.equal(2); @@ -185,10 +186,10 @@ describe("Database Handler", () => { const queryString: string = "SELECT * FROM input_validation;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("SELECT"); - expect(result.rowCount).to.be.equal(18); + expect(result.rowCount).to.be.equal(14); done(); }); }); @@ -197,7 +198,7 @@ describe("Database Handler", () => { const queryString: string = "DELETE FROM input_validation WHERE id=21;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(0); @@ -206,13 +207,13 @@ describe("Database Handler", () => { }); it("should remove existent input validations", (done) => { - const queryString: string = "DELETE FROM input_validation WHERE id=17 OR id=18;"; + const queryString: string = "DELETE FROM input_validation WHERE id=9 OR id=10 OR id=13 OR id=14;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); - expect(result.rowCount).to.be.equal(2); + expect(result.rowCount).to.be.equal(4); done(); }); }); @@ -224,7 +225,7 @@ describe("Database Handler", () => { (2, 2, '2');"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("INSERT"); expect(result.rowCount).to.be.equal(2); @@ -237,10 +238,10 @@ describe("Database Handler", () => { const queryString: string = "SELECT * FROM input_validation_argument;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("SELECT"); - expect(result.rowCount).to.be.equal(13); + expect(result.rowCount).to.be.equal(9); done(); }); }); @@ -249,7 +250,7 @@ describe("Database Handler", () => { const queryString: string = "DELETE FROM input_validation_argument WHERE id=15;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(0); @@ -258,13 +259,13 @@ describe("Database Handler", () => { }); it("should remove existent input validations arguments", (done) => { - const queryString: string = "DELETE FROM input_validation_argument WHERE id=12 OR id=13;"; + const queryString: string = "DELETE FROM input_validation_argument WHERE id=6;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.form.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); - expect(result.rowCount).to.be.equal(2); + expect(result.rowCount).to.be.equal(1); done(); }); }); @@ -276,7 +277,7 @@ describe("Database Handler", () => { (9, 3, '2018-06-03 10:11:25-03');"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.answer.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("INSERT"); expect(result.rowCount).to.be.equal(2); @@ -288,7 +289,7 @@ describe("Database Handler", () => { const queryString: string = "SELECT * FROM form_answer;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.answer.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("SELECT"); expect(result.rowCount).to.be.equal(9); @@ -300,7 +301,7 @@ describe("Database Handler", () => { const queryString: string = "DELETE FROM form_answer WHERE id=11;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.answer.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(0); @@ -312,7 +313,7 @@ describe("Database Handler", () => { const queryString: string = "DELETE FROM form_answer WHERE id=8 OR id=9;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.answer.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(2); @@ -328,7 +329,7 @@ describe("Database Handler", () => { (19,1, 7,'Answer to Question 2 Form 3',2);"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.answer.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("INSERT"); expect(result.rowCount).to.be.equal(2); @@ -340,7 +341,7 @@ describe("Database Handler", () => { const queryString: string = "SELECT * FROM input_answer;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.answer.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("SELECT"); expect(result.rowCount).to.be.equal(19); @@ -352,7 +353,7 @@ describe("Database Handler", () => { const queryString: string = "DELETE FROM input_answer WHERE id=25;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.answer.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(0); @@ -364,7 +365,7 @@ describe("Database Handler", () => { const queryString: string = "DELETE FROM input_answer WHERE id=18 OR id=19;"; const query: QueryOptions = {query: queryString, parameters: []}; - dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + dbhandler.answer.executeQuery(query, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); expect(result.command).to.be.equal("DELETE"); expect(result.rowCount).to.be.equal(2); @@ -374,9 +375,10 @@ describe("Database Handler", () => { }); describe("Read and Write on Database", () => { + const dbhandler = new DbHandler(configs.poolconfig); it("should list all forms", (done) => { - dbhandler.listForms((err: Error, forms?: Form[]) => { + dbhandler.form.list((err: Error, forms?: Form[]) => { expect(err).to.be.a("null"); expect(forms.length).to.be.equal(4); for (let i = 0; i < forms.length; i++){ @@ -389,7 +391,8 @@ describe("Read and Write on Database", () => { }); it("should read an existent form", (done) => { - dbhandler.readForm(1, (err: Error, form: Form) => { + dbhandler.form.read(1, (err: Error, form: Form) => { + expect(err).to.be.a("null"); const inputObj1: InputOptions = { @@ -398,10 +401,24 @@ describe("Read and Write on Database", () => { , question: "Question 1 Form 1" , type: InputType.TEXT , validation: [] + , sugestions: [] , id: 1 }; const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , sugestions: [] + , id: 2 + }; + + const inputObj3: InputOptions = { placement: 2 , description: "Description Question 2 Form 1" , question: "Question 2 Form 1" @@ -410,42 +427,29 @@ describe("Read and Write on Database", () => { { type: ValidationType.MAXCHAR, arguments: ["10"] } , { type: ValidationType.MINCHAR, arguments: ["2"] } ] + , sugestions: [] , id: 3 }; - const inputObj3: InputOptions = { - placement: 1 - , description: "Description Question 3 Form 1" - , question: "Question 3 Form 1" - , type: InputType.TEXT - , validation: [ - { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } - , { type: ValidationType.MANDATORY, arguments: [] } - ] - , id: 2 - }; const formObj: FormOptions = { id: 1 , title: "Form Title 1" , description: "Form Description 1" , inputs: [ OptHandler.input(inputObj1) - , OptHandler.input(inputObj3) , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) ] }; - - TestHandler.testForm(form, new Form(OptHandler.form(formObj))); - + TestHandler.testForm(form, new Form(formObj)); done(); }); }); it("should read a non existent form", (done) => { - dbhandler.readForm(10, (err: Error, form: Form) => { - expect(err).to.not.equal(null); + dbhandler.form.read(10, (err: Error, form?: Form) => { + expect(err).to.be.not.equal(null); expect(form).to.be.undefined; - done(); }); }); @@ -484,7 +488,7 @@ describe("Read and Write on Database", () => { }; const form = new Form(OptHandler.form(formObj)); - dbhandler.writeForm(form, (err: Error, formResult: Form) => { + dbhandler.form.write(form, (err: Error, formResult: Form) => { expect(err).to.be.a("null"); expect(formResult.id).to.be.equal(5); let inputId: number = 16; @@ -494,7 +498,6 @@ describe("Read and Write on Database", () => { } done(); }); - }); it("should read an existent form Answer", (done) => { @@ -518,26 +521,29 @@ describe("Read and Write on Database", () => { , value: "Answer to Question 3 Form 1" }; - const inputAnswerOptionsDict: InputAnswerOptionsDict = {1: [inputAnswersOpt1], 2: [inputAnswersOpt2], 3: [inputAnswersOpt3]}; - const data: Date = new Date("2019-02-21 12:10:25"); - dbhandler.readForm(1, (error: Error, form: Form) => { + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 1: [inputAnswersOpt1] + , 2: [inputAnswersOpt2] + , 3: [inputAnswersOpt3] + }; + + const date: Date = new Date("2019-02-21 12:10:25"); + dbhandler.form.read(1, (error: Error, form: Form) => { const formAnswerOptions: FormAnswerOptions = { id: 3 , form - , timestamp: data + , timestamp: date , inputsAnswerOptions: inputAnswerOptionsDict - }; - dbhandler.readFormAnswer(3, (err: Error, formAnswer: FormAnswer) => { - TestHandler.testFormAnswer(formAnswer, new FormAnswer(OptHandler.formAnswer(formAnswerOptions))); - done(); - + }; + dbhandler.answer.read(3, (err: Error, formAnswer: FormAnswer) => { + TestHandler.testFormAnswer(formAnswer, new FormAnswer(formAnswerOptions)); + done(); }); }); - }); it("should read a non existent form Answer", (done) => { - dbhandler.readFormAnswer(25, (err: Error, formAnswer: FormAnswer) => { + dbhandler.answer.read(25, (err: Error, formAnswer: FormAnswer) => { expect(err).to.not.equal(null); expect(formAnswer).to.be.undefined; @@ -553,46 +559,52 @@ describe("Read and Write on Database", () => { , idInput: 1 , placement: 0 , value: "Answer to Question 1 Form 1" - }; + }; + const inputAnswersOpt2: InputAnswerOptions = { id: 6 , idInput: 2 , placement: 0 , value: "Answer to Question 2 Form 1" - }; + }; + const inputAnswersOpt3: InputAnswerOptions = { id: 7 , idInput: 3 , placement: 0 , value: "Answer to Question 3 Form 1" - }; - const inputAnswerOptionsDict: InputAnswerOptionsDict = {1: [inputAnswersOpt1], 2: [inputAnswersOpt2], 3: [inputAnswersOpt3]}; - const data: Date = new Date(2019, 6, 4); - dbhandler.readForm(1, (error: Error, form: Form) => { - const formAnswerOptions: FormAnswerOptions = { - form - , timestamp: data - , inputsAnswerOptions: inputAnswerOptionsDict - }; - const formAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); - - dbhandler.writeFormAnswer(formAnswer, (err: Error, formAnswerResult: FormAnswer) => { - expect(err).to.be.a("null"); - expect(formAnswerResult.id).to.be.equal(8); - let inputAnswerId: number = 18; - for (const key of Object.keys(formAnswerResult.inputAnswers)){ - for (const inputAnswer of formAnswerResult.inputAnswers[parseInt(key, 10)]){ - expect(inputAnswer.id).to.be.equal(inputAnswerId); - inputAnswerId++; - } - - } - done(); - }); - }); + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 1: [inputAnswersOpt1] + , 2: [inputAnswersOpt2] + , 3: [inputAnswersOpt3] + }; + + dbhandler.form.read(1, (error: Error, form: Form) => { + const formAnswerOptions: FormAnswerOptions = { + form + , timestamp: new Date() + , inputsAnswerOptions: inputAnswerOptionsDict + }; + const formAnswerObj = new FormAnswer(formAnswerOptions); + + dbhandler.answer.write(formAnswerObj, (err: Error, formAnswerResult: FormAnswer) => { + expect(err).to.be.a("null"); + expect(formAnswerResult.id).to.be.equal(8); + let inputAnswerId: number = 18; + for (const key of Object.keys(formAnswerResult.inputAnswers)){ + for (const inputAnswer of formAnswerResult.inputAnswers[parseInt(key, 10)]){ + expect(inputAnswer.id).to.be.equal(inputAnswerId); + inputAnswerId++; + } + } + done(); + }); + }); }); - it("should insert an formUpdate", (done) => { + it("should update a form and insert FormUpdate", (done) => { const inputObj1: InputOptions = { placement: 0 , description: "Description Question 1 Form 1" @@ -654,11 +666,11 @@ describe("Read and Write on Database", () => { , inputObj3 ] }; - const dateTMP: Date = new Date(); const formUpdateObj: FormUpdateOptions = { id: 1 , form: formObj - , updateDate: dateTMP + , updateDate: new Date() + , changed: true , inputUpdates: [ updateObj1 , updateObj2 @@ -666,10 +678,168 @@ describe("Read and Write on Database", () => { ] }; - let formUpdateTmp: FormUpdate; + dbhandler.form.update(new FormUpdate (formUpdateObj), (err: Error) => { + expect(err).to.be.a("null"); + done(); + }); + }); + + it("should update inputs and insert FormUpdate", (done) => { + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const updateObj1: InputUpdateOptions = { + id: 1 + , input: inputObj1 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const updateObj2: InputUpdateOptions = { + id: 2 + , input: inputObj2 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const updateObj3: InputUpdateOptions = { + id: 3 + , input: inputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; - formUpdateTmp = new FormUpdate(formUpdateObj); - dbhandler.updateForm(formUpdateTmp, (err: Error) => { + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + ] + }; + const formUpdateObj: FormUpdateOptions = { + id: 1 + , form: formObj + , updateDate: new Date() + , changed: false + , inputUpdates: [ + updateObj1 + , updateObj2 + , updateObj3 + ] + }; + + dbhandler.form.update(new FormUpdate (formUpdateObj), (err: Error) => { + expect(err).to.be.a("null"); + done(); + }); + }); + + it("should reenabled inputs and insert FormUpdate", (done) => { + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const updateObj1: InputUpdateOptions = { + id: 1 + , input: inputObj1 + , inputOperation: UpdateType.REENABLED + , value: null + }; + + const inputObj2: InputOptions = { + placement: 1 + + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const updateObj2: InputUpdateOptions = { + id: 2 + , input: inputObj2 + , inputOperation: UpdateType.REENABLED + , value: null + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const updateObj3: InputUpdateOptions = { + id: 3 + , input: inputObj3 + , inputOperation: UpdateType.REENABLED + , value: null + }; + + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + ] + }; + const formUpdateObj: FormUpdateOptions = { + id: 1 + , form: formObj + , updateDate: new Date() + , changed: false + , inputUpdates: [ + updateObj1 + , updateObj2 + , updateObj3 + ] + }; + + dbhandler.form.update(new FormUpdate (formUpdateObj), (err: Error) => { expect(err).to.be.a("null"); done(); }); @@ -703,9 +873,302 @@ describe("Read and Write on Database", () => { , inputUpdates: [ inputUpdateObj ] }; - dbhandler.updateDatabase(formUpdateObj, (err: Error) => { + dbhandler.form.update(formUpdateObj, (err: Error) => { expect(err).to.be.not.a("null"); done(); }); }); + + it("should insert a input with answer sugestions", (done) => { + + const inputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 6" + , question: "Question 1 Form 6" + , enabled: true + , type: InputType.CHECKBOX + , sugestions: [ + { value: "Sugestion 1", placement: 0 } + , { value: "Sugestion 2", placement: 1 } + , { value: "Sugestion 3", placement: 2 } + ] + , validation: [ + { type: ValidationType.SOMECHECKBOX, arguments: [] } + ] + , id: 18 + }; + + const inputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 6" + , question: "Question 2 Form 6" + , enabled: true + , type: InputType.RADIO + , sugestions: [ + { value: "Sugestion 4", placement: 0 } + , { value: "Sugestion 5", placement: 1 } + , { value: "Sugestion 6", placement: 2 } + ] + , validation: [] + , id: 19 + }; + + const inputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 6" + , question: "Question 3 Form 6" + , enabled: true + , type: InputType.SELECT + , sugestions: [ + { value: "Sugestion 1", placement: 0 } + , { value: "Sugestion 2", placement: 1 } + ] + , validation: [ + { type: ValidationType.DEPENDENCY, arguments: ["19", "1"] } + , { type: ValidationType.DEPENDENCY, arguments: ["20", "1"] } + ] + , id: 20 + }; + + const inputObj4: Input = { + placement: 3 + , description: "Description Question 4 Form 6" + , question: "Question 4 Form 6" + , enabled: true + , type: InputType.CHECKBOX + , sugestions: [ + { value: "Sugestion n", placement: 0 } + , { value: "Sugestion n+1", placement: 1 } + , { value: "Sugestion n+2", placement: 2 } + , { value: "Sugestion n+3", placement: 4 } + ] + , validation: [ + { type: ValidationType.SOMECHECKBOX, arguments: [] } + ] + , id: 21 + }; + + const inputObj5: Input = { + placement: 4 + , description: "Description Question 5 Form 6" + , question: "Question 5 Form 6" + , enabled: true + , type: InputType.TEXT + , sugestions: [] + , validation: [ + { type: ValidationType.DEPENDENCY, arguments: ["18", "2"] } + ] + , id: 22 + }; + + const formObj: Form = { + title: "Form Title 6" + , description: "Form Description 6" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + , inputObj4 + , inputObj5 + ] + , id: 6 + }; + + dbhandler.form.write(formObj, (err: Error, formResult: Form) => { + expect(err).to.be.a("null"); + TestHandler.testForm(formObj, formResult); + done(); + }); + }); + + it("should insert a form with typeof validation", (done) => { + + const inputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 7" + , question: "Question 1 form 7" + , enabled: true + , type: InputType.TEXT + , sugestions: [] + , validation: [ + { type: ValidationType.TYPEOF, arguments: ["int"] } + , { type: ValidationType.MAXANSWERS, arguments: ["2"] } + ] + , id: 23 + }; + + const inputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 7" + , question: "Question 2 form 7" + , enabled: true + , type: InputType.TEXT + , sugestions: [] + , validation: [ + { type: ValidationType.TYPEOF, arguments: ["int"] } + , { type: ValidationType.MAXANSWERS, arguments: ["1"] } + ] + , id: 24 + }; + + const inputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 7" + , question: "Question 3 form 7" + , enabled: true + , type: InputType.TEXT + , sugestions: [] + , validation: [ + { type: ValidationType.TYPEOF, arguments: ["float"] } + ] + , id: 25 + }; + + const inputObj4: Input = { + placement: 3 + , description: "Description Question 4 Form 7" + , question: "Question 4 form 7" + , enabled: true + , type: InputType.TEXT + , sugestions: [] + , validation: [ + { type: ValidationType.TYPEOF, arguments: ["float"] } + ] + , id: 26 + }; + + const inputObj5: Input = { + placement: 4 + , description: "Description Question 1 Form 8" + , question: "Question 1 form 8" + , enabled: true + , type: InputType.TEXT + , sugestions: [] + , validation: [ + { type: ValidationType.TYPEOF, arguments: ["date"] } + , { type: ValidationType.MAXANSWERS, arguments: ["2"] } + ] + , id: 27 + }; + + const inputObj6: Input = { + placement: 5 + , description: "Description Question 2 Form 8" + , question: "Question 2 form 8" + , enabled: true + , type: InputType.TEXT + , sugestions: [] + , validation: [ + { type: ValidationType.TYPEOF, arguments: ["date"] } + ] + , id: 28 + }; + + const inputObj7: Input = { + placement: 6 + , description: "Description Question 3 Form 8" + , question: "Question 3 form 8" + , enabled: true + , type: InputType.TEXT + , sugestions: [] + , validation: [ + { type: ValidationType.TYPEOF, arguments: ["invalid"] } + , { type: ValidationType.MAXANSWERS, arguments: ["invalid"] } + , { type: ValidationType.DEPENDENCY, arguments: ["28", "invalid"] } + ] + , id: 29 + }; + + const formObj: Form = { + title: "Form Title 7" + , description: "Form Description 7" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + , inputObj4 + , inputObj5 + , inputObj6 + , inputObj7 + ] + , id: 7 + }; + + dbhandler.form.write(formObj, (err: Error, formResult: Form) => { + expect(err).to.be.a("null"); + TestHandler.testForm(formObj, formResult); + done(); + }); + }); + + it("should insert form answer validation somecheckbox", (done) => { + + const inputAnswersOpt1: InputAnswerOptions = { + id: undefined + , idInput: 18 + , placement: 0 + , value: "true" + }; + + const inputAnswersOpt2: InputAnswerOptions = { + id: undefined + , idInput: 18 + , placement: 1 + , value: "true" + }; + + const inputAnswersOpt3: InputAnswerOptions = { + id: undefined + , idInput: 18 + , placement: 2 + , value: "false" + }; + + const inputAnswersOpt4: InputAnswerOptions = { + id: undefined + , idInput: 19 + , placement: 1 + , value: "true" + }; + + const inputAnswersOpt5: InputAnswerOptions = { + id: undefined + , idInput: 20 + , placement: 1 + , value: "true" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 18: [ + inputAnswersOpt1 + , inputAnswersOpt2 + , inputAnswersOpt3 + ] + , 19: [inputAnswersOpt4] + , 20: [inputAnswersOpt5] + , 21: [] + }; + + dbhandler.form.read(6, (error: Error, form: Form) => { + const formAnswerOptions: FormAnswerOptions = { + form + , timestamp: new Date() + , inputsAnswerOptions: inputAnswerOptionsDict + }; + const formAnswerObj = new FormAnswer(formAnswerOptions); + + dbhandler.answer.write(formAnswerObj, (err: Error, formAnswerResult: FormAnswer) => { + expect(err).to.be.a("null"); + expect(formAnswerResult.id).to.be.equal(9); + let inputAnswerId: number = 21; + for (const key of Object.keys(formAnswerResult.inputAnswers)){ + for (const inputAnswer of formAnswerResult.inputAnswers[parseInt(key, 10)]){ + expect(inputAnswer.id).to.be.equal(inputAnswerId); + inputAnswerId++; + } + } + done(); + }); + }); + }); }); diff --git a/src/utils/dbHandler.ts b/src/utils/dbHandler.ts index b7b06542be5bedee40930b1a9ce0801d6654785c..6d8217870080a22745d8902a0cee01417c090968 100644 --- a/src/utils/dbHandler.ts +++ b/src/utils/dbHandler.ts @@ -19,928 +19,31 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { eachOfSeries, eachSeries, map, seq, waterfall } from "async"; import { Pool, PoolConfig, QueryResult } from "pg"; -import { Form, FormOptions } from "../core/form"; -import { FormAnswer, FormAnswerOptions } from "../core/formAnswer"; -import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; -import { Input, InputOptions, Validation } from "../core/input"; -import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; -import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; -import { EnumHandler, InputType, UpdateType, ValidationType } from "./enumHandler"; -import { ErrorHandler} from "./errorHandler"; -import { OptHandler } from "./optHandler"; -import { Sorter } from "./sorter"; +import { AnswerQueryBuilder } from "./answerQueryBuilder"; +import { configs } from "./config"; +import { FormQueryBuilder } from "./formQueryBuilder"; -/** Parameters used to create a parametrized query, to avoid SQL injection */ -export interface QueryOptions { - /** query string to execute */ - query: string; - - /** Array of input. containing question */ - parameters: any[]; -} /** * Class of the SGBD from the Form Creator Api perspective. Used to * perform all the operations into the database that the Form Creator Api - * requires. This operations include read and write data. + * requires. */ - export class DbHandler { + /** Object used to control Form operations. */ + public readonly form: FormQueryBuilder; + /** Object used to control FormAnswer operations. */ + public readonly answer: AnswerQueryBuilder; /** Information used to connect with a PostgreSQL database. */ private pool: Pool; /** * Creates a new adapter with the database connection configuration. - * @param config - The information required to create a connection with - * the database. */ constructor(config: PoolConfig) { this.pool = new Pool(config); + this.form = new FormQueryBuilder(this.pool); + this.answer = new AnswerQueryBuilder(this.form, this.pool); } - - /** - * Asynchronously executes a query and get its result. - * @param query - Query (SQL format) to be executed. - * @param cb - Callback function which contains the data read. - * @param cb.err - Error information when the method fails. - * @param cb.result - Query result. - */ - - public executeQuery(query: QueryOptions , cb: (err: Error, result?: QueryResult) => void): void{ - - this.pool.connect((err, client, done) => { - - if (err) { - cb(err); - return; - } - - client.query(query.query, query.parameters, (error, result) => { - // call 'done()' to release client back to pool - done(); - cb(error, (result) ? result : null); - }); - }); - } - - /** - * Asynchronously ends a transaction - */ - public commit(cb: (err: Error, result?: QueryResult) => void){ - this.executeQuery({query: "COMMIT;", parameters: []}, cb); - } - /** - * Asynchronously rollback a transaction - */ - public rollback(cb: (err: Error, result?: QueryResult) => void){ - this.executeQuery({query: "ROLLBACK;", parameters: []}, cb); - } - - /** - * Asynchronously starts a transaction - */ - public begin(cb: (err: Error, result?: QueryResult) => void){ - this.executeQuery({query: "BEGIN;", parameters: []}, cb); - } - - /** - * Asynchronously list all forms in database. - * @param cb - Callback function which contains the data read. - * @param cb.err - Error information when the method fails. - * @param cb.form - list of form or a empty list if there is no form on database. - */ - public listForms(cb: (err: Error, forms?: Form[]) => void){ - const query: QueryOptions = {query: "SELECT id, title, description FROM form;", parameters: []}; - const forms: Form[] = []; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - if (err) { - cb(err); - return; - } - - for (const row of result.rows){ - const formObj: FormOptions = { - id: row["id"] - , title: row["title"] - , description: row["description"] - , inputs: [] - }; - let formTmp: Form; - try{ - formTmp = new Form ( OptHandler.form(formObj)); - } - catch (e){ - cb(e); - return; - } - - forms.push(formTmp); - } - cb(err, forms); - }); - } - - /** - * Asynchronously executes a query and get a Form. - * @param id - Form identifier to be founded. - * @param cb - Callback function which contains the data read. - * @param cb.err - Error information when the method fails. - * @param cb.form - Form or null if form not exists. - */ - public readForm(id: number, cb: (err: Error, form?: Form) => void){ - - const queryString: string = "SELECT id, title, description FROM form WHERE id=$1;"; - const query: QueryOptions = {query: queryString, parameters: [id]}; - - waterfall([ - (callback: (err: Error, result?: QueryResult) => void) => { - this.begin(callback); - }, - (result: QueryResult, callback: (err: Error, result?: QueryResult) => void) => { - this.executeQuery(query, callback); - }, - (result: QueryResult, callback: (err: Error, form?: Form) => void) => { - - if (result.rowCount !== 1){ - callback(ErrorHandler.badIdAmount(result.rowCount)); - return; - } - this.readInputWithFormId(id, (error: Error, inputsResult: Input[]) => { - const formObj: FormOptions = { - id: result.rows[0]["id"] - , title: result.rows[0]["title"] - , description: result.rows[0]["description"] - , inputs: inputsResult - }; - let formTmp: Form; - - try{ - formTmp = new Form ( OptHandler.form(formObj)); - } - catch (e){ - callback(e); - return; - } - - callback(error, formTmp); - }); - }, - (form: Form, callback: (err: Error, form?: Form) => void) => { - - this.commit((error: Error, result?: QueryResult) => { - callback(error, form); - }); - } - ], (err, result: Form) => { - - if (err){ - this.rollback( (error: Error, results?: QueryResult) => { - cb(err); - return; - }); - return; - } - cb(err, result); - - }); - - } - - /** - * A private method to asynchronously executes a query and get a list of Inputs. - * @param id - Form identifier which inputs are linked to. - * @param cb - Callback function which contains the data read. - * @param cb.err - Error information when the method fails. - * @param cb.inputs - Input array or an empty list if there is no input linked to form. - */ - private readInputWithFormId(id: number, cb: (err: Error, inputs?: InputOptions[]) => void){ - const queryString: string = "SELECT id, placement, input_type, question, description FROM input WHERE id_form=$1 and enabled=true;"; - const query: QueryOptions = {query: queryString, parameters: [id]}; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - map(result.rows, (innerResult, callback) => { - this.readInputValidationWithInputId(innerResult["id"], (error: Error, validationResult: Validation[]) => { - const inputObj: InputOptions = { - id: innerResult["id"] - , placement: innerResult["placement"] - , description: innerResult["description"] - , question: innerResult["question"] - , validation: validationResult - , type: EnumHandler.parseInputType(innerResult["input_type"]) - }; - let inputTmp: InputOptions; - try{ - inputTmp = OptHandler.input(inputObj); - } - catch (e){ - callback(e); - return; - } - - callback(error, inputTmp); - }); - }, (errors, inputs: InputOptions[]) => { - - if (errors){ - this.rollback( (error: Error, results?: QueryResult) => { - cb(errors); - return; - }); - return; - } - - const sortedInputs: InputOptions[] = Sorter.sortByPlacement(inputs); - - cb(errors, sortedInputs); - }); - }); - } - - /** - * A private method to asynchronously executes a query and get a list of Validations based on a Input id. - * @param id - Input identifier which validations are linked to. - * @param cb - Callback function which contains the data read. - * @param cb.err - Error information when the method fails. - * @param cb.validations - Validation array or an empty list if there is no validation for selected input. - */ - private readInputValidationWithInputId(id: number, cb: (err: Error, validations?: Validation[]) => void){ - const queryString: string = "SELECT id, validation_type FROM input_validation WHERE id_input=$1;"; - const query: QueryOptions = { - query: queryString - , parameters: [id] - }; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - map(result.rows, (innerResult, callback) => { - this.readInputValidationArgumentWithInputValidationId(innerResult["id"], (error, argumentsArray) => { - const validationTmp: Validation = { - type: EnumHandler.parseValidationType(innerResult["validation_type"]), - arguments: argumentsArray - }; - callback(error, validationTmp); - }); - }, (errors, validation: Validation[]) => { - cb(errors, validation); - }); - }); - - } - - /** - * A private method to asynchronously executes a query and get a list of Validation Argument based on a Validation id. - * @param id - Validation identifier which Validation Arguments are linked to. - * @param cb - Callback function which contains the data read. - * @param cb.err - Error information when the method fails. - * @param cb.argumentsArray - Validation Arguments array or an empty list if there is no validation argument for selected input. - */ - private readInputValidationArgumentWithInputValidationId(id: number, cb: (err: Error, argumentsArray?: any[]) => void){ - const queryString: string = "SELECT id, argument, placement FROM \ -input_validation_argument WHERE \ -id_input_validation=$1;"; - const query: QueryOptions = { - query: queryString - , parameters: [id] - }; - - // cb(null); - - this.executeQuery(query, (error: Error, result?: QueryResult) => { - - const sortedResult: any[] = Sorter.sortByPlacement(result.rows); - const argumentArrayTmp = []; - for (const innerResult of sortedResult){ - argumentArrayTmp.push(innerResult["argument"]); - } - - cb(error, argumentArrayTmp); - }); - - } - - /** - * Asynchronously insert a form on Database and return it. - * @param form - Form to be inserted. - * @param cb - Callback function which contains the inserted data. - * @param cb.err - Error information when the method fails. - * @param cb.formResult - Form or null if form any error occurs. - */ - - public writeForm(form: Form, cb: (err: Error, formResult?: Form) => void){ - const queryString: string = "INSERT INTO form (title, description) VALUES( $1, $2 ) RETURNING id;"; - const query: QueryOptions = { - query: queryString - , parameters: [ - form.title - , form.description - ] - }; - - waterfall([ - (callback: (err: Error, result?: QueryResult) => void) => { - this.begin(callback); - }, - (result: QueryResult, callback: (err: Error, result?: QueryResult) => void) => { - this.executeQuery(query, callback); - }, - (result: QueryResult, callback: (err: Error, formId?: number) => void) => { - if (result.rowCount !== 1){ - callback(ErrorHandler.notInserted("Form")); - return; - } - eachSeries(form.inputs, (input: Input, innerCallback) => { - this.writeInputWithFormId(result.rows[0]["id"], input, innerCallback); - }, (error) => { - if (error){ - callback(error); - return; - - } - callback(error, result.rows[0]["id"]); - - }); - }, - (formId: number, callback: (err: Error, formId?: number) => void) => { - - this.commit((error: Error, result?: QueryResult) => { - callback(error, formId); - }); - }, - (formId: number, callback: (err: Error, formResult?: Form) => void) => { - - this.readForm(formId, callback); - - } - ], (err, result: Form) => { - - if (err){ - this.rollback( (error: Error, results?: QueryResult) => { - cb(err); - return; - }); - return; - } - cb(err, result); - - }); - } - - /** - * Asynchronously insert a Input on Database and return it. - * @param formId - Form identifier to relate with Input. - * @param input - Input to be inserted. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - */ - private writeInputWithFormId(formId: number, input: Input, cb: (err: Error, result?: number) => void){ - const queryString: string = "INSERT INTO input (\ -id_form, placement, input_type, enabled, question, description)\ -VALUES ( $1, $2, $3, $4, $5, $6) RETURNING id;"; - - const query: QueryOptions = { - query: queryString - , parameters: [ - formId - , input.placement - , EnumHandler.stringifyInputType(input.type) - , true - , input.question - , input.description - ] - }; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - if (err){ - cb(err); - return; - } - - if (result.rowCount !== 1){ - cb(ErrorHandler.notInserted("Input")); - return; - } - - eachSeries(input.validation, (validation: Validation, callback) => { - - this.writeValidationWithInputId(result.rows[0]["id"], validation, callback); - - }, (error) => { - - cb(error, result.rows[0]["id"]); - - }); - }); - - } - /** - * Asynchronously insert a Validation on Database and return it. - * @param inputId - Input id to relate with Validation. - * @param validation - Validation to be inserted. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - */ - private writeValidationWithInputId(inputId: number, validation: Validation, cb: (err: Error) => void){ - - const queryString: string = "INSERT INTO input_validation\ -( id_input, validation_type) VALUES\ -( $1, $2 ) RETURNING id;"; - - const query: QueryOptions = { - query: queryString - , parameters: [ - inputId - , EnumHandler.stringifyValidationType(validation.type) - ] - }; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - - if (err){ - cb(err); - return; - } - - if (result.rowCount !== 1){ - cb(ErrorHandler.notInserted("Validation")); - return; - } - - eachOfSeries(validation.arguments, (argument, placement: number, callback) => { - - this.writeValidationArgumentWithInputIdAndPlacement(result.rows[0]["id"], argument, placement, callback); - }, (error) => { - cb(error); - }); - - }); - - } - - /** - * Asynchronously insert a Validation Argument on Database and return it. - * @param validationId - Validation identifier to relate with Argument. - * @param argument - Argument to be inserted. - * @param placement - placement where argument should be. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - */ - private writeValidationArgumentWithInputIdAndPlacement(validationId: number, argument: string, placement: number, cb: (err: Error) => void){ - - const queryString: string = "INSERT INTO input_validation_argument \ -( id_input_validation, argument, placement ) VALUES\ -( $1, $2, $3 ) RETURNING id;"; - - const query: QueryOptions = { - query: queryString - , parameters: [ - validationId - , argument - , placement - ] - }; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - - if (err){ - cb(err); - return; - } - - if (result.rowCount !== 1){ - cb(ErrorHandler.notInserted("Validation Argument")); - return; - } - - cb(err); - - }); - - } - - /** - * Asynchronously insert a form answer on Database and return it. - * @param formAnswer - FormAnswer to be inserted. - * @param cb - Callback function which contains the inserted data. - * @param cb.err - Error information when the method fails. - * @param cb.formAnswerResult - Form or null if form any error occurs. - */ - public writeFormAnswer(formAnswer: FormAnswer, cb: (err: Error, formAnswerResult?: FormAnswer) => void){ - const queryString: string = "INSERT INTO form_answer (id_form, answered_at) VALUES( $1, $2 ) RETURNING id;"; - const query: QueryOptions = { - query: queryString - , parameters: [ - formAnswer.form.id - , formAnswer.timestamp - ] - }; - - waterfall([ - (callback: (err: Error, result?: QueryResult) => void) => { - this.begin(callback); - }, - (result: QueryResult, callback: (err: Error, result?: QueryResult) => void) => { - this.executeQuery(query, callback); - }, - (result: QueryResult, callback: (err: Error, formAnswerId?: number) => void) => { - if (result.rowCount !== 1){ - callback(ErrorHandler.notInserted("FormAnswer")); - return; - } - // NOTE: Although this two "FOR"s the complexity is O(n) - // This first eachSeries iterates over the keys of the dictonary - eachSeries(Object.keys(formAnswer.inputAnswers), (key, outerCallback) => { - // this second one iterates over the array of the objects within the current key - - eachSeries(formAnswer.inputAnswers[parseInt(key, 10)], (inputsAnswer: InputAnswer, innerCallback) => { - this.writeInputAnswerWithFormId(result.rows[0]["id"], inputsAnswer, innerCallback); - }, (error) => { - if (error){ - outerCallback(error); - return; - } - outerCallback(error); - }); - }, (error) => { - if (error){ - callback(error); - return; - } - callback(error, result.rows[0]["id"]); - }); - - }, - (formAnswerId: number, callback: (err: Error, formId?: number) => void) => { - - this.commit((error: Error, result?: QueryResult) => { - callback(error, formAnswerId); - }); - }, - (formAnswerId: number, callback: (err: Error, formResult?: FormAnswer) => void) => { - - this.readFormAnswer(formAnswerId, callback); - - } - ], (err, result: FormAnswer) => { - - if (err){ - this.rollback( (error: Error, results?: QueryResult) => { - cb(err); - return; - }); - return; - } - cb(err, result); - - }); - } - - /** - * Asynchronously insert a Input Answer on Database and return it. - * @param formAnswerId - Form Answer identifier to relate with Input Answer. - * @param inputsAnswer - InputsAnswer to be inserted. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - */ - private writeInputAnswerWithFormId(formAnswerId: number, inputAnswer: InputAnswer, cb: (err: Error) => void){ - const queryString: string = "INSERT INTO input_answer (\ -id_form_answer, id_input, value, placement)\ -VALUES ( $1, $2, $3, $4) RETURNING id;"; - - const query: QueryOptions = { - query: queryString - , parameters: [ - formAnswerId - , inputAnswer.idInput - , inputAnswer.value - , inputAnswer.placement - ] - }; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - if (err){ - cb(err); - return; - } - - if (result.rowCount !== 1){ - cb(ErrorHandler.notInserted("InputsAnswer")); - return; - } - cb(err); - }); - - } - - /** - * Asynchronously executes a query and get a Form. - * @param id - Form identifier to be founded. - * @param cb - Callback function which contains the data read. - * @param cb.err - Error information when the method fails. - * @param cb.form - Form or null if form not exists. - */ - public readFormAnswer(id: number, cb: (err: Error, formAnswer?: FormAnswer) => void){ - - const queryString: string = "SELECT id, id_form, answered_at FROM form_answer WHERE id=$1;"; - const query: QueryOptions = {query: queryString, parameters: [id]}; - - waterfall([ - (callback: (err: Error, result?: QueryResult) => void) => { - this.begin(callback); - }, - (result: QueryResult, callback: (err: Error, result?: QueryResult) => void) => { - this.executeQuery(query, callback); - }, - (result: QueryResult, callback: (err: Error, formAnswer?: FormAnswer) => void) => { - if (result.rowCount !== 1){ - callback(ErrorHandler.badIdAmount(result.rowCount)); - return; - } - - this.readForm(result.rows[0]["id_form"], (err: Error, formResult: Form) => { - this.readInputAnswerWithFormAnswerId(result.rows[0]["id"], (error: Error, inputsAnswerOptionsResult: InputAnswerOptionsDict) => { - const formAnswerOpt: FormAnswerOptions = { - id: result.rows[0]["id"] - , form: formResult - , inputsAnswerOptions: inputsAnswerOptionsResult - , timestamp : result.rows[0]["answered_at"] - }; - let formAnswerTmp: FormAnswer; - - try{ - formAnswerTmp = new FormAnswer ( OptHandler.formAnswer(formAnswerOpt)); - } - catch (e){ - callback(e); - return; - } - - callback(error, formAnswerTmp); - }); - }); - }, - (formAnswer: FormAnswer, callback: (err: Error, formAnswer?: FormAnswer) => void) => { - - this.commit((error: Error, result?: QueryResult) => { - callback(error, formAnswer); - }); - } - ], (err, result: FormAnswer) => { - - if (err){ - this.rollback( (error: Error, results?: QueryResult) => { - cb(err); - return; - }); - return; - } - cb(err, result); - - }); - - } - - /** - * A private method to asynchronously executes a query and get a list of Inputs. - * @param id - Form identifier which inputs are linked to. - * @param cb - Callback function which contains the data read. - * @param cb.err - Error information when the method fails. - * @param cb.inputs - Input array or an empty list if there is no input linked to form. - */ - private readInputAnswerWithFormAnswerId(id: number, cb: (err: Error, inputsAnswerResult?: InputAnswerOptionsDict) => void){ - const queryString: string = "SELECT id, id_form_answer, id_input, value,\ -placement FROM input_answer WHERE id_form_answer=$1;"; - const query: QueryOptions = {query: queryString, parameters: [id]}; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - - const inputAnswersOpts: InputAnswerOptions[] = result.rows.map( (inputsAnswerResult) => { - const inputAnswersOpt: InputAnswerOptions = { - id: inputsAnswerResult["id"] - , idInput: inputsAnswerResult["id_input"] - , value: inputsAnswerResult["value"] - , placement: inputsAnswerResult["placement"] - }; - return OptHandler.inputAnswer(inputAnswersOpt); - }); - - const inputsAnswerResults: InputAnswerOptionsDict = {}; - for (const i of inputAnswersOpts){ - // FIXME: There is no coverage teste for this "IF" - // it happens because, until this date (04/Jun/2019) I did not implement multivalored inputs - // I hope someday someone implements it - if ( inputsAnswerResults[i["idInput"]] ) { - inputsAnswerResults[i["idInput"]].push(i); - inputsAnswerResults[i["idInput"]] = Sorter.sortByPlacement(inputsAnswerResults[i["idInput"]]); - }else{ - inputsAnswerResults[i["idInput"]] = [i]; - } - } - cb(err, inputsAnswerResults); - }); - } - - /** - * Asynchronously insert a formUpdate and inputUpdates on Database. - * @param formUpdate - A FormUpdate with InputUpdates that should be inserted in the database. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - */ - public updateForm(formUpdate: FormUpdate, cb: (err: Error) => void) { - - waterfall([ - (callback: (err: Error, result?: QueryResult) => void) => { - this.begin(callback); - }, - (result: QueryResult, callback: (err: Error, result?: number) => void) => { - this.writeFormUpdate(formUpdate, (err, idFormUpdate: number) => { - if (err) { - callback(err); - return; - } - callback(null, idFormUpdate); - }); - }, - (idFormUpdate: number, callback: (err: Error) => void) => { - eachSeries(formUpdate.inputUpdates, (inputUpdates: InputUpdate, innerCallback) => { - this.writeInputUpdate(idFormUpdate, inputUpdates, innerCallback); - }, (error) => { - callback(error); - }); - }, - (callback: (err: Error) => void) => { - this.commit((error: Error, result?: QueryResult) => { - callback(error); - }); - } - ], (err) => { - if (err){ - this.rollback( (error: Error, results?: QueryResult) => { - cb(err); - return; - }); - return; - } - cb(null); - }); - - } - - /** - * A private method that execute a query and insert a FormUpdate in the database - * @param formUpdate - Form Update to be inserted. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - * @param cb.formUpdateId - The id of the inserted FormUpdate. - */ - private writeFormUpdate(formUpdate: FormUpdate, cb: (err: Error, formUpdateId?: number) => void) { - - const queryString: string = "INSERT INTO form_update (id_form, update_date) \ - VALUES ( $1, $2 ) RETURNING id;"; - const query: QueryOptions = { - query: queryString - , parameters: [ - formUpdate.form.id - , formUpdate.updateDate - ] - }; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - if (err) { - cb(err); - return; - } - cb(err, result.rows[0].id); - }); - - } - - /** - * A private method that execute a query and insert a InputUpdate in the database - * @param inputUpdate - Input Update to be inserted. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - */ - private writeInputUpdate(idFormUpdate: number, inputUpdate: InputUpdate, cb: (err: Error) => void) { - - const queryString: string = "INSERT INTO input_update (id_form_update, id_input, input_operation_id, value) \ - VALUES ( $1, $2, $3, $4 );"; - const query: QueryOptions = { - query: queryString - , parameters: [ - idFormUpdate - , inputUpdate.input.id - , inputUpdate.inputOperation - , inputUpdate.value - ] - }; - - this.executeQuery(query, (err: Error, result?: QueryResult) => { - cb(err); - }); - } - - /** - * A method that update the database based on a given FormUpdate object - * @param formUpdate - FormUpdate with the parameters to update the database. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - * @param cb.formUpdateResult - A FormUpdate with updated id's. - */ - public updateDatabase(formUpdate: FormUpdate, cb: (err: Error, formUpdateResult?: FormUpdate) => void) { - - const formUpdateResult: FormUpdate = { - form: formUpdate.form - , updateDate: formUpdate.updateDate - , inputUpdates: [] - }; - - eachSeries(formUpdate.inputUpdates, (inputUpdate, callback) => { - switch (inputUpdate.inputOperation) { - case UpdateType.ADD: { - this.writeInputWithFormId(formUpdate.form.id, inputUpdate.input, (err: Error, id: number) => { - if (err) { - callback(err); - } - const inputOpt: InputOptions = inputUpdate.input; - inputOpt.id = id; - - const inputUpdateOpt: InputUpdateOptions = { - input: inputOpt - , inputOperation: UpdateType.ADD - , value: null - }; - formUpdateResult.inputUpdates.push(new InputUpdate(inputUpdateOpt)); - callback(null); - }); - break; - } - case UpdateType.REMOVE: { - // Set enabled option in database as false - this.updateInput(0, inputUpdate.input.id, "enabled", (err: Error) => { - if (err) { - callback(err); - } - formUpdateResult.inputUpdates.push(inputUpdate); - callback(null); - }); - break; - } - case UpdateType.SWAP: { - // Update placement option in database of a input - this.updateInput(inputUpdate.input.placement, inputUpdate.input.id, "placement", (err: Error) => { - if (err) { - callback(err); - } - formUpdateResult.inputUpdates.push(inputUpdate); - callback(null); - }); - break; - } - case UpdateType.REENABLED: { - // Set enabled option in database as true - this.updateInput(1, inputUpdate.input.id, "enabled", (err: Error) => { - if (err) { - callback(err); - } - formUpdateResult.inputUpdates.push(inputUpdate); - callback(null); - }); - break; - } - default: { - callback(new Error ("Operation not recognized")); - } - } - }, (error) => { - if (error) { - cb(error); - } - cb(null, formUpdateResult); - }); - } - - /** - * A method that execute a query and update a input based on given parameters - * @param value - A number to be inserted in the database. - * @param id - The input id that should be updated - * @param field - The field on database that should be updated. - * @param cb - Callback function. - * @param cb.err - Error information when the method fails. - */ - private updateInput(value: number, id: number, field: string, cb: (err: Error) => void) { - const queryString: string = "UPDATE input SET " + field + " = $1 WHERE id = $2"; - const query: QueryOptions = { - query: queryString - , parameters: [ - value - , id - ] - }; - this.executeQuery(query, (err: Error, result?: QueryResult) => { - cb(err); - }); - } - } diff --git a/src/utils/diffHandler.spec.ts b/src/utils/diffHandler.spec.ts index 3867a89b77eb81b4e0df5cfa513ecf1293d38c19..66d4c77b78e7488a3ab51f285e2d57e9e1e1c65e 100644 --- a/src/utils/diffHandler.spec.ts +++ b/src/utils/diffHandler.spec.ts @@ -1038,4 +1038,78 @@ describe("Diff Handler", () => { done(); }); + it("should return a valid formUpdate wich changes the title", (done) => { + const newInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const newInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const newFormObj: Form = { + id: 1 + , title: "Title 1" + , description: "Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + ] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , changed: true + , inputUpdates: [] + }; + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); }); diff --git a/src/utils/diffHandler.ts b/src/utils/diffHandler.ts index 84cdc704e349b40a76651d9082709790acf7ac88..c033ce7e9ea62656016245657caa9f8b97fa31f4 100644 --- a/src/utils/diffHandler.ts +++ b/src/utils/diffHandler.ts @@ -46,6 +46,7 @@ export class DiffHandler { const formUpdate: FormUpdate = { form: newForm , updateDate: new Date() + , changed: ((newForm.title !== oldForm.title) || (newForm.description !== oldForm.description)) , inputUpdates: [] }; diff --git a/src/utils/enumHandler.spec.ts b/src/utils/enumHandler.spec.ts index dcc32ae289f123bd953a2ecbc09aee9744bb2c7e..b992999db09082db0648ac1b0aa089aa9d7458c3 100644 --- a/src/utils/enumHandler.spec.ts +++ b/src/utils/enumHandler.spec.ts @@ -65,21 +65,37 @@ describe("Enum Handler", () => { const inputNone = EnumHandler.stringifyInputType(InputType.NONE); const inputText = EnumHandler.stringifyInputType(InputType.TEXT); + const inputRadio = EnumHandler.stringifyInputType(InputType.RADIO); + const inputCheckbox = EnumHandler.stringifyInputType(InputType.CHECKBOX); expect(inputNone).to.be.equal(""); expect(inputText).to.be.equal("text"); + expect(inputCheckbox).to.be.equal("checkbox"); + expect(inputRadio).to.be.equal("radio"); }); it("should parse string to InputType", () => { + const inputNone = EnumHandler.parseInputType(""); const inputText = EnumHandler.parseInputType("text"); const inputTextCapitalLetters = EnumHandler.parseInputType("TEXT"); - const inputNone = EnumHandler.parseInputType(""); + const inputRadio = EnumHandler.parseInputType("radio"); + const inputRadioCapitalLetters = EnumHandler.parseInputType("RADIO"); + const inputCheckbox = EnumHandler.parseInputType("checkbox"); + const inputCheckboxCapitalLetters = EnumHandler.parseInputType("CHECKBOX"); const inputFOOL = EnumHandler.parseInputType("fool"); + const inputSelect = EnumHandler.parseInputType("select"); + const inputSelectCapitalLetters = EnumHandler.parseInputType("SELECT"); expect(inputText).to.be.equal(InputType.TEXT); expect(inputTextCapitalLetters).to.be.equal(InputType.TEXT); expect(inputNone).to.be.equal(InputType.NONE); expect(inputFOOL).to.be.equal(InputType.NONE); + expect(inputCheckbox).to.be.equal(InputType.CHECKBOX); + expect(inputCheckboxCapitalLetters).to.be.equal(InputType.CHECKBOX); + expect(inputRadio).to.be.equal(InputType.RADIO); + expect(inputRadioCapitalLetters).to.be.equal(InputType.RADIO); + expect(inputSelect).to.be.equal(InputType.SELECT); + expect(inputSelectCapitalLetters).to.be.equal(InputType.SELECT); }); it("should stringify ValidationType ", () => { @@ -88,6 +104,9 @@ describe("Enum Handler", () => { const validationMandatory = EnumHandler.stringifyValidationType(ValidationType.MANDATORY); const validationMaxChar = EnumHandler.stringifyValidationType(ValidationType.MAXCHAR); const validationMinChar = EnumHandler.stringifyValidationType(ValidationType.MINCHAR); + const validationTypeOf = EnumHandler.stringifyValidationType(ValidationType.TYPEOF); + const validationSomeCheckbox = EnumHandler.stringifyValidationType(ValidationType.SOMECHECKBOX); + const validationMaxAnswers = EnumHandler.stringifyValidationType(ValidationType.MAXANSWERS); const validationNone = EnumHandler.stringifyValidationType(ValidationType.NONE); expect(validationNone).to.be.equal(""); @@ -95,6 +114,9 @@ describe("Enum Handler", () => { expect(validationMandatory).to.be.equal("mandatory"); expect(validationMaxChar).to.be.equal("maxchar"); expect(validationMinChar).to.be.equal("minchar"); + expect(validationTypeOf).to.be.equal("typeof"); + expect(validationSomeCheckbox).to.be.equal("somecheckbox"); + expect(validationMaxAnswers).to.be.equal("maxanswers"); }); it("should parse string to ValidationType", () => { @@ -106,6 +128,14 @@ describe("Enum Handler", () => { const validationMaxCharyCapitalized = EnumHandler.parseValidationType("MAXCHAR"); const validationMinChar = EnumHandler.parseValidationType("minchar"); const validationMinCharyCapitalized = EnumHandler.parseValidationType("MINCHAR"); + const validationTypeOf = EnumHandler.parseValidationType("typeof"); + const validationTypeOfCapitalized = EnumHandler.parseValidationType("TYPEOF"); + const validationSomeCheckbox = EnumHandler.parseValidationType("somecheckbox"); + const validationSomeCheckboxCapitalized = EnumHandler.parseValidationType("SOMECHECKBOX"); + const validationMaxAnswers = EnumHandler.parseValidationType("maxanswers"); + const validationMaxAnswersCapitalized = EnumHandler.parseValidationType("MAXANSWERS"); + const validationDependency = EnumHandler.parseValidationType("dependency"); + const validationDependencyCapitalized = EnumHandler.parseValidationType("DEPENDENCY"); const validationNone = EnumHandler.parseValidationType(""); const validatioFOOL = EnumHandler.parseValidationType("fool"); @@ -117,6 +147,14 @@ describe("Enum Handler", () => { expect(validationMaxCharyCapitalized).to.be.equal(ValidationType.MAXCHAR); expect(validationMinChar).to.be.equal(ValidationType.MINCHAR); expect(validationMinCharyCapitalized).to.be.equal(ValidationType.MINCHAR); + expect(validationTypeOf).to.be.equal(ValidationType.TYPEOF); + expect(validationTypeOfCapitalized).to.be.equal(ValidationType.TYPEOF); + expect(validationSomeCheckbox).to.be.equal(ValidationType.SOMECHECKBOX); + expect(validationSomeCheckboxCapitalized).to.be.equal(ValidationType.SOMECHECKBOX); + expect(validationMaxAnswers).to.be.equal(ValidationType.MAXANSWERS); + expect(validationMaxAnswersCapitalized).to.be.equal(ValidationType.MAXANSWERS); + expect(validationDependency).to.be.equal(ValidationType.DEPENDENCY); + expect(validationDependencyCapitalized).to.be.equal(ValidationType.DEPENDENCY); expect(validationNone).to.be.equal(ValidationType.NONE); expect(validatioFOOL).to.be.equal(ValidationType.NONE); }); diff --git a/src/utils/enumHandler.ts b/src/utils/enumHandler.ts index fdc1a2a9a64b93915f6562fc7fe5ab0e6172a35e..b7d2600f797f8a4eb2ae23a2c76d2944faf3bf68 100644 --- a/src/utils/enumHandler.ts +++ b/src/utils/enumHandler.ts @@ -17,14 +17,17 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ + */ /** * Available Input types */ export enum InputType { - /** Text type, when input is a text */ + /** Text type, when input is a text */ TEXT, + CHECKBOX, + RADIO, + SELECT, NONE } @@ -37,35 +40,49 @@ export enum UpdateType { } /** - * Available Validation types + * Available Validation types. */ export enum ValidationType { - /** Used as error code. No suitable validation found. */ - NONE, - /** Regex type, when input need a regex to validate */ - REGEX, - /** Mandatory type, when input is mandatory */ - MANDATORY, - /** MaxChars type, when input has maximum char limit */ - MAXCHAR, - /** MinChars type, when input has minimum char limit */ - MINCHAR + /** Used as error code. No suitable validation found. */ + NONE, + /** Regex type, when input need a regex to validate. */ + REGEX, + /** Mandatory type, when input is mandatory. */ + MANDATORY, + /** MaxChars type, when input has maximum char limit. */ + MAXCHAR, + /** MinChars type, when input has minimum char limit. */ + MINCHAR, + /** TypeOf type, when input needs to be a certain type. */ + TYPEOF, + /** SomeCheckbox type, when at least one checkbox must be selected. */ + SOMECHECKBOX, + /** MAXANSWERS type, when input has maximum answer limit. */ + MAXANSWERS, + /** DEPENDENCY type, when input depends on another input. */ + DEPENDENCY } /** - * Enum's handler. Manage parse through the project. + * ENUM's handler. Manage parse through the project. */ export class EnumHandler { /** * Parse an enum(Input type) to string. * @param a - Input type to be stringified. - * @returns - Input Type as string + * @returns - Input Type as string. */ public static stringifyInputType(a: InputType): string { switch (a) { case InputType.TEXT: return "text"; + case InputType.CHECKBOX: + return "checkbox"; + case InputType.RADIO: + return "radio"; + case InputType.SELECT: + return "select"; default: return ""; } @@ -81,6 +98,12 @@ export class EnumHandler { switch (str) { case "text": return InputType.TEXT; + case "checkbox": + return InputType.CHECKBOX; + case "radio": + return InputType.RADIO; + case "select": + return InputType.SELECT; default: return InputType.NONE; } @@ -101,6 +124,14 @@ export class EnumHandler { return "maxchar"; case ValidationType.MINCHAR: return "minchar"; + case ValidationType.TYPEOF: + return "typeof"; + case ValidationType.SOMECHECKBOX: + return "somecheckbox"; + case ValidationType.MAXANSWERS: + return "maxanswers"; + case ValidationType.DEPENDENCY: + return "dependency"; default: return ""; } @@ -121,6 +152,14 @@ export class EnumHandler { return ValidationType.MAXCHAR; case "minchar": return ValidationType.MINCHAR; + case "typeof": + return ValidationType.TYPEOF; + case "somecheckbox": + return ValidationType.SOMECHECKBOX; + case "maxanswers": + return ValidationType.MAXANSWERS; + case "dependency": + return ValidationType.DEPENDENCY; default: return ValidationType.NONE; } diff --git a/src/utils/formQueryBuilder.ts b/src/utils/formQueryBuilder.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d906b3fdb329a507cd97be12c324ca05797ec09 --- /dev/null +++ b/src/utils/formQueryBuilder.ts @@ -0,0 +1,1009 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { eachSeries, map, waterfall } from "async"; +import { Pool, PoolConfig, QueryResult } from "pg"; +import { Form, FormOptions } from "../core/form"; +import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; +import { Input, InputOptions, Sugestion, Validation } from "../core/input"; +import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; +import { EnumHandler, InputType, UpdateType, ValidationType } from "./enumHandler"; +import { ErrorHandler} from "./errorHandler"; +import { OptHandler } from "./optHandler"; +import { QueryBuilder, QueryOptions } from "./queryBuilder"; +import { Sorter } from "./sorter"; + +/** Paramenters used to create a temporary Validation */ +interface ValidationTmp { + /** Validation identifier */ + id: number; + /** Input id */ + inputId: number; + /** Validation of a input */ + validation: Validation; +} + +/** + * Class used to manage all Form operations into the database. + * This operations include read, write and update data. + */ +export class FormQueryBuilder extends QueryBuilder { + + constructor(pool: Pool) { + super(pool); + } + + /** + * Asynchronously list all forms using a transaction. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - list of form or a empty list if there is no form on database. + */ + public list(cb: (err: Error, result?: Form[]) => void) { + + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin((error: Error, results?: QueryResult) => { + callback(error); + }); + }, + + (callback: (err: Error, result?: Form[]) => void) => { + this.executeListForms((error: Error, results?: Form[]) => { + callback(error, results); + }); + }, + + (forms: Form[], callback: (err: Error, result?: Form[]) => void) => { + this.commit((error: Error, results?: QueryResult) => { + callback(error, forms); + }); + } + + ], (err, forms?: Form[]) => { + if (err) { + this.rollback((error: Error, results?: QueryResult) => { + cb(err); + }); + } + cb(null, forms); + }); + } + + /** + * Asynchronously get all forms from database without transactions. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - list of form or a empty list if there is no form on database. + */ + private executeListForms(cb: (err: Error, result?: Form[]) => void) { + const queryString: string = "SELECT id, title, description FROM form;"; + const query: QueryOptions = { + query: queryString + , parameters: [] + }; + + const forms: Form[] = []; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + + for (const row of result.rows) { + const formOpt: FormOptions = { + id: row["id"] + , title: row["title"] + , description: row["description"] + , inputs: [] + }; + + const formTmp: Form = new Form(formOpt); + + forms.push(formTmp); + } + + cb(null, forms); + }); + + } + + /** + * Asynchronously read a form from database. + * @param id - Form identifier to be founded. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + public read(id: number, cb: (err: Error, form?: Form) => void) { + + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin((error: Error, results?: QueryResult) => { + callback(error); + }); + }, + + (callback: (err: Error, result?: Form) => void) => { + this.readController(id, (error: Error, resultForm?: Form) => { + callback(error, resultForm); + }); + }, + + (form: Form, callback: (err: Error, result?: Form) => void) => { + this.commit((error: Error, results?: QueryResult) => { + callback(error, form); + }); + } + + ], (err, form?: Form) => { + if (err) { + this.rollback((error: Error, results?: QueryResult) => { + cb(err); + }); + return; + } + cb(null, form); + }); + } + + /** + * Asynchronously read a form from database without transactions. + * @param id - Form identifier to be founded. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + private readController(id: number, cb: (err: Error, form?: Form) => void) { + waterfall([ + (callback: (err: Error, result?: Form) => void) => { + this.executeReadForm(id, (error: Error, result: QueryResult) => { + + if (result.rowCount !== 1) { + callback(ErrorHandler.badIdAmount(result.rowCount)); + return; + } + + const formTmp: Form = new Form ({ + id: result.rows[0]["id"] + , title: result.rows[0]["title"] + , description: result.rows[0]["description"] + , inputs: [] + }); + + callback(null, formTmp); + }); + }, + (form: Form, callback: (err: Error, form?: Form, resultValidations?: ValidationTmp[]) => void) => { + this.executeReadValidation(id, (error: Error, result?: QueryResult) => { + + if (error) { + callback(error); + return; + } + + const arrayValidationTmp: ValidationTmp[] = []; + let validationTmp: any; + for (const i of result.rows) { + validationTmp = { + id: i["id"] + , inputId: i["id_input"] + , validation: { + type: EnumHandler.parseValidationType(i["validation_type"]) + , arguments: [] + } + }; + arrayValidationTmp.push(validationTmp); + } + + callback(null, form, Sorter.sortById(arrayValidationTmp)); + }); + }, + (form: Form, validations: ValidationTmp[], callback: (err: Error, form?: Form, resultValidations?: ValidationTmp[]) => void) => { + + this.executeReadArgument(id, (error: Error, result?: QueryResult) => { + + if (error) { + callback(error); + return; + } + + const sortedResults: any = Sorter.sortById(result.rows); + let k: number = 0; + let i: number = 0; + while ((i < validations.length) && (k < sortedResults.length)) { + if (validations[i].id === sortedResults[k].id_validation) { + validations[i].validation.arguments.push(sortedResults[k].argument); + k++; + } + else { + i++; + } + } + callback(null, form, validations); + }); + }, + (form: Form, validations: ValidationTmp[], callback: (err: Error, form?: Form, resultInputs?: Input[]) => void) => { + this.executeReadInput(id, (error: Error, result: QueryResult) => { + + if (error) { + callback(error); + return; + } + + const validationArray: any = Sorter.sortByInputId(validations); + const inputArrayTmp: Input[] = []; + let inputTmp: InputOptions; + + for (const i of result.rows) { + inputTmp = { + id: i["id"] + , placement: i["placement"] + , description: i["description"] + , question: i["question"] + , type: EnumHandler.parseInputType(i["input_type"]) + , enabled: i["enabled"] + , validation: [] + , sugestions: [] + }; + inputArrayTmp.push(new Input(inputTmp)); + } + + let j: number = 0; + let k: number = 0; + while ((j < inputArrayTmp.length) && (k < validationArray.length)) { + if (inputArrayTmp[j].id === validationArray[k].inputId){ + inputArrayTmp[j].validation.push(validationArray[k].validation); + k++; + } else { + j++; + } + } + + callback(null, form, Sorter.sortByPlacement(inputArrayTmp)); + }); + }, + (form: Form, inputs: Input[], callback: (err: Error, form?: Form) => void) => { + this.executeReadSugestion(id, (error: Error, result: QueryResult) => { + + if (error) { + callback(error); + return; + } + + let i: number = 0; + let k: number = 0; + while ((i < inputs.length) && (k < result.rows.length)) { + if (inputs[i].id === result.rows[k]["id_input"]) { + inputs[i].sugestions.push({value: result.rows[k]["value"], placement: result.rows[k]["placement"]}); + k++; + } else { + i++; + } + } + + for (const j of inputs) { + form.inputs.push(j); + } + + callback(null, form); + }); + } + ], (err, result?: Form) => { + if (err) { + cb(err); + return; + } + cb(null, result); + }); + } + + /** + * Asynchronously read a form without transactions. + * @param id - Form identifier to be founded. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + private executeReadForm(id: number, cb: (err: Error, form?: QueryResult) => void) { + const queryString: string = "SELECT id, title, description FROM form WHERE id=$1;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err, result); + }); + } + + /** + * Asynchronously read inputs from database without transactions. + * @param id - Form identifier which inputs are linked to. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + private executeReadInput(id: number, cb: (err: Error, result?: QueryResult) => void) { + const queryString: string = "SELECT input.id, id_form, placement, input_type, question, \ + enabled, input.description FROM form f \ + INNER JOIN input ON f.id=id_form \ + WHERE f.id=$1 AND enabled=true ORDER BY input.id;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err, result); + }); + } + + /** + * Asynchronously read validations from database without transactions. + * @param id - Form identifier which validations from inputs are linked to. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + private executeReadValidation(id: number, cb: (err: Error, result?: QueryResult) => void) { + const queryString: string = "SELECT input_validation.id, id_input, validation_type FROM form f \ + INNER JOIN input ON f.id=id_form \ + INNER JOIN input_validation ON input.id=id_input \ + WHERE f.id=$1 AND enabled=true;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err, result); + }); + } + + /** + * Asynchronously read arguments from database without transactions. + * @param id - Form identifier which arguments from validatation are linked to. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + private executeReadArgument(id: number, cb: (err: Error, result?: QueryResult) => void) { + const queryString: string = "SELECT input_validation_argument.id, id_input_validation AS id_validation, \ + input_validation_argument.placement, argument FROM form f\ + INNER JOIN input i ON f.id=id_form \ + INNER JOIN input_validation iv ON i.id=id_input \ + INNER JOIN input_validation_argument ON iv.id=id_input_validation \ + WHERE f.id=$1 AND enabled=true;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err, result); + }); + } + + /** + * Asynchronously read sugestions from database without transactions. + * @param id - Form identifier wich arguments from inputs are linked to. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + private executeReadSugestion(id: number, cb: (err: Error, result?: QueryResult) => void) { + const queryString: string = "SELECT input_sugestion.id, id_input, value, input_sugestion.placement \ + FROM form f \ + INNER JOIN input ON f.id=id_form \ + INNER JOIN input_sugestion ON input.id=id_input \ + WHERE f.id=$1 AND enabled=true ORDER BY input.id;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err, result); + }); + } + + /** + * Asynchronously write a form on database. + * @param form - Form to be inserted. + * @param cb - Callback function which contains the inserted data. + * @param cb.err - Error information when the method fails. + * @param cb.formResult - Form or null if form any error occurs. + */ + public write(form: Form, cb: (err: Error, form?: Form) => void) { + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin((error: Error, results?: QueryResult) => { + callback(error); + }); + }, + (callback: (err: Error, result?: number) => void) => { + this.writeController(form, (error: Error, resultId?: number) => { + callback(error, resultId); + }); + }, + (formId: number, callback: (err: Error, result?: number) => void) => { + this.commit((error: Error, results?: QueryResult) => { + callback(error, formId); + }); + }, + (formId: number, callback: (err: Error, result?: Form) => void) => { + this.read(formId, (error: Error, resultForm?: Form) => { + callback(error, resultForm); + }); + } + ], (err, formResult?: Form) => { + if (err) { + this.rollback((error: Error, results?: QueryResult) => { + cb(err); + }); + return; + } + cb(null, formResult); + }); + } + + /** + * Asynchronously write a form on database without transactions. + * @param form - Form to be inserted. + * @param cb - Callback function which contains the inserted data. + * @param cb.err - Error information when the method fails. + * @param cb.formId - Form identifier or null if form any error occurs. + */ + private writeController(form: Form, cb: (Err: Error, formId?: number) => void) { + waterfall([ + (callback: (err: Error, result?: number) => void) => { + this.executeWriteForm(form, (error: Error, resultId?: number) => { + callback(error, resultId); + }); + }, + (formId: number, callback: (err: Error, result?: number) => void) => { + this.writeInputController(formId, form.inputs, (error: Error) => { + callback(error, formId); + }); + } + ], (err, id: number) => { + if (err) { + cb(err); + } + cb(null, id); + }); + + } + + /** + * Asynchronously write a input on database without transactions. + * @param formId - Form identifier which inputs are linked to. + * @param inputs - A list of inputs to be inserted. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + */ + private writeInputController(formId: number, inputs: Input[], cb: (err: Error) => void) { + eachSeries(inputs, (input, outerCallback) => { + waterfall([ + (callback: (err: Error, result?: number) => void) => { + this.executeWriteInput(formId, input, (error: Error, resultInputId?: number) => { + if (error) { + callback(error); + return; + } + callback(null, resultInputId); + }); + }, + (inputId: number, callback: (err: Error, resultInputId?: number) => void) => { + this.writeValidationController(inputId, input.validation, (error: Error) => { + callback(error, inputId); + }); + }, + (inputId: number, callback: (err: Error) => void) => { + this.writeSugestionController(inputId, input.sugestions, (error: Error) => { + callback(error); + }); + } + ], (err) => { + if (err) { + outerCallback(err); + return; + } + outerCallback(null); + }); + }, (e) => { + cb(e); + }); + } + + /** + * Asynchronously write a validation on database without transactions. + * @param inputId - Input identifier which validations are linked to. + * @param validations - A list of validations to be inserted. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + */ + private writeValidationController(inputId: number, validations: Validation[], cb: (err: Error) => void) { + let i: number; + + eachSeries(validations, (validation, callback) => { + this.executeWriteValidation(inputId, validation, (err: Error, validationId?: number) => { + i = 0; + eachSeries(validation.arguments, (argument, innerCallback) => { + this.executeWriteArgument(validationId, argument, i++, innerCallback); + }, (error) => { + callback(error); + }); + }); + }, (err) => { + cb(err); + }); + } + + /** + * Asynchronously write a sugestion on database without transactions. + * @param inputId - Input identifier which sugestions are linked to. + * @param sugestions - A list of sugestions to be inserted. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + */ + private writeSugestionController(inputId: number, sugestions: Sugestion[], cb: (err: Error) => void) { + eachSeries(sugestions, (sugestion, callback) => { + this.executeWriteSugestion(inputId, sugestion, callback); + }, (err) => { + cb(err); + }); + } + + /** + * Asynchronously insert a form on database without transactions. + * @param form - Form to be inserted. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + * @param cb.result - Form identifier or null if any error occurs. + */ + private executeWriteForm(form: Form, cb: (err: Error, result?: number) => void) { + const queryString: string = "INSERT INTO form (title, description) \ + VALUES ($1, $2) \ + RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + form.title + , form.description + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null, result.rows[0]["id"]); + }); + } + + /** + * Asynchronously insert a input on database without transactions. + * @param formId - Form identifier which input are linked to. + * @param input - A input to be inserted. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + * @param cb.resultId - Input identifier or null if any error occurs. + */ + private executeWriteInput(formId: number, input: Input, cb: (err: Error, resultId?: number) => void) { + const queryString: string = "INSERT INTO input (id_form, placement, input_type, enabled, question, description) \ + VALUES ($1, $2, $3, $4, $5, $6) \ + RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + formId + , input.placement + , EnumHandler.stringifyInputType(input.type) + , true + , input.question + , input.description + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err, result.rows[0]["id"]); + }); + } + + /** + * Asynchronously insert a validation on database without transactions. + * @param inputId - Input identifier which validations are linked to. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + * @param cb.result - Validation identifier or null if any error occurs. + */ + private executeWriteValidation(inputId: number, validation: Validation, cb: (err: Error, result?: number) => void) { + const queryString: string = "INSERT INTO input_validation (id_input, validation_type) \ + VALUES ($1, $2) \ + RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + inputId + , EnumHandler.stringifyValidationType(validation.type) + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err, result.rows[0]["id"]); + }); + } + + /** + * Asynchronously insert a argument on database without transactions. + * @param validationId - Validation identifier which argument are linked to. + * @param argument - The argument string to be inserted. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + */ + private executeWriteArgument(validationId: number, argument: string, placement: number, cb: (err: Error) => void) { + const queryString: string = "INSERT INTO input_validation_argument (id_input_validation, argument, placement) \ + VALUES ($1, $2, $3) \ + RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + validationId + , argument + , placement + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err); + }); + } + + /** + * Asynchronously insert a sugestion on database without transactions. + * @param inputId - Input identifier which sugestion are linked to. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + * @param cb.result - Form identifier or null if any error occurs. + */ + private executeWriteSugestion(inputId: number, sugestion: Sugestion, cb: (err: Error) => void) { + const queryString: string = "INSERT INTO input_sugestion (id_input, value, placement) \ + VALUES ($1, $2, $3);"; + const query: QueryOptions = { + query: queryString + , parameters: [ + inputId + , sugestion.value + , sugestion.placement + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err); + }); + } + + /** + * Asynchronously update a form on database. + * @param form - Form to be updated. + * @param cb - Callback function which contains information about method's execution. + * @param cb.err - Error information when the method fails. + */ + public update(formUpdate: FormUpdate, cb: (err: Error) => void) { + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin((error: Error, results?: QueryResult) => { + callback(error); + }); + }, + (callback: (err: Error, result?: Form) => void) => { + this.updateController(formUpdate, (error: Error, resultForm?: Form) => { + callback(error, resultForm); + }); + }, + (form: Form, callback: (err: Error, result?: Form) => void) => { + this.commit((error: Error, results?: QueryResult) => { + callback(error, form); + }); + } + ], (err) => { + if (err) { + this.rollback((error: Error, results?: QueryResult) => { + cb(err); + }); + return; + } + cb(null); + }); + } + + /** + * Asynchronously update a form on database without transactions. + * @param formUpdate - FormUpdate object that contains the data to update. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + */ + private updateController(formUpdate: FormUpdate, cb: (err: Error) => void) { + waterfall([ + // Update form fields on database + (callback: (err: Error) => void) => { + if (formUpdate.changed) { + this.updateFormController(formUpdate.form, (error: Error) => { + callback(error); + }); + } else { + callback(null); + } + }, + // Update inputs on database + (callback: (err: Error, formUpdateResult?: FormUpdate) => void) => { + this.updateInputsController(formUpdate.form.id, formUpdate.inputUpdates, (error: Error, inputUpdateResult: InputUpdate[]) => { + if (error) { + callback(error); + return; + } + + const formUpdateTmp: FormUpdate = { + id: formUpdate.id + , form: formUpdate.form + , updateDate: formUpdate.updateDate + , changed: formUpdate.changed + , inputUpdates: inputUpdateResult + }; + + callback(null, formUpdateTmp); + }); + }, + // Write formUpdate on database + (formUpdateTmp: FormUpdate, callback: (err: Error, formUpdateTmp?: FormUpdate, resultId?: number) => void) => { + this.executeWriteFormUpdate(formUpdateTmp, (error: Error, formUpdateResultId?: number) => { + if (error) { + callback(error); + return; + } + callback(null, formUpdateTmp, formUpdateResultId); + }); + }, + // Write inputUpdate on database + (formUpdateTmp: FormUpdate, formUpdateId: number, callback: (err: Error) => void) => { + eachSeries(formUpdateTmp.inputUpdates, (inputUpdate, innerCallback) => { + this.executeWriteInputUpdate(formUpdateId, inputUpdate, innerCallback); + }, (err) => { + callback(err); + }); + } + ], (err) => { + cb(err); + }); + } + + /** + * Asynchronously update a form on database. + * @param form - Form to be updated. + * @param cb - Callback function which contains information about method's execution. + * @param cb.err - Error information when the method fails. + */ + private updateFormController(form: Form, cb: (err: Error) => void) { + waterfall([ + (callback: (err: Error) => void) => { + this.executeUpdateForm(form.title, form.id, "title", callback); + }, + (callback: (err: Error) => void) => { + this.executeUpdateForm(form.description, form.id, "description", callback); + } + ], (error) => { + cb(error); + }); + } + + /** + * Asynchronously update a list of inputs on database. + * @param formId - Form identifier which update are linked to. + * @param inputUpdates - InputUpdate array which contains the update information. + * @param cb - Callback function which contains information about method's execution. + * @param cb.err - Error information when the method fails. + * @param inputUpdateResult - InputUpdate or null if method fails. + */ + private updateInputsController(formId: number, inputUpdates: InputUpdate[], cb: (err: Error, inputUpdateResult?: InputUpdate[]) => void) { + + const inputUpdatesTmp: InputUpdate[] = []; + + eachSeries(inputUpdates, (inputUpdate, callback) => { + switch (inputUpdate.inputOperation) { + case UpdateType.ADD: { + this.executeWriteInput(formId, inputUpdate.input, (err: Error, id: number) => { + + if (err) { + callback(err); + return; + } + + const inputOpt: InputOptions = inputUpdate.input; + inputOpt.id = id; + + const inputUpdateOpt: InputUpdateOptions = { + input: inputOpt + , inputOperation: UpdateType.ADD + , value: null + }; + inputUpdatesTmp.push(new InputUpdate(inputUpdateOpt)); + callback(null); + }); + break; + } + case UpdateType.REMOVE: { + // Set enabled option in database as false + this.executeUpdateInput(0, inputUpdate.input.id, "enabled", (err: Error) => { + + if (err) { + callback(err); + return; + } + + inputUpdatesTmp.push(inputUpdate); + callback(null); + }); + break; + } + case UpdateType.SWAP: { + // Update placement option in database of a input + this.executeUpdateInput(inputUpdate.input.placement, inputUpdate.input.id, "placement", (err: Error) => { + + if (err) { + callback(err); + return; + } + + inputUpdatesTmp.push(inputUpdate); + callback(null); + }); + break; + } + case UpdateType.REENABLED: { + // Set enabled option in database as true + this.executeUpdateInput(1, inputUpdate.input.id, "enabled", (err: Error) => { + + if (err) { + callback(err); + return; + } + + inputUpdatesTmp.push(inputUpdate); + callback(null); + }); + break; + } + default: { + callback(new Error ("Operation " + inputUpdate.inputOperation + " not recognized")); + break; + } + } + }, (error) => { + if (error) { + cb(error); + return; + } + cb(null, inputUpdatesTmp); + }); + } + + /** + * Asynchronously update form's fields on database. + * @param value - A string to be inserted in the database. + * @param id - The form id that should be updated. + * @param field - The field on database that should be updated. + * @param cb - Callback function. + * @param cb.err - Error information when method fails. + */ + private executeUpdateForm(value: string, id: number, field: string, cb: (err: Error) => void) { + const queryString: string = "UPDATE form SET " + field + " = $1 WHERE id = $2"; + const query: QueryOptions = { + query: queryString + , parameters: [ + value + , id + ] + }; + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err); + }); + } + + /** + * Asynchronously update a input field on database. + * @param value - A number to be inserted in the database. + * @param id - The input id that should be updated. + * @param field - The field on database that should be updated. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private executeUpdateInput(value: number, id: number, field: string, cb: (err: Error) => void) { + const queryString: string = "UPDATE input SET " + field + " = $1 WHERE id = $2"; + const query: QueryOptions = { + query: queryString + , parameters: [ + value + , id + ] + }; + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err); + }); + } + + /** + * Asynchronously insert a formUpdate on database. + * @param formUpdate - Form Update to be inserted. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + * @param cb.formUpdateId - The id of the inserted FormUpdate. + */ + private executeWriteFormUpdate(formUpdate: FormUpdate, cb: (err: Error, formUpdateId?: number) => void) { + + const queryString: string = "INSERT INTO form_update (id_form, update_date) \ + VALUES ( $1, $2 ) \ + RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + formUpdate.form.id + , formUpdate.updateDate + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null, result.rows[0].id); + }); + } + + /** + * Asynchronously insert a inputUpdate on database. + * @param inputUpdate - Input Update to be inserted. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private executeWriteInputUpdate(idFormUpdate: number, inputUpdate: InputUpdate, cb: (err: Error) => void) { + + const queryString: string = "INSERT INTO input_update (id_form_update, id_input, input_operation_id, value) \ + VALUES ( $1, $2, $3, $4 );"; + const query: QueryOptions = { + query: queryString + , parameters: [ + idFormUpdate + , inputUpdate.input.id + , inputUpdate.inputOperation + , inputUpdate.value + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err); + }); + } +} diff --git a/src/utils/optHandler.spec.ts b/src/utils/optHandler.spec.ts index 19a542f5fa1ea2a2dd26a59d5c4b55ebc0fd6f9f..5170308e67bcbe00450de8d431b5dece573fced4 100644 --- a/src/utils/optHandler.spec.ts +++ b/src/utils/optHandler.spec.ts @@ -1347,4 +1347,56 @@ describe("Options Handler", () => { expect(updateTmp).to.be.a("undefined"); } }); + + it("should get error Input with malformed sugestion missing key 'placement'", () => { + + const inputObj: any = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [] + , sugestions: [ + { value: "Malformed Sugestion" } + , { value: "Sugestion", placement: 0 } + ] + , id: 1 + }; + + let inputTmp: Input; + + try { + inputTmp = new Input(OptHandler.input(inputObj)); + } catch (e) { + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Sugestion placement").message); + expect(inputTmp).to.be.a("undefined"); + } + }); + + it("should get error Input with malformed sugestion missing key 'value'", () => { + + const inputObj: any = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [] + , sugestions: [ + { placement: 0} + , { value: "Sugestion", placement: 1 } + ] + , id: 1 + }; + + let inputTmp: Input; + + try { + inputTmp = new Input(OptHandler.input(inputObj)); + } catch (e) { + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Sugestion value").message); + expect(inputTmp).to.be.a("undefined"); + } + }); }); diff --git a/src/utils/optHandler.ts b/src/utils/optHandler.ts index 36e8d46e3086bc427dc27ecb2861d54bc77e8ec2..e2bc778c2930359b0de2b7fc2b55609a92f11e4c 100644 --- a/src/utils/optHandler.ts +++ b/src/utils/optHandler.ts @@ -19,23 +19,23 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - import { Form, FormOptions } from "../core/form"; - import { FormAnswerOptions } from "../core/formAnswer"; - import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; - import { InputOptions, Validation } from "../core/input"; - import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; - import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; - import { InputType, UpdateType} from "./enumHandler"; - import { ErrorHandler} from "./errorHandler"; +import { Form, FormOptions } from "../core/form"; +import { FormAnswerOptions } from "../core/formAnswer"; +import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; +import { InputOptions, Sugestion, Validation } from "../core/input"; +import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; +import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; +import { InputType, UpdateType} from "./enumHandler"; +import { ErrorHandler} from "./errorHandler"; /** * OptHandler to handle an object and transform into a Classoptions to be used in Class's constructor */ - export class OptHandler { - /** - * Return an FormOptions instance with a parsed object, The main objective is parse any error previously - * @param obj - object that should be parsed. - * @returns - An FormOptions instance. - */ +export class OptHandler { + /** + * Return an FormOptions instance with a parsed object, The main objective is parse any error previously + * @param obj - object that should be parsed. + * @returns - An FormOptions instance. + */ public static form(obj: any): FormOptions{ if (obj.title === undefined ){ @@ -89,9 +89,16 @@ type: obj.type, validation: obj.validation.map((v: any) => { return {type: v.type, arguments: v.arguments}; - }) + }), + sugestions: [] }; + if (obj.sugestions instanceof Array) { + option.sugestions = obj.sugestions.map((v: any) => { + return OptHandler.sugestion(v); + }); + } + return option; } @@ -117,7 +124,6 @@ inputsAnswerOptionsTmp[parseInt(key, 10)] = obj.inputsAnswerOptions[parseInt(key, 10)].map( (i: InputAnswerOptions) => { return OptHandler.inputAnswer(i); }); - } const option: FormAnswerOptions = { @@ -157,7 +163,7 @@ } /** - * Return an FormUpdateOptions instance with a parsed and validated object, The main objective is parse any error previously + * Return an FormUpdateOptions instance with a parsed and validated object. The main objective is parse any error previously * @param obj - object that should be parsed. * @returns - An FormUpdateOptions instance. */ @@ -180,7 +186,7 @@ } /** - * Return an FormUpdateOptions instance with a parsed and validated object, The main objective is to detect parsing errors previously + * Return an FormUpdateOptions instance with a parsed and validated object. The main objective is to detect parsing errors previously * @param obj - object that should be parsed. * @returns - An FormUpdateOptions instance. */ @@ -205,4 +211,25 @@ return option; } + /** + * Return a parsed and validated sugestion. + * @param obj - object that should be parsed. + * @returns - Sugestion instance. + */ + public static sugestion(obj: any): Sugestion { + + if (typeof(obj.value) !== "string") { + throw ErrorHandler.notFound("Sugestion value"); + } + if (typeof(obj.placement) !== "number") { + throw ErrorHandler.notFound("Sugestion placement"); + } + + const option: Sugestion = { + value: obj.value + , placement: obj.placement + }; + + return option; + } } diff --git a/src/utils/queryBuilder.ts b/src/utils/queryBuilder.ts new file mode 100644 index 0000000000000000000000000000000000000000..b8ef174b266c5d870265388a935f1643c4ead0f4 --- /dev/null +++ b/src/utils/queryBuilder.ts @@ -0,0 +1,93 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { Pool, PoolConfig, QueryResult } from "pg"; + +/** Parameters used to create a parametrized query, to avoid SQL injection */ +export interface QueryOptions { + /** Query string to execute */ + query: string; + /** Array of input. containing question */ + parameters: any[]; +} + +/** + * Class used to build and execute queries in the database. + * Querybuilder classes should be used to abstract the access of objects in the database. + */ +export abstract class QueryBuilder { + + /** Information used to connect with a PostgreSQL database. */ + private pool: Pool; + + /** + * Creates a new adapter with the database connection configuration. + * @param config - The information required to create a connection with the database. + */ + constructor(pool: Pool) { + this.pool = pool; + } + + /** + * Asynchronously executes a query and get its result. + * @param query - Query (SQL format) to be executed. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.result - Query result. + */ + public executeQuery(query: QueryOptions, cb: (err: Error, result?: QueryResult) => void): void { + + this.pool.connect((err, client, done) => { + + if (err) { + cb(err); + return; + } + + client.query(query.query, query.parameters, (error, result) => { + // call 'done()' to release client back to pool + done(); + cb(error, (result) ? result : null); + }); + }); + } + + /** + * Asynchronously ends a transaction + */ + public commit(cb: (err: Error, result?: QueryResult) => void) { + this.executeQuery({query: "COMMIT;", parameters: []}, cb); + } + + /** + * Asynchronously rollback a transaction + */ + public rollback(cb: (err: Error, result?: QueryResult) => void) { + this.executeQuery({query: "ROLLBACK;", parameters: []}, cb); + } + + /** + * Asynchronously starts a transaction + */ + public begin(cb: (err: Error, result?: QueryResult) => void) { + this.executeQuery({query: "BEGIN;", parameters: []}, cb); + } +} diff --git a/src/utils/sorter.ts b/src/utils/sorter.ts index 41414791a6318fd9066802c41d7d43ef6559ed10..64156fc714cfe63ece9a54e2f1c57f9febe5fc37 100644 --- a/src/utils/sorter.ts +++ b/src/utils/sorter.ts @@ -22,7 +22,7 @@ export class Sorter { /** - * A public method to return a array sorted by placement field + * A method to return a array sorted by placement field * @param array - Array with objects that have placement field * @returns - A sorted array by placement */ @@ -44,7 +44,7 @@ export class Sorter { } /** - * A public method to return a array sorted by id field + * A method to return a array sorted by id field * @param array - Array with objects that have id field * @returns - A sorted array by id */ @@ -64,4 +64,26 @@ export class Sorter { return sortedByIdArray; } + + /** + * A method to return a array sorted by inputId field + * @param array - Array with objects that have id field + * @returns - A sorted array by id + */ + public static sortByInputId(array: any[]): any[] { + + const sortedByInputIdArray: any[] = array.sort((obj1, obj2) => { + if (obj1["inputId"] > obj2["inputId"]) { + return 1; + } + + if (obj1["inputId"] < obj2["inputId"]) { + return -1; + } + + return 0; + }); + + return sortedByInputIdArray; + } } diff --git a/src/utils/validationError.ts b/src/utils/validationError.ts index f440f973c472edac87d2f20e45aca7a52ea8d53e..ea4e967cff9b142a9ba6487fec30b1a7104b6459 100644 --- a/src/utils/validationError.ts +++ b/src/utils/validationError.ts @@ -31,11 +31,11 @@ * ValidationError: Extends Error class * Has a dict that allow us to know which answer is invalide */ - export class ValidationError extends Error{ + export class ValidationError extends Error { /** A dict that allows user to know which Input Answer is invalid. */ public readonly validationDict: ValidationDict; - constructor(validationDict: ValidationDict, ...params: any[]){ + constructor(validationDict: ValidationDict, ...params: any[]) { super(...params); this.validationDict = validationDict; } diff --git a/src/utils/validationHandler.spec.ts b/src/utils/validationHandler.spec.ts index d526f647145ab5483979a418195556e42a3bbfda..996ade015ec5f63cd983f9d1b52d98e4ef92477c 100644 --- a/src/utils/validationHandler.spec.ts +++ b/src/utils/validationHandler.spec.ts @@ -48,11 +48,13 @@ describe("Validation Handler", () => { , 5: [inputAnswersOpt2] }; - const data: Date = new Date(2019, 6, 4); - dbhandler.readForm(2, (error: Error, form: Form) => { + const date: Date = new Date(2019, 6, 4); + dbhandler.form.read(2, (error: Error, form: Form) => { + expect(error).to.be.a("null"); + const formAnswerOptions: FormAnswerOptions = { form - , timestamp: data + , timestamp: date , inputsAnswerOptions: inputAnswerOptionsDict }; const formAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); @@ -83,7 +85,7 @@ describe("Validation Handler", () => { }; const data: Date = new Date(2019, 6, 4); - dbhandler.readForm(2, (error: Error, form: Form) => { + dbhandler.form.read(2, (error: Error, form: Form) => { const formAnswerOptions: FormAnswerOptions = { form , timestamp: data @@ -123,7 +125,7 @@ describe("Validation Handler", () => { , 3: [inputAnswersOpt3] }; const data: Date = new Date(2019, 6, 4); - dbhandler.readForm(1, (error: Error, form: Form) => { + dbhandler.form.read(1, (error: Error, form: Form) => { const formAnswerOptions: FormAnswerOptions = { form , timestamp: data @@ -165,7 +167,7 @@ describe("Validation Handler", () => { , 3: [inputAnswersOpt3] }; const data: Date = new Date(2019, 6, 4); - dbhandler.readForm(1, (error: Error, form: Form) => { + dbhandler.form.read(1, (error: Error, form: Form) => { const formAnswerOptions: FormAnswerOptions = { form , timestamp: data @@ -181,7 +183,158 @@ describe("Validation Handler", () => { } done(); }); + }); + + it("should test when input is number", (done) => { + const inputAnswerOpt1: InputAnswerOptions = { + idInput: 23 + , placement: 0 + , value: "Not a number" + }; + + const inputAnswerOpt2: InputAnswerOptions = { + idInput: 23 + , placement: 0 + , value: "23" + }; + + const inputAnswerOpt3: InputAnswerOptions = { + idInput: 23 + , placement: 0 + , value: "24" + }; + + const inputAnswerOpt4: InputAnswerOptions = { + idInput: 24 + , placement: 0 + , value: "25" + }; + + const inputAnswerOpt5: InputAnswerOptions = { + idInput: 25 + , placement: 0 + , value: "Not a Float" + }; + + const inputAnswerOpt6: InputAnswerOptions = { + idInput: 26 + , placement: 0 + , value: "1.83" + }; + const inputAnswerOpt7: InputAnswerOptions = { + idInput: 27 + , placement: 0 + , value: "Not a date" + }; + + const inputAnswerOpt8: InputAnswerOptions = { + idInput: 28 + , placement: 0 + , value: "02/02/2002" + }; + + const inputAnswerOpt9: InputAnswerOptions = { + idInput: 29 + , placement: 0 + , value: "Invalid argument causes invalid answer" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 23: [ + inputAnswerOpt1 + , inputAnswerOpt2 + , inputAnswerOpt3 + ] + , 24: [inputAnswerOpt4] + , 25: [inputAnswerOpt5] + , 26: [inputAnswerOpt6] + , 27: [inputAnswerOpt7] + , 28: [inputAnswerOpt8] + , 29: [inputAnswerOpt9] + }; + + dbhandler.form.read(7, (error: Error, form: Form) => { + const formAnswerOptions: FormAnswerOptions = { + form + , timestamp: new Date() + , inputsAnswerOptions: inputAnswerOptionsDict + }; + const formAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + try { + ValidationHandler.validateFormAnswer(formAnswer); + } catch (e) { + expect(e.validationDict["23"]).to.be.equal("Input answer must be a int;Number of input answers must be lower than 2"); + expect(e.validationDict["24"]).to.be.undefined; + expect(e.validationDict["25"]).to.be.equal("Input answer must be a float"); + expect(e.validationDict["26"]).to.be.undefined; + expect(e.validationDict["27"]).to.be.equal("Input answer must be a date"); + expect(e.validationDict["28"]).to.be.undefined; + expect(e.validationDict["29"]).to.be.equal("Input answer must be a invalid;Number of input answers must be lower than invalid;Must answer question with id 28 and placement invalid"); + } + done(); + }); }); + it("should test when input has sugestion", (done) => { + + const inputAnswerOpt1: InputAnswerOptions = { + idInput: 18 + , placement: 5 + , value: "Invalid Placement" + }; + + const inputAnswerOpt2: InputAnswerOptions = { + idInput: 19 + , placement: 1 + , value: "true" + }; + + const inputAnswerOpt3: InputAnswerOptions = { + idInput: 20 + , placement: 1 + , value: "true" + }; + + const inputAnswerOpt4: InputAnswerOptions = { + idInput: 21 + , placement: 0 + , value: "true" + }; + + const inputAnswerOpt5: InputAnswerOptions = { + idInput: 22 + , placement: 0 + , value: "Answer question 5 form 6" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 18: [ + inputAnswerOpt1 + ] + , 19: [inputAnswerOpt2] + , 20: [inputAnswerOpt3] + , 21: [inputAnswerOpt4] + , 22: [inputAnswerOpt5] + }; + + dbhandler.form.read(6, (error: Error, form: Form) => { + const formAnswerOptions: FormAnswerOptions = { + form + , timestamp: new Date() + , inputsAnswerOptions: inputAnswerOptionsDict + }; + const formAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + try { + ValidationHandler.validateFormAnswer(formAnswer); + } catch (e) { + expect(e.validationDict["18"]).to.be.equal("Input answer must have a answer"); + expect(e.validationDict["19"]).to.be.undefined; + expect(e.validationDict["20"]).to.be.undefined; + expect(e.validationDict["21"]).to.be.undefined; + expect(e.validationDict["22"]).to.be.equal("Must answer question with id 18 and placement 2"); + } + done(); + }); + }); }); diff --git a/src/utils/validationHandler.ts b/src/utils/validationHandler.ts index d04a2d4db0c3359b06fc90c7edb63fa3c4683ae6..85db98349cd54843fcc6c7d02918a1d59b828eaa 100644 --- a/src/utils/validationHandler.ts +++ b/src/utils/validationHandler.ts @@ -21,6 +21,7 @@ import { FormAnswer } from "../core/formAnswer"; import { Input } from "../core/input"; +import { InputAnswer, InputAnswerDict } from "../core/inputAnswer"; import { ValidationType } from "./enumHandler"; import { ValidationDict, ValidationError } from "./validationError"; @@ -33,93 +34,220 @@ export class ValidationHandler { * Validate a string according given a regex. * @param answer - Answer to be validated. * @param regex - Regex to validate answer. - * @returns - true if answer match regex, else false. + * @returns - True if answer match regex, else false. */ - private static validateByRegex(answer: string, regex: string): boolean{ + private static validateByRegex(answer: string, regex: string): boolean { const regexp = new RegExp(regex); return regexp.test(answer); } /** - * Validate if is null, undefined nor "" + * Validate if is null, undefined nor "". * @param answer - answer to be validated. - * @returns - true if not null, "" nor undefined, else false. + * @returns - True if not null, "" nor undefined, else false. */ - private static validateMandatory(answer: string): boolean{ + private static validateMandatory(answer: string): boolean { return ((!answer) === false); } /** - * Validate if answer has minimum number of chars + * Validate if answer has minimum number of chars. * @param answer - Answer to be validated. * @param size - Minimum size that answer should have. - * @returns - true if has at least Size chars, else false. + * @returns - True if has at least Size chars, else false. */ - private static validateMinChar(answer: string, size: string): boolean{ - return (answer !== null && answer !== undefined && parseInt(size, 10) <= answer.length); + private static validateMinChar(answer: string, size: string): boolean { + return (answer !== null && answer !== undefined && parseInt(size, 10) <= answer.length); } /** - * Validate if answer has minimum number of chars + * Validate if answer has minimum number of chars. * @param answer - Answer to be validated. * @param size - Maximum size that answer should have. - * @returns - true if has at max Size chars, else false. + * @returns - True if has at max Size chars, else false. */ - private static validateMaxChar(answer: string, size: string): boolean{ - return (answer !== null && answer !== undefined && parseInt(size, 10) >= answer.length); + private static validateMaxChar(answer: string, size: string): boolean { + return (answer !== null && answer !== undefined && parseInt(size, 10) >= answer.length); } /** - * Validate if answer has minimum number of chars + * Validate if answer is of a determined type. + * @param answer - Answer to be validated. + * @param type - Type that answer should be. + * @returns - True if it is of the determined type, else false. + */ + private static validateTypeOf(answer: string, type: string): boolean { + // Using string here to avoid validate validations + if (type === "int") { + return(!isNaN(parseInt(answer, 10))); + } else if (type === "float") { + return(!isNaN(parseFloat(answer))); + } else if (type === "date") { + return((new Date(answer)).toString() !== "Invalid Date"); + } else { + return(false); + } + } + + /** + * Validate if answer has minimum one checkbox checked. + * @param input - Input that checkbox belongs to. + * @param inputAnswer - Answers to checkbox. + * @returns - true if has at minimum one checkbox marked, else false. + */ + private static validateSomeCheckbox(input: Input, inputAnswers: InputAnswerDict): boolean { + let result: boolean = false; + for (const answer of inputAnswers[input.id]) { + if ((answer.value === "true") && this.inputSugestionExists(input, answer.placement)) { + result = true; + } + } + return result; + } + + /** + * Validate if a sugestion exists. + * @param input - Input that have sugestions to be verified. + * @param placement - Value of answer to be verified. + * @returns - True if sugestion exists, else false. + */ + private static inputSugestionExists(input: Input, placement: number): boolean { + let result: boolean = false; + for (const sugestion of input.sugestions) { + if (sugestion.placement === placement) { + result = true; + } + } + return result; + } + + /** + * Validate if a input has a minimum number of answers. + * @param inputAnswers - Dictionary of InputAnswers to be verified. + * @param id - Input to be searched. + * @param argument - Max number of answers. + * @returns - True if has minimum answers, else false. + */ + private static validateMaxAnswers(inputAnswers: InputAnswerDict, id: number, argument: string): boolean { + const max: number = parseInt(argument, 10); + // Verify if argument is an integer + if (!(isNaN(max))) { + return (inputAnswers[id].length <= max); + } else { + return false; + } + } + + /** + * Validate if exists a answer for a dependent input. + * @param inputAnswers - Dictionary of InputAnswers to be verified. + * @param argument - Placement of the dependent input. + * @returns - True if the input was answered, else false. + */ + private static validateDependency(inputAnswers: InputAnswer[], argument: string): boolean { + let result: boolean = false; + const placement: number = parseInt(argument, 10); + if (!(isNaN(placement))) { + for (const inputAnswer of inputAnswers) { + if (inputAnswer.placement === placement) { + result = (inputAnswer.value === "true"); + } + } + } + return result; + } + + /** + * Validate if answer has minimum number of chars. * @param input - Input to validate answer. - * @param answer - Answer of input + * @param answer - Answer of input. * @returns - A string with all errors. */ - private static validateInput(input: Input, answer: string): string{ + private static validateInput(input: Input, inputAnswers: InputAnswerDict): string { const errors: string[] = []; - for ( const validation of input.validation){ + + for (const validation of input.validation) { switch (validation.type) { + case ValidationType.REGEX: - if (!this.validateByRegex(answer, validation.arguments[0])){ - errors.push("RegEx do not match"); + for (const answer of inputAnswers[input.id]) { + if (!this.validateByRegex(answer.value, validation.arguments[0])) { + errors.push("RegEx do not match"); + } } break; + case ValidationType.MANDATORY: - if (!(this.validateMandatory(answer))){ - errors.push("Input answer is mandatory"); + for (const answer of inputAnswers[input.id]) { + if (!(this.validateMandatory(answer.value))) { + errors.push("Input answer is mandatory"); + } } break; + case ValidationType.MAXCHAR: - if (!(this.validateMaxChar(answer, validation.arguments[0]))){ - errors.push("Input answer must be lower than " + validation.arguments[0]); + for (const answer of inputAnswers[input.id]) { + if (!(this.validateMaxChar(answer.value, validation.arguments[0]))) { + errors.push("Input answer must be lower than " + validation.arguments[0]); + } } break; + case ValidationType.MINCHAR: - if (!(this.validateMinChar(answer, validation.arguments[0]))){ - errors.push("Input answer must be greater than " + validation.arguments[0]); + for (const answer of inputAnswers[input.id]) { + if (!(this.validateMinChar(answer.value, validation.arguments[0]))) { + errors.push("Input answer must be greater than " + validation.arguments[0]); + } } break; - } + case ValidationType.TYPEOF: + for (const answer of inputAnswers[input.id]) { + if (!(this.validateTypeOf(answer.value, validation.arguments[0]))) { + errors.push("Input answer must be a " + validation.arguments[0]) + " type"; + } + } + break; + + case ValidationType.SOMECHECKBOX: + if (!(this.validateSomeCheckbox(input, inputAnswers))) { + errors.push("Input answer must have a answer"); + } + break; + + case ValidationType.MAXANSWERS: + if (!(this.validateMaxAnswers(inputAnswers, input.id, validation.arguments[0]))) { + errors.push("Number of input answers must be lower than " + validation.arguments[0]); + } + break; + + case ValidationType.DEPENDENCY: + const id: number = parseInt(validation.arguments[0], 10); + if (!(isNaN(id)) && !(this.validateDependency(inputAnswers[id], validation.arguments[1]))) { + errors.push("Must answer question with id " + validation.arguments[0] + " and placement " + validation.arguments[1]); + } + break; + } } - return errors.join(";"); + return errors.join(";"); } - public static validateFormAnswer(formAnswer: FormAnswer): void{ + /** + * Validate if form answer is valid. + * @param formAnswer - FormAnswer to be validated. + */ + public static validateFormAnswer(formAnswer: FormAnswer): void { const errorsDict: ValidationDict = {}; - - for ( const input of formAnswer.form.inputs){ - for (const answer of formAnswer.inputAnswers[input.id]){ - const error: string = this.validateInput(input, answer.value); - if (error !== "" && error !== undefined){ - errorsDict[input.id] = error; - } + for (const input of formAnswer.form.inputs) { + const error: string = this.validateInput(input, formAnswer.inputAnswers); + if (error !== "" && error !== undefined) { + errorsDict[input.id] = error; } } - if ( Object.keys(errorsDict).length > 0){ + if ( Object.keys(errorsDict).length > 0) { throw new ValidationError(errorsDict, "Validation Error"); } }