Commit 9673fa3b authored by Matheus Horstmann's avatar Matheus Horstmann 🐴 Committed by Lucas Fernandes de Oliveira

Issue #7: Write and read Inputs and Forms

Signed-off-by: Matheus Horstmann's avatarMatheus Horstmann <mch15@inf.ufpr.br>
parent 25427545
......@@ -5,6 +5,16 @@ 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.6 - 01-04-2019
### Changed
- Input class to match with database model (Add id and description) (Horstmann)
- Form class to match with database model (Remove version add description) (Horstmann)
- enumHandler to remove sides whitespaces (Horstmann)
### Added
- Create readForm method to read form from database #7 (Horstmann)
- Create readInput method to read input from database #7 (Horstmann)
- Create writeForm method to insert form into database (Horstmann)
- Comments to coverage ignore errors that are not reached on tests.
## 0.0.5 - 19-03-2019
### Changed
- Remove tslint-stylish from package.json, package is deprecated (Horstmann)
......
{
"name": "form-creator-api",
"version": "0.0.5",
"version": "0.0.6",
"description": "RESTful API used to manage and answer forms.",
"main": "index.js",
"scripts": {
......
......@@ -20,28 +20,32 @@
*/
import { Input } from "./input";
/**
* Form Class to manage project's forms
*/
export class Form {
/** Form Header, as name and propose */
public readonly header: string;
/** Unique identifier of a Form instance */
public readonly id: number;
/** Form's title. An human-understandable identifier. Not unique */
public readonly title: string;
/** Form Description, as propose */
public readonly description: string;
/** Array of input. containing question */
public readonly inputs: Input[];
/** Version, a incremental numbert */
public readonly version: number;
/**
* Creates a new instance of Input Class
* @param header - Forms's name and propose.
* @param title - Form's title.
* @param description - Form's description
* @param inputs - Forms's question
* @param version - Incremental version of Form
* @param id - Form's identifier
*/
constructor(header: string, inputs: Input[], version: number) {
this.header = header;
constructor(title: string, description: string, inputs: Input[], id?: number) {
this.id = id ? id : null;
this.title = title;
this.description = description;
this.inputs = inputs;
this.version = version;
}
}
......@@ -35,8 +35,12 @@ export interface Validation {
*/
export class Input {
/** Unique identifier of a Input instance. */
public readonly id: number;
/** Place where input should be in the form. */
public readonly placement: number;
/** Input's Description */
public readonly description: string;
/** Question of input */
public readonly question: string;
/** Type of input */
......@@ -47,12 +51,17 @@ export class Input {
/**
* Creates a new instance of Input Class
* @param placement - Question position in forms.
* @param description - Input description
* @param question - Input question of this input
* @param type - InputType
* @param validation - Array of Validation
* @param id - Input identifier
*/
constructor(placement: number, question: string, type: InputType, validation: Validation[]) {
constructor(placement: number, description: string, question: string, type: InputType, validation: Validation[], id?: number) {
this.id = id ? id : null;
this.placement = placement;
this.description = description;
this.question = question;
this.type = type;
this.validation = validation;
......
This diff is collapsed.
......@@ -19,7 +19,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { eachOfSeries, eachSeries , map, waterfall } from "async";
import { Pool, PoolConfig, QueryResult } from "pg";
import { Form } from "../core/form";
import { Input, Validation } from "../core/input";
import { EnumHandler, InputType, ValidationType } from "./enumHandler";
/**
* Class of the SGBD from the Form Creator Api perspective. Used to
......@@ -45,13 +49,14 @@ export class DbHandler {
* 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.error - Error information when the method fails.
* @param cb.err - Error information when the method fails.
* @param cb.result - Query result.
*/
public executeQuery(query: string, cb: (err: Error, result?: QueryResult) => void): void{
this.pool.connect((err, client, done) => {
if (err) {
cb(err);
return;
......@@ -71,6 +76,12 @@ export class DbHandler {
public commit(cb: (err: Error, result?: QueryResult) => void){
this.executeQuery("COMMIT;", cb);
}
/**
* Asynchronously rollback a transaction
*/
public rollback(cb: (err: Error, result?: QueryResult) => void){
this.executeQuery("ROLLBACK;", cb);
}
/**
* Asynchronously starts a transaction
......@@ -79,4 +90,340 @@ export class DbHandler {
this.executeQuery("BEGIN;", cb);
}
/**
* 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 query: string = "SELECT id, title, description FROM form WHERE id=" + 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(new Error("Bad number of ids returned: found '" + result.rowCount + "' should be 1"));
return;
}
this.readInputWithFormId(id, (error: Error, inputs: Input[]) => {
const formTmp = new Form (
result.rows[0]["title"],
result.rows[0]["description"],
inputs,
result.rows[0]["id"]
);
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?: Input[]) => void){
const query: string = "SELECT id, placement, input_type, question, description FROM input WHERE id_form=" + id + ";";
this.executeQuery(query, (err: Error, result?: QueryResult) => {
map(result.rows, (innerResult, callback) => {
this.readInputValidationWithInputId(innerResult["id"], (error: Error, validation: Validation[]) => {
const inputTmp = new Input (
innerResult["placement"],
innerResult["description"],
innerResult["question"],
EnumHandler.parseInputType(innerResult["input_type"]),
validation,
innerResult["id"]);
callback(error, inputTmp);
});
}, (errors, inputs: Input[]) => {
if (errors){
this.rollback( (error: Error, results?: QueryResult) => {
cb(errors);
return;
});
return;
}
const sortedInputs: Input[] = this.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 query: string = "SELECT id, validation_type FROM input_validation WHERE id_input=" + 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 query: string = "SELECT id, argument, placement FROM \
input_validation_argument WHERE \
id_input_validation=" + id + ";";
// cb(null);
this.executeQuery(query, (error: Error, result?: QueryResult) => {
const sortedResult: any[] = this.sortByPlacement(result.rows);
const argumentArrayTmp = [];
for (const innerResult of sortedResult){
argumentArrayTmp.push(innerResult["argument"]);
}
cb(error, argumentArrayTmp);
});
}
/**
* A private method to return a array sorted by placement field
* I personally don't think that this method should be here, it is quite weird to have a sort method on database handler
* @param array - Array with objects that have placement field
* @returns - A sorted array by placement
*/
private sortByPlacement(array: any[]): any[] {
const sortedArray: any[] = array.sort((obj1, obj2) => {
if (obj1["placement"] > obj2["placement"]) {
return 1;
}
if (obj1["placement"] < obj2["placement"]) {
return -1;
}
return 0;
});
return sortedArray;
}
/**
* 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 query: string = "INSERT INTO form (title, description) VALUES\
( \'" + form.title + "\', \'" +
form.description + "\' ) RETURNING 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, formId?: number) => void) => {
if (result.rowCount !== 1){
callback(new Error("Form not inserted"));
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 form 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) => void){
const query: string = "INSERT INTO input (\
id_form, placement, input_type, question, description)\
VALUES ( " + formId + ", " + input.placement +
", \'" + EnumHandler.stringfyInputType(input.type) +
"\', \'" + input.question + "\', \'" + input.description +
"\' ) RETURNING id;";
this.executeQuery(query, (err: Error, result?: QueryResult) => {
if (err){
cb(err);
return;
}
if (result.rowCount !== 1){
cb(new Error("Input not Inserted"));
return;
}
eachSeries(input.validation, (validation: Validation, callback) => {
this.writeValidationWithInputId(result.rows[0]["id"], validation, callback);
}, (error) => {
cb(error);
});
});
}
/**
* Asynchronously insert a form 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 query: string = "INSERT INTO input_validation ( id_input, \
validation_type) VALUES ( " + inputId + ", \'" +
EnumHandler.stringfyValidationType(validation.type) +
"\' ) RETURNING id;";
this.executeQuery(query, (err: Error, result?: QueryResult) => {
if (err){
cb(err);
return;
}
if (result.rowCount !== 1){
cb(new Error("Validation not Inserted"));
return;
}
eachOfSeries(validation.arguments, (argument, placement: number, callback) => {
this.writeValidationArgumentWithInputIdAndPlacement(result.rows[0]["id"], argument, placement, callback);
}, (error) => {
cb(error);
});
});
}
/**
* Asynchronously insert a form 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: any, placement: number, cb: (err: Error) => void){
const query: string = "INSERT INTO input_validation_argument \
( id_input_validation, argument, placement )\
VALUES ( \'" + validationId + "\', \'" +
argument + "\', \'" + placement + "\' ) RETURNING id;";
this.executeQuery(query, (err: Error, result?: QueryResult) => {
if (err){
cb(err);
return;
}
if (result.rowCount !== 1){
cb(new Error("Validation Argument not Inserted"));
return;
}
cb(err);
});
}
}
......@@ -68,7 +68,7 @@ export class EnumHandler {
* @returns - Matching InputType
*/
public static parseInputType(str: string): InputType {
str = str.toLocaleLowerCase();
str = str.toLocaleLowerCase().trimLeft().trimRight();
switch (str) {
case "text":
return InputType.TEXT;
......@@ -102,7 +102,7 @@ export class EnumHandler {
* @returns - Matching ValidationType
*/
public static parseValidationType(str: string): ValidationType {
str = str.toLocaleLowerCase();
str = str.toLocaleLowerCase().trimLeft().trimRight();
switch (str) {
case "mandatory":
return ValidationType.MANDATORY;
......
......@@ -27,7 +27,7 @@ import {ValidationHandler } from "./validationHandler";
describe("Validation Handler", () => {
it("should test when Input is mandatory", () => {
const validation: Validation[] = [{type: ValidationType.MANDATORY, arguments: []}];
const input = new Input (1, "Question", InputType.TEXT, validation);
const input = new Input (1, "Description", "Question", InputType.TEXT, validation);
expect(ValidationHandler.validateInput(input, "FOOL")).to.be.true;
expect(ValidationHandler.validateInput(input, "")).to.be.false;
......@@ -37,7 +37,7 @@ describe("Validation Handler", () => {
it("should test when Input has a regex", () => {
const validation: Validation[] = [{type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"]}];
const input = new Input (1, "Question", InputType.TEXT, validation);
const input = new Input (1, "Description", "Question", InputType.TEXT, validation);
expect(ValidationHandler.validateInput(input, "88888-888")).to.be.true;
expect(ValidationHandler.validateInput(input, "88888-88")).to.be.false;
......@@ -48,7 +48,7 @@ describe("Validation Handler", () => {
it("should test when Input has a minimum char number", () => {
const validation: Validation[] = [{type: ValidationType.MINCHAR, arguments: [5]}];
const input = new Input (1, "Question", InputType.TEXT, validation);
const input = new Input (1, "Description", "Question", InputType.TEXT, validation);
expect(ValidationHandler.validateInput(input, "12345")).to.be.true;
expect(ValidationHandler.validateInput(input, "123456")).to.be.true;
......@@ -59,7 +59,7 @@ describe("Validation Handler", () => {
});
it("should test when Input has a maximum char number", () => {
const validation: Validation[] = [{type: ValidationType.MAXCHAR, arguments: [5]}];
const input = new Input (1, "Question", InputType.TEXT, validation);
const input = new Input (1, "Description", "Question", InputType.TEXT, validation);
expect(ValidationHandler.validateInput(input, "1234")).to.be.true;
expect(ValidationHandler.validateInput(input, "12345")).to.be.true;
......
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