diff --git a/package-lock.json b/package-lock.json index 107e5ed..95dbfce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,10 @@ "axios": "^0.21.1", "dotenv": "^10.0.0", "express": "^4.17.1", + "fastest-validator": "^1.11.1", "firebase-admin": "^9.11.0" }, "devDependencies": { - "@types/convict": "^6.1.1", "@types/express": "^4.17.13", "@types/jest": "^26.0.24", "@types/node": "^16.3.3", @@ -1481,15 +1481,6 @@ "@types/node": "*" } }, - "node_modules/@types/convict": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.1.tgz", - "integrity": "sha512-R+JLaTvhsD06p4jyjUDtbd5xMtZTRE3c0iI+lrFWZogSVEjgTWPYwvJPVf+t92E+yrlbXa4X4Eg9ro6gPdUt4w==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -3613,6 +3604,11 @@ "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", "optional": true }, + "node_modules/fastest-validator": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/fastest-validator/-/fastest-validator-1.11.1.tgz", + "integrity": "sha512-y0pXBCgGfY3mqbBL9sn2LtAxfJICyOr5cuOnCjyiKg4VoXVmDaLBDwnnj4QC1pSY5B4upln5k8RyLszlLXzXlw==" + }, "node_modules/fastq": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz", @@ -9460,15 +9456,6 @@ "@types/node": "*" } }, - "@types/convict": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.1.tgz", - "integrity": "sha512-R+JLaTvhsD06p4jyjUDtbd5xMtZTRE3c0iI+lrFWZogSVEjgTWPYwvJPVf+t92E+yrlbXa4X4Eg9ro6gPdUt4w==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -11129,6 +11116,11 @@ "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", "optional": true }, + "fastest-validator": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/fastest-validator/-/fastest-validator-1.11.1.tgz", + "integrity": "sha512-y0pXBCgGfY3mqbBL9sn2LtAxfJICyOr5cuOnCjyiKg4VoXVmDaLBDwnnj4QC1pSY5B4upln5k8RyLszlLXzXlw==" + }, "fastq": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz", diff --git a/package.json b/package.json index e3ff1c8..07f4d39 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "author": "Victor Martinez", "license": "MIT", "devDependencies": { - "@types/convict": "^6.1.1", "@types/express": "^4.17.13", "@types/jest": "^26.0.24", "@types/node": "^16.3.3", @@ -41,6 +40,7 @@ "axios": "^0.21.1", "dotenv": "^10.0.0", "express": "^4.17.1", + "fastest-validator": "^1.11.1", "firebase-admin": "^9.11.0" } } diff --git a/src/api/controllers/createSentenceController.ts b/src/api/controllers/createSentenceController.ts index efd9825..7427a4d 100644 --- a/src/api/controllers/createSentenceController.ts +++ b/src/api/controllers/createSentenceController.ts @@ -1,7 +1,18 @@ import { Request, Response, NextFunction } from 'express' +import { CrudModel } from '../../interfaces/crudModel' +import { Sentence } from '../../interfaces/sentence' -export const createSentenceController = () => { - return (_req: Request, _res: Response, _next: NextFunction) => { - +export const createSentenceController = (model: CrudModel) => { + return async (req: Request, res: Response, next: NextFunction) => { + const { text, category } = req.body + try { + const id = await model.create({ text, category }) + return res.status(201).json({ + success: true, + data: id + }) + } catch (err) { + return next(err) + } } } \ No newline at end of file diff --git a/src/api/controllers/deleteSentenceController.ts b/src/api/controllers/deleteSentenceController.ts index 32f877e..a4e22ab 100644 --- a/src/api/controllers/deleteSentenceController.ts +++ b/src/api/controllers/deleteSentenceController.ts @@ -1,7 +1,25 @@ import { Request, Response, NextFunction } from 'express' +import { NotFoundError } from '../../errors' +import { CrudModel } from '../../interfaces/crudModel' +import { Sentence } from '../../interfaces/sentence' -export const deleteSentenceController = () => { - return (_req: Request, _res: Response, _next: NextFunction) => { - +export const deleteSentenceController = (model: CrudModel) => { + return async (req: Request, res: Response, next: NextFunction) => { + const { id } = req.params + try { + const deletedId = await model.del(id) + return res.status(201).json({ + success: true, + data: deletedId + }) + } catch (err) { + if(err instanceof NotFoundError) { + return res.status(404).json({ + success: false, + msg: err.message + }) + } + return next(err) + } } } \ No newline at end of file diff --git a/src/api/controllers/getSentenceController.ts b/src/api/controllers/getSentenceController.ts index 3031fff..b2b29d9 100644 --- a/src/api/controllers/getSentenceController.ts +++ b/src/api/controllers/getSentenceController.ts @@ -1,7 +1,25 @@ import { Request, Response, NextFunction } from 'express' +import { NotFoundError } from '../../errors' +import { CrudModel } from '../../interfaces/crudModel' +import { Sentence } from '../../interfaces/sentence' -export const getSentenceController = () => { - return (_req: Request, _res: Response, _next: NextFunction) => { - +export const getSentenceController = (model: CrudModel) => { + return async (req: Request, res: Response, next: NextFunction) => { + const { id } = req.params + try { + const sentence: Sentence = await model.getById(id) + return res.status(201).json({ + success: true, + data: sentence + }) + } catch (err) { + if(err instanceof NotFoundError) { + return res.status(404).json({ + success: false, + msg: err.message + }) + } + return next(err) + } } } \ No newline at end of file diff --git a/src/api/controllers/listSentencesController.ts b/src/api/controllers/listSentencesController.ts index b6c1825..a570d6d 100644 --- a/src/api/controllers/listSentencesController.ts +++ b/src/api/controllers/listSentencesController.ts @@ -1,7 +1,25 @@ import { Request, Response, NextFunction } from 'express' +import { NotFoundError } from '../../errors' +import { CrudModel } from '../../interfaces/crudModel' +import { Sentence } from '../../interfaces/sentence' -export const listSentencesController = () => { - return (_req: Request, _res: Response, _next: NextFunction) => { - +export const listSentencesController = (model: CrudModel) => { + return async (req: Request, res: Response, next: NextFunction) => { + const { orderBy, order, page } = req.query + try { + const sentences: Sentence[] = await model.list({ orderBy, order, page }) + return res.status(201).json({ + success: true, + data: sentences + }) + } catch (err) { + if (err instanceof NotFoundError) { + return res.status(404).json({ + success: false, + msg: err.message + }) + } + return next(err) + } } } \ No newline at end of file diff --git a/src/api/controllers/updateSentenceController.ts b/src/api/controllers/updateSentenceController.ts index d4d7df6..f471532 100644 --- a/src/api/controllers/updateSentenceController.ts +++ b/src/api/controllers/updateSentenceController.ts @@ -1,7 +1,26 @@ import { Request, Response, NextFunction } from 'express' +import { NotFoundError } from '../../errors' +import { CrudModel } from '../../interfaces/crudModel' +import { Sentence } from '../../interfaces/sentence' -export const updateSentenceController = () => { - return (_req: Request, _res: Response, _next: NextFunction) => { - +export const updateSentenceController = (model: CrudModel) => { + return async (req: Request, res: Response, next: NextFunction) => { + const { id } = req.params + const { text, category } = req.body + try { + const sentence: Sentence = await model.update(id, { text, category }) + return res.status(201).json({ + success: true, + data: sentence + }) + } catch (err) { + if (err instanceof NotFoundError) { + return res.status(404).json({ + success: false, + msg: err.message + }) + } + return next(err) + } } } \ No newline at end of file diff --git a/src/api/middlewares/bodySchemaValidator.ts b/src/api/middlewares/bodySchemaValidator.ts new file mode 100644 index 0000000..1214082 --- /dev/null +++ b/src/api/middlewares/bodySchemaValidator.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from "express" +import Validator from 'fastest-validator' + +export const bodySchemaValidator = (schema: any, validator: Validator) => { + return async (req: Request, res: Response, next: NextFunction) => { + const body = req.body + const valid = await validator.validate(body, schema) + if(valid !== true) { + return res.status(400).json({ + success: false, + errors: valid + }) + } + return next() + } +} \ No newline at end of file diff --git a/src/api/middlewares/querySchemaValidator.ts b/src/api/middlewares/querySchemaValidator.ts new file mode 100644 index 0000000..fe75929 --- /dev/null +++ b/src/api/middlewares/querySchemaValidator.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from "express" +import Validator from 'fastest-validator' + +export const querySchemaValidator = (schema: any, validator: Validator) => { + return async (req: Request, res: Response, next: NextFunction) => { + const query = req.query + const valid = await validator.validate(query, schema) + if(valid !== true) { + return res.status(400).json({ + success: false, + errors: valid + }) + } + return next() + } +} \ No newline at end of file diff --git a/src/api/router.ts b/src/api/router.ts index 43da30d..60387ef 100644 --- a/src/api/router.ts +++ b/src/api/router.ts @@ -1,19 +1,27 @@ +import { bodySchemaValidator } from './middlewares/bodySchemaValidator' +import { querySchemaValidator } from './middlewares/querySchemaValidator' +import { createSchema, listSchema, updateSchema } from './schemas' +import { sentenceModel } from '../models/sentenceModel' +import Validator from 'fastest-validator' import { Router } from 'express' import { - createSentenceController, - updateSentenceController, - deleteSentenceController, - listSentencesController, - getSentenceController + createSentenceController, + updateSentenceController, + deleteSentenceController, + listSentencesController, + getSentenceController } from './controllers' +import db from '../db' const router = Router() +const validator = new Validator() +const model = sentenceModel(db) -router.get('/list', listSentencesController()) -router.get('/:id', getSentenceController()) -router.post('/', createSentenceController()) -router.put('/:id', updateSentenceController()) -router.delete('/:id', deleteSentenceController()) +router.get('/list', querySchemaValidator(listSchema, validator), listSentencesController(model)) +router.get('/:id', getSentenceController(model)) +router.post('/', bodySchemaValidator(createSchema, validator), createSentenceController(model)) +router.put('/:id', bodySchemaValidator(updateSchema, validator), updateSentenceController(model)) +router.delete('/:id', deleteSentenceController(model)) export default router diff --git a/src/api/schemas/createSchema.ts b/src/api/schemas/createSchema.ts new file mode 100644 index 0000000..214b468 --- /dev/null +++ b/src/api/schemas/createSchema.ts @@ -0,0 +1,6 @@ +import { ValidationSchema } from "fastest-validator"; + +export const createSchema: ValidationSchema = { + text: 'string', + category: 'string' +} \ No newline at end of file diff --git a/src/api/schemas/index.ts b/src/api/schemas/index.ts new file mode 100644 index 0000000..f78c854 --- /dev/null +++ b/src/api/schemas/index.ts @@ -0,0 +1,3 @@ +export * from './listSchema' +export * from './createSchema' +export * from './updateSchema' \ No newline at end of file diff --git a/src/api/schemas/listSchema.ts b/src/api/schemas/listSchema.ts new file mode 100644 index 0000000..9886c0e --- /dev/null +++ b/src/api/schemas/listSchema.ts @@ -0,0 +1,11 @@ +import { ValidationSchema } from "fastest-validator"; + +export const listSchema: ValidationSchema = { + orderBy: 'string', + order: { + optional: true, + type: 'equal', + values: ['asc', 'desc'] + }, + page: 'number|integer|positive|optional' +} \ No newline at end of file diff --git a/src/api/schemas/updateSchema.ts b/src/api/schemas/updateSchema.ts new file mode 100644 index 0000000..31ff835 --- /dev/null +++ b/src/api/schemas/updateSchema.ts @@ -0,0 +1,6 @@ +import { ValidationSchema } from "fastest-validator"; + +export const updateSchema: ValidationSchema = { + text: 'string|optional', + category: 'string|optional' +} \ No newline at end of file diff --git a/src/api/server.ts b/src/api/server.ts index eef915b..b5baa71 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -1,7 +1,7 @@ import express, { NextFunction, Request, Response } from 'express' -import config from 'src/config' -import { errorService } from 'src/services' import authMiddleware from './middlewares/authMiddleware' +import { errorService } from '../services/errorService' +import config from '../config' import router from './router' const { handle } = errorService() diff --git a/src/interfaces/crudModel.ts b/src/interfaces/crudModel.ts new file mode 100644 index 0000000..94a4acc --- /dev/null +++ b/src/interfaces/crudModel.ts @@ -0,0 +1,7 @@ +export interface CrudModel { + create(data: any): Promise; + getById(id: string): Promise; + list(options?: any): Promise; + del(id: string): Promise; + update(id: string, data: any): Promise; +} \ No newline at end of file diff --git a/src/interfaces/queryOptions.ts b/src/interfaces/queryOptions.ts index 4bbc29a..29b225d 100644 --- a/src/interfaces/queryOptions.ts +++ b/src/interfaces/queryOptions.ts @@ -7,4 +7,9 @@ export interface ListSentencesOptions { export interface UpdateSentenceOptions { text?: string, category?: string +} + +export interface CreateSentenceOptions { + text: string, + category: string } \ No newline at end of file diff --git a/src/models/sentenceModel.ts b/src/models/sentenceModel.ts index ab8e08d..e26bbe5 100644 --- a/src/models/sentenceModel.ts +++ b/src/models/sentenceModel.ts @@ -1,18 +1,24 @@ -import { NotFoundError } from "src/errors"; -import { ListSentencesOptions, UpdateSentenceOptions } from "src/interfaces/queryOptions" -import { Sentence } from "src/interfaces/sentence" +import { NotFoundError } from "../errors"; +import { CrudModel } from "../interfaces/crudModel"; +import { CreateSentenceOptions, ListSentencesOptions, UpdateSentenceOptions } from "../interfaces/queryOptions" +import { Sentence } from "../interfaces/sentence" -export const sentenceModel = (db: DB) => { +export const sentenceModel = (db: DB): CrudModel => { const PAGE_LIMIT = 20 - async function getSentence(id: string): Promise { + async function create(data: CreateSentenceOptions): Promise { + const sentenceRaw = await db.collection('sentence').add(data); + return sentenceRaw.id + } + + async function getById(id: string): Promise { const sentenceRaw = await db.collection('sentence').doc(id).get(); if (!sentenceRaw.exists) throw new NotFoundError(`Sentence with id ${id} not found`) const sentence: Sentence = { id, ...sentenceRaw.data() } as Sentence return sentence } - async function listSentence(options?: ListSentencesOptions): Promise { + async function list(options?: ListSentencesOptions): Promise { const { page = 0, orderBy = null, order = 'desc' } = options || {} const sentences: Sentence[] = [] let query = db.collection('sentences'); @@ -24,19 +30,19 @@ export const sentenceModel = (db: DB) => { return sentences } - async function deleteSentence(id: string): Promise { + async function del(id: string): Promise { const sentenceRaw = await db.collection('sentence').doc(id).get(); if (!sentenceRaw.exists) throw new NotFoundError(`Sentence with id ${id} not found`) await sentenceRaw.ref.delete(); return id } - async function updateSentence(id: string, data: UpdateSentenceOptions): Promise { + async function update(id: string, data: UpdateSentenceOptions): Promise { const sentenceRaw = await db.collection('sentence').doc(id).get(); if (!sentenceRaw.exists) throw new NotFoundError(`Sentence with id ${id} not found`) await sentenceRaw.ref.update(data); return { id, ...sentenceRaw.data() } as Sentence } - return { getSentence, listSentence, deleteSentence, updateSentence } + return { create, getById, list, update, del } } \ No newline at end of file diff --git a/src/services/createSentenceService.ts b/src/services/createSentenceService.ts deleted file mode 100644 index a9eec44..0000000 --- a/src/services/createSentenceService.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const createSentenceService = () => { - -} \ No newline at end of file diff --git a/src/services/deleteSentenceService.ts b/src/services/deleteSentenceService.ts deleted file mode 100644 index 105826d..0000000 --- a/src/services/deleteSentenceService.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const deleteSentenceService = () => { - -} \ No newline at end of file diff --git a/src/services/errorService.ts b/src/services/errorService.ts index 31607f7..c94063e 100644 --- a/src/services/errorService.ts +++ b/src/services/errorService.ts @@ -1,5 +1,5 @@ import { Response } from 'express' -import { ApiError, DatabaseError, RequestError } from 'src/errors' +import { ApiError, DatabaseError, RequestError } from '../errors' export const errorService = () => { diff --git a/src/services/getSentenceService.ts b/src/services/getSentenceService.ts deleted file mode 100644 index 2db007d..0000000 --- a/src/services/getSentenceService.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const getSentenceService = () => { - -} \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts deleted file mode 100644 index 13c5c78..0000000 --- a/src/services/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './createSentenceService' -export * from './deleteSentenceService' -export * from './listSentenceService' -export * from './updateSentenceService' -export * from './errorService' \ No newline at end of file diff --git a/src/services/listSentenceService.ts b/src/services/listSentenceService.ts deleted file mode 100644 index 2a8b8ce..0000000 --- a/src/services/listSentenceService.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const listSentenceService = () => { - -} \ No newline at end of file diff --git a/src/services/updateSentenceService.ts b/src/services/updateSentenceService.ts deleted file mode 100644 index b3b0966..0000000 --- a/src/services/updateSentenceService.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const updateSentenceService = () => { - -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f63f95e..62d6e39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,35 +1,22 @@ { "compilerOptions": { - "target": "es2020", - "module": "es2020", - "lib": [ - "es2020" - ], - "skipLibCheck": true, + "target": "esnext", + "module": "commonjs", + "declaration": true, + "declarationMap": true, "sourceMap": true, - "outDir": "./dist", - "moduleResolution": "node", + "outDir": "dist", "removeComments": true, - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "noImplicitThis": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "allowSyntheticDefaultImports": true, + "strict": true, "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "resolveJsonModule": true, - "baseUrl": "." + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true }, "exclude": [ "node_modules", "**/*.test.ts" ], "include": [ - "./src/**/*.ts", + "./src/**/*.ts" ] } \ No newline at end of file