@beignet/cli 0.0.3 → 0.0.4
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 +205 -0
- package/README.md +379 -61
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +72 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +88 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +42 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +136 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +4 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +16 -26
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +657 -588
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1756 -394
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates.d.ts +3 -32
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +1002 -527
- package/dist/templates.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +137 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +182 -0
- package/src/create.ts +24 -31
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1067 -803
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2181 -405
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates.ts +1073 -567
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/src/create-bin.ts +0 -11
package/dist/templates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { featureChoices, integrationChoices, presetChoices, } from "./choices.js";
|
|
1
2
|
const externalVersions = {
|
|
2
3
|
next: "^16.2.4",
|
|
3
4
|
react: "^19.2.5",
|
|
@@ -11,9 +12,9 @@ const externalVersions = {
|
|
|
11
12
|
tanstackReactQuery: "^5.100.7",
|
|
12
13
|
hookformResolvers: "^5.0.0",
|
|
13
14
|
reactHookForm: "^7.74.0",
|
|
14
|
-
betterAuth: "
|
|
15
|
-
drizzleKit: "^0.
|
|
16
|
-
drizzleOrm: "^0.
|
|
15
|
+
betterAuth: "1.6.11",
|
|
16
|
+
drizzleKit: "^0.31.10",
|
|
17
|
+
drizzleOrm: "^0.45.2",
|
|
17
18
|
inngest: "^3.0.0",
|
|
18
19
|
libsqlClient: "^0.14.0",
|
|
19
20
|
pino: "^9.7.0",
|
|
@@ -30,42 +31,15 @@ function hasDrizzleTurso(ctx) {
|
|
|
30
31
|
function packageRunner(ctx) {
|
|
31
32
|
switch (ctx.packageManager) {
|
|
32
33
|
case "npm":
|
|
33
|
-
return "
|
|
34
|
+
return "npm run beignet --";
|
|
34
35
|
case "pnpm":
|
|
35
|
-
return "pnpm
|
|
36
|
+
return "pnpm beignet";
|
|
36
37
|
case "yarn":
|
|
37
|
-
return "yarn
|
|
38
|
+
return "yarn beignet";
|
|
38
39
|
default:
|
|
39
|
-
return "
|
|
40
|
+
return "bun beignet";
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Supported scaffold feature choices.
|
|
44
|
-
*/
|
|
45
|
-
export const featureChoices = [
|
|
46
|
-
"client",
|
|
47
|
-
"react-query",
|
|
48
|
-
"forms",
|
|
49
|
-
"openapi",
|
|
50
|
-
];
|
|
51
|
-
/**
|
|
52
|
-
* Supported scaffold preset choices.
|
|
53
|
-
*/
|
|
54
|
-
export const presetChoices = [
|
|
55
|
-
"minimal",
|
|
56
|
-
"standard",
|
|
57
|
-
];
|
|
58
|
-
/**
|
|
59
|
-
* Supported scaffold integration choices.
|
|
60
|
-
*/
|
|
61
|
-
export const integrationChoices = [
|
|
62
|
-
"better-auth",
|
|
63
|
-
"drizzle-turso",
|
|
64
|
-
"inngest",
|
|
65
|
-
"pino",
|
|
66
|
-
"resend",
|
|
67
|
-
"upstash-rate-limit",
|
|
68
|
-
];
|
|
69
43
|
const integrationDeps = {
|
|
70
44
|
"better-auth": {
|
|
71
45
|
"@beignet/provider-auth-better-auth": "",
|
|
@@ -158,6 +132,7 @@ function packageJson(ctx) {
|
|
|
158
132
|
}
|
|
159
133
|
}
|
|
160
134
|
const devDependencies = {
|
|
135
|
+
"@beignet/cli": ctx.beignetVersion,
|
|
161
136
|
"@types/bun": externalVersions.typesBun,
|
|
162
137
|
"@types/node": externalVersions.typesNode,
|
|
163
138
|
"@types/react": externalVersions.typesReact,
|
|
@@ -170,6 +145,7 @@ function packageJson(ctx) {
|
|
|
170
145
|
}
|
|
171
146
|
}
|
|
172
147
|
const scripts = {
|
|
148
|
+
beignet: "beignet",
|
|
173
149
|
dev: "next dev",
|
|
174
150
|
build: "next build",
|
|
175
151
|
start: "next start",
|
|
@@ -179,8 +155,12 @@ function packageJson(ctx) {
|
|
|
179
155
|
if (hasDrizzleTurso(ctx)) {
|
|
180
156
|
scripts["db:generate"] = "drizzle-kit generate";
|
|
181
157
|
scripts["db:migrate"] = "drizzle-kit migrate";
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|
|
184
164
|
}
|
|
185
165
|
return json({
|
|
186
166
|
name: ctx.name,
|
|
@@ -200,12 +180,28 @@ function readme(ctx) {
|
|
|
200
180
|
const run = ctx.packageManager === "npm"
|
|
201
181
|
? "npm run dev"
|
|
202
182
|
: `${ctx.packageManager} run dev`;
|
|
183
|
+
const cli = packageRunner(ctx);
|
|
184
|
+
const envSetup = ctx.integrations.length > 0 ? "cp .env.example .env.local\n" : "";
|
|
203
185
|
const featureList = ctx.features.length > 0
|
|
204
186
|
? ctx.features.map((feature) => `- \`${feature}\``).join("\n")
|
|
205
187
|
: "- API routes only";
|
|
206
188
|
const integrationList = ctx.integrations.length > 0
|
|
207
189
|
? ctx.integrations.map((integration) => `- \`${integration}\``).join("\n")
|
|
208
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
|
+
: "";
|
|
209
205
|
return `# ${ctx.name}
|
|
210
206
|
|
|
211
207
|
Beignet app scaffolded with \`@beignet/cli\`.
|
|
@@ -214,19 +210,21 @@ Beignet app scaffolded with \`@beignet/cli\`.
|
|
|
214
210
|
|
|
215
211
|
\`\`\`bash
|
|
216
212
|
${install}
|
|
217
|
-
${run}
|
|
213
|
+
${envSetup}${run}
|
|
218
214
|
\`\`\`
|
|
219
215
|
|
|
220
|
-
Open http://localhost:3000 and start from the todos workflow.
|
|
221
|
-
|
|
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}
|
|
222
218
|
## Files to know
|
|
223
219
|
|
|
224
220
|
- \`features/todos/contracts.ts\` defines the API contract.
|
|
225
|
-
- \`features/todos/use-cases
|
|
221
|
+
- \`features/todos/use-cases/\` contains application logic with input and output validation.
|
|
226
222
|
- \`ports/index.ts\` defines the app's external dependencies.
|
|
227
|
-
- \`
|
|
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.
|
|
228
226
|
- \`app/api/[[...path]]/route.ts\` exposes contract-backed API routes to Next.js.
|
|
229
|
-
${hasFeature(ctx, "react-query") ? "- `features/todos/components/todo-app.tsx` contains the starter UI.\n" : ""}
|
|
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" : ""}
|
|
230
228
|
|
|
231
229
|
## Selected preset
|
|
232
230
|
|
|
@@ -247,6 +245,9 @@ function standardReadme(ctx) {
|
|
|
247
245
|
const run = ctx.packageManager === "npm"
|
|
248
246
|
? "npm run dev"
|
|
249
247
|
: `${ctx.packageManager} run dev`;
|
|
248
|
+
const test = ctx.packageManager === "npm"
|
|
249
|
+
? "npm run test"
|
|
250
|
+
: `${ctx.packageManager} run test`;
|
|
250
251
|
const typecheck = ctx.packageManager === "npm"
|
|
251
252
|
? "npm run typecheck"
|
|
252
253
|
: `${ctx.packageManager} run typecheck`;
|
|
@@ -267,10 +268,24 @@ Beignet app scaffolded with \`@beignet/cli\`.
|
|
|
267
268
|
\`\`\`bash
|
|
268
269
|
${install}
|
|
269
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
|
|
270
285
|
${run}
|
|
271
286
|
\`\`\`
|
|
272
287
|
|
|
273
|
-
Open http://localhost:3000 for the todo workflow
|
|
288
|
+
Open http://localhost:3000 for the todo workflow. The homepage links to the health check, OpenAPI document, devtools, and Beignet docs.
|
|
274
289
|
|
|
275
290
|
## First checks
|
|
276
291
|
|
|
@@ -284,15 +299,32 @@ ${typecheck}
|
|
|
284
299
|
|
|
285
300
|
\`routes\` shows the contracts Beignet can inspect. \`lint\` checks dependency direction. \`doctor\` catches route, OpenAPI, and resource drift.
|
|
286
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
|
+
|
|
287
316
|
## App map
|
|
288
317
|
|
|
289
|
-
- \`features/todos/contracts.ts\` owns the HTTP contract and reuses
|
|
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.
|
|
290
320
|
- \`features/todos/use-cases/\` owns application behavior and validation.
|
|
291
321
|
- \`ports/\` defines app-owned dependencies.
|
|
292
322
|
- \`infra/\` implements ports for the selected runtime.
|
|
293
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.
|
|
294
325
|
- \`server/\` wires context, providers, routes, hooks, and error handling.
|
|
295
|
-
- \`server/outbox.ts\` registers durable events and jobs for the outbox drain route.
|
|
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.
|
|
296
328
|
- \`features/shared/errors.ts\` keeps application errors and route-owned error schemas together.
|
|
297
329
|
- \`features/todos/domain/events/\`, \`jobs/\`, \`listeners/\`, \`notifications/\`, \`schedules/\`, and \`uploads/\` show the feature-owned background workflow shape.
|
|
298
330
|
- \`app/storage/[...key]/route.ts\` serves public local storage objects.
|
|
@@ -309,6 +341,8 @@ ${beforeDeploy}
|
|
|
309
341
|
- Keep \`/api/devtools\` development-only unless you add authentication and stricter redaction.
|
|
310
342
|
- Set \`APP_URL\`, \`BETTER_AUTH_SECRET\`, \`CRON_SECRET\`, \`LOG_LEVEL\`, and service-specific integration variables in your hosting environment.
|
|
311
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.
|
|
312
346
|
- Review the starter authorization policy before exposing user-owned data.
|
|
313
347
|
${ctx.integrations.length > 0 ? "\nSee `docs/integrations.md` for setup notes.\n" : ""}
|
|
314
348
|
`;
|
|
@@ -322,13 +356,32 @@ function integrationsDoc(ctx) {
|
|
|
322
356
|
];
|
|
323
357
|
for (const integration of ctx.integrations) {
|
|
324
358
|
if (integration === "better-auth") {
|
|
325
|
-
lines.push("## Better Auth", "", "- Package: `@beignet/provider-auth-better-auth`", "- Peer dependency: `better-auth`", "- The
|
|
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
|
+
]), "");
|
|
326
369
|
}
|
|
327
370
|
if (integration === "drizzle-turso") {
|
|
328
|
-
lines.push("## Drizzle + Turso", "", "- Package: `@beignet/provider-drizzle-turso`", "- Peer dependencies: `@libsql/client` and `drizzle-orm`", "- Dev dependency: `drizzle-kit`", "- Set `TURSO_DB_URL` and optional `TURSO_DB_AUTH_TOKEN`.",
|
|
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
|
+
: []), "");
|
|
329
382
|
}
|
|
330
383
|
if (integration === "inngest") {
|
|
331
|
-
lines.push("## Inngest", "", "- Package: `@beignet/core` (`@beignet/core/events`, `@beignet/core/jobs`)", "- Package: `@beignet/provider-inngest`", "- Peer dependency: `inngest`", "- The standard preset wires `inngestProvider` in `server/providers.ts` and adds `jobs: JobDispatcherPort` to `AppPorts`.", "- Define jobs with `
|
|
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.", "");
|
|
332
385
|
}
|
|
333
386
|
if (integration === "pino") {
|
|
334
387
|
lines.push("## Pino logger", "", "- Package: `@beignet/provider-logger-pino`", "- Peer dependency: `pino`", "- Use this provider when you want structured request and handler logging.", "");
|
|
@@ -367,7 +420,7 @@ function envExample(ctx) {
|
|
|
367
420
|
lines.push("RESEND_API_KEY=", "RESEND_FROM=", "");
|
|
368
421
|
}
|
|
369
422
|
if (hasIntegration(ctx, "upstash-rate-limit")) {
|
|
370
|
-
lines.push("UPSTASH_REDIS_REST_URL=", "UPSTASH_REDIS_REST_TOKEN=", "UPSTASH_PREFIX=
|
|
423
|
+
lines.push("UPSTASH_REDIS_REST_URL=", "UPSTASH_REDIS_REST_TOKEN=", "UPSTASH_PREFIX=beignet:ratelimit", "");
|
|
371
424
|
}
|
|
372
425
|
if (hasIntegration(ctx, "better-auth")) {
|
|
373
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", "");
|
|
@@ -432,6 +485,11 @@ function page(ctx) {
|
|
|
432
485
|
<p>
|
|
433
486
|
<a href="/api/todos">Open todos API</a>
|
|
434
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>
|
|
435
493
|
</section>
|
|
436
494
|
</div>
|
|
437
495
|
</main>
|
|
@@ -455,6 +513,14 @@ export default function HomePage() {
|
|
|
455
513
|
</p>
|
|
456
514
|
</header>
|
|
457
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
|
+
|
|
458
524
|
<TodoApp />
|
|
459
525
|
</div>
|
|
460
526
|
</main>
|
|
@@ -463,47 +529,91 @@ export default function HomePage() {
|
|
|
463
529
|
`;
|
|
464
530
|
}
|
|
465
531
|
function serverProviders(ctx) {
|
|
466
|
-
const
|
|
532
|
+
const packageImports = [];
|
|
533
|
+
const appImports = [];
|
|
534
|
+
const declarations = [];
|
|
467
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
|
+
}
|
|
468
541
|
if (hasIntegration(ctx, "pino")) {
|
|
469
|
-
|
|
542
|
+
packageImports.push('import { loggerPinoProvider } from "@beignet/provider-logger-pino";');
|
|
470
543
|
entries.push("loggerPinoProvider");
|
|
471
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
|
+
}
|
|
472
551
|
if (hasIntegration(ctx, "inngest")) {
|
|
473
|
-
|
|
552
|
+
packageImports.push('import { inngestProvider } from "@beignet/provider-inngest";');
|
|
474
553
|
entries.push("inngestProvider");
|
|
475
554
|
}
|
|
476
555
|
if (hasIntegration(ctx, "resend")) {
|
|
477
|
-
|
|
556
|
+
packageImports.push('import { mailResendProvider } from "@beignet/provider-mail-resend";');
|
|
478
557
|
entries.push("mailResendProvider");
|
|
479
558
|
}
|
|
480
559
|
if (hasIntegration(ctx, "upstash-rate-limit")) {
|
|
481
|
-
|
|
560
|
+
packageImports.push('import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";');
|
|
482
561
|
entries.push("upstashRateLimitProvider");
|
|
483
562
|
}
|
|
484
|
-
|
|
563
|
+
const importBlock = [...packageImports.sort(), ...appImports].join("\n");
|
|
564
|
+
const declarationBlock = declarations.length > 0 ? `\n${declarations.join("\n")}\n` : "";
|
|
565
|
+
return `${importBlock}
|
|
566
|
+
${declarationBlock}
|
|
485
567
|
export const providers = [
|
|
486
568
|
${entries.join(",\n\t")},
|
|
487
569
|
];
|
|
488
570
|
`;
|
|
489
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
|
+
}
|
|
490
596
|
function server(ctx) {
|
|
491
|
-
const hasProviders = ctx.integrations.
|
|
492
|
-
|
|
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";
|
|
493
610
|
${hasProviders ? 'import { providers } from "./providers";\n' : ""}import { routes } from "./routes";
|
|
611
|
+
import type { AppContext } from "@/app-context";
|
|
494
612
|
import { appPorts, type AppPorts } from "@/ports";
|
|
495
613
|
|
|
496
|
-
export type AppContext = {
|
|
497
|
-
requestId: string;
|
|
498
|
-
ports: AppPorts;
|
|
499
|
-
};
|
|
500
|
-
|
|
501
614
|
export const server = await createNextServer<AppContext, AppPorts>({
|
|
502
615
|
ports: appPorts,
|
|
503
|
-
${hasProviders ? "\tproviders,\n" : ""}
|
|
504
|
-
requestId: crypto.randomUUID(),
|
|
505
|
-
ports,
|
|
506
|
-
}),
|
|
616
|
+
${hasProviders ? "\tproviders,\n" : ""}${providerConfigBlock} context: appContext,
|
|
507
617
|
routes,
|
|
508
618
|
mapUnhandledError: ({ err, ctx }) => {
|
|
509
619
|
const ports = ctx?.ports as
|
|
@@ -544,17 +654,128 @@ ${hasProviders ? "\tproviders,\n" : ""} createContext: async ({ ports }) => ({
|
|
|
544
654
|
});
|
|
545
655
|
`;
|
|
546
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
|
+
}
|
|
547
768
|
function todoApp(ctx) {
|
|
548
769
|
const listTodosQueryOptions = ctx.preset === "standard" ? "{ query: {} }" : "";
|
|
549
770
|
if (hasFeature(ctx, "forms")) {
|
|
550
771
|
return `"use client";
|
|
551
772
|
|
|
552
|
-
import {
|
|
773
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
553
774
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
775
|
+
import { rq } from "@/client";
|
|
776
|
+
import { rhf } from "@/client/forms";
|
|
554
777
|
import { createTodo, listTodos } from "@/features/todos/contracts";
|
|
555
|
-
import { rq } from "@/client/rq";
|
|
556
778
|
|
|
557
|
-
const rhf = createReactHookForm();
|
|
558
779
|
const createTodoForm = rhf(createTodo);
|
|
559
780
|
|
|
560
781
|
export function TodoApp() {
|
|
@@ -567,19 +788,21 @@ export function TodoApp() {
|
|
|
567
788
|
rq(createTodo).mutationOptions({
|
|
568
789
|
onSuccess: async () => {
|
|
569
790
|
form.reset();
|
|
570
|
-
await
|
|
571
|
-
|
|
572
|
-
|
|
791
|
+
await rq(listTodos).invalidate(queryClient);
|
|
792
|
+
},
|
|
793
|
+
onError: (error) => {
|
|
794
|
+
form.setError("root", rootFormError(error, "Could not create the todo."));
|
|
573
795
|
},
|
|
574
796
|
}),
|
|
575
797
|
);
|
|
576
798
|
|
|
577
799
|
const onSubmit = form.handleSubmit((body) => {
|
|
800
|
+
form.clearErrors("root");
|
|
578
801
|
createTodoMutation.mutate({ body });
|
|
579
802
|
});
|
|
580
803
|
|
|
581
804
|
return (
|
|
582
|
-
<section className="workspace">
|
|
805
|
+
<section id="todos" className="workspace">
|
|
583
806
|
<form className="composer" onSubmit={onSubmit}>
|
|
584
807
|
<label htmlFor="todo-title">New todo</label>
|
|
585
808
|
<div className="composer-row">
|
|
@@ -595,6 +818,9 @@ export function TodoApp() {
|
|
|
595
818
|
{form.formState.errors.title ? (
|
|
596
819
|
<p className="error">{form.formState.errors.title.message}</p>
|
|
597
820
|
) : null}
|
|
821
|
+
{form.formState.errors.root ? (
|
|
822
|
+
<p className="error">{form.formState.errors.root.message}</p>
|
|
823
|
+
) : null}
|
|
598
824
|
</form>
|
|
599
825
|
|
|
600
826
|
<div className="todos-panel">
|
|
@@ -624,8 +850,8 @@ export function TodoApp() {
|
|
|
624
850
|
|
|
625
851
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
626
852
|
import { useState } from "react";
|
|
853
|
+
import { rq } from "@/client";
|
|
627
854
|
import { createTodo, listTodos } from "@/features/todos/contracts";
|
|
628
|
-
import { rq } from "@/client/rq";
|
|
629
855
|
|
|
630
856
|
export function TodoApp() {
|
|
631
857
|
const [title, setTitle] = useState("");
|
|
@@ -635,15 +861,13 @@ export function TodoApp() {
|
|
|
635
861
|
rq(createTodo).mutationOptions({
|
|
636
862
|
onSuccess: async () => {
|
|
637
863
|
setTitle("");
|
|
638
|
-
await
|
|
639
|
-
queryKey: rq(listTodos).key(),
|
|
640
|
-
});
|
|
864
|
+
await rq(listTodos).invalidate(queryClient);
|
|
641
865
|
},
|
|
642
866
|
}),
|
|
643
867
|
);
|
|
644
868
|
|
|
645
869
|
return (
|
|
646
|
-
<section className="workspace">
|
|
870
|
+
<section id="todos" className="workspace">
|
|
647
871
|
<form
|
|
648
872
|
className="composer"
|
|
649
873
|
onSubmit={(event) => {
|
|
@@ -684,6 +908,55 @@ export function TodoApp() {
|
|
|
684
908
|
}
|
|
685
909
|
`;
|
|
686
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
|
+
}
|
|
687
960
|
const files = {
|
|
688
961
|
gitignore: `.next
|
|
689
962
|
node_modules
|
|
@@ -784,6 +1057,29 @@ input {
|
|
|
784
1057
|
color: #475569;
|
|
785
1058
|
}
|
|
786
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
|
+
|
|
787
1083
|
.panel {
|
|
788
1084
|
margin-top: 24px;
|
|
789
1085
|
padding: 20px;
|
|
@@ -910,32 +1206,26 @@ input {
|
|
|
910
1206
|
`,
|
|
911
1207
|
appProviders: `"use client";
|
|
912
1208
|
|
|
913
|
-
import {
|
|
1209
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
914
1210
|
import { type ReactNode, useState } from "react";
|
|
1211
|
+
import { makeQueryClient } from "@/client";
|
|
915
1212
|
|
|
916
1213
|
export function Providers({ children }: { children: ReactNode }) {
|
|
917
|
-
const [queryClient] = useState(() =>
|
|
1214
|
+
const [queryClient] = useState(() => makeQueryClient());
|
|
918
1215
|
|
|
919
1216
|
return (
|
|
920
1217
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
921
1218
|
);
|
|
922
1219
|
}
|
|
923
1220
|
`,
|
|
924
|
-
|
|
1221
|
+
formsClient: `import { createReactHookForm } from "@beignet/react-hook-form";
|
|
925
1222
|
|
|
926
|
-
export const
|
|
927
|
-
validate: true,
|
|
928
|
-
});
|
|
1223
|
+
export const rhf = createReactHookForm();
|
|
929
1224
|
`,
|
|
930
|
-
|
|
931
|
-
import { apiClient } from "./api-client";
|
|
932
|
-
|
|
933
|
-
export const rq = createReactQuery(apiClient);
|
|
934
|
-
`,
|
|
935
|
-
contractsTodos: `import { createContractGroup } from "@beignet/core/contracts";
|
|
1225
|
+
contractsTodos: `import { defineContractGroup } from "@beignet/core/contracts";
|
|
936
1226
|
import { z } from "zod";
|
|
937
1227
|
|
|
938
|
-
const todos =
|
|
1228
|
+
const todos = defineContractGroup().namespace("todos");
|
|
939
1229
|
|
|
940
1230
|
export const TodoSchema = z.object({
|
|
941
1231
|
id: z.string(),
|
|
@@ -1000,15 +1290,145 @@ export const appPorts = definePorts({
|
|
|
1000
1290
|
|
|
1001
1291
|
export type AppPorts = typeof appPorts;
|
|
1002
1292
|
`,
|
|
1003
|
-
|
|
1004
|
-
import { z } from "zod";
|
|
1005
|
-
import { CreateTodoSchema, TodoSchema } from "@/features/todos/contracts";
|
|
1006
|
-
import type { AppPorts } from "@/ports";
|
|
1293
|
+
appContext: `import type { AppPorts } from "@/ports";
|
|
1007
1294
|
|
|
1008
|
-
type AppContext = {
|
|
1295
|
+
export type AppContext = {
|
|
1009
1296
|
requestId: string;
|
|
1010
1297
|
ports: AppPorts;
|
|
1011
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";
|
|
1012
1432
|
|
|
1013
1433
|
const useCase = createUseCase<AppContext>();
|
|
1014
1434
|
|
|
@@ -1025,7 +1445,7 @@ export const ListTodosOutputSchema = z.object({
|
|
|
1025
1445
|
|
|
1026
1446
|
export const listTodos = useCase
|
|
1027
1447
|
.query("listTodos")
|
|
1028
|
-
.input(z.
|
|
1448
|
+
.input(z.object({}))
|
|
1029
1449
|
.output(ListTodosOutputSchema)
|
|
1030
1450
|
.run(async ({ ctx }) => {
|
|
1031
1451
|
const todos = await ctx.ports.todos.list();
|
|
@@ -1047,33 +1467,23 @@ export const createTodo = useCase
|
|
|
1047
1467
|
.output(TodoSchema)
|
|
1048
1468
|
.run(async ({ ctx, input }) => ctx.ports.todos.create(input));
|
|
1049
1469
|
`,
|
|
1050
|
-
todoRoutes: `import {
|
|
1051
|
-
import
|
|
1470
|
+
todoRoutes: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
1471
|
+
import { defineRouteGroup } from "@beignet/next";
|
|
1472
|
+
import type { AppContext } from "@/app-context";
|
|
1052
1473
|
import { createTodo, listTodos } from "@/features/todos/use-cases";
|
|
1053
1474
|
import * as contracts from "@/features/todos/contracts";
|
|
1054
1475
|
|
|
1055
1476
|
export const todoRoutes = defineRouteGroup<AppContext>({
|
|
1056
1477
|
name: "todos",
|
|
1057
1478
|
routes: [
|
|
1058
|
-
{
|
|
1059
|
-
|
|
1060
|
-
handle: async ({ ctx }) => ({
|
|
1061
|
-
status: 200,
|
|
1062
|
-
body: await listTodos.run({ ctx, input: undefined }),
|
|
1063
|
-
}),
|
|
1064
|
-
},
|
|
1065
|
-
{
|
|
1066
|
-
contract: contracts.createTodo,
|
|
1067
|
-
handle: async ({ ctx, body }) => ({
|
|
1068
|
-
status: 201,
|
|
1069
|
-
body: await createTodo.run({ ctx, input: body }),
|
|
1070
|
-
}),
|
|
1071
|
-
},
|
|
1479
|
+
{ contract: contracts.listTodos, useCase: listTodos },
|
|
1480
|
+
{ contract: contracts.createTodo, useCase: createTodo },
|
|
1072
1481
|
],
|
|
1073
1482
|
});
|
|
1074
1483
|
`,
|
|
1075
|
-
serverRoutes: `import {
|
|
1076
|
-
import
|
|
1484
|
+
serverRoutes: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
1485
|
+
import { contractsFromRoutes, defineRoutes } from "@beignet/next";
|
|
1486
|
+
import type { AppContext } from "@/app-context";
|
|
1077
1487
|
import { todoRoutes } from "@/features/todos/routes";
|
|
1078
1488
|
|
|
1079
1489
|
export const routes = defineRoutes<AppContext>([
|
|
@@ -1131,24 +1541,28 @@ export const env = createEnv({
|
|
|
1131
1541
|
|
|
1132
1542
|
export const isProduction = env.NODE_ENV === "production";
|
|
1133
1543
|
`,
|
|
1134
|
-
productionAppContext: `import type {
|
|
1135
|
-
import type {
|
|
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";
|
|
1136
1547
|
import type { AppGate, AppPorts } from "@/ports";
|
|
1137
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>;
|
|
1138
1555
|
|
|
1139
1556
|
export type AppContext = {
|
|
1140
1557
|
requestId: string;
|
|
1141
1558
|
actor: ActivityActor;
|
|
1142
1559
|
auth: AuthSession | null;
|
|
1143
1560
|
gate: AppGate;
|
|
1144
|
-
ports:
|
|
1145
|
-
devtools: DevtoolsPort;
|
|
1146
|
-
storage: StoragePort;
|
|
1147
|
-
};
|
|
1561
|
+
ports: AppRuntimePorts;
|
|
1148
1562
|
tenant?: ActivityTenant;
|
|
1149
|
-
} & Partial<
|
|
1563
|
+
} & Partial<TraceContext>;
|
|
1150
1564
|
`,
|
|
1151
|
-
|
|
1565
|
+
productionSharedErrors: `import { createAppError, defineErrors } from "@beignet/core/errors";
|
|
1152
1566
|
|
|
1153
1567
|
export const errors = defineErrors({
|
|
1154
1568
|
Unauthorized: {
|
|
@@ -1207,13 +1621,36 @@ export type Todo = z.infer<typeof TodoSchema>;
|
|
|
1207
1621
|
export type CreateTodoInput = z.infer<typeof CreateTodoInputSchema>;
|
|
1208
1622
|
export type ListTodosInput = z.infer<typeof ListTodosInputSchema>;
|
|
1209
1623
|
`,
|
|
1210
|
-
productionUseCaseBuilder: `import {
|
|
1211
|
-
import {
|
|
1624
|
+
productionUseCaseBuilder: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
1625
|
+
import { createUseCase } from "@beignet/core/application";
|
|
1212
1626
|
import type { AppContext } from "@/app-context";
|
|
1213
1627
|
|
|
1214
|
-
export const useCase = createUseCase<AppContext>(
|
|
1215
|
-
|
|
1216
|
-
}
|
|
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>();
|
|
1217
1654
|
`,
|
|
1218
1655
|
productionAuthHelpers: `import type { AppContext } from "@/app-context";
|
|
1219
1656
|
import type { AuthSession, AuthUser } from "@/ports/auth";
|
|
@@ -1231,9 +1668,10 @@ export function requireUser(ctx: AppContext): AuthUser {
|
|
|
1231
1668
|
return requireSession(ctx).user;
|
|
1232
1669
|
}
|
|
1233
1670
|
`,
|
|
1234
|
-
productionListTodosUseCase: `import {
|
|
1671
|
+
productionListTodosUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
1672
|
+
import { normalizeOffsetPage } from "@beignet/core/pagination";
|
|
1235
1673
|
import { useCase } from "@/lib/use-case";
|
|
1236
|
-
import { ListTodosInputSchema, ListTodosOutputSchema } from "
|
|
1674
|
+
import { ListTodosInputSchema, ListTodosOutputSchema } from "../schemas";
|
|
1237
1675
|
|
|
1238
1676
|
export const listTodosUseCase = useCase
|
|
1239
1677
|
.query("todos.list")
|
|
@@ -1248,9 +1686,10 @@ export const listTodosUseCase = useCase
|
|
|
1248
1686
|
return ctx.ports.todos.list(page);
|
|
1249
1687
|
});
|
|
1250
1688
|
`,
|
|
1251
|
-
productionGetTodoUseCase: `import {
|
|
1689
|
+
productionGetTodoUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
1690
|
+
import { appError } from "@/features/shared/errors";
|
|
1252
1691
|
import { useCase } from "@/lib/use-case";
|
|
1253
|
-
import { GetTodoInputSchema, TodoSchema } from "
|
|
1692
|
+
import { GetTodoInputSchema, TodoSchema } from "../schemas";
|
|
1254
1693
|
|
|
1255
1694
|
export const getTodoUseCase = useCase
|
|
1256
1695
|
.query("todos.get")
|
|
@@ -1265,10 +1704,11 @@ export const getTodoUseCase = useCase
|
|
|
1265
1704
|
return todo;
|
|
1266
1705
|
});
|
|
1267
1706
|
`,
|
|
1268
|
-
productionCreateTodoUseCase: `import {
|
|
1707
|
+
productionCreateTodoUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
1708
|
+
import { TodoCreated } from "@/features/todos/domain/events";
|
|
1269
1709
|
import { auditEntry } from "@/lib/audit";
|
|
1270
1710
|
import { useCase } from "@/lib/use-case";
|
|
1271
|
-
import { CreateTodoInputSchema, TodoSchema } from "
|
|
1711
|
+
import { CreateTodoInputSchema, TodoSchema } from "../schemas";
|
|
1272
1712
|
|
|
1273
1713
|
export const createTodoUseCase = useCase
|
|
1274
1714
|
.command("todos.create")
|
|
@@ -1308,7 +1748,7 @@ export {
|
|
|
1308
1748
|
type CreateTodoInput,
|
|
1309
1749
|
type ListTodosInput,
|
|
1310
1750
|
type Todo,
|
|
1311
|
-
} from "
|
|
1751
|
+
} from "../schemas";
|
|
1312
1752
|
`,
|
|
1313
1753
|
productionTodoRepositoryPort: `import type {
|
|
1314
1754
|
OffsetPage,
|
|
@@ -1318,7 +1758,7 @@ export {
|
|
|
1318
1758
|
import type {
|
|
1319
1759
|
CreateTodoInput,
|
|
1320
1760
|
Todo,
|
|
1321
|
-
} from "@/features/todos/
|
|
1761
|
+
} from "@/features/todos/schemas";
|
|
1322
1762
|
|
|
1323
1763
|
export type ListTodosResult = PageResult<Todo, OffsetPageInfo>;
|
|
1324
1764
|
|
|
@@ -1411,11 +1851,12 @@ export const todoPolicy = definePolicy({
|
|
|
1411
1851
|
},
|
|
1412
1852
|
});
|
|
1413
1853
|
`,
|
|
1414
|
-
productionInMemoryTodoRepository: `import {
|
|
1854
|
+
productionInMemoryTodoRepository: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
1855
|
+
import { offsetPageResult } from "@beignet/core/pagination";
|
|
1415
1856
|
import type {
|
|
1416
1857
|
CreateTodoInput,
|
|
1417
1858
|
Todo,
|
|
1418
|
-
} from "@/features/todos/
|
|
1859
|
+
} from "@/features/todos/schemas";
|
|
1419
1860
|
import type {
|
|
1420
1861
|
TodoAttachmentRepository,
|
|
1421
1862
|
TodoRepository,
|
|
@@ -1497,27 +1938,7 @@ export const appPorts = definePorts({
|
|
|
1497
1938
|
logger: fallbackLogger,
|
|
1498
1939
|
uow: createNoopUnitOfWork(() => ({
|
|
1499
1940
|
todos,
|
|
1500
|
-
})),
|
|
1501
|
-
});
|
|
1502
|
-
`,
|
|
1503
|
-
productionInfrastructurePortsWithDrizzleTurso: `import { createGate, definePorts } from "@beignet/core/ports";
|
|
1504
|
-
import { todoPolicy } from "@/features/todos/policy";
|
|
1505
|
-
import { appError } from "@/features/shared/errors";
|
|
1506
|
-
import { fallbackLogger } from "./logger";
|
|
1507
|
-
|
|
1508
|
-
const gate = createGate({
|
|
1509
|
-
policies: [todoPolicy],
|
|
1510
|
-
onDeny(decision) {
|
|
1511
|
-
return appError("Forbidden", {
|
|
1512
|
-
message: decision.reason ?? "Forbidden",
|
|
1513
|
-
details: decision.details,
|
|
1514
|
-
});
|
|
1515
|
-
},
|
|
1516
|
-
});
|
|
1517
|
-
|
|
1518
|
-
export const appPorts = definePorts({
|
|
1519
|
-
gate,
|
|
1520
|
-
logger: fallbackLogger,
|
|
1941
|
+
})),
|
|
1521
1942
|
});
|
|
1522
1943
|
`,
|
|
1523
1944
|
productionDrizzleConfig: `export default {
|
|
@@ -1689,8 +2110,34 @@ export const outboxMessages = sqliteTable(
|
|
|
1689
2110
|
),
|
|
1690
2111
|
}),
|
|
1691
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
|
+
);
|
|
1692
2138
|
`,
|
|
1693
|
-
productionDrizzleTodoRepository: `import {
|
|
2139
|
+
productionDrizzleTodoRepository: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
2140
|
+
import { offsetPageResult } from "@beignet/core/pagination";
|
|
1694
2141
|
import { count, desc, eq } from "drizzle-orm";
|
|
1695
2142
|
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
1696
2143
|
import type {
|
|
@@ -1700,7 +2147,7 @@ import type {
|
|
|
1700
2147
|
import type {
|
|
1701
2148
|
CreateTodoInput,
|
|
1702
2149
|
Todo,
|
|
1703
|
-
} from "@/features/todos/
|
|
2150
|
+
} from "@/features/todos/schemas";
|
|
1704
2151
|
import * as schema from "@/infra/db/schema";
|
|
1705
2152
|
|
|
1706
2153
|
type TodoRow = typeof schema.todos.$inferSelect;
|
|
@@ -1814,7 +2261,10 @@ import * as schema from "./schema";
|
|
|
1814
2261
|
|
|
1815
2262
|
export function createRepositories(
|
|
1816
2263
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
1817
|
-
): Omit<
|
|
2264
|
+
): Omit<
|
|
2265
|
+
AppTransactionPorts,
|
|
2266
|
+
"audit" | "events" | "idempotency" | "jobs" | "outbox"
|
|
2267
|
+
> {
|
|
1818
2268
|
return {
|
|
1819
2269
|
todoAttachments: createDrizzleTodoAttachmentRepository(db),
|
|
1820
2270
|
todos: createDrizzleTodoRepository(db),
|
|
@@ -1867,7 +2317,7 @@ export function createDrizzleAuditLog(
|
|
|
1867
2317
|
};
|
|
1868
2318
|
}
|
|
1869
2319
|
`,
|
|
1870
|
-
productionDbBootstrap: `import { createDrizzleTursoOutboxSetupStatements } from "@beignet/provider-drizzle-turso";
|
|
2320
|
+
productionDbBootstrap: `import { createDrizzleTursoIdempotencySetupStatements, createDrizzleTursoOutboxSetupStatements } from "@beignet/provider-drizzle-turso";
|
|
1871
2321
|
import type { Client } from "@libsql/client";
|
|
1872
2322
|
|
|
1873
2323
|
type BootstrapOptions = {
|
|
@@ -1961,6 +2411,7 @@ const setupStatements = [
|
|
|
1961
2411
|
"CREATE INDEX IF NOT EXISTS audit_log_request_idx ON audit_log (request_id)",
|
|
1962
2412
|
"CREATE INDEX IF NOT EXISTS audit_log_resource_idx ON audit_log (resource_type, resource_id)",
|
|
1963
2413
|
"CREATE INDEX IF NOT EXISTS audit_log_tenant_idx ON audit_log (tenant_id)",
|
|
2414
|
+
...createDrizzleTursoIdempotencySetupStatements(),
|
|
1964
2415
|
...createDrizzleTursoOutboxSetupStatements(),
|
|
1965
2416
|
];
|
|
1966
2417
|
|
|
@@ -1987,6 +2438,7 @@ const resetTables = [
|
|
|
1987
2438
|
"todo_attachments",
|
|
1988
2439
|
"todos",
|
|
1989
2440
|
"audit_log",
|
|
2441
|
+
"idempotency_records",
|
|
1990
2442
|
"outbox_messages",
|
|
1991
2443
|
] as const;
|
|
1992
2444
|
|
|
@@ -2025,7 +2477,6 @@ export async function resetStarterDatabase(client: Client): Promise<void> {
|
|
|
2025
2477
|
}
|
|
2026
2478
|
`,
|
|
2027
2479
|
productionDbProvider: `import { registerListeners } from "@beignet/core/events";
|
|
2028
|
-
import { createMemoryIdempotencyStore } from "@beignet/core/idempotency";
|
|
2029
2480
|
import { createInlineJobDispatcher } from "@beignet/core/jobs";
|
|
2030
2481
|
import { createMemoryMailer } from "@beignet/core/mail";
|
|
2031
2482
|
import { createInlineNotificationDispatcher } from "@beignet/core/notifications";
|
|
@@ -2034,19 +2485,15 @@ import {
|
|
|
2034
2485
|
createOutboxJobDispatcher,
|
|
2035
2486
|
} from "@beignet/core/outbox";
|
|
2036
2487
|
import {
|
|
2488
|
+
createInstrumentedAuditLog,
|
|
2037
2489
|
createSystemActor,
|
|
2038
|
-
createTenant,
|
|
2039
2490
|
} from "@beignet/core/ports";
|
|
2040
2491
|
import {
|
|
2041
2492
|
createProvider,
|
|
2042
2493
|
createProviderInstrumentation,
|
|
2043
2494
|
} from "@beignet/core/providers";
|
|
2044
2495
|
import {
|
|
2045
|
-
|
|
2046
|
-
createDevtoolsTraceContext,
|
|
2047
|
-
type DevtoolsPort,
|
|
2048
|
-
} from "@beignet/devtools";
|
|
2049
|
-
import {
|
|
2496
|
+
createDrizzleTursoIdempotencyPort,
|
|
2050
2497
|
createDrizzleTursoOutboxPort,
|
|
2051
2498
|
createDrizzleTursoUnitOfWork,
|
|
2052
2499
|
type DbPort,
|
|
@@ -2056,42 +2503,30 @@ import type { AppContext } from "@/app-context";
|
|
|
2056
2503
|
import { todoListeners } from "@/features/todos/listeners";
|
|
2057
2504
|
import { createDrizzleAuditLog } from "@/infra/audit/drizzle-audit-log";
|
|
2058
2505
|
import type { AppPorts } from "@/ports";
|
|
2506
|
+
import type { AppServiceContextInput } from "@/server/context";
|
|
2059
2507
|
import { ensureStarterDatabase } from "./bootstrap";
|
|
2060
2508
|
import { createRepositories } from "./repositories";
|
|
2061
2509
|
import type * as schema from "./schema";
|
|
2062
2510
|
|
|
2063
|
-
export const starterDatabaseProvider = createProvider
|
|
2511
|
+
export const starterDatabaseProvider = createProvider<
|
|
2512
|
+
{ db: DbPort<typeof schema> },
|
|
2513
|
+
AppContext,
|
|
2514
|
+
AppServiceContextInput
|
|
2515
|
+
>()({
|
|
2064
2516
|
name: "starter-database",
|
|
2065
2517
|
|
|
2066
|
-
async setup({ ports }) {
|
|
2067
|
-
const dbPort =
|
|
2068
|
-
const devtools = (ports as { devtools?: DevtoolsPort }).devtools;
|
|
2518
|
+
async setup({ ports, createServiceContext }) {
|
|
2519
|
+
const dbPort = ports.db;
|
|
2069
2520
|
if (!dbPort) {
|
|
2070
2521
|
throw new Error(
|
|
2071
2522
|
"starterDatabaseProvider requires a db port. Register createDrizzleTursoProvider({ schema }) before it.",
|
|
2072
2523
|
);
|
|
2073
2524
|
}
|
|
2074
2525
|
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
const actor = createSystemActor("starter-background");
|
|
2082
|
-
const tenant = createTenant("tenant_default");
|
|
2083
|
-
const traceContext = createDevtoolsTraceContext();
|
|
2084
|
-
|
|
2085
|
-
return {
|
|
2086
|
-
actor,
|
|
2087
|
-
auth: null,
|
|
2088
|
-
gate: currentPorts.gate.bind({ actor, auth: null }),
|
|
2089
|
-
requestId: crypto.randomUUID(),
|
|
2090
|
-
...traceContext,
|
|
2091
|
-
ports: currentPorts,
|
|
2092
|
-
tenant,
|
|
2093
|
-
};
|
|
2094
|
-
};
|
|
2526
|
+
const createBackgroundContext = () =>
|
|
2527
|
+
createServiceContext({
|
|
2528
|
+
actor: createSystemActor("starter-background"),
|
|
2529
|
+
});
|
|
2095
2530
|
|
|
2096
2531
|
const mailInstrumentation = createProviderInstrumentation(ports, {
|
|
2097
2532
|
providerName: "memory-mailer",
|
|
@@ -2125,12 +2560,12 @@ export const starterDatabaseProvider = createProvider({
|
|
|
2125
2560
|
});
|
|
2126
2561
|
},
|
|
2127
2562
|
});
|
|
2128
|
-
const audit =
|
|
2563
|
+
const audit = createInstrumentedAuditLog({
|
|
2129
2564
|
audit: createDrizzleAuditLog(dbPort.db),
|
|
2130
|
-
|
|
2565
|
+
instrumentation: ports,
|
|
2131
2566
|
});
|
|
2132
2567
|
const repositories = createRepositories(dbPort.db);
|
|
2133
|
-
const idempotency =
|
|
2568
|
+
const idempotency = createDrizzleTursoIdempotencyPort(dbPort.db);
|
|
2134
2569
|
const outbox = createDrizzleTursoOutboxPort(dbPort.db);
|
|
2135
2570
|
const unregisterListeners = registerListeners(eventBus, todoListeners, {
|
|
2136
2571
|
ctx: createBackgroundContext,
|
|
@@ -2142,34 +2577,52 @@ export const starterDatabaseProvider = createProvider({
|
|
|
2142
2577
|
},
|
|
2143
2578
|
});
|
|
2144
2579
|
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
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({
|
|
2161
2609
|
audit: createDrizzleAuditLog(tx),
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
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() {
|
|
2171
2625
|
await ensureStarterDatabase(dbPort.client);
|
|
2172
|
-
currentPorts = ctx.ports as AppContext["ports"];
|
|
2173
2626
|
},
|
|
2174
2627
|
stop() {
|
|
2175
2628
|
unregisterListeners();
|
|
@@ -2178,14 +2631,16 @@ export const starterDatabaseProvider = createProvider({
|
|
|
2178
2631
|
},
|
|
2179
2632
|
});
|
|
2180
2633
|
`,
|
|
2181
|
-
productionContractsTodos: `import {
|
|
2634
|
+
productionContractsTodos: `import { defineContractGroup } from "@beignet/core/contracts";
|
|
2182
2635
|
import { z } from "zod";
|
|
2183
2636
|
import { errors } from "@/features/shared/errors";
|
|
2184
2637
|
import {
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2638
|
+
CreateTodoInputSchema,
|
|
2639
|
+
GetTodoInputSchema,
|
|
2640
|
+
ListTodosInputSchema,
|
|
2641
|
+
ListTodosOutputSchema,
|
|
2642
|
+
TodoSchema,
|
|
2643
|
+
} from "@/features/todos/schemas";
|
|
2189
2644
|
|
|
2190
2645
|
const ErrorResponseSchema = z.object({
|
|
2191
2646
|
code: z.string(),
|
|
@@ -2193,7 +2648,7 @@ const ErrorResponseSchema = z.object({
|
|
|
2193
2648
|
requestId: z.string().optional(),
|
|
2194
2649
|
});
|
|
2195
2650
|
|
|
2196
|
-
const todos =
|
|
2651
|
+
const todos = defineContractGroup()
|
|
2197
2652
|
.namespace("todos")
|
|
2198
2653
|
.errors({ Unauthorized: errors.Unauthorized })
|
|
2199
2654
|
.responses({
|
|
@@ -2202,28 +2657,29 @@ const todos = createContractGroup()
|
|
|
2202
2657
|
|
|
2203
2658
|
export const listTodos = todos
|
|
2204
2659
|
.get("/api/todos")
|
|
2205
|
-
.query(
|
|
2660
|
+
.query(ListTodosInputSchema)
|
|
2206
2661
|
.responses({
|
|
2207
|
-
200:
|
|
2662
|
+
200: ListTodosOutputSchema,
|
|
2208
2663
|
});
|
|
2209
2664
|
|
|
2210
2665
|
export const createTodo = todos
|
|
2211
2666
|
.post("/api/todos")
|
|
2212
|
-
.body(
|
|
2667
|
+
.body(CreateTodoInputSchema)
|
|
2213
2668
|
.errors({ Forbidden: errors.Forbidden })
|
|
2214
2669
|
.responses({
|
|
2215
|
-
201:
|
|
2670
|
+
201: TodoSchema,
|
|
2216
2671
|
});
|
|
2217
2672
|
|
|
2218
2673
|
export const getTodo = todos
|
|
2219
2674
|
.get("/api/todos/:id")
|
|
2220
|
-
.pathParams(
|
|
2675
|
+
.pathParams(GetTodoInputSchema)
|
|
2221
2676
|
.errors({ TodoNotFound: errors.TodoNotFound })
|
|
2222
2677
|
.responses({
|
|
2223
|
-
200:
|
|
2678
|
+
200: TodoSchema,
|
|
2224
2679
|
});
|
|
2225
2680
|
`,
|
|
2226
|
-
productionTodoRoutes: `import {
|
|
2681
|
+
productionTodoRoutes: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
2682
|
+
import { defineRouteGroup } from "@beignet/next";
|
|
2227
2683
|
import type { AppContext } from "@/app-context";
|
|
2228
2684
|
import { createTodo, getTodo, listTodos } from "@/features/todos/contracts";
|
|
2229
2685
|
import {
|
|
@@ -2235,31 +2691,14 @@ import {
|
|
|
2235
2691
|
export const todoRoutes = defineRouteGroup<AppContext>({
|
|
2236
2692
|
name: "todos",
|
|
2237
2693
|
routes: [
|
|
2238
|
-
{
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
status: 200,
|
|
2242
|
-
body: await listTodosUseCase.run({ ctx, input: query }),
|
|
2243
|
-
}),
|
|
2244
|
-
},
|
|
2245
|
-
{
|
|
2246
|
-
contract: createTodo,
|
|
2247
|
-
handle: async ({ ctx, body }) => ({
|
|
2248
|
-
status: 201,
|
|
2249
|
-
body: await createTodoUseCase.run({ ctx, input: body }),
|
|
2250
|
-
}),
|
|
2251
|
-
},
|
|
2252
|
-
{
|
|
2253
|
-
contract: getTodo,
|
|
2254
|
-
handle: async ({ ctx, path }) => ({
|
|
2255
|
-
status: 200,
|
|
2256
|
-
body: await getTodoUseCase.run({ ctx, input: path }),
|
|
2257
|
-
}),
|
|
2258
|
-
},
|
|
2694
|
+
{ contract: listTodos, useCase: listTodosUseCase },
|
|
2695
|
+
{ contract: createTodo, useCase: createTodoUseCase },
|
|
2696
|
+
{ contract: getTodo, useCase: getTodoUseCase },
|
|
2259
2697
|
],
|
|
2260
2698
|
});
|
|
2261
2699
|
`,
|
|
2262
|
-
productionServerRoutes: `import {
|
|
2700
|
+
productionServerRoutes: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
2701
|
+
import { contractsFromRoutes, defineRoutes } from "@beignet/next";
|
|
2263
2702
|
import type { AppContext } from "@/app-context";
|
|
2264
2703
|
import { todoRoutes } from "@/features/todos/routes";
|
|
2265
2704
|
|
|
@@ -2268,16 +2707,72 @@ export const routes = defineRoutes<AppContext>([
|
|
|
2268
2707
|
]);
|
|
2269
2708
|
export const contracts = contractsFromRoutes(routes);
|
|
2270
2709
|
`,
|
|
2271
|
-
|
|
2272
|
-
import { createNextServer } from "@beignet/next";
|
|
2710
|
+
productionServerContext: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
2273
2711
|
import {
|
|
2712
|
+
type ActivityActor,
|
|
2274
2713
|
createAnonymousActor,
|
|
2714
|
+
createServiceActor,
|
|
2275
2715
|
createTenant,
|
|
2276
2716
|
createUserActor,
|
|
2277
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";
|
|
2278
2772
|
import type { AppContext } from "@/app-context";
|
|
2279
2773
|
import { appPorts } from "@/infra/app-ports";
|
|
2280
2774
|
import { env } from "@/lib/env";
|
|
2775
|
+
import { appContext } from "./context";
|
|
2281
2776
|
import { providers } from "./providers";
|
|
2282
2777
|
import { routes } from "./routes";
|
|
2283
2778
|
|
|
@@ -2290,29 +2785,8 @@ export const server = await createNextServer({
|
|
|
2290
2785
|
DB_AUTH_TOKEN: env.TURSO_DB_AUTH_TOKEN,
|
|
2291
2786
|
},
|
|
2292
2787
|
},
|
|
2293
|
-
hooks: [
|
|
2294
|
-
|
|
2295
|
-
requestIdHeader: "x-request-id",
|
|
2296
|
-
}),
|
|
2297
|
-
],
|
|
2298
|
-
createContext: async ({ req, ports }) => {
|
|
2299
|
-
const auth = await ports.auth.getSession(req);
|
|
2300
|
-
const tenantId = req.headers.get("x-tenant-id") || undefined;
|
|
2301
|
-
const context = {
|
|
2302
|
-
requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
|
|
2303
|
-
actor: auth
|
|
2304
|
-
? createUserActor(auth.user.id, { displayName: auth.user.name })
|
|
2305
|
-
: createAnonymousActor(),
|
|
2306
|
-
auth,
|
|
2307
|
-
ports,
|
|
2308
|
-
tenant: tenantId ? createTenant(tenantId) : undefined,
|
|
2309
|
-
};
|
|
2310
|
-
|
|
2311
|
-
return {
|
|
2312
|
-
...context,
|
|
2313
|
-
gate: ports.gate.bind(context),
|
|
2314
|
-
};
|
|
2315
|
-
},
|
|
2788
|
+
hooks: [createIdempotencyHooks<AppContext>()],
|
|
2789
|
+
context: appContext,
|
|
2316
2790
|
routes,
|
|
2317
2791
|
mapUnhandledError: ({ err, ctx }) => {
|
|
2318
2792
|
ctx?.ports.logger.error("Unhandled API error", {
|
|
@@ -2332,16 +2806,13 @@ export const server = await createNextServer({
|
|
|
2332
2806
|
},
|
|
2333
2807
|
});
|
|
2334
2808
|
`,
|
|
2335
|
-
productionServerWithDrizzleTurso: `import {
|
|
2809
|
+
productionServerWithDrizzleTurso: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
2336
2810
|
import { createNextServer } from "@beignet/next";
|
|
2337
|
-
import {
|
|
2338
|
-
createAnonymousActor,
|
|
2339
|
-
createTenant,
|
|
2340
|
-
createUserActor,
|
|
2341
|
-
} from "@beignet/core/ports";
|
|
2811
|
+
import { createIdempotencyHooks } from "@beignet/core/server";
|
|
2342
2812
|
import type { AppContext } from "@/app-context";
|
|
2343
2813
|
import { appPorts } from "@/infra/app-ports";
|
|
2344
2814
|
import { env } from "@/lib/env";
|
|
2815
|
+
import { appContext } from "./context";
|
|
2345
2816
|
import { providers } from "./providers";
|
|
2346
2817
|
import { routes } from "./routes";
|
|
2347
2818
|
|
|
@@ -2354,30 +2825,8 @@ export const server = await createNextServer({
|
|
|
2354
2825
|
DB_AUTH_TOKEN: env.TURSO_DB_AUTH_TOKEN,
|
|
2355
2826
|
},
|
|
2356
2827
|
},
|
|
2357
|
-
hooks: [
|
|
2358
|
-
|
|
2359
|
-
requestIdHeader: "x-request-id",
|
|
2360
|
-
}),
|
|
2361
|
-
],
|
|
2362
|
-
createContext: async ({ req, ports }) => {
|
|
2363
|
-
const auth = await ports.auth.getSession(req);
|
|
2364
|
-
const tenantId = req.headers.get("x-tenant-id") || undefined;
|
|
2365
|
-
|
|
2366
|
-
const context = {
|
|
2367
|
-
requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
|
|
2368
|
-
actor: auth
|
|
2369
|
-
? createUserActor(auth.user.id, { displayName: auth.user.name })
|
|
2370
|
-
: createAnonymousActor(),
|
|
2371
|
-
auth,
|
|
2372
|
-
ports,
|
|
2373
|
-
tenant: tenantId ? createTenant(tenantId) : undefined,
|
|
2374
|
-
};
|
|
2375
|
-
|
|
2376
|
-
return {
|
|
2377
|
-
...context,
|
|
2378
|
-
gate: ports.gate.bind(context),
|
|
2379
|
-
};
|
|
2380
|
-
},
|
|
2828
|
+
hooks: [createIdempotencyHooks<AppContext>()],
|
|
2829
|
+
context: appContext,
|
|
2381
2830
|
routes,
|
|
2382
2831
|
mapUnhandledError: ({ err, ctx }) => {
|
|
2383
2832
|
ctx?.ports.logger.error("Unhandled API error", {
|
|
@@ -2526,6 +2975,55 @@ try {
|
|
|
2526
2975
|
} finally {
|
|
2527
2976
|
client.close();
|
|
2528
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
|
+
}
|
|
2529
3027
|
`,
|
|
2530
3028
|
productionAuditHelper: `import type { AuditLogEntryInput } from "@beignet/core/ports";
|
|
2531
3029
|
import type { AppContext } from "@/app-context";
|
|
@@ -2558,14 +3056,12 @@ export const TodoCreated = defineEvent("todos.created", {
|
|
|
2558
3056
|
|
|
2559
3057
|
export const todoEvents = [TodoCreated] as const;
|
|
2560
3058
|
`,
|
|
2561
|
-
productionTodoJobs: `import {
|
|
3059
|
+
productionTodoJobs: `import { retry } from "@beignet/core/jobs";
|
|
2562
3060
|
import { z } from "zod";
|
|
2563
|
-
import type { AppContext } from "@/app-context";
|
|
2564
3061
|
import { auditEntry } from "@/lib/audit";
|
|
3062
|
+
import { defineJob } from "@/lib/jobs";
|
|
2565
3063
|
|
|
2566
|
-
const
|
|
2567
|
-
|
|
2568
|
-
export const LogTodoCreatedJob = jobs.defineJob("todos.log-created", {
|
|
3064
|
+
export const LogTodoCreatedJob = defineJob("todos.log-created", {
|
|
2569
3065
|
payload: z.object({
|
|
2570
3066
|
todoId: z.string().uuid(),
|
|
2571
3067
|
title: z.string(),
|
|
@@ -2591,16 +3087,11 @@ export const LogTodoCreatedJob = jobs.defineJob("todos.log-created", {
|
|
|
2591
3087
|
|
|
2592
3088
|
export const todoJobs = [LogTodoCreatedJob] as const;
|
|
2593
3089
|
`,
|
|
2594
|
-
productionTodoNotifications: `import {
|
|
2595
|
-
createNotificationHandlers,
|
|
2596
|
-
defineMailNotificationChannel,
|
|
2597
|
-
} from "@beignet/core/notifications";
|
|
3090
|
+
productionTodoNotifications: `import { defineMailNotificationChannel } from "@beignet/core/notifications";
|
|
2598
3091
|
import { z } from "zod";
|
|
2599
|
-
import
|
|
3092
|
+
import { defineNotification } from "@/lib/notifications";
|
|
2600
3093
|
|
|
2601
|
-
const
|
|
2602
|
-
|
|
2603
|
-
export const TodoCreatedNotification = notifications.defineNotification(
|
|
3094
|
+
export const TodoCreatedNotification = defineNotification(
|
|
2604
3095
|
"todos.created",
|
|
2605
3096
|
{
|
|
2606
3097
|
payload: z.object({
|
|
@@ -2617,16 +3108,13 @@ export const TodoCreatedNotification = notifications.defineNotification(
|
|
|
2617
3108
|
},
|
|
2618
3109
|
);
|
|
2619
3110
|
`,
|
|
2620
|
-
productionTodoListeners: `import {
|
|
2621
|
-
import type { AppContext } from "@/app-context";
|
|
2622
|
-
import { TodoCreated } from "@/features/todos/domain/events";
|
|
3111
|
+
productionTodoListeners: `import { TodoCreated } from "@/features/todos/domain/events";
|
|
2623
3112
|
import { LogTodoCreatedJob } from "@/features/todos/jobs";
|
|
2624
3113
|
import { TodoCreatedNotification } from "@/features/todos/notifications";
|
|
2625
3114
|
import { auditEntry } from "@/lib/audit";
|
|
3115
|
+
import { defineListener } from "@/lib/listeners";
|
|
2626
3116
|
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2629
|
-
export const enqueueTodoCreatedWork = listeners.defineListener(TodoCreated, {
|
|
3117
|
+
export const enqueueTodoCreatedWork = defineListener(TodoCreated, {
|
|
2630
3118
|
name: "todos.enqueue-created-work",
|
|
2631
3119
|
async handle({ ctx, payload }) {
|
|
2632
3120
|
await ctx.ports.jobs.dispatch(LogTodoCreatedJob, payload);
|
|
@@ -2648,14 +3136,11 @@ export const enqueueTodoCreatedWork = listeners.defineListener(TodoCreated, {
|
|
|
2648
3136
|
export const todoListeners = [enqueueTodoCreatedWork] as const;
|
|
2649
3137
|
`,
|
|
2650
3138
|
productionTodoSchedules: `import { normalizeOffsetPage } from "@beignet/core/pagination";
|
|
2651
|
-
import { createScheduleHandlers } from "@beignet/core/schedules";
|
|
2652
3139
|
import { z } from "zod";
|
|
2653
|
-
import type { AppContext } from "@/app-context";
|
|
2654
3140
|
import { auditEntry } from "@/lib/audit";
|
|
3141
|
+
import { defineSchedule } from "@/lib/schedules";
|
|
2655
3142
|
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
export const LogDailyTodoSummarySchedule = schedules.defineSchedule(
|
|
3143
|
+
export const LogDailyTodoSummarySchedule = defineSchedule(
|
|
2659
3144
|
"todos.log-daily-summary",
|
|
2660
3145
|
{
|
|
2661
3146
|
cron: "0 9 * * *",
|
|
@@ -2699,6 +3184,23 @@ export const LogDailyTodoSummarySchedule = schedules.defineSchedule(
|
|
|
2699
3184
|
);
|
|
2700
3185
|
|
|
2701
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
|
+
}
|
|
2702
3204
|
`,
|
|
2703
3205
|
productionTodoUploads: `import { defineUpload, defineUploads } from "@beignet/core/uploads";
|
|
2704
3206
|
import { z } from "zod";
|
|
@@ -2780,13 +3282,26 @@ export const todoUploads = defineUploads({
|
|
|
2780
3282
|
});
|
|
2781
3283
|
`,
|
|
2782
3284
|
productionOutbox: `import { defineOutboxRegistry } from "@beignet/core/outbox";
|
|
3285
|
+
import { createServiceActor } from "@beignet/core/ports";
|
|
3286
|
+
import type { AppContext } from "@/app-context";
|
|
2783
3287
|
import { todoEvents } from "@/features/todos/domain/events";
|
|
2784
3288
|
import { todoJobs } from "@/features/todos/jobs";
|
|
3289
|
+
import { server } from "./index.js";
|
|
2785
3290
|
|
|
2786
3291
|
export const outboxRegistry = defineOutboxRegistry({
|
|
2787
3292
|
events: todoEvents,
|
|
2788
3293
|
jobs: todoJobs,
|
|
2789
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
|
+
}
|
|
2790
3305
|
`,
|
|
2791
3306
|
productionOutboxDrainRoute: `import { createOutboxDrainRoute } from "@beignet/next";
|
|
2792
3307
|
import { env } from "@/lib/env";
|
|
@@ -2801,111 +3316,23 @@ export const { GET, POST } = createOutboxDrainRoute({
|
|
|
2801
3316
|
secret: env.CRON_SECRET,
|
|
2802
3317
|
});
|
|
2803
3318
|
`,
|
|
2804
|
-
productionScheduleRoute: `import {
|
|
2805
|
-
import type { AppContext } from "@/app-context";
|
|
2806
|
-
import { LogDailyTodoSummarySchedule } from "@/features/todos/schedules";
|
|
3319
|
+
productionScheduleRoute: `import { createScheduleRoute } from "@beignet/next";
|
|
2807
3320
|
import { env } from "@/lib/env";
|
|
2808
3321
|
import { server } from "@/server";
|
|
3322
|
+
import { schedules } from "@/server/schedules";
|
|
2809
3323
|
|
|
2810
|
-
|
|
2811
|
-
if (!env.CRON_SECRET) {
|
|
2812
|
-
return Response.json(
|
|
2813
|
-
{
|
|
2814
|
-
ok: false,
|
|
2815
|
-
error: "CRON_SECRET is not configured.",
|
|
2816
|
-
scheduleName: LogDailyTodoSummarySchedule.name,
|
|
2817
|
-
},
|
|
2818
|
-
{ status: 500 },
|
|
2819
|
-
);
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
|
-
if (request.headers.get("authorization") !== \`Bearer \${env.CRON_SECRET}\`) {
|
|
2823
|
-
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
const ctx = await server.createContextFromNext();
|
|
2827
|
-
const runner = createInlineScheduleRunner<AppContext>({
|
|
2828
|
-
ctx,
|
|
2829
|
-
onStart({ run, schedule }) {
|
|
2830
|
-
ctx.ports.devtools.record({
|
|
2831
|
-
type: "schedule",
|
|
2832
|
-
watcher: "schedules",
|
|
2833
|
-
requestId: ctx.requestId,
|
|
2834
|
-
traceId: ctx.traceId,
|
|
2835
|
-
scheduleName: schedule.name,
|
|
2836
|
-
status: "started",
|
|
2837
|
-
cron: schedule.cron,
|
|
2838
|
-
timezone: schedule.timezone,
|
|
2839
|
-
details: {
|
|
2840
|
-
source: run.source,
|
|
2841
|
-
scheduledAt: run.scheduledAt?.toISOString(),
|
|
2842
|
-
},
|
|
2843
|
-
});
|
|
2844
|
-
},
|
|
2845
|
-
onSuccess({ run, schedule }) {
|
|
2846
|
-
ctx.ports.devtools.record({
|
|
2847
|
-
type: "schedule",
|
|
2848
|
-
watcher: "schedules",
|
|
2849
|
-
requestId: ctx.requestId,
|
|
2850
|
-
traceId: ctx.traceId,
|
|
2851
|
-
scheduleName: schedule.name,
|
|
2852
|
-
status: "completed",
|
|
2853
|
-
cron: schedule.cron,
|
|
2854
|
-
timezone: schedule.timezone,
|
|
2855
|
-
details: {
|
|
2856
|
-
source: run.source,
|
|
2857
|
-
scheduledAt: run.scheduledAt?.toISOString(),
|
|
2858
|
-
},
|
|
2859
|
-
});
|
|
2860
|
-
},
|
|
2861
|
-
onError({ error, run, schedule }) {
|
|
2862
|
-
ctx.ports.devtools.record({
|
|
2863
|
-
type: "schedule",
|
|
2864
|
-
watcher: "schedules",
|
|
2865
|
-
requestId: ctx.requestId,
|
|
2866
|
-
traceId: ctx.traceId,
|
|
2867
|
-
scheduleName: schedule.name,
|
|
2868
|
-
status: "failed",
|
|
2869
|
-
cron: schedule.cron,
|
|
2870
|
-
timezone: schedule.timezone,
|
|
2871
|
-
details: {
|
|
2872
|
-
error,
|
|
2873
|
-
source: run.source,
|
|
2874
|
-
scheduledAt: run.scheduledAt?.toISOString(),
|
|
2875
|
-
},
|
|
2876
|
-
});
|
|
2877
|
-
ctx.ports.logger.error("Daily todo summary schedule failed", {
|
|
2878
|
-
error,
|
|
2879
|
-
scheduleName: schedule.name,
|
|
2880
|
-
});
|
|
2881
|
-
},
|
|
2882
|
-
});
|
|
2883
|
-
|
|
2884
|
-
try {
|
|
2885
|
-
await runner.run(LogDailyTodoSummarySchedule, {
|
|
2886
|
-
source: "starter-cron-route",
|
|
2887
|
-
});
|
|
2888
|
-
} catch {
|
|
2889
|
-
return Response.json(
|
|
2890
|
-
{
|
|
2891
|
-
ok: false,
|
|
2892
|
-
error: "Schedule failed",
|
|
2893
|
-
scheduleName: LogDailyTodoSummarySchedule.name,
|
|
2894
|
-
},
|
|
2895
|
-
{ status: 500 },
|
|
2896
|
-
);
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
return Response.json({
|
|
2900
|
-
ok: true,
|
|
2901
|
-
scheduleName: LogDailyTodoSummarySchedule.name,
|
|
2902
|
-
});
|
|
2903
|
-
}
|
|
3324
|
+
export const runtime = "nodejs";
|
|
2904
3325
|
|
|
2905
|
-
export const GET =
|
|
2906
|
-
|
|
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
|
+
});
|
|
2907
3333
|
`,
|
|
2908
|
-
productionUploadsRoute: `import {
|
|
3334
|
+
productionUploadsRoute: `import { resolveProviderInstrumentationPort } from "@beignet/core/providers";
|
|
3335
|
+
import { createUploadRouter, uploadsFromRegistry } from "@beignet/core/uploads";
|
|
2909
3336
|
import { createUploadRoute } from "@beignet/next";
|
|
2910
3337
|
import type { AppContext } from "@/app-context";
|
|
2911
3338
|
import { todoUploads } from "@/features/todos/uploads";
|
|
@@ -2915,17 +3342,10 @@ const uploadRouter = createUploadRouter<AppContext>({
|
|
|
2915
3342
|
uploads: uploadsFromRegistry(todoUploads),
|
|
2916
3343
|
ctx: () => server.createContextFromNext(),
|
|
2917
3344
|
storage: server.ports.storage,
|
|
2918
|
-
instrumentation: server.ports
|
|
3345
|
+
instrumentation: resolveProviderInstrumentationPort(server.ports),
|
|
2919
3346
|
});
|
|
2920
3347
|
|
|
2921
3348
|
export const { POST } = createUploadRoute(uploadRouter);
|
|
2922
|
-
`,
|
|
2923
|
-
productionUploadClient: `import { createUploadClient } from "@beignet/core/uploads/client";
|
|
2924
|
-
import type { todoUploads } from "@/features/todos/uploads";
|
|
2925
|
-
|
|
2926
|
-
export const uploads = createUploadClient<typeof todoUploads>({
|
|
2927
|
-
baseUrl: "/api/uploads",
|
|
2928
|
-
});
|
|
2929
3349
|
`,
|
|
2930
3350
|
};
|
|
2931
3351
|
function productionProviderPortImports(ctx) {
|
|
@@ -2942,6 +3362,69 @@ function productionProviderPortFields(ctx) {
|
|
|
2942
3362
|
}
|
|
2943
3363
|
return fields;
|
|
2944
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
|
+
}
|
|
2945
3428
|
function productionPorts(ctx) {
|
|
2946
3429
|
const mailImport = 'import type { MailerPort } from "@beignet/core/mail";\n';
|
|
2947
3430
|
const imports = [
|
|
@@ -2969,6 +3452,7 @@ import type { TodoAttachmentRepository, TodoRepository } from "@/features/todos/
|
|
|
2969
3452
|
export type AppTransactionPorts = {
|
|
2970
3453
|
audit: AuditLogPort;
|
|
2971
3454
|
events: import("@beignet/core/ports").BufferedDomainEventRecorder;
|
|
3455
|
+
idempotency: IdempotencyPort;
|
|
2972
3456
|
jobs: JobDispatcherPort;
|
|
2973
3457
|
outbox: OutboxPort;
|
|
2974
3458
|
todoAttachments: TodoAttachmentRepository;
|
|
@@ -3019,14 +3503,10 @@ import type { AuthorizationContext, todoPolicy } from "@/features/todos/policy";
|
|
|
3019
3503
|
import type { AuthPort } from "./auth";
|
|
3020
3504
|
import type { TodoAttachmentRepository, TodoRepository } from "@/features/todos/ports";
|
|
3021
3505
|
|
|
3022
|
-
export type AppDatabasePort = {
|
|
3023
|
-
db: unknown;
|
|
3024
|
-
client: unknown;
|
|
3025
|
-
};
|
|
3026
|
-
|
|
3027
3506
|
export type AppTransactionPorts = {
|
|
3028
3507
|
audit: AuditLogPort;
|
|
3029
3508
|
events: import("@beignet/core/ports").BufferedDomainEventRecorder;
|
|
3509
|
+
idempotency: IdempotencyPort;
|
|
3030
3510
|
jobs: JobDispatcherPort;
|
|
3031
3511
|
outbox: OutboxPort;
|
|
3032
3512
|
todoAttachments: TodoAttachmentRepository;
|
|
@@ -3038,7 +3518,6 @@ export type AppGate = BoundGate<[typeof todoPolicy]>;
|
|
|
3038
3518
|
export type AppPorts = {
|
|
3039
3519
|
audit: AuditLogPort;
|
|
3040
3520
|
auth: AuthPort;
|
|
3041
|
-
db: AppDatabasePort;
|
|
3042
3521
|
eventBus: EventBusPort;
|
|
3043
3522
|
gate: GatePort<AuthorizationContext, [typeof todoPolicy]>;
|
|
3044
3523
|
idempotency: IdempotencyPort;
|
|
@@ -3056,6 +3535,7 @@ ${providerFields.length > 0 ? `${providerFields.join("\n")}\n` : ""} uow: UnitOf
|
|
|
3056
3535
|
}
|
|
3057
3536
|
function productionServerProviders(ctx) {
|
|
3058
3537
|
const imports = [
|
|
3538
|
+
'import type { beignetServerOnly } from "@beignet/core/server-only";',
|
|
3059
3539
|
'import { createDevtoolsProvider } from "@beignet/devtools";',
|
|
3060
3540
|
hasIntegration(ctx, "better-auth")
|
|
3061
3541
|
? 'import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";'
|
|
@@ -3107,38 +3587,20 @@ function productionServerProviders(ctx) {
|
|
|
3107
3587
|
${declarations}
|
|
3108
3588
|
export const providers = [
|
|
3109
3589
|
${entries.join("\n")}
|
|
3110
|
-
];
|
|
3590
|
+
] as const;
|
|
3111
3591
|
`;
|
|
3112
3592
|
}
|
|
3113
|
-
function productionTodosTest(
|
|
3114
|
-
const portImports = [
|
|
3115
|
-
hasIntegration(ctx, "upstash-rate-limit")
|
|
3116
|
-
? "createMemoryRateLimiter"
|
|
3117
|
-
: undefined,
|
|
3118
|
-
"createMemoryStorage",
|
|
3119
|
-
"createNoopUnitOfWork",
|
|
3120
|
-
"createUserActor",
|
|
3121
|
-
].filter((item) => Boolean(item));
|
|
3122
|
-
const extraPorts = [
|
|
3123
|
-
hasDrizzleTurso(ctx)
|
|
3124
|
-
? "\t\t\tdb: { db: undefined as never, client: undefined as never },"
|
|
3125
|
-
: undefined,
|
|
3126
|
-
hasIntegration(ctx, "upstash-rate-limit")
|
|
3127
|
-
? "\t\t\trateLimit: createMemoryRateLimiter(),"
|
|
3128
|
-
: undefined,
|
|
3129
|
-
].filter((entry) => Boolean(entry));
|
|
3593
|
+
function productionTodosTest(_ctx) {
|
|
3130
3594
|
return `import { describe, expect, it } from "bun:test";
|
|
3131
3595
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
3132
|
-
import { createMemoryIdempotencyStore } from "@beignet/core/idempotency";
|
|
3133
|
-
import { createMemoryMailer } from "@beignet/core/mail";
|
|
3134
|
-
import { createMemoryNotificationPort } from "@beignet/core/notifications";
|
|
3135
|
-
import { createMemoryOutbox } from "@beignet/core/outbox";
|
|
3136
3596
|
import { offsetPageResult, type OffsetPage } from "@beignet/core/pagination";
|
|
3137
3597
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3138
|
-
import {
|
|
3139
|
-
import {
|
|
3598
|
+
import { createDomainEventRecorder } from "@beignet/core/ports";
|
|
3599
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
3600
|
+
import { createTestUserActor } from "@beignet/core/ports/testing";
|
|
3140
3601
|
import type { AppContext } from "@/app-context";
|
|
3141
3602
|
import { appPorts } from "@/infra/app-ports";
|
|
3603
|
+
import type { AppTransactionPorts } from "@/ports";
|
|
3142
3604
|
import {
|
|
3143
3605
|
createTodoUseCase,
|
|
3144
3606
|
getTodoUseCase,
|
|
@@ -3203,14 +3665,6 @@ describe("todos resource", () => {
|
|
|
3203
3665
|
it("creates, gets, and lists todos", async () => {
|
|
3204
3666
|
const todos = createTestTodoRepository();
|
|
3205
3667
|
const todoAttachments = createTestTodoAttachmentRepository();
|
|
3206
|
-
const audit = createMemoryAuditLog();
|
|
3207
|
-
const eventBus = createInMemoryEventBus();
|
|
3208
|
-
const jobs = { dispatch: async () => undefined };
|
|
3209
|
-
const mailer = createMemoryMailer({
|
|
3210
|
-
defaultFrom: "Test <test@example.local>",
|
|
3211
|
-
});
|
|
3212
|
-
const notifications = createMemoryNotificationPort();
|
|
3213
|
-
const outbox = createMemoryOutbox();
|
|
3214
3668
|
const auth = {
|
|
3215
3669
|
user: {
|
|
3216
3670
|
id: "user_test",
|
|
@@ -3219,38 +3673,31 @@ describe("todos resource", () => {
|
|
|
3219
3673
|
},
|
|
3220
3674
|
session: { id: "session_test" },
|
|
3221
3675
|
};
|
|
3222
|
-
const
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
...appPorts,
|
|
3227
|
-
audit,
|
|
3228
|
-
eventBus,
|
|
3229
|
-
idempotency: createMemoryIdempotencyStore(),
|
|
3230
|
-
jobs,
|
|
3231
|
-
mailer,
|
|
3232
|
-
notifications,
|
|
3233
|
-
outbox,
|
|
3234
|
-
todoAttachments,
|
|
3235
|
-
todos,
|
|
3236
|
-
${extraPorts.length > 0 ? `${extraPorts.join("\n")}\n` : ""} uow: createNoopUnitOfWork(() => ({
|
|
3237
|
-
audit,
|
|
3238
|
-
events: createDomainEventRecorder(),
|
|
3239
|
-
jobs,
|
|
3240
|
-
outbox,
|
|
3676
|
+
const testFixture = createTestPorts<AppContext["ports"], AppTransactionPorts>({
|
|
3677
|
+
base: appPorts,
|
|
3678
|
+
overrides: {
|
|
3679
|
+
gate: appPorts.gate,
|
|
3241
3680
|
todoAttachments,
|
|
3242
3681
|
todos,
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
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
|
+
}),
|
|
3250
3698
|
auth,
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
}));
|
|
3699
|
+
});
|
|
3700
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
3254
3701
|
|
|
3255
3702
|
const ctx = await tester.ctx();
|
|
3256
3703
|
const created = await tester.run(
|
|
@@ -3321,9 +3768,8 @@ function getProductionTemplateFiles(ctx) {
|
|
|
3321
3768
|
path: "app/api/uploads/[uploadName]/[action]/route.ts",
|
|
3322
3769
|
content: files.productionUploadsRoute,
|
|
3323
3770
|
},
|
|
3324
|
-
{ path: "client/
|
|
3325
|
-
{ path: "client/
|
|
3326
|
-
{ path: "client/uploads.ts", content: files.productionUploadClient },
|
|
3771
|
+
{ path: "client/index.ts", content: clientIndex(ctx) },
|
|
3772
|
+
{ path: "client/forms.ts", content: files.formsClient },
|
|
3327
3773
|
{
|
|
3328
3774
|
path: "features/todos/contracts.ts",
|
|
3329
3775
|
content: files.productionContractsTodos,
|
|
@@ -3334,7 +3780,7 @@ function getProductionTemplateFiles(ctx) {
|
|
|
3334
3780
|
{
|
|
3335
3781
|
path: "infra/app-ports.ts",
|
|
3336
3782
|
content: usesDrizzleTurso
|
|
3337
|
-
?
|
|
3783
|
+
? productionInfrastructurePortsWithDrizzleTurso(ctx)
|
|
3338
3784
|
: files.productionInfrastructurePorts,
|
|
3339
3785
|
},
|
|
3340
3786
|
{ path: "lib/env.ts", content: files.productionEnv },
|
|
@@ -3384,7 +3830,7 @@ function getProductionTemplateFiles(ctx) {
|
|
|
3384
3830
|
},
|
|
3385
3831
|
{
|
|
3386
3832
|
path: "features/shared/errors.ts",
|
|
3387
|
-
content: files.
|
|
3833
|
+
content: files.productionSharedErrors,
|
|
3388
3834
|
},
|
|
3389
3835
|
{
|
|
3390
3836
|
path: "server/index.ts",
|
|
@@ -3392,13 +3838,22 @@ function getProductionTemplateFiles(ctx) {
|
|
|
3392
3838
|
? files.productionServerWithDrizzleTurso
|
|
3393
3839
|
: files.productionServer,
|
|
3394
3840
|
},
|
|
3841
|
+
{ path: "server/context.ts", content: files.productionServerContext },
|
|
3395
3842
|
{ path: "server/routes.ts", content: files.productionServerRoutes },
|
|
3396
3843
|
{ path: "server/outbox.ts", content: files.productionOutbox },
|
|
3844
|
+
{ path: "server/schedules.ts", content: files.productionSchedules },
|
|
3397
3845
|
{
|
|
3398
3846
|
path: "server/providers.ts",
|
|
3399
3847
|
content: productionServerProviders(ctx),
|
|
3400
3848
|
},
|
|
3401
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
|
+
},
|
|
3402
3857
|
{
|
|
3403
3858
|
path: "features/todos/use-cases/create-todo.ts",
|
|
3404
3859
|
content: files.productionCreateTodoUseCase,
|
|
@@ -3416,7 +3871,7 @@ function getProductionTemplateFiles(ctx) {
|
|
|
3416
3871
|
content: files.productionUseCasesIndex,
|
|
3417
3872
|
},
|
|
3418
3873
|
{
|
|
3419
|
-
path: "features/todos/
|
|
3874
|
+
path: "features/todos/schemas.ts",
|
|
3420
3875
|
content: files.productionTodoSchemas,
|
|
3421
3876
|
},
|
|
3422
3877
|
{
|
|
@@ -3438,6 +3893,9 @@ function getProductionTemplateFiles(ctx) {
|
|
|
3438
3893
|
}, {
|
|
3439
3894
|
path: "infra/db/reset.ts",
|
|
3440
3895
|
content: files.productionDbReset,
|
|
3896
|
+
}, {
|
|
3897
|
+
path: "infra/db/test-database.ts",
|
|
3898
|
+
content: files.productionDbTestDatabase,
|
|
3441
3899
|
}, {
|
|
3442
3900
|
path: "infra/db/provider.ts",
|
|
3443
3901
|
content: files.productionDbProvider,
|
|
@@ -3478,21 +3936,29 @@ export function getTemplateFiles(ctx) {
|
|
|
3478
3936
|
{ path: "app/layout.tsx", content: layout(ctx) },
|
|
3479
3937
|
{ path: "app/page.tsx", content: page(ctx) },
|
|
3480
3938
|
{ path: "app/api/[[...path]]/route.ts", content: files.apiCatchAllRoute },
|
|
3939
|
+
{ path: "app-context.ts", content: minimalAppContext(ctx) },
|
|
3481
3940
|
{ path: "features/todos/contracts.ts", content: files.contractsTodos },
|
|
3482
3941
|
{ path: "features/todos/routes.ts", content: files.todoRoutes },
|
|
3483
|
-
{ path: "ports/index.ts", content:
|
|
3942
|
+
{ path: "ports/index.ts", content: minimalPorts(ctx) },
|
|
3484
3943
|
{ path: "server/index.ts", content: server(ctx) },
|
|
3944
|
+
{ path: "server/context.ts", content: serverContext(ctx) },
|
|
3485
3945
|
{ path: "server/routes.ts", content: files.serverRoutes },
|
|
3486
|
-
{ path: "features/todos/use-cases.ts", content: files.useCasesTodos },
|
|
3946
|
+
{ path: "features/todos/use-cases/index.ts", content: files.useCasesTodos },
|
|
3487
3947
|
];
|
|
3488
3948
|
if (hasFeature(ctx, "client")) {
|
|
3489
3949
|
templateFiles.push({
|
|
3490
|
-
path: "client/
|
|
3491
|
-
content:
|
|
3950
|
+
path: "client/index.ts",
|
|
3951
|
+
content: clientIndex(ctx),
|
|
3492
3952
|
});
|
|
3493
3953
|
}
|
|
3494
3954
|
if (hasFeature(ctx, "react-query")) {
|
|
3495
|
-
templateFiles.push({ path: "app/providers.tsx", content: files.appProviders }, { path: "
|
|
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
|
+
});
|
|
3496
3962
|
}
|
|
3497
3963
|
if (hasFeature(ctx, "openapi")) {
|
|
3498
3964
|
templateFiles.push({
|
|
@@ -3500,14 +3966,23 @@ export function getTemplateFiles(ctx) {
|
|
|
3500
3966
|
content: files.apiOpenApiRoute,
|
|
3501
3967
|
});
|
|
3502
3968
|
}
|
|
3503
|
-
if (ctx
|
|
3504
|
-
templateFiles.push({
|
|
3505
|
-
path: "
|
|
3506
|
-
content:
|
|
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,
|
|
3507
3979
|
});
|
|
3508
3980
|
}
|
|
3509
3981
|
if (ctx.integrations.length > 0) {
|
|
3510
3982
|
templateFiles.push({
|
|
3983
|
+
path: "server/providers.ts",
|
|
3984
|
+
content: serverProviders(ctx),
|
|
3985
|
+
}, {
|
|
3511
3986
|
path: ".env.example",
|
|
3512
3987
|
content: envExample(ctx),
|
|
3513
3988
|
}, {
|