.
This commit is contained in:
parent
cf38a2ad9d
commit
085a390aa6
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -9,7 +9,6 @@
|
||||
"prettier.useEditorConfig": false,
|
||||
"prettier.useTabs": false,
|
||||
"prettier.configPath": ".prettierrc",
|
||||
"asciidoc.antora.enableAntoraSupport": true,
|
||||
|
||||
// other vscode settings
|
||||
"tailwindCSS.rootFontSize": 16,
|
||||
|
||||
7
apps/server/jest.config.js
Normal file
7
apps/server/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} **/
|
||||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
"^.+.tsx?$": ["ts-jest",{}],
|
||||
},
|
||||
};
|
||||
@ -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",
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { UniqueID } from "./value-objects/unique-id";
|
||||
|
||||
export abstract class DomainEntity<T extends object> {
|
||||
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<T extends object> {
|
||||
}, {});
|
||||
}
|
||||
|
||||
get id(): UniqueID {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
equals(other: DomainEntity<T>): 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)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
50
apps/server/src/common/domain/value-objects/email-address.ts
Normal file
50
apps/server/src/common/domain/value-objects/email-address.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Maybe, Result, ValueObject } from "@common/domain";
|
||||
import { z } from "zod";
|
||||
|
||||
interface EmailAddressProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class EmailAddress extends ValueObject<EmailAddressProps> {
|
||||
static create(value: string): Result<EmailAddress, Error> {
|
||||
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<Maybe<EmailAddress>, Error> {
|
||||
if (!value || value.trim() === "") {
|
||||
return Result.ok(Maybe.None<EmailAddress>());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
41
apps/server/src/common/domain/value-objects/name.spec.ts
Normal file
41
apps/server/src/common/domain/value-objects/name.spec.ts
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
61
apps/server/src/common/domain/value-objects/name.ts
Normal file
61
apps/server/src/common/domain/value-objects/name.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { Maybe, Result, ValueObject } from "@common/domain";
|
||||
import { z } from "zod";
|
||||
|
||||
interface NameProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class Name extends ValueObject<NameProps> {
|
||||
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<Maybe<Name>, Error> {
|
||||
if (!value || value.trim() === "") {
|
||||
return Result.ok(Maybe.None<Name>());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
63
apps/server/src/common/domain/value-objects/phone-number.ts
Normal file
63
apps/server/src/common/domain/value-objects/phone-number.ts
Normal file
@ -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<PhoneNumberProps> {
|
||||
static create(value: string): Result<PhoneNumber> {
|
||||
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<Maybe<PhoneNumber>, Error> {
|
||||
if (!value || value.trim() === "") {
|
||||
return Result.ok(Maybe.None<PhoneNumber>());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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<IPostalAddressProps> {
|
||||
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<PostalAddress, Error> {
|
||||
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<Maybe<PostalAddress>, Error> {
|
||||
if (!values || Object.values(values).every((value) => value.trim() === "")) {
|
||||
return Result.ok(Maybe.None<PostalAddress>());
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
41
apps/server/src/common/domain/value-objects/slug.spec.ts
Normal file
41
apps/server/src/common/domain/value-objects/slug.spec.ts
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
48
apps/server/src/common/domain/value-objects/slug.ts
Normal file
48
apps/server/src/common/domain/value-objects/slug.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Maybe, Result, ValueObject } from "@common/domain";
|
||||
import { z } from "zod";
|
||||
|
||||
interface SlugProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class Slug extends ValueObject<SlugProps> {
|
||||
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<Maybe<Slug>, Error> {
|
||||
if (!value || value.trim() === "") {
|
||||
return Result.ok(Maybe.None<Slug>());
|
||||
}
|
||||
|
||||
return Slug.create(value!).map((value) => Maybe.Some(value));
|
||||
}
|
||||
|
||||
getValue(): string {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.getValue();
|
||||
}
|
||||
}
|
||||
51
apps/server/src/common/domain/value-objects/tin-number.ts
Normal file
51
apps/server/src/common/domain/value-objects/tin-number.ts
Normal file
@ -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<TINNumberProps> {
|
||||
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<TINNumber, Error> {
|
||||
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<Maybe<TINNumber>, Error> {
|
||||
if (!value || value.trim() === "") {
|
||||
return Result.ok(Maybe.None<TINNumber>());
|
||||
}
|
||||
|
||||
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) : "";
|
||||
}
|
||||
}
|
||||
@ -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<string | undefined> {
|
||||
protected readonly _hasId!: boolean;
|
||||
|
||||
protected constructor(id?: string) {
|
||||
super(id);
|
||||
this._hasId = id != UNDEFINED_ID;
|
||||
}
|
||||
|
||||
export class UniqueID extends ValueObject<string> {
|
||||
static create(id?: string, generateOnEmpty: boolean = false): Result<UniqueID, Error> {
|
||||
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<string | undefined> {
|
||||
}
|
||||
|
||||
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<UniqueID, never> {
|
||||
return Result.ok(new UniqueID(uuidv4()));
|
||||
}
|
||||
|
||||
static generateUndefinedID(): Result<UniqueID, never> {
|
||||
return Result.ok(new UniqueID(UNDEFINED_ID));
|
||||
getValue(): string {
|
||||
return this.props;
|
||||
}
|
||||
|
||||
isDefined(): boolean {
|
||||
return this._hasId;
|
||||
toString(): string {
|
||||
return this.props;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,33 +1,31 @@
|
||||
import { shallowEqual } from "shallow-equal-object";
|
||||
|
||||
export abstract class ValueObject<T> {
|
||||
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<T>): 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) : "";
|
||||
}*/
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
40
apps/server/src/common/helpers/collection.spec.ts
Normal file
40
apps/server/src/common/helpers/collection.spec.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Collection } from "./collection";
|
||||
|
||||
describe("Collection<T>", () => {
|
||||
let collection: Collection<number>;
|
||||
|
||||
beforeEach(() => {
|
||||
collection = new Collection<number>([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);
|
||||
});
|
||||
});
|
||||
82
apps/server/src/common/helpers/collection.ts
Normal file
82
apps/server/src/common/helpers/collection.ts
Normal file
@ -0,0 +1,82 @@
|
||||
// Interfaz para definir las operaciones básicas de una colección
|
||||
/*interface ICollection<T> {
|
||||
reset(): void;
|
||||
add(item: T): void;
|
||||
remove(item: T): boolean;
|
||||
getAll(): T[];
|
||||
size(): number;
|
||||
total(): number | null;
|
||||
}*/
|
||||
|
||||
export class Collection<T> /*implements ICollection<T>*/ {
|
||||
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<U>(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);
|
||||
}
|
||||
}
|
||||
4
apps/server/src/common/helpers/index.ts
Normal file
4
apps/server/src/common/helpers/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./collection";
|
||||
export * from "./maybe";
|
||||
export * from "./result";
|
||||
export * from "./utils";
|
||||
31
apps/server/src/common/helpers/maybe.spec.ts
Normal file
31
apps/server/src/common/helpers/maybe.spec.ts
Normal file
@ -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<number>();
|
||||
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<number>();
|
||||
const maybeTransformed = maybeEmpty.map((n) => n * 2);
|
||||
|
||||
expect(maybeTransformed.isSome()).toBe(false);
|
||||
expect(maybeTransformed.getValue()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
34
apps/server/src/common/helpers/maybe.ts
Normal file
34
apps/server/src/common/helpers/maybe.ts
Normal file
@ -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<number>();
|
||||
* console.log(noValue.isSome()); // false
|
||||
**/
|
||||
|
||||
export class Maybe<T> {
|
||||
private constructor(private readonly value?: T) {}
|
||||
|
||||
static Some<T>(value: T): Maybe<T> {
|
||||
return new Maybe<T>(value);
|
||||
}
|
||||
|
||||
static None<T>(): Maybe<T> {
|
||||
return new Maybe<T>();
|
||||
}
|
||||
|
||||
isSome(): boolean {
|
||||
return this.value !== undefined;
|
||||
}
|
||||
|
||||
getValue(): T | undefined {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Maybe<U> {
|
||||
return this.isSome() ? Maybe.Some(fn(this.value as T)) : Maybe.None();
|
||||
}
|
||||
}
|
||||
73
apps/server/src/common/helpers/result.spec.ts
Normal file
73
apps/server/src/common/helpers/result.spec.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
@ -67,6 +67,13 @@ export class Result<T, E extends Error = Error> {
|
||||
return this._data as T;
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Result<U, Error> {
|
||||
if (this.isSuccess && this._data !== undefined) {
|
||||
return Result.ok<U>(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`.
|
||||
46
apps/server/src/common/helpers/utils.spec.ts
Normal file
46
apps/server/src/common/helpers/utils.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
// Función genérica para asegurar valores básicos
|
||||
function ensure<T>(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");
|
||||
});
|
||||
});
|
||||
12
apps/server/src/common/helpers/utils.ts
Normal file
12
apps/server/src/common/helpers/utils.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// Función genérica para asegurar valores básicos
|
||||
function ensure<T>(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);
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./sequelize-mapper";
|
||||
export * from "./sequelize-repository";
|
||||
export * from "./sequelize-transaction-manager";
|
||||
|
||||
@ -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<any> {}
|
||||
class MockModel extends Model {
|
||||
dataValues: Record<string, any> = {};
|
||||
}
|
||||
|
||||
// Mock Mapper
|
||||
class MockSequelizeMapper extends SequelizeMapper<MockModel, Record<string, any>, MockEntity> {
|
||||
public mapToDomain(source: MockModel, params?: Record<string, any>): Result<MockEntity, Error> {
|
||||
return Result.ok(new MockEntity({ ...source.dataValues, ...params }));
|
||||
}
|
||||
|
||||
public mapToPersistence(
|
||||
source: MockEntity,
|
||||
params?: Record<string, any>
|
||||
): Result<Record<string, any>, 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");
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,121 @@
|
||||
import { DomainEntity } from "@common/domain";
|
||||
import { Collection, Result } from "@common/helpers";
|
||||
import { Model } from "sequelize";
|
||||
|
||||
export type MapperParamsType = Record<string, any>;
|
||||
|
||||
interface IDomainMapper<TModel extends Model, TEntity extends DomainEntity<any>> {
|
||||
mapToDomain(source: TModel, params?: MapperParamsType): Result<TEntity, Error>;
|
||||
mapArrayToDomain(source: TModel[], params?: MapperParamsType): Result<Collection<TEntity>, Error>;
|
||||
mapArrayAndCountToDomain(
|
||||
source: TModel[],
|
||||
totalCount: number,
|
||||
params?: MapperParamsType
|
||||
): Result<Collection<TEntity>, Error>;
|
||||
}
|
||||
|
||||
interface IPersistenceMapper<TModelAttributes, TEntity extends DomainEntity<any>> {
|
||||
mapToPersistence(source: TEntity, params?: MapperParamsType): Result<TModelAttributes, Error>;
|
||||
mapCollectionToPersistence(
|
||||
source: Collection<TEntity>,
|
||||
params?: MapperParamsType
|
||||
): Result<TModelAttributes[], Error>;
|
||||
}
|
||||
|
||||
export interface ISequelizeMapper<
|
||||
TModel extends Model,
|
||||
TModelAttributes,
|
||||
TEntity extends DomainEntity<any>,
|
||||
> extends IDomainMapper<TModel, TEntity>,
|
||||
IPersistenceMapper<TModelAttributes, TEntity> {}
|
||||
|
||||
export abstract class SequelizeMapper<
|
||||
TModel extends Model,
|
||||
TModelAttributes,
|
||||
TEntity extends DomainEntity<any>,
|
||||
> implements ISequelizeMapper<TModel, TModelAttributes, TEntity>
|
||||
{
|
||||
public abstract mapToDomain(source: TModel, params?: MapperParamsType): Result<TEntity, Error>;
|
||||
|
||||
public mapArrayToDomain(
|
||||
source: TModel[],
|
||||
params?: MapperParamsType
|
||||
): Result<Collection<TEntity>, Error> {
|
||||
return this.mapArrayAndCountToDomain(source, source.length, params);
|
||||
}
|
||||
|
||||
public mapArrayAndCountToDomain(
|
||||
source: TModel[],
|
||||
totalCount: number,
|
||||
params?: MapperParamsType
|
||||
): Result<Collection<TEntity>, 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<TModelAttributes, Error>;
|
||||
|
||||
public mapCollectionToPersistence(
|
||||
source: Collection<TEntity>,
|
||||
params?: MapperParamsType
|
||||
): Result<TModelAttributes[], Error> {
|
||||
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<T>(operation: () => T, key: string): Result<T, Error> {
|
||||
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<any, Error>,
|
||||
params: MapperParamsType = { defaultValue: null }
|
||||
): Result<any, Error> {
|
||||
return customMapFn(row?.dataValues[key] ?? params.defaultValue, params);
|
||||
}
|
||||
|
||||
protected mapsAssociation(
|
||||
row: TModel,
|
||||
associationName: string,
|
||||
customMapper: IDomainMapper<any, any>,
|
||||
params: MapperParamsType = {}
|
||||
): Result<any, Error> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -44,15 +44,15 @@ export class AuthenticatedUser
|
||||
}
|
||||
|
||||
verifyPassword(candidatePassword: PlainPassword): Promise<boolean> {
|
||||
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,
|
||||
};
|
||||
|
||||
@ -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<IUserProps> 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<IUserProps> 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<IUserProps> 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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,15 +54,15 @@ export class JWTPayload extends DomainEntity<IJWTPayloadProps> 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 {
|
||||
|
||||
@ -48,14 +48,14 @@ export class LoginData extends DomainEntity<ILoginDataProps> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,10 +43,10 @@ export class LogoutData extends DomainEntity<ILogoutDataProps> implements ILogou
|
||||
}
|
||||
|
||||
get email(): EmailAddress {
|
||||
return this._props.email;
|
||||
return this.props.email;
|
||||
}
|
||||
|
||||
get tabId(): UniqueID {
|
||||
return this._props.tabId;
|
||||
return this.props.tabId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,14 +48,14 @@ export class RegisterData extends DomainEntity<IRegisterDataProps> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,16 +41,16 @@ export class TabContext extends DomainEntity<ITabContextProps> 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(),
|
||||
};
|
||||
|
||||
@ -17,6 +17,6 @@ export class UserRoles extends ValueObject<string[]> {
|
||||
}
|
||||
|
||||
hasRole(role: string): boolean {
|
||||
return this._value.includes(role);
|
||||
return this.props.includes(role);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string | null> {
|
||||
static create(email: string | null): Result<EmailAddress, Error> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,7 +2,11 @@ import { Result, ValueObject } from "@common/domain";
|
||||
import bcrypt from "bcrypt";
|
||||
import { z } from "zod";
|
||||
|
||||
export class HashPassword extends ValueObject<string> {
|
||||
interface HashPasswordProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class HashPassword extends ValueObject<HashPasswordProps> {
|
||||
private static readonly SALT_ROUNDS = 10;
|
||||
|
||||
static createFromPlainText(plainTextPassword: string): Result<HashPassword, Error> {
|
||||
@ -13,7 +17,7 @@ export class HashPassword extends ValueObject<string> {
|
||||
}
|
||||
|
||||
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<string> {
|
||||
}
|
||||
|
||||
static createFromHash(hashedPassword: string): Result<HashPassword, Error> {
|
||||
return Result.ok(new HashPassword(hashedPassword));
|
||||
return Result.ok(new HashPassword({ value: hashedPassword }));
|
||||
}
|
||||
|
||||
async verifyPassword(plainTextPassword: string): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./auth-user-roles";
|
||||
export * from "./email-address";
|
||||
export * from "./hash-password";
|
||||
export * from "./plain-password";
|
||||
export * from "./token";
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { Result, ValueObject } from "@common/domain";
|
||||
import { z } from "zod";
|
||||
|
||||
export class PlainPassword extends ValueObject<string> {
|
||||
interface PlainPasswordProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class PlainPassword extends ValueObject<PlainPasswordProps> {
|
||||
static create(plainTextPassword: string): Result<PlainPassword, Error> {
|
||||
const result = PlainPassword.validate(plainTextPassword);
|
||||
|
||||
@ -9,11 +13,19 @@ export class PlainPassword extends ValueObject<string> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { Result, ValueObject } from "@common/domain";
|
||||
import { z } from "zod";
|
||||
|
||||
export class Token extends ValueObject<string> {
|
||||
interface TokenProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class Token extends ValueObject<TokenProps> {
|
||||
static create(token: string): Result<Token, Error> {
|
||||
const result = Token.validate(token);
|
||||
|
||||
@ -9,11 +13,19 @@ export class Token extends ValueObject<string> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import { Result, ValueObject } from "@common/domain";
|
||||
import { z } from "zod";
|
||||
|
||||
export class Username extends ValueObject<string> {
|
||||
interface UsernameProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class Username extends ValueObject<UsernameProps> {
|
||||
static create(username: string): Result<Username, Error> {
|
||||
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<string> {
|
||||
|
||||
return schema.safeParse(username);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.props.value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -10,7 +10,7 @@ export class ListUsersController extends ExpressController {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async executeImpl(): Promise<void | any> {
|
||||
protected async executeImpl() {
|
||||
const usersOrError = await this.listUsers.execute();
|
||||
|
||||
if (usersOrError.isFailure) {
|
||||
|
||||
@ -10,5 +10,6 @@ export const listUsersPresenter: IListUsersPresenter = {
|
||||
users.map((user) => ({
|
||||
id: user.id.toString(),
|
||||
email: user.email.toString(),
|
||||
username: user.username.toString(),
|
||||
})),
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export interface IListUsersResponseDTO {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-companies";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-companies.use-case";
|
||||
@ -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<Result<User[], Error>> {
|
||||
return await this.transactionManager.complete(async (transaction) => {
|
||||
return await this.userService.findCompanies(transaction);
|
||||
});
|
||||
}
|
||||
}
|
||||
130
apps/server/src/contexts/companies/domain/aggregates/company.ts
Normal file
130
apps/server/src/contexts/companies/domain/aggregates/company.ts
Normal file
@ -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<string>;
|
||||
website: Maybe<string>;
|
||||
fax: Maybe<PhoneNumber>;
|
||||
logo: Maybe<string>;
|
||||
}
|
||||
|
||||
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<string>;
|
||||
fax: Maybe<PhoneNumber>;
|
||||
website: Maybe<string>;
|
||||
logo: Maybe<string>;
|
||||
|
||||
isCompany: boolean;
|
||||
isFreelancer: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class Company extends AggregateRoot<ICompanyProps> implements ICompany {
|
||||
static create(props: ICompanyProps, id?: UniqueID): Result<Company, Error> {
|
||||
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<PhoneNumber> {
|
||||
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";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./company";
|
||||
@ -0,0 +1,7 @@
|
||||
import { Result, UniqueID } from "@common/domain";
|
||||
import { Company } from "../aggregates";
|
||||
|
||||
export interface ICompanyService {
|
||||
findCompanies(transaction?: any): Promise<Result<Company[], Error>>;
|
||||
findCompanyById(userId: UniqueID, transaction?: any): Promise<Result<Company>>;
|
||||
}
|
||||
@ -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<Result<Company[], Error>> {
|
||||
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<Result<Company>> {
|
||||
return await this.companyRepository.findById(companyId, transaction);
|
||||
}
|
||||
}
|
||||
@ -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<CompanyModel, CompanyCreationAttributes, Company> {}
|
||||
|
||||
export class CompanyMapper
|
||||
extends SequelizeMapper<CompanyModel, CompanyCreationAttributes, Company>
|
||||
implements ICompanyMapper
|
||||
{
|
||||
public mapToDomain(source: CompanyModel, params?: MapperParamsType): Result<Company, Error> {
|
||||
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<CompanyCreationAttributes, Error> {
|
||||
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 };
|
||||
@ -0,0 +1 @@
|
||||
export * from "./company.mapper";
|
||||
@ -0,0 +1,170 @@
|
||||
import {
|
||||
CreationOptional,
|
||||
DataTypes,
|
||||
InferAttributes,
|
||||
InferCreationAttributes,
|
||||
Model,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
|
||||
export type CompanyCreationAttributes = InferCreationAttributes<CompanyModel, {}> & {};
|
||||
|
||||
export class CompanyModel extends Model<
|
||||
InferAttributes<CompanyModel>,
|
||||
InferCreationAttributes<CompanyModel>
|
||||
> {
|
||||
// To avoid table creation
|
||||
/*static async sync(): Promise<any> {
|
||||
return Promise.resolve();
|
||||
}*/
|
||||
|
||||
declare id: string;
|
||||
|
||||
declare is_freelancer: boolean;
|
||||
declare name: string;
|
||||
declare trade_name: CreationOptional<string>;
|
||||
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<string>;
|
||||
declare website: CreationOptional<string>;
|
||||
|
||||
declare legal_record: string;
|
||||
|
||||
declare default_tax: number;
|
||||
declare status: string;
|
||||
declare lang_code: string;
|
||||
declare currency_code: string;
|
||||
declare logo: CreationOptional<string>;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@ -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<Company> 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<Result<Collection<Company>, 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<Result<Company, Error>> {
|
||||
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<Result<Company, Error>> {
|
||||
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 };
|
||||
@ -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;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-companies";
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<Company>) => IListCompaniesResponseDTO[];
|
||||
}
|
||||
|
||||
export const listCompaniesPresenter: IListCompaniesPresenter = {
|
||||
toDTO: (companies: Collection<Company>): 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()),
|
||||
})),
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export interface IListCompaniesRequestDTO {}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ListCompaniesSchema = z.object({});
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./companies.request.dto";
|
||||
export * from "./companies.response.dto";
|
||||
export * from "./companies.validation.dto";
|
||||
2
apps/server/src/contexts/companies/presentation/index.ts
Normal file
2
apps/server/src/contexts/companies/presentation/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./controllers";
|
||||
export * from "./dto";
|
||||
@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
@ -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: {}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user