diff --git a/CHANGELOG.md b/CHANGELOG.md index eae785546e948a1cebd62aaac0aa3db182e5976f..363346f786289f04f4b4eb6e3e7692e6256d65d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ 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.13 - 10-02-2020 +### Added +- Route to read Form Answer #66 (Richard Heise & Gianfranco) +- Method to get IDs from form answers from a form +- Read all answers from a form +- Scenario form test to read + + ## 1.1.12 - 04-02-2020 ### Added - Route to update an user #65 (Richard Heise) diff --git a/package.json b/package.json index 49532a74d45e8c46857837ed07b7228198a54b46..c5bacdc80153ab59511ecbb6b7dc763fbc17040f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "form-creator-api", - "version": "1.1.12", + "version": "1.1.13", "description": "RESTful API used to manage and answer forms.", "main": "index.js", "scripts": { diff --git a/src/api/controllers/formAnswer.spec.ts b/src/api/controllers/formAnswer.spec.ts index 1155f0ef79778dfc89a575717c449cf905a9a20a..b92c7cf54df2b248a94ff3e1bb124a0438c494bb 100644 --- a/src/api/controllers/formAnswer.spec.ts +++ b/src/api/controllers/formAnswer.spec.ts @@ -21,13 +21,16 @@ import * as request from "supertest"; import { expect } from "chai"; -import { formAnswerScenario } from "../../../test/scenario"; +import { formAnswerScenario, dbHandlerScenario } from "../../../test/scenario"; 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 { testToken } from "./form.spec"; +import { FormAnswer, FormAnswerOptions } from "../../core/formAnswer"; +const util = require('util'); describe("API data controller", () => { @@ -76,4 +79,32 @@ describe("API data controller", () => { }) .end(done); }); + + it("Should respond 200 when reading a form answer", (done) => { + + request(server) + .get("/answer/1") + .set("Authorization", "bearer " + testToken) + .expect(200) + + .expect((res: any) => { + for (let i = 0; i < 3; ++i) { + TestHandler.testFormAnswer(res.body[i], formAnswerScenario.formAnswerRead[i]); + } + }) + .end(done); + }); + + it("Should respond 500 when failing to read a form answer", (done) => { + + request(server) + .get("/answer/500") + .set("Authorization", "bearer " + testToken) + .expect(500) + + .expect((res: any) => { + expect(res.body.error).to.be.equal("User dont own this form."); + }) + .end(done); + }); }); diff --git a/src/api/controllers/formAnswer.ts b/src/api/controllers/formAnswer.ts index 3c28d2dc7d78579d435870e8fc1aed7746d8a1a9..8234153255c1ae2c42093896c8008cc144275854 100644 --- a/src/api/controllers/formAnswer.ts +++ b/src/api/controllers/formAnswer.ts @@ -26,6 +26,8 @@ import { FormAnswer, FormAnswerOptions } from "../../core/formAnswer"; import { ValidationHandler } from "../../utils/validationHandler"; import { Response, NextFunction } from "express"; import { Request } from "../apiTypes"; +import { waterfall } from "async"; +const util = require('util'); export class AnswerCtrl { @@ -91,4 +93,39 @@ export class AnswerCtrl { } }); } + + public static read(req: Request, res: Response, next: NextFunction) { + + waterfall([ + (callback: (err: Error) => void) => { + req.db.form.list(Object(req.userData).id, (err: Error, forms?: Form[]) => { + if (err) { + callback(err); + return; + } + + const e: Error = new Error("User dont own this form."); + callback((forms.some((obj) => obj.id === Number(req.params.id))) ? null : e); + }); + }, + (callback: (err: Error, answer?: FormAnswer[]) => void) => { + req.db.answer.readAll(req.params.id, (err: Error, resultAnswer?: FormAnswer[]) => { + if (err) { + callback(err); + return; + } + + res.status(200).json( resultAnswer ) + }); + } + ], (error: Error) => { + if (error) { + res.status(500).json({ + message: "Some error has ocurred. Check error property for details.", + error: error.message + }); + return; + } + }); + } } diff --git a/src/api/controllers/user.ts b/src/api/controllers/user.ts index 9004cfb24cde6acd3a6187a381ea3066798cf1d0..7650132da62cf0195e2b927c7920e04248050e17 100644 --- a/src/api/controllers/user.ts +++ b/src/api/controllers/user.ts @@ -196,13 +196,9 @@ export class UserCtrl { callback(err); return; } - - try { - newUser = new User(OptHandler.User(user, password)); - } catch (err) { - callback(err); - return; - } + + newUser = new User(OptHandler.User(user, password)); + callback(null, newUser); }); }, diff --git a/src/main.ts b/src/main.ts index 62dccccf5174d103a889da159e80eafa23854115..70d6a76d84b69b58af5558346dc9d2edf91c2075 100755 --- a/src/main.ts +++ b/src/main.ts @@ -60,6 +60,7 @@ app.delete("/user/deleteData/:id", tokenValidation(), UserCtrl.deleteData); app.put("/user/changePassword", tokenValidation(), UserCtrl.changePassword); app.get("/user/list/:id", UserCtrl.listForms); app.put("/user/update", tokenValidation(), UserCtrl.update); +app.get("/answer/:id", tokenValidation(), AnswerCtrl.read); // Listening diff --git a/src/utils/answerQueryBuilder.ts b/src/utils/answerQueryBuilder.ts index e367167dd840c873f7d391102592fd32ad1147d4..56e7a8f26af5458ddf0eec51dd33472cbb75eb98 100644 --- a/src/utils/answerQueryBuilder.ts +++ b/src/utils/answerQueryBuilder.ts @@ -19,13 +19,11 @@ * 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 { eachSeries, waterfall } from "async"; +import { Pool, QueryResult } from "pg"; +import { Form } 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 { InputAnswer, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; import { ErrorHandler} from "./errorHandler"; import { FormQueryBuilder } from "./formQueryBuilder"; import { OptHandler } from "./optHandler"; @@ -55,7 +53,7 @@ export class AnswerQueryBuilder extends QueryBuilder { waterfall([ (callback: (err: Error, result?: QueryResult) => void) => { - this.begin((error: Error, results?: QueryResult) => { + this.begin((error: Error) => { callback(error); }); }, @@ -65,7 +63,7 @@ export class AnswerQueryBuilder extends QueryBuilder { }); }, (formAnswerId: number, callback: (err: Error, result?: number) => void) => { - this.commit((error: Error, results?: QueryResult) => { + this.commit((error: Error) => { callback(error, formAnswerId); }); }, @@ -77,7 +75,7 @@ export class AnswerQueryBuilder extends QueryBuilder { ], (err, formAnswerResult?: FormAnswer) => { if (err) { - this.rollback((error: Error, results?: QueryResult) => { + this.rollback(() => { cb(err); }); return; @@ -140,7 +138,7 @@ export class AnswerQueryBuilder extends QueryBuilder { callback(err); }); } - ], (err) => { + ], () => { cb(null); return; }); @@ -280,17 +278,17 @@ export class AnswerQueryBuilder extends QueryBuilder { }); } - /** - * 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. - */ + /** + * 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) => { + this.begin((error: Error) => { callback(error); }); }, @@ -300,7 +298,7 @@ export class AnswerQueryBuilder extends QueryBuilder { }); }, (formAnswer: FormAnswerOptions, callback: (err: Error, result?: FormAnswerOptions) => void) => { - this.commit((error: Error, results?: QueryResult) => { + this.commit((error: Error) => { callback(error, formAnswer); }); }, @@ -317,7 +315,7 @@ export class AnswerQueryBuilder extends QueryBuilder { } ], (err, formAnswer?: FormAnswer) => { if (err) { - this.rollback((error: Error, results?: QueryResult) => { + this.rollback(() => { cb(err); }); return; @@ -326,6 +324,76 @@ export class AnswerQueryBuilder extends QueryBuilder { }); } + /** + * Asynchronously read all FormAnswer from a form database with transaction. + * @param formId - 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.formAnswers - FormAnswers array object or null if form not exists. + */ + public readAll(formId: number, cb: (err: Error, formAnswers?: FormAnswer[]) => void) { + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin((error: Error) => { + callback(error); + }); + }, + (callback: (err: Error, result?: number[]) => void) => { + this.getFormAnswerIDs(formId, (err: Error, result: number[]) => { + callback(err, result); + }); + }, + (formAnswerIDs: number[], callback: (err: Error, result?: FormAnswerOptions[]) => void) => { + const formAnswers: FormAnswerOptions[] = []; + eachSeries(formAnswerIDs, (formAnswerId, innerCallback) => { + this.readController(formAnswerId, (error: Error, result?: FormAnswerOptions) => { + if (result) { + formAnswers.push(result); + } + + innerCallback(error); + }); + }, (error) => { + callback(error, formAnswers); + }); + }, + (formAnswer: FormAnswerOptions[], callback: (err: Error, result?: FormAnswerOptions[]) => void) => { + this.commit((error: Error) => { + callback(error, formAnswer); + }); + }, + (formAnswersOpt: FormAnswerOptions[], callback: (err: Error, result?: FormAnswer[]) => void) => { + + const formAnswers: FormAnswer[] = []; + + eachSeries(formAnswersOpt, (formAnswer, innerCallback) => { + this.formQueryBuilder.read(formAnswer.form.id, (error: Error, resultForm?: Form) => { + + const formAnswerObj: FormAnswerOptions = { + id: formAnswer.id + , form: resultForm + , timestamp: formAnswer.timestamp + , inputsAnswerOptions: formAnswer.inputsAnswerOptions + }; + + formAnswers.push(new FormAnswer(formAnswerObj)); + innerCallback(error); + }); + }, (error) => { + callback(error, formAnswers); + }); + } + ], (err, formAnswers?: FormAnswer[]) => { + if (err) { + this.rollback(() => { + cb(err); + }); + return; + } + cb(null, formAnswers); + }); + } + /** * Asynchronously read a FormAnswer from database without transaction. * @param formAnswerId - FormAnswer identifier to be founded. @@ -378,7 +446,7 @@ export class AnswerQueryBuilder extends QueryBuilder { const inputAnswersOpts: InputAnswerOptions[] = []; eachSeries(inputAnswersTmp, (inputAnswer, innerCallback) => { this.readSubFormController(inputAnswer, inputAnswersOpts, innerCallback); - }, (err) => { + }, () => { const inputsAnswerResults: InputAnswerOptionsDict = {}; for (const i of inputAnswersOpts) { if (inputsAnswerResults[i["idInput"]]) { @@ -438,6 +506,25 @@ export class AnswerQueryBuilder extends QueryBuilder { }); } + private getFormAnswerIDs(formId: number, cb: (err: Error, formAnswerIds: number[]) => void) { + const queryString: string = "SELECT form_answer.id FROM form \ + INNER JOIN form_answer \ + ON form_answer.id_form=form.id \ + WHERE form.id=$1;"; + const query: QueryOptions = { + query: queryString + , parameters: [formId] + }; + + this.executeQuery(query, (error: Error, result?: QueryResult) => { + const ids: number[] = []; + for (const i of result.rows) { + ids.push(i.id); + } + cb(error, ids); + }); + } + /** * Asynchronously read a SubFormAnswer (FormAnswer object) on database without transactions. * @param inputAnswer - InputAnswerOptions object that contains the subForm (if not null) that should be readed. diff --git a/src/utils/testHandler.ts b/src/utils/testHandler.ts index 5a875c43fdea8ea5c1f737b813ab74dee2193a4b..3666abc9c36b14dd5d0444d7295c9cc9b65eadb4 100644 --- a/src/utils/testHandler.ts +++ b/src/utils/testHandler.ts @@ -96,7 +96,6 @@ export class TestHandler { }); } - expect(formAnswer.timestamp.toISOString()).to.be.equal(stub.timestamp.toISOString()); } diff --git a/test/scenario.ts b/test/scenario.ts index 3da766f9d002c181fd44d811b321f9482b1f77e8..e8bcd67708daa5cd16f04c3dc3b3cad88682f57a 100644 --- a/test/scenario.ts +++ b/test/scenario.ts @@ -630,7 +630,16 @@ const form8: Form = { , Input2 ] }; - +/** New form created */ +const formToRead: Form = { + id: 1 + , title: "Title 1" + , description: "Description 1" + , inputs: [ + Input1 + , Input2 + ] +}; /** Old form that serves as a base for comparison */ const formBase: Form = { id: 1 @@ -1323,6 +1332,16 @@ const fullFormOptions: FormOptions = { , OptHandler.input(optsInput3) ] }; +const formForAnswer: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + Input1 + , Input2 + , Input3 + ] +}; /** Valid form created used fullFormOptions */ const tmpForm: Form = new Form(OptHandler.form(fullFormOptions)); @@ -3153,6 +3172,204 @@ const formOptionsToUpdateMissingProperties: any = { } ] }; + +const formReadAnswer: FormAnswer[] = + [ { + id: 3, + form: + new Form ({ + id: 1, + title: "Form Title 1", + description: "Form Description 1", + inputs: + [ { + id: 1, + placement: 0, + description: "Description Question 1 Form 1", + question: "Question 1 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: [] }, + { + id: 2, + placement: 1, + description: "Description Question 3 Form 1", + question: "Question 3 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: + [ { type: 1, arguments: [ "\\d{5}-\\d{3}" ] }, + { type: 2, arguments: [] } ] }, + { + id: 3, + placement: 2, + description: "Description Question 2 Form 1", + question: "Question 2 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: + [ { type: 3, arguments: [ "10" ] }, + { type: 4, arguments: [ "2" ] } ] } ] }), + timestamp: new Date("21 february 2019 12:10:25 UTC"), + inputAnswers: + { 1: + [ { + id: 5, + idInput: 1, + placement: 0, + value: "Answer to Question 1 Form 1", + subForm: null } ], + 2: + [ { + id: 6, + idInput: 2, + placement: 0, + value: "Answer to Question 2 Form 1", + subForm: null } ], + 3: + [ { + id: 7, + idInput: 3, + placement: 0, + value: "Answer to Question 3 Form 1", + subForm: null } ] } }, + { + id: 6, + form: + new Form ({ + id: 1, + title: "Form Title 1", + description: "Form Description 1", + inputs: + [ { + id: 1, + placement: 0, + description: "Description Question 1 Form 1", + question: "Question 1 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: [] }, + { + id: 2, + placement: 1, + description: "Description Question 3 Form 1", + question: "Question 3 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: + [ { type: 1, arguments: [ "\\d{5}-\\d{3}" ] }, + { type: 2, arguments: [] } ] }, + { + id: 3, + placement: 2, + description: "Description Question 2 Form 1", + question: "Question 2 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: + [ { type: 3, arguments: [ "10" ] }, + { type: 4, arguments: [ "2" ] } ] } ] }), + timestamp: new Date("22 january 2019 19:10:25"), + inputAnswers: + { 1: + [ { + id: 12, + idInput: 1, + placement: 0, + value: "Answer to Question 1 Form 1", + subForm: null } ], + 2: + [ { + id: 13, + idInput: 2, + placement: 0, + value: "Answer to Question 2 Form 1", + subForm: null } ], + 3: + [ { + id: 14, + idInput: 3, + placement: 0, + value: "Answer to Question 3 Form 1", + subForm: null } ] } }, + { + id: 7, + form: + new Form ({ + id: 1, + title: "Form Title 1", + description: "Form Description 1", + inputs: + [ { + id: 1, + placement: 0, + description: "Description Question 1 Form 1", + question: "Question 1 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: [] }, + { + id: 2, + placement: 1, + description: "Description Question 3 Form 1", + question: "Question 3 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: + [ { type: 1, arguments: [ "\\d{5}-\\d{3}" ] }, + { type: 2, arguments: [] } ] }, + { + id: 3, + placement: 2, + description: "Description Question 2 Form 1", + question: "Question 2 Form 1", + enabled: true, + type: 0, + sugestions: [], + subForm: null, + validation: + [ { type: 3, arguments: [ "10" ] }, + { type: 4, arguments: [ "2" ] } ] } ] }), + timestamp: new Date("10 february 2020 14:07:49 UTC"), + inputAnswers: + { 1: + [ { + id: 15, + idInput: 1, + placement: 0, + value: "Answer to Question 1 Form 1", + subForm: null } ], + 2: + [ { + id: 16, + idInput: 2, + placement: 0, + value: "12345-000", + subForm: null } ], + 3: + [ { + id: 17, + idInput: 3, + placement: 0, + value: "MAXCHAR 10", + subForm: null } ] } } ]; + /** A message that is used in cases where the update is a success */ const successMsg = "Updated"; /** A message that is used in cases where the update is unsuccess */ @@ -3730,7 +3947,8 @@ export const dbHandlerScenario = { /** User obj to be used to update another user */ toupdate : userToUpdate, /** User obj to be used to update another user */ - updateEnable : userToUpdate2 + updateEnable : userToUpdate2, + formTest: form6 }; /** form testing scenario */ @@ -3770,6 +3988,8 @@ export const formAnswerScenario = { validAnswer : validFormAnswers, /** Invalid formAnswer being recieved by a post */ invalidAnswer : invalidFormAnswer, + /** Form Awnser to be read */ + formAnswerRead : formReadAnswer }; /** User testing scenario */