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