@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.
- package/dist/{create-servers-Bq9lnpg6.d.cts → create-servers-BV-E4Rp-.d.cts} +2 -2
- package/dist/{create-servers-dXpZiSOT.d.ts → create-servers-BhUavex0.d.ts} +2 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/servers/hono/cli.cjs +109 -39
- package/dist/servers/hono/cli.cjs.map +1 -1
- package/dist/servers/hono/cli.js +109 -39
- package/dist/servers/hono/cli.js.map +1 -1
- package/dist/servers/index.d.cts +1 -1
- package/dist/servers/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/servers/hono/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {existsSync,mkdirSync,writeFileSync,readFileSync,readdirSync,statSync}from'fs';import {resolve,dirname,relative,join,sep}from'path';import {stdin,stdout}from'process';import {createInterface}from'readline/promises';var
|
|
2
|
+
import {existsSync,mkdirSync,writeFileSync,readFileSync,readdirSync,statSync}from'fs';import {resolve,dirname,relative,join,sep}from'path';import {stdin,stdout}from'process';import {createInterface}from'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=dirname(r.outFile);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 writeFileSync(r.outFile,f,"utf8"),{outFile:r.outFile,routeCount:e.length,derivedPaths:c}}var
|
|
19
|
+
`;return 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=readdirSync(r);}catch{return}for(let i of o){if(s.excludeSegments.includes(i))continue;let a=join(r,i),n;try{n=statSync(a);}catch{continue}if(n.isDirectory())Y(e,a,s,t);else if(n.isFile()&&i===s.routesFile){let c=relative(e,a).split(sep).join("/"),f=c.replace(/\/?[^/]+$/,"");t.push({absPath:a,relPath:c,relDir:f});}}}var se=".frsrc.json";function z(e=process.cwd()){let r=resolve(e,se);if(!existsSync(r))return {};try{return JSON.parse(readFileSync(r,"utf8"))}catch{return {}}}function ve(e,r=process.cwd()){let s=resolve(r,se),o={...z(r),...e};return 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||!stdin.isTTY)return {ask:async(s,t)=>t??"",askChoice:async(s,t,o)=>o??"",askBool:async(s,t)=>t,close:()=>{}};let r=createInterface({input:stdin,output: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=
|
|
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||!stdin.isTTY)return {ask:async(s,t)=>t??"",askChoice:async(s,t,o)=>o??"",askBool:async(s,t)=>t,close:()=>{}};let r=createInterface({input:stdin,output: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=resolve(process.cwd(),s);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: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=resolve(process.cwd(),n),y=resolve(P,a,g,i),F=resolve(y,"routes.ts"),x=`${a}.${i}.useCase`,A=resolve(y,`${x}.ts`),C=resolve(y,`${x}.test.ts`);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
|
-
`,
|
|
123
|
-
source: "query",`:"",
|
|
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
|
-
${
|
|
126
|
+
${N}
|
|
127
127
|
export default defineRoutes([
|
|
128
128
|
useCaseRoute(${$}, {
|
|
129
129
|
api: "${f}",
|
|
130
|
-
method: "${c}",${
|
|
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=[],
|
|
162
|
+
`,j=[],G=[],M=(v,oe)=>{if(existsSync(v)&&!h){G.push(v);return}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
|
-
`;
|
|
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=resolve(process.cwd(),o),p=resolve(process.cwd(),i),b=resolve(process.cwd(),a),h=resolve(dirname(p),"app-error.ts"),P=resolve(dirname(p),"base-usecase.ts"),y=resolve(g,"__generated__"),F=resolve(y,"routes.ts"),x=[],A=[],C=(u,j)=>{if(mkdirSync(dirname(u),{recursive:!0}),existsSync(u)&&!t){A.push(u);return}writeFileSync(u,j,"utf8"),x.push(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=resolve(process.cwd(),o),p=resolve(process.cwd(),i),b=resolve(process.cwd(),a),h=resolve(dirname(p),"app-error.ts"),P=resolve(dirname(p),"base-usecase.ts"),y=resolve(g,"__generated__"),F=resolve(y,"routes.ts"),x=[],A=[],C=(u,j)=>{if(mkdirSync(dirname(u),{recursive:!0}),existsSync(u)&&!t){A.push(u);return}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 "${
|
|
195
|
-
import { services } from "${
|
|
194
|
+
import { AppErrorHandler, appLogger } from "${R(dirname(p),h)}";
|
|
195
|
+
import { services } from "${R(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(dirname(p),b)}";
|
|
|
206
206
|
*/
|
|
207
207
|
export const apis = createApiRegistry(
|
|
208
208
|
{
|
|
209
|
-
${
|
|
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.
|
|
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
|
-
|
|
268
|
-
|
|
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:
|
|
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
|
|
281
|
-
return new AppError(
|
|
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
|
|
285
|
-
return new AppError(
|
|
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\`
|
|
308
|
-
* \`AppError\` (with an optional GCP logs deep link),
|
|
309
|
-
* the logger, and unmatched errors fall back to the
|
|
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
|
|
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 "${
|
|
344
|
-
import type { Services } from "${
|
|
413
|
+
import { AppError, appLogger } from "${R(dirname(P),h)}";
|
|
414
|
+
import type { Services } from "${R(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
|
|
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,
|
|
443
|
+
`;C(F,L);let U=ve({root:o,apisFile:i,servicesFile:a,apis:c});x.push(U);let H=R(dirname(p),p),N=R(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 "${
|
|
380
|
-
import { routes } from "${
|
|
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
|
|
463
|
+
`);}finally{s.close();}}function R(e,r){let s=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=resolve(process.cwd(),w);if(existsSync(E)){n=E;break}}if(!n){let w=a.map(E=>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??resolve(dirname(n),"services"),f=resolve(process.cwd(),c);mkdirSync(f,{recursive:true});let g=`${r.charAt(0).toUpperCase()}${r.slice(1)}Service`,p=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
|
-
`;existsSync(p)&&!t?console.log(`[frs] skipped ${p} (use --force to overwrite)`):(writeFileSync(p,b,"utf8"),console.log(`[frs] wrote ${p}`));let h=readFileSync(n,"utf8"),P=
|
|
495
|
+
`;existsSync(p)&&!t?console.log(`[frs] skipped ${p} (use --force to overwrite)`):(writeFileSync(p,b,"utf8"),console.log(`[frs] wrote ${p}`));let h=readFileSync(n,"utf8"),P=R(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
|
-
`),
|
|
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($);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,dirname(e),dirname(dirname(e))];for(let o of t)for(let i of s){let a=resolve(o,i);if(existsSync(a)){let n=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,dirname(e),dirname(dirname(e))];for(let o of t)for(let i of s){let a=resolve(o,i);if(existsSync(a)){let n=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,dirname(e),dirname(dirname(e))];for(let o of t)for(let i of s){let a=resolve(o,i);if(existsSync(a)){let n=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.js.map
|
|
430
500
|
//# sourceMappingURL=cli.js.map
|