Commit 1bea58f0 authored by Matheus Horstmann's avatar Matheus Horstmann 🐴 Committed by Lucas Fernandes de Oliveira

Issue #24: Insert Form Answers in DB

Signed-off-by: Matheus Horstmann's avatarMatheus Horstmann <mch15@inf.ufpr.br>
parent a575e0ec
......@@ -4,6 +4,13 @@ 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).
## 0.0.18 - 25-05-2019
### Added
- Create readFormAnswer method to read formAnswer from database #24 (Horstmann)
- Create writeFormAnswer method to insert formAnswer into database #24 (Horstmann)
- Create TestHandler to tests FormAnswers
### Changed
- Fix OptHandler to return id in inputAnswer
## 0.0.17 - 25-05-2019
### Added
......
{
"name": "form-creator-api",
"version": "0.0.16",
"version": "0.0.18",
"description": "RESTful API used to manage and answer forms.",
"main": "index.js",
"scripts": {
......
......@@ -22,7 +22,9 @@ import { series } from "async";
import { expect } from "chai";
import { QueryResult } from "pg";
import { Form, FormOptions } from "../core/form";
import { FormAnswer, FormAnswerOptions } from "../core/formAnswer";
import { Input, InputOptions, Validation } from "../core/input";
import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer";
import { configs } from "./config";
import { DbHandler, QueryOptions } from "./dbHandler";
import { InputType, ValidationType } from "./enumHandler";
......@@ -490,4 +492,100 @@ describe("Read and Write on Database", () => {
});
it("should read an existent form Answer", (done) => {
const inputAnswersOpt1: InputAnswerOptions = {
id: 5
, 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-02-21 12:10:25");
dbhandler.readForm(1, (error: Error, form: Form) => {
const formAnswerOptions: FormAnswerOptions = {
id: 3
, form
, timestamp: data
, inputsAnswerOptions: inputAnswerOptionsDict
};
dbhandler.readFormAnswer(3, (err: Error, formAnswer: FormAnswer) => {
TestHandler.testFormAnswer(formAnswer, new FormAnswer(OptHandler.formAnswer(formAnswerOptions)));
done();
});
});
});
it("should read a non existent form Answer", (done) => {
dbhandler.readFormAnswer(25, (err: Error, formAnswer: FormAnswer) => {
expect(err).to.not.equal(null);
expect(formAnswer).to.be.undefined;
done();
});
});
it("should write form Answer", (done) => {
const inputAnswersOpt1: InputAnswerOptions = {
id: 5
, 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(7);
let inputAnswerId: number = 15;
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();
});
});
});
});
......@@ -22,7 +22,9 @@
import { eachOfSeries, 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, Validation } from "../core/input";
import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer";
import { EnumHandler, InputType, ValidationType } from "./enumHandler";
import { ErrorHandler} from "./errorHandler";
import { OptHandler } from "./optHandler";
......@@ -406,7 +408,7 @@ export class DbHandler {
}
/**
* Asynchronously insert a form on Database and return it.
* 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.
......@@ -452,7 +454,7 @@ export class DbHandler {
}
/**
* Asynchronously insert a form on Database and return it.
* 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.
......@@ -496,7 +498,7 @@ export class DbHandler {
}
/**
* Asynchronously insert a form on Database and return it.
* 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.
......@@ -535,4 +537,227 @@ export class DbHandler {
});
}
/**
* 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"]] = DbHandler.sortByPlacement(inputsAnswerResults[i["idInput"]]);
}else{
inputsAnswerResults[i["idInput"]] = [i];
}
}
cb(err, inputsAnswerResults);
});
}
}
......@@ -144,7 +144,8 @@
}
const option: InputAnswerOptions = {
idInput: obj.idInput
id: obj.id
, idInput: obj.idInput
, placement: obj.placement
, value: obj.value
};
......
......@@ -21,7 +21,9 @@
import { expect } from "chai";
import { Form } from "../core/form";
import { FormAnswer} from "../core/formAnswer";
import { Input, Validation } from "../core/input";
import { InputAnswer } from "../core/inputAnswer";
import { EnumHandler, InputType, ValidationType } from "./enumHandler";
/**
......@@ -69,4 +71,36 @@
}
}
/**
* Verify if two formAnswers are semantically equal;
* @param formAnswer - Form Answer that should be tested
* @param stub - A model formAnswer that first param itend to be.
* @returns - True if formAnswers are equal else false
*/
public static testFormAnswer(formAnswer: FormAnswer, stub: FormAnswer){
expect(formAnswer.id).to.be.equal(stub.id);
TestHandler.testForm(formAnswer.form, stub.form);
for (const key of Object.keys(formAnswer.inputAnswers)){
formAnswer.inputAnswers[parseInt(key, 10)].forEach((inputAnswer, i) => {
TestHandler.testInputAnswer(inputAnswer, stub.inputAnswers[parseInt(key, 10)][i]);
});
}
expect(formAnswer.timestamp.toISOString()).to.be.equal(stub.timestamp.toISOString());
}
/**
* Verify if two inputAnswer are semantically equal;
* @param inputAnswer - inputAnswer that should be tested
* @param stub - A model inputAnswer that first param itend to be.
* @returns - True if inputAnswer are equal else false
*/
public static testInputAnswer(inputAnswer: InputAnswer, stub: InputAnswer){
expect(inputAnswer.id).to.be.equal(stub.id);
expect(inputAnswer.idInput).to.be.equal(stub.idInput);
expect(inputAnswer.placement).to.be.equal(stub.placement);
expect(inputAnswer.value).to.be.equal(stub.value);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment