@lpdjs/firestore-repo-service 2.6.2-beta.1 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -14
- package/dist/servers/hono/cli.cjs +103 -79
- package/dist/servers/hono/cli.cjs.map +1 -1
- package/dist/servers/hono/cli.js +103 -79
- package/dist/servers/hono/cli.js.map +1 -1
- package/dist/servers/hono/index.cjs +3 -3
- package/dist/servers/hono/index.cjs.map +1 -1
- package/dist/servers/hono/index.d.cts +175 -16
- package/dist/servers/hono/index.d.ts +175 -16
- package/dist/servers/hono/index.js +3 -3
- package/dist/servers/hono/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -340,28 +340,52 @@ export const apis = createApiRegistry({
|
|
|
340
340
|
});
|
|
341
341
|
|
|
342
342
|
export const defineRoute = apis.defineRoute;
|
|
343
|
+
export const useCaseRoute = apis.useCaseRoute;
|
|
343
344
|
```
|
|
344
345
|
|
|
345
346
|
### Write a route
|
|
346
347
|
|
|
348
|
+
A useCase owns its Zod `input` / `output` schemas (as `static` members) and the
|
|
349
|
+
business logic; `useCaseRoute` wires it into an endpoint in one line.
|
|
350
|
+
|
|
347
351
|
```typescript
|
|
348
|
-
// src/domains/posts/useCases/createPost/
|
|
352
|
+
// src/domains/posts/useCases/createPost/useCase.ts
|
|
349
353
|
import { z } from "zod";
|
|
350
|
-
import {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
354
|
+
import { UseCase } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
355
|
+
import type { Services } from "../../../../services.js";
|
|
356
|
+
|
|
357
|
+
const input = z.object({ title: z.string() });
|
|
358
|
+
const output = z.object({ id: z.string() });
|
|
359
|
+
|
|
360
|
+
export class CreatePostUseCase extends UseCase<typeof input, typeof output, Services> {
|
|
361
|
+
static readonly input = input;
|
|
362
|
+
static readonly output = output;
|
|
363
|
+
|
|
364
|
+
async execute(payload: z.infer<typeof input>): Promise<z.infer<typeof output>> {
|
|
365
|
+
return { id: payload.title };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// src/domains/posts/useCases/createPost/routes.ts
|
|
372
|
+
import { defineRoutes } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
373
|
+
import { useCaseRoute } from "../../../../apis.js";
|
|
374
|
+
import { CreatePostUseCase } from "./useCase.js";
|
|
375
|
+
|
|
376
|
+
export default defineRoutes([
|
|
377
|
+
useCaseRoute(CreatePostUseCase, {
|
|
378
|
+
api: "v1", // typed: only registered tags accepted
|
|
379
|
+
method: "post",
|
|
380
|
+
summary: "Create a post",
|
|
381
|
+
tags: ["posts"],
|
|
382
|
+
}),
|
|
383
|
+
]);
|
|
361
384
|
```
|
|
362
385
|
|
|
363
|
-
|
|
364
|
-
|
|
386
|
+
Need full control (no useCase)? Use `defineRoute({...})` with an inline
|
|
387
|
+
`handler` instead. Expose the same useCase under several APIs by adding more
|
|
388
|
+
`useCaseRoute(...)` entries to the `defineRoutes([...])` array.
|
|
365
389
|
|
|
366
390
|
### Wire Cloud Functions
|
|
367
391
|
|
|
@@ -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
|
|
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 G(e,t=I){let s=new Set(t.skipSegments.map(r=>r.toLowerCase()));return "/"+e.split("/").filter(Boolean).filter(r=>!s.has(r.toLowerCase())).map(r=>t.casing==="kebab"?te(r):r).join("/")}function te(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase()}function B(e,t,s){let i=z(e),r=z(t),n=0;for(;n<i.length&&n<r.length&&i[n]===r[n];)n++;let c=i.length-n,o=r.slice(n),m=(o[o.length-1]??"").replace(/\.[mc]?[tj]sx?$/i,""),u=s===""?m:`${m}${s}`;return o[o.length-1]=u,(c===0?"./":"../".repeat(c))+o.join("/")}function z(e){return e.replace(/\\/g,"/").replace(/\/+$/,"").split("/").filter((s,i)=>!(i===0&&s===""))}var ie="/**\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 M(e,t){let s=path.dirname(t.outFile);fs.mkdirSync(s,{recursive:true});let i=t.banner??ie,r=(t.now??new Date).toISOString(),n=t.importExtension,c=[],o=[],a=[];e.forEach((u,l)=>{let h=B(s,u.absPath,n),v=G(u.relDir,t.derive);c.push(`import mod${l} from ${JSON.stringify(h)};`),o.push(` { __derivedPath: ${JSON.stringify(v)}, mod: mod${l} },`),a.push({source:u.relPath,url:v});});let m=`${i}// Generated at ${r} \u2014 ${e.length} route file${e.length===1?"":"s"}.
|
|
3
3
|
|
|
4
4
|
import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
5
5
|
|
|
@@ -8,16 +8,16 @@ import type { AnyRouteDef, RouteModuleDefault } from "@lpdjs/firestore-repo-serv
|
|
|
8
8
|
|
|
9
9
|
`:`
|
|
10
10
|
`)+`const __defs: { __derivedPath: string; mod: RouteModuleDefault }[] = [
|
|
11
|
-
`+
|
|
12
|
-
`)+(
|
|
11
|
+
`+o.join(`
|
|
12
|
+
`)+(o.length?`
|
|
13
13
|
`:"")+`];
|
|
14
14
|
|
|
15
15
|
export const routes: AnyRouteDef[] = __defs.flatMap(({ __derivedPath, mod }) => {
|
|
16
16
|
const list = Array.isArray(mod) ? mod : [mod];
|
|
17
17
|
return list.map((route) => ({ ...route, path: route.path ?? __derivedPath }));
|
|
18
18
|
});
|
|
19
|
-
`;return fs.writeFileSync(t.outFile,
|
|
20
|
-
`,"utf8"),s}function
|
|
19
|
+
`;return fs.writeFileSync(t.outFile,m,"utf8"),{outFile:t.outFile,routeCount:e.length,derivedPaths:a}}var E={routesFile:"routes.ts",excludeSegments:["node_modules","__generated__","tests","__tests__",".turbo","dist","build",".next"]};function H(e,t=E){let s=[];return W(e,e,t,s),s.sort((i,r)=>i.relPath.localeCompare(r.relPath)),s}function W(e,t,s,i){let r;try{r=fs.readdirSync(t);}catch{return}for(let n of r){if(s.excludeSegments.includes(n))continue;let c=path.join(t,n),o;try{o=fs.statSync(c);}catch{continue}if(o.isDirectory())W(e,c,s,i);else if(o.isFile()&&n===s.routesFile){let a=path.relative(e,c).split(path.sep).join("/"),m=a.replace(/\/?[^/]+$/,"");i.push({absPath:c,relPath:a,relDir:m});}}}var Z=".frsrc.json";function Q(e=process.cwd()){let t=path.resolve(e,Z);if(!fs.existsSync(t))return {};try{return JSON.parse(fs.readFileSync(t,"utf8"))}catch{return {}}}function fe(e,t=process.cwd()){let s=path.resolve(t,Z),r={...Q(t),...e};return fs.writeFileSync(s,`${JSON.stringify(r,null,2)}
|
|
20
|
+
`,"utf8"),s}function me(e){let[t,...s]=e,i={};for(let r=0;r<s.length;r++){let n=s[r];if(!n.startsWith("--"))continue;let c=n.slice(2),o=s[r+1];o&&!o.startsWith("--")?(i[c]=o,r++):i[c]=true;}return {command:t??"help",flags:i}}function K(){console.log(`frs \u2014 Hono file-based codegen
|
|
21
21
|
|
|
22
22
|
Usage:
|
|
23
23
|
frs init [flags]
|
|
@@ -78,82 +78,105 @@ Examples:
|
|
|
78
78
|
frs new createPost --domain posts --method post
|
|
79
79
|
frs new listPosts --domain posts --method get --api v1
|
|
80
80
|
frs add service postRepo
|
|
81
|
-
`);}function
|
|
82
|
-
* ${
|
|
83
|
-
*
|
|
81
|
+
`);}function V(e){if(typeof e=="string")return e.split(",").map(t=>t.trim()).filter(Boolean)}function f(e){return typeof e=="string"?e:void 0}function X(e){if(e||!process$1.stdin.isTTY)return {ask:async(s,i)=>i??"",askChoice:async(s,i,r)=>r??"",askBool:async(s,i)=>i,close:()=>{}};let t=promises.createInterface({input:process$1.stdin,output:process$1.stdout});return {async ask(s,i){let r=i?` (${i})`:"";return (await t.question(`? ${s}${r} \u203A `)).trim()||i||""},async askChoice(s,i,r){let n=` [${i.join("/")}${r?`, default: ${r}`:""}]`;for(;;){let c=(await t.question(`? ${s}${n} \u203A `)).trim().toLowerCase();if(!c&&r)return r;if(i.includes(c))return c;console.log(` invalid choice \u2014 pick one of: ${i.join(", ")}`);}},async askBool(s,i){let r=` (${i?"Y/n":"y/N"})`,n=(await t.question(`? ${s}${r} \u203A `)).trim().toLowerCase();return n?n==="y"||n==="yes"||n==="true":i},close:()=>t.close()}}async function ge(e){let t=f(e.root);t||(console.error("[frs] --root is required"),process.exit(2));let s=path.resolve(process.cwd(),t);fs.existsSync(s)||(console.error(`[frs] root not found: ${s}`),process.exit(2));let i=f(e.out)??"__generated__/routes.ts",r=V(e.skip)??I.skipSegments,n=f(e.casing)==="kebab"?"kebab":I.casing,c={skipSegments:r,casing:n},o=f(e.ext)??".js",a=V(e.exclude)??E.excludeSegments,m=f(e["routes-file"])??E.routesFile,l=H(s,{routesFile:m,excludeSegments:a});l.length===0&&console.warn(`[frs] no "${m}" files found under ${s} \u2014 generated an empty manifest.`);let h=M(l,{outFile:path.resolve(s,i),derive:c,importExtension:o});if(!e.silent){console.log(`[frs] wrote ${h.outFile} (${h.routeCount} route${h.routeCount===1?"":"s"})`);for(let{source:v,url:y}of h.derivedPaths)console.log(` ${y.padEnd(48)} \u2190 ${v}`);}}async function he(e,t){let s=t.yes===true,i=X(s);try{let r=e&&!e.startsWith("--")?e:void 0;r||(r=(await i.ask("Route name (e.g. createPost)")).trim(),r||(console.error("[frs] route name is required"),process.exit(2)));let n=f(t.domain);n||(n=(await i.ask("Domain name (e.g. posts)")).trim(),n||(console.error("[frs] --domain is required"),process.exit(2)));let c=f(t.root)??"src/domains",o=f(t.method)?.toLowerCase();o||(o=await i.askChoice("HTTP method",["get","post","put","patch","delete"],"post")),["get","post","put","patch","delete"].includes(o)||(console.error(`[frs] invalid --method: ${o}`),process.exit(2));let a=f(t.api);a||(a=(await i.ask("API tag","v1")).trim()||"v1");let m=f(t["usecase-folder"])??"useCases",u=t["with-usecase"]===void 0?s?!0:await i.askBool("Scaffold useCase.ts?",!0):t["with-usecase"]!==!1,l=t["with-test"]===void 0?s||!u?u:await i.askBool("Scaffold useCase.test.ts (Vitest)?",!0):t["with-test"]!==!1,h=t.force===!0,v=path.resolve(process.cwd(),c),y=path.resolve(v,n,m,r),b=path.resolve(y,"routes.ts"),F=path.resolve(y,"useCase.ts"),x=path.resolve(y,"useCase.test.ts");fs.mkdirSync(y,{recursive:!0});let $=`${r.charAt(0).toUpperCase()}${r.slice(1)}UseCase`,P="@lpdjs/firestore-repo-service/servers/hono",k=we(v,y),D=o==="get"?"// GET \u2192 lu depuis les query params":`// ${o.toUpperCase()} \u2192 lu depuis le body JSON`,A=`/**
|
|
82
|
+
* ${$} \u2014 pure business logic, no HTTP awareness.
|
|
83
|
+
*
|
|
84
|
+
* Owns its Zod \`input\` / \`output\` schemas (declared as \`static\` members, the
|
|
85
|
+
* single source of truth shared with \`routes.ts\`) and runs the logic in
|
|
86
|
+
* \`execute\`. The shared \`services\` container is injected by the \`UseCase\` base
|
|
87
|
+
* class via the constructor.
|
|
84
88
|
*/
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
+
import { z } from "zod";
|
|
91
|
+
import { UseCase } from "${P}";
|
|
92
|
+
import type { Services } from "${k}";
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
94
|
+
const input = z.object({
|
|
95
|
+
${D}
|
|
96
|
+
example: z.string(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const output = z.object({
|
|
100
|
+
id: z.string(),
|
|
101
|
+
});
|
|
95
102
|
|
|
96
|
-
export class ${
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
export class ${$} extends UseCase<typeof input, typeof output, Services> {
|
|
104
|
+
static readonly input = input;
|
|
105
|
+
static readonly output = output;
|
|
99
106
|
|
|
100
|
-
async execute(
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
async execute(
|
|
108
|
+
payload: z.infer<typeof input>,
|
|
109
|
+
): Promise<z.infer<typeof output>> {
|
|
110
|
+
// TODO: implement using \`this.services\`
|
|
111
|
+
return { id: payload.example };
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
|
-
`,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
import { defineRoute } from "${f(t["apis-import"])??ve(y,$)}";
|
|
117
|
-
${A}
|
|
118
|
-
export default defineRoute({
|
|
119
|
-
api: "${a}",
|
|
120
|
-
method: "${n}",
|
|
121
|
-
|
|
122
|
-
input: ${F},
|
|
123
|
-
|
|
124
|
-
output: z.object({
|
|
125
|
-
id: z.string(),
|
|
114
|
+
`,g=o==="get"?`
|
|
115
|
+
source: "query",`:"",C=u?`import { ${$} } from "./useCase.js";
|
|
116
|
+
`:"",j=f(t["apis-import"])??$e(v,y),d=u?`import { defineRoutes } from "${P}";
|
|
117
|
+
import { useCaseRoute } from "${j}";
|
|
118
|
+
${C}
|
|
119
|
+
export default defineRoutes([
|
|
120
|
+
useCaseRoute(${$}, {
|
|
121
|
+
api: "${a}",
|
|
122
|
+
method: "${o}",${g}
|
|
123
|
+
summary: "TODO: ${r}",
|
|
124
|
+
tags: ["${n}"],
|
|
126
125
|
}),
|
|
126
|
+
]);
|
|
127
|
+
`:`import { z } from "zod";
|
|
128
|
+
import { defineRoutes } from "${P}";
|
|
129
|
+
import { defineRoute } from "${j}";
|
|
130
|
+
|
|
131
|
+
export default defineRoutes([
|
|
132
|
+
defineRoute({
|
|
133
|
+
api: "${a}",
|
|
134
|
+
method: "${o}",
|
|
135
|
+
|
|
136
|
+
input: z.object({
|
|
137
|
+
${D}
|
|
138
|
+
example: z.string(),
|
|
139
|
+
}),
|
|
140
|
+
|
|
141
|
+
output: z.object({
|
|
142
|
+
id: z.string(),
|
|
143
|
+
}),
|
|
144
|
+
|
|
145
|
+
summary: "TODO: ${r}",
|
|
146
|
+
tags: ["${n}"],
|
|
147
|
+
|
|
148
|
+
handler: async ({ input }) => {
|
|
149
|
+
// TODO: business logic
|
|
150
|
+
return { id: input.example };
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
]);
|
|
154
|
+
`,_=[],U=[],q=(S,ee)=>{if(fs.existsSync(S)&&!h){U.push(S);return}fs.writeFileSync(S,ee,"utf8"),_.push(S);};if(q(b,d),u&&q(F,A),u&&l){let S=`import { describe, it, expect } from "vitest";
|
|
155
|
+
import type { Services } from "${k}";
|
|
156
|
+
import { ${$} } from "./useCase.js";
|
|
127
157
|
|
|
128
|
-
|
|
129
|
-
tags: ["${i}"],
|
|
130
|
-
|
|
131
|
-
handler: async ({ input }) => {
|
|
132
|
-
${_}
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
`,j=[],u=[],R=(w,Q)=>{if(fs.existsSync(w)&&!v){u.push(w);return}fs.writeFileSync(w,Q,"utf8"),j.push(w);};if(R(b,S),p&&R(D,C),p&&l){let w=`import { describe, it, expect } from "vitest";
|
|
136
|
-
import { ${m} } from "./useCase.js";
|
|
137
|
-
|
|
138
|
-
describe("${m}", () => {
|
|
158
|
+
describe("${$}", () => {
|
|
139
159
|
it("returns a response shaped like the output schema", async () => {
|
|
140
|
-
|
|
160
|
+
// TODO: replace with real mocks for the services the useCase consumes.
|
|
161
|
+
const services = {} as unknown as Services;
|
|
162
|
+
|
|
163
|
+
const useCase = new ${$}(services);
|
|
141
164
|
const result = await useCase.execute({ example: "hello" });
|
|
142
165
|
expect(result).toMatchObject({ id: expect.any(String) });
|
|
143
166
|
});
|
|
144
167
|
|
|
145
168
|
// TODO: add error-path tests, repository mocks, etc.
|
|
146
169
|
});
|
|
147
|
-
`;
|
|
148
|
-
[frs] reminder: run "frs gen --root ${c}" to refresh the manifest.`);}finally{
|
|
149
|
-
basePath: "${
|
|
170
|
+
`;q(x,S);}for(let S of _)console.log(`[frs] wrote ${S}`);for(let S of U)console.log(`[frs] skipped ${S} (use --force to overwrite)`);console.log(`
|
|
171
|
+
[frs] reminder: run "frs gen --root ${c}" to refresh the manifest.`);}finally{i.close();}}async function ve(e){let t=e.yes===true,s=X(t);try{let i=e.force===!0,r=f(e.root);r||(r=(await s.ask("Domain root","src/domains")).trim()||"src/domains");let n=f(e["apis-file"]);n||(n=(await s.ask("apis.ts location","src/apis.ts")).trim()||"src/apis.ts");let c=f(e["services-file"]);if(!c){let d=n.replace(/apis\.ts$/,"services.ts")||"src/services.ts";c=(await s.ask("services.ts location",d)).trim()||d;}let o=f(e.apis);o||(o=(await s.ask("API tags (comma-separated)","v1")).trim()||"v1");let a=o.split(",").map(d=>d.trim()).filter(Boolean);a.length===0&&(console.error("[frs] at least one API tag is required"),process.exit(2));let m=f(e["base-path"]),u=path.resolve(process.cwd(),r),l=path.resolve(process.cwd(),n),h=path.resolve(process.cwd(),c),v=path.resolve(u,"__generated__"),y=path.resolve(v,"routes.ts"),b=[],F=[],x=(d,_)=>{if(fs.mkdirSync(path.dirname(d),{recursive:!0}),fs.existsSync(d)&&!i){F.push(d);return}fs.writeFileSync(d,_,"utf8"),b.push(d);},$=a.map(d=>{let _=m??`/${d}`;return ` ${d}: {
|
|
172
|
+
basePath: "${_}",
|
|
150
173
|
openapi: {
|
|
151
|
-
info: { title: "${
|
|
174
|
+
info: { title: "${d.toUpperCase()} API", version: "1.0.0", description: "" },
|
|
152
175
|
},
|
|
153
176
|
verbose: process.env["NODE_ENV"] !== "production",
|
|
154
177
|
},`}).join(`
|
|
155
|
-
`),
|
|
156
|
-
import { services } from "${T(path.dirname(l),
|
|
178
|
+
`),P=`import { createApiRegistry } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
179
|
+
import { services } from "${T(path.dirname(l),h)}";
|
|
157
180
|
|
|
158
181
|
/**
|
|
159
182
|
* Single source of truth for every API exposed by this project.
|
|
@@ -165,14 +188,15 @@ import { services } from "${T(path.dirname(l),v)}";
|
|
|
165
188
|
*/
|
|
166
189
|
export const apis = createApiRegistry(
|
|
167
190
|
{
|
|
168
|
-
${
|
|
191
|
+
${$}
|
|
169
192
|
},
|
|
170
193
|
{ services },
|
|
171
194
|
);
|
|
172
195
|
|
|
173
|
-
/** Typed
|
|
196
|
+
/** Typed helpers used inside every route file. */
|
|
174
197
|
export const defineRoute = apis.defineRoute;
|
|
175
|
-
|
|
198
|
+
export const useCaseRoute = apis.useCaseRoute;
|
|
199
|
+
`;x(l,P),x(h,`import { createServices } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
176
200
|
|
|
177
201
|
/**
|
|
178
202
|
* Global DI container \u2014 declare every singleton (repositories, SDK
|
|
@@ -205,20 +229,20 @@ export const services = createServices({
|
|
|
205
229
|
|
|
206
230
|
/** Convenience type \u2014 \`function fn(svc: Services) { ... }\`. */
|
|
207
231
|
export type Services = typeof services;
|
|
208
|
-
`);let
|
|
232
|
+
`);let D=`// AUTO-GENERATED by frs \u2014 do not edit.
|
|
209
233
|
// Run \`frs gen --root ${r}\` to refresh.
|
|
210
234
|
|
|
211
235
|
import type { AnyRouteDef } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
212
236
|
|
|
213
237
|
export const routes: AnyRouteDef[] = [];
|
|
214
|
-
`;x(
|
|
238
|
+
`;x(y,D);let A=fe({root:r,apisFile:n,servicesFile:c});b.push(A);let g=T(path.dirname(l),l),C=T(path.dirname(l),y),j=a.length===1?`export const { ${a[0]} } = apis.toFunctions(routes, onRequest, {`:`export const { ${a.join(", ")} } = apis.toFunctions(routes, onRequest, {`;for(let d of b)console.log(`[frs] wrote ${d}`);for(let d of F)console.log(`[frs] skipped ${d} (use --force to overwrite)`);console.log(`
|
|
215
239
|
Next steps:
|
|
216
240
|
|
|
217
241
|
1. Wire the registry in your Functions entrypoint (e.g. src/index.ts):
|
|
218
242
|
|
|
219
243
|
import { onRequest } from "firebase-functions/v2/https";
|
|
220
|
-
import { apis } from "${
|
|
221
|
-
import { routes } from "${
|
|
244
|
+
import { apis } from "${g}";
|
|
245
|
+
import { routes } from "${C}";
|
|
222
246
|
|
|
223
247
|
${j}
|
|
224
248
|
defaults: { region: "us-central1", invoker: "public" },
|
|
@@ -231,17 +255,17 @@ Next steps:
|
|
|
231
255
|
3. Refresh the manifest before each build:
|
|
232
256
|
|
|
233
257
|
frs gen --root ${r}
|
|
234
|
-
`);}finally{s.close();}}function T(e,t){let s=path.relative(e,t).replace(/\\/g,"/");return s=s.replace(/\.ts$/,".js"),s.startsWith(".")||(s=`./${s}`),s}async function
|
|
258
|
+
`);}finally{s.close();}}function T(e,t){let s=path.relative(e,t).replace(/\\/g,"/");return s=s.replace(/\.ts$/,".js"),s.startsWith(".")||(s=`./${s}`),s}async function ye(e,t,s){e!=="service"&&(console.error(`[frs] unknown "add" target: ${e??"(missing)"} \u2014 supported: service`),process.exit(2)),t||(console.error("[frs] service name is required: frs add service <name>"),process.exit(2));let i=s.force===true,r=Q(),c=[f(s["services-file"]),r.servicesFile,"src/services.ts","services.ts"].filter(g=>typeof g=="string"&&g.length>0),o;for(let g of c){let C=path.resolve(process.cwd(),g);if(fs.existsSync(C)){o=C;break}}if(!o){let g=c.map(C=>path.resolve(process.cwd(),C)).join(`
|
|
235
259
|
`);console.error(`[frs] services file not found. Tried:
|
|
236
|
-
${
|
|
237
|
-
Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let a=f(s["services-dir"])??r.servicesDir??path.resolve(path.dirname(
|
|
260
|
+
${g}
|
|
261
|
+
Run \`frs init\` first or pass --services-file <path>.`),process.exit(2);}let a=f(s["services-dir"])??r.servicesDir??path.resolve(path.dirname(o),"services"),m=path.resolve(process.cwd(),a);fs.mkdirSync(m,{recursive:true});let u=`${t.charAt(0).toUpperCase()}${t.slice(1)}Service`,l=path.resolve(m,`${t}.ts`),h=`import type { RequestContext } from "@lpdjs/firestore-repo-service/servers/hono";
|
|
238
262
|
|
|
239
263
|
/**
|
|
240
|
-
* ${
|
|
264
|
+
* ${u} \u2014 generated by \`frs add service ${t}\`.
|
|
241
265
|
*
|
|
242
266
|
* Registered with a **factory** in \`services.ts\` so dependencies are
|
|
243
267
|
* destructured at registration time. Add new constructor parameters here
|
|
244
|
-
* and update the factory line (\`({ ctx, otherSvc }) => new ${
|
|
268
|
+
* and update the factory line (\`({ ctx, otherSvc }) => new ${u}(ctx, otherSvc)\`)
|
|
245
269
|
* \u2014 TypeScript will tell you when something is missing.
|
|
246
270
|
*
|
|
247
271
|
* Async resources (DB connections, SDK clients) should stay lazy-loaded
|
|
@@ -255,7 +279,7 @@ Next steps:
|
|
|
255
279
|
* }
|
|
256
280
|
* \`\`\`
|
|
257
281
|
*/
|
|
258
|
-
export class ${
|
|
282
|
+
export class ${u} {
|
|
259
283
|
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
260
284
|
constructor(private readonly ctx: RequestContext) {}
|
|
261
285
|
|
|
@@ -263,9 +287,9 @@ export class ${p} {
|
|
|
263
287
|
return \`hello from ${t} \u2014 user=\${this.ctx.maybeC?.get("user")?.id ?? "anonymous"}\`;
|
|
264
288
|
}
|
|
265
289
|
}
|
|
266
|
-
`;fs.existsSync(l)&&!
|
|
267
|
-
`)
|
|
268
|
-
`),
|
|
269
|
-
`+
|
|
270
|
-
`),
|
|
290
|
+
`;fs.existsSync(l)&&!i?console.log(`[frs] skipped ${l} (use --force to overwrite)`):(fs.writeFileSync(l,h,"utf8"),console.log(`[frs] wrote ${l}`));let v=fs.readFileSync(o,"utf8"),y=T(path.dirname(o),l),b=`import { ${u} } from "${y}";`,F=` ${t}: ({ ctx }) => new ${u}(ctx),`;if(v.includes(b)){console.log(`[frs] services.ts already registers "${t}" \u2014 skipping.`);return}let x=v.split(`
|
|
291
|
+
`),$=-1;for(let g=0;g<x.length;g++)/^import\s/.test(x[g])&&($=g);$>=0?x.splice($+1,0,b):x.unshift(b);let P=x.join(`
|
|
292
|
+
`),k=P.match(/createServices\s*\(\s*\{/);if(!k){console.error(`[frs] could not find \`createServices({\` in ${o} \u2014 register "${t}" manually.`);return}let D=k.index+k[0].length,A=P.slice(0,D)+`
|
|
293
|
+
`+F+P.slice(D);fs.writeFileSync(o,A,"utf8"),console.log(`[frs] updated ${o} (+ ${t})`);}function $e(e,t){let s=["apis.ts","apis.js","api.ts","api.js"],i=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let r of i)for(let n of s){let c=path.resolve(r,n);if(fs.existsSync(c)){let o=path.relative(t,c).replace(/\\/g,"/");return o=o.replace(/\.ts$/,".js").replace(/\.js$/,".js"),o.startsWith(".")||(o=`./${o}`),o}}return "../../../../apis.js"}function we(e,t){let s=["services.ts","services.js"],i=[e,path.dirname(e),path.dirname(path.dirname(e))];for(let r of i)for(let n of s){let c=path.resolve(r,n);if(fs.existsSync(c)){let o=path.relative(t,c).replace(/\\/g,"/");return o=o.replace(/\.ts$/,".js"),o.startsWith(".")||(o=`./${o}`),o}}return "../../../../services.js"}async function xe(){let e=process.argv.slice(2),{command:t,flags:s}=me(e);switch(t){case "init":await ve(s);return;case "gen":await ge(s);return;case "new":await he(e[1],s);return;case "add":await ye(e[1],e[2],s);return;case "help":case "--help":case "-h":K();return;default:console.error(`[frs] unknown command: ${t}
|
|
294
|
+
`),K(),process.exit(2);}}xe().catch(e=>{console.error(e),process.exit(1);});//# sourceMappingURL=cli.cjs.map
|
|
271
295
|
//# sourceMappingURL=cli.cjs.map
|