@lpdjs/firestore-repo-service 2.6.12 → 2.6.13

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.
@@ -76,9 +76,9 @@ declare function createServers<TRepos extends Record<string, ConfiguredRepositor
76
76
  * Returns a Cloud Function when `onRequest` was passed to `createServers`,
77
77
  * otherwise the raw HTTP handler.
78
78
  */
79
- admin(options: BoundAdminServerOptions<TRepos>): (((req: any, res: any) => Promise<void>) & {
79
+ admin(options: BoundAdminServerOptions<TRepos>): firebase_functions_v2_https.HttpsFunction | (((req: any, res: any) => Promise<void>) & {
80
80
  httpsOptions?: HttpsOptions;
81
- }) | firebase_functions_v2_https.HttpsFunction;
81
+ });
82
82
  /**
83
83
  * Build the CRUD REST API handler with `repo` auto-injected from each
84
84
  * registry key. Returns a Cloud Function when `onRequest` was passed to
@@ -76,9 +76,9 @@ declare function createServers<TRepos extends Record<string, ConfiguredRepositor
76
76
  * Returns a Cloud Function when `onRequest` was passed to `createServers`,
77
77
  * otherwise the raw HTTP handler.
78
78
  */
79
- admin(options: BoundAdminServerOptions<TRepos>): (((req: any, res: any) => Promise<void>) & {
79
+ admin(options: BoundAdminServerOptions<TRepos>): firebase_functions_v2_https.HttpsFunction | (((req: any, res: any) => Promise<void>) & {
80
80
  httpsOptions?: HttpsOptions;
81
- }) | firebase_functions_v2_https.HttpsFunction;
81
+ });
82
82
  /**
83
83
  * Build the CRUD REST API handler with `repo` auto-injected from each
84
84
  * registry key. Returns a Cloud Function when `onRequest` was passed to
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@ import { Query, QuerySnapshot, Firestore } from 'firebase-admin/firestore';
3
3
  import { z } from 'zod';
4
4
  import { h as QueryOptions, P as PaginationOptions, f as RepositoryConfig, i as RelationConfig, C as ConfiguredRepository } from './types-C_alF2Xe.cjs';
5
5
  export { A as ApiResponse, b as CrudRepoConfig, a as CrudServerOptions, E as ExtractDocumentRefSignature, j as ExtractUpdateSignature, g as FieldPath, F as FieldRole, t as GenerateGetMethods, u as GenerateQueryMethods, G as GetOptions, k as GetResult, I as IncludeConfigTyped, L as ListResponseData, o as PaginationResult, v as PaginationWithIncludeOptionsTyped, w as PopulateOptionsTyped, Q as QueryRequestBody, l as RelationalKeys, R as RepoFieldPath, e as RepoRelationKeys, p as SystemBackfillFailure, q as SystemBackfillOptions, r as SystemBackfillResult, U as UserFieldPath, W as WhereClause, m as createPaginationIterator, n as executePaginatedQuery } from './types-C_alF2Xe.cjs';
6
- export { B as BoundAdminRepoConfig, a as BoundAdminServerOptions, b as BoundCrudRepoConfig, d as BoundCrudServerOptions, e as BoundFirestoreSyncConfig, C as CreateServersDeps, c as createServers } from './create-servers-Bq9lnpg6.cjs';
6
+ export { B as BoundAdminRepoConfig, a as BoundAdminServerOptions, b as BoundCrudRepoConfig, d as BoundCrudServerOptions, e as BoundFirestoreSyncConfig, C as CreateServersDeps, c as createServers } from './create-servers-BV-E4Rp-.cjs';
7
7
  export { a as AdminRepoConfig, b as AdminRepoEntry, A as AdminServerOptions, B as BasicAuthConfig } from './index-CXWiqnFs.cjs';
8
8
  export { M as MiniRouter } from './firebase-auth-t1CAR-lp.cjs';
9
9
  import 'firebase-functions/v2/firestore';
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import { Query, QuerySnapshot, Firestore } from 'firebase-admin/firestore';
3
3
  import { z } from 'zod';
4
4
  import { h as QueryOptions, P as PaginationOptions, f as RepositoryConfig, i as RelationConfig, C as ConfiguredRepository } from './types-C6I3WtJS.js';
5
5
  export { A as ApiResponse, b as CrudRepoConfig, a as CrudServerOptions, E as ExtractDocumentRefSignature, j as ExtractUpdateSignature, g as FieldPath, F as FieldRole, t as GenerateGetMethods, u as GenerateQueryMethods, G as GetOptions, k as GetResult, I as IncludeConfigTyped, L as ListResponseData, o as PaginationResult, v as PaginationWithIncludeOptionsTyped, w as PopulateOptionsTyped, Q as QueryRequestBody, l as RelationalKeys, R as RepoFieldPath, e as RepoRelationKeys, p as SystemBackfillFailure, q as SystemBackfillOptions, r as SystemBackfillResult, U as UserFieldPath, W as WhereClause, m as createPaginationIterator, n as executePaginatedQuery } from './types-C6I3WtJS.js';
6
- export { B as BoundAdminRepoConfig, a as BoundAdminServerOptions, b as BoundCrudRepoConfig, d as BoundCrudServerOptions, e as BoundFirestoreSyncConfig, C as CreateServersDeps, c as createServers } from './create-servers-dXpZiSOT.js';
6
+ export { B as BoundAdminRepoConfig, a as BoundAdminServerOptions, b as BoundCrudRepoConfig, d as BoundCrudServerOptions, e as BoundFirestoreSyncConfig, C as CreateServersDeps, c as createServers } from './create-servers-BhUavex0.js';
7
7
  export { a as AdminRepoConfig, b as AdminRepoEntry, A as AdminServerOptions, B as BasicAuthConfig } from './index-4gzZw5m_.js';
8
8
  export { M as MiniRouter } from './firebase-auth-t1CAR-lp.js';
9
9
  import 'firebase-functions/v2/firestore';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';var fs=require('fs'),path=require('path'),process$1=require('process'),promises=require('readline/promises');var I={skipSegments:["useCases","useCase","use-cases","use-case"],casing:"preserve"};function J(e,r=I){let s=new Set(r.skipSegments.map(o=>o.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(o=>!s.has(o.toLowerCase())).map(o=>r.casing==="kebab"?ne(o):o).join("/")}function ne(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function Z(e,r,s){let t=W(e),o=W(r),i=0;for(;i<t.length&&i<o.length&&t[i]===o[i];)i++;let a=t.length-i,n=o.slice(i),f=(n[n.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),g=s===""?f:`${f}${s}`;return n[n.length-1]=g,(a===0?"./":"../".repeat(a))+n.join("/")}function W(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((s,t)=>!(t===0&&s===""))}var pe="/**\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 K(e,r){let s=path.dirname(r.outFile);fs.mkdirSync(s,{recursive:true});let t=r.banner??pe,o=(r.now??new Date).toISOString(),i=r.importExtension,a=[],n=[],c=[];e.forEach((g,p)=>{let b=Z(s,g.absPath,i),h=J(g.relDir,r.derive);a.push(`import mod${p} from ${JSON.stringify(b)};`),n.push(` { __derivedPath: ${JSON.stringify(h)}, mod: mod${p} },`),c.push({source:g.relPath,url:h});});let f=`${t}// 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 J(e,r=_){let s=new Set(r.skipSegments.map(o=>o.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(o=>!s.has(o.toLowerCase())).map(o=>r.casing==="kebab"?ne(o):o).join("/")}function ne(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function Z(e,r,s){let t=W(e),o=W(r),i=0;for(;i<t.length&&i<o.length&&t[i]===o[i];)i++;let a=t.length-i,n=o.slice(i),f=(n[n.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),g=s===""?f:`${f}${s}`;return n[n.length-1]=g,(a===0?"./":"../".repeat(a))+n.join("/")}function W(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((s,t)=>!(t===0&&s===""))}var pe="/**\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 K(e,r){let s=path.dirname(r.outFile);fs.mkdirSync(s,{recursive:true});let t=r.banner??pe,o=(r.now??new Date).toISOString(),i=r.importExtension,a=[],n=[],c=[];e.forEach((g,p)=>{let b=Z(s,g.absPath,i),h=J(g.relDir,r.derive);a.push(`import mod${p} from ${JSON.stringify(b)};`),n.push(` { __derivedPath: ${JSON.stringify(h)}, mod: mod${p} },`),c.push({source:g.relPath,url:h});});let f=`${t}// Generated at ${o} \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
 
@@ -16,7 +16,7 @@ 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(r.outFile,f,"utf8"),{outFile:r.outFile,routeCount:e.length,derivedPaths:c}}var L={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function V(e,r=L){let s=[];return Y(e,e,r,s),s.sort((t,o)=>t.relPath.localeCompare(o.relPath)),s}function Y(e,r,s,t){let o;try{o=fs.readdirSync(r);}catch{return}for(let i of o){if(s.excludeSegments.includes(i))continue;let a=path.join(r,i),n;try{n=fs.statSync(a);}catch{continue}if(n.isDirectory())Y(e,a,s,t);else if(n.isFile()&&i===s.routesFile){let c=path.relative(e,a).split(path.sep).join("/"),f=c.replace(/\/?[^/]+$/,"");t.push({absPath:a,relPath:c,relDir:f});}}}var se=".frsrc.json";function U(e=process.cwd()){let r=path.resolve(e,se);if(!fs.existsSync(r))return {};try{return JSON.parse(fs.readFileSync(r,"utf8"))}catch{return {}}}function ve(e,r=process.cwd()){let s=path.resolve(r,se),o={...U(r),...e};return fs.writeFileSync(s,`${JSON.stringify(o,null,2)}
19
+ `;return fs.writeFileSync(r.outFile,f,"utf8"),{outFile:r.outFile,routeCount:e.length,derivedPaths:c}}var I={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function V(e,r=I){let s=[];return Y(e,e,r,s),s.sort((t,o)=>t.relPath.localeCompare(o.relPath)),s}function Y(e,r,s,t){let o;try{o=fs.readdirSync(r);}catch{return}for(let i of o){if(s.excludeSegments.includes(i))continue;let a=path.join(r,i),n;try{n=fs.statSync(a);}catch{continue}if(n.isDirectory())Y(e,a,s,t);else if(n.isFile()&&i===s.routesFile){let c=path.relative(e,a).split(path.sep).join("/"),f=c.replace(/\/?[^/]+$/,"");t.push({absPath:a,relPath:c,relDir:f});}}}var se=".frsrc.json";function z(e=process.cwd()){let r=path.resolve(e,se);if(!fs.existsSync(r))return {};try{return JSON.parse(fs.readFileSync(r,"utf8"))}catch{return {}}}function ve(e,r=process.cwd()){let s=path.resolve(r,se),o={...z(r),...e};return fs.writeFileSync(s,`${JSON.stringify(o,null,2)}
20
20
  `,"utf8"),s}function ye(e){let[r,...s]=e,t={};for(let o=0;o<s.length;o++){let i=s[o];if(!i.startsWith("--"))continue;let a=i.slice(2),n=s[o+1];n&&!n.startsWith("--")?(t[a]=n,o++):t[a]=true;}return {command:r??"help",flags:t}}function X(){console.log(`frs \u2014 Hono file-based codegen
21
21
 
22
22
  Usage:
@@ -83,7 +83,7 @@ Examples:
83
83
  frs new createPost --domain posts --method post
84
84
  frs new listPosts --domain posts --method get --api v1
85
85
  frs add service postRepo
86
- `);}function ee(e){if(typeof e=="string")return e.split(",").map(r=>r.trim()).filter(Boolean)}function m(e){return typeof e=="string"?e:void 0}function te(e){if(e||!process$1.stdin.isTTY)return {ask:async(s,t)=>t??"",askChoice:async(s,t,o)=>o??"",askBool:async(s,t)=>t,close:()=>{}};let r=promises.createInterface({input:process$1.stdin,output:process$1.stdout});return {async ask(s,t){let o=t?` (${t})`:"";return (await r.question(`? ${s}${o} \u203A `)).trim()||t||""},async askChoice(s,t,o){let i=` [${t.join("/")}${o?`, default: ${o}`:""}]`;for(;;){let a=(await r.question(`? ${s}${i} \u203A `)).trim().toLowerCase();if(!a&&o)return o;if(t.includes(a))return a;console.log(` invalid choice \u2014 pick one of: ${t.join(", ")}`);}},async askBool(s,t){let o=` (${t?"Y/n":"y/N"})`,i=(await r.question(`? ${s}${o} \u203A `)).trim().toLowerCase();return i?i==="y"||i==="yes"||i==="true":t},close:()=>r.close()}}async function xe(e){let r=U(),s=m(e.root)??r.root;s||(console.error("[frs] --root is required (or run `frs init` to write it to .frsrc.json)"),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=m(e.out)??r.out??"__generated__/routes.ts",i=ee(e.skip)??I.skipSegments,a=m(e.casing)==="kebab"?"kebab":I.casing,n={skipSegments:i,casing:a},c=m(e.ext)??".js",f=ee(e.exclude)??L.excludeSegments,g=m(e["routes-file"])??L.routesFile,b=V(t,{routesFile:g,excludeSegments:f});b.length===0&&console.warn(`[frs] no "${g}" files found under ${t} \u2014 generated an empty manifest.`);let h=K(b,{outFile:path.resolve(t,o),derive:n,importExtension:c});if(!e.silent){console.log(`[frs] wrote ${h.outFile} (${h.routeCount} route${h.routeCount===1?"":"s"})`);for(let{source:P,url:y}of h.derivedPaths)console.log(` ${y.padEnd(48)} \u2190 ${P}`);}}async function we(e,r){let s=r.yes===true,t=te(s),o=U();try{let i=e&&!e.startsWith("--")?e:void 0;i||(i=(await t.ask("Route name (e.g. createPost)")).trim(),i||(console.error("[frs] route name is required"),process.exit(2)));let a=m(r.domain);a||(a=(await t.ask("Domain name (e.g. posts)")).trim(),a||(console.error("[frs] --domain is required"),process.exit(2)));let n=m(r.root)??o.root??"src/domains",c=m(r.method)?.toLowerCase();c||(c=await t.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(c)||(console.error(`[frs] invalid --method: ${c}`),process.exit(2));let f=m(r.api);if(!f){let v=o.apis?.[0]??"v1";f=(await t.ask("API tag",v)).trim()||v;}let g=m(r["usecase-folder"])??o.useCaseFolder??"useCases",p=r["with-usecase"]===void 0?s?!0:await t.askBool("Scaffold useCase.ts?",!0):r["with-usecase"]!==!1,b=r["with-test"]===void 0?s||!p?p:await t.askBool("Scaffold useCase.test.ts (Vitest)?",!0):r["with-test"]!==!1,h=r.force===!0,P=path.resolve(process.cwd(),n),y=path.resolve(P,a,g,i),F=path.resolve(y,"routes.ts"),x=`${a}.${i}.useCase`,A=path.resolve(y,`${x}.ts`),C=path.resolve(y,`${x}.test.ts`);fs.mkdirSync(y,{recursive:!0});let R=v=>v.charAt(0).toUpperCase()+v.slice(1),$=`${R(a)}${R(i)}UseCase`,D="@lpdjs/firestore-repo-service/servers/hono",w=Ce(P,y),E=Se(P,y),_=c==="get"?"// GET \u2192 lu depuis les query params":`// ${c.toUpperCase()} \u2192 lu depuis le body JSON`,q=`/**
86
+ `);}function ee(e){if(typeof e=="string")return e.split(",").map(r=>r.trim()).filter(Boolean)}function m(e){return typeof e=="string"?e:void 0}function te(e){if(e||!process$1.stdin.isTTY)return {ask:async(s,t)=>t??"",askChoice:async(s,t,o)=>o??"",askBool:async(s,t)=>t,close:()=>{}};let r=promises.createInterface({input:process$1.stdin,output:process$1.stdout});return {async ask(s,t){let o=t?` (${t})`:"";return (await r.question(`? ${s}${o} \u203A `)).trim()||t||""},async askChoice(s,t,o){let i=` [${t.join("/")}${o?`, default: ${o}`:""}]`;for(;;){let a=(await r.question(`? ${s}${i} \u203A `)).trim().toLowerCase();if(!a&&o)return o;if(t.includes(a))return a;console.log(` invalid choice \u2014 pick one of: ${t.join(", ")}`);}},async askBool(s,t){let o=` (${t?"Y/n":"y/N"})`,i=(await r.question(`? ${s}${o} \u203A `)).trim().toLowerCase();return i?i==="y"||i==="yes"||i==="true":t},close:()=>r.close()}}async function xe(e){let r=z(),s=m(e.root)??r.root;s||(console.error("[frs] --root is required (or run `frs init` to write it to .frsrc.json)"),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=m(e.out)??r.out??"__generated__/routes.ts",i=ee(e.skip)??_.skipSegments,a=m(e.casing)==="kebab"?"kebab":_.casing,n={skipSegments:i,casing:a},c=m(e.ext)??".js",f=ee(e.exclude)??I.excludeSegments,g=m(e["routes-file"])??I.routesFile,b=V(t,{routesFile:g,excludeSegments:f});b.length===0&&console.warn(`[frs] no "${g}" files found under ${t} \u2014 generated an empty manifest.`);let h=K(b,{outFile:path.resolve(t,o),derive:n,importExtension:c});if(!e.silent){console.log(`[frs] wrote ${h.outFile} (${h.routeCount} route${h.routeCount===1?"":"s"})`);for(let{source:P,url:y}of h.derivedPaths)console.log(` ${y.padEnd(48)} \u2190 ${P}`);}}async function we(e,r){let s=r.yes===true,t=te(s),o=z();try{let i=e&&!e.startsWith("--")?e:void 0;i||(i=(await t.ask("Route name (e.g. createPost)")).trim(),i||(console.error("[frs] route name is required"),process.exit(2)));let a=m(r.domain);a||(a=(await t.ask("Domain name (e.g. posts)")).trim(),a||(console.error("[frs] --domain is required"),process.exit(2)));let n=m(r.root)??o.root??"src/domains",c=m(r.method)?.toLowerCase();c||(c=await t.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(c)||(console.error(`[frs] invalid --method: ${c}`),process.exit(2));let f=m(r.api);if(!f){let v=o.apis?.[0]??"v1";f=(await t.ask("API tag",v)).trim()||v;}let g=m(r["usecase-folder"])??o.useCaseFolder??"useCases",p=r["with-usecase"]===void 0?s?!0:await t.askBool("Scaffold useCase.ts?",!0):r["with-usecase"]!==!1,b=r["with-test"]===void 0?s||!p?p:await t.askBool("Scaffold useCase.test.ts (Vitest)?",!0):r["with-test"]!==!1,h=r.force===!0,P=path.resolve(process.cwd(),n),y=path.resolve(P,a,g,i),F=path.resolve(y,"routes.ts"),x=`${a}.${i}.useCase`,A=path.resolve(y,`${x}.ts`),C=path.resolve(y,`${x}.test.ts`);fs.mkdirSync(y,{recursive:!0});let k=v=>v.charAt(0).toUpperCase()+v.slice(1),$=`${k(a)}${k(i)}UseCase`,D="@lpdjs/firestore-repo-service/servers/hono",w=Ce(P,y),E=Se(P,y),L=c==="get"?"// GET \u2192 lu depuis les query params":`// ${c.toUpperCase()} \u2192 lu depuis le body JSON`,U=`/**
87
87
  * ${$} \u2014 pure business logic, no HTTP awareness.
88
88
  *
89
89
  * Owns its Zod \`input\` / \`output\` schemas (declared as \`static\` members, the
@@ -96,7 +96,7 @@ import { z } from "zod";
96
96
  import { AppUseCase } from "${E}";
97
97
 
98
98
  const input = z.object({
99
- ${_}
99
+ ${L}
100
100
  example: z.string(),
101
101
  });
102
102
 
@@ -119,15 +119,15 @@ export class ${$} extends AppUseCase<typeof input, typeof output> {
119
119
  return { id: payload.example };
120
120
  }
121
121
  }
122
- `,N=c==="get"?`
123
- source: "query",`:"",z=p?`import { ${$} } from "./${x}.js";
122
+ `,H=c==="get"?`
123
+ source: "query",`:"",N=p?`import { ${$} } from "./${x}.js";
124
124
  `:"",O=m(r["apis-import"])??Pe(P,y),u=p?`import { defineRoutes } from "${D}";
125
125
  import { useCaseRoute } from "${O}";
126
- ${z}
126
+ ${N}
127
127
  export default defineRoutes([
128
128
  useCaseRoute(${$}, {
129
129
  api: "${f}",
130
- method: "${c}",${N}
130
+ method: "${c}",${H}
131
131
  summary: "TODO: ${i}",
132
132
  tags: ["${a}"],
133
133
  }),
@@ -142,7 +142,7 @@ export default defineRoutes([
142
142
  method: "${c}",
143
143
 
144
144
  input: z.object({
145
- ${_}
145
+ ${L}
146
146
  example: z.string(),
147
147
  }),
148
148
 
@@ -159,7 +159,7 @@ export default defineRoutes([
159
159
  },
160
160
  }),
161
161
  ]);
162
- `,j=[],M=[],B=(v,oe)=>{if(fs.existsSync(v)&&!h){M.push(v);return}fs.writeFileSync(v,oe,"utf8"),j.push(v);};if(B(F,u),p&&B(A,q),p&&b){let v=`import { describe, it, expect } from "vitest";
162
+ `,j=[],G=[],M=(v,oe)=>{if(fs.existsSync(v)&&!h){G.push(v);return}fs.writeFileSync(v,oe,"utf8"),j.push(v);};if(M(F,u),p&&M(A,U),p&&b){let v=`import { describe, it, expect } from "vitest";
163
163
  import type { Services } from "${w}";
164
164
  import { ${$} } from "./${x}.js";
165
165
 
@@ -175,8 +175,8 @@ describe("${$}", () => {
175
175
 
176
176
  // TODO: add error-path tests, repository mocks, etc.
177
177
  });
178
- `;B(C,v);}for(let v of j)console.log(`[frs] wrote ${v}`);for(let v of M)console.log(`[frs] skipped ${v} (use --force to overwrite)`);console.log(`
179
- [frs] reminder: run "frs gen --root ${n}" to refresh the manifest.`);}finally{t.close();}}async function $e(e){let r=e.yes===true,s=te(r);try{let t=e.force===!0,o=m(e.root);o||(o=(await s.ask("Domain root","src/domains")).trim()||"src/domains");let i=m(e["apis-file"]);i||(i=(await s.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let a=m(e["services-file"]);if(!a){let u=i.replace(/apis\.ts$/,"services.ts")||"src/services.ts";a=(await s.ask("services.ts location",u)).trim()||u;}let n=m(e.apis);n||(n=(await s.ask("API tags (comma-separated)","v1")).trim()||"v1");let c=n.split(",").map(u=>u.trim()).filter(Boolean);c.length===0&&(console.error("[frs] at least one API tag is required"),process.exit(2));let f=m(e["base-path"]),g=path.resolve(process.cwd(),o),p=path.resolve(process.cwd(),i),b=path.resolve(process.cwd(),a),h=path.resolve(path.dirname(p),"app-error.ts"),P=path.resolve(path.dirname(p),"base-usecase.ts"),y=path.resolve(g,"__generated__"),F=path.resolve(y,"routes.ts"),x=[],A=[],C=(u,j)=>{if(fs.mkdirSync(path.dirname(u),{recursive:!0}),fs.existsSync(u)&&!t){A.push(u);return}fs.writeFileSync(u,j,"utf8"),x.push(u);},R=c.map(u=>{let j=f??`/${u}`;return ` ${u}: {
178
+ `;M(C,v);}for(let v of j)console.log(`[frs] wrote ${v}`);for(let v of G)console.log(`[frs] skipped ${v} (use --force to overwrite)`);console.log(`
179
+ [frs] reminder: run "frs gen --root ${n}" to refresh the manifest.`);}finally{t.close();}}async function $e(e){let r=e.yes===true,s=te(r);try{let t=e.force===!0,o=m(e.root);o||(o=(await s.ask("Domain root","src/domains")).trim()||"src/domains");let i=m(e["apis-file"]);i||(i=(await s.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let a=m(e["services-file"]);if(!a){let u=i.replace(/apis\.ts$/,"services.ts")||"src/services.ts";a=(await s.ask("services.ts location",u)).trim()||u;}let n=m(e.apis);n||(n=(await s.ask("API tags (comma-separated)","v1")).trim()||"v1");let c=n.split(",").map(u=>u.trim()).filter(Boolean);c.length===0&&(console.error("[frs] at least one API tag is required"),process.exit(2));let f=m(e["base-path"]),g=path.resolve(process.cwd(),o),p=path.resolve(process.cwd(),i),b=path.resolve(process.cwd(),a),h=path.resolve(path.dirname(p),"app-error.ts"),P=path.resolve(path.dirname(p),"base-usecase.ts"),y=path.resolve(g,"__generated__"),F=path.resolve(y,"routes.ts"),x=[],A=[],C=(u,j)=>{if(fs.mkdirSync(path.dirname(u),{recursive:!0}),fs.existsSync(u)&&!t){A.push(u);return}fs.writeFileSync(u,j,"utf8"),x.push(u);},k=c.map(u=>{let j=f??`/${u}`;return ` ${u}: {
180
180
  basePath: "${j}",
181
181
  openapi: {
182
182
  info: { title: "${u.toUpperCase()} API", version: "1.0.0", description: "" },
@@ -191,8 +191,8 @@ describe("${$}", () => {
191
191
  verbose: process.env["NODE_ENV"] !== "production",
192
192
  },`}).join(`
193
193
  `),$=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
194
- import { AppErrorHandler, appLogger } from "${k(path.dirname(p),h)}";
195
- import { services } from "${k(path.dirname(p),b)}";
194
+ import { AppErrorHandler, appLogger } from "${R(path.dirname(p),h)}";
195
+ import { services } from "${R(path.dirname(p),b)}";
196
196
 
197
197
  /**
198
198
  * Single source of truth for every API exposed by this project.
@@ -206,7 +206,7 @@ import { services } from "${k(path.dirname(p),b)}";
206
206
  */
207
207
  export const apis = createApiRegistry(
208
208
  {
209
- ${R}
209
+ ${k}
210
210
  },
211
211
  { services },
212
212
  );
@@ -254,36 +254,103 @@ export type Services = typeof services;
254
254
  type LogSeverity,
255
255
  } from "@lpdjs/firestore-repo-service/servers/hono";
256
256
 
257
+ /** Supported locales \u2014 the single source of truth (runtime + type). */
258
+ export const LOCALES = ["en", "fr"] as const;
259
+
260
+ /** A supported locale, derived from {@link LOCALES}. */
261
+ export type Locale = (typeof LOCALES)[number];
262
+
263
+ /** Localized message \u2014 one string per supported locale. */
264
+ export type LocalizedMessage = Record<Locale, string>;
265
+
257
266
  /**
258
267
  * Domain error \u2014 pure business semantics, zero HTTP awareness. Thrown anywhere
259
268
  * in useCases / handlers; the \`AppErrorHandler\` below maps it to an HTTP
260
- * response. Add your own factory methods as your domain grows.
269
+ * response. Carries a localized message; add your own factory methods as your
270
+ * domain grows.
261
271
  */
262
272
  export class AppError extends Error {
263
273
  readonly statusCode: number;
264
274
  readonly userFacing: boolean;
265
275
  readonly errorId: string;
266
-
267
- private constructor(message: string, statusCode: number, userFacing = false) {
268
- super(message);
276
+ readonly localizedMessage: LocalizedMessage;
277
+
278
+ private constructor(
279
+ localizedMessage: LocalizedMessage,
280
+ statusCode: number,
281
+ userFacing = false,
282
+ ) {
283
+ super(localizedMessage.en);
269
284
  this.name = "AppError";
270
285
  this.statusCode = statusCode;
271
286
  this.userFacing = userFacing;
287
+ this.localizedMessage = localizedMessage;
272
288
  this.errorId = Math.random().toString(36).slice(2, 12);
273
289
  }
274
290
 
275
291
  /** Business message shown directly to the user \u2014 HTTP 412. */
276
- static userMessage(message: string): AppError {
292
+ static userMessage(message: LocalizedMessage): AppError {
277
293
  return new AppError(message, 412, true);
278
294
  }
295
+
279
296
  /** Resource not found \u2014 HTTP 404. */
280
- static notFound(resource = "Resource"): AppError {
281
- return new AppError(\`\${resource} not found\`, 404);
297
+ static notFound(resource?: string): AppError {
298
+ return new AppError(
299
+ {
300
+ en: \`\${resource ?? "Resource"} not found\`,
301
+ fr: \`\${resource ?? "Ressource"} introuvable\`,
302
+ },
303
+ 404,
304
+ );
282
305
  }
306
+
283
307
  /** Malformed request / invalid data \u2014 HTTP 400. */
284
- static badRequest(detail = "invalid parameters"): AppError {
285
- return new AppError(\`Bad request: \${detail}\`, 400);
308
+ static badRequest(detail?: string): AppError {
309
+ return new AppError(
310
+ {
311
+ en: \`Bad request: \${detail ?? "invalid parameters"}\`,
312
+ fr: \`Requ\xEAte invalide : \${detail ?? "param\xE8tres incorrects"}\`,
313
+ },
314
+ 400,
315
+ );
286
316
  }
317
+
318
+ /** Generic fallback message for non-user-facing errors. */
319
+ static default(locale: Locale): string {
320
+ return locale === "fr" ? "Une erreur est survenue" : "An error occurred";
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Pick the response locale from the \`Accept-Language\` header.
326
+ *
327
+ * Parses the comma-separated, q-weighted list (e.g.
328
+ * \`fr-FR,fr;q=0.9,en;q=0.8\`), keeps the supported locales, and returns the one
329
+ * with the highest quality. Falls back to \`"en"\`.
330
+ */
331
+ function pickLocale(c: {
332
+ req: { header(name: string): string | undefined };
333
+ }): Locale {
334
+ const header = c.req.header("accept-language");
335
+ if (!header) return "en";
336
+
337
+ const ranked = header
338
+ .split(",")
339
+ .map((part) => {
340
+ const [tag = "", ...params] = part.trim().split(";");
341
+ const qParam = params.find((p) => p.trim().startsWith("q="));
342
+ const q = qParam ? Number.parseFloat(qParam.split("=")[1] ?? "") : 1;
343
+ return {
344
+ lang: tag.trim().toLowerCase().split("-")[0] ?? "",
345
+ quality: Number.isFinite(q) ? q : 1,
346
+ };
347
+ })
348
+ .filter((x): x is { lang: Locale; quality: number } =>
349
+ (LOCALES as readonly string[]).includes(x.lang),
350
+ )
351
+ .sort((a, b) => b.quality - a.quality);
352
+
353
+ return ranked[0]?.lang ?? "en";
287
354
  }
288
355
 
289
356
  /**
@@ -304,10 +371,10 @@ export class AppLogger extends BaseLogger {
304
371
  export const appLogger = new AppLogger();
305
372
 
306
373
  /**
307
- * Project error strategy \u2014 extends \`BaseErrorHandler\`: \`mapError\` handles our
308
- * \`AppError\` (with an optional GCP logs deep link), \`logError\` routes through
309
- * the logger, and unmatched errors fall back to the built-in mapping via
310
- * \`super\`. Wired per API in apis.ts.
374
+ * Project error strategy \u2014 extends \`BaseErrorHandler\`: \`mapError\` localizes our
375
+ * \`AppError\` (user-facing aware, with an optional GCP logs deep link),
376
+ * \`logError\` routes through the logger, and unmatched errors fall back to the
377
+ * built-in mapping via \`super\`. Wired per API in apis.ts.
311
378
  */
312
379
  export class AppErrorHandler extends BaseErrorHandler {
313
380
  protected override mapError({
@@ -316,11 +383,14 @@ export class AppErrorHandler extends BaseErrorHandler {
316
383
  }: ErrorHandlerContext): Response | null {
317
384
  if (!(error instanceof AppError)) return null; // \u2192 built-in mapping
318
385
 
386
+ const locale = pickLocale(c);
319
387
  const logsUrl = this.gcpLogsUrl(error.errorId); // undefined when disabled
320
388
  return c.json(
321
389
  {
322
- // expose the message only when it is meant for the user
323
- error: error.userFacing ? error.message : "An error occurred",
390
+ // expose the localized message only when it is meant for the user
391
+ error: error.userFacing
392
+ ? error.localizedMessage[locale]
393
+ : AppError.default(locale),
324
394
  errorId: error.errorId,
325
395
  ...(logsUrl ? { logsUrl } : {}),
326
396
  },
@@ -340,8 +410,8 @@ export class AppErrorHandler extends BaseErrorHandler {
340
410
  }
341
411
  `);let E=`import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
342
412
  import type { z } from "zod";
343
- import { AppError, appLogger } from "${k(path.dirname(P),h)}";
344
- import type { Services } from "${k(path.dirname(P),b)}";
413
+ import { AppError, appLogger } from "${R(path.dirname(P),h)}";
414
+ import type { Services } from "${R(path.dirname(P),b)}";
345
415
 
346
416
  /**
347
417
  * Project base class for every useCase \u2014 extends the package's {@link UseCase}
@@ -364,20 +434,20 @@ export abstract class AppUseCase<
364
434
  */
365
435
  protected readonly error = AppError;
366
436
  }
367
- `;C(P,E);let _=`// AUTO-GENERATED by frs \u2014 do not edit.
437
+ `;C(P,E);let L=`// AUTO-GENERATED by frs \u2014 do not edit.
368
438
  // Run \`frs gen --root ${o}\` to refresh.
369
439
 
370
440
  import type { AnyRouteDef } from "@lpdjs/firestore-repo-service/servers/hono";
371
441
 
372
442
  export const routes: AnyRouteDef[] = [];
373
- `;C(F,_);let q=ve({root:o,apisFile:i,servicesFile:a,apis:c});x.push(q);let N=k(path.dirname(p),p),z=k(path.dirname(p),F),O=c.length===1?`export const { ${c[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${c.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let u of x)console.log(`[frs] wrote ${u}`);for(let u of A)console.log(`[frs] skipped ${u} (use --force to overwrite)`);console.log(`
443
+ `;C(F,L);let U=ve({root:o,apisFile:i,servicesFile:a,apis:c});x.push(U);let H=R(path.dirname(p),p),N=R(path.dirname(p),F),O=c.length===1?`export const { ${c[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${c.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let u of x)console.log(`[frs] wrote ${u}`);for(let u of A)console.log(`[frs] skipped ${u} (use --force to overwrite)`);console.log(`
374
444
  Next steps:
375
445
 
376
446
  1. Wire the registry in your Functions entrypoint (e.g. src/index.ts):
377
447
 
378
448
  import { onRequest } from "firebase-functions/v2/https";
379
- import { apis } from "${N}";
380
- import { routes } from "${z}";
449
+ import { apis } from "${H}";
450
+ import { routes } from "${N}";
381
451
 
382
452
  ${O}
383
453
  defaults: { region: "us-central1", invoker: "public" },
@@ -390,7 +460,7 @@ Next steps:
390
460
  3. Refresh the manifest before each build:
391
461
 
392
462
  frs gen --root ${o}
393
- `);}finally{s.close();}}function k(e,r){let s=path.relative(e,r).replace(/\\/g,"/");return s=s.replace(/\.ts$/,".js"),s.startsWith(".")||(s=`./${s}`),s}async function be(e,r,s){e!=="service"&&(console.error(`[frs] unknown "add" target: ${e??"(missing)"} \u2014 supported: service`),process.exit(2)),r||(console.error("[frs] service name is required: frs add service <name>"),process.exit(2));let t=s.force===true,o=U(),a=[m(s["services-file"]),o.servicesFile,"src/services.ts","services.ts"].filter(w=>typeof w=="string"&&w.length>0),n;for(let w of a){let E=path.resolve(process.cwd(),w);if(fs.existsSync(E)){n=E;break}}if(!n){let w=a.map(E=>path.resolve(process.cwd(),E)).join(`
463
+ `);}finally{s.close();}}function R(e,r){let s=path.relative(e,r).replace(/\\/g,"/");return s=s.replace(/\.ts$/,".js"),s.startsWith(".")||(s=`./${s}`),s}async function be(e,r,s){e!=="service"&&(console.error(`[frs] unknown "add" target: ${e??"(missing)"} \u2014 supported: service`),process.exit(2)),r||(console.error("[frs] service name is required: frs add service <name>"),process.exit(2));let t=s.force===true,o=z(),a=[m(s["services-file"]),o.servicesFile,"src/services.ts","services.ts"].filter(w=>typeof w=="string"&&w.length>0),n;for(let w of a){let E=path.resolve(process.cwd(),w);if(fs.existsSync(E)){n=E;break}}if(!n){let w=a.map(E=>path.resolve(process.cwd(),E)).join(`
394
464
  `);console.error(`[frs] services file not found. Tried:
395
465
  ${w}
396
466
  Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let c=m(s["services-dir"])??o.servicesDir??path.resolve(path.dirname(n),"services"),f=path.resolve(process.cwd(),c);fs.mkdirSync(f,{recursive:true});let g=`${r.charAt(0).toUpperCase()}${r.slice(1)}Service`,p=path.resolve(f,`${r}.ts`),b=`import type { RequestContext } from "@lpdjs/firestore-repo-service/servers/hono";
@@ -422,9 +492,9 @@ export class ${g} {
422
492
  return \`hello from ${r} \u2014 user=\${this.ctx.maybeC?.get("user")?.id ?? "anonymous"}\`;
423
493
  }
424
494
  }
425
- `;fs.existsSync(p)&&!t?console.log(`[frs] skipped ${p} (use --force to overwrite)`):(fs.writeFileSync(p,b,"utf8"),console.log(`[frs] wrote ${p}`));let h=fs.readFileSync(n,"utf8"),P=k(path.dirname(n),p),y=`import { ${g} } from "${P}";`,F=` ${r}: ({ ctx }) => new ${g}(ctx),`;if(h.includes(y)){console.log(`[frs] services.ts already registers "${r}" \u2014 skipping.`);return}let x=h.split(`
495
+ `;fs.existsSync(p)&&!t?console.log(`[frs] skipped ${p} (use --force to overwrite)`):(fs.writeFileSync(p,b,"utf8"),console.log(`[frs] wrote ${p}`));let h=fs.readFileSync(n,"utf8"),P=R(path.dirname(n),p),y=`import { ${g} } from "${P}";`,F=` ${r}: ({ ctx }) => new ${g}(ctx),`;if(h.includes(y)){console.log(`[frs] services.ts already registers "${r}" \u2014 skipping.`);return}let x=h.split(`
426
496
  `),A=-1;for(let w=0;w<x.length;w++)/^import\s/.test(x[w])&&(A=w);A>=0?x.splice(A+1,0,y):x.unshift(y);let C=x.join(`
427
- `),R=C.match(/createServices\s*\(\s*\{/);if(!R){console.error(`[frs] could not find \`createServices({\` in ${n} \u2014 register "${r}" manually.`);return}let $=R.index+R[0].length,D=C.slice(0,$)+`
497
+ `),k=C.match(/createServices\s*\(\s*\{/);if(!k){console.error(`[frs] could not find \`createServices({\` in ${n} \u2014 register "${r}" manually.`);return}let $=k.index+k[0].length,D=C.slice(0,$)+`
428
498
  `+F+C.slice($);fs.writeFileSync(n,D,"utf8"),console.log(`[frs] updated ${n} (+ ${r})`);}function Pe(e,r){let s=["apis.ts","apis.js","api.ts","api.js"],t=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let o of t)for(let i of s){let a=path.resolve(o,i);if(fs.existsSync(a)){let n=path.relative(r,a).replace(/\\/g,"/");return n=n.replace(/\.ts$/,".js").replace(/\.js$/,".js"),n.startsWith(".")||(n=`./${n}`),n}}return "../../../../apis.js"}function Ce(e,r){let s=["services.ts","services.js"],t=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let o of t)for(let i of s){let a=path.resolve(o,i);if(fs.existsSync(a)){let n=path.relative(r,a).replace(/\\/g,"/");return n=n.replace(/\.ts$/,".js"),n.startsWith(".")||(n=`./${n}`),n}}return "../../../../services.js"}function Se(e,r){let s=["base-usecase.ts","base-usecase.js"],t=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let o of t)for(let i of s){let a=path.resolve(o,i);if(fs.existsSync(a)){let n=path.relative(r,a).replace(/\\/g,"/");return n=n.replace(/\.ts$/,".js"),n.startsWith(".")||(n=`./${n}`),n}}return "../../../../base-usecase.js"}async function Ae(){let e=process.argv.slice(2),{command:r,flags:s}=ye(e);switch(r){case "init":await $e(s);return;case "gen":await xe(s);return;case "new":await we(e[1],s);return;case "add":await be(e[1],e[2],s);return;case "help":case "--help":case "-h":X();return;default:console.error(`[frs] unknown command: ${r}
429
499
  `),X(),process.exit(2);}}Ae().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.cjs.map
430
500
  //# sourceMappingURL=cli.cjs.map