@beignet/cli 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/choices.d.ts +5 -28
  3. package/dist/choices.d.ts.map +1 -1
  4. package/dist/choices.js +0 -35
  5. package/dist/choices.js.map +1 -1
  6. package/dist/create-prompts.d.ts +5 -6
  7. package/dist/create-prompts.d.ts.map +1 -1
  8. package/dist/create-prompts.js +19 -77
  9. package/dist/create-prompts.js.map +1 -1
  10. package/dist/create.d.ts +6 -4
  11. package/dist/create.d.ts.map +1 -1
  12. package/dist/create.js +15 -24
  13. package/dist/create.js.map +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +32 -39
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib.d.ts +1 -1
  18. package/dist/lib.d.ts.map +1 -1
  19. package/dist/make.d.ts.map +1 -1
  20. package/dist/make.js +153 -1
  21. package/dist/make.js.map +1 -1
  22. package/dist/templates/base.d.ts +13 -0
  23. package/dist/templates/base.d.ts.map +1 -0
  24. package/dist/templates/base.js +300 -0
  25. package/dist/templates/base.js.map +1 -0
  26. package/dist/templates/db.d.ts +15 -0
  27. package/dist/templates/db.d.ts.map +1 -0
  28. package/dist/templates/db.js +1001 -0
  29. package/dist/templates/db.js.map +1 -0
  30. package/dist/templates/index.d.ts +14 -0
  31. package/dist/templates/index.d.ts.map +1 -0
  32. package/dist/templates/index.js +124 -0
  33. package/dist/templates/index.js.map +1 -0
  34. package/dist/templates/server.d.ts +26 -0
  35. package/dist/templates/server.d.ts.map +1 -0
  36. package/dist/templates/server.js +525 -0
  37. package/dist/templates/server.js.map +1 -0
  38. package/dist/templates/shadcn.d.ts +5 -0
  39. package/dist/templates/shadcn.d.ts.map +1 -0
  40. package/dist/templates/shadcn.js +555 -0
  41. package/dist/templates/shadcn.js.map +1 -0
  42. package/dist/templates/shared.d.ts +49 -0
  43. package/dist/templates/shared.d.ts.map +1 -0
  44. package/dist/templates/shared.js +57 -0
  45. package/dist/templates/shared.js.map +1 -0
  46. package/dist/templates/shell.d.ts +5 -0
  47. package/dist/templates/shell.d.ts.map +1 -0
  48. package/dist/templates/shell.js +1198 -0
  49. package/dist/templates/shell.js.map +1 -0
  50. package/dist/templates/todos.d.ts +17 -0
  51. package/dist/templates/todos.d.ts.map +1 -0
  52. package/dist/templates/todos.js +607 -0
  53. package/dist/templates/todos.js.map +1 -0
  54. package/package.json +2 -2
  55. package/src/choices.ts +4 -58
  56. package/src/create-prompts.ts +20 -88
  57. package/src/create.ts +21 -38
  58. package/src/index.ts +39 -45
  59. package/src/lib.ts +1 -1
  60. package/src/make.ts +208 -1
  61. package/src/templates/base.ts +377 -0
  62. package/src/templates/db.ts +1002 -0
  63. package/src/templates/index.ts +149 -0
  64. package/src/templates/server.ts +533 -0
  65. package/src/templates/shadcn.ts +570 -0
  66. package/src/templates/shared.ts +90 -0
  67. package/src/templates/shell.ts +1227 -0
  68. package/src/templates/todos.ts +607 -0
  69. package/dist/templates.d.ts +0 -26
  70. package/dist/templates.d.ts.map +0 -1
  71. package/dist/templates.js +0 -3995
  72. package/dist/templates.js.map +0 -1
  73. package/src/templates.ts +0 -4280
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/templates/shell.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ declare const files: {
2
+ schemas: string;
3
+ contracts: string;
4
+ ports: string;
5
+ policy: string;
6
+ routes: string;
7
+ listTodosUseCase: string;
8
+ createTodoUseCase: string;
9
+ updateTodoUseCase: string;
10
+ deleteTodoUseCase: string;
11
+ useCasesIndex: string;
12
+ testHelpers: string;
13
+ useCaseTest: string;
14
+ routesTest: string;
15
+ };
16
+ export { files as todosFiles };
17
+ //# sourceMappingURL=todos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todos.d.ts","sourceRoot":"","sources":["../../src/templates/todos.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,KAAK;;;;;;;;;;;;;;CA4lBV,CAAC;AAEF,OAAO,EAAE,KAAK,IAAI,UAAU,EAAE,CAAC"}
@@ -0,0 +1,607 @@
1
+ const files = {
2
+ schemas: `import { z } from "zod";
3
+
4
+ export const TodoSchema = z.object({
5
+ id: z.string().uuid(),
6
+ userId: z.string(),
7
+ title: z.string().min(1),
8
+ completed: z.boolean(),
9
+ createdAt: z.string().datetime(),
10
+ });
11
+
12
+ export const ListTodosInputSchema = z.object({
13
+ limit: z.coerce.number().int().min(1).max(100).default(20),
14
+ offset: z.coerce.number().int().min(0).default(0),
15
+ });
16
+
17
+ export const ListTodosOutputSchema = z.object({
18
+ items: z.array(TodoSchema),
19
+ page: z.object({
20
+ kind: z.literal("offset"),
21
+ limit: z.number().int().min(1),
22
+ offset: z.number().int().min(0),
23
+ total: z.number().int().min(0),
24
+ hasMore: z.boolean(),
25
+ }),
26
+ });
27
+
28
+ export const TodoIdInputSchema = z.object({
29
+ id: z.string().uuid(),
30
+ });
31
+
32
+ export const CreateTodoInputSchema = z.object({
33
+ title: z.string().min(1).max(120),
34
+ });
35
+
36
+ export const UpdateTodoBodySchema = z.object({
37
+ completed: z.boolean(),
38
+ });
39
+
40
+ export const UpdateTodoInputSchema = TodoIdInputSchema.merge(
41
+ UpdateTodoBodySchema,
42
+ );
43
+
44
+ export const DeleteTodoOutputSchema = z.void();
45
+
46
+ export type Todo = z.infer<typeof TodoSchema>;
47
+ export type CreateTodoInput = z.infer<typeof CreateTodoInputSchema>;
48
+ export type ListTodosInput = z.infer<typeof ListTodosInputSchema>;
49
+ export type UpdateTodoInput = z.infer<typeof UpdateTodoInputSchema>;
50
+ `,
51
+ contracts: `import { defineContractGroup } from "@beignet/core/contracts";
52
+ import { z } from "zod";
53
+ import { errors } from "@/features/shared/errors";
54
+ import {
55
+ CreateTodoInputSchema,
56
+ ListTodosOutputSchema,
57
+ TodoIdInputSchema,
58
+ TodoSchema,
59
+ UpdateTodoBodySchema,
60
+ } from "@/features/todos/schemas";
61
+
62
+ const ErrorResponseSchema = z.object({
63
+ code: z.string(),
64
+ message: z.string(),
65
+ requestId: z.string().optional(),
66
+ });
67
+
68
+ const todos = defineContractGroup()
69
+ .namespace("todos")
70
+ .errors({ Unauthorized: errors.Unauthorized })
71
+ .responses({
72
+ 500: ErrorResponseSchema,
73
+ });
74
+
75
+ export const listTodos = todos.get("/api/todos").responses({
76
+ 200: ListTodosOutputSchema,
77
+ });
78
+
79
+ export const createTodo = todos
80
+ .post("/api/todos")
81
+ .body(CreateTodoInputSchema)
82
+ .meta({ idempotency: { header: "idempotency-key", scope: "actor" } })
83
+ .errors({ Forbidden: errors.Forbidden })
84
+ .responses({
85
+ 201: TodoSchema,
86
+ });
87
+
88
+ export const updateTodo = todos
89
+ .patch("/api/todos/:id")
90
+ .pathParams(TodoIdInputSchema)
91
+ .body(UpdateTodoBodySchema)
92
+ .errors({
93
+ Forbidden: errors.Forbidden,
94
+ TodoNotFound: errors.TodoNotFound,
95
+ })
96
+ .responses({
97
+ 200: TodoSchema,
98
+ });
99
+
100
+ export const deleteTodo = todos
101
+ .delete("/api/todos/:id")
102
+ .pathParams(TodoIdInputSchema)
103
+ .errors({
104
+ Forbidden: errors.Forbidden,
105
+ TodoNotFound: errors.TodoNotFound,
106
+ })
107
+ .responses({
108
+ 204: null,
109
+ });
110
+ `,
111
+ ports: `import type {
112
+ OffsetPage,
113
+ OffsetPageInfo,
114
+ PageResult,
115
+ } from "@beignet/core/pagination";
116
+ import type { Todo } from "@/features/todos/schemas";
117
+
118
+ export type ListTodosResult = PageResult<Todo, OffsetPageInfo>;
119
+
120
+ export type NewTodo = {
121
+ userId: string;
122
+ title: string;
123
+ };
124
+
125
+ export type UpdateTodoData = {
126
+ completed: boolean;
127
+ };
128
+
129
+ export interface TodoRepository {
130
+ list(userId: string, page: OffsetPage): Promise<ListTodosResult>;
131
+ findById(id: string): Promise<Todo | null>;
132
+ create(input: NewTodo): Promise<Todo>;
133
+ update(id: string, input: UpdateTodoData): Promise<Todo>;
134
+ delete(id: string): Promise<void>;
135
+ }
136
+ `,
137
+ policy: `import { type ActivityActor, definePolicy, deny } from "@beignet/core/ports";
138
+ import type { Todo } from "@/features/todos/schemas";
139
+ import type { AuthSession } from "@/ports/auth";
140
+
141
+ export type AuthorizationContext = {
142
+ actor: ActivityActor;
143
+ auth: AuthSession | null;
144
+ };
145
+
146
+ function authorizeOwner(
147
+ ctx: AuthorizationContext,
148
+ todo: Todo,
149
+ action: "update" | "delete",
150
+ ) {
151
+ if (ctx.actor.type !== "user") {
152
+ return deny(\`You must be signed in to \${action} todos.\`);
153
+ }
154
+
155
+ if (todo.userId !== ctx.actor.id) {
156
+ return deny({
157
+ reason: \`Only the owner can \${action} this todo.\`,
158
+ code: "NOT_TODO_OWNER",
159
+ details: { todoId: todo.id },
160
+ });
161
+ }
162
+
163
+ return true;
164
+ }
165
+
166
+ export const todoPolicy = definePolicy({
167
+ "todos.create": (ctx: AuthorizationContext) => {
168
+ if (ctx.actor.type === "user") return true;
169
+ return deny("You must be signed in to create todos.");
170
+ },
171
+ "todos.update": (ctx: AuthorizationContext, todo: Todo) =>
172
+ authorizeOwner(ctx, todo, "update"),
173
+ "todos.delete": (ctx: AuthorizationContext, todo: Todo) =>
174
+ authorizeOwner(ctx, todo, "delete"),
175
+ });
176
+ `,
177
+ routes: `import type { beignetServerOnly } from "@beignet/core/server-only";
178
+ import { defineRouteGroup } from "@beignet/next";
179
+ import type { AppContext } from "@/app-context";
180
+ import {
181
+ createTodo,
182
+ deleteTodo,
183
+ listTodos,
184
+ updateTodo,
185
+ } from "@/features/todos/contracts";
186
+ import {
187
+ createTodoUseCase,
188
+ deleteTodoUseCase,
189
+ listTodosUseCase,
190
+ updateTodoUseCase,
191
+ } from "@/features/todos/use-cases";
192
+
193
+ export const todoRoutes = defineRouteGroup<AppContext>({
194
+ name: "todos",
195
+ routes: [
196
+ { contract: listTodos, useCase: listTodosUseCase },
197
+ { contract: createTodo, useCase: createTodoUseCase },
198
+ { contract: updateTodo, useCase: updateTodoUseCase },
199
+ { contract: deleteTodo, useCase: deleteTodoUseCase },
200
+ ],
201
+ });
202
+ `,
203
+ listTodosUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
204
+ import { normalizeOffsetPage } from "@beignet/core/pagination";
205
+ import { requireUser } from "@/lib/auth";
206
+ import { useCase } from "@/lib/use-case";
207
+ import { ListTodosInputSchema, ListTodosOutputSchema } from "../schemas";
208
+
209
+ export const listTodosUseCase = useCase
210
+ .query("todos.list")
211
+ .input(ListTodosInputSchema)
212
+ .output(ListTodosOutputSchema)
213
+ .run(async ({ ctx, input }) => {
214
+ const user = requireUser(ctx);
215
+ const page = normalizeOffsetPage(input, {
216
+ defaultLimit: 20,
217
+ maxLimit: 100,
218
+ });
219
+
220
+ return ctx.ports.todos.list(user.id, page);
221
+ });
222
+ `,
223
+ createTodoUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
224
+ import { requireUser } from "@/lib/auth";
225
+ import { useCase } from "@/lib/use-case";
226
+ import { CreateTodoInputSchema, TodoSchema } from "../schemas";
227
+
228
+ export const createTodoUseCase = useCase
229
+ .command("todos.create")
230
+ .input(CreateTodoInputSchema)
231
+ .output(TodoSchema)
232
+ .run(async ({ ctx, input }) => {
233
+ const user = requireUser(ctx);
234
+ await ctx.gate.authorize("todos.create");
235
+
236
+ return ctx.ports.uow.transaction(async (tx) =>
237
+ tx.todos.create({ userId: user.id, title: input.title }),
238
+ );
239
+ });
240
+ `,
241
+ updateTodoUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
242
+ import { appError } from "@/features/shared/errors";
243
+ import { requireUser } from "@/lib/auth";
244
+ import { useCase } from "@/lib/use-case";
245
+ import { TodoSchema, UpdateTodoInputSchema } from "../schemas";
246
+
247
+ export const updateTodoUseCase = useCase
248
+ .command("todos.update")
249
+ .input(UpdateTodoInputSchema)
250
+ .output(TodoSchema)
251
+ .run(async ({ ctx, input }) => {
252
+ requireUser(ctx);
253
+
254
+ return ctx.ports.uow.transaction(async (tx) => {
255
+ const todo = await tx.todos.findById(input.id);
256
+ if (!todo) {
257
+ throw appError("TodoNotFound", { details: { id: input.id } });
258
+ }
259
+
260
+ await ctx.gate.authorize("todos.update", todo);
261
+
262
+ return tx.todos.update(input.id, { completed: input.completed });
263
+ });
264
+ });
265
+ `,
266
+ deleteTodoUseCase: `import type { beignetServerOnly } from "@beignet/core/server-only";
267
+ import { appError } from "@/features/shared/errors";
268
+ import { requireUser } from "@/lib/auth";
269
+ import { useCase } from "@/lib/use-case";
270
+ import { DeleteTodoOutputSchema, TodoIdInputSchema } from "../schemas";
271
+
272
+ export const deleteTodoUseCase = useCase
273
+ .command("todos.delete")
274
+ .input(TodoIdInputSchema)
275
+ .output(DeleteTodoOutputSchema)
276
+ .run(async ({ ctx, input }) => {
277
+ requireUser(ctx);
278
+
279
+ await ctx.ports.uow.transaction(async (tx) => {
280
+ const todo = await tx.todos.findById(input.id);
281
+ if (!todo) {
282
+ throw appError("TodoNotFound", { details: { id: input.id } });
283
+ }
284
+
285
+ await ctx.gate.authorize("todos.delete", todo);
286
+
287
+ await tx.todos.delete(input.id);
288
+ });
289
+ });
290
+ `,
291
+ useCasesIndex: `export { createTodoUseCase } from "./create-todo";
292
+ export { deleteTodoUseCase } from "./delete-todo";
293
+ export { listTodosUseCase } from "./list-todos";
294
+ export { updateTodoUseCase } from "./update-todo";
295
+ export {
296
+ CreateTodoInputSchema,
297
+ ListTodosInputSchema,
298
+ ListTodosOutputSchema,
299
+ TodoIdInputSchema,
300
+ TodoSchema,
301
+ UpdateTodoInputSchema,
302
+ type CreateTodoInput,
303
+ type ListTodosInput,
304
+ type Todo,
305
+ type UpdateTodoInput,
306
+ } from "../schemas";
307
+ `,
308
+ testHelpers: `import type { OffsetPage } from "@beignet/core/pagination";
309
+ import { offsetPageResult } from "@beignet/core/pagination";
310
+ import type {
311
+ NewTodo,
312
+ TodoRepository,
313
+ UpdateTodoData,
314
+ } from "@/features/todos/ports";
315
+ import type { Todo } from "@/features/todos/schemas";
316
+
317
+ export function createTestTodoRepository(): TodoRepository {
318
+ const todos = new Map<string, Todo>();
319
+
320
+ return {
321
+ async list(userId: string, page: OffsetPage) {
322
+ const owned = Array.from(todos.values())
323
+ .filter((todo) => todo.userId === userId)
324
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt));
325
+
326
+ return offsetPageResult(
327
+ owned.slice(page.offset, page.offset + page.limit),
328
+ page,
329
+ owned.length,
330
+ );
331
+ },
332
+ async findById(id: string) {
333
+ return todos.get(id) ?? null;
334
+ },
335
+ async create(input: NewTodo) {
336
+ const todo: Todo = {
337
+ id: crypto.randomUUID(),
338
+ userId: input.userId,
339
+ title: input.title,
340
+ completed: false,
341
+ createdAt: new Date().toISOString(),
342
+ };
343
+ todos.set(todo.id, todo);
344
+ return todo;
345
+ },
346
+ async update(id: string, input: UpdateTodoData) {
347
+ const todo = todos.get(id);
348
+ if (!todo) {
349
+ throw new Error(\`Todo not found: \${id}\`);
350
+ }
351
+
352
+ const next = { ...todo, completed: input.completed };
353
+ todos.set(id, next);
354
+ return next;
355
+ },
356
+ async delete(id: string) {
357
+ todos.delete(id);
358
+ },
359
+ };
360
+ }
361
+ `,
362
+ useCaseTest: `import { describe, expect, it } from "bun:test";
363
+ import { createUseCaseTester } from "@beignet/core/application";
364
+ import { createTestUserActor } from "@beignet/core/ports/testing";
365
+ import {
366
+ createTestContextFactory,
367
+ createTestPorts,
368
+ } from "@beignet/core/testing";
369
+ import { createInMemoryDevtools } from "@beignet/devtools";
370
+ import type { AppContext } from "@/app-context";
371
+ import type { TodoRepository } from "@/features/todos/ports";
372
+ import { appPorts } from "@/infra/app-ports";
373
+ import type { AppTransactionPorts } from "@/ports";
374
+ import {
375
+ createTodoUseCase,
376
+ deleteTodoUseCase,
377
+ listTodosUseCase,
378
+ updateTodoUseCase,
379
+ } from "../use-cases";
380
+ import { createTestTodoRepository } from "./helpers";
381
+
382
+ function createTester(userId: string, todos: TodoRepository) {
383
+ const auth = {
384
+ user: {
385
+ id: userId,
386
+ email: \`\${userId}@example.com\`,
387
+ name: "Test User",
388
+ },
389
+ session: { id: \`session_\${userId}\` },
390
+ };
391
+ const testFixture = createTestPorts<AppContext["ports"], AppTransactionPorts>({
392
+ base: appPorts,
393
+ overrides: {
394
+ gate: appPorts.gate,
395
+ todos,
396
+ devtools: createInMemoryDevtools(),
397
+ },
398
+ transaction: {
399
+ ports: (ports) => ({
400
+ ...ports,
401
+ todos,
402
+ }),
403
+ },
404
+ });
405
+ const createTestContext = createTestContextFactory<
406
+ AppContext,
407
+ AppContext["ports"]
408
+ >({
409
+ ports: testFixture.ports,
410
+ actor: createTestUserActor(auth.user.id, {
411
+ displayName: auth.user.name,
412
+ }),
413
+ auth,
414
+ });
415
+
416
+ return createUseCaseTester<AppContext>(createTestContext);
417
+ }
418
+
419
+ describe("todos use cases", () => {
420
+ it("creates, lists, updates, and deletes the signed-in user's todos", async () => {
421
+ const todos = createTestTodoRepository();
422
+ const tester = createTester("user_test", todos);
423
+ const ctx = await tester.ctx();
424
+
425
+ const created = await tester.run(
426
+ createTodoUseCase,
427
+ { title: "First todo" },
428
+ { ctx },
429
+ );
430
+ const updated = await tester.run(
431
+ updateTodoUseCase,
432
+ { id: created.id, completed: true },
433
+ { ctx },
434
+ );
435
+ const listed = await tester.run(
436
+ listTodosUseCase,
437
+ { limit: 20, offset: 0 },
438
+ { ctx },
439
+ );
440
+
441
+ expect(created.userId).toBe("user_test");
442
+ expect(updated.completed).toBe(true);
443
+ expect(listed.page.total).toBe(1);
444
+ expect(listed.items).toEqual([updated]);
445
+
446
+ await tester.run(deleteTodoUseCase, { id: created.id }, { ctx });
447
+ const afterDelete = await tester.run(
448
+ listTodosUseCase,
449
+ { limit: 20, offset: 0 },
450
+ { ctx },
451
+ );
452
+
453
+ expect(afterDelete.page.total).toBe(0);
454
+ });
455
+
456
+ it("denies updates to todos owned by another user", async () => {
457
+ const todos = createTestTodoRepository();
458
+ const owner = createTester("user_owner", todos);
459
+ const intruder = createTester("user_intruder", todos);
460
+
461
+ const ownerCtx = await owner.ctx();
462
+ const created = await owner.run(
463
+ createTodoUseCase,
464
+ { title: "Owned" },
465
+ { ctx: ownerCtx },
466
+ );
467
+
468
+ const intruderCtx = await intruder.ctx();
469
+ await expect(
470
+ intruder.run(
471
+ updateTodoUseCase,
472
+ { id: created.id, completed: true },
473
+ { ctx: intruderCtx },
474
+ ),
475
+ ).rejects.toThrow("Only the owner can update this todo.");
476
+ });
477
+ });
478
+ `,
479
+ routesTest: `import { describe, expect, it } from "bun:test";
480
+ import { createStaticAuth } from "@beignet/core/ports";
481
+ import { createIdempotencyHooks, defineRoutes } from "@beignet/core/server";
482
+ import { createTestPorts } from "@beignet/core/testing";
483
+ import { createInMemoryDevtools } from "@beignet/devtools";
484
+ import { createTestApp, createTestRequester } from "@beignet/web/testing";
485
+ import type { AppContext } from "@/app-context";
486
+ import type { TodoRepository } from "@/features/todos/ports";
487
+ import { appPorts } from "@/infra/app-ports";
488
+ import type { AppPorts, AppTransactionPorts } from "@/ports";
489
+ import type { AuthRequest, AuthSessionMetadata, AuthUser } from "@/ports/auth";
490
+ import { type AppServiceContextInput, appContext } from "@/server/context";
491
+ import { createTodo, deleteTodo, listTodos, updateTodo } from "../contracts";
492
+ import { todoRoutes } from "../routes";
493
+ import { createTestTodoRepository } from "./helpers";
494
+
495
+ function createSignedInAuth(userId: string): AppPorts["auth"] {
496
+ return createStaticAuth<AuthUser, AuthSessionMetadata, AuthRequest>({
497
+ user: { id: userId, email: \`\${userId}@example.com\`, name: "Test User" },
498
+ session: { id: \`session_\${userId}\` },
499
+ });
500
+ }
501
+
502
+ function createSignedOutAuth(): AppPorts["auth"] {
503
+ return createStaticAuth<AuthUser, AuthSessionMetadata, AuthRequest>(null);
504
+ }
505
+
506
+ async function createTodosTestApp(
507
+ options: { auth?: AppPorts["auth"]; todos?: TodoRepository } = {},
508
+ ) {
509
+ const todos = options.todos ?? createTestTodoRepository();
510
+ const fixture = createTestPorts<AppContext["ports"], AppTransactionPorts>({
511
+ base: appPorts,
512
+ overrides: {
513
+ gate: appPorts.gate,
514
+ todos,
515
+ devtools: createInMemoryDevtools(),
516
+ auth: options.auth ?? createSignedOutAuth(),
517
+ },
518
+ transaction: {
519
+ ports: (ports) => ({
520
+ ...ports,
521
+ todos,
522
+ }),
523
+ },
524
+ });
525
+
526
+ return createTestApp<AppContext, AppContext["ports"], AppServiceContextInput>(
527
+ {
528
+ ports: fixture.ports,
529
+ context: appContext,
530
+ hooks: [createIdempotencyHooks<AppContext>()],
531
+ routes: defineRoutes<AppContext>([todoRoutes]),
532
+ },
533
+ );
534
+ }
535
+
536
+ describe("todoRoutes", () => {
537
+ it("requires a signed-in user", async () => {
538
+ const app = await createTodosTestApp();
539
+ const requester = createTestRequester(app, {});
540
+
541
+ const result = await requester.safeRequest(listTodos);
542
+
543
+ await app.stop();
544
+
545
+ expect(result.ok).toBe(false);
546
+ if (result.ok) throw new Error("Expected an error result.");
547
+ expect(result.status).toBe(401);
548
+ expect(result.error.code).toBe("UNAUTHORIZED");
549
+ });
550
+
551
+ it("creates, updates, lists, and deletes the user's todos", async () => {
552
+ const app = await createTodosTestApp({
553
+ auth: createSignedInAuth("user_test"),
554
+ });
555
+ const requester = createTestRequester(app, {});
556
+
557
+ const created = await requester.request(createTodo, {
558
+ body: { title: "Ship the starter" },
559
+ idempotencyKey: "todo-create-1",
560
+ });
561
+ // Replaying the same idempotency key returns the stored result instead
562
+ // of creating a second todo.
563
+ const replayed = await requester.request(createTodo, {
564
+ body: { title: "Ship the starter" },
565
+ idempotencyKey: "todo-create-1",
566
+ });
567
+ const updated = await requester.request(updateTodo, {
568
+ path: { id: created.id },
569
+ body: { completed: true },
570
+ });
571
+ const listed = await requester.request(listTodos);
572
+
573
+ expect(replayed.id).toBe(created.id);
574
+ expect(updated.completed).toBe(true);
575
+ expect(listed.page.total).toBe(1);
576
+ expect(listed.items).toEqual([updated]);
577
+
578
+ await requester.request(deleteTodo, { path: { id: created.id } });
579
+ const afterDelete = await requester.request(listTodos);
580
+
581
+ await app.stop();
582
+
583
+ expect(afterDelete.page.total).toBe(0);
584
+ });
585
+
586
+ it("hides other users' todos from the list", async () => {
587
+ const todos = createTestTodoRepository();
588
+ await todos.create({ userId: "user_other", title: "Not yours" });
589
+ const app = await createTodosTestApp({
590
+ auth: createSignedInAuth("user_owner"),
591
+ todos,
592
+ });
593
+ const requester = createTestRequester(app, {});
594
+
595
+ await requester.request(createTodo, { body: { title: "Owned" } });
596
+
597
+ const listed = await requester.request(listTodos);
598
+ await app.stop();
599
+
600
+ expect(listed.page.total).toBe(1);
601
+ expect(listed.items[0]?.userId).toBe("user_owner");
602
+ });
603
+ });
604
+ `,
605
+ };
606
+ export { files as todosFiles };
607
+ //# sourceMappingURL=todos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todos.js","sourceRoot":"","sources":["../../src/templates/todos.ts"],"names":[],"mappings":"AAAA,MAAM,KAAK,GAAG;IACZ,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDV;IACC,SAAS,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2DZ;IACC,KAAK,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;CAyBR;IACC,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCT;IACC,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;CAyBT;IACC,gBAAgB,EAAE;;;;;;;;;;;;;;;;;;;CAmBnB;IACC,iBAAiB,EAAE;;;;;;;;;;;;;;;;;CAiBpB;IACC,iBAAiB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;CAwBpB;IACC,iBAAiB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;CAwBpB;IACC,aAAa,EAAE;;;;;;;;;;;;;;;;CAgBhB;IACC,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDd;IACC,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoHd;IACC,UAAU,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6Hb;CACA,CAAC;AAEF,OAAO,EAAE,KAAK,IAAI,UAAU,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beignet/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "description": "CLI for creating and maintaining Beignet apps.",
6
6
  "main": "./dist/lib.js",
@@ -66,7 +66,7 @@
66
66
  "typescript": "^5.3.0"
67
67
  },
68
68
  "dependencies": {
69
- "@beignet/core": "^0.0.4",
69
+ "@beignet/core": "^0.0.6",
70
70
  "@clack/prompts": "^1.5.1",
71
71
  "@stricli/core": "^1.2.7",
72
72
  "jiti": "^2.7.0"