diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..1d8309a56b34f50343a9534ec68217e30754cb64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.dockerignore +docker-compose.yml +docker-compose.yaml +Dockerfile +.env* +package-lock.json +node_modules +*.bak +.editorconfig diff --git a/.env-form-api b/.env-form-api new file mode 100644 index 0000000000000000000000000000000000000000..72506c40968dca4e9ec64d22abd4f8a6c59457a4 --- /dev/null +++ b/.env-form-api @@ -0,0 +1 @@ +FORM_CREATOR_API_NODE_ENV=development diff --git a/.gitignore b/.gitignore index dd27e1a2765997dd47f9cd31bb36672e3269ce13..73ebd01968826de5bb4291d1bcd9d54c64e1d6e8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ /coverage /build /dist - +*.bak +.env +docker-compose.yaml +.editorconfig diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 294eafe63e05610927b192f5074483021dfef1c7..fdf4292066895a8eb033a0321249efb3e7869930 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,17 @@ -image: node:6.12.2 +image: node:10-stretch +services: + - postgres:10 + +variables: + POSTGRES_DB: 'form-creator' + POSTGRES_HOST: 'postgres' + PGPASSWORD: '123mudar' + POSTGRES_PASSWORD: '123mudar' + POSTGRES_USER: 'runner' + POSTGRES_PASSWORD: '123mudar' + POSTGRES_PORT: 5432 + image_version: '' + DOCKER_URL: 'dockerregistry.c3sl.ufpr.br:5000/c3sl/simmctic-form-creator-api' cache: paths: @@ -6,10 +19,24 @@ cache: stages: - test - + - build + - deploy run_test: stage: test + before_script: + - apt-get update -q -y + - apt-get install wget gnupg -y + - echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" > /etc/apt/sources.list.d/pgdg.list + - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - + - apt-get update -q -y + - apt-get install -y postgresql-client-10 + - git clone --recurse-submodules https://gitlab.c3sl.ufpr.br/simmctic/form-creator/form-creator-database.git form-creator-database + - cd form-creator-database + - psql-manager/manager.sh create workspace + - psql-manager/manager.sh fixture workspace + - cd .. + - rm -rf form-creator-database script: - yarn install --frozen-lockfile --silent --non-interactive - ln -s config.env.example config/test.env @@ -17,3 +44,24 @@ run_test: - yarn run lint tags: - node + +build: + stage: build + script: + - image_version=$(grep \"version\" ./package.json | cut -f2 -d':'| sed -e 's/"\|,//g' | tr -d ' ') + - docker build -t ${DOCKER_URL}:${image_version} -t ${DOCKER_URL}:latest . + tags: + - docker + - build + +deploy: + stage: deploy + script: + - image_version=$(grep \"version\" ./package.json | cut -f2 -d':'| sed -e 's/"\|,//g' | tr -d ' ') + - docker push ${DOCKER_URL}:${image_version} + - docker push ${DOCKER_URL}:latest + tags: + - docker + - build + only: + - master diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..b7f04653bcfc2a735badcc78f03337f20f3ddbc6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,233 @@ +# 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.0.0 - 19-08-2019 +### Changed +- 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) + + +## 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 +- Create updateForm method to unify writeFormUpdate and writeInputUpdate methods +- 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) + + +## 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 + + +## 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 + + +## 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 + + +## 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 +### Security +- 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) + + +## 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 + + +## 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) +### 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) +- ValidationType to has as arguments an array of strings +- ValidationHandler to receive a string as size instead number as validation arguments +- 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 + + +## 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 + + +## 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) + + +## 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) + + +## 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) + + +## 0.0.2 - 05-02-2019 +### Added +- 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. +- 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). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c0266c20f44ff59c5f6be6ab9f3d7d36c6348b08 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM node:10-stretch-slim + +# Set an environment variable to store where the app is installed to inside of the Docker image. +ENV WORKSPACE /app +RUN mkdir -p $WORKSPACE + +# Change WORKSPACE owner +RUN chown -R node:node $WORKSPACE + +# Change running user +USER node +# This sets the context of where commands will be ran in and is documented +# on Docker's website extensively. +# Set app directory +WORKDIR $WORKSPACE + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY --chown=node:node package.json . +COPY --chown=node:node yarn.lock . + + + + +RUN yarn install --frozen-lockfile --silent --non-interactive +# If you are building your code for production +# RUN yarn install --production --frozen-lockfile --silent --non-interactive + + +# Bundle app source +COPY --chown=node:node . . + + +EXPOSE 3000 + + +CMD [ "yarn", "start" ] diff --git a/package.json b/package.json index 0b24d0421bef9961f4fc3da9420b9a82e0c86a2f..eef8fd87e5eea05c61f639a7607921047210ea5e 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "ts-api-start-up", - "version": "0.0.1", - "description": "Typescript (API) base project", + "name": "form-creator-api", + "version": "1.0.0", + "description": "RESTful API used to manage and answer forms.", "main": "index.js", "scripts": { - "start": "scripts/start.sh config/config.env" - , "test": "scripts/test.sh config/test.env" - , "lint": "tslint -s node_modules/tslint-stylish -t stylish src/**/*.ts test/**/*.ts" - , "show-coverage": "xdg-open coverage/lcov-report/index.html" - , "doc-code": "typedoc --mode 'file' --module 'commonjs' --target 'ES6' --ignoreCompilerErrors --exclude '**/*.spec.ts' --out 'doc/code' 'src'" + "start": "scripts/start.sh config/config.env", + "test": "scripts/test.sh config/test.env", + "lint": "tslint -s node_modules/tslint-stylish -t stylish src/**/*.ts test/**/*.ts", + "show-coverage": "xdg-open coverage/lcov-report/index.html", + "doc-code": "typedoc --mode 'file' --module 'commonjs' --target 'ES6' --ignoreCompilerErrors --exclude '**/*.spec.ts' --out 'doc/code' 'src'" }, "repository": { "type": "git", @@ -19,8 +19,10 @@ "dependencies": { "@types/async": "^2.0.50", "@types/express": "^4.16.0", + "@types/pg": "^7.4.13", "async": "^2.6.1", "express": "^4.16.4", + "pg": "^7.8.1", "ts-node": "^7.0.1", "typescript": "^3.2.2" }, @@ -32,8 +34,7 @@ "istanbul": "1.1.0-alpha.1", "mocha": "^5.2.0", "supertest": "^3.3.0", - "tslint": "^5.12.1", - "tslint-stylish": "^2.1.0", + "tslint": "^5.13.1", "typedoc": "^0.14.1" } } diff --git a/src/api/apiTypes.ts b/src/api/apiTypes.ts index bd77cd86d235c392b919fb051ae9178c3b557ef4..716e3f4a4f2b18c2134f72e973aad7120ce5968d 100644 --- a/src/api/apiTypes.ts +++ b/src/api/apiTypes.ts @@ -20,7 +20,7 @@ */ import * as express from "express"; -import { Collection } from "../core/collection"; +import { DbHandler } from "../utils/dbHandler"; /** * Extension of Express requests that suports the addition @@ -31,8 +31,8 @@ import { Collection } from "../core/collection"; * return a error the extension must be made. */ export interface Request extends express.Request { - /** A collection object. Simulate a database. */ - collection: Collection; + /** A Database handler instace */ + db: DbHandler; } /** diff --git a/src/api/controllers/form.spec.ts b/src/api/controllers/form.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef1e8a001543d3c174f087d78409ea5ab57b582a --- /dev/null +++ b/src/api/controllers/form.spec.ts @@ -0,0 +1,520 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { series, waterfall } from "async"; +import * as request from "supertest"; +import { expect } from "chai"; +import { QueryResult } from "pg"; +import * as server from "../../main"; +import { EnumHandler, InputType, UpdateType, ValidationType } from "../../utils/enumHandler"; +import { TestHandler } from "../../utils/testHandler"; +import { OptHandler } from "../../utils/optHandler"; +import { Form, FormOptions } from "../../core/form"; +import { FormUpdate, FormUpdateOptions } from "../../core/formUpdate"; +import { Input, InputOptions, Validation } from "../../core/input"; +import { InputUpdate, InputUpdateOptions } from "../../core/inputUpdate"; +import { DbHandler, QueryOptions } from "../../utils/dbHandler"; +import { configs } from "../../utils/config"; + +describe("API data controller", () => { + + const dbhandler = new DbHandler(configs.poolconfig); + + it("should respond 200 when getting a list of forms", (done) => { + request(server) + .get("/form") + .expect(200) + .expect((res: any) => { + expect(res.body).to.be.an("array"); + expect(res.body[0].id).to.be.equal(1); + expect(res.body[0].title).to.be.equal("Form Title 1"); + expect(res.body[0].description).to.be.equal("Form Description 1"); + + expect(res.body[1].id).to.be.equal(2); + expect(res.body[1].title).to.be.equal("Form Title 2"); + expect(res.body[1].description).to.be.equal("Form Description 2"); + + expect(res.body[2].id).to.be.equal(3); + expect(res.body[2].title).to.be.equal("Form Title 3"); + expect(res.body[2].description).to.be.equal("Form Description 3"); + + }) + .end(done); + }); + + + it("should respond 200 when getting valid form", (done) => { + request(server) + .get("/form/1") + .expect(200) + .expect((res: any) => { + expect(res.body).to.be.an("object"); + + const form: Form = new Form(OptHandler.form(res.body)); + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + ] + }; + TestHandler.testForm(form, new Form( OptHandler.form(formObj))); + + }) + .end(done); + }); + + it("should respond 500 when getting inexistent form", (done) => { + request(server) + .get("/form/10") + .expect(500) + .expect((res: any) => { + const message = "Form with id: '10' not found. Some error has occurred. Check error property for details."; + + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("error"); + expect(res.body.message).to.be.equal(message); + }) + .end(done); + }); + + it("should respond 200 when posting valid form", (done) => { + + request(server) + .post("/form") + .send({ + title: 'Form Title 4' + , description: 'Form Description 4' + , inputs: [ + { + placement: 0 + , description: 'Description Question 1 Form 4' + , question: 'Question 1 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [] + } + , { + placement: 1 + , description: 'Description Question 2 Form 4' + , question: 'Question 2 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + } + , { + placement: 2 + , description: 'Description Question 3 Form 4' + , question: 'Question 3 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 1, arguments: [ '\\d{5}-\\d{3}' ] } + , { type: 2, arguments: [] } + ] + } + ] + }) + .expect(200) + .end(done); + }); + + it("should respond 500 when posting malformed form", (done) => { + + request(server) + .post("/form") + .send({ + description: 'Form Description 4' + , inputs: [ + { + placement: 0 + , description: 'Description Question 1 Form 4' + , question: 'Question 1 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [] + } + , { + placement: 1 + , description: 'Description Question 2 Form 4' + , question: 'Question 2 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + } + , { + placement: 2 + , description: 'Description Question 3 Form 4' + , question: 'Question 3 Form 4' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 1, arguments: [ '\\d{5}-\\d{3}' ] } + , { type: 2, arguments: [] } + ] + } + ] + }) + .expect(500) + .expect((res: any) => { + const message = "Invalid Form. Check error property for details."; + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("error"); + expect(res.body.message).to.be.equal(message); + }) + .end(done); + }); + + it("should respond 200 when putting valid form update swap inputs", (done) => { + request(server) + .put("/form/1") + .send({ + id: 1 + , title: 'Form Title 1' + , description: 'Form Description 1' + , inputs: [ + { + placement: 0 + , description: 'Description Question 1 Form 1' + , question: 'Question 1 Form 1' + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + } + , { + placement: 1 + , description: 'Description Question 2 Form 1' + , question: 'Question 2 Form 1' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 2 + } + , { + placement: 2 + , description: 'Description Question 3 Form 1' + , question: 'Question 3 Form 1' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 3 + } + ] + }) + .expect(200) + .expect((res: any) => { + const message = "Updated" + expect(res.body.message).to.be.equal(message); + }) + .end(done); + }); + + it("should respond 200 when putting valid form update add inputs", (done) => { + request(server) + .put("/form/3") + .send({ + id: 3 + , title: 'Form Title 3' + , description: 'Form Description 3' + , inputs: [ + { + placement: 0 + , description: 'Description Question 1 Form 3' + , question: 'Question 1 Form 3' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 6 + } + , { + placement: 1 + , description: 'Description Question 2 Form 3' + , question: 'Question 2 Form 3' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 7 + } + , { + placement: 3 + , description: 'Description Question 4 Form 3' + , question: 'Question 4 Form 3' + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: undefined + } + , { + placement: 4 + , description: 'Description Question 5 Form 3' + , question: 'Question 5 Form 3' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: undefined + } + , { + placement: 5 + , description: 'Description Question 6 Form 3' + , question: 'Question 6 Form 3' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: undefined + } + ] + }) + .expect(200) + .expect((res: any) => { + const message = "Updated" + expect(res.body.message).to.be.equal(message); + }) + .end(done); + }); + + it("should respond 200 when putting valid form update remove inputs", (done) => { + request(server) + .put("/form/3") + .send({ + id: 3 + , title: 'Form Title 3' + , description: 'Form Description 3' + , inputs: [] + }) + + .expect(200) + .expect((res: any) => { + const message = "Updated" + expect(res.body.message).to.be.equal(message); + }) + .end(done); + }); + + it("should respond 200 when putting valid form update reenabled inputs", (done) => { + request(server) + .put("/form/3") + .send({ + id: 3 + , title: 'Form Title 3' + , description: 'Form Description 3' + , inputs: [ + { + placement: 0 + , description: 'Description Question 1 Form 3' + , question: 'Question 1 Form 3' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 6 + } + , { + placement: 1 + , description: 'Description Question 2 Form 1' + , question: 'Question 2 Form 1' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 7 + } + ] + }) + + .expect(200) + .expect((res: any) => { + const message = "Updated" + expect(res.body.message).to.be.equal(message); + }) + + .end(done); + }); + + it("should respond 500 when putting a valid form update for an inexistent form", (done) => { + request(server) + .put("/form/10") + .send({ + id: 1 + , title: 'Form Title 1' + , description: 'Form Description 1' + , inputs:[ + { + placement: 0 + , description: 'Description Question 1 Form 6' + , question: 'Question 1 Form 6' + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + } + , { + placement: 1 + , description: 'Description Question 2 Form 6' + , question: 'Question 2 Form 6' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 2 + } + , { + placement: 2 + , description: 'Description Question 3 Form 6' + , question: 'Question 3 Form 6' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + } + ] + }) + .expect(500) + .expect((res: any) => { + const message = "Could not update Form. Some error has ocurred. Check error property for details."; + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("error"); + expect(res.body.message).to.be.equal(message); + }) + .end(done); + }); + + it("should respond 500 when putting an malformed form update", (done) => { + request(server) + .put("/form/1") + .send({ + id: 1 + , inputs:[ + { + placement: 0 + , description: 'Description Question 1 Form 1' + , question: 'Question 1 Form 1' + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + } + , { + placement: 1 + , description: 'Description Question 2 Form 1' + , question: 'Question 2 Form 1' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + , id: 2 + } + , { + placement: 2 + , description: 'Description Question 3 Form 1' + , question: 'Question 3 Form 1' + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: 3, arguments: [ '10' ] } + , { type: 4, arguments: [ '2' ] } + ] + } + ] + }) + .expect(500) + .expect((res: any) => { + const message = "Invalid Form. Check error property for details."; + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("error"); + expect(res.body.message).to.be.equal(message); + }) + .end(done); + }); + +}); diff --git a/src/api/controllers/form.ts b/src/api/controllers/form.ts new file mode 100644 index 0000000000000000000000000000000000000000..df60bc1fcbbac3d04d17f08aad90c19b79f504a4 --- /dev/null +++ b/src/api/controllers/form.ts @@ -0,0 +1,177 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { waterfall } from "async"; +import { OptHandler } from "../../utils/optHandler"; +import { DiffHandler } from "../../utils/diffHandler"; +import { Form, FormOptions} from "../../core/form"; +import { FormUpdate, FormUpdateOptions } from "../../core/formUpdate"; +import { Response, NextFunction } from "express"; +import { Request } from "../apiTypes"; + +export class FormCtrl { + + public static list(req: Request, res: Response, next: NextFunction) { + + req.db.listForms((err: Error, forms?: Form[]) => { + if (err){ + res.status(500).json({ + message: "Could not list forms. Some error has occurred. Check error property for details.", + error: err + }); + return; + } + else{ + const mappedForms = forms.map(form => ({ + id: form.id, + title: form.title, + description: form.description + })); + res.json(mappedForms); + return; + } + + }); + + } + + public static read(req: Request, res: Response, next: NextFunction) { + req.db.readForm(req.params.id, (err: Error, form?: Form) => { + if (err){ + res.status(500).json({ + message: "Form with id: '" + req.params.id + "' not found. Some error has occurred. Check error property for details.", + error: err + }); + return; + } + else{ + res.json(form); + return; + } + }); + } + + public static write(req: Request, res: Response, next: NextFunction) { + + let form: Form; + try { + form = new Form(OptHandler.form(req.body)); + } catch(e) { + res.status(500).json({ + message: "Invalid Form. Check error property for details.", + error: e.message + }); + return; + } + waterfall ([ + (callback: (err: Error, result?: FormUpdate) => void) => { + req.db.writeForm(form, (err: Error, formResult: Form) => { + if (err) { + callback(err); + } + else { + const formOpts: FormOptions = { + id: formResult.id + , title: formResult.title + , description: formResult.description + , inputs: [] + }; + const formUpdate: FormUpdate = DiffHandler.diff(formResult, new Form (formOpts)); + + callback(null, formUpdate); + } + }); + }, + (formUpdate: FormUpdate, callback: (err: Error, formId: number) => void) => { + req.db.updateForm(formUpdate, (err: Error) => { + callback(err, formUpdate.form.id); + }); + } + ], (err, resultId) => { + if (err) { + res.status(500).json({ + message: "Could not insert form. Some error has occurred. Check error property for details.", + error: err.message + }); + } + else { + res.json({ + id: resultId + , message: "Form added. Id on key 'id'" + }); + } + return; + }); + } + + public static update(req: Request, res: Response, next: NextFunction) { + + let newForm: Form; + try { + newForm = new Form(OptHandler.form(req.body)); + } catch(e) { + res.status(500).json({ + message: "Invalid Form. Check error property for details.", + error: e.message + }); + return; + } + waterfall([ + (callback: (err: Error, result?: FormUpdate) => void) => { + req.db.readForm(req.params.id, (err: Error, oldForm: Form) => { + if (err) { + callback(err); + } + else { + const formUpdate: FormUpdate = DiffHandler.diff(newForm, oldForm); + callback(null, formUpdate); + } + }); + }, + (formUpdate: FormUpdate, callback: (err: Error, formUpdateResult?: FormUpdate) => void) => { + req.db.updateDatabase(formUpdate, (err: Error, formUpdateResult: FormUpdate) => { + if (err) { + callback(err); + } + callback(null,formUpdateResult); + }); + }, + (formUpdate: FormUpdate, callback: (err: Error) => void) => { + req.db.updateForm(formUpdate, (err: Error) => { + callback(err); + }); + } + ], (err) => { + if (err) { + res.status(500).json({ + message: "Could not update Form. Some error has ocurred. Check error property for details.", + error: err.message + }); + } + else { + res.json({ + message: "Updated" + }); + } + return; + }); + } +} diff --git a/src/api/controllers/formAnswer.spec.ts b/src/api/controllers/formAnswer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c88b2f0e621e89745952d51bb65dd9737ff83b56 --- /dev/null +++ b/src/api/controllers/formAnswer.spec.ts @@ -0,0 +1,72 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + + import * as request from "supertest"; + import { expect } from "chai"; + import * as server from "../../main"; + import { EnumHandler,InputType, ValidationType } from "../../utils/enumHandler"; + import { TestHandler } from "../../utils/testHandler"; + import { OptHandler } from "../../utils/optHandler"; + import { Form, FormOptions } from "../../core/form"; + import { Input, InputOptions, Validation } from "../../core/input"; + + + describe("API data controller", () => { + + it("should respond 200 when posting valid form Answer", (done) => { + + request(server) + .post("/answer/1") + .send({ + 1:["Answer to Question 1 Form 1"] + , 2:["12345-000"] + , 3:["MAXCHAR 10"] + }) + .expect(200) + .expect((res: any) => { + expect(res.body.id).to.be.equal(7); + expect(res.body.message).to.be.equal("Answered"); + }) + .end(done); + }); + + it("should respond 500 when posting invalid form Answer", (done) => { + + request(server) + .post("/answer/1") + .send({ + 1:["Answer to Question 1 Form 1"] + , 2:["12a345-000"] + , 3:["MAXCHAR 10 AND MORE"] + }) + .expect(500) + .expect((res: any) => { + const message = "Could not Create form Answer. Some error has occurred. Check error property for details."; + expect(res.body).to.be.an("object"); + expect(res.body).to.have.property("error"); + expect(res.body.message).to.be.equal(message); + expect(res.body.error["2"]).to.be.equal("RegEx do not match"); + expect(res.body.error["3"]).to.be.equal("Input answer must be lower than 10"); + }) + .end(done); + }); + + }); diff --git a/src/api/controllers/formAnswer.ts b/src/api/controllers/formAnswer.ts new file mode 100644 index 0000000000000000000000000000000000000000..9549a66dd1c643f57f29b9e6e0f636c6b89658e1 --- /dev/null +++ b/src/api/controllers/formAnswer.ts @@ -0,0 +1,102 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { OptHandler } from "../../utils/optHandler"; +import { InputAnswerOptions, InputAnswerOptionsDict } from "../../core/inputAnswer"; +import { Form, FormOptions } from "../../core/form"; +import { FormAnswer, FormAnswerOptions } from "../../core/formAnswer"; +import { ValidationHandler } from "../../utils/validationHandler"; +import { Response, NextFunction } from "express"; +import { Request } from "../apiTypes"; + +export class AnswerCtrl { + + public static write(req: Request, res: Response, next: NextFunction) { + + req.db.readForm(req.params.id, (err: Error, form?: Form) => { + if (err) { + res.status(500).json({ + message: "Form with id: '" + req.params.id + "' not found. Some error has occurred. Check error property for details.", + error: err + }); + return; + } + else { + let inputAnswerOptionsDict: InputAnswerOptionsDict = {} + + for (const key of Object.keys(req.body)) { + inputAnswerOptionsDict[parseInt(key, 10)] = []; + for (const i in req.body[key]) { + const tmpInputAnswerOption: InputAnswerOptions = { + idInput: parseInt(key, 10) + , placement: parseInt(i, 10) + , value: req.body[key][i] + } + inputAnswerOptionsDict[parseInt(key, 10)].push(tmpInputAnswerOption); + } + } + + + let formAnswerOpt: FormAnswerOptions = { + form: form + , timestamp: new Date(Date.now()) + , inputsAnswerOptions: inputAnswerOptionsDict + } + + try{ + const formAnswer: FormAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOpt)); + ValidationHandler.validateFormAnswer(formAnswer); + req.db.writeFormAnswer(formAnswer, (err: Error, formAnswerResult: FormAnswer) => { + if (err){ + throw err; + } + else{ + res.json({ + id: formAnswerResult.id + , message: "Answered" + }); + return; + } + }); + } + catch (e) { + + if( e.validationDict !== undefined){ + res.status(500).json({ + message: "Could not Create form Answer. Some error has occurred. Check error property for details." + , error: e.validationDict + }); + }else{ + res.status(500).json({ + message: "Could not Create form Answer. Some error has occurred. Check error property for details." + , error: e.message + }); + } + return + } + + } + }); + + } + + +} diff --git a/src/api/controllers/item.spec.ts b/src/api/controllers/item.spec.ts deleted file mode 100644 index f767e0f3ef6b71e0a1ac359b83f78cf34f6b744c..0000000000000000000000000000000000000000 --- a/src/api/controllers/item.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * form-creator-api. RESTful API to manage and answer forms. - * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre - * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR - * - * This file is part of form-creator-api. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -import * as request from "supertest"; -import { expect } from "chai"; -import * as server from "../../main"; - -describe("API data controller", () => { - - it("should respond 200 when posting valid item", (done) => { - request(server) - .post("/item") - .send({qtd: 30, name: "Arrow"}) - .expect(200) - .expect((res: any) => { - const message = "Item added. Id on key 'id'"; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("id"); - expect(res.body).to.have.property("status"); - expect(res.body.id).to.be.eql(0); - expect(res.body.status).to.be.eql(message); - }) - .end(done); - }); - - it("should respond 500 when posting invalid item", (done) => { - request(server) - .post("/item") - .send({name: "Arrow"}) - .expect(500) - .expect((res: any) => { - const message = "Invalid body. Check out request body keys"; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("error"); - expect(res.body.error).to.be.eql(message); - }) - .end(done); - }); - - it("should respond 200 when putting valid item", (done) => { - request(server) - .put("/item/0") - .send({qtd: 29, name: "Arrow"}) - .expect(200) - .expect((res: any) => { - const message = "Item updated. Id on key 'id'"; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("id"); - expect(res.body).to.have.property("status"); - expect(res.body.id).to.be.eql("0"); - expect(res.body.status).to.be.eql(message); - }) - .end(done); - }); - - it("should respond 500 when putting invalid item", (done) => { - request(server) - .put("/item/0") - .send({name: "Arrow"}) - .expect(500) - .expect((res: any) => { - const message = "Invalid body. Check out request body keys"; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("error"); - expect(res.body.error).to.be.eql(message); - }) - .end(done); - }); - - it("should respond 500 when putting inexistent item", (done) => { - request(server) - .put("/item/1") - .send({name: "Arrow", qtd: 30}) - .expect(500) - .expect((res: any) => { - const message = "Item with id: '1' not found"; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("error"); - expect(res.body.error).to.be.eql(message); - }) - .end(done); - }); - - it("should respond 200 when getting valid item", (done) => { - request(server) - .get("/item/0") - .expect(200) - .expect((res: any) => { - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("qtd"); - expect(res.body).to.have.property("name"); - expect(res.body.qtd).to.be.eql(29); - expect(res.body.name).to.be.eql("Arrow"); - }) - .end(done); - }); - - it("should respond 500 when getting inexistent item", (done) => { - request(server) - .get("/item/1") - .expect(500) - .expect((res: any) => { - const message = "Item with id: '1' not found"; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("error"); - expect(res.body.error).to.be.eql(message); - }) - .end(done); - }); - - it("should respond 200 when deleting valid item", (done) => { - request(server) - .delete("/item/0") - .expect(200) - .expect((res: any) => { - const message = "Item deleted. Id on key 'id'"; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("id"); - expect(res.body).to.have.property("status"); - expect(res.body.id).to.be.eql("0"); - expect(res.body.status).to.be.eql(message); - }) - .end(done); - }); - - it("should respond 500 when deleting inexistent item", (done) => { - request(server) - .delete("/item/0") - .expect(500) - .expect((res: any) => { - const message = "Item with id: '0' not found"; - expect(res.body).to.be.an("object"); - expect(res.body).to.have.property("error"); - expect(res.body.error).to.be.eql(message); - }) - .end(done); - }); -}); diff --git a/src/api/controllers/item.ts b/src/api/controllers/item.ts deleted file mode 100644 index 022a5bcf4c4250bebce2b1af26dbd30c0dfbf2f9..0000000000000000000000000000000000000000 --- a/src/api/controllers/item.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * form-creator-api. RESTful API to manage and answer forms. - * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre - * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR - * - * This file is part of form-creator-api. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -import { Item } from "../../core/item"; -import { Response, NextFunction } from "express"; -import { Request } from "../apiTypes"; - -export class ItemCtrl { - public static read(req: Request, res: Response, next: NextFunction) { - const item = req.collection.getItem(req.params.id); - if (item) { - res.json(item); - return; - } - - else { - res.status(500).json({ - error: "Item with id: '" + req.params.id + "' not found" - }); - return; - } - } - - public static write(req: Request, res: Response, next: NextFunction) { - if (!Item.validator(req.body)) { - res.status(500).json({ - error: "Invalid body. Check out request body keys" - }); - return; - } - - const item: Item = new Item(req.body.qtd, req.body.name); - const id = req.collection.addItem(item); - res.json({ - id: id - , status: "Item added. Id on key 'id'" - }); - } - - public static update(req: Request, res: Response, next: NextFunction) { - if (!Item.validator(req.body)) { - res.status(500).json({ - error: "Invalid body. Check out request body keys" - }); - return; - } - - const item: Item = new Item(req.body.qtd, req.body.name); - const updated = req.collection.updateItem(req.params.id, item); - if (updated) { - res.json({ - id: req.params.id - , status: "Item updated. Id on key 'id'" - }); - } - - else { - res.status(500).json({ - error: "Item with id: '" + req.params.id + "' not found" - }); - } - } - - public static del(req: Request, res: Response, next: NextFunction) { - const i = req.collection.deleteItem(req.params.id); - if (i) { - res.json({ - id: req.params.id - , status: "Item deleted. Id on key 'id'" - }); - } - - else { - res.status(500).json({ - error: "Item with id: '" + req.params.id + "' not found" - }); - } - } -} diff --git a/src/api/middlewares/collection.ts b/src/api/middlewares/dbHandler.ts similarity index 75% rename from src/api/middlewares/collection.ts rename to src/api/middlewares/dbHandler.ts index 65a322498b8d8f011935650f833bd9fe5de85381..42bc7f4eb741c8fdd4b3aa3b5eb5b9f621985abc 100644 --- a/src/api/middlewares/collection.ts +++ b/src/api/middlewares/dbHandler.ts @@ -19,18 +19,20 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Collection } from "../../core/collection"; +import { configs } from "../../utils/config"; +import { DbHandler } from "../../utils/dbHandler"; import { Middleware } from "../apiTypes"; /** * Creates a engine and middleware that - * inserts the collection into the request objects. + * inserts the handler into the request objects. */ -export function CollectionMw (): Middleware { - const collection: Collection = new Collection(); - return function collectionMiddleware(req, res, next) { - req.collection = collection; +export function DbHandlerMw (): Middleware { + const db: DbHandler = new DbHandler(configs.poolconfig); + + return function dbHandlerMiddleware(req, res, next) { + req.db = db; next(); }; } diff --git a/src/core/collection.spec.ts b/src/core/collection.spec.ts deleted file mode 100644 index a5c9f0f9a4e33eb1246fbca528495be8eb5ef090..0000000000000000000000000000000000000000 --- a/src/core/collection.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * form-creator-api. RESTful API to manage and answer forms. - * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre - * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR - * - * This file is part of form-creator-api. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -import { expect } from "chai"; -import { Collection } from "./collection"; -import { Item } from "./item"; - -describe("collection class", () => { - - const collection = new Collection (); - - it("should add 3 items to the collection", () => { - const items: Item[] = [ - new Item(1, "Bow") - , new Item(30, "Arrow") - , new Item(1, "Sword") - ]; - - for (let i = 0; i < items.length; ++i) { - const id = collection.addItem(items[i]); - expect(id).to.be.equal(i); - } - }); - - it("should update the second item", () => { - const item: Item = new Item(29, "Arrow"); - - const updated = collection.updateItem(1, item); - expect(updated).to.be.true; - }); - - it("should NOT update a inexistent item", () => { - const item: Item = new Item(29, "Arrow"); - - const updated = collection.updateItem(3, item); - expect(updated).to.be.false; - }); - - it("should delete the third item", () => { - const deleted = collection.deleteItem(2); - expect(deleted).to.be.true; - }); - - it("should NOT delete a inexistent item", () => { - const deleted = collection.deleteItem(2); - expect(deleted).to.be.false; - }); - - it("should get the first item", () => { - const item = collection.getItem(0); - expect(item).to.be.an("object"); - expect(item).to.have.property("qtd"); - expect(item).to.have.property("name"); - expect(item.qtd).to.be.equal(1); - expect(item.name).to.be.equal("Bow"); - }); - - it("should NOT get a inexistent item", () => { - const item = collection.getItem(2); - expect(item).to.be.null; - }); -}); diff --git a/src/core/item.ts b/src/core/form.spec.ts similarity index 74% rename from src/core/item.ts rename to src/core/form.spec.ts index c627201768a3bea5921cb9b2db35d1f005c84c12..e70e86c32c425444b514a039a87452919a093d21 100644 --- a/src/core/item.ts +++ b/src/core/form.spec.ts @@ -18,17 +18,3 @@ * 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/>. */ - -export class Item { - public readonly qtd: number; - public readonly name: string; - - constructor(qtd: number, name: string) { - this.qtd = qtd; - this.name = name; - } - - public static validator(json: any) { - return (typeof json.qtd === "number" && typeof json.name === "string"); - } -} diff --git a/src/core/form.ts b/src/core/form.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc16ac3caa0d0a9e4bf230498e259401a9d76211 --- /dev/null +++ b/src/core/form.ts @@ -0,0 +1,65 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { OptHandler } from "../utils/optHandler"; +import { Input, InputOptions } from "./input"; + +/** Parameters used to create a form object. */ +export interface FormOptions { + /** Unique identifier of a Form instance */ + id?: number; + + /** Form's title. An human-understandable identifier. Not unique */ + title: string; + + /** Form Description, as propose */ + description: string; + + /** Array of input. containing question */ + inputs: InputOptions[]; +} +/** + * Form Class to manage project's forms + */ +export class Form { + /** 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[]; + + /** + * Creates a new instance of Form Class + * @param options - FormOptions instance to create a form. + */ + constructor(options: FormOptions) { + this.id = options.id ? options.id : null; + this.title = options.title; + this.description = options.description; + this.inputs = options.inputs.map((i: any) => { + return new Input(OptHandler.input(i)); + }); + } + + } diff --git a/src/core/formAnswer.spec.ts b/src/core/formAnswer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e70e86c32c425444b514a039a87452919a093d21 --- /dev/null +++ b/src/core/formAnswer.spec.ts @@ -0,0 +1,20 @@ +/* + * 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/>. + */ diff --git a/src/core/formAnswer.ts b/src/core/formAnswer.ts new file mode 100644 index 0000000000000000000000000000000000000000..04f2ba671d2a879212bdc36bda1e519740c0850d --- /dev/null +++ b/src/core/formAnswer.ts @@ -0,0 +1,71 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { OptHandler } from "../utils/optHandler"; +import { Form } from "./form"; +import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "./inputAnswer"; + +/** Parameters used to create a FormAnswer object. */ +export interface FormAnswerOptions { + /** Unique identifier of a FormAnswer instance */ + id?: number; + + /** Form which answer is related to */ + form: Form; + + /** Time when it is answered */ + timestamp: Date; + + /** A dictionary of inputsAnswers. containing the answers */ + inputsAnswerOptions: InputAnswerOptionsDict; +} + +/** + * Form Class to manage project's forms + */ +export class FormAnswer { + /** Unique identifier of a FormAnswer instance */ + public readonly id: number; + /** Form which answer is related to */ + public readonly form: Form; + /** Time when it is answered */ + public readonly timestamp: Date; + /** Array of inputsAnswer. containing the answers */ + public readonly inputAnswers: InputAnswerDict; + + /** + * Creates a new instance of FormAnswer Class + * @param options - FormAnswerOptions instance to create a FormAnswer. + */ + constructor(options: FormAnswerOptions) { + this.id = options.id ? options.id : null; + this.form = options.form; + this.timestamp = options.timestamp; + this.inputAnswers = {}; + for (const key of Object.keys(options.inputsAnswerOptions)){ + this.inputAnswers[parseInt(key, 10)] = options.inputsAnswerOptions[parseInt(key, 10)].map( (i: InputAnswerOptions) => { + return new InputAnswer(i); + }); + + } + } + + } diff --git a/src/core/formUpdate.spec.ts b/src/core/formUpdate.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c54fe5df5900adac41751690faf6c95274c0dd3 --- /dev/null +++ b/src/core/formUpdate.spec.ts @@ -0,0 +1,92 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; +import { InputType, UpdateType, ValidationType } from "../utils/enumHandler"; +import { TestHandler } from "../utils/testHandler"; +import { Form, FormOptions } from "./form"; +import { FormUpdate, FormUpdateOptions } from "./formUpdate"; +import { Input, InputOptions } from "./input"; +import { InputUpdate, InputUpdateOptions } from "./inputUpdate"; + +describe("FormUpdate", () => { + it("should create a valid FormUpdate with id null", (done) => { + + const inputObj: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ inputObj ] + }; + const inputUpdateObj: InputUpdateOptions = { + input: inputObj + , inputOperation: UpdateType.ADD + , value: null + }; + const formUpdateObj: FormUpdateOptions = { + form: formObj + , updateDate: new Date() + , inputUpdates: [ inputUpdateObj ] + }; + + const resFormUpdate: FormUpdate = new FormUpdate(formUpdateObj); + + const expInput: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const expForm: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ expInput ] + }; + const expInputUpdate: InputUpdate = { + input: expInput + , inputOperation: UpdateType.ADD + , value: null + , id: null + }; + const expFormUpdate: FormUpdate = { + form: expForm + , updateDate: new Date() + , inputUpdates: [ expInputUpdate] + , id: null + }; + + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); +}); diff --git a/src/core/formUpdate.ts b/src/core/formUpdate.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ee927dce8b5cbe52e8052a7df0c84742653477f --- /dev/null +++ b/src/core/formUpdate.ts @@ -0,0 +1,61 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { Form, FormOptions } from "./form"; +import { InputUpdate, InputUpdateOptions } from "./inputUpdate"; + +/** Parameters used to update a form object. */ +export interface FormUpdateOptions { + /** Unique identifier of a FormUpdate instance. */ + id?: number; + /** Changed Form. */ + form: FormOptions; + /** Date which form was updated. */ + updateDate: Date; + /** Array of InputUpdate containing changes on inputs. */ + inputUpdates: InputUpdateOptions[]; +} + +/** + * FormUpdate Class to manage project's forms + */ +export class FormUpdate { + /** Unique identifier of a FormUpdate instance. */ + public readonly id?: number; + /** Changed Form. */ + public readonly form: Form; + /** Date which form was updated. */ + public readonly updateDate: Date; + /** Array of InputUpdate containing changes on inputs. */ + public readonly inputUpdates: InputUpdate[]; + /** + * Creates a new instance of FormUpdate Class + * @param options - FormUpdateOptions instance to update a form. + */ + constructor(options: FormUpdateOptions) { + this.id = options.id ? options.id : null; + this.form = new Form (options.form); + this.updateDate = options.updateDate; + this.inputUpdates = options.inputUpdates.map((i: any) => { + return new InputUpdate(i); + }); + } +} diff --git a/src/core/input.spec.ts b/src/core/input.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..58df25055ad292cc0d05412c00283f102bc02c63 --- /dev/null +++ b/src/core/input.spec.ts @@ -0,0 +1,99 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; +import { InputType, ValidationType } from "../utils/enumHandler"; +import { Input, InputOptions } from "./input"; + +describe("Input with enabled atribute", () => { + + it("should create a Input with option key 'enabled' as undefined", (done) => { + const options: InputOptions = { + placement: 0 + , description: "Description Question 1" + , question: "Question 1" + , type: InputType.TEXT + , validation: [] + }; + + const input: Input = new Input(options); + + expect(input).to.be.a("object"); + expect(input.enabled).to.be.equal(true); + + done(); + }); + + it("should create a Input with option key 'enabled' as null", (done) => { + const options: InputOptions = { + placement: 1 + , description: "Description Question 2" + , question: "Question 2" + , enabled: null + , type: InputType.TEXT + , validation: [] + }; + + const input: Input = new Input(options); + + expect(input).to.be.a("object"); + expect(input.enabled).to.be.equal(true); + + done(); + }); + + it("should create a Input with option key 'enabled' as true", (done) => { + const options: InputOptions = { + placement: 1 + , description: "Description Question 2" + , question: "Question 2" + , enabled: true + , type: InputType.TEXT + , validation: [] + }; + + const input: Input = new Input(options); + + expect(input).to.be.a("object"); + expect(input.enabled).to.be.equal(true); + + done(); + }); + + it("should create a Input with option key 'enabled' as false", (done) => { + const options: InputOptions = { + placement: 1 + , description: "Description Question 2" + , question: "Question 2" + , enabled: false + , type: InputType.TEXT + , validation: [] + }; + + const input: Input = new Input(options); + + expect(input).to.be.a("object"); + expect(input.enabled).to.be.equal(false); + + done(); + }); + +}); diff --git a/src/core/input.ts b/src/core/input.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7097aa46aede4916908aa5a513db39bb8b9c5fa --- /dev/null +++ b/src/core/input.ts @@ -0,0 +1,89 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { InputType, ValidationType } from "../utils/enumHandler"; + +/** Parameters used to create a input object. */ +export interface InputOptions { + /** Unique identifier of a Input instance. */ + id?: number; + /** Place where input should be in the form. */ + placement: number; + /** Input's Description */ + description: string; + /** Question of input */ + question: string; + /** Enabled/Disable input */ + enabled?: boolean; + /** Type of input */ + type: InputType; + /** Array contain all input's validation */ + validation: Validation[]; +} +/** Validation contains the type of it, and n arguments to validate if necessary */ +export interface Validation { + + /** Enum Validation Type to identify the type of current validation */ + type: ValidationType; + /** Array of any, to store informations about validadtion */ + arguments: string[]; +} + +/** + * Input Class to manage project's inputs forms + */ + +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; + /** Enabled/Disable input */ + public readonly enabled: boolean; + /** Type of input */ + public readonly type: InputType; + /** Array contain all input's validation */ + public readonly validation: Validation[]; + + /** + * Creates a new instance of Input Class + * @param options - InputOptions instance to create a input. + */ + constructor(options: InputOptions) { + this.id = options.id ? options.id : null; + this.placement = options.placement; + this.description = options.description; + this.question = options.question; + if ((options.enabled === undefined) || (options.enabled === null) ) { + this.enabled = true; + } + else { + this.enabled = options.enabled; + } + this.type = options.type; + this.validation = options.validation; + } + +} diff --git a/src/core/inputAnswer.spec.ts b/src/core/inputAnswer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e70e86c32c425444b514a039a87452919a093d21 --- /dev/null +++ b/src/core/inputAnswer.spec.ts @@ -0,0 +1,20 @@ +/* + * 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/>. + */ diff --git a/src/core/inputAnswer.ts b/src/core/inputAnswer.ts new file mode 100644 index 0000000000000000000000000000000000000000..6737a5cb0c1e14c6dce27a2195c129e906310f4a --- /dev/null +++ b/src/core/inputAnswer.ts @@ -0,0 +1,70 @@ +/* + * 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 input object. */ + export interface InputAnswerOptions { + /** Unique identifier of a InputAnswer instance. */ + id?: number; + /** Input id which this answer came from. */ + idInput: number; + /** Place where answers should be (multivalored answers). */ + placement: number; + /** Input's answer */ + value: string; + } + +/** Parameters used to create a dictionary to uses as an collection of InputAnswerOptions Object. */ + export interface InputAnswerOptionsDict { + /** A key is a identifier of a Input instance */ + [key: number]: InputAnswerOptions[]; +} + +/** Parameters used to create a dictionary to uses as an collection of InputAnswer Object.. */ + export interface InputAnswerDict { + /** A key is a identifier of a Input instance */ + [key: number]: InputAnswer[]; +} + +/** + * Input Class to manage project's inputs forms + */ + export class InputAnswer { + /** Unique identifier of a Input Answer instance. */ + public readonly id: number; + /** A identifier of a Input instance. */ + public readonly idInput: number; + /** Place where input should be in the form. */ + public readonly placement: number; + /** Input's Description */ + public readonly value: string; + + /** + * Creates a new instance of InputAnswer Class + * @param options - InputOptionsAnswer instance to create a inputAnswer. + */ + constructor(options: InputAnswerOptions) { + this.id = options.id ? options.id : null; + this.idInput = options.idInput; + this.placement = options.placement; + this.value = options.value; + } + +} diff --git a/src/core/inputUpdate.spec.ts b/src/core/inputUpdate.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e6b91247229ee6af78a099bbdde620154ef8875 --- /dev/null +++ b/src/core/inputUpdate.spec.ts @@ -0,0 +1,67 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; +import { InputType, UpdateType, ValidationType } from "../utils/enumHandler"; +import { TestHandler } from "../utils/testHandler"; +import { Input, InputOptions } from "./input"; +import { InputUpdate, InputUpdateOptions } from "./inputUpdate"; + +describe("InputUpdate", () => { + it("should create a valid InputUpdate with id null", (done) => { + + const inputObj: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const inputUpdateObj: InputUpdateOptions = { + input: inputObj + , inputOperation: UpdateType.ADD + , value: null + }; + + const resInputUpdate: InputUpdate = new InputUpdate(inputUpdateObj); + + const expInput: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const expInputUpdate: InputUpdate = { + input: expInput + , inputOperation: UpdateType.ADD + , value: null + , id: null + }; + + TestHandler.testInputUpdate(resInputUpdate, expInputUpdate); + done(); + }); +}); diff --git a/src/core/inputUpdate.ts b/src/core/inputUpdate.ts new file mode 100644 index 0000000000000000000000000000000000000000..48799dc322f7b277005dec3a2254ffbc707962d8 --- /dev/null +++ b/src/core/inputUpdate.ts @@ -0,0 +1,59 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { UpdateType } from "../utils/enumHandler"; +import { Input, InputOptions } from "./input"; + +/** Parameters used to update a input object. */ +export interface InputUpdateOptions { + /** Unique identifier of a FormUpdate instance. */ + id?: number; + /** Changed Input. */ + input: InputOptions; + /** Update operation. */ + inputOperation: UpdateType; + /** Update description. */ + value: string; +} + +/** + * InputUpdate Class to change project's inputs forms + */ +export class InputUpdate { + /** Unique identifier of a FormUpdate instance. */ + public readonly id?: number; + /** Changed Input. */ + public readonly input: Input; + /** Update operation. */ + public readonly inputOperation: UpdateType; + /** Update description. */ + public readonly value: string; + /** + * Creates a new instance of InputUpdate Class + * @param options - InputUpdateOptions instance to change a input. + */ + constructor(options: InputUpdateOptions) { + this.id = options.id ? options.id : null; + this.input = new Input(options.input); + this.inputOperation = options.inputOperation; + this.value = options.value; + } +} diff --git a/src/main.ts b/src/main.ts index 28fd22a24ec6ada905e5319e9db9c4e8d89ada62..110396cbee8c8c194ccfde88f431705abdc2ca28 100755 --- a/src/main.ts +++ b/src/main.ts @@ -36,23 +36,25 @@ const app = module.exports = express(); const port = process.env.PORT; // Include middlewares -import { CollectionMw } from "./api/middlewares/collection"; +import { DbHandlerMw } from "./api/middlewares/dbHandler"; // Include controllers -import { ItemCtrl } from "./api/controllers/item"; +import { FormCtrl } from "./api/controllers/form"; +import { AnswerCtrl } from "./api/controllers/formAnswer"; // Setup middlewares app.use("/", bodyParser.json()); -app.use("/", CollectionMw()); +app.use("/", DbHandlerMw()); // Setup routes -app.get("/item/:id", ItemCtrl.read); -app.post("/item", ItemCtrl.write); -app.put("/item/:id", ItemCtrl.update); -app.delete("/item/:id", ItemCtrl.del); +app.get("/form/", FormCtrl.list); +app.get("/form/:id", FormCtrl.read); +app.put("/form/:id", FormCtrl.update); +app.post("/form", FormCtrl.write); +app.post("/answer/:id", AnswerCtrl.write); // Listening app.listen(port); -console.log("Server listening on port 3000"); +console.log("Server listening on port " + port); diff --git a/src/utils/config.spec.ts b/src/utils/config.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3daebb1368f5adf30c78a566d116ddf957a60ad3 --- /dev/null +++ b/src/utils/config.spec.ts @@ -0,0 +1,33 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + import { expect } from "chai"; + import { configs } from "./config"; + + describe("Config Handler", () => { + it("should test configs", () => { + + expect(configs.poolconfig["user"]).to.be.equal(process.env["POSTGRES_USER"]); + expect(configs.poolconfig["host"]).to.be.equal(process.env["POSTGRES_HOST"]); + expect(configs.poolconfig["database"]).to.be.equal(process.env["POSTGRES_DB"]); + expect(configs.poolconfig["password"]).to.be.equal(process.env["POSTGRES_PASSWORD"]); + expect(configs.poolconfig["port"]).to.be.equal(parseInt(process.env["POSTGRES_PORT"], 10)); + }); + }); diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce2eef0aa7496ac0a65c3b959576cca8073ff59c --- /dev/null +++ b/src/utils/config.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/>. + */ + +import { PoolConfig } from "pg"; + +class Config { + private static instance: Config; + private user: string; + private host: string; + private database: string; + private password: string; + private port: number; + public readonly poolconfig: PoolConfig; + + private constructor(){ + this.user = process.env["POSTGRES_USER"]; + this.host = process.env["POSTGRES_HOST"]; + this.database = process.env["POSTGRES_DB"]; + this.password = process.env["POSTGRES_PASSWORD"]; + this.port = parseInt(process.env["POSTGRES_PORT"], 10); + this.poolconfig = { + user: this.user, + database: this.database, + password: this.password, + host: this.host, + port: this.port, + max: 10, + idleTimeoutMillis: 3000 + }; + + } + + public static get Configurations(){ + return this.instance || (this.instance = new this()); + } +} + +export const configs = Config.Configurations; diff --git a/src/utils/dbHandler.spec.ts b/src/utils/dbHandler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..441b866e4f6187196bf6cbde6d578f0a12a32486 --- /dev/null +++ b/src/utils/dbHandler.spec.ts @@ -0,0 +1,711 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +import { series } from "async"; +import { expect } from "chai"; +import { QueryResult } from "pg"; +import { Form, FormOptions } from "../core/form"; +import { FormAnswer, FormAnswerOptions } from "../core/formAnswer"; +import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; +import { Input, InputOptions, Validation } from "../core/input"; +import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; +import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; +import { configs } from "./config"; +import { DbHandler, QueryOptions } from "./dbHandler"; +import { InputType, UpdateType, ValidationType } from "./enumHandler"; +import { OptHandler } from "./optHandler"; +import { TestHandler } from "./testHandler"; + +describe("Database Handler", () => { + const dbhandler = new DbHandler(configs.poolconfig); + + it("should insert a form", (done) => { + const queryString: string = "INSERT INTO form(id, title, description)\ + VALUES (5, 'Form Title 5', 'Form Description 5');"; + const query: QueryOptions = {query: queryString, parameters: []}; + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("INSERT"); + expect(result.rowCount).to.be.equal(1); + done(); + + }); + }); + + it("should insert a form and then rollback", (done) => { + series([ + (cb: (err: Error, result?: QueryResult) => void) => { + dbhandler.begin(cb); + }, + (callback: (err: Error, result?: QueryResult) => void) => { + const queryString: string = "INSERT INTO form(id, title, description)\ + VALUES (6, 'Form Title 6', 'Form Description 6');"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, callback); + }, + (cb: (err: Error, result?: QueryResult) => void) => { + dbhandler.rollback(cb); + } + ], (err, results) => { + + expect(err).to.be.a("null"); + expect(results[0].command).to.be.equal("BEGIN"); + expect(results[1].command).to.be.equal("INSERT"); + expect(results[1].rowCount).to.be.equal(1); + expect(results[2].command).to.be.equal("ROLLBACK"); + done(); + + }); + }); + + it("should select all forms", (done) => { + const queryString: string = "SELECT * FROM form;"; + const query: QueryOptions = {query: queryString, parameters: []}; + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("SELECT"); + expect(result.rowCount).to.be.equal(5); + done(); + + }); + }); + + it("should remove non existent form", (done) => { + const queryString: string = "DELETE FROM form WHERE id=6;"; + + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(0); + done(); + + }); + }); + + it("should remove existent form", (done) => { + const queryString: string = "DELETE FROM form WHERE id=5;"; + const query: QueryOptions = {query: queryString, parameters: []}; + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(1); + done(); + + }); + }); + + it("should insert a input", (done) => { + const queryString: string = "INSERT INTO input(id_form, placement, input_type, enabled, question, description)\ + VALUES\ + (2, 3,'TEXT', TRUE, 'Question 3 Form 2', 'Description Question 3 Form 2'),\ + (2, 4,'TEXT', TRUE, 'Question 4 Form 2', 'Description Question 4 Form 2');"; + const query: QueryOptions = {query: queryString, parameters: []}; + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("INSERT"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + }); + + it("should select all inputs", (done) => { + const queryString: string = "SELECT * FROM input;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("SELECT"); + expect(result.rowCount).to.be.equal(15); + done(); + + }); + }); + + it("should remove non existent input", (done) => { + const queryString: string = "DELETE FROM input WHERE id=20;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(0); + done(); + }); + }); + + it("should remove existent input", (done) => { + const queryString: string = "DELETE FROM input WHERE id=12 OR id=13 OR id=14 OR id=15;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(4); + done(); + + }); + }); + + it("should insert a input validations", (done) => { + const queryString: string = "INSERT INTO input_validation(id_input, validation_type)\ + VALUES\ + (2, 'MAXCHAR'),\ + (5, 'MANDATORY');"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("INSERT"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + }); + + it("should select all input validations", (done) => { + const queryString: string = "SELECT * FROM input_validation;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("SELECT"); + expect(result.rowCount).to.be.equal(18); + done(); + }); + }); + + it("should remove non existent input validations", (done) => { + const queryString: string = "DELETE FROM input_validation WHERE id=21;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(0); + done(); + }); + }); + + it("should remove existent input validations", (done) => { + const queryString: string = "DELETE FROM input_validation WHERE id=17 OR id=18;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + }); + + it("should insert a input validations arguments", (done) => { + const queryString: string = "INSERT INTO input_validation_argument(id_input_validation, placement, argument)\ + VALUES\ + (1, 2, '10'),\ + (2, 2, '2');"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("INSERT"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + + }); + + it("should select all input validations arguments", (done) => { + const queryString: string = "SELECT * FROM input_validation_argument;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("SELECT"); + expect(result.rowCount).to.be.equal(13); + done(); + }); + }); + + it("should remove non existent input validations arguments", (done) => { + const queryString: string = "DELETE FROM input_validation_argument WHERE id=15;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(0); + done(); + }); + }); + + it("should remove existent input validations arguments", (done) => { + const queryString: string = "DELETE FROM input_validation_argument WHERE id=12 OR id=13;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + }); + + it("should insert a form answers", (done) => { + const queryString: string = "INSERT INTO form_answer(id ,id_form, answered_at)\ + VALUES\ + (8, 2, '2018-07-02 10:10:25-03'),\ + (9, 3, '2018-06-03 10:11:25-03');"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("INSERT"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + }); + + it("should select all form answers", (done) => { + const queryString: string = "SELECT * FROM form_answer;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("SELECT"); + expect(result.rowCount).to.be.equal(9); + done(); + }); + }); + + it("should remove non existent form answer", (done) => { + const queryString: string = "DELETE FROM form_answer WHERE id=11;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(0); + done(); + }); + }); + + it("should remove existent form answers", (done) => { + const queryString: string = "DELETE FROM form_answer WHERE id=8 OR id=9;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + + }); + + it("should insert a input answers", (done) => { + const queryString: string = "INSERT INTO input_answer(id, id_form_answer, id_input, value, placement)\ + VALUES\ + (18,1, 6,'Answer to Question 1 Form 3',1),\ + (19,1, 7,'Answer to Question 2 Form 3',2);"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("INSERT"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + }); + + it("should select all form answers", (done) => { + const queryString: string = "SELECT * FROM input_answer;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("SELECT"); + expect(result.rowCount).to.be.equal(19); + done(); + }); + }); + + it("should remove non existent input answer", (done) => { + const queryString: string = "DELETE FROM input_answer WHERE id=25;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(0); + done(); + }); + }); + + it("should remove existent input answers", (done) => { + const queryString: string = "DELETE FROM input_answer WHERE id=18 OR id=19;"; + const query: QueryOptions = {query: queryString, parameters: []}; + + dbhandler.executeQuery(query, (err: Error, result?: QueryResult) => { + expect(err).to.be.a("null"); + expect(result.command).to.be.equal("DELETE"); + expect(result.rowCount).to.be.equal(2); + done(); + }); + }); +}); + +describe("Read and Write on Database", () => { + const dbhandler = new DbHandler(configs.poolconfig); + it("should list all forms", (done) => { + dbhandler.listForms((err: Error, forms?: Form[]) => { + expect(err).to.be.a("null"); + expect(forms.length).to.be.equal(4); + for (let i = 0; i < forms.length; i++){ + expect(forms[i].id).to.be.equal(i + 1); + expect(forms[i].title).to.be.equal("Form Title " + (i + 1)); + expect(forms[i].description).to.be.equal("Form Description " + (i + 1)); + } + done(); + }); + }); + + it("should read an existent form", (done) => { + dbhandler.readForm(1, (err: Error, form: Form) => { + expect(err).to.be.a("null"); + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 2 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 1 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj3) + , OptHandler.input(inputObj2) + ] + }; + + TestHandler.testForm(form, new Form(OptHandler.form(formObj))); + + done(); + }); + }); + + it("should read a non existent form", (done) => { + dbhandler.readForm(10, (err: Error, form: Form) => { + expect(err).to.not.equal(null); + expect(form).to.be.undefined; + + done(); + }); + }); + + it("should write form", (done) => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MANDATORY, arguments: [] } + ] + }; + + const inputObj2 = { + placement: 1 + , description: "Description Question 2 Form 2" + , question: "Question 2 Form 2" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MINCHAR, arguments: ["5"] } + ] + }; + + const formObj: FormOptions = { + title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + ] + }; + + const form = new Form(OptHandler.form(formObj)); + dbhandler.writeForm(form, (err: Error, formResult: Form) => { + expect(err).to.be.a("null"); + expect(formResult.id).to.be.equal(5); + let inputId: number = 16; + for (const input of formResult.inputs){ + expect(input.id).to.be.equal(inputId); + inputId++; + } + done(); + }); + + }); + + 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(8); + let inputAnswerId: number = 18; + for (const key of Object.keys(formAnswerResult.inputAnswers)){ + for (const inputAnswer of formAnswerResult.inputAnswers[parseInt(key, 10)]){ + expect(inputAnswer.id).to.be.equal(inputAnswerId); + inputAnswerId++; + } + + } + done(); + }); + }); + }); + + it("should insert an formUpdate", (done) => { + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const updateObj1: InputUpdateOptions = { + id: 1 + , input: inputObj1 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const updateObj2: InputUpdateOptions = { + id: 2 + , input: inputObj2 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const updateObj3: InputUpdateOptions = { + id: 3 + , input: inputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + ] + }; + const dateTMP: Date = new Date(); + const formUpdateObj: FormUpdateOptions = { + id: 1 + , form: formObj + , updateDate: dateTMP + , inputUpdates: [ + updateObj1 + , updateObj2 + , updateObj3 + ] + }; + + let formUpdateTmp: FormUpdate; + + formUpdateTmp = new FormUpdate(formUpdateObj); + dbhandler.updateForm(formUpdateTmp, (err: Error) => { + expect(err).to.be.a("null"); + done(); + }); + }); + + it("should not update database operation not recognized", (done) => { + const inputObj: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const formObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ inputObj ] + }; + + const inputUpdateObj: InputUpdate = { + input: inputObj + , inputOperation: 10 + , value: "Invalid Operation" + }; + const formUpdateObj: FormUpdate = { + form: formObj + , updateDate: new Date() + , inputUpdates: [ inputUpdateObj ] + }; + + dbhandler.updateDatabase(formUpdateObj, (err: Error) => { + expect(err).to.be.not.a("null"); + done(); + }); + }); +}); diff --git a/src/utils/dbHandler.ts b/src/utils/dbHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7b06542be5bedee40930b1a9ce0801d6654785c --- /dev/null +++ b/src/utils/dbHandler.ts @@ -0,0 +1,946 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { eachOfSeries, eachSeries, map, seq, waterfall } from "async"; +import { Pool, PoolConfig, QueryResult } from "pg"; +import { Form, FormOptions } from "../core/form"; +import { FormAnswer, FormAnswerOptions } from "../core/formAnswer"; +import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; +import { Input, InputOptions, Validation } from "../core/input"; +import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; +import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; +import { EnumHandler, InputType, UpdateType, ValidationType } from "./enumHandler"; +import { ErrorHandler} from "./errorHandler"; +import { OptHandler } from "./optHandler"; +import { Sorter } from "./sorter"; + +/** Parameters used to create a parametrized query, to avoid SQL injection */ +export interface QueryOptions { + /** query string to execute */ + query: string; + + /** Array of input. containing question */ + parameters: any[]; +} +/** + * Class of the SGBD from the Form Creator Api perspective. Used to + * perform all the operations into the database that the Form Creator Api + * requires. This operations include read and write data. + */ + +export class DbHandler { + + /** Information used to connect with a PostgreSQL database. */ + private pool: Pool; + + /** + * Creates a new adapter with the database connection configuration. + * @param config - The information required to create a connection with + * the database. + */ + constructor(config: PoolConfig) { + this.pool = new Pool(config); + } + + /** + * Asynchronously executes a query and get its result. + * @param query - Query (SQL format) to be executed. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.result - Query result. + */ + + public executeQuery(query: QueryOptions , cb: (err: Error, result?: QueryResult) => void): void{ + + this.pool.connect((err, client, done) => { + + if (err) { + cb(err); + return; + } + + client.query(query.query, query.parameters, (error, result) => { + // call 'done()' to release client back to pool + done(); + cb(error, (result) ? result : null); + }); + }); + } + + /** + * Asynchronously ends a transaction + */ + public commit(cb: (err: Error, result?: QueryResult) => void){ + this.executeQuery({query: "COMMIT;", parameters: []}, cb); + } + /** + * Asynchronously rollback a transaction + */ + public rollback(cb: (err: Error, result?: QueryResult) => void){ + this.executeQuery({query: "ROLLBACK;", parameters: []}, cb); + } + + /** + * Asynchronously starts a transaction + */ + public begin(cb: (err: Error, result?: QueryResult) => void){ + this.executeQuery({query: "BEGIN;", parameters: []}, cb); + } + + /** + * Asynchronously list all forms in database. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - list of form or a empty list if there is no form on database. + */ + public listForms(cb: (err: Error, forms?: Form[]) => void){ + const query: QueryOptions = {query: "SELECT id, title, description FROM form;", parameters: []}; + const forms: Form[] = []; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + + for (const row of result.rows){ + const formObj: FormOptions = { + id: row["id"] + , title: row["title"] + , description: row["description"] + , inputs: [] + }; + let formTmp: Form; + try{ + formTmp = new Form ( OptHandler.form(formObj)); + } + catch (e){ + cb(e); + return; + } + + forms.push(formTmp); + } + cb(err, forms); + }); + } + + /** + * Asynchronously executes a query and get a Form. + * @param id - Form identifier to be founded. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + public readForm(id: number, cb: (err: Error, form?: Form) => void){ + + const queryString: string = "SELECT id, title, description FROM form WHERE id=$1;"; + const query: QueryOptions = {query: queryString, parameters: [id]}; + + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin(callback); + }, + (result: QueryResult, callback: (err: Error, result?: QueryResult) => void) => { + this.executeQuery(query, callback); + }, + (result: QueryResult, callback: (err: Error, form?: Form) => void) => { + + if (result.rowCount !== 1){ + callback(ErrorHandler.badIdAmount(result.rowCount)); + return; + } + this.readInputWithFormId(id, (error: Error, inputsResult: Input[]) => { + const formObj: FormOptions = { + id: result.rows[0]["id"] + , title: result.rows[0]["title"] + , description: result.rows[0]["description"] + , inputs: inputsResult + }; + let formTmp: Form; + + try{ + formTmp = new Form ( OptHandler.form(formObj)); + } + catch (e){ + callback(e); + return; + } + + callback(error, formTmp); + }); + }, + (form: Form, callback: (err: Error, form?: Form) => void) => { + + this.commit((error: Error, result?: QueryResult) => { + callback(error, form); + }); + } + ], (err, result: Form) => { + + if (err){ + this.rollback( (error: Error, results?: QueryResult) => { + cb(err); + return; + }); + return; + } + cb(err, result); + + }); + + } + + /** + * A private method to asynchronously executes a query and get a list of Inputs. + * @param id - Form identifier which inputs are linked to. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.inputs - Input array or an empty list if there is no input linked to form. + */ + private readInputWithFormId(id: number, cb: (err: Error, inputs?: InputOptions[]) => void){ + const queryString: string = "SELECT id, placement, input_type, question, description FROM input WHERE id_form=$1 and enabled=true;"; + const query: QueryOptions = {query: queryString, parameters: [id]}; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + map(result.rows, (innerResult, callback) => { + this.readInputValidationWithInputId(innerResult["id"], (error: Error, validationResult: Validation[]) => { + const inputObj: InputOptions = { + id: innerResult["id"] + , placement: innerResult["placement"] + , description: innerResult["description"] + , question: innerResult["question"] + , validation: validationResult + , type: EnumHandler.parseInputType(innerResult["input_type"]) + }; + let inputTmp: InputOptions; + try{ + inputTmp = OptHandler.input(inputObj); + } + catch (e){ + callback(e); + return; + } + + callback(error, inputTmp); + }); + }, (errors, inputs: InputOptions[]) => { + + if (errors){ + this.rollback( (error: Error, results?: QueryResult) => { + cb(errors); + return; + }); + return; + } + + const sortedInputs: InputOptions[] = Sorter.sortByPlacement(inputs); + + cb(errors, sortedInputs); + }); + }); + } + + /** + * A private method to asynchronously executes a query and get a list of Validations based on a Input id. + * @param id - Input identifier which validations are linked to. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.validations - Validation array or an empty list if there is no validation for selected input. + */ + private readInputValidationWithInputId(id: number, cb: (err: Error, validations?: Validation[]) => void){ + const queryString: string = "SELECT id, validation_type FROM input_validation WHERE id_input=$1;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + map(result.rows, (innerResult, callback) => { + this.readInputValidationArgumentWithInputValidationId(innerResult["id"], (error, argumentsArray) => { + const validationTmp: Validation = { + type: EnumHandler.parseValidationType(innerResult["validation_type"]), + arguments: argumentsArray + }; + callback(error, validationTmp); + }); + }, (errors, validation: Validation[]) => { + cb(errors, validation); + }); + }); + + } + + /** + * A private method to asynchronously executes a query and get a list of Validation Argument based on a Validation id. + * @param id - Validation identifier which Validation Arguments are linked to. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.argumentsArray - Validation Arguments array or an empty list if there is no validation argument for selected input. + */ + private readInputValidationArgumentWithInputValidationId(id: number, cb: (err: Error, argumentsArray?: any[]) => void){ + const queryString: string = "SELECT id, argument, placement FROM \ +input_validation_argument WHERE \ +id_input_validation=$1;"; + const query: QueryOptions = { + query: queryString + , parameters: [id] + }; + + // cb(null); + + this.executeQuery(query, (error: Error, result?: QueryResult) => { + + const sortedResult: any[] = Sorter.sortByPlacement(result.rows); + const argumentArrayTmp = []; + for (const innerResult of sortedResult){ + argumentArrayTmp.push(innerResult["argument"]); + } + + cb(error, argumentArrayTmp); + }); + + } + + /** + * Asynchronously insert a form on Database and return it. + * @param form - Form to be inserted. + * @param cb - Callback function which contains the inserted data. + * @param cb.err - Error information when the method fails. + * @param cb.formResult - Form or null if form any error occurs. + */ + + public writeForm(form: Form, cb: (err: Error, formResult?: Form) => void){ + const queryString: string = "INSERT INTO form (title, description) VALUES( $1, $2 ) RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + form.title + , form.description + ] + }; + + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin(callback); + }, + (result: QueryResult, callback: (err: Error, result?: QueryResult) => void) => { + this.executeQuery(query, callback); + }, + (result: QueryResult, callback: (err: Error, formId?: number) => void) => { + if (result.rowCount !== 1){ + callback(ErrorHandler.notInserted("Form")); + return; + } + eachSeries(form.inputs, (input: Input, innerCallback) => { + this.writeInputWithFormId(result.rows[0]["id"], input, innerCallback); + }, (error) => { + if (error){ + callback(error); + return; + + } + callback(error, result.rows[0]["id"]); + + }); + }, + (formId: number, callback: (err: Error, formId?: number) => void) => { + + this.commit((error: Error, result?: QueryResult) => { + callback(error, formId); + }); + }, + (formId: number, callback: (err: Error, formResult?: Form) => void) => { + + this.readForm(formId, callback); + + } + ], (err, result: Form) => { + + if (err){ + this.rollback( (error: Error, results?: QueryResult) => { + cb(err); + return; + }); + return; + } + cb(err, result); + + }); + } + + /** + * Asynchronously insert a Input on Database and return it. + * @param formId - Form identifier to relate with Input. + * @param input - Input to be inserted. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private writeInputWithFormId(formId: number, input: Input, cb: (err: Error, result?: number) => void){ + const queryString: string = "INSERT INTO input (\ +id_form, placement, input_type, enabled, question, description)\ +VALUES ( $1, $2, $3, $4, $5, $6) RETURNING id;"; + + const query: QueryOptions = { + query: queryString + , parameters: [ + formId + , input.placement + , EnumHandler.stringifyInputType(input.type) + , true + , input.question + , input.description + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err){ + cb(err); + return; + } + + if (result.rowCount !== 1){ + cb(ErrorHandler.notInserted("Input")); + return; + } + + eachSeries(input.validation, (validation: Validation, callback) => { + + this.writeValidationWithInputId(result.rows[0]["id"], validation, callback); + + }, (error) => { + + cb(error, result.rows[0]["id"]); + + }); + }); + + } + /** + * Asynchronously insert a Validation on Database and return it. + * @param inputId - Input id to relate with Validation. + * @param validation - Validation to be inserted. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private writeValidationWithInputId(inputId: number, validation: Validation, cb: (err: Error) => void){ + + const queryString: string = "INSERT INTO input_validation\ +( id_input, validation_type) VALUES\ +( $1, $2 ) RETURNING id;"; + + const query: QueryOptions = { + query: queryString + , parameters: [ + inputId + , EnumHandler.stringifyValidationType(validation.type) + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + + if (err){ + cb(err); + return; + } + + if (result.rowCount !== 1){ + cb(ErrorHandler.notInserted("Validation")); + return; + } + + eachOfSeries(validation.arguments, (argument, placement: number, callback) => { + + this.writeValidationArgumentWithInputIdAndPlacement(result.rows[0]["id"], argument, placement, callback); + }, (error) => { + cb(error); + }); + + }); + + } + + /** + * Asynchronously insert a Validation Argument on Database and return it. + * @param validationId - Validation identifier to relate with Argument. + * @param argument - Argument to be inserted. + * @param placement - placement where argument should be. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private writeValidationArgumentWithInputIdAndPlacement(validationId: number, argument: string, placement: number, cb: (err: Error) => void){ + + const queryString: string = "INSERT INTO input_validation_argument \ +( id_input_validation, argument, placement ) VALUES\ +( $1, $2, $3 ) RETURNING id;"; + + const query: QueryOptions = { + query: queryString + , parameters: [ + validationId + , argument + , placement + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + + if (err){ + cb(err); + return; + } + + if (result.rowCount !== 1){ + cb(ErrorHandler.notInserted("Validation Argument")); + return; + } + + cb(err); + + }); + + } + + /** + * Asynchronously insert a form answer on Database and return it. + * @param formAnswer - FormAnswer to be inserted. + * @param cb - Callback function which contains the inserted data. + * @param cb.err - Error information when the method fails. + * @param cb.formAnswerResult - Form or null if form any error occurs. + */ + public writeFormAnswer(formAnswer: FormAnswer, cb: (err: Error, formAnswerResult?: FormAnswer) => void){ + const queryString: string = "INSERT INTO form_answer (id_form, answered_at) VALUES( $1, $2 ) RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + formAnswer.form.id + , formAnswer.timestamp + ] + }; + + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin(callback); + }, + (result: QueryResult, callback: (err: Error, result?: QueryResult) => void) => { + this.executeQuery(query, callback); + }, + (result: QueryResult, callback: (err: Error, formAnswerId?: number) => void) => { + if (result.rowCount !== 1){ + callback(ErrorHandler.notInserted("FormAnswer")); + return; + } + // NOTE: Although this two "FOR"s the complexity is O(n) + // This first eachSeries iterates over the keys of the dictonary + eachSeries(Object.keys(formAnswer.inputAnswers), (key, outerCallback) => { + // this second one iterates over the array of the objects within the current key + + eachSeries(formAnswer.inputAnswers[parseInt(key, 10)], (inputsAnswer: InputAnswer, innerCallback) => { + this.writeInputAnswerWithFormId(result.rows[0]["id"], inputsAnswer, innerCallback); + }, (error) => { + if (error){ + outerCallback(error); + return; + } + outerCallback(error); + }); + }, (error) => { + if (error){ + callback(error); + return; + } + callback(error, result.rows[0]["id"]); + }); + + }, + (formAnswerId: number, callback: (err: Error, formId?: number) => void) => { + + this.commit((error: Error, result?: QueryResult) => { + callback(error, formAnswerId); + }); + }, + (formAnswerId: number, callback: (err: Error, formResult?: FormAnswer) => void) => { + + this.readFormAnswer(formAnswerId, callback); + + } + ], (err, result: FormAnswer) => { + + if (err){ + this.rollback( (error: Error, results?: QueryResult) => { + cb(err); + return; + }); + return; + } + cb(err, result); + + }); + } + + /** + * Asynchronously insert a Input Answer on Database and return it. + * @param formAnswerId - Form Answer identifier to relate with Input Answer. + * @param inputsAnswer - InputsAnswer to be inserted. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private writeInputAnswerWithFormId(formAnswerId: number, inputAnswer: InputAnswer, cb: (err: Error) => void){ + const queryString: string = "INSERT INTO input_answer (\ +id_form_answer, id_input, value, placement)\ +VALUES ( $1, $2, $3, $4) RETURNING id;"; + + const query: QueryOptions = { + query: queryString + , parameters: [ + formAnswerId + , inputAnswer.idInput + , inputAnswer.value + , inputAnswer.placement + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err){ + cb(err); + return; + } + + if (result.rowCount !== 1){ + cb(ErrorHandler.notInserted("InputsAnswer")); + return; + } + cb(err); + }); + + } + + /** + * Asynchronously executes a query and get a Form. + * @param id - Form identifier to be founded. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.form - Form or null if form not exists. + */ + public readFormAnswer(id: number, cb: (err: Error, formAnswer?: FormAnswer) => void){ + + const queryString: string = "SELECT id, id_form, answered_at FROM form_answer WHERE id=$1;"; + const query: QueryOptions = {query: queryString, parameters: [id]}; + + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin(callback); + }, + (result: QueryResult, callback: (err: Error, result?: QueryResult) => void) => { + this.executeQuery(query, callback); + }, + (result: QueryResult, callback: (err: Error, formAnswer?: FormAnswer) => void) => { + if (result.rowCount !== 1){ + callback(ErrorHandler.badIdAmount(result.rowCount)); + return; + } + + this.readForm(result.rows[0]["id_form"], (err: Error, formResult: Form) => { + this.readInputAnswerWithFormAnswerId(result.rows[0]["id"], (error: Error, inputsAnswerOptionsResult: InputAnswerOptionsDict) => { + const formAnswerOpt: FormAnswerOptions = { + id: result.rows[0]["id"] + , form: formResult + , inputsAnswerOptions: inputsAnswerOptionsResult + , timestamp : result.rows[0]["answered_at"] + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer ( OptHandler.formAnswer(formAnswerOpt)); + } + catch (e){ + callback(e); + return; + } + + callback(error, formAnswerTmp); + }); + }); + }, + (formAnswer: FormAnswer, callback: (err: Error, formAnswer?: FormAnswer) => void) => { + + this.commit((error: Error, result?: QueryResult) => { + callback(error, formAnswer); + }); + } + ], (err, result: FormAnswer) => { + + if (err){ + this.rollback( (error: Error, results?: QueryResult) => { + cb(err); + return; + }); + return; + } + cb(err, result); + + }); + + } + + /** + * A private method to asynchronously executes a query and get a list of Inputs. + * @param id - Form identifier which inputs are linked to. + * @param cb - Callback function which contains the data read. + * @param cb.err - Error information when the method fails. + * @param cb.inputs - Input array or an empty list if there is no input linked to form. + */ + private readInputAnswerWithFormAnswerId(id: number, cb: (err: Error, inputsAnswerResult?: InputAnswerOptionsDict) => void){ + const queryString: string = "SELECT id, id_form_answer, id_input, value,\ +placement FROM input_answer WHERE id_form_answer=$1;"; + const query: QueryOptions = {query: queryString, parameters: [id]}; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + + const inputAnswersOpts: InputAnswerOptions[] = result.rows.map( (inputsAnswerResult) => { + const inputAnswersOpt: InputAnswerOptions = { + id: inputsAnswerResult["id"] + , idInput: inputsAnswerResult["id_input"] + , value: inputsAnswerResult["value"] + , placement: inputsAnswerResult["placement"] + }; + return OptHandler.inputAnswer(inputAnswersOpt); + }); + + const inputsAnswerResults: InputAnswerOptionsDict = {}; + for (const i of inputAnswersOpts){ + // FIXME: There is no coverage teste for this "IF" + // it happens because, until this date (04/Jun/2019) I did not implement multivalored inputs + // I hope someday someone implements it + if ( inputsAnswerResults[i["idInput"]] ) { + inputsAnswerResults[i["idInput"]].push(i); + inputsAnswerResults[i["idInput"]] = Sorter.sortByPlacement(inputsAnswerResults[i["idInput"]]); + }else{ + inputsAnswerResults[i["idInput"]] = [i]; + } + } + cb(err, inputsAnswerResults); + }); + } + + /** + * Asynchronously insert a formUpdate and inputUpdates on Database. + * @param formUpdate - A FormUpdate with InputUpdates that should be inserted in the database. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + public updateForm(formUpdate: FormUpdate, cb: (err: Error) => void) { + + waterfall([ + (callback: (err: Error, result?: QueryResult) => void) => { + this.begin(callback); + }, + (result: QueryResult, callback: (err: Error, result?: number) => void) => { + this.writeFormUpdate(formUpdate, (err, idFormUpdate: number) => { + if (err) { + callback(err); + return; + } + callback(null, idFormUpdate); + }); + }, + (idFormUpdate: number, callback: (err: Error) => void) => { + eachSeries(formUpdate.inputUpdates, (inputUpdates: InputUpdate, innerCallback) => { + this.writeInputUpdate(idFormUpdate, inputUpdates, innerCallback); + }, (error) => { + callback(error); + }); + }, + (callback: (err: Error) => void) => { + this.commit((error: Error, result?: QueryResult) => { + callback(error); + }); + } + ], (err) => { + if (err){ + this.rollback( (error: Error, results?: QueryResult) => { + cb(err); + return; + }); + return; + } + cb(null); + }); + + } + + /** + * A private method that execute a query and insert a FormUpdate in the database + * @param formUpdate - Form Update to be inserted. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + * @param cb.formUpdateId - The id of the inserted FormUpdate. + */ + private writeFormUpdate(formUpdate: FormUpdate, cb: (err: Error, formUpdateId?: number) => void) { + + const queryString: string = "INSERT INTO form_update (id_form, update_date) \ + VALUES ( $1, $2 ) RETURNING id;"; + const query: QueryOptions = { + query: queryString + , parameters: [ + formUpdate.form.id + , formUpdate.updateDate + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + if (err) { + cb(err); + return; + } + cb(err, result.rows[0].id); + }); + + } + + /** + * A private method that execute a query and insert a InputUpdate in the database + * @param inputUpdate - Input Update to be inserted. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private writeInputUpdate(idFormUpdate: number, inputUpdate: InputUpdate, cb: (err: Error) => void) { + + const queryString: string = "INSERT INTO input_update (id_form_update, id_input, input_operation_id, value) \ + VALUES ( $1, $2, $3, $4 );"; + const query: QueryOptions = { + query: queryString + , parameters: [ + idFormUpdate + , inputUpdate.input.id + , inputUpdate.inputOperation + , inputUpdate.value + ] + }; + + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err); + }); + } + + /** + * A method that update the database based on a given FormUpdate object + * @param formUpdate - FormUpdate with the parameters to update the database. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + * @param cb.formUpdateResult - A FormUpdate with updated id's. + */ + public updateDatabase(formUpdate: FormUpdate, cb: (err: Error, formUpdateResult?: FormUpdate) => void) { + + const formUpdateResult: FormUpdate = { + form: formUpdate.form + , updateDate: formUpdate.updateDate + , inputUpdates: [] + }; + + eachSeries(formUpdate.inputUpdates, (inputUpdate, callback) => { + switch (inputUpdate.inputOperation) { + case UpdateType.ADD: { + this.writeInputWithFormId(formUpdate.form.id, inputUpdate.input, (err: Error, id: number) => { + if (err) { + callback(err); + } + const inputOpt: InputOptions = inputUpdate.input; + inputOpt.id = id; + + const inputUpdateOpt: InputUpdateOptions = { + input: inputOpt + , inputOperation: UpdateType.ADD + , value: null + }; + formUpdateResult.inputUpdates.push(new InputUpdate(inputUpdateOpt)); + callback(null); + }); + break; + } + case UpdateType.REMOVE: { + // Set enabled option in database as false + this.updateInput(0, inputUpdate.input.id, "enabled", (err: Error) => { + if (err) { + callback(err); + } + formUpdateResult.inputUpdates.push(inputUpdate); + callback(null); + }); + break; + } + case UpdateType.SWAP: { + // Update placement option in database of a input + this.updateInput(inputUpdate.input.placement, inputUpdate.input.id, "placement", (err: Error) => { + if (err) { + callback(err); + } + formUpdateResult.inputUpdates.push(inputUpdate); + callback(null); + }); + break; + } + case UpdateType.REENABLED: { + // Set enabled option in database as true + this.updateInput(1, inputUpdate.input.id, "enabled", (err: Error) => { + if (err) { + callback(err); + } + formUpdateResult.inputUpdates.push(inputUpdate); + callback(null); + }); + break; + } + default: { + callback(new Error ("Operation not recognized")); + } + } + }, (error) => { + if (error) { + cb(error); + } + cb(null, formUpdateResult); + }); + } + + /** + * A method that execute a query and update a input based on given parameters + * @param value - A number to be inserted in the database. + * @param id - The input id that should be updated + * @param field - The field on database that should be updated. + * @param cb - Callback function. + * @param cb.err - Error information when the method fails. + */ + private updateInput(value: number, id: number, field: string, cb: (err: Error) => void) { + const queryString: string = "UPDATE input SET " + field + " = $1 WHERE id = $2"; + const query: QueryOptions = { + query: queryString + , parameters: [ + value + , id + ] + }; + this.executeQuery(query, (err: Error, result?: QueryResult) => { + cb(err); + }); + } + +} diff --git a/src/utils/diffHandler.spec.ts b/src/utils/diffHandler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3867a89b77eb81b4e0df5cfa513ecf1293d38c19 --- /dev/null +++ b/src/utils/diffHandler.spec.ts @@ -0,0 +1,1041 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; +import { Form, FormOptions } from "../core/form"; +import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; +import { Input, InputOptions, Validation } from "../core/input"; +import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; +import { DiffHandler } from "./diffHandler"; +import { InputType, UpdateType, ValidationType } from "./enumHandler"; +import { TestHandler } from "./testHandler"; + +describe("Diff Handler", () => { + it("should return a valid formUpdate remove type", (done) => { + const newInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const newInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + ] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const oldInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + , oldInputObj3 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate: InputUpdate = { + input: oldInputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ expInputUpdate ] + }; + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + + it("should return a valid formUpdate add type", (done) => { + const newInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + + }; + const newInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const newInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const newInputObj4: Input = { + placement: 3 + , description: "Description Question 4 Form 1" + , question: "Question 4 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: undefined + }; + + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + , newInputObj3 + , newInputObj4 + ] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const oldInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + , oldInputObj3 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate: InputUpdate = { + input: newInputObj4 + , inputOperation: UpdateType.ADD + , value: null + }; + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ expInputUpdate ] + }; + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + + it("should return a valid formUpdate swap type", (done) => { + const newInputObj1: Input = { + placement: 1 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + + }; + const newInputObj2: Input = { + placement: 0 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const newInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + , newInputObj3 + ] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const oldInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + , oldInputObj3 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate1: InputUpdate = { + input: newInputObj1 + , inputOperation: UpdateType.SWAP + , value: "" + 0 + }; + + const expInputUpdate2: InputUpdate = { + input: newInputObj2 + , inputOperation: UpdateType.SWAP + , value: "" + 1 + }; + + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ + expInputUpdate1 + , expInputUpdate2 + ] + }; + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + + it("should return a valid formUpdate remove and add all", (done) => { + const newInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: undefined + }; + const newInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: undefined + }; + const newInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: undefined + }; + + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + , newInputObj3 + ] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const oldInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + , oldInputObj3 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate1: InputUpdate = { + input: oldInputObj1 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate2: InputUpdate = { + input: oldInputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate3: InputUpdate = { + input: oldInputObj2 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate4: InputUpdate = { + input: newInputObj1 + , inputOperation: UpdateType.ADD + , value: null + }; + const expInputUpdate5: InputUpdate = { + input: newInputObj2 + , inputOperation: UpdateType.ADD + , value: null + }; + const expInputUpdate6: InputUpdate = { + input: newInputObj3 + , inputOperation: UpdateType.ADD + , value: null + }; + + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ + expInputUpdate1 + , expInputUpdate2 + , expInputUpdate3 + , expInputUpdate4 + , expInputUpdate5 + , expInputUpdate6 + ] + }; + + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + + it("should return a valid formUpdate all operations", (done) => { + const newInputObj1: Input = { + placement: 1 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const newInputObj2: Input = { + placement: 0 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 2 + }; + const newInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 3 + }; + const newInputObj4: Input = { + placement: 3 + , description: "Description Question 4 Form 1" + , question: "Question 4 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: undefined + }; + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + , newInputObj3 + , newInputObj4 + ] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 2 + }; + const oldInputObj3: Input = { + placement: 2 + , description: "Description Question 4 Form 1" + , question: "Question 4 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 4 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + , oldInputObj3 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate1: InputUpdate = { + input: newInputObj1 + , inputOperation: UpdateType.SWAP + , value: "" + 0 + }; + const expInputUpdate2: InputUpdate = { + input: newInputObj2 + , inputOperation: UpdateType.SWAP + , value: "" + 1 + }; + const expInputUpdate3: InputUpdate = { + input: newInputObj3 + , inputOperation: UpdateType.REENABLED + , value: null + }; + const expInputUpdate4: InputUpdate = { + input: oldInputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate5: InputUpdate = { + input: newInputObj4 + , inputOperation: UpdateType.ADD + , value: null + }; + + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ + expInputUpdate1 + , expInputUpdate2 + , expInputUpdate3 + , expInputUpdate4 + , expInputUpdate5 + ] + }; + + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + it("should return a valid formUpdate remove and add all", (done) => { + const newInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: undefined + }; + const newInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: undefined + }; + const newInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: undefined + }; + + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + , newInputObj3 + ] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const oldInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + , oldInputObj3 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate1: InputUpdate = { + input: oldInputObj1 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate2: InputUpdate = { + input: oldInputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate3: InputUpdate = { + input: oldInputObj2 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate4: InputUpdate = { + input: newInputObj1 + , inputOperation: UpdateType.ADD + , value: null + }; + const expInputUpdate5: InputUpdate = { + input: newInputObj2 + , inputOperation: UpdateType.ADD + , value: null + }; + const expInputUpdate6: InputUpdate = { + input: newInputObj3 + , inputOperation: UpdateType.ADD + , value: null + }; + + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ + expInputUpdate1 + , expInputUpdate2 + , expInputUpdate3 + , expInputUpdate4 + , expInputUpdate5 + , expInputUpdate6 + ] + }; + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + + it("should return a valid formUpdate restore inputs", (done) => { + const newInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const newInputObj2: Input = { + placement: 2 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 2 + }; + const newInputObj3: Input = { + placement: 1 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 3 + }; + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + , newInputObj3 + ] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate1: InputUpdate = { + input: newInputObj2 + , inputOperation: UpdateType.REENABLED + , value: null + }; + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ + expInputUpdate1 + ] + }; + + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + + it("should return a valid formUpdate create a new Form", (done) => { + const newInputObj1: Input = { + placement: 1 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: null + }; + const newInputObj2: Input = { + placement: 0 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: null + }; + const newInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: null + }; + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + newInputObj1 + , newInputObj2 + , newInputObj3 + ] + }; + + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate1: InputUpdate = { + input: newInputObj1 + , inputOperation: UpdateType.ADD + , value: null + }; + const expInputUpdate2: InputUpdate = { + input: newInputObj2 + , inputOperation: UpdateType.ADD + , value: null + }; + const expInputUpdate3: InputUpdate = { + input: newInputObj3 + , inputOperation: UpdateType.ADD + , value: null + }; + + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ + expInputUpdate1 + , expInputUpdate2 + , expInputUpdate3 + ] + }; + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + + it("should return a valid formUpdate remove all inputs", (done) => { + + const newFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [] + }; + + const oldInputObj1: Input = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const oldInputObj2: Input = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const oldInputObj3: Input = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , enabled: true + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const oldFormObj: Form = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + oldInputObj1 + , oldInputObj2 + , oldInputObj3 + ] + }; + + const resFormUpdate = DiffHandler.diff(newFormObj, oldFormObj); + + const expInputUpdate1: InputUpdate = { + input: oldInputObj1 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate2: InputUpdate = { + input: oldInputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; + const expInputUpdate3: InputUpdate = { + input: oldInputObj2 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const expFormUpdate: FormUpdate = { + form: newFormObj + , updateDate: new Date() + , inputUpdates: [ + expInputUpdate1 + , expInputUpdate2 + , expInputUpdate3 + ] + }; + + TestHandler.testFormUpdate(resFormUpdate, expFormUpdate); + done(); + }); + +}); diff --git a/src/utils/diffHandler.ts b/src/utils/diffHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..84cdc704e349b40a76651d9082709790acf7ac88 --- /dev/null +++ b/src/utils/diffHandler.ts @@ -0,0 +1,157 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { Form, FormOptions } from "../core/form"; +import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; +import { Input, InputOptions } from "../core/input"; +import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; +import { InputType, UpdateType, ValidationType } from "./enumHandler"; +import { Sorter } from "./sorter"; + +/** + * DiffHandler to find out the difference between two Forms then create and return a FormUpdate + */ +export class DiffHandler { + + /** + * Return an FormUpdate object with the result of the difference between two Forms. + * @param newForm - The new Form that should be compared. + * @param oldForm - The old Form that was in the database. + * @returns - An FormUpdate object. + */ + public static diff(newForm: Form, oldForm: Form): FormUpdate { + + const sortedNewInputs: Input[] = Sorter.sortById(newForm.inputs).filter(DiffHandler.isIdValid); + const sortedOldInputs: Input[] = Sorter.sortById(oldForm.inputs); + const inputsToAdd: Input[] = Sorter.sortById(newForm.inputs).filter(DiffHandler.isNotIdValid); + + const formUpdate: FormUpdate = { + form: newForm + , updateDate: new Date() + , inputUpdates: [] + }; + + let i: number = 0; + let j: number = 0; + + while ((i < sortedOldInputs.length) && (j < sortedNewInputs.length)) { + if (sortedNewInputs[j]["id"] === sortedOldInputs[i]["id"]) { + if ( sortedNewInputs[j].placement !== sortedOldInputs[i].placement ) { + formUpdate.inputUpdates.push(DiffHandler.swapInput(sortedNewInputs[j], sortedOldInputs[i])); + } + j++; + i++; + } + else if (sortedNewInputs[j]["id"] < sortedOldInputs[i]["id"]) { + formUpdate.inputUpdates.push(DiffHandler.reenabledInput(sortedNewInputs[j])); + j++; + } + else { + formUpdate.inputUpdates.push(DiffHandler.removeInput(sortedOldInputs[i])); + i++; + } + } + + while ((i < sortedOldInputs.length)) { + formUpdate.inputUpdates.push(DiffHandler.removeInput(sortedOldInputs[i])); + i++; + } + + while ((j < sortedNewInputs.length)) { + if (sortedNewInputs[j].id !== null) { + formUpdate.inputUpdates.push(DiffHandler.reenabledInput(sortedNewInputs[j])); + } + j++; + } + + j = 0; + while ((j < inputsToAdd.length)) { + formUpdate.inputUpdates.push(DiffHandler.addInput(inputsToAdd[j])); + j++; + } + return formUpdate; + } + + /** + * Create an InputUpdate object which removes the given Input. + * @param input - An input to be removed + * @returns - An InputUpdate object. + */ + private static removeInput(input: Input): InputUpdate { + const inputUpdate: InputUpdate = { + input + , inputOperation: UpdateType.REMOVE + , value: null + }; + return inputUpdate; + } + + /** + * Create an InputUpdate object which add the given Input. + * @param input - An input to be added + * @returns - An InputUpdate object. + */ + private static addInput(input: Input): InputUpdate { + const inputUpdate: InputUpdate = { + input + , inputOperation: UpdateType.ADD + , value: null + }; + return inputUpdate; + } + + /** + * Create an InputUpdate object which change the placement of the given Input. + * @param input - An input to be changed + * @returns - An InputUpdate object. + */ + private static swapInput(newInput: Input, oldInput: Input): InputUpdate { + const inputUpdate: InputUpdate = { + input: newInput + , inputOperation: UpdateType.SWAP + , value: "" + oldInput.placement + }; + return inputUpdate; + } + + /** + * Create an InputUpdate object which reenabled given Input. + * @param input - An input to be reenabled + * @returns - An InputUpdate object. + */ + private static reenabledInput(input: Input): InputUpdate { + const inputUpdate: InputUpdate = { + input + , inputOperation: UpdateType.REENABLED + , value: null + }; + return inputUpdate; + } + + private static isIdValid(obj: any): boolean { + return ((obj.id !== null) && (obj.id !== undefined) && (obj.id > 0) && (typeof obj.id === "number")); + } + + private static isNotIdValid(obj: any): boolean { + return !DiffHandler.isIdValid(obj); + } + +} diff --git a/src/utils/enumHandler.spec.ts b/src/utils/enumHandler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..dcc32ae289f123bd953a2ecbc09aee9744bb2c7e --- /dev/null +++ b/src/utils/enumHandler.spec.ts @@ -0,0 +1,124 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; +import { EnumHandler, InputType, UpdateType, ValidationType } from "./enumHandler"; + +describe("Enum Handler", () => { + + it("should stringify UpdateType ", () => { + + const updateNone = EnumHandler.stringifyUpdateType(UpdateType.NONE); + const updateAdd = EnumHandler.stringifyUpdateType(UpdateType.ADD); + const updateRemove = EnumHandler.stringifyUpdateType(UpdateType.REMOVE); + const updateSwap = EnumHandler.stringifyUpdateType(UpdateType.SWAP); + const updateReenabled = EnumHandler.stringifyUpdateType(UpdateType.REENABLED); + + expect(updateNone).to.be.equal(""); + expect(updateAdd).to.be.equal("add"); + expect(updateRemove).to.be.equal("remove"); + expect(updateSwap).to.be.equal("swap"); + expect(updateReenabled).to.be.equal("reenabled"); + }); + + it("should parse string to UpdateType", () => { + const updateAdd = EnumHandler.parseUpdateType("add"); + const updateAddCapitalLetters = EnumHandler.parseUpdateType("ADD"); + const updateRemove = EnumHandler.parseUpdateType("remove"); + const updateRemoveCapitalLetters = EnumHandler.parseUpdateType("REMOVE"); + const updateSwap = EnumHandler.parseUpdateType("swap"); + const updateSwapCapitalLetters = EnumHandler.parseUpdateType("SWAP"); + const updateReenabled = EnumHandler.parseUpdateType("reenabled"); + const updateReenabledCapitalLetters = EnumHandler.parseUpdateType("REENABLED"); + const updateNone = EnumHandler.parseUpdateType(""); + const updateFOOL = EnumHandler.parseUpdateType("fool"); + + expect(updateAdd).to.be.equal(UpdateType.ADD); + expect(updateAddCapitalLetters).to.be.equal(UpdateType.ADD); + expect(updateRemove).to.be.equal(UpdateType.REMOVE); + expect(updateRemoveCapitalLetters).to.be.equal(UpdateType.REMOVE); + expect(updateSwap).to.be.equal(UpdateType.SWAP); + expect(updateSwapCapitalLetters).to.be.equal(UpdateType.SWAP); + expect(updateNone).to.be.equal(UpdateType.NONE); + expect(updateFOOL).to.be.equal(UpdateType.NONE); + }); + + it("should stringify InputType ", () => { + + const inputNone = EnumHandler.stringifyInputType(InputType.NONE); + const inputText = EnumHandler.stringifyInputType(InputType.TEXT); + + expect(inputNone).to.be.equal(""); + expect(inputText).to.be.equal("text"); + }); + + it("should parse string to InputType", () => { + const inputText = EnumHandler.parseInputType("text"); + const inputTextCapitalLetters = EnumHandler.parseInputType("TEXT"); + const inputNone = EnumHandler.parseInputType(""); + const inputFOOL = EnumHandler.parseInputType("fool"); + + expect(inputText).to.be.equal(InputType.TEXT); + expect(inputTextCapitalLetters).to.be.equal(InputType.TEXT); + expect(inputNone).to.be.equal(InputType.NONE); + expect(inputFOOL).to.be.equal(InputType.NONE); + }); + + it("should stringify ValidationType ", () => { + + const validationRegex = EnumHandler.stringifyValidationType(ValidationType.REGEX); + const validationMandatory = EnumHandler.stringifyValidationType(ValidationType.MANDATORY); + const validationMaxChar = EnumHandler.stringifyValidationType(ValidationType.MAXCHAR); + const validationMinChar = EnumHandler.stringifyValidationType(ValidationType.MINCHAR); + const validationNone = EnumHandler.stringifyValidationType(ValidationType.NONE); + + expect(validationNone).to.be.equal(""); + expect(validationRegex).to.be.equal("regex"); + expect(validationMandatory).to.be.equal("mandatory"); + expect(validationMaxChar).to.be.equal("maxchar"); + expect(validationMinChar).to.be.equal("minchar"); + }); + + it("should parse string to ValidationType", () => { + const validationRegex = EnumHandler.parseValidationType("regex"); + const validationRegexCapitalized = EnumHandler.parseValidationType("REGEX"); + const validationMandatory = EnumHandler.parseValidationType("mandatory"); + const validationMandatoryCapitalized = EnumHandler.parseValidationType("MANDATORY"); + const validationMaxChar = EnumHandler.parseValidationType("maxchar"); + const validationMaxCharyCapitalized = EnumHandler.parseValidationType("MAXCHAR"); + const validationMinChar = EnumHandler.parseValidationType("minchar"); + const validationMinCharyCapitalized = EnumHandler.parseValidationType("MINCHAR"); + const validationNone = EnumHandler.parseValidationType(""); + const validatioFOOL = EnumHandler.parseValidationType("fool"); + + expect(validationRegex).to.be.equal(ValidationType.REGEX); + expect(validationRegexCapitalized).to.be.equal(ValidationType.REGEX); + expect(validationMandatory).to.be.equal(ValidationType.MANDATORY); + expect(validationMandatoryCapitalized).to.be.equal(ValidationType.MANDATORY); + expect(validationMaxChar).to.be.equal(ValidationType.MAXCHAR); + expect(validationMaxCharyCapitalized).to.be.equal(ValidationType.MAXCHAR); + expect(validationMinChar).to.be.equal(ValidationType.MINCHAR); + expect(validationMinCharyCapitalized).to.be.equal(ValidationType.MINCHAR); + expect(validationNone).to.be.equal(ValidationType.NONE); + expect(validatioFOOL).to.be.equal(ValidationType.NONE); + }); + +}); diff --git a/src/utils/enumHandler.ts b/src/utils/enumHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..fdc1a2a9a64b93915f6562fc7fe5ab0e6172a35e --- /dev/null +++ b/src/utils/enumHandler.ts @@ -0,0 +1,169 @@ +/* + * 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/>. +*/ + +/** + * Available Input types + */ +export enum InputType { + /** Text type, when input is a text */ + TEXT, + NONE +} + +export enum UpdateType { + ADD, + REMOVE, + SWAP, + REENABLED, + NONE +} + +/** + * Available Validation types + */ +export enum ValidationType { + /** Used as error code. No suitable validation found. */ + NONE, + /** Regex type, when input need a regex to validate */ + REGEX, + /** Mandatory type, when input is mandatory */ + MANDATORY, + /** MaxChars type, when input has maximum char limit */ + MAXCHAR, + /** MinChars type, when input has minimum char limit */ + MINCHAR +} + +/** + * Enum's handler. Manage parse through the project. + */ +export class EnumHandler { + + /** + * Parse an enum(Input type) to string. + * @param a - Input type to be stringified. + * @returns - Input Type as string + */ + public static stringifyInputType(a: InputType): string { + switch (a) { + case InputType.TEXT: + return "text"; + default: + return ""; + } + } + + /** + * Parse a string to enum(InputType). + * @param str - InputType in string format. + * @returns - Matching InputType + */ + public static parseInputType(str: string): InputType { + str = str.toLocaleLowerCase().trimLeft().trimRight(); + switch (str) { + case "text": + return InputType.TEXT; + default: + return InputType.NONE; + } + } + + /** + * Parse an enum(Validation type) to string. + * @param a - Validation Type to be stringified. + * @returns - Validation Type as string + */ + public static stringifyValidationType(a: ValidationType): string { + switch (a) { + case ValidationType.REGEX: + return "regex"; + case ValidationType.MANDATORY: + return "mandatory"; + case ValidationType.MAXCHAR: + return "maxchar"; + case ValidationType.MINCHAR: + return "minchar"; + default: + return ""; + } + } + /** + * Parse a string to enum(InputType). + * @param str - InputType in string format. + * @returns - Matching ValidationType + */ + public static parseValidationType(str: string): ValidationType { + str = str.toLocaleLowerCase().trimLeft().trimRight(); + switch (str) { + case "mandatory": + return ValidationType.MANDATORY; + case "regex": + return ValidationType.REGEX; + case "maxchar": + return ValidationType.MAXCHAR; + case "minchar": + return ValidationType.MINCHAR; + default: + return ValidationType.NONE; + } + } + + /** + * Parse an UpdateType object to string + * @param a - Update type to be stringified. + * @returns - Update Type as string + */ + public static stringifyUpdateType(a: UpdateType): string { + switch (a) { + case UpdateType.ADD: + return "add"; + case UpdateType.REMOVE: + return "remove"; + case UpdateType.SWAP: + return "swap"; + case UpdateType.REENABLED: + return "reenabled"; + default: + return ""; + } + } + + /** + * Parse a string to UpdateType object + * @param str - UpdateType in string format. + * @returns - Matching UpdateType + */ + public static parseUpdateType(str: string): UpdateType { + str = str.toLocaleLowerCase().trimLeft().trimRight(); + switch (str) { + case "add": + return UpdateType.ADD; + case "remove": + return UpdateType.REMOVE; + case "swap": + return UpdateType.SWAP; + case "reenabled": + return UpdateType.REENABLED; + default: + return UpdateType.NONE; + } + } +} diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..685c07e17e94e260bfaf63889b2188370f8c1728 --- /dev/null +++ b/src/utils/errorHandler.ts @@ -0,0 +1,53 @@ +/* + * 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/>. + */ + /** + * Error's handler. Manage error message through the project. + */ + export class ErrorHandler { + + /** + * Return an error instance with a default message when ids amount is different than 1. + * @param idNumber - Amount of ids found. + * @returns - An error instance with correct message. + */ + public static badIdAmount(idNumber: number): Error{ + return new Error("Bad amount of ids returned: found '" + idNumber + "' should be 1"); + } + + /** + * Return an error instance when a object is not inserted. + * @param objectName - Name of object, usually the name of the table. + * @returns - An error instance with correct message. + */ + public static notInserted(objectName: string): Error{ + return new Error(objectName + " not inserted"); + } + + /** + * Return an error when a object is not found. + * @param objectName - Name of object, usually the name of the table. + * @returns - An error instance with correct message. + */ + public static notFound(objectName: string): Error{ + return new Error("The dataType named '" + objectName + "' was not found"); + } + + } diff --git a/src/utils/optHandler.spec.ts b/src/utils/optHandler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..19a542f5fa1ea2a2dd26a59d5c4b55ebc0fd6f9f --- /dev/null +++ b/src/utils/optHandler.spec.ts @@ -0,0 +1,1350 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +import { expect } from "chai"; +import { Form, FormOptions } from "../core/form"; +import { FormAnswer, FormAnswerOptions } from "../core/formAnswer"; +import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; +import { Input, InputOptions, Validation } from "../core/input"; +import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; +import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; +import { InputType, UpdateType, ValidationType } from "./enumHandler"; +import { ErrorHandler } from "./errorHandler"; +import { OptHandler } from "./optHandler"; + +describe("Options Handler", () => { + + it("should get error when missing Form title", () => { + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: any = { + id: 1 + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + let formTmp: Form; + try{ + formTmp = new Form ( OptHandler.form(formObj)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Form title").message); + expect(formTmp).to.be.a("undefined"); + } + }); + + it("should get error when missing Form description", () => { + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: any = { + id: 1 + , title: "Form Title 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + let formTmp: Form; + + try{ + formTmp = new Form ( OptHandler.form(formObj)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Form description").message); + expect(formTmp).to.be.a("undefined"); + } + + }); + + it("should get error when missing Form inputs", () => { + const formObj: any = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + }; + let formTmp: Form; + + try{ + formTmp = new Form ( OptHandler.form(formObj)); + + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Form inputs").message); + expect(formTmp).to.be.a("undefined"); + } + + }); + + it("should get error when Form inputs is not an array", () => { + const formObj: any = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + }; + let formTmp: Form; + + try{ + formTmp = new Form ( OptHandler.form(formObj)); + + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Form inputs").message); + expect(formTmp).to.be.a("undefined"); + } + + }); + + it("should get error when missing Input placement", () => { + const inputObj: any = { + description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , inputs: "Fool" + , id: 1 + }; + let inputTmp: Input; + + try{ + inputTmp = new Input(OptHandler.input(inputObj)); + + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Input placement").message); + expect(inputTmp).to.be.a("undefined"); + } + + }); + + it("should get error when missing Input description", () => { + const inputObj: any = { + placement: 0 + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + let inputTmp: Input; + + try{ + inputTmp = new Input(OptHandler.input(inputObj)); + + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Input description").message); + expect(inputTmp).to.be.a("undefined"); + } + + }); + + it("should get error when missing Input question ", () => { + const inputObj: any = { + placement: 0 + , description: "Description Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + let inputTmp: Input; + + try{ + inputTmp = new Input(OptHandler.input(inputObj)); + + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Input question").message); + expect(inputTmp).to.be.a("undefined"); + } + + }); + + it("should get error when missing Input type", () => { + const inputObj: any = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , validation: [] + , id: 1 + }; + let inputTmp: Input; + + try{ + inputTmp = new Input(OptHandler.input(inputObj)); + + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Input type").message); + expect(inputTmp).to.be.a("undefined"); + } + + }); + + it("should get error when missing Input validation", () => { + const inputObj: any = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , id: 1 + }; + let inputTmp: Input; + + try{ + inputTmp = new Input(OptHandler.input(inputObj)); + + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Input validation").message); + expect(inputTmp).to.be.a("undefined"); + } + + }); + + it("should get error when Input validation is not an array", () => { + const inputObj: any = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: "fool" + , id: 1 + }; + let inputTmp: Input; + + try{ + inputTmp = new Input(OptHandler.input(inputObj)); + + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Input validation").message); + expect(inputTmp).to.be.a("undefined"); + } + + }); + + it("should get error when FormAnswer has no form associated", () => { + const inputAnswersOpt1: InputAnswerOptions = { + id: 1 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 1 Form 1" + }; + const inputAnswersOpt2: InputAnswerOptions = { + id: 2 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 2 Form" + }; + const inputAnswersOpt3: InputAnswerOptions = { + id: 3 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 3 Form" + }; + const inputAnswersOpt4: InputAnswerOptions = { + id: 4 + , idInput: null + , placement: 1 + , value: "Answer 2 to Question 3 Form" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = {1: [inputAnswersOpt1], 2: [inputAnswersOpt2], 3: [inputAnswersOpt3, inputAnswersOpt4]}; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: any = { + id: 1 + , timestamp: data + , inputsAnswerOptions: inputAnswerOptionsDict + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Form").message); + expect(formAnswerTmp).to.be.a("undefined"); + } + + }); + + it("should get error when FormAnswer has a form associated but not type form", () => { + + const inputAnswersOpt1: InputAnswerOptions = { + id: 1 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 1 Form 1" + }; + const inputAnswersOpt2: InputAnswerOptions = { + id: 2 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 2 Form" + }; + const inputAnswersOpt3: InputAnswerOptions = { + id: 3 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 3 Form" + }; + const inputAnswersOpt4: InputAnswerOptions = { + id: 4 + , idInput: null + , placement: 1 + , value: "Answer 2 to Question 3 Form" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = {1: [inputAnswersOpt1], 2: [inputAnswersOpt2], 3: [inputAnswersOpt3, inputAnswersOpt4]}; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: any = { + id: 1 + , form: inputAnswersOpt4 + , timestamp: data + , inputsAnswerOptions: inputAnswerOptionsDict + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Form").message); + expect(formAnswerTmp).to.be.a("undefined"); + } + + }); + + it("should get error when FormAnswer has no date associated", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + const formTmp: Form = new Form(OptHandler.form(formObj)); + + const inputAnswersOpt1: InputAnswerOptions = { + id: 1 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 1 Form 1" + }; + const inputAnswersOpt2: InputAnswerOptions = { + id: 2 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 2 Form" + }; + const inputAnswersOpt3: InputAnswerOptions = { + id: 3 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 3 Form" + }; + const inputAnswersOpt4: InputAnswerOptions = { + id: 4 + , idInput: null + , placement: 1 + , value: "Answer 2 to Question 3 Form" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = {1: [inputAnswersOpt1], 2: [inputAnswersOpt2], 3: [inputAnswersOpt3, inputAnswersOpt4]}; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: any = { + id: 1 + , form: formTmp + , inputsAnswerOptions: inputAnswerOptionsDict + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Answer Date").message); + expect(formAnswerTmp).to.be.a("undefined"); + } + + }); + + it("should get error when FormAnswer has a date associated but not type Date", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + const formTmp: Form = new Form(OptHandler.form(formObj)); + + const inputAnswersOpt1: InputAnswerOptions = { + id: 1 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 1 Form 1" + }; + const inputAnswersOpt2: InputAnswerOptions = { + id: 2 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 2 Form" + }; + const inputAnswersOpt3: InputAnswerOptions = { + id: 3 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 3 Form" + }; + const inputAnswersOpt4: InputAnswerOptions = { + id: 4 + , idInput: null + , placement: 1 + , value: "Answer 2 to Question 3 Form" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = {1: [inputAnswersOpt1], 2: [inputAnswersOpt2], 3: [inputAnswersOpt3, inputAnswersOpt4]}; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: any = { + id: 1 + , form: formTmp + , timestamp: "data" + , inputsAnswerOptions: inputAnswerOptionsDict + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Answer Date").message); + expect(formAnswerTmp).to.be.a("undefined"); + } + + }); + + it("should get error when FormAnswer has no inputAnswerOptionsDict associatedr", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + + const formTmp: Form = new Form(OptHandler.form(formObj)); + + const inputAnswersOpt1: InputAnswerOptions = { + id: 1 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 1 Form 1" + }; + const inputAnswersOpt2: InputAnswerOptions = { + id: 2 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 2 Form" + }; + const inputAnswersOpt3: InputAnswerOptions = { + id: 3 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 3 Form" + }; + const inputAnswersOpt4: InputAnswerOptions = { + id: 4 + , idInput: null + , placement: 1 + , value: "Answer 2 to Question 3 Form" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 1: [inputAnswersOpt1] + , 2: [inputAnswersOpt2] + , 3: [inputAnswersOpt3, inputAnswersOpt4] + }; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: any = { + id: 1 + , form: formTmp + , timestamp: data + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Input Answers").message); + expect(formAnswerTmp).to.be.a("undefined"); + } + + }); + + it("should test InputAnswerOptions when missing idInput", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + const formTmp: Form = new Form(OptHandler.form(formObj)); + + const inputAnswersOpt1: any = { + id: 1 + , placement: 0 + , value: "Answer 1 to Question 1 Form 1" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 1: [inputAnswersOpt1] + }; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: FormAnswerOptions = { + id: 1 + , form: formTmp + , timestamp: data + , inputsAnswerOptions: inputAnswerOptionsDict + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("idInput").message); + expect(formAnswerTmp).to.be.a("undefined"); + } + }); + + it("should test InputAnswerOptions when missing placement", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + const formTmp: Form = new Form(OptHandler.form(formObj)); + + const inputAnswersOpt1: any = { + id: 1 + , idInput: null + , value: "Answer 1 to Question 1 Form 1" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 1: [inputAnswersOpt1] + }; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: FormAnswerOptions = { + id: 1 + , form: formTmp + , timestamp: data + , inputsAnswerOptions: inputAnswerOptionsDict + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Placement").message); + expect(formAnswerTmp).to.be.a("undefined"); + } + + }); + + it("should test InputAnswerOptions when missing Value", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + const formTmp: Form = new Form(OptHandler.form(formObj)); + + const inputAnswersOpt1: any = { + id: 1 + , idInput: null + , placement: 0 + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 1: [inputAnswersOpt1] + }; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: FormAnswerOptions = { + id: 1 + , form: formTmp + , timestamp: data + , inputsAnswerOptions: inputAnswerOptionsDict + }; + let formAnswerTmp: FormAnswer; + + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }catch (e){ + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("Value").message); + expect(formAnswerTmp).to.be.a("undefined"); + } + + }); + + it("should test FormAnswer no error", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + OptHandler.input(inputObj1) + , OptHandler.input(inputObj2) + , OptHandler.input(inputObj3) + ] + }; + const formTmp: Form = new Form(OptHandler.form(formObj)); + + const inputAnswersOpt1: InputAnswerOptions = { + id: 1 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 1 Form 1" + }; + const inputAnswersOpt2: InputAnswerOptions = { + id: 2 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 2 Form" + }; + const inputAnswersOpt3: InputAnswerOptions = { + id: 3 + , idInput: null + , placement: 0 + , value: "Answer 1 to Question 3 Form" + }; + const inputAnswersOpt4: InputAnswerOptions = { + id: 4 + , idInput: null + , placement: 1 + , value: "Answer 2 to Question 3 Form" + }; + + const inputAnswerOptionsDict: InputAnswerOptionsDict = {1: [inputAnswersOpt1], 2: [inputAnswersOpt2], 3: [inputAnswersOpt3, inputAnswersOpt4]}; + const data: Date = new Date(2019, 5, 15); + const formAnswerOptions: any = { + id: 1 + , form: formTmp + , timestamp: data + , inputsAnswerOptions: inputAnswerOptionsDict + }; + let formAnswerTmp: FormAnswer; + try{ + formAnswerTmp = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + }finally{ + expect(formAnswerTmp).to.not.be.a("undefined"); + } + }); + + it("should test FormUpdate no error", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const updateObj1: InputUpdateOptions = { + id: 1 + , input: inputObj1 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const updateObj2: InputUpdateOptions = { + id: 2 + , input: inputObj2 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const updateObj3: InputUpdateOptions = { + id: 3 + , input: inputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + ] + }; + const dateTMP: Date = new Date(); + const formUpdateObj: any = { + id: 1 + , form: formObj + , updateDate: dateTMP + , inputUpdates: [ + updateObj1 + , updateObj2 + , updateObj3 + ] + }; + + let formUpdateTmp: FormUpdate; + + try { + formUpdateTmp = new FormUpdate(OptHandler.formUpdate(formUpdateObj)); + } finally { + expect(formUpdateTmp).to.be.a("object"); + } + }); + + it("should get error FormUpdate inputUpdates is not an array", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const updateObj: InputUpdateOptions = { + id: 1 + , input: inputObj1 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + ] + }; + const dateTMP: Date = new Date(); + const formUpdateObj: any = { + id: 1 + , form: formObj + , updateDate: dateTMP + , inputUpdates: updateObj + }; + + let formUpdateTmp: FormUpdate; + + try { + formUpdateTmp = new FormUpdate(OptHandler.formUpdate(formUpdateObj)); + } catch (e) { + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("inputUpdates").message); + expect(formUpdateTmp).to.be.a("undefined"); + } + }); + + it("should get error FormUpdate missing option key 'form'", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + const updateObj1: InputUpdateOptions = { + id: 1 + , input: inputObj1 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + const updateObj2: InputUpdateOptions = { + id: 2 + , input: inputObj2 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + const updateObj3: InputUpdateOptions = { + id: 3 + , input: inputObj3 + , inputOperation: UpdateType.REMOVE + , value: null + }; + + const dateTMP: Date = new Date(); + const formUpdateObj: any = { + id: 1 + , updateDate: dateTMP + , inputUpdates: [ + updateObj1 + , updateObj2 + , updateObj3 + ] + }; + + let formUpdateTmp: FormUpdate; + + try { + formUpdateTmp = new FormUpdate(OptHandler.formUpdate(formUpdateObj)); + } catch (e) { + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("form").message); + expect(formUpdateTmp).to.be.a("undefined"); + } + }); + + it("should get error FormUpdate missing option key 'inputUpdates'", () => { + + const inputObj1: InputOptions = { + placement: 0 + , description: "Description Question 1 Form 1" + , question: "Question 1 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const inputObj2: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.MAXCHAR, arguments: ["10"] } + , { type: ValidationType.MINCHAR, arguments: ["2"] } + ] + , id: 3 + }; + + const inputObj3: InputOptions = { + placement: 2 + , description: "Description Question 3 Form 1" + , question: "Question 3 Form 1" + , type: InputType.TEXT + , validation: [ + { type: ValidationType.REGEX, arguments: ["\\d{5}-\\d{3}"] } + , { type: ValidationType.MANDATORY, arguments: [] } + ] + , id: 2 + }; + + const formObj: FormOptions = { + id: 1 + , title: "Form Title 1" + , description: "Form Description 1" + , inputs: [ + inputObj1 + , inputObj2 + , inputObj3 + ] + }; + + const dateTMP: Date = new Date(); + const formUpdateObj: any = { + id: 1 + , form: formObj + , updateDate: dateTMP + }; + + let formUpdateTmp: FormUpdate; + + try { + formUpdateTmp = new FormUpdate(OptHandler.formUpdate(formUpdateObj)); + } catch (e) { + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("inputUpdates").message); + expect(formUpdateTmp).to.be.a("undefined"); + } + }); + + it("should get error InputUpdate missing option key 'input'", () => { + const updateObj: any = { + id: 1 + , input: undefined + , inputOperation: UpdateType.REMOVE + , value: null + }; + + let updateTmp: InputUpdate; + + try { + updateTmp = new InputUpdate(OptHandler.inputUpdate(updateObj)); + } catch (e) { + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("input").message); + expect(updateTmp).to.be.a("undefined"); + } + }); + + it("should get error InputUpdate missing option key 'inputOperation'", () => { + + const inputObj: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const updateObj: any = { + id: 1 + , input: inputObj + , value: "Remove question 3 from form 1" + }; + + let updateTmp: InputUpdate; + + try { + updateTmp = new InputUpdate(OptHandler.inputUpdate(updateObj)); + } catch (e) { + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("inputOperation").message); + expect(updateTmp).to.be.a("undefined"); + } + }); + + it("should get error InputUpdate missing option key 'value'", () => { + + const inputObj: InputOptions = { + placement: 1 + , description: "Description Question 2 Form 1" + , question: "Question 2 Form 1" + , type: InputType.TEXT + , validation: [] + , id: 1 + }; + + const updateObj: any = { + id: 1 + , input: inputObj + , inputOperation: UpdateType.REMOVE + }; + + let updateTmp: InputUpdate; + + try { + updateTmp = new InputUpdate(OptHandler.inputUpdate(updateObj)); + } catch (e) { + expect(e).to.be.not.a("null"); + expect(e.message).to.be.equal(ErrorHandler.notFound("value").message); + expect(updateTmp).to.be.a("undefined"); + } + }); +}); diff --git a/src/utils/optHandler.ts b/src/utils/optHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..36e8d46e3086bc427dc27ecb2861d54bc77e8ec2 --- /dev/null +++ b/src/utils/optHandler.ts @@ -0,0 +1,208 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + + import { Form, FormOptions } from "../core/form"; + import { FormAnswerOptions } from "../core/formAnswer"; + import { FormUpdate, FormUpdateOptions } from "../core/formUpdate"; + import { InputOptions, Validation } from "../core/input"; + import { InputAnswer, InputAnswerDict, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; + import { InputUpdate, InputUpdateOptions } from "../core/inputUpdate"; + import { InputType, UpdateType} from "./enumHandler"; + import { ErrorHandler} from "./errorHandler"; +/** + * OptHandler to handle an object and transform into a Classoptions to be used in Class's constructor + */ + export class OptHandler { + /** + * Return an FormOptions instance with a parsed object, The main objective is parse any error previously + * @param obj - object that should be parsed. + * @returns - An FormOptions instance. + */ + public static form(obj: any): FormOptions{ + + if (obj.title === undefined ){ + throw ErrorHandler.notFound("Form title"); + } + if (obj.description === undefined){ + throw ErrorHandler.notFound("Form description"); + } + if (obj.inputs === undefined || !(obj.inputs instanceof Array)){ + throw ErrorHandler.notFound("Form inputs"); + } + const option: FormOptions = { + title: obj.title, + description: obj.description, + id: obj.id, + inputs: obj.inputs.map((i: any) => OptHandler.input(i)) + }; + + return option; + + } + + /** + * Return an InputOptions instance with a parsed object, The main objective is parse any error previously + * @param obj - object that should be parsed. + * @returns - An InputOptions instance. + */ + public static input(obj: any): InputOptions{ + if (obj.placement === undefined){ + throw ErrorHandler.notFound("Input placement"); + } + if (obj.description === undefined){ + throw ErrorHandler.notFound("Input description"); + } + if (obj.question === undefined){ + throw ErrorHandler.notFound("Input question"); + } + if (obj.type === undefined){ + throw ErrorHandler.notFound("Input type"); + } + if (obj.validation === undefined || !(obj.validation instanceof Array)){ + throw ErrorHandler.notFound("Input validation"); + } + + const option: InputOptions = { + id: obj.id, + placement: obj.placement, + description: obj.description, + question: obj.question, + enabled: obj.enabled, + type: obj.type, + validation: obj.validation.map((v: any) => { + return {type: v.type, arguments: v.arguments}; + }) + }; + + return option; + } + + /** + * Return an formAnswerOptions instance with a parsed and validated object, The main objective is parse any error previously + * @param obj - object that should be parsed. + * @returns - An FormAnswerOptions instance. + */ + public static formAnswer(obj: any): FormAnswerOptions{ + if (obj.form === undefined || !(obj.form instanceof Form)){ + throw ErrorHandler.notFound("Form"); + } + if (obj.timestamp === undefined || !(obj.timestamp instanceof Date)){ + throw ErrorHandler.notFound("Answer Date"); + } + + if (obj.inputsAnswerOptions === undefined ){ + throw ErrorHandler.notFound("Input Answers"); + } + + const inputsAnswerOptionsTmp: InputAnswerOptionsDict = {}; + for (const key of Object.keys(obj.inputsAnswerOptions)){ + inputsAnswerOptionsTmp[parseInt(key, 10)] = obj.inputsAnswerOptions[parseInt(key, 10)].map( (i: InputAnswerOptions) => { + return OptHandler.inputAnswer(i); + }); + + } + + const option: FormAnswerOptions = { + id: obj.id + , form: obj.form + , timestamp: obj.timestamp + , inputsAnswerOptions: inputsAnswerOptionsTmp + }; + + return option; + } + + /** + * Return an InputAnswerOptions instance with a parsed and validated object, The main objective is parse any error previously + * @param obj - object that should be parsed. + * @returns - An InputAnswerOptions instance. + */ + public static inputAnswer(obj: any): InputAnswerOptions{ + if (obj.idInput === undefined){ + throw ErrorHandler.notFound("idInput"); + } + if (obj.placement === undefined){ + throw ErrorHandler.notFound("Placement"); + } + if (obj.value === undefined){ + throw ErrorHandler.notFound("Value"); + } + + const option: InputAnswerOptions = { + id: obj.id + , idInput: obj.idInput + , placement: obj.placement + , value: obj.value + }; + + return option; + } + + /** + * Return an FormUpdateOptions instance with a parsed and validated object, The main objective is parse any error previously + * @param obj - object that should be parsed. + * @returns - An FormUpdateOptions instance. + */ + public static formUpdate(obj: any): FormUpdateOptions { + if (obj.form === undefined) { + throw ErrorHandler.notFound("form"); + } + if ((obj.inputUpdates === undefined) || !(obj.inputUpdates instanceof Array)) { + throw ErrorHandler.notFound("inputUpdates"); + } + + const option: FormUpdateOptions = { + id: obj.id + , form: OptHandler.form(obj.form) + , updateDate: obj.updateDate + , inputUpdates: obj.inputUpdates.map((i: any) => OptHandler.inputUpdate(i)) + }; + + return option; + } + + /** + * Return an FormUpdateOptions instance with a parsed and validated object, The main objective is to detect parsing errors previously + * @param obj - object that should be parsed. + * @returns - An FormUpdateOptions instance. + */ + public static inputUpdate(obj: any): InputUpdateOptions { + if (obj.input === undefined) { + throw ErrorHandler.notFound("input"); + } + if (obj.inputOperation === undefined) { + throw ErrorHandler.notFound("inputOperation"); + } + if (obj.value === undefined) { + throw ErrorHandler.notFound("value"); + } + + const option: InputUpdateOptions = { + id: obj.id + , input: OptHandler.input(obj.input) + , inputOperation: obj.inputOperation + , value: obj.value + }; + + return option; + } + +} diff --git a/src/utils/sorter.spec.ts b/src/utils/sorter.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8251e410087c2f63f682d6a6cc934f248d4bf66 --- /dev/null +++ b/src/utils/sorter.spec.ts @@ -0,0 +1,62 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; +import { Sorter } from "./sorter"; + +describe("Sorter", () => { + + it("should sort objects with placement option", (done) => { + + const reqArray: any[] = [ + { placement: 5 } + , { placemente: 3 } + , { placemente: 1 } + , { placemente: 2 } + , { placemente: 1 } + , { placemente: 0 } + , { placemente: 4 } + , { placemente: 4 } + , { placemente: 3 } + , { placemente: 0 } + ]; + + const resArray: any[] = Sorter.sortByPlacement(reqArray); + + const expArray: any[] = [ + { placement: 0 } + , { placement: 0 } + , { placement: 1 } + , { placement: 1 } + , { placement: 2 } + , { placement: 3 } + , { placement: 3 } + , { placement: 4 } + , { placement: 4 } + , { placement: 5 } + ]; + + for (let i = 0; i < resArray.length; i++) { + expect(resArray[i].placement).to.be.equal(reqArray[i].placement); + } + done(); + }); +}); diff --git a/src/utils/sorter.ts b/src/utils/sorter.ts new file mode 100644 index 0000000000000000000000000000000000000000..41414791a6318fd9066802c41d7d43ef6559ed10 --- /dev/null +++ b/src/utils/sorter.ts @@ -0,0 +1,67 @@ +/* + * 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/>. + */ + +export class Sorter { + + /** + * A public method to return a array sorted by placement field + * @param array - Array with objects that have placement field + * @returns - A sorted array by placement + */ + public static sortByPlacement(array: any[]): any[] { + + const sortedByPlacementArray: any[] = array.sort((obj1, obj2) => { + if (obj1["placement"] > obj2["placement"]) { + return 1; + } + + if (obj1["placement"] < obj2["placement"]) { + return -1; + } + + return 0; + }); + + return sortedByPlacementArray; + } + + /** + * A public method to return a array sorted by id field + * @param array - Array with objects that have id field + * @returns - A sorted array by id + */ + public static sortById(array: any[]): any[] { + + const sortedByIdArray: any[] = array.sort((obj1, obj2) => { + if (obj1["id"] > obj2["id"]) { + return 1; + } + + if (obj1["id"] < obj2["id"]) { + return -1; + } + + return 0; + }); + + return sortedByIdArray; + } +} diff --git a/src/utils/testHandler.spec.ts b/src/utils/testHandler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e70e86c32c425444b514a039a87452919a093d21 --- /dev/null +++ b/src/utils/testHandler.spec.ts @@ -0,0 +1,20 @@ +/* + * 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/>. + */ diff --git a/src/utils/testHandler.ts b/src/utils/testHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9cf76057d51f6a8c848fcd18f3c70b2ce8f6b74 --- /dev/null +++ b/src/utils/testHandler.ts @@ -0,0 +1,137 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; +import { Form } from "../core/form"; +import { FormAnswer } from "../core/formAnswer"; +import { FormUpdate } from "../core/formUpdate"; +import { Input, Validation } from "../core/input"; +import { InputAnswer } from "../core/inputAnswer"; +import { InputUpdate } from "../core/inputUpdate"; +import { EnumHandler, InputType, UpdateType, ValidationType } from "./enumHandler"; +import { Sorter } from "./sorter"; + +/** + * Test's handler. Manage tests through the project. + */ +export class TestHandler { + + /** + * Verify if two forms are semantically equal; + * @param form - Form that should be tested + * @param stub - A model form that first param itend to be. + * @returns - True if forms are equal else false + */ + public static testForm(form: Form, stub: Form){ + expect(form.id).to.be.equal(stub.id); + expect(form.title).to.be.equal(stub.title); + expect(form.description).to.be.equal(stub.description); + expect(form.inputs.length).to.be.equal(stub.inputs.length); + for (let i = 0; i < form.inputs.length ; i++){ + TestHandler.testInput(form.inputs[i], stub.inputs[i]); + } + + } + + /** + * Verify if two inputs are semantically equal; + * @param input - Input that should be tested + * @param stub - A model input that first param itend to be. + * @returns - True if inputs are equal else false + */ + public static testInput(input: Input, stub: Input){ + expect(input.id).to.be.equal(stub.id); + expect(input.placement).to.be.equal(stub.placement); + expect(input.description).to.be.equal(stub.description); + expect(input.question).to.be.equal(stub.question); + expect(input.type).to.be.equal(stub.type); + expect(input.validation.length).to.be.equal(stub.validation.length); + for (let i = 0; i < input.validation.length ; i++){ + expect(input.validation[i].type).to.be.equal(stub.validation[i].type); + expect(input.validation[i].arguments.length).to.be.equal(stub.validation[i].arguments.length); + for (let j = 0; j < input.validation[i].arguments.length ; j++){ + expect(input.validation[i].arguments[j]).to.be.equal(stub.validation[i].arguments[j]); + } + } + } + + /** + * 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); + } + + /** + * Verify if two FormUpdate are semantically equal; + * @param formUpdate - FormUpdate that should be tested + * @param stub - A model FormUpdate that first param itend to be. + * @returns - True if FormUpdate are equal else false + */ + public static testFormUpdate(formUpdate: FormUpdate, stub: FormUpdate) { + const sortedFormInputs: InputUpdate[] = Sorter.sortById(formUpdate.inputUpdates); + const sortedStubInputs: InputUpdate[] = Sorter.sortById(stub.inputUpdates); + + expect(formUpdate.id).to.be.equal(stub.id); + TestHandler.testForm(formUpdate.form, stub.form); + for (let i = 0; i < formUpdate.inputUpdates.length; i++) { + TestHandler.testInputUpdate(sortedFormInputs[i], sortedStubInputs[i]); + } + } + + /** + * Verify if two InputUpdate are semantically equal; + * @param inputUpdate - InputUpdate that should be tested + * @param stub - A model InputUpdate that first param itend to be. + * @returns - True if InputUpdate are equal else false + */ + public static testInputUpdate(inputUpdate: InputUpdate, stub: InputUpdate) { + expect(inputUpdate.id).to.be.equal(stub.id); + TestHandler.testInput(inputUpdate.input, stub.input); + expect(inputUpdate.inputOperation).to.be.equal(stub.inputOperation); + expect(inputUpdate.value).to.be.equal(stub.value); + } +} diff --git a/src/utils/validationError.spec.ts b/src/utils/validationError.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e70e86c32c425444b514a039a87452919a093d21 --- /dev/null +++ b/src/utils/validationError.spec.ts @@ -0,0 +1,20 @@ +/* + * 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/>. + */ diff --git a/src/core/collection.ts b/src/utils/validationError.ts similarity index 52% rename from src/core/collection.ts rename to src/utils/validationError.ts index ad7ebc2d83e3e69131aba9a7ddcd8f0dd9b09163..f440f973c472edac87d2f20e45aca7a52ea8d53e 100644 --- a/src/core/collection.ts +++ b/src/utils/validationError.ts @@ -19,49 +19,24 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import { Item } from "./item"; - -export class Collection { - private items: Item[]; - - constructor() { - this.items = []; - } - - public addItem(i: Item): number { - this.items.push(i); - return this.items.length - 1; - } - - public updateItem(id: number, it: Item): boolean { - if (this.items[id]) { - this.items[id] = it; - return true; - } - - else { - return false; - } - } - - public deleteItem(id: number): boolean { - if (this.items[id]) { - this.items[id] = null; - return true; - } - - else { - return false; - } - } - - public getItem(id: number): Item { - if (this.items[id]) { - return this.items[id]; - } - - else { - return null; - } - } -} + /** + * Parameters used to create a dictionary to uses as an object of erro, returning all validations erros just once + */ + export interface ValidationDict { + /** A key is a identifier of a Input instance, and the string is all invalid messages */ + [key: number]: string; + } + + /** + * ValidationError: Extends Error class + * Has a dict that allow us to know which answer is invalide + */ + export class ValidationError extends Error{ + /** A dict that allows user to know which Input Answer is invalid. */ + public readonly validationDict: ValidationDict; + + constructor(validationDict: ValidationDict, ...params: any[]){ + super(...params); + this.validationDict = validationDict; + } + } diff --git a/src/utils/validationHandler.spec.ts b/src/utils/validationHandler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d526f647145ab5483979a418195556e42a3bbfda --- /dev/null +++ b/src/utils/validationHandler.spec.ts @@ -0,0 +1,187 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; +import { Form } from "../core/form"; +import { FormAnswer, FormAnswerOptions } from "../core/formAnswer"; +import { InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer"; +import { configs } from "./config"; +import { DbHandler } from "./dbHandler"; +import { EnumHandler, InputType, ValidationType } from "./enumHandler"; +import { OptHandler } from "./optHandler"; +import { ValidationHandler } from "./validationHandler"; + +describe("Validation Handler", () => { + const dbhandler = new DbHandler(configs.poolconfig); + + it("should test when Input has a minimum char number", (done) => { + const inputAnswersOpt1: InputAnswerOptions = { + idInput: 4 + , placement: 0 + , value: "MIN 8" + }; + const inputAnswersOpt2: InputAnswerOptions = { + idInput: 5 + , placement: 0 + , value: "Answer to Question 2 Form 2" + }; + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 4: [inputAnswersOpt1] + , 5: [inputAnswersOpt2] + }; + + const data: Date = new Date(2019, 6, 4); + dbhandler.readForm(2, (error: Error, form: Form) => { + const formAnswerOptions: FormAnswerOptions = { + form + , timestamp: data + , inputsAnswerOptions: inputAnswerOptionsDict + }; + const formAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + try { + ValidationHandler.validateFormAnswer(formAnswer); + } catch (e) { + expect(e.validationDict["4"]).to.be.equal("Input answer must be greater than 8"); + expect(e.validationDict["5"]).to.be.a("undefined"); + } + done(); + }); + }); + + it("should test when Input is mandatory", (done) => { + const inputAnswersOpt1: InputAnswerOptions = { + idInput: 4 + , placement: 0 + , value: "Answer to Question 1 Form 2" + }; + const inputAnswersOpt2: InputAnswerOptions = { + idInput: 5 + , placement: 0 + , value: "" + }; + const inputAnswerOptionsDict: InputAnswerOptionsDict = { + 4: [inputAnswersOpt1] + , 5: [inputAnswersOpt2] + }; + + const data: Date = new Date(2019, 6, 4); + dbhandler.readForm(2, (error: Error, form: Form) => { + const formAnswerOptions: FormAnswerOptions = { + form + , timestamp: data + , inputsAnswerOptions: inputAnswerOptionsDict + }; + const formAnswer = new FormAnswer(OptHandler.formAnswer(formAnswerOptions)); + try { + ValidationHandler.validateFormAnswer(formAnswer); + } catch (e) { + expect(e.validationDict["4"]).to.be.a("undefined"); + expect(e.validationDict["5"]).to.be.equal("Input answer is mandatory"); + } + done(); + }); + }); + + it("should test when Input has a maximum char number", (done) => { + + const inputAnswersOpt1: InputAnswerOptions = { + idInput: 1 + , placement: 0 + , value: "Answer to Question 1 Form 1" + }; + const inputAnswersOpt2: InputAnswerOptions = { + idInput: 2 + , placement: 0 + , value: "12345-000" + }; + const inputAnswersOpt3: InputAnswerOptions = { + idInput: 3 + , placement: 0 + , value: "MORE THEN 10 CHAR" + }; + 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)); + try { + ValidationHandler.validateFormAnswer(formAnswer); + } catch (e) { + expect(e.validationDict["1"]).to.be.a("undefined"); + expect(e.validationDict["2"]).to.be.a("undefined"); + expect(e.validationDict["3"]).to.be.equal("Input answer must be lower than 10"); + } + done(); + }); + + }); + + it("should test when Input has a RegEx", (done) => { + + const inputAnswersOpt1: InputAnswerOptions = { + idInput: 1 + , placement: 0 + , value: "Answer to Question 1 Form 1" + }; + const inputAnswersOpt2: InputAnswerOptions = { + idInput: 2 + , placement: 0 + , value: "12aa345-000" + }; + const inputAnswersOpt3: InputAnswerOptions = { + idInput: 3 + , placement: 0 + , value: "MAXCHAR 10" + }; + 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)); + try { + ValidationHandler.validateFormAnswer(formAnswer); + } catch (e) { + expect(e.validationDict["1"]).to.be.a("undefined"); + expect(e.validationDict["3"]).to.be.a("undefined"); + expect(e.validationDict["2"]).to.be.equal("RegEx do not match"); + } + done(); + }); + + }); + +}); diff --git a/src/utils/validationHandler.ts b/src/utils/validationHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..d04a2d4db0c3359b06fc90c7edb63fa3c4683ae6 --- /dev/null +++ b/src/utils/validationHandler.ts @@ -0,0 +1,126 @@ +/* + * form-creator-api. RESTful API to manage and answer forms. + * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR + * + * This file is part of form-creator-api. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import { FormAnswer } from "../core/formAnswer"; +import { Input } from "../core/input"; +import { ValidationType } from "./enumHandler"; +import { ValidationDict, ValidationError } from "./validationError"; + +/** + * Validation's handler. Manage parse validation through the project. + */ +export class ValidationHandler { + + /** + * Validate a string according given a regex. + * @param answer - Answer to be validated. + * @param regex - Regex to validate answer. + * @returns - true if answer match regex, else false. + */ + private static validateByRegex(answer: string, regex: string): boolean{ + const regexp = new RegExp(regex); + return regexp.test(answer); + } + + /** + * Validate if is null, undefined nor "" + * @param answer - answer to be validated. + * @returns - true if not null, "" nor undefined, else false. + */ + private static validateMandatory(answer: string): boolean{ + return ((!answer) === false); + } + + /** + * Validate if answer has minimum number of chars + * @param answer - Answer to be validated. + * @param size - Minimum size that answer should have. + * @returns - true if has at least Size chars, else false. + */ + private static validateMinChar(answer: string, size: string): boolean{ + return (answer !== null && answer !== undefined && parseInt(size, 10) <= answer.length); + } + + /** + * Validate if answer has minimum number of chars + * @param answer - Answer to be validated. + * @param size - Maximum size that answer should have. + * @returns - true if has at max Size chars, else false. + */ + private static validateMaxChar(answer: string, size: string): boolean{ + return (answer !== null && answer !== undefined && parseInt(size, 10) >= answer.length); + } + + /** + * Validate if answer has minimum number of chars + * @param input - Input to validate answer. + * @param answer - Answer of input + * @returns - A string with all errors. + */ + private static validateInput(input: Input, answer: string): string{ + const errors: string[] = []; + for ( const validation of input.validation){ + + switch (validation.type) { + case ValidationType.REGEX: + if (!this.validateByRegex(answer, validation.arguments[0])){ + errors.push("RegEx do not match"); + } + break; + case ValidationType.MANDATORY: + if (!(this.validateMandatory(answer))){ + errors.push("Input answer is mandatory"); + } + break; + case ValidationType.MAXCHAR: + if (!(this.validateMaxChar(answer, validation.arguments[0]))){ + errors.push("Input answer must be lower than " + validation.arguments[0]); + } + break; + case ValidationType.MINCHAR: + if (!(this.validateMinChar(answer, validation.arguments[0]))){ + errors.push("Input answer must be greater than " + validation.arguments[0]); + } + break; + } + + } + return errors.join(";"); + + } + + public static validateFormAnswer(formAnswer: FormAnswer): void{ + const errorsDict: ValidationDict = {}; + + for ( const input of formAnswer.form.inputs){ + for (const answer of formAnswer.inputAnswers[input.id]){ + const error: string = this.validateInput(input, answer.value); + if (error !== "" && error !== undefined){ + errorsDict[input.id] = error; + } + } + } + + if ( Object.keys(errorsDict).length > 0){ + throw new ValidationError(errorsDict, "Validation Error"); + } + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..769e026da49d12db58011eef969866ed49de96fd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1652 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/async@^2.0.50": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/async/-/async-2.4.1.tgz#43c3b2c60eab41c25ca0009c07ca7d619d943119" + integrity sha512-C09BK/wXzbW+/JK9zckhe+FeSbg7NmvVjUWwApnw7ksRpUq3ecGLiq2Aw1LlY4Z/VmtdhSaIs7jO5/MWRYMcOA== + +"@types/body-parser@*": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" + integrity sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/chai@^4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a" + integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== + +"@types/connect@*": + version "3.4.32" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" + integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg== + dependencies: + "@types/node" "*" + +"@types/cookiejar@*": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" + integrity sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw== + +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/express-serve-static-core@*": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.1.tgz#35df7b302299a4ab138a643617bd44078e74d44e" + integrity sha512-QgbIMRU1EVRry5cIu1ORCQP4flSYqLM1lS5LYyGWfKnFT3E58f0gKto7BR13clBFVrVZ0G0rbLZ1hUpSkgQQOA== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express@^4.16.0": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.1.tgz#d756bd1a85c34d87eaf44c888bad27ba8a4b7cf0" + integrity sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/fs-extra@^5.0.3": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.5.tgz#080d90a792f3fa2c5559eb44bd8ef840aae9104b" + integrity sha512-w7iqhDH9mN8eLClQOYTkhdYUOSpp25eXxfc6VbFOGtzxW34JcvctH2bKjj4jD4++z4R5iO5D+pg48W2e03I65A== + dependencies: + "@types/node" "*" + +"@types/glob@*": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/handlebars@^4.0.38": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.1.0.tgz#3fcce9bf88f85fe73dc932240ab3fb682c624850" + integrity sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA== + dependencies: + handlebars "*" + +"@types/highlight.js@^9.12.3": + version "9.12.3" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.3.tgz#b672cfaac25cbbc634a0fd92c515f66faa18dbca" + integrity sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ== + +"@types/lodash@^4.14.110": + version "4.14.122" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.122.tgz#3e31394c38cf1e5949fb54c1192cbc406f152c6c" + integrity sha512-9IdED8wU93ty8gP06ninox+42SBSJHp2IAamsSYMUY76mshRTeUsid/gtbl8ovnOwy8im41ib4cxTiIYMXGKew== + +"@types/marked@^0.4.0": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.4.2.tgz#64a89e53ea37f61cc0f3ee1732c555c2dbf6452f" + integrity sha512-cDB930/7MbzaGF6U3IwSQp6XBru8xWajF5PV2YZZeV8DyiliTuld11afVztGI9+yJZ29il5E+NpGA6ooV/Cjkg== + +"@types/mime@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" + integrity sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA== + +"@types/minimatch@*", "@types/minimatch@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/mocha@^5.2.5": + version "5.2.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.6.tgz#b8622d50557dd155e9f2f634b7d68fd38de5e94b" + integrity sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw== + +"@types/node@*": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.0.tgz#070e9ce7c90e727aca0e0c14e470f9a93ffe9390" + integrity sha512-D5Rt+HXgEywr3RQJcGlZUCTCx1qVbCZpVk3/tOOA6spLNZdGm8BU+zRgdRYDoF1pO3RuXLxADzMrF903JlQXqg== + +"@types/pg-types@*": + version "1.11.4" + resolved "https://registry.yarnpkg.com/@types/pg-types/-/pg-types-1.11.4.tgz#8d7c59fb509ce3dca3f8bae589252051c639a9a8" + integrity sha512-WdIiQmE347LGc1Vq3Ki8sk3iyCuLgnccqVzgxek6gEHp2H0p3MQ3jniIHt+bRODXKju4kNQ+mp53lmP5+/9moQ== + dependencies: + moment ">=2.14.0" + +"@types/pg@^7.4.13": + version "7.4.13" + resolved "https://registry.yarnpkg.com/@types/pg/-/pg-7.4.13.tgz#4630494096bceee5ee562339502e3448c09703a8" + integrity sha512-jk/hFADnywyjnWXZ6IeNsAk2lZtKDInZWGs1ASxmLb3QBpwLDGcSvH5s6IIRlPqsXjwzu8btnNbQ5Cx+AX/CjA== + dependencies: + "@types/node" "*" + "@types/pg-types" "*" + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48" + integrity sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/shelljs@^0.8.0": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.3.tgz#f713f312dbae49ab5025290007e71ea32998e9a9" + integrity sha512-miY41hqc5SkRlsZDod3heDa4OS9xv8G77EMBQuSpqq86HBn66l7F+f8y9YKm+1PIuwC8QEZVwN8YxOOG7Y67fA== + dependencies: + "@types/glob" "*" + "@types/node" "*" + +"@types/superagent@*": + version "3.8.6" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.8.6.tgz#3676d8920d8979ea4ca57513f27995064f92dc43" + integrity sha512-YQjdsk27MLb6uyXjjywGyYeuqavwV3CirHt6btBz00HkKJyowdB8gjjB1zIZxrOybDRqO8FLjTZeEtmtC2hqxA== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.7.tgz#46ff6508075cd4519736be060f0d6331a5c8ca7b" + integrity sha512-GibTh4OTkal71btYe2fpZP/rVHIPnnUsYphEaoywVHo+mo2a/LhlOFkIm5wdN0H0DA0Hx8x+tKgCYMD9elHu5w== + dependencies: + "@types/superagent" "*" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@1.0.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= + +accepts@~1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" + integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= + dependencies: + mime-types "~2.1.18" + negotiator "0.6.1" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +append-transform@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" + integrity sha1-126/jKlNJ24keja61EpLdKthGZE= + dependencies: + default-require-extensions "^1.0.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +async@1.x: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + +async@^2.1.4, async@^2.5.0, async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" + integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg== + dependencies: + lodash "^4.17.11" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-generator@^6.18.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= + dependencies: + babel-runtime "^6.22.0" + +babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.16.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.18.0, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.18.0, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-from@^1.0.0, buffer-from@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +chai@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + pathval "^1.1.0" + type-detect "^4.0.5" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +combined-stream@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + dependencies: + delayed-stream "~1.0.0" + +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== + +commander@^2.12.1: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +commander@~2.17.1: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== + +component-emitter@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookiejar@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + +core-js@^2.4.0: + version "2.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" + integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +debug@2.6.9, debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + dependencies: + type-detect "^4.0.0" + +default-require-extensions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" + integrity sha1-836hXT4T/9m0N9M+GnW1+5eHTLg= + dependencies: + strip-bom "^2.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= + dependencies: + repeating "^2.0.0" + +diff@3.5.0, diff@^3.1.0, diff@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.16.4: + version "4.16.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.3" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.4" + qs "6.5.2" + range-parser "~1.2.0" + safe-buffer "5.1.2" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +fileset@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" + integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA= + dependencies: + glob "^7.0.3" + minimatch "^3.0.3" + +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + +form-data@^2.3.1: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +formidable@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" + integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg== + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs-extra@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + +glob@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +handlebars@*, handlebars@^4.0.3, handlebars@^4.0.6: + version "4.1.0" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a" + integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w== + dependencies: + async "^2.5.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= + +highlight.js@^9.13.1: + version "9.15.6" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.6.tgz#72d4d8d779ec066af9a17cb14360c3def0aa57c4" + integrity sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ== + +http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +interpret@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" + integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== + +invariant@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +ipaddr.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" + integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= + dependencies: + number-is-nan "^1.0.0" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +istanbul-api@^1.1.0-alpha: + version "1.3.7" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.7.tgz#a86c770d2b03e11e3f778cd7aedd82d2722092aa" + integrity sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA== + dependencies: + async "^2.1.4" + fileset "^2.0.2" + istanbul-lib-coverage "^1.2.1" + istanbul-lib-hook "^1.2.2" + istanbul-lib-instrument "^1.10.2" + istanbul-lib-report "^1.1.5" + istanbul-lib-source-maps "^1.2.6" + istanbul-reports "^1.5.1" + js-yaml "^3.7.0" + mkdirp "^0.5.1" + once "^1.4.0" + +istanbul-lib-coverage@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" + integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== + +istanbul-lib-hook@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz#bc6bf07f12a641fbf1c85391d0daa8f0aea6bf86" + integrity sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw== + dependencies: + append-transform "^0.4.0" + +istanbul-lib-instrument@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" + integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.18.0" + istanbul-lib-coverage "^1.2.1" + semver "^5.3.0" + +istanbul-lib-report@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz#f2a657fc6282f96170aaf281eb30a458f7f4170c" + integrity sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw== + dependencies: + istanbul-lib-coverage "^1.2.1" + mkdirp "^0.5.1" + path-parse "^1.0.5" + supports-color "^3.1.2" + +istanbul-lib-source-maps@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f" + integrity sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg== + dependencies: + debug "^3.1.0" + istanbul-lib-coverage "^1.2.1" + mkdirp "^0.5.1" + rimraf "^2.6.1" + source-map "^0.5.3" + +istanbul-reports@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.5.1.tgz#97e4dbf3b515e8c484caea15d6524eebd3ff4e1a" + integrity sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw== + dependencies: + handlebars "^4.0.3" + +istanbul@1.1.0-alpha.1: + version "1.1.0-alpha.1" + resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-1.1.0-alpha.1.tgz#781795656018a2174c5f60f367ee5d361cb57b77" + integrity sha1-eBeVZWAYohdMX2DzZ+5dNhy1e3c= + dependencies: + abbrev "1.0.x" + async "1.x" + istanbul-api "^1.1.0-alpha" + js-yaml "3.x" + mkdirp "0.5.x" + nopt "3.x" + which "^1.1.1" + wordwrap "^1.0.0" + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= + +js-yaml@3.x, js-yaml@^3.7.0: + version "3.12.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc" + integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + +marked@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.4.0.tgz#9ad2c2a7a1791f10a852e0112f77b571dce10c66" + integrity sha512-tMsdNBgOsrUophCAFQl0XPe6Zqk/uy9gnue+jIIKhykO51hxyu6uNx7zBPy0+y/WKYVZZMspV9YeXLNdKk+iYw== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@~1.37.0: + version "1.37.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" + integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== + +mime-types@^2.1.12, mime-types@~2.1.18: + version "2.1.21" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" + integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== + dependencies: + mime-db "~1.37.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== + +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +moment@>=2.14.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= + +nopt@3.x: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= + dependencies: + abbrev "1" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + +parseurl@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-parse@^1.0.5, path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +pathval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= + +pg-connection-string@0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" + integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc= + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-2.0.6.tgz#7b561a482feb0a0e599b58b5137fd2db3ad8111c" + integrity sha512-hod2zYQxM8Gt482q+qONGTYcg/qVcV32VHVPtktbBJs0us3Dj7xibISw0BAAXVMCzt8A/jhfJvpZaxUlqtqs0g== + +pg-types@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.0.0.tgz#038ddc302a0340efcdb46d0581cc7caa2303cbba" + integrity sha512-THUD7gQll5tys+5eQ8Rvs7DjHiIC3bLqixk3gMN9Hu8UrCBAOjf35FoI39rTGGc3lM2HU/R+Knpxvd11mCwOMA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.0" + postgres-interval "^1.1.0" + +pg@^7.8.1: + version "7.8.2" + resolved "https://registry.yarnpkg.com/pg/-/pg-7.8.2.tgz#d53ffcbbaa789e15e80ffec570603294d90116e8" + integrity sha512-5U4fjV43DnQxelkhyPdU3YfUbYVa21bNmreXRCM/gFFw09YxWaitWWITm/u0twUNF5EYOSDhkgyEAocgtpP9JQ== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "0.1.3" + pg-pool "^2.0.4" + pg-types "~2.0.0" + pgpass "1.x" + semver "4.3.2" + +pgpass@1.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" + integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY= + dependencies: + split "^1.0.0" + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= + +postgres-date@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.3.tgz#e2d89702efdb258ff9d9cee0fe91bd06975257a8" + integrity sha1-4tiXAu/bJY/52c7g/pG9BpdSV6g= + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-addr@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" + integrity sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.8.0" + +qs@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +qs@^6.5.1: + version "6.6.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.6.0.tgz#a99c0f69a8d26bf7ef012f871cdabb0aee4424c2" + integrity sha512-KIJqT9jQJDQx5h5uAVPimw6yVg2SekOKu959OCtktD3FjzbpvaPr8i4zzg07DOMz+igA4W/aNM7OV8H37pFYfA== + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= + +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +readable-stream@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +resolve@^1.1.6, resolve@^1.3.2: + version "1.10.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" + integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== + dependencies: + path-parse "^1.0.6" + +rimraf@^2.6.1: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" + integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= + +semver@^5.3.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== + +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +shelljs@^0.8.2: + version "0.8.3" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" + integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +source-map-support@^0.5.6: + version "0.5.10" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c" + integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.5.3, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +split@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +superagent@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + +supertest@^3.3.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.4.2.tgz#bad7de2e43d60d27c8caeb8ab34a67c8a5f71aad" + integrity sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA== + dependencies: + methods "^1.1.2" + superagent "^3.8.3" + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== + dependencies: + has-flag "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^3.1.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= + dependencies: + has-flag "^1.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +through@2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= + +ts-node@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" + integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + +tslib@^1.8.0, tslib@^1.8.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + +tslint@^5.13.1: + version "5.13.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.13.1.tgz#fbc0541c425647a33cd9108ce4fd4cd18d7904ed" + integrity sha512-fplQqb2miLbcPhyHoMV4FU9PtNRbgmm/zI5d3SZwwmJQM6V0eodju+hplpyfhLWpmwrDNfNYU57uYRb8s0zZoQ== + dependencies: + babel-code-frame "^6.22.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^3.2.0" + glob "^7.1.1" + js-yaml "^3.7.0" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.27.2" + +tsutils@^2.27.2: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-is@~1.6.16: + version "1.6.16" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" + integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.18" + +typedoc-default-themes@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.5.0.tgz#6dc2433e78ed8bea8e887a3acde2f31785bd6227" + integrity sha1-bcJDPnjti+qOiHo6zeLzF4W9Yic= + +typedoc@^0.14.1: + version "0.14.2" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.14.2.tgz#769f457f4f9e4bdb8b5f3b177c86b6a31d8c3dc3" + integrity sha512-aEbgJXV8/KqaVhcedT7xG6d2r+mOvB5ep3eIz1KuB5sc4fDYXcepEEMdU7XSqLFO5hVPu0nllHi1QxX2h/QlpQ== + dependencies: + "@types/fs-extra" "^5.0.3" + "@types/handlebars" "^4.0.38" + "@types/highlight.js" "^9.12.3" + "@types/lodash" "^4.14.110" + "@types/marked" "^0.4.0" + "@types/minimatch" "3.0.3" + "@types/shelljs" "^0.8.0" + fs-extra "^7.0.0" + handlebars "^4.0.6" + highlight.js "^9.13.1" + lodash "^4.17.10" + marked "^0.4.0" + minimatch "^3.0.0" + progress "^2.0.0" + shelljs "^0.8.2" + typedoc-default-themes "^0.5.0" + typescript "3.2.x" + +typescript@3.2.x: + version "3.2.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d" + integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg== + +typescript@^3.2.2: + version "3.3.3333" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6" + integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw== + +uglify-js@^3.1.4: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +which@^1.1.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= + +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" + integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=