From ff0e9478f29f10b0e1f457194ea7814bb843d66f Mon Sep 17 00:00:00 2001 From: Luiz Carlos Date: Sun, 13 Mar 2022 09:55:55 +0000 Subject: [PATCH] feat: Finish category CRUD --- .devcontainer/devcontainer.json | 4 +- .gitignore | 3 +- jest.config.ts | 9 +- package-lock.json | 120 ++++++++-------- package.json | 4 +- src/@core/@seedwork/@types/jest.d.ts | 4 +- .../errors/bad-entity-operation.error.ts | 8 ++ src/@core/@seedwork/application/use-case.ts | 3 + .../domain/entity/__tests__/entity.spec.ts | 39 ++++- src/@core/@seedwork/domain/entity/entity.ts | 17 ++- .../@seedwork/domain/entity/notification.ts | 101 +++++++++++++ .../domain/errors/__tests__/error-bag.spec.ts | 33 +++++ .../@seedwork/domain/errors/error-bag.ts | 25 ++++ .../domain/errors/load-entity.error.ts | 8 ++ .../domain/errors/validation.error.ts | 4 +- .../__tests__/in-memory.repository.spec.ts | 53 ++++++- .../__tests__/repository-contracts.spec.ts | 6 +- .../domain/repository/in-memory.repository.ts | 30 ++-- .../domain/repository/repository-contracts.ts | 1 + .../@seedwork/domain/tests/validation.ts | 92 ++++++++---- ...pec.ts => class-validator-fields.ispec.ts} | 19 ++- .../__tests__/class-validator-fields.spec.ts | 59 ++++++++ .../__tests__/class.validator.spec.ts | 53 ------- .../domain/validators/class.validator.ts | 36 +++-- .../validators/validator-fields-interface.ts | 10 ++ .../domain/validators/validator-interface.ts | 8 -- .../domain/validators/validator-rules.ts | 1 + .../__tests__/unique-entity-id.spec.ts | 39 +++-- .../__tests__/value-object.spec.ts | 40 +++++- .../domain/value-objects/unique-entity-id.ts | 22 ++- .../domain/value-objects/value-object.ts | 10 +- .../delete-category.use-case.spec.ts | 23 +++ .../__tests__/get-category.use-case.spec.ts | 23 +++ .../update-category.use-case.spec.ts | 73 ++++++++++ .../use-cases/create-category.use-case.ts | 8 +- .../use-cases/delete-category.use-case.ts | 19 +++ .../use-cases/get-category.use-case.ts | 27 ++++ .../use-cases/list-categories.use-case.ts | 3 +- .../use-cases/update-category.use-case.ts | 48 +++++++ .../entities/__tests__/category.ispec.ts | 136 ++++++++++++++++-- .../entities/__tests__/category.spec.ts | 107 +++++++++++++- .../category/domain/entities/category.ts | 38 ++++- .../domain/validators/category.validator.ts | 38 +++-- 43 files changed, 1155 insertions(+), 249 deletions(-) create mode 100644 src/@core/@seedwork/application/errors/bad-entity-operation.error.ts create mode 100644 src/@core/@seedwork/application/use-case.ts create mode 100644 src/@core/@seedwork/domain/entity/notification.ts create mode 100644 src/@core/@seedwork/domain/errors/__tests__/error-bag.spec.ts create mode 100644 src/@core/@seedwork/domain/errors/error-bag.ts create mode 100644 src/@core/@seedwork/domain/errors/load-entity.error.ts rename src/@core/@seedwork/domain/validators/__tests__/{class.validator.ispec.ts => class-validator-fields.ispec.ts} (58%) create mode 100644 src/@core/@seedwork/domain/validators/__tests__/class-validator-fields.spec.ts delete mode 100644 src/@core/@seedwork/domain/validators/__tests__/class.validator.spec.ts create mode 100644 src/@core/@seedwork/domain/validators/validator-fields-interface.ts delete mode 100644 src/@core/@seedwork/domain/validators/validator-interface.ts create mode 100644 src/@core/category/application/use-cases/__tests__/delete-category.use-case.spec.ts create mode 100644 src/@core/category/application/use-cases/__tests__/get-category.use-case.spec.ts create mode 100644 src/@core/category/application/use-cases/__tests__/update-category.use-case.spec.ts create mode 100644 src/@core/category/application/use-cases/delete-category.use-case.ts create mode 100644 src/@core/category/application/use-cases/get-category.use-case.ts create mode 100644 src/@core/category/application/use-cases/update-category.use-case.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1d79f5a..36dd046 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -28,7 +28,7 @@ // Add the IDs of extensions you want installed when the container is created. "extensions": [ "firsttris.vscode-jest-runner" - ] + ], // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], @@ -37,7 +37,7 @@ // "runServices": [], // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", + "shutdownAction": "stopCompose", // Uncomment the next line to run commands after the container is created - for example installing curl. // "postCreateCommand": "apt-get update && apt-get install -y curl", diff --git a/.gitignore b/.gitignore index ff63c33..28e3e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ .history/ .docker/zsh/history/.zsh_history -tsconfig.tsbuildinfo \ No newline at end of file +tsconfig.tsbuildinfo +src/__coverage \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index 9996888..768b04c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -23,7 +23,7 @@ export default { // collectCoverageFrom: undefined, // The directory where Jest should output its coverage files - coverageDirectory: "coverage", + coverageDirectory: "__coverage", // An array of regexp pattern strings used to skip coverage collection // coveragePathIgnorePatterns: [ @@ -34,12 +34,13 @@ export default { coverageProvider: "v8", // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", + coverageReporters: [ + "json", + "html" // "text", // "lcov", // "clover" - // ], + ], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: undefined, diff --git a/package-lock.json b/package-lock.json index f17fc19..4e04f72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -707,121 +707,121 @@ } }, "@swc/core": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.140.tgz", - "integrity": "sha512-RKaWVry/+lVeKGD0eI2sxx5BBk/PTh2nQcLijhF8hThI9ipJtyo1JAJqE+mOynOXegb9puOEsj13ihngAVTBZQ==", - "dev": true, - "requires": { - "@swc/core-android-arm-eabi": "1.2.140", - "@swc/core-android-arm64": "1.2.140", - "@swc/core-darwin-arm64": "1.2.140", - "@swc/core-darwin-x64": "1.2.140", - "@swc/core-freebsd-x64": "1.2.140", - "@swc/core-linux-arm-gnueabihf": "1.2.140", - "@swc/core-linux-arm64-gnu": "1.2.140", - "@swc/core-linux-arm64-musl": "1.2.140", - "@swc/core-linux-x64-gnu": "1.2.140", - "@swc/core-linux-x64-musl": "1.2.140", - "@swc/core-win32-arm64-msvc": "1.2.140", - "@swc/core-win32-ia32-msvc": "1.2.140", - "@swc/core-win32-x64-msvc": "1.2.140" + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.154.tgz", + "integrity": "sha512-Wz61emNkLRnnbBriqV2DEn4oqfsanKeT5OrBw1ZpIXlZ1qx5h3ft5yuJm0jvSBj+oiTeX+BZcE4f2VEwE1xvqQ==", + "dev": true, + "requires": { + "@swc/core-android-arm-eabi": "1.2.154", + "@swc/core-android-arm64": "1.2.154", + "@swc/core-darwin-arm64": "1.2.154", + "@swc/core-darwin-x64": "1.2.154", + "@swc/core-freebsd-x64": "1.2.154", + "@swc/core-linux-arm-gnueabihf": "1.2.154", + "@swc/core-linux-arm64-gnu": "1.2.154", + "@swc/core-linux-arm64-musl": "1.2.154", + "@swc/core-linux-x64-gnu": "1.2.154", + "@swc/core-linux-x64-musl": "1.2.154", + "@swc/core-win32-arm64-msvc": "1.2.154", + "@swc/core-win32-ia32-msvc": "1.2.154", + "@swc/core-win32-x64-msvc": "1.2.154" } }, "@swc/core-android-arm-eabi": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.140.tgz", - "integrity": "sha512-wxJNMg6BS0jZhuNHdemcI2GJtJzzM+DbcNpjafpWhmITRK06UoSMeN6V0C1WLU/vgFHZQkxfhqwXhuabVAWwiQ==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.154.tgz", + "integrity": "sha512-zwpOQ6EVHUQSK0AiwruRv/uwvLKBbj9wJ7n3Y6hipoeXYNGYzlDpuxsWzBgNtpLYozSKsL9ErByvNGWjR1uvRw==", "dev": true, "optional": true }, "@swc/core-android-arm64": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.140.tgz", - "integrity": "sha512-jq7vKqisa0Hm02HwXnf0SaLiow1Ezjm2IQorsPcMVh82i461k6YDiay6qgcvgDhkqVPVL+FR7dF39jjmk0v5Bw==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.154.tgz", + "integrity": "sha512-MYNCY1KXYfcHPfkDdokVZqm5NqmQrhURXf9/PjpkDgcNu/7IDBxvPsFoBTq6Czl/FyEF1QyilfgoTpP/liyYNA==", "dev": true, "optional": true }, "@swc/core-darwin-arm64": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.140.tgz", - "integrity": "sha512-TWlyLablaopI3Cf6Zooknw0kd0Sla2r7ZCqfC/lK/8a8zwN8335gJcSXtyVeWnF3nx5F+rZZY9EPdJqJ3jL0Cw==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.154.tgz", + "integrity": "sha512-30Zpyp/RdR9ywJcneDYoESRdoJVI/mVMzTvWzPDHUpkABgPTXA4h53kL1e6oIYEp4uOkcKFFD4iAzbz1tj/vhw==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.140.tgz", - "integrity": "sha512-d44A0PW+ZP8wRh/iFhu0UzUwRkcswGkUXJy1187Q3jLm6uxdW2rH4DoFQHRuABXh4tDjd3cXRClz/xSURNIJDA==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.154.tgz", + "integrity": "sha512-t3oRoDd3q2jlRdpCO1gG3GB6nbl0Z+jHFsIcKzdeBVhnG3R6pqOflA6GYrNEaIyskPf0WT5POcpBv38I7liyPQ==", "dev": true, "optional": true }, "@swc/core-freebsd-x64": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.140.tgz", - "integrity": "sha512-L4luX3JDrNDouYcjvDUsdE2CLk/EMpEHHveUSM+dHvyVeJ/eVPZFTyeiBpc1E/rymHWa2C2nIYAe/v8yZqcjDA==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.154.tgz", + "integrity": "sha512-YSQF9AekPRbQTPNNlaWmJqds1QfbS9qDDL9zyUcC2XVk5bAKQQMmVYSRTJm4aV6no2Vy+C5yQeuR2vDwVUUXvw==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.140.tgz", - "integrity": "sha512-rNjGhWvBazig/5/gqdMziPXsSse3/sv5zTy8GfKP8F9ZhToxJ+Hg5Vr+DqrrzVgVE5zhzbiWxoiScr6W+wER6g==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.154.tgz", + "integrity": "sha512-xAg+eP7wsQ2I8jOPDmvbqe6N1UvjUWD1/1jq1zF3kCwH7352kZWljFdJ0WcojUN0pX5lyIl4QWN1xarhtprFhg==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.140.tgz", - "integrity": "sha512-ws7uMHVbwXkXr8CRwmYgQca5mB36iJ/Y4LNXvfpaPsmQsCDVBvXmg4pEk4g/0e53By02GcErn31kPisJsiHt5w==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.154.tgz", + "integrity": "sha512-lxrZyD6sONGZsL/wgB4q65/U4lwdWHJv81NSp+ScjvNSKeSJz/a+fxMB7FVf/iEpCtZobLgFUZO+sRVtO2jH3g==", "dev": true, "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.140.tgz", - "integrity": "sha512-YwueGxb9IXfDh5B/3ezI3F8deoyhiNNuItnUjoUG2+xuNyjGBr8EOoddfbnZD1HxqRyyZXIhed4Lohsz5LQi+g==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.154.tgz", + "integrity": "sha512-dnPzLhCHyTLAKmJYHq7W6WJ1Pp+0vnM4mX091EjN5Pw+JTSF1ItNAWya8n87PV3LaDz5sF0JqGwHEqAzgfokHg==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.140.tgz", - "integrity": "sha512-z3O9uDHQA/fhiJeYp7jvoeiHiFsS5Y0t1SBU2eqQnCSlJK7V+V0Fn6WmcmVlnowI3ffI+k1lyncoQNGhlrUZZQ==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.154.tgz", + "integrity": "sha512-kdDxQxbt5ANoYXJE804GBJmh/ppiKWI996Ax2vuiUPUil9PtzIe5uxEAh/OwM9Tq+7YSfNcHUF2ZG/a6N7CvmA==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.140.tgz", - "integrity": "sha512-PYlKrlzTkYVpFg2LnLI+Lk4I8X+/xWZvUZCAPo+GjbMpRDmp2/K9GWFNQxMBAHft+Z97jqzbL/GA37Op954XbQ==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.154.tgz", + "integrity": "sha512-g8GJCj2W4jla9R9EvbFnrEiykLK1utZLTQxi8TEx3Vjjv3iTDkA3VEq+JUHDZUzj8wsRcmxB2x23RxXXJ9PbtA==", "dev": true, "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.140.tgz", - "integrity": "sha512-OJrpVLHzrA+U0vLXEoQ4xWGuZr3oTwmR/J+g8+899wCMA6NAekWoO9k+mLku6RkOHD8Q6cHmVA7nEJFdkvi2ng==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.154.tgz", + "integrity": "sha512-zRa0L56etjgK6YjIY3bh9XlT+Zf8tWgdGGkXqimm9RMMgGBrJ0e+Wg8j5LCM0a9YMONPNwqM1P9UK2i32AFM+Q==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.140.tgz", - "integrity": "sha512-tH7nQPI0QopAgl+/lQIu8bIohmiof0zHOWd17TYV2C4gPka4VQH4CvF8NJLEdLnRhn+r0tHlrREWnuPakzN+Lw==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.154.tgz", + "integrity": "sha512-Vojk27M5Dnpxw2Oh2D2mnTc8Jl2ZWpjOASYSwOZ+pM3qx1LWhVrTjR3NxZIMMlQ7uygE2GxjwolCVaknMDHXJw==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.2.140", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.140.tgz", - "integrity": "sha512-OtRarHcQ30WrkmIUXB96K1h1bsY830POJ0kb02VxTn/U9366BWs42fIoURa+637jPCZLfAceln10jxjfII1Iyg==", + "version": "1.2.154", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.154.tgz", + "integrity": "sha512-8+x7yoLsw42VE/9b92yCigpmodMlKaMbUvmVtXFcmx/KpzdUpgN+5e4XfljkmYTBZTSYbwqGM8K2jceGsfZmLg==", "dev": true, "optional": true }, "@swc/jest": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.17.tgz", - "integrity": "sha512-n/g989+O8xxMcTZnP0phDrrgezGZBQBf7cX4QuzEsn07QkWbqmMsfaCxdF0kzajXublXWJ8yk5vRe3VNk1tczA==", + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.20.tgz", + "integrity": "sha512-5qSUBYY1wyIMn7p0Vl9qqV4hMI69oJwZCIPUpBsTFWN2wlwn6RDugzdgCn+bLXVYh+Cxi8bJcZ1uumDgsoL+FA==", "dev": true, "requires": { "@jest/create-cache-key-function": "^27.4.2" diff --git a/package.json b/package.json index 7e439aa..2e1310b 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "license": "ISC", "devDependencies": { "@sucrase/jest-plugin": "^2.2.0", - "@swc/core": "^1.2.140", - "@swc/jest": "^0.2.17", + "@swc/core": "^1.2.154", + "@swc/jest": "^0.2.20", "@types/jest": "^27.4.0", "@types/node": "^17.0.18", "@types/uuid": "^8.3.4", diff --git a/src/@core/@seedwork/@types/jest.d.ts b/src/@core/@seedwork/@types/jest.d.ts index fcf63e0..66dcb24 100644 --- a/src/@core/@seedwork/@types/jest.d.ts +++ b/src/@core/@seedwork/@types/jest.d.ts @@ -1,8 +1,10 @@ /// +type Expected = { validator: ClassValidatorFields; data: any } | Entity; + declare namespace jest { // noinspection JSUnusedGlobalSymbols interface Matchers { - containErrorMessages: (expected: { [field: string]: string[] }) => R; + containErrorMessages: (expected: Expected) => R; } } diff --git a/src/@core/@seedwork/application/errors/bad-entity-operation.error.ts b/src/@core/@seedwork/application/errors/bad-entity-operation.error.ts new file mode 100644 index 0000000..6c75733 --- /dev/null +++ b/src/@core/@seedwork/application/errors/bad-entity-operation.error.ts @@ -0,0 +1,8 @@ +import ErrorBag from "../../domain/errors/error-bag"; + +export default class BadEntityOperationError extends Error { + constructor(public error: ErrorBag, message?: string) { + super(message ?? "An entity operation was executed with error"); + this.name = "BadEntityOperationError"; + } +} diff --git a/src/@core/@seedwork/application/use-case.ts b/src/@core/@seedwork/application/use-case.ts new file mode 100644 index 0000000..0d86731 --- /dev/null +++ b/src/@core/@seedwork/application/use-case.ts @@ -0,0 +1,3 @@ +export default interface UseCase{ + execute(input: Input): Output | Promise; +} \ No newline at end of file diff --git a/src/@core/@seedwork/domain/entity/__tests__/entity.spec.ts b/src/@core/@seedwork/domain/entity/__tests__/entity.spec.ts index e6f2e4e..7aa2fab 100644 --- a/src/@core/@seedwork/domain/entity/__tests__/entity.spec.ts +++ b/src/@core/@seedwork/domain/entity/__tests__/entity.spec.ts @@ -1,14 +1,26 @@ import Entity from "../entity"; import { validate as uuidValidate } from "uuid"; import UniqueEntityId from "../../value-objects/unique-entity-id"; +import ErrorBag from "../../errors/error-bag"; -class StubEntity extends Entity<{ prop1: string; prop2: number }> {} +class StubEntity extends Entity<{ prop1: string; prop2: number }> { + protected validate(): boolean { + if (this.error.hasError()) { + return false; + } + if (this.props.prop1 === "invalid") { + this.error.add("The prop1 is invalid"); + } + return this.error.notHasError(); + } +} describe("Entity Unit Tests", () => { it("should set props and id", () => { const entity = new StubEntity({ prop1: "prop1 value", prop2: 10 }); expect(entity.props).toMatchObject({ prop1: "prop1 value", prop2: 10 }); expect(entity.id).not.toBeNull(); + expect(entity.uniqueEntityId).toBeInstanceOf(UniqueEntityId); expect(uuidValidate(entity.id)).toBeTruthy(); }); @@ -17,6 +29,7 @@ describe("Entity Unit Tests", () => { { prop1: "prop1 value", prop2: 10 }, new UniqueEntityId("5490020a-e866-4229-9adc-aa44b83234c4") ); + expect(entity.uniqueEntityId).toBeInstanceOf(UniqueEntityId); expect(entity.id).toBe("5490020a-e866-4229-9adc-aa44b83234c4"); }); @@ -31,4 +44,28 @@ describe("Entity Unit Tests", () => { prop2: 10, }); }); + + test('is_valid getter', () => { + let entity = new StubEntity({prop1: 'valid', prop2: 0}); + entity['validate'](); + expect(entity.is_valid).toBeTruthy(); + + entity = new StubEntity({prop1: 'invalid', prop2: 0}); + entity['validate'](); + expect(entity.is_valid).toBeFalsy(); + }); + + test("validate method and error variable", () => { + const mockIsValidMethod = jest.spyOn(StubEntity.prototype as any, "validate"); + let entity = new StubEntity({prop1: 'valid', prop2: 0}); + expect(entity['validate']()).toBeTruthy(); + expect(entity.error).toStrictEqual(new ErrorBag); + expect(mockIsValidMethod).toHaveBeenCalled(); + + const errorBag = new ErrorBag(); + errorBag.add("The prop1 is invalid"); + entity = new StubEntity({prop1: 'invalid', prop2: 0}); + expect(entity['validate']()).toBeFalsy(); + expect(entity.error).toStrictEqual(errorBag); + }); }); diff --git a/src/@core/@seedwork/domain/entity/entity.ts b/src/@core/@seedwork/domain/entity/entity.ts index 4b0364e..7db041f 100644 --- a/src/@core/@seedwork/domain/entity/entity.ts +++ b/src/@core/@seedwork/domain/entity/entity.ts @@ -1,19 +1,28 @@ +import ErrorBag from "../errors/error-bag"; import UniqueEntityId from "../value-objects/unique-entity-id"; export default abstract class Entity { - protected readonly _id: UniqueEntityId; + readonly uniqueEntityId: UniqueEntityId; + readonly error = new ErrorBag; constructor(public readonly props: Props, id?: UniqueEntityId) { - this._id = id || new UniqueEntityId(); + this.uniqueEntityId = id || new UniqueEntityId(); + } + + protected abstract validate(): boolean; + + get is_valid(): boolean{ + return this.error.notHasError(); } get id(): string { - return this._id.value; + return this.uniqueEntityId.value; } + toJSON(): Required<{ id: string } & Props> { return { - id: this._id.value, + id: this.uniqueEntityId.value, ...this.props, } as Required<{id: string} & Props>; } diff --git a/src/@core/@seedwork/domain/entity/notification.ts b/src/@core/@seedwork/domain/entity/notification.ts new file mode 100644 index 0000000..69269f5 --- /dev/null +++ b/src/@core/@seedwork/domain/entity/notification.ts @@ -0,0 +1,101 @@ +// class Notification {} + +// Notification.add(ValidationException); + +// category = this.categoryRepo.find(id); + +// if (category.isInvalid) { +// throw new LoadEntityError(); +// } + +// const search = new Search(); + +// if (search.isInvalid) { +// throw new InvalidSearchParamsError(); +// } + +// this.categoryRepo.search(); + +// type Error = string | { [key: string]: string | string[] }; + +// class ErrorBag { +// private _errors: Error[] = []; + +// get errors() { +// return this._errors; +// } + +// addError(error: Error) { +// this._errors.push(error); +// } + +// hasError() { +// return !this._errors.length; +// } + +// notHasError() { +// return !this.hasError(); +// } +// } + +// class Category { +// private error = new ErrorBag(); + +// private constructor(props) { +// //colocar os campos +// this.isValid(); +// } + +// static create(props): Category { +// const error = !this.canCreate(props); +// if (error) { +// throw new InvalidEntityOperationError("create", error); +// } + +// return new Category(props); +// } + +// static canCreate(props): ErrorBag | true { +// const category = new Category(props); +// if (!category.isValid()) { +// return category.error; +// } + +// return true; +// } + +// isValid() { +// if (this.error.hasError()) { +// return false; +// } + +// if (!this.props.id.isValid()) { +// this.error.addError({ id: this.props.id.error.errors }); +// } + +// const categoryValidator = CategoryFactory.create(); +// const isValid = categoryValidator.isValid(this); + +// if (!isValid) { +// for (const field of Object.keys(categoryValidator.errors)) { +// this.error.addError({ [field]: categoryValidator.errors[field] }); +// } +// } + +// return this.error.hasError(); +// } +// } + +// class UniqueEntityId { +// private error = new ErrorBag(); +// isValid() { +// if (this.error.hasError()) { +// return false; +// } +// const isValid = uuidValidate(this._value); +// if (!isValid) { +// this.error.addError("ID must be a valid UUID"); +// } +// return isValid; +// } +// } diff --git a/src/@core/@seedwork/domain/errors/__tests__/error-bag.spec.ts b/src/@core/@seedwork/domain/errors/__tests__/error-bag.spec.ts new file mode 100644 index 0000000..27e4e45 --- /dev/null +++ b/src/@core/@seedwork/domain/errors/__tests__/error-bag.spec.ts @@ -0,0 +1,33 @@ +import ErrorBag from '../error-bag'; + +describe('ErrorBag Unit Tests', () => { + let errorBag: ErrorBag; + + beforeEach(() => errorBag = new ErrorBag()) + it('should add an error', () => { + errorBag.add('error test'); + expect(errorBag.errors).toStrictEqual(['error test']) + + errorBag.add({field: ['new error']}); + expect(errorBag.errors).toStrictEqual([ + 'error test', + {field: ['new error']} + ]) + }); + + test('hasError method', () => { + expect(errorBag.hasError()).toBeFalsy(); + + errorBag.add('error test'); + + expect(errorBag.hasError()).toBeTruthy(); + }); + + test('notHasError method', () => { + expect(errorBag.notHasError()).toBeTruthy(); + + errorBag.add('error test'); + + expect(errorBag.notHasError()).toBeFalsy(); + }) +}) \ No newline at end of file diff --git a/src/@core/@seedwork/domain/errors/error-bag.ts b/src/@core/@seedwork/domain/errors/error-bag.ts new file mode 100644 index 0000000..68ca3ad --- /dev/null +++ b/src/@core/@seedwork/domain/errors/error-bag.ts @@ -0,0 +1,25 @@ +export type ErrorValue = string | { [key: string]: string[] }; + +export default class ErrorBag { + private _errors: ErrorValue[] = []; + + add(error: ErrorValue) { + this._errors.push(error); + } + + clear(){ + this._errors = []; + } + + hasError() { + return this._errors.length > 0; + } + + notHasError() { + return !this.hasError(); + } + + get errors() { + return this._errors; + } +} \ No newline at end of file diff --git a/src/@core/@seedwork/domain/errors/load-entity.error.ts b/src/@core/@seedwork/domain/errors/load-entity.error.ts new file mode 100644 index 0000000..96a499c --- /dev/null +++ b/src/@core/@seedwork/domain/errors/load-entity.error.ts @@ -0,0 +1,8 @@ +import ErrorBag from "./error-bag"; + +export default class LoadEntityError extends Error { + constructor(public error: ErrorBag, message?: string) { + super(message ?? "An entity not be loaded"); + this.name = "LoadEntityError"; + } +} diff --git a/src/@core/@seedwork/domain/errors/validation.error.ts b/src/@core/@seedwork/domain/errors/validation.error.ts index 6cbd89c..e56b955 100644 --- a/src/@core/@seedwork/domain/errors/validation.error.ts +++ b/src/@core/@seedwork/domain/errors/validation.error.ts @@ -1,11 +1,11 @@ -import { ValidationErrorFields } from "../validators/validator-interface"; +import { FieldsErrors } from "../validators/validator-fields-interface"; export class SimpleValidationError extends Error{ } export default class ValidationError extends Error { - constructor(public error: ValidationErrorFields) { + constructor(public error: FieldsErrors) { super("Validation Error"); this.name = "EntityValidationError"; } diff --git a/src/@core/@seedwork/domain/repository/__tests__/in-memory.repository.spec.ts b/src/@core/@seedwork/domain/repository/__tests__/in-memory.repository.spec.ts index 51c7e44..bec1665 100644 --- a/src/@core/@seedwork/domain/repository/__tests__/in-memory.repository.spec.ts +++ b/src/@core/@seedwork/domain/repository/__tests__/in-memory.repository.spec.ts @@ -1,4 +1,5 @@ import Entity from "../../entity/entity"; +import LoadEntityError from "../../errors/load-entity.error"; import NotFoundError from "../../errors/not-found.error"; import UniqueEntityId from "../../value-objects/unique-entity-id"; import InMemoryRepository from "../in-memory.repository"; @@ -8,7 +9,18 @@ type StubEntityProps = { price: number; }; -class StubEntity extends Entity {} +class StubEntity extends Entity { + constructor(props: StubEntityProps, id?: UniqueEntityId){ + super(props, id); + this.validate(); + } + protected validate(): boolean { + if (this.props.name === "invalid") { + this.error.add("The name field is invalid"); + } + return this.error.hasError(); + } +} class StubInMemoryRepository extends InMemoryRepository {} @@ -17,6 +29,13 @@ describe("InMemoryRepository Unit Tests", () => { beforeEach(() => (repository = new StubInMemoryRepository())); + test("validateEntity Method", () => { + const entity = new StubEntity({ name: "invalid", price: 0 }); + expect(() => repository.validateEntity(entity)).toThrow( + new LoadEntityError(entity.error) + ); + }); + it("should insert a new entity", async () => { const entity = new StubEntity({ name: "test", price: 0 }); await repository.insert(entity); @@ -36,6 +55,14 @@ describe("InMemoryRepository Unit Tests", () => { ); }); + it("should throws error in findById method when a entity is invalid", () => { + const entity = new StubEntity({ name: "invalid", price: 0 }); + repository.items = [entity]; + expect(repository.findById(entity.id)).rejects.toThrow( + new LoadEntityError(entity.error) + ); + }); + it("should find a entity by id", async () => { const entity = new StubEntity({ name: "test", price: 0 }); await repository.insert(entity); @@ -47,6 +74,14 @@ describe("InMemoryRepository Unit Tests", () => { expect(entity.toJSON()).toStrictEqual(entityFound.toJSON()); }); + it("should throws error in findAll method when a entity is invalid", () => { + const entity = new StubEntity({ name: "invalid", price: 0 }); + repository.items = [entity]; + expect(repository.findAll()).rejects.toThrow( + new LoadEntityError(entity.error) + ); + }); + it("should returns all entities persisted", async () => { const entity = new StubEntity({ name: "test", price: 0 }); await repository.insert(entity); @@ -62,6 +97,14 @@ describe("InMemoryRepository Unit Tests", () => { ); }); + it("should throws error in update method when find a invalid entity", () => { + const entity = new StubEntity({ name: "invalid", price: 0 }); + repository.items = [entity]; + expect(repository.update(entity)).rejects.toThrow( + new LoadEntityError(entity.error) + ); + }); + it("should update a entity", async () => { const entity = new StubEntity({ name: "test", price: 0 }); await repository.insert(entity); @@ -87,6 +130,14 @@ describe("InMemoryRepository Unit Tests", () => { ); }); + it("should throws error in delete method when find a invalid entity", () => { + const entity = new StubEntity({ name: "invalid", price: 0 }); + repository.items = [entity]; + expect(repository.delete(entity.id)).rejects.toThrow( + new LoadEntityError(entity.error) + ); + }); + it("should delete a entity", async () => { const entity = new StubEntity({ name: "test", price: 0 }); await repository.insert(entity); diff --git a/src/@core/@seedwork/domain/repository/__tests__/repository-contracts.spec.ts b/src/@core/@seedwork/domain/repository/__tests__/repository-contracts.spec.ts index 014c7d0..bec5ee8 100644 --- a/src/@core/@seedwork/domain/repository/__tests__/repository-contracts.spec.ts +++ b/src/@core/@seedwork/domain/repository/__tests__/repository-contracts.spec.ts @@ -154,7 +154,11 @@ describe("SearchParams Unit Tests", () => { }); }); -class StubEntity extends Entity {} +class StubEntity extends Entity { + protected validate(){ + return true; + } +} describe("SearchResult Unit Tests", () => { test("constructor params", () => { diff --git a/src/@core/@seedwork/domain/repository/in-memory.repository.ts b/src/@core/@seedwork/domain/repository/in-memory.repository.ts index 05c1c62..4d4b54c 100644 --- a/src/@core/@seedwork/domain/repository/in-memory.repository.ts +++ b/src/@core/@seedwork/domain/repository/in-memory.repository.ts @@ -1,6 +1,8 @@ import Entity from "../entity/entity"; import UniqueEntityId from "../value-objects/unique-entity-id"; import NotFoundError from "../errors/not-found.error"; +import LoadEntityError from "../errors/load-entity.error"; + import { RepositoryInterface, SearchableRepositoryInterface, @@ -24,7 +26,10 @@ export default abstract class InMemoryRepository } async findAll(): Promise { - return this.items; + return this.items.map((i) => { + this.validateEntity(i); + return i; + }); } async update(entity: E): Promise { @@ -45,8 +50,15 @@ export default abstract class InMemoryRepository if (!item) { throw new NotFoundError(`Entity Not Found using ID '${id}'`); } + this.validateEntity(item); return item; } + + validateEntity(entity: E) { + if (!entity.is_valid) { + throw new LoadEntityError(entity.error); + } + } } export abstract class InMemorySearchableRepository @@ -62,11 +74,14 @@ export abstract class InMemorySearchableRepository input.sort, input.sort_dir ); - let itemsPaginated = await this.applyPaginate( + let itemsPaginated = (await this.applyPaginate( itemsSorted, input.page, input.per_page - ); + )).map((i) => { + this.validateEntity(i); + return i; + }); return new SearchResult({ items: itemsPaginated, @@ -75,14 +90,11 @@ export abstract class InMemorySearchableRepository per_page: input.per_page, sort: input.sort, sort_dir: input.sort_dir, - filter: input.filter + filter: input.filter, }); } - protected abstract applyFilter( - items: E[], - filter?: string - ): Promise; + protected abstract applyFilter(items: E[], filter?: string): Promise; protected async applySort( items: E[], @@ -92,7 +104,7 @@ export abstract class InMemorySearchableRepository if (sort && this.sortableFields.includes(sort)) { return [...items].sort((a, b) => { const field = sort as keyof E; - + if (a.props[field] < b.props[field]) { return sort_dir === "asc" ? -1 : 1; } diff --git a/src/@core/@seedwork/domain/repository/repository-contracts.ts b/src/@core/@seedwork/domain/repository/repository-contracts.ts index d878796..13d13ef 100644 --- a/src/@core/@seedwork/domain/repository/repository-contracts.ts +++ b/src/@core/@seedwork/domain/repository/repository-contracts.ts @@ -135,6 +135,7 @@ export interface RepositoryInterface { findAll(): Promise; update(entity: E): Promise; delete(id: string | UniqueEntityId): Promise; + validateEntity(entity: E): void; } export interface SearchableRepositoryInterface< diff --git a/src/@core/@seedwork/domain/tests/validation.ts b/src/@core/@seedwork/domain/tests/validation.ts index 0c9c801..04cbdee 100644 --- a/src/@core/@seedwork/domain/tests/validation.ts +++ b/src/@core/@seedwork/domain/tests/validation.ts @@ -1,6 +1,7 @@ import { objectContaining } from "expect"; -import ValidationError from "../errors/validation.error"; -import ClassValidator from "../validators/class.validator"; +import Entity from "../entity/entity"; +//import ValidationError from "../errors/validation.error"; +import ClassValidatorFields from "../validators/class.validator"; // declare global { // namespace jest { @@ -10,7 +11,7 @@ import ClassValidator from "../validators/class.validator"; // } // } -type Expected = { validator: ClassValidator; data: any } | (() => any); +type Expected = { validator: ClassValidatorFields; data: any } | Entity; //type Expected = any; expect.extend({ @@ -18,31 +19,74 @@ expect.extend({ expected: Expected, received: { [field: string]: string[] } ) { - try { - if (typeof expected === "function") { - expected(); - } else { - expected.validator.validate(expected.data); - } + const isValid = + expected instanceof Entity + ? expected.is_valid + : expected.validator.validate(expected.data); + + if (isValid) { return { pass: false, message: () => `The data is valid`, }; - } catch (e) { - const error = e as ValidationError; - const isMatch = objectContaining(received).asymmetricMatch(error.error); - return isMatch - ? { - pass: true, - message: () => "", - } - : { - pass: false, - message: () => - `The validation errors not contains ${JSON.stringify( - received - )}. Current: ${JSON.stringify(error.error)}`, - }; } + const errors = + expected instanceof Entity + ? expected.error.errors.reduce<{ [key: string]: string[] }>( + (prevValue, current) => { + return {...prevValue, ...current as object} + }, + {} + ) + : expected.validator.errors; + const isMatch = objectContaining(received).asymmetricMatch(errors); + return isMatch + ? { + pass: true, + message: () => "", + } + : { + pass: false, + message: () => + `The validation errors not contains ${JSON.stringify( + received + )}. Current: ${JSON.stringify(errors)}`, + }; }, }); + +//type Expected = { validator: ClassValidatorFields; data: any } | (() => any); + +// expect.extend({ +// containErrorMessages( +// expected: Expected, +// received: { [field: string]: string[] } +// ) { +// try { +// if (typeof expected === "function") { +// expected(); +// } else { +// expected.validator.validate(expected.data); +// } +// return { +// pass: false, +// message: () => `The data is valid`, +// }; +// } catch (e) { +// const error = e as ValidationError; +// const isMatch = objectContaining(received).asymmetricMatch(error.error); +// return isMatch +// ? { +// pass: true, +// message: () => "", +// } +// : { +// pass: false, +// message: () => +// `The validation errors not contains ${JSON.stringify( +// received +// )}. Current: ${JSON.stringify(error.error)}`, +// }; +// } +// }, +// }); diff --git a/src/@core/@seedwork/domain/validators/__tests__/class.validator.ispec.ts b/src/@core/@seedwork/domain/validators/__tests__/class-validator-fields.ispec.ts similarity index 58% rename from src/@core/@seedwork/domain/validators/__tests__/class.validator.ispec.ts rename to src/@core/@seedwork/domain/validators/__tests__/class-validator-fields.ispec.ts index d140225..0d0306e 100644 --- a/src/@core/@seedwork/domain/validators/__tests__/class.validator.ispec.ts +++ b/src/@core/@seedwork/domain/validators/__tests__/class-validator-fields.ispec.ts @@ -1,4 +1,4 @@ -import ClassValidator from "../class.validator"; +import ClassValidatorFields from "../class.validator"; import ValidationError from "../../errors/validation.error"; import { IsNotEmpty, IsNumber, IsString, MaxLength } from "class-validator"; @@ -17,17 +17,18 @@ class StubRules { } } -class StubClassValidator extends ClassValidator { - validate(data: any): void { - super._validate(new StubRules(data)); +class StubClassValidator extends ClassValidatorFields { + validate(data: any): boolean { + return super._validate(new StubRules(data)); } } describe("ClassValidator Integration Tests", () => { it("should throws validation errors", () => { const validator = new StubClassValidator(); - expect(() => validator.validate(null)).toThrow(ValidationError); - expect(validator.errors).toMatchObject({ + //expect(() => validator.validate(null)).toThrow(ValidationError); + expect(validator.validate(null)).toBeFalsy(); + expect(validator.errors).toStrictEqual({ name: [ "name should not be empty", "name must be a string", @@ -39,4 +40,10 @@ describe("ClassValidator Integration Tests", () => { ], }); }); + + it('should be valid', () => { + const validator = new StubClassValidator(); + expect(validator.validate({name: 'name test', money: 5})).toBeTruthy(); + expect(validator.errors).toBeNull(); + }) }); diff --git a/src/@core/@seedwork/domain/validators/__tests__/class-validator-fields.spec.ts b/src/@core/@seedwork/domain/validators/__tests__/class-validator-fields.spec.ts new file mode 100644 index 0000000..d2d709e --- /dev/null +++ b/src/@core/@seedwork/domain/validators/__tests__/class-validator-fields.spec.ts @@ -0,0 +1,59 @@ +import ClassValidatorFields from "../class.validator"; +import * as libValidator from "class-validator"; +//import ValidationError from "../../errors/validation.error"; + +class StubClassValidator extends ClassValidatorFields { + validate(data: any): boolean { + return super._validate(data); + } +} + +describe("ClassValidator Unit Tests", () => { + test("_errors variable and errors method", () => { + const validator = new StubClassValidator(); + expect(validator.errors).toBeNull(); + + validator["_errors"] = { field: ["some error"] }; + expect(validator.errors).toMatchObject({ field: ["some error"] }); + }); + + it("should validate without errors", () => { + const validateSyncSpy = jest.spyOn(libValidator, "validateSync"); + const validator = new StubClassValidator(); + + expect(validator.validate({ field: "value" })).toBeTruthy(); + expect(validator.errors).toBeNull(); + expect(validateSyncSpy).toHaveBeenCalled(); + + validateSyncSpy.mockReturnValue([ + { property: "field", constraints: { isRequired: "some error" } }, + ]); + expect(validator.validate({ field: "value" })).toBeFalsy(); + expect(validator.errors).toStrictEqual({ field: ["some error"] }); + }); + + // it("should validate with throw ValidatorError", () => { + // const validateSyncSpy = jest + // .spyOn(libValidator, "validateSync") + // .mockReturnValue([ + // { + // property: "field", + // constraints: { + // isRequired: "The field is required", + // otherRule: "some error message", + // }, + // }, + // ]); + // const validator = new StubClassValidator(); + + // expect(() => validator.validate({ field: "value" })).toThrow( + // new ValidationError({ + // field: ["The field is required", "some error message"], + // }) + // ); + // expect(validator.errors).toMatchObject({ + // field: ["The field is required", "some error message"], + // }); + // expect(validateSyncSpy).toHaveBeenCalled(); + // }); +}); diff --git a/src/@core/@seedwork/domain/validators/__tests__/class.validator.spec.ts b/src/@core/@seedwork/domain/validators/__tests__/class.validator.spec.ts deleted file mode 100644 index 8c78242..0000000 --- a/src/@core/@seedwork/domain/validators/__tests__/class.validator.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import ClassValidator, { Rules } from "../class.validator"; -import * as libValidator from "class-validator"; -import ValidationError from "../../errors/validation.error"; - -class StubClassValidator extends ClassValidator { - validate(data: any): void { - super._validate(data); - } -} - -describe("ClassValidator Unit Tests", () => { - test("_errors variable and errors method", () => { - const validator = new StubClassValidator(); - expect(validator.errors).toBeNull(); - - validator["_errors"] = { field: ["some error"] }; - expect(validator.errors).toMatchObject({ field: ["some error"] }); - }); - - it("should validate without errors", () => { - const validateSyncSpy = jest.spyOn(libValidator, "validateSync"); - const validator = new StubClassValidator(); - validator.validate({ field: "value" }); - - expect(validator.errors).toBeNull(); - expect(validateSyncSpy).toHaveBeenCalled(); - }); - - it("should validate with throw ValidatorError", () => { - const validateSyncSpy = jest - .spyOn(libValidator, "validateSync") - .mockReturnValue([ - { - property: "field", - constraints: { - isRequired: "The field is required", - otherRule: "some error message", - }, - }, - ]); - const validator = new StubClassValidator(); - - expect(() => validator.validate({ field: "value" })).toThrow( - new ValidationError({ - field: ["The field is required", "some error message"], - }) - ); - expect(validator.errors).toMatchObject({ - field: ["The field is required", "some error message"], - }); - expect(validateSyncSpy).toHaveBeenCalled(); - }); -}); diff --git a/src/@core/@seedwork/domain/validators/class.validator.ts b/src/@core/@seedwork/domain/validators/class.validator.ts index c4fe6ea..c395604 100644 --- a/src/@core/@seedwork/domain/validators/class.validator.ts +++ b/src/@core/@seedwork/domain/validators/class.validator.ts @@ -1,15 +1,30 @@ -import ValidatorInterface, { - ValidationErrorFields, -} from "./validator-interface"; +import ValidatorFieldsInterface, { + FieldsErrors, +} from "./validator-fields-interface"; import { validateSync } from "class-validator"; -import ValidationError from "../errors/validation.error"; +//import ValidationError from "../errors/validation.error"; export type Rules = new (...args: any[]) => any; -export default abstract class ClassValidator implements ValidatorInterface { - protected _errors: ValidationErrorFields = null; +export default abstract class ClassValidatorFields + implements ValidatorFieldsInterface +{ + protected _errors: FieldsErrors = null; - protected _validate(data: object) { + // protected _isValid(data: any): boolean { + // const errors = validateSync(data, {}); + + // if (errors.length > 0) { + // this._errors = {}; + // for (const error of errors) { + // const field = error.property; + // this._errors[field] = Object.values(error.constraints); + // } + // } + // return !this.errors; + // } + + protected _validate(data: any) { const errors = validateSync(data, {}); if (errors.length > 0) { @@ -18,13 +33,14 @@ export default abstract class ClassValidator implements ValidatorInterface { const field = error.property; this._errors[field] = Object.values(error.constraints); } - throw new ValidationError(this._errors); } + return !this.errors; } - get errors(): ValidationErrorFields { + get errors(): FieldsErrors { return this._errors; } - abstract validate(data: any): void; + abstract validate(data: any): boolean; + } diff --git a/src/@core/@seedwork/domain/validators/validator-fields-interface.ts b/src/@core/@seedwork/domain/validators/validator-fields-interface.ts new file mode 100644 index 0000000..ad41c7d --- /dev/null +++ b/src/@core/@seedwork/domain/validators/validator-fields-interface.ts @@ -0,0 +1,10 @@ +export type FieldsErrors = { + [field: string]: string[]; +}; + +export default interface ValidatorFieldsInterface { + // validate(data: any): void; + // isValid(data: any): boolean; + validate(data: any): void; + get errors(): FieldsErrors; +} diff --git a/src/@core/@seedwork/domain/validators/validator-interface.ts b/src/@core/@seedwork/domain/validators/validator-interface.ts deleted file mode 100644 index c33c604..0000000 --- a/src/@core/@seedwork/domain/validators/validator-interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type ValidationErrorFields = { - [field: string]: string[]; -}; - -export default interface ValidatorInterface { - validate(data: any): void; - get errors(): ValidationErrorFields; -} diff --git a/src/@core/@seedwork/domain/validators/validator-rules.ts b/src/@core/@seedwork/domain/validators/validator-rules.ts index 40c05d6..5938aca 100644 --- a/src/@core/@seedwork/domain/validators/validator-rules.ts +++ b/src/@core/@seedwork/domain/validators/validator-rules.ts @@ -1,5 +1,6 @@ import { SimpleValidationError } from "../errors/validation.error"; +//GuardClause export default class ValidatorRules { private constructor(private value: any, private property: string) {} diff --git a/src/@core/@seedwork/domain/value-objects/__tests__/unique-entity-id.spec.ts b/src/@core/@seedwork/domain/value-objects/__tests__/unique-entity-id.spec.ts index 1b45daa..fee558e 100644 --- a/src/@core/@seedwork/domain/value-objects/__tests__/unique-entity-id.spec.ts +++ b/src/@core/@seedwork/domain/value-objects/__tests__/unique-entity-id.spec.ts @@ -1,26 +1,19 @@ import UniqueEntityId from "../unique-entity-id"; import { validate as uuidValidate } from "uuid"; -import InvalidUuidError from "../../errors/invalid-uuid.error"; +//import InvalidUuidError from "../../errors/invalid-uuid.error"; function mockValidateMethod() { return jest.spyOn(UniqueEntityId.prototype as any, "validate"); } describe("UniqueEntityId Unit Tests", () => { - it("should throw error when uuid is invalid", () => { - const validateMethodMock = mockValidateMethod(); - expect(() => new UniqueEntityId("fake id")).toThrow( - new InvalidUuidError() - ); - expect(validateMethodMock).toHaveBeenCalled(); - }); it("should accept a uuid passed in constructor", () => { - const validateMethodMock = mockValidateMethod(); + const isValidMethodMock = mockValidateMethod(); const uuid = "5490020a-e866-4229-9adc-aa44b83234c4"; const id = new UniqueEntityId(uuid); expect(id.value).toBe(uuid); - expect(validateMethodMock).toHaveBeenCalled(); + expect(isValidMethodMock).toHaveBeenCalled(); }); it("should define a id when pass id not defined in constructor", () => { @@ -29,4 +22,30 @@ describe("UniqueEntityId Unit Tests", () => { expect(uuidValidate(id.value)).toBeTruthy(); expect(validateMethodMock).toHaveBeenCalled(); }); + + // it("should throw error when uuid is invalid", () => { + // const validateMethodMock = mockIsValidMethod(); + // expect(() => new UniqueEntityId("fake id")).toThrow( + // new InvalidUuidError() + // ); + // expect(validateMethodMock).toHaveBeenCalled(); + // }); + + test("isValid method", () => { + let vo = new UniqueEntityId(); + expect(vo['validate']()).toBeTruthy(); + + vo = new UniqueEntityId("5490020a-e866-4229-9adc-aa44b83234c4"); + expect(vo['validate']()).toBeTruthy(); + + vo = new UniqueEntityId("fake id"); + const spyHasError = jest.spyOn(vo.error, 'hasError'); + expect(vo['validate']()).toBeFalsy(); + expect(vo.error.errors).toStrictEqual(['ID must be a valid UUID']); + expect(spyHasError).toBeCalled(); + expect(spyHasError.mock.results[0].value).toBeTruthy(); + + vo['validate'](); + expect(spyHasError.mock.results[1].value).toBeTruthy(); + }); }); diff --git a/src/@core/@seedwork/domain/value-objects/__tests__/value-object.spec.ts b/src/@core/@seedwork/domain/value-objects/__tests__/value-object.spec.ts index 913b1ba..33443c6 100644 --- a/src/@core/@seedwork/domain/value-objects/__tests__/value-object.spec.ts +++ b/src/@core/@seedwork/domain/value-objects/__tests__/value-object.spec.ts @@ -1,11 +1,24 @@ +import ErrorBag from "../../errors/error-bag"; import ValueObject from "../../value-objects/value-object"; -class StubValueObject extends ValueObject {} +class StubValueObject extends ValueObject { + protected validate(): boolean { + if (this.error.hasError()) { + return false; + } + if (this.value === "invalid") { + this.error.add("The value is invalid"); + } + return this.error.notHasError(); + } +} describe("ValueObject Unit Tests", () => { it("should set value", () => { + const spyValidateMethod = jest.spyOn(StubValueObject.prototype as any, 'validate'); const vo1 = new StubValueObject("string value"); expect(vo1.value).toBe("string value"); + expect(spyValidateMethod).toBeCalled(); const vo2 = new StubValueObject({ prop1: "value1" }); expect(vo2.value).toMatchObject({ prop1: "value1" }); @@ -44,4 +57,29 @@ describe("ValueObject Unit Tests", () => { "Cannot assign to read only property 'param2' of object '#'" ); }); + + test('is_valid getter', () => { + let vo = new StubValueObject("valid"); + expect(vo.is_valid).toBeTruthy(); + + vo = new StubValueObject("invalid"); + expect(vo.is_valid).toBeFalsy(); + }); + + test("validate method and error variable", () => { + const mockValidateMethod = jest.spyOn( + StubValueObject.prototype as any, + "validate" + ); + let vo = new StubValueObject("valid"); + expect(vo["validate"]()).toBeTruthy(); + expect(vo.error).toStrictEqual(new ErrorBag()); + expect(mockValidateMethod).toHaveBeenCalled(); + + const errorBag = new ErrorBag(); + errorBag.add("The value is invalid"); + vo = new StubValueObject("invalid"); + expect(vo["validate"]()).toBeFalsy(); + expect(vo.error).toStrictEqual(errorBag); + }); }); diff --git a/src/@core/@seedwork/domain/value-objects/unique-entity-id.ts b/src/@core/@seedwork/domain/value-objects/unique-entity-id.ts index fab3611..af6cc9e 100644 --- a/src/@core/@seedwork/domain/value-objects/unique-entity-id.ts +++ b/src/@core/@seedwork/domain/value-objects/unique-entity-id.ts @@ -1,17 +1,27 @@ -import ValueObject from './value-object'; -import { v4 as uuidv4, validate as uuidValidate } from 'uuid'; -import InvalidUuidError from '../errors/invalid-uuid.error'; +import ValueObject from "./value-object"; +import { v4 as uuidv4, validate as uuidValidate } from "uuid"; +//import InvalidUuidError from "../errors/invalid-uuid.error"; export default class UniqueEntityId extends ValueObject { constructor(id?: string) { super(id || uuidv4()); - this.validate(); } - private validate() { + // private validate() { + // const isValid = uuidValidate(this._value); + // if (!isValid) { + // throw new InvalidUuidError() + // } + // } + + protected validate(): boolean { + if (this.error.hasError()) { + return false; + } const isValid = uuidValidate(this._value); if (!isValid) { - throw new InvalidUuidError() + this.error.add("ID must be a valid UUID"); } + return isValid; } } diff --git a/src/@core/@seedwork/domain/value-objects/value-object.ts b/src/@core/@seedwork/domain/value-objects/value-object.ts index 955fc4b..6547571 100644 --- a/src/@core/@seedwork/domain/value-objects/value-object.ts +++ b/src/@core/@seedwork/domain/value-objects/value-object.ts @@ -1,16 +1,24 @@ import { deepFreeze } from "../utils/object"; - +import ErrorBag from '../errors/error-bag'; export default abstract class ValueObject { protected readonly _value: Value; + public readonly error = new ErrorBag; constructor(value: Value) { this._value = deepFreeze(value); + this.validate(); } get value(): Value { return this._value; } + protected abstract validate(): boolean; + + get is_valid(): boolean{ + return this.error.notHasError(); + } + public toString = (): string => { if (typeof this._value !== "object") { try { diff --git a/src/@core/category/application/use-cases/__tests__/delete-category.use-case.spec.ts b/src/@core/category/application/use-cases/__tests__/delete-category.use-case.spec.ts new file mode 100644 index 0000000..633f87c --- /dev/null +++ b/src/@core/category/application/use-cases/__tests__/delete-category.use-case.spec.ts @@ -0,0 +1,23 @@ +import Category from "../../../domain/entities/category"; +import CategoryInMemoryRepository from "../../../infra/repositories/category-in-memory.repository"; +import DeleteCategoryUseCase from "../delete-category.use-case"; + +describe("DeleteCategoryUseCase Unit Tests", () => { + let useCase: DeleteCategoryUseCase; + let repository: CategoryInMemoryRepository; + + beforeEach(() => { + repository = new CategoryInMemoryRepository(); + useCase = new DeleteCategoryUseCase(repository); + }); + + it("should update a category", async () => { + const items = [new Category({ name: "test 1" })]; + repository.items = items; + let output = await useCase.execute({ + id: items[0].id, + }); + expect(output).toBe(undefined); + expect(repository.items).toHaveLength(0); + }); +}); diff --git a/src/@core/category/application/use-cases/__tests__/get-category.use-case.spec.ts b/src/@core/category/application/use-cases/__tests__/get-category.use-case.spec.ts new file mode 100644 index 0000000..b9e887b --- /dev/null +++ b/src/@core/category/application/use-cases/__tests__/get-category.use-case.spec.ts @@ -0,0 +1,23 @@ +import GetCategoryUseCase, { Input } from "../get-category.use-case"; +import CategoryInMemoryRepository from "../../../infra/repositories/category-in-memory.repository"; +import Category from "../../../domain/entities/category"; + +describe("GetCategory Unit Tests", () => { + let useCase: GetCategoryUseCase; + let repository: CategoryInMemoryRepository; + + beforeEach(() => { + repository = new CategoryInMemoryRepository(); + useCase = new GetCategoryUseCase(repository); + }); + + it("should returns the output with a category", async () => { + const items = [ + new Category({ name: "test 1" }), + ]; + repository.items = items; + + const output = await useCase.execute({id: items[0].id}); + expect(output).toStrictEqual(items[0].toJSON()); + }); +}); \ No newline at end of file diff --git a/src/@core/category/application/use-cases/__tests__/update-category.use-case.spec.ts b/src/@core/category/application/use-cases/__tests__/update-category.use-case.spec.ts new file mode 100644 index 0000000..591b4f0 --- /dev/null +++ b/src/@core/category/application/use-cases/__tests__/update-category.use-case.spec.ts @@ -0,0 +1,73 @@ +import Category from "../../../domain/entities/category"; +import CategoryInMemoryRepository from "../../../infra/repositories/category-in-memory.repository"; +import UpdateCategoryUseCase from "../update-category.use-case"; + +describe("UpdateCategoryUseCase Unit Tests", () => { + let useCase: UpdateCategoryUseCase; + let repository: CategoryInMemoryRepository; + + beforeEach(() => { + repository = new CategoryInMemoryRepository(); + useCase = new UpdateCategoryUseCase(repository); + }); + + it("should update a category", async () => { + const items = [new Category({ name: "test 1" })]; + repository.items = items; + let output = await useCase.execute({ + id: items[0].id, + name: "test changed", + }); + expect(output).toStrictEqual({ + id: repository.items[0].id, + name: "test changed", + description: null, + is_active: true, + created_at: repository.items[0].props.created_at, + }); + + output = await useCase.execute({ + id: items[0].id, + name: "test changed", + description: "description changed", + }); + + expect(output).toStrictEqual({ + id: repository.items[0].id, + name: "test changed", + description: "description changed", + is_active: true, + created_at: repository.items[0].props.created_at, + }); + + output = await useCase.execute({ + id: items[0].id, + name: "test changed", + description: "description changed", + is_active: false, + }); + + expect(output).toStrictEqual({ + id: repository.items[0].id, + name: "test changed", + description: "description changed", + is_active: false, + created_at: repository.items[0].props.created_at, + }); + + output = await useCase.execute({ + id: items[0].id, + name: "test changed", + description: "description changed", + is_active: true, + }); + + expect(output).toStrictEqual({ + id: repository.items[0].id, + name: "test changed", + description: "description changed", + is_active: true, + created_at: repository.items[0].props.created_at, + }); + }); +}); diff --git a/src/@core/category/application/use-cases/create-category.use-case.ts b/src/@core/category/application/use-cases/create-category.use-case.ts index 380e239..7596bbe 100644 --- a/src/@core/category/application/use-cases/create-category.use-case.ts +++ b/src/@core/category/application/use-cases/create-category.use-case.ts @@ -1,12 +1,16 @@ import CategoryRepository from "../../domain/repositories/category.repository"; import { CategoryOutputDto } from "./dto/category.dto"; import Category from "../../domain/entities/category"; - -export class CreateCategoryUseCase { +import UseCase from '../../../@seedwork/application/use-case'; +import BadEntityOperationError from '../../../@seedwork/application/errors/bad-entity-operation.error'; +export class CreateCategoryUseCase implements UseCase { constructor(private categoryRepository: CategoryRepository) {} async execute(input: Input): Promise { const entity = Input.toEntity(input); + if(!entity.is_valid){ + throw new BadEntityOperationError(entity.error); + } await this.categoryRepository.insert(entity); return this.toOutput(entity); } diff --git a/src/@core/category/application/use-cases/delete-category.use-case.ts b/src/@core/category/application/use-cases/delete-category.use-case.ts new file mode 100644 index 0000000..4abc2e2 --- /dev/null +++ b/src/@core/category/application/use-cases/delete-category.use-case.ts @@ -0,0 +1,19 @@ +import CategoryRepository from "../../domain/repositories/category.repository"; +import UseCase from "../../../@seedwork/application/use-case"; + +export class DeleteCategoryUseCase implements UseCase { + constructor(private categoryRepository: CategoryRepository) {} + + async execute(input: Input): Promise { + const entity = await this.categoryRepository.findById(input.id); + return this.categoryRepository.delete(entity.id); + } +} + +export default DeleteCategoryUseCase; + +export class Input { + id: string; +} + +export type Output = void; diff --git a/src/@core/category/application/use-cases/get-category.use-case.ts b/src/@core/category/application/use-cases/get-category.use-case.ts new file mode 100644 index 0000000..abd1a5c --- /dev/null +++ b/src/@core/category/application/use-cases/get-category.use-case.ts @@ -0,0 +1,27 @@ +import CategoryRepository from "../../domain/repositories/category.repository"; +import { CategoryOutputDto } from "./dto/category.dto"; +import Category from "../../domain/entities/category"; +import UseCase from '../../../@seedwork/application/use-case'; + +export class GetCategoryUseCase implements UseCase { + constructor(private categoryRepository: CategoryRepository) {} + + async execute(input: Input): Promise { + const entity = await this.categoryRepository.findById(input.id); + return this.toOutput(entity); + } + + toOutput(entity: Category): Output { + return Output.fromEntity(entity); + } +} + +export default GetCategoryUseCase; + +export class Input { + id: string; +} + +export class Output extends CategoryOutputDto { + +} diff --git a/src/@core/category/application/use-cases/list-categories.use-case.ts b/src/@core/category/application/use-cases/list-categories.use-case.ts index ac8a7bb..29b5c2a 100644 --- a/src/@core/category/application/use-cases/list-categories.use-case.ts +++ b/src/@core/category/application/use-cases/list-categories.use-case.ts @@ -5,8 +5,9 @@ import CategoryRepository, { } from "../../domain/repositories/category.repository"; import { PaginationOutputDto } from "../../../@seedwork/application/dto/pagination-output.dto"; import { CategoryOutputDto } from "./dto/category.dto"; +import UseCase from '../../../@seedwork/application/use-case'; -export class ListCategoriesUseCase { +export class ListCategoriesUseCase implements UseCase { constructor(private categoryRepository: CategoryRepository) {} async execute(input: Input = null): Promise { diff --git a/src/@core/category/application/use-cases/update-category.use-case.ts b/src/@core/category/application/use-cases/update-category.use-case.ts new file mode 100644 index 0000000..2223d5a --- /dev/null +++ b/src/@core/category/application/use-cases/update-category.use-case.ts @@ -0,0 +1,48 @@ +import CategoryRepository from "../../domain/repositories/category.repository"; +import { CategoryOutputDto } from "./dto/category.dto"; +import Category from "../../domain/entities/category"; +import UseCase from '../../../@seedwork/application/use-case'; +import BadEntityOperationError from '../../../@seedwork/application/errors/bad-entity-operation.error'; + +export class UpdateCategoryUseCase implements UseCase { + constructor(private categoryRepository: CategoryRepository) {} + + async execute(input: Input): Promise { + + const entity = await this.categoryRepository.findById(input.id); + + entity.update(input.name, input.description); + + if(input.is_active === true){ + entity.activate() + } + + if(input.is_active === false){ + entity.deactivate(); + } + + if(!entity.is_valid){ + throw new BadEntityOperationError(entity.error); + } + + await this.categoryRepository.update(entity); + return this.toOutput(entity); + } + + toOutput(entity: Category): Output { + return Output.fromEntity(entity); + } +} + +export default UpdateCategoryUseCase; + +export class Input { + id: string; + name: string; + description?: string; + is_active?: boolean; +} + +export class Output extends CategoryOutputDto { + +} diff --git a/src/@core/category/domain/entities/__tests__/category.ispec.ts b/src/@core/category/domain/entities/__tests__/category.ispec.ts index 87fe71d..41af167 100644 --- a/src/@core/category/domain/entities/__tests__/category.ispec.ts +++ b/src/@core/category/domain/entities/__tests__/category.ispec.ts @@ -1,40 +1,47 @@ //import { SimpleValidationError } from "./../../@seedwork/errors/validation.error"; -import Category from "../category"; +import UniqueEntityId from "../../../../@seedwork/domain/value-objects/unique-entity-id"; +import Category, { CategoryProperties } from "../category"; describe("Category Integration Tests", () => { it("should a valid entity", () => { - expect.assertions(0); + const arrange: CategoryProperties[] = [ + {name:'Movie'}, + {name:'Movie', description: null}, + {name:'Movie', is_active: true}, + {name:'Movie', is_active: false}, + {name:'Movie', description: 'description test', is_active: true}, + ] - let category = new Category({ - name: "Movie", + arrange.forEach(i => { + const category = new Category(i); + expect(category.is_valid).toBeTruthy(); }); - category._validate(); - category.props.description = null; - category.props.is_active = false; - - category._validate(); + arrange.forEach(i => { + const category = new Category(i, new UniqueEntityId("5490020a-e866-4229-9adc-aa44b83234c4")); + expect(category.is_valid).toBeTruthy(); + }); }); it("should a invalid entity using name field", () => { - expect(() => new Category({ name: null })).containErrorMessages({ + expect( new Category({ name: null })).containErrorMessages({ name: [ "name should not be empty", "name must be a string", "name must be shorter than or equal to 255 characters", ], }); - expect(() => new Category({ name: "" })).containErrorMessages({ + expect( new Category({ name: "" })).containErrorMessages({ name: ["name should not be empty"], }); - expect(() => new Category({ name: "t".repeat(256) })).containErrorMessages({ + expect( new Category({ name: "t".repeat(256) })).containErrorMessages({ name: ["name must be shorter than or equal to 255 characters"], }); }); it("should a invalid entity using description field", () => { - expect(() => new Category({ name: null, description: 5 as any })).containErrorMessages({ + expect(new Category({ name: null, description: 5 as any })).containErrorMessages({ description: [ "description must be a string", ], @@ -42,7 +49,7 @@ describe("Category Integration Tests", () => { }); it("should a invalid entity using is_active field", () => { - expect(() => new Category({ name: null, is_active: 5 as any })).containErrorMessages({ + expect( new Category({ name: null, is_active: 5 as any })).containErrorMessages({ is_active: [ "is_active must be a boolean value", ], @@ -100,3 +107,104 @@ describe("Category Integration Tests", () => { // ); // }); }); + + +// describe("Category Integration Tests", () => { + +// it("should a valid entity", () => { +// expect.assertions(0); + +// let category = new Category({ +// name: "Movie", +// }); +// category._validate(); + +// category.props.description = null; +// category.props.is_active = false; + +// category._validate(); +// }); + +// it("should a invalid entity using name field", () => { +// expect(() => new Category({ name: null })).containErrorMessages({ +// name: [ +// "name should not be empty", +// "name must be a string", +// "name must be shorter than or equal to 255 characters", +// ], +// }); +// expect(() => new Category({ name: "" })).containErrorMessages({ +// name: ["name should not be empty"], +// }); +// expect(() => new Category({ name: "t".repeat(256) })).containErrorMessages({ +// name: ["name must be shorter than or equal to 255 characters"], +// }); +// }); + +// it("should a invalid entity using description field", () => { +// expect(() => new Category({ name: null, description: 5 as any })).containErrorMessages({ +// description: [ +// "description must be a string", +// ], +// }); +// }); + +// it("should a invalid entity using is_active field", () => { +// expect(() => new Category({ name: null, is_active: 5 as any })).containErrorMessages({ +// is_active: [ +// "is_active must be a boolean value", +// ], +// }); +// }); + +// // it("should a invalid entity using name field", () => { +// // expect(() => new Category({ name: null })).containErrorMessages({ +// // name: [ +// // "name should not be empty", +// // "name must be a string", +// // "name must be shorter than or equal to 255 characters", +// // ], +// // }); +// // expect(() => new Category({ name: "" })).containErrorMessages({ +// // name: ["name should not be empty"], +// // }); +// // }); + +// // it("should a invalid entity", () => { +// // let category = new Category({ +// // name: "", +// // }); +// // expect(() => category._validate()).toThrow( +// // new SimpleValidationError("The name is required") +// // ); +// // validar quando é string +// // category = new Category({ +// // name: "t".repeat(256), +// // }); +// // expect(() => category._validate()).toThrow( +// // new SimpleValidationError( +// // "The name must be less or equal than 255 characters" +// // ) +// // ); + +// // category = new Category({ +// // name: 'Movie', +// // description: 5 as any, +// // }); +// // expect(() => category._validate()).toThrow( +// // new SimpleValidationError( +// // "The description must be a string" +// // ) +// // ); + +// // category = new Category({ +// // name: 'Movie', +// // }); +// // category.props.is_active = 1 as any; +// // expect(() => category._validate()).toThrow( +// // new SimpleValidationError( +// // "The is_active must be a boolean" +// // ) +// // ); +// // }); +// }); diff --git a/src/@core/category/domain/entities/__tests__/category.spec.ts b/src/@core/category/domain/entities/__tests__/category.spec.ts index 391bfa3..9d74f30 100644 --- a/src/@core/category/domain/entities/__tests__/category.spec.ts +++ b/src/@core/category/domain/entities/__tests__/category.spec.ts @@ -3,14 +3,14 @@ import Category from "../category"; describe("Category Unit Tests", () => { beforeEach(() => { - Category.prototype.validate = jest.fn() + Category.prototype['validate'] = jest.fn(); }) - test("constructor of Category", () => { + + it("should create a category", () => { const category1 = new Category({ name: "Movie", }); - expect(Category.prototype.validate).toBeCalled(); - + expect(Category.prototype['validate']).toBeCalled(); expect(category1.props).toMatchObject({ name: "Movie", description: null, @@ -46,7 +46,7 @@ describe("Category Unit Tests", () => { is_active: false, }); category.activate(); - expect(Category.prototype.validate).toBeCalled(); + expect(Category.prototype['validate']).toBeCalled(); expect(category.is_active).toBeTruthy(); }); @@ -56,7 +56,7 @@ describe("Category Unit Tests", () => { is_active: true, }); category.deactivate(); - expect(Category.prototype.validate).toBeCalled(); + expect(Category.prototype['validate']).toBeCalled(); expect(category.is_active).toBeFalsy(); }); @@ -65,7 +65,7 @@ describe("Category Unit Tests", () => { name: "Movie", }); category.update("Documentary", "some description"); - expect(Category.prototype.validate).toBeCalledTimes(2); + expect(Category.prototype['validate']).toBeCalled(); expect(category.name).toBe("Documentary"); expect(category.description).toBe("some description"); }); @@ -92,3 +92,96 @@ describe("Category Unit Tests", () => { expect(category.props.description).toBe(null); }); }); + + +// describe("Category Unit Tests", () => { +// beforeEach(() => { +// Category.prototype.validate = jest.fn() +// }) +// test("constructor of Category", () => { +// const category1 = new Category({ +// name: "Movie", +// }); +// expect(Category.prototype.validate).toBeCalled(); + +// expect(category1.props).toMatchObject({ +// name: "Movie", +// description: null, +// is_active: true, +// created_at: category1.props.created_at, +// }); +// expect(category1.props.created_at).toBeInstanceOf(Date); + +// const date = new Date(); +// const category2 = new Category({ +// name: "Movie", +// description: "some description", +// is_active: false, +// created_at: date, +// }); +// expect(category2.props).toMatchObject({ +// name: "Movie", +// description: "some description", +// is_active: false, +// created_at: date, +// }); + +// const category3 = new Category({ +// name: "Movie", +// is_active: true, +// }); +// expect(category3.props.is_active).toBeTruthy(); +// }); + +// it("should active a category", () => { +// const category = new Category({ +// name: "Filmes", +// is_active: false, +// }); +// category.activate(); +// expect(Category.prototype.validate).toBeCalled(); +// expect(category.is_active).toBeTruthy(); +// }); + +// test("should disable a category", () => { +// const category = new Category({ +// name: "Filmes", +// is_active: true, +// }); +// category.deactivate(); +// expect(Category.prototype.validate).toBeCalled(); +// expect(category.is_active).toBeFalsy(); +// }); + +// test("should be updated props", () => { +// const category = new Category({ +// name: "Movie", +// }); +// category.update("Documentary", "some description"); +// expect(Category.prototype.validate).toBeCalledTimes(2); +// expect(category.name).toBe("Documentary"); +// expect(category.description).toBe("some description"); +// }); + +// test("props getters", () => { +// const category = new Category({ +// name: "Movie", +// description: "any description", +// }); +// category.update("Documentary", "some description"); +// expect(category.props.name).toBe("Documentary"); +// expect(category.props.description).toBe("some description"); +// }); + +// test("description prop setter", () => { +// const category = new Category({ +// name: "Movie", +// }); +// category["description"] = undefined; +// expect(category.props.description).toBe(null); +// category["description"] = "value"; +// expect(category.props.description).toBe("value"); +// category["description"] = null; +// expect(category.props.description).toBe(null); +// }); +// }); diff --git a/src/@core/category/domain/entities/category.ts b/src/@core/category/domain/entities/category.ts index f2d8023..bddb9df 100644 --- a/src/@core/category/domain/entities/category.ts +++ b/src/@core/category/domain/entities/category.ts @@ -1,4 +1,4 @@ -import entity from "../../../@seedwork/domain/entity/aggregate-root"; +import AggregateRoot from "../../../@seedwork/domain/entity/aggregate-root"; import UniqueEntityId from "../../../@seedwork/domain/value-objects/unique-entity-id"; import CategoryValidatorFactory from "../validators/category.validator"; import ValidatorRules from "../../../@seedwork/domain/validators/validator-rules"; @@ -9,8 +9,9 @@ export type CategoryProperties = { is_active?: boolean; created_at?: Date; }; -export class Category extends entity { - constructor(readonly props: CategoryProperties, id?: UniqueEntityId) { +export class Category extends AggregateRoot { + + constructor(props: CategoryProperties, id?: UniqueEntityId) { super(props, id); this.description = this.props.description; this.props.is_active = props.is_active ?? this.activate(); @@ -35,8 +36,33 @@ export class Category extends entity { .boolean(); } - validate() { - CategoryValidatorFactory.create().validate({ id: this._id, ...this.props }); + //validate() { + // CategoryValidatorFactory.create().validate({ + // id: this.uniqueEntityId, + // ...this.props, + // }); + //} + + protected validate() { + this.error.clear(); + + if (this.uniqueEntityId instanceof UniqueEntityId && !this.uniqueEntityId.is_valid) { + this.error.add({ id: this.uniqueEntityId.error.errors as string[] }); + } + + const categoryValidator = CategoryValidatorFactory.create(); + const isValid = categoryValidator.validate({ + id: this.uniqueEntityId, + ...this.props + }); + + if (!isValid) { + for (const field of Object.keys(categoryValidator.errors)) { + this.error.add({ [field]: categoryValidator.errors[field] }); + } + } + + return this.error.hasError(); } activate(): true { @@ -66,4 +92,4 @@ export class Category extends entity { } } -export default Category; \ No newline at end of file +export default Category; diff --git a/src/@core/category/domain/validators/category.validator.ts b/src/@core/category/domain/validators/category.validator.ts index 55cc673..cc3bce2 100644 --- a/src/@core/category/domain/validators/category.validator.ts +++ b/src/@core/category/domain/validators/category.validator.ts @@ -1,7 +1,14 @@ -import domainValidators from '../../../@seedwork/domain/value-objects/unique-entity-id'; import { CategoryProperties } from "../entities/category"; -import ClassValidator from "../../../@seedwork/domain/validators/class.validator"; -import { IsBoolean, IsInstance, IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator"; +import ClassValidatorFields from "../../../@seedwork/domain/validators/class.validator"; +import { + IsBoolean, + IsInstance, + IsNotEmpty, + IsOptional, + IsString, + MaxLength, +} from "class-validator"; +import UniqueEntityId from "../../../@seedwork/domain/value-objects/unique-entity-id"; export class CategoryValidatorFactory { static create() { @@ -9,12 +16,12 @@ export class CategoryValidatorFactory { } } -export default CategoryValidatorFactory +export default CategoryValidatorFactory; export class CategoryRules { - @IsInstance(domainValidators) + @IsInstance(UniqueEntityId) @IsNotEmpty() - id: domainValidators + id: UniqueEntityId; @MaxLength(255) @IsString() @@ -27,16 +34,23 @@ export class CategoryRules { @IsBoolean() @IsOptional() - is_active: boolean + is_active: boolean; - constructor(data: CategoryProperties & {id: domainValidators}) { + constructor(data: CategoryProperties & { id: UniqueEntityId }) { Object.assign(this, data); } } -export class CategoryValidator extends ClassValidator { - validate(data: CategoryProperties & {id: domainValidators}): void { - const rules = new CategoryRules(data); - super._validate(rules); +export class CategoryValidator extends ClassValidatorFields { + validate(data: CategoryProperties & { id: UniqueEntityId }): boolean { + return super._validate(this.makeRules(data)); + } + + // isValid(data: CategoryProperties & { id: UniqueEntityId }): boolean { + // return super._isValid(this.makeRules(data)); + // } + + private makeRules(data: any) { + return new CategoryRules(data); } }