diff --git a/packages/navigation/src/application/repositories/header-repository.interface.ts b/packages/navigation/src/application/repositories/header-repository.interface.ts new file mode 100644 index 0000000..6b1753d --- /dev/null +++ b/packages/navigation/src/application/repositories/header-repository.interface.ts @@ -0,0 +1,5 @@ +import type { Header } from "../../entities/header"; + +export interface IHeaderRepository { + getHeader(): Promise
; +} diff --git a/packages/navigation/src/application/use-cases/get-header.use-case.test.ts b/packages/navigation/src/application/use-cases/get-header.use-case.test.ts new file mode 100644 index 0000000..cc4831c --- /dev/null +++ b/packages/navigation/src/application/use-cases/get-header.use-case.test.ts @@ -0,0 +1,23 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { navigationContainer } from "@/di/container"; +import { NAVIGATION_SYMBOLS } from "@/di/symbols"; +import { MockHeaderRepository } from "@/infrastructure/repositories/mock-header.repository"; +import type { IHeaderRepository } from "@/application/repositories/header-repository.interface"; +import { getHeaderUseCase } from "./get-header.use-case"; + +describe("getHeaderUseCase", () => { + beforeEach(() => { + if (navigationContainer.isBound(NAVIGATION_SYMBOLS.IHeaderRepository)) { + navigationContainer.unbind(NAVIGATION_SYMBOLS.IHeaderRepository); + } + navigationContainer + .bind(NAVIGATION_SYMBOLS.IHeaderRepository) + .toConstantValue(new MockHeaderRepository()); + }); + + it("returns the seeded header items", async () => { + const result = await getHeaderUseCase(); + expect(result.items.length).toBeGreaterThan(0); + expect(result.items[0]?.label).toBe("Home"); + }); +}); diff --git a/packages/navigation/src/application/use-cases/get-header.use-case.ts b/packages/navigation/src/application/use-cases/get-header.use-case.ts new file mode 100644 index 0000000..7b39a24 --- /dev/null +++ b/packages/navigation/src/application/use-cases/get-header.use-case.ts @@ -0,0 +1,11 @@ +import type { Header } from "../../entities/header"; +import { navigationContainer } from "../../di/container"; +import { NAVIGATION_SYMBOLS } from "../../di/symbols"; +import type { IHeaderRepository } from "../repositories/header-repository.interface"; + +export async function getHeaderUseCase(): Promise
{ + const repo = navigationContainer.get( + NAVIGATION_SYMBOLS.IHeaderRepository, + ); + return repo.getHeader(); +} diff --git a/packages/navigation/src/di/container.test.ts b/packages/navigation/src/di/container.test.ts new file mode 100644 index 0000000..afb4330 --- /dev/null +++ b/packages/navigation/src/di/container.test.ts @@ -0,0 +1,24 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { navigationContainer } from "./container"; +import { NAVIGATION_SYMBOLS } from "./symbols"; +import { NavigationModule } from "./module"; +import { MockHeaderRepository } from "@/infrastructure/repositories/mock-header.repository"; +import type { IHeaderRepository } from "@/application/repositories/header-repository.interface"; + +describe("navigationContainer", () => { + beforeEach(() => { + navigationContainer.unbindAll(); + navigationContainer.load(NavigationModule); + }); + + afterEach(() => { + navigationContainer.unbindAll(); + }); + + it("resolves IHeaderRepository to MockHeaderRepository", () => { + const repo = navigationContainer.get( + NAVIGATION_SYMBOLS.IHeaderRepository, + ); + expect(repo).toBeInstanceOf(MockHeaderRepository); + }); +}); diff --git a/packages/navigation/src/di/container.ts b/packages/navigation/src/di/container.ts new file mode 100644 index 0000000..3ec1791 --- /dev/null +++ b/packages/navigation/src/di/container.ts @@ -0,0 +1,6 @@ +import "reflect-metadata"; +import { Container } from "inversify"; +import { NavigationModule } from "./module"; + +export const navigationContainer = new Container({ defaultScope: "Singleton" }); +navigationContainer.load(NavigationModule); diff --git a/packages/navigation/src/di/module.ts b/packages/navigation/src/di/module.ts new file mode 100644 index 0000000..3f882b6 --- /dev/null +++ b/packages/navigation/src/di/module.ts @@ -0,0 +1,11 @@ +import { ContainerModule, type interfaces } from "inversify"; + +import type { IHeaderRepository } from "../application/repositories/header-repository.interface"; +import { MockHeaderRepository } from "../infrastructure/repositories/mock-header.repository"; +import { NAVIGATION_SYMBOLS } from "./symbols"; + +export const NavigationModule = new ContainerModule((bind: interfaces.Bind) => { + bind(NAVIGATION_SYMBOLS.IHeaderRepository).to( + MockHeaderRepository, + ); +}); diff --git a/packages/navigation/src/di/symbols.ts b/packages/navigation/src/di/symbols.ts new file mode 100644 index 0000000..5b2ae02 --- /dev/null +++ b/packages/navigation/src/di/symbols.ts @@ -0,0 +1,3 @@ +export const NAVIGATION_SYMBOLS = { + IHeaderRepository: Symbol.for("navigation:IHeaderRepository"), +} as const; diff --git a/packages/navigation/src/entities/header.ts b/packages/navigation/src/entities/header.ts new file mode 100644 index 0000000..72af75a --- /dev/null +++ b/packages/navigation/src/entities/header.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const headerItemSchema = z.object({ + label: z.string().min(1).max(64), + href: z.string().min(1), + external: z.boolean().default(false), +}); + +export const headerSchema = z.object({ + logoId: z.string().optional(), + items: z.array(headerItemSchema), +}); + +export type Header = z.infer; +export type HeaderItem = z.infer; diff --git a/packages/navigation/src/infrastructure/repositories/mock-header.repository.ts b/packages/navigation/src/infrastructure/repositories/mock-header.repository.ts new file mode 100644 index 0000000..fa9ae04 --- /dev/null +++ b/packages/navigation/src/infrastructure/repositories/mock-header.repository.ts @@ -0,0 +1,18 @@ +import "reflect-metadata"; +import { injectable } from "inversify"; + +import type { IHeaderRepository } from "../../application/repositories/header-repository.interface"; +import type { Header } from "../../entities/header"; + +@injectable() +export class MockHeaderRepository implements IHeaderRepository { + async getHeader(): Promise
{ + return { + items: [ + { label: "Home", href: "/", external: false }, + { label: "Blog", href: "/blog", external: false }, + { label: "About", href: "/about", external: false }, + ], + }; + } +} diff --git a/packages/navigation/src/infrastructure/repositories/payload-header.repository.ts b/packages/navigation/src/infrastructure/repositories/payload-header.repository.ts new file mode 100644 index 0000000..06c2968 --- /dev/null +++ b/packages/navigation/src/infrastructure/repositories/payload-header.repository.ts @@ -0,0 +1,48 @@ +import "reflect-metadata"; +import { injectable } from "inversify"; +import { getPayload } from "payload"; +import type { SanitizedConfig } from "payload"; + +import type { IHeaderRepository } from "../../application/repositories/header-repository.interface"; +import type { Header, HeaderItem } from "../../entities/header"; + +type PayloadHeaderGlobal = { + logo?: string | number | { id: string | number } | null; + items?: Array<{ + label?: string | null; + href?: string | null; + external?: boolean | null; + }> | null; +}; + +@injectable() +export class PayloadHeaderRepository implements IHeaderRepository { + private config: SanitizedConfig; + + constructor(config: SanitizedConfig) { + this.config = config; + } + + async getHeader(): Promise
{ + const payload = await getPayload({ config: this.config }); + const doc = (await payload.findGlobal({ + slug: "header", + overrideAccess: false, + })) as PayloadHeaderGlobal; + + const logoId = + typeof doc.logo === "object" && doc.logo !== null + ? String(doc.logo.id) + : doc.logo != null + ? String(doc.logo) + : undefined; + + const items: HeaderItem[] = (doc.items ?? []).map((item) => ({ + label: item.label ?? "", + href: item.href ?? "", + external: item.external ?? false, + })); + + return { logoId, items }; + } +}