@lpdjs/firestore-repo-service 2.2.9-beta.8 → 2.2.9

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.
@@ -1,4 +1,4 @@
1
- import { MiddlewareHandler, Context, Hono } from 'hono';
1
+ import { Env, MiddlewareHandler, Context, Hono } from 'hono';
2
2
  import { IncomingMessage, ServerResponse } from 'node:http';
3
3
  import { z, ZodError } from 'zod';
4
4
 
@@ -16,20 +16,24 @@ type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
16
16
  /** Where the validated payload comes from. */
17
17
  type PayloadSource = "json" | "query" | "form" | "param";
18
18
  /** Handler signature — receives a single typed context object. */
19
- type RouteHandler<TIn, TOut> = (ctx: {
19
+ type RouteHandler<TIn, TOut, TEnv extends Env = Env> = (ctx: {
20
20
  /** Validated (and typed) request payload. `void` when no `input` schema is defined. */
21
21
  input: TIn;
22
22
  /** Raw Hono `Context` for headers, set status, redirect, etc. */
23
- c: Context;
23
+ c: Context<TEnv>;
24
24
  }) => Promise<TOut | Response> | TOut | Response;
25
25
  /**
26
26
  * One route declaration. Default-exported by every `routes.ts` file inside the
27
27
  * domain tree. Use {@link defineRoute} for full type inference.
28
28
  */
29
- interface RouteDef<TIn extends z.ZodTypeAny | undefined = z.ZodTypeAny | undefined, TOut extends z.ZodTypeAny | undefined = z.ZodTypeAny | undefined> {
29
+ interface RouteDef<TIn extends z.ZodTypeAny | undefined = z.ZodTypeAny | undefined, TOut extends z.ZodTypeAny | undefined = z.ZodTypeAny | undefined, TEnv extends Env = Env> {
30
30
  /**
31
31
  * Logical API tag — routes sharing the same `api` are mounted on the same
32
32
  * `HonoServer` (typically one Cloud Function per `api`).
33
+ *
34
+ * To expose the **same logic** under several APIs with different
35
+ * inputs/outputs, export multiple `defineRoute({...})` from the same file
36
+ * (default + named exports are both picked up by the codegen).
33
37
  */
34
38
  api: string;
35
39
  /** HTTP method. */
@@ -56,7 +60,7 @@ interface RouteDef<TIn extends z.ZodTypeAny | undefined = z.ZodTypeAny | undefin
56
60
  /** Status code for the success response. Default: 200. */
57
61
  status?: number;
58
62
  /** Hono middlewares applied to this route only (after global middlewares). */
59
- middlewares?: MiddlewareHandler[];
63
+ middlewares?: MiddlewareHandler<TEnv>[];
60
64
  summary?: string;
61
65
  description?: string;
62
66
  tags?: string[];
@@ -65,7 +69,7 @@ interface RouteDef<TIn extends z.ZodTypeAny | undefined = z.ZodTypeAny | undefin
65
69
  /** Security requirements (operationId-level override). */
66
70
  security?: Array<Record<string, string[]>>;
67
71
  /** The request handler. */
68
- handler: RouteHandler<TIn extends z.ZodTypeAny ? z.infer<TIn> : void, TOut extends z.ZodTypeAny ? z.infer<TOut> : unknown>;
72
+ handler: RouteHandler<TIn extends z.ZodTypeAny ? z.infer<TIn> : void, TOut extends z.ZodTypeAny ? z.infer<TOut> : unknown, TEnv>;
69
73
  }
70
74
  /** Erased `RouteDef` used by registry/codegen — handler signature is opaque. */
71
75
  type AnyRouteDef = RouteDef<any, any>;
@@ -119,7 +123,7 @@ declare class ValidationError extends Error {
119
123
  * }
120
124
  * ```
121
125
  */
122
- type RouteInterceptor = (ctx: {
126
+ type RouteInterceptor<TEnv extends Env = Env> = (ctx: {
123
127
  /**
124
128
  * Calls validation + handler and returns the raw value.
125
129
  * Throws {@link ValidationError} on Zod failure or any error thrown by the handler.
@@ -128,7 +132,7 @@ type RouteInterceptor = (ctx: {
128
132
  /** Route metadata (read-only). */
129
133
  route: AnyRouteDef;
130
134
  /** Hono request context. */
131
- c: Context;
135
+ c: Context<TEnv>;
132
136
  }) => Promise<Response | unknown> | Response | unknown;
133
137
  /** OpenAPI document info (subset of the spec used by the helper). */
134
138
  interface OpenAPIInfo {
@@ -155,7 +159,7 @@ interface OpenAPIConfig {
155
159
  security?: Array<Record<string, string[]>>;
156
160
  }
157
161
  /** Options consumed by the {@link HonoServer} constructor. */
158
- interface HonoServerOptions {
162
+ interface HonoServerOptions<TEnv extends Env = Env> {
159
163
  /**
160
164
  * API tag — only routes whose `api` matches this value are mounted.
161
165
  * If omitted, every route in the registry is mounted.
@@ -166,12 +170,12 @@ interface HonoServerOptions {
166
170
  /** URL prefix mounted before every route path. Default: `""`. */
167
171
  basePath?: string;
168
172
  /** Hono middlewares applied to every route (after the built-ins). */
169
- middlewares?: MiddlewareHandler[];
173
+ middlewares?: MiddlewareHandler<TEnv>[];
170
174
  /**
171
175
  * Alias for `middlewares` — global middlewares applied to every route.
172
176
  * If both are provided, `globalMiddlewares` is appended after `middlewares`.
173
177
  */
174
- globalMiddlewares?: MiddlewareHandler[];
178
+ globalMiddlewares?: MiddlewareHandler<TEnv>[];
175
179
  /**
176
180
  * If `true`, the server validates the value returned by every handler
177
181
  * against the route's `output` schema and rejects mismatches with a 500
@@ -183,15 +187,15 @@ interface HonoServerOptions {
183
187
  /** OpenAPI configuration. Omit to disable. */
184
188
  openapi?: OpenAPIConfig;
185
189
  /** Custom 404 handler. */
186
- notFound?: (c: Context) => Response | Promise<Response>;
190
+ notFound?: (c: Context<TEnv>) => Response | Promise<Response>;
187
191
  /** Custom error handler. */
188
- onError?: (err: unknown, c: Context) => Response | Promise<Response>;
192
+ onError?: (err: unknown, c: Context<TEnv>) => Response | Promise<Response>;
189
193
  /**
190
194
  * Cross-cutting interceptor wrapping every handler call.
191
195
  * Ideal for response envelopes, business-error mapping, tracing.
192
196
  * See {@link RouteInterceptor}.
193
197
  */
194
- interceptor?: RouteInterceptor;
198
+ interceptor?: RouteInterceptor<TEnv>;
195
199
  }
196
200
 
197
201
  /**
@@ -212,15 +216,15 @@ interface HonoServerOptions {
212
216
  * stays decoupled from a specific firebase-functions version. We import the
213
217
  * real type only when users pass `onRequest` to `toFunction(...)`.
214
218
  */
215
- type OnRequestFn = (...args: any[]) => any;
216
- declare class HonoServer {
219
+ type OnRequestFn$1 = (...args: any[]) => any;
220
+ declare class HonoServer<TEnv extends Env = Env> {
217
221
  private readonly app;
218
222
  private readonly options;
219
223
  private readonly mountedRoutes;
220
224
  private cachedSpec;
221
- constructor(options: HonoServerOptions);
225
+ constructor(options: HonoServerOptions<TEnv>);
222
226
  /** Underlying Hono instance — useful for advanced composition / tests. */
223
- get hono(): Hono;
227
+ get hono(): Hono<TEnv>;
224
228
  /** Raw `(req, res)` handler suitable for `onRequest()` / `http.createServer`. */
225
229
  get nodeHandler(): (req: IncomingMessage, res: ServerResponse) => void;
226
230
  /**
@@ -231,7 +235,7 @@ declare class HonoServer {
231
235
  * @param httpsOptions Options forwarded as the first argument to
232
236
  * `onRequest()` (region, memory, invoker, etc.).
233
237
  */
234
- toFunction(onRequest: OnRequestFn, httpsOptions?: Record<string, unknown>): any;
238
+ toFunction(onRequest: OnRequestFn$1, httpsOptions?: Record<string, unknown>): any;
235
239
  /** Generate (and cache) the OpenAPI 3.1 spec for the mounted routes. */
236
240
  buildOpenApiSpec(): Record<string, unknown>;
237
241
  private mountRoutes;
@@ -239,32 +243,101 @@ declare class HonoServer {
239
243
  }
240
244
 
241
245
  /**
242
- * Type-safe route factory. Use as the **default export** of every
243
- * `routes.ts` file under your `domains/` tree.
246
+ * Typed multi-API registry.
247
+ *
248
+ * Lets you declare every API tag (= every Cloud Function) in **one place**,
249
+ * with full TypeScript safety: the `api` field of {@link defineRoute} is
250
+ * narrowed to the registered tags, and {@link toFunctions} returns one
251
+ * `onRequest` Cloud Function per tag, named after its key.
244
252
  *
245
253
  * @example
246
254
  * ```ts
247
- * import { defineRoute } from "@lpdjs/firestore-repo-service/servers/hono";
248
- * import { execute as input } from "./input.js";
249
- * import { execute as output } from "./output.js";
255
+ * // apis.ts
256
+ * import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
257
+ * import { enrichUser } from "./middlewares/enrich-user.js";
250
258
  *
251
- * export default defineRoute({
252
- * api: "v1",
253
- * method: "post",
254
- * input,
255
- * output,
256
- * summary: "Create or update a custom activity",
257
- * tags: ["activities"],
258
- * handler: async (_c, payload) => {
259
- * // payload is typed as z.infer<typeof input>
260
- * const useCase = new ActivitiesCreateOrUpdateCustomUseCase(new RepositoryActivities());
261
- * return useCase.execute(payload);
259
+ * export const apis = createApiRegistry({
260
+ * v1: {
261
+ * basePath: "/v1",
262
+ * middlewares: [enrichUser],
263
+ * openapi: { info: { title: "Public API", version: "1.0.0" } },
264
+ * },
265
+ * webhooks: {
266
+ * basePath: "/hooks",
267
+ * openapi: { info: { title: "Webhooks", version: "1.0.0" } },
262
268
  * },
263
269
  * });
270
+ *
271
+ * // Use in routes — `api` is now typed "v1" | "webhooks".
272
+ * export const defineRoute = apis.defineRoute;
273
+ *
274
+ * // index.ts (Cloud Functions entrypoint)
275
+ * import { onRequest } from "firebase-functions/v2/https";
276
+ * import { apis } from "./apis.js";
277
+ * import { routes } from "./domains/__generated__/routes.js";
278
+ *
279
+ * export const { v1, webhooks } = apis.toFunctions(routes, onRequest, {
280
+ * defaults: { region: "us-central1", invoker: "public" },
281
+ * per: { v1: { memory: "512MiB" } },
282
+ * });
283
+ * // → URLs: https://<region>-<project>.cloudfunctions.net/v1/posts
284
+ * // https://<region>-<project>.cloudfunctions.net/webhooks/...
264
285
  * ```
265
286
  */
266
287
 
267
- declare function defineRoute<TIn extends z.ZodTypeAny | undefined = undefined, TOut extends z.ZodTypeAny | undefined = undefined>(def: RouteDef<TIn, TOut>): RouteDef<TIn, TOut>;
288
+ type OnRequestFn = (...args: any[]) => any;
289
+ /**
290
+ * Per-API configuration. Same shape as {@link HonoServerOptions} minus the
291
+ * `routes` (resolved by the registry) and `api` (the registry key).
292
+ */
293
+ type ApiConfig<TEnv extends Env = Env> = Omit<HonoServerOptions<TEnv>, "routes" | "api">;
294
+ /** Map of API tag → its config. */
295
+ type ApiConfigMap = Record<string, ApiConfig>;
296
+ interface ApiRegistry<TMap extends ApiConfigMap> {
297
+ /** The registered configs (read-only). */
298
+ readonly configs: TMap;
299
+ /**
300
+ * Typed `defineRoute` — the `api` field is constrained to `keyof TMap`.
301
+ *
302
+ * To expose the same logical endpoint under several APIs with different
303
+ * `input` / `output` schemas, call `defineRoute` once per route and wrap
304
+ * them in an array — per-call inference is preserved:
305
+ *
306
+ * ```ts
307
+ * export default [
308
+ * defineRoute({ api: "v1", input: V1Input, handler: ({ input }) => ... }),
309
+ * defineRoute({ api: "v2", input: V2Input, handler: ({ input }) => ... }),
310
+ * ];
311
+ * ```
312
+ */
313
+ defineRoute<TIn extends z.ZodTypeAny | undefined = undefined, TOut extends z.ZodTypeAny | undefined = undefined>(def: Omit<RouteDef<TIn, TOut>, "api"> & {
314
+ api: keyof TMap & string;
315
+ }): RouteDef<TIn, TOut> & {
316
+ api: keyof TMap & string;
317
+ };
318
+ /**
319
+ * Build one Cloud Function per registered API and return them as a map
320
+ * keyed by API tag — spread it directly into your `index.ts` exports.
321
+ *
322
+ * @param routes Pre-resolved route registry (typically the codegen output).
323
+ * @param onRequest The `onRequest` factory imported from
324
+ * `firebase-functions/v2/https`.
325
+ * @param opts Optional defaults and per-API overrides for `httpsOptions`.
326
+ */
327
+ toFunctions(routes: AnyRouteDef[], onRequest: OnRequestFn, opts?: {
328
+ defaults?: Record<string, unknown>;
329
+ per?: Partial<Record<keyof TMap & string, Record<string, unknown>>>;
330
+ }): {
331
+ [K in keyof TMap & string]: ReturnType<OnRequestFn>;
332
+ };
333
+ /** Build the underlying {@link HonoServer} for a given API (escape hatch). */
334
+ serverFor<K extends keyof TMap & string>(api: K, routes: AnyRouteDef[]): HonoServer;
335
+ }
336
+ /**
337
+ * Factory — declare every API tag once and get back a typed `defineRoute`
338
+ * + `toFunctions`. See the file-level example.
339
+ */
340
+ declare function createApiRegistry<const TMap extends ApiConfigMap>(configs: TMap): ApiRegistry<TMap>;
268
341
 
269
342
  /**
270
343
  * OpenAPI 3.1 spec generator from {@link RouteDef} entries.
@@ -379,4 +452,4 @@ declare function generateRoutesManifest(routes: ScannedRoute[], opts: GeneratorO
379
452
  /** Convenience helper used by the CLI — combines scan + generate in one call. */
380
453
  declare function generateFromRoot(rootAbs: string, outFileRel: string, derive: PathDeriveOptions, importExtension: string, scan: (root: string) => ScannedRoute[]): GenerationResult;
381
454
 
382
- export { type AnyRouteDef, 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 RouteDef, type RouteHandler, type RouteInterceptor, type RouteModuleDefault, type ScannedRoute, type ScannerOptions, ValidationError, buildOpenApiDocument, defineRoute, derivePath, generateFromRoot, generateRoutesManifest, renderDocsHtml, scanRoutes, toImportSpecifier };
455
+ export { type AnyRouteDef, type ApiConfig, type ApiConfigMap, type ApiRegistry, 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 RouteDef, type RouteHandler, type RouteInterceptor, type RouteModuleDefault, type ScannedRoute, type ScannerOptions, ValidationError, buildOpenApiDocument, createApiRegistry, derivePath, generateFromRoot, generateRoutesManifest, renderDocsHtml, scanRoutes, toImportSpecifier };
@@ -1,4 +1,4 @@
1
- import {Hono}from'hono';import {getRequestListener}from'@hono/node-server';import {OpenAPIRegistry,OpenApiGeneratorV31}from'@asteasolutions/zod-to-openapi';import {readdirSync,statSync,mkdirSync,writeFileSync}from'fs';import {join,relative,sep,dirname}from'path';var m=class extends Error{constructor(n,o){super("Request validation failed");this.zodError=n;this.source=o;this.statusCode=400;this.name="ValidationError";}};var O="Successful response";function z(t){return t==="get"?"query":"json"}function R(t,e,n){let o=new OpenAPIRegistry;if(n.securitySchemes)for(let[s,i]of Object.entries(n.securitySchemes))o.registerComponent("securitySchemes",s,i);for(let s of t){let i=s.method,p=s.source??z(i),u=F(e,s.path??"/"),c=s.status??200,d=C(i,p,s.input),l=v(p,s.input,"query"),f=v(p,s.input,"param"),h=q(i,u);o.registerPath({method:i,path:j(u),operationId:h,summary:s.summary,description:s.description,tags:s.tags,deprecated:s.deprecated,security:s.security,request:{...l?{query:l}:{},...f?{params:f}:{},...d?{body:d}:{}},responses:s.output?{[c]:{description:O,content:{"application/json":{schema:s.output}}}}:{[c]:{description:O}}});}return new OpenApiGeneratorV31(o.definitions).generateDocument({openapi:"3.1.0",info:n.info,servers:n.servers,security:n.security})}function C(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 v(t,e,n){if(e&&(n==="query"&&t==="query"||n==="param"&&t==="param"))return e}function j(t){return t.replace(/:([A-Za-z0-9_]+)/g,"{$1}")}function F(t,e){let n=t.endsWith("/")?t.slice(0,-1):t,o=e.startsWith("/")?e:`/${e}`,r=`${n}${o}`;return r===""?"/":r}function q(t,e){let n=e.replace(/[{}]/g,"").replace(/\/+/g,"_").replace(/[^A-Za-z0-9_]/g,"").replace(/^_+|_+$/g,"");return `${t}_${n||"root"}`}function P(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 {readdirSync,statSync,mkdirSync,writeFileSync}from'fs';import {join,relative,sep,dirname}from'path';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 S="Successful response";function j(t){return t==="get"?"query":"json"}function v(t,e,n){let o=new OpenAPIRegistry;if(n.securitySchemes)for(let[s,a]of Object.entries(n.securitySchemes))o.registerComponent("securitySchemes",s,a);for(let s of t){let a=s.method,u=s.source??j(a),p=z(e,s.path??"/"),c=s.status??200,d=$(a,u,s.input),l=w(u,s.input,"query"),f=w(u,s.input,"param"),g=Z(a,p);o.registerPath({method:a,path:q(p),operationId:g,summary:s.summary,description:s.description,tags:s.tags,deprecated:s.deprecated,security:s.security,request:{...l?{query:l}:{},...f?{params:f}:{},...d?{body:d}:{}},responses:s.output?{[c]:{description:S,content:{"application/json":{schema:s.output}}}}:{[c]:{description:S}}});}return new OpenApiGeneratorV31(o.definitions).generateDocument({openapi:"3.1.0",info:n.info,servers:n.servers,security:n.security})}function $(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 w(t,e,n){if(e&&(n==="query"&&t==="query"||n==="param"&&t==="param"))return e}function q(t){return t.replace(/:([A-Za-z0-9_]+)/g,"{$1}")}function z(t,e){let n=t.endsWith("/")?t.slice(0,-1):t,o=e.startsWith("/")?e:`/${e}`,r=`${n}${o}`;return r===""?"/":r}function Z(t,e){let n=e.replace(/[{}]/g,"").replace(/\/+/g,"_").replace(/[^A-Za-z0-9_]/g,"").replace(/^_+|_+$/g,"");return `${t}_${n||"root"}`}function E(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 w=class{constructor(e){this.cachedSpec=null;this.options=e,this.app=new Hono,this.mountedRoutes=N(e.routes,e.api);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=R(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 a=S(e,r.path),s=r.middlewares??[],i=r.source??(r.method==="get"?"query":"json"),p=G(r,i,n,this.options.interceptor),u=r.method.toUpperCase();this.app.on(u,[a],...s,p),o&&console.log(`[HonoServer] ${r.method.toUpperCase().padEnd(6)} ${a}`);}}mountOpenApi(){let e=this.options.openapi;if(!e)return;let n=e.path??"/openapi.json",o=e.docsPath===void 0?"/docs":e.docsPath,r=S(this.options.basePath??"",n),a=o===false?null:S(this.options.basePath??"",o);this.app.get(r,s=>s.json(this.buildOpenApiSpec())),a&&this.app.get(a,s=>s.html(P(r,e.info.title)));}};function N(t,e){return e?t.filter(n=>n.api===e):t.slice()}function S(t,e){let n=t.endsWith("/")?t.slice(0,-1):t,o=e.startsWith("/")?e:`/${e}`,r=`${n}${o}`;return r===""?"/":r}function G(t,e,n,o){let r=t.input,a=t.output,s=t.status??200;return async i=>{let p=async()=>{let c;if(r){let l;try{l=await U(i,e,t.method);}catch(h){throw new g(h instanceof Error?h.message:String(h))}let f=r.safeParse(l);if(!f.success)throw new m(f.error,e);c=f.data;}let d=await t.handler({input:c,c:i});if(n&&a&&!(d instanceof Response)){let l=a.safeParse(d);if(!l.success)throw new y(l.error);return l.data}return d},u;if(o)u=await o({next:p,route:t,c:i});else try{u=await p();}catch(c){let d=L(i,c);if(d)return d;throw c}return u instanceof Response?u:i.json(u,s)}}var g=class extends Error{constructor(n){super(n);this.statusCode=400;this.name="BadRequestError";}},y=class extends Error{constructor(n){super("Output validation failed");this.zodError=n;this.statusCode=500;this.name="OutputValidationError";}};function L(t,e){return e instanceof m?t.json({success:false,error:"Validation failed",issues:D(e.zodError)},400):e instanceof g?t.json({success:false,error:"Bad Request",message:e.message},400):e instanceof y?t.json({success:false,error:"Output validation failed",issues:D(e.zodError)},500):null}async function U(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 D(t){return t.issues.map(e=>({path:e.path.join("."),code:e.code,message:e.message}))}function B(t){return t}var T={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function x(t,e=T){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"?V(r):r).join("/")}function V(t){return t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function A(t,e,n){let o=E(t),r=E(e),a=0;for(;a<o.length&&a<r.length&&o[a]===r[a];)a++;let s=o.length-a,i=r.slice(a),u=(i[i.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),c=n===""?u:`${u}${n}`;return i[i.length-1]=c,(s===0?"./":"../".repeat(s))+i.join("/")}function E(t){return t.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((n,o)=>!(o===0&&n===""))}var b={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function Y(t,e=b){let n=[];return I(t,t,e,n),n.sort((o,r)=>o.relPath.localeCompare(r.relPath)),n}function I(t,e,n,o){let r;try{r=readdirSync(e);}catch{return}for(let a of r){if(n.excludeSegments.includes(a))continue;let s=join(e,a),i;try{i=statSync(s);}catch{continue}if(i.isDirectory())I(t,s,n,o);else if(i.isFile()&&a===n.routesFile){let p=relative(t,s).split(sep).join("/"),u=p.replace(/\/?[^/]+$/,"");o.push({absPath:s,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 H(t,e){let n=dirname(e.outFile);mkdirSync(n,{recursive:true});let o=e.banner??_,r=(e.now??new Date).toISOString(),a=e.importExtension,s=[],i=[],p=[];t.forEach((c,d)=>{let l=A(n,c.absPath,a),f=x(c.relDir,e.derive);s.push(`import mod${d} from ${JSON.stringify(l)};`),i.push(` { __derivedPath: ${JSON.stringify(f)}, mod: mod${d} },`),p.push({source:c.relPath,url:f});});let u=`${o}// Generated at ${r} \u2014 ${t.length} route file${t.length===1?"":"s"}.
12
+ </html>`}var y=class{constructor(e){this.cachedSpec=null;this.options=e,this.app=new Hono,this.mountedRoutes=L(e.routes,e.api);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=v(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 i=A(e,r.path),s=r.middlewares??[],a=r.source??(r.method==="get"?"query":"json"),u=B(r,a,n,this.options.interceptor),p=r.method.toUpperCase();this.app.on(p,[i],...s,u),o&&console.log(`[HonoServer] ${r.method.toUpperCase().padEnd(6)} ${i}`);}}mountOpenApi(){let e=this.options.openapi;if(!e)return;let n=e.path??"/openapi.json",o=e.docsPath===void 0?"/docs":e.docsPath,r=A(this.options.basePath??"",n),i=o===false?null:A(this.options.basePath??"",o);if(this.app.get(r,s=>s.json(this.buildOpenApiSpec())),i){let s=U(i,r);this.app.get(i,a=>a.html(E(s,e.info.title)));}}};function L(t,e){return e?t.filter(n=>Array.isArray(n.api)?n.api.includes(e):n.api===e):t.slice()}function A(t,e){let n=t.endsWith("/")?t.slice(0,-1):t,o=e.startsWith("/")?e:`/${e}`,r=`${n}${o}`;return r===""?"/":r}function U(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 i=n.length-r;return [...Array(i).fill(".."),...o.slice(r)].join("/")||"./"}function B(t,e,n,o){let r=t.input,i=t.output,s=t.status??200;return async a=>{let u=async()=>{let c;if(r){let l;try{l=await W(a,e,t.method);}catch(g){throw new h(g instanceof Error?g.message:String(g))}let f=r.safeParse(l);if(!f.success)throw new m(f.error,e);c=f.data;}let d=await t.handler({input:c,c:a});if(n&&i&&!(d instanceof Response)){let l=i.safeParse(d);if(!l.success)throw new R(l.error);return l.data}return d},p;if(o)p=await o({next:u,route:t,c:a});else try{p=await u();}catch(c){let d=V(a,c);if(d)return d;throw c}return p instanceof Response?p:a.json(p,s)}}var h=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 V(t,e){return e instanceof m?t.json({success:false,error:"Validation failed",issues:x(e.zodError)},400):e instanceof h?t.json({success:false,error:"Bad Request",message:e.message},400):e instanceof R?t.json({success:false,error:"Output validation failed",issues:x(e.zodError)},500):null}async function W(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 x(t){return t.issues.map(e=>({path:e.path.join("."),code:e.code,message:e.message}))}function J(t){return {configs:t,defineRoute(e){return e},serverFor(e,n){let o=t[e];if(!o)throw new Error(`[ApiRegistry] unknown api "${e}". Registered: ${Object.keys(t).join(", ")}`);return new y({...o,api:e,routes:n})},toFunctions(e,n,o){let r={};for(let i of Object.keys(t)){let s={...o?.defaults??{},...o?.per?.[i]??{}},a=new y({...t[i],api:i,routes:e});r[i]=Object.keys(s).length?a.toFunction(n,s):a.toFunction(n);}return r}}}var D={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function P(t,e=D){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"?K(r):r).join("/")}function K(t){return t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function T(t,e,n){let o=O(t),r=O(e),i=0;for(;i<o.length&&i<r.length&&o[i]===r[i];)i++;let s=o.length-i,a=r.slice(i),p=(a[a.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),c=n===""?p:`${p}${n}`;return a[a.length-1]=c,(s===0?"./":"../".repeat(s))+a.join("/")}function O(t){return t.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((n,o)=>!(o===0&&n===""))}var b={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function ne(t,e=b){let n=[];return k(t,t,e,n),n.sort((o,r)=>o.relPath.localeCompare(r.relPath)),n}function k(t,e,n,o){let r;try{r=readdirSync(e);}catch{return}for(let i of r){if(n.excludeSegments.includes(i))continue;let s=join(e,i),a;try{a=statSync(s);}catch{continue}if(a.isDirectory())k(t,s,n,o);else if(a.isFile()&&i===n.routesFile){let u=relative(t,s).split(sep).join("/"),p=u.replace(/\/?[^/]+$/,"");o.push({absPath:s,relPath:u,relDir:p});}}}var M="/**\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 I(t,e){let n=dirname(e.outFile);mkdirSync(n,{recursive:true});let o=e.banner??M,r=(e.now??new Date).toISOString(),i=e.importExtension,s=[],a=[],u=[];t.forEach((c,d)=>{let l=T(n,c.absPath,i),f=P(c.relDir,e.derive);s.push(`import mod${d} from ${JSON.stringify(l)};`),a.push(` { __derivedPath: ${JSON.stringify(f)}, mod: mod${d} },`),u.push({source:c.relPath,url:f});});let p=`${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
 
@@ -18,13 +18,13 @@ import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-serv
18
18
 
19
19
  `:`
20
20
  `)+`const __defs: { __derivedPath: string; mod: RouteModuleDefault }[] = [
21
- `+i.join(`
22
- `)+(i.length?`
21
+ `+a.join(`
22
+ `)+(a.length?`
23
23
  `:"")+`];
24
24
 
25
25
  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,u,"utf8"),{outFile:e.outFile,routeCount:t.length,derivedPaths:p}}function oe(t,e,n,o,r){let a=r(t),s=join(t,e);return H(a,{outFile:s,derive:n,importExtension:o})}export{T as DEFAULT_DERIVE,_ as DEFAULT_GENERATOR_BANNER,b as DEFAULT_SCANNER,w as HonoServer,m as ValidationError,R as buildOpenApiDocument,B as defineRoute,x as derivePath,oe as generateFromRoot,H as generateRoutesManifest,P as renderDocsHtml,Y as scanRoutes,A as toImportSpecifier};//# sourceMappingURL=index.js.map
29
+ `;return writeFileSync(e.outFile,p,"utf8"),{outFile:e.outFile,routeCount:t.length,derivedPaths:u}}function ae(t,e,n,o,r){let i=r(t),s=join(t,e);return I(i,{outFile:s,derive:n,importExtension:o})}export{D as DEFAULT_DERIVE,M as DEFAULT_GENERATOR_BANNER,b as DEFAULT_SCANNER,y as HonoServer,m as ValidationError,v as buildOpenApiDocument,J as createApiRegistry,P as derivePath,ae as generateFromRoot,I as generateRoutesManifest,E as renderDocsHtml,ne as scanRoutes,T as toImportSpecifier};//# sourceMappingURL=index.js.map
30
30
  //# sourceMappingURL=index.js.map