@beignet/cli 0.0.22 → 0.0.24
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 +26 -0
- package/README.md +35 -5
- package/dist/choices.d.ts +8 -0
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +4 -0
- package/dist/choices.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +184 -0
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/make.d.ts +4 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +349 -45
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +6 -2
- package/dist/mcp.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +9 -5
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +4 -2
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/mysql.js +2 -2
- package/dist/templates/db/postgres.js +2 -2
- package/dist/templates/db/sqlite.js +2 -2
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +1 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts +1 -0
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +90 -20
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shell.d.ts.map +1 -1
- package/dist/templates/shell.js +60 -29
- package/dist/templates/shell.js.map +1 -1
- package/dist/templates/todos.js +5 -5
- package/package.json +2 -2
- package/src/choices.ts +10 -0
- package/src/index.ts +11 -0
- package/src/inspect.ts +319 -0
- package/src/lib.ts +2 -1
- package/src/make.ts +449 -53
- package/src/mcp.ts +7 -1
- package/src/templates/agents.ts +9 -5
- package/src/templates/base.ts +4 -2
- package/src/templates/db/mysql.ts +2 -2
- package/src/templates/db/postgres.ts +2 -2
- package/src/templates/db/sqlite.ts +2 -2
- package/src/templates/index.ts +1 -0
- package/src/templates/server.ts +90 -20
- package/src/templates/shell.ts +61 -29
- package/src/templates/todos.ts +5 -5
package/src/mcp.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
makeFactory,
|
|
13
13
|
makeFeature,
|
|
14
14
|
makeFeatureAddonChoices,
|
|
15
|
+
makeFeatureRecipeChoices,
|
|
15
16
|
makeJob,
|
|
16
17
|
makeListener,
|
|
17
18
|
makeNotification,
|
|
@@ -76,6 +77,10 @@ const makeInputSchema = {
|
|
|
76
77
|
.describe(
|
|
77
78
|
"feature only: add feature-owned artifacts such as policy, factories, seeds, events, listeners, jobs, notifications, schedules, tasks, ui, and uploads.",
|
|
78
79
|
),
|
|
80
|
+
recipe: z
|
|
81
|
+
.enum(makeFeatureRecipeChoices)
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("feature only: generate a named recipe such as full-slice."),
|
|
79
84
|
event: z
|
|
80
85
|
.string()
|
|
81
86
|
.optional()
|
|
@@ -132,6 +137,7 @@ type MakeToolInput = {
|
|
|
132
137
|
artifact: MakeArtifact;
|
|
133
138
|
name?: string;
|
|
134
139
|
with?: Array<(typeof makeFeatureAddonChoices)[number]>;
|
|
140
|
+
recipe?: (typeof makeFeatureRecipeChoices)[number];
|
|
135
141
|
event?: string;
|
|
136
142
|
cron?: string;
|
|
137
143
|
timezone?: string;
|
|
@@ -216,7 +222,7 @@ async function runMakeTool(
|
|
|
216
222
|
case "factory":
|
|
217
223
|
return makeFactory(base);
|
|
218
224
|
case "feature":
|
|
219
|
-
return makeFeature({ ...base, with: input.with });
|
|
225
|
+
return makeFeature({ ...base, with: input.with, recipe: input.recipe });
|
|
220
226
|
case "job":
|
|
221
227
|
return makeJob(base);
|
|
222
228
|
case "listener":
|
package/src/templates/agents.ts
CHANGED
|
@@ -52,8 +52,9 @@ is a silent failure — the file exists but never runs:
|
|
|
52
52
|
|
|
53
53
|
The starter ships no workflow registries — generators create them on first
|
|
54
54
|
use, so their absence is fine. \`${cli} make event\`, \`${cli} make job\`,
|
|
55
|
-
\`${cli} make
|
|
56
|
-
\`${cli} make upload\` create or update their
|
|
55
|
+
\`${cli} make listener\`, \`${cli} make schedule\`, \`${cli} make task\`,
|
|
56
|
+
\`${cli} make seed\`, and \`${cli} make upload\` create or update their
|
|
57
|
+
required app entrypoints.
|
|
57
58
|
\`${cli} doctor\` detects registration drift. \`${cli} doctor --fix\` repairs
|
|
58
59
|
route-group, schedule, task, and outbox registration; listener drift is
|
|
59
60
|
report-only and must be fixed by hand.
|
|
@@ -62,8 +63,10 @@ report-only and must be fixed by hand.
|
|
|
62
63
|
|
|
63
64
|
\`${cli} make <artifact> <name>\` creates correctly placed, pre-registered
|
|
64
65
|
files — prefer it over hand-writing them. See \`${cli} make --help\` for the
|
|
65
|
-
artifact list.
|
|
66
|
-
|
|
66
|
+
artifact list. Use \`${cli} make feature <name> --recipe full-slice\` when you
|
|
67
|
+
need a richer reference slice with policy, client helpers, workflow artifacts,
|
|
68
|
+
events, listener registration, jobs, and outbox wiring. After changing the Drizzle schema in
|
|
69
|
+
\`infra/db/schema/\`, run \`${cli} db generate\` then \`${cli} db migrate\`.
|
|
67
70
|
|
|
68
71
|
## Validation loop
|
|
69
72
|
|
|
@@ -98,7 +101,8 @@ skill-loading block.
|
|
|
98
101
|
|
|
99
102
|
- Feature artifacts live under \`features/<feature>/\`, with tests in
|
|
100
103
|
\`features/<feature>/tests/\` (not \`__tests__/\`).
|
|
101
|
-
- Feature-specific client data-fetching helpers
|
|
104
|
+
- Feature-specific client data-fetching helpers, query options, mutation
|
|
105
|
+
options, invalidation helpers, and hooks live in
|
|
102
106
|
\`features/<feature>/client/\`; shared client setup stays in root \`client/\`.
|
|
103
107
|
- Domain and use-case code must not import infra, providers, or React.
|
|
104
108
|
- Routes must not import concrete infra.
|
package/src/templates/base.ts
CHANGED
|
@@ -273,6 +273,7 @@ ${cli} doctor
|
|
|
273
273
|
\`\`\`
|
|
274
274
|
|
|
275
275
|
\`make feature\` creates a contract-to-test vertical slice with Drizzle schema and repository files, so regenerate and migrate the database before running the app against the new feature.
|
|
276
|
+
Use \`${cli} make feature projects --recipe full-slice\` when you want a richer reference slice with policy, feature client helpers, workflow artifacts, events, listener registration, jobs, and outbox wiring.
|
|
276
277
|
|
|
277
278
|
## App map
|
|
278
279
|
|
|
@@ -418,7 +419,7 @@ export function envExample(ctx: TemplateContext): string {
|
|
|
418
419
|
"# Copy to .env.local and fill in values for your app.",
|
|
419
420
|
"APP_URL=http://localhost:3000",
|
|
420
421
|
"",
|
|
421
|
-
"# Dev-only default; remove in production.",
|
|
422
|
+
"# Dev-only default; remove or protect in production.",
|
|
422
423
|
"DEVTOOLS_ENABLED=true",
|
|
423
424
|
"",
|
|
424
425
|
"LOG_LEVEL=info",
|
|
@@ -427,7 +428,8 @@ export function envExample(ctx: TemplateContext): string {
|
|
|
427
428
|
"",
|
|
428
429
|
...databaseLines,
|
|
429
430
|
"",
|
|
430
|
-
"
|
|
431
|
+
"# Optional for local development; required in production.",
|
|
432
|
+
"# BETTER_AUTH_SECRET=replace-with-a-random-32-character-secret",
|
|
431
433
|
"BETTER_AUTH_URL=http://localhost:3000",
|
|
432
434
|
"# BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000",
|
|
433
435
|
"",
|
|
@@ -115,7 +115,7 @@ export const todos = mysqlTable(
|
|
|
115
115
|
}),
|
|
116
116
|
);
|
|
117
117
|
`,
|
|
118
|
-
drizzleTodoRepository: `import
|
|
118
|
+
drizzleTodoRepository: `import "@beignet/core/server-only";
|
|
119
119
|
import { offsetPageResult } from "@beignet/core/pagination";
|
|
120
120
|
import type { DrizzleMysqlDatabase } from "@beignet/provider-db-drizzle/mysql";
|
|
121
121
|
import { count, desc, eq } from "drizzle-orm";
|
|
@@ -255,7 +255,7 @@ async function prepare(pool: Pool): Promise<void> {
|
|
|
255
255
|
await migrate(drizzle(pool), { migrationsFolder: "drizzle" });
|
|
256
256
|
}
|
|
257
257
|
`,
|
|
258
|
-
dbProvider: `import
|
|
258
|
+
dbProvider: `import "@beignet/core/server-only";
|
|
259
259
|
import { createProvider } from "@beignet/core/providers";
|
|
260
260
|
import {
|
|
261
261
|
createDrizzleMysqlIdempotencyPort,
|
|
@@ -105,7 +105,7 @@ export const todos = pgTable(
|
|
|
105
105
|
}),
|
|
106
106
|
);
|
|
107
107
|
`,
|
|
108
|
-
drizzleTodoRepository: `import
|
|
108
|
+
drizzleTodoRepository: `import "@beignet/core/server-only";
|
|
109
109
|
import { offsetPageResult } from "@beignet/core/pagination";
|
|
110
110
|
import type { DrizzlePostgresDatabase } from "@beignet/provider-db-drizzle/postgres";
|
|
111
111
|
import { count, desc, eq } from "drizzle-orm";
|
|
@@ -243,7 +243,7 @@ async function prepare(pool: Pool): Promise<void> {
|
|
|
243
243
|
await migrate(drizzle(pool), { migrationsFolder: "drizzle" });
|
|
244
244
|
}
|
|
245
245
|
`,
|
|
246
|
-
dbProvider: `import
|
|
246
|
+
dbProvider: `import "@beignet/core/server-only";
|
|
247
247
|
import { createProvider } from "@beignet/core/providers";
|
|
248
248
|
import {
|
|
249
249
|
createDrizzlePostgresIdempotencyPort,
|
|
@@ -101,7 +101,7 @@ export const todos = sqliteTable(
|
|
|
101
101
|
}),
|
|
102
102
|
);
|
|
103
103
|
`,
|
|
104
|
-
drizzleTodoRepository: `import
|
|
104
|
+
drizzleTodoRepository: `import "@beignet/core/server-only";
|
|
105
105
|
import { offsetPageResult } from "@beignet/core/pagination";
|
|
106
106
|
import type { DrizzleSqliteDatabase } from "@beignet/provider-db-drizzle/sqlite";
|
|
107
107
|
import { count, desc, eq } from "drizzle-orm";
|
|
@@ -242,7 +242,7 @@ async function prepare(client: Client): Promise<void> {
|
|
|
242
242
|
await migrate(drizzle(client), { migrationsFolder: "drizzle" });
|
|
243
243
|
}
|
|
244
244
|
`,
|
|
245
|
-
dbProvider: `import
|
|
245
|
+
dbProvider: `import "@beignet/core/server-only";
|
|
246
246
|
import { createProvider } from "@beignet/core/providers";
|
|
247
247
|
import {
|
|
248
248
|
createDrizzleSqliteIdempotencyPort,
|
package/src/templates/index.ts
CHANGED
|
@@ -132,6 +132,7 @@ export function getTemplateFiles(ctx: TemplateContext): TemplateFile[] {
|
|
|
132
132
|
{ path: "lib/env.ts", content: env(ctx) },
|
|
133
133
|
{ path: "lib/auth.ts", content: serverFiles.authHelpers },
|
|
134
134
|
{ path: "lib/better-auth.ts", content: betterAuth(ctx) },
|
|
135
|
+
{ path: "lib/tenant.ts", content: serverFiles.tenantHelper },
|
|
135
136
|
{ path: "lib/use-case.ts", content: serverFiles.useCaseBuilder },
|
|
136
137
|
{ path: "server/index.ts", content: server(ctx) },
|
|
137
138
|
{ path: "server/context.ts", content: serverFiles.serverContext },
|
package/src/templates/server.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
export function serverProviders(ctx: TemplateContext): string {
|
|
9
9
|
const db = databaseDescriptor(ctx);
|
|
10
10
|
const imports = [
|
|
11
|
-
'import
|
|
11
|
+
'import "@beignet/core/server-only";',
|
|
12
12
|
'import { createDevtoolsProvider } from "@beignet/devtools";',
|
|
13
13
|
'import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";',
|
|
14
14
|
`import { ${db.providerFactory} } from "@beignet/provider-db-drizzle/${db.subpath}";`,
|
|
@@ -155,6 +155,19 @@ export function env(ctx: TemplateContext): string {
|
|
|
155
155
|
import { z } from "zod";
|
|
156
156
|
|
|
157
157
|
const BooleanEnv = z.enum(["true", "false"]).transform((value) => value === "true");
|
|
158
|
+
const LOCAL_BETTER_AUTH_SECRET = "local-dev-better-auth-secret-change-me";
|
|
159
|
+
const isProductionRuntime =
|
|
160
|
+
process.env.NODE_ENV === "production" &&
|
|
161
|
+
process.env.NEXT_PHASE !== "phase-production-build";
|
|
162
|
+
const BetterAuthSecret = z.string().min(32).superRefine((value, ctx) => {
|
|
163
|
+
if (isProductionRuntime && value === LOCAL_BETTER_AUTH_SECRET) {
|
|
164
|
+
ctx.addIssue({
|
|
165
|
+
code: "custom",
|
|
166
|
+
message:
|
|
167
|
+
"BETTER_AUTH_SECRET must be unique in production. Generate a strong secret instead of using the local development placeholder.",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
158
171
|
|
|
159
172
|
export const env = createEnv({
|
|
160
173
|
server: {
|
|
@@ -163,10 +176,9 @@ export const env = createEnv({
|
|
|
163
176
|
.default("development"),
|
|
164
177
|
APP_URL: z.string().url().default("http://localhost:3000"),
|
|
165
178
|
DEVTOOLS_ENABLED: BooleanEnv.optional(),
|
|
166
|
-
BETTER_AUTH_SECRET:
|
|
167
|
-
|
|
168
|
-
.
|
|
169
|
-
.default("local-dev-better-auth-secret-change-me"),
|
|
179
|
+
BETTER_AUTH_SECRET: isProductionRuntime
|
|
180
|
+
? BetterAuthSecret
|
|
181
|
+
: BetterAuthSecret.default(LOCAL_BETTER_AUTH_SECRET),
|
|
170
182
|
BETTER_AUTH_URL: z.string().url().default("http://localhost:3000"),
|
|
171
183
|
BETTER_AUTH_TRUSTED_ORIGINS: z.string().optional(),
|
|
172
184
|
${databaseEnvLines}
|
|
@@ -283,7 +295,7 @@ export function server(ctx: TemplateContext): string {
|
|
|
283
295
|
\t\t\tDB_URL: env.${db.urlEnvVar},
|
|
284
296
|
\t\t},`;
|
|
285
297
|
|
|
286
|
-
return `import
|
|
298
|
+
return `import "@beignet/core/server-only";
|
|
287
299
|
import { createNextServer } from "@beignet/next";
|
|
288
300
|
import { createErrorReportingHooks, createIdempotencyHooks } from "@beignet/core/server";
|
|
289
301
|
import type { AppContext } from "@/app-context";
|
|
@@ -369,7 +381,7 @@ export const errors = defineErrors({
|
|
|
369
381
|
|
|
370
382
|
export const appError = createAppError(errors);
|
|
371
383
|
`,
|
|
372
|
-
useCaseBuilder: `import
|
|
384
|
+
useCaseBuilder: `import "@beignet/core/server-only";
|
|
373
385
|
import { createUseCase } from "@beignet/core/application";
|
|
374
386
|
import type { AppContext } from "@/app-context";
|
|
375
387
|
|
|
@@ -403,10 +415,14 @@ export type AuthUser = {
|
|
|
403
415
|
id: string;
|
|
404
416
|
email?: string;
|
|
405
417
|
name?: string;
|
|
418
|
+
tenantId?: string;
|
|
419
|
+
organizationId?: string;
|
|
406
420
|
};
|
|
407
421
|
|
|
408
422
|
export type AuthSessionMetadata = {
|
|
409
423
|
id?: string;
|
|
424
|
+
tenantId?: string;
|
|
425
|
+
organizationId?: string;
|
|
410
426
|
};
|
|
411
427
|
|
|
412
428
|
export type AuthSession = BeignetAuthSession<
|
|
@@ -420,7 +436,7 @@ export type AuthPort = BeignetAuthPort<
|
|
|
420
436
|
AuthRequest
|
|
421
437
|
>;
|
|
422
438
|
`,
|
|
423
|
-
serverRoutes: `import
|
|
439
|
+
serverRoutes: `import "@beignet/core/server-only";
|
|
424
440
|
import { contractsFromRoutes, defineRoutes } from "@beignet/next";
|
|
425
441
|
import type { AppContext } from "@/app-context";
|
|
426
442
|
import { todoRoutes } from "@/features/todos/routes";
|
|
@@ -430,17 +446,17 @@ export const routes = defineRoutes<AppContext>([
|
|
|
430
446
|
]);
|
|
431
447
|
export const contracts = contractsFromRoutes(routes);
|
|
432
448
|
`,
|
|
433
|
-
serverContext: `import
|
|
449
|
+
serverContext: `import "@beignet/core/server-only";
|
|
434
450
|
import {
|
|
435
451
|
type ActivityActor,
|
|
436
452
|
createAnonymousActor,
|
|
437
453
|
createServiceActor,
|
|
438
|
-
createTenant,
|
|
439
454
|
createUserActor,
|
|
440
455
|
} from "@beignet/core/ports";
|
|
441
456
|
import { defineServerContext } from "@beignet/core/server";
|
|
442
457
|
import type { TraceContext } from "@beignet/core/tracing";
|
|
443
458
|
import type { AppContext, AppRuntimePorts } from "@/app-context";
|
|
459
|
+
import { resolveRequestTenant, resolveServiceTenant } from "@/lib/tenant";
|
|
444
460
|
|
|
445
461
|
export type AppServiceContextInput =
|
|
446
462
|
| {
|
|
@@ -456,7 +472,7 @@ export const appContext = defineServerContext<AppContext, AppRuntimePorts>()({
|
|
|
456
472
|
gate: (ports) => ports.gate,
|
|
457
473
|
request: async ({ req, ports, requestId, trace }) => {
|
|
458
474
|
const auth = await ports.auth.getSession(req);
|
|
459
|
-
const
|
|
475
|
+
const tenant = resolveRequestTenant({ auth });
|
|
460
476
|
|
|
461
477
|
return {
|
|
462
478
|
requestId,
|
|
@@ -466,7 +482,7 @@ export const appContext = defineServerContext<AppContext, AppRuntimePorts>()({
|
|
|
466
482
|
auth,
|
|
467
483
|
...trace,
|
|
468
484
|
ports,
|
|
469
|
-
tenant
|
|
485
|
+
...(tenant ? { tenant } : {}),
|
|
470
486
|
};
|
|
471
487
|
},
|
|
472
488
|
service: ({
|
|
@@ -479,15 +495,67 @@ export const appContext = defineServerContext<AppContext, AppRuntimePorts>()({
|
|
|
479
495
|
input: AppServiceContextInput;
|
|
480
496
|
requestId: string;
|
|
481
497
|
trace: TraceContext;
|
|
482
|
-
}) =>
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
498
|
+
}) => {
|
|
499
|
+
const tenant = resolveServiceTenant(input?.tenantId);
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
requestId,
|
|
503
|
+
actor: input?.actor ?? createServiceActor("beignet-service"),
|
|
504
|
+
auth: null,
|
|
505
|
+
...trace,
|
|
506
|
+
ports,
|
|
507
|
+
...(tenant ? { tenant } : {}),
|
|
508
|
+
};
|
|
509
|
+
},
|
|
490
510
|
});
|
|
511
|
+
`,
|
|
512
|
+
tenantHelper: `import { type ActivityTenant, createTenant } from "@beignet/core/ports";
|
|
513
|
+
import type { AuthSession } from "@/ports/auth";
|
|
514
|
+
|
|
515
|
+
export type TenantResolutionInput = {
|
|
516
|
+
auth: AuthSession | null;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export function resolveRequestTenant({
|
|
520
|
+
auth,
|
|
521
|
+
}: TenantResolutionInput): ActivityTenant | undefined {
|
|
522
|
+
const tenantId = auth ? tenantIdFromAuth(auth) : undefined;
|
|
523
|
+
|
|
524
|
+
return tenantId ? createTenant(tenantId) : undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function resolveServiceTenant(
|
|
528
|
+
tenantId: string | undefined,
|
|
529
|
+
): ActivityTenant | undefined {
|
|
530
|
+
const normalizedTenantId = normalizeTenantId(tenantId);
|
|
531
|
+
|
|
532
|
+
return normalizedTenantId ? createTenant(normalizedTenantId) : undefined;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function tenantIdFromAuth(auth: AuthSession) {
|
|
536
|
+
return (
|
|
537
|
+
stringProperty(auth.session, "tenantId") ??
|
|
538
|
+
stringProperty(auth.session, "organizationId") ??
|
|
539
|
+
stringProperty(auth.user, "tenantId") ??
|
|
540
|
+
stringProperty(auth.user, "organizationId")
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function stringProperty(value: unknown, key: string) {
|
|
545
|
+
if (!isRecord(value)) return undefined;
|
|
546
|
+
|
|
547
|
+
return normalizeTenantId(value[key]);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function normalizeTenantId(value: unknown) {
|
|
551
|
+
return typeof value === "string" && value.trim().length > 0
|
|
552
|
+
? value.trim()
|
|
553
|
+
: undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
557
|
+
return typeof value === "object" && value !== null;
|
|
558
|
+
}
|
|
491
559
|
`,
|
|
492
560
|
apiCatchAllRoute: `import { server } from "@/server";
|
|
493
561
|
|
|
@@ -553,11 +621,13 @@ export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
|
|
|
553
621
|
});
|
|
554
622
|
`,
|
|
555
623
|
openApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
|
|
624
|
+
import { env } from "@/lib/env";
|
|
556
625
|
import { server } from "@/server";
|
|
557
626
|
|
|
558
627
|
export const GET = createOpenAPIHandler(server.contracts, {
|
|
559
628
|
title: "Beignet starter API",
|
|
560
629
|
version: "0.1.0",
|
|
630
|
+
servers: [{ url: env.APP_URL }],
|
|
561
631
|
});
|
|
562
632
|
`,
|
|
563
633
|
authRoute: `import { toNextJsHandler } from "better-auth/next-js";
|
package/src/templates/shell.ts
CHANGED
|
@@ -1035,12 +1035,41 @@ export function SettingsNav() {
|
|
|
1035
1035
|
}
|
|
1036
1036
|
`;
|
|
1037
1037
|
|
|
1038
|
+
const todoClientQueries = `import type { QueryClient } from "@tanstack/react-query";
|
|
1039
|
+
import { rq } from "@/client";
|
|
1040
|
+
import {
|
|
1041
|
+
createTodo,
|
|
1042
|
+
deleteTodo,
|
|
1043
|
+
listTodos,
|
|
1044
|
+
updateTodo,
|
|
1045
|
+
} from "@/features/todos/contracts";
|
|
1046
|
+
|
|
1047
|
+
export function listTodosQueryOptions() {
|
|
1048
|
+
return rq(listTodos).queryOptions();
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export function createTodoMutationOptions() {
|
|
1052
|
+
return rq(createTodo).mutationOptions();
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export function updateTodoMutationOptions() {
|
|
1056
|
+
return rq(updateTodo).mutationOptions();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export function deleteTodoMutationOptions() {
|
|
1060
|
+
return rq(deleteTodo).mutationOptions();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
export function invalidateTodos(queryClient: QueryClient) {
|
|
1064
|
+
return rq(listTodos).invalidate(queryClient);
|
|
1065
|
+
}
|
|
1066
|
+
`;
|
|
1067
|
+
|
|
1038
1068
|
const todoApp = `"use client";
|
|
1039
1069
|
|
|
1040
1070
|
import { rootFormError } from "@beignet/react-hook-form";
|
|
1041
1071
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
1042
1072
|
import { Trash2Icon } from "lucide-react";
|
|
1043
|
-
import { rq } from "@/client";
|
|
1044
1073
|
import { rhf } from "@/client/forms";
|
|
1045
1074
|
import { Button } from "@/components/ui/button";
|
|
1046
1075
|
import { Card, CardContent } from "@/components/ui/card";
|
|
@@ -1048,45 +1077,44 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|
|
1048
1077
|
import { Input } from "@/components/ui/input";
|
|
1049
1078
|
import { Label } from "@/components/ui/label";
|
|
1050
1079
|
import {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1080
|
+
createTodoMutationOptions,
|
|
1081
|
+
deleteTodoMutationOptions,
|
|
1082
|
+
invalidateTodos,
|
|
1083
|
+
listTodosQueryOptions,
|
|
1084
|
+
updateTodoMutationOptions,
|
|
1085
|
+
} from "@/features/todos/client/queries";
|
|
1086
|
+
import { createTodo } from "@/features/todos/contracts";
|
|
1056
1087
|
import { cn } from "@/lib/utils";
|
|
1057
1088
|
|
|
1058
1089
|
const createTodoForm = rhf(createTodo);
|
|
1059
1090
|
|
|
1060
1091
|
export function TodoApp() {
|
|
1061
1092
|
const queryClient = useQueryClient();
|
|
1062
|
-
const todosQuery = useQuery(
|
|
1093
|
+
const todosQuery = useQuery(listTodosQueryOptions());
|
|
1063
1094
|
const form = createTodoForm.useForm({
|
|
1064
1095
|
defaultValues: { title: "" },
|
|
1065
1096
|
});
|
|
1066
1097
|
|
|
1067
|
-
const
|
|
1098
|
+
const invalidateTodoQueries = () => invalidateTodos(queryClient);
|
|
1068
1099
|
|
|
1069
|
-
const createTodoMutation = useMutation(
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
onSuccess: invalidateTodos,
|
|
1088
|
-
}),
|
|
1089
|
-
);
|
|
1100
|
+
const createTodoMutation = useMutation({
|
|
1101
|
+
...createTodoMutationOptions(),
|
|
1102
|
+
onSuccess: async () => {
|
|
1103
|
+
form.reset();
|
|
1104
|
+
await invalidateTodoQueries();
|
|
1105
|
+
},
|
|
1106
|
+
onError: (error) => {
|
|
1107
|
+
form.setError("root", rootFormError(error, "Could not create the todo."));
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
const updateTodoMutation = useMutation({
|
|
1111
|
+
...updateTodoMutationOptions(),
|
|
1112
|
+
onSuccess: invalidateTodoQueries,
|
|
1113
|
+
});
|
|
1114
|
+
const deleteTodoMutation = useMutation({
|
|
1115
|
+
...deleteTodoMutationOptions(),
|
|
1116
|
+
onSuccess: invalidateTodoQueries,
|
|
1117
|
+
});
|
|
1090
1118
|
|
|
1091
1119
|
const onSubmit = form.handleSubmit((body) => {
|
|
1092
1120
|
form.clearErrors("root");
|
|
@@ -1215,6 +1243,10 @@ export function shellTemplateFiles(ctx: TemplateContext): TemplateFile[] {
|
|
|
1215
1243
|
{ path: "components/theme-toggle.tsx", content: themeToggle },
|
|
1216
1244
|
{ path: "components/app-sidebar.tsx", content: appSidebar },
|
|
1217
1245
|
{ path: "components/settings-nav.tsx", content: settingsNav },
|
|
1246
|
+
{
|
|
1247
|
+
path: "features/todos/client/queries.ts",
|
|
1248
|
+
content: todoClientQueries,
|
|
1249
|
+
},
|
|
1218
1250
|
{
|
|
1219
1251
|
path: "features/todos/components/todo-app.tsx",
|
|
1220
1252
|
content: todoApp,
|
package/src/templates/todos.ts
CHANGED
|
@@ -174,7 +174,7 @@ export const todoPolicy = definePolicy({
|
|
|
174
174
|
authorizeOwner(ctx, todo, "delete"),
|
|
175
175
|
});
|
|
176
176
|
`,
|
|
177
|
-
routes: `import
|
|
177
|
+
routes: `import "@beignet/core/server-only";
|
|
178
178
|
import { defineRouteGroup } from "@beignet/next";
|
|
179
179
|
import type { AppContext } from "@/app-context";
|
|
180
180
|
import {
|
|
@@ -200,7 +200,7 @@ export const todoRoutes = defineRouteGroup<AppContext>()({
|
|
|
200
200
|
],
|
|
201
201
|
});
|
|
202
202
|
`,
|
|
203
|
-
listTodosUseCase: `import
|
|
203
|
+
listTodosUseCase: `import "@beignet/core/server-only";
|
|
204
204
|
import { normalizeOffsetPage } from "@beignet/core/pagination";
|
|
205
205
|
import { requireUser } from "@/lib/auth";
|
|
206
206
|
import { useCase } from "@/lib/use-case";
|
|
@@ -220,7 +220,7 @@ export const listTodosUseCase = useCase
|
|
|
220
220
|
return ctx.ports.todos.list(user.id, page);
|
|
221
221
|
});
|
|
222
222
|
`,
|
|
223
|
-
createTodoUseCase: `import
|
|
223
|
+
createTodoUseCase: `import "@beignet/core/server-only";
|
|
224
224
|
import { requireUser } from "@/lib/auth";
|
|
225
225
|
import { useCase } from "@/lib/use-case";
|
|
226
226
|
import { CreateTodoInputSchema, TodoSchema } from "../schemas";
|
|
@@ -238,7 +238,7 @@ export const createTodoUseCase = useCase
|
|
|
238
238
|
);
|
|
239
239
|
});
|
|
240
240
|
`,
|
|
241
|
-
updateTodoUseCase: `import
|
|
241
|
+
updateTodoUseCase: `import "@beignet/core/server-only";
|
|
242
242
|
import { appError } from "@/features/shared/errors";
|
|
243
243
|
import { requireUser } from "@/lib/auth";
|
|
244
244
|
import { useCase } from "@/lib/use-case";
|
|
@@ -263,7 +263,7 @@ export const updateTodoUseCase = useCase
|
|
|
263
263
|
});
|
|
264
264
|
});
|
|
265
265
|
`,
|
|
266
|
-
deleteTodoUseCase: `import
|
|
266
|
+
deleteTodoUseCase: `import "@beignet/core/server-only";
|
|
267
267
|
import { appError } from "@/features/shared/errors";
|
|
268
268
|
import { requireUser } from "@/lib/auth";
|
|
269
269
|
import { useCase } from "@/lib/use-case";
|