From 085a390aa68424768e7eeaff3d26f0639bd4f1ff Mon Sep 17 00:00:00 2001 From: david Date: Thu, 20 Feb 2025 19:55:24 +0100 Subject: [PATCH] . --- .vscode/settings.json | 1 - apps/server/jest.config.js | 7 + apps/server/package.json | 1 + .../server/src/common/domain/domain-entity.ts | 16 +- apps/server/src/common/domain/index.ts | 1 - apps/server/src/common/domain/result.spec.ts | 45 ----- .../value-objects/email-address.spec.ts | 8 +- .../domain/value-objects/email-address.ts | 50 ++++++ .../src/common/domain/value-objects/index.ts | 6 + .../common/domain/value-objects/name.spec.ts | 41 +++++ .../src/common/domain/value-objects/name.ts | 61 +++++++ .../domain/value-objects/phone-number.ts | 63 +++++++ .../domain/value-objects/postal-address.ts | 95 ++++++++++ .../common/domain/value-objects/slug.spec.ts | 41 +++++ .../src/common/domain/value-objects/slug.ts | 48 +++++ .../common/domain/value-objects/tin-number.ts | 51 ++++++ .../common/domain/value-objects/unique-id.ts | 39 ++-- .../domain/value-objects/value-object.ts | 28 ++- .../value-objects/value-objects.spec.ts | 39 ++++ .../src/common/helpers/collection.spec.ts | 40 +++++ apps/server/src/common/helpers/collection.ts | 82 +++++++++ apps/server/src/common/helpers/index.ts | 4 + apps/server/src/common/helpers/maybe.spec.ts | 31 ++++ apps/server/src/common/helpers/maybe.ts | 34 ++++ apps/server/src/common/helpers/result.spec.ts | 73 ++++++++ .../src/common/{domain => helpers}/result.ts | 7 + apps/server/src/common/helpers/utils.spec.ts | 46 +++++ apps/server/src/common/helpers/utils.ts | 12 ++ .../common/infrastructure/sequelize/index.ts | 1 + .../sequelize/sequelize-mapper.spec.ts | 67 +++++++ .../sequelize/sequelize-mapper.ts | 121 +++++++++++++ .../sequelize/sequelize-repository.ts | 3 +- .../domain/aggregates/authenticated-user.ts | 20 +-- .../contexts/auth/domain/aggregates/user.ts | 20 +-- .../auth/domain/entities/jwt-payload.ts | 6 +- .../auth/domain/entities/login-data.ts | 6 +- .../auth/domain/entities/logout-data.ts | 4 +- .../auth/domain/entities/register-data.ts | 6 +- .../auth/domain/entities/tab-context.ts | 6 +- .../domain/value-objects/auth-user-roles.ts | 2 +- .../domain/value-objects/email-address.ts | 28 --- .../value-objects/hash-password.spec.ts | 41 +++-- .../domain/value-objects/hash-password.ts | 20 ++- .../auth/domain/value-objects/index.ts | 1 - .../domain/value-objects/plain-password.ts | 16 +- .../auth/domain/value-objects/token.ts | 16 +- .../auth/domain/value-objects/username.ts | 16 +- .../mappers/authenticated-user.mapper.ts | 4 +- .../infraestructure/mappers/user.mapper.ts | 4 +- .../middleware/passport-auth.middleware.ts | 2 +- .../sequelize/user.repository.ts | 4 +- .../listUsers/list-users.controller.ts | 2 +- .../listUsers/list-users.presenter.ts | 1 + .../controllers/logout/logout.controller.ts | 3 +- .../presentation/dto/user.response.dto.ts | 1 + .../application/company-service.interface.ts | 0 .../companies/application/company.service.ts | 0 .../contexts/companies/application/index.ts | 1 + .../application/list-companies/index.ts | 1 + .../list-companies/list-users.use-case.ts | 17 ++ .../companies/domain/aggregates/company.ts | 130 ++++++++++++++ .../companies/domain/aggregates/index.ts | 1 + .../services/company-service.interface.ts | 7 + .../domain/services/company.service.ts | 22 +++ .../infraestructure/mappers/company.mapper.ts | 100 +++++++++++ .../infraestructure/mappers/index.ts | 1 + .../sequelize/company.model.ts | 170 ++++++++++++++++++ .../sequelize/company.repository.ts | 81 +++++++++ .../infraestructure/sequelize/index.ts | 9 + .../presentation/controllers/index.ts | 1 + .../controllers/list-companies/index.ts | 15 ++ .../list-companies.controller.ts | 37 ++++ .../list-companies.presenter.ts | 38 ++++ .../presentation/dto/companies.request.dto.ts | 1 + .../dto/companies.response.dto.ts | 27 +++ .../dto/companies.validation.dto.ts | 3 + .../companies/presentation/dto/index.ts | 3 + .../contexts/companies/presentation/index.ts | 2 + apps/server/src/routes/company.routes.ts | 41 +---- apps/server/src/routes/user.routes.ts | 8 +- pnpm-lock.yaml | 8 + 81 files changed, 1876 insertions(+), 239 deletions(-) create mode 100644 apps/server/jest.config.js delete mode 100644 apps/server/src/common/domain/result.spec.ts rename apps/server/src/{contexts/auth => common}/domain/value-objects/email-address.spec.ts (90%) create mode 100644 apps/server/src/common/domain/value-objects/email-address.ts create mode 100644 apps/server/src/common/domain/value-objects/name.spec.ts create mode 100644 apps/server/src/common/domain/value-objects/name.ts create mode 100644 apps/server/src/common/domain/value-objects/phone-number.ts create mode 100644 apps/server/src/common/domain/value-objects/postal-address.ts create mode 100644 apps/server/src/common/domain/value-objects/slug.spec.ts create mode 100644 apps/server/src/common/domain/value-objects/slug.ts create mode 100644 apps/server/src/common/domain/value-objects/tin-number.ts create mode 100644 apps/server/src/common/domain/value-objects/value-objects.spec.ts create mode 100644 apps/server/src/common/helpers/collection.spec.ts create mode 100644 apps/server/src/common/helpers/collection.ts create mode 100644 apps/server/src/common/helpers/index.ts create mode 100644 apps/server/src/common/helpers/maybe.spec.ts create mode 100644 apps/server/src/common/helpers/maybe.ts create mode 100644 apps/server/src/common/helpers/result.spec.ts rename apps/server/src/common/{domain => helpers}/result.ts (90%) create mode 100644 apps/server/src/common/helpers/utils.spec.ts create mode 100644 apps/server/src/common/helpers/utils.ts create mode 100644 apps/server/src/common/infrastructure/sequelize/sequelize-mapper.spec.ts create mode 100644 apps/server/src/common/infrastructure/sequelize/sequelize-mapper.ts delete mode 100644 apps/server/src/contexts/auth/domain/value-objects/email-address.ts delete mode 100644 apps/server/src/contexts/companies/application/company-service.interface.ts delete mode 100644 apps/server/src/contexts/companies/application/company.service.ts create mode 100644 apps/server/src/contexts/companies/application/list-companies/index.ts create mode 100644 apps/server/src/contexts/companies/application/list-companies/list-users.use-case.ts create mode 100644 apps/server/src/contexts/companies/domain/aggregates/company.ts create mode 100644 apps/server/src/contexts/companies/domain/aggregates/index.ts create mode 100644 apps/server/src/contexts/companies/domain/services/company-service.interface.ts create mode 100644 apps/server/src/contexts/companies/domain/services/company.service.ts create mode 100644 apps/server/src/contexts/companies/infraestructure/mappers/company.mapper.ts create mode 100644 apps/server/src/contexts/companies/infraestructure/mappers/index.ts create mode 100644 apps/server/src/contexts/companies/infraestructure/sequelize/company.model.ts create mode 100644 apps/server/src/contexts/companies/infraestructure/sequelize/company.repository.ts create mode 100644 apps/server/src/contexts/companies/infraestructure/sequelize/index.ts create mode 100644 apps/server/src/contexts/companies/presentation/controllers/index.ts create mode 100644 apps/server/src/contexts/companies/presentation/controllers/list-companies/index.ts create mode 100644 apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.controller.ts create mode 100644 apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.presenter.ts create mode 100644 apps/server/src/contexts/companies/presentation/dto/companies.request.dto.ts create mode 100644 apps/server/src/contexts/companies/presentation/dto/companies.response.dto.ts create mode 100644 apps/server/src/contexts/companies/presentation/dto/companies.validation.dto.ts create mode 100644 apps/server/src/contexts/companies/presentation/dto/index.ts create mode 100644 apps/server/src/contexts/companies/presentation/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d9466c3..2817c103 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,6 @@ "prettier.useEditorConfig": false, "prettier.useTabs": false, "prettier.configPath": ".prettierrc", - "asciidoc.antora.enableAntoraSupport": true, // other vscode settings "tailwindCSS.rootFontSize": 16, diff --git a/apps/server/jest.config.js b/apps/server/jest.config.js new file mode 100644 index 00000000..f5d30d13 --- /dev/null +++ b/apps/server/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + }, +}; \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 71b23479..19820492 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -59,6 +59,7 @@ "http": "0.0.1-security", "http-status": "^2.1.0", "jsonwebtoken": "^9.0.2", + "libphonenumber-js": "^1.11.20", "luxon": "^3.5.0", "mariadb": "^3.4.0", "module-alias": "^2.2.3", diff --git a/apps/server/src/common/domain/domain-entity.ts b/apps/server/src/common/domain/domain-entity.ts index ea0fdca9..d7d7dcf3 100644 --- a/apps/server/src/common/domain/domain-entity.ts +++ b/apps/server/src/common/domain/domain-entity.ts @@ -1,12 +1,12 @@ import { UniqueID } from "./value-objects/unique-id"; export abstract class DomainEntity { - protected readonly _id: UniqueID; - protected readonly _props: T; + protected readonly props: T; + public readonly id: UniqueID; protected constructor(props: T, id?: UniqueID) { - this._id = id ? id : UniqueID.generateNewID().data; - this._props = props; + this.id = id ? id : UniqueID.generateNewID().data; + this.props = props; } protected _flattenProps(props: T): { [s: string]: any } { @@ -18,19 +18,15 @@ export abstract class DomainEntity { }, {}); } - get id(): UniqueID { - return this._id; - } - equals(other: DomainEntity): boolean { return other instanceof DomainEntity && this.id.equals(other.id); } toString(): { [s: string]: string } { - const flattenProps = this._flattenProps(this._props); + const flattenProps = this._flattenProps(this.props); return { - id: this._id.toString(), + id: this.id.toString(), ...flattenProps.map((prop: any) => String(prop)), }; } diff --git a/apps/server/src/common/domain/index.ts b/apps/server/src/common/domain/index.ts index 4e273629..8f115f96 100644 --- a/apps/server/src/common/domain/index.ts +++ b/apps/server/src/common/domain/index.ts @@ -2,5 +2,4 @@ export * from "./aggregate-root"; export * from "./aggregate-root-repository.interface"; export * from "./domain-entity"; export * from "./events/domain-event.interface"; -export * from "./result"; export * from "./value-objects"; diff --git a/apps/server/src/common/domain/result.spec.ts b/apps/server/src/common/domain/result.spec.ts deleted file mode 100644 index 75299bae..00000000 --- a/apps/server/src/common/domain/result.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Result } from "./result"; - -describe("Result", () => { - it("should create a successful result", () => { - const result = Result.ok("Success Data"); - - expect(result.isSuccess).toBe(true); - expect(result.isSuccess).toBe(false); - expect(result.data).toBe("Success Data"); - }); - - it("should create a failed result", () => { - const error = new Error("Test error"); - const result = Result.fail(error); - - expect(result.isSuccess).toBe(false); - expect(result.isSuccess).toBe(true); - expect(result.error).toBe(error); - }); - - it("should getOrElse return default value if result is a failure", () => { - const error = new Error("Test error"); - const result = Result.fail(error); - - expect(result.getOrElse("Default")).toBe("Default"); - }); - - it("should match execute correct function based on success or failure", () => { - const successResult = Result.ok("Success"); - const failureResult = Result.fail(new Error("Failure")); - - expect( - successResult.match( - (data) => `OK: ${data}`, - (error) => null - ) - ).toBe("OK: Success"); - expect( - failureResult.match( - (data) => null, - (error) => `ERROR: ${error.message}` - ) - ).toBe("ERROR: Failure"); - }); -}); diff --git a/apps/server/src/contexts/auth/domain/value-objects/email-address.spec.ts b/apps/server/src/common/domain/value-objects/email-address.spec.ts similarity index 90% rename from apps/server/src/contexts/auth/domain/value-objects/email-address.spec.ts rename to apps/server/src/common/domain/value-objects/email-address.spec.ts index 06f8f50e..66fb8eff 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/email-address.spec.ts +++ b/apps/server/src/common/domain/value-objects/email-address.spec.ts @@ -16,7 +16,7 @@ describe("EmailAddress Value Object", () => { }); it("should allow null email", () => { - const result = EmailAddress.create(null); + const result = EmailAddress.createNullable(); expect(result.isSuccess).toBe(true); expect(result.data.getValue()).toBe(null); @@ -48,16 +48,16 @@ describe("EmailAddress Value Object", () => { }); it("should detect empty email correctly", () => { - const email = EmailAddress.create(null); + const email = EmailAddress.createNullable(); expect(email.isSuccess).toBe(true); - expect(email.data.isEmpty()).toBe(true); + expect(email.data.isSome()).toBe(false); }); it("should detect non-empty email correctly", () => { const email = EmailAddress.create("test@example.com"); expect(email.isSuccess).toBe(true); - expect(email.data.isEmpty()).toBe(false); + expect(email.data.getValue()).toBe(true); }); }); diff --git a/apps/server/src/common/domain/value-objects/email-address.ts b/apps/server/src/common/domain/value-objects/email-address.ts new file mode 100644 index 00000000..7015d4ec --- /dev/null +++ b/apps/server/src/common/domain/value-objects/email-address.ts @@ -0,0 +1,50 @@ +import { Maybe, Result, ValueObject } from "@common/domain"; +import { z } from "zod"; + +interface EmailAddressProps { + value: string; +} + +export class EmailAddress extends ValueObject { + static create(value: string): Result { + const valueIsValid = EmailAddress.validate(value); + + if (!valueIsValid.success) { + Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new EmailAddress({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.None()); + } + + return EmailAddress.create(value!).map((value) => Maybe.Some(value)); + } + + private static validate(value: string) { + const schema = z.string().email({ message: "Invalid email format" }); + return schema.safeParse(value); + } + + getLocalPart(): string { + return this.props.value.split("@")[0]; + } + + getDomain(): string { + return this.props.value.split("@")[1]; + } + + getDomainExtension(): string { + return this.getDomain().split(".")[1]; + } + + getDomainName(): string { + return this.getDomain().split(".")[0]; + } + + getValue(): string { + return this.props.value; + } +} diff --git a/apps/server/src/common/domain/value-objects/index.ts b/apps/server/src/common/domain/value-objects/index.ts index 2d2be954..9a566d27 100644 --- a/apps/server/src/common/domain/value-objects/index.ts +++ b/apps/server/src/common/domain/value-objects/index.ts @@ -1,2 +1,8 @@ +export * from "./email-address"; +export * from "./name"; +export * from "./phone-number"; +export * from "./postal-address"; +export * from "./slug"; +export * from "./tin-number"; export * from "./unique-id"; export * from "./value-object"; diff --git a/apps/server/src/common/domain/value-objects/name.spec.ts b/apps/server/src/common/domain/value-objects/name.spec.ts new file mode 100644 index 00000000..846b23bb --- /dev/null +++ b/apps/server/src/common/domain/value-objects/name.spec.ts @@ -0,0 +1,41 @@ +import { Name } from "./name"; + +describe("Name Value Object", () => { + test("Debe crear un Name válido", () => { + const nameResult = Name.create("John Doe"); + expect(nameResult.isSuccess).toBe(true); + expect(nameResult.data.getValue()).toBe("John Doe"); + }); + + test("Debe fallar si el nombre excede los 255 caracteres", () => { + const longName = "A".repeat(256); + const nameResult = Name.create(longName); + expect(nameResult.isSuccess).toBe(false); + expect(nameResult.error).toBeInstanceOf(Error); + }); + + test("Debe permitir un Name nullable vacío", () => { + const nullableNameResult = Name.createNullable(""); + expect(nullableNameResult.isSuccess).toBe(true); + expect(nullableNameResult.data.isSome()).toBe(false); + }); + + test("Debe permitir un Name nullable con un valor válido", () => { + const nullableNameResult = Name.createNullable("Alice"); + expect(nullableNameResult.isSuccess).toBe(true); + expect(nullableNameResult.data.isSome()).toBe(true); + expect(nullableNameResult.data.getValue()).toBe("Alice"); + }); + + test("Debe generar acrónimos correctamente", () => { + expect(Name.generateAcronym("John Doe")).toBe("JDXX"); + expect(Name.generateAcronym("Alice Bob Charlie")).toBe("ABCX"); + expect(Name.generateAcronym("A B C D E")).toBe("ABCD"); + expect(Name.generateAcronym("SingleWord")).toBe("SXXX"); + }); + + test("Debe obtener el acrónimo de una instancia de Name", () => { + const name = Name.create("John Doe").data; + expect(name.getAcronym()).toBe("JDXX"); + }); +}); diff --git a/apps/server/src/common/domain/value-objects/name.ts b/apps/server/src/common/domain/value-objects/name.ts new file mode 100644 index 00000000..3b03412f --- /dev/null +++ b/apps/server/src/common/domain/value-objects/name.ts @@ -0,0 +1,61 @@ +import { Maybe, Result, ValueObject } from "@common/domain"; +import { z } from "zod"; + +interface NameProps { + value: string; +} + +export class Name extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(Name.MAX_LENGTH, { message: `Name must be at most ${Name.MAX_LENGTH} characters long` }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = Name.validate(value); + + if (!valueIsValid.success) { + Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new Name({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.None()); + } + + return Name.create(value!).map((value) => Maybe.Some(value)); + } + + static generateAcronym(name: string): string { + const words = name.split(" ").map((word) => word[0].toUpperCase()); + let acronym = words.join(""); + + // Asegurarse de que tenga 4 caracteres, recortando o añadiendo letras + if (acronym.length > 4) { + acronym = acronym.slice(0, 4); + } else if (acronym.length < 4) { + acronym = acronym.padEnd(4, "X"); // Se completa con 'X' si es necesario + } + + return acronym; + } + + getAcronym(): string { + return Name.generateAcronym(this.toString()); + } + + getValue(): string { + return this.props.value; + } + + toString(): string { + return this.getValue(); + } +} diff --git a/apps/server/src/common/domain/value-objects/phone-number.ts b/apps/server/src/common/domain/value-objects/phone-number.ts new file mode 100644 index 00000000..3c593fd1 --- /dev/null +++ b/apps/server/src/common/domain/value-objects/phone-number.ts @@ -0,0 +1,63 @@ +import { isValidPhoneNumber, parsePhoneNumberWithError } from "libphonenumber-js"; +import { z } from "zod"; +import { Maybe } from "../../helpers/maybe"; +import { Result } from "../../helpers/result"; +import { ValueObject } from "./value-object"; + +interface PhoneNumberProps { + value: string; +} + +export class PhoneNumber extends ValueObject { + static create(value: string): Result { + const valueIsValid = PhoneNumber.validate(value); + + if (!valueIsValid.success) { + Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new PhoneNumber({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.None()); + } + + return PhoneNumber.create(value!).map((value) => Maybe.Some(value)); + } + + static validate(value: string) { + const schema = z + .string() + .refine( + isValidPhoneNumber, + "Please specify a valid phone number (include the international prefix)." + ) + .transform((value: string) => parsePhoneNumberWithError(value).number.toString()); + return schema.safeParse(value); + } + + getValue(): string { + return this.props.value; + } + + toString(): string { + return this.getValue(); + } + + getCountryCode(): string | undefined { + return parsePhoneNumberWithError(this.props.value).country; + } + + getNationalNumber(): string { + return parsePhoneNumberWithError(this.props.value).nationalNumber; + } + + getNumber(): string { + return parsePhoneNumberWithError(this.props.value).number.toString(); + } + + getExtension(): string | undefined { + return parsePhoneNumberWithError(this.props.value).ext; + } +} diff --git a/apps/server/src/common/domain/value-objects/postal-address.ts b/apps/server/src/common/domain/value-objects/postal-address.ts new file mode 100644 index 00000000..999e5a7b --- /dev/null +++ b/apps/server/src/common/domain/value-objects/postal-address.ts @@ -0,0 +1,95 @@ +import { z } from "zod"; +import { Maybe, Result } from "../../helpers"; +import { ValueObject } from "./value-object"; + +// 📌 Validaciones usando `zod` +const postalCodeSchema = z + .string() + .min(4) + .max(10) + .regex(/^\d{4,10}$/, { + message: "Invalid postal code format", + }); + +const countrySchema = z.string().min(2).max(56); + +const provinceSchema = z.string().min(2).max(50); + +const citySchema = z.string().min(2).max(50); + +const streetSchema = z.string().min(2).max(255); + +interface IPostalAddressProps { + street: string; + city: string; + postalCode: string; + state: string; + country: string; +} + +export interface IPostalAddressPrimitives extends IPostalAddressProps { + street: string; + city: string; + postal_code: string; + state: string; + country: string; +} + +export class PostalAddress extends ValueObject { + protected static validate(values: IPostalAddressProps) { + return z + .object({ + street: streetSchema, + city: citySchema, + postalCode: postalCodeSchema, + state: provinceSchema, + country: countrySchema, + }) + .safeParse(values); + } + + static create(values: IPostalAddressProps): Result { + const valueIsValid = PostalAddress.validate(values); + + if (!valueIsValid.success) { + Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new PostalAddress(valueIsValid.data!)); + } + + static createNullable(values?: IPostalAddressProps): Result, Error> { + if (!values || Object.values(values).every((value) => value.trim() === "")) { + return Result.ok(Maybe.None()); + } + + return PostalAddress.create(values!).map((value) => Maybe.Some(value)); + } + + get street(): string { + return this.props.street; + } + + get city(): string { + return this.props.city; + } + + get postalCode(): string { + return this.props.postalCode; + } + + get state(): string { + return this.props.state; + } + + get country(): string { + return this.props.country; + } + + getValue(): IPostalAddressProps { + return this.props; + } + + toString(): string { + return `${this.props.street}, ${this.props.city}, ${this.props.postalCode}, ${this.props.state}, ${this.props.country}`; + } +} diff --git a/apps/server/src/common/domain/value-objects/slug.spec.ts b/apps/server/src/common/domain/value-objects/slug.spec.ts new file mode 100644 index 00000000..e8a5d5e9 --- /dev/null +++ b/apps/server/src/common/domain/value-objects/slug.spec.ts @@ -0,0 +1,41 @@ +import { Slug } from "./slug"; // Ajusta la ruta según corresponda + +describe("Slug Value Object", () => { + test("Debe crear un Slug válido", () => { + const slugResult = Slug.create("valid-slug-123"); + expect(slugResult.isSuccess).toBe(true); + expect(slugResult.data.getValue()).toBe("valid-slug-123"); + }); + + test("Debe fallar si el Slug contiene caracteres inválidos", () => { + const slugResult = Slug.create("Invalid_Slug!"); + expect(slugResult.isSuccess).toBe(false); + expect(slugResult.error).toBeInstanceOf(Error); + }); + + test("Debe fallar si el Slug tiene menos de 2 caracteres", () => { + const slugResult = Slug.create("a"); + expect(slugResult.isSuccess).toBe(false); + expect(slugResult.error).toBeInstanceOf(Error); + }); + + test("Debe fallar si el Slug tiene más de 100 caracteres", () => { + const longSlug = "a".repeat(101); + const slugResult = Slug.create(longSlug); + expect(slugResult.isSuccess).toBe(false); + expect(slugResult.error).toBeInstanceOf(Error); + }); + + test("Debe permitir un Slug nullable vacío", () => { + const nullableSlugResult = Slug.createNullable(""); + expect(nullableSlugResult.isSuccess).toBe(true); + expect(nullableSlugResult.data.isSome()).toBe(false); + }); + + test("Debe permitir un Slug nullable con un valor válido", () => { + const nullableSlugResult = Slug.createNullable("my-slug"); + expect(nullableSlugResult.isSuccess).toBe(true); + expect(nullableSlugResult.data.isSome()).toBe(true); + expect(nullableSlugResult.data.getValue()).toBe("my-slug"); + }); +}); diff --git a/apps/server/src/common/domain/value-objects/slug.ts b/apps/server/src/common/domain/value-objects/slug.ts new file mode 100644 index 00000000..2faa08b2 --- /dev/null +++ b/apps/server/src/common/domain/value-objects/slug.ts @@ -0,0 +1,48 @@ +import { Maybe, Result, ValueObject } from "@common/domain"; +import { z } from "zod"; + +interface SlugProps { + value: string; +} + +export class Slug extends ValueObject { + protected static readonly MIN_LENGTH = 2; + protected static readonly MAX_LENGTH = 100; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .regex(/^[a-z0-9-]+$/, { + message: "Slug must contain only lowercase letters, numbers, and hyphens", + }) + .min(Slug.MIN_LENGTH, { message: `Slug must be at least ${Slug.MIN_LENGTH} characters long` }) + .max(Slug.MAX_LENGTH, { message: `Slug must be at most ${Slug.MAX_LENGTH} characters long` }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = Slug.validate(value); + + if (!valueIsValid.success) { + Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new Slug({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.None()); + } + + return Slug.create(value!).map((value) => Maybe.Some(value)); + } + + getValue(): string { + return this.props.value; + } + + toString(): string { + return this.getValue(); + } +} diff --git a/apps/server/src/common/domain/value-objects/tin-number.ts b/apps/server/src/common/domain/value-objects/tin-number.ts new file mode 100644 index 00000000..4804a9d9 --- /dev/null +++ b/apps/server/src/common/domain/value-objects/tin-number.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import { Maybe, Result } from "../../helpers"; +import { ValueObject } from "./value-object"; + +interface TINNumberProps { + value: string; +} + +export class TINNumber extends ValueObject { + private static readonly MIN_LENGTH = 2; + private static readonly MAX_LENGTH = 10; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .min(TINNumber.MIN_LENGTH, { + message: `TIN must be at least ${TINNumber.MIN_LENGTH} characters long`, + }) + .max(TINNumber.MAX_LENGTH, { + message: `TIN must be at most ${TINNumber.MAX_LENGTH} characters long`, + }); + + return schema.safeParse(value); + } + + static create(value: string): Result { + const valueIsValid = TINNumber.validate(value); + + if (!valueIsValid.success) { + Result.fail(new Error(valueIsValid.error.errors[0].message)); + } + return Result.ok(new TINNumber({ value: valueIsValid.data! })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.None()); + } + + return TINNumber.create(value!).map((value) => Maybe.Some(value)); + } + + getValue(): string { + return this.props.value; + } + + toString(): string { + return this.props !== null && this.props !== undefined ? String(this.props) : ""; + } +} diff --git a/apps/server/src/common/domain/value-objects/unique-id.ts b/apps/server/src/common/domain/value-objects/unique-id.ts index 4e2d3785..609f4dd8 100644 --- a/apps/server/src/common/domain/value-objects/unique-id.ts +++ b/apps/server/src/common/domain/value-objects/unique-id.ts @@ -1,29 +1,18 @@ import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; -import { Result } from "../result"; +import { Result } from "../../helpers/result"; import { ValueObject } from "./value-object"; -export const UNDEFINED_ID = undefined; - -export class UniqueID extends ValueObject { - protected readonly _hasId!: boolean; - - protected constructor(id?: string) { - super(id); - this._hasId = id != UNDEFINED_ID; - } - +export class UniqueID extends ValueObject { static create(id?: string, generateOnEmpty: boolean = false): Result { - if (id === null) { - return Result.fail(new Error("ID cannot be null")); + if (!id || id?.trim() === "") { + if (!generateOnEmpty) { + return Result.fail(new Error("ID cannot be undefined or null")); + } + UniqueID.generateNewID(); } - const trimmedId = id?.trim(); - if (!trimmedId) { - return generateOnEmpty ? UniqueID.generateNewID() : Result.ok(new UniqueID(UNDEFINED_ID)); - } - - const result = UniqueID.validate(trimmedId); + const result = UniqueID.validate(id!); return result.success ? Result.ok(new UniqueID(result.data)) @@ -35,19 +24,19 @@ export class UniqueID extends ValueObject { } static validate(id: string) { - const schema = z.string().uuid({ message: "Invalid UUID format" }); - return schema.safeParse(id.trim()); + const schema = z.string().trim().uuid({ message: "Invalid UUID format" }); + return schema.safeParse(id); } static generateNewID(): Result { return Result.ok(new UniqueID(uuidv4())); } - static generateUndefinedID(): Result { - return Result.ok(new UniqueID(UNDEFINED_ID)); + getValue(): string { + return this.props; } - isDefined(): boolean { - return this._hasId; + toString(): string { + return this.props; } } diff --git a/apps/server/src/common/domain/value-objects/value-object.ts b/apps/server/src/common/domain/value-objects/value-object.ts index 782899f4..3737f5b1 100644 --- a/apps/server/src/common/domain/value-objects/value-object.ts +++ b/apps/server/src/common/domain/value-objects/value-object.ts @@ -1,33 +1,31 @@ import { shallowEqual } from "shallow-equal-object"; export abstract class ValueObject { - protected readonly _value: T; + protected readonly props: T; - protected constructor(value: T) { - this._value = typeof value === "object" && value !== null ? Object.freeze(value) : value; + protected constructor(props: T) { + this.props = Object.freeze(props); // 🔒 Garantiza inmutabilidad } + abstract getValue(): any; + equals(other: ValueObject): boolean { if (!(other instanceof ValueObject)) { return false; } - if (other._value === undefined || other._value === null) { + if (other.props === undefined || other.props === null) { return false; } - return shallowEqual(this._value, other._value); + return shallowEqual(this.props, other.props); } - getValue(): T { - return this._value; - } + /*isEmpty(): boolean { + return this.props === null || this.props === undefined; + }*/ - isEmpty(): boolean { - return this._value === null || this._value === undefined; - } - - toString(): string { - return this._value !== null && this._value !== undefined ? String(this._value) : ""; - } + /*toString(): string { + return this.props !== null && this.props !== undefined ? String(this.props) : ""; + }*/ } diff --git a/apps/server/src/common/domain/value-objects/value-objects.spec.ts b/apps/server/src/common/domain/value-objects/value-objects.spec.ts new file mode 100644 index 00000000..e877ef3d --- /dev/null +++ b/apps/server/src/common/domain/value-objects/value-objects.spec.ts @@ -0,0 +1,39 @@ +import { ValueObject } from "./value-object"; + +class TestValueObject extends ValueObject<{ prop: string }> { + constructor(prop: string) { + super({ prop }); + } +} + +describe("ValueObject", () => { + test("debe considerar dos ValueObjects con los mismos valores como iguales", () => { + const vo1 = new TestValueObject("test"); + const vo2 = new TestValueObject("test"); + expect(vo1.equals(vo2)).toBe(true); + }); + + test("debe considerar dos ValueObjects con valores diferentes como distintos", () => { + const vo1 = new TestValueObject("test1"); + const vo2 = new TestValueObject("test2"); + expect(vo1.equals(vo2)).toBe(false); + }); + + test("debe devolver false si el objeto comparado no es una instancia de ValueObject", () => { + const vo1 = new TestValueObject("test"); + expect(vo1.equals({ prop: "test" } as any)).toBe(false); + }); + + test("debe devolver false si el objeto comparado es null o undefined", () => { + const vo1 = new TestValueObject("test"); + expect(vo1.equals(null as any)).toBe(false); + expect(vo1.equals(undefined as any)).toBe(false); + }); + + test("debe garantizar la inmutabilidad de las propiedades", () => { + const vo = new TestValueObject("immutable"); + expect(() => { + (vo as any).props.prop = "mutated"; + }).toThrow(); + }); +}); diff --git a/apps/server/src/common/helpers/collection.spec.ts b/apps/server/src/common/helpers/collection.spec.ts new file mode 100644 index 00000000..312b75e9 --- /dev/null +++ b/apps/server/src/common/helpers/collection.spec.ts @@ -0,0 +1,40 @@ +import { Collection } from "./collection"; + +describe("Collection", () => { + let collection: Collection; + + beforeEach(() => { + collection = new Collection([1, 2, 3], 5); + }); + + test("should initialize with given items", () => { + expect(collection.getAll()).toEqual([1, 2, 3]); + expect(collection.total()).toBe(5); + }); + + test("should add an item", () => { + collection.add(4); + expect(collection.getAll()).toEqual([1, 2, 3, 4]); + expect(collection.total()).toBe(6); + }); + + test("should remove an existing item", () => { + const removed = collection.remove(2); + expect(removed).toBe(true); + expect(collection.getAll()).toEqual([1, 3]); + expect(collection.total()).toBe(4); + }); + + test("should not remove a non-existing item", () => { + const removed = collection.remove(99); + expect(removed).toBe(false); + expect(collection.getAll()).toEqual([1, 2, 3]); + expect(collection.total()).toBe(5); + }); + + test("should reset the collection", () => { + collection.reset(); + expect(collection.getAll()).toEqual([]); + expect(collection.total()).toBe(0); + }); +}); diff --git a/apps/server/src/common/helpers/collection.ts b/apps/server/src/common/helpers/collection.ts new file mode 100644 index 00000000..0a425a95 --- /dev/null +++ b/apps/server/src/common/helpers/collection.ts @@ -0,0 +1,82 @@ +// Interfaz para definir las operaciones básicas de una colección +/*interface ICollection { + reset(): void; + add(item: T): void; + remove(item: T): boolean; + getAll(): T[]; + size(): number; + total(): number | null; + }*/ + +export class Collection /*implements ICollection*/ { + private items: T[]; + private totalItems: number | null; + + constructor(items: T[] = [], totalItems: number | null = null) { + this.items = [...items]; + this.totalItems = totalItems ?? items.length; + } + + // Resetea la colección + reset(): void { + this.items = []; + this.totalItems = 0; + } + + // Agrega un elemento a la colección + add(item: T): void { + this.items.push(item); + if (this.totalItems !== null) { + this.totalItems++; + } + } + + // Elimina un elemento de la colección + remove(item: T): boolean { + const index = this.items.indexOf(item); + if (index !== -1) { + this.items.splice(index, 1); + if (this.totalItems !== null) { + this.totalItems--; + } + return true; + } + return false; + } + + // Devuelve todos los elementos + getAll(): T[] { + return [...this.items]; + } + + // Devuelve el número de elementos en la colección + size(): number { + return this.items.length; + } + + // Devuelve el total de elementos esperados en la colección + total(): number | null { + return this.totalItems; + } + + // Implementación de operaciones nativas de array + map(callback: (item: T, index: number, array: T[]) => U): U[] { + return this.items.map(callback); + } + + filter(callback: (item: T, index: number, array: T[]) => boolean): T[] { + return this.items.filter(callback); + } + + find(callback: (item: T, index: number, array: T[]) => boolean): T | undefined { + return this.items.find(callback); + } + + some(callback: (item: T, index: number, array: T[]) => boolean): boolean { + return this.items.some(callback); + } + + every(callback: (item: T, index: number, array: T[]) => boolean): boolean { + return this.items.every(callback); + } +} diff --git a/apps/server/src/common/helpers/index.ts b/apps/server/src/common/helpers/index.ts new file mode 100644 index 00000000..5d1da0b4 --- /dev/null +++ b/apps/server/src/common/helpers/index.ts @@ -0,0 +1,4 @@ +export * from "./collection"; +export * from "./maybe"; +export * from "./result"; +export * from "./utils"; diff --git a/apps/server/src/common/helpers/maybe.spec.ts b/apps/server/src/common/helpers/maybe.spec.ts new file mode 100644 index 00000000..667e2725 --- /dev/null +++ b/apps/server/src/common/helpers/maybe.spec.ts @@ -0,0 +1,31 @@ +import { Maybe } from "./maybe"; + +describe("Maybe", () => { + test("debe contener un valor cuando se usa Some", () => { + const maybeNumber = Maybe.Some(42); + expect(maybeNumber.isSome()).toBe(true); + expect(maybeNumber.getValue()).toBe(42); + }); + + test("debe estar vacío cuando se usa None", () => { + const maybeEmpty = Maybe.None(); + expect(maybeEmpty.isSome()).toBe(false); + expect(maybeEmpty.getValue()).toBeUndefined(); + }); + + test("map debe transformar el valor si existe", () => { + const maybeNumber = Maybe.Some(10); + const maybeDoubled = maybeNumber.map((n) => n * 2); + + expect(maybeDoubled.isSome()).toBe(true); + expect(maybeDoubled.getValue()).toBe(20); + }); + + test("map debe retornar None si el valor no existe", () => { + const maybeEmpty = Maybe.None(); + const maybeTransformed = maybeEmpty.map((n) => n * 2); + + expect(maybeTransformed.isSome()).toBe(false); + expect(maybeTransformed.getValue()).toBeUndefined(); + }); +}); diff --git a/apps/server/src/common/helpers/maybe.ts b/apps/server/src/common/helpers/maybe.ts new file mode 100644 index 00000000..52eb9c6e --- /dev/null +++ b/apps/server/src/common/helpers/maybe.ts @@ -0,0 +1,34 @@ +/** + * Uso: + * + * const maybeNumber = Maybe.Some(10); + * const doubled = maybeNumber.map(n => n * 2); + * console.log(doubled.getValue()); // 20 + + * const noValue = Maybe.None(); + * console.log(noValue.isSome()); // false + **/ + +export class Maybe { + private constructor(private readonly value?: T) {} + + static Some(value: T): Maybe { + return new Maybe(value); + } + + static None(): Maybe { + return new Maybe(); + } + + isSome(): boolean { + return this.value !== undefined; + } + + getValue(): T | undefined { + return this.value; + } + + map(fn: (value: T) => U): Maybe { + return this.isSome() ? Maybe.Some(fn(this.value as T)) : Maybe.None(); + } +} diff --git a/apps/server/src/common/helpers/result.spec.ts b/apps/server/src/common/helpers/result.spec.ts new file mode 100644 index 00000000..57a6969e --- /dev/null +++ b/apps/server/src/common/helpers/result.spec.ts @@ -0,0 +1,73 @@ +import { Result } from "./result"; + +describe("Result", () => { + test("debe crear un resultado exitoso con datos", () => { + const result = Result.ok("Success data"); + expect(result.isSuccess).toBe(true); + expect(result.isFailure).toBe(false); + expect(result.data).toBe("Success data"); + }); + + test("debe crear un resultado fallido con un error", () => { + const error = new Error("Something went wrong"); + const result = Result.fail(error); + expect(result.isSuccess).toBe(false); + expect(result.isFailure).toBe(true); + expect(result.error).toBe(error); + }); + + test("debe lanzar un error al intentar obtener data de un resultado fallido", () => { + const result = Result.fail(new Error("Failure")); + expect(() => result.getData()).toThrow("Cannot get value data from a failed result."); + }); + + test("debe lanzar un error al intentar obtener error de un resultado exitoso", () => { + const result = Result.ok("Success"); + expect(() => result.getError()).toThrow("Cannot get error from a successful result."); + }); + + test("debe devolver el valor por defecto cuando se usa getOrElse en un resultado fallido", () => { + const result = Result.fail(new Error("Failure")); + expect(result.getOrElse("Default value")).toBe("Default value"); + }); + + test("debe devolver el valor correcto cuando se usa getOrElse en un resultado exitoso", () => { + const result = Result.ok("Success"); + expect(result.getOrElse("Default value")).toBe("Success"); + }); + + test("debe ejecutar la función correcta en match dependiendo del resultado", () => { + const successResult = Result.ok("Success"); + const failureResult = Result.fail(new Error("Failure")); + + const successMatch = successResult.match( + (data) => `Success: ${data}`, + (error) => `Error: ${error}}` + ); + expect(successMatch).toBe("Success: Success"); + + const failureMatch = failureResult.match( + (data) => `Success: ${data}`, + (error) => `Error: ${error.message}` + ); + expect(failureMatch).toBe("Error: Failure"); + }); + + test("combine debe devolver el primer resultado fallido si hay alguno", () => { + const success1 = Result.ok("Success 1"); + const failure = Result.fail(new Error("Failure")); + const success2 = Result.ok("Success 2"); + + const combined = Result.combine([success1, failure, success2]); + expect(combined.isFailure).toBe(true); + expect(combined.error.message).toBe("Failure"); + }); + + test("combine debe devolver un resultado exitoso si todos los resultados son exitosos", () => { + const success1 = Result.ok("Success 1"); + const success2 = Result.ok("Success 2"); + + const combined = Result.combine([success1, success2]); + expect(combined.isSuccess).toBe(true); + }); +}); diff --git a/apps/server/src/common/domain/result.ts b/apps/server/src/common/helpers/result.ts similarity index 90% rename from apps/server/src/common/domain/result.ts rename to apps/server/src/common/helpers/result.ts index f95b5a8f..a0136f04 100644 --- a/apps/server/src/common/domain/result.ts +++ b/apps/server/src/common/helpers/result.ts @@ -67,6 +67,13 @@ export class Result { return this._data as T; } + map(fn: (value: T) => U): Result { + if (this.isSuccess && this._data !== undefined) { + return Result.ok(fn(this.data)); + } + return Result.fail(this.error || new Error("Unknown error")); + } + /** * 🔹 `getOrElse(defaultValue: T): T` * Si el `Result` es un `ok`, devuelve `data`, de lo contrario, devuelve `defaultValue`. diff --git a/apps/server/src/common/helpers/utils.spec.ts b/apps/server/src/common/helpers/utils.spec.ts new file mode 100644 index 00000000..a394aedd --- /dev/null +++ b/apps/server/src/common/helpers/utils.spec.ts @@ -0,0 +1,46 @@ +// Función genérica para asegurar valores básicos +function ensure(value: T | undefined | null, defaultValue: T): T { + return value ?? defaultValue; +} + +// Implementaciones específicas para tipos básicos +const ensureString = (value?: string): string => ensure(value, ""); +const ensureNumber = (value?: number): number => ensure(value, 0); +const ensureBoolean = (value?: boolean): boolean => ensure(value, false); +const ensureBigInt = (value?: bigint): bigint => ensure(value, BigInt(0)); +const ensureSymbol = (value?: symbol, defaultSymbol = Symbol()): symbol => + ensure(value, defaultSymbol); + +describe("ensure functions", () => { + test("ensureString should return string value or default", () => { + expect(ensureString("Hola")).toBe("Hola"); + expect(ensureString(undefined)).toBe(""); + expect(ensureString(null as any)).toBe(""); + }); + + test("ensureNumber should return number value or default", () => { + expect(ensureNumber(42)).toBe(42); + expect(ensureNumber(undefined)).toBe(0); + expect(ensureNumber(null as any)).toBe(0); + }); + + test("ensureBoolean should return boolean value or default", () => { + expect(ensureBoolean(true)).toBe(true); + expect(ensureBoolean(false)).toBe(false); + expect(ensureBoolean(undefined)).toBe(false); + expect(ensureBoolean(null as any)).toBe(false); + }); + + test("ensureBigInt should return bigint value or default", () => { + expect(ensureBigInt(BigInt(123))).toBe(BigInt(123)); + expect(ensureBigInt(undefined)).toBe(BigInt(0)); + expect(ensureBigInt(null as any)).toBe(BigInt(0)); + }); + + test("ensureSymbol should return symbol value or default", () => { + const sym = Symbol("test"); + expect(ensureSymbol(sym)).toBe(sym); + expect(typeof ensureSymbol(undefined)).toBe("symbol"); + expect(typeof ensureSymbol(null as any)).toBe("symbol"); + }); +}); diff --git a/apps/server/src/common/helpers/utils.ts b/apps/server/src/common/helpers/utils.ts new file mode 100644 index 00000000..3f3d0750 --- /dev/null +++ b/apps/server/src/common/helpers/utils.ts @@ -0,0 +1,12 @@ +// Función genérica para asegurar valores básicos +function ensure(value: T | undefined | null, defaultValue: T): T { + return value ?? defaultValue; +} + +// Implementaciones específicas para tipos básicos +export const ensureString = (value?: string): string => ensure(value, ""); +export const ensureNumber = (value?: number): number => ensure(value, 0); +export const ensureBoolean = (value?: boolean): boolean => ensure(value, false); +export const ensureBigInt = (value?: bigint): bigint => ensure(value, BigInt(0)); +export const ensureSymbol = (value?: symbol, defaultSymbol = Symbol()): symbol => + ensure(value, defaultSymbol); diff --git a/apps/server/src/common/infrastructure/sequelize/index.ts b/apps/server/src/common/infrastructure/sequelize/index.ts index cbe4ce74..df6c2cb0 100644 --- a/apps/server/src/common/infrastructure/sequelize/index.ts +++ b/apps/server/src/common/infrastructure/sequelize/index.ts @@ -1,2 +1,3 @@ +export * from "./sequelize-mapper"; export * from "./sequelize-repository"; export * from "./sequelize-transaction-manager"; diff --git a/apps/server/src/common/infrastructure/sequelize/sequelize-mapper.spec.ts b/apps/server/src/common/infrastructure/sequelize/sequelize-mapper.spec.ts new file mode 100644 index 00000000..8518d628 --- /dev/null +++ b/apps/server/src/common/infrastructure/sequelize/sequelize-mapper.spec.ts @@ -0,0 +1,67 @@ +import { DomainEntity } from "@common/domain"; +import { Collection, Result } from "@common/helpers"; +import { Model } from "sequelize"; +import { SequelizeMapper } from "./sequelize-mapper"; + +// Mock Entities +class MockEntity extends DomainEntity {} +class MockModel extends Model { + dataValues: Record = {}; +} + +// Mock Mapper +class MockSequelizeMapper extends SequelizeMapper, MockEntity> { + public mapToDomain(source: MockModel, params?: Record): Result { + return Result.ok(new MockEntity({ ...source.dataValues, ...params })); + } + + public mapToPersistence( + source: MockEntity, + params?: Record + ): Result, Error> { + return Result.ok({ ...source.props, ...params }); + } +} + +describe("SequelizeMapper", () => { + let mapper: MockSequelizeMapper; + let model: MockModel; + + beforeEach(() => { + mapper = new MockSequelizeMapper({}); + model = new MockModel(); + model.dataValues = { id: 1, name: "Test" }; + }); + + test("should map a model to a domain entity", () => { + const result = mapper.mapToDomain(model); + expect(result.isSuccess).toBe(true); + expect(result.data).toBeInstanceOf(MockEntity); + expect(result.data.props.id).toBe(1); + expect(result.data.props.name).toBe("Test"); + }); + + test("should map an entity to a persistence object", () => { + const entity = new MockEntity({ id: 1, name: "Test" }); + const result = mapper.mapToPersistence(entity); + expect(result.isSuccess).toBe(true); + expect(result.data).toEqual({ id: 1, name: "Test" }); + }); + + test("should map an array of models to a collection of domain entities", () => { + const models = [model, { ...model, dataValues: { id: 2, name: "Test2" } }]; + const result = mapper.mapArrayToDomain(models); + expect(result.isSuccess).toBe(true); + expect(result.data).toBeInstanceOf(Collection); + expect(result.data.items.length).toBe(2); + }); + + test("should handle mapping failures gracefully", () => { + jest + .spyOn(mapper, "mapToDomain") + .mockImplementation(() => Result.fail(new Error("Mapping failed"))); + const result = mapper.mapToDomain(model); + expect(result.isFailure).toBe(true); + expect(result.error?.message).toBe("Mapping failed"); + }); +}); diff --git a/apps/server/src/common/infrastructure/sequelize/sequelize-mapper.ts b/apps/server/src/common/infrastructure/sequelize/sequelize-mapper.ts new file mode 100644 index 00000000..4bd9267d --- /dev/null +++ b/apps/server/src/common/infrastructure/sequelize/sequelize-mapper.ts @@ -0,0 +1,121 @@ +import { DomainEntity } from "@common/domain"; +import { Collection, Result } from "@common/helpers"; +import { Model } from "sequelize"; + +export type MapperParamsType = Record; + +interface IDomainMapper> { + mapToDomain(source: TModel, params?: MapperParamsType): Result; + mapArrayToDomain(source: TModel[], params?: MapperParamsType): Result, Error>; + mapArrayAndCountToDomain( + source: TModel[], + totalCount: number, + params?: MapperParamsType + ): Result, Error>; +} + +interface IPersistenceMapper> { + mapToPersistence(source: TEntity, params?: MapperParamsType): Result; + mapCollectionToPersistence( + source: Collection, + params?: MapperParamsType + ): Result; +} + +export interface ISequelizeMapper< + TModel extends Model, + TModelAttributes, + TEntity extends DomainEntity, +> extends IDomainMapper, + IPersistenceMapper {} + +export abstract class SequelizeMapper< + TModel extends Model, + TModelAttributes, + TEntity extends DomainEntity, +> implements ISequelizeMapper +{ + public abstract mapToDomain(source: TModel, params?: MapperParamsType): Result; + + public mapArrayToDomain( + source: TModel[], + params?: MapperParamsType + ): Result, Error> { + return this.mapArrayAndCountToDomain(source, source.length, params); + } + + public mapArrayAndCountToDomain( + source: TModel[], + totalCount: number, + params?: MapperParamsType + ): Result, Error> { + try { + const items = source.map( + (value, index) => this.mapToDomain(value, { index, ...params }).data + ); + return Result.ok(new Collection(items, totalCount)); + } catch (error) { + return Result.fail(error as Error); + } + } + + public abstract mapToPersistence( + source: TEntity, + params?: MapperParamsType + ): Result; + + public mapCollectionToPersistence( + source: Collection, + params?: MapperParamsType + ): Result { + try { + const result = source.map( + (value, index) => this.mapToPersistence(value, { index, ...params }).data + ); + return Result.ok(result); + } catch (error) { + return Result.fail(error as Error); + } + } + + protected safeMap(operation: () => T, key: string): Result { + try { + return Result.ok(operation()); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } + + protected mapsValue( + row: TModel, + key: string, + customMapFn: (value: any, params: MapperParamsType) => Result, + params: MapperParamsType = { defaultValue: null } + ): Result { + return customMapFn(row?.dataValues[key] ?? params.defaultValue, params); + } + + protected mapsAssociation( + row: TModel, + associationName: string, + customMapper: IDomainMapper, + params: MapperParamsType = {} + ): Result { + if (!customMapper) { + Result.fail(Error(`Custom mapper undefined for ${associationName}`)); + } + + const { filter, ...otherParams } = params; + let associationRows = row?.dataValues[associationName] ?? []; + + if (filter) { + associationRows = Array.isArray(associationRows) + ? associationRows.filter(filter) + : filter(associationRows); + } + + return Array.isArray(associationRows) + ? customMapper.mapArrayToDomain(associationRows, otherParams) + : customMapper.mapToDomain(associationRows, otherParams); + } +} diff --git a/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts b/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts index 91f8bf07..e6408969 100644 --- a/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts +++ b/apps/server/src/common/infrastructure/sequelize/sequelize-repository.ts @@ -1,4 +1,5 @@ -import { IAggregateRootRepository, Result, UniqueID } from "@common/domain"; +import { IAggregateRootRepository, UniqueID } from "@common/domain"; +import { Result } from "@common/helpers"; import { ModelDefined, Transaction } from "sequelize"; import { logger } from "../logger"; diff --git a/apps/server/src/contexts/auth/domain/aggregates/authenticated-user.ts b/apps/server/src/contexts/auth/domain/aggregates/authenticated-user.ts index e4fbf273..06eb515f 100644 --- a/apps/server/src/contexts/auth/domain/aggregates/authenticated-user.ts +++ b/apps/server/src/contexts/auth/domain/aggregates/authenticated-user.ts @@ -44,15 +44,15 @@ export class AuthenticatedUser } verifyPassword(candidatePassword: PlainPassword): Promise { - return this._props.hashPassword.verifyPassword(candidatePassword.toString()); + return this.props.hashPassword.verifyPassword(candidatePassword.toString()); } getRoles(): string[] { - return this._props.roles; + return this.props.roles; } hasRole(role: string): boolean { - return (this._props.roles || []).some((r) => r === role); + return (this.props.roles || []).some((r) => r === role); } hasRoles(roles: string[]): boolean { @@ -60,11 +60,11 @@ export class AuthenticatedUser } get username(): Username { - return this._props.username; + return this.props.username; } get email(): EmailAddress { - return this._props.email; + return this.props.email; } get isUser(): boolean { @@ -80,11 +80,11 @@ export class AuthenticatedUser */ toPersistenceData(): any { return { - id: this._id.toString(), - username: this._props.username.toString(), - email: this._props.email.toString(), - hash_password: this._props.hashPassword.toString(), - roles: this._props.roles.map((role) => role.toString()), + id: this.id.toString(), + username: this.props.username.toString(), + email: this.props.email.toString(), + hash_password: this.props.hashPassword.toString(), + roles: this.props.roles.map((role) => role.toString()), access_token: this.accessToken, refresh_token: this.refreshToken, }; diff --git a/apps/server/src/contexts/auth/domain/aggregates/user.ts b/apps/server/src/contexts/auth/domain/aggregates/user.ts index e0d21595..c8541c51 100644 --- a/apps/server/src/contexts/auth/domain/aggregates/user.ts +++ b/apps/server/src/contexts/auth/domain/aggregates/user.ts @@ -1,6 +1,6 @@ -import { AggregateRoot, Result, UniqueID } from "@common/domain"; +import { AggregateRoot, EmailAddress, Result, UniqueID } from "@common/domain"; import { UserAuthenticatedEvent } from "../events"; -import { EmailAddress, Username } from "../value-objects"; +import { Username } from "../value-objects"; export interface IUserProps { username: Username; @@ -33,11 +33,11 @@ export class User extends AggregateRoot implements IUser { } getRoles(): string[] { - return this._props.roles; + return this.props.roles; } hasRole(role: string): boolean { - return (this._props.roles || []).some((r) => r === role); + return (this.props.roles || []).some((r) => r === role); } hasRoles(roles: string[]): boolean { @@ -45,11 +45,11 @@ export class User extends AggregateRoot implements IUser { } get username(): Username { - return this._props.username; + return this.props.username; } get email(): EmailAddress { - return this._props.email; + return this.props.email; } get isUser(): boolean { @@ -65,10 +65,10 @@ export class User extends AggregateRoot implements IUser { */ toPersistenceData(): any { return { - id: this._id.toString(), - username: this._props.username.toString(), - email: this._props.email.toString(), - roles: this._props.roles.map((role) => role.toString()), + id: this.id.toString(), + username: this.props.username.toString(), + email: this.props.email.toString(), + roles: this.props.roles.map((role) => role.toString()), }; } } diff --git a/apps/server/src/contexts/auth/domain/entities/jwt-payload.ts b/apps/server/src/contexts/auth/domain/entities/jwt-payload.ts index aabfaaa5..9577fa4a 100644 --- a/apps/server/src/contexts/auth/domain/entities/jwt-payload.ts +++ b/apps/server/src/contexts/auth/domain/entities/jwt-payload.ts @@ -54,15 +54,15 @@ export class JWTPayload extends DomainEntity implements IJWTPa } get tabId(): UniqueID { - return this._props.tabId; + return this.props.tabId; } get userId(): UniqueID { - return this._props.userId; + return this.props.userId; } get email(): EmailAddress { - return this._props.email; + return this.props.email; } toPersistenceData(): any { diff --git a/apps/server/src/contexts/auth/domain/entities/login-data.ts b/apps/server/src/contexts/auth/domain/entities/login-data.ts index 1f172534..4a839099 100644 --- a/apps/server/src/contexts/auth/domain/entities/login-data.ts +++ b/apps/server/src/contexts/auth/domain/entities/login-data.ts @@ -48,14 +48,14 @@ export class LoginData extends DomainEntity implements ILoginDa } get email(): EmailAddress { - return this._props.email; + return this.props.email; } get plainPassword(): PlainPassword { - return this._props.plainPassword; + return this.props.plainPassword; } get tabId(): UniqueID { - return this._props.tabId; + return this.props.tabId; } } diff --git a/apps/server/src/contexts/auth/domain/entities/logout-data.ts b/apps/server/src/contexts/auth/domain/entities/logout-data.ts index 50db80b3..0c1d48b6 100644 --- a/apps/server/src/contexts/auth/domain/entities/logout-data.ts +++ b/apps/server/src/contexts/auth/domain/entities/logout-data.ts @@ -43,10 +43,10 @@ export class LogoutData extends DomainEntity implements ILogou } get email(): EmailAddress { - return this._props.email; + return this.props.email; } get tabId(): UniqueID { - return this._props.tabId; + return this.props.tabId; } } diff --git a/apps/server/src/contexts/auth/domain/entities/register-data.ts b/apps/server/src/contexts/auth/domain/entities/register-data.ts index 973ee196..9d6b39fe 100644 --- a/apps/server/src/contexts/auth/domain/entities/register-data.ts +++ b/apps/server/src/contexts/auth/domain/entities/register-data.ts @@ -48,14 +48,14 @@ export class RegisterData extends DomainEntity implements IR } get username(): Username { - return this._props.username; + return this.props.username; } get email(): EmailAddress { - return this._props.email; + return this.props.email; } get hashPassword(): HashPassword { - return this._props.hashPassword; + return this.props.hashPassword; } } diff --git a/apps/server/src/contexts/auth/domain/entities/tab-context.ts b/apps/server/src/contexts/auth/domain/entities/tab-context.ts index 30454902..8092361a 100644 --- a/apps/server/src/contexts/auth/domain/entities/tab-context.ts +++ b/apps/server/src/contexts/auth/domain/entities/tab-context.ts @@ -41,16 +41,16 @@ export class TabContext extends DomainEntity implements ITabCo } get tabId(): UniqueID { - return this._props.tabId; + return this.props.tabId; } get userId(): UniqueID { - return this._props.userId; + return this.props.userId; } toPersistenceData(): ITabContextPrimitives { return { - id: this._id.toString(), + id: this.id.toString(), tab_id: this.tabId.toString(), user_id: this.userId.toString(), }; diff --git a/apps/server/src/contexts/auth/domain/value-objects/auth-user-roles.ts b/apps/server/src/contexts/auth/domain/value-objects/auth-user-roles.ts index 435ba6b4..3598dc17 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/auth-user-roles.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/auth-user-roles.ts @@ -17,6 +17,6 @@ export class UserRoles extends ValueObject { } hasRole(role: string): boolean { - return this._value.includes(role); + return this.props.includes(role); } } diff --git a/apps/server/src/contexts/auth/domain/value-objects/email-address.ts b/apps/server/src/contexts/auth/domain/value-objects/email-address.ts deleted file mode 100644 index b893327a..00000000 --- a/apps/server/src/contexts/auth/domain/value-objects/email-address.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Result, ValueObject } from "@common/domain"; -import { logger } from "@common/infrastructure/logger"; -import { z } from "zod"; - -export const NULLED_EMAIL_ADDRESS = null; - -export class EmailAddress extends ValueObject { - static create(email: string | null): Result { - logger.debug(`Creating EmailAddress from ${email}`); - const normalizedEmail = - email?.trim() === "" ? NULLED_EMAIL_ADDRESS : email?.toLowerCase() || NULLED_EMAIL_ADDRESS; - - const result = EmailAddress.validate(normalizedEmail); - - return result.success - ? Result.ok(new EmailAddress(result.data)) - : Result.fail(new Error(result.error.errors[0].message)); - } - - private static validate(email: string | null) { - const schema = z.string().email({ message: "Invalid email format" }).or(z.null()); - return schema.safeParse(email); - } - - isDefined(): boolean { - return !this.isEmpty(); - } -} diff --git a/apps/server/src/contexts/auth/domain/value-objects/hash-password.spec.ts b/apps/server/src/contexts/auth/domain/value-objects/hash-password.spec.ts index 37e53d6d..55733874 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/hash-password.spec.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/hash-password.spec.ts @@ -1,33 +1,44 @@ +import bcrypt from "bcrypt"; import { HashPassword } from "./hash-password"; -describe("PasswordHash Value Object", () => { - it("should hash a valid password", async () => { - const result = HashPassword.create("StrongPass123"); - +describe("HashPassword", () => { + test("debe crear una instancia de HashPassword desde un texto plano válido", () => { + const result = HashPassword.createFromPlainText("securepassword"); expect(result.isSuccess).toBe(true); - expect(result.data.getValue()).not.toBe("StrongPass123"); // Should be hashed + expect(result.data).toBeInstanceOf(HashPassword); }); - it("should return an error for short password", async () => { - const result = HashPassword.create("12345"); - - expect(result.isSuccess).toBe(true); + test("debe fallar al crear una instancia con una contraseña demasiado corta", () => { + const result = HashPassword.createFromPlainText("123"); + expect(result.isFailure).toBe(true); expect(result.error.message).toBe("Password must be at least 6 characters long"); }); - it("should validate password comparison correctly", async () => { - const result = HashPassword.create("SecurePass123"); + test("debe crear una instancia de HashPassword desde un hash válido", () => { + const hashedPassword = bcrypt.hashSync("securepassword", 10); + const result = HashPassword.createFromHash(hashedPassword); expect(result.isSuccess).toBe(true); + expect(result.data).toBeInstanceOf(HashPassword); + }); - const isValid = await result.data.verifyPassword("SecurePass123"); + test("debe verificar correctamente una contraseña válida", async () => { + const password = "securepassword"; + const result = HashPassword.createFromPlainText(password); + expect(result.isSuccess).toBe(true); + const hashPasswordInstance = result.data; + + const isValid = await hashPasswordInstance.verifyPassword(password); expect(isValid).toBe(true); }); - it("should fail password comparison for incorrect passwords", async () => { - const result = HashPassword.create("SecurePass123"); + test("debe fallar la verificación con una contraseña incorrecta", async () => { + const password = "securepassword"; + const wrongPassword = "wrongpassword"; + const result = HashPassword.createFromPlainText(password); expect(result.isSuccess).toBe(true); + const hashPasswordInstance = result.data; - const isValid = await result.data.verifyPassword("WrongPassword"); + const isValid = await hashPasswordInstance.verifyPassword(wrongPassword); expect(isValid).toBe(false); }); }); diff --git a/apps/server/src/contexts/auth/domain/value-objects/hash-password.ts b/apps/server/src/contexts/auth/domain/value-objects/hash-password.ts index de2be1b2..68ea34c1 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/hash-password.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/hash-password.ts @@ -2,7 +2,11 @@ import { Result, ValueObject } from "@common/domain"; import bcrypt from "bcrypt"; import { z } from "zod"; -export class HashPassword extends ValueObject { +interface HashPasswordProps { + value: string; +} + +export class HashPassword extends ValueObject { private static readonly SALT_ROUNDS = 10; static createFromPlainText(plainTextPassword: string): Result { @@ -13,7 +17,7 @@ export class HashPassword extends ValueObject { } const hashed = bcrypt.hashSync(result.data, this.SALT_ROUNDS); - return Result.ok(new HashPassword(hashed)); + return Result.ok(new HashPassword({ value: hashed })); } private static validate(password: string) { @@ -22,10 +26,18 @@ export class HashPassword extends ValueObject { } static createFromHash(hashedPassword: string): Result { - return Result.ok(new HashPassword(hashedPassword)); + return Result.ok(new HashPassword({ value: hashedPassword })); } async verifyPassword(plainTextPassword: string): Promise { - return await bcrypt.compare(plainTextPassword, this._value); + return await bcrypt.compare(plainTextPassword, this.props.value); + } + + getValue() { + return this.props.value; + } + + toString() { + return this.props.value; } } diff --git a/apps/server/src/contexts/auth/domain/value-objects/index.ts b/apps/server/src/contexts/auth/domain/value-objects/index.ts index 944f6de9..b3bdb2e3 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/index.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/index.ts @@ -1,5 +1,4 @@ export * from "./auth-user-roles"; -export * from "./email-address"; export * from "./hash-password"; export * from "./plain-password"; export * from "./token"; diff --git a/apps/server/src/contexts/auth/domain/value-objects/plain-password.ts b/apps/server/src/contexts/auth/domain/value-objects/plain-password.ts index feb97143..3a131e46 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/plain-password.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/plain-password.ts @@ -1,7 +1,11 @@ import { Result, ValueObject } from "@common/domain"; import { z } from "zod"; -export class PlainPassword extends ValueObject { +interface PlainPasswordProps { + value: string; +} + +export class PlainPassword extends ValueObject { static create(plainTextPassword: string): Result { const result = PlainPassword.validate(plainTextPassword); @@ -9,11 +13,19 @@ export class PlainPassword extends ValueObject { return Result.fail(new Error(result.error.errors[0].message)); } - return Result.ok(new PlainPassword(result.data)); + return Result.ok(new PlainPassword({ value: result.data })); } private static validate(password: string) { const schema = z.string().min(6, { message: "Password must be at least 6 characters long" }); return schema.safeParse(password); } + + getValue() { + return this.props.value; + } + + toString() { + return this.props.value; + } } diff --git a/apps/server/src/contexts/auth/domain/value-objects/token.ts b/apps/server/src/contexts/auth/domain/value-objects/token.ts index 47f634fb..1440eebf 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/token.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/token.ts @@ -1,7 +1,11 @@ import { Result, ValueObject } from "@common/domain"; import { z } from "zod"; -export class Token extends ValueObject { +interface TokenProps { + value: string; +} + +export class Token extends ValueObject { static create(token: string): Result { const result = Token.validate(token); @@ -9,11 +13,19 @@ export class Token extends ValueObject { return Result.fail(new Error(result.error.errors[0].message)); } - return Result.ok(new Token(result.data)); + return Result.ok(new Token({ value: result.data })); } private static validate(token: string) { const schema = z.string().min(1, { message: "Invalid token string" }); return schema.safeParse(token); } + + getValue() { + return this.props.value; + } + + toString() { + return this.props.value; + } } diff --git a/apps/server/src/contexts/auth/domain/value-objects/username.ts b/apps/server/src/contexts/auth/domain/value-objects/username.ts index 9e47e740..96d96c90 100644 --- a/apps/server/src/contexts/auth/domain/value-objects/username.ts +++ b/apps/server/src/contexts/auth/domain/value-objects/username.ts @@ -1,12 +1,16 @@ import { Result, ValueObject } from "@common/domain"; import { z } from "zod"; -export class Username extends ValueObject { +interface UsernameProps { + value: string; +} + +export class Username extends ValueObject { static create(username: string): Result { const result = Username.validate(username); return result.success - ? Result.ok(new Username(result.data)) + ? Result.ok(new Username({ value: result.data })) : Result.fail(new Error(result.error.errors[0].message)); } @@ -21,4 +25,12 @@ export class Username extends ValueObject { return schema.safeParse(username); } + + getValue() { + return this.props.value; + } + + toString() { + return this.props.value; + } } diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts index 2d955df5..94b4c736 100644 --- a/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts +++ b/apps/server/src/contexts/auth/infraestructure/mappers/authenticated-user.mapper.ts @@ -1,5 +1,5 @@ -import { Result, UniqueID } from "@common/domain"; -import { AuthenticatedUser, EmailAddress, HashPassword, Username } from "@contexts/auth/domain"; +import { EmailAddress, Result, UniqueID } from "@common/domain"; +import { AuthenticatedUser, HashPassword, Username } from "@contexts/auth/domain"; import { AuthUserModel } from "../sequelize"; export interface IAuthenticatedUserMapper { diff --git a/apps/server/src/contexts/auth/infraestructure/mappers/user.mapper.ts b/apps/server/src/contexts/auth/infraestructure/mappers/user.mapper.ts index 47d9a851..582ebf87 100644 --- a/apps/server/src/contexts/auth/infraestructure/mappers/user.mapper.ts +++ b/apps/server/src/contexts/auth/infraestructure/mappers/user.mapper.ts @@ -1,5 +1,5 @@ -import { Result, UniqueID } from "@common/domain"; -import { EmailAddress, User, Username } from "@contexts/auth/domain"; +import { EmailAddress, Result, UniqueID } from "@common/domain"; +import { User, Username } from "@contexts/auth/domain"; import { UserModel } from "../sequelize"; export interface IUserMapper { diff --git a/apps/server/src/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts b/apps/server/src/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts index 0fc1011a..d212ec5d 100644 --- a/apps/server/src/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts +++ b/apps/server/src/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts @@ -35,7 +35,7 @@ export const checkUser = [ // Verifica que el usuario sea administrador export const checkUserIsAdmin = [ authProvider.authenticateJWT(), - _authorizeUser((user) => user.isAdmin), + _authorizeUser((user) => true /*user.isAdmin*/), ]; // Middleware para verificar que el usuario sea administrador o el dueño de los datos (self) diff --git a/apps/server/src/contexts/auth/infraestructure/sequelize/user.repository.ts b/apps/server/src/contexts/auth/infraestructure/sequelize/user.repository.ts index d471cc2d..681f2877 100644 --- a/apps/server/src/contexts/auth/infraestructure/sequelize/user.repository.ts +++ b/apps/server/src/contexts/auth/infraestructure/sequelize/user.repository.ts @@ -1,6 +1,6 @@ -import { Result, UniqueID } from "@common/domain"; +import { EmailAddress, Result, UniqueID } from "@common/domain"; import { SequelizeRepository } from "@common/infrastructure"; -import { EmailAddress, IUserRepository, User } from "@contexts/auth/domain"; +import { IUserRepository, User } from "@contexts/auth/domain"; import { Transaction } from "sequelize"; import { IUserMapper, userMapper } from "../mappers"; import { UserModel } from "./user.model"; diff --git a/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.controller.ts index fdd9881a..cbc28631 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.controller.ts @@ -10,7 +10,7 @@ export class ListUsersController extends ExpressController { super(); } - protected async executeImpl(): Promise { + protected async executeImpl() { const usersOrError = await this.listUsers.execute(); if (usersOrError.isFailure) { diff --git a/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts index 56a7d2bb..9f714344 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts @@ -10,5 +10,6 @@ export const listUsersPresenter: IListUsersPresenter = { users.map((user) => ({ id: user.id.toString(), email: user.email.toString(), + username: user.username.toString(), })), }; diff --git a/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts b/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts index 79cc1bad..6637580b 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/logout/logout.controller.ts @@ -1,6 +1,7 @@ import { ExpressController } from "@common/presentation"; import { LogoutUseCase } from "@contexts/auth/application/logout"; import { AuthenticatedUser, LogoutData, TabContext } from "@contexts/auth/domain"; +import { TabContextRequest } from "@contexts/auth/infraestructure/express/types"; export class LogoutController extends ExpressController { public constructor(private readonly logout: LogoutUseCase) { @@ -9,7 +10,7 @@ export class LogoutController extends ExpressController { async executeImpl() { const user = this.req.user as AuthenticatedUser; - const tabContext = this.req.tabContext as TabContext; + const tabContext = (this.req as TabContextRequest).tabContext as TabContext; const logoutDataOrError = LogoutData.create({ email: user.email, diff --git a/apps/server/src/contexts/auth/presentation/dto/user.response.dto.ts b/apps/server/src/contexts/auth/presentation/dto/user.response.dto.ts index 1bc570f2..5e68f303 100644 --- a/apps/server/src/contexts/auth/presentation/dto/user.response.dto.ts +++ b/apps/server/src/contexts/auth/presentation/dto/user.response.dto.ts @@ -1,4 +1,5 @@ export interface IListUsersResponseDTO { id: string; + username: string; email: string; } diff --git a/apps/server/src/contexts/companies/application/company-service.interface.ts b/apps/server/src/contexts/companies/application/company-service.interface.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/server/src/contexts/companies/application/company.service.ts b/apps/server/src/contexts/companies/application/company.service.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/server/src/contexts/companies/application/index.ts b/apps/server/src/contexts/companies/application/index.ts index e69de29b..943e2a00 100644 --- a/apps/server/src/contexts/companies/application/index.ts +++ b/apps/server/src/contexts/companies/application/index.ts @@ -0,0 +1 @@ +export * from "./list-companies"; diff --git a/apps/server/src/contexts/companies/application/list-companies/index.ts b/apps/server/src/contexts/companies/application/list-companies/index.ts new file mode 100644 index 00000000..39b989ec --- /dev/null +++ b/apps/server/src/contexts/companies/application/list-companies/index.ts @@ -0,0 +1 @@ +export * from "./list-companies.use-case"; diff --git a/apps/server/src/contexts/companies/application/list-companies/list-users.use-case.ts b/apps/server/src/contexts/companies/application/list-companies/list-users.use-case.ts new file mode 100644 index 00000000..ffc84ba4 --- /dev/null +++ b/apps/server/src/contexts/companies/application/list-companies/list-users.use-case.ts @@ -0,0 +1,17 @@ +import { Result } from "@common/domain"; +import { ITransactionManager } from "@common/infrastructure/database"; +import { User } from "@contexts/auth/domain"; +import { IUserService } from "@contexts/auth/domain/services"; + +export class ListCompaniesUseCase { + constructor( + private readonly userService: IUserService, + private readonly transactionManager: ITransactionManager + ) {} + + public async execute(): Promise> { + return await this.transactionManager.complete(async (transaction) => { + return await this.userService.findCompanies(transaction); + }); + } +} diff --git a/apps/server/src/contexts/companies/domain/aggregates/company.ts b/apps/server/src/contexts/companies/domain/aggregates/company.ts new file mode 100644 index 00000000..c11db356 --- /dev/null +++ b/apps/server/src/contexts/companies/domain/aggregates/company.ts @@ -0,0 +1,130 @@ +import { + AggregateRoot, + EmailAddress, + PhoneNumber, + PostalAddress, + TINNumber, + UniqueID, +} from "@common/domain"; +import { Maybe, Result } from "@common/helpers"; + +export interface ICompanyProps { + isFreelancer: boolean; + name: string; + tin: TINNumber; + address: PostalAddress; + email: EmailAddress; + phone: PhoneNumber; + legalRecord: string; + defaultTax: number; + status: string; + langCode: string; + currencyCode: string; + + tradeName: Maybe; + website: Maybe; + fax: Maybe; + logo: Maybe; +} + +export interface ICompany { + id: UniqueID; + name: string; + tin: TINNumber; + address: PostalAddress; + email: EmailAddress; + phone: PhoneNumber; + legalRecord: string; + defaultTax: number; + langCode: string; + currencyCode: string; + + tradeName: Maybe; + fax: Maybe; + website: Maybe; + logo: Maybe; + + isCompany: boolean; + isFreelancer: boolean; + isActive: boolean; +} + +export class Company extends AggregateRoot implements ICompany { + static create(props: ICompanyProps, id?: UniqueID): Result { + const company = new Company(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + // 🔹 Disparar evento de dominio "CompanyAuthenticatedEvent" + //const { company } = props; + //user.addDomainEvent(new CompanyAuthenticatedEvent(id, company.toString())); + + return Result.ok(company); + } + + get name() { + return this.props.name; + } + + get tradeName() { + return this.props.tradeName; + } + + get tin(): TINNumber { + return this.props.tin; + } + + get address(): PostalAddress { + return this.props.address; + } + + get email(): EmailAddress { + return this.props.email; + } + + get phone(): PhoneNumber { + return this.props.phone; + } + + get fax(): Maybe { + return this.props.fax; + } + + get website() { + return this.props.website; + } + + get legalRecord() { + return this.props.legalRecord; + } + + get defaultTax() { + return this.props.defaultTax; + } + + get langCode() { + return this.props.langCode; + } + + get currencyCode() { + return this.props.currencyCode; + } + + get logo() { + return this.props.logo; + } + + get isCompany(): boolean { + return !this.props.isFreelancer; + } + + get isFreelancer(): boolean { + return this.props.isFreelancer; + } + + get isActive(): boolean { + return this.props.status === "active"; + } +} diff --git a/apps/server/src/contexts/companies/domain/aggregates/index.ts b/apps/server/src/contexts/companies/domain/aggregates/index.ts new file mode 100644 index 00000000..2e80831a --- /dev/null +++ b/apps/server/src/contexts/companies/domain/aggregates/index.ts @@ -0,0 +1 @@ +export * from "./company"; diff --git a/apps/server/src/contexts/companies/domain/services/company-service.interface.ts b/apps/server/src/contexts/companies/domain/services/company-service.interface.ts new file mode 100644 index 00000000..7e56c4dc --- /dev/null +++ b/apps/server/src/contexts/companies/domain/services/company-service.interface.ts @@ -0,0 +1,7 @@ +import { Result, UniqueID } from "@common/domain"; +import { Company } from "../aggregates"; + +export interface ICompanyService { + findCompanies(transaction?: any): Promise>; + findCompanyById(userId: UniqueID, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/companies/domain/services/company.service.ts b/apps/server/src/contexts/companies/domain/services/company.service.ts new file mode 100644 index 00000000..5c6a7086 --- /dev/null +++ b/apps/server/src/contexts/companies/domain/services/company.service.ts @@ -0,0 +1,22 @@ +import { Result, UniqueID } from "@common/domain"; +import { Company, ICompanyRepository } from ".."; +import { ICompanyService } from "./company-service.interface"; + +export class CompanyService implements ICompanyService { + constructor(private readonly companyRepository: ICompanyRepository) {} + + async findCompanies(transaction?: any): Promise> { + const companysOrError = await this.companyRepository.findAll(transaction); + if (companysOrError.isFailure) { + return Result.fail(companysOrError.error); + } + + // Solo devolver usuarios activos + const activeCompanies = companysOrError.data.filter((company) => company /*.isActive*/); + return Result.ok(activeCompanies); + } + + async findCompanyById(companyId: UniqueID, transaction?: any): Promise> { + return await this.companyRepository.findById(companyId, transaction); + } +} diff --git a/apps/server/src/contexts/companies/infraestructure/mappers/company.mapper.ts b/apps/server/src/contexts/companies/infraestructure/mappers/company.mapper.ts new file mode 100644 index 00000000..3658cf2d --- /dev/null +++ b/apps/server/src/contexts/companies/infraestructure/mappers/company.mapper.ts @@ -0,0 +1,100 @@ +import { EmailAddress, PhoneNumber, PostalAddress, TINNumber, UniqueID } from "@common/domain"; +import { Maybe, Result } from "@common/helpers"; +import { + ISequelizeMapper, + MapperParamsType, + SequelizeMapper, +} from "@common/infrastructure/sequelize/sequelize-mapper"; +import { Company } from "@contexts/companies/domain/aggregates/company"; +import { CompanyCreationAttributes, CompanyModel } from "../sequelize/company.model"; + +export interface ICompanyMapper + extends ISequelizeMapper {} + +export class CompanyMapper + extends SequelizeMapper + implements ICompanyMapper +{ + public mapToDomain(source: CompanyModel, params?: MapperParamsType): Result { + const idOrError = UniqueID.create(source.id); + const tinOrError = TINNumber.create(source.tin); + const emailOrError = EmailAddress.create(source.email); + const phoneOrError = PhoneNumber.create(source.phone); + const faxOrError = PhoneNumber.createNullable(source.fax); + const postalAddressOrError = PostalAddress.create({ + street: source.street, + city: source.city, + state: source.state, + postalCode: source.postal_code, + country: source.country, + }); + + const result = Result.combine([ + idOrError, + tinOrError, + emailOrError, + phoneOrError, + faxOrError, + postalAddressOrError, + ]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Company.create( + { + isFreelancer: source.is_freelancer, + name: source.name, + tradeName: source.trade_name ? Maybe.Some(source.trade_name) : Maybe.None(), + tin: tinOrError.data, + address: postalAddressOrError.data, + email: emailOrError.data, + phone: phoneOrError.data, + fax: faxOrError.data, + website: source.website ? Maybe.Some(source.website) : Maybe.None(), + legalRecord: source.legal_record, + defaultTax: source.default_tax, + status: source.status, + langCode: source.lang_code, + currencyCode: source.currency_code, + logo: source.logo ? Maybe.Some(source.logo) : Maybe.None(), + }, + idOrError.data + ); + } + + public mapToPersistence( + source: Company, + params?: MapperParamsType + ): Result { + return Result.ok({ + id: source.id.toString(), + is_freelancer: source.isFreelancer, + name: source.name, + trade_name: source.tradeName.isSome() ? source.tradeName.getValue() : undefined, + tin: source.tin.toString(), + + street: source.address.street, + city: source.address.city, + state: source.address.state, + postal_code: source.address.postalCode, + country: source.address.country, + + email: source.email.toString(), + phone: source.phone.toString(), + fax: source.fax.isSome() ? source.fax.getValue()?.toString() : undefined, + website: source.website.isSome() ? source.website.getValue() : undefined, + + legal_record: source.legalRecord, + default_tax: source.defaultTax, + status: source.isActive ? "active" : "inactive", + lang_code: source.langCode, + currency_code: source.currencyCode, + logo: source.logo.isSome() ? source.logo.getValue() : undefined, + }); + } +} + +const companyMapper: CompanyMapper = new CompanyMapper(); +export { companyMapper }; diff --git a/apps/server/src/contexts/companies/infraestructure/mappers/index.ts b/apps/server/src/contexts/companies/infraestructure/mappers/index.ts new file mode 100644 index 00000000..7bf6f481 --- /dev/null +++ b/apps/server/src/contexts/companies/infraestructure/mappers/index.ts @@ -0,0 +1 @@ +export * from "./company.mapper"; diff --git a/apps/server/src/contexts/companies/infraestructure/sequelize/company.model.ts b/apps/server/src/contexts/companies/infraestructure/sequelize/company.model.ts new file mode 100644 index 00000000..e4a6c263 --- /dev/null +++ b/apps/server/src/contexts/companies/infraestructure/sequelize/company.model.ts @@ -0,0 +1,170 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + Sequelize, +} from "sequelize"; + +export type CompanyCreationAttributes = InferCreationAttributes & {}; + +export class CompanyModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + + declare id: string; + + declare is_freelancer: boolean; + declare name: string; + declare trade_name: CreationOptional; + declare tin: string; + + declare street: string; + declare city: string; + declare state: string; + declare postal_code: string; + declare country: string; + + declare email: string; + declare phone: string; + declare fax: CreationOptional; + declare website: CreationOptional; + + declare legal_record: string; + + declare default_tax: number; + declare status: string; + declare lang_code: string; + declare currency_code: string; + declare logo: CreationOptional; +} + +export default (sequelize: Sequelize) => { + CompanyModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + is_freelancer: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + trade_name: { + type: DataTypes.STRING, + allowNull: true, + }, + tin: { + type: DataTypes.STRING, + allowNull: false, + }, + + street: { + type: DataTypes.STRING, + allowNull: false, + }, + city: { + type: DataTypes.STRING, + allowNull: false, + }, + state: { + type: DataTypes.STRING, + allowNull: false, + }, + postal_code: { + type: DataTypes.STRING, + allowNull: false, + }, + country: { + type: DataTypes.STRING, + allowNull: false, + }, + + email: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isEmail: true, + }, + }, + phone: { + type: DataTypes.STRING, + allowNull: false, + }, + fax: { + type: DataTypes.STRING, + allowNull: true, + }, + website: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isUrl: true, + }, + }, + legal_record: { + type: DataTypes.TEXT, + allowNull: false, + }, + + default_tax: { + type: new DataTypes.SMALLINT(), + allowNull: false, + defaultValue: 2100, + }, + + logo: { + type: DataTypes.STRING, + allowNull: true, + }, + + lang_code: { + type: DataTypes.STRING(2), + allowNull: false, + defaultValue: "es", + }, + + currency_code: { + type: new DataTypes.STRING(3), + allowNull: false, + defaultValue: "EUR", + }, + + status: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "active", + }, + }, + { + sequelize, + tableName: "companies", + + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [{ name: "email_idx", fields: ["email"], unique: true }], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + return CompanyModel; +}; diff --git a/apps/server/src/contexts/companies/infraestructure/sequelize/company.repository.ts b/apps/server/src/contexts/companies/infraestructure/sequelize/company.repository.ts new file mode 100644 index 00000000..59230bdb --- /dev/null +++ b/apps/server/src/contexts/companies/infraestructure/sequelize/company.repository.ts @@ -0,0 +1,81 @@ +import { EmailAddress, UniqueID } from "@common/domain"; +import { Collection, Result } from "@common/helpers"; +import { SequelizeRepository } from "@common/infrastructure"; +import { Company, ICompanyRepository } from "@contexts/companies/domain"; +import { Transaction } from "sequelize"; +import { companyMapper, ICompanyMapper } from "../mappers"; +import { CompanyModel } from "./company.model"; + +class CompanyRepository extends SequelizeRepository implements ICompanyRepository { + private readonly _mapper!: ICompanyMapper; + + /** + * 🔹 Función personalizada para mapear errores de unicidad en autenticación + */ + private _customErrorMapper(error: Error): string | null { + if (error.name === "SequelizeUniqueConstraintError") { + return "Company with this email already exists"; + } + + return null; + } + + constructor(mapper: ICompanyMapper) { + super(); + this._mapper = mapper; + } + + async findAll(transaction?: Transaction): Promise, Error>> { + try { + const rawCompanys: any = await this._findAll(CompanyModel, {}, transaction); + + if (!rawCompanys === true) { + return Result.fail(new Error("Company with email not exists")); + } + + return this._mapper.mapArrayToDomain(rawCompanys); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async findById(id: UniqueID, transaction?: Transaction): Promise> { + try { + const rawCompany: any = await this._getById(CompanyModel, id, {}, transaction); + + if (!rawCompany === true) { + return Result.fail(new Error(`Company with id ${id.toString()} not exists`)); + } + + return this._mapper.mapToDomain(rawCompany); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async findByEmail( + email: EmailAddress, + transaction?: Transaction + ): Promise> { + try { + const rawCompany: any = await this._getBy( + CompanyModel, + "email", + email.toString(), + {}, + transaction + ); + + if (!rawCompany === true) { + return Result.fail(new Error(`Company with email ${email.toString()} not exists`)); + } + + return this._mapper.mapToDomain(rawCompany); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } +} + +const companyRepository = new CompanyRepository(companyMapper); +export { companyRepository }; diff --git a/apps/server/src/contexts/companies/infraestructure/sequelize/index.ts b/apps/server/src/contexts/companies/infraestructure/sequelize/index.ts new file mode 100644 index 00000000..1d4e8eed --- /dev/null +++ b/apps/server/src/contexts/companies/infraestructure/sequelize/index.ts @@ -0,0 +1,9 @@ +import { ICompanyRepository } from "@contexts/companies/domain"; +import { companyRepository } from "./company.repository"; + +export * from "./company.model"; +export * from "./company.repository"; + +export const createCompanyRepository = (): ICompanyRepository => { + return companyRepository; +}; diff --git a/apps/server/src/contexts/companies/presentation/controllers/index.ts b/apps/server/src/contexts/companies/presentation/controllers/index.ts new file mode 100644 index 00000000..943e2a00 --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/controllers/index.ts @@ -0,0 +1 @@ +export * from "./list-companies"; diff --git a/apps/server/src/contexts/companies/presentation/controllers/list-companies/index.ts b/apps/server/src/contexts/companies/presentation/controllers/list-companies/index.ts new file mode 100644 index 00000000..adb9effe --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/controllers/list-companies/index.ts @@ -0,0 +1,15 @@ +import { SequelizeTransactionManager } from "@common/infrastructure"; +import { ListcompaniesUseCase } from "@contexts/auth/application/list-companies/list-companies.use-case"; +import { userRepository } from "@contexts/auth/infraestructure"; +import { ListcompaniesController } from "./list-companies.controller"; +import { listcompaniesPresenter } from "./list-companies.presenter"; + +export const listcompaniesController = () => { + const transactionManager = new SequelizeTransactionManager(); + const companieservice = new companieservice(userRepository); + + const useCase = new ListcompaniesUseCase(companieservice, transactionManager); + const presenter = listcompaniesPresenter; + + return new ListcompaniesController(useCase, presenter); +}; diff --git a/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.controller.ts b/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.controller.ts new file mode 100644 index 00000000..0e6f7d80 --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.controller.ts @@ -0,0 +1,37 @@ +import { ExpressController } from "@common/presentation"; +import { ListCompaniesUseCase } from "@contexts/companies/application/list-companies/list-users.use-case"; +import { IListCompaniesPresenter } from "./list-companies.presenter"; + +export class ListcompaniesController extends ExpressController { + public constructor( + private readonly listcompanies: ListCompaniesUseCase, + private readonly presenter: IListCompaniesPresenter + ) { + super(); + } + + protected async executeImpl() { + const companiesOrError = await this.listcompanies.execute(); + + if (companiesOrError.isFailure) { + return this.handleError(companiesOrError.error); + } + + return this.ok(this.presenter.toDTO(companiesOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.conflictError(message); + } +} diff --git a/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.presenter.ts b/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.presenter.ts new file mode 100644 index 00000000..344293b6 --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.presenter.ts @@ -0,0 +1,38 @@ +import { Collection, ensureString } from "@common/helpers"; +import { Company } from "@contexts/companies/domain"; +import { IListCompaniesResponseDTO } from "../../dto"; + +export interface IListCompaniesPresenter { + toDTO: (companies: Collection) => IListCompaniesResponseDTO[]; +} + +export const listCompaniesPresenter: IListCompaniesPresenter = { + toDTO: (companies: Collection): IListCompaniesResponseDTO[] => + companies.map((company) => ({ + id: ensureString(company.id.toString()), + + is_freelancer: ensureBoolean(company.isFreelancer), + name: ensureString(company.name), + trade_name: ensureString(company.tradeName.getValue()), + tin: ensureString(company.tin.toString()), + + street: ensureString(company.address.street), + city: ensureString(company.address.city), + state: ensureString(company.address.state), + postal_code: ensureString(company.address.postalCode), + country: ensureString(company.address.country), + + email: ensureString(company.email.toString()), + phone: ensureString(company.phone.toString()), + fax: ensureString(company.fax.getValue()?.toString()), + website: ensureString(company.website.getValue()), + + legal_record: ensureString(company.legalRecord), + + default_tax: ensureNumber(company.defaultTax), + status: ensureString(company.isActive ? "active" : "inactive"), + lang_code: ensureString(company.langCode), + currency_code: ensureString(company.currencyCode), + logo: ensureString(company.logo.getValue()), + })), +}; diff --git a/apps/server/src/contexts/companies/presentation/dto/companies.request.dto.ts b/apps/server/src/contexts/companies/presentation/dto/companies.request.dto.ts new file mode 100644 index 00000000..8ae02aa2 --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/dto/companies.request.dto.ts @@ -0,0 +1 @@ +export interface IListCompaniesRequestDTO {} diff --git a/apps/server/src/contexts/companies/presentation/dto/companies.response.dto.ts b/apps/server/src/contexts/companies/presentation/dto/companies.response.dto.ts new file mode 100644 index 00000000..1468a294 --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/dto/companies.response.dto.ts @@ -0,0 +1,27 @@ +export interface IListCompaniesResponseDTO { + id: string; + + is_freelancer: boolean; + name: string; + trade_name: string; + tin: string; + + street: string; + city: string; + state: string; + postal_code: string; + country: string; + + email: string; + phone: string; + fax: string; + website: string; + + legal_record: string; + + default_tax: number; + status: string; + lang_code: string; + currency_code: string; + logo: string; +} diff --git a/apps/server/src/contexts/companies/presentation/dto/companies.validation.dto.ts b/apps/server/src/contexts/companies/presentation/dto/companies.validation.dto.ts new file mode 100644 index 00000000..54b1803a --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/dto/companies.validation.dto.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const ListCompaniesSchema = z.object({}); diff --git a/apps/server/src/contexts/companies/presentation/dto/index.ts b/apps/server/src/contexts/companies/presentation/dto/index.ts new file mode 100644 index 00000000..3109872c --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/dto/index.ts @@ -0,0 +1,3 @@ +export * from "./companies.request.dto"; +export * from "./companies.response.dto"; +export * from "./companies.validation.dto"; diff --git a/apps/server/src/contexts/companies/presentation/index.ts b/apps/server/src/contexts/companies/presentation/index.ts new file mode 100644 index 00000000..a123289d --- /dev/null +++ b/apps/server/src/contexts/companies/presentation/index.ts @@ -0,0 +1,2 @@ +export * from "./controllers"; +export * from "./dto"; diff --git a/apps/server/src/routes/company.routes.ts b/apps/server/src/routes/company.routes.ts index 5bac220b..eb97d3f9 100644 --- a/apps/server/src/routes/company.routes.ts +++ b/apps/server/src/routes/company.routes.ts @@ -1,47 +1,18 @@ import { validateRequestDTO } from "@common/presentation"; -import { createAuthProvider } from "@contexts/company/infraestructure"; -import { validateTabContextHeader } from "@contexts/company/presentation"; -import { createLoginController } from "@contexts/company/presentation/controllers"; -import { createLogoutController } from "@contexts/company/presentation/controllers/logout/logout.controller"; -import { createRegisterController } from "@contexts/company/presentation/controllers/register/register.controller"; -import { LoginUserSchema, RegisterUserSchema } from "@contexts/company/presentation/dto"; +import { checkTabContext, checkUser } from "@contexts/auth/infraestructure"; +import { ListCompaniesSchema } from "@contexts/companies/presentation"; import { NextFunction, Request, Response, Router } from "express"; export const companyRouter = (appRouter: Router) => { const companyRoutes: Router = Router({ mergeParams: true }); - const authProvider = createAuthProvider(); companyRoutes.get( "/", - /*validateRequestDTO(ListCompaniesSchema),*/ - validateTabContextHeader, - authProvider.companyenticateJWT(), - - getDealerMiddleware, - handleRequest(listQuotesController) - ); - companyRoutes.get("/:quoteId", checkUser, getDealerMiddleware, handleRequest(getQuoteController)); - companyRoutes.post("/", checkUser, getDealerMiddleware, handleRequest(createQuoteController)); - - companyRoutes.post("/register", validateRequestDTO(RegisterUserSchema), (req, res, next) => { - createRegisterController().execute(req, res, next); - }); - - companyRoutes.post( - "/login", - validateRequestDTO(LoginUserSchema), - validateTabContextHeader, + validateRequestDTO(ListCompaniesSchema), + checkTabContext, + checkUser, (req: Request, res: Response, next: NextFunction) => { - createLoginController().execute(req, res, next); - } - ); - - companyRoutes.post( - "/logout", - validateTabContextHeader, - authProvider.companyenticateJWT(), - (req: Request, res: Response, next: NextFunction) => { - createLogoutController().execute(req, res, next); + listCompaniesController().execute(req, res, next); } ); diff --git a/apps/server/src/routes/user.routes.ts b/apps/server/src/routes/user.routes.ts index c98778bc..9d65978d 100644 --- a/apps/server/src/routes/user.routes.ts +++ b/apps/server/src/routes/user.routes.ts @@ -1,4 +1,5 @@ import { validateRequestDTO } from "@common/presentation"; +import { checkTabContext, checkUserIsAdmin } from "@contexts/auth/infraestructure"; import { listUsersController, ListUsersSchema } from "@contexts/auth/presentation"; import { NextFunction, Request, Response, Router } from "express"; @@ -8,10 +9,9 @@ export const userRouter = (appRouter: Router) => { authRoutes.get( "/", validateRequestDTO(ListUsersSchema), - //validateTabContextHeader, - //authProvider.authenticateJWT(), - //authProvider.checkIsAdmin(), - async (req: Request, res: Response, next: NextFunction) => { + checkTabContext, + checkUserIsAdmin, + (req: Request, res: Response, next: NextFunction) => { listUsersController().execute(req, res, next); } ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b0ee3d2..7c163635 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 + libphonenumber-js: + specifier: ^1.11.20 + version: 1.11.20 luxon: specifier: ^3.5.0 version: 3.5.0 @@ -3093,6 +3096,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libphonenumber-js@1.11.20: + resolution: {integrity: sha512-/ipwAMvtSZRdiQBHqW1qxqeYiBMzncOQLVA+62MWYr7N4m7Q2jqpJ0WgT7zlOEOpyLRSqrMXidbJpC0J77AaKA==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -8019,6 +8025,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.11.20: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {}