@beignet/cli 0.0.3 → 0.0.5
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/CHANGELOG.md +219 -0
- package/README.md +379 -61
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +49 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +53 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +41 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +78 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +10 -5
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +28 -47
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -603
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1896 -382
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates/base.d.ts +13 -0
- package/dist/templates/base.d.ts.map +1 -0
- package/dist/templates/base.js +300 -0
- package/dist/templates/base.js.map +1 -0
- package/dist/templates/db.d.ts +14 -0
- package/dist/templates/db.d.ts.map +1 -0
- package/dist/templates/db.js +962 -0
- package/dist/templates/db.js.map +1 -0
- package/dist/templates/index.d.ts +14 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +123 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/server.d.ts +26 -0
- package/dist/templates/server.d.ts.map +1 -0
- package/dist/templates/server.js +520 -0
- package/dist/templates/server.js.map +1 -0
- package/dist/templates/shadcn.d.ts +5 -0
- package/dist/templates/shadcn.d.ts.map +1 -0
- package/dist/templates/shadcn.js +555 -0
- package/dist/templates/shadcn.js.map +1 -0
- package/dist/templates/shared.d.ts +49 -0
- package/dist/templates/shared.d.ts.map +1 -0
- package/dist/templates/shared.js +57 -0
- package/dist/templates/shared.js.map +1 -0
- package/dist/templates/shell.d.ts +5 -0
- package/dist/templates/shell.d.ts.map +1 -0
- package/dist/templates/shell.js +1190 -0
- package/dist/templates/shell.js.map +1 -0
- package/dist/templates/todos.d.ts +17 -0
- package/dist/templates/todos.d.ts.map +1 -0
- package/dist/templates/todos.js +607 -0
- package/dist/templates/todos.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +83 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +114 -0
- package/src/create.ts +40 -64
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1075 -817
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2389 -406
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates/base.ts +377 -0
- package/src/templates/db.ts +963 -0
- package/src/templates/index.ts +148 -0
- package/src/templates/server.ts +528 -0
- package/src/templates/shadcn.ts +570 -0
- package/src/templates/shared.ts +90 -0
- package/src/templates/shell.ts +1219 -0
- package/src/templates/todos.ts +607 -0
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/dist/templates.d.ts +0 -55
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -3520
- package/dist/templates.js.map +0 -1
- package/src/create-bin.ts +0 -11
- package/src/templates.ts +0 -3774
package/dist/templates.js
DELETED
|
@@ -1,3520 +0,0 @@
|
|
|
1
|
-
const externalVersions = {
|
|
2
|
-
next: "^16.2.4",
|
|
3
|
-
react: "^19.2.5",
|
|
4
|
-
reactDom: "^19.2.5",
|
|
5
|
-
typescript: "^5.3.0",
|
|
6
|
-
typesBun: "^1.3.13",
|
|
7
|
-
typesNode: "^20.10.0",
|
|
8
|
-
typesReact: "^19.0.0",
|
|
9
|
-
typesReactDom: "^19.0.0",
|
|
10
|
-
zod: "^4.0.0",
|
|
11
|
-
tanstackReactQuery: "^5.100.7",
|
|
12
|
-
hookformResolvers: "^5.0.0",
|
|
13
|
-
reactHookForm: "^7.74.0",
|
|
14
|
-
betterAuth: "^1.3.26",
|
|
15
|
-
drizzleKit: "^0.30.0",
|
|
16
|
-
drizzleOrm: "^0.38.0",
|
|
17
|
-
inngest: "^3.0.0",
|
|
18
|
-
libsqlClient: "^0.14.0",
|
|
19
|
-
pino: "^9.7.0",
|
|
20
|
-
resend: "^4.0.1",
|
|
21
|
-
upstashRateLimit: "^2.0.0",
|
|
22
|
-
upstashRedis: "^1.0.0",
|
|
23
|
-
};
|
|
24
|
-
function isStandardPreset(ctx) {
|
|
25
|
-
return ctx.preset === "standard";
|
|
26
|
-
}
|
|
27
|
-
function hasDrizzleTurso(ctx) {
|
|
28
|
-
return hasIntegration(ctx, "drizzle-turso");
|
|
29
|
-
}
|
|
30
|
-
function packageRunner(ctx) {
|
|
31
|
-
switch (ctx.packageManager) {
|
|
32
|
-
case "npm":
|
|
33
|
-
return "npx -p @beignet/cli beignet";
|
|
34
|
-
case "pnpm":
|
|
35
|
-
return "pnpm dlx @beignet/cli beignet";
|
|
36
|
-
case "yarn":
|
|
37
|
-
return "yarn dlx @beignet/cli beignet";
|
|
38
|
-
default:
|
|
39
|
-
return "bunx -p @beignet/cli beignet";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Supported scaffold feature choices.
|
|
44
|
-
*/
|
|
45
|
-
export const featureChoices = [
|
|
46
|
-
"client",
|
|
47
|
-
"react-query",
|
|
48
|
-
"forms",
|
|
49
|
-
"openapi",
|
|
50
|
-
];
|
|
51
|
-
/**
|
|
52
|
-
* Supported scaffold preset choices.
|
|
53
|
-
*/
|
|
54
|
-
export const presetChoices = [
|
|
55
|
-
"minimal",
|
|
56
|
-
"standard",
|
|
57
|
-
];
|
|
58
|
-
/**
|
|
59
|
-
* Supported scaffold integration choices.
|
|
60
|
-
*/
|
|
61
|
-
export const integrationChoices = [
|
|
62
|
-
"better-auth",
|
|
63
|
-
"drizzle-turso",
|
|
64
|
-
"inngest",
|
|
65
|
-
"pino",
|
|
66
|
-
"resend",
|
|
67
|
-
"upstash-rate-limit",
|
|
68
|
-
];
|
|
69
|
-
const integrationDeps = {
|
|
70
|
-
"better-auth": {
|
|
71
|
-
"@beignet/provider-auth-better-auth": "",
|
|
72
|
-
"better-auth": externalVersions.betterAuth,
|
|
73
|
-
},
|
|
74
|
-
"drizzle-turso": {
|
|
75
|
-
"@beignet/provider-drizzle-turso": "",
|
|
76
|
-
"@libsql/client": externalVersions.libsqlClient,
|
|
77
|
-
"drizzle-orm": externalVersions.drizzleOrm,
|
|
78
|
-
},
|
|
79
|
-
inngest: {
|
|
80
|
-
"@beignet/provider-inngest": "",
|
|
81
|
-
inngest: externalVersions.inngest,
|
|
82
|
-
},
|
|
83
|
-
pino: {
|
|
84
|
-
"@beignet/provider-logger-pino": "",
|
|
85
|
-
pino: externalVersions.pino,
|
|
86
|
-
},
|
|
87
|
-
resend: {
|
|
88
|
-
"@beignet/provider-mail-resend": "",
|
|
89
|
-
resend: externalVersions.resend,
|
|
90
|
-
},
|
|
91
|
-
"upstash-rate-limit": {
|
|
92
|
-
"@beignet/provider-rate-limit-upstash": "",
|
|
93
|
-
"@upstash/ratelimit": externalVersions.upstashRateLimit,
|
|
94
|
-
"@upstash/redis": externalVersions.upstashRedis,
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
const integrationDevDeps = {
|
|
98
|
-
"drizzle-turso": {
|
|
99
|
-
"drizzle-kit": externalVersions.drizzleKit,
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
const featureDeps = {
|
|
103
|
-
client: {},
|
|
104
|
-
"react-query": {
|
|
105
|
-
"@beignet/react-query": "",
|
|
106
|
-
"@tanstack/react-query": externalVersions.tanstackReactQuery,
|
|
107
|
-
},
|
|
108
|
-
forms: {
|
|
109
|
-
"@beignet/react-hook-form": "",
|
|
110
|
-
"@hookform/resolvers": externalVersions.hookformResolvers,
|
|
111
|
-
"react-hook-form": externalVersions.reactHookForm,
|
|
112
|
-
},
|
|
113
|
-
openapi: {
|
|
114
|
-
"@beignet/core": "",
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
function hasFeature(ctx, feature) {
|
|
118
|
-
return ctx.features.includes(feature);
|
|
119
|
-
}
|
|
120
|
-
function hasIntegration(ctx, integration) {
|
|
121
|
-
return ctx.integrations.includes(integration);
|
|
122
|
-
}
|
|
123
|
-
function json(value) {
|
|
124
|
-
return `${JSON.stringify(value, null, "\t")}\n`;
|
|
125
|
-
}
|
|
126
|
-
function gitignore(ctx) {
|
|
127
|
-
const lines = [files.gitignore.trimEnd()];
|
|
128
|
-
if (isStandardPreset(ctx)) {
|
|
129
|
-
lines.push("/storage/");
|
|
130
|
-
}
|
|
131
|
-
if (hasDrizzleTurso(ctx)) {
|
|
132
|
-
lines.push("local.db", "local.db-*");
|
|
133
|
-
}
|
|
134
|
-
return `${lines.join("\n")}\n`;
|
|
135
|
-
}
|
|
136
|
-
function packageJson(ctx) {
|
|
137
|
-
const dependencies = {
|
|
138
|
-
"@beignet/core": ctx.beignetVersion,
|
|
139
|
-
"@beignet/next": ctx.beignetVersion,
|
|
140
|
-
next: externalVersions.next,
|
|
141
|
-
react: externalVersions.react,
|
|
142
|
-
"react-dom": externalVersions.reactDom,
|
|
143
|
-
zod: externalVersions.zod,
|
|
144
|
-
};
|
|
145
|
-
if (isStandardPreset(ctx)) {
|
|
146
|
-
dependencies["@beignet/devtools"] = ctx.beignetVersion;
|
|
147
|
-
dependencies["@beignet/provider-event-bus-memory"] = ctx.beignetVersion;
|
|
148
|
-
dependencies["@beignet/provider-storage-local"] = ctx.beignetVersion;
|
|
149
|
-
}
|
|
150
|
-
for (const integration of ctx.integrations) {
|
|
151
|
-
for (const [name, version] of Object.entries(integrationDeps[integration])) {
|
|
152
|
-
dependencies[name] = version || ctx.beignetVersion;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
for (const feature of ctx.features) {
|
|
156
|
-
for (const [name, version] of Object.entries(featureDeps[feature])) {
|
|
157
|
-
dependencies[name] = version || ctx.beignetVersion;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const devDependencies = {
|
|
161
|
-
"@types/bun": externalVersions.typesBun,
|
|
162
|
-
"@types/node": externalVersions.typesNode,
|
|
163
|
-
"@types/react": externalVersions.typesReact,
|
|
164
|
-
"@types/react-dom": externalVersions.typesReactDom,
|
|
165
|
-
typescript: externalVersions.typescript,
|
|
166
|
-
};
|
|
167
|
-
for (const integration of ctx.integrations) {
|
|
168
|
-
for (const [name, version] of Object.entries(integrationDevDeps[integration] ?? {})) {
|
|
169
|
-
devDependencies[name] = version;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const scripts = {
|
|
173
|
-
dev: "next dev",
|
|
174
|
-
build: "next build",
|
|
175
|
-
start: "next start",
|
|
176
|
-
test: "bun test",
|
|
177
|
-
typecheck: "tsc --noEmit",
|
|
178
|
-
};
|
|
179
|
-
if (hasDrizzleTurso(ctx)) {
|
|
180
|
-
scripts["db:generate"] = "drizzle-kit generate";
|
|
181
|
-
scripts["db:migrate"] = "drizzle-kit migrate";
|
|
182
|
-
scripts["db:seed"] = "bun infra/db/seed.ts";
|
|
183
|
-
scripts["db:reset"] = "bun infra/db/reset.ts";
|
|
184
|
-
}
|
|
185
|
-
return json({
|
|
186
|
-
name: ctx.name,
|
|
187
|
-
version: "0.1.0",
|
|
188
|
-
private: true,
|
|
189
|
-
type: "module",
|
|
190
|
-
scripts,
|
|
191
|
-
dependencies,
|
|
192
|
-
devDependencies,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
function readme(ctx) {
|
|
196
|
-
if (isStandardPreset(ctx)) {
|
|
197
|
-
return standardReadme(ctx);
|
|
198
|
-
}
|
|
199
|
-
const install = `${ctx.packageManager} install`;
|
|
200
|
-
const run = ctx.packageManager === "npm"
|
|
201
|
-
? "npm run dev"
|
|
202
|
-
: `${ctx.packageManager} run dev`;
|
|
203
|
-
const featureList = ctx.features.length > 0
|
|
204
|
-
? ctx.features.map((feature) => `- \`${feature}\``).join("\n")
|
|
205
|
-
: "- API routes only";
|
|
206
|
-
const integrationList = ctx.integrations.length > 0
|
|
207
|
-
? ctx.integrations.map((integration) => `- \`${integration}\``).join("\n")
|
|
208
|
-
: "- None";
|
|
209
|
-
return `# ${ctx.name}
|
|
210
|
-
|
|
211
|
-
Beignet app scaffolded with \`@beignet/cli\`.
|
|
212
|
-
|
|
213
|
-
## Getting started
|
|
214
|
-
|
|
215
|
-
\`\`\`bash
|
|
216
|
-
${install}
|
|
217
|
-
${run}
|
|
218
|
-
\`\`\`
|
|
219
|
-
|
|
220
|
-
Open http://localhost:3000 and start from the todos workflow.
|
|
221
|
-
|
|
222
|
-
## Files to know
|
|
223
|
-
|
|
224
|
-
- \`features/todos/contracts.ts\` defines the API contract.
|
|
225
|
-
- \`features/todos/use-cases.ts\` contains application logic with input and output validation.
|
|
226
|
-
- \`ports/index.ts\` defines the app's external dependencies.
|
|
227
|
-
- \`server/index.ts\` wires contracts to handlers.
|
|
228
|
-
- \`app/api/[[...path]]/route.ts\` exposes contract-backed API routes to Next.js.
|
|
229
|
-
${hasFeature(ctx, "react-query") ? "- `features/todos/components/todo-app.tsx` contains the starter UI.\n" : ""}
|
|
230
|
-
|
|
231
|
-
## Selected preset
|
|
232
|
-
|
|
233
|
-
\`${ctx.preset}\`
|
|
234
|
-
|
|
235
|
-
## Selected features
|
|
236
|
-
|
|
237
|
-
${featureList}
|
|
238
|
-
|
|
239
|
-
## Selected integrations
|
|
240
|
-
|
|
241
|
-
${integrationList}
|
|
242
|
-
${ctx.integrations.length > 0 ? "\nSee `docs/integrations.md` for setup notes.\n" : ""}
|
|
243
|
-
`;
|
|
244
|
-
}
|
|
245
|
-
function standardReadme(ctx) {
|
|
246
|
-
const install = `${ctx.packageManager} install`;
|
|
247
|
-
const run = ctx.packageManager === "npm"
|
|
248
|
-
? "npm run dev"
|
|
249
|
-
: `${ctx.packageManager} run dev`;
|
|
250
|
-
const typecheck = ctx.packageManager === "npm"
|
|
251
|
-
? "npm run typecheck"
|
|
252
|
-
: `${ctx.packageManager} run typecheck`;
|
|
253
|
-
const cli = packageRunner(ctx);
|
|
254
|
-
const beforeDeploy = hasDrizzleTurso(ctx)
|
|
255
|
-
? [
|
|
256
|
-
"- Create a Turso database or keep `TURSO_DB_URL=file:local.db` for local libSQL development.",
|
|
257
|
-
`- Run \`${cli} db generate\` and \`${cli} db migrate\` after changing the Drizzle schema.`,
|
|
258
|
-
`- Run \`${cli} db seed\` to load starter data or \`${cli} db reset\` to rebuild a local SQLite database.`,
|
|
259
|
-
].join("\n")
|
|
260
|
-
: "- Replace the in-memory todo repository with a durable adapter.";
|
|
261
|
-
return `# ${ctx.name}
|
|
262
|
-
|
|
263
|
-
Beignet app scaffolded with \`@beignet/cli\`.
|
|
264
|
-
|
|
265
|
-
## Getting started
|
|
266
|
-
|
|
267
|
-
\`\`\`bash
|
|
268
|
-
${install}
|
|
269
|
-
cp .env.example .env.local
|
|
270
|
-
${run}
|
|
271
|
-
\`\`\`
|
|
272
|
-
|
|
273
|
-
Open http://localhost:3000 for the todo workflow, http://localhost:3000/api/health for health checks, and http://localhost:3000/api/devtools while developing.
|
|
274
|
-
|
|
275
|
-
## First checks
|
|
276
|
-
|
|
277
|
-
\`\`\`bash
|
|
278
|
-
# in another terminal
|
|
279
|
-
${cli} routes
|
|
280
|
-
${cli} lint
|
|
281
|
-
${cli} doctor
|
|
282
|
-
${typecheck}
|
|
283
|
-
\`\`\`
|
|
284
|
-
|
|
285
|
-
\`routes\` shows the contracts Beignet can inspect. \`lint\` checks dependency direction. \`doctor\` catches route, OpenAPI, and resource drift.
|
|
286
|
-
|
|
287
|
-
## App map
|
|
288
|
-
|
|
289
|
-
- \`features/todos/contracts.ts\` owns the HTTP contract and reuses use case schemas.
|
|
290
|
-
- \`features/todos/use-cases/\` owns application behavior and validation.
|
|
291
|
-
- \`ports/\` defines app-owned dependencies.
|
|
292
|
-
- \`infra/\` implements ports for the selected runtime.
|
|
293
|
-
- \`server/routes.ts\` keeps the central route registry and OpenAPI contract list.
|
|
294
|
-
- \`server/\` wires context, providers, routes, hooks, and error handling.
|
|
295
|
-
- \`server/outbox.ts\` registers durable events and jobs for the outbox drain route.
|
|
296
|
-
- \`features/shared/errors.ts\` keeps application errors and route-owned error schemas together.
|
|
297
|
-
- \`features/todos/domain/events/\`, \`jobs/\`, \`listeners/\`, \`notifications/\`, \`schedules/\`, and \`uploads/\` show the feature-owned background workflow shape.
|
|
298
|
-
- \`app/storage/[...key]/route.ts\` serves public local storage objects.
|
|
299
|
-
- \`app/api/cron/outbox/drain/route.ts\` is the bounded serverless entrypoint for durable event/job delivery.
|
|
300
|
-
- \`lib/env.ts\` validates deployment configuration at startup.
|
|
301
|
-
- \`lib/auth.ts\` exposes \`requireUser(ctx)\` for protected use cases.
|
|
302
|
-
- \`lib/better-auth.ts\` owns Better Auth setup and keeps provider-specific auth details outside use cases.
|
|
303
|
-
- \`features/todos/policy.ts\` defines authorization rules registered with \`createGate(...)\`.
|
|
304
|
-
${hasDrizzleTurso(ctx) ? "- `infra/db/schema/` contains the Drizzle schema, `infra/db/repositories.ts` creates app repositories, and `infra/todos/` contains the durable todo repository adapter.\n" : ""}
|
|
305
|
-
|
|
306
|
-
## Before deploying
|
|
307
|
-
|
|
308
|
-
${beforeDeploy}
|
|
309
|
-
- Keep \`/api/devtools\` development-only unless you add authentication and stricter redaction.
|
|
310
|
-
- Set \`APP_URL\`, \`BETTER_AUTH_SECRET\`, \`CRON_SECRET\`, \`LOG_LEVEL\`, and service-specific integration variables in your hosting environment.
|
|
311
|
-
- Configure a scheduled request to \`/api/cron/outbox/drain\` with \`Authorization: Bearer <CRON_SECRET>\`.
|
|
312
|
-
- Review the starter authorization policy before exposing user-owned data.
|
|
313
|
-
${ctx.integrations.length > 0 ? "\nSee `docs/integrations.md` for setup notes.\n" : ""}
|
|
314
|
-
`;
|
|
315
|
-
}
|
|
316
|
-
function integrationsDoc(ctx) {
|
|
317
|
-
const lines = [
|
|
318
|
-
"# Integrations",
|
|
319
|
-
"",
|
|
320
|
-
"The starter app added dependencies for the integrations you selected. Runtime providers are wired in `server/providers.ts` when Beignet can do so safely; integrations that need app-owned setup list their follow-up steps below.",
|
|
321
|
-
"",
|
|
322
|
-
];
|
|
323
|
-
for (const integration of ctx.integrations) {
|
|
324
|
-
if (integration === "better-auth") {
|
|
325
|
-
lines.push("## Better Auth", "", "- Package: `@beignet/provider-auth-better-auth`", "- Peer dependency: `better-auth`", "- The standard starter wires `lib/better-auth.ts`, `app/api/auth/[...all]/route.ts`, and `createAuthBetterAuthProvider(...)`.", "- Keep auth behind the shared `AuthPort`, then call `requireUser(ctx)` in use cases that need a signed-in user.", "- Put repeated ownership or role rules in policies registered with `createGate(...)`.", "- The generated SQLite schema includes the Better Auth `user`, `session`, `account`, and `verification` tables for local development.", "");
|
|
326
|
-
}
|
|
327
|
-
if (integration === "drizzle-turso") {
|
|
328
|
-
lines.push("## Drizzle + Turso", "", "- Package: `@beignet/provider-drizzle-turso`", "- Peer dependencies: `@libsql/client` and `drizzle-orm`", "- Dev dependency: `drizzle-kit`", "- Set `TURSO_DB_URL` and optional `TURSO_DB_AUTH_TOKEN`.", "- The scaffold wires a typed Drizzle repository behind the `TodoRepository` port and a transaction-backed Unit of Work.", `- Run \`${packageRunner(ctx)} db generate\` and \`${packageRunner(ctx)} db migrate\` after changing \`infra/db/schema/index.ts\`.`, `- Run \`${packageRunner(ctx)} db seed\` for idempotent starter data and \`${packageRunner(ctx)} db reset\` for destructive local resets.`, "");
|
|
329
|
-
}
|
|
330
|
-
if (integration === "inngest") {
|
|
331
|
-
lines.push("## Inngest", "", "- Package: `@beignet/core` (`@beignet/core/events`, `@beignet/core/jobs`)", "- Package: `@beignet/provider-inngest`", "- Peer dependency: `inngest`", "- The standard preset wires `inngestProvider` in `server/providers.ts` and adds `jobs: JobDispatcherPort` to `AppPorts`.", "- Define jobs with `createJobHandlers(...).defineJob(...)` and dispatch them through `ctx.ports.jobs.dispatch(...)`.", "- Keep direct Inngest client usage inside infrastructure code unless you intentionally add an app-owned escape-hatch port.", "");
|
|
332
|
-
}
|
|
333
|
-
if (integration === "pino") {
|
|
334
|
-
lines.push("## Pino logger", "", "- Package: `@beignet/provider-logger-pino`", "- Peer dependency: `pino`", "- Use this provider when you want structured request and handler logging.", "");
|
|
335
|
-
}
|
|
336
|
-
if (integration === "resend") {
|
|
337
|
-
lines.push("## Resend mail", "", "- Package: `@beignet/provider-mail-resend`", "- Peer dependency: `resend`", "- The standard preset wires `mailResendProvider` in `server/providers.ts` and adds `mailer: MailerPort` to `AppPorts`.", "- Set `RESEND_API_KEY` and `RESEND_FROM` before starting the app.", "");
|
|
338
|
-
}
|
|
339
|
-
if (integration === "upstash-rate-limit") {
|
|
340
|
-
lines.push("## Upstash rate limit", "", "- Package: `@beignet/provider-rate-limit-upstash`", "- Peer dependencies: `@upstash/ratelimit` and `@upstash/redis`", "- The standard preset wires `upstashRateLimitProvider` in `server/providers.ts` and adds `rateLimit: RateLimitPort` to `AppPorts`.", "- Set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` before starting the app.", "");
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return `${lines.join("\n")}\n`;
|
|
344
|
-
}
|
|
345
|
-
function envExample(ctx) {
|
|
346
|
-
const lines = [
|
|
347
|
-
"# Copy to .env.local and fill in values for your app.",
|
|
348
|
-
"NODE_ENV=development",
|
|
349
|
-
"APP_URL=http://localhost:3000",
|
|
350
|
-
"CRON_SECRET=",
|
|
351
|
-
"DEVTOOLS_ENABLED=true",
|
|
352
|
-
"",
|
|
353
|
-
];
|
|
354
|
-
if (isStandardPreset(ctx)) {
|
|
355
|
-
lines.push("STORAGE_ROOT=storage/app", "# STORAGE_PUBLIC_BASE_URL=/storage", "");
|
|
356
|
-
}
|
|
357
|
-
if (hasIntegration(ctx, "pino")) {
|
|
358
|
-
lines.push("LOG_LEVEL=info", "LOG_FORMAT=json", `LOG_SERVICE=${ctx.name}`, "");
|
|
359
|
-
}
|
|
360
|
-
if (hasIntegration(ctx, "inngest")) {
|
|
361
|
-
lines.push("INNGEST_APP_NAME=beignet-app", "# INNGEST_EVENT_KEY=", "");
|
|
362
|
-
}
|
|
363
|
-
if (hasIntegration(ctx, "drizzle-turso")) {
|
|
364
|
-
lines.push("# Use file:local.db for local libSQL or libsql://... for Turso cloud.", "TURSO_DB_URL=file:local.db", "# TURSO_DB_AUTH_TOKEN=", "");
|
|
365
|
-
}
|
|
366
|
-
if (hasIntegration(ctx, "resend")) {
|
|
367
|
-
lines.push("RESEND_API_KEY=", "RESEND_FROM=", "");
|
|
368
|
-
}
|
|
369
|
-
if (hasIntegration(ctx, "upstash-rate-limit")) {
|
|
370
|
-
lines.push("UPSTASH_REDIS_REST_URL=", "UPSTASH_REDIS_REST_TOKEN=", "UPSTASH_PREFIX=ck:ratelimit", "");
|
|
371
|
-
}
|
|
372
|
-
if (hasIntegration(ctx, "better-auth")) {
|
|
373
|
-
lines.push("BETTER_AUTH_SECRET=local-dev-better-auth-secret-change-me", "BETTER_AUTH_URL=http://localhost:3000", "# BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000", "");
|
|
374
|
-
}
|
|
375
|
-
return `${lines.join("\n")}\n`;
|
|
376
|
-
}
|
|
377
|
-
function layout(ctx) {
|
|
378
|
-
if (!hasFeature(ctx, "react-query")) {
|
|
379
|
-
return `import "./globals.css";
|
|
380
|
-
import type { Metadata } from "next";
|
|
381
|
-
import type { ReactNode } from "react";
|
|
382
|
-
|
|
383
|
-
export const metadata: Metadata = {
|
|
384
|
-
title: "Beignet app",
|
|
385
|
-
description: "A contract-first Next.js app.",
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
389
|
-
return (
|
|
390
|
-
<html lang="en">
|
|
391
|
-
<body>{children}</body>
|
|
392
|
-
</html>
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
`;
|
|
396
|
-
}
|
|
397
|
-
return `import "./globals.css";
|
|
398
|
-
import type { Metadata } from "next";
|
|
399
|
-
import type { ReactNode } from "react";
|
|
400
|
-
import { Providers } from "./providers";
|
|
401
|
-
|
|
402
|
-
export const metadata: Metadata = {
|
|
403
|
-
title: "Beignet app",
|
|
404
|
-
description: "A contract-first Next.js app.",
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
408
|
-
return (
|
|
409
|
-
<html lang="en">
|
|
410
|
-
<body>
|
|
411
|
-
<Providers>{children}</Providers>
|
|
412
|
-
</body>
|
|
413
|
-
</html>
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
`;
|
|
417
|
-
}
|
|
418
|
-
function page(ctx) {
|
|
419
|
-
if (!hasFeature(ctx, "react-query")) {
|
|
420
|
-
return `export default function HomePage() {
|
|
421
|
-
return (
|
|
422
|
-
<main className="page">
|
|
423
|
-
<div className="shell">
|
|
424
|
-
<h1>Beignet app</h1>
|
|
425
|
-
<p className="muted">
|
|
426
|
-
This app starts with one contract-backed todos API route.
|
|
427
|
-
</p>
|
|
428
|
-
|
|
429
|
-
<section className="panel">
|
|
430
|
-
<h2>Try the API</h2>
|
|
431
|
-
<pre className="code">GET /api/todos</pre>
|
|
432
|
-
<p>
|
|
433
|
-
<a href="/api/todos">Open todos API</a>
|
|
434
|
-
</p>
|
|
435
|
-
</section>
|
|
436
|
-
</div>
|
|
437
|
-
</main>
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
`;
|
|
441
|
-
}
|
|
442
|
-
return `import { TodoApp } from "@/features/todos/components/todo-app";
|
|
443
|
-
|
|
444
|
-
export default function HomePage() {
|
|
445
|
-
return (
|
|
446
|
-
<main className="page">
|
|
447
|
-
<div className="shell">
|
|
448
|
-
<header className="hero">
|
|
449
|
-
<p className="eyebrow">Beignet starter</p>
|
|
450
|
-
<h1>Type-safe full-stack workflows without codegen.</h1>
|
|
451
|
-
<p className="muted">
|
|
452
|
-
Contracts, use cases, ports, server routes, a typed client, React Query,
|
|
453
|
-
and form validation are wired together so you can start changing product
|
|
454
|
-
code immediately.
|
|
455
|
-
</p>
|
|
456
|
-
</header>
|
|
457
|
-
|
|
458
|
-
<TodoApp />
|
|
459
|
-
</div>
|
|
460
|
-
</main>
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
`;
|
|
464
|
-
}
|
|
465
|
-
function serverProviders(ctx) {
|
|
466
|
-
const imports = [];
|
|
467
|
-
const entries = [];
|
|
468
|
-
if (hasIntegration(ctx, "pino")) {
|
|
469
|
-
imports.push('import { loggerPinoProvider } from "@beignet/provider-logger-pino";');
|
|
470
|
-
entries.push("loggerPinoProvider");
|
|
471
|
-
}
|
|
472
|
-
if (hasIntegration(ctx, "inngest")) {
|
|
473
|
-
imports.push('import { inngestProvider } from "@beignet/provider-inngest";');
|
|
474
|
-
entries.push("inngestProvider");
|
|
475
|
-
}
|
|
476
|
-
if (hasIntegration(ctx, "resend")) {
|
|
477
|
-
imports.push('import { mailResendProvider } from "@beignet/provider-mail-resend";');
|
|
478
|
-
entries.push("mailResendProvider");
|
|
479
|
-
}
|
|
480
|
-
if (hasIntegration(ctx, "upstash-rate-limit")) {
|
|
481
|
-
imports.push('import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";');
|
|
482
|
-
entries.push("upstashRateLimitProvider");
|
|
483
|
-
}
|
|
484
|
-
return `${imports.join("\n")}
|
|
485
|
-
export const providers = [
|
|
486
|
-
${entries.join(",\n\t")},
|
|
487
|
-
];
|
|
488
|
-
`;
|
|
489
|
-
}
|
|
490
|
-
function server(ctx) {
|
|
491
|
-
const hasProviders = ctx.integrations.some((integration) => ["pino", "inngest", "resend", "upstash-rate-limit"].includes(integration));
|
|
492
|
-
return `import { createNextServer } from "@beignet/next";
|
|
493
|
-
${hasProviders ? 'import { providers } from "./providers";\n' : ""}import { routes } from "./routes";
|
|
494
|
-
import { appPorts, type AppPorts } from "@/ports";
|
|
495
|
-
|
|
496
|
-
export type AppContext = {
|
|
497
|
-
requestId: string;
|
|
498
|
-
ports: AppPorts;
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
export const server = await createNextServer<AppContext, AppPorts>({
|
|
502
|
-
ports: appPorts,
|
|
503
|
-
${hasProviders ? "\tproviders,\n" : ""} createContext: async ({ ports }) => ({
|
|
504
|
-
requestId: crypto.randomUUID(),
|
|
505
|
-
ports,
|
|
506
|
-
}),
|
|
507
|
-
routes,
|
|
508
|
-
mapUnhandledError: ({ err, ctx }) => {
|
|
509
|
-
const ports = ctx?.ports as
|
|
510
|
-
| {
|
|
511
|
-
logger?: {
|
|
512
|
-
error: (
|
|
513
|
-
message: string,
|
|
514
|
-
meta?: Record<string, unknown>,
|
|
515
|
-
) => void;
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
| undefined;
|
|
519
|
-
|
|
520
|
-
if (ports?.logger) {
|
|
521
|
-
const logger = ports.logger as {
|
|
522
|
-
error: (message: string, meta?: Record<string, unknown>) => void;
|
|
523
|
-
};
|
|
524
|
-
logger.error("Unhandled API error", {
|
|
525
|
-
error: err,
|
|
526
|
-
requestId: ctx?.requestId,
|
|
527
|
-
});
|
|
528
|
-
} else {
|
|
529
|
-
console.error("Unhandled API error", {
|
|
530
|
-
error: err,
|
|
531
|
-
requestId: ctx?.requestId,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return {
|
|
536
|
-
status: 500,
|
|
537
|
-
body: {
|
|
538
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
539
|
-
message: "Internal server error",
|
|
540
|
-
requestId: ctx?.requestId,
|
|
541
|
-
},
|
|
542
|
-
};
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
`;
|
|
546
|
-
}
|
|
547
|
-
function todoApp(ctx) {
|
|
548
|
-
const listTodosQueryOptions = ctx.preset === "standard" ? "{ query: {} }" : "";
|
|
549
|
-
if (hasFeature(ctx, "forms")) {
|
|
550
|
-
return `"use client";
|
|
551
|
-
|
|
552
|
-
import { createReactHookForm } from "@beignet/react-hook-form";
|
|
553
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
554
|
-
import { createTodo, listTodos } from "@/features/todos/contracts";
|
|
555
|
-
import { rq } from "@/client/rq";
|
|
556
|
-
|
|
557
|
-
const rhf = createReactHookForm();
|
|
558
|
-
const createTodoForm = rhf(createTodo);
|
|
559
|
-
|
|
560
|
-
export function TodoApp() {
|
|
561
|
-
const queryClient = useQueryClient();
|
|
562
|
-
const todosQuery = useQuery(rq(listTodos).queryOptions(${listTodosQueryOptions}));
|
|
563
|
-
const form = createTodoForm.useForm({
|
|
564
|
-
defaultValues: { title: "" },
|
|
565
|
-
});
|
|
566
|
-
const createTodoMutation = useMutation(
|
|
567
|
-
rq(createTodo).mutationOptions({
|
|
568
|
-
onSuccess: async () => {
|
|
569
|
-
form.reset();
|
|
570
|
-
await queryClient.invalidateQueries({
|
|
571
|
-
queryKey: rq(listTodos).key(),
|
|
572
|
-
});
|
|
573
|
-
},
|
|
574
|
-
}),
|
|
575
|
-
);
|
|
576
|
-
|
|
577
|
-
const onSubmit = form.handleSubmit((body) => {
|
|
578
|
-
createTodoMutation.mutate({ body });
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
return (
|
|
582
|
-
<section className="workspace">
|
|
583
|
-
<form className="composer" onSubmit={onSubmit}>
|
|
584
|
-
<label htmlFor="todo-title">New todo</label>
|
|
585
|
-
<div className="composer-row">
|
|
586
|
-
<input
|
|
587
|
-
id="todo-title"
|
|
588
|
-
placeholder="Ship something type-safe"
|
|
589
|
-
{...form.register("title")}
|
|
590
|
-
/>
|
|
591
|
-
<button type="submit" disabled={createTodoMutation.isPending}>
|
|
592
|
-
{createTodoMutation.isPending ? "Adding" : "Add"}
|
|
593
|
-
</button>
|
|
594
|
-
</div>
|
|
595
|
-
{form.formState.errors.title ? (
|
|
596
|
-
<p className="error">{form.formState.errors.title.message}</p>
|
|
597
|
-
) : null}
|
|
598
|
-
</form>
|
|
599
|
-
|
|
600
|
-
<div className="todos-panel">
|
|
601
|
-
<div className="panel-heading">
|
|
602
|
-
<h2>Todos</h2>
|
|
603
|
-
<span>{todosQuery.data?.page.total ?? 0} total</span>
|
|
604
|
-
</div>
|
|
605
|
-
{todosQuery.isLoading ? <p className="muted">Loading todos...</p> : null}
|
|
606
|
-
{todosQuery.isError ? (
|
|
607
|
-
<p className="error">Could not load todos.</p>
|
|
608
|
-
) : null}
|
|
609
|
-
<ul className="todo-list">
|
|
610
|
-
{todosQuery.data?.items.map((todo) => (
|
|
611
|
-
<li key={todo.id}>
|
|
612
|
-
<span>{todo.title}</span>
|
|
613
|
-
<small>{todo.completed ? "Done" : "Open"}</small>
|
|
614
|
-
</li>
|
|
615
|
-
))}
|
|
616
|
-
</ul>
|
|
617
|
-
</div>
|
|
618
|
-
</section>
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
`;
|
|
622
|
-
}
|
|
623
|
-
return `"use client";
|
|
624
|
-
|
|
625
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
626
|
-
import { useState } from "react";
|
|
627
|
-
import { createTodo, listTodos } from "@/features/todos/contracts";
|
|
628
|
-
import { rq } from "@/client/rq";
|
|
629
|
-
|
|
630
|
-
export function TodoApp() {
|
|
631
|
-
const [title, setTitle] = useState("");
|
|
632
|
-
const queryClient = useQueryClient();
|
|
633
|
-
const todosQuery = useQuery(rq(listTodos).queryOptions(${listTodosQueryOptions}));
|
|
634
|
-
const createTodoMutation = useMutation(
|
|
635
|
-
rq(createTodo).mutationOptions({
|
|
636
|
-
onSuccess: async () => {
|
|
637
|
-
setTitle("");
|
|
638
|
-
await queryClient.invalidateQueries({
|
|
639
|
-
queryKey: rq(listTodos).key(),
|
|
640
|
-
});
|
|
641
|
-
},
|
|
642
|
-
}),
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
return (
|
|
646
|
-
<section className="workspace">
|
|
647
|
-
<form
|
|
648
|
-
className="composer"
|
|
649
|
-
onSubmit={(event) => {
|
|
650
|
-
event.preventDefault();
|
|
651
|
-
createTodoMutation.mutate({ body: { title } });
|
|
652
|
-
}}
|
|
653
|
-
>
|
|
654
|
-
<label htmlFor="todo-title">New todo</label>
|
|
655
|
-
<div className="composer-row">
|
|
656
|
-
<input
|
|
657
|
-
id="todo-title"
|
|
658
|
-
value={title}
|
|
659
|
-
onChange={(event) => setTitle(event.currentTarget.value)}
|
|
660
|
-
placeholder="Ship something type-safe"
|
|
661
|
-
/>
|
|
662
|
-
<button type="submit" disabled={!title || createTodoMutation.isPending}>
|
|
663
|
-
{createTodoMutation.isPending ? "Adding" : "Add"}
|
|
664
|
-
</button>
|
|
665
|
-
</div>
|
|
666
|
-
</form>
|
|
667
|
-
|
|
668
|
-
<div className="todos-panel">
|
|
669
|
-
<div className="panel-heading">
|
|
670
|
-
<h2>Todos</h2>
|
|
671
|
-
<span>{todosQuery.data?.page.total ?? 0} total</span>
|
|
672
|
-
</div>
|
|
673
|
-
<ul className="todo-list">
|
|
674
|
-
{todosQuery.data?.items.map((todo) => (
|
|
675
|
-
<li key={todo.id}>
|
|
676
|
-
<span>{todo.title}</span>
|
|
677
|
-
<small>{todo.completed ? "Done" : "Open"}</small>
|
|
678
|
-
</li>
|
|
679
|
-
))}
|
|
680
|
-
</ul>
|
|
681
|
-
</div>
|
|
682
|
-
</section>
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
`;
|
|
686
|
-
}
|
|
687
|
-
const files = {
|
|
688
|
-
gitignore: `.next
|
|
689
|
-
node_modules
|
|
690
|
-
dist
|
|
691
|
-
coverage
|
|
692
|
-
.env
|
|
693
|
-
.env.local
|
|
694
|
-
`,
|
|
695
|
-
nextEnv: `/// <reference types="next" />
|
|
696
|
-
/// <reference types="next/image-types/global" />
|
|
697
|
-
|
|
698
|
-
// This file should not be edited.
|
|
699
|
-
`,
|
|
700
|
-
nextConfig: `/** @type {import("next").NextConfig} */
|
|
701
|
-
const nextConfig = {};
|
|
702
|
-
|
|
703
|
-
export default nextConfig;
|
|
704
|
-
`,
|
|
705
|
-
tsconfig: json({
|
|
706
|
-
compilerOptions: {
|
|
707
|
-
target: "ES2017",
|
|
708
|
-
lib: ["dom", "dom.iterable", "esnext"],
|
|
709
|
-
allowJs: true,
|
|
710
|
-
skipLibCheck: true,
|
|
711
|
-
strict: true,
|
|
712
|
-
noEmit: true,
|
|
713
|
-
esModuleInterop: true,
|
|
714
|
-
module: "esnext",
|
|
715
|
-
moduleResolution: "bundler",
|
|
716
|
-
resolveJsonModule: true,
|
|
717
|
-
isolatedModules: true,
|
|
718
|
-
jsx: "react-jsx",
|
|
719
|
-
incremental: true,
|
|
720
|
-
plugins: [{ name: "next" }],
|
|
721
|
-
paths: {
|
|
722
|
-
"@/*": ["./*"],
|
|
723
|
-
},
|
|
724
|
-
},
|
|
725
|
-
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
726
|
-
exclude: ["node_modules"],
|
|
727
|
-
}),
|
|
728
|
-
globals: `:root {
|
|
729
|
-
color-scheme: light;
|
|
730
|
-
background: #f8fafc;
|
|
731
|
-
color: #172033;
|
|
732
|
-
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
* {
|
|
736
|
-
box-sizing: border-box;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
body {
|
|
740
|
-
margin: 0;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
a {
|
|
744
|
-
color: #4f46e5;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
button,
|
|
748
|
-
input {
|
|
749
|
-
font: inherit;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
.page {
|
|
753
|
-
min-height: 100vh;
|
|
754
|
-
padding: 48px 24px 64px;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
.shell {
|
|
758
|
-
max-width: 880px;
|
|
759
|
-
margin: 0 auto;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
.hero {
|
|
763
|
-
margin-bottom: 28px;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
.hero h1 {
|
|
767
|
-
max-width: 720px;
|
|
768
|
-
margin: 0 0 12px;
|
|
769
|
-
font-size: 40px;
|
|
770
|
-
line-height: 1.05;
|
|
771
|
-
letter-spacing: 0;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
.eyebrow {
|
|
775
|
-
margin: 0 0 12px;
|
|
776
|
-
color: #4f46e5;
|
|
777
|
-
font-size: 13px;
|
|
778
|
-
font-weight: 700;
|
|
779
|
-
text-transform: uppercase;
|
|
780
|
-
letter-spacing: 0.12em;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
.muted {
|
|
784
|
-
color: #475569;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
.panel {
|
|
788
|
-
margin-top: 24px;
|
|
789
|
-
padding: 20px;
|
|
790
|
-
border: 1px solid #e2e8f0;
|
|
791
|
-
border-radius: 8px;
|
|
792
|
-
background: white;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
.code {
|
|
796
|
-
padding: 16px;
|
|
797
|
-
overflow-x: auto;
|
|
798
|
-
border-radius: 8px;
|
|
799
|
-
background: #111827;
|
|
800
|
-
color: #f8fafc;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
.workspace {
|
|
804
|
-
display: grid;
|
|
805
|
-
gap: 18px;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
.composer,
|
|
809
|
-
.todos-panel {
|
|
810
|
-
padding: 18px;
|
|
811
|
-
border: 1px solid #dbe3ef;
|
|
812
|
-
border-radius: 8px;
|
|
813
|
-
background: #ffffff;
|
|
814
|
-
box-shadow: 0 1px 2px rgb(15 23 42 / 0.05);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
.composer label,
|
|
818
|
-
.panel-heading h2 {
|
|
819
|
-
margin: 0;
|
|
820
|
-
color: #172033;
|
|
821
|
-
font-size: 15px;
|
|
822
|
-
font-weight: 700;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
.composer-row {
|
|
826
|
-
display: flex;
|
|
827
|
-
gap: 10px;
|
|
828
|
-
margin-top: 10px;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
.composer input {
|
|
832
|
-
width: 100%;
|
|
833
|
-
min-width: 0;
|
|
834
|
-
border: 1px solid #cbd5e1;
|
|
835
|
-
border-radius: 8px;
|
|
836
|
-
padding: 10px 12px;
|
|
837
|
-
color: #172033;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
.composer button {
|
|
841
|
-
border: 0;
|
|
842
|
-
border-radius: 8px;
|
|
843
|
-
padding: 10px 14px;
|
|
844
|
-
background: #4f46e5;
|
|
845
|
-
color: #ffffff;
|
|
846
|
-
font-weight: 700;
|
|
847
|
-
cursor: pointer;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
.composer button:disabled {
|
|
851
|
-
cursor: not-allowed;
|
|
852
|
-
opacity: 0.6;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
.panel-heading {
|
|
856
|
-
display: flex;
|
|
857
|
-
align-items: center;
|
|
858
|
-
justify-content: space-between;
|
|
859
|
-
gap: 16px;
|
|
860
|
-
margin-bottom: 12px;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
.panel-heading span {
|
|
864
|
-
color: #64748b;
|
|
865
|
-
font-size: 13px;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
.todo-list {
|
|
869
|
-
display: grid;
|
|
870
|
-
gap: 8px;
|
|
871
|
-
margin: 0;
|
|
872
|
-
padding: 0;
|
|
873
|
-
list-style: none;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
.todo-list li {
|
|
877
|
-
display: flex;
|
|
878
|
-
align-items: center;
|
|
879
|
-
justify-content: space-between;
|
|
880
|
-
gap: 16px;
|
|
881
|
-
padding: 12px;
|
|
882
|
-
border: 1px solid #e2e8f0;
|
|
883
|
-
border-radius: 8px;
|
|
884
|
-
background: #f8fafc;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
.todo-list small {
|
|
888
|
-
color: #64748b;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
.error {
|
|
892
|
-
margin: 10px 0 0;
|
|
893
|
-
color: #dc2626;
|
|
894
|
-
font-size: 14px;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
@media (max-width: 640px) {
|
|
898
|
-
.page {
|
|
899
|
-
padding: 32px 16px 48px;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
.hero h1 {
|
|
903
|
-
font-size: 32px;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
.composer-row {
|
|
907
|
-
flex-direction: column;
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
`,
|
|
911
|
-
appProviders: `"use client";
|
|
912
|
-
|
|
913
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
914
|
-
import { type ReactNode, useState } from "react";
|
|
915
|
-
|
|
916
|
-
export function Providers({ children }: { children: ReactNode }) {
|
|
917
|
-
const [queryClient] = useState(() => new QueryClient());
|
|
918
|
-
|
|
919
|
-
return (
|
|
920
|
-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
921
|
-
);
|
|
922
|
-
}
|
|
923
|
-
`,
|
|
924
|
-
apiClient: `import { createNextClient } from "@beignet/next";
|
|
925
|
-
|
|
926
|
-
export const apiClient = createNextClient({
|
|
927
|
-
validate: true,
|
|
928
|
-
});
|
|
929
|
-
`,
|
|
930
|
-
rq: `import { createReactQuery } from "@beignet/react-query";
|
|
931
|
-
import { apiClient } from "./api-client";
|
|
932
|
-
|
|
933
|
-
export const rq = createReactQuery(apiClient);
|
|
934
|
-
`,
|
|
935
|
-
contractsTodos: `import { createContractGroup } from "@beignet/core/contracts";
|
|
936
|
-
import { z } from "zod";
|
|
937
|
-
|
|
938
|
-
const todos = createContractGroup().namespace("todos");
|
|
939
|
-
|
|
940
|
-
export const TodoSchema = z.object({
|
|
941
|
-
id: z.string(),
|
|
942
|
-
title: z.string().min(1),
|
|
943
|
-
completed: z.boolean(),
|
|
944
|
-
createdAt: z.string(),
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
export const CreateTodoSchema = z.object({
|
|
948
|
-
title: z.string().min(1),
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
export const listTodos = todos.get("/api/todos").responses({
|
|
952
|
-
200: z.object({
|
|
953
|
-
items: z.array(TodoSchema),
|
|
954
|
-
page: z.object({
|
|
955
|
-
kind: z.literal("offset"),
|
|
956
|
-
limit: z.number(),
|
|
957
|
-
offset: z.number(),
|
|
958
|
-
total: z.number(),
|
|
959
|
-
hasMore: z.boolean(),
|
|
960
|
-
}),
|
|
961
|
-
}),
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
export const createTodo = todos
|
|
965
|
-
.post("/api/todos")
|
|
966
|
-
.body(CreateTodoSchema)
|
|
967
|
-
.responses({
|
|
968
|
-
201: TodoSchema,
|
|
969
|
-
});
|
|
970
|
-
`,
|
|
971
|
-
ports: `import { definePorts } from "@beignet/core/ports";
|
|
972
|
-
import type { z } from "zod";
|
|
973
|
-
import type { CreateTodoSchema, TodoSchema } from "@/features/todos/contracts";
|
|
974
|
-
|
|
975
|
-
export type Todo = z.infer<typeof TodoSchema>;
|
|
976
|
-
export type CreateTodoInput = z.infer<typeof CreateTodoSchema>;
|
|
977
|
-
|
|
978
|
-
const todos = new Map<string, Todo>();
|
|
979
|
-
|
|
980
|
-
function createTodo(input: CreateTodoInput): Todo {
|
|
981
|
-
const todo = {
|
|
982
|
-
id: crypto.randomUUID(),
|
|
983
|
-
title: input.title,
|
|
984
|
-
completed: false,
|
|
985
|
-
createdAt: new Date().toISOString(),
|
|
986
|
-
};
|
|
987
|
-
todos.set(todo.id, todo);
|
|
988
|
-
return todo;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
createTodo({ title: "Read the generated contract" });
|
|
992
|
-
createTodo({ title: "Add your first use case" });
|
|
993
|
-
|
|
994
|
-
export const appPorts = definePorts({
|
|
995
|
-
todos: {
|
|
996
|
-
list: async () => Array.from(todos.values()),
|
|
997
|
-
create: async (input: CreateTodoInput) => createTodo(input),
|
|
998
|
-
},
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
export type AppPorts = typeof appPorts;
|
|
1002
|
-
`,
|
|
1003
|
-
useCasesTodos: `import { createUseCase } from "@beignet/core/application";
|
|
1004
|
-
import { z } from "zod";
|
|
1005
|
-
import { CreateTodoSchema, TodoSchema } from "@/features/todos/contracts";
|
|
1006
|
-
import type { AppPorts } from "@/ports";
|
|
1007
|
-
|
|
1008
|
-
type AppContext = {
|
|
1009
|
-
requestId: string;
|
|
1010
|
-
ports: AppPorts;
|
|
1011
|
-
};
|
|
1012
|
-
|
|
1013
|
-
const useCase = createUseCase<AppContext>();
|
|
1014
|
-
|
|
1015
|
-
export const ListTodosOutputSchema = z.object({
|
|
1016
|
-
items: z.array(TodoSchema),
|
|
1017
|
-
page: z.object({
|
|
1018
|
-
kind: z.literal("offset"),
|
|
1019
|
-
limit: z.number(),
|
|
1020
|
-
offset: z.number(),
|
|
1021
|
-
total: z.number(),
|
|
1022
|
-
hasMore: z.boolean(),
|
|
1023
|
-
}),
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
export const listTodos = useCase
|
|
1027
|
-
.query("listTodos")
|
|
1028
|
-
.input(z.void())
|
|
1029
|
-
.output(ListTodosOutputSchema)
|
|
1030
|
-
.run(async ({ ctx }) => {
|
|
1031
|
-
const todos = await ctx.ports.todos.list();
|
|
1032
|
-
return {
|
|
1033
|
-
items: todos,
|
|
1034
|
-
page: {
|
|
1035
|
-
kind: "offset" as const,
|
|
1036
|
-
limit: todos.length,
|
|
1037
|
-
offset: 0,
|
|
1038
|
-
total: todos.length,
|
|
1039
|
-
hasMore: false,
|
|
1040
|
-
},
|
|
1041
|
-
};
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
export const createTodo = useCase
|
|
1045
|
-
.command("createTodo")
|
|
1046
|
-
.input(CreateTodoSchema)
|
|
1047
|
-
.output(TodoSchema)
|
|
1048
|
-
.run(async ({ ctx, input }) => ctx.ports.todos.create(input));
|
|
1049
|
-
`,
|
|
1050
|
-
todoRoutes: `import { defineRouteGroup } from "@beignet/next";
|
|
1051
|
-
import type { AppContext } from "@/server";
|
|
1052
|
-
import { createTodo, listTodos } from "@/features/todos/use-cases";
|
|
1053
|
-
import * as contracts from "@/features/todos/contracts";
|
|
1054
|
-
|
|
1055
|
-
export const todoRoutes = defineRouteGroup<AppContext>({
|
|
1056
|
-
name: "todos",
|
|
1057
|
-
routes: [
|
|
1058
|
-
{
|
|
1059
|
-
contract: contracts.listTodos,
|
|
1060
|
-
handle: async ({ ctx }) => ({
|
|
1061
|
-
status: 200,
|
|
1062
|
-
body: await listTodos.run({ ctx, input: undefined }),
|
|
1063
|
-
}),
|
|
1064
|
-
},
|
|
1065
|
-
{
|
|
1066
|
-
contract: contracts.createTodo,
|
|
1067
|
-
handle: async ({ ctx, body }) => ({
|
|
1068
|
-
status: 201,
|
|
1069
|
-
body: await createTodo.run({ ctx, input: body }),
|
|
1070
|
-
}),
|
|
1071
|
-
},
|
|
1072
|
-
],
|
|
1073
|
-
});
|
|
1074
|
-
`,
|
|
1075
|
-
serverRoutes: `import { contractsFromRoutes, defineRoutes } from "@beignet/next";
|
|
1076
|
-
import type { AppContext } from "@/server";
|
|
1077
|
-
import { todoRoutes } from "@/features/todos/routes";
|
|
1078
|
-
|
|
1079
|
-
export const routes = defineRoutes<AppContext>([
|
|
1080
|
-
todoRoutes,
|
|
1081
|
-
]);
|
|
1082
|
-
export const contracts = contractsFromRoutes(routes);
|
|
1083
|
-
`,
|
|
1084
|
-
apiCatchAllRoute: `import { server } from "@/server";
|
|
1085
|
-
|
|
1086
|
-
export const GET = server.api;
|
|
1087
|
-
export const HEAD = server.api;
|
|
1088
|
-
export const OPTIONS = server.api;
|
|
1089
|
-
export const PATCH = server.api;
|
|
1090
|
-
export const POST = server.api;
|
|
1091
|
-
export const PUT = server.api;
|
|
1092
|
-
export const DELETE = server.api;
|
|
1093
|
-
`,
|
|
1094
|
-
apiOpenApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
|
|
1095
|
-
import { server } from "@/server";
|
|
1096
|
-
|
|
1097
|
-
export const GET = createOpenAPIHandler(server.contracts, {
|
|
1098
|
-
title: "Beignet starter API",
|
|
1099
|
-
version: "0.1.0",
|
|
1100
|
-
});
|
|
1101
|
-
`,
|
|
1102
|
-
productionEnv: `import { createEnv } from "@beignet/core/config";
|
|
1103
|
-
import { z } from "zod";
|
|
1104
|
-
|
|
1105
|
-
const BooleanEnv = z.enum(["true", "false"]).transform((value) => value === "true");
|
|
1106
|
-
|
|
1107
|
-
export const env = createEnv({
|
|
1108
|
-
server: {
|
|
1109
|
-
NODE_ENV: z
|
|
1110
|
-
.enum(["development", "test", "production"])
|
|
1111
|
-
.default("development"),
|
|
1112
|
-
APP_URL: z.string().url().default("http://localhost:3000"),
|
|
1113
|
-
CRON_SECRET: z.string().min(1).optional(),
|
|
1114
|
-
DEVTOOLS_ENABLED: BooleanEnv.optional(),
|
|
1115
|
-
BETTER_AUTH_SECRET: z
|
|
1116
|
-
.string()
|
|
1117
|
-
.min(32)
|
|
1118
|
-
.default("local-dev-better-auth-secret-change-me"),
|
|
1119
|
-
BETTER_AUTH_URL: z.string().url().default("http://localhost:3000"),
|
|
1120
|
-
BETTER_AUTH_TRUSTED_ORIGINS: z.string().optional(),
|
|
1121
|
-
TURSO_DB_URL: z.string().default("file:local.db"),
|
|
1122
|
-
TURSO_DB_AUTH_TOKEN: z.string().optional(),
|
|
1123
|
-
LOG_LEVEL: z
|
|
1124
|
-
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
|
|
1125
|
-
.default("info"),
|
|
1126
|
-
LOG_FORMAT: z.enum(["pretty", "json"]).default("json"),
|
|
1127
|
-
LOG_SERVICE: z.string().default("beignet-app"),
|
|
1128
|
-
},
|
|
1129
|
-
runtimeEnv: process.env,
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
export const isProduction = env.NODE_ENV === "production";
|
|
1133
|
-
`,
|
|
1134
|
-
productionAppContext: `import type { DevtoolsPort, DevtoolsTraceContext } from "@beignet/devtools";
|
|
1135
|
-
import type { ActivityActor, ActivityTenant, StoragePort } from "@beignet/core/ports";
|
|
1136
|
-
import type { AppGate, AppPorts } from "@/ports";
|
|
1137
|
-
import type { AuthSession } from "@/ports/auth";
|
|
1138
|
-
|
|
1139
|
-
export type AppContext = {
|
|
1140
|
-
requestId: string;
|
|
1141
|
-
actor: ActivityActor;
|
|
1142
|
-
auth: AuthSession | null;
|
|
1143
|
-
gate: AppGate;
|
|
1144
|
-
ports: AppPorts & {
|
|
1145
|
-
devtools: DevtoolsPort;
|
|
1146
|
-
storage: StoragePort;
|
|
1147
|
-
};
|
|
1148
|
-
tenant?: ActivityTenant;
|
|
1149
|
-
} & Partial<DevtoolsTraceContext>;
|
|
1150
|
-
`,
|
|
1151
|
-
productimapUnhandledErrors: `import { createAppError, defineErrors } from "@beignet/core/errors";
|
|
1152
|
-
|
|
1153
|
-
export const errors = defineErrors({
|
|
1154
|
-
Unauthorized: {
|
|
1155
|
-
code: "UNAUTHORIZED",
|
|
1156
|
-
status: 401,
|
|
1157
|
-
message: "Authentication required",
|
|
1158
|
-
},
|
|
1159
|
-
Forbidden: {
|
|
1160
|
-
code: "FORBIDDEN",
|
|
1161
|
-
status: 403,
|
|
1162
|
-
message: "Forbidden",
|
|
1163
|
-
},
|
|
1164
|
-
TodoNotFound: {
|
|
1165
|
-
code: "TODO_NOT_FOUND",
|
|
1166
|
-
status: 404,
|
|
1167
|
-
message: "Todo not found",
|
|
1168
|
-
},
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
export const appError = createAppError(errors);
|
|
1172
|
-
`,
|
|
1173
|
-
productionTodoSchemas: `import { z } from "zod";
|
|
1174
|
-
|
|
1175
|
-
export const TodoSchema = z.object({
|
|
1176
|
-
id: z.string().uuid(),
|
|
1177
|
-
title: z.string().min(1),
|
|
1178
|
-
completed: z.boolean(),
|
|
1179
|
-
createdAt: z.string().datetime(),
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
export const ListTodosInputSchema = z.object({
|
|
1183
|
-
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
1184
|
-
offset: z.coerce.number().int().min(0).default(0),
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
export const ListTodosOutputSchema = z.object({
|
|
1188
|
-
items: z.array(TodoSchema),
|
|
1189
|
-
page: z.object({
|
|
1190
|
-
kind: z.literal("offset"),
|
|
1191
|
-
limit: z.number().int().min(1),
|
|
1192
|
-
offset: z.number().int().min(0),
|
|
1193
|
-
total: z.number().int().min(0),
|
|
1194
|
-
hasMore: z.boolean(),
|
|
1195
|
-
}),
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
export const GetTodoInputSchema = z.object({
|
|
1199
|
-
id: z.string().uuid(),
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
export const CreateTodoInputSchema = z.object({
|
|
1203
|
-
title: z.string().min(1).max(120),
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
export type Todo = z.infer<typeof TodoSchema>;
|
|
1207
|
-
export type CreateTodoInput = z.infer<typeof CreateTodoInputSchema>;
|
|
1208
|
-
export type ListTodosInput = z.infer<typeof ListTodosInputSchema>;
|
|
1209
|
-
`,
|
|
1210
|
-
productionUseCaseBuilder: `import { createUseCase } from "@beignet/core/application";
|
|
1211
|
-
import { createDevtoolsUseCaseObserver } from "@beignet/devtools";
|
|
1212
|
-
import type { AppContext } from "@/app-context";
|
|
1213
|
-
|
|
1214
|
-
export const useCase = createUseCase<AppContext>({
|
|
1215
|
-
onRun: createDevtoolsUseCaseObserver<AppContext>(),
|
|
1216
|
-
});
|
|
1217
|
-
`,
|
|
1218
|
-
productionAuthHelpers: `import type { AppContext } from "@/app-context";
|
|
1219
|
-
import type { AuthSession, AuthUser } from "@/ports/auth";
|
|
1220
|
-
import { appError } from "@/features/shared/errors";
|
|
1221
|
-
|
|
1222
|
-
export function requireSession(ctx: AppContext): AuthSession {
|
|
1223
|
-
if (!ctx.auth) {
|
|
1224
|
-
throw appError("Unauthorized");
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
return ctx.auth;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
export function requireUser(ctx: AppContext): AuthUser {
|
|
1231
|
-
return requireSession(ctx).user;
|
|
1232
|
-
}
|
|
1233
|
-
`,
|
|
1234
|
-
productionListTodosUseCase: `import { normalizeOffsetPage } from "@beignet/core/pagination";
|
|
1235
|
-
import { useCase } from "@/lib/use-case";
|
|
1236
|
-
import { ListTodosInputSchema, ListTodosOutputSchema } from "./schemas";
|
|
1237
|
-
|
|
1238
|
-
export const listTodosUseCase = useCase
|
|
1239
|
-
.query("todos.list")
|
|
1240
|
-
.input(ListTodosInputSchema)
|
|
1241
|
-
.output(ListTodosOutputSchema)
|
|
1242
|
-
.run(async ({ ctx, input }) => {
|
|
1243
|
-
const page = normalizeOffsetPage(input, {
|
|
1244
|
-
defaultLimit: 20,
|
|
1245
|
-
maxLimit: 100,
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1248
|
-
return ctx.ports.todos.list(page);
|
|
1249
|
-
});
|
|
1250
|
-
`,
|
|
1251
|
-
productionGetTodoUseCase: `import { appError } from "@/features/shared/errors";
|
|
1252
|
-
import { useCase } from "@/lib/use-case";
|
|
1253
|
-
import { GetTodoInputSchema, TodoSchema } from "./schemas";
|
|
1254
|
-
|
|
1255
|
-
export const getTodoUseCase = useCase
|
|
1256
|
-
.query("todos.get")
|
|
1257
|
-
.input(GetTodoInputSchema)
|
|
1258
|
-
.output(TodoSchema)
|
|
1259
|
-
.run(async ({ ctx, input }) => {
|
|
1260
|
-
const todo = await ctx.ports.todos.findById(input.id);
|
|
1261
|
-
if (!todo) {
|
|
1262
|
-
throw appError("TodoNotFound", { details: { id: input.id } });
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
return todo;
|
|
1266
|
-
});
|
|
1267
|
-
`,
|
|
1268
|
-
productionCreateTodoUseCase: `import { TodoCreated } from "@/features/todos/domain/events";
|
|
1269
|
-
import { auditEntry } from "@/lib/audit";
|
|
1270
|
-
import { useCase } from "@/lib/use-case";
|
|
1271
|
-
import { CreateTodoInputSchema, TodoSchema } from "./schemas";
|
|
1272
|
-
|
|
1273
|
-
export const createTodoUseCase = useCase
|
|
1274
|
-
.command("todos.create")
|
|
1275
|
-
.input(CreateTodoInputSchema)
|
|
1276
|
-
.output(TodoSchema)
|
|
1277
|
-
.emits([TodoCreated])
|
|
1278
|
-
.run(async ({ ctx, input, events }) => {
|
|
1279
|
-
await ctx.gate.authorize("todos.create");
|
|
1280
|
-
const todo = await ctx.ports.uow.transaction(async (tx) => {
|
|
1281
|
-
const created = await tx.todos.create(input);
|
|
1282
|
-
await events.record(tx.events, TodoCreated, {
|
|
1283
|
-
todoId: created.id,
|
|
1284
|
-
title: created.title,
|
|
1285
|
-
});
|
|
1286
|
-
await tx.audit.record(
|
|
1287
|
-
auditEntry(ctx, {
|
|
1288
|
-
action: "todos.create",
|
|
1289
|
-
resource: { type: "todo", id: created.id, name: created.title },
|
|
1290
|
-
metadata: { completed: created.completed },
|
|
1291
|
-
}),
|
|
1292
|
-
);
|
|
1293
|
-
return created;
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
return todo;
|
|
1297
|
-
});
|
|
1298
|
-
`,
|
|
1299
|
-
productionUseCasesIndex: `export { createTodoUseCase } from "./create-todo";
|
|
1300
|
-
export { getTodoUseCase } from "./get-todo";
|
|
1301
|
-
export { listTodosUseCase } from "./list-todos";
|
|
1302
|
-
export {
|
|
1303
|
-
CreateTodoInputSchema,
|
|
1304
|
-
GetTodoInputSchema,
|
|
1305
|
-
ListTodosInputSchema,
|
|
1306
|
-
ListTodosOutputSchema,
|
|
1307
|
-
TodoSchema,
|
|
1308
|
-
type CreateTodoInput,
|
|
1309
|
-
type ListTodosInput,
|
|
1310
|
-
type Todo,
|
|
1311
|
-
} from "./schemas";
|
|
1312
|
-
`,
|
|
1313
|
-
productionTodoRepositoryPort: `import type {
|
|
1314
|
-
OffsetPage,
|
|
1315
|
-
OffsetPageInfo,
|
|
1316
|
-
PageResult,
|
|
1317
|
-
} from "@beignet/core/pagination";
|
|
1318
|
-
import type {
|
|
1319
|
-
CreateTodoInput,
|
|
1320
|
-
Todo,
|
|
1321
|
-
} from "@/features/todos/use-cases/schemas";
|
|
1322
|
-
|
|
1323
|
-
export type ListTodosResult = PageResult<Todo, OffsetPageInfo>;
|
|
1324
|
-
|
|
1325
|
-
export type TodoAttachment = {
|
|
1326
|
-
id: string;
|
|
1327
|
-
todoId: string;
|
|
1328
|
-
key: string;
|
|
1329
|
-
fileName: string;
|
|
1330
|
-
contentType: string;
|
|
1331
|
-
size: number;
|
|
1332
|
-
createdAt: string;
|
|
1333
|
-
};
|
|
1334
|
-
|
|
1335
|
-
export interface TodoRepository {
|
|
1336
|
-
list(page: OffsetPage): Promise<ListTodosResult>;
|
|
1337
|
-
findById(id: string): Promise<Todo | null>;
|
|
1338
|
-
create(input: CreateTodoInput): Promise<Todo>;
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
export interface TodoAttachmentRepository {
|
|
1342
|
-
create(input: Omit<TodoAttachment, "createdAt">): Promise<TodoAttachment>;
|
|
1343
|
-
findMany(todoId: string): Promise<TodoAttachment[]>;
|
|
1344
|
-
}
|
|
1345
|
-
`,
|
|
1346
|
-
productionAuthPort: `import type {
|
|
1347
|
-
AuthPort as BeignetAuthPort,
|
|
1348
|
-
AuthRequestLike,
|
|
1349
|
-
AuthSession as BeignetAuthSession,
|
|
1350
|
-
} from "@beignet/core/ports";
|
|
1351
|
-
|
|
1352
|
-
export type AuthRequest = AuthRequestLike;
|
|
1353
|
-
|
|
1354
|
-
export type AuthUser = {
|
|
1355
|
-
id: string;
|
|
1356
|
-
email?: string;
|
|
1357
|
-
name?: string;
|
|
1358
|
-
};
|
|
1359
|
-
|
|
1360
|
-
export type AuthSessionMetadata = {
|
|
1361
|
-
id?: string;
|
|
1362
|
-
};
|
|
1363
|
-
|
|
1364
|
-
export type AuthSession = BeignetAuthSession<
|
|
1365
|
-
AuthUser,
|
|
1366
|
-
AuthSessionMetadata
|
|
1367
|
-
>;
|
|
1368
|
-
|
|
1369
|
-
export type AuthPort = BeignetAuthPort<
|
|
1370
|
-
AuthUser,
|
|
1371
|
-
AuthSessionMetadata,
|
|
1372
|
-
AuthRequest
|
|
1373
|
-
>;
|
|
1374
|
-
`,
|
|
1375
|
-
productionLogger: `import type { LoggerPort } from "@beignet/core/ports";
|
|
1376
|
-
|
|
1377
|
-
export const fallbackLogger: LoggerPort = {
|
|
1378
|
-
trace: (message, meta) => console.trace(message, meta),
|
|
1379
|
-
debug: (message, meta) => console.debug(message, meta),
|
|
1380
|
-
info: (message, meta) => console.info(message, meta),
|
|
1381
|
-
warn: (message, meta) => console.warn(message, meta),
|
|
1382
|
-
error: (message, meta) => console.error(message, meta),
|
|
1383
|
-
fatal: (message, meta) => console.error(message, meta),
|
|
1384
|
-
child: () => fallbackLogger,
|
|
1385
|
-
};
|
|
1386
|
-
`,
|
|
1387
|
-
productionAnonymousAuth: `import { createAnonymousAuth as createAnonymousAuthPort } from "@beignet/core/ports";
|
|
1388
|
-
import type {
|
|
1389
|
-
AuthPort,
|
|
1390
|
-
AuthRequest,
|
|
1391
|
-
AuthSessionMetadata,
|
|
1392
|
-
AuthUser,
|
|
1393
|
-
} from "@/ports/auth";
|
|
1394
|
-
|
|
1395
|
-
export function createAnonymousAuth(): AuthPort {
|
|
1396
|
-
return createAnonymousAuthPort<AuthUser, AuthSessionMetadata, AuthRequest>();
|
|
1397
|
-
}
|
|
1398
|
-
`,
|
|
1399
|
-
productionTodoPolicy: `import { type ActivityActor, definePolicy, deny } from "@beignet/core/ports";
|
|
1400
|
-
import type { AuthSession } from "@/ports/auth";
|
|
1401
|
-
|
|
1402
|
-
export type AuthorizationContext = {
|
|
1403
|
-
actor: ActivityActor;
|
|
1404
|
-
auth: AuthSession | null;
|
|
1405
|
-
};
|
|
1406
|
-
|
|
1407
|
-
export const todoPolicy = definePolicy({
|
|
1408
|
-
"todos.create": (ctx: AuthorizationContext) => {
|
|
1409
|
-
if (ctx.actor.type === "user") return true;
|
|
1410
|
-
return deny("You must be signed in to create todos.");
|
|
1411
|
-
},
|
|
1412
|
-
});
|
|
1413
|
-
`,
|
|
1414
|
-
productionInMemoryTodoRepository: `import { offsetPageResult } from "@beignet/core/pagination";
|
|
1415
|
-
import type {
|
|
1416
|
-
CreateTodoInput,
|
|
1417
|
-
Todo,
|
|
1418
|
-
} from "@/features/todos/use-cases/schemas";
|
|
1419
|
-
import type {
|
|
1420
|
-
TodoAttachmentRepository,
|
|
1421
|
-
TodoRepository,
|
|
1422
|
-
} from "@/features/todos/ports";
|
|
1423
|
-
|
|
1424
|
-
export function createInMemoryTodoRepository(
|
|
1425
|
-
seed: Todo[] = [],
|
|
1426
|
-
): TodoRepository {
|
|
1427
|
-
const todos = new Map(seed.map((todo) => [todo.id, todo]));
|
|
1428
|
-
|
|
1429
|
-
return {
|
|
1430
|
-
async list(page) {
|
|
1431
|
-
const allTodos = Array.from(todos.values()).sort((left, right) =>
|
|
1432
|
-
left.createdAt.localeCompare(right.createdAt),
|
|
1433
|
-
);
|
|
1434
|
-
|
|
1435
|
-
return offsetPageResult(
|
|
1436
|
-
allTodos.slice(page.offset, page.offset + page.limit),
|
|
1437
|
-
page,
|
|
1438
|
-
allTodos.length,
|
|
1439
|
-
);
|
|
1440
|
-
},
|
|
1441
|
-
async findById(id: string) {
|
|
1442
|
-
return todos.get(id) ?? null;
|
|
1443
|
-
},
|
|
1444
|
-
async create(input: CreateTodoInput) {
|
|
1445
|
-
const todo: Todo = {
|
|
1446
|
-
id: crypto.randomUUID(),
|
|
1447
|
-
title: input.title,
|
|
1448
|
-
completed: false,
|
|
1449
|
-
createdAt: new Date().toISOString(),
|
|
1450
|
-
};
|
|
1451
|
-
todos.set(todo.id, todo);
|
|
1452
|
-
return todo;
|
|
1453
|
-
},
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
|
-
`,
|
|
1457
|
-
productionInfrastructurePorts: `import {
|
|
1458
|
-
createGate,
|
|
1459
|
-
createNoopUnitOfWork,
|
|
1460
|
-
definePorts,
|
|
1461
|
-
} from "@beignet/core/ports";
|
|
1462
|
-
import { todoPolicy } from "@/features/todos/policy";
|
|
1463
|
-
import { appError } from "@/features/shared/errors";
|
|
1464
|
-
import { createAnonymousAuth } from "./auth/anonymous-auth";
|
|
1465
|
-
import { fallbackLogger } from "./logger";
|
|
1466
|
-
import { createInMemoryTodoRepository } from "./todos/in-memory-todo-repository";
|
|
1467
|
-
|
|
1468
|
-
const todos = createInMemoryTodoRepository([
|
|
1469
|
-
{
|
|
1470
|
-
id: "00000000-0000-4000-8000-000000000001",
|
|
1471
|
-
title: "Review the starter boundaries",
|
|
1472
|
-
completed: false,
|
|
1473
|
-
createdAt: new Date().toISOString(),
|
|
1474
|
-
},
|
|
1475
|
-
{
|
|
1476
|
-
id: "00000000-0000-4000-8000-000000000002",
|
|
1477
|
-
title: "Replace the repository with durable persistence",
|
|
1478
|
-
completed: false,
|
|
1479
|
-
createdAt: new Date().toISOString(),
|
|
1480
|
-
},
|
|
1481
|
-
]);
|
|
1482
|
-
|
|
1483
|
-
const gate = createGate({
|
|
1484
|
-
policies: [todoPolicy],
|
|
1485
|
-
onDeny(decision) {
|
|
1486
|
-
return appError("Forbidden", {
|
|
1487
|
-
message: decision.reason ?? "Forbidden",
|
|
1488
|
-
details: decision.details,
|
|
1489
|
-
});
|
|
1490
|
-
},
|
|
1491
|
-
});
|
|
1492
|
-
|
|
1493
|
-
export const appPorts = definePorts({
|
|
1494
|
-
auth: createAnonymousAuth(),
|
|
1495
|
-
gate,
|
|
1496
|
-
todos,
|
|
1497
|
-
logger: fallbackLogger,
|
|
1498
|
-
uow: createNoopUnitOfWork(() => ({
|
|
1499
|
-
todos,
|
|
1500
|
-
})),
|
|
1501
|
-
});
|
|
1502
|
-
`,
|
|
1503
|
-
productionInfrastructurePortsWithDrizzleTurso: `import { createGate, definePorts } from "@beignet/core/ports";
|
|
1504
|
-
import { todoPolicy } from "@/features/todos/policy";
|
|
1505
|
-
import { appError } from "@/features/shared/errors";
|
|
1506
|
-
import { fallbackLogger } from "./logger";
|
|
1507
|
-
|
|
1508
|
-
const gate = createGate({
|
|
1509
|
-
policies: [todoPolicy],
|
|
1510
|
-
onDeny(decision) {
|
|
1511
|
-
return appError("Forbidden", {
|
|
1512
|
-
message: decision.reason ?? "Forbidden",
|
|
1513
|
-
details: decision.details,
|
|
1514
|
-
});
|
|
1515
|
-
},
|
|
1516
|
-
});
|
|
1517
|
-
|
|
1518
|
-
export const appPorts = definePorts({
|
|
1519
|
-
gate,
|
|
1520
|
-
logger: fallbackLogger,
|
|
1521
|
-
});
|
|
1522
|
-
`,
|
|
1523
|
-
productionDrizzleConfig: `export default {
|
|
1524
|
-
schema: "./infra/db/schema/index.ts",
|
|
1525
|
-
out: "./drizzle",
|
|
1526
|
-
dialect: "sqlite",
|
|
1527
|
-
dbCredentials: {
|
|
1528
|
-
url: process.env.TURSO_DB_URL ?? "file:local.db",
|
|
1529
|
-
authToken: process.env.TURSO_DB_AUTH_TOKEN,
|
|
1530
|
-
},
|
|
1531
|
-
};
|
|
1532
|
-
`,
|
|
1533
|
-
productionDbSchema: `import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
1534
|
-
|
|
1535
|
-
export const user = sqliteTable("user", {
|
|
1536
|
-
id: text("id").primaryKey(),
|
|
1537
|
-
name: text("name").notNull(),
|
|
1538
|
-
email: text("email").notNull().unique(),
|
|
1539
|
-
emailVerified: integer("email_verified", { mode: "boolean" })
|
|
1540
|
-
.notNull()
|
|
1541
|
-
.default(false),
|
|
1542
|
-
image: text("image"),
|
|
1543
|
-
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
1544
|
-
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
1545
|
-
});
|
|
1546
|
-
|
|
1547
|
-
export const session = sqliteTable("session", {
|
|
1548
|
-
id: text("id").primaryKey(),
|
|
1549
|
-
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
1550
|
-
token: text("token").notNull().unique(),
|
|
1551
|
-
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
1552
|
-
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
1553
|
-
ipAddress: text("ip_address"),
|
|
1554
|
-
userAgent: text("user_agent"),
|
|
1555
|
-
userId: text("user_id")
|
|
1556
|
-
.notNull()
|
|
1557
|
-
.references(() => user.id, { onDelete: "cascade" }),
|
|
1558
|
-
});
|
|
1559
|
-
|
|
1560
|
-
export const account = sqliteTable("account", {
|
|
1561
|
-
id: text("id").primaryKey(),
|
|
1562
|
-
accountId: text("account_id").notNull(),
|
|
1563
|
-
providerId: text("provider_id").notNull(),
|
|
1564
|
-
userId: text("user_id")
|
|
1565
|
-
.notNull()
|
|
1566
|
-
.references(() => user.id, { onDelete: "cascade" }),
|
|
1567
|
-
accessToken: text("access_token"),
|
|
1568
|
-
refreshToken: text("refresh_token"),
|
|
1569
|
-
idToken: text("id_token"),
|
|
1570
|
-
accessTokenExpiresAt: integer("access_token_expires_at", {
|
|
1571
|
-
mode: "timestamp",
|
|
1572
|
-
}),
|
|
1573
|
-
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
|
1574
|
-
mode: "timestamp",
|
|
1575
|
-
}),
|
|
1576
|
-
scope: text("scope"),
|
|
1577
|
-
password: text("password"),
|
|
1578
|
-
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
1579
|
-
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
1580
|
-
});
|
|
1581
|
-
|
|
1582
|
-
export const verification = sqliteTable("verification", {
|
|
1583
|
-
id: text("id").primaryKey(),
|
|
1584
|
-
identifier: text("identifier").notNull(),
|
|
1585
|
-
value: text("value").notNull(),
|
|
1586
|
-
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
1587
|
-
createdAt: integer("created_at", { mode: "timestamp" }),
|
|
1588
|
-
updatedAt: integer("updated_at", { mode: "timestamp" }),
|
|
1589
|
-
});
|
|
1590
|
-
|
|
1591
|
-
export const todos = sqliteTable("todos", {
|
|
1592
|
-
id: text("id").primaryKey(),
|
|
1593
|
-
title: text("title").notNull(),
|
|
1594
|
-
completed: integer("completed", { mode: "boolean" }).notNull().default(false),
|
|
1595
|
-
createdAt: text("created_at").notNull(),
|
|
1596
|
-
});
|
|
1597
|
-
|
|
1598
|
-
export const todoAttachments = sqliteTable(
|
|
1599
|
-
"todo_attachments",
|
|
1600
|
-
{
|
|
1601
|
-
id: text("id").primaryKey(),
|
|
1602
|
-
todoId: text("todo_id")
|
|
1603
|
-
.notNull()
|
|
1604
|
-
.references(() => todos.id, { onDelete: "cascade" }),
|
|
1605
|
-
key: text("key").notNull(),
|
|
1606
|
-
fileName: text("file_name").notNull(),
|
|
1607
|
-
contentType: text("content_type").notNull(),
|
|
1608
|
-
size: integer("size").notNull(),
|
|
1609
|
-
createdAt: text("created_at").notNull(),
|
|
1610
|
-
},
|
|
1611
|
-
(table) => ({
|
|
1612
|
-
todoIdx: index("todo_attachments_todo_idx").on(
|
|
1613
|
-
table.todoId,
|
|
1614
|
-
table.createdAt,
|
|
1615
|
-
),
|
|
1616
|
-
keyIdx: index("todo_attachments_key_idx").on(table.key),
|
|
1617
|
-
}),
|
|
1618
|
-
);
|
|
1619
|
-
|
|
1620
|
-
export const auditLog = sqliteTable(
|
|
1621
|
-
"audit_log",
|
|
1622
|
-
{
|
|
1623
|
-
id: text("id").primaryKey(),
|
|
1624
|
-
action: text("action").notNull(),
|
|
1625
|
-
actorType: text("actor_type", {
|
|
1626
|
-
enum: ["anonymous", "service", "system", "user"],
|
|
1627
|
-
}).notNull(),
|
|
1628
|
-
actorId: text("actor_id"),
|
|
1629
|
-
actorDisplayName: text("actor_display_name"),
|
|
1630
|
-
tenantId: text("tenant_id"),
|
|
1631
|
-
tenantSlug: text("tenant_slug"),
|
|
1632
|
-
resourceType: text("resource_type"),
|
|
1633
|
-
resourceId: text("resource_id"),
|
|
1634
|
-
resourceName: text("resource_name"),
|
|
1635
|
-
outcome: text("outcome", { enum: ["success", "failure"] })
|
|
1636
|
-
.notNull()
|
|
1637
|
-
.default("success"),
|
|
1638
|
-
requestId: text("request_id"),
|
|
1639
|
-
traceId: text("trace_id"),
|
|
1640
|
-
message: text("message"),
|
|
1641
|
-
metadata: text("metadata"),
|
|
1642
|
-
actorMetadata: text("actor_metadata"),
|
|
1643
|
-
tenantMetadata: text("tenant_metadata"),
|
|
1644
|
-
resourceMetadata: text("resource_metadata"),
|
|
1645
|
-
occurredAt: text("occurred_at").notNull(),
|
|
1646
|
-
},
|
|
1647
|
-
(table) => ({
|
|
1648
|
-
actionIdx: index("audit_log_action_idx").on(table.action),
|
|
1649
|
-
actorIdx: index("audit_log_actor_idx").on(table.actorType, table.actorId),
|
|
1650
|
-
occurredAtIdx: index("audit_log_occurred_at_idx").on(table.occurredAt),
|
|
1651
|
-
requestIdx: index("audit_log_request_idx").on(table.requestId),
|
|
1652
|
-
resourceIdx: index("audit_log_resource_idx").on(
|
|
1653
|
-
table.resourceType,
|
|
1654
|
-
table.resourceId,
|
|
1655
|
-
),
|
|
1656
|
-
tenantIdx: index("audit_log_tenant_idx").on(table.tenantId),
|
|
1657
|
-
}),
|
|
1658
|
-
);
|
|
1659
|
-
|
|
1660
|
-
export const outboxMessages = sqliteTable(
|
|
1661
|
-
"outbox_messages",
|
|
1662
|
-
{
|
|
1663
|
-
id: text("id").primaryKey(),
|
|
1664
|
-
kind: text("kind", { enum: ["event", "job"] }).notNull(),
|
|
1665
|
-
name: text("name").notNull(),
|
|
1666
|
-
payloadJson: text("payload_json").notNull(),
|
|
1667
|
-
status: text("status", {
|
|
1668
|
-
enum: ["pending", "claimed", "delivered", "deadLettered"],
|
|
1669
|
-
}).notNull(),
|
|
1670
|
-
attempts: integer("attempts").notNull().default(0),
|
|
1671
|
-
maxAttempts: integer("max_attempts").notNull().default(3),
|
|
1672
|
-
availableAt: text("available_at").notNull(),
|
|
1673
|
-
claimedAt: text("claimed_at"),
|
|
1674
|
-
lockedUntil: text("locked_until"),
|
|
1675
|
-
claimToken: text("claim_token"),
|
|
1676
|
-
deliveredAt: text("delivered_at"),
|
|
1677
|
-
lastErrorJson: text("last_error_json"),
|
|
1678
|
-
createdAt: text("created_at").notNull(),
|
|
1679
|
-
updatedAt: text("updated_at").notNull(),
|
|
1680
|
-
},
|
|
1681
|
-
(table) => ({
|
|
1682
|
-
availableIdx: index("outbox_messages_available_idx").on(
|
|
1683
|
-
table.status,
|
|
1684
|
-
table.availableAt,
|
|
1685
|
-
),
|
|
1686
|
-
lockedIdx: index("outbox_messages_locked_idx").on(
|
|
1687
|
-
table.status,
|
|
1688
|
-
table.lockedUntil,
|
|
1689
|
-
),
|
|
1690
|
-
}),
|
|
1691
|
-
);
|
|
1692
|
-
`,
|
|
1693
|
-
productionDrizzleTodoRepository: `import { offsetPageResult } from "@beignet/core/pagination";
|
|
1694
|
-
import { count, desc, eq } from "drizzle-orm";
|
|
1695
|
-
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
1696
|
-
import type {
|
|
1697
|
-
TodoAttachmentRepository,
|
|
1698
|
-
TodoRepository,
|
|
1699
|
-
} from "@/features/todos/ports";
|
|
1700
|
-
import type {
|
|
1701
|
-
CreateTodoInput,
|
|
1702
|
-
Todo,
|
|
1703
|
-
} from "@/features/todos/use-cases/schemas";
|
|
1704
|
-
import * as schema from "@/infra/db/schema";
|
|
1705
|
-
|
|
1706
|
-
type TodoRow = typeof schema.todos.$inferSelect;
|
|
1707
|
-
|
|
1708
|
-
function toTodo(row: TodoRow): Todo {
|
|
1709
|
-
return {
|
|
1710
|
-
id: row.id,
|
|
1711
|
-
title: row.title,
|
|
1712
|
-
completed: row.completed,
|
|
1713
|
-
createdAt: row.createdAt,
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
export function createDrizzleTodoRepository(
|
|
1718
|
-
db: DrizzleTursoDatabase<typeof schema>,
|
|
1719
|
-
): TodoRepository {
|
|
1720
|
-
return {
|
|
1721
|
-
async list(page) {
|
|
1722
|
-
const rows = await db
|
|
1723
|
-
.select()
|
|
1724
|
-
.from(schema.todos)
|
|
1725
|
-
.orderBy(desc(schema.todos.createdAt))
|
|
1726
|
-
.limit(page.limit)
|
|
1727
|
-
.offset(page.offset);
|
|
1728
|
-
const [{ total }] = await db.select({ total: count() }).from(schema.todos);
|
|
1729
|
-
|
|
1730
|
-
return offsetPageResult(rows.map(toTodo), page, total);
|
|
1731
|
-
},
|
|
1732
|
-
async findById(id: string) {
|
|
1733
|
-
const [row] = await db
|
|
1734
|
-
.select()
|
|
1735
|
-
.from(schema.todos)
|
|
1736
|
-
.where(eq(schema.todos.id, id))
|
|
1737
|
-
.limit(1);
|
|
1738
|
-
|
|
1739
|
-
return row ? toTodo(row) : null;
|
|
1740
|
-
},
|
|
1741
|
-
async create(input: CreateTodoInput) {
|
|
1742
|
-
const todo = {
|
|
1743
|
-
id: crypto.randomUUID(),
|
|
1744
|
-
title: input.title,
|
|
1745
|
-
completed: false,
|
|
1746
|
-
createdAt: new Date().toISOString(),
|
|
1747
|
-
};
|
|
1748
|
-
const [row] = await db.insert(schema.todos).values(todo).returning();
|
|
1749
|
-
|
|
1750
|
-
if (!row) {
|
|
1751
|
-
throw new Error("Failed to create todo");
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
return toTodo(row);
|
|
1755
|
-
},
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
export function createDrizzleTodoAttachmentRepository(
|
|
1760
|
-
db: DrizzleTursoDatabase<typeof schema>,
|
|
1761
|
-
): TodoAttachmentRepository {
|
|
1762
|
-
return {
|
|
1763
|
-
async create(input) {
|
|
1764
|
-
const attachment = {
|
|
1765
|
-
...input,
|
|
1766
|
-
createdAt: new Date().toISOString(),
|
|
1767
|
-
};
|
|
1768
|
-
const [row] = await db
|
|
1769
|
-
.insert(schema.todoAttachments)
|
|
1770
|
-
.values(attachment)
|
|
1771
|
-
.returning();
|
|
1772
|
-
|
|
1773
|
-
if (!row) {
|
|
1774
|
-
throw new Error("Failed to create todo attachment");
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
return {
|
|
1778
|
-
id: row.id,
|
|
1779
|
-
todoId: row.todoId,
|
|
1780
|
-
key: row.key,
|
|
1781
|
-
fileName: row.fileName,
|
|
1782
|
-
contentType: row.contentType,
|
|
1783
|
-
size: row.size,
|
|
1784
|
-
createdAt: row.createdAt,
|
|
1785
|
-
};
|
|
1786
|
-
},
|
|
1787
|
-
async findMany(todoId) {
|
|
1788
|
-
const rows = await db
|
|
1789
|
-
.select()
|
|
1790
|
-
.from(schema.todoAttachments)
|
|
1791
|
-
.where(eq(schema.todoAttachments.todoId, todoId))
|
|
1792
|
-
.orderBy(desc(schema.todoAttachments.createdAt));
|
|
1793
|
-
|
|
1794
|
-
return rows.map((row) => ({
|
|
1795
|
-
id: row.id,
|
|
1796
|
-
todoId: row.todoId,
|
|
1797
|
-
key: row.key,
|
|
1798
|
-
fileName: row.fileName,
|
|
1799
|
-
contentType: row.contentType,
|
|
1800
|
-
size: row.size,
|
|
1801
|
-
createdAt: row.createdAt,
|
|
1802
|
-
}));
|
|
1803
|
-
},
|
|
1804
|
-
};
|
|
1805
|
-
}
|
|
1806
|
-
`,
|
|
1807
|
-
productionDbRepositories: `import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
1808
|
-
import {
|
|
1809
|
-
createDrizzleTodoAttachmentRepository,
|
|
1810
|
-
createDrizzleTodoRepository,
|
|
1811
|
-
} from "@/infra/todos/drizzle-todo-repository";
|
|
1812
|
-
import type { AppTransactionPorts } from "@/ports";
|
|
1813
|
-
import * as schema from "./schema";
|
|
1814
|
-
|
|
1815
|
-
export function createRepositories(
|
|
1816
|
-
db: DrizzleTursoDatabase<typeof schema>,
|
|
1817
|
-
): Omit<AppTransactionPorts, "audit" | "events" | "jobs" | "outbox"> {
|
|
1818
|
-
return {
|
|
1819
|
-
todoAttachments: createDrizzleTodoAttachmentRepository(db),
|
|
1820
|
-
todos: createDrizzleTodoRepository(db),
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
`,
|
|
1824
|
-
productionDrizzleAuditLog: `import type { AuditLogEntry, AuditLogPort } from "@beignet/core/ports";
|
|
1825
|
-
import {
|
|
1826
|
-
normalizeAuditLogEntry,
|
|
1827
|
-
redactAuditLogEntry,
|
|
1828
|
-
} from "@beignet/core/ports";
|
|
1829
|
-
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
1830
|
-
import * as schema from "@/infra/db/schema";
|
|
1831
|
-
|
|
1832
|
-
function serialize(value: unknown): string | null {
|
|
1833
|
-
return value === undefined ? null : JSON.stringify(value);
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
export function createDrizzleAuditLog(
|
|
1837
|
-
db: DrizzleTursoDatabase<typeof schema>,
|
|
1838
|
-
): AuditLogPort {
|
|
1839
|
-
return {
|
|
1840
|
-
async record(input) {
|
|
1841
|
-
const entry: AuditLogEntry = redactAuditLogEntry(
|
|
1842
|
-
normalizeAuditLogEntry(input),
|
|
1843
|
-
);
|
|
1844
|
-
|
|
1845
|
-
await db.insert(schema.auditLog).values({
|
|
1846
|
-
id: crypto.randomUUID(),
|
|
1847
|
-
action: entry.action,
|
|
1848
|
-
actorType: entry.actor.type,
|
|
1849
|
-
actorId: entry.actor.id ?? null,
|
|
1850
|
-
actorDisplayName: entry.actor.displayName ?? null,
|
|
1851
|
-
tenantId: entry.tenant?.id ?? null,
|
|
1852
|
-
tenantSlug: entry.tenant?.slug ?? null,
|
|
1853
|
-
resourceType: entry.resource?.type ?? null,
|
|
1854
|
-
resourceId: entry.resource?.id ?? null,
|
|
1855
|
-
resourceName: entry.resource?.name ?? null,
|
|
1856
|
-
outcome: entry.outcome,
|
|
1857
|
-
requestId: entry.requestId ?? null,
|
|
1858
|
-
traceId: entry.traceId ?? null,
|
|
1859
|
-
message: entry.message ?? null,
|
|
1860
|
-
metadata: serialize(entry.metadata),
|
|
1861
|
-
actorMetadata: serialize(entry.actor.metadata),
|
|
1862
|
-
tenantMetadata: serialize(entry.tenant?.metadata),
|
|
1863
|
-
resourceMetadata: serialize(entry.resource?.metadata),
|
|
1864
|
-
occurredAt: entry.occurredAt.toISOString(),
|
|
1865
|
-
});
|
|
1866
|
-
},
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
`,
|
|
1870
|
-
productionDbBootstrap: `import { createDrizzleTursoOutboxSetupStatements } from "@beignet/provider-drizzle-turso";
|
|
1871
|
-
import type { Client } from "@libsql/client";
|
|
1872
|
-
|
|
1873
|
-
type BootstrapOptions = {
|
|
1874
|
-
seed?: boolean;
|
|
1875
|
-
};
|
|
1876
|
-
|
|
1877
|
-
const setupStatements = [
|
|
1878
|
-
\`CREATE TABLE IF NOT EXISTS user (
|
|
1879
|
-
id text PRIMARY KEY NOT NULL,
|
|
1880
|
-
name text NOT NULL,
|
|
1881
|
-
email text NOT NULL UNIQUE,
|
|
1882
|
-
email_verified integer DEFAULT false NOT NULL,
|
|
1883
|
-
image text,
|
|
1884
|
-
created_at integer NOT NULL,
|
|
1885
|
-
updated_at integer NOT NULL
|
|
1886
|
-
)\`,
|
|
1887
|
-
\`CREATE TABLE IF NOT EXISTS session (
|
|
1888
|
-
id text PRIMARY KEY NOT NULL,
|
|
1889
|
-
expires_at integer NOT NULL,
|
|
1890
|
-
token text NOT NULL UNIQUE,
|
|
1891
|
-
created_at integer NOT NULL,
|
|
1892
|
-
updated_at integer NOT NULL,
|
|
1893
|
-
ip_address text,
|
|
1894
|
-
user_agent text,
|
|
1895
|
-
user_id text NOT NULL REFERENCES user(id) ON DELETE cascade
|
|
1896
|
-
)\`,
|
|
1897
|
-
\`CREATE TABLE IF NOT EXISTS account (
|
|
1898
|
-
id text PRIMARY KEY NOT NULL,
|
|
1899
|
-
account_id text NOT NULL,
|
|
1900
|
-
provider_id text NOT NULL,
|
|
1901
|
-
user_id text NOT NULL REFERENCES user(id) ON DELETE cascade,
|
|
1902
|
-
access_token text,
|
|
1903
|
-
refresh_token text,
|
|
1904
|
-
id_token text,
|
|
1905
|
-
access_token_expires_at integer,
|
|
1906
|
-
refresh_token_expires_at integer,
|
|
1907
|
-
scope text,
|
|
1908
|
-
password text,
|
|
1909
|
-
created_at integer NOT NULL,
|
|
1910
|
-
updated_at integer NOT NULL
|
|
1911
|
-
)\`,
|
|
1912
|
-
\`CREATE TABLE IF NOT EXISTS verification (
|
|
1913
|
-
id text PRIMARY KEY NOT NULL,
|
|
1914
|
-
identifier text NOT NULL,
|
|
1915
|
-
value text NOT NULL,
|
|
1916
|
-
expires_at integer NOT NULL,
|
|
1917
|
-
created_at integer,
|
|
1918
|
-
updated_at integer
|
|
1919
|
-
)\`,
|
|
1920
|
-
\`CREATE TABLE IF NOT EXISTS todos (
|
|
1921
|
-
id text PRIMARY KEY NOT NULL,
|
|
1922
|
-
title text NOT NULL,
|
|
1923
|
-
completed integer DEFAULT false NOT NULL,
|
|
1924
|
-
created_at text NOT NULL
|
|
1925
|
-
)\`,
|
|
1926
|
-
\`CREATE TABLE IF NOT EXISTS todo_attachments (
|
|
1927
|
-
id text PRIMARY KEY NOT NULL,
|
|
1928
|
-
todo_id text NOT NULL REFERENCES todos(id) ON DELETE cascade,
|
|
1929
|
-
key text NOT NULL,
|
|
1930
|
-
file_name text NOT NULL,
|
|
1931
|
-
content_type text NOT NULL,
|
|
1932
|
-
size integer NOT NULL,
|
|
1933
|
-
created_at text NOT NULL
|
|
1934
|
-
)\`,
|
|
1935
|
-
"CREATE INDEX IF NOT EXISTS todo_attachments_todo_idx ON todo_attachments (todo_id, created_at)",
|
|
1936
|
-
"CREATE INDEX IF NOT EXISTS todo_attachments_key_idx ON todo_attachments (key)",
|
|
1937
|
-
\`CREATE TABLE IF NOT EXISTS audit_log (
|
|
1938
|
-
id text PRIMARY KEY NOT NULL,
|
|
1939
|
-
action text NOT NULL,
|
|
1940
|
-
actor_type text NOT NULL,
|
|
1941
|
-
actor_id text,
|
|
1942
|
-
actor_display_name text,
|
|
1943
|
-
tenant_id text,
|
|
1944
|
-
tenant_slug text,
|
|
1945
|
-
resource_type text,
|
|
1946
|
-
resource_id text,
|
|
1947
|
-
resource_name text,
|
|
1948
|
-
outcome text DEFAULT 'success' NOT NULL,
|
|
1949
|
-
request_id text,
|
|
1950
|
-
trace_id text,
|
|
1951
|
-
message text,
|
|
1952
|
-
metadata text,
|
|
1953
|
-
actor_metadata text,
|
|
1954
|
-
tenant_metadata text,
|
|
1955
|
-
resource_metadata text,
|
|
1956
|
-
occurred_at text NOT NULL
|
|
1957
|
-
)\`,
|
|
1958
|
-
"CREATE INDEX IF NOT EXISTS audit_log_action_idx ON audit_log (action)",
|
|
1959
|
-
"CREATE INDEX IF NOT EXISTS audit_log_actor_idx ON audit_log (actor_type, actor_id)",
|
|
1960
|
-
"CREATE INDEX IF NOT EXISTS audit_log_occurred_at_idx ON audit_log (occurred_at)",
|
|
1961
|
-
"CREATE INDEX IF NOT EXISTS audit_log_request_idx ON audit_log (request_id)",
|
|
1962
|
-
"CREATE INDEX IF NOT EXISTS audit_log_resource_idx ON audit_log (resource_type, resource_id)",
|
|
1963
|
-
"CREATE INDEX IF NOT EXISTS audit_log_tenant_idx ON audit_log (tenant_id)",
|
|
1964
|
-
...createDrizzleTursoOutboxSetupStatements(),
|
|
1965
|
-
];
|
|
1966
|
-
|
|
1967
|
-
const seedTodos = [
|
|
1968
|
-
{
|
|
1969
|
-
id: "00000000-0000-4000-8000-000000000001",
|
|
1970
|
-
title: "Review the starter boundaries",
|
|
1971
|
-
completed: 0,
|
|
1972
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
1973
|
-
},
|
|
1974
|
-
{
|
|
1975
|
-
id: "00000000-0000-4000-8000-000000000002",
|
|
1976
|
-
title: "Create your first feature",
|
|
1977
|
-
completed: 0,
|
|
1978
|
-
createdAt: "2026-01-01T00:05:00.000Z",
|
|
1979
|
-
},
|
|
1980
|
-
] as const;
|
|
1981
|
-
|
|
1982
|
-
const resetTables = [
|
|
1983
|
-
"verification",
|
|
1984
|
-
"account",
|
|
1985
|
-
"session",
|
|
1986
|
-
"user",
|
|
1987
|
-
"todo_attachments",
|
|
1988
|
-
"todos",
|
|
1989
|
-
"audit_log",
|
|
1990
|
-
"outbox_messages",
|
|
1991
|
-
] as const;
|
|
1992
|
-
|
|
1993
|
-
export async function ensureStarterDatabase(
|
|
1994
|
-
client: Client,
|
|
1995
|
-
options: BootstrapOptions = {},
|
|
1996
|
-
): Promise<void> {
|
|
1997
|
-
for (const statement of setupStatements) {
|
|
1998
|
-
await client.execute(statement);
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
if (options.seed === false) return;
|
|
2002
|
-
|
|
2003
|
-
const result = await client.execute("SELECT count(*) as total FROM todos");
|
|
2004
|
-
const total = Number(result.rows[0]?.total ?? 0);
|
|
2005
|
-
if (total > 0) return;
|
|
2006
|
-
|
|
2007
|
-
for (const todo of seedTodos) {
|
|
2008
|
-
await client.execute({
|
|
2009
|
-
sql: "INSERT INTO todos (id, title, completed, created_at) VALUES (?, ?, ?, ?)",
|
|
2010
|
-
args: [todo.id, todo.title, todo.completed, todo.createdAt],
|
|
2011
|
-
});
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
export async function resetStarterDatabase(client: Client): Promise<void> {
|
|
2016
|
-
await client.execute("PRAGMA foreign_keys = OFF");
|
|
2017
|
-
try {
|
|
2018
|
-
for (const table of resetTables) {
|
|
2019
|
-
await client.execute(\`DROP TABLE IF EXISTS \${table}\`);
|
|
2020
|
-
}
|
|
2021
|
-
} finally {
|
|
2022
|
-
await client.execute("PRAGMA foreign_keys = ON");
|
|
2023
|
-
}
|
|
2024
|
-
await ensureStarterDatabase(client, { seed: false });
|
|
2025
|
-
}
|
|
2026
|
-
`,
|
|
2027
|
-
productionDbProvider: `import { registerListeners } from "@beignet/core/events";
|
|
2028
|
-
import { createMemoryIdempotencyStore } from "@beignet/core/idempotency";
|
|
2029
|
-
import { createInlineJobDispatcher } from "@beignet/core/jobs";
|
|
2030
|
-
import { createMemoryMailer } from "@beignet/core/mail";
|
|
2031
|
-
import { createInlineNotificationDispatcher } from "@beignet/core/notifications";
|
|
2032
|
-
import {
|
|
2033
|
-
createOutboxEventRecorder,
|
|
2034
|
-
createOutboxJobDispatcher,
|
|
2035
|
-
} from "@beignet/core/outbox";
|
|
2036
|
-
import {
|
|
2037
|
-
createSystemActor,
|
|
2038
|
-
createTenant,
|
|
2039
|
-
} from "@beignet/core/ports";
|
|
2040
|
-
import {
|
|
2041
|
-
createProvider,
|
|
2042
|
-
createProviderInstrumentation,
|
|
2043
|
-
} from "@beignet/core/providers";
|
|
2044
|
-
import {
|
|
2045
|
-
createDevtoolsAuditLog,
|
|
2046
|
-
createDevtoolsTraceContext,
|
|
2047
|
-
type DevtoolsPort,
|
|
2048
|
-
} from "@beignet/devtools";
|
|
2049
|
-
import {
|
|
2050
|
-
createDrizzleTursoOutboxPort,
|
|
2051
|
-
createDrizzleTursoUnitOfWork,
|
|
2052
|
-
type DbPort,
|
|
2053
|
-
} from "@beignet/provider-drizzle-turso";
|
|
2054
|
-
import { createInMemoryEventBus } from "@beignet/provider-event-bus-memory";
|
|
2055
|
-
import type { AppContext } from "@/app-context";
|
|
2056
|
-
import { todoListeners } from "@/features/todos/listeners";
|
|
2057
|
-
import { createDrizzleAuditLog } from "@/infra/audit/drizzle-audit-log";
|
|
2058
|
-
import type { AppPorts } from "@/ports";
|
|
2059
|
-
import { ensureStarterDatabase } from "./bootstrap";
|
|
2060
|
-
import { createRepositories } from "./repositories";
|
|
2061
|
-
import type * as schema from "./schema";
|
|
2062
|
-
|
|
2063
|
-
export const starterDatabaseProvider = createProvider({
|
|
2064
|
-
name: "starter-database",
|
|
2065
|
-
|
|
2066
|
-
async setup({ ports }) {
|
|
2067
|
-
const dbPort = (ports as { db: DbPort<typeof schema> }).db;
|
|
2068
|
-
const devtools = (ports as { devtools?: DevtoolsPort }).devtools;
|
|
2069
|
-
if (!dbPort) {
|
|
2070
|
-
throw new Error(
|
|
2071
|
-
"starterDatabaseProvider requires a db port. Register createDrizzleTursoProvider({ schema }) before it.",
|
|
2072
|
-
);
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
let currentPorts: AppContext["ports"] | undefined;
|
|
2076
|
-
const createBackgroundContext = (): AppContext => {
|
|
2077
|
-
if (!currentPorts) {
|
|
2078
|
-
throw new Error("Starter background context is not ready.");
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
const actor = createSystemActor("starter-background");
|
|
2082
|
-
const tenant = createTenant("tenant_default");
|
|
2083
|
-
const traceContext = createDevtoolsTraceContext();
|
|
2084
|
-
|
|
2085
|
-
return {
|
|
2086
|
-
actor,
|
|
2087
|
-
auth: null,
|
|
2088
|
-
gate: currentPorts.gate.bind({ actor, auth: null }),
|
|
2089
|
-
requestId: crypto.randomUUID(),
|
|
2090
|
-
...traceContext,
|
|
2091
|
-
ports: currentPorts,
|
|
2092
|
-
tenant,
|
|
2093
|
-
};
|
|
2094
|
-
};
|
|
2095
|
-
|
|
2096
|
-
const mailInstrumentation = createProviderInstrumentation(ports, {
|
|
2097
|
-
providerName: "memory-mailer",
|
|
2098
|
-
watcher: "mail",
|
|
2099
|
-
});
|
|
2100
|
-
const eventBus = createInMemoryEventBus({
|
|
2101
|
-
instrumentation: ports,
|
|
2102
|
-
onHandlerError(error: unknown, eventName: string) {
|
|
2103
|
-
console.error("Event handler failed", { error, eventName });
|
|
2104
|
-
},
|
|
2105
|
-
});
|
|
2106
|
-
const jobs = createInlineJobDispatcher<AppContext>({
|
|
2107
|
-
ctx: createBackgroundContext,
|
|
2108
|
-
});
|
|
2109
|
-
const notifications = createInlineNotificationDispatcher<AppContext>({
|
|
2110
|
-
ctx: createBackgroundContext,
|
|
2111
|
-
instrumentation: ports,
|
|
2112
|
-
});
|
|
2113
|
-
const mailer = createMemoryMailer({
|
|
2114
|
-
defaultFrom: "Beignet Starter <noreply@example.local>",
|
|
2115
|
-
onSend(delivery) {
|
|
2116
|
-
mailInstrumentation.custom({
|
|
2117
|
-
name: "mail.sent",
|
|
2118
|
-
label: "Mail sent",
|
|
2119
|
-
summary: delivery.message.subject,
|
|
2120
|
-
details: {
|
|
2121
|
-
to: delivery.message.to,
|
|
2122
|
-
subject: delivery.message.subject,
|
|
2123
|
-
id: delivery.id,
|
|
2124
|
-
},
|
|
2125
|
-
});
|
|
2126
|
-
},
|
|
2127
|
-
});
|
|
2128
|
-
const audit = createDevtoolsAuditLog({
|
|
2129
|
-
audit: createDrizzleAuditLog(dbPort.db),
|
|
2130
|
-
devtools,
|
|
2131
|
-
});
|
|
2132
|
-
const repositories = createRepositories(dbPort.db);
|
|
2133
|
-
const idempotency = createMemoryIdempotencyStore();
|
|
2134
|
-
const outbox = createDrizzleTursoOutboxPort(dbPort.db);
|
|
2135
|
-
const unregisterListeners = registerListeners(eventBus, todoListeners, {
|
|
2136
|
-
ctx: createBackgroundContext,
|
|
2137
|
-
onError(error, listener) {
|
|
2138
|
-
console.error("Event listener failed", {
|
|
2139
|
-
error,
|
|
2140
|
-
listenerName: listener.name,
|
|
2141
|
-
});
|
|
2142
|
-
},
|
|
2143
|
-
});
|
|
2144
|
-
|
|
2145
|
-
return {
|
|
2146
|
-
ports: {
|
|
2147
|
-
audit,
|
|
2148
|
-
...repositories,
|
|
2149
|
-
eventBus,
|
|
2150
|
-
idempotency,
|
|
2151
|
-
jobs,
|
|
2152
|
-
mailer,
|
|
2153
|
-
notifications,
|
|
2154
|
-
outbox,
|
|
2155
|
-
uow: createDrizzleTursoUnitOfWork({
|
|
2156
|
-
db: dbPort.db,
|
|
2157
|
-
createTransactionPorts: (tx) => {
|
|
2158
|
-
const txOutbox = createDrizzleTursoOutboxPort(tx);
|
|
2159
|
-
|
|
2160
|
-
return {
|
|
2161
|
-
audit: createDrizzleAuditLog(tx),
|
|
2162
|
-
...createRepositories(tx),
|
|
2163
|
-
events: createOutboxEventRecorder(txOutbox),
|
|
2164
|
-
jobs: createOutboxJobDispatcher(txOutbox),
|
|
2165
|
-
outbox: txOutbox,
|
|
2166
|
-
};
|
|
2167
|
-
},
|
|
2168
|
-
}),
|
|
2169
|
-
} satisfies Partial<AppPorts>,
|
|
2170
|
-
async start(ctx) {
|
|
2171
|
-
await ensureStarterDatabase(dbPort.client);
|
|
2172
|
-
currentPorts = ctx.ports as AppContext["ports"];
|
|
2173
|
-
},
|
|
2174
|
-
stop() {
|
|
2175
|
-
unregisterListeners();
|
|
2176
|
-
},
|
|
2177
|
-
};
|
|
2178
|
-
},
|
|
2179
|
-
});
|
|
2180
|
-
`,
|
|
2181
|
-
productionContractsTodos: `import { createContractGroup } from "@beignet/core/contracts";
|
|
2182
|
-
import { z } from "zod";
|
|
2183
|
-
import { errors } from "@/features/shared/errors";
|
|
2184
|
-
import {
|
|
2185
|
-
createTodoUseCase,
|
|
2186
|
-
getTodoUseCase,
|
|
2187
|
-
listTodosUseCase,
|
|
2188
|
-
} from "@/features/todos/use-cases";
|
|
2189
|
-
|
|
2190
|
-
const ErrorResponseSchema = z.object({
|
|
2191
|
-
code: z.string(),
|
|
2192
|
-
message: z.string(),
|
|
2193
|
-
requestId: z.string().optional(),
|
|
2194
|
-
});
|
|
2195
|
-
|
|
2196
|
-
const todos = createContractGroup()
|
|
2197
|
-
.namespace("todos")
|
|
2198
|
-
.errors({ Unauthorized: errors.Unauthorized })
|
|
2199
|
-
.responses({
|
|
2200
|
-
500: ErrorResponseSchema,
|
|
2201
|
-
});
|
|
2202
|
-
|
|
2203
|
-
export const listTodos = todos
|
|
2204
|
-
.get("/api/todos")
|
|
2205
|
-
.query(listTodosUseCase.inputSchema)
|
|
2206
|
-
.responses({
|
|
2207
|
-
200: listTodosUseCase.outputSchema,
|
|
2208
|
-
});
|
|
2209
|
-
|
|
2210
|
-
export const createTodo = todos
|
|
2211
|
-
.post("/api/todos")
|
|
2212
|
-
.body(createTodoUseCase.inputSchema)
|
|
2213
|
-
.errors({ Forbidden: errors.Forbidden })
|
|
2214
|
-
.responses({
|
|
2215
|
-
201: createTodoUseCase.outputSchema,
|
|
2216
|
-
});
|
|
2217
|
-
|
|
2218
|
-
export const getTodo = todos
|
|
2219
|
-
.get("/api/todos/:id")
|
|
2220
|
-
.pathParams(getTodoUseCase.inputSchema)
|
|
2221
|
-
.errors({ TodoNotFound: errors.TodoNotFound })
|
|
2222
|
-
.responses({
|
|
2223
|
-
200: getTodoUseCase.outputSchema,
|
|
2224
|
-
});
|
|
2225
|
-
`,
|
|
2226
|
-
productionTodoRoutes: `import { defineRouteGroup } from "@beignet/next";
|
|
2227
|
-
import type { AppContext } from "@/app-context";
|
|
2228
|
-
import { createTodo, getTodo, listTodos } from "@/features/todos/contracts";
|
|
2229
|
-
import {
|
|
2230
|
-
createTodoUseCase,
|
|
2231
|
-
getTodoUseCase,
|
|
2232
|
-
listTodosUseCase,
|
|
2233
|
-
} from "@/features/todos/use-cases";
|
|
2234
|
-
|
|
2235
|
-
export const todoRoutes = defineRouteGroup<AppContext>({
|
|
2236
|
-
name: "todos",
|
|
2237
|
-
routes: [
|
|
2238
|
-
{
|
|
2239
|
-
contract: listTodos,
|
|
2240
|
-
handle: async ({ ctx, query }) => ({
|
|
2241
|
-
status: 200,
|
|
2242
|
-
body: await listTodosUseCase.run({ ctx, input: query }),
|
|
2243
|
-
}),
|
|
2244
|
-
},
|
|
2245
|
-
{
|
|
2246
|
-
contract: createTodo,
|
|
2247
|
-
handle: async ({ ctx, body }) => ({
|
|
2248
|
-
status: 201,
|
|
2249
|
-
body: await createTodoUseCase.run({ ctx, input: body }),
|
|
2250
|
-
}),
|
|
2251
|
-
},
|
|
2252
|
-
{
|
|
2253
|
-
contract: getTodo,
|
|
2254
|
-
handle: async ({ ctx, path }) => ({
|
|
2255
|
-
status: 200,
|
|
2256
|
-
body: await getTodoUseCase.run({ ctx, input: path }),
|
|
2257
|
-
}),
|
|
2258
|
-
},
|
|
2259
|
-
],
|
|
2260
|
-
});
|
|
2261
|
-
`,
|
|
2262
|
-
productionServerRoutes: `import { contractsFromRoutes, defineRoutes } from "@beignet/next";
|
|
2263
|
-
import type { AppContext } from "@/app-context";
|
|
2264
|
-
import { todoRoutes } from "@/features/todos/routes";
|
|
2265
|
-
|
|
2266
|
-
export const routes = defineRoutes<AppContext>([
|
|
2267
|
-
todoRoutes,
|
|
2268
|
-
]);
|
|
2269
|
-
export const contracts = contractsFromRoutes(routes);
|
|
2270
|
-
`,
|
|
2271
|
-
productionServer: `import { createDevtoolsHooks } from "@beignet/devtools";
|
|
2272
|
-
import { createNextServer } from "@beignet/next";
|
|
2273
|
-
import {
|
|
2274
|
-
createAnonymousActor,
|
|
2275
|
-
createTenant,
|
|
2276
|
-
createUserActor,
|
|
2277
|
-
} from "@beignet/core/ports";
|
|
2278
|
-
import type { AppContext } from "@/app-context";
|
|
2279
|
-
import { appPorts } from "@/infra/app-ports";
|
|
2280
|
-
import { env } from "@/lib/env";
|
|
2281
|
-
import { providers } from "./providers";
|
|
2282
|
-
import { routes } from "./routes";
|
|
2283
|
-
|
|
2284
|
-
export const server = await createNextServer({
|
|
2285
|
-
ports: appPorts,
|
|
2286
|
-
providers,
|
|
2287
|
-
providerConfig: {
|
|
2288
|
-
"drizzle-turso": {
|
|
2289
|
-
DB_URL: env.TURSO_DB_URL,
|
|
2290
|
-
DB_AUTH_TOKEN: env.TURSO_DB_AUTH_TOKEN,
|
|
2291
|
-
},
|
|
2292
|
-
},
|
|
2293
|
-
hooks: [
|
|
2294
|
-
createDevtoolsHooks<AppContext>({
|
|
2295
|
-
requestIdHeader: "x-request-id",
|
|
2296
|
-
}),
|
|
2297
|
-
],
|
|
2298
|
-
createContext: async ({ req, ports }) => {
|
|
2299
|
-
const auth = await ports.auth.getSession(req);
|
|
2300
|
-
const tenantId = req.headers.get("x-tenant-id") || undefined;
|
|
2301
|
-
const context = {
|
|
2302
|
-
requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
|
|
2303
|
-
actor: auth
|
|
2304
|
-
? createUserActor(auth.user.id, { displayName: auth.user.name })
|
|
2305
|
-
: createAnonymousActor(),
|
|
2306
|
-
auth,
|
|
2307
|
-
ports,
|
|
2308
|
-
tenant: tenantId ? createTenant(tenantId) : undefined,
|
|
2309
|
-
};
|
|
2310
|
-
|
|
2311
|
-
return {
|
|
2312
|
-
...context,
|
|
2313
|
-
gate: ports.gate.bind(context),
|
|
2314
|
-
};
|
|
2315
|
-
},
|
|
2316
|
-
routes,
|
|
2317
|
-
mapUnhandledError: ({ err, ctx }) => {
|
|
2318
|
-
ctx?.ports.logger.error("Unhandled API error", {
|
|
2319
|
-
error: err,
|
|
2320
|
-
requestId: ctx?.requestId,
|
|
2321
|
-
environment: env.NODE_ENV,
|
|
2322
|
-
});
|
|
2323
|
-
|
|
2324
|
-
return {
|
|
2325
|
-
status: 500,
|
|
2326
|
-
body: {
|
|
2327
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
2328
|
-
message: "Internal server error",
|
|
2329
|
-
requestId: ctx?.requestId,
|
|
2330
|
-
},
|
|
2331
|
-
};
|
|
2332
|
-
},
|
|
2333
|
-
});
|
|
2334
|
-
`,
|
|
2335
|
-
productionServerWithDrizzleTurso: `import { createDevtoolsHooks } from "@beignet/devtools";
|
|
2336
|
-
import { createNextServer } from "@beignet/next";
|
|
2337
|
-
import {
|
|
2338
|
-
createAnonymousActor,
|
|
2339
|
-
createTenant,
|
|
2340
|
-
createUserActor,
|
|
2341
|
-
} from "@beignet/core/ports";
|
|
2342
|
-
import type { AppContext } from "@/app-context";
|
|
2343
|
-
import { appPorts } from "@/infra/app-ports";
|
|
2344
|
-
import { env } from "@/lib/env";
|
|
2345
|
-
import { providers } from "./providers";
|
|
2346
|
-
import { routes } from "./routes";
|
|
2347
|
-
|
|
2348
|
-
export const server = await createNextServer({
|
|
2349
|
-
ports: appPorts,
|
|
2350
|
-
providers,
|
|
2351
|
-
providerConfig: {
|
|
2352
|
-
"drizzle-turso": {
|
|
2353
|
-
DB_URL: env.TURSO_DB_URL,
|
|
2354
|
-
DB_AUTH_TOKEN: env.TURSO_DB_AUTH_TOKEN,
|
|
2355
|
-
},
|
|
2356
|
-
},
|
|
2357
|
-
hooks: [
|
|
2358
|
-
createDevtoolsHooks<AppContext>({
|
|
2359
|
-
requestIdHeader: "x-request-id",
|
|
2360
|
-
}),
|
|
2361
|
-
],
|
|
2362
|
-
createContext: async ({ req, ports }) => {
|
|
2363
|
-
const auth = await ports.auth.getSession(req);
|
|
2364
|
-
const tenantId = req.headers.get("x-tenant-id") || undefined;
|
|
2365
|
-
|
|
2366
|
-
const context = {
|
|
2367
|
-
requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
|
|
2368
|
-
actor: auth
|
|
2369
|
-
? createUserActor(auth.user.id, { displayName: auth.user.name })
|
|
2370
|
-
: createAnonymousActor(),
|
|
2371
|
-
auth,
|
|
2372
|
-
ports,
|
|
2373
|
-
tenant: tenantId ? createTenant(tenantId) : undefined,
|
|
2374
|
-
};
|
|
2375
|
-
|
|
2376
|
-
return {
|
|
2377
|
-
...context,
|
|
2378
|
-
gate: ports.gate.bind(context),
|
|
2379
|
-
};
|
|
2380
|
-
},
|
|
2381
|
-
routes,
|
|
2382
|
-
mapUnhandledError: ({ err, ctx }) => {
|
|
2383
|
-
ctx?.ports.logger.error("Unhandled API error", {
|
|
2384
|
-
error: err,
|
|
2385
|
-
requestId: ctx?.requestId,
|
|
2386
|
-
environment: env.NODE_ENV,
|
|
2387
|
-
});
|
|
2388
|
-
|
|
2389
|
-
return {
|
|
2390
|
-
status: 500,
|
|
2391
|
-
body: {
|
|
2392
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
2393
|
-
message: "Internal server error",
|
|
2394
|
-
requestId: ctx?.requestId,
|
|
2395
|
-
},
|
|
2396
|
-
};
|
|
2397
|
-
},
|
|
2398
|
-
});
|
|
2399
|
-
`,
|
|
2400
|
-
productionApiCatchAllRoute: `import { server } from "@/server";
|
|
2401
|
-
|
|
2402
|
-
export const GET = server.api;
|
|
2403
|
-
export const HEAD = server.api;
|
|
2404
|
-
export const OPTIONS = server.api;
|
|
2405
|
-
export const PATCH = server.api;
|
|
2406
|
-
export const POST = server.api;
|
|
2407
|
-
export const PUT = server.api;
|
|
2408
|
-
export const DELETE = server.api;
|
|
2409
|
-
`,
|
|
2410
|
-
productionHealthRoute: `import { env } from "@/lib/env";
|
|
2411
|
-
|
|
2412
|
-
export function GET() {
|
|
2413
|
-
return Response.json(
|
|
2414
|
-
{
|
|
2415
|
-
status: "ok",
|
|
2416
|
-
environment: env.NODE_ENV,
|
|
2417
|
-
timestamp: new Date().toISOString(),
|
|
2418
|
-
},
|
|
2419
|
-
{
|
|
2420
|
-
headers: {
|
|
2421
|
-
"cache-control": "no-store",
|
|
2422
|
-
},
|
|
2423
|
-
},
|
|
2424
|
-
);
|
|
2425
|
-
}
|
|
2426
|
-
`,
|
|
2427
|
-
productionDevtoolsRoute: `import { createDevtoolsRoute } from "@beignet/devtools";
|
|
2428
|
-
import { server } from "@/server";
|
|
2429
|
-
|
|
2430
|
-
export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
|
|
2431
|
-
basePath: "/api/devtools",
|
|
2432
|
-
});
|
|
2433
|
-
`,
|
|
2434
|
-
productionStorageRoute: `import { createStorageRoute } from "@beignet/next";
|
|
2435
|
-
import { server } from "@/server";
|
|
2436
|
-
|
|
2437
|
-
export const { GET, HEAD } = createStorageRoute(server.ports.storage, {
|
|
2438
|
-
basePath: "/storage",
|
|
2439
|
-
});
|
|
2440
|
-
`,
|
|
2441
|
-
productionOpenApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
|
|
2442
|
-
import { server } from "@/server";
|
|
2443
|
-
|
|
2444
|
-
export const GET = createOpenAPIHandler(server.contracts, {
|
|
2445
|
-
title: "Beignet starter API",
|
|
2446
|
-
version: "0.1.0",
|
|
2447
|
-
});
|
|
2448
|
-
`,
|
|
2449
|
-
productionAuthRoute: `import { toNextJsHandler } from "better-auth/next-js";
|
|
2450
|
-
import { auth } from "@/lib/better-auth";
|
|
2451
|
-
|
|
2452
|
-
export const { GET, POST } = toNextJsHandler(auth);
|
|
2453
|
-
`,
|
|
2454
|
-
productionBetterAuth: `import { createClient } from "@libsql/client";
|
|
2455
|
-
import { betterAuth } from "better-auth";
|
|
2456
|
-
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
2457
|
-
import { drizzle } from "drizzle-orm/libsql";
|
|
2458
|
-
import * as schema from "@/infra/db/schema";
|
|
2459
|
-
import { ensureStarterDatabase } from "@/infra/db/bootstrap";
|
|
2460
|
-
import { env } from "@/lib/env";
|
|
2461
|
-
|
|
2462
|
-
const client = createClient({
|
|
2463
|
-
url: env.TURSO_DB_URL,
|
|
2464
|
-
authToken: env.TURSO_DB_AUTH_TOKEN,
|
|
2465
|
-
});
|
|
2466
|
-
|
|
2467
|
-
await ensureStarterDatabase(client);
|
|
2468
|
-
|
|
2469
|
-
const db = drizzle(client, { schema });
|
|
2470
|
-
|
|
2471
|
-
const trustedOrigins = [
|
|
2472
|
-
env.APP_URL,
|
|
2473
|
-
env.BETTER_AUTH_URL,
|
|
2474
|
-
...(env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",")
|
|
2475
|
-
.map((origin) => origin.trim())
|
|
2476
|
-
.filter(Boolean) ?? []),
|
|
2477
|
-
];
|
|
2478
|
-
|
|
2479
|
-
export const auth = betterAuth({
|
|
2480
|
-
baseURL: env.BETTER_AUTH_URL,
|
|
2481
|
-
secret: env.BETTER_AUTH_SECRET,
|
|
2482
|
-
trustedOrigins: [...new Set(trustedOrigins)],
|
|
2483
|
-
database: drizzleAdapter(db, {
|
|
2484
|
-
provider: "sqlite",
|
|
2485
|
-
schema,
|
|
2486
|
-
}),
|
|
2487
|
-
emailAndPassword: {
|
|
2488
|
-
enabled: true,
|
|
2489
|
-
},
|
|
2490
|
-
});
|
|
2491
|
-
`,
|
|
2492
|
-
productionDbSeed: `import { createClient } from "@libsql/client";
|
|
2493
|
-
import { ensureStarterDatabase } from "@/infra/db/bootstrap";
|
|
2494
|
-
import { env } from "@/lib/env";
|
|
2495
|
-
|
|
2496
|
-
const client = createClient({
|
|
2497
|
-
url: env.TURSO_DB_URL,
|
|
2498
|
-
authToken: env.TURSO_DB_AUTH_TOKEN,
|
|
2499
|
-
});
|
|
2500
|
-
|
|
2501
|
-
try {
|
|
2502
|
-
await ensureStarterDatabase(client, { seed: true });
|
|
2503
|
-
console.log("Database seeded.");
|
|
2504
|
-
} finally {
|
|
2505
|
-
client.close();
|
|
2506
|
-
}
|
|
2507
|
-
`,
|
|
2508
|
-
productionDbReset: `import { createClient } from "@libsql/client";
|
|
2509
|
-
import { resetStarterDatabase } from "@/infra/db/bootstrap";
|
|
2510
|
-
import { env } from "@/lib/env";
|
|
2511
|
-
|
|
2512
|
-
if (!env.TURSO_DB_URL.startsWith("file:") && process.env.BEIGNET_ALLOW_DATABASE_RESET !== "true") {
|
|
2513
|
-
throw new Error(
|
|
2514
|
-
"Refusing to reset a non-local database. Set BEIGNET_ALLOW_DATABASE_RESET=true if this is intentional.",
|
|
2515
|
-
);
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
const client = createClient({
|
|
2519
|
-
url: env.TURSO_DB_URL,
|
|
2520
|
-
authToken: env.TURSO_DB_AUTH_TOKEN,
|
|
2521
|
-
});
|
|
2522
|
-
|
|
2523
|
-
try {
|
|
2524
|
-
await resetStarterDatabase(client);
|
|
2525
|
-
console.log("Database reset.");
|
|
2526
|
-
} finally {
|
|
2527
|
-
client.close();
|
|
2528
|
-
}
|
|
2529
|
-
`,
|
|
2530
|
-
productionAuditHelper: `import type { AuditLogEntryInput } from "@beignet/core/ports";
|
|
2531
|
-
import type { AppContext } from "@/app-context";
|
|
2532
|
-
|
|
2533
|
-
export function auditEntry(
|
|
2534
|
-
ctx: AppContext,
|
|
2535
|
-
entry: Omit<
|
|
2536
|
-
AuditLogEntryInput,
|
|
2537
|
-
"actor" | "tenant" | "requestId" | "traceId"
|
|
2538
|
-
>,
|
|
2539
|
-
): AuditLogEntryInput {
|
|
2540
|
-
return {
|
|
2541
|
-
...entry,
|
|
2542
|
-
actor: ctx.actor,
|
|
2543
|
-
tenant: ctx.tenant,
|
|
2544
|
-
requestId: ctx.requestId,
|
|
2545
|
-
traceId: ctx.traceId,
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
`,
|
|
2549
|
-
productionTodoEvents: `import { defineEvent } from "@beignet/core/events";
|
|
2550
|
-
import { z } from "zod";
|
|
2551
|
-
|
|
2552
|
-
export const TodoCreated = defineEvent("todos.created", {
|
|
2553
|
-
payload: z.object({
|
|
2554
|
-
todoId: z.string().uuid(),
|
|
2555
|
-
title: z.string(),
|
|
2556
|
-
}),
|
|
2557
|
-
});
|
|
2558
|
-
|
|
2559
|
-
export const todoEvents = [TodoCreated] as const;
|
|
2560
|
-
`,
|
|
2561
|
-
productionTodoJobs: `import { createJobHandlers, retry } from "@beignet/core/jobs";
|
|
2562
|
-
import { z } from "zod";
|
|
2563
|
-
import type { AppContext } from "@/app-context";
|
|
2564
|
-
import { auditEntry } from "@/lib/audit";
|
|
2565
|
-
|
|
2566
|
-
const jobs = createJobHandlers<AppContext>();
|
|
2567
|
-
|
|
2568
|
-
export const LogTodoCreatedJob = jobs.defineJob("todos.log-created", {
|
|
2569
|
-
payload: z.object({
|
|
2570
|
-
todoId: z.string().uuid(),
|
|
2571
|
-
title: z.string(),
|
|
2572
|
-
}),
|
|
2573
|
-
retry: retry.exponential({
|
|
2574
|
-
attempts: 3,
|
|
2575
|
-
initialDelay: "1s",
|
|
2576
|
-
maxDelay: "1m",
|
|
2577
|
-
}),
|
|
2578
|
-
async handle({ ctx, payload }) {
|
|
2579
|
-
ctx.ports.logger.info("Todo created job handled", {
|
|
2580
|
-
todoId: payload.todoId,
|
|
2581
|
-
});
|
|
2582
|
-
await ctx.ports.audit.record(
|
|
2583
|
-
auditEntry(ctx, {
|
|
2584
|
-
action: "jobs.todos.log-created",
|
|
2585
|
-
resource: { type: "todo", id: payload.todoId, name: payload.title },
|
|
2586
|
-
metadata: { jobName: "todos.log-created" },
|
|
2587
|
-
}),
|
|
2588
|
-
);
|
|
2589
|
-
},
|
|
2590
|
-
});
|
|
2591
|
-
|
|
2592
|
-
export const todoJobs = [LogTodoCreatedJob] as const;
|
|
2593
|
-
`,
|
|
2594
|
-
productionTodoNotifications: `import {
|
|
2595
|
-
createNotificationHandlers,
|
|
2596
|
-
defineMailNotificationChannel,
|
|
2597
|
-
} from "@beignet/core/notifications";
|
|
2598
|
-
import { z } from "zod";
|
|
2599
|
-
import type { AppContext } from "@/app-context";
|
|
2600
|
-
|
|
2601
|
-
const notifications = createNotificationHandlers<AppContext>();
|
|
2602
|
-
|
|
2603
|
-
export const TodoCreatedNotification = notifications.defineNotification(
|
|
2604
|
-
"todos.created",
|
|
2605
|
-
{
|
|
2606
|
-
payload: z.object({
|
|
2607
|
-
todoId: z.string().uuid(),
|
|
2608
|
-
title: z.string(),
|
|
2609
|
-
}),
|
|
2610
|
-
channels: {
|
|
2611
|
-
email: defineMailNotificationChannel(({ payload }) => ({
|
|
2612
|
-
to: "ops@example.local",
|
|
2613
|
-
subject: \`Todo created: \${payload.title}\`,
|
|
2614
|
-
text: \`Todo \${payload.todoId} was created.\`,
|
|
2615
|
-
})),
|
|
2616
|
-
},
|
|
2617
|
-
},
|
|
2618
|
-
);
|
|
2619
|
-
`,
|
|
2620
|
-
productionTodoListeners: `import { createEventHandlers } from "@beignet/core/events";
|
|
2621
|
-
import type { AppContext } from "@/app-context";
|
|
2622
|
-
import { TodoCreated } from "@/features/todos/domain/events";
|
|
2623
|
-
import { LogTodoCreatedJob } from "@/features/todos/jobs";
|
|
2624
|
-
import { TodoCreatedNotification } from "@/features/todos/notifications";
|
|
2625
|
-
import { auditEntry } from "@/lib/audit";
|
|
2626
|
-
|
|
2627
|
-
const listeners = createEventHandlers<AppContext>();
|
|
2628
|
-
|
|
2629
|
-
export const enqueueTodoCreatedWork = listeners.defineListener(TodoCreated, {
|
|
2630
|
-
name: "todos.enqueue-created-work",
|
|
2631
|
-
async handle({ ctx, payload }) {
|
|
2632
|
-
await ctx.ports.jobs.dispatch(LogTodoCreatedJob, payload);
|
|
2633
|
-
await ctx.ports.notifications.send(TodoCreatedNotification, payload);
|
|
2634
|
-
await ctx.ports.audit.record(
|
|
2635
|
-
auditEntry(ctx, {
|
|
2636
|
-
action: "listeners.todos.enqueue-created-work",
|
|
2637
|
-
resource: { type: "todo", id: payload.todoId, name: payload.title },
|
|
2638
|
-
metadata: {
|
|
2639
|
-
eventName: TodoCreated.name,
|
|
2640
|
-
jobName: LogTodoCreatedJob.name,
|
|
2641
|
-
notificationName: TodoCreatedNotification.name,
|
|
2642
|
-
},
|
|
2643
|
-
}),
|
|
2644
|
-
);
|
|
2645
|
-
},
|
|
2646
|
-
});
|
|
2647
|
-
|
|
2648
|
-
export const todoListeners = [enqueueTodoCreatedWork] as const;
|
|
2649
|
-
`,
|
|
2650
|
-
productionTodoSchedules: `import { normalizeOffsetPage } from "@beignet/core/pagination";
|
|
2651
|
-
import { createScheduleHandlers } from "@beignet/core/schedules";
|
|
2652
|
-
import { z } from "zod";
|
|
2653
|
-
import type { AppContext } from "@/app-context";
|
|
2654
|
-
import { auditEntry } from "@/lib/audit";
|
|
2655
|
-
|
|
2656
|
-
const schedules = createScheduleHandlers<AppContext>();
|
|
2657
|
-
|
|
2658
|
-
export const LogDailyTodoSummarySchedule = schedules.defineSchedule(
|
|
2659
|
-
"todos.log-daily-summary",
|
|
2660
|
-
{
|
|
2661
|
-
cron: "0 9 * * *",
|
|
2662
|
-
timezone: "America/Chicago",
|
|
2663
|
-
payload: z.object({
|
|
2664
|
-
date: z.string(),
|
|
2665
|
-
}),
|
|
2666
|
-
createPayload({ run }) {
|
|
2667
|
-
const date = run.scheduledAt ?? run.triggeredAt;
|
|
2668
|
-
return { date: date.toISOString().slice(0, 10) };
|
|
2669
|
-
},
|
|
2670
|
-
async handle({ ctx, payload, run }) {
|
|
2671
|
-
const { page } = await ctx.ports.todos.list(
|
|
2672
|
-
normalizeOffsetPage(
|
|
2673
|
-
{ limit: 1, offset: 0 },
|
|
2674
|
-
{ defaultLimit: 1, maxLimit: 1 },
|
|
2675
|
-
),
|
|
2676
|
-
);
|
|
2677
|
-
ctx.ports.logger.info("Daily todo summary schedule handled", {
|
|
2678
|
-
date: payload.date,
|
|
2679
|
-
todoCount: page.total,
|
|
2680
|
-
});
|
|
2681
|
-
await ctx.ports.audit.record(
|
|
2682
|
-
auditEntry(ctx, {
|
|
2683
|
-
action: "schedules.todos.daily-summary",
|
|
2684
|
-
resource: {
|
|
2685
|
-
type: "schedule",
|
|
2686
|
-
id: "todos.log-daily-summary",
|
|
2687
|
-
name: "Daily todo summary",
|
|
2688
|
-
},
|
|
2689
|
-
metadata: {
|
|
2690
|
-
date: payload.date,
|
|
2691
|
-
todoCount: page.total,
|
|
2692
|
-
source: run.source ?? "inline",
|
|
2693
|
-
triggeredAt: run.triggeredAt.toISOString(),
|
|
2694
|
-
},
|
|
2695
|
-
}),
|
|
2696
|
-
);
|
|
2697
|
-
},
|
|
2698
|
-
},
|
|
2699
|
-
);
|
|
2700
|
-
|
|
2701
|
-
export const todoSchedules = [LogDailyTodoSummarySchedule] as const;
|
|
2702
|
-
`,
|
|
2703
|
-
productionTodoUploads: `import { defineUpload, defineUploads } from "@beignet/core/uploads";
|
|
2704
|
-
import { z } from "zod";
|
|
2705
|
-
import type { AppContext } from "@/app-context";
|
|
2706
|
-
import { auditEntry } from "@/lib/audit";
|
|
2707
|
-
|
|
2708
|
-
const todoAttachmentMetadataSchema = z.object({
|
|
2709
|
-
todoId: z.string().uuid(),
|
|
2710
|
-
});
|
|
2711
|
-
|
|
2712
|
-
export const TodoAttachmentUpload = defineUpload<
|
|
2713
|
-
"todos.attachment",
|
|
2714
|
-
typeof todoAttachmentMetadataSchema,
|
|
2715
|
-
AppContext,
|
|
2716
|
-
{ attachmentIds: string[] }
|
|
2717
|
-
>("todos.attachment", {
|
|
2718
|
-
metadata: todoAttachmentMetadataSchema,
|
|
2719
|
-
file: {
|
|
2720
|
-
contentTypes: ["application/pdf", "text/plain", "text/plain;charset=utf-8"],
|
|
2721
|
-
maxSizeBytes: 5 * 1024 * 1024,
|
|
2722
|
-
maxFiles: 3,
|
|
2723
|
-
visibility: "private",
|
|
2724
|
-
cacheControl: "private, max-age=0",
|
|
2725
|
-
},
|
|
2726
|
-
authorize({ ctx }) {
|
|
2727
|
-
return ctx.actor.type === "user"
|
|
2728
|
-
? true
|
|
2729
|
-
: {
|
|
2730
|
-
allowed: false,
|
|
2731
|
-
reason: "You must be signed in to upload todo attachments.",
|
|
2732
|
-
};
|
|
2733
|
-
},
|
|
2734
|
-
key({ ctx, metadata, file, uploadId }) {
|
|
2735
|
-
const tenantId = ctx.tenant?.id ?? "tenant_default";
|
|
2736
|
-
const extension = file.name.includes(".")
|
|
2737
|
-
? file.name.split(".").pop()
|
|
2738
|
-
: undefined;
|
|
2739
|
-
const suffix = extension ? \`.\${extension}\` : "";
|
|
2740
|
-
return \`todos/\${tenantId}/\${metadata.todoId}/attachments/\${uploadId}\${suffix}\`;
|
|
2741
|
-
},
|
|
2742
|
-
storageMetadata({ ctx, metadata }) {
|
|
2743
|
-
return {
|
|
2744
|
-
tenantId: ctx.tenant?.id ?? "tenant_default",
|
|
2745
|
-
todoId: metadata.todoId,
|
|
2746
|
-
};
|
|
2747
|
-
},
|
|
2748
|
-
async onComplete({ ctx, metadata, files }) {
|
|
2749
|
-
const attachments = await Promise.all(
|
|
2750
|
-
files.map((file) =>
|
|
2751
|
-
ctx.ports.todoAttachments.create({
|
|
2752
|
-
id: file.uploadId,
|
|
2753
|
-
todoId: metadata.todoId,
|
|
2754
|
-
key: file.key,
|
|
2755
|
-
fileName: file.name,
|
|
2756
|
-
contentType: file.contentType,
|
|
2757
|
-
size: file.object.size,
|
|
2758
|
-
}),
|
|
2759
|
-
),
|
|
2760
|
-
);
|
|
2761
|
-
await ctx.ports.audit.record(
|
|
2762
|
-
auditEntry(ctx, {
|
|
2763
|
-
action: "todos.attachment.upload",
|
|
2764
|
-
resource: { type: "todo", id: metadata.todoId },
|
|
2765
|
-
metadata: {
|
|
2766
|
-
attachmentCount: attachments.length,
|
|
2767
|
-
keys: attachments.map((attachment) => attachment.key),
|
|
2768
|
-
},
|
|
2769
|
-
}),
|
|
2770
|
-
);
|
|
2771
|
-
|
|
2772
|
-
return {
|
|
2773
|
-
attachmentIds: attachments.map((attachment) => attachment.id),
|
|
2774
|
-
};
|
|
2775
|
-
},
|
|
2776
|
-
});
|
|
2777
|
-
|
|
2778
|
-
export const todoUploads = defineUploads({
|
|
2779
|
-
todoAttachment: TodoAttachmentUpload,
|
|
2780
|
-
});
|
|
2781
|
-
`,
|
|
2782
|
-
productionOutbox: `import { defineOutboxRegistry } from "@beignet/core/outbox";
|
|
2783
|
-
import { todoEvents } from "@/features/todos/domain/events";
|
|
2784
|
-
import { todoJobs } from "@/features/todos/jobs";
|
|
2785
|
-
|
|
2786
|
-
export const outboxRegistry = defineOutboxRegistry({
|
|
2787
|
-
events: todoEvents,
|
|
2788
|
-
jobs: todoJobs,
|
|
2789
|
-
});
|
|
2790
|
-
`,
|
|
2791
|
-
productionOutboxDrainRoute: `import { createOutboxDrainRoute } from "@beignet/next";
|
|
2792
|
-
import { env } from "@/lib/env";
|
|
2793
|
-
import { server } from "@/server";
|
|
2794
|
-
import { outboxRegistry } from "@/server/outbox";
|
|
2795
|
-
|
|
2796
|
-
export const runtime = "nodejs";
|
|
2797
|
-
|
|
2798
|
-
export const { GET, POST } = createOutboxDrainRoute({
|
|
2799
|
-
server,
|
|
2800
|
-
registry: outboxRegistry,
|
|
2801
|
-
secret: env.CRON_SECRET,
|
|
2802
|
-
});
|
|
2803
|
-
`,
|
|
2804
|
-
productionScheduleRoute: `import { createInlineScheduleRunner } from "@beignet/core/schedules";
|
|
2805
|
-
import type { AppContext } from "@/app-context";
|
|
2806
|
-
import { LogDailyTodoSummarySchedule } from "@/features/todos/schedules";
|
|
2807
|
-
import { env } from "@/lib/env";
|
|
2808
|
-
import { server } from "@/server";
|
|
2809
|
-
|
|
2810
|
-
async function runDailyTodoSummary(request: Request) {
|
|
2811
|
-
if (!env.CRON_SECRET) {
|
|
2812
|
-
return Response.json(
|
|
2813
|
-
{
|
|
2814
|
-
ok: false,
|
|
2815
|
-
error: "CRON_SECRET is not configured.",
|
|
2816
|
-
scheduleName: LogDailyTodoSummarySchedule.name,
|
|
2817
|
-
},
|
|
2818
|
-
{ status: 500 },
|
|
2819
|
-
);
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
|
-
if (request.headers.get("authorization") !== \`Bearer \${env.CRON_SECRET}\`) {
|
|
2823
|
-
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
const ctx = await server.createContextFromNext();
|
|
2827
|
-
const runner = createInlineScheduleRunner<AppContext>({
|
|
2828
|
-
ctx,
|
|
2829
|
-
onStart({ run, schedule }) {
|
|
2830
|
-
ctx.ports.devtools.record({
|
|
2831
|
-
type: "schedule",
|
|
2832
|
-
watcher: "schedules",
|
|
2833
|
-
requestId: ctx.requestId,
|
|
2834
|
-
traceId: ctx.traceId,
|
|
2835
|
-
scheduleName: schedule.name,
|
|
2836
|
-
status: "started",
|
|
2837
|
-
cron: schedule.cron,
|
|
2838
|
-
timezone: schedule.timezone,
|
|
2839
|
-
details: {
|
|
2840
|
-
source: run.source,
|
|
2841
|
-
scheduledAt: run.scheduledAt?.toISOString(),
|
|
2842
|
-
},
|
|
2843
|
-
});
|
|
2844
|
-
},
|
|
2845
|
-
onSuccess({ run, schedule }) {
|
|
2846
|
-
ctx.ports.devtools.record({
|
|
2847
|
-
type: "schedule",
|
|
2848
|
-
watcher: "schedules",
|
|
2849
|
-
requestId: ctx.requestId,
|
|
2850
|
-
traceId: ctx.traceId,
|
|
2851
|
-
scheduleName: schedule.name,
|
|
2852
|
-
status: "completed",
|
|
2853
|
-
cron: schedule.cron,
|
|
2854
|
-
timezone: schedule.timezone,
|
|
2855
|
-
details: {
|
|
2856
|
-
source: run.source,
|
|
2857
|
-
scheduledAt: run.scheduledAt?.toISOString(),
|
|
2858
|
-
},
|
|
2859
|
-
});
|
|
2860
|
-
},
|
|
2861
|
-
onError({ error, run, schedule }) {
|
|
2862
|
-
ctx.ports.devtools.record({
|
|
2863
|
-
type: "schedule",
|
|
2864
|
-
watcher: "schedules",
|
|
2865
|
-
requestId: ctx.requestId,
|
|
2866
|
-
traceId: ctx.traceId,
|
|
2867
|
-
scheduleName: schedule.name,
|
|
2868
|
-
status: "failed",
|
|
2869
|
-
cron: schedule.cron,
|
|
2870
|
-
timezone: schedule.timezone,
|
|
2871
|
-
details: {
|
|
2872
|
-
error,
|
|
2873
|
-
source: run.source,
|
|
2874
|
-
scheduledAt: run.scheduledAt?.toISOString(),
|
|
2875
|
-
},
|
|
2876
|
-
});
|
|
2877
|
-
ctx.ports.logger.error("Daily todo summary schedule failed", {
|
|
2878
|
-
error,
|
|
2879
|
-
scheduleName: schedule.name,
|
|
2880
|
-
});
|
|
2881
|
-
},
|
|
2882
|
-
});
|
|
2883
|
-
|
|
2884
|
-
try {
|
|
2885
|
-
await runner.run(LogDailyTodoSummarySchedule, {
|
|
2886
|
-
source: "starter-cron-route",
|
|
2887
|
-
});
|
|
2888
|
-
} catch {
|
|
2889
|
-
return Response.json(
|
|
2890
|
-
{
|
|
2891
|
-
ok: false,
|
|
2892
|
-
error: "Schedule failed",
|
|
2893
|
-
scheduleName: LogDailyTodoSummarySchedule.name,
|
|
2894
|
-
},
|
|
2895
|
-
{ status: 500 },
|
|
2896
|
-
);
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
return Response.json({
|
|
2900
|
-
ok: true,
|
|
2901
|
-
scheduleName: LogDailyTodoSummarySchedule.name,
|
|
2902
|
-
});
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
export const GET = runDailyTodoSummary;
|
|
2906
|
-
export const POST = runDailyTodoSummary;
|
|
2907
|
-
`,
|
|
2908
|
-
productionUploadsRoute: `import { createUploadRouter, uploadsFromRegistry } from "@beignet/core/uploads";
|
|
2909
|
-
import { createUploadRoute } from "@beignet/next";
|
|
2910
|
-
import type { AppContext } from "@/app-context";
|
|
2911
|
-
import { todoUploads } from "@/features/todos/uploads";
|
|
2912
|
-
import { server } from "@/server";
|
|
2913
|
-
|
|
2914
|
-
const uploadRouter = createUploadRouter<AppContext>({
|
|
2915
|
-
uploads: uploadsFromRegistry(todoUploads),
|
|
2916
|
-
ctx: () => server.createContextFromNext(),
|
|
2917
|
-
storage: server.ports.storage,
|
|
2918
|
-
instrumentation: server.ports.devtools,
|
|
2919
|
-
});
|
|
2920
|
-
|
|
2921
|
-
export const { POST } = createUploadRoute(uploadRouter);
|
|
2922
|
-
`,
|
|
2923
|
-
productionUploadClient: `import { createUploadClient } from "@beignet/core/uploads/client";
|
|
2924
|
-
import type { todoUploads } from "@/features/todos/uploads";
|
|
2925
|
-
|
|
2926
|
-
export const uploads = createUploadClient<typeof todoUploads>({
|
|
2927
|
-
baseUrl: "/api/uploads",
|
|
2928
|
-
});
|
|
2929
|
-
`,
|
|
2930
|
-
};
|
|
2931
|
-
function productionProviderPortImports(ctx) {
|
|
2932
|
-
const imports = [];
|
|
2933
|
-
if (hasIntegration(ctx, "upstash-rate-limit")) {
|
|
2934
|
-
imports.push("\tRateLimitPort,");
|
|
2935
|
-
}
|
|
2936
|
-
return imports;
|
|
2937
|
-
}
|
|
2938
|
-
function productionProviderPortFields(ctx) {
|
|
2939
|
-
const fields = [];
|
|
2940
|
-
if (hasIntegration(ctx, "upstash-rate-limit")) {
|
|
2941
|
-
fields.push("\trateLimit: RateLimitPort;");
|
|
2942
|
-
}
|
|
2943
|
-
return fields;
|
|
2944
|
-
}
|
|
2945
|
-
function productionPorts(ctx) {
|
|
2946
|
-
const mailImport = 'import type { MailerPort } from "@beignet/core/mail";\n';
|
|
2947
|
-
const imports = [
|
|
2948
|
-
"\tAuditLogPort,",
|
|
2949
|
-
"\tBoundGate,",
|
|
2950
|
-
"\tEventBusPort,",
|
|
2951
|
-
"\tGatePort,",
|
|
2952
|
-
"\tJobDispatcherPort,",
|
|
2953
|
-
"\tLoggerPort,",
|
|
2954
|
-
...productionProviderPortImports(ctx),
|
|
2955
|
-
"\tStoragePort,",
|
|
2956
|
-
"\tUnitOfWorkPort,",
|
|
2957
|
-
];
|
|
2958
|
-
const providerFields = productionProviderPortFields(ctx);
|
|
2959
|
-
return `import type { IdempotencyPort } from "@beignet/core/idempotency";
|
|
2960
|
-
${mailImport}import type { NotificationPort } from "@beignet/core/notifications";
|
|
2961
|
-
import type { OutboxPort } from "@beignet/core/outbox";
|
|
2962
|
-
import type {
|
|
2963
|
-
${imports.join("\n")}
|
|
2964
|
-
} from "@beignet/core/ports";
|
|
2965
|
-
import type { AuthorizationContext, todoPolicy } from "@/features/todos/policy";
|
|
2966
|
-
import type { AuthPort } from "./auth";
|
|
2967
|
-
import type { TodoAttachmentRepository, TodoRepository } from "@/features/todos/ports";
|
|
2968
|
-
|
|
2969
|
-
export type AppTransactionPorts = {
|
|
2970
|
-
audit: AuditLogPort;
|
|
2971
|
-
events: import("@beignet/core/ports").BufferedDomainEventRecorder;
|
|
2972
|
-
jobs: JobDispatcherPort;
|
|
2973
|
-
outbox: OutboxPort;
|
|
2974
|
-
todoAttachments: TodoAttachmentRepository;
|
|
2975
|
-
todos: TodoRepository;
|
|
2976
|
-
};
|
|
2977
|
-
|
|
2978
|
-
export type AppGate = BoundGate<[typeof todoPolicy]>;
|
|
2979
|
-
|
|
2980
|
-
export type AppPorts = {
|
|
2981
|
-
audit: AuditLogPort;
|
|
2982
|
-
auth: AuthPort;
|
|
2983
|
-
eventBus: EventBusPort;
|
|
2984
|
-
gate: GatePort<AuthorizationContext, [typeof todoPolicy]>;
|
|
2985
|
-
idempotency: IdempotencyPort;
|
|
2986
|
-
jobs: JobDispatcherPort;
|
|
2987
|
-
mailer: MailerPort;
|
|
2988
|
-
notifications: NotificationPort;
|
|
2989
|
-
outbox: OutboxPort;
|
|
2990
|
-
storage: StoragePort;
|
|
2991
|
-
todoAttachments: TodoAttachmentRepository;
|
|
2992
|
-
todos: TodoRepository;
|
|
2993
|
-
logger: LoggerPort;
|
|
2994
|
-
${providerFields.length > 0 ? `${providerFields.join("\n")}\n` : ""} uow: UnitOfWorkPort<AppTransactionPorts>;
|
|
2995
|
-
};
|
|
2996
|
-
`;
|
|
2997
|
-
}
|
|
2998
|
-
function productionPortsWithDrizzleTurso(ctx) {
|
|
2999
|
-
const mailImport = 'import type { MailerPort } from "@beignet/core/mail";\n';
|
|
3000
|
-
const imports = [
|
|
3001
|
-
"\tAuditLogPort,",
|
|
3002
|
-
"\tBoundGate,",
|
|
3003
|
-
"\tEventBusPort,",
|
|
3004
|
-
"\tGatePort,",
|
|
3005
|
-
"\tJobDispatcherPort,",
|
|
3006
|
-
"\tLoggerPort,",
|
|
3007
|
-
...productionProviderPortImports(ctx),
|
|
3008
|
-
"\tStoragePort,",
|
|
3009
|
-
"\tUnitOfWorkPort,",
|
|
3010
|
-
];
|
|
3011
|
-
const providerFields = productionProviderPortFields(ctx);
|
|
3012
|
-
return `import type { IdempotencyPort } from "@beignet/core/idempotency";
|
|
3013
|
-
${mailImport}import type { NotificationPort } from "@beignet/core/notifications";
|
|
3014
|
-
import type { OutboxPort } from "@beignet/core/outbox";
|
|
3015
|
-
import type {
|
|
3016
|
-
${imports.join("\n")}
|
|
3017
|
-
} from "@beignet/core/ports";
|
|
3018
|
-
import type { AuthorizationContext, todoPolicy } from "@/features/todos/policy";
|
|
3019
|
-
import type { AuthPort } from "./auth";
|
|
3020
|
-
import type { TodoAttachmentRepository, TodoRepository } from "@/features/todos/ports";
|
|
3021
|
-
|
|
3022
|
-
export type AppDatabasePort = {
|
|
3023
|
-
db: unknown;
|
|
3024
|
-
client: unknown;
|
|
3025
|
-
};
|
|
3026
|
-
|
|
3027
|
-
export type AppTransactionPorts = {
|
|
3028
|
-
audit: AuditLogPort;
|
|
3029
|
-
events: import("@beignet/core/ports").BufferedDomainEventRecorder;
|
|
3030
|
-
jobs: JobDispatcherPort;
|
|
3031
|
-
outbox: OutboxPort;
|
|
3032
|
-
todoAttachments: TodoAttachmentRepository;
|
|
3033
|
-
todos: TodoRepository;
|
|
3034
|
-
};
|
|
3035
|
-
|
|
3036
|
-
export type AppGate = BoundGate<[typeof todoPolicy]>;
|
|
3037
|
-
|
|
3038
|
-
export type AppPorts = {
|
|
3039
|
-
audit: AuditLogPort;
|
|
3040
|
-
auth: AuthPort;
|
|
3041
|
-
db: AppDatabasePort;
|
|
3042
|
-
eventBus: EventBusPort;
|
|
3043
|
-
gate: GatePort<AuthorizationContext, [typeof todoPolicy]>;
|
|
3044
|
-
idempotency: IdempotencyPort;
|
|
3045
|
-
jobs: JobDispatcherPort;
|
|
3046
|
-
mailer: MailerPort;
|
|
3047
|
-
notifications: NotificationPort;
|
|
3048
|
-
outbox: OutboxPort;
|
|
3049
|
-
storage: StoragePort;
|
|
3050
|
-
todoAttachments: TodoAttachmentRepository;
|
|
3051
|
-
todos: TodoRepository;
|
|
3052
|
-
logger: LoggerPort;
|
|
3053
|
-
${providerFields.length > 0 ? `${providerFields.join("\n")}\n` : ""} uow: UnitOfWorkPort<AppTransactionPorts>;
|
|
3054
|
-
};
|
|
3055
|
-
`;
|
|
3056
|
-
}
|
|
3057
|
-
function productionServerProviders(ctx) {
|
|
3058
|
-
const imports = [
|
|
3059
|
-
'import { createDevtoolsProvider } from "@beignet/devtools";',
|
|
3060
|
-
hasIntegration(ctx, "better-auth")
|
|
3061
|
-
? 'import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";'
|
|
3062
|
-
: undefined,
|
|
3063
|
-
hasDrizzleTurso(ctx)
|
|
3064
|
-
? 'import { createDrizzleTursoProvider } from "@beignet/provider-drizzle-turso";'
|
|
3065
|
-
: undefined,
|
|
3066
|
-
hasIntegration(ctx, "inngest")
|
|
3067
|
-
? 'import { inngestProvider } from "@beignet/provider-inngest";'
|
|
3068
|
-
: undefined,
|
|
3069
|
-
'import { loggerPinoProvider } from "@beignet/provider-logger-pino";',
|
|
3070
|
-
hasIntegration(ctx, "resend")
|
|
3071
|
-
? 'import { mailResendProvider } from "@beignet/provider-mail-resend";'
|
|
3072
|
-
: undefined,
|
|
3073
|
-
'import { localStorageProvider } from "@beignet/provider-storage-local";',
|
|
3074
|
-
hasIntegration(ctx, "upstash-rate-limit")
|
|
3075
|
-
? 'import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";'
|
|
3076
|
-
: undefined,
|
|
3077
|
-
hasDrizzleTurso(ctx)
|
|
3078
|
-
? 'import * as schema from "@/infra/db/schema";'
|
|
3079
|
-
: undefined,
|
|
3080
|
-
hasIntegration(ctx, "better-auth")
|
|
3081
|
-
? 'import { auth } from "@/lib/better-auth";'
|
|
3082
|
-
: undefined,
|
|
3083
|
-
'import { starterDatabaseProvider } from "@/infra/db/provider";',
|
|
3084
|
-
hasIntegration(ctx, "better-auth")
|
|
3085
|
-
? 'import type { AuthSessionMetadata, AuthUser } from "@/ports/auth";'
|
|
3086
|
-
: undefined,
|
|
3087
|
-
].filter((line) => Boolean(line));
|
|
3088
|
-
const declarations = hasDrizzleTurso(ctx)
|
|
3089
|
-
? "\nconst drizzleTursoProvider = createDrizzleTursoProvider({ schema });\n"
|
|
3090
|
-
: "";
|
|
3091
|
-
const entries = [
|
|
3092
|
-
"\tcreateDevtoolsProvider(),",
|
|
3093
|
-
hasIntegration(ctx, "better-auth")
|
|
3094
|
-
? "\tcreateAuthBetterAuthProvider<AuthUser, AuthSessionMetadata>(auth),"
|
|
3095
|
-
: undefined,
|
|
3096
|
-
"\tlocalStorageProvider,",
|
|
3097
|
-
"\tloggerPinoProvider,",
|
|
3098
|
-
hasDrizzleTurso(ctx) ? "\tdrizzleTursoProvider," : undefined,
|
|
3099
|
-
"\tstarterDatabaseProvider,",
|
|
3100
|
-
hasIntegration(ctx, "inngest") ? "\tinngestProvider," : undefined,
|
|
3101
|
-
hasIntegration(ctx, "resend") ? "\tmailResendProvider," : undefined,
|
|
3102
|
-
hasIntegration(ctx, "upstash-rate-limit")
|
|
3103
|
-
? "\tupstashRateLimitProvider,"
|
|
3104
|
-
: undefined,
|
|
3105
|
-
].filter((entry) => Boolean(entry));
|
|
3106
|
-
return `${imports.join("\n")}
|
|
3107
|
-
${declarations}
|
|
3108
|
-
export const providers = [
|
|
3109
|
-
${entries.join("\n")}
|
|
3110
|
-
];
|
|
3111
|
-
`;
|
|
3112
|
-
}
|
|
3113
|
-
function productionTodosTest(ctx) {
|
|
3114
|
-
const portImports = [
|
|
3115
|
-
hasIntegration(ctx, "upstash-rate-limit")
|
|
3116
|
-
? "createMemoryRateLimiter"
|
|
3117
|
-
: undefined,
|
|
3118
|
-
"createMemoryStorage",
|
|
3119
|
-
"createNoopUnitOfWork",
|
|
3120
|
-
"createUserActor",
|
|
3121
|
-
].filter((item) => Boolean(item));
|
|
3122
|
-
const extraPorts = [
|
|
3123
|
-
hasDrizzleTurso(ctx)
|
|
3124
|
-
? "\t\t\tdb: { db: undefined as never, client: undefined as never },"
|
|
3125
|
-
: undefined,
|
|
3126
|
-
hasIntegration(ctx, "upstash-rate-limit")
|
|
3127
|
-
? "\t\t\trateLimit: createMemoryRateLimiter(),"
|
|
3128
|
-
: undefined,
|
|
3129
|
-
].filter((entry) => Boolean(entry));
|
|
3130
|
-
return `import { describe, expect, it } from "bun:test";
|
|
3131
|
-
import { createUseCaseTester } from "@beignet/core/application";
|
|
3132
|
-
import { createMemoryIdempotencyStore } from "@beignet/core/idempotency";
|
|
3133
|
-
import { createMemoryMailer } from "@beignet/core/mail";
|
|
3134
|
-
import { createMemoryNotificationPort } from "@beignet/core/notifications";
|
|
3135
|
-
import { createMemoryOutbox } from "@beignet/core/outbox";
|
|
3136
|
-
import { offsetPageResult, type OffsetPage } from "@beignet/core/pagination";
|
|
3137
|
-
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3138
|
-
import { createInMemoryEventBus } from "@beignet/provider-event-bus-memory";
|
|
3139
|
-
import { ${["createDomainEventRecorder", "createMemoryAuditLog", ...portImports].join(", ")} } from "@beignet/core/ports";
|
|
3140
|
-
import type { AppContext } from "@/app-context";
|
|
3141
|
-
import { appPorts } from "@/infra/app-ports";
|
|
3142
|
-
import {
|
|
3143
|
-
createTodoUseCase,
|
|
3144
|
-
getTodoUseCase,
|
|
3145
|
-
listTodosUseCase,
|
|
3146
|
-
type CreateTodoInput,
|
|
3147
|
-
type Todo,
|
|
3148
|
-
} from "../use-cases";
|
|
3149
|
-
|
|
3150
|
-
function createTestTodoRepository() {
|
|
3151
|
-
const todos = new Map<string, Todo>();
|
|
3152
|
-
|
|
3153
|
-
return {
|
|
3154
|
-
async list(page: OffsetPage) {
|
|
3155
|
-
const allTodos = Array.from(todos.values()).sort((left, right) =>
|
|
3156
|
-
left.createdAt.localeCompare(right.createdAt),
|
|
3157
|
-
);
|
|
3158
|
-
|
|
3159
|
-
return offsetPageResult(
|
|
3160
|
-
allTodos.slice(page.offset, page.offset + page.limit),
|
|
3161
|
-
page,
|
|
3162
|
-
allTodos.length,
|
|
3163
|
-
);
|
|
3164
|
-
},
|
|
3165
|
-
async findById(id: string) {
|
|
3166
|
-
return todos.get(id) ?? null;
|
|
3167
|
-
},
|
|
3168
|
-
async create(input: CreateTodoInput) {
|
|
3169
|
-
const todo: Todo = {
|
|
3170
|
-
id: crypto.randomUUID(),
|
|
3171
|
-
title: input.title,
|
|
3172
|
-
completed: false,
|
|
3173
|
-
createdAt: new Date().toISOString(),
|
|
3174
|
-
};
|
|
3175
|
-
todos.set(todo.id, todo);
|
|
3176
|
-
return todo;
|
|
3177
|
-
},
|
|
3178
|
-
};
|
|
3179
|
-
}
|
|
3180
|
-
|
|
3181
|
-
function createTestTodoAttachmentRepository() {
|
|
3182
|
-
return {
|
|
3183
|
-
async create(input: {
|
|
3184
|
-
id: string;
|
|
3185
|
-
todoId: string;
|
|
3186
|
-
key: string;
|
|
3187
|
-
fileName: string;
|
|
3188
|
-
contentType: string;
|
|
3189
|
-
size: number;
|
|
3190
|
-
}) {
|
|
3191
|
-
return {
|
|
3192
|
-
...input,
|
|
3193
|
-
createdAt: new Date().toISOString(),
|
|
3194
|
-
};
|
|
3195
|
-
},
|
|
3196
|
-
async findMany() {
|
|
3197
|
-
return [];
|
|
3198
|
-
},
|
|
3199
|
-
};
|
|
3200
|
-
}
|
|
3201
|
-
|
|
3202
|
-
describe("todos resource", () => {
|
|
3203
|
-
it("creates, gets, and lists todos", async () => {
|
|
3204
|
-
const todos = createTestTodoRepository();
|
|
3205
|
-
const todoAttachments = createTestTodoAttachmentRepository();
|
|
3206
|
-
const audit = createMemoryAuditLog();
|
|
3207
|
-
const eventBus = createInMemoryEventBus();
|
|
3208
|
-
const jobs = { dispatch: async () => undefined };
|
|
3209
|
-
const mailer = createMemoryMailer({
|
|
3210
|
-
defaultFrom: "Test <test@example.local>",
|
|
3211
|
-
});
|
|
3212
|
-
const notifications = createMemoryNotificationPort();
|
|
3213
|
-
const outbox = createMemoryOutbox();
|
|
3214
|
-
const auth = {
|
|
3215
|
-
user: {
|
|
3216
|
-
id: "user_test",
|
|
3217
|
-
email: "test@example.com",
|
|
3218
|
-
name: "Test User",
|
|
3219
|
-
},
|
|
3220
|
-
session: { id: "session_test" },
|
|
3221
|
-
};
|
|
3222
|
-
const actor = createUserActor(auth.user.id, {
|
|
3223
|
-
displayName: auth.user.name,
|
|
3224
|
-
});
|
|
3225
|
-
const testPorts = {
|
|
3226
|
-
...appPorts,
|
|
3227
|
-
audit,
|
|
3228
|
-
eventBus,
|
|
3229
|
-
idempotency: createMemoryIdempotencyStore(),
|
|
3230
|
-
jobs,
|
|
3231
|
-
mailer,
|
|
3232
|
-
notifications,
|
|
3233
|
-
outbox,
|
|
3234
|
-
todoAttachments,
|
|
3235
|
-
todos,
|
|
3236
|
-
${extraPorts.length > 0 ? `${extraPorts.join("\n")}\n` : ""} uow: createNoopUnitOfWork(() => ({
|
|
3237
|
-
audit,
|
|
3238
|
-
events: createDomainEventRecorder(),
|
|
3239
|
-
jobs,
|
|
3240
|
-
outbox,
|
|
3241
|
-
todoAttachments,
|
|
3242
|
-
todos,
|
|
3243
|
-
})) as unknown as AppContext["ports"]["uow"],
|
|
3244
|
-
devtools: createInMemoryDevtools(),
|
|
3245
|
-
storage: createMemoryStorage(),
|
|
3246
|
-
} as unknown as AppContext["ports"];
|
|
3247
|
-
const tester = createUseCaseTester<AppContext>(() => ({
|
|
3248
|
-
requestId: "test-request",
|
|
3249
|
-
actor,
|
|
3250
|
-
auth,
|
|
3251
|
-
gate: testPorts.gate.bind({ actor, auth }),
|
|
3252
|
-
ports: testPorts,
|
|
3253
|
-
}));
|
|
3254
|
-
|
|
3255
|
-
const ctx = await tester.ctx();
|
|
3256
|
-
const created = await tester.run(
|
|
3257
|
-
createTodoUseCase,
|
|
3258
|
-
{ title: "First todo" },
|
|
3259
|
-
{ ctx },
|
|
3260
|
-
);
|
|
3261
|
-
const found = await tester.run(getTodoUseCase, { id: created.id }, { ctx });
|
|
3262
|
-
const result = await tester.run(
|
|
3263
|
-
listTodosUseCase,
|
|
3264
|
-
{ limit: 20, offset: 0 },
|
|
3265
|
-
{ ctx },
|
|
3266
|
-
);
|
|
3267
|
-
|
|
3268
|
-
expect(created.title).toBe("First todo");
|
|
3269
|
-
expect(found.id).toBe(created.id);
|
|
3270
|
-
expect(result.page.total).toBe(1);
|
|
3271
|
-
expect(result.items).toEqual([created]);
|
|
3272
|
-
});
|
|
3273
|
-
});
|
|
3274
|
-
`;
|
|
3275
|
-
}
|
|
3276
|
-
function getProductionTemplateFiles(ctx) {
|
|
3277
|
-
const usesDrizzleTurso = hasDrizzleTurso(ctx);
|
|
3278
|
-
const templateFiles = [
|
|
3279
|
-
{ path: "package.json", content: packageJson(ctx) },
|
|
3280
|
-
{ path: "README.md", content: readme(ctx) },
|
|
3281
|
-
{ path: ".gitignore", content: gitignore(ctx) },
|
|
3282
|
-
{ path: ".env.example", content: envExample(ctx) },
|
|
3283
|
-
{ path: "next-env.d.ts", content: files.nextEnv },
|
|
3284
|
-
{ path: "next.config.js", content: files.nextConfig },
|
|
3285
|
-
{ path: "tsconfig.json", content: files.tsconfig },
|
|
3286
|
-
{ path: "app/globals.css", content: files.globals },
|
|
3287
|
-
{ path: "app/layout.tsx", content: layout(ctx) },
|
|
3288
|
-
{ path: "app/page.tsx", content: page(ctx) },
|
|
3289
|
-
{ path: "app/providers.tsx", content: files.appProviders },
|
|
3290
|
-
{ path: "app-context.ts", content: files.productionAppContext },
|
|
3291
|
-
{
|
|
3292
|
-
path: "app/api/[[...path]]/route.ts",
|
|
3293
|
-
content: files.productionApiCatchAllRoute,
|
|
3294
|
-
},
|
|
3295
|
-
{ path: "app/api/health/route.ts", content: files.productionHealthRoute },
|
|
3296
|
-
{
|
|
3297
|
-
path: "app/api/devtools/[[...path]]/route.ts",
|
|
3298
|
-
content: files.productionDevtoolsRoute,
|
|
3299
|
-
},
|
|
3300
|
-
{
|
|
3301
|
-
path: "app/storage/[...key]/route.ts",
|
|
3302
|
-
content: files.productionStorageRoute,
|
|
3303
|
-
},
|
|
3304
|
-
{
|
|
3305
|
-
path: "app/api/openapi/route.ts",
|
|
3306
|
-
content: files.productionOpenApiRoute,
|
|
3307
|
-
},
|
|
3308
|
-
{
|
|
3309
|
-
path: "app/api/auth/[...all]/route.ts",
|
|
3310
|
-
content: files.productionAuthRoute,
|
|
3311
|
-
},
|
|
3312
|
-
{
|
|
3313
|
-
path: "app/api/cron/outbox/drain/route.ts",
|
|
3314
|
-
content: files.productionOutboxDrainRoute,
|
|
3315
|
-
},
|
|
3316
|
-
{
|
|
3317
|
-
path: "app/api/cron/todos/daily-summary/route.ts",
|
|
3318
|
-
content: files.productionScheduleRoute,
|
|
3319
|
-
},
|
|
3320
|
-
{
|
|
3321
|
-
path: "app/api/uploads/[uploadName]/[action]/route.ts",
|
|
3322
|
-
content: files.productionUploadsRoute,
|
|
3323
|
-
},
|
|
3324
|
-
{ path: "client/api-client.ts", content: files.apiClient },
|
|
3325
|
-
{ path: "client/rq.ts", content: files.rq },
|
|
3326
|
-
{ path: "client/uploads.ts", content: files.productionUploadClient },
|
|
3327
|
-
{
|
|
3328
|
-
path: "features/todos/contracts.ts",
|
|
3329
|
-
content: files.productionContractsTodos,
|
|
3330
|
-
},
|
|
3331
|
-
{ path: "features/todos/routes.ts", content: files.productionTodoRoutes },
|
|
3332
|
-
{ path: "features/todos/components/todo-app.tsx", content: todoApp(ctx) },
|
|
3333
|
-
{ path: "infra/logger.ts", content: files.productionLogger },
|
|
3334
|
-
{
|
|
3335
|
-
path: "infra/app-ports.ts",
|
|
3336
|
-
content: usesDrizzleTurso
|
|
3337
|
-
? files.productionInfrastructurePortsWithDrizzleTurso
|
|
3338
|
-
: files.productionInfrastructurePorts,
|
|
3339
|
-
},
|
|
3340
|
-
{ path: "lib/env.ts", content: files.productionEnv },
|
|
3341
|
-
{ path: "lib/audit.ts", content: files.productionAuditHelper },
|
|
3342
|
-
{ path: "lib/auth.ts", content: files.productionAuthHelpers },
|
|
3343
|
-
{ path: "lib/better-auth.ts", content: files.productionBetterAuth },
|
|
3344
|
-
{ path: "features/todos/policy.ts", content: files.productionTodoPolicy },
|
|
3345
|
-
{
|
|
3346
|
-
path: "features/todos/domain/events/index.ts",
|
|
3347
|
-
content: files.productionTodoEvents,
|
|
3348
|
-
},
|
|
3349
|
-
{ path: "features/todos/jobs/index.ts", content: files.productionTodoJobs },
|
|
3350
|
-
{
|
|
3351
|
-
path: "features/todos/listeners/index.ts",
|
|
3352
|
-
content: files.productionTodoListeners,
|
|
3353
|
-
},
|
|
3354
|
-
{
|
|
3355
|
-
path: "features/todos/notifications/index.ts",
|
|
3356
|
-
content: files.productionTodoNotifications,
|
|
3357
|
-
},
|
|
3358
|
-
{
|
|
3359
|
-
path: "features/todos/schedules/index.ts",
|
|
3360
|
-
content: files.productionTodoSchedules,
|
|
3361
|
-
},
|
|
3362
|
-
{
|
|
3363
|
-
path: "features/todos/uploads/index.ts",
|
|
3364
|
-
content: files.productionTodoUploads,
|
|
3365
|
-
},
|
|
3366
|
-
...(!hasIntegration(ctx, "better-auth")
|
|
3367
|
-
? [
|
|
3368
|
-
{
|
|
3369
|
-
path: "infra/auth/anonymous-auth.ts",
|
|
3370
|
-
content: files.productionAnonymousAuth,
|
|
3371
|
-
},
|
|
3372
|
-
]
|
|
3373
|
-
: []),
|
|
3374
|
-
{ path: "ports/auth.ts", content: files.productionAuthPort },
|
|
3375
|
-
{
|
|
3376
|
-
path: "ports/index.ts",
|
|
3377
|
-
content: usesDrizzleTurso
|
|
3378
|
-
? productionPortsWithDrizzleTurso(ctx)
|
|
3379
|
-
: productionPorts(ctx),
|
|
3380
|
-
},
|
|
3381
|
-
{
|
|
3382
|
-
path: "features/todos/ports.ts",
|
|
3383
|
-
content: files.productionTodoRepositoryPort,
|
|
3384
|
-
},
|
|
3385
|
-
{
|
|
3386
|
-
path: "features/shared/errors.ts",
|
|
3387
|
-
content: files.productimapUnhandledErrors,
|
|
3388
|
-
},
|
|
3389
|
-
{
|
|
3390
|
-
path: "server/index.ts",
|
|
3391
|
-
content: usesDrizzleTurso
|
|
3392
|
-
? files.productionServerWithDrizzleTurso
|
|
3393
|
-
: files.productionServer,
|
|
3394
|
-
},
|
|
3395
|
-
{ path: "server/routes.ts", content: files.productionServerRoutes },
|
|
3396
|
-
{ path: "server/outbox.ts", content: files.productionOutbox },
|
|
3397
|
-
{
|
|
3398
|
-
path: "server/providers.ts",
|
|
3399
|
-
content: productionServerProviders(ctx),
|
|
3400
|
-
},
|
|
3401
|
-
{ path: "lib/use-case.ts", content: files.productionUseCaseBuilder },
|
|
3402
|
-
{
|
|
3403
|
-
path: "features/todos/use-cases/create-todo.ts",
|
|
3404
|
-
content: files.productionCreateTodoUseCase,
|
|
3405
|
-
},
|
|
3406
|
-
{
|
|
3407
|
-
path: "features/todos/use-cases/get-todo.ts",
|
|
3408
|
-
content: files.productionGetTodoUseCase,
|
|
3409
|
-
},
|
|
3410
|
-
{
|
|
3411
|
-
path: "features/todos/use-cases/list-todos.ts",
|
|
3412
|
-
content: files.productionListTodosUseCase,
|
|
3413
|
-
},
|
|
3414
|
-
{
|
|
3415
|
-
path: "features/todos/use-cases/index.ts",
|
|
3416
|
-
content: files.productionUseCasesIndex,
|
|
3417
|
-
},
|
|
3418
|
-
{
|
|
3419
|
-
path: "features/todos/use-cases/schemas.ts",
|
|
3420
|
-
content: files.productionTodoSchemas,
|
|
3421
|
-
},
|
|
3422
|
-
{
|
|
3423
|
-
path: "features/todos/tests/todos.test.ts",
|
|
3424
|
-
content: productionTodosTest(ctx),
|
|
3425
|
-
},
|
|
3426
|
-
{ path: "docs/integrations.md", content: integrationsDoc(ctx) },
|
|
3427
|
-
];
|
|
3428
|
-
if (usesDrizzleTurso) {
|
|
3429
|
-
templateFiles.push({ path: "drizzle.config.ts", content: files.productionDrizzleConfig }, {
|
|
3430
|
-
path: "infra/audit/drizzle-audit-log.ts",
|
|
3431
|
-
content: files.productionDrizzleAuditLog,
|
|
3432
|
-
}, {
|
|
3433
|
-
path: "infra/db/bootstrap.ts",
|
|
3434
|
-
content: files.productionDbBootstrap,
|
|
3435
|
-
}, {
|
|
3436
|
-
path: "infra/db/seed.ts",
|
|
3437
|
-
content: files.productionDbSeed,
|
|
3438
|
-
}, {
|
|
3439
|
-
path: "infra/db/reset.ts",
|
|
3440
|
-
content: files.productionDbReset,
|
|
3441
|
-
}, {
|
|
3442
|
-
path: "infra/db/provider.ts",
|
|
3443
|
-
content: files.productionDbProvider,
|
|
3444
|
-
}, {
|
|
3445
|
-
path: "infra/db/schema/index.ts",
|
|
3446
|
-
content: files.productionDbSchema,
|
|
3447
|
-
}, {
|
|
3448
|
-
path: "infra/db/repositories.ts",
|
|
3449
|
-
content: files.productionDbRepositories,
|
|
3450
|
-
}, {
|
|
3451
|
-
path: "infra/todos/drizzle-todo-repository.ts",
|
|
3452
|
-
content: files.productionDrizzleTodoRepository,
|
|
3453
|
-
});
|
|
3454
|
-
}
|
|
3455
|
-
else {
|
|
3456
|
-
templateFiles.push({
|
|
3457
|
-
path: "infra/todos/in-memory-todo-repository.ts",
|
|
3458
|
-
content: files.productionInMemoryTodoRepository,
|
|
3459
|
-
});
|
|
3460
|
-
}
|
|
3461
|
-
return templateFiles;
|
|
3462
|
-
}
|
|
3463
|
-
/**
|
|
3464
|
-
* Render all template files for a new Beignet app.
|
|
3465
|
-
*/
|
|
3466
|
-
export function getTemplateFiles(ctx) {
|
|
3467
|
-
if (isStandardPreset(ctx)) {
|
|
3468
|
-
return getProductionTemplateFiles(ctx);
|
|
3469
|
-
}
|
|
3470
|
-
const templateFiles = [
|
|
3471
|
-
{ path: "package.json", content: packageJson(ctx) },
|
|
3472
|
-
{ path: "README.md", content: readme(ctx) },
|
|
3473
|
-
{ path: ".gitignore", content: gitignore(ctx) },
|
|
3474
|
-
{ path: "next-env.d.ts", content: files.nextEnv },
|
|
3475
|
-
{ path: "next.config.js", content: files.nextConfig },
|
|
3476
|
-
{ path: "tsconfig.json", content: files.tsconfig },
|
|
3477
|
-
{ path: "app/globals.css", content: files.globals },
|
|
3478
|
-
{ path: "app/layout.tsx", content: layout(ctx) },
|
|
3479
|
-
{ path: "app/page.tsx", content: page(ctx) },
|
|
3480
|
-
{ path: "app/api/[[...path]]/route.ts", content: files.apiCatchAllRoute },
|
|
3481
|
-
{ path: "features/todos/contracts.ts", content: files.contractsTodos },
|
|
3482
|
-
{ path: "features/todos/routes.ts", content: files.todoRoutes },
|
|
3483
|
-
{ path: "ports/index.ts", content: files.ports },
|
|
3484
|
-
{ path: "server/index.ts", content: server(ctx) },
|
|
3485
|
-
{ path: "server/routes.ts", content: files.serverRoutes },
|
|
3486
|
-
{ path: "features/todos/use-cases.ts", content: files.useCasesTodos },
|
|
3487
|
-
];
|
|
3488
|
-
if (hasFeature(ctx, "client")) {
|
|
3489
|
-
templateFiles.push({
|
|
3490
|
-
path: "client/api-client.ts",
|
|
3491
|
-
content: files.apiClient,
|
|
3492
|
-
});
|
|
3493
|
-
}
|
|
3494
|
-
if (hasFeature(ctx, "react-query")) {
|
|
3495
|
-
templateFiles.push({ path: "app/providers.tsx", content: files.appProviders }, { path: "client/rq.ts", content: files.rq }, { path: "features/todos/components/todo-app.tsx", content: todoApp(ctx) });
|
|
3496
|
-
}
|
|
3497
|
-
if (hasFeature(ctx, "openapi")) {
|
|
3498
|
-
templateFiles.push({
|
|
3499
|
-
path: "app/api/openapi/route.ts",
|
|
3500
|
-
content: files.apiOpenApiRoute,
|
|
3501
|
-
});
|
|
3502
|
-
}
|
|
3503
|
-
if (ctx.integrations.some((integration) => ["pino", "inngest", "resend", "upstash-rate-limit"].includes(integration))) {
|
|
3504
|
-
templateFiles.push({
|
|
3505
|
-
path: "server/providers.ts",
|
|
3506
|
-
content: serverProviders(ctx),
|
|
3507
|
-
});
|
|
3508
|
-
}
|
|
3509
|
-
if (ctx.integrations.length > 0) {
|
|
3510
|
-
templateFiles.push({
|
|
3511
|
-
path: ".env.example",
|
|
3512
|
-
content: envExample(ctx),
|
|
3513
|
-
}, {
|
|
3514
|
-
path: "docs/integrations.md",
|
|
3515
|
-
content: integrationsDoc(ctx),
|
|
3516
|
-
});
|
|
3517
|
-
}
|
|
3518
|
-
return templateFiles;
|
|
3519
|
-
}
|
|
3520
|
-
//# sourceMappingURL=templates.js.map
|