diff --git a/.gitignore b/.gitignore index 035dd4a..6350b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ dist .swc/ *.tsbuildinfo apps/api/src/generated/ +packages/api-contract/src/generated/ # Tool caches .eslintcache diff --git a/.prettierignore b/.prettierignore index 1033c36..1a0af73 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,5 +4,6 @@ .codex/ .omx/ .turbo/ +packages/api-contract/src/generated/ pnpm-lock.yaml tmp/ diff --git a/AGENTS.md b/AGENTS.md index 671f988..cca0325 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,28 +3,29 @@ ## Repository Structure Repo = Turborepo monorepo. -Preserve monorepo shape. Do not break workspace layout, package boundaries, shared config, task graph, repo conventions. +Keep monorepo shape. +No break workspace layout, package boundaries, shared config, task graph, repo conventions. Follow canonical Turborepo layout in `.agents/skills/turborepo/references/best-practices/RULE.md`. Keep deployable apps in `apps/`. Keep shared libs + shared config in `packages/`. No nested packages. -Do not move package responsibilities into repo root. +No move package responsibility to repo root. ## Documentation Language Policy All durable docs stay synced in Chinese + English. -- Rule covers all docs under `docs/`: plans, brainstorms, solutions, other long-lived docs. -- Chinese docs live in `docs/zh-Hans/`. -- English docs live in `docs/en/`. +- Rule covers all docs under `docs/`: plans, brainstorms, solutions, other long-life docs. +- Chinese docs in `docs/zh-Hans/`. +- English docs in `docs/en/`. - If one language doc exists, matching other-language doc must exist. -- Both versions stay semantically synced. Update both in same work. No drift. +- Both versions stay same meaning. Update both in same work. No drift. ## Repository Docs Convention Use language-scoped layout: `docs/en/` = English, `docs/zh-Hans/` = Simplified Chinese. -Place each doc in matching category dir for both languages. +Put each doc in matching category dir for both languages. - Brainstorms in `docs/{lang}/brainstorms/` - requirements, ideas, options, early framing. - Plans in `docs/{lang}/plans/` - implementation plans, milestones, delivery order, progress tracking. @@ -40,8 +41,8 @@ Use closest category below. - `integration-issues/` - project integrations, generated outputs, external platform behavior mismatch; cross-platform issues; third-party API/service mismatch. - `workflow-issues/` - agent workflow patterns, skill design, orchestration improvements, repo process decisions, repeatable execution guidance. -If none fits perfectly, use closest existing category. -Do not add new category unless both language trees intentionally expand. +If none fits perfect, use closest existing category. +No add new category unless both language trees expand on purpose. ## Skill-Level Conventions @@ -52,11 +53,24 @@ Do not add new category unless both language trees intentionally expand. - For `apps/web` frontend architecture work, also reference `feature-sliced-design`. - When `next-best-practices` and `feature-sliced-design` overlap, use rules below: - Next.js owns framework entry semantics + special files. Keep root `app/` as App Router entry. Put required files like `layout.tsx`, `page.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `template.tsx`, `default.tsx`, `route.ts` there when Next.js requires. - - FSD owns business structure inside `src/`, but do **not** create `src/pages` in `apps/web`. Next.js treats `src/pages` as Pages Router root; this conflicts with root App Router `app/`. Adapt FSD around framework rule. - - In `apps/web`, prefer `src/app`, `src/widgets`, `src/features`, `src/entities`, `src/shared` for business structure. If page-scoped business slice needed, keep under `src/app` or `src/widgets` with route-aligned name. Do not add `src/pages`. - - Keep root `app/` thin: route entrypoints, top-level providers, metadata wiring, route handlers, minimal bridge code only. Do not make root `app/` main home for reusable business slices. + - FSD owns business structure inside `src/`, but do **not** create `src/pages` in `apps/web`. Next.js treats `src/pages` as Pages Router root; this conflicts with root App Router `app/`. Bend FSD around framework rule. + - In `apps/web`, prefer `src/app`, `src/widgets`, `src/features`, `src/entities`, `src/shared` for business structure. If page-scoped business slice needed, keep under `src/app` or `src/widgets` with route-aligned name. No add `src/pages`. + - Keep root `app/` thin: route entrypoints, top-level providers, metadata wiring, route handlers, minimal bridge code only. No make root `app/` main home for reusable business slices. - Follow `next-best-practices` first for RSC boundaries, Server vs Client Components, Server Actions, route handlers, metadata, async Next.js APIs, runtime constraints. - Follow `feature-sliced-design` first for slice boundaries, public API usage, import direction, Pages First decomposition inside business layer. - If FSD placement conflicts with required Next.js convention, keep Next.js convention and bend FSD around it. `src/pages` vs root `app/` = explicit example. - For `apps/api` NestJS implementation work, invoke `nestjs-best-practices`. - For `apps/api` backend architecture, follow `.agents/skills/nestjs-best-practices/rules/arch-feature-modules.md` and organize by feature modules. Prefer self-contained feature folders grouping controllers, services, DTOs, entities, repositories, module defs. Avoid repo-wide tech-layer folders unless deeper scoped rule overrides. + +## Plan Completion Checks + +- After finishing a plan, check if GitHub Actions need updates; if yes, update relevant workflow files in same work. +- Before handoff, run full repo validation from root: `pnpm lint`, `pnpm format:check`, `pnpm typecheck`, `pnpm test`, and `pnpm test:e2e`. +- If any command fails, fix root issue first and rerun full set before closing turn. + +## Git Hook Discipline + +- Never bypass Git hooks or hook-time checks with flags that suppress warnings, ignore files, or hide failure. +- If a hook or staged check fails, fix underlying config or code first. + Do not use `--no-warn-ignored`, `--quiet`, or similar skip-style workarounds to make hook pass. +- Keep hook behavior honest: passing commit or push means check really ran and passed, not got silenced. diff --git a/apps/api/README.md b/apps/api/README.md index 3397a94..f48e51b 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -3,7 +3,7 @@ This app owns the PostgreSQL-backed feed ingestion backbone for the repository. It reads `apps/api/feeds.opml`, ingests feeds on boot with best-effort isolation, attempts article body extraction during ingestion, and serves the -existing read-only `/articles` contract from persisted data. +existing read-only `/articles` and health contracts from persisted data. ## Local Run @@ -102,7 +102,7 @@ inside the container instead of the app-local default `./feeds.opml`. - Prepared summaries use the official `openai` SDK against the configured OpenAI-compatible gateway, validate the structured result with `zod`, and persist canonical Markdown, `translatedTitle`, and any terminal failure reason - in `summaryErrorReason`. + in `summaryError`. - Same-process sleep/freeze recovery is allowed to trigger a background feed auto-refresh in the future, but `INGEST_ON_BOOT` remains startup-only; the wake interval is an elapsed-hours check, not a cron schedule. @@ -132,14 +132,14 @@ inside the container instead of the app-local default `./feeds.opml`. - `sourceTitle` - `publishedAt` - `summary` - - `summaryErrorReason` + - `summaryError` - `originalUrl` - Returns `404` for unknown article IDs. Article body markdown stays internal in this slice. The public `GET /articles` and `GET /articles/:id` payloads expose the original `title`, the prepared `translatedTitle`, the canonical Markdown `summary`, and any persisted -`summaryErrorReason`, but they still never expose `contentMarkdown` or +`summaryError`, but they still never expose `contentMarkdown` or `contentExtractedAt`. ## Validation @@ -152,6 +152,7 @@ pnpm --filter api db:reset pnpm --filter api db:seed pnpm --filter api test pnpm --filter api test:e2e +pnpm --filter api contract:refresh ``` Use `pnpm --filter api db:deploy` to align the local development database with @@ -189,3 +190,12 @@ behind. - `GET /health/live` reports process liveness for orchestration probes. - `GET /health/ready` reports readiness only when bootstrap has completed and the database ping succeeds. + +## OpenAPI Contract + +- `apps/api/src/openapi/openapi-refresh.ts` refreshes the checked-in contract at + `packages/api-contract/openapi/openapi.yaml`. +- `packages/api-contract` generates the client/types that `apps/web` imports. +- Use `pnpm --filter api contract:refresh` first, then + `pnpm --filter @repo/api-contract contract:refresh` when the API surface + changes. diff --git a/apps/api/e2e/openapi-contract.e2e-spec.ts b/apps/api/e2e/openapi-contract.e2e-spec.ts new file mode 100644 index 0000000..baf107e --- /dev/null +++ b/apps/api/e2e/openapi-contract.e2e-spec.ts @@ -0,0 +1,65 @@ +import { afterAll, beforeAll, describe, expect, it, jest } from "@jest/globals"; +import { type INestApplication } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import YAML from "yaml"; + +import { AppModule } from "../src/app.module"; +import { ArticlesService } from "../src/articles/articles.service"; +import { PrismaService } from "../src/prisma/prisma.service"; +import { createOpenApiDocument } from "../src/openapi/openapi-document"; +import { FeedBootstrapService } from "../src/feeds/feed-bootstrap.service"; + +describe("OpenAPI contract", () => { + let app: INestApplication | undefined; + + beforeAll(async () => { + process.env["TEST_DATABASE_URL"] ??= + "postgresql://rssift:rssift@127.0.0.1:5432/rssift_test"; + process.env["DATABASE_URL"] = process.env["TEST_DATABASE_URL"]; + process.env["INGEST_ON_BOOT"] = "false"; + + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(ArticlesService) + .useValue({ + getArticleById: jest.fn(), + getArticles: jest.fn(), + }) + .overrideProvider(PrismaService) + .useValue({ + $queryRawUnsafe: jest.fn(), + }) + .overrideProvider(FeedBootstrapService) + .useValue({ + onApplicationBootstrap: () => undefined, + }) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + if (app) { + await app.close(); + } + }); + + it("matches the checked-in contract", () => { + const emitted = createOpenApiDocument(app as INestApplication); + const checkedIn: unknown = YAML.parse( + readFileSync( + resolve( + __dirname, + "../../../packages/api-contract/openapi/openapi.yaml", + ), + "utf8", + ), + ); + + expect(emitted).toEqual(checkedIn); + }); +}); diff --git a/apps/api/package.json b/apps/api/package.json index f67a7a8..edfeff0 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,6 +9,7 @@ "db:seed": "prisma db execute --config ./prisma.config.ts --file ./prisma/seed/seed.sql", "db:studio": "prisma studio --config ./prisma.config.ts", "dev": "nest start --watch", + "contract:refresh": "tsx src/openapi/openapi-refresh.ts", "prebuild": "pnpm db:generate", "build": "nest build", "start": "nest start", @@ -29,6 +30,7 @@ "@nestjs/config": "4.0.4", "@nestjs/core": "^11.1.19", "@nestjs/platform-express": "^11.1.19", + "@nestjs/swagger": "11.4.1", "@prisma/adapter-pg": "7.8.0", "@prisma/client": "7.8.0", "escape-html": "^1.0.3", @@ -63,6 +65,8 @@ "ts-jest": "^29.4.9", "ts-loader": "^9.5.7", "tsconfig-paths": "^4.2.0", + "tsx": "4.21.0", + "yaml": "2.8.3", "typescript": "6.0.3" } } diff --git a/apps/api/src/articles/articles.controller.ts b/apps/api/src/articles/articles.controller.ts index f2e4b5d..0e056a0 100644 --- a/apps/api/src/articles/articles.controller.ts +++ b/apps/api/src/articles/articles.controller.ts @@ -1,19 +1,33 @@ import { Controller, Get, NotFoundException, Param } from "@nestjs/common"; +import { + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags, +} from "@nestjs/swagger"; import { ArticleDetailItemDto } from "./dto/article-detail-item.dto"; import { ArticleListItemDto } from "./dto/article-list-item.dto"; import { ArticlesService } from "./articles.service"; +@ApiTags("articles") @Controller("articles") export class ArticlesController { constructor(private readonly articlesService: ArticlesService) {} @Get() + @ApiOperation({ operationId: "articles" }) + @ApiOkResponse({ type: ArticleListItemDto, isArray: true }) async getArticles(): Promise { return this.articlesService.getArticles(); } @Get(":id") + @ApiOperation({ operationId: "articleById" }) + @ApiParam({ name: "id", required: true, type: String }) + @ApiOkResponse({ type: ArticleDetailItemDto }) + @ApiNotFoundResponse({ description: "Article not found" }) async getArticleById(@Param("id") id: string): Promise { const article = await this.articlesService.getArticleById(id); diff --git a/apps/api/src/articles/dto/article-detail-item.dto.ts b/apps/api/src/articles/dto/article-detail-item.dto.ts index 03185e5..ad8d4b3 100644 --- a/apps/api/src/articles/dto/article-detail-item.dto.ts +++ b/apps/api/src/articles/dto/article-detail-item.dto.ts @@ -1,17 +1,41 @@ +import { ApiProperty } from "@nestjs/swagger"; + export class ArticleSummaryErrorDto { + @ApiProperty({ type: String }) action!: string; + + @ApiProperty({ type: String }) code!: string; + + @ApiProperty({ type: String }) copyText!: string; + + @ApiProperty({ type: String }) message!: string; + + @ApiProperty({ type: String }) title!: string; } export class ArticleDetailItemDto { + @ApiProperty({ type: String }) title!: string; + + @ApiProperty({ type: String }) translatedTitle!: string; + + @ApiProperty({ type: String }) sourceTitle!: string; + + @ApiProperty({ type: String, format: "date-time" }) publishedAt!: string; + + @ApiProperty({ type: String }) summary!: string; + + @ApiProperty({ type: () => ArticleSummaryErrorDto, nullable: true }) summaryError!: ArticleSummaryErrorDto | null; + + @ApiProperty({ type: String }) originalUrl!: string; } diff --git a/apps/api/src/articles/dto/article-list-item.dto.ts b/apps/api/src/articles/dto/article-list-item.dto.ts index 48df7ea..5d2e30c 100644 --- a/apps/api/src/articles/dto/article-list-item.dto.ts +++ b/apps/api/src/articles/dto/article-list-item.dto.ts @@ -1,8 +1,21 @@ +import { ApiProperty } from "@nestjs/swagger"; + export class ArticleListItemDto { + @ApiProperty({ type: String }) id!: string; + + @ApiProperty({ type: String }) title!: string; + + @ApiProperty({ type: String }) translatedTitle!: string; + + @ApiProperty({ type: String }) sourceTitle!: string; + + @ApiProperty({ type: String, format: "date-time" }) publishedAt!: string; + + @ApiProperty({ type: String }) originalUrl!: string; } diff --git a/apps/api/src/health/dto/health-response.dto.ts b/apps/api/src/health/dto/health-response.dto.ts new file mode 100644 index 0000000..11d9ba7 --- /dev/null +++ b/apps/api/src/health/dto/health-response.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class HealthLiveChecksDto { + @ApiProperty({ type: String }) + application!: string; +} + +export class HealthReadyChecksDto { + @ApiProperty({ type: String }) + application!: string; + + @ApiProperty({ type: String }) + database!: string; +} + +export class HealthLiveResponseDto { + @ApiProperty({ type: () => HealthLiveChecksDto }) + checks!: HealthLiveChecksDto; + + @ApiProperty({ type: String }) + service!: string; + + @ApiProperty({ type: String }) + status!: string; +} + +export class HealthReadyResponseDto { + @ApiProperty({ type: () => HealthReadyChecksDto }) + checks!: HealthReadyChecksDto; + + @ApiProperty({ type: String }) + service!: string; + + @ApiProperty({ type: String }) + status!: string; +} diff --git a/apps/api/src/health/health.controller.ts b/apps/api/src/health/health.controller.ts index be914a7..a3415a6 100644 --- a/apps/api/src/health/health.controller.ts +++ b/apps/api/src/health/health.controller.ts @@ -1,8 +1,19 @@ import { Controller, Get, OnApplicationBootstrap, Res } from "@nestjs/common"; +import { + ApiOkResponse, + ApiOperation, + ApiServiceUnavailableResponse, + ApiTags, +} from "@nestjs/swagger"; import type { Response } from "express"; import { PrismaService } from "../prisma/prisma.service"; +import { + HealthLiveResponseDto, + HealthReadyResponseDto, +} from "./dto/health-response.dto"; +@ApiTags("health") @Controller("health") export class HealthController implements OnApplicationBootstrap { private appReady = false; @@ -14,6 +25,8 @@ export class HealthController implements OnApplicationBootstrap { } @Get("live") + @ApiOperation({ operationId: "healthLive" }) + @ApiOkResponse({ type: HealthLiveResponseDto }) live() { return { checks: { @@ -25,6 +38,9 @@ export class HealthController implements OnApplicationBootstrap { } @Get("ready") + @ApiOperation({ operationId: "healthReady" }) + @ApiOkResponse({ type: HealthReadyResponseDto }) + @ApiServiceUnavailableResponse({ type: HealthReadyResponseDto }) async ready(@Res({ passthrough: true }) response: Response) { if (!this.appReady) { response.status(503); diff --git a/apps/api/src/openapi/openapi-document.ts b/apps/api/src/openapi/openapi-document.ts new file mode 100644 index 0000000..230f8c0 --- /dev/null +++ b/apps/api/src/openapi/openapi-document.ts @@ -0,0 +1,36 @@ +import { type INestApplication } from "@nestjs/common"; +import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; + +import { + ArticleDetailItemDto, + ArticleSummaryErrorDto, +} from "../articles/dto/article-detail-item.dto"; +import { ArticleListItemDto } from "../articles/dto/article-list-item.dto"; +import { + HealthLiveChecksDto, + HealthLiveResponseDto, + HealthReadyChecksDto, + HealthReadyResponseDto, +} from "../health/dto/health-response.dto"; + +export function createOpenApiDocument(app: INestApplication) { + const config = new DocumentBuilder() + .setTitle("RSSift API") + .setDescription("OpenAPI contract for the RSSift API surface") + .setVersion("0.1.0") + .build(); + + return SwaggerModule.createDocument(app, config, { + deepScanRoutes: true, + extraModels: [ + ArticleDetailItemDto, + ArticleListItemDto, + ArticleSummaryErrorDto, + HealthLiveChecksDto, + HealthLiveResponseDto, + HealthReadyChecksDto, + HealthReadyResponseDto, + ], + operationIdFactory: (_controllerKey, methodKey) => methodKey, + }); +} diff --git a/apps/api/src/openapi/openapi-refresh.module.ts b/apps/api/src/openapi/openapi-refresh.module.ts new file mode 100644 index 0000000..7460f05 --- /dev/null +++ b/apps/api/src/openapi/openapi-refresh.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; + +import { ArticlesModule } from "../articles/articles.module"; +import { FeedBootstrapService } from "../feeds/feed-bootstrap.service"; +import { HealthModule } from "../health/health.module"; +import { PrismaModule } from "../prisma/prisma.module"; + +@Module({ + imports: [ArticlesModule, HealthModule, PrismaModule], + providers: [FeedBootstrapService], +}) +export class OpenApiRefreshModule {} diff --git a/apps/api/src/openapi/openapi-refresh.ts b/apps/api/src/openapi/openapi-refresh.ts new file mode 100644 index 0000000..6471e56 --- /dev/null +++ b/apps/api/src/openapi/openapi-refresh.ts @@ -0,0 +1,47 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import YAML from "yaml"; + +import { Test } from "@nestjs/testing"; +import { FeedBootstrapService } from "../feeds/feed-bootstrap.service"; +import { createOpenApiDocument } from "./openapi-document"; +import { OpenApiRefreshModule } from "./openapi-refresh.module"; + +const contractFilePath = resolve( + __dirname, + "../../../../packages/api-contract/openapi/openapi.yaml", +); + +export async function refreshOpenApiContract() { + const moduleRef = await Test.createTestingModule({ + imports: [OpenApiRefreshModule], + }) + .overrideProvider(FeedBootstrapService) + .useValue({ + onApplicationBootstrap: () => undefined, + }) + .compile(); + + const app = moduleRef.createNestApplication(); + + try { + await app.init(); + + const document = createOpenApiDocument(app); + const yaml = YAML.stringify(document, { + sortMapEntries: true, + }); + + await mkdir(dirname(contractFilePath), { recursive: true }); + await writeFile(contractFilePath, `${yaml.trimEnd()}\n`, "utf8"); + } finally { + await app.close(); + } +} + +if (require.main === module) { + void refreshOpenApiContract().catch((error: unknown) => { + console.error(error); + process.exit(1); + }); +} diff --git a/apps/web/README.md b/apps/web/README.md index fdf0d2a..a34b243 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -2,7 +2,9 @@ This app renders the first runnable reader slice for the repository. It consumes the persisted `apps/api` article contract over real HTTP and presents a -two-pane reading shell on port `3001`. +two-pane reading shell on port `3001`. It imports generated contract types and +helpers from `@repo/api-contract`, so the web side no longer owns handwritten +DTO-like contract shapes. The current reader contract is summary-first: @@ -10,9 +12,13 @@ The current reader contract is summary-first: `title` when the prepared translation is still missing - the detail pane renders canonical Markdown summary content directly - an empty prepared `summary` renders either the persisted - `summaryErrorReason` or the pending-state copy `Summary pending` while + `summaryError` or the pending-state copy `Summary pending` while keeping the rest of the reader chrome visible +The checked-in OpenAPI document is the source of truth for that contract. The +web seam stays thin and only handles `API_BASE_URL`, cache policy, URL +encoding, and the `404 -> null` reader fallback. + ## Local Development 1. Start the API in a separate terminal: @@ -34,6 +40,9 @@ pnpm --dir apps/web dev ``` The web package now bootstraps `@repo/ui` before `dev`, and bootstraps both `@repo/ui` plus `@repo/jest-config` before `build`, `typecheck`, and `test`, so a clean checkout only needs `pnpm install` first. You do not need pre-existing `packages/ui/dist` or `packages/jest-config/dist` directories. +It also bootstraps `@repo/api-contract` before `dev`, `build`, `lint`, +`typecheck`, and `test`, so the generated contract package stays in sync with +the web app entrypoints. The local default contract is: @@ -74,6 +83,8 @@ pnpm --filter web test ``` Direct Jest invocations also resolve `@repo/ui` from `packages/ui/src`, which keeps ad-hoc test runs safe even when `packages/ui/dist` has not been built yet. +The contract package stays in the normal workspace graph, so generated types are +available without manual copying. Run the browser flow against both apps: @@ -91,6 +102,13 @@ The Playwright suite starts both the API and the web app, then verifies: API-to-web seam - `/api/health` stays available as a stable, dependency-light probe surface +## Contract Refresh + +- Refresh the API OpenAPI file with `pnpm --filter api contract:refresh`. +- Regenerate the client/types with `pnpm --filter @repo/api-contract contract:refresh`. +- Do not hand-edit the reader contract shape in + `apps/web/src/widgets/article-reader/api/articles-api.ts`. + ## Shared UI Boundary Reusable shadcn/Tailwind primitives live in `packages/ui`. App-specific reader data loading and composition stay in `apps/web`. diff --git a/apps/web/app/next-config.spec.ts b/apps/web/app/next-config.spec.ts index dae53dd..39a93b9 100644 --- a/apps/web/app/next-config.spec.ts +++ b/apps/web/app/next-config.spec.ts @@ -15,13 +15,19 @@ describe("web next config", () => { expect(config.allowedDevOrigins).toEqual(["127.0.0.1"]); expect(config.output).toBe("standalone"); expect(config.outputFileTracingRoot).toBeUndefined(); - expect(config.transpilePackages).toEqual(["@repo/ui"]); + expect(config.transpilePackages).toEqual([ + "@repo/api-contract", + "@repo/ui", + ]); }); it("keeps monorepo tracing enabled outside the dev server", () => { const config = nextConfig(PHASE_PRODUCTION_BUILD); expect(config.outputFileTracingRoot).toBe(resolve(process.cwd(), "../..")); - expect(config.transpilePackages).toEqual(["@repo/ui"]); + expect(config.transpilePackages).toEqual([ + "@repo/api-contract", + "@repo/ui", + ]); }); }); diff --git a/apps/web/jest.config.ts b/apps/web/jest.config.ts index c714e04..3c889b7 100644 --- a/apps/web/jest.config.ts +++ b/apps/web/jest.config.ts @@ -5,6 +5,9 @@ export default createNextJestConfig("./", { modulePathIgnorePatterns: ["/.next/standalone"], moduleNameMapper: { // Keep direct Jest runs independent from `packages/ui/dist` bootstrap state. + "^@repo/api-contract$": + "/../../packages/api-contract/src/index.ts", + "^@repo/api-contract/(.*)$": "/../../packages/api-contract/src/$1", "^@repo/ui$": "/../../packages/ui/src/index.ts", "^@repo/ui/(.*)$": "/../../packages/ui/src/$1", "^react-markdown$": "/test-support/react-markdown.mock.tsx", diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index a43538e..bbb068b 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -10,7 +10,7 @@ const workspaceRoot = resolve( const baseConfig = { allowedDevOrigins: ["127.0.0.1"], output: "standalone", - transpilePackages: ["@repo/ui"], + transpilePackages: ["@repo/api-contract", "@repo/ui"], } satisfies NextConfig; export default function nextConfig(phase: string): NextConfig { diff --git a/apps/web/package.json b/apps/web/package.json index 9346c73..eb2821a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,19 +3,21 @@ "type": "module", "private": true, "scripts": { - "predev": "pnpm --filter @repo/ui build", + "predev": "pnpm --filter @repo/ui build && pnpm --filter @repo/api-contract build", "dev": "NEXT_TELEMETRY_DISABLED=1 next dev --turbopack --port 3001", - "prebuild": "pnpm --filter @repo/ui build && pnpm --filter @repo/jest-config build", + "prebuild": "pnpm --filter @repo/ui build && pnpm --filter @repo/api-contract build && pnpm --filter @repo/jest-config build", "build": "NEXT_TELEMETRY_DISABLED=1 next build", "start": "NEXT_TELEMETRY_DISABLED=1 next start --port 3001", + "prelint": "pnpm --filter @repo/ui build && pnpm --filter @repo/api-contract build", "lint": "eslint . --max-warnings 0", - "pretypecheck": "pnpm --filter @repo/ui build && pnpm --filter @repo/jest-config build", + "pretypecheck": "pnpm --filter @repo/ui build && pnpm --filter @repo/api-contract build && pnpm --filter @repo/jest-config build", "typecheck": "tsc --noEmit", - "pretest": "pnpm --filter @repo/ui build && pnpm --filter @repo/jest-config build", + "pretest": "pnpm --filter @repo/ui build && pnpm --filter @repo/api-contract build && pnpm --filter @repo/jest-config build", "test": "jest --config ./jest.config.ts", "test:e2e": "playwright test" }, "dependencies": { + "@repo/api-contract": "workspace:*", "@repo/ui": "workspace:*", "next": "16.2.4", "react": "^19.2.5", diff --git a/apps/web/src/widgets/article-reader/api/articles-api.ts b/apps/web/src/widgets/article-reader/api/articles-api.ts index 9ca290b..f6636a8 100644 --- a/apps/web/src/widgets/article-reader/api/articles-api.ts +++ b/apps/web/src/widgets/article-reader/api/articles-api.ts @@ -1,29 +1,14 @@ -export type ArticleListItem = { - id: string; - originalUrl: string; - publishedAt: string; - sourceTitle: string; - title: string; - translatedTitle: string; -}; - -export type ArticleSummaryError = { - action: string; - code: string; - copyText: string; - message: string; - title: string; -}; - -export type ArticleDetail = { - originalUrl: string; - publishedAt: string; - sourceTitle: string; - summary: string; - summaryError: ArticleSummaryError | null; - title: string; - translatedTitle: string; -}; +import { + getArticleById, + getArticles, + type ArticleDetailItemDto, + type ArticleListItemDto, + type ArticleSummaryErrorDto, +} from "@repo/api-contract"; + +export type ArticleListItem = ArticleListItemDto; +export type ArticleSummaryError = ArticleSummaryErrorDto; +export type ArticleDetail = ArticleDetailItemDto; export class MissingApiBaseUrlError extends Error { constructor() { @@ -44,41 +29,34 @@ function getApiBaseUrl() { return value.replace(/\/$/, ""); } -async function fetchJson(path: string): Promise { - const response = await fetch(`${getApiBaseUrl()}${path}`, { +export async function fetchArticles() { + const response = await getArticles(getApiBaseUrl(), { cache: "no-store", }); - if (!response.ok) { + if (response.status < 200 || response.status >= 300) { throw new Error( - `API request failed for ${path}: ${String(response.status)}`, + `API request failed for /articles: ${String(response.status)}`, ); } - return (await response.json()) as T; -} - -export async function fetchArticles() { - return fetchJson("/articles"); + return response.data; } export async function fetchArticleDetail(articleId: string) { - const response = await fetch( - `${getApiBaseUrl()}/articles/${encodeURIComponent(articleId)}`, - { - cache: "no-store", - }, - ); + const response = await getArticleById(getApiBaseUrl(), articleId, { + cache: "no-store", + }); if (response.status === 404) { return null; } - if (!response.ok) { + if (response.status < 200 || response.status >= 300) { throw new Error( `API request failed for /articles/${articleId}: ${String(response.status)}`, ); } - return (await response.json()) as ArticleDetail; + return response.data; } diff --git a/docs/en/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md b/docs/en/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md new file mode 100644 index 0000000..fbc0bee --- /dev/null +++ b/docs/en/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md @@ -0,0 +1,143 @@ +--- +date: 2026-04-24 +topic: web-api-openapi-contract-first +--- + +# Make the Web/API Read Boundary OpenAPI-First + +## Problem Frame + +`apps/web` still reads `apps/api` through hand-written fetch wrappers. +`apps/web/src/widgets/article-reader/api/articles-api.ts` defines its own return +types and then trusts `response.json() as T`. On the API side, `apps/api` keeps a +separate set of DTO classes and controller mappings. That leaves a gap between +"looks typed" and "is actually contract-safe." + +The goal of this refactor is not to switch to a different RPC style. It is to +collapse the Web/API read boundary onto a single OpenAPI contract that is +reviewable, generatable, and checkable. The current external read surface is +narrow, but it is the template for every future web->api read path, so it should +become the repo standard first. + +```mermaid +flowchart LR + SPEC[Repo-owned OpenAPI contract] --> GEN[Generated types / client] + SPEC --> API[apps/api REST implementation] + API --> DOC[OpenAPI document] + DOC --> GEN + GEN --> WEB[apps/web readers] + WEB --> UI[Article reader UI] + CI[CI drift / breaking-change gate] --> SPEC + CI --> API +``` + +## Requirements + +**Contract Source of Truth** + +- R1. The repository must provide a single OpenAPI contract source for every API + read surface consumed by `apps/web`. +- R2. The contract must cover all API response shapes currently consumed by + `apps/web`; this cannot only fix the article reader while leaving another hand- + written contract behind. +- R3. The contract must be a reviewable repo artifact in PRs, not something that + exists only at runtime. + +**Web Consumption** + +- R4. `apps/web` must read API data through generated types or a generated client + derived from the contract, not through hand-written DTO-like types. +- R5. `apps/web` must remove every unchecked `response.json() as T` or equivalent + cast for API responses. +- R6. The current reader experience must remain intact: default article + selection, URL persistence, 404 handling, summary fallback copy, and detail + rendering must not regress. + +**Backend Ownership and Validation** + +- R7. `apps/api` remains the owner of the REST implementation, runtime + validation, response mapping, and data sourcing. +- R8. Any response-shape change must land together with the contract change in + the same PR; the backend must not move first and the contract later. +- R9. The contract layer must make breaking changes visible before merge, not + only at runtime. + +**Workflow** + +- R10. The repository must provide a repeatable contract refresh flow so + developers can regenerate the OpenAPI contract and web-side generated + artifacts reliably. +- R11. CI or an equivalent repo-level check must detect drift between the + implementation and the published contract. +- R12. This migration must cover all current web->api read seams, not just the + article reader, so no parallel hand-written path remains behind. + +## Success Criteria + +- `apps/web` no longer relies on hand-written API return types as the only + contract source. +- The current reader page still works and user-visible behavior does not regress. +- Contract changes are explicit in PRs, so reviewers can see the impact surface + directly. +- If implementation and contract diverge, repo-level checks catch it before + merge. + +## Scope Boundaries + +- This will not migrate to tRPC or turn REST into an internal RPC style. +- This will not change ingestion, summary handling, Prisma schema, or database + persistence semantics. +- This does not require exposing more reader data, such as `contentMarkdown`. +- This is not a broader frontend data-layer redesign; it only solves the + Web/API read contract layer. + +## Key Decisions + +- OpenAPI contract-first is the chosen direction, not tRPC. +- The OpenAPI contract will be a repo asset that is reviewed and synchronized, + not something that exists only as an implicit runtime output. +- Migration scope is defined as "all current web->api read seams," not just the + article reader. +- `apps/api` keeps the Nest REST shape, and the contract and implementation stay + on the same mainline instead of splitting into two sources of truth. + +**NestJS Reference Docs** + +- OpenAPI introduction: +- OpenAPI CLI plugin: +- Validation: +- Monorepo / workspace: + +**Next.js / OpenAPI Frontend References** + +- Data fetching / Client Components: + +- Orval: + +- Orval React Query: + + +## Dependencies / Assumptions + +- Assume the current REST response shapes can be represented clearly in + OpenAPI without redesigning the API first. +- Assume `apps/web` can accept Orval-generated clients/hooks instead of + continuing to maintain handwritten fetch wrappers. +- Assume the existing CI flow can host contract-drift checks without a full + pipeline rebuild. + +## Outstanding Questions + +### Deferred to Planning + +- [Affects R1][Technical] Should the canonical OpenAPI file be YAML or JSON? +- [Affects R10][Technical] Which script or workspace task should own contract + generation and web client generation? +- [Affects R10][Needs research] Which contract diff / breaking-change gate fits + this repo best? +- [Affects R4][Technical] Should web-generated artifacts read directly from the + repo contract or from an exported artifact? + +## Next Steps + +-> /ce:plan for structured implementation planning diff --git a/docs/en/plans/2026-04-24-001-feat-web-api-openapi-contract-first-plan.md b/docs/en/plans/2026-04-24-001-feat-web-api-openapi-contract-first-plan.md new file mode 100644 index 0000000..5fdfd70 --- /dev/null +++ b/docs/en/plans/2026-04-24-001-feat-web-api-openapi-contract-first-plan.md @@ -0,0 +1,396 @@ +--- +title: refactor: Make the Web/API Read Boundary OpenAPI-First +type: refactor +status: completed +date: 2026-04-24 +origin: + - docs/en/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md + - docs/zh-Hans/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md +--- + +# refactor: Make the Web/API Read Boundary OpenAPI-First + +## Likely Additional Packages + +- Any new dependency must use the newest stable version available at the time + of implementation whenever possible; only step down when compatibility, + lockfile policy, or repo constraints require it. +- `@nestjs/swagger` - generate the OpenAPI document from the Nest article + controllers/DTOs. +- `orval` - generate the web-side client/types from the checked-in OpenAPI + contract. + +## Overview + +Turn the current `apps/api` public HTTP surface into a repo-owned OpenAPI +contract that is checked in, reviewable, and used to generate the web-side +client/types. The API still owns the runtime REST implementation and response +mapping, but the contract becomes the shared source that both sides must +follow. This keeps the change bounded to the current public endpoints instead +of opening a broader API redesign. + +The current `apps/api` public interface set is four routes: `GET /health/live`, +`GET /health/ready`, `GET /articles`, and `GET /articles/:id`. Web only +consumes the article read seam, but the health probes also need to live in the +OpenAPI contract and drift check. + +## Problem Frame + +`apps/web` still consumes `apps/api` through a handwritten wrapper in +`apps/web/src/widgets/article-reader/api/articles-api.ts`. That file defines its +own return types and trusts `response.json() as T`, so the web layer can drift +from the API without an obvious review signal. + +`apps/api` already has two public HTTP entry points: article read routes in +`apps/api/src/articles/*` and liveness/readiness probes in +`apps/api/src/health/health.controller.ts`. Those runtime shapes are not +published as a single repo-owned OpenAPI artifact yet, so there is still a gap +between "typed enough" and "contract-safe enough". + +This plan makes the OpenAPI contract the reviewed repo artifact, generates the +web client from that artifact, and adds a drift check so the API implementation +cannot move without the contract moving with it. + +## Requirements Trace + +- R1. Provide one OpenAPI source of truth for every public HTTP surface in + `apps/api`. +- R2. Cover every current public response shape, not just the article reader + seam; health probes must be included too. +- R3. Keep the contract reviewable in PRs as a checked-in repo asset. +- R4. Move `apps/web` to generated types or a generated client instead of + handwritten DTO-like types. +- R5. Remove unchecked `response.json() as T` casts from the article-reader + seam. +- R6. Keep the current reader behavior intact: default selection, URL + persistence, 404 handling, summary fallback copy, and detail rendering. +- R7. Keep `apps/api` as the owner of runtime validation, response mapping, and + persistence. +- R8. Land response-shape changes and contract changes together. +- R9. Make breaking changes visible before merge. +- R10. Provide a repeatable contract refresh flow. +- R11. Add a repo-level drift check that catches implementation/contract + divergence. +- R12. Ensure the migration covers all current public HTTP interfaces, not just + a single hand-written path. + +## Scope Boundaries + +- No tRPC migration. +- No ingestion, summary-generation, Prisma-schema, or persistence-semantic + changes. +- No expansion of the public reader payload, including `contentMarkdown`. +- No broader frontend data-layer rewrite. +- No broader API redesign beyond folding the existing public endpoints into the + contract. +- No public Swagger UI route is required; the contract artifact itself is the + reviewable output. + +## Context & Research + +### Relevant Code and Patterns + +- `apps/web/src/widgets/article-reader/api/articles-api.ts` is the current + manual seam with local DTO-like types and `response.json() as T`. +- `apps/web/src/widgets/article-reader/ui/article-reader-page.tsx`, + `apps/web/src/widgets/article-reader/ui/article-list.tsx`, and + `apps/web/src/widgets/article-reader/ui/article-detail.tsx` are the reader UI + consumers that must keep the same visible behavior. +- `apps/api/src/articles/articles.controller.ts`, + `apps/api/src/articles/articles.service.ts`, and + `apps/api/src/articles/article.repository.ts` define the current response + mapping boundary. +- `apps/api/src/articles/dto/article-list-item.dto.ts` and + `apps/api/src/articles/dto/article-detail-item.dto.ts` are the current DTO + shapes that the OpenAPI document should describe. +- `apps/api/src/health/health.controller.ts` and + `apps/api/e2e/health.e2e-spec.ts` define the liveness/readiness public + contract and need to be in the same OpenAPI view. +- `apps/api/e2e/articles.e2e-spec.ts` already asserts the article payload + shape, and `apps/web/app/page.spec.tsx` plus `apps/web/e2e/home.spec.ts` lock + the reader behavior. +- `apps/api/README.md` and `apps/web/README.md` still describe the current seam + in prose, including stale field naming in places, so they need to be updated + with the new contract path. +- `packages/ui/package.json` is the closest existing pattern for a shared + workspace package that is built and imported across app boundaries. + +## Key Technical Decisions + +- Keep the canonical contract as YAML in `packages/api-contract/openapi/openapi.yaml`. + YAML is the reviewable artifact, and the refresh flow can read and rewrite it + directly without a second exported-spec layer. +- Add a shared `packages/api-contract` workspace package that exports the + generated web client/types. This keeps the contract artifact and the consumer + boundary in the normal Turborepo package graph instead of a one-off root + script path. +- Keep API runtime ownership in `apps/api/src/articles` and + `apps/api/src/health`, using explicit OpenAPI decorators and a shared + document helper instead of a separate public docs server. +- Keep the web seam thin: it still owns `API_BASE_URL`, request caching policy, + URL encoding, and 404-to-null translation, but it no longer owns the contract + shapes themselves; health does not need a web adapter. +- Use a contract drift check that compares the emitted OpenAPI document against + the checked-in YAML and fails on drift. +- Keep the reader route flow server-side; generated client code should replace + the manual payload types, not introduce a new client-state model. + +## Open Questions + +### Resolved During Planning + +- YAML vs JSON: YAML wins because this contract is meant to be human-reviewed + and committed. +- Where the canonical artifact lives: `packages/api-contract/openapi/openapi.yaml`. +- How web reads the contract: `apps/web` imports the generated + `@repo/api-contract` package, while the refresh flow reads the YAML directly. + Web only imports the article read surface; health stays in the contract and + drift check. +- Whether to expose a public Swagger route: no; the checked-in artifact is the + contract. + +### Deferred to Implementation + +- The exact generated file names and export names inside + `packages/api-contract`. +- Whether the API document helper is only for the refresh script, reused by the + e2e drift test, or shared by both. +- The exact shape of the thin web adapter, as long as it still owns + `API_BASE_URL` and 404 handling. + +## High-Level Technical Design + +> This illustrates the intended approach and is directional guidance for review, +> not implementation specification. The implementing agent should treat it as +> context, not code to reproduce. + +```mermaid +flowchart LR + API_IMPL[apps/api/src/articles/* + apps/api/src/health/*] --> DOC_HELPER[OpenAPI document helper] + DOC_HELPER --> SPEC[packages/api-contract/openapi/openapi.yaml] + SPEC --> CLIENT_GEN[packages/api-contract generated client/types] + CLIENT_GEN --> WEB_ADAPTER[apps/web/src/widgets/article-reader/api/articles-api.ts] + WEB_ADAPTER --> UI[Article reader UI] + DOC_HELPER --> DRIFT_TEST[apps/api/e2e/openapi-contract.e2e-spec.ts] + DRIFT_TEST --> SPEC +``` + +## Implementation Units + +- [x] **Unit 1: Publish the shared contract package** + +**Goal:** Create the repo-owned OpenAPI package and the checked-in canonical +API contract that the web app can consume. + +**Requirements:** R1, R2, R3, R4, R10 + +**Dependencies:** Current response shapes in `apps/api/src/articles/*` and +`apps/api/src/health/*`, plus the existing reader seam in `apps/web`. + +**Files:** + +- Create: `packages/api-contract/package.json` +- Create: `packages/api-contract/tsconfig.json` +- Create: `packages/api-contract/orval.config.ts` +- Create: `packages/api-contract/openapi/openapi.yaml` +- Create: `packages/api-contract/src/generated/api-client.ts` +- Create: `packages/api-contract/src/index.ts` +- Modify: `package.json` +- Modify: `apps/api/package.json` +- Modify: `apps/web/package.json` +- Modify: `apps/api/README.md` +- Modify: `apps/web/README.md` + +**Approach:** + +- Keep the checked-in YAML as the canonical contract artifact and generate the + web-facing client/types from that file; the generated surface can include the + health operations even though the web reader only imports the article + helpers. +- Expose the generated surface from `packages/api-contract/src/index.ts` so the + web app imports a normal workspace package instead of reading the spec file + directly at runtime. +- Add explicit refresh entrypoints in the API and contract package, plus a + repo-level alias, so developers have one obvious way to regenerate the + contract and the generated client together. + +**Execution note:** Start from the contract artifact and keep the package +boundary clean; do not recreate a parallel handwritten web contract. + +**Patterns to follow:** + +- `packages/ui/package.json` +- `packages/ui/src/index.ts` +- `apps/web/package.json` +- `apps/api/package.json` + +**Test scenarios:** + +- Happy path: shared package can build from the checked-in YAML and export a + usable client surface for the article reader. +- Edge case: the contract file preserves the current article list/detail field + set, including `summaryError` on detail and no `contentMarkdown`; health live + and ready schemas still match the current implementation. +- Integration: `apps/web` can depend on `@repo/api-contract` without falling + back to hand-written DTO-like types. + +**Verification:** + +- A single checked-in contract artifact exists, the generated client builds from + it, and the repo-level refresh flow is discoverable. + +- [x] **Unit 2: Add API emission and drift validation** + +**Goal:** Make `apps/api` emit the full public OpenAPI document from the real +Nest implementation and fail when the checked-in contract diverges. + +**Requirements:** R7, R8, R9, R11, R12 + +**Dependencies:** Unit 1 and the current article/health controller and DTO +mapping. + +**Files:** + +- Create: `apps/api/src/openapi/openapi-document.ts` +- Create: `apps/api/src/openapi/openapi-refresh.ts` +- Modify: `apps/api/src/articles/articles.controller.ts` +- Modify: `apps/api/src/articles/dto/article-list-item.dto.ts` +- Modify: `apps/api/src/articles/dto/article-detail-item.dto.ts` +- Modify: `apps/api/src/health/health.controller.ts` +- Modify: `apps/api/src/articles/articles.controller.spec.ts` +- Modify: `apps/api/src/health/health.controller.spec.ts` +- Create: `apps/api/e2e/openapi-contract.e2e-spec.ts` +- Modify: `apps/api/e2e/articles.e2e-spec.ts` +- Modify: `apps/api/e2e/health.e2e-spec.ts` + +**Approach:** + +- Annotate the current article and health DTOs/controllers with explicit + OpenAPI metadata so the generated document mirrors the full public surface + instead of inferring it indirectly. +- Factor document creation into a reusable helper so the refresh path and the + drift test use the same source of truth. +- Compare the emitted document against `packages/api-contract/openapi/openapi.yaml` + and fail on drift or breaking changes before merge. + +**Execution note:** Start with the drift test and the checked-in contract, then +make the controller metadata satisfy that contract. + +**Patterns to follow:** + +- `apps/api/src/articles/articles.controller.ts` +- `apps/api/src/articles/articles.controller.spec.ts` +- `apps/api/src/health/health.controller.ts` +- `apps/api/src/health/health.controller.spec.ts` +- `apps/api/e2e/articles.e2e-spec.ts` +- `apps/api/e2e/health.e2e-spec.ts` +- `apps/api/README.md` + +**Test scenarios:** + +- Happy path: the generated OpenAPI document includes `GET /health/live`, + `GET /health/ready`, `GET /articles`, and `GET /articles/{id}` with the + current response fields. +- Edge case: the readiness 503 response and the article detail schema still + expose `summaryError` as nullable while keeping internal persistence fields + out of the public contract. +- Error path: any implementation change that adds, removes, or renames public + fields without updating the contract fails the drift check. +- Integration: the OpenAPI document and the existing HTTP e2e assertions + describe the same health/article payloads, 404 behavior, and 503 readiness + behavior. + +**Verification:** + +- The API test suite can regenerate the contract document and detect mismatches + against the checked-in YAML before the change merges. + +- [x] **Unit 3: Swap the web seam to the generated contract** + +**Goal:** Remove the handwritten article-client contract from `apps/web` and +keep the reader behavior intact through the generated package. + +**Requirements:** R4, R5, R6, R12 + +**Dependencies:** Units 1 and 2. + +**Files:** + +- Modify: `apps/web/src/widgets/article-reader/api/articles-api.ts` +- Modify: `apps/web/src/widgets/article-reader/ui/article-reader-page.tsx` +- Modify: `apps/web/src/widgets/article-reader/ui/article-list.tsx` +- Modify: `apps/web/src/widgets/article-reader/ui/article-detail.tsx` +- Modify: `apps/web/package.json` +- Modify: `apps/web/next.config.ts` +- Modify: `apps/web/app/next-config.spec.ts` +- Modify: `apps/web/app/page.spec.tsx` +- Modify: `apps/web/e2e/home.spec.ts` + +**Approach:** + +- Keep a thin web adapter for `API_BASE_URL`, `cache: "no-store"`, URL + encoding, and 404-to-null translation, but source its types and request + surface from `@repo/api-contract`. The contract package may also contain + health operations, but this seam only consumes the article read helpers. +- Remove the local DTO-like type declarations from the seam file so the web app + stops owning the contract shape by hand. +- Update the web package wiring so direct app runs still build the shared + contract package before the reader code imports it. + +## System-Wide Impact + +- **Interaction graph:** `apps/api/src/articles/*` + `apps/api/src/health/*` + -> OpenAPI document helper -> `packages/api-contract/openapi/openapi.yaml` + -> generated client/types -> `apps/web/src/widgets/article-reader/api/articles-api.ts` + -> reader UI. +- **Error propagation:** 404 remains a 404 in the contract and API layer, but + the web adapter still translates it to `null` so the unavailable state stays + unchanged. +- **State lifecycle risks:** the checked-in contract or generated client can + drift from the Nest implementation; the drift test must catch that before the + mismatch reaches the browser. +- **API surface parity:** `/health/live`, `/health/ready`, `/articles`, and + `/articles/:id` all live in the same public contract, and `summaryError` + remains the public failure detail instead of any internal persistence field. +- **Integration coverage:** the OpenAPI drift test, HTTP health/article e2e + suite, and browser reader spec together prove the same contract from + different layers. +- **Unchanged invariants:** ingestion, summary generation, Prisma schema, + article IDs, URL-driven selection, and current summary fallback copy all stay + as they are. + +## Risks & Dependencies + +| Risk | Mitigation | +| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| Generated client shape feels heavier than the current thin wrapper | Keep `apps/web/src/widgets/article-reader/api/articles-api.ts` as a small adapter that only owns env, encoding, and 404 translation. | +| Contract artifact drifts from the Nest implementation | Add a dedicated API drift test that compares the emitted document to `packages/api-contract/openapi/openapi.yaml`. | +| New shared package creates workspace friction | Keep `packages/api-contract` private, exported, and aligned with the existing `packages/ui` pattern. | +| App README prose lags behind the contract | Update both app READMEs in the same pass and replace stale field names at the same time. | + +## Documentation / Operational Notes + +- Update `apps/api/README.md` and `apps/web/README.md` to point at the new + contract package and refresh flow. +- Keep the YAML contract as the reviewable repo artifact; do not require a + separate public docs server for this feature. +- When the contract changes later, refresh the checked-in YAML and regenerate + the client from that file instead of editing web copies by hand. + +## Sources & References + +- **Origin documents:** `docs/en/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md` + + `docs/zh-Hans/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md` +- Related code: `apps/api/src/articles/articles.controller.ts`, + `apps/api/src/articles/articles.service.ts`, + `apps/api/src/articles/article.repository.ts`, + `apps/api/src/health/health.controller.ts`, + `apps/web/src/widgets/article-reader/api/articles-api.ts`, + `apps/web/src/widgets/article-reader/ui/article-reader-page.tsx`, + `apps/api/e2e/articles.e2e-spec.ts`, + `apps/api/e2e/health.e2e-spec.ts`, + `apps/web/app/page.spec.tsx`, + `apps/web/e2e/home.spec.ts` +- Related docs: `apps/api/README.md`, `apps/web/README.md`, + `packages/ui/package.json` +- External docs: NestJS OpenAPI introduction and Orval diff --git a/docs/zh-Hans/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md b/docs/zh-Hans/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md new file mode 100644 index 0000000..17f33e8 --- /dev/null +++ b/docs/zh-Hans/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md @@ -0,0 +1,124 @@ +--- +date: 2026-04-24 +topic: web-api-openapi-contract-first +--- + +# Web/API 读取边界改为 OpenAPI 合同优先 + +## Problem Frame + +当前 `apps/web` 里读 `apps/api` 的方式还是手写 fetch 包装: +`apps/web/src/widgets/article-reader/api/articles-api.ts` 里自己定义返回 +类型,再通过 `response.json() as T` 信任 payload。与此同时, +`apps/api` 侧又有一套独立的 DTO class 和控制器映射。这个结构让“看起来有类型” +和“真的有合同”之间仍然有缝。 + +这次重构的目标不是换成另一种 RPC 风格,而是把整个 Web/API 读取边界收敛到 +一个可审查、可生成、可校验的 OpenAPI 合同上。当前已经能确认的外部读取面很窄, +但这条 seam 是后续所有 web->api 读路径的模板,所以它需要先变成 repo 级标准。 + +```mermaid +flowchart LR + SPEC[Repo-owned OpenAPI contract] --> GEN[Generated types / client] + SPEC --> API[apps/api REST implementation] + API --> DOC[OpenAPI document] + DOC --> GEN + GEN --> WEB[apps/web readers] + WEB --> UI[Article reader UI] + CI[CI drift / breaking-change gate] --> SPEC + CI --> API +``` + +## Requirements + +**Contract Source of Truth** + +- R1. 仓库必须为所有 `apps/web` 消费的 API read surface 提供一个单一的 + OpenAPI 合同来源。 +- R2. 这个合同必须能覆盖当前 `apps/web` 已消费的所有 API 响应形状,不得只修 + 文章 reader 这一条 seam 而保留另一套手写合同。 +- R3. 合同必须成为 PR 中可审查的仓库资产,避免只在运行时生成而无法被人类直接 + 评审。 + +**Web Consumption** + +- R4. `apps/web` 必须通过由合同生成的类型或 client 读取 API 数据,不再保留手写 + DTO-like 类型作为契约来源。 +- R5. `apps/web` 必须移除所有 unchecked 的 `response.json() as T` 或等价类型断言。 +- R6. 当前 reader 体验必须保持:默认文章选择、URL 保持、404 处理、摘要 fallback + 文案、文章详情渲染都不能因为迁移而退化。 + +**Backend Ownership and Validation** + +- R7. `apps/api` 继续作为 REST 实现的所有者,负责运行时校验、响应映射和数据来源。 +- R8. 任何响应形状变化都必须和合同变化一起提交,不能让 backend 实现先变、合同 + 后补。 +- R9. 合同层必须能让 breaking change 在合并前被看见,而不是等到 web 运行时才暴露。 + +**Workflow** + +- R10. 仓库必须提供一个可重复的 contract 更新流程,让开发者可以稳定生成或刷新 + OpenAPI 合同及 web 侧生成产物。 +- R11. CI 或等价的仓库级检查必须能发现实现与已发布合同之间的漂移。 +- R12. 这次迁移必须覆盖当前所有 web->api 读取 seam,不能只迁 article reader 后把 + 另一条手写路径留在仓库里。 + +## Success Criteria + +- `apps/web` 不再依赖手写的 API 返回类型作为唯一契约来源。 +- 当前 reader 页面仍能正常工作,且用户可见行为没有退化。 +- 合同变更会在 PR 里变得显式,review 时能直接看到影响范围。 +- 如果实现和合同不一致,仓库级检查能在合并前把问题拦住。 + +## Scope Boundaries + +- 本次不迁移到 tRPC,也不把 REST 改成内部 RPC 风格。 +- 本次不改 ingestion、summary、Prisma schema 或数据库持久化语义。 +- 本次不要求公开更多 reader 数据,例如 `contentMarkdown`。 +- 本次不重构成新的前端数据层架构,只解决 Web/API 读取合同这一层。 + +## Key Decisions + +- 选择 OpenAPI contract-first,而不是 tRPC。 +- OpenAPI 合同会作为 repo 资产被审查和同步,而不是只在运行时隐式生成。 +- 迁移范围按“所有当前 web->api 读取 seam”定义,不只限于 article reader。 +- backend 仍保留 Nest REST 形态,合同与实现保持同一条主线,而不是拆成两套真相。 + +**NestJS 参考文档** + +- OpenAPI 介绍: +- OpenAPI CLI plugin: +- 输入校验: +- Monorepo / workspace: + +**Next.js / OpenAPI 前端参考** + +- 数据获取 / Client Components: + +- Orval: + +- Orval React Query: + + +## Dependencies / Assumptions + +- 假设当前 REST response 形状可以被 OpenAPI 清晰表达,不需要先重设计接口。 +- 假设 web 侧可以接受 Orval 生成的 client / hooks,而不是继续维护手写 fetch + 包装。 +- 假设现有 CI 流程可以承接 contract drift 检查,而不需要重新搭一套独立流水线。 + +## Outstanding Questions + +### Deferred to Planning + +- [Affects R1][Technical] OpenAPI 合同的 canonical 文件格式应选 YAML 还是 JSON。 +- [Affects R10][Technical] 合同生成和 web client 生成应分别放在哪个脚本或 workspace + 任务里。 +- [Affects R10][Needs research] 适合当前 repo 的 contract diff / breaking-change gate + 工具是哪一个。 +- [Affects R4][Technical] web 侧生成产物应直接从 repo 里的合同读取,还是从某个导出 + artifact 读取。 + +## Next Steps + +-> /ce:plan for structured implementation planning diff --git a/docs/zh-Hans/plans/2026-04-24-001-feat-web-api-openapi-contract-first-plan.md b/docs/zh-Hans/plans/2026-04-24-001-feat-web-api-openapi-contract-first-plan.md new file mode 100644 index 0000000..1064022 --- /dev/null +++ b/docs/zh-Hans/plans/2026-04-24-001-feat-web-api-openapi-contract-first-plan.md @@ -0,0 +1,382 @@ +--- +title: refactor: 让 Web/API 读取边界改为 OpenAPI 优先 +type: refactor +status: completed +date: 2026-04-24 +origin: + - docs/en/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md + - docs/zh-Hans/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md +--- + +# refactor: 让 Web/API 读取边界改为 OpenAPI 优先 + +## 可能引入的额外包 + +- 新安装的依赖必须尽可能使用当前可用的最新稳定版本;只有在兼容 + 性、锁定策略或仓库约束明确不允许时,才向下退让。 +- `@nestjs/swagger` - 从 Nest 的 article controller/DTO 生成 OpenAPI + 文档。 +- `orval` - 从 checked-in 的 OpenAPI 合同生成 web 侧 client/types。 + +## 概览 + +把当前 `apps/api` 的对外 HTTP surface 收敛成一个仓库自有的 OpenAPI +合同:合同文件要检查入库、可审查、可生成,并作为 web 侧 client/types +的生成来源。API 仍然负责运行时 REST 实现和响应映射,但 wire contract +要变成双方都必须遵守的共享来源。这样可以把变化限制在当前公开接口, +而不是扩大成更大的 API 重构。 + +这次范围里 `apps/api` 的公开接口一共有 4 个:`GET /health/live`、 +`GET /health/ready`、`GET /articles`、`GET /articles/:id`。其中 web 只 +消费 articles 读取 seam,但 health 也必须进入 OpenAPI 和 drift check。 + +## 问题背景 + +`apps/web` 现在还是通过 `apps/web/src/widgets/article-reader/api/articles-api.ts` +里的手写包装去读 `apps/api`。这个文件自己定义返回类型,再通过 +`response.json() as T` 直接信任 payload,所以 web 层可以在不明显的 +情况下和 API 漂移。 + +`apps/api` 已经有两类对外 HTTP 入口:`apps/api/src/articles/*` 里的 +article 读取接口,以及 `apps/api/src/health/health.controller.ts` 里的 +liveness/readiness probes。当前它们各自有 runtime 实现,但还没有被统一 +发布成一个仓库级的 OpenAPI 资产。结果就是“看起来有类型”和“真的有 +合同”之间仍然有缝。 + +这份 plan 要做的是:把 OpenAPI 合同变成可审查的仓库资产,从它生成 +web 侧 client/types,并加一层 drift check,确保 API 实现不能先动、 +合同后补。 + +## 需求追踪 + +- R1. 为 `apps/api` 所有对外 HTTP surface 提供唯一 OpenAPI 来源。 +- R2. 覆盖当前所有公开响应形状,不只修 reader 这一条 seam;health + probes 也要进入合同。 +- R3. 把合同保留为 PR 里可审查的仓库资产。 +- R4. `apps/web` 必须改用生成的 types/client,而不是手写 DTO-like 类型。 +- R5. 清掉 reader seam 里的 `response.json() as T` 之类 unchecked 断言。 +- R6. reader 行为保持不退化:默认选择、URL 保持、404 处理、摘要 + fallback 文案、详情渲染都不变。 +- R7. `apps/api` 继续负责运行时校验、响应映射和持久化。 +- R8. 响应形状变化必须和合同变化一起提交。 +- R9. breaking change 要在合并前可见。 +- R10. 提供可重复的 contract refresh 流程。 +- R11. 加一个 repo 级 drift check,能抓住实现和发布合同的偏差。 +- R12. 迁移必须覆盖当前所有公开 HTTP 接口,不能只留一条手写路径。 + +## 范围边界 + +- 不迁移到 tRPC。 +- 不改 ingestion、summary 生成、Prisma schema 或持久化语义。 +- 不扩展公开 reader payload,包括 `contentMarkdown`。 +- 不重做整套前端数据层。 +- 不把这次 refactor 扩大成更大的 API 重构,只把现有公开接口收进合同。 +- 不需要 public Swagger UI route;合同文件本身就是可审查产物。 + +## 背景与调研 + +### 相关代码与模式 + +- `apps/web/src/widgets/article-reader/api/articles-api.ts` 是当前手写 seam, + 里面有 DTO-like 类型和 `response.json() as T`。 +- `apps/web/src/widgets/article-reader/ui/article-reader-page.tsx`、 + `apps/web/src/widgets/article-reader/ui/article-list.tsx` 和 + `apps/web/src/widgets/article-reader/ui/article-detail.tsx` 是 reader UI + 消费者,必须继续保持同样的可见行为。 +- `apps/api/src/articles/articles.controller.ts`、 + `apps/api/src/articles/articles.service.ts` 和 + `apps/api/src/articles/article.repository.ts` 定义了当前响应映射边界。 +- `apps/api/src/articles/dto/article-list-item.dto.ts` 与 + `apps/api/src/articles/dto/article-detail-item.dto.ts` 是当前 DTO 形状, + OpenAPI 文档应该描述的就是它们。 +- `apps/api/src/health/health.controller.ts` 与 + `apps/api/e2e/health.e2e-spec.ts` 定义了 liveness/readiness 对外合同, + 也必须进入同一份 OpenAPI 视野。 +- `apps/api/e2e/articles.e2e-spec.ts` 已经在断言文章 payload 形状, + `apps/web/app/page.spec.tsx` 和 `apps/web/e2e/home.spec.ts` 则锁住了 + reader 行为。 +- `apps/api/README.md` 和 `apps/web/README.md` 现在仍在用 prose 描述这条 + seam,部分字段命名还偏旧,所以需要一起更新到新的合同路径。 +- `packages/ui/package.json` 是最接近的现成模式:一个跨 app 的共享 + workspace package,被 build 后再被别的 app import。 + +### 仓库记忆里的相关经验 + +- 这条 seam 还没有专门的 OpenAPI 合同方案文档;最接近的仓库约束是: + contract drift 要在合并前可见,workspace 边界要保持 package-local, + 不要漏到 repo root。 +- 现有记忆里对 `articles-api.ts` 的合同安全风险有过同类提醒: + 手写 client 类型和像 `summaryErrorReason` 这种旧命名,正是这类 plan + 要防的漂移点。 + +### 外部参考 + +- NestJS OpenAPI introduction: +- NestJS OpenAPI CLI plugin: +- Orval: +- Orval React Query guide: + +## 关键技术决策 + +- canonical 合同放在 `packages/api-contract/openapi/openapi.yaml`,并且用 + YAML 做可审查产物。refresh 流程直接读写这份文件,不再多包一层导出 + 的 spec artifact。 +- 新增一个共享的 `packages/api-contract` workspace package,对外导出 + 生成后的 web client/types。这样合同资产和消费边界都落在正常的 + Turborepo package graph 里,而不是用一个临时 root 脚本硬接。 +- API 的 runtime 归属继续留在 `apps/api/src/articles` 和 + `apps/api/src/health`,用显式 OpenAPI decorator 和共享的 document + helper 来描述,不额外搭一个 public docs server。 +- web seam 保持很薄:它仍然负责 `API_BASE_URL`、请求缓存策略、URL + 编码和 404 -> null 转换,但不再自己定义合同 shape;health 不需要 + web adapter。 +- 用一个 contract drift check,把 emitted OpenAPI 文档和 checked-in YAML + 的偏差挡在 merge 前。 +- reader 仍然走 server-side flow;生成 client 取代手写 payload 类型, + 但不要引入新的 client-state 模型。 + +## Open Questions + +### 已在规划阶段解决 + +- YAML 还是 JSON:选 YAML,因为这份合同就是要给人 review 的,并且要 + 一起进入仓库。 +- canonical artifact 放哪:`packages/api-contract/openapi/openapi.yaml`。 +- web 怎么读合同:`apps/web` import 生成后的 `@repo/api-contract` + package;refresh 流程则直接读 YAML。web 只取 articles 读取 seam, + health 只进入 spec 和 drift check。 +- 是否需要 public Swagger route:不需要;checked-in artifact 就是合同。 + +### 延后到实现 + +- `packages/api-contract` 里具体的 generated 文件名和 export 名称。 +- API document helper 是只给 refresh script 用,还是也给 e2e drift test + 复用,或者两者都复用。 +- web 侧薄 adapter 的具体形状,只要还能保住 `API_BASE_URL` 和 404 + 处理归属即可。 + +## 高层技术设计 + +> 这张图只表达 intended approach,给 review 用,不是实现规格。落地时 +> 只把它当上下文,不要把它照抄成代码。 + +```mermaid +flowchart LR + API_IMPL[apps/api/src/articles/* + apps/api/src/health/*] --> DOC_HELPER[OpenAPI document helper] + DOC_HELPER --> SPEC[packages/api-contract/openapi/openapi.yaml] + SPEC --> CLIENT_GEN[packages/api-contract generated client/types] + CLIENT_GEN --> WEB_ADAPTER[apps/web/src/widgets/article-reader/api/articles-api.ts] + WEB_ADAPTER --> UI[Article reader UI] + DOC_HELPER --> DRIFT_TEST[apps/api/e2e/openapi-contract.e2e-spec.ts] + DRIFT_TEST --> SPEC +``` + +## Implementation Units + +- [x] **Unit 1: 发布共享合同 package** + +**Goal:** 创建仓库自有的 OpenAPI package,以及 web 可以消费的、检查入库 +的 canonical API contract。 + +**Requirements:** R1, R2, R3, R4, R10 + +**Dependencies:** `apps/api/src/articles/*`、`apps/api/src/health/*` 里的 +当前响应形状,以及 `apps/web` 里的现有 reader seam。 + +**Files:** + +- Create: `packages/api-contract/package.json` +- Create: `packages/api-contract/tsconfig.json` +- Create: `packages/api-contract/orval.config.ts` +- Create: `packages/api-contract/openapi/openapi.yaml` +- Create: `packages/api-contract/src/generated/api-client.ts` +- Create: `packages/api-contract/src/index.ts` +- Modify: `package.json` +- Modify: `apps/api/package.json` +- Modify: `apps/web/package.json` +- Modify: `apps/api/README.md` +- Modify: `apps/web/README.md` + +**Approach:** + +- 把 checked-in YAML 作为 canonical contract artifact,用它生成 web-facing + client/types;生成产物里可以包含 articles 和 health 的操作,但 web + 只会 import articles 读取 seam。 +- 通过 `packages/api-contract/src/index.ts` 暴露生成后的 surface,让 web + import 一个正常的 workspace package,而不是运行时去直接读 spec 文件。 +- 在 API package、contract package 和 repo root 上都给出明确的 refresh + 入口,保证开发者有一条很清楚的合同刷新路径。 + +**Execution note:** 从合同文件本身开始,保持 package 边界干净;不要再 +搞一套平行的手写 web contract。 + +**Patterns to follow:** + +- `packages/ui/package.json` +- `packages/ui/src/index.ts` +- `apps/web/package.json` +- `apps/api/package.json` + +**Test scenarios:** + +- Happy path: shared package 能基于 checked-in YAML 构建,并导出 + article reader 可用的 client surface。 +- Edge case: 合同文件保留当前 article list/detail 字段集,包括 detail + 上的 `summaryError`,并且不出现 `contentMarkdown`;health 的 live/ready + schema 也保持和当前实现一致。 +- Integration: `apps/web` 可以依赖 `@repo/api-contract`,而不会回退到 + 手写 DTO-like 类型。 + +**Verification:** + +- 仓库里只有一份可审查的合同文件,生成后的 client 可以从它构建, + 并且 refresh 流程对开发者是可发现的。 + +- [x] **Unit 2: 增加 API emit 和 drift validation** + +**Goal:** 让 `apps/api` 从真实 Nest 实现里 emit 全部公开 HTTP OpenAPI +文档,并在 checked-in contract 偏离时失败。 + +**Requirements:** R7, R8, R9, R11, R12 + +**Dependencies:** Unit 1,以及当前 article / health controller 与 DTO +mapping。 + +**Files:** + +- Create: `apps/api/src/openapi/openapi-document.ts` +- Create: `apps/api/src/openapi/openapi-refresh.ts` +- Modify: `apps/api/src/articles/articles.controller.ts` +- Modify: `apps/api/src/articles/dto/article-list-item.dto.ts` +- Modify: `apps/api/src/articles/dto/article-detail-item.dto.ts` +- Modify: `apps/api/src/health/health.controller.ts` +- Modify: `apps/api/src/articles/articles.controller.spec.ts` +- Modify: `apps/api/src/health/health.controller.spec.ts` +- Create: `apps/api/e2e/openapi-contract.e2e-spec.ts` +- Modify: `apps/api/e2e/articles.e2e-spec.ts` +- Modify: `apps/api/e2e/health.e2e-spec.ts` + +**Approach:** + +- 给当前 article 和 health controller / DTO 加显式的 OpenAPI metadata, + 让生成出来的文档描述全部 public surface,而不是靠隐式推断。 +- 把 document creation 抽到一个可复用 helper 里,这样 refresh path 和 + drift test 用的是同一份 source of truth。 +- 把 emitted document 和 `packages/api-contract/openapi/openapi.yaml` + 做比较;只要有 drift 或 breaking change,就在 merge 前失败。 + +**Execution note:** 先把 drift test 和 checked-in contract 锁住,再去 +改 controller metadata 让它们对齐。 + +**Patterns to follow:** + +- `apps/api/src/articles/articles.controller.ts` +- `apps/api/src/articles/articles.controller.spec.ts` +- `apps/api/src/health/health.controller.ts` +- `apps/api/src/health/health.controller.spec.ts` +- `apps/api/e2e/articles.e2e-spec.ts` +- `apps/api/e2e/health.e2e-spec.ts` +- `apps/api/README.md` + +**Test scenarios:** + +- Happy path: 生成的 OpenAPI 文档包含 `GET /health/live`、 + `GET /health/ready`、`GET /articles` 和 `GET /articles/{id}`,并且字段和 + 当前 public surface 一致。 +- Edge case: health readiness 的 503 响应和 article detail schema 继续把 + `summaryError` 作为 nullable 暴露,并且不把内部持久化字段带进公开合同。 +- Error path: 任何没有同步更新合同的字段新增、删除或重命名,都会让 + drift check 失败。 +- Integration: OpenAPI 文档和现有 HTTP e2e 断言描述的是同一组 + health/article payload、404 行为和 503 readiness 行为。 + +**Verification:** + +- API 测试套件可以重新生成合同文档,并在 merge 前抓住它和 checked-in + YAML 的不一致。 + +- [x] **Unit 3: 把 web seam 切到生成后的合同** + +**Goal:** 移除 `apps/web` 里手写的 article-client contract,并通过生成 +package 保持 reader 行为不变。 + +**Requirements:** R4, R5, R6, R12 + +**Dependencies:** Units 1 和 2。 + +**Files:** + +- Modify: `apps/web/src/widgets/article-reader/api/articles-api.ts` +- Modify: `apps/web/src/widgets/article-reader/ui/article-reader-page.tsx` +- Modify: `apps/web/src/widgets/article-reader/ui/article-list.tsx` +- Modify: `apps/web/src/widgets/article-reader/ui/article-detail.tsx` +- Modify: `apps/web/package.json` +- Modify: `apps/web/next.config.ts` +- Modify: `apps/web/app/next-config.spec.ts` +- Modify: `apps/web/app/page.spec.tsx` +- Modify: `apps/web/e2e/home.spec.ts` + +**Approach:** + +- 让 web seam 继续只负责 `API_BASE_URL`、`cache: "no-store"`、URL 编码 + 和 404 -> null 转换,但 types 和 request surface 从 `@repo/api-contract` + 来;contract package 里即使包含 health operations,reader seam 也只会 + 读取 articles 相关导出。 +- 删掉 seam 文件里本地手写的 DTO-like 类型,让 web app 不再手工拥有 + contract shape。 +- 更新 web package wiring,保证直接跑 app 时也会先 build shared + contract package,再让 reader 代码 import 它。 + +## 全局影响 + +- **Interaction graph:** `apps/api/src/articles/*` + `apps/api/src/health/*` + -> OpenAPI document helper -> `packages/api-contract/openapi/openapi.yaml` + -> generated client/types -> `apps/web/src/widgets/article-reader/api/articles-api.ts` + -> reader UI. +- **Error propagation:** 404 在合同和 API 层仍然是 404,但 web adapter + 继续把它转成 `null`,让 unavailable state 保持原样。 +- **State lifecycle risks:** checked-in contract 或 generated client 可能会和 + Nest 实现漂移;drift test 必须在它们到 browser 之前把问题拦住。 +- **API surface parity:** `/health/live`、`/health/ready`、`/articles` 和 + `/articles/:id` 都纳入同一份公开合同,`summaryError` 仍然是公开 failure + detail,而不是任何内部持久化字段。 +- **Integration coverage:** OpenAPI drift test、HTTP health/article e2e 和 + browser reader spec 三层一起,才能证明同一份合同。 +- **Unchanged invariants:** ingestion、summary 生成、Prisma schema、 + article id、URL-driven selection,以及现有 summary fallback copy 都不变。 + +## 风险与依赖 + +| Risk | Mitigation | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| generated client 比现在的薄 wrapper 更重 | 保持 `apps/web/src/widgets/article-reader/api/articles-api.ts` 只是很薄的 adapter,只负责 env、编码和 404 转换。 | +| 合同文件和 Nest 实现漂移 | 增加专门的 API drift test,把 emitted document 和 `packages/api-contract/openapi/openapi.yaml` 做比较。 | +| 新 shared package 增加 workspace 摩擦 | 让 `packages/api-contract` 保持 private、可导出,并对齐现有 `packages/ui` 模式。 | +| app README 文案落后于合同 | 在同一轮里同步更新 `apps/api/README.md` 和 `apps/web/README.md`,并一起修掉旧字段名。 | + +## 文档 / 运维说明 + +- 更新 `apps/api/README.md` 和 `apps/web/README.md`,把新的合同 package + 和 refresh 流程写进去。 +- 保持 YAML 合同作为可审查的仓库产物;这个 feature 不要求再加一个 + public docs server。 +- 以后只要合同变了,就先 refresh checked-in YAML,再从这份文件重新 + generate client,不要手改 web 副本。 + +## 来源与参考 + +- **Origin documents:** `docs/en/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md` + + `docs/zh-Hans/brainstorms/2026-04-24-web-api-openapi-contract-first-requirements.md` +- 相关代码:`apps/api/src/articles/articles.controller.ts`、 + `apps/api/src/articles/articles.service.ts`、 + `apps/api/src/articles/article.repository.ts`、 + `apps/api/src/health/health.controller.ts`、 + `apps/web/src/widgets/article-reader/api/articles-api.ts`、 + `apps/web/src/widgets/article-reader/ui/article-reader-page.tsx`、 + `apps/api/e2e/articles.e2e-spec.ts`、 + `apps/api/e2e/health.e2e-spec.ts`、 + `apps/web/app/page.spec.tsx`、 + `apps/web/e2e/home.spec.ts` +- 相关文档:`apps/api/README.md`、`apps/web/README.md`、 + `packages/ui/package.json` +- 外部文档:NestJS OpenAPI introduction 和 Orval diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs index 364daae..bcb7d54 100644 --- a/lint-staged.config.mjs +++ b/lint-staged.config.mjs @@ -8,6 +8,7 @@ const jsTsPattern = "**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}"; const prettierOnlyPattern = "**/*.{css,html,json,md,mdx}"; const eslintPackageRoots = ["apps", "packages"]; const rootScopedIgnoredEslintPrefixes = [".github/scripts/fixtures/"]; +const rootScopedIgnoredLintPrefixes = ["packages/api-contract/src/generated/"]; /** * Quote a file path for safe shell usage. @@ -22,14 +23,31 @@ const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`; * @param {string} file * @returns {string} */ +const normalizePathSeparators = (file) => file.replace(/\\/g, "/"); + const toWorkspaceRelativePath = (file) => { if (path.isAbsolute(file)) { - return path.relative(workspaceRoot, file); + return normalizePathSeparators(path.relative(workspaceRoot, file)); } - return file; + return normalizePathSeparators(file); }; +/** + * Keep generated contract files out of staged format/lint routing. + * @param {string} file + * @returns {boolean} + */ +const isIgnoredByLintRouting = (file) => + rootScopedIgnoredLintPrefixes.some((prefix) => file.startsWith(prefix)); + +/** + * Keep generated files out of formatting routing as well. + * @param {string} file + * @returns {boolean} + */ +const isIgnoredByFormattingRouting = (file) => isIgnoredByLintRouting(file); + /** * Discover package directories that own their own ESLint config. * @returns {string[]} @@ -120,7 +138,11 @@ const runPackageEslint = (packageDir, files) => { const packageAbsoluteDir = path.join(workspaceRoot, packageDir); const packageRelativeFiles = lintableFiles.map((file) => - quote(path.relative(packageAbsoluteDir, path.join(workspaceRoot, file))), + quote( + normalizePathSeparators( + path.relative(packageAbsoluteDir, path.join(workspaceRoot, file)), + ), + ), ); return [ @@ -128,13 +150,32 @@ const runPackageEslint = (packageDir, files) => { ]; }; +/** + * Build a prettier command only when there are files left to format. + * @param {string[]} files + * @returns {string[]} + */ +const runPrettier = (files) => { + const formattedFiles = files + .map(toWorkspaceRelativePath) + .filter((file) => !isIgnoredByFormattingRouting(file)); + + if (formattedFiles.length === 0) { + return []; + } + + return [`prettier --write ${formattedFiles.map(quote).join(" ")}`]; +}; + export default { /** * @param {string[]} files * @returns {string[]} */ [jsTsPattern]: (files) => { - const normalizedFiles = files.map(toWorkspaceRelativePath); + const normalizedFiles = files + .map(toWorkspaceRelativePath) + .filter((file) => !isIgnoredByLintRouting(file)); const rootFiles = []; const packageFiles = new Map(); @@ -152,7 +193,7 @@ export default { } return [ - `prettier --write ${normalizedFiles.map(quote).join(" ")}`, + ...runPrettier(normalizedFiles), ...runRootEslint(rootFiles), ...Array.from(packageFiles.entries()).flatMap( ([packageDir, packageDirFiles]) => @@ -160,5 +201,5 @@ export default { ), ]; }, - [prettierOnlyPattern]: ["prettier --write"], + [prettierOnlyPattern]: (files) => runPrettier(files), }; diff --git a/package.json b/package.json index 3cf1747..7a32bae 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "dev": "turbo run dev", "build": "turbo run build", + "contract:refresh": "turbo run contract:refresh --filter=api && turbo run contract:refresh --filter=@repo/api-contract", "prepare": "husky .husky", "typecheck": "turbo run typecheck", "test": "pnpm test:root && turbo run test --ui=stream", diff --git a/packages/api-contract/openapi/openapi.yaml b/packages/api-contract/openapi/openapi.yaml new file mode 100644 index 0000000..502d918 --- /dev/null +++ b/packages/api-contract/openapi/openapi.yaml @@ -0,0 +1,194 @@ +components: + schemas: + ArticleDetailItemDto: + properties: + originalUrl: + type: string + publishedAt: + format: date-time + type: string + sourceTitle: + type: string + summary: + type: string + summaryError: + allOf: + - $ref: "#/components/schemas/ArticleSummaryErrorDto" + nullable: true + type: object + title: + type: string + translatedTitle: + type: string + required: + - title + - translatedTitle + - sourceTitle + - publishedAt + - summary + - summaryError + - originalUrl + type: object + ArticleListItemDto: + properties: + id: + type: string + originalUrl: + type: string + publishedAt: + format: date-time + type: string + sourceTitle: + type: string + title: + type: string + translatedTitle: + type: string + required: + - id + - title + - translatedTitle + - sourceTitle + - publishedAt + - originalUrl + type: object + ArticleSummaryErrorDto: + properties: + action: + type: string + code: + type: string + copyText: + type: string + message: + type: string + title: + type: string + required: + - action + - code + - copyText + - message + - title + type: object + HealthLiveChecksDto: + properties: + application: + type: string + required: + - application + type: object + HealthLiveResponseDto: + properties: + checks: + $ref: "#/components/schemas/HealthLiveChecksDto" + service: + type: string + status: + type: string + required: + - checks + - service + - status + type: object + HealthReadyChecksDto: + properties: + application: + type: string + database: + type: string + required: + - application + - database + type: object + HealthReadyResponseDto: + properties: + checks: + $ref: "#/components/schemas/HealthReadyChecksDto" + service: + type: string + status: + type: string + required: + - checks + - service + - status + type: object +info: + contact: {} + description: OpenAPI contract for the RSSift API surface + title: RSSift API + version: 0.1.0 +openapi: 3.0.0 +paths: + /articles: + get: + operationId: articles + parameters: [] + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/ArticleListItemDto" + type: array + description: "" + summary: "" + tags: &a1 + - articles + /articles/{id}: + get: + operationId: articleById + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ArticleDetailItemDto" + description: "" + "404": + description: Article not found + summary: "" + tags: *a1 + /health/live: + get: + operationId: healthLive + parameters: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/HealthLiveResponseDto" + description: "" + summary: "" + tags: &a2 + - health + /health/ready: + get: + operationId: healthReady + parameters: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/HealthReadyResponseDto" + description: "" + "503": + content: + application/json: + schema: + $ref: "#/components/schemas/HealthReadyResponseDto" + description: "" + summary: "" + tags: *a2 +servers: [] +tags: [] diff --git a/packages/api-contract/orval.config.ts b/packages/api-contract/orval.config.ts new file mode 100644 index 0000000..922804c --- /dev/null +++ b/packages/api-contract/orval.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "orval"; + +export default defineConfig({ + api: { + input: { + target: "./openapi/openapi.yaml", + }, + output: { + baseUrl: "/", + client: "fetch", + mode: "single", + target: "./src/generated/api-client.ts", + }, + }, +}); diff --git a/packages/api-contract/package.json b/packages/api-contract/package.json new file mode 100644 index 0000000..bb4e390 --- /dev/null +++ b/packages/api-contract/package.json @@ -0,0 +1,29 @@ +{ + "name": "@repo/api-contract", + "type": "module", + "private": true, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "generate": "orval --config ./orval.config.ts", + "contract:refresh": "pnpm generate", + "prebuild": "pnpm generate", + "build": "tsc -p tsconfig.build.json", + "lint": "eslint . --max-warnings 0", + "pretypecheck": "pnpm generate", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "eslint": "catalog:", + "orval": "8.8.1", + "typescript": "6.0.3" + } +} diff --git a/packages/api-contract/src/index.ts b/packages/api-contract/src/index.ts new file mode 100644 index 0000000..24d9c2e --- /dev/null +++ b/packages/api-contract/src/index.ts @@ -0,0 +1,178 @@ +import { + articleById, + articles, + getArticleByIdUrl, + getArticlesUrl, + getHealthLiveUrl, + getHealthReadyUrl, + healthLive, + healthReady, + type ArticleDetailItemDto, + type ArticleListItemDto, + type ArticleSummaryErrorDto, + type HealthLiveResponseDto, + type HealthReadyResponseDto, +} from "./generated/api-client"; + +export type { + ArticleDetailItemDto, + ArticleListItemDto, + ArticleSummaryErrorDto, + HealthLiveResponseDto, + HealthReadyResponseDto, +}; + +export type ApiResponse = { + data: T; + status: number; +}; + +const DEFAULT_FETCH_TIMEOUT_MS = 10_000; + +export { + articleById, + articles, + getArticleByIdUrl, + getArticlesUrl, + getHealthLiveUrl, + getHealthReadyUrl, + healthLive, + healthReady, +}; + +type ManagedFetch = { + cleanup: () => void; + signal: AbortSignal; +}; + +function createManagedFetchSignal( + signal?: AbortSignal | null, + timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, +): ManagedFetch { + const controller = new AbortController(); + const timeout = setTimeout(() => { + controller.abort(); + }, timeoutMs); + + const onAbort = () => { + controller.abort(); + }; + + if (signal) { + if (signal.aborted) { + controller.abort(); + } else { + signal.addEventListener("abort", onAbort, { once: true }); + } + } + + return { + cleanup: () => { + clearTimeout(timeout); + signal?.removeEventListener("abort", onAbort); + }, + signal: controller.signal, + }; +} + +async function fetchJson( + url: string, + init?: RequestInit, +): Promise> { + const { cleanup, signal } = createManagedFetchSignal(init?.signal); + + try { + const response = await fetch(url, { + ...init, + signal, + }); + const data = response.body + ? ((await response.json()) as T) + : (undefined as unknown as T); + + return { + data, + status: response.status, + }; + } finally { + cleanup(); + } +} + +export async function getArticles( + apiBaseUrl: string, + init?: RequestInit, +): Promise> { + return fetchJson( + new URL(getArticlesUrl(), apiBaseUrl).toString(), + { + ...init, + method: "GET", + }, + ); +} + +export async function getArticleById( + apiBaseUrl: string, + articleId: string, + init?: RequestInit, +): Promise> { + const { cleanup, signal } = createManagedFetchSignal(init?.signal); + try { + const response = await fetch( + new URL( + getArticleByIdUrl(encodeURIComponent(articleId)), + apiBaseUrl, + ).toString(), + { + ...init, + signal, + method: "GET", + }, + ); + + if (response.status === 404) { + return { + data: null, + status: response.status, + }; + } + + const data = response.body + ? ((await response.json()) as ArticleDetailItemDto) + : (undefined as unknown as ArticleDetailItemDto); + + return { + data, + status: response.status, + }; + } finally { + cleanup(); + } +} + +export async function getHealthLive( + apiBaseUrl: string, + init?: RequestInit, +): Promise> { + return fetchJson( + new URL(getHealthLiveUrl(), apiBaseUrl).toString(), + { + ...init, + method: "GET", + }, + ); +} + +export async function getHealthReady( + apiBaseUrl: string, + init?: RequestInit, +): Promise> { + return fetchJson( + new URL(getHealthReadyUrl(), apiBaseUrl).toString(), + { + ...init, + method: "GET", + }, + ); +} diff --git a/packages/api-contract/tsconfig.build.json b/packages/api-contract/tsconfig.build.json new file mode 100644 index 0000000..2736dc9 --- /dev/null +++ b/packages/api-contract/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "ignoreDeprecations": "6.0", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/api-contract/tsconfig.json b/packages/api-contract/tsconfig.json new file mode 100644 index 0000000..c256054 --- /dev/null +++ b/packages/api-contract/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@repo/typescript-config/nextjs.json", + "compilerOptions": { + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/eslint-config/base.js b/packages/eslint-config/base.js index 1a7a2e3..f87a722 100644 --- a/packages/eslint-config/base.js +++ b/packages/eslint-config/base.js @@ -11,6 +11,7 @@ export const baseConfig = defineConfig( "**/node_modules/**", "**/.next/**", "**/dist/**", + "apps/api/src/generated/**", "**/coverage/**", "**/.turbo/**", "**/playwright-report/**", @@ -35,6 +36,8 @@ export const baseConfig = defineConfig( "*.mjs", ".github/scripts/*.mjs", ".github/scripts/*.test.mjs", + "orval.config.ts", + "packages/api-contract/orval.config.ts", ], }, }, @@ -68,6 +71,18 @@ export const baseConfig = defineConfig( "@typescript-eslint/restrict-template-expressions": "off", }, }, + { + files: ["packages/api-contract/src/generated/api-client.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-invalid-void-type": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + }, + }, eslintConfigPrettier, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7e922b..b119554 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: '@nestjs/platform-express': specifier: ^11.1.19 version: 11.1.19(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) + '@nestjs/swagger': + specifier: 11.4.1 + version: 11.4.1(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2) '@prisma/adapter-pg': specifier: 7.8.0 version: 7.8.0 @@ -162,18 +165,27 @@ importers: tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 + tsx: + specifier: 4.21.0 + version: 4.21.0 typescript: specifier: 6.0.3 version: 6.0.3 + yaml: + specifier: 2.8.3 + version: 2.8.3 apps/web: dependencies: + '@repo/api-contract': + specifier: workspace:* + version: link:../../packages/api-contract '@repo/ui': specifier: workspace:* version: link:../../packages/ui next: specifier: 16.2.4 - version: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 16.2.4(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: specifier: ^19.2.5 version: 19.2.5 @@ -239,6 +251,24 @@ importers: specifier: 6.0.3 version: 6.0.3 + packages/api-contract: + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@repo/typescript-config': + specifier: workspace:* + version: link:../typescript-config + eslint: + specifier: 'catalog:' + version: 9.39.4(jiti@2.6.1) + orval: + specifier: 8.8.1 + version: 8.8.1(prettier@3.8.3)(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + packages/eslint-config: devDependencies: '@eslint/js': @@ -291,7 +321,7 @@ importers: version: 30.3.0(@types/node@25.6.0)(ts-node@10.9.2(@types/node@25.6.0)(typescript@6.0.3)) next: specifier: 16.2.4 - version: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 16.2.4(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) typescript: specifier: 6.0.3 version: 6.0.3 @@ -546,6 +576,11 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@commander-js/extra-typings@14.0.0': + resolution: {integrity: sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==} + peerDependencies: + commander: ~14.0.0 + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -601,6 +636,162 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -643,6 +834,9 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@gerrit0/mini-shiki@3.23.0': + resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} + '@hono/node-server@1.19.11': resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} engines: {node: '>=18.14.1'} @@ -1098,6 +1292,9 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -1158,6 +1355,19 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/platform-express@11.1.19': resolution: {integrity: sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==} peerDependencies: @@ -1173,6 +1383,23 @@ packages: prettier: optional: true + '@nestjs/swagger@11.4.1': + resolution: {integrity: sha512-GuGzs8F1Cb3n+eEarmOqB4nt2ai+x4XGOYUXNYplOtDeB59DaFY5E16bsHsBWXiWgD1ywbyKQ5OVv02bQtB1Dw==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/testing@11.1.19': resolution: {integrity: sha512-/UFNWXvPEdu4v4DlC5oWLbGKmD27LehLK06b8oLzs6D6lf4vAQTdST8LRAXBadyMUQnVEQWMuBo3CtAVtlfXtQ==} peerDependencies: @@ -1268,6 +1495,44 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true + '@orval/angular@8.8.1': + resolution: {integrity: sha512-/6YxMY27dGNEeHR+f5+WgNDUYaXpcUmZ1JXJzW/dGmVl8fHTPAMgmJAlKQcTsD2/bdfGl6zkgHsI5XOT5oALow==} + + '@orval/axios@8.8.1': + resolution: {integrity: sha512-IVVtH3krs9c7t6bgzZ955KpNGAvn/FM2Y+30SRpgLGVn6y/0puo94JjuaziVZMY3FBZpXlFB5/9vnbTi87MTLA==} + + '@orval/core@8.8.1': + resolution: {integrity: sha512-x0u4O9FdoHaB/kDjCSGPkaRw/xe3qws9ERqQrcnGhvoBPjcrmdOR87siqUnQFDHsRbv3iocQcdg4hr62WFORBQ==} + peerDependencies: + '@faker-js/faker': '>=10' + peerDependenciesMeta: + '@faker-js/faker': + optional: true + + '@orval/fetch@8.8.1': + resolution: {integrity: sha512-nd8qS181wF8tVfCNl7PjdH3edNCMnKDDew0tJ3eakwlZqQ7e4x3JAPm2deNYP7sll94M9MVPdz8beFfbhA7APg==} + + '@orval/hono@8.8.1': + resolution: {integrity: sha512-GX6e2C4cO+Qi9xdtBl69+2bg5Fb79CjYrqiHLmuNf/n5avmgVLXB94+MGE6NRAZoVFhYiaovlhChYZ06wyaUfw==} + + '@orval/mcp@8.8.1': + resolution: {integrity: sha512-rdliCwbo1akUoV3oX39sTNAlYbHSD4Qj9MBE5CA9DzIoZXS+1KqhDpH+rGnfZL9st6dxHXxsWX1yV16G10LngA==} + + '@orval/mock@8.8.1': + resolution: {integrity: sha512-NWpMl0M3tvRGCtuvydoHna1sVCxmHH3gI6gMAlO3VOXDnAJxANpuyVEXSsGlYTzGtdMG4vvjYpwcpBM7OKQnAA==} + + '@orval/query@8.8.1': + resolution: {integrity: sha512-t177W0bXOVV0K0brnc3rDOQwaNX+mejenhfA8yLaNDBAX5g1nTIedhAbrbjJAmkHY/f3kxjiY05DztG+pwkTgg==} + + '@orval/solid-start@8.8.1': + resolution: {integrity: sha512-54ygWCI5aGLALhsTHmqsIPyMzTDVEg+VeGEfDSE+MJ6/8GicE+f96qBdaCAa/cSu0a5EPRDwwNaGUKMZrhh4Hw==} + + '@orval/swr@8.8.1': + resolution: {integrity: sha512-sWdzZSdp1MFwQa0ngh7HfSwT5A6qmNWNGB6aRnTgBlKxRuRrA40YnwAfb5VlbxK2UOzII7VlY4vinz/e/0UNag==} + + '@orval/zod@8.8.1': + resolution: {integrity: sha512-TkYH4deLNqnxhXNZpHZ3/SkqTOUszHFq7UEYdTEkkuF257rFSM+bRTo30kajX6PfmG/jJrcRhXvkycp/6z7Kkw==} + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -1512,9 +1777,58 @@ packages: '@types/react': optional: true + '@scalar/helpers@0.5.2': + resolution: {integrity: sha512-Pi1GAl8jO6ungmGj2sjDfCfqiBNrKW6HXDZmminV94ybGU/KtRLOqHwd0n9FIhY3j0RYGpGC0VCuniCICfQPHg==} + engines: {node: '>=22'} + + '@scalar/json-magic@0.12.8': + resolution: {integrity: sha512-a559iO8tmFeA90JJAAM3U5x1Asf3mr0Z8uDC1PmyLTDjdSOfajP7EY9VzNoXE2cM48ilf9qrjmkbw/d4VCFjQw==} + engines: {node: '>=22'} + + '@scalar/openapi-parser@0.25.12': + resolution: {integrity: sha512-1hajBAbc7cbEcsSZEQxaPXZyCjMf6h6hObV+SO32jkC6rrxinPXQIucDu9HTu/jm/FaaMnNhc8/XDWz5/E49cQ==} + engines: {node: '>=22'} + + '@scalar/openapi-types@0.6.1': + resolution: {integrity: sha512-P1RvyTFN0vRSL136OqWjlZfSFjY9JoJfuD6LM1mIjoocfwmqX3WuzsFEFX6hAeeDlTh6gjbiy+OdhSee8GFfSA==} + engines: {node: '>=22'} + + '@scalar/openapi-types@0.8.0': + resolution: {integrity: sha512-WmaxVSfvY5K/TwcG2B2TU1WOe1As1uc2s7myswtP6dBlcjU3hM08SApxv/jmyGaCE8t4gO5BBhmHY4pDUfmr2g==} + engines: {node: '>=22'} + + '@scalar/openapi-upgrader@0.2.6': + resolution: {integrity: sha512-pvEmfSCDNYR4+lygidUqfo+shzyp4OSh9+UgK110rzA8Oot6WbJBM03Fuq3M255G7G6R9iXyfsebB7MBUocPkw==} + engines: {node: '>=22'} + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sinclair/typebox@0.34.49': resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -2051,6 +2365,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -2442,6 +2764,9 @@ packages: resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} engines: {node: '>= 6'} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -2674,6 +2999,14 @@ packages: resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -2728,6 +3061,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2861,6 +3199,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + exit-x@0.2.2: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} @@ -2890,6 +3232,10 @@ packages: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2927,6 +3273,10 @@ packages: feedsmith@2.9.3: resolution: {integrity: sha512-H2Dj/gax2p61HszgUdhORg4Wtpfz9wu6w6fhloEWovcx2xF9+QzzNJvHisn4Vr2yoozjQpH75pq2OLf9/JY8Gg==} + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2951,6 +3301,10 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + find-up@8.0.0: + resolution: {integrity: sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==} + engines: {node: '>=20'} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2993,6 +3347,10 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} @@ -3057,10 +3415,17 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + giget@3.2.0: resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} hasBin: true @@ -3101,6 +3466,10 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globby@16.1.0: + resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==} + engines: {node: '>=20'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3191,6 +3560,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -3341,6 +3714,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -3370,6 +3747,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -3386,6 +3767,10 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -3631,6 +4016,10 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3642,6 +4031,10 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + leven@4.1.0: + resolution: {integrity: sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3723,6 +4116,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + lint-staged@16.4.0: resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} engines: {node: '>=20.17'} @@ -3748,6 +4144,10 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + locate-path@8.0.0: + resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==} + engines: {node: '>=20'} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -3794,6 +4194,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -3810,6 +4213,10 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -3865,6 +4272,9 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4111,6 +4521,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -4180,6 +4594,16 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + orval@8.8.1: + resolution: {integrity: sha512-iOW+AoFd5SzGNqBx9gjedH+eS26UwmJ5897KOCLsZ8W9kCz70I8qUlk1rOEVq+EXrcQNyMEELSLEdXKxjUe2tQ==} + engines: {node: '>=22.18.0'} + hasBin: true + peerDependencies: + prettier: '>=3.0.0' + peerDependenciesMeta: + prettier: + optional: true + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -4192,6 +4616,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -4200,6 +4628,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -4218,6 +4650,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -4241,6 +4677,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -4439,6 +4879,10 @@ packages: resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + prisma@7.8.0: resolution: {integrity: sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==} engines: {node: ^20.19 || ^22.12 || >=24.0} @@ -4465,6 +4909,10 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4552,6 +5000,9 @@ packages: remeda@2.33.4: resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + remeda@2.33.7: + resolution: {integrity: sha512-cXlyjevWx5AcslOUEETG4o8XYi9UkoCXcJmj7XhPFVbla+ITuOBxv6ijBrmbeg+ZhzmDThkNdO+iXKUfrJep1w==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4572,6 +5023,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@2.0.0-next.6: resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} engines: {node: '>= 0.4'} @@ -4719,6 +5173,10 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} @@ -4851,6 +5309,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -4901,6 +5363,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-ui-dist@5.32.4: + resolution: {integrity: sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -5054,6 +5519,16 @@ packages: '@swc/wasm': optional: true + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + tsconfig-paths-webpack-plugin@4.2.0: resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} engines: {node: '>=10.13.0'} @@ -5065,6 +5540,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + turbo@2.9.6: resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} hasBin: true @@ -5119,6 +5599,25 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typedoc-plugin-coverage@4.0.3: + resolution: {integrity: sha512-baim3wyMkqpX7rBzL/6iZ7wzKJuSr9ffP16RHOsdTUNoHUZeXLIZHSUBtUhXmNHaUNRgfqdmKLBwyggbJjGdeQ==} + engines: {node: '>= 18'} + peerDependencies: + typedoc: 0.28.x + + typedoc-plugin-markdown@4.11.0: + resolution: {integrity: sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw==} + engines: {node: '>= 18'} + peerDependencies: + typedoc: 0.28.x + + typedoc@0.28.19: + resolution: {integrity: sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==} + engines: {node: '>= 18', pnpm: '>= 10'} + hasBin: true + peerDependencies: + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x + typescript-eslint@8.59.0: resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5136,6 +5635,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -5159,6 +5661,14 @@ packages: undici-types@7.25.0: resolution: {integrity: sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -5384,10 +5894,18 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.3: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zeptomatch@2.1.0: resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} @@ -5637,59 +6155,141 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@borewit/text-codec@0.2.2': {} + '@borewit/text-codec@0.2.2': {} + + '@colors/colors@1.5.0': + optional: true + + '@commander-js/extra-typings@14.0.0(commander@14.0.3)': + dependencies: + commander: 14.0.3 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@electric-sql/pglite-socket@0.1.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true - '@colors/colors@1.5.0': + '@esbuild/linux-ppc64@0.27.7': optional: true - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 + '@esbuild/linux-riscv64@0.27.7': + optional: true - '@csstools/color-helpers@5.1.0': {} + '@esbuild/linux-s390x@0.27.7': + optional: true - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@esbuild/linux-x64@0.27.7': + optional: true - '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/color-helpers': 5.1.0 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@esbuild/netbsd-arm64@0.27.7': + optional: true - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': - dependencies: - '@csstools/css-tokenizer': 3.0.4 + '@esbuild/netbsd-x64@0.27.7': + optional: true - '@csstools/css-tokenizer@3.0.4': {} + '@esbuild/openbsd-arm64@0.27.7': + optional: true - '@electric-sql/pglite-socket@0.1.1(@electric-sql/pglite@0.4.1)': - dependencies: - '@electric-sql/pglite': 0.4.1 + '@esbuild/openbsd-x64@0.27.7': + optional: true - '@electric-sql/pglite-tools@0.3.1(@electric-sql/pglite@0.4.1)': - dependencies: - '@electric-sql/pglite': 0.4.1 + '@esbuild/openharmony-arm64@0.27.7': + optional: true - '@electric-sql/pglite@0.4.1': {} + '@esbuild/sunos-x64@0.27.7': + optional: true - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 + '@esbuild/win32-arm64@0.27.7': optional: true - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 + '@esbuild/win32-ia32@0.27.7': optional: true - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 + '@esbuild/win32-x64@0.27.7': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': @@ -5743,6 +6343,14 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@gerrit0/mini-shiki@3.23.0': + dependencies: + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@hono/node-server@1.19.11(hono@4.12.15)': dependencies: hono: 4.12.15 @@ -6240,6 +6848,8 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.16.0': {} + '@mixmark-io/domino@2.2.0': {} '@mozilla/readability@0.6.0': {} @@ -6312,6 +6922,11 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + '@nestjs/platform-express@11.1.19(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)': dependencies: '@nestjs/common': 11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -6350,6 +6965,18 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/swagger@11.4.1(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.4 + '@nestjs/testing@11.1.19(@nestjs/common@11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-express@11.1.19)': dependencies: '@nestjs/common': 11.1.19(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -6408,6 +7035,115 @@ snapshots: dependencies: consola: 3.4.2 + '@orval/angular@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/axios@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/core@8.8.1(typescript@6.0.3)': + dependencies: + '@scalar/openapi-types': 0.6.1 + acorn: 8.16.0 + compare-versions: 6.1.1 + debug: 4.4.3 + esbuild: 0.27.7 + esutils: 2.0.3 + fs-extra: 11.3.4 + globby: 16.1.0 + jiti: 2.6.1 + remeda: 2.33.7 + typedoc: 0.28.19(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@orval/fetch@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + '@scalar/openapi-types': 0.6.1 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/hono@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + '@orval/zod': 8.8.1(typescript@6.0.3) + fs-extra: 11.3.4 + remeda: 2.33.7 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/mcp@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + '@orval/fetch': 8.8.1(typescript@6.0.3) + '@orval/zod': 8.8.1(typescript@6.0.3) + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/mock@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + remeda: 2.33.7 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/query@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + '@orval/fetch': 8.8.1(typescript@6.0.3) + remeda: 2.33.7 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/solid-start@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + '@scalar/openapi-types': 0.6.1 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/swr@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + '@orval/fetch': 8.8.1(typescript@6.0.3) + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + + '@orval/zod@8.8.1(typescript@6.0.3)': + dependencies: + '@orval/core': 8.8.1(typescript@6.0.3) + remeda: 2.33.7 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -6648,8 +7384,65 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@scalar/helpers@0.5.2': {} + + '@scalar/json-magic@0.12.8': + dependencies: + '@scalar/helpers': 0.5.2 + pathe: 2.0.3 + yaml: 2.8.3 + + '@scalar/openapi-parser@0.25.12': + dependencies: + '@scalar/helpers': 0.5.2 + '@scalar/json-magic': 0.12.8 + '@scalar/openapi-types': 0.8.0 + '@scalar/openapi-upgrader': 0.2.6 + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-formats: 3.0.1(ajv@8.18.0) + jsonpointer: 5.0.1 + leven: 4.1.0 + yaml: 2.8.3 + + '@scalar/openapi-types@0.6.1': + dependencies: + zod: 4.3.6 + + '@scalar/openapi-types@0.8.0': {} + + '@scalar/openapi-upgrader@0.2.6': + dependencies: + '@scalar/openapi-types': 0.8.0 + + '@scarf/scarf@1.4.0': {} + + '@sec-ant/readable-stream@0.4.1': {} + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sinclair/typebox@0.34.49': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -7198,6 +7991,10 @@ snapshots: agent-base@7.1.4: {} + ajv-draft-04@1.0.0(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -7614,6 +8411,8 @@ snapshots: array-timsort: 1.0.3 esprima: 4.0.1 + compare-versions@6.1.1: {} + component-emitter@1.3.1: {} concat-map@0.0.1: {} @@ -7804,6 +8603,13 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + entities@4.5.0: {} + entities@6.0.1: {} entities@7.0.1: {} @@ -7919,6 +8725,35 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -8088,6 +8923,21 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + exit-x@0.2.2: {} expect@30.3.0: @@ -8150,6 +9000,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -8186,6 +9044,10 @@ snapshots: entities: 7.0.1 fast-xml-parser: 5.7.1 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -8224,6 +9086,11 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + find-up@8.0.0: + dependencies: + locate-path: 8.0.0 + unicorn-magic: 0.3.0 + flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -8281,6 +9148,12 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-monkey@1.1.0: {} fs.realpath@1.0.0: {} @@ -8340,12 +9213,21 @@ snapshots: get-stream@6.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + giget@3.2.0: {} glob-parent@5.1.2: @@ -8391,6 +9273,15 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + globby@16.1.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + is-path-inside: 4.0.0 + slash: 5.1.0 + unicorn-magic: 0.4.0 + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -8496,6 +9387,8 @@ snapshots: human-signals@2.1.0: {} + human-signals@8.0.1: {} + husky@9.1.7: {} iconv-lite@0.6.3: @@ -8633,6 +9526,8 @@ snapshots: is-number@7.0.0: {} + is-path-inside@4.0.0: {} + is-plain-obj@4.1.0: {} is-potential-custom-element-name@1.0.1: {} @@ -8656,6 +9551,8 @@ snapshots: is-stream@2.0.1: {} + is-stream@4.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -8673,6 +9570,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -9125,6 +10024,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpointer@5.0.1: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -9138,6 +10039,8 @@ snapshots: leven@3.1.0: {} + leven@4.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -9194,6 +10097,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + lint-staged@16.4.0: dependencies: commander: 14.0.3 @@ -9224,6 +10131,10 @@ snapshots: dependencies: p-locate: 5.0.0 + locate-path@8.0.0: + dependencies: + p-locate: 6.0.0 + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} @@ -9265,6 +10176,8 @@ snapshots: dependencies: react: 19.2.5 + lunr@2.3.9: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -9283,6 +10196,15 @@ snapshots: dependencies: tmpl: 1.0.5 + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} math-intrinsics@1.1.0: {} @@ -9442,6 +10364,8 @@ snapshots: mdn-data@2.27.1: {} + mdurl@2.0.0: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -9725,7 +10649,7 @@ snapshots: neo-async@2.6.2: {} - next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@16.2.4(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 16.2.4 '@swc/helpers': 0.5.15 @@ -9734,7 +10658,7 @@ snapshots: postcss: 8.4.31 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5) + styled-jsx: 5.1.6(react@19.2.5) optionalDependencies: '@next/swc-darwin-arm64': 16.2.4 '@next/swc-darwin-x64': 16.2.4 @@ -9773,6 +10697,11 @@ snapshots: dependencies: path-key: 3.1.1 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nwsapi@2.2.23: {} object-assign@4.1.1: {} @@ -9855,6 +10784,44 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + orval@8.8.1(prettier@3.8.3)(typescript@6.0.3): + dependencies: + '@commander-js/extra-typings': 14.0.0(commander@14.0.3) + '@orval/angular': 8.8.1(typescript@6.0.3) + '@orval/axios': 8.8.1(typescript@6.0.3) + '@orval/core': 8.8.1(typescript@6.0.3) + '@orval/fetch': 8.8.1(typescript@6.0.3) + '@orval/hono': 8.8.1(typescript@6.0.3) + '@orval/mcp': 8.8.1(typescript@6.0.3) + '@orval/mock': 8.8.1(typescript@6.0.3) + '@orval/query': 8.8.1(typescript@6.0.3) + '@orval/solid-start': 8.8.1(typescript@6.0.3) + '@orval/swr': 8.8.1(typescript@6.0.3) + '@orval/zod': 8.8.1(typescript@6.0.3) + '@scalar/json-magic': 0.12.8 + '@scalar/openapi-parser': 0.25.12 + '@scalar/openapi-types': 0.6.1 + chokidar: 5.0.0 + commander: 14.0.3 + enquirer: 2.4.1 + execa: 9.6.1 + find-up: 8.0.0 + fs-extra: 11.3.4 + jiti: 2.6.1 + js-yaml: 4.1.1 + remeda: 2.33.7 + string-argv: 0.3.2 + tsconfck: 3.1.6(typescript@6.0.3) + typedoc: 0.28.19(typescript@6.0.3) + typedoc-plugin-coverage: 4.0.3(typedoc@0.28.19(typescript@6.0.3)) + typedoc-plugin-markdown: 4.11.0(typedoc@0.28.19(typescript@6.0.3)) + optionalDependencies: + prettier: 3.8.3 + transitivePeerDependencies: + - '@faker-js/faker' + - supports-color + - typescript + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -9869,6 +10836,10 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -9877,6 +10848,10 @@ snapshots: dependencies: p-limit: 3.1.0 + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -9902,6 +10877,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-ms@4.0.0: {} + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -9916,6 +10893,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -10041,6 +11020,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + prisma@7.8.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3): dependencies: '@prisma/config': 7.8.0 @@ -10077,6 +11060,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + punycode.js@2.3.1: {} + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -10200,6 +11185,8 @@ snapshots: remeda@2.33.4: {} + remeda@2.33.7: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -10212,6 +11199,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@2.0.0-next.6: dependencies: es-errors: 1.3.0 @@ -10430,6 +11419,8 @@ snapshots: slash@3.0.0: {} + slash@5.1.0: {} + slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -10578,6 +11569,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@3.1.1: {} strnum@2.2.3: {} @@ -10594,12 +11587,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5): + styled-jsx@5.1.6(react@19.2.5): dependencies: client-only: 0.0.1 react: 19.2.5 - optionalDependencies: - '@babel/core': 7.29.0 superagent@10.3.0: dependencies: @@ -10633,6 +11624,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swagger-ui-dist@5.32.4: + dependencies: + '@scarf/scarf': 1.4.0 + symbol-observable@4.0.0: {} symbol-tree@3.2.4: {} @@ -10761,6 +11756,10 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tsconfck@3.1.6(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 @@ -10776,6 +11775,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + turbo@2.9.6: optionalDependencies: '@turbo/darwin-64': 2.9.6 @@ -10847,6 +11853,23 @@ snapshots: typedarray@0.0.6: {} + typedoc-plugin-coverage@4.0.3(typedoc@0.28.19(typescript@6.0.3)): + dependencies: + typedoc: 0.28.19(typescript@6.0.3) + + typedoc-plugin-markdown@4.11.0(typedoc@0.28.19(typescript@6.0.3)): + dependencies: + typedoc: 0.28.19(typescript@6.0.3) + + typedoc@0.28.19(typescript@6.0.3): + dependencies: + '@gerrit0/mini-shiki': 3.23.0 + lunr: 2.3.9 + markdown-it: 14.1.1 + minimatch: 10.2.5 + typescript: 6.0.3 + yaml: 2.8.3 + typescript-eslint@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3): dependencies: '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.3) @@ -10862,6 +11885,8 @@ snapshots: typescript@6.0.3: {} + uc.micro@2.1.0: {} + uglify-js@3.19.3: optional: true @@ -10882,6 +11907,10 @@ snapshots: undici-types@7.25.0: {} + unicorn-magic@0.3.0: {} + + unicorn-magic@0.4.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -11159,8 +12188,12 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + yoctocolors-cjs@2.1.3: {} + yoctocolors@2.1.2: {} + zeptomatch@2.1.0: dependencies: grammex: 3.1.12 diff --git a/turbo.json b/turbo.json index b2cc252..cb4352a 100644 --- a/turbo.json +++ b/turbo.json @@ -34,6 +34,9 @@ "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, + "contract:refresh": { + "cache": false + }, "lint": { "dependsOn": ["^build", "^lint"], "outputs": []