@lpdjs/firestore-repo-service 2.6.2 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,14 +26,14 @@ import { z, ZodError } from 'zod';
26
26
  *
27
27
  * @example
28
28
  * ```ts
29
- * // src/services.ts
29
+ * // src/services.ts — infrastructure singletons only (no useCases here)
30
30
  * import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
31
31
  * import { PostRepo } from "./domains/posts/PostRepo.js";
32
- * import { CreatePostUseCase } from "./domains/posts/useCases/createPost/useCase.js";
32
+ * import { Mailer } from "./services/Mailer.js";
33
33
  *
34
34
  * export const services = createServices({
35
- * postRepo: PostRepo, // class form (auto-injected)
36
- * createPostUseCase: CreatePostUseCase, // class form (auto-injected)
35
+ * postRepo: PostRepo, // class form (auto-injected)
36
+ * mailer: ({ ctx }) => new Mailer(ctx), // factory form
37
37
  * });
38
38
  *
39
39
  * export type Services = typeof services;
@@ -51,15 +51,21 @@ import { z, ZodError } from 'zod';
51
51
  *
52
52
  * @example
53
53
  * ```ts
54
- * // Inside a useCase registered as a class
54
+ * // Inside a useCase extends the UseCase base, owns its Zod schemas
55
+ * import { z } from "zod";
56
+ * import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
55
57
  * import type { Services } from "../../services.js";
56
58
  *
57
- * export class CreatePostUseCase {
58
- * constructor(private readonly services: Services) {}
59
+ * const input = z.object({ title: z.string() });
60
+ * const output = z.object({ id: z.string() });
61
+ *
62
+ * export class CreatePostUseCase extends UseCase<typeof input, typeof output, Services> {
63
+ * static readonly input = input;
64
+ * static readonly output = output;
59
65
  *
60
- * async execute(input: { title: string }) {
66
+ * async execute(payload: z.infer<typeof input>): Promise<z.infer<typeof output>> {
61
67
  * const user = this.services.ctx.c.get("user");
62
- * return this.services.postRepo.create({ ...input, authorId: user.id });
68
+ * return this.services.postRepo.create({ ...payload, authorId: user.id });
63
69
  * }
64
70
  * }
65
71
  * ```
@@ -169,14 +175,14 @@ type ServicesContainer<TMap> = TMap;
169
175
  *
170
176
  * @example Class form (zero-boilerplate auto-injection)
171
177
  * ```ts
172
- * class CreatePostUseCase {
178
+ * class RepositoryService {
173
179
  * constructor(private readonly services: Services) {}
174
- * async execute() { return this.services.postRepo.list(); }
180
+ * get posts() { return this.services.db.posts; }
175
181
  * }
176
182
  *
177
183
  * createServices({
178
- * postRepo: PostRepo, // ← bare class
179
- * createPostUseCase: CreatePostUseCase, // ← bare class
184
+ * db: () => getFirestore(), // ← factory
185
+ * repository: RepositoryService, // ← bare class (deps auto-injected)
180
186
  * });
181
187
  * ```
182
188
  */
@@ -268,9 +274,51 @@ type AnyRouteDef = RouteDef<any, any>;
268
274
  * What `routes.ts` can default-export:
269
275
  * - a single {@link RouteDef} (most common case),
270
276
  * - or an array of {@link RouteDef} (e.g. expose the same useCase on
271
- * multiple `api` tags or under multiple paths).
277
+ * multiple `api` tags or under multiple paths). `readonly` arrays are
278
+ * accepted so that tuple-preserving helpers like {@link defineRoutes} (or a
279
+ * plain `as const` array) can be default-exported directly.
280
+ */
281
+ type RouteModuleDefault = AnyRouteDef | readonly AnyRouteDef[];
282
+ /**
283
+ * Identity helper that **preserves the tuple type** of a `routes.ts` default
284
+ * export. A plain array literal (`export default [a, b]`) collapses every
285
+ * element to a single common type, which would lose each route's individual
286
+ * schema. Wrapping the array with `defineRoutes([...])` keeps the per-route
287
+ * types intact so {@link RouteInput} / {@link RouteOutput} can aggregate them
288
+ * into a union.
289
+ *
290
+ * @example
291
+ * export default defineRoutes([
292
+ * defineRoute({ ... }),
293
+ * defineRoute({ ... }),
294
+ * ]);
295
+ */
296
+ declare function defineRoutes<const T extends readonly AnyRouteDef[]>(routes: T): T;
297
+ /**
298
+ * Normalizes a `routes.ts` default export — a single {@link RouteDef} or an
299
+ * array of them — to a *union* of its individual route members.
272
300
  */
273
- type RouteModuleDefault = AnyRouteDef | AnyRouteDef[];
301
+ type RoutesUnion<T> = T extends readonly (infer R)[] ? R : T;
302
+ type InferRouteInput<R> = R extends {
303
+ input?: infer I;
304
+ } ? I extends z.ZodTypeAny ? z.infer<I> : void : void;
305
+ type InferRouteOutput<R> = R extends {
306
+ output?: infer O;
307
+ } ? O extends z.ZodTypeAny ? z.infer<O> : unknown : unknown;
308
+ /**
309
+ * Union of validated request payloads across every route in a `routes.ts`
310
+ * module. Use it to type a useCase input without duplicating the Zod schema:
311
+ *
312
+ * @example
313
+ * type Routes = typeof import("./routes.js").default;
314
+ * export type CreatePostUseCaseInput = RouteInput<Routes>;
315
+ */
316
+ type RouteInput<T> = InferRouteInput<RoutesUnion<T>>;
317
+ /**
318
+ * Union of success-response payloads across every route in a `routes.ts`
319
+ * module. Mirror of {@link RouteInput} for the useCase output type.
320
+ */
321
+ type RouteOutput<T> = InferRouteOutput<RoutesUnion<T>>;
274
322
  /**
275
323
  * Thrown by the server when the incoming request fails Zod validation.
276
324
  * Caught by the {@link RouteInterceptor} (if any) so users can shape the
@@ -447,6 +495,102 @@ declare class HonoServer<TEnv extends Env = Env> {
447
495
  private mountOpenApi;
448
496
  }
449
497
 
498
+ /**
499
+ * useCase ⇆ route bridge.
500
+ *
501
+ * A useCase owns its Zod `input` / `output` schemas (declared as `static`
502
+ * members) and the business logic in {@link UseCase.execute}. Routes never
503
+ * re-declare those schemas: they wire a useCase into an HTTP endpoint with the
504
+ * one-liner {@link ApiRegistry.useCaseRoute} (or the standalone
505
+ * {@link useCaseRoute}), keeping `routes.ts` flat and readable while the
506
+ * types can never drift from the schemas.
507
+ *
508
+ * @example
509
+ * ```ts
510
+ * // useCase.ts — single source of truth for the I/O shape
511
+ * import { z } from "zod";
512
+ * import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
513
+ * import type { Services } from "../../../../services.js";
514
+ *
515
+ * const input = z.object({ example: z.string() });
516
+ * const output = z.object({ id: z.string(), warning: z.string().nullable() });
517
+ *
518
+ * export class CreatePostUseCase extends UseCase<typeof input, typeof output, Services> {
519
+ * static readonly input = input;
520
+ * static readonly output = output;
521
+ *
522
+ * async execute(payload: z.infer<typeof input>): Promise<z.infer<typeof output>> {
523
+ * return { id: payload.example, warning: null };
524
+ * }
525
+ * }
526
+ *
527
+ * // routes.ts — one line per route, `api` stays typed
528
+ * export default defineRoutes([
529
+ * useCaseRoute(CreatePostUseCase, { api: "v1", method: "post", tags: ["posts"] }),
530
+ * ]);
531
+ * ```
532
+ */
533
+
534
+ /**
535
+ * Base class for every useCase — pure business logic, no HTTP awareness.
536
+ * The shared {@link AnyServicesContainer} is injected via the constructor; the
537
+ * `input` / `output` Zod schemas are declared as `static` members on the
538
+ * concrete subclass (see {@link UseCaseClass}).
539
+ *
540
+ * @typeParam TInput Zod schema of the validated request payload.
541
+ * @typeParam TOutput Zod schema of the success response.
542
+ * @typeParam TServices Concrete services container injected into the useCase.
543
+ */
544
+ declare abstract class UseCase<TInput extends z.ZodTypeAny = z.ZodTypeAny, TOutput extends z.ZodTypeAny = z.ZodTypeAny, TServices extends AnyServicesContainer = AnyServicesContainer> {
545
+ protected readonly services: TServices;
546
+ constructor(services: TServices);
547
+ /** Run the business logic. Input is already validated against the schema. */
548
+ abstract execute(input: z.infer<TInput>): Promise<z.infer<TOutput>>;
549
+ }
550
+ /**
551
+ * Structural type of a concrete {@link UseCase} subclass — i.e. a constructor
552
+ * that also exposes the `static input` / `static output` schemas. Consumed by
553
+ * {@link useCaseRoute} to derive the route's Zod schemas and the handler types.
554
+ */
555
+ interface UseCaseClass<TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny, TServices extends AnyServicesContainer = AnyServicesContainer> {
556
+ readonly input: TInput;
557
+ readonly output: TOutput;
558
+ new (services: TServices): UseCase<TInput, TOutput, TServices>;
559
+ }
560
+ /**
561
+ * HTTP metadata for {@link useCaseRoute} — everything a route needs **except**
562
+ * the input/output schemas and the handler, which are derived from the useCase.
563
+ */
564
+ interface UseCaseRouteMeta<TApi extends string = string> {
565
+ /** API tag the route is mounted under. */
566
+ api: TApi;
567
+ /** HTTP method. */
568
+ method: HttpMethod;
569
+ /** URL path. Defaults to the codegen-derived path when omitted. */
570
+ path?: string;
571
+ /** Where the payload comes from. Defaults per method (see {@link RouteDef}). */
572
+ source?: PayloadSource;
573
+ /** Success status code. Default: 200. */
574
+ status?: number;
575
+ summary?: string;
576
+ description?: string;
577
+ tags?: string[];
578
+ deprecated?: boolean;
579
+ security?: Array<Record<string, string[]>>;
580
+ }
581
+ /**
582
+ * Build a {@link RouteDef} from a useCase class and HTTP metadata. The route's
583
+ * `input` / `output` schemas are read from the useCase's `static` members and
584
+ * the handler instantiates the useCase with the request `services` and runs
585
+ * {@link UseCase.execute}.
586
+ *
587
+ * Prefer the registry-bound `apis.useCaseRoute` (returned by
588
+ * `createApiRegistry`) so that `meta.api` is narrowed to the registered tags.
589
+ */
590
+ declare function useCaseRoute<TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny, TServices extends AnyServicesContainer, TApi extends string = string>(useCaseClass: UseCaseClass<TInput, TOutput, TServices>, meta: UseCaseRouteMeta<TApi>): RouteDef<TInput, TOutput> & {
591
+ api: TApi;
592
+ };
593
+
450
594
  /**
451
595
  * Typed multi-API registry.
452
596
  *
@@ -475,6 +619,7 @@ declare class HonoServer<TEnv extends Env = Env> {
475
619
  *
476
620
  * // Use in routes — `api` is now typed "v1" | "webhooks".
477
621
  * export const defineRoute = apis.defineRoute;
622
+ * export const useCaseRoute = apis.useCaseRoute;
478
623
  *
479
624
  * // index.ts (Cloud Functions entrypoint)
480
625
  * import { onRequest } from "firebase-functions/v2/https";
@@ -527,6 +672,20 @@ interface ApiRegistry<TMap extends ApiConfigMap, TServices extends AnyServicesCo
527
672
  }): RouteDef<TIn, TOut> & {
528
673
  api: keyof TMap & string;
529
674
  };
675
+ /**
676
+ * Typed `useCaseRoute` — wires a {@link UseCase} class into a route in one
677
+ * line. The route's `input` / `output` schemas are read from the useCase's
678
+ * `static` members and the handler instantiates it with the request
679
+ * `services`. The `api` field is constrained to `keyof TMap`.
680
+ *
681
+ * @example
682
+ * export default defineRoutes([
683
+ * useCaseRoute(CreatePostUseCase, { api: "v1", method: "post", tags: ["posts"] }),
684
+ * ]);
685
+ */
686
+ useCaseRoute<TIn extends z.ZodTypeAny, TOut extends z.ZodTypeAny>(useCaseClass: UseCaseClass<TIn, TOut, TServices>, meta: UseCaseRouteMeta<keyof TMap & string>): RouteDef<TIn, TOut> & {
687
+ api: keyof TMap & string;
688
+ };
530
689
  /**
531
690
  * Build one Cloud Function per registered API and return them as a map
532
691
  * keyed by API tag — spread it directly into your `index.ts` exports.
@@ -669,4 +828,4 @@ declare function generateRoutesManifest(routes: ScannedRoute[], opts: GeneratorO
669
828
  /** Convenience helper used by the CLI — combines scan + generate in one call. */
670
829
  declare function generateFromRoot(rootAbs: string, outFileRel: string, derive: PathDeriveOptions, importExtension: string, scan: (root: string) => ScannedRoute[]): GenerationResult;
671
830
 
672
- export { type AnyRouteDef, type AnyServicesContainer, type ApiConfig, type ApiConfigMap, type ApiRegistry, type ApiRegistryOptions, DEFAULT_DERIVE, DEFAULT_GENERATOR_BANNER, DEFAULT_SCANNER, type GenerationResult, type GeneratorOptions, HonoServer, type HonoServerOptions, type HttpMethod, type OpenAPIConfig, type OpenAPIInfo, type PathDeriveOptions, type PayloadSource, type RequestContext, type RouteDef, type RouteHandler, type RouteInterceptor, type RouteModuleDefault, type ScannedRoute, type ScannerOptions, type ServiceProvider, type ServiceProviderMap, type ServicesContainer, type ServicesOf, ValidationError, buildOpenApiDocument, createApiRegistry, createRequestContextMiddleware, createServices, derivePath, generateFromRoot, generateRoutesManifest, renderDocsHtml, scanRoutes, toImportSpecifier, withRequestContext };
831
+ export { type AnyRouteDef, type AnyServicesContainer, type ApiConfig, type ApiConfigMap, type ApiRegistry, type ApiRegistryOptions, DEFAULT_DERIVE, DEFAULT_GENERATOR_BANNER, DEFAULT_SCANNER, type GenerationResult, type GeneratorOptions, HonoServer, type HonoServerOptions, type HttpMethod, type OpenAPIConfig, type OpenAPIInfo, type PathDeriveOptions, type PayloadSource, type RequestContext, type RouteDef, type RouteHandler, type RouteInput, type RouteInterceptor, type RouteModuleDefault, type RouteOutput, type RoutesUnion, type ScannedRoute, type ScannerOptions, type ServiceProvider, type ServiceProviderMap, type ServicesContainer, type ServicesOf, UseCase, type UseCaseClass, type UseCaseRouteMeta, ValidationError, buildOpenApiDocument, createApiRegistry, createRequestContextMiddleware, createServices, defineRoutes, derivePath, generateFromRoot, generateRoutesManifest, renderDocsHtml, scanRoutes, toImportSpecifier, useCaseRoute, withRequestContext };
@@ -26,14 +26,14 @@ import { z, ZodError } from 'zod';
26
26
  *
27
27
  * @example
28
28
  * ```ts
29
- * // src/services.ts
29
+ * // src/services.ts — infrastructure singletons only (no useCases here)
30
30
  * import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
31
31
  * import { PostRepo } from "./domains/posts/PostRepo.js";
32
- * import { CreatePostUseCase } from "./domains/posts/useCases/createPost/useCase.js";
32
+ * import { Mailer } from "./services/Mailer.js";
33
33
  *
34
34
  * export const services = createServices({
35
- * postRepo: PostRepo, // class form (auto-injected)
36
- * createPostUseCase: CreatePostUseCase, // class form (auto-injected)
35
+ * postRepo: PostRepo, // class form (auto-injected)
36
+ * mailer: ({ ctx }) => new Mailer(ctx), // factory form
37
37
  * });
38
38
  *
39
39
  * export type Services = typeof services;
@@ -51,15 +51,21 @@ import { z, ZodError } from 'zod';
51
51
  *
52
52
  * @example
53
53
  * ```ts
54
- * // Inside a useCase registered as a class
54
+ * // Inside a useCase extends the UseCase base, owns its Zod schemas
55
+ * import { z } from "zod";
56
+ * import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
55
57
  * import type { Services } from "../../services.js";
56
58
  *
57
- * export class CreatePostUseCase {
58
- * constructor(private readonly services: Services) {}
59
+ * const input = z.object({ title: z.string() });
60
+ * const output = z.object({ id: z.string() });
61
+ *
62
+ * export class CreatePostUseCase extends UseCase<typeof input, typeof output, Services> {
63
+ * static readonly input = input;
64
+ * static readonly output = output;
59
65
  *
60
- * async execute(input: { title: string }) {
66
+ * async execute(payload: z.infer<typeof input>): Promise<z.infer<typeof output>> {
61
67
  * const user = this.services.ctx.c.get("user");
62
- * return this.services.postRepo.create({ ...input, authorId: user.id });
68
+ * return this.services.postRepo.create({ ...payload, authorId: user.id });
63
69
  * }
64
70
  * }
65
71
  * ```
@@ -169,14 +175,14 @@ type ServicesContainer<TMap> = TMap;
169
175
  *
170
176
  * @example Class form (zero-boilerplate auto-injection)
171
177
  * ```ts
172
- * class CreatePostUseCase {
178
+ * class RepositoryService {
173
179
  * constructor(private readonly services: Services) {}
174
- * async execute() { return this.services.postRepo.list(); }
180
+ * get posts() { return this.services.db.posts; }
175
181
  * }
176
182
  *
177
183
  * createServices({
178
- * postRepo: PostRepo, // ← bare class
179
- * createPostUseCase: CreatePostUseCase, // ← bare class
184
+ * db: () => getFirestore(), // ← factory
185
+ * repository: RepositoryService, // ← bare class (deps auto-injected)
180
186
  * });
181
187
  * ```
182
188
  */
@@ -268,9 +274,51 @@ type AnyRouteDef = RouteDef<any, any>;
268
274
  * What `routes.ts` can default-export:
269
275
  * - a single {@link RouteDef} (most common case),
270
276
  * - or an array of {@link RouteDef} (e.g. expose the same useCase on
271
- * multiple `api` tags or under multiple paths).
277
+ * multiple `api` tags or under multiple paths). `readonly` arrays are
278
+ * accepted so that tuple-preserving helpers like {@link defineRoutes} (or a
279
+ * plain `as const` array) can be default-exported directly.
280
+ */
281
+ type RouteModuleDefault = AnyRouteDef | readonly AnyRouteDef[];
282
+ /**
283
+ * Identity helper that **preserves the tuple type** of a `routes.ts` default
284
+ * export. A plain array literal (`export default [a, b]`) collapses every
285
+ * element to a single common type, which would lose each route's individual
286
+ * schema. Wrapping the array with `defineRoutes([...])` keeps the per-route
287
+ * types intact so {@link RouteInput} / {@link RouteOutput} can aggregate them
288
+ * into a union.
289
+ *
290
+ * @example
291
+ * export default defineRoutes([
292
+ * defineRoute({ ... }),
293
+ * defineRoute({ ... }),
294
+ * ]);
295
+ */
296
+ declare function defineRoutes<const T extends readonly AnyRouteDef[]>(routes: T): T;
297
+ /**
298
+ * Normalizes a `routes.ts` default export — a single {@link RouteDef} or an
299
+ * array of them — to a *union* of its individual route members.
272
300
  */
273
- type RouteModuleDefault = AnyRouteDef | AnyRouteDef[];
301
+ type RoutesUnion<T> = T extends readonly (infer R)[] ? R : T;
302
+ type InferRouteInput<R> = R extends {
303
+ input?: infer I;
304
+ } ? I extends z.ZodTypeAny ? z.infer<I> : void : void;
305
+ type InferRouteOutput<R> = R extends {
306
+ output?: infer O;
307
+ } ? O extends z.ZodTypeAny ? z.infer<O> : unknown : unknown;
308
+ /**
309
+ * Union of validated request payloads across every route in a `routes.ts`
310
+ * module. Use it to type a useCase input without duplicating the Zod schema:
311
+ *
312
+ * @example
313
+ * type Routes = typeof import("./routes.js").default;
314
+ * export type CreatePostUseCaseInput = RouteInput<Routes>;
315
+ */
316
+ type RouteInput<T> = InferRouteInput<RoutesUnion<T>>;
317
+ /**
318
+ * Union of success-response payloads across every route in a `routes.ts`
319
+ * module. Mirror of {@link RouteInput} for the useCase output type.
320
+ */
321
+ type RouteOutput<T> = InferRouteOutput<RoutesUnion<T>>;
274
322
  /**
275
323
  * Thrown by the server when the incoming request fails Zod validation.
276
324
  * Caught by the {@link RouteInterceptor} (if any) so users can shape the
@@ -447,6 +495,102 @@ declare class HonoServer<TEnv extends Env = Env> {
447
495
  private mountOpenApi;
448
496
  }
449
497
 
498
+ /**
499
+ * useCase ⇆ route bridge.
500
+ *
501
+ * A useCase owns its Zod `input` / `output` schemas (declared as `static`
502
+ * members) and the business logic in {@link UseCase.execute}. Routes never
503
+ * re-declare those schemas: they wire a useCase into an HTTP endpoint with the
504
+ * one-liner {@link ApiRegistry.useCaseRoute} (or the standalone
505
+ * {@link useCaseRoute}), keeping `routes.ts` flat and readable while the
506
+ * types can never drift from the schemas.
507
+ *
508
+ * @example
509
+ * ```ts
510
+ * // useCase.ts — single source of truth for the I/O shape
511
+ * import { z } from "zod";
512
+ * import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
513
+ * import type { Services } from "../../../../services.js";
514
+ *
515
+ * const input = z.object({ example: z.string() });
516
+ * const output = z.object({ id: z.string(), warning: z.string().nullable() });
517
+ *
518
+ * export class CreatePostUseCase extends UseCase<typeof input, typeof output, Services> {
519
+ * static readonly input = input;
520
+ * static readonly output = output;
521
+ *
522
+ * async execute(payload: z.infer<typeof input>): Promise<z.infer<typeof output>> {
523
+ * return { id: payload.example, warning: null };
524
+ * }
525
+ * }
526
+ *
527
+ * // routes.ts — one line per route, `api` stays typed
528
+ * export default defineRoutes([
529
+ * useCaseRoute(CreatePostUseCase, { api: "v1", method: "post", tags: ["posts"] }),
530
+ * ]);
531
+ * ```
532
+ */
533
+
534
+ /**
535
+ * Base class for every useCase — pure business logic, no HTTP awareness.
536
+ * The shared {@link AnyServicesContainer} is injected via the constructor; the
537
+ * `input` / `output` Zod schemas are declared as `static` members on the
538
+ * concrete subclass (see {@link UseCaseClass}).
539
+ *
540
+ * @typeParam TInput Zod schema of the validated request payload.
541
+ * @typeParam TOutput Zod schema of the success response.
542
+ * @typeParam TServices Concrete services container injected into the useCase.
543
+ */
544
+ declare abstract class UseCase<TInput extends z.ZodTypeAny = z.ZodTypeAny, TOutput extends z.ZodTypeAny = z.ZodTypeAny, TServices extends AnyServicesContainer = AnyServicesContainer> {
545
+ protected readonly services: TServices;
546
+ constructor(services: TServices);
547
+ /** Run the business logic. Input is already validated against the schema. */
548
+ abstract execute(input: z.infer<TInput>): Promise<z.infer<TOutput>>;
549
+ }
550
+ /**
551
+ * Structural type of a concrete {@link UseCase} subclass — i.e. a constructor
552
+ * that also exposes the `static input` / `static output` schemas. Consumed by
553
+ * {@link useCaseRoute} to derive the route's Zod schemas and the handler types.
554
+ */
555
+ interface UseCaseClass<TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny, TServices extends AnyServicesContainer = AnyServicesContainer> {
556
+ readonly input: TInput;
557
+ readonly output: TOutput;
558
+ new (services: TServices): UseCase<TInput, TOutput, TServices>;
559
+ }
560
+ /**
561
+ * HTTP metadata for {@link useCaseRoute} — everything a route needs **except**
562
+ * the input/output schemas and the handler, which are derived from the useCase.
563
+ */
564
+ interface UseCaseRouteMeta<TApi extends string = string> {
565
+ /** API tag the route is mounted under. */
566
+ api: TApi;
567
+ /** HTTP method. */
568
+ method: HttpMethod;
569
+ /** URL path. Defaults to the codegen-derived path when omitted. */
570
+ path?: string;
571
+ /** Where the payload comes from. Defaults per method (see {@link RouteDef}). */
572
+ source?: PayloadSource;
573
+ /** Success status code. Default: 200. */
574
+ status?: number;
575
+ summary?: string;
576
+ description?: string;
577
+ tags?: string[];
578
+ deprecated?: boolean;
579
+ security?: Array<Record<string, string[]>>;
580
+ }
581
+ /**
582
+ * Build a {@link RouteDef} from a useCase class and HTTP metadata. The route's
583
+ * `input` / `output` schemas are read from the useCase's `static` members and
584
+ * the handler instantiates the useCase with the request `services` and runs
585
+ * {@link UseCase.execute}.
586
+ *
587
+ * Prefer the registry-bound `apis.useCaseRoute` (returned by
588
+ * `createApiRegistry`) so that `meta.api` is narrowed to the registered tags.
589
+ */
590
+ declare function useCaseRoute<TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny, TServices extends AnyServicesContainer, TApi extends string = string>(useCaseClass: UseCaseClass<TInput, TOutput, TServices>, meta: UseCaseRouteMeta<TApi>): RouteDef<TInput, TOutput> & {
591
+ api: TApi;
592
+ };
593
+
450
594
  /**
451
595
  * Typed multi-API registry.
452
596
  *
@@ -475,6 +619,7 @@ declare class HonoServer<TEnv extends Env = Env> {
475
619
  *
476
620
  * // Use in routes — `api` is now typed "v1" | "webhooks".
477
621
  * export const defineRoute = apis.defineRoute;
622
+ * export const useCaseRoute = apis.useCaseRoute;
478
623
  *
479
624
  * // index.ts (Cloud Functions entrypoint)
480
625
  * import { onRequest } from "firebase-functions/v2/https";
@@ -527,6 +672,20 @@ interface ApiRegistry<TMap extends ApiConfigMap, TServices extends AnyServicesCo
527
672
  }): RouteDef<TIn, TOut> & {
528
673
  api: keyof TMap & string;
529
674
  };
675
+ /**
676
+ * Typed `useCaseRoute` — wires a {@link UseCase} class into a route in one
677
+ * line. The route's `input` / `output` schemas are read from the useCase's
678
+ * `static` members and the handler instantiates it with the request
679
+ * `services`. The `api` field is constrained to `keyof TMap`.
680
+ *
681
+ * @example
682
+ * export default defineRoutes([
683
+ * useCaseRoute(CreatePostUseCase, { api: "v1", method: "post", tags: ["posts"] }),
684
+ * ]);
685
+ */
686
+ useCaseRoute<TIn extends z.ZodTypeAny, TOut extends z.ZodTypeAny>(useCaseClass: UseCaseClass<TIn, TOut, TServices>, meta: UseCaseRouteMeta<keyof TMap & string>): RouteDef<TIn, TOut> & {
687
+ api: keyof TMap & string;
688
+ };
530
689
  /**
531
690
  * Build one Cloud Function per registered API and return them as a map
532
691
  * keyed by API tag — spread it directly into your `index.ts` exports.
@@ -669,4 +828,4 @@ declare function generateRoutesManifest(routes: ScannedRoute[], opts: GeneratorO
669
828
  /** Convenience helper used by the CLI — combines scan + generate in one call. */
670
829
  declare function generateFromRoot(rootAbs: string, outFileRel: string, derive: PathDeriveOptions, importExtension: string, scan: (root: string) => ScannedRoute[]): GenerationResult;
671
830
 
672
- export { type AnyRouteDef, type AnyServicesContainer, type ApiConfig, type ApiConfigMap, type ApiRegistry, type ApiRegistryOptions, DEFAULT_DERIVE, DEFAULT_GENERATOR_BANNER, DEFAULT_SCANNER, type GenerationResult, type GeneratorOptions, HonoServer, type HonoServerOptions, type HttpMethod, type OpenAPIConfig, type OpenAPIInfo, type PathDeriveOptions, type PayloadSource, type RequestContext, type RouteDef, type RouteHandler, type RouteInterceptor, type RouteModuleDefault, type ScannedRoute, type ScannerOptions, type ServiceProvider, type ServiceProviderMap, type ServicesContainer, type ServicesOf, ValidationError, buildOpenApiDocument, createApiRegistry, createRequestContextMiddleware, createServices, derivePath, generateFromRoot, generateRoutesManifest, renderDocsHtml, scanRoutes, toImportSpecifier, withRequestContext };
831
+ export { type AnyRouteDef, type AnyServicesContainer, type ApiConfig, type ApiConfigMap, type ApiRegistry, type ApiRegistryOptions, DEFAULT_DERIVE, DEFAULT_GENERATOR_BANNER, DEFAULT_SCANNER, type GenerationResult, type GeneratorOptions, HonoServer, type HonoServerOptions, type HttpMethod, type OpenAPIConfig, type OpenAPIInfo, type PathDeriveOptions, type PayloadSource, type RequestContext, type RouteDef, type RouteHandler, type RouteInput, type RouteInterceptor, type RouteModuleDefault, type RouteOutput, type RoutesUnion, type ScannedRoute, type ScannerOptions, type ServiceProvider, type ServiceProviderMap, type ServicesContainer, type ServicesOf, UseCase, type UseCaseClass, type UseCaseRouteMeta, ValidationError, buildOpenApiDocument, createApiRegistry, createRequestContextMiddleware, createServices, defineRoutes, derivePath, generateFromRoot, generateRoutesManifest, renderDocsHtml, scanRoutes, toImportSpecifier, useCaseRoute, withRequestContext };
@@ -1,4 +1,4 @@
1
- import {Hono}from'hono';import {getRequestListener}from'@hono/node-server';import {extendZodWithOpenApi,OpenAPIRegistry,OpenApiGeneratorV31}from'@asteasolutions/zod-to-openapi';import {z}from'zod';import {AsyncLocalStorage}from'async_hooks';import {readdirSync,statSync,mkdirSync,writeFileSync}from'fs';import {join,relative,sep,dirname}from'path';var g=class extends Error{constructor(n,o){super("Request validation failed");this.zodError=n;this.source=o;this.statusCode=400;this.name="ValidationError";}};extendZodWithOpenApi(z);var M="Successful response";function N(t){return t==="get"?"query":"json"}function w(t,e,n){let o=new OpenAPIRegistry;if(n.securitySchemes)for(let[i,a]of Object.entries(n.securitySchemes))o.registerComponent("securitySchemes",i,a);for(let i of t){let a=i.method,p=i.source??N(a),c=U(e,i.path??"/"),f=i.status??200,u=L(a,p,i.input),l=D(p,i.input,"query"),d=D(p,i.input,"param"),y=K(a,c);o.registerPath({method:a,path:G(c),operationId:y,summary:i.summary,description:i.description,tags:i.tags,deprecated:i.deprecated,security:i.security,request:{...l?{query:l}:{},...d?{params:d}:{},...u?{body:u}:{}},responses:i.output?{[f]:{description:M,content:{"application/json":{schema:i.output}}}}:{[f]:{description:M}}});}return new OpenApiGeneratorV31(o.definitions).generateDocument({openapi:"3.1.0",info:n.info,servers:n.servers,security:n.security})}function L(t,e,n){return !n||t==="get"?null:e==="json"?{content:{"application/json":{schema:n}}}:e==="form"?{content:{"application/x-www-form-urlencoded":{schema:n}}}:null}function D(t,e,n){if(e&&(n==="query"&&t==="query"||n==="param"&&t==="param"))return e}function G(t){return t.replace(/:([A-Za-z0-9_]+)/g,"{$1}")}function U(t,e){let n=t.endsWith("/")?t.slice(0,-1):t,o=e.startsWith("/")?e:`/${e}`,r=`${n}${o}`;return r===""?"/":r}function K(t,e){let n=e.replace(/[{}]/g,"").replace(/\/+/g,"_").replace(/[^A-Za-z0-9_]/g,"").replace(/^_+|_+$/g,"");return `${t}_${n||"root"}`}function A(t,e){let n=t.replace(/"/g,"&quot;");return `<!doctype html>
1
+ import {Hono}from'hono';import {getRequestListener}from'@hono/node-server';import {extendZodWithOpenApi,OpenAPIRegistry,OpenApiGeneratorV31}from'@asteasolutions/zod-to-openapi';import {z as z$1}from'zod';import {AsyncLocalStorage}from'async_hooks';import {readdirSync,statSync,mkdirSync,writeFileSync}from'fs';import {join,relative,sep,dirname}from'path';function Z(t){return t}var m=class extends Error{constructor(n,o){super("Request validation failed");this.zodError=n;this.source=o;this.statusCode=400;this.name="ValidationError";}};extendZodWithOpenApi(z$1);var I="Successful response";function G(t){return t==="get"?"query":"json"}function A(t,e,n){let o=new OpenAPIRegistry;if(n.securitySchemes)for(let[i,a]of Object.entries(n.securitySchemes))o.registerComponent("securitySchemes",i,a);for(let i of t){let a=i.method,p=i.source??G(a),u=V(e,i.path??"/"),f=i.status??200,c=K(a,p,i.input),y=b(p,i.input,"query"),d=b(p,i.input,"param"),l=W(a,u);o.registerPath({method:a,path:B(u),operationId:l,summary:i.summary,description:i.description,tags:i.tags,deprecated:i.deprecated,security:i.security,request:{...y?{query:y}:{},...d?{params:d}:{},...c?{body:c}:{}},responses:i.output?{[f]:{description:I,content:{"application/json":{schema:i.output}}}}:{[f]:{description:I}}});}return new OpenApiGeneratorV31(o.definitions).generateDocument({openapi:"3.1.0",info:n.info,servers:n.servers,security:n.security})}function K(t,e,n){return !n||t==="get"?null:e==="json"?{content:{"application/json":{schema:n}}}:e==="form"?{content:{"application/x-www-form-urlencoded":{schema:n}}}:null}function b(t,e,n){if(e&&(n==="query"&&t==="query"||n==="param"&&t==="param"))return e}function B(t){return t.replace(/:([A-Za-z0-9_]+)/g,"{$1}")}function V(t,e){let n=t.endsWith("/")?t.slice(0,-1):t,o=e.startsWith("/")?e:`/${e}`,r=`${n}${o}`;return r===""?"/":r}function W(t,e){let n=e.replace(/[{}]/g,"").replace(/\/+/g,"_").replace(/[^A-Za-z0-9_]/g,"").replace(/^_+|_+$/g,"");return `${t}_${n||"root"}`}function w(t,e){let n=t.replace(/"/g,"&quot;");return `<!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
@@ -9,7 +9,7 @@ import {Hono}from'hono';import {getRequestListener}from'@hono/node-server';impor
9
9
  <script id="api-reference" data-url="${n}"></script>
10
10
  <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
11
11
  </body>
12
- </html>`}var v=new AsyncLocalStorage,V=Object.freeze({get c(){let t=v.getStore();if(!t)throw new Error("[services] requestContext.c was accessed outside of a request. Wrap non-HTTP code paths (cron, triggers, scripts, tests) in `withRequestContext({ c }, () => ...)` to supply a Hono Context.");return t.c},get maybeC(){return v.getStore()?.c}});function P(){return async(t,e)=>{await v.run({c:t},async()=>{await e();});}}function W(t,e){return v.run({c:t.c},async()=>e())}var h="ctx";function J(t){let e=new Map,n=[],o=new Proxy({},{get(r,s){if(typeof s!="string")return;if(s===h)return V;if(e.has(s))return e.get(s);let i=t[s];if(typeof i!="function")throw new Error(`[services] unknown service "${s}". Registered: ${[h,...Object.keys(t)].join(", ")}`);if(n.includes(s))throw new Error(`[services] circular dependency detected: ${[...n,s].join(" \u2192 ")}`);n.push(s);try{let a=Y(i)?new i(o):i(o);return e.set(s,a),a}finally{n.pop();}},has(r,s){return typeof s!="string"?false:s===h||s in t},ownKeys(){return [h,...Object.keys(t)]},getOwnPropertyDescriptor(r,s){if(typeof s=="string"&&(s===h||s in t))return {enumerable:true,configurable:true}}});return o}function Y(t){return /^class[\s{]/.test(Function.prototype.toString.call(t))}var ee=Object.freeze({}),m=class{constructor(e){this.cachedSpec=null;this.options=e,this.app=new Hono,this.mountedRoutes=te(e.routes,e.api),e.services&&this.app.use("*",P());let n=[...e.middlewares??[],...e.globalMiddlewares??[]];for(let o of n)this.app.use("*",o);this.mountRoutes(),this.mountOpenApi(),e.notFound&&this.app.notFound(e.notFound),e.onError&&this.app.onError(e.onError);}get hono(){return this.app}get nodeHandler(){return getRequestListener(this.app.fetch,{overrideGlobalObjects:false})}toFunction(e,n){let o=this.nodeHandler;return n?e(n,o):e(o)}buildOpenApiSpec(){if(this.cachedSpec)return this.cachedSpec;if(!this.options.openapi)throw new Error("[HonoServer] openapi config not set");return this.cachedSpec=w(this.mountedRoutes,this.options.basePath??"",this.options.openapi),this.cachedSpec}mountRoutes(){let e=this.options.basePath??"",n=this.options.validateOutput??false,o=this.options.verbose??false;for(let r of this.mountedRoutes){if(!r.path)throw new Error(`[HonoServer] route "${r.method.toUpperCase()} (no path)" \u2014 missing \`path\`. Run the codegen so the path is derived from the file location, or set it explicitly.`);let s=E(e,r.path),i=r.middlewares??[],a=r.source??(r.method==="get"?"query":"json"),p=re(r,a,n,this.options.interceptor,this.options.services),c=r.method.toUpperCase();this.app.on(c,[s],...i,p),o&&console.log(`[HonoServer] ${r.method.toUpperCase().padEnd(6)} ${s}`);}}mountOpenApi(){let e=this.options.openapi;if(!e)return;let n=e.path??"/openapi.json",o=e.docsPath===void 0?"/docs":e.docsPath,r=E(this.options.basePath??"",n),s=o===false?null:E(this.options.basePath??"",o);if(this.app.get(r,i=>i.json(this.buildOpenApiSpec())),s){let i=ne(s,r);this.app.get(s,a=>a.html(A(i,e.info.title)));}}};function te(t,e){return e?t.filter(n=>Array.isArray(n.api)?n.api.includes(e):n.api===e):t.slice()}function E(t,e){let n=t.endsWith("/")?t.slice(0,-1):t,o=e.startsWith("/")?e:`/${e}`,r=`${n}${o}`;return r===""?"/":r}function ne(t,e){let n=t.split("/").filter(Boolean),o=e.split("/").filter(Boolean);n.pop();let r=0;for(;r<n.length&&r<o.length&&n[r]===o[r];)r++;let s=n.length-r;return [...Array(s).fill(".."),...o.slice(r)].join("/")||"./"}function re(t,e,n,o,r){let s=t.input,i=t.output,a=t.status??200,p=r??ee;return async c=>{let f=async()=>{let l;if(s){let y;try{y=await se(c,e,t.method);}catch(T){throw new R(T instanceof Error?T.message:String(T))}let x=s.safeParse(y);if(!x.success)throw new g(x.error,e);l=x.data;}let d=await t.handler({input:l,c,services:p});if(n&&i&&!(d instanceof Response)){let y=i.safeParse(d);if(!y.success)throw new S(y.error);return y.data}return d},u;if(o)u=await o({next:f,route:t,c,services:p});else try{u=await f();}catch(l){let d=oe(c,l);if(d)return d;throw l}return u instanceof Response?u:c.json(u,a)}}var R=class extends Error{constructor(n){super(n);this.statusCode=400;this.name="BadRequestError";}},S=class extends Error{constructor(n){super("Output validation failed");this.zodError=n;this.statusCode=500;this.name="OutputValidationError";}};function oe(t,e){return e instanceof g?t.json({success:false,error:"Validation failed",issues:b(e.zodError)},400):e instanceof R?t.json({success:false,error:"Bad Request",message:e.message},400):e instanceof S?t.json({success:false,error:"Output validation failed",issues:b(e.zodError)},500):null}async function se(t,e,n){switch(e){case "json":{if(n==="get")return t.req.query();let o=await t.req.text();if(!o)return {};try{return JSON.parse(o)}catch(r){throw new Error(`Invalid JSON body: ${r instanceof Error?r.message:String(r)}`)}}case "query":return t.req.query();case "form":return await t.req.parseBody();case "param":return t.req.param();default:return {}}}function b(t){return t.issues.map(e=>({path:e.path.join("."),code:e.code,message:e.message}))}function ie(t,e){let n=e?.services;return {configs:t,defineRoute(o){return o},serverFor(o,r){let s=t[o];if(!s)throw new Error(`[ApiRegistry] unknown api "${o}". Registered: ${Object.keys(t).join(", ")}`);return new m({...s,api:o,routes:r,services:n})},toFunctions(o,r,s){let i={};for(let a of Object.keys(t)){let p={...s?.defaults??{},...s?.per?.[a]??{}},c=new m({...t[a],api:a,routes:o,services:n});i[a]=Object.keys(p).length?c.toFunction(r,p):c.toFunction(r);}return i}}}var H={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function O(t,e=H){let n=new Set(e.skipSegments.map(r=>r.toLowerCase()));return "/"+t.split("/").filter(Boolean).filter(r=>!n.has(r.toLowerCase())).map(r=>e.casing==="kebab"?ae(r):r).join("/")}function ae(t){return t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function C(t,e,n){let o=k(t),r=k(e),s=0;for(;s<o.length&&s<r.length&&o[s]===r[s];)s++;let i=o.length-s,a=r.slice(s),c=(a[a.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),f=n===""?c:`${c}${n}`;return a[a.length-1]=f,(i===0?"./":"../".repeat(i))+a.join("/")}function k(t){return t.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((n,o)=>!(o===0&&n===""))}var q={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function le(t,e=q){let n=[];return F(t,t,e,n),n.sort((o,r)=>o.relPath.localeCompare(r.relPath)),n}function F(t,e,n,o){let r;try{r=readdirSync(e);}catch{return}for(let s of r){if(n.excludeSegments.includes(s))continue;let i=join(e,s),a;try{a=statSync(i);}catch{continue}if(a.isDirectory())F(t,i,n,o);else if(a.isFile()&&s===n.routesFile){let p=relative(t,i).split(sep).join("/"),c=p.replace(/\/?[^/]+$/,"");o.push({absPath:i,relPath:p,relDir:c});}}}var I="/**\n * AUTO-GENERATED by `@lpdjs/firestore-repo-service` Hono codegen.\n * Do not edit by hand \u2014 re-run `hono:gen` after adding / removing route files.\n */\n";function _(t,e){let n=dirname(e.outFile);mkdirSync(n,{recursive:true});let o=e.banner??I,r=(e.now??new Date).toISOString(),s=e.importExtension,i=[],a=[],p=[];t.forEach((f,u)=>{let l=C(n,f.absPath,s),d=O(f.relDir,e.derive);i.push(`import mod${u} from ${JSON.stringify(l)};`),a.push(` { __derivedPath: ${JSON.stringify(d)}, mod: mod${u} },`),p.push({source:f.relPath,url:d});});let c=`${o}// Generated at ${r} \u2014 ${t.length} route file${t.length===1?"":"s"}.
12
+ </html>`}var h=new AsyncLocalStorage,Y=Object.freeze({get c(){let t=h.getStore();if(!t)throw new Error("[services] requestContext.c was accessed outside of a request. Wrap non-HTTP code paths (cron, triggers, scripts, tests) in `withRequestContext({ c }, () => ...)` to supply a Hono Context.");return t.c},get maybeC(){return h.getStore()?.c}});function O(){return async(t,e)=>{await h.run({c:t},async()=>{await e();});}}function Q(t,e){return h.run({c:t.c},async()=>e())}var v="ctx";function X(t){let e=new Map,n=[],o=new Proxy({},{get(r,s){if(typeof s!="string")return;if(s===v)return Y;if(e.has(s))return e.get(s);let i=t[s];if(typeof i!="function")throw new Error(`[services] unknown service "${s}". Registered: ${[v,...Object.keys(t)].join(", ")}`);if(n.includes(s))throw new Error(`[services] circular dependency detected: ${[...n,s].join(" \u2192 ")}`);n.push(s);try{let a=ee(i)?new i(o):i(o);return e.set(s,a),a}finally{n.pop();}},has(r,s){return typeof s!="string"?false:s===v||s in t},ownKeys(){return [v,...Object.keys(t)]},getOwnPropertyDescriptor(r,s){if(typeof s=="string"&&(s===v||s in t))return {enumerable:true,configurable:true}}});return o}function ee(t){return /^class[\s{]/.test(Function.prototype.toString.call(t))}var re=Object.freeze({}),g=class{constructor(e){this.cachedSpec=null;this.options=e,this.app=new Hono,this.mountedRoutes=oe(e.routes,e.api),e.services&&this.app.use("*",O());let n=[...e.middlewares??[],...e.globalMiddlewares??[]];for(let o of n)this.app.use("*",o);this.mountRoutes(),this.mountOpenApi(),e.notFound&&this.app.notFound(e.notFound),e.onError&&this.app.onError(e.onError);}get hono(){return this.app}get nodeHandler(){return getRequestListener(this.app.fetch,{overrideGlobalObjects:false})}toFunction(e,n){let o=this.nodeHandler;return n?e(n,o):e(o)}buildOpenApiSpec(){if(this.cachedSpec)return this.cachedSpec;if(!this.options.openapi)throw new Error("[HonoServer] openapi config not set");return this.cachedSpec=A(this.mountedRoutes,this.options.basePath??"",this.options.openapi),this.cachedSpec}mountRoutes(){let e=this.options.basePath??"",n=this.options.validateOutput??false,o=this.options.verbose??false;for(let r of this.mountedRoutes){if(!r.path)throw new Error(`[HonoServer] route "${r.method.toUpperCase()} (no path)" \u2014 missing \`path\`. Run the codegen so the path is derived from the file location, or set it explicitly.`);let s=P(e,r.path),i=r.middlewares??[],a=r.source??(r.method==="get"?"query":"json"),p=ie(r,a,n,this.options.interceptor,this.options.services),u=r.method.toUpperCase();this.app.on(u,[s],...i,p),o&&console.log(`[HonoServer] ${r.method.toUpperCase().padEnd(6)} ${s}`);}}mountOpenApi(){let e=this.options.openapi;if(!e)return;let n=e.path??"/openapi.json",o=e.docsPath===void 0?"/docs":e.docsPath,r=P(this.options.basePath??"",n),s=o===false?null:P(this.options.basePath??"",o);if(this.app.get(r,i=>i.json(this.buildOpenApiSpec())),s){let i=se(s,r);this.app.get(s,a=>a.html(w(i,e.info.title)));}}};function oe(t,e){return e?t.filter(n=>Array.isArray(n.api)?n.api.includes(e):n.api===e):t.slice()}function P(t,e){let n=t.endsWith("/")?t.slice(0,-1):t,o=e.startsWith("/")?e:`/${e}`,r=`${n}${o}`;return r===""?"/":r}function se(t,e){let n=t.split("/").filter(Boolean),o=e.split("/").filter(Boolean);n.pop();let r=0;for(;r<n.length&&r<o.length&&n[r]===o[r];)r++;let s=n.length-r;return [...Array(s).fill(".."),...o.slice(r)].join("/")||"./"}function ie(t,e,n,o,r){let s=t.input,i=t.output,a=t.status??200,p=r??re;return async u=>{let f=async()=>{let y;if(s){let l;try{l=await ue(u,e,t.method);}catch(x){throw new T(x instanceof Error?x.message:String(x))}let S=s.safeParse(l);if(!S.success)throw new m(S.error,e);y=S.data;}let d=await t.handler({input:y,c:u,services:p});if(n&&i&&!(d instanceof Response)){let l=i.safeParse(d);if(!l.success)throw new R(l.error);return l.data}return d},c;if(o)c=await o({next:f,route:t,c:u,services:p});else try{c=await f();}catch(y){let d=ae(u,y);if(d)return d;throw y}return c instanceof Response?c:u.json(c,a)}}var T=class extends Error{constructor(n){super(n);this.statusCode=400;this.name="BadRequestError";}},R=class extends Error{constructor(n){super("Output validation failed");this.zodError=n;this.statusCode=500;this.name="OutputValidationError";}};function ae(t,e){return e instanceof m?t.json({success:false,error:"Validation failed",issues:k(e.zodError)},400):e instanceof T?t.json({success:false,error:"Bad Request",message:e.message},400):e instanceof R?t.json({success:false,error:"Output validation failed",issues:k(e.zodError)},500):null}async function ue(t,e,n){switch(e){case "json":{if(n==="get")return t.req.query();let o=await t.req.text();if(!o)return {};try{return JSON.parse(o)}catch(r){throw new Error(`Invalid JSON body: ${r instanceof Error?r.message:String(r)}`)}}case "query":return t.req.query();case "form":return await t.req.parseBody();case "param":return t.req.param();default:return {}}}function k(t){return t.issues.map(e=>({path:e.path.join("."),code:e.code,message:e.message}))}var C=class{constructor(e){this.services=e;}};function E(t,e){return {...e,input:t.input,output:t.output,handler:({input:n,services:o})=>new t(o).execute(n)}}function pe(t,e){let n=e?.services;return {configs:t,defineRoute(o){return o},useCaseRoute(o,r){return E(o,r)},serverFor(o,r){let s=t[o];if(!s)throw new Error(`[ApiRegistry] unknown api "${o}". Registered: ${Object.keys(t).join(", ")}`);return new g({...s,api:o,routes:r,services:n})},toFunctions(o,r,s){let i={};for(let a of Object.keys(t)){let p={...s?.defaults??{},...s?.per?.[a]??{}},u=new g({...t[a],api:a,routes:o,services:n});i[a]=Object.keys(p).length?u.toFunction(r,p):u.toFunction(r);}return i}}}var H={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function M(t,e=H){let n=new Set(e.skipSegments.map(r=>r.toLowerCase()));return "/"+t.split("/").filter(Boolean).filter(r=>!n.has(r.toLowerCase())).map(r=>e.casing==="kebab"?ce(r):r).join("/")}function ce(t){return t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function D(t,e,n){let o=z(t),r=z(e),s=0;for(;s<o.length&&s<r.length&&o[s]===r[s];)s++;let i=o.length-s,a=r.slice(s),u=(a[a.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),f=n===""?u:`${u}${n}`;return a[a.length-1]=f,(i===0?"./":"../".repeat(i))+a.join("/")}function z(t){return t.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((n,o)=>!(o===0&&n===""))}var q={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function ge(t,e=q){let n=[];return F(t,t,e,n),n.sort((o,r)=>o.relPath.localeCompare(r.relPath)),n}function F(t,e,n,o){let r;try{r=readdirSync(e);}catch{return}for(let s of r){if(n.excludeSegments.includes(s))continue;let i=join(e,s),a;try{a=statSync(i);}catch{continue}if(a.isDirectory())F(t,i,n,o);else if(a.isFile()&&s===n.routesFile){let p=relative(t,i).split(sep).join("/"),u=p.replace(/\/?[^/]+$/,"");o.push({absPath:i,relPath:p,relDir:u});}}}var _="/**\n * AUTO-GENERATED by `@lpdjs/firestore-repo-service` Hono codegen.\n * Do not edit by hand \u2014 re-run `hono:gen` after adding / removing route files.\n */\n";function j(t,e){let n=dirname(e.outFile);mkdirSync(n,{recursive:true});let o=e.banner??_,r=(e.now??new Date).toISOString(),s=e.importExtension,i=[],a=[],p=[];t.forEach((f,c)=>{let y=D(n,f.absPath,s),d=M(f.relDir,e.derive);i.push(`import mod${c} from ${JSON.stringify(y)};`),a.push(` { __derivedPath: ${JSON.stringify(d)}, mod: mod${c} },`),p.push({source:f.relPath,url:d});});let u=`${o}// Generated at ${r} \u2014 ${t.length} route file${t.length===1?"":"s"}.
13
13
 
14
14
  import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-service/servers/hono";
15
15
 
@@ -26,5 +26,5 @@ export const routes: AnyRouteDef[] = __defs.flatMap(({ __derivedPath, mod }) =>
26
26
  const list = Array.isArray(mod) ? mod : [mod];
27
27
  return list.map((route) => ({ ...route, path: route.path ?? __derivedPath }));
28
28
  });
29
- `;return writeFileSync(e.outFile,c,"utf8"),{outFile:e.outFile,routeCount:t.length,derivedPaths:p}}function ve(t,e,n,o,r){let s=r(t),i=join(t,e);return _(s,{outFile:i,derive:n,importExtension:o})}export{H as DEFAULT_DERIVE,I as DEFAULT_GENERATOR_BANNER,q as DEFAULT_SCANNER,m as HonoServer,g as ValidationError,w as buildOpenApiDocument,ie as createApiRegistry,P as createRequestContextMiddleware,J as createServices,O as derivePath,ve as generateFromRoot,_ as generateRoutesManifest,A as renderDocsHtml,le as scanRoutes,C as toImportSpecifier,W as withRequestContext};//# sourceMappingURL=index.js.map
29
+ `;return writeFileSync(e.outFile,u,"utf8"),{outFile:e.outFile,routeCount:t.length,derivedPaths:p}}function Se(t,e,n,o,r){let s=r(t),i=join(t,e);return j(s,{outFile:i,derive:n,importExtension:o})}export{H as DEFAULT_DERIVE,_ as DEFAULT_GENERATOR_BANNER,q as DEFAULT_SCANNER,g as HonoServer,C as UseCase,m as ValidationError,A as buildOpenApiDocument,pe as createApiRegistry,O as createRequestContextMiddleware,X as createServices,Z as defineRoutes,M as derivePath,Se as generateFromRoot,j as generateRoutesManifest,w as renderDocsHtml,ge as scanRoutes,D as toImportSpecifier,E as useCaseRoute,Q as withRequestContext};//# sourceMappingURL=index.js.map
30
30
  //# sourceMappingURL=index.js.map