diff --git a/src/controllers/PlantController.ts b/src/controllers/PlantController.ts index b17401a..a7ca89a 100644 --- a/src/controllers/PlantController.ts +++ b/src/controllers/PlantController.ts @@ -2,7 +2,11 @@ import { Request, Response, NextFunction } from 'express'; import PlantService from '../services/PlantService'; class PlantController { - public service: PlantService = new PlantService(); + private readonly service: PlantService; + + constructor(service: PlantService) { + this.service = service; + } public async getAll(_req: Request, res: Response, next: NextFunction): Promise { try { @@ -21,6 +25,36 @@ class PlantController { next(error); } } + + public async getById(req: Request, res: Response, next: NextFunction): Promise { + const { id } = req.params; + try { + const plant = await this.service.getById(id); + return res.status(200).json(plant); + } catch (error) { + next(error); + } + } + + public async remove(req: Request, res: Response, next: NextFunction): Promise { + const { id } = req.params; + try { + await this.service.removeById(id); + return res.status(204).end(); + } catch (error) { + next(error); + } + } + + public async update(req: Request, res: Response, next: NextFunction): Promise { + const { id } = req.params; + try { + const plant = await this.service.update(id, req.body); + return res.status(200).json(plant); + } catch (error) { + next(error); + } + } } export default PlantController; diff --git a/src/exceptions/BadRequest.ts b/src/exceptions/BadRequest.ts new file mode 100644 index 0000000..bcfd1fc --- /dev/null +++ b/src/exceptions/BadRequest.ts @@ -0,0 +1,9 @@ +import HttpException from './HttpException'; + +export default class BadRequestException extends HttpException { + private static status = 400; + + constructor(message?: string) { + super(BadRequestException.status, message || 'Bad request'); + } +} diff --git a/src/exceptions/HttpException.ts b/src/exceptions/HttpException.ts index a7a7414..5fb5efe 100644 --- a/src/exceptions/HttpException.ts +++ b/src/exceptions/HttpException.ts @@ -1,4 +1,4 @@ -class HttpException extends Error { +abstract class HttpException extends Error { status: number; constructor(status: number, message: string) { diff --git a/src/exceptions/NotFound.ts b/src/exceptions/NotFound.ts new file mode 100644 index 0000000..0104ddf --- /dev/null +++ b/src/exceptions/NotFound.ts @@ -0,0 +1,9 @@ +import HttpException from './HttpException'; + +export default class NotFoundException extends HttpException { + private static status = 404; + + constructor(message?: string) { + super(NotFoundException.status, message || 'Not Found'); + } +} diff --git a/src/exceptions/index.ts b/src/exceptions/index.ts new file mode 100644 index 0000000..8194be8 --- /dev/null +++ b/src/exceptions/index.ts @@ -0,0 +1,2 @@ +export { default as BadRequestException } from './BadRequest'; +export { default as NotFoundException } from './NotFound'; diff --git a/src/interfaces/IPlant.ts b/src/interfaces/IPlant.ts new file mode 100644 index 0000000..c0acb52 --- /dev/null +++ b/src/interfaces/IPlant.ts @@ -0,0 +1,10 @@ +export interface IPlant { + id: number, + breed: string, + needsSun: boolean, + origin: string, + size: number, + waterFrequency: number, +} + +export type INewPlant = Omit; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..5d7fd5c --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1 @@ +export { IPlant, INewPlant } from './IPlant'; diff --git a/src/models/HandleFile.ts b/src/models/HandleFile.ts new file mode 100644 index 0000000..3b1e4af --- /dev/null +++ b/src/models/HandleFile.ts @@ -0,0 +1,23 @@ +import fs from 'fs/promises'; +import path from 'path'; + +export type FileType = 'plants' | 'opsInfo'; + +const PATHS = { + plants: path.join(__dirname, 'database', 'plantsData.json'), + opsInfo: path.join(__dirname, 'database', 'opsInfo.json'), +}; + +export class HandleFile { + private PATHS = PATHS; + + public async saveFile(type: FileType, data: T): Promise { + await fs.writeFile(this.PATHS[`${type}`], JSON.stringify(data, null, 2)); + } + + public async readFile(type: FileType): Promise { + const dataRaw = await fs.readFile(this.PATHS[`${type}`], { encoding: 'utf8' }); + const data: T = JSON.parse(dataRaw); + return data; + } +} diff --git a/src/models/PlantModel.ts b/src/models/PlantModel.ts new file mode 100644 index 0000000..a5f93fd --- /dev/null +++ b/src/models/PlantModel.ts @@ -0,0 +1,72 @@ +import { HandleFile, FileType } from './HandleFile'; +import { IModel, IOpsInfo } from './interfaces'; +import { IPlant } from '../interfaces'; + +class PlantModel implements IModel { + private fileTypePlant: FileType = 'plants'; + + private fileTypeOpsInfo: FileType = 'opsInfo'; + + private handleFile = new HandleFile(); + + private async updateOpsInfo(incrementAmount = 1): Promise { + const opsInfo = await this.handleFile.readFile(this.fileTypeOpsInfo); + opsInfo.createdPlants += incrementAmount; + + await this.handleFile.saveFile(this.fileTypeOpsInfo, opsInfo); + + return opsInfo.createdPlants; + } + + public async getAll(): Promise { + const plants = await this.handleFile.readFile(this.fileTypePlant); + return plants; + } + + public async create(plant: Omit): Promise { + const plants = await this.getAll(); + + const newPlantId = await this.updateOpsInfo(1); + const newPlant = { id: newPlantId, ...plant }; + plants.push(newPlant); + + await this.handleFile.saveFile(this.fileTypePlant, plants); + + return newPlant; + } + + public async getById(id: string): Promise { + const plants = await this.getAll(); + + const plantById = plants.find((plant) => plant.id === parseInt(id, 10)); + if (!plantById) return null; + return plantById; + } + + public async removeById(id: string): Promise { + const plants = await this.getAll(); + + const removedPlant = plants.find((plant) => plant.id === parseInt(id, 10)); + if (!removedPlant) return false; + + const newPlants = plants.filter((plant) => plant.id !== parseInt(id, 10)); + this.handleFile.saveFile(this.fileTypePlant, newPlants); + + return true; + } + + public async update(plant: IPlant): Promise { + const plants = await this.getAll(); + + const updatedPlants = plants.map((editPlant) => { + if (plant.id === editPlant.id) return { ...plant }; + return editPlant; + }); + + await this.handleFile.saveFile(this.fileTypePlant, updatedPlants); + + return plant; + } +} + +export default PlantModel; diff --git a/src/models/interfaces/IModel.ts b/src/models/interfaces/IModel.ts new file mode 100644 index 0000000..3bc71d6 --- /dev/null +++ b/src/models/interfaces/IModel.ts @@ -0,0 +1,17 @@ +export interface IModelReader { + getAll(): Promise; + getById(id: string): Promise +} + +export interface IModelWriter { + create(arg: Omit): Promise + update(arg: T): Promise +} +export interface IModelDelete { + removeById(id: string): Promise +} + +export interface IModel extends + IModelReader, + IModelWriter, + IModelDelete {} diff --git a/src/models/interfaces/IOpsInfo.ts b/src/models/interfaces/IOpsInfo.ts new file mode 100644 index 0000000..c82f8e2 --- /dev/null +++ b/src/models/interfaces/IOpsInfo.ts @@ -0,0 +1,3 @@ +export default interface IOpsInfo { + createdPlants: number +} diff --git a/src/models/interfaces/index.ts b/src/models/interfaces/index.ts new file mode 100644 index 0000000..c1a708b --- /dev/null +++ b/src/models/interfaces/index.ts @@ -0,0 +1,7 @@ +export { + IModel, + IModelReader, + IModelWriter, + IModelDelete, +} from './IModel'; +export { default as IOpsInfo } from './IOpsInfo'; diff --git a/src/router/PlantRouter.ts b/src/router/PlantRouter.ts index 053e9d1..9e70574 100644 --- a/src/router/PlantRouter.ts +++ b/src/router/PlantRouter.ts @@ -1,12 +1,19 @@ import { Router } from 'express'; import PlantController from '../controllers/PlantController'; +import PlantModel from '../models/PlantModel'; +import PlantService from '../services/PlantService'; -const plantController = new PlantController(); +const plantModel = new PlantModel(); +const plantService = new PlantService(plantModel); +const plantController = new PlantController(plantService); const plantRouter = Router(); plantRouter.get('/', (req, res, next) => plantController.getAll(req, res, next)); plantRouter.post('/', (req, res, next) => plantController.create(req, res, next)); +plantRouter.get('/:id', (req, res, next) => plantController.getById(req, res, next)); +plantRouter.delete('/:id', (req, res, next) => plantController.remove(req, res, next)); +plantRouter.put('/:id', (req, res, next) => plantController.update(req, res, next)); export default plantRouter; diff --git a/src/services/PlantService.ts b/src/services/PlantService.ts index c935e35..8438a74 100644 --- a/src/services/PlantService.ts +++ b/src/services/PlantService.ts @@ -1,80 +1,52 @@ -import fs from 'fs/promises'; -import path from 'path'; -import HttpException from '../exceptions/HttpException'; +import { INewPlant, IPlant } from '../interfaces'; +import { IService } from './interfaces'; +import { IModel } from '../models/interfaces'; +import { NotFoundException } from '../exceptions'; +import PlantValidate from './validations/PlantValidate'; -interface IPlant { - id: number, - breed: string, - needsSun: boolean, - origin: string, - size: number, - waterFrequency: number, -} - -type INewPlant = Omit; - -interface IOpsInfo { - createdPlants: number -} - -class PlantService { - private readonly plantsFile = path.join(__dirname, '..', 'models', 'database', 'plantsData.json'); +class PlantService implements IService { + private readonly model: IModel; - private readonly opsFile = path.join(__dirname, '..', 'models', 'database', 'opsInfo.json'); - - private async updateOpsInfo(incrementAmount = 1): Promise { - const dataRaw = await fs.readFile(this.opsFile, { encoding: 'utf8' }); - const opsInfo: IOpsInfo = JSON.parse(dataRaw); - opsInfo.createdPlants += incrementAmount; - - await fs.writeFile(this.opsFile, JSON.stringify(opsInfo, null, 2)); - - return opsInfo.createdPlants; + constructor(model: IModel) { + this.model = model; } public async getAll(): Promise { - const dataRaw = await fs.readFile(this.plantsFile, { encoding: 'utf8' }); - const plants: IPlant[] = JSON.parse(dataRaw); + const plants = await this.model.getAll(); return plants; } public async create(plant: INewPlant): Promise { - const { - breed, - needsSun, - origin, - size, - } = plant; - - if (typeof breed !== 'string') { - throw new HttpException(400, 'Attribute "breed" must be string.'); - } - - if (typeof needsSun !== 'boolean') { - throw new HttpException(400, 'Attribute "needsSun" must be boolen.'); - } - - if (typeof origin !== 'string') { - throw new HttpException(400, 'Attribute "origin" must be string.'); - } - - if (typeof size !== 'number') { - throw new HttpException(400, 'Attribute "size" must be number.'); - } + PlantValidate.validateAttibutes(plant); + const { needsSun, size, origin } = plant; const waterFrequency = needsSun ? size * 0.77 + (origin === 'Brazil' ? 8 : 7) : (size / 2) * 1.33 + (origin === 'Brazil' ? 8 : 7); - const dataRaw = await fs.readFile(this.plantsFile, { encoding: 'utf8' }); - const plants: IPlant[] = JSON.parse(dataRaw); + const newPlant = await this.model.create({ ...plant, waterFrequency }); + return newPlant; + } + + public async getById(id: string): Promise { + const plant = await this.model.getById(id); + if (!plant) throw new NotFoundException('Plant not Found!'); + return plant; + } - const newPlantId = await this.updateOpsInfo(1); - const newPlant = { id: newPlantId, ...plant, waterFrequency }; - plants.push(newPlant); + public async removeById(id: string): Promise { + const isPlantRemoved = await this.model.removeById(id); + if (!isPlantRemoved) throw new NotFoundException('Plant not Found!'); + } - await fs.writeFile(this.plantsFile, JSON.stringify(plants, null, 2)); - return newPlant; + public async update(id: string, plant: Omit): Promise { + const plantExists = await this.model.getById(id); + if (!plantExists) throw new NotFoundException('Plant not Found!'); + + PlantValidate.validateAttibutes(plant); + + const editedPlant = await this.model.update({ id: parseInt(id, 10), ...plant }); + return editedPlant; } } diff --git a/src/services/interfaces/IService.ts b/src/services/interfaces/IService.ts new file mode 100644 index 0000000..36291ae --- /dev/null +++ b/src/services/interfaces/IService.ts @@ -0,0 +1,17 @@ +export interface IServiceReader { + getAll(): Promise; + getById(id: string): Promise +} + +export interface IServiceWriter { + create(arg: U): Promise + update(id: string, arg: Omit): Promise +} +export interface IServiceDelete { + removeById(id: string): Promise +} + +export interface IService extends + IServiceReader, + IServiceWriter, + IServiceDelete {} diff --git a/src/services/interfaces/index.ts b/src/services/interfaces/index.ts new file mode 100644 index 0000000..0edb36b --- /dev/null +++ b/src/services/interfaces/index.ts @@ -0,0 +1,6 @@ +export { + IService, + IServiceReader, + IServiceWriter, + IServiceDelete, +} from './IService'; diff --git a/src/services/validations/PlantValidate.ts b/src/services/validations/PlantValidate.ts new file mode 100644 index 0000000..b3eb8ee --- /dev/null +++ b/src/services/validations/PlantValidate.ts @@ -0,0 +1,35 @@ +import { INewPlant } from '../../interfaces'; +import { BadRequestException } from '../../exceptions'; + +export default class PlantValidate { + private static validateBreed(breed: string): void { + if (typeof breed !== 'string') { + throw new BadRequestException('Attribute "breed" must be string.'); + } + } + + private static validateNeedsSun(needsSun: boolean): void { + if (typeof needsSun !== 'boolean') { + throw new BadRequestException('Attribute "needsSun" must be boolen.'); + } + } + + private static validateOrigin(origin: string): void { + if (typeof origin !== 'string') { + throw new BadRequestException('Attribute "origin" must be string.'); + } + } + + private static validateSize(size: number): void { + if (typeof size !== 'number') { + throw new BadRequestException('Attribute "size" must be number.'); + } + } + + public static validateAttibutes(plant: INewPlant): void { + PlantValidate.validateBreed(plant.breed); + PlantValidate.validateNeedsSun(plant.needsSun); + PlantValidate.validateOrigin(plant.origin); + PlantValidate.validateSize(plant.size); + } +}