From ec3f19c062986bd6b2e0b52ca50f11eada5d8dc0 Mon Sep 17 00:00:00 2001 From: Samuel Leite <shpl19@inf.ufpr.br> Date: Fri, 13 Aug 2021 11:40:57 -0300 Subject: [PATCH] Issue #81: Add object Table and changes to support it. --- .gitlab-ci.yml | 1 - CHANGELOG.md | 272 ++++++++++++++++++++++-------- form-creator-database | 2 +- package.json | 2 +- src/api/controllers/formAnswer.ts | 20 ++- src/core/input.ts | 11 ++ src/core/inputAnswer.ts | 10 +- src/core/inputUpdate.spec.ts | 8 + src/core/table.ts | 56 ++++++ src/utils/answerQueryBuilder.ts | 258 ++++++++++++++++++++++++++-- src/utils/dbHandler.spec.ts | 25 +++ src/utils/diffHandler.ts | 61 +++++++ src/utils/enumHandler.ts | 8 + src/utils/formQueryBuilder.ts | 259 +++++++++++++++++++++++++++- src/utils/optHandler.ts | 27 +++ src/utils/testHandler.ts | 22 +++ src/utils/validationHandler.ts | 10 +- test/scenario.ts | 108 +++++++++++- 18 files changed, 1053 insertions(+), 107 deletions(-) create mode 100644 src/core/table.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7a05ed7..6d504fb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,7 +34,6 @@ run_test: - 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 - yarn install --frozen-lockfile --silent --non-interactive diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb554c..6119d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,120 +1,160 @@ # Changelog + 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.3.0 - 08/10/2021 + +## Added + +- New object type (Table) added #81 (Samuel Leite) +- Added tests related to Table object. +## Changed + +- Changed query methods to account for Table. ## 1.2.7 - 13/10/2020 + ## Added -- Property times added to form table #80 (Richard Heise) +- Property times added to form table #80 (Richard Heise) ## 1.2.6 - 19/06/2020 + ## Added -- A extra function on optHandler to better handle form edits #77 (Richard Heise) +- A extra function on optHandler to better handle form edits #77 (Richard Heise) ## 1.2.5 - 02/06/2020 + ## Changed + - Route to list forms now returns all the dates and answers of the forms #75 (Richard Heise) - Added two extras steps on route waterfall using eachSeries. - Tests weren't changed since the steps where tested by themselfs in other routes. - ## 1.2.4 - 01/06/2020 + ## Added + - Created route to get modified dates of a form #76 (Richard Heise) - Created methods to communicate with the DB - ## 1.2.3 - 29/04/2020 -## Changed + +## Changed + - Api to create a subform input without needing input ID from body #73 (Richard Heise) - OptHandler to not return error when there's no input ID - Subform in core/ to have an optional input ID - ## 1.2.2 - 07-04-2020 -## Added -- Route to return the number of answers in a form #72 (Richard Heise) +## Added + +- Route to return the number of answers in a form #72 (Richard Heise) ## 1.2.1 - 05-03-2020 + ## Added + - Cors to integrate front-end with back-end #68 (Richard Heise) + ### Changed + - Minor route order changes on main.ts to make it more intuitive - Port on config.env from 3000 to 3333 for integration purpose - ## 1.2.0 - 19-02-2020 + ### Changed -- Created a stable version with user +- Created a stable version with user ## 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) -## Changed + +## Changed + - Opthandler can create user without hash - UserQueryBuilder Update now needs an id - UserOptions hash is not obrigatory - ## 1.1.11 - 04-02-2020 + ## Changed -- Form controller update route to verify if a user own the form #62 (Gianfranco) +- Form controller update route to verify if a user own the form #62 (Gianfranco) ## 1.1.10 - 03-02-2020 + ### Added + - Route to list forms #61 (Richard Heise) + ## Changed -- List from FormQueryBuilder now lists an user's forms +- List from FormQueryBuilder now lists an user's forms ## 1.1.9 - 03-02-2020 + ### Added + - Route to change an user's password #63 (Richard Heise) + ## Changed -- Delete route now has the token validation +- Delete route now has the token validation ## 1.1.8 - 30-01-2020 + ### Added + - Route to assign users to forms #60 (Richard Heise) + ## Changed + - Route to write a form now has an extra stage in the waterfall - This stage assigns the user to a form by ID - ## 1.1.7 - 29-01-2020 + ### Added + - Function to assign users to forms #54 (Gianfranco) - Assign added to userQueryBuilder file - ## 1.1.6 - 24-01-2020 + ### Added + - Middleware to validate tokens #56 (Richard Heise) - Route do delete an user #59 (Richard Heise) - Delete control methods on UserQueryBuilder + ## Changed -- Initial user tests are now on the form.spec.ts +- Initial user tests are now on the form.spec.ts ## 1.1.5 - 22-01-2020 + ### Added + - SubForm class #57 (Gianfranco) - InputType SubForm - FormQueryBuilder methods to handle with SubForms @@ -122,179 +162,224 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - OptHandler method to validate SubForm - TestHandler method to compare two SubForms - Database table for SubForms + ## Changed + - Input class to contain a SubForm object - OptHandler methods to handle SubForms Answers - Scenario file with new objects to test SubForms - TestHandler method to compare two SubFormsAnswer - ## 1.1.4 - 19-12-2019 + ### Added + - SignIn route to possibilitate logins #55 (Richard Heise) - JWT (Json Web Token) library and its types to dependencies - Tokens can be created once an user is considered valid + ## Changed + - UserQueryBuilder has, now, a public function to verify if an email is in the DB - The requires are now imports for padronization reasons - ## 1.1.3 - 12-12-2019 + ### Added + - UserCtrl class created to control the user routes #52 (Richard Heise) - The bcrypt library and its types to dependencies - User route tests + ### Changed -- User Query Builder now has writeController to validate an unique email user in the database +- User Query Builder now has writeController to validate an unique email user in the database ## 1.1.2 - 25-11-2019 + ### Added + - User class to create an user object #53 (Richard Heise) - User Query Builder to write, read and update an user in the database - User tests - An user table in the database - ## 1.1.1 - 17-10-2019 + ### Added + - Fixture class to manage the database #49 (Gianfranco) - Database module inside the api - Before test to call fixture class + ### Changed + - Database to work with usql-manager - Config class to receive the new parameters - ## 1.1.0 - 11-10-2019 + ### Changed -- Create a stable version +- 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 +- ValidateInput method to receive a vector of inputs ## 1.0.9 - 09-10-2019 + ### Added -- Validation type MAXANSWERS #38 (Gianfranco) +- 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 +- ValidateInput method to receive a vector of input answers ## 1.0.7 - 01-10-2019 + ### Added -- Validation type TYPEOF #37 (Gianfranco) +- 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 +- DbHandler to only have database connections ## 1.0.5 - 27-09-2019 + ### Added -- Input type Select #36 (Gianfranco) +- Input type Select #36 (Gianfranco) ## 1.0.4 - 26-09-2019 + ### Added -- Input type Radio #35 (Gianfranco) +- 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 +- Create a stable version ## 0.0.27 - 15-08-2019 + ### Added + - DbHandler methods to update database #32 (Gianfranco) - Reenabled UpdateType on EnumHandler wich reenabled a input - Update route tests + ### Changed + - writeInputWithFormId method to return the id - readInputWithFormId to not list the disabled inputs - Fix tests of DiffHandler and DbHandler class - DiffHandler to detect reenabled requests - Update route to call the updateDatabase method - ## 0.0.26 - 26-07-2019 + ### Changed + - Write form route to create a formUpdate on create a new form #33 (Gianfranco) - Fix DiffHandler to add inputs with id - Fix tests of DiffHandler, OptHandler and Form route - ## 0.0.25 - 25-07-2019 + ### Changed -- Renamed routes from 'forms' to 'form' #31 (Gianfranco) +- Renamed routes from 'forms' to 'form' #31 (Gianfranco) ## 0.0.24 - 23-07-2019 + ### Added + - DiffHandler to find out the differences between two forms (Gianfranco) - Sorter with methods to sort arrays - DiffHandler and Sorter tests + ### Changed + - Main archive to add an update route #27 - Api controller to update forms - TestHandler to test two FormUpdate objects - ## 0.0.23 - 10-07-2019 + ### Changed + - FormUpdate to receive an Form class (Gianfranco) - InputUpdate to receive an Input class - FormUpdate to receive an array of inputs - OptHandler to handle with the new features - ## 0.0.22 - 09-07-2019 + ### Added + - Created a new enum UpdateType (Gianfranco) - Add stringfy and parse methods to UpdateType - ## 0.0.21 - 01-07-2019 + ### Added + - Add InputUpdate and a FormUpdate Class to store updates from inputs and forms #26 (Gianfranco) - Create writeFormUpdate method to insert a FormUpdate into database - Create writeInputUpdate method to insert a InputUpdate into database @@ -302,97 +387,126 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Create inputUpdate method in OptHandler - Create formUpdate method in OptHandler - ## 0.0.20 - 28-06-2019 + ## Changed -- Class Input to receive a Enabled atribute (Gianfranco) +- Class Input to receive a Enabled atribute (Gianfranco) ## 0.0.19 - 12-06-2019 + ### Added + - A route to POST a form Answer #21 (Horstmann) - Create ValidationError Class that extends error class, with the objective to return a dictionary of invalid answers (Horstmann) + ### Changed + - DbHandler's tests to suite with new forms answers added (Horstmann) - ValidationHandler to validate a Forms Answer instead of inputsAnswer - ValidationHandler's tests to suite new method to validate a Forms Answer - ## 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 +- Fix OptHandler to return id in inputAnswer ## 0.0.17 - 25-05-2019 + ### Added + - inputAnswer method in OptHandler #23 (Horstmann) - formAnswer method in OptHandler #23 (Horstmann) ### Changed -- FormsAnswer class to have an dictionary of InputsAnswer -- FormsAnswer's constructor to use dictionary +- FormsAnswer class to have an dictionary of InputsAnswer +- FormsAnswer's constructor to use dictionary ## 0.0.16 - 06-05-2019 + ### Added + - A FormsAnswer Class to store answers from forms #22 (Horstmann) - A inputsAnswer Class to be the answer for each input in form #22 (Horstmann) + ### Changed -- Form's constructor documentation -- Input's constructor documentation +- Form's constructor documentation +- Input's constructor documentation ## 0.0.15 - 26-04-2019 + ### Added + - A QueryOptions interface, that is used on dbHandler's executeQuery #20 + ### Changed -- dbHandler's tests to fit into new interface + +- dbHandler's tests to fit into new interface + ### Security -- Now dbHandler's executeQuery uses parametrized query to avoid SQL injection +- Now dbHandler's executeQuery uses parametrized query to avoid SQL injection ## 0.0.14 - 26-04-2019 + ### Removed -- Dummies files as Item and Collection #16 (Horstmann) +- Dummies files as Item and Collection #16 (Horstmann) ## 0.0.13 - 26-04-2019 + ### Added + - A route to POST a form #10 (Horstmann) - Tests on route POST (Horstmann) + ### Changed -- dbHandler's tests to suit with new forms and inputs insertion +- dbHandler's tests to suit with new forms and inputs insertion ## 0.0.12 - 25-04-2019 + ### Added + - OptHandler to standardize constructors from Forms and Inputs #19 (Horstmann) -- InputOptions interface on class Input #19 (Horstmann) -- FormOptions interface on class Form #19 (Horstmann) +- InputOptions interface on class Input #19 (Horstmann) +- FormOptions interface on class Form #19 (Horstmann) + ### Changed + - Tests to adapt to new standard of options - dbHandler's readInputValidationWithInputId method to return a InputOptions instead of an input - Tests to adapt to new standard of options - ErrorHandler to add a new error message - - ## 0.0.11 - 17-04-2019 + ### Added + - ErrorHandler to standardize errors message through the project #17 (Horstmann) + ### Changed + - TestHandler documentation title - dbHandler tests to use ErrorHandler #18 (Horstmann) - ## 0.0.10 - 16-04-2019 + ### Added + - TestHandler to test form and inputs #18 (Horstmann) + ### Changed + - controller form tests to use testHandler #18 (Horstmann) - controller form to improve code coverage - dbHandler tests to use testHandler #18 (Horstmann) @@ -401,68 +515,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ValidationHandler to cast size to number - ValidationHandler tests to use string instead of number as validation arguments - ## 0.0.9 - 10-04-2019 + ### Added -- Method read in Form controller to get a Form +- Method read in Form controller to get a Form ## 0.0.8 - 10-04-2019 + ### Changed + - main.ts to remove more dummie class - dbHandler to include method listForms + ### Added -- Form controller and method to list all forms +- Form controller and method to list all forms ## 0.0.7 - 10-04-2019 + ### Changed + - main.ts to include dbHandler Middleware and remove dummie class + ### Added -- Create dbHandler Middleware to be able to access by routes #15 (Horstmann) +- Create dbHandler Middleware to be able to access by routes #15 (Horstmann) ## 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) - Update yarn.lock to avoid vulnerabilities (Horstmann) - Update CI file to handle database (Horstmann) + ### Added + - Class config using singleton patern, to centralize all configuration in one module (Horstmann) - DbHandler to be a layer between API and database #1 (Horstmann) ## 0.0.4 - 12-02-2019 + ### Added -- Class Form #3 (Horstmann) +- Class Form #3 (Horstmann) ## 0.0.3 - 07-02-2019 + ### Changed + - Added a new type of enum ValitationType #2 (Horstmann) + ### Added -- ValidationHandle to valited answer given a input #2 (Horstmann) +- ValidationHandle to valited answer given a input #2 (Horstmann) ## 0.0.2 - 05-02-2019 + ### Added -- EnunHandler to handle types of inputs #4 (Horstmann) +- EnunHandler to handle types of inputs #4 (Horstmann) ## 0.0.1 - 04-02-2019 + ### Added -- This CHANGELOG file to hopefully serve as an evolving example of a standardized open source project CHANGELOG. + +- This CHANGELOG file to hopefully serve as an evolving example of a standardized open source project CHANGELOG. - CI file to enable Gitlab Continuous Integration. - Docker files, as Dockerfile and docker-compose, to make easy development and Deploy #6 (Horstmann). -- Update Node to 10.* #6 (Horstmann). +- Update Node to 10.\* #6 (Horstmann). diff --git a/form-creator-database b/form-creator-database index ed5adee..d378c2e 160000 --- a/form-creator-database +++ b/form-creator-database @@ -1 +1 @@ -Subproject commit ed5adee518fdc089faa39bee67e6aac7dcc91ca9 +Subproject commit d378c2e3c5324a90a0a72479ce4be68e5ff65c68 diff --git a/package.json b/package.json index 4fed803..6a9befb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "form-creator-api", - "version": "1.2.7", + "version": "1.3.0", "description": "RESTful API used to manage and answer forms.", "main": "index.js", "scripts": { diff --git a/src/api/controllers/formAnswer.ts b/src/api/controllers/formAnswer.ts index dee84f5..b75b9f8 100644 --- a/src/api/controllers/formAnswer.ts +++ b/src/api/controllers/formAnswer.ts @@ -19,15 +19,15 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { OptHandler } from "../../utils/optHandler"; -import { InputAnswerOptions, InputAnswerOptionsDict } from "../../core/inputAnswer"; +import { waterfall } from "async"; +import { NextFunction, Response } from "express"; import { Form, FormOptions } from "../../core/form"; import { FormAnswer, FormAnswerOptions } from "../../core/formAnswer"; +import { InputAnswerOptions, InputAnswerOptionsDict } from "../../core/inputAnswer"; +import { OptHandler } from "../../utils/optHandler"; import { ValidationHandler } from "../../utils/validationHandler"; -import { Response, NextFunction } from "express"; import { Request } from "../apiTypes"; -import { waterfall } from "async"; -const util = require('util'); +const util = require("util"); export class AnswerCtrl { @@ -42,25 +42,27 @@ export class AnswerCtrl { return; } - let inputAnswerOptionsDict: InputAnswerOptionsDict = {} + const inputAnswerOptionsDict: InputAnswerOptionsDict = {}; for (const key of Object.keys(req.body)) { inputAnswerOptionsDict[parseInt(key, 10)] = []; for (const i in req.body[key]) { + console.log(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); } } + console.log(inputAnswerOptionsDict); - let formAnswerOpt: FormAnswerOptions = { + const formAnswerOpt: FormAnswerOptions = { form: form , timestamp: new Date(Date.now()) , inputsAnswerOptions: inputAnswerOptionsDict - } + }; try { const formAnswer: FormAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOpt)); diff --git a/src/core/input.ts b/src/core/input.ts index 59d161c..1b09886 100644 --- a/src/core/input.ts +++ b/src/core/input.ts @@ -21,6 +21,7 @@ import { InputType, ValidationType } from "../utils/enumHandler"; import { SubForm, SubFormOptions } from "./subForm"; +import { Table, TableOptions } from "./table"; /** Parameters used to create a input object. */ export interface InputOptions { @@ -42,6 +43,8 @@ export interface InputOptions { sugestions?: Sugestion[]; /** A SubForm object that is a reference to another Form */ subForm?: SubFormOptions; + /** A table object */ + table?: Table; } /** Validation contains the type of it, and n arguments to validate if necessary */ export interface Validation { @@ -82,6 +85,9 @@ export class Input { public readonly sugestions: Sugestion[]; /** A SubForm object that is a reference to another Form */ public readonly subForm: SubForm; + /** A table object */ + public table?: Table; + /** * Creates a new instance of Input Class * @param options - InputOptions instance to create a input. @@ -108,6 +114,11 @@ export class Input { } else { this.subForm = null; } + if (options.table) { + this.table = options.table; + } else { + this.table = null; + } this.validation = options.validation; } } diff --git a/src/core/inputAnswer.ts b/src/core/inputAnswer.ts index c706e3e..654a56e 100644 --- a/src/core/inputAnswer.ts +++ b/src/core/inputAnswer.ts @@ -21,6 +21,7 @@ import { OptHandler } from "../utils/optHandler"; import { FormAnswer, FormAnswerOptions } from "./formAnswer"; +import { Table, TableOptions } from "./table"; /** Parameters used to create a input object. */ export interface InputAnswerOptions { @@ -31,9 +32,11 @@ export interface InputAnswerOptions { /** Place where answers should be (multivalored answers). */ placement: number; /** Input's answer */ - value: string; + value: string | Table; /** SubForm's answer */ subForm?: FormAnswerOptions; + /** Table object for answers to table questions */ + table?: Table; } /** Parameters used to create a dictionary to uses as an collection of InputAnswerOptions Object. */ @@ -59,9 +62,11 @@ export class InputAnswer { /** Place where input should be in the form. */ public readonly placement: number; /** Input's Description. */ - public readonly value: string; + public readonly value: string | Table; /** SubForm Answer. */ public readonly subForm: FormAnswer; + /** Table object for answers to table questions */ + public table: Table; /** * Creates a new instance of InputAnswer Class @@ -73,6 +78,7 @@ export class InputAnswer { this.placement = options.placement; this.value = options.value; this.subForm = options.subForm ? new FormAnswer(OptHandler.formAnswer(options.subForm)) : null; + this.table = options.table ? options.table : null; } } diff --git a/src/core/inputUpdate.spec.ts b/src/core/inputUpdate.spec.ts index dc7fe8d..5f4c0a8 100644 --- a/src/core/inputUpdate.spec.ts +++ b/src/core/inputUpdate.spec.ts @@ -21,6 +21,7 @@ import { expect } from "chai"; import { inputUpdateScenario } from "../../test/scenario"; +import { DiffHandler } from "../utils/diffHandler"; import { InputType, UpdateType, ValidationType } from "../utils/enumHandler"; import { TestHandler } from "../utils/testHandler"; import { Input, InputOptions } from "./input"; @@ -31,4 +32,11 @@ describe("InputUpdate", () => { TestHandler.testInputUpdate(inputUpdateScenario.resInputUpdate, inputUpdateScenario.expInputUpdate); done(); }); + + it("should create a valid inputUpdate which changes the table", (done) => { + const resInputUpdate = DiffHandler.diff(inputUpdateScenario.formWithDiffTable1, inputUpdateScenario.formWithTable1); + + TestHandler.testInputUpdate(resInputUpdate.inputUpdates[0], inputUpdateScenario.expInputUpdate2); + done(); + }); }); diff --git a/src/core/table.ts b/src/core/table.ts new file mode 100644 index 0000000..b173d51 --- /dev/null +++ b/src/core/table.ts @@ -0,0 +1,56 @@ +/* + * 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/>. + */ + +/** Parameters used to create a form object. */ +export interface TableOptions { + /** Unique identifier of a Table instance */ + id?: number; + + rows: number; + + columns: number; + + matrix: any[][]; +} +/** + * Form Class to manage project's forms + */ +export class Table { + /** Unique identifier of a Form instance */ + public readonly id: number; + /** Form's title. An human-understandable identifier. Not unique */ + public readonly rows: number; + /** Form Description, as propose */ + public readonly columns: number; + /** Array of input. containing question */ + public readonly matrix: any[][]; + + /** + * Creates a new instance of Form Class + * @param options - TableOptions instance to create a table. + */ + constructor(options: TableOptions) { + this.id = options.id ? options.id : null; + this.rows = options.rows; + this.columns = options.columns; + this.matrix = new Array(this.rows).fill(null).map(() => new Array(this.columns).fill(null)); + } + } diff --git a/src/utils/answerQueryBuilder.ts b/src/utils/answerQueryBuilder.ts index 9ec441b..64938b8 100644 --- a/src/utils/answerQueryBuilder.ts +++ b/src/utils/answerQueryBuilder.ts @@ -23,7 +23,10 @@ import { eachSeries, waterfall } from "async"; import { Pool, QueryResult } from "pg"; import { Form } from "../core/form"; import { FormAnswer, FormAnswerOptions } from "../core/formAnswer"; +import { Input } from "../core/input"; import { InputAnswer, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; +import { Table, TableOptions } from "../core/table"; +import { InputType } from "./enumHandler"; import { ErrorHandler } from "./errorHandler"; import { FormQueryBuilder } from "./formQueryBuilder"; import { OptHandler } from "./optHandler"; @@ -105,7 +108,9 @@ export class AnswerQueryBuilder extends QueryBuilder { (formAnswerId: number, callback: (err: Error, result?: any) => void) => { eachSeries(Object.keys(formAnswer.inputAnswers), (key, outerCallback) => { eachSeries(formAnswer.inputAnswers[parseInt(key, 10)], (inputsAnswer, innerCallback) => { - this.writeInputController(formAnswerId, inputsAnswer, innerCallback); + const inputTmp = formAnswer.form.inputs.find((input) => input.id === inputsAnswer.idInput); + const type: number = inputTmp ? inputTmp.type : InputType.SUBFORM; + this.writeInputController(formAnswerId, inputsAnswer, type, innerCallback); }, (error) => { outerCallback(error); }); @@ -126,13 +131,29 @@ export class AnswerQueryBuilder extends QueryBuilder { * @param cb - Callback function which contains the data read. * @param cb.err - Error information when the method fails. */ - private writeInputController(formAnswerId: number, inputAnswer: InputAnswer, cb: (err: Error) => void) { + private writeInputController(formAnswerId: number, inputAnswer: InputAnswer, inputType: InputType, cb: (err: Error) => void) { waterfall([ (callback: (err: Error, result?: number) => void) => { - this.executeWriteInput(formAnswerId, inputAnswer, (err: Error, id?: number) => { + /* if (inputType === InputType.TABLE) + { + callback(null, id); + return; + } */ + this.executeWriteInput(formAnswerId, inputAnswer, inputType, (err: Error, id?: number) => { callback(err, id); }); }, + (inputId: number, callback: (err: Error, id?: number) => void) => { + if (inputType !== InputType.TABLE) + { + callback(null, inputId); + return; + } + this.writeTableController(inputId, inputAnswer, (err: Error) => { + + callback(err, inputId); + }); + }, (inputId: number, callback: (err: Error) => void) => { this.writeSubFormController(inputAnswer.subForm, inputId, (err: Error) => { callback(err); @@ -213,7 +234,7 @@ export class AnswerQueryBuilder extends QueryBuilder { * @param cb.err - Error information when the method fails. * @param cb.result - InputAnswer identifier or null if any error occurs. */ - private executeWriteInput(formAnswerId: number, inputAnswer: InputAnswer, cb: (err: Error, result?: number) => void) { + private executeWriteInput(formAnswerId: number, inputAnswer: InputAnswer, inputType: InputType, cb: (err: Error, result?: number) => void) { const queryString: string = "INSERT INTO input_answer (id_form_answer, id_input, id_sub_form, value, placement) \ VALUES ( $1, $2, $3, $4, $5 ) \ RETURNING id;"; @@ -223,7 +244,7 @@ export class AnswerQueryBuilder extends QueryBuilder { formAnswerId , inputAnswer.idInput , inputAnswer.subForm ? inputAnswer.subForm.id : null - , inputAnswer.value + , (inputType !== InputType.TABLE) ? inputAnswer.value : null , inputAnswer.placement ] }; @@ -443,14 +464,38 @@ export class AnswerQueryBuilder extends QueryBuilder { this.readSubFormController(inputAnswer, inputAnswersOpts, innerCallback); }, () => { 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]; + const tableInputAnswers: InputAnswerOptions[] = inputAnswersOpts.filter((obj) => obj.value === null && obj.subForm === null); + const otherInputAnswers: InputAnswerOptions[] = inputAnswersOpts.filter((obj) => obj.value !== null || obj.subForm !== null); + + eachSeries(tableInputAnswers, (input, anotherCallback) => { + this.executeReadTable(input.id, (error: Error, table?: Table) => { + const inputAnswerTmp: InputAnswerOptions = { + id: input.id + , idInput: input.idInput + , placement: input.placement + , value: table + , subForm: input.subForm + }; + otherInputAnswers.push(inputAnswerTmp); + + anotherCallback(error); + }); + }, (e) => { + if (e) { + callback(e, null); + return; } - } + + for (const i of otherInputAnswers) { + if (inputsAnswerResults[i["idInput"]]) { + inputsAnswerResults[i["idInput"]].push(i); + inputsAnswerResults[i["idInput"]] = Sorter.sortByPlacement(inputsAnswerResults[i["idInput"]]); + } else { + inputsAnswerResults[i["idInput"]] = [i]; + } + } + } + ); callback(null, inputsAnswerResults); }); }, @@ -604,4 +649,193 @@ export class AnswerQueryBuilder extends QueryBuilder { cb(null, result); }); } + + /** + * Asynchronously write a table on database without transactions. + * @param answerId - Input identifier which validations are linked to. + * @param answer - The input containing the table to be added. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + */ + private writeTableController(answerId: number, answer: InputAnswer, cb: (err: Error) => void) { + let row: number = -1; + let column: number; + + waterfall([ + (callback: (err: Error, tableId: number) => void) => { + this.executeCreateTable (answerId, callback); + }, + (tableId: number, callback: (err: Error) => void) => { + const tableTmp = answer.value as Table; + eachSeries(tableTmp.matrix, (line, outerCallback) => { + column = 0; + row++; + eachSeries(line, (value, innerCallback) => { + this.executeWriteCell(tableId, row, column, value, innerCallback); + column++; + }, (err) => { + outerCallback(err); + }); + }, (err) => { + callback(err); + }); + } + ], (err) => { + if (err) { + cb(err); + return; + } + cb(null); + }); + } + + /** + * Asynchronously insert or update a table cell on database without transactions. + * @param tableId - Id of the table to be changed. + * @param row - The cell's row. + * @param column - The cell's column. + * @param value - The value of the cell. + * @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 executeWriteCell(tableId: number, row: number, column: number, value: string, cb: (err: Error) => void) { + const queryString: string = "INSERT INTO tables_answer (table_id, row, column_t, value) \ + VALUES ($1, $2, $3, $4) \ + ON CONFLICT ON CONSTRAINT table_answer_id_row_col \ + DO UPDATE SET (table_id, row, column_t, value, enabled) = ($1, $2, $3, $4, true);"; + const query: QueryOptions = { + query: queryString + , parameters: [ + tableId + , row + , column + , value + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null); + }); + } + + /** + * Asynchronously create a table on database without transactions. + * @param answerId - Answer that has a table associated. + * @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 executeCreateTable(answerId: number, cb: (err: Error, result?: number) => void) { + const queryString: string = "INSERT INTO answer_table (answer_id) \ + VALUES ($1) \ + RETURNING table_id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + answerId + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null, result.rows[0]["table_id"]); + }); + } + + /** + * Asynchronously delete a table cell on database without transactions. + * @param tableId - Id of the table to be changed. + * @param row - The cell's row. + * @param column - The cell's column. + * @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 executeDeleteCell(tableId: number, row: number, column: number, cb: (err: Error) => void) { + const queryString: string = "UPDATE tables_answer SET enabled=false \ + WHERE table_id=$1 AND row=$2 AND column_t=$3;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + tableId, + row, + column + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null); + }); + } + + /** + * Asynchronously read tables 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.result - inputs or null if inputs not exists. + */ + private executeReadTable(id: number, cb: (err: Error, table?: Table) => void) { + waterfall([ + (callback: (err: Error, options: TableOptions) => void) => { + const queryString: string = "SELECT t.table_id, COUNT (DISTINCT row) AS row, COUNT (DISTINCT column_t) as column_t \ + FROM tables_answer t \ + INNER JOIN answer_table i ON t.table_id = i.table_id \ + WHERE i.answer_id=$1 AND t.enabled=true \ + GROUP BY t.table_id;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + const tableOptions: TableOptions = { + id: result.rows[0]["table_id"], + rows: parseInt(result.rows[0]["row"], 10), + columns: parseInt(result.rows[0]["column_t"], 10), + matrix: [] + }; + callback(err, tableOptions); + }); + }, + (options: TableOptions, callback: (err: Error, table?: Table) => void) => { + const queryString: string = "SELECT id, table_id, row, column_t, value \ + FROM tables_answer \ + WHERE table_id=$1 AND enabled=true ORDER BY id;"; + const query: QueryOptions = { + query: queryString + , parameters: [options.id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + const table: Table = new Table(options); + for (const row of result.rows) { + const tRow: number = row["row"]; + const tColumn: number = row["column_t"]; + table.matrix[tRow][tColumn] = row["value"]; + } + callback(err, table); + }); + } + ], (err, table?: Table) => { + if (err) { + cb(err); + return; + } + cb(null, table); + }); + } + } diff --git a/src/utils/dbHandler.spec.ts b/src/utils/dbHandler.spec.ts index f8e3d6a..f33e5d9 100644 --- a/src/utils/dbHandler.spec.ts +++ b/src/utils/dbHandler.spec.ts @@ -149,6 +149,23 @@ describe("Database Handler", () => { }); }); + it("should insert a table cell", (done) => { + dbhandler.form.executeQuery(dbHandlerScenario.insertCell, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("INSERT"); + expect(result.rowCount).to.be.equal(1); + done(); + }); + }); + + it("should remove an existent table cell", (done) => { + dbhandler.form.executeQuery(dbHandlerScenario.deleteCell, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("UPDATE"); + done(); + }); + }); + it("should select all input validations", (done) => { dbhandler.form.executeQuery(dbHandlerScenario.selectInputValidations, (err: Error, result?: QueryResult) => { expect(err).to.be.a("null"); @@ -639,4 +656,12 @@ describe("Read and Write on Database", () => { done(); }); + it("should insert an input with table", (done) => { + dbhandler.form.write(dbHandlerScenario.formWithTable1, (err: Error, formResult: Form) => { + expect(err).to.be.a("null"); + TestHandler.testForm(dbHandlerScenario.formWithTable1, formResult); + done(); + }); + }); + }); diff --git a/src/utils/diffHandler.ts b/src/utils/diffHandler.ts index bfc5755..02c4077 100644 --- a/src/utils/diffHandler.ts +++ b/src/utils/diffHandler.ts @@ -23,6 +23,7 @@ import { Form, FormOptions } from "../core/form"; import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; import { Input, InputOptions } from "../core/input"; import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; +import { Table } from "../core/table"; import { InputType, UpdateType, ValidationType } from "./enumHandler"; import { Sorter } from "./sorter"; @@ -61,6 +62,51 @@ export class DiffHandler { if (sortedNewInputs[j].placement !== sortedOldInputs[i].placement) { formUpdate.inputUpdates.push(DiffHandler.swapInput(sortedNewInputs[j], sortedOldInputs[i])); } + + if (sortedNewInputs[j].type === InputType.TABLE){ + let rowsMax: number = sortedOldInputs[i].table.rows; + let columnsMax: number = sortedOldInputs[i].table.columns; + + if (sortedNewInputs[j].table.rows > rowsMax){ + rowsMax = sortedNewInputs[j].table.rows; + } + if (sortedNewInputs[j].table.columns > columnsMax){ + columnsMax = sortedNewInputs[j].table.columns; + } + + const matrixTmp: any[][] = new Array(rowsMax).fill(-1).map(() => new Array(columnsMax).fill(-1)); + + let k: number = 0; + let l: number = 0; + for (k; k < sortedNewInputs[j].table.rows; k++){ + l = 0; + for (l; l < sortedNewInputs[j].table.columns; l++){ + matrixTmp[k][l] = sortedNewInputs[j].table.matrix[k][l]; + } + } + + const tableTmp: Table = { + id: sortedNewInputs[j].table.id, + rows: rowsMax, + columns: columnsMax, + matrix: matrixTmp + }; + + const inputTmp: Input = { + id: sortedNewInputs[j].id, + placement: sortedNewInputs[j].placement, + description: sortedNewInputs[j].description, + enabled: sortedNewInputs[j].enabled, + question: sortedNewInputs[j].question, + type: sortedNewInputs[j].type, + validation: sortedNewInputs[j].validation, + sugestions: sortedNewInputs[j].sugestions, + subForm: sortedNewInputs[j].subForm, + table: tableTmp + + }; + formUpdate.inputUpdates.push(DiffHandler.tableInput(inputTmp)); + } j++; i++; } @@ -151,6 +197,21 @@ export class DiffHandler { return inputUpdate; } + /** + * Create an InputUpdate object which updates an Input that has a table. + * @param input - An input that has a table + * @returns - An InputUpdate object. + */ + private static tableInput(newInput: Input): InputUpdate { + + const inputUpdate: InputUpdate = { + input: newInput + , inputOperation: UpdateType.TABLE + , value: null + }; + return inputUpdate; + } + private static isIdValid(obj: any): boolean { return ((obj.id !== null) && (obj.id !== undefined) && (obj.id > 0) && (typeof obj.id === "number")); } diff --git a/src/utils/enumHandler.ts b/src/utils/enumHandler.ts index 5035452..582c375 100644 --- a/src/utils/enumHandler.ts +++ b/src/utils/enumHandler.ts @@ -29,6 +29,7 @@ export enum InputType { RADIO, SELECT, SUBFORM, + TABLE, NONE } @@ -37,6 +38,7 @@ export enum UpdateType { REMOVE, SWAP, REENABLED, + TABLE, NONE } @@ -86,6 +88,8 @@ export class EnumHandler { return "select"; case InputType.SUBFORM: return "subform"; + case InputType.TABLE: + return "table"; default: return ""; } @@ -109,6 +113,8 @@ export class EnumHandler { return InputType.SELECT; case "subform": return InputType.SUBFORM; + case "table": + return InputType.TABLE; default: return InputType.NONE; } @@ -185,6 +191,8 @@ export class EnumHandler { return "swap"; case UpdateType.REENABLED: return "reenabled"; + case UpdateType.TABLE: + return "table"; default: return ""; } diff --git a/src/utils/formQueryBuilder.ts b/src/utils/formQueryBuilder.ts index 4d4b7f8..e6a40ab 100644 --- a/src/utils/formQueryBuilder.ts +++ b/src/utils/formQueryBuilder.ts @@ -26,6 +26,7 @@ import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; import { Input, InputOptions, Sugestion, Validation } from "../core/input"; import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; import { SubForm } from "../core/subForm"; +import { Table, TableOptions } from "../core/table"; import { EnumHandler, InputType, UpdateType, ValidationType } from "./enumHandler"; import { ErrorHandler } from "./errorHandler"; import { OptHandler } from "./optHandler"; @@ -246,6 +247,7 @@ export class FormQueryBuilder extends QueryBuilder { return; } + // console.log("TIPO FORM: ", typeof(result.rows[0]["id"])); const formTmp: Form = new Form({ id: result.rows[0]["id"] , title: result.rows[0]["title"] @@ -329,6 +331,7 @@ export class FormQueryBuilder extends QueryBuilder { , enabled: i["enabled"] , validation: [] , sugestions: [] + , table: undefined , subForm: undefined }; inputArrayTmp.push(new Input(inputTmp)); @@ -393,6 +396,7 @@ export class FormQueryBuilder extends QueryBuilder { , type: input.type , validation: input.validation , sugestions: input.sugestions + , table: input.table , subForm: new SubForm({ id: i.id , inputId: i.id_input @@ -411,22 +415,46 @@ export class FormQueryBuilder extends QueryBuilder { outerCallback(null, newSubFormInputs); }); }, - (newSubFormInputs: Input[], anotherCallback: (er: Error, newForm: Form) => void) => { + (newSubFormInputs: Input[], anotherCallback: (er: Error, newTableInputs?: Input[]) => void) => { let inputsTmp = inputs; for (const i of newSubFormInputs) { inputsTmp.push(i); } - inputsTmp = Sorter.sortByPlacement(inputsTmp); + // inputsTmp = Sorter.sortByPlacement(inputsTmp); + const tableInputs: Input[] = inputsTmp.filter((obj) => obj.type === InputType.TABLE); + const otherInputs: Input[] = inputsTmp.filter((obj) => obj.type !== InputType.TABLE); + + eachSeries(tableInputs, (input, innerCallback) => { + this.executeReadTable(input.id, (error: Error, table?: Table) => { + input.table = table; + // console.log("INPUT C/ TAB: ", input); + innerCallback(error); + }); + }, (e) => { + if (e) { + anotherCallback(e, null); + return; + } + inputsTmp = otherInputs; + for (const i of tableInputs) { + inputsTmp.push(i); + } + inputsTmp = Sorter.sortByPlacement(inputsTmp); + anotherCallback(null, inputsTmp); + }); + }, + (newTableInputs: Input[], tableCallback: (er: Error, form?: Form) => void) => { + // console.log("INPUTS TMP: ", newTableInputs); const formTmp: Form = new Form({ id: form.id , title: form.title , description: form.description - , inputs: inputsTmp + , inputs: newTableInputs , answerTimes: form.answerTimes , status: form.status }); - anotherCallback(null, formTmp); + tableCallback(null, formTmp); } ], (er, formTmp?: Form) => { if (er) { @@ -505,6 +533,68 @@ export class FormQueryBuilder extends QueryBuilder { }); } + /** + * Asynchronously read tables 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.result - inputs or null if inputs not exists. + */ + private executeReadTable(id: number, cb: (err: Error, table?: Table) => void) { + waterfall([ + (callback: (err: Error, options: TableOptions) => void) => { + const queryString: string = "SELECT t.table_id, COUNT (DISTINCT row) AS row, COUNT (DISTINCT column_t) as column_t \ + FROM tables t \ + INNER JOIN input_table i ON t.table_id = i.table_id \ + WHERE i.input_id=$1 AND t.enabled=true \ + GROUP BY t.table_id;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + // console.log("RESULTADO: ", result); + const tableOptions: TableOptions = { + id: result.rows[0]["table_id"], + rows: parseInt(result.rows[0]["row"], 10), + columns: parseInt(result.rows[0]["column_t"], 10), + matrix: [] + }; + callback(err, tableOptions); + }); + }, + (options: TableOptions, callback: (err: Error, table?: Table) => void) => { + const queryString: string = "SELECT id, table_id, row, column_t, value \ + FROM tables \ + WHERE table_id=$1 AND enabled=true ORDER BY id;"; + const query: QueryOptions = { + query: queryString + , parameters: [options.id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + // console.log(options); + const table: Table = new Table(options); + // console.log(table.matrix); + for (const row of result.rows) { + const tRow: number = row["row"]; + const tColumn: number = row["column_t"]; + table.matrix[tRow][tColumn] = row["value"]; + } + // console.log(table.matrix); + callback(err, table); + }); + } + ], (err, table?: Table) => { + if (err) { + cb(err); + return; + } + cb(null, table); + }); + } + /** * Asynchronously read validations from database without transactions. * @param id - Form identifier which validations from inputs are linked to. @@ -659,6 +749,11 @@ export class FormQueryBuilder extends QueryBuilder { (callback: (err: Error, result?: number) => void) => { this.executeWriteInput(formId, input, callback); }, + (inputId: number, callback: (err: Error, resultInputId?: number) => void) => { + this.writeTableController(inputId, input, (error: Error) => { + callback(error, inputId); + }); + }, (inputId: number, callback: (err: Error, resultInputId?: number) => void) => { this.writeValidationController(inputId, input.validation, (error: Error) => { callback(error, inputId); @@ -772,6 +867,140 @@ export class FormQueryBuilder extends QueryBuilder { }); } + /** + * Asynchronously write a table on database without transactions. + * @param inputId - Input identifier which validations are linked to. + * @param input - The input containing the table to be added. + * @param cb - Callback function which contains informations about method's execution. + * @param cb.err - Error information when the method fails. + */ + private writeTableController(inputId: number, input: Input, cb: (err: Error) => void) { + let row: number = -1; + let column: number; + + if (input.type !== InputType.TABLE){ + cb(null); + return; + } else { + waterfall([ + (callback: (err: Error, tableId: number) => void) => { + this.executeCreateTable (inputId, callback); + }, + (tableId: number, callback: (err: Error) => void) => { + eachSeries(input.table.matrix, (line, outerCallback) => { + column = 0; + row++; + eachSeries(line, (value, innerCallback) => { + this.executeWriteCell(tableId, row, column, value, innerCallback); + column++; + }, (err) => { + outerCallback(err); + }); + }, (err) => { + callback(err); + }); + } + ], (err) => { + if (err) { + cb(err); + return; + } + cb(null); + }); + } + } + + /** + * Asynchronously insert or update a table cell on database without transactions. + * @param tableId - Id of the table to be changed. + * @param row - The cell's row. + * @param column - The cell's column. + * @param value - The value of the cell. + * @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 executeWriteCell(tableId: number, row: number, column: number, value: string, cb: (err: Error) => void) { + const queryString: string = "INSERT INTO tables (table_id, row, column_t, value) \ + VALUES ($1, $2, $3, $4) \ + ON CONFLICT ON CONSTRAINT table_id_row_col \ + DO UPDATE SET (table_id, row, column_t, value, enabled) = ($1, $2, $3, $4, true);"; + const query: QueryOptions = { + query: queryString + , parameters: [ + tableId + , row + , column + , value + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null); + }); + } + + /** + * Asynchronously create a table on database without transactions. + * @param inputId - Input that has a table associated. + * @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 executeCreateTable(inputId: number, cb: (err: Error, result?: number) => void) { + const queryString: string = "INSERT INTO input_table (input_id) \ + VALUES ($1) \ + RETURNING table_id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + inputId + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null, result.rows[0]["table_id"]); + }); + } + + /** + * Asynchronously delete a table cell on database without transactions. + * @param tableId - Id of the table to be changed. + * @param row - The cell's row. + * @param column - The cell's column. + * @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 executeDeleteCell(tableId: number, row: number, column: number, cb: (err: Error) => void) { + const queryString: string = "UPDATE tables SET enabled=false \ + WHERE table_id=$1 AND row=$2 AND column_t=$3;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + tableId, + row, + column + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(null); + }); + } + /** * Asynchronously insert a form on database without transactions. * @param form - Form to be inserted. @@ -1185,6 +1414,28 @@ export class FormQueryBuilder extends QueryBuilder { }); break; } + case UpdateType.TABLE: { + let row: number = -1; + let column: number; + + eachSeries(inputUpdate.input.table.matrix, (line, outerCallback) => { + column = 0; + row++; + eachSeries(line, (value, innerCallback) => { + if (value !== -1){ + this.executeWriteCell(inputUpdate.input.table.id, row, column, value, innerCallback); + } else { + this.executeDeleteCell(inputUpdate.input.table.id, row, column, innerCallback); + } + column++; + }, (err) => { + outerCallback(err); + }); + }, (err) => { + callback(err); + }); + break; + } default: { callback(new Error("Operation " + inputUpdate.inputOperation + " not recognized")); break; diff --git a/src/utils/optHandler.ts b/src/utils/optHandler.ts index c6dd1a9..00cc606 100644 --- a/src/utils/optHandler.ts +++ b/src/utils/optHandler.ts @@ -26,6 +26,7 @@ import { InputOptions, Sugestion, Validation } from "../core/input"; import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; import { SubForm, SubFormOptions } from "../core/subForm"; +import { Table } from "../core/table"; import { User, UserOptions } from "./../core/user"; import { InputType, UpdateType } from "./enumHandler"; import { ErrorHandler } from "./errorHandler"; @@ -87,6 +88,9 @@ export class OptHandler { if ((obj.type === InputType.SUBFORM) && ((obj.subForm === undefined) || (obj.subForm === null))) { throw ErrorHandler.notFound("Input subform"); } + if ((obj.type === InputType.TABLE) && ((obj.table === undefined) || (obj.table === null))) { + throw ErrorHandler.notFound("Input table"); + } let subForm: SubForm = null; if ((obj.subForm !== null) && (obj.subForm !== undefined)) { @@ -104,6 +108,7 @@ export class OptHandler { return { type: v.type, arguments: v.arguments }; }), sugestions: [], + table: obj.table, subForm }; @@ -333,4 +338,26 @@ export class OptHandler { return option; } + + public static table(obj: any): Table { + + if (obj.id === undefined) { + throw ErrorHandler.notFound("Table id"); + } + if (obj.rows === undefined) { + throw ErrorHandler.notFound("Table row count"); + } + if (obj.columns === undefined) { + throw ErrorHandler.notFound("Table columns count"); + } + + const option: Table = { + id: obj.id + , rows: obj.rows + , columns: obj.columns + , matrix: [[]] + }; + + return option; + } } diff --git a/src/utils/testHandler.ts b/src/utils/testHandler.ts index 3666abc..c92b4f0 100644 --- a/src/utils/testHandler.ts +++ b/src/utils/testHandler.ts @@ -27,6 +27,7 @@ import { Input, Validation } from "../core/input"; import { InputAnswer } from "../core/inputAnswer"; import { InputUpdate } from "../core/inputUpdate"; import { SubForm } from "../core/subForm"; +import { Table } from "../core/table"; import { User } from "../core/user"; import { EnumHandler, InputType, UpdateType, ValidationType } from "./enumHandler"; import { Sorter } from "./sorter"; @@ -79,6 +80,10 @@ export class TestHandler { } else { expect(input.subForm).to.be.equal(null); } + + if (input.type === InputType.TABLE) { + TestHandler.testTable(input.table, stub.table); + } } /** @@ -168,4 +173,21 @@ export class TestHandler { expect(subForm.inputId).to.be.equal(stub.inputId); expect(subForm.contentFormId).to.be.equal(stub.contentFormId); } + + /** + * Verify if two tables are semantically equal; + * @param table - table that should be tested + * @param stub - A model table that first param should be equal to. + * @returns - True if tables are equal else false + */ + public static testTable(table: Table, stub: Table) { + expect(table.id).to.be.equal(stub.id); + expect(table.rows).to.be.equal(stub.rows); + expect(table.columns).to.be.equal(stub.columns); + for (let r = 0; r < table.rows; r++){ + for (let c = 0; c < table.columns; c++){ + expect(table.matrix[r][c]).to.be.equal(stub.matrix[r][c]); + } + } + } } diff --git a/src/utils/validationHandler.ts b/src/utils/validationHandler.ts index 5d49c0e..5dcb53e 100644 --- a/src/utils/validationHandler.ts +++ b/src/utils/validationHandler.ts @@ -185,7 +185,7 @@ export class ValidationHandler { case ValidationType.REGEX: for (const answer of inputAnswers[input.id]) { - if (!this.validateByRegex(answer.value, validation.arguments[0])) { + if (!this.validateByRegex(answer.value as string, validation.arguments[0])) { errors.push("RegEx do not match"); } } @@ -193,7 +193,7 @@ export class ValidationHandler { case ValidationType.MANDATORY: for (const answer of inputAnswers[input.id]) { - if (!(this.validateMandatory(answer.value))) { + if (!(this.validateMandatory(answer.value as string))) { errors.push("Input answer is mandatory"); } } @@ -201,7 +201,7 @@ export class ValidationHandler { case ValidationType.MAXCHAR: for (const answer of inputAnswers[input.id]) { - if (!(this.validateMaxChar(answer.value, validation.arguments[0]))) { + if (!(this.validateMaxChar(answer.value as string, validation.arguments[0]))) { errors.push("Input answer must be lower than " + validation.arguments[0]); } } @@ -209,7 +209,7 @@ export class ValidationHandler { case ValidationType.MINCHAR: for (const answer of inputAnswers[input.id]) { - if (!(this.validateMinChar(answer.value, validation.arguments[0]))) { + if (!(this.validateMinChar(answer.value as string, validation.arguments[0]))) { errors.push("Input answer must be greater than " + validation.arguments[0]); } } @@ -217,7 +217,7 @@ export class ValidationHandler { case ValidationType.TYPEOF: for (const answer of inputAnswers[input.id]) { - if (!(this.validateTypeOf(answer.value, validation.arguments[0]))) { + if (!(this.validateTypeOf(answer.value as string, validation.arguments[0]))) { errors.push("Input answer must be a " + validation.arguments[0]) + " type"; } } diff --git a/test/scenario.ts b/test/scenario.ts index 6763eab..85585dd 100644 --- a/test/scenario.ts +++ b/test/scenario.ts @@ -26,6 +26,7 @@ import { Input, InputOptions, Validation } from "../src/core/input"; import { InputAnswerOptions, InputAnswerOptionsDict } from "../src/core/inputAnswer"; import { InputUpdate, InputUpdateOptions } from "../src/core/inputUpdate"; import { SubForm, SubFormOptions } from "../src/core/subForm"; +import { Table, TableOptions } from "../src/core/table"; import { User } from "../src/core/user"; import { EnumHandler, InputType, UpdateType, ValidationType } from "../src/utils/enumHandler"; import { OptHandler } from "../src/utils/optHandler"; @@ -526,6 +527,60 @@ const Input4Placement2id4: Input = { , sugestions: [] , subForm: null }; +/* input with table */ +const InputWithTable1: Input = { + id: 38 + , placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TABLE + , validation: [] + , table: { + id: 1, + rows: 2, + columns: 2, + matrix: [["1", "2"], ["3", "4"]] + } + , sugestions: [] + , subForm: null +}; +/* input with table */ +const InputWithDiffTable1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TABLE + , validation: [] + , id: 38 + , table: { + id: 1, + rows: 1, + columns: 3, + matrix: [["a", "b", "c"]] + } + , sugestions: [] + , subForm: null +}; +/* input with table */ +const InputUpdateTable1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TABLE + , validation: [] + , id: 38 + , table: { + id: 1, + rows: 2, + columns: 3, + matrix: [["a", "b", "c"], [-1, -1, -1]] + } + , sugestions: [] + , subForm: null +}; /** input to be compared */ const mixedInput1: Input = { placement: 1 @@ -697,6 +752,28 @@ const formBase3: Form = { answerTimes: true , status: true }; +/** Form with table 1 */ +const formWithTable1: Form = { + id: 12 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + InputWithTable1 + ], + answerTimes: true + , status: true +}; +/** Form with table 1 */ +const formWithDiffTable1: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + InputWithDiffTable1 + ], + answerTimes: true + , status: true +}; /** Empty form used as a base for comparison */ const emptyForm: Form = { id: 1 @@ -906,6 +983,11 @@ const expInputUpdate: InputUpdate = { , value: null , id: null }; +const expInputUpdate2: InputUpdate = { + input: InputUpdateTable1 + , inputOperation: UpdateType.TABLE + , value: null +}; /** ============================================= */ /** Input testing Scenario */ @@ -2142,6 +2224,14 @@ const queryToRemoveInputAnswers: QueryOptions = { query: queryStringRemoveInputA const queryStringRemoveForm3: string = "UPDATE form SET status='false' WHERE id=3;"; /** Query obj to be used to delete form */ const queryToRemoveForm3: QueryOptions = { query: queryStringRemoveForm3, parameters: [] }; +/** QueryString to be used on a query to add table cell */ +const queryStringInsertCell: string = "INSERT INTO tables (table_id, row, column_t, value) VALUES (0, 0, 0, 'test');"; +/** Query obj to be used to insert cell */ +const queryToInsertCell: QueryOptions = { query: queryStringInsertCell, parameters: [] }; +/** QueryString to be used on a query to add table cell */ +const queryStringDeleteCell: string = "UPDATE tables SET enabled=false WHERE table_id=0 AND row=0 AND column_t=0;"; +/** Query obj to be used to insert cell */ +const queryToDeleteCell: QueryOptions = { query: queryStringDeleteCell, parameters: [] }; /** 2nd describe */ /** Input Options object to be used on a formOptions */ @@ -2251,14 +2341,14 @@ const inputObjToUpdateDBH3: InputOptions = { ] , id: 2 }; -/** InputUpdateOptions object to do an remotion update */ +/** InputUpdateOptions object to do a remotion update */ const updateObjDBH3REMOVE: InputUpdateOptions = { id: 3 , input: inputObjToUpdateDBH3 , inputOperation: UpdateType.REMOVE , value: null }; -/** InputUpdateOptions object to do an reenable update */ +/** InputUpdateOptions object to do a reenable update */ const updateObjDBH3REENABLE: InputUpdateOptions = { id: 3 , input: inputObjToUpdateDBH3 @@ -3820,7 +3910,13 @@ export const inputUpdateScenario = { /** Answer input recieved to update */ resInputUpdate: resInputUpdate, /** Expected input after the update */ - expInputUpdate: expInputUpdate + expInputUpdate: expInputUpdate, + /** Input with table 1 */ + formWithTable1: formWithTable1, + /** Input with table 2 */ + formWithDiffTable1: formWithDiffTable1, + /** Expected inputUpdate object */ + expInputUpdate2: expInputUpdate2 }; /** input testing scenario */ export const inputScenario = { @@ -4086,6 +4182,10 @@ export const dbHandlerScenario = { removeInputAnswers: queryToRemoveInputAnswers, /** Query to delete form with id 3 */ deleteForm3: queryToRemoveForm3, + /** Query to insert table cell on the Database */ + insertCell: queryToInsertCell, + /** Query to delete table cell on the Database */ + deleteCell: queryToDeleteCell, /** Form options that will have to be read */ formToRead: formOptsObjDBH, @@ -4137,6 +4237,8 @@ export const dbHandlerScenario = { formUpdateWithSubFormLoop: formUpdateWithSubForm4, /** FormAnswer for Form with SubForm */ formAnswerWithSubForms: formAnswerOptionsForm11, + /** Form with a table */ + formWithTable1: formWithTable1, /** User obj to be inserted */ toBeInserted: defaultUser, -- GitLab