@lpdjs/firestore-repo-service 2.4.3 → 2.5.2-beta.0

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.
package/README.md CHANGED
@@ -102,20 +102,23 @@ if (post) {
102
102
  }
103
103
 
104
104
  // Update
105
- const updated = await repos.users.update("user123", { name: "New name", age: 31 });
105
+ const updated = await repos.users.update("user123", {
106
+ name: "New name",
107
+ age: 31,
108
+ });
106
109
  ```
107
110
 
108
111
  ## API reference
109
112
 
110
113
  ### `createRepositoryConfig()`
111
114
 
112
- | Option | Description |
113
- | ------------- | ------------------------------------------------------- |
114
- | `path` | Collection path in Firestore |
115
- | `isGroup` | `true` for collection group, `false` for simple |
116
- | `foreignKeys` | Keys for `get.by*` methods (single document lookup) |
117
- | `queryKeys` | Keys for `query.by*` methods (multi-document query) |
118
- | `refCb` | Function that returns the document reference |
115
+ | Option | Description |
116
+ | ------------- | --------------------------------------------------- |
117
+ | `path` | Collection path in Firestore |
118
+ | `isGroup` | `true` for collection group, `false` for simple |
119
+ | `foreignKeys` | Keys for `get.by*` methods (single document lookup) |
120
+ | `queryKeys` | Keys for `query.by*` methods (multi-document query) |
121
+ | `refCb` | Function that returns the document reference |
119
122
 
120
123
  **Sub-collection example:**
121
124
 
@@ -134,8 +137,8 @@ comments: createRepositoryConfig<CommentModel>()({
134
137
 
135
138
  ```typescript
136
139
  interface QueryOptions<T> {
137
- where?: [keyof T, WhereFilterOp, any][]; // AND conditions
138
- orWhere?: [keyof T, WhereFilterOp, any][][]; // OR conditions
140
+ where?: [keyof T, WhereFilterOp, any][]; // AND conditions
141
+ orWhere?: [keyof T, WhereFilterOp, any][][]; // OR conditions
139
142
  orderBy?: { field: keyof T; direction?: "asc" | "desc" }[];
140
143
  limit?: number;
141
144
  offset?: number;
@@ -226,7 +229,9 @@ const page = await repos.posts.query.paginate({
226
229
  ```typescript
227
230
  import { count, sum, average } from "@lpdjs/firestore-repo-service";
228
231
 
229
- const activeCount = await repos.users.aggregate.count({ where: [["isActive", "==", true]] });
232
+ const activeCount = await repos.users.aggregate.count({
233
+ where: [["isActive", "==", true]],
234
+ });
230
235
  const totalViews = await repos.posts.aggregate.sum("views");
231
236
  const avgAge = await repos.users.aggregate.average("age");
232
237
  ```
@@ -237,7 +242,9 @@ const avgAge = await repos.users.aggregate.average("age");
237
242
  const result = await repos.users.transaction.run(async (txn) => {
238
243
  const user = await txn.get(repos.users.documentRef("user123"));
239
244
  if (user.exists()) {
240
- txn.update(repos.users.documentRef("user123"), { age: user.data().age + 1 });
245
+ txn.update(repos.users.documentRef("user123"), {
246
+ age: user.data().age + 1,
247
+ });
241
248
  }
242
249
  return { success: true };
243
250
  });
@@ -249,8 +256,14 @@ const result = await repos.users.transaction.run(async (txn) => {
249
256
  // (status = 'active' AND age >= 18) OR (status = 'pending' AND verified = true)
250
257
  const users = await repos.users.query.by({
251
258
  orWhere: [
252
- [["status", "==", "active"], ["age", ">=", 18]],
253
- [["status", "==", "pending"], ["verified", "==", true]],
259
+ [
260
+ ["status", "==", "active"],
261
+ ["age", ">=", 18],
262
+ ],
263
+ [
264
+ ["status", "==", "pending"],
265
+ ["verified", "==", true],
266
+ ],
254
267
  ],
255
268
  });
256
269
  ```
@@ -304,9 +317,9 @@ npm i -D @asteasolutions/zod-to-openapi
304
317
  ### Bootstrap
305
318
 
306
319
  ```bash
307
- npx frs-hono init # interactive — creates apis.ts + manifest stub
308
- npx frs-hono new createPost --domain posts --method post --api v1
309
- npx frs-hono gen --root src/domains # refresh manifest (run before each build)
320
+ npx frs init # interactive — creates apis.ts + manifest stub
321
+ npx frs new createPost --domain posts --method post --api v1
322
+ npx frs gen --root src/domains # refresh manifest (run before each build)
310
323
  ```
311
324
 
312
325
  ### Configure your APIs (`apis.ts`)
@@ -337,7 +350,7 @@ import { z } from "zod";
337
350
  import { defineRoute } from "../../../../apis.js";
338
351
 
339
352
  export default defineRoute({
340
- api: "v1", // typed: only registered tags accepted
353
+ api: "v1", // typed: only registered tags accepted
341
354
  method: "post",
342
355
  input: z.object({ title: z.string() }),
343
356
  output: z.object({ id: z.string() }),
@@ -365,16 +378,16 @@ export const { v1 } = apis.toFunctions(routes, onRequest, {
365
378
 
366
379
  ### Key features
367
380
 
368
- | Feature | Details |
369
- | --- | --- |
370
- | **File-based routing** | `routes.ts` next to each useCase, scanned at build time |
371
- | **Multi-API registry** | One Cloud Function per tag, typed `api` field |
372
- | **Zod validation** | Body / query / path params + optional response validation |
373
- | **OpenAPI 3.1** | Auto-generated from Zod schemas; Scalar UI at `/docs` |
374
- | **Interceptor** | Around-style hook for envelopes, error mapping, tracing |
375
- | **Middlewares** | Per-API and per-route Hono middlewares |
376
- | **Typed context** | Augment `ContextVariableMap` once, `c.get("user")` typed everywhere |
377
- | **CLI** | `init` / `new` (interactive) / `gen` |
381
+ | Feature | Details |
382
+ | ---------------------- | ------------------------------------------------------------------- |
383
+ | **File-based routing** | `routes.ts` next to each useCase, scanned at build time |
384
+ | **Multi-API registry** | One Cloud Function per tag, typed `api` field |
385
+ | **Zod validation** | Body / query / path params + optional response validation |
386
+ | **OpenAPI 3.1** | Auto-generated from Zod schemas; Scalar UI at `/docs` |
387
+ | **Interceptor** | Around-style hook for envelopes, error mapping, tracing |
388
+ | **Middlewares** | Per-API and per-route Hono middlewares |
389
+ | **Typed context** | Augment `ContextVariableMap` once, `c.get("user")` typed everywhere |
390
+ | **CLI** | `init` / `new` (interactive) / `gen` |
378
391
 
379
392
  Full documentation: [frs.lpdjs.fr/guide/hono](https://frs.lpdjs.fr/guide/hono)
380
393
 
@@ -446,9 +459,18 @@ export const { functions } = servers.sync({
446
459
 
447
460
  // Spread Cloud Functions into your exports
448
461
  export const {
449
- users_onCreate, users_onUpdate, users_onDelete, sync_users,
450
- posts_onCreate, posts_onUpdate, posts_onDelete, sync_posts,
451
- comments_onCreate, comments_onUpdate, comments_onDelete, sync_comments,
462
+ users_onCreate,
463
+ users_onUpdate,
464
+ users_onDelete,
465
+ sync_users,
466
+ posts_onCreate,
467
+ posts_onUpdate,
468
+ posts_onDelete,
469
+ sync_posts,
470
+ comments_onCreate,
471
+ comments_onUpdate,
472
+ comments_onDelete,
473
+ sync_comments,
452
474
  adminsync,
453
475
  } = functions;
454
476
  ```
@@ -488,7 +510,6 @@ Firestore emulator runs on `localhost:8080`, UI on `http://localhost:4000`.
488
510
 
489
511
  MIT
490
512
 
491
-
492
513
  ## Installation
493
514
 
494
515
  ```bash
@@ -579,20 +600,23 @@ if (post) {
579
600
  }
580
601
 
581
602
  // Update
582
- const updated = await repos.users.update("user123", { name: "New name", age: 31 });
603
+ const updated = await repos.users.update("user123", {
604
+ name: "New name",
605
+ age: 31,
606
+ });
583
607
  ```
584
608
 
585
609
  ## API reference
586
610
 
587
611
  ### `createRepositoryConfig()`
588
612
 
589
- | Option | Description |
590
- | ------------- | ------------------------------------------------------- |
591
- | `path` | Collection path in Firestore |
592
- | `isGroup` | `true` for collection group, `false` for simple |
593
- | `foreignKeys` | Keys for `get.by*` methods (single document lookup) |
594
- | `queryKeys` | Keys for `query.by*` methods (multi-document query) |
595
- | `refCb` | Function that returns the document reference |
613
+ | Option | Description |
614
+ | ------------- | --------------------------------------------------- |
615
+ | `path` | Collection path in Firestore |
616
+ | `isGroup` | `true` for collection group, `false` for simple |
617
+ | `foreignKeys` | Keys for `get.by*` methods (single document lookup) |
618
+ | `queryKeys` | Keys for `query.by*` methods (multi-document query) |
619
+ | `refCb` | Function that returns the document reference |
596
620
 
597
621
  **Sub-collection example:**
598
622
 
@@ -611,8 +635,8 @@ comments: createRepositoryConfig<CommentModel>()({
611
635
 
612
636
  ```typescript
613
637
  interface QueryOptions<T> {
614
- where?: [keyof T, WhereFilterOp, any][]; // AND conditions
615
- orWhere?: [keyof T, WhereFilterOp, any][][]; // OR conditions
638
+ where?: [keyof T, WhereFilterOp, any][]; // AND conditions
639
+ orWhere?: [keyof T, WhereFilterOp, any][][]; // OR conditions
616
640
  orderBy?: { field: keyof T; direction?: "asc" | "desc" }[];
617
641
  limit?: number;
618
642
  offset?: number;
@@ -703,7 +727,9 @@ const page = await repos.posts.query.paginate({
703
727
  ```typescript
704
728
  import { count, sum, average } from "@lpdjs/firestore-repo-service";
705
729
 
706
- const activeCount = await repos.users.aggregate.count({ where: [["isActive", "==", true]] });
730
+ const activeCount = await repos.users.aggregate.count({
731
+ where: [["isActive", "==", true]],
732
+ });
707
733
  const totalViews = await repos.posts.aggregate.sum("views");
708
734
  const avgAge = await repos.users.aggregate.average("age");
709
735
  ```
@@ -714,7 +740,9 @@ const avgAge = await repos.users.aggregate.average("age");
714
740
  const result = await repos.users.transaction.run(async (txn) => {
715
741
  const user = await txn.get(repos.users.documentRef("user123"));
716
742
  if (user.exists()) {
717
- txn.update(repos.users.documentRef("user123"), { age: user.data().age + 1 });
743
+ txn.update(repos.users.documentRef("user123"), {
744
+ age: user.data().age + 1,
745
+ });
718
746
  }
719
747
  return { success: true };
720
748
  });
@@ -726,8 +754,14 @@ const result = await repos.users.transaction.run(async (txn) => {
726
754
  // (status = 'active' AND age >= 18) OR (status = 'pending' AND verified = true)
727
755
  const users = await repos.users.query.by({
728
756
  orWhere: [
729
- [["status", "==", "active"], ["age", ">=", 18]],
730
- [["status", "==", "pending"], ["verified", "==", true]],
757
+ [
758
+ ["status", "==", "active"],
759
+ ["age", ">=", 18],
760
+ ],
761
+ [
762
+ ["status", "==", "pending"],
763
+ ["verified", "==", true],
764
+ ],
731
765
  ],
732
766
  });
733
767
  ```
@@ -798,9 +832,18 @@ export const { functions } = servers.sync({
798
832
 
799
833
  // Spread Cloud Functions into your exports
800
834
  export const {
801
- users_onCreate, users_onUpdate, users_onDelete, sync_users,
802
- posts_onCreate, posts_onUpdate, posts_onDelete, sync_posts,
803
- comments_onCreate, comments_onUpdate, comments_onDelete, sync_comments,
835
+ users_onCreate,
836
+ users_onUpdate,
837
+ users_onDelete,
838
+ sync_users,
839
+ posts_onCreate,
840
+ posts_onUpdate,
841
+ posts_onDelete,
842
+ sync_posts,
843
+ comments_onCreate,
844
+ comments_onUpdate,
845
+ comments_onDelete,
846
+ sync_comments,
804
847
  adminsync,
805
848
  } = functions;
806
849
  ```
@@ -837,4 +880,3 @@ Firestore emulator runs on `localhost:8080`, UI on `http://localhost:4000`.
837
880
  ## License
838
881
 
839
882
  MIT
840
-
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';var path=require('path'),fs=require('fs'),promises=require('readline/promises'),process$1=require('process');var b={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function I(e,s=b){let t=new Set(s.skipSegments.map(o=>o.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(o=>!t.has(o.toLowerCase())).map(o=>s.casing==="kebab"?Z(o):o).join("/")}function Z(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function T(e,s,t){let r=E(e),o=E(s),i=0;for(;i<r.length&&i<o.length&&r[i]===o[i];)i++;let a=r.length-i,n=o.slice(i),d=(n[n.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),c=t===""?d:`${d}${t}`;return n[n.length-1]=c,(a===0?"./":"../".repeat(a))+n.join("/")}function E(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((t,r)=>!(r===0&&t===""))}var S={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function N(e,s=S){let t=[];return q(e,e,s,t),t.sort((r,o)=>r.relPath.localeCompare(o.relPath)),t}function q(e,s,t,r){let o;try{o=fs.readdirSync(s);}catch{return}for(let i of o){if(t.excludeSegments.includes(i))continue;let a=path.join(s,i),n;try{n=fs.statSync(a);}catch{continue}if(n.isDirectory())q(e,a,t,r);else if(n.isFile()&&i===t.routesFile){let l=path.relative(e,a).split(path.sep).join("/"),d=l.replace(/\/?[^/]+$/,"");r.push({absPath:a,relPath:l,relDir:d});}}}var ne="/**\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 L(e,s){let t=path.dirname(s.outFile);fs.mkdirSync(t,{recursive:true});let r=s.banner??ne,o=(s.now??new Date).toISOString(),i=s.importExtension,a=[],n=[],l=[];e.forEach((c,h)=>{let g=T(t,c.absPath,i),y=I(c.relDir,s.derive);a.push(`import mod${h} from ${JSON.stringify(g)};`),n.push(` { __derivedPath: ${JSON.stringify(y)}, mod: mod${h} },`),l.push({source:c.relPath,url:y});});let d=`${r}// Generated at ${o} \u2014 ${e.length} route file${e.length===1?"":"s"}.
2
+ 'use strict';var fs=require('fs'),path=require('path'),process$1=require('process'),promises=require('readline/promises');var _={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function L(e,s=_){let t=new Set(s.skipSegments.map(r=>r.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(r=>!t.has(r.toLowerCase())).map(r=>s.casing==="kebab"?Y(r):r).join("/")}function Y(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function U(e,s,t){let o=N(e),r=N(s),n=0;for(;n<o.length&&n<r.length&&o[n]===r[n];)n++;let a=o.length-n,i=r.slice(n),u=(i[i.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),d=t===""?u:`${u}${t}`;return i[i.length-1]=d,(a===0?"./":"../".repeat(a))+i.join("/")}function N(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((t,o)=>!(o===0&&t===""))}var ee="/**\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 B(e,s){let t=path.dirname(s.outFile);fs.mkdirSync(t,{recursive:true});let o=s.banner??ee,r=(s.now??new Date).toISOString(),n=s.importExtension,a=[],i=[],c=[];e.forEach((d,f)=>{let h=U(t,d.absPath,n),v=L(d.relDir,s.derive);a.push(`import mod${f} from ${JSON.stringify(h)};`),i.push(` { __derivedPath: ${JSON.stringify(v)}, mod: mod${f} },`),c.push({source:d.relPath,url:v});});let u=`${o}// Generated at ${r} \u2014 ${e.length} route file${e.length===1?"":"s"}.
3
3
 
4
4
  import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-service/servers/hono";
5
5
 
@@ -8,25 +8,29 @@ import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-serv
8
8
 
9
9
  `:`
10
10
  `)+`const __defs: { __derivedPath: string; mod: RouteModuleDefault }[] = [
11
- `+n.join(`
12
- `)+(n.length?`
11
+ `+i.join(`
12
+ `)+(i.length?`
13
13
  `:"")+`];
14
14
 
15
15
  export const routes: AnyRouteDef[] = __defs.flatMap(({ __derivedPath, mod }) => {
16
16
  const list = Array.isArray(mod) ? mod : [mod];
17
17
  return list.map((route) => ({ ...route, path: route.path ?? __derivedPath }));
18
18
  });
19
- `;return fs.writeFileSync(s.outFile,d,"utf8"),{outFile:s.outFile,routeCount:e.length,derivedPaths:l}}function ce(e){let[s,...t]=e,r={};for(let o=0;o<t.length;o++){let i=t[o];if(!i.startsWith("--"))continue;let a=i.slice(2),n=t[o+1];n&&!n.startsWith("--")?(r[a]=n,o++):r[a]=true;}return {command:s??"help",flags:r}}function B(){console.log(`frs-hono \u2014 Hono file-based codegen
19
+ `;return fs.writeFileSync(s.outFile,u,"utf8"),{outFile:s.outFile,routeCount:e.length,derivedPaths:c}}var A={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function G(e,s=A){let t=[];return z(e,e,s,t),t.sort((o,r)=>o.relPath.localeCompare(r.relPath)),t}function z(e,s,t,o){let r;try{r=fs.readdirSync(s);}catch{return}for(let n of r){if(t.excludeSegments.includes(n))continue;let a=path.join(s,n),i;try{i=fs.statSync(a);}catch{continue}if(i.isDirectory())z(e,a,t,o);else if(i.isFile()&&n===t.routesFile){let c=path.relative(e,a).split(path.sep).join("/"),u=c.replace(/\/?[^/]+$/,"");o.push({absPath:a,relPath:c,relDir:u});}}}function pe(e){let[s,...t]=e,o={};for(let r=0;r<t.length;r++){let n=t[r];if(!n.startsWith("--"))continue;let a=n.slice(2),i=t[r+1];i&&!i.startsWith("--")?(o[a]=i,r++):o[a]=true;}return {command:s??"help",flags:o}}function H(){console.log(`frs \u2014 Hono file-based codegen
20
20
 
21
21
  Usage:
22
- frs-hono init [flags]
23
- frs-hono gen [flags]
24
- frs-hono new <name> [flags]
25
- frs-hono help
22
+ frs init [flags]
23
+ frs gen [flags]
24
+ frs new <name> [flags]
25
+ frs add service <name> [flags]
26
+ frs help
26
27
 
27
28
  Flags (init):
28
29
  --root <dir> Domain root to create (default: src/domains)
29
30
  --apis-file <path> Path to the apis.ts file to create (default: src/apis.ts)
31
+ --services-file <path>
32
+ Path to the services.ts file to create
33
+ (default: src/services.ts)
30
34
  --apis <list> Comma-separated API tags to register (default: v1)
31
35
  --base-path <prefix> basePath shared by all APIs (default: derived from tag)
32
36
  --force Overwrite existing files
@@ -61,122 +65,189 @@ Flags (new <name>):
61
65
  --force Overwrite if files already exist
62
66
  --yes Skip prompts, use defaults / flag values
63
67
 
68
+ Flags (add service <name>):
69
+ --services-file <path>
70
+ Path to the services.ts file (default: src/services.ts)
71
+ --services-dir <dir> Directory hosting individual service files
72
+ (default: <dir-of-services-file>/services)
73
+ --force Overwrite existing files
74
+
64
75
  Examples:
65
- frs-hono init
66
- frs-hono new createPost --domain posts --method post
67
- frs-hono new listPosts --domain posts --method get --api v1
68
- `);}function G(e){if(typeof e=="string")return e.split(",").map(s=>s.trim()).filter(Boolean)}function p(e){return typeof e=="string"?e:void 0}function J(e){if(e||!process$1.stdin.isTTY)return {ask:async(t,r)=>r??"",askChoice:async(t,r,o)=>o??"",askBool:async(t,r)=>r,close:()=>{}};let s=promises.createInterface({input:process$1.stdin,output:process$1.stdout});return {async ask(t,r){let o=r?` (${r})`:"";return (await s.question(`? ${t}${o} \u203A `)).trim()||r||""},async askChoice(t,r,o){let i=` [${r.join("/")}${o?`, default: ${o}`:""}]`;for(;;){let a=(await s.question(`? ${t}${i} \u203A `)).trim().toLowerCase();if(!a&&o)return o;if(r.includes(a))return a;console.log(` invalid choice \u2014 pick one of: ${r.join(", ")}`);}},async askBool(t,r){let o=` (${r?"Y/n":"y/N"})`,i=(await s.question(`? ${t}${o} \u203A `)).trim().toLowerCase();return i?i==="y"||i==="yes"||i==="true":r},close:()=>s.close()}}async function ue(e){let s=p(e.root);s||(console.error("[frs-hono] --root is required"),process.exit(2));let t=path.resolve(process.cwd(),s);fs.existsSync(t)||(console.error(`[frs-hono] root not found: ${t}`),process.exit(2));let r=p(e.out)??"__generated__/routes.ts",o=G(e.skip)??b.skipSegments,i=p(e.casing)==="kebab"?"kebab":b.casing,a={skipSegments:o,casing:i},n=p(e.ext)??".js",l=G(e.exclude)??S.excludeSegments,d=p(e["routes-file"])??S.routesFile,h=N(t,{routesFile:d,excludeSegments:l});h.length===0&&console.warn(`[frs-hono] no "${d}" files found under ${t} \u2014 generated an empty manifest.`);let g=L(h,{outFile:path.resolve(t,r),derive:a,importExtension:n});if(!e.silent){console.log(`[frs-hono] wrote ${g.outFile} (${g.routeCount} route${g.routeCount===1?"":"s"})`);for(let{source:y,url:$}of g.derivedPaths)console.log(` ${$.padEnd(48)} \u2190 ${y}`);}}async function pe(e,s){let t=s.yes===true,r=J(t);try{let o=e&&!e.startsWith("--")?e:void 0;o||(o=(await r.ask("Route name (e.g. createPost)")).trim(),o||(console.error("[frs-hono] route name is required"),process.exit(2)));let i=p(s.domain);i||(i=(await r.ask("Domain name (e.g. posts)")).trim(),i||(console.error("[frs-hono] --domain is required"),process.exit(2)));let a=p(s.root)??"src/domains",n=p(s.method)?.toLowerCase();n||(n=await r.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(n)||(console.error(`[frs-hono] invalid --method: ${n}`),process.exit(2));let l=p(s.api);l||(l=(await r.ask("API tag","v1")).trim()||"v1");let d=p(s["usecase-folder"])??"useCases",c=s["with-usecase"]===void 0?t?!0:await r.askBool("Scaffold useCase.ts?",!0):s["with-usecase"]!==!1,h=s["with-test"]===void 0?t||!c?c:await r.askBool("Scaffold useCase.test.ts (Vitest)?",!0):s["with-test"]!==!1,g=s.force===!0,y=path.resolve(process.cwd(),a),$=path.resolve(y,i,d,o),P=path.resolve($,"routes.ts"),A=path.resolve($,"useCase.ts"),k=path.resolve($,"useCase.test.ts");fs.mkdirSync($,{recursive:!0});let f=`${o.charAt(0).toUpperCase()}${o.slice(1)}UseCase`,R=`/**
69
- * ${f} \u2014 pure business logic, no HTTP awareness.
76
+ frs init
77
+ frs new createPost --domain posts --method post
78
+ frs new listPosts --domain posts --method get --api v1
79
+ frs add service postRepo
80
+ `);}function W(e){if(typeof e=="string")return e.split(",").map(s=>s.trim()).filter(Boolean)}function l(e){return typeof e=="string"?e:void 0}function K(e){if(e||!process$1.stdin.isTTY)return {ask:async(t,o)=>o??"",askChoice:async(t,o,r)=>r??"",askBool:async(t,o)=>o,close:()=>{}};let s=promises.createInterface({input:process$1.stdin,output:process$1.stdout});return {async ask(t,o){let r=o?` (${o})`:"";return (await s.question(`? ${t}${r} \u203A `)).trim()||o||""},async askChoice(t,o,r){let n=` [${o.join("/")}${r?`, default: ${r}`:""}]`;for(;;){let a=(await s.question(`? ${t}${n} \u203A `)).trim().toLowerCase();if(!a&&r)return r;if(o.includes(a))return a;console.log(` invalid choice \u2014 pick one of: ${o.join(", ")}`);}},async askBool(t,o){let r=` (${o?"Y/n":"y/N"})`,n=(await s.question(`? ${t}${r} \u203A `)).trim().toLowerCase();return n?n==="y"||n==="yes"||n==="true":o},close:()=>s.close()}}async function ue(e){let s=l(e.root);s||(console.error("[frs] --root is required"),process.exit(2));let t=path.resolve(process.cwd(),s);fs.existsSync(t)||(console.error(`[frs] root not found: ${t}`),process.exit(2));let o=l(e.out)??"__generated__/routes.ts",r=W(e.skip)??_.skipSegments,n=l(e.casing)==="kebab"?"kebab":_.casing,a={skipSegments:r,casing:n},i=l(e.ext)??".js",c=W(e.exclude)??A.excludeSegments,u=l(e["routes-file"])??A.routesFile,f=G(t,{routesFile:u,excludeSegments:c});f.length===0&&console.warn(`[frs] no "${u}" files found under ${t} \u2014 generated an empty manifest.`);let h=B(f,{outFile:path.resolve(t,o),derive:a,importExtension:i});if(!e.silent){console.log(`[frs] wrote ${h.outFile} (${h.routeCount} route${h.routeCount===1?"":"s"})`);for(let{source:v,url:y}of h.derivedPaths)console.log(` ${y.padEnd(48)} \u2190 ${v}`);}}async function le(e,s){let t=s.yes===true,o=K(t);try{let r=e&&!e.startsWith("--")?e:void 0;r||(r=(await o.ask("Route name (e.g. createPost)")).trim(),r||(console.error("[frs] route name is required"),process.exit(2)));let n=l(s.domain);n||(n=(await o.ask("Domain name (e.g. posts)")).trim(),n||(console.error("[frs] --domain is required"),process.exit(2)));let a=l(s.root)??"src/domains",i=l(s.method)?.toLowerCase();i||(i=await o.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(i)||(console.error(`[frs] invalid --method: ${i}`),process.exit(2));let c=l(s.api);c||(c=(await o.ask("API tag","v1")).trim()||"v1");let u=l(s["usecase-folder"])??"useCases",d=s["with-usecase"]===void 0?t?!0:await o.askBool("Scaffold useCase.ts?",!0):s["with-usecase"]!==!1,f=s["with-test"]===void 0?t||!d?d:await o.askBool("Scaffold useCase.test.ts (Vitest)?",!0):s["with-test"]!==!1,h=s.force===!0,v=path.resolve(process.cwd(),a),y=path.resolve(v,n,u,r),$=path.resolve(y,"routes.ts"),b=path.resolve(y,"useCase.ts"),x=path.resolve(y,"useCase.test.ts");fs.mkdirSync(y,{recursive:!0});let m=`${r.charAt(0).toUpperCase()}${r.slice(1)}UseCase`,k=`/**
81
+ * ${m} \u2014 pure business logic, no HTTP awareness.
70
82
  * Reusable across multiple routes / cron jobs / triggers.
71
83
  */
72
84
 
73
- export interface ${f}Input {
85
+ export interface ${m}Input {
74
86
  // TODO: define the input shape
75
87
  example: string;
76
88
  }
77
89
 
78
- export interface ${f}Output {
90
+ export interface ${m}Output {
79
91
  // TODO: define the output shape
80
92
  id: string;
81
93
  }
82
94
 
83
- export class ${f} {
95
+ export class ${m} {
84
96
  // TODO: inject repositories / services via the constructor.
85
97
  // constructor(private readonly repo: SomeRepository) {}
86
98
 
87
- async execute(input: ${f}Input): Promise<${f}Output> {
99
+ async execute(input: ${m}Input): Promise<${m}Output> {
88
100
  // TODO: implement
89
101
  return { id: input.example };
90
102
  }
91
103
  }
92
- `,C=n==="get"?`z.object({
104
+ `,D=i==="get"?`z.object({
93
105
  // GET \u2192 lu depuis les query params
94
106
  example: z.string(),
95
107
  })`:`z.object({
96
- // ${n.toUpperCase()} \u2192 lu depuis le body JSON
108
+ // ${i.toUpperCase()} \u2192 lu depuis le body JSON
97
109
  example: z.string(),
98
- })`,D=c?` const useCase = new ${f}();
110
+ })`,S=d?` const useCase = new ${m}();
99
111
  const data = await useCase.execute(input);
100
112
  return data;`:` // TODO: business logic
101
- return { id: input.example };`,u=c?`import { ${f} } from "./useCase.js";
102
- `:"",V=`import { z } from "zod";
103
- import { defineRoute } from "${p(s["apis-import"])??de(y,$)}";
104
- ${u}
113
+ return { id: input.example };`,j=d?`import { ${m} } from "./useCase.js";
114
+ `:"",I=`import { z } from "zod";
115
+ import { defineRoute } from "${l(s["apis-import"])??me(v,y)}";
116
+ ${j}
105
117
  export default defineRoute({
106
- api: "${l}",
107
- method: "${n}",
118
+ api: "${c}",
119
+ method: "${i}",
108
120
 
109
- input: ${C},
121
+ input: ${D},
110
122
 
111
123
  output: z.object({
112
124
  id: z.string(),
113
125
  }),
114
126
 
115
- summary: "TODO: ${o}",
116
- tags: ["${i}"],
127
+ summary: "TODO: ${r}",
128
+ tags: ["${n}"],
117
129
 
118
130
  handler: async ({ input }) => {
119
- ${D}
131
+ ${S}
120
132
  },
121
133
  });
122
- `,j=[],F=[],O=(v,Y)=>{if(fs.existsSync(v)&&!g){F.push(v);return}fs.writeFileSync(v,Y,"utf8"),j.push(v);};if(O(P,V),c&&O(A,R),c&&h){let v=`import { describe, it, expect } from "vitest";
123
- import { ${f} } from "./useCase.js";
134
+ `,p=[],C=[],E=(w,V)=>{if(fs.existsSync(w)&&!h){C.push(w);return}fs.writeFileSync(w,V,"utf8"),p.push(w);};if(E($,I),d&&E(b,k),d&&f){let w=`import { describe, it, expect } from "vitest";
135
+ import { ${m} } from "./useCase.js";
124
136
 
125
- describe("${f}", () => {
137
+ describe("${m}", () => {
126
138
  it("returns a response shaped like the output schema", async () => {
127
- const useCase = new ${f}();
139
+ const useCase = new ${m}();
128
140
  const result = await useCase.execute({ example: "hello" });
129
141
  expect(result).toMatchObject({ id: expect.any(String) });
130
142
  });
131
143
 
132
144
  // TODO: add error-path tests, repository mocks, etc.
133
145
  });
134
- `;O(k,v);}for(let v of j)console.log(`[frs-hono] wrote ${v}`);for(let v of F)console.log(`[frs-hono] skipped ${v} (use --force to overwrite)`);console.log(`
135
- [frs-hono] reminder: run "frs-hono gen --root ${a}" to refresh the manifest.`);}finally{r.close();}}async function le(e){let s=e.yes===true,t=J(s);try{let r=e.force===!0,o=p(e.root);o||(o=(await t.ask("Domain root","src/domains")).trim()||"src/domains");let i=p(e["apis-file"]);i||(i=(await t.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let a=p(e.apis);a||(a=(await t.ask("API tags (comma-separated)","v1")).trim()||"v1");let n=a.split(",").map(u=>u.trim()).filter(Boolean);n.length===0&&(console.error("[frs-hono] at least one API tag is required"),process.exit(2));let l=p(e["base-path"]),d=path.resolve(process.cwd(),o),c=path.resolve(process.cwd(),i),h=path.resolve(d,"__generated__"),g=path.resolve(h,"routes.ts"),y=[],$=[],P=(u,x)=>{if(fs.mkdirSync(path.dirname(u),{recursive:!0}),fs.existsSync(u)&&!r){$.push(u);return}fs.writeFileSync(u,x,"utf8"),y.push(u);},k=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
146
+ `;E(x,w);}for(let w of p)console.log(`[frs] wrote ${w}`);for(let w of C)console.log(`[frs] skipped ${w} (use --force to overwrite)`);console.log(`
147
+ [frs] reminder: run "frs gen --root ${a}" to refresh the manifest.`);}finally{o.close();}}async function de(e){let s=e.yes===true,t=K(s);try{let o=e.force===!0,r=l(e.root);r||(r=(await t.ask("Domain root","src/domains")).trim()||"src/domains");let n=l(e["apis-file"]);n||(n=(await t.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let a=l(e["services-file"]);if(!a){let p=n.replace(/apis\.ts$/,"services.ts")||"src/services.ts";a=(await t.ask("services.ts location",p)).trim()||p;}let i=l(e.apis);i||(i=(await t.ask("API tags (comma-separated)","v1")).trim()||"v1");let c=i.split(",").map(p=>p.trim()).filter(Boolean);c.length===0&&(console.error("[frs] at least one API tag is required"),process.exit(2));let u=l(e["base-path"]),d=path.resolve(process.cwd(),r),f=path.resolve(process.cwd(),n),h=path.resolve(process.cwd(),a),v=path.resolve(d,"__generated__"),y=path.resolve(v,"routes.ts"),$=[],b=[],x=(p,C)=>{if(fs.mkdirSync(path.dirname(p),{recursive:!0}),fs.existsSync(p)&&!o){b.push(p);return}fs.writeFileSync(p,C,"utf8"),$.push(p);},m=c.map(p=>{let C=u??`/${p}`;return ` ${p}: {
148
+ basePath: "${C}",
149
+ openapi: {
150
+ info: { title: "${p.toUpperCase()} API", version: "1.0.0", description: "" },
151
+ },
152
+ verbose: process.env["NODE_ENV"] !== "production",
153
+ },`}).join(`
154
+ `),k=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
155
+ import { services } from "${O(path.dirname(f),h)}";
136
156
 
137
157
  /**
138
158
  * Single source of truth for every API exposed by this project.
139
159
  * Add per-API middlewares, interceptors, OpenAPI metadata here.
160
+ *
161
+ * The shared \`services\` container is injected into every HonoServer the
162
+ * registry builds \u2014 handlers / interceptors receive it via \`{ services }\`
163
+ * and the built-in \`services.ctx.c\` resolves to the current request.
140
164
  */
141
- export const apis = createApiRegistry({
142
- ${n.map(u=>{let x=l??`/${u}`;return ` ${u}: {
143
- basePath: "${x}",
144
- openapi: {
145
- info: { title: "${u.toUpperCase()} API", version: "1.0.0", description: "" },
146
- },
147
- verbose: process.env["NODE_ENV"] !== "production",
148
- },`}).join(`
149
- `)}
150
- });
165
+ export const apis = createApiRegistry(
166
+ {
167
+ ${m}
168
+ },
169
+ { services },
170
+ );
151
171
 
152
172
  /** Typed helper used inside every route file. */
153
173
  export const defineRoute = apis.defineRoute;
154
- `;P(c,k);let f=`// AUTO-GENERATED by frs-hono \u2014 do not edit.
155
- // Run \`frs-hono gen --root ${o}\` to refresh.
174
+ `;x(f,k),x(h,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
175
+
176
+ /**
177
+ * Global DI container \u2014 declare every singleton (repositories, SDK
178
+ * clients, loggers, useCases) here. Each factory is invoked once on first
179
+ * access and the instance is cached for the process lifetime.
180
+ *
181
+ * Factories receive a typed proxy of every other service plus the
182
+ * built-in \`ctx\` (current request \`Context\` via AsyncLocalStorage).
183
+ * Destructure what you need \u2014 TypeScript will infer everything.
184
+ *
185
+ * @example
186
+ * \`\`\`ts
187
+ * postRepo: () => new PostRepo(),
188
+ * createPostUseCase: ({ ctx, postRepo }) =>
189
+ * new CreatePostUseCase(ctx, postRepo),
190
+ * \`\`\`
191
+ */
192
+ export const services = createServices({
193
+ // TODO: declare your services here.
194
+ // Example:
195
+ // db: () => getFirestore(),
196
+ // postRepo: ({ db }) => new PostRepo(db),
197
+ });
198
+
199
+ /** Convenience type \u2014 \`function fn(svc: Services) { ... }\`. */
200
+ export type Services = typeof services;
201
+ `);let S=`// AUTO-GENERATED by frs \u2014 do not edit.
202
+ // Run \`frs gen --root ${r}\` to refresh.
156
203
 
157
204
  import type { AnyRouteDef } from "@lpdjs/firestore-repo-service/servers/hono";
158
205
 
159
206
  export const routes: AnyRouteDef[] = [];
160
- `;P(g,f);let R=z(path.dirname(c),c),C=z(path.dirname(c),g),D=n.length===1?`export const { ${n[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${n.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let u of y)console.log(`[frs-hono] wrote ${u}`);for(let u of $)console.log(`[frs-hono] skipped ${u} (use --force to overwrite)`);console.log(`
207
+ `;x(y,S);let j=O(path.dirname(f),f),q=O(path.dirname(f),y),I=c.length===1?`export const { ${c[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${c.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let p of $)console.log(`[frs] wrote ${p}`);for(let p of b)console.log(`[frs] skipped ${p} (use --force to overwrite)`);console.log(`
161
208
  Next steps:
162
209
 
163
210
  1. Wire the registry in your Functions entrypoint (e.g. src/index.ts):
164
211
 
165
212
  import { onRequest } from "firebase-functions/v2/https";
166
- import { apis } from "${R}";
167
- import { routes } from "${C}";
213
+ import { apis } from "${j}";
214
+ import { routes } from "${q}";
168
215
 
169
- ${D}
216
+ ${I}
170
217
  defaults: { region: "us-central1", invoker: "public" },
171
218
  });
172
219
 
173
220
  2. Scaffold a first route:
174
221
 
175
- frs-hono new createPost --domain posts --method post --api ${n[0]}
222
+ frs new createPost --domain posts --method post --api ${c[0]}
176
223
 
177
224
  3. Refresh the manifest before each build:
178
225
 
179
- frs-hono gen --root ${o}
180
- `);}finally{t.close();}}function z(e,s){let t=path.relative(e,s).replace(/\\/g,"/");return t=t.replace(/\.ts$/,".js"),t.startsWith(".")||(t=`./${t}`),t}function de(e,s){let t=["apis.ts","apis.js","api.ts","api.js"],r=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let o of r)for(let i of t){let a=path.resolve(o,i);if(fs.existsSync(a)){let n=path.relative(s,a).replace(/\\/g,"/");return n=n.replace(/\.ts$/,".js").replace(/\.js$/,".js"),n.startsWith(".")||(n=`./${n}`),n}}return "../../../../apis.js"}async function fe(){let e=process.argv.slice(2),{command:s,flags:t}=ce(e);switch(s){case "init":await le(t);return;case "gen":await ue(t);return;case "new":await pe(e[1],t);return;case "help":case "--help":case "-h":B();return;default:console.error(`[frs-hono] unknown command: ${s}
181
- `),B(),process.exit(2);}}fe().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.cjs.map
226
+ frs gen --root ${r}
227
+ `);}finally{t.close();}}function O(e,s){let t=path.relative(e,s).replace(/\\/g,"/");return t=t.replace(/\.ts$/,".js"),t.startsWith(".")||(t=`./${t}`),t}async function fe(e,s,t){e!=="service"&&(console.error(`[frs] unknown "add" target: ${e??"(missing)"} \u2014 supported: service`),process.exit(2)),s||(console.error("[frs] service name is required: frs add service <name>"),process.exit(2));let o=t.force===true,r=l(t["services-file"])??"src/services.ts",n=path.resolve(process.cwd(),r),a=l(t["services-dir"])??path.resolve(path.dirname(n),"services"),i=path.resolve(process.cwd(),a);fs.existsSync(n)||(console.error(`[frs] services file not found: ${n}
228
+ Run \`frs init\` first or pass --services-file <path>.`),process.exit(2)),fs.mkdirSync(i,{recursive:true});let c=`${s.charAt(0).toUpperCase()}${s.slice(1)}Service`,u=path.resolve(i,`${s}.ts`),d=`/**
229
+ * ${c} \u2014 generated by \`frs add service ${s}\`.
230
+ *
231
+ * Singleton instantiated lazily on first access. Async resources (DB
232
+ * connections, SDK clients) should be lazy-loaded inside the class:
233
+ *
234
+ * @example
235
+ * \`\`\`ts
236
+ * private _client: SomeClient | undefined;
237
+ * get client(): SomeClient {
238
+ * return (this._client ??= new SomeClient({...}));
239
+ * }
240
+ * \`\`\`
241
+ */
242
+ export class ${c} {
243
+ // TODO: add fields, methods, dependencies.
244
+ hello(): string {
245
+ return "hello from ${s}";
246
+ }
247
+ }
248
+ `;fs.existsSync(u)&&!o?console.log(`[frs] skipped ${u} (use --force to overwrite)`):(fs.writeFileSync(u,d,"utf8"),console.log(`[frs] wrote ${u}`));let f=fs.readFileSync(n,"utf8"),h=O(path.dirname(n),u),v=`import { ${c} } from "${h}";`,y=` ${s}: () => new ${c}(),`;if(f.includes(v)){console.log(`[frs] services.ts already registers "${s}" \u2014 skipping.`);return}let $=f.split(`
249
+ `),b=-1;for(let S=0;S<$.length;S++)/^import\s/.test($[S])&&(b=S);b>=0?$.splice(b+1,0,v):$.unshift(v);let x=$.join(`
250
+ `),m=x.match(/createServices\s*\(\s*\{/);if(!m){console.error(`[frs] could not find \`createServices({\` in ${n} \u2014 register "${s}" manually.`);return}let k=m.index+m[0].length,D=x.slice(0,k)+`
251
+ `+y+x.slice(k);fs.writeFileSync(n,D,"utf8"),console.log(`[frs] updated ${n} (+ ${s})`);}function me(e,s){let t=["apis.ts","apis.js","api.ts","api.js"],o=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let r of o)for(let n of t){let a=path.resolve(r,n);if(fs.existsSync(a)){let i=path.relative(s,a).replace(/\\/g,"/");return i=i.replace(/\.ts$/,".js").replace(/\.js$/,".js"),i.startsWith(".")||(i=`./${i}`),i}}return "../../../../apis.js"}async function ge(){let e=process.argv.slice(2),{command:s,flags:t}=pe(e);switch(s){case "init":await de(t);return;case "gen":await ue(t);return;case "new":await le(e[1],t);return;case "add":await fe(e[1],e[2],t);return;case "help":case "--help":case "-h":H();return;default:console.error(`[frs] unknown command: ${s}
252
+ `),H(),process.exit(2);}}ge().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.cjs.map
182
253
  //# sourceMappingURL=cli.cjs.map