@lpdjs/firestore-repo-service 2.6.11 → 2.6.12
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-BV-E4Rp-.d.cts → create-servers-Bq9lnpg6.d.cts} +2 -2
- package/dist/{create-servers-BhUavex0.d.ts → create-servers-dXpZiSOT.d.ts} +2 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/servers/hono/cli.cjs +187 -65
- package/dist/servers/hono/cli.cjs.map +1 -1
- package/dist/servers/hono/cli.js +187 -65
- package/dist/servers/hono/cli.js.map +1 -1
- package/dist/servers/hono/index.cjs +1 -1
- package/dist/servers/hono/index.cjs.map +1 -1
- package/dist/servers/hono/index.js +1 -1
- package/dist/servers/hono/index.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
|
@@ -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>):
|
|
79
|
+
admin(options: BoundAdminServerOptions<TRepos>): (((req: any, res: any) => Promise<void>) & {
|
|
80
80
|
httpsOptions?: HttpsOptions;
|
|
81
|
-
});
|
|
81
|
+
}) | firebase_functions_v2_https.HttpsFunction;
|
|
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>):
|
|
79
|
+
admin(options: BoundAdminServerOptions<TRepos>): (((req: any, res: any) => Promise<void>) & {
|
|
80
80
|
httpsOptions?: HttpsOptions;
|
|
81
|
-
});
|
|
81
|
+
}) | firebase_functions_v2_https.HttpsFunction;
|
|
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-
|
|
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';
|
|
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-
|
|
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';
|
|
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,10 +1,10 @@
|
|
|
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
|
|
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"}.
|
|
3
3
|
|
|
4
4
|
import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
5
5
|
|
|
6
|
-
`+
|
|
7
|
-
`)+(
|
|
6
|
+
`+a.join(`
|
|
7
|
+
`)+(a.length?`
|
|
8
8
|
|
|
9
9
|
`:`
|
|
10
10
|
`)+`const __defs: { __derivedPath: string; mod: RouteModuleDefault }[] = [
|
|
@@ -16,8 +16,8 @@ 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(
|
|
20
|
-
`,"utf8"),s}function
|
|
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)}
|
|
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:
|
|
23
23
|
frs init [flags]
|
|
@@ -83,21 +83,20 @@ 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
|
|
87
|
-
* ${
|
|
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=`/**
|
|
87
|
+
* ${$} \u2014 pure business logic, no HTTP awareness.
|
|
88
88
|
*
|
|
89
89
|
* Owns its Zod \`input\` / \`output\` schemas (declared as \`static\` members, the
|
|
90
90
|
* single source of truth shared with \`routes.ts\`) and runs the logic in
|
|
91
|
-
* \`execute\`.
|
|
92
|
-
*
|
|
91
|
+
* \`execute\`. Extends \`AppUseCase\`, so \`this.services\`, \`this.logger\` and
|
|
92
|
+
* \`this.error\` are all available.
|
|
93
93
|
*/
|
|
94
94
|
|
|
95
95
|
import { z } from "zod";
|
|
96
|
-
import {
|
|
97
|
-
import type { Services } from "${h}";
|
|
96
|
+
import { AppUseCase } from "${E}";
|
|
98
97
|
|
|
99
98
|
const input = z.object({
|
|
100
|
-
${
|
|
99
|
+
${_}
|
|
101
100
|
example: z.string(),
|
|
102
101
|
});
|
|
103
102
|
|
|
@@ -105,41 +104,45 @@ const output = z.object({
|
|
|
105
104
|
id: z.string(),
|
|
106
105
|
});
|
|
107
106
|
|
|
108
|
-
export class ${
|
|
107
|
+
export class ${$} extends AppUseCase<typeof input, typeof output> {
|
|
109
108
|
static readonly input = input;
|
|
110
109
|
static readonly output = output;
|
|
111
110
|
|
|
112
111
|
async execute(
|
|
113
112
|
payload: z.infer<typeof input>,
|
|
114
113
|
): Promise<z.infer<typeof output>> {
|
|
114
|
+
this.logger.info("${$} called", { example: payload.example });
|
|
115
|
+
// Guard example \u2014 mapped to HTTP by the AppErrorHandler:
|
|
116
|
+
// if (!payload.example) throw this.error.badRequest("example is required");
|
|
117
|
+
|
|
115
118
|
// TODO: implement using \`this.services\`
|
|
116
119
|
return { id: payload.example };
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
|
-
`,
|
|
120
|
-
source: "query",`:"",
|
|
121
|
-
`:"",
|
|
122
|
-
import { useCaseRoute } from "${
|
|
123
|
-
${
|
|
122
|
+
`,N=c==="get"?`
|
|
123
|
+
source: "query",`:"",z=p?`import { ${$} } from "./${x}.js";
|
|
124
|
+
`:"",O=m(r["apis-import"])??Pe(P,y),u=p?`import { defineRoutes } from "${D}";
|
|
125
|
+
import { useCaseRoute } from "${O}";
|
|
126
|
+
${z}
|
|
124
127
|
export default defineRoutes([
|
|
125
|
-
useCaseRoute(${
|
|
126
|
-
api: "${
|
|
127
|
-
method: "${
|
|
128
|
+
useCaseRoute(${$}, {
|
|
129
|
+
api: "${f}",
|
|
130
|
+
method: "${c}",${N}
|
|
128
131
|
summary: "TODO: ${i}",
|
|
129
|
-
tags: ["${
|
|
132
|
+
tags: ["${a}"],
|
|
130
133
|
}),
|
|
131
134
|
]);
|
|
132
135
|
`:`import { z } from "zod";
|
|
133
136
|
import { defineRoutes } from "${D}";
|
|
134
|
-
import { defineRoute } from "${
|
|
137
|
+
import { defineRoute } from "${O}";
|
|
135
138
|
|
|
136
139
|
export default defineRoutes([
|
|
137
140
|
defineRoute({
|
|
138
|
-
api: "${
|
|
139
|
-
method: "${
|
|
141
|
+
api: "${f}",
|
|
142
|
+
method: "${c}",
|
|
140
143
|
|
|
141
144
|
input: z.object({
|
|
142
|
-
${
|
|
145
|
+
${_}
|
|
143
146
|
example: z.string(),
|
|
144
147
|
}),
|
|
145
148
|
|
|
@@ -148,7 +151,7 @@ export default defineRoutes([
|
|
|
148
151
|
}),
|
|
149
152
|
|
|
150
153
|
summary: "TODO: ${i}",
|
|
151
|
-
tags: ["${
|
|
154
|
+
tags: ["${a}"],
|
|
152
155
|
|
|
153
156
|
handler: async ({ input }) => {
|
|
154
157
|
// TODO: business logic
|
|
@@ -156,38 +159,40 @@ export default defineRoutes([
|
|
|
156
159
|
},
|
|
157
160
|
}),
|
|
158
161
|
]);
|
|
159
|
-
`,
|
|
160
|
-
import type { Services } from "${
|
|
161
|
-
import { ${
|
|
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";
|
|
163
|
+
import type { Services } from "${w}";
|
|
164
|
+
import { ${$} } from "./${x}.js";
|
|
162
165
|
|
|
163
|
-
describe("${
|
|
166
|
+
describe("${$}", () => {
|
|
164
167
|
it("returns a response shaped like the output schema", async () => {
|
|
165
168
|
// TODO: replace with real mocks for the services the useCase consumes.
|
|
166
169
|
const services = {} as unknown as Services;
|
|
167
170
|
|
|
168
|
-
const useCase = new ${
|
|
171
|
+
const useCase = new ${$}(services);
|
|
169
172
|
const result = await useCase.execute({ example: "hello" });
|
|
170
173
|
expect(result).toMatchObject({ id: expect.any(String) });
|
|
171
174
|
});
|
|
172
175
|
|
|
173
176
|
// TODO: add error-path tests, repository mocks, etc.
|
|
174
177
|
});
|
|
175
|
-
`;
|
|
176
|
-
[frs] reminder: run "frs gen --root ${n}" to refresh the manifest.`);}finally{
|
|
177
|
-
basePath: "${
|
|
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}: {
|
|
180
|
+
basePath: "${j}",
|
|
178
181
|
openapi: {
|
|
179
|
-
info: { title: "${
|
|
182
|
+
info: { title: "${u.toUpperCase()} API", version: "1.0.0", description: "" },
|
|
180
183
|
},
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
errorHandler: new
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
logger
|
|
184
|
+
// Maps your AppError \u2192 HTTP (extend it in app-error.ts). \`gcpLogs\` adds a
|
|
185
|
+
// dev-only deep link to the matching GCP log in the error response.
|
|
186
|
+
errorHandler: new AppErrorHandler({
|
|
187
|
+
gcpLogs: { enabled: process.env["NODE_ENV"] !== "production" },
|
|
188
|
+
}),
|
|
189
|
+
// Shared structured logger (same instance exposed as \`this.logger\`).
|
|
190
|
+
logger: appLogger,
|
|
187
191
|
verbose: process.env["NODE_ENV"] !== "production",
|
|
188
192
|
},`}).join(`
|
|
189
|
-
`)
|
|
190
|
-
import {
|
|
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)}";
|
|
191
196
|
|
|
192
197
|
/**
|
|
193
198
|
* Single source of truth for every API exposed by this project.
|
|
@@ -196,8 +201,8 @@ import { services } from "${I(path.dirname(p),P)}";
|
|
|
196
201
|
* Per-API resources injected into every handler / interceptor / error-handler
|
|
197
202
|
* context (override them per API above):
|
|
198
203
|
* - \`services\` \u2014 shared DI container (\`services.ctx.c\` = current request);
|
|
199
|
-
* - \`errorHandler\` \u2014 maps thrown
|
|
200
|
-
* - \`logger\` \u2014 structured logging (
|
|
204
|
+
* - \`errorHandler\` \u2014 maps thrown \`AppError\`s \u2192 HTTP (see app-error.ts);
|
|
205
|
+
* - \`logger\` \u2014 structured logging (also \`this.logger\` in useCases).
|
|
201
206
|
*/
|
|
202
207
|
export const apis = createApiRegistry(
|
|
203
208
|
{
|
|
@@ -209,7 +214,7 @@ ${R}
|
|
|
209
214
|
/** Typed helpers used inside every route file. */
|
|
210
215
|
export const defineRoute = apis.defineRoute;
|
|
211
216
|
export const useCaseRoute = apis.useCaseRoute;
|
|
212
|
-
|
|
217
|
+
`;C(p,$),C(b,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
213
218
|
|
|
214
219
|
/**
|
|
215
220
|
* Global DI container \u2014 declare every singleton (repositories, SDK
|
|
@@ -242,43 +247,160 @@ export const services = createServices({
|
|
|
242
247
|
|
|
243
248
|
/** Convenience type \u2014 \`function fn(svc: Services) { ... }\`. */
|
|
244
249
|
export type Services = typeof services;
|
|
245
|
-
`)
|
|
250
|
+
`),C(h,`import {
|
|
251
|
+
BaseErrorHandler,
|
|
252
|
+
BaseLogger,
|
|
253
|
+
type ErrorHandlerContext,
|
|
254
|
+
type LogSeverity,
|
|
255
|
+
} from "@lpdjs/firestore-repo-service/servers/hono";
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Domain error \u2014 pure business semantics, zero HTTP awareness. Thrown anywhere
|
|
259
|
+
* in useCases / handlers; the \`AppErrorHandler\` below maps it to an HTTP
|
|
260
|
+
* response. Add your own factory methods as your domain grows.
|
|
261
|
+
*/
|
|
262
|
+
export class AppError extends Error {
|
|
263
|
+
readonly statusCode: number;
|
|
264
|
+
readonly userFacing: boolean;
|
|
265
|
+
readonly errorId: string;
|
|
266
|
+
|
|
267
|
+
private constructor(message: string, statusCode: number, userFacing = false) {
|
|
268
|
+
super(message);
|
|
269
|
+
this.name = "AppError";
|
|
270
|
+
this.statusCode = statusCode;
|
|
271
|
+
this.userFacing = userFacing;
|
|
272
|
+
this.errorId = Math.random().toString(36).slice(2, 12);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Business message shown directly to the user \u2014 HTTP 412. */
|
|
276
|
+
static userMessage(message: string): AppError {
|
|
277
|
+
return new AppError(message, 412, true);
|
|
278
|
+
}
|
|
279
|
+
/** Resource not found \u2014 HTTP 404. */
|
|
280
|
+
static notFound(resource = "Resource"): AppError {
|
|
281
|
+
return new AppError(\`\${resource} not found\`, 404);
|
|
282
|
+
}
|
|
283
|
+
/** Malformed request / invalid data \u2014 HTTP 400. */
|
|
284
|
+
static badRequest(detail = "invalid parameters"): AppError {
|
|
285
|
+
return new AppError(\`Bad request: \${detail}\`, 400);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Project logger \u2014 extends \`BaseLogger\` and overrides the single \`write\` hook.
|
|
291
|
+
* Swap \`console\` for \`firebase-functions/v2\` \`logger\` in real code.
|
|
292
|
+
*/
|
|
293
|
+
export class AppLogger extends BaseLogger {
|
|
294
|
+
protected override write(
|
|
295
|
+
severity: LogSeverity,
|
|
296
|
+
payload: Record<string, unknown>,
|
|
297
|
+
): void {
|
|
298
|
+
// eslint-disable-next-line no-console
|
|
299
|
+
console.log(JSON.stringify({ severity, ...payload }));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Shared logger instance (per-API \`logger\` + \`this.logger\` in useCases). */
|
|
304
|
+
export const appLogger = new AppLogger();
|
|
305
|
+
|
|
306
|
+
/**
|
|
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.
|
|
311
|
+
*/
|
|
312
|
+
export class AppErrorHandler extends BaseErrorHandler {
|
|
313
|
+
protected override mapError({
|
|
314
|
+
error,
|
|
315
|
+
c,
|
|
316
|
+
}: ErrorHandlerContext): Response | null {
|
|
317
|
+
if (!(error instanceof AppError)) return null; // \u2192 built-in mapping
|
|
318
|
+
|
|
319
|
+
const logsUrl = this.gcpLogsUrl(error.errorId); // undefined when disabled
|
|
320
|
+
return c.json(
|
|
321
|
+
{
|
|
322
|
+
// expose the message only when it is meant for the user
|
|
323
|
+
error: error.userFacing ? error.message : "An error occurred",
|
|
324
|
+
errorId: error.errorId,
|
|
325
|
+
...(logsUrl ? { logsUrl } : {}),
|
|
326
|
+
},
|
|
327
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
328
|
+
error.statusCode as any,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
protected override logError({ error, logger }: ErrorHandlerContext): void {
|
|
333
|
+
const log = logger ?? appLogger;
|
|
334
|
+
if (error instanceof AppError && error.statusCode < 500) {
|
|
335
|
+
log.warn(error.message);
|
|
336
|
+
} else {
|
|
337
|
+
log.error(error);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
`);let E=`import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
342
|
+
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)}";
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Project base class for every useCase \u2014 extends the package's {@link UseCase}
|
|
348
|
+
* (which injects \`this.services\` via the constructor) and adds two shared
|
|
349
|
+
* ergonomics: \`this.logger\` (structured logger) and \`this.error\` (the
|
|
350
|
+
* {@link AppError} factory, mapped to HTTP by the \`AppErrorHandler\`).
|
|
351
|
+
* Subclasses still declare \`static input\` / \`static output\`.
|
|
352
|
+
*/
|
|
353
|
+
export abstract class AppUseCase<
|
|
354
|
+
TInput extends z.ZodTypeAny = z.ZodTypeAny,
|
|
355
|
+
TOutput extends z.ZodTypeAny = z.ZodTypeAny,
|
|
356
|
+
> extends UseCase<TInput, TOutput, Services> {
|
|
357
|
+
/** Shared structured logger instance (same one injected per-API). */
|
|
358
|
+
protected readonly logger = appLogger;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Domain error factory \u2014 \`throw this.error.notFound(...)\` /
|
|
362
|
+
* \`this.error.badRequest(...)\` / \`this.error.userMessage(...)\`. The thrown
|
|
363
|
+
* {@link AppError} is mapped to an HTTP response by the \`AppErrorHandler\`.
|
|
364
|
+
*/
|
|
365
|
+
protected readonly error = AppError;
|
|
366
|
+
}
|
|
367
|
+
`;C(P,E);let _=`// AUTO-GENERATED by frs \u2014 do not edit.
|
|
246
368
|
// Run \`frs gen --root ${o}\` to refresh.
|
|
247
369
|
|
|
248
370
|
import type { AnyRouteDef } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
249
371
|
|
|
250
372
|
export const routes: AnyRouteDef[] = [];
|
|
251
|
-
|
|
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(`
|
|
252
374
|
Next steps:
|
|
253
375
|
|
|
254
376
|
1. Wire the registry in your Functions entrypoint (e.g. src/index.ts):
|
|
255
377
|
|
|
256
378
|
import { onRequest } from "firebase-functions/v2/https";
|
|
257
|
-
import { apis } from "${
|
|
258
|
-
import { routes } from "${
|
|
379
|
+
import { apis } from "${N}";
|
|
380
|
+
import { routes } from "${z}";
|
|
259
381
|
|
|
260
|
-
${
|
|
382
|
+
${O}
|
|
261
383
|
defaults: { region: "us-central1", invoker: "public" },
|
|
262
384
|
});
|
|
263
385
|
|
|
264
386
|
2. Scaffold a first route:
|
|
265
387
|
|
|
266
|
-
frs new createPost --domain posts --method post --api ${
|
|
388
|
+
frs new createPost --domain posts --method post --api ${c[0]}
|
|
267
389
|
|
|
268
390
|
3. Refresh the manifest before each build:
|
|
269
391
|
|
|
270
392
|
frs gen --root ${o}
|
|
271
|
-
`);}finally{s.close();}}function
|
|
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(`
|
|
272
394
|
`);console.error(`[frs] services file not found. Tried:
|
|
273
|
-
${
|
|
274
|
-
Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let
|
|
395
|
+
${w}
|
|
396
|
+
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";
|
|
275
397
|
|
|
276
398
|
/**
|
|
277
|
-
* ${
|
|
399
|
+
* ${g} \u2014 generated by \`frs add service ${r}\`.
|
|
278
400
|
*
|
|
279
401
|
* Registered with a **factory** in \`services.ts\` so dependencies are
|
|
280
402
|
* destructured at registration time. Add new constructor parameters here
|
|
281
|
-
* and update the factory line (\`({ ctx, otherSvc }) => new ${
|
|
403
|
+
* and update the factory line (\`({ ctx, otherSvc }) => new ${g}(ctx, otherSvc)\`)
|
|
282
404
|
* \u2014 TypeScript will tell you when something is missing.
|
|
283
405
|
*
|
|
284
406
|
* Async resources (DB connections, SDK clients) should stay lazy-loaded
|
|
@@ -292,17 +414,17 @@ Next steps:
|
|
|
292
414
|
* }
|
|
293
415
|
* \`\`\`
|
|
294
416
|
*/
|
|
295
|
-
export class ${
|
|
417
|
+
export class ${g} {
|
|
296
418
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
297
419
|
constructor(private readonly ctx: RequestContext) {}
|
|
298
420
|
|
|
299
421
|
hello(): string {
|
|
300
|
-
return \`hello from ${
|
|
422
|
+
return \`hello from ${r} \u2014 user=\${this.ctx.maybeC?.get("user")?.id ?? "anonymous"}\`;
|
|
301
423
|
}
|
|
302
424
|
}
|
|
303
|
-
`;fs.existsSync(p)&&!
|
|
304
|
-
`),
|
|
305
|
-
`),
|
|
306
|
-
`+
|
|
307
|
-
`),
|
|
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(`
|
|
426
|
+
`),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,$)+`
|
|
428
|
+
`+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
|
+
`),X(),process.exit(2);}}Ae().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.cjs.map
|
|
308
430
|
//# sourceMappingURL=cli.cjs.map
|