@beignet/cli 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +219 -0
- package/README.md +379 -61
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +49 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +53 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +41 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +78 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +10 -5
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +28 -47
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -603
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1896 -382
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates/base.d.ts +13 -0
- package/dist/templates/base.d.ts.map +1 -0
- package/dist/templates/base.js +300 -0
- package/dist/templates/base.js.map +1 -0
- package/dist/templates/db.d.ts +14 -0
- package/dist/templates/db.d.ts.map +1 -0
- package/dist/templates/db.js +962 -0
- package/dist/templates/db.js.map +1 -0
- package/dist/templates/index.d.ts +14 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +123 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/server.d.ts +26 -0
- package/dist/templates/server.d.ts.map +1 -0
- package/dist/templates/server.js +520 -0
- package/dist/templates/server.js.map +1 -0
- package/dist/templates/shadcn.d.ts +5 -0
- package/dist/templates/shadcn.d.ts.map +1 -0
- package/dist/templates/shadcn.js +555 -0
- package/dist/templates/shadcn.js.map +1 -0
- package/dist/templates/shared.d.ts +49 -0
- package/dist/templates/shared.d.ts.map +1 -0
- package/dist/templates/shared.js +57 -0
- package/dist/templates/shared.js.map +1 -0
- package/dist/templates/shell.d.ts +5 -0
- package/dist/templates/shell.d.ts.map +1 -0
- package/dist/templates/shell.js +1190 -0
- package/dist/templates/shell.js.map +1 -0
- package/dist/templates/todos.d.ts +17 -0
- package/dist/templates/todos.d.ts.map +1 -0
- package/dist/templates/todos.js +607 -0
- package/dist/templates/todos.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +83 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +114 -0
- package/src/create.ts +40 -64
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1075 -817
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2389 -406
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates/base.ts +377 -0
- package/src/templates/db.ts +963 -0
- package/src/templates/index.ts +148 -0
- package/src/templates/server.ts +528 -0
- package/src/templates/shadcn.ts +570 -0
- package/src/templates/shared.ts +90 -0
- package/src/templates/shell.ts +1219 -0
- package/src/templates/todos.ts +607 -0
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/dist/templates.d.ts +0 -55
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -3520
- package/dist/templates.js.map +0 -1
- package/src/create-bin.ts +0 -11
- package/src/templates.ts +0 -3774
package/src/make.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import type { MakeFeatureAddon } from "./choices.js";
|
|
3
5
|
import {
|
|
4
6
|
type BeignetConfig,
|
|
5
7
|
directoryPath,
|
|
@@ -15,27 +17,22 @@ type MakeResourceOptions = {
|
|
|
15
17
|
force?: boolean;
|
|
16
18
|
dryRun?: boolean;
|
|
17
19
|
config?: BeignetConfig;
|
|
20
|
+
auth?: boolean;
|
|
21
|
+
tenant?: boolean;
|
|
22
|
+
events?: boolean;
|
|
23
|
+
softDelete?: boolean;
|
|
18
24
|
};
|
|
19
25
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
"event",
|
|
23
|
-
"events",
|
|
24
|
-
"job",
|
|
25
|
-
"jobs",
|
|
26
|
-
"notification",
|
|
27
|
-
"notifications",
|
|
28
|
-
"upload",
|
|
29
|
-
"uploads",
|
|
30
|
-
] as const;
|
|
31
|
-
|
|
32
|
-
export type MakeFeatureAddon = (typeof makeFeatureAddonChoices)[number];
|
|
26
|
+
export type { MakeFeatureAddon } from "./choices.js";
|
|
27
|
+
export { makeFeatureAddonChoices } from "./choices.js";
|
|
33
28
|
|
|
34
29
|
type NormalizedMakeFeatureAddon =
|
|
35
30
|
| "policy"
|
|
31
|
+
| "task"
|
|
36
32
|
| "event"
|
|
37
33
|
| "job"
|
|
38
34
|
| "notification"
|
|
35
|
+
| "ui"
|
|
39
36
|
| "upload";
|
|
40
37
|
|
|
41
38
|
type MakeFeatureOptions = MakeResourceOptions & {
|
|
@@ -58,6 +55,14 @@ type MakeUseCaseOptions = {
|
|
|
58
55
|
config?: BeignetConfig;
|
|
59
56
|
};
|
|
60
57
|
|
|
58
|
+
type MakeTaskOptions = {
|
|
59
|
+
name: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
force?: boolean;
|
|
62
|
+
dryRun?: boolean;
|
|
63
|
+
config?: BeignetConfig;
|
|
64
|
+
};
|
|
65
|
+
|
|
61
66
|
type MakeTestOptions = {
|
|
62
67
|
name: string;
|
|
63
68
|
cwd?: string;
|
|
@@ -159,6 +164,7 @@ type MakeUploadOptions = {
|
|
|
159
164
|
};
|
|
160
165
|
|
|
161
166
|
type MakeResult = {
|
|
167
|
+
schemaVersion: 1;
|
|
162
168
|
name: string;
|
|
163
169
|
targetDir: string;
|
|
164
170
|
dryRun: boolean;
|
|
@@ -184,6 +190,10 @@ export type MakeContractResult = MakeResult;
|
|
|
184
190
|
* Result returned by `beignet make use-case`.
|
|
185
191
|
*/
|
|
186
192
|
export type MakeUseCaseResult = MakeResult;
|
|
193
|
+
/**
|
|
194
|
+
* Result returned by `beignet make task`.
|
|
195
|
+
*/
|
|
196
|
+
export type MakeTaskResult = MakeResult;
|
|
187
197
|
/**
|
|
188
198
|
* Result returned by `beignet make test`.
|
|
189
199
|
*/
|
|
@@ -243,6 +253,15 @@ type ResourceNames = {
|
|
|
243
253
|
pluralPascal: string;
|
|
244
254
|
};
|
|
245
255
|
|
|
256
|
+
type ResourceGenerationMode = "feature" | "resource";
|
|
257
|
+
|
|
258
|
+
type ResourceGenerationOptions = {
|
|
259
|
+
auth: boolean;
|
|
260
|
+
tenant: boolean;
|
|
261
|
+
events: boolean;
|
|
262
|
+
softDelete: boolean;
|
|
263
|
+
};
|
|
264
|
+
|
|
246
265
|
type GeneratedFile = {
|
|
247
266
|
path: string;
|
|
248
267
|
content: string;
|
|
@@ -300,6 +319,13 @@ type JobNames = FeatureArtifactNames & {
|
|
|
300
319
|
payloadTypeName: string;
|
|
301
320
|
};
|
|
302
321
|
|
|
322
|
+
type TaskNames = FeatureArtifactNames & {
|
|
323
|
+
taskName: string;
|
|
324
|
+
taskExportName: string;
|
|
325
|
+
inputSchemaName: string;
|
|
326
|
+
inputTypeName: string;
|
|
327
|
+
};
|
|
328
|
+
|
|
303
329
|
type FactoryNames = FeatureArtifactNames & {
|
|
304
330
|
factoryName: string;
|
|
305
331
|
factoryExportName: string;
|
|
@@ -339,8 +365,51 @@ type UploadNames = FeatureArtifactNames & {
|
|
|
339
365
|
metadataTypeName: string;
|
|
340
366
|
};
|
|
341
367
|
|
|
368
|
+
type FeatureUiNames = ResourceNames & {
|
|
369
|
+
componentExportName: string;
|
|
370
|
+
componentFileName: string;
|
|
371
|
+
};
|
|
372
|
+
|
|
342
373
|
export async function makeResource(
|
|
343
374
|
options: MakeResourceOptions,
|
|
375
|
+
): Promise<MakeResourceResult> {
|
|
376
|
+
return makeResourceSlice(options, "resource");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function makeFeature(
|
|
380
|
+
options: MakeFeatureOptions,
|
|
381
|
+
): Promise<MakeFeatureResult> {
|
|
382
|
+
const addons = normalizeMakeFeatureAddons(options.with ?? []);
|
|
383
|
+
if (addons.length === 0) {
|
|
384
|
+
return makeFeatureResource(options);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (options.dryRun) {
|
|
388
|
+
return makeFeatureSlice(options, addons);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
392
|
+
return makeFeatureSlice(options, addons);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function makeFeatureResource(
|
|
396
|
+
options: MakeResourceOptions,
|
|
397
|
+
): Promise<MakeResourceResult> {
|
|
398
|
+
try {
|
|
399
|
+
return await makeResourceSlice(options, "feature");
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error instanceof Error && error.message.includes("make resource")) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
error.message.replaceAll("make resource", "make feature"),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function makeResourceSlice(
|
|
411
|
+
options: MakeResourceOptions,
|
|
412
|
+
mode: ResourceGenerationMode,
|
|
344
413
|
): Promise<MakeResourceResult> {
|
|
345
414
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
346
415
|
const names = resourceNames(options.name);
|
|
@@ -349,9 +418,24 @@ export async function makeResource(
|
|
|
349
418
|
: await loadBeignetConfig(targetDir);
|
|
350
419
|
|
|
351
420
|
await assertStandardApp(targetDir, config);
|
|
421
|
+
if (
|
|
422
|
+
mode === "resource" &&
|
|
423
|
+
!(await fileExists(path.join(targetDir, resourceSharedErrorsPath(config))))
|
|
424
|
+
) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`beignet make resource expects an app error catalog. Missing ${resourceSharedErrorsPath(config)}.`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
352
429
|
|
|
353
430
|
const persistence = await detectResourcePersistence(targetDir, config);
|
|
354
|
-
const
|
|
431
|
+
const generationOptions = resourceGenerationOptions(options, mode);
|
|
432
|
+
const generatedFiles = resourceFiles(
|
|
433
|
+
names,
|
|
434
|
+
config,
|
|
435
|
+
persistence,
|
|
436
|
+
mode,
|
|
437
|
+
generationOptions,
|
|
438
|
+
);
|
|
355
439
|
const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
|
|
356
440
|
force: Boolean(options.force),
|
|
357
441
|
});
|
|
@@ -362,6 +446,8 @@ export async function makeResource(
|
|
|
362
446
|
persistence,
|
|
363
447
|
{
|
|
364
448
|
dryRun: true,
|
|
449
|
+
mode,
|
|
450
|
+
generationOptions,
|
|
365
451
|
},
|
|
366
452
|
);
|
|
367
453
|
const createdFiles = plannedFiles
|
|
@@ -385,11 +471,14 @@ export async function makeResource(
|
|
|
385
471
|
...(await updateResourceWiring(targetDir, names, config, {
|
|
386
472
|
persistence,
|
|
387
473
|
dryRun: false,
|
|
474
|
+
mode,
|
|
475
|
+
generationOptions,
|
|
388
476
|
})),
|
|
389
477
|
);
|
|
390
478
|
}
|
|
391
479
|
|
|
392
480
|
return {
|
|
481
|
+
schemaVersion: 1,
|
|
393
482
|
name: names.pluralKebab,
|
|
394
483
|
targetDir,
|
|
395
484
|
dryRun: Boolean(options.dryRun),
|
|
@@ -400,35 +489,20 @@ export async function makeResource(
|
|
|
400
489
|
};
|
|
401
490
|
}
|
|
402
491
|
|
|
403
|
-
|
|
404
|
-
options: MakeFeatureOptions,
|
|
405
|
-
): Promise<MakeFeatureResult> {
|
|
406
|
-
const addons = normalizeMakeFeatureAddons(options.with ?? []);
|
|
407
|
-
if (addons.length === 0) {
|
|
408
|
-
return makeFeatureResource(options);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (options.dryRun) {
|
|
412
|
-
return makeFeatureSlice(options, addons);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
416
|
-
return makeFeatureSlice(options, addons);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
async function makeFeatureResource(
|
|
492
|
+
function resourceGenerationOptions(
|
|
420
493
|
options: MakeResourceOptions,
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (error instanceof Error && error.message.includes("make resource")) {
|
|
426
|
-
throw new Error(
|
|
427
|
-
error.message.replaceAll("make resource", "make feature"),
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
throw error;
|
|
494
|
+
mode: ResourceGenerationMode,
|
|
495
|
+
): ResourceGenerationOptions {
|
|
496
|
+
if (mode !== "resource") {
|
|
497
|
+
return { auth: false, tenant: false, events: false, softDelete: false };
|
|
431
498
|
}
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
auth: Boolean(options.auth),
|
|
502
|
+
tenant: Boolean(options.tenant),
|
|
503
|
+
events: Boolean(options.events),
|
|
504
|
+
softDelete: Boolean(options.softDelete),
|
|
505
|
+
};
|
|
432
506
|
}
|
|
433
507
|
|
|
434
508
|
async function makeFeatureSlice(
|
|
@@ -462,6 +536,9 @@ async function makeFeatureAddon(
|
|
|
462
536
|
if (addon === "policy") {
|
|
463
537
|
return makePolicy({ ...shared, name: feature });
|
|
464
538
|
}
|
|
539
|
+
if (addon === "task") {
|
|
540
|
+
return makeTask({ ...shared, name: `${feature}/backfill` });
|
|
541
|
+
}
|
|
465
542
|
if (addon === "event") {
|
|
466
543
|
return makeEvent({ ...shared, name: `${feature}/created` });
|
|
467
544
|
}
|
|
@@ -471,6 +548,9 @@ async function makeFeatureAddon(
|
|
|
471
548
|
if (addon === "notification") {
|
|
472
549
|
return makeNotification({ ...shared, name: `${feature}/created` });
|
|
473
550
|
}
|
|
551
|
+
if (addon === "ui") {
|
|
552
|
+
return makeFeatureUi({ ...shared, name: feature });
|
|
553
|
+
}
|
|
474
554
|
|
|
475
555
|
return makeUpload({ ...shared, name: `${feature}/attachment` });
|
|
476
556
|
}
|
|
@@ -482,6 +562,7 @@ function normalizeMakeFeatureAddons(
|
|
|
482
562
|
|
|
483
563
|
for (const addon of addons) {
|
|
484
564
|
if (addon === "events") normalized.add("event");
|
|
565
|
+
else if (addon === "tasks") normalized.add("task");
|
|
485
566
|
else if (addon === "jobs") normalized.add("job");
|
|
486
567
|
else if (addon === "notifications") normalized.add("notification");
|
|
487
568
|
else if (addon === "uploads") normalized.add("upload");
|
|
@@ -493,9 +574,11 @@ function normalizeMakeFeatureAddons(
|
|
|
493
574
|
|
|
494
575
|
const featureAddonOrder: readonly NormalizedMakeFeatureAddon[] = [
|
|
495
576
|
"policy",
|
|
577
|
+
"task",
|
|
496
578
|
"event",
|
|
497
579
|
"job",
|
|
498
580
|
"notification",
|
|
581
|
+
"ui",
|
|
499
582
|
"upload",
|
|
500
583
|
];
|
|
501
584
|
|
|
@@ -537,6 +620,7 @@ export async function makeContract(
|
|
|
537
620
|
}
|
|
538
621
|
|
|
539
622
|
return {
|
|
623
|
+
schemaVersion: 1,
|
|
540
624
|
name: names.pluralKebab,
|
|
541
625
|
targetDir,
|
|
542
626
|
dryRun: Boolean(options.dryRun),
|
|
@@ -582,6 +666,7 @@ export async function makeUseCase(
|
|
|
582
666
|
if (indexResult === "skipped") skippedFiles.push(indexFile.path);
|
|
583
667
|
|
|
584
668
|
return {
|
|
669
|
+
schemaVersion: 1,
|
|
585
670
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
586
671
|
targetDir,
|
|
587
672
|
dryRun: Boolean(options.dryRun),
|
|
@@ -627,6 +712,7 @@ export async function makeTest(
|
|
|
627
712
|
}
|
|
628
713
|
|
|
629
714
|
return {
|
|
715
|
+
schemaVersion: 1,
|
|
630
716
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
631
717
|
targetDir,
|
|
632
718
|
dryRun: Boolean(options.dryRun),
|
|
@@ -689,6 +775,7 @@ export async function makePort(
|
|
|
689
775
|
}
|
|
690
776
|
|
|
691
777
|
return {
|
|
778
|
+
schemaVersion: 1,
|
|
692
779
|
name: names.kebab,
|
|
693
780
|
targetDir,
|
|
694
781
|
dryRun: Boolean(options.dryRun),
|
|
@@ -734,6 +821,7 @@ export async function makeAdapter(
|
|
|
734
821
|
}
|
|
735
822
|
|
|
736
823
|
return {
|
|
824
|
+
schemaVersion: 1,
|
|
737
825
|
name: names.kebab,
|
|
738
826
|
targetDir,
|
|
739
827
|
dryRun: Boolean(options.dryRun),
|
|
@@ -771,6 +859,7 @@ export async function makePolicy(
|
|
|
771
859
|
}
|
|
772
860
|
|
|
773
861
|
return {
|
|
862
|
+
schemaVersion: 1,
|
|
774
863
|
name: names.kebab,
|
|
775
864
|
targetDir,
|
|
776
865
|
dryRun: Boolean(options.dryRun),
|
|
@@ -821,7 +910,10 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
821
910
|
force: Boolean(options.force),
|
|
822
911
|
dryRun: Boolean(options.dryRun),
|
|
823
912
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
824
|
-
files:
|
|
913
|
+
files: [
|
|
914
|
+
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
915
|
+
...jobFiles(names, config),
|
|
916
|
+
],
|
|
825
917
|
index: featureArtifactIndexFile("job", names, config),
|
|
826
918
|
dependencies: {
|
|
827
919
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -829,6 +921,49 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
829
921
|
});
|
|
830
922
|
}
|
|
831
923
|
|
|
924
|
+
export async function makeTask(
|
|
925
|
+
options: MakeTaskOptions,
|
|
926
|
+
): Promise<MakeTaskResult> {
|
|
927
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
928
|
+
const names = taskNames(options.name);
|
|
929
|
+
const config = options.config
|
|
930
|
+
? resolveConfig(options.config)
|
|
931
|
+
: await loadBeignetConfig(targetDir);
|
|
932
|
+
|
|
933
|
+
await assertFeatureArtifactApp(targetDir, config, "task");
|
|
934
|
+
|
|
935
|
+
const result = await makeFeatureArtifact({
|
|
936
|
+
targetDir,
|
|
937
|
+
config,
|
|
938
|
+
force: Boolean(options.force),
|
|
939
|
+
dryRun: Boolean(options.dryRun),
|
|
940
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
941
|
+
files: [
|
|
942
|
+
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
943
|
+
...taskFiles(names, config),
|
|
944
|
+
],
|
|
945
|
+
index: featureArtifactIndexFile("task", names, config),
|
|
946
|
+
dependencies: {
|
|
947
|
+
"@beignet/core": beignetDependencyVersion,
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const registryResult = await updateTaskRegistry(targetDir, names, config, {
|
|
952
|
+
dryRun: Boolean(options.dryRun),
|
|
953
|
+
});
|
|
954
|
+
if (registryResult === "created")
|
|
955
|
+
result.createdFiles.push(config.paths.tasks);
|
|
956
|
+
if (registryResult === "updated")
|
|
957
|
+
result.updatedFiles.push(config.paths.tasks);
|
|
958
|
+
if (registryResult === "skipped")
|
|
959
|
+
result.skippedFiles.push(config.paths.tasks);
|
|
960
|
+
if (!result.files.includes(config.paths.tasks)) {
|
|
961
|
+
result.files.push(config.paths.tasks);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
|
|
832
967
|
export async function makeFactory(
|
|
833
968
|
options: MakeFactoryOptions,
|
|
834
969
|
): Promise<MakeFactoryResult> {
|
|
@@ -896,7 +1031,14 @@ export async function makeNotification(
|
|
|
896
1031
|
force: Boolean(options.force),
|
|
897
1032
|
dryRun: Boolean(options.dryRun),
|
|
898
1033
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
899
|
-
files:
|
|
1034
|
+
files: [
|
|
1035
|
+
...(await missingCapabilityBuilderFiles(
|
|
1036
|
+
targetDir,
|
|
1037
|
+
"notifications",
|
|
1038
|
+
config,
|
|
1039
|
+
)),
|
|
1040
|
+
...notificationFiles(names, config),
|
|
1041
|
+
],
|
|
900
1042
|
index: featureArtifactIndexFile("notification", names, config),
|
|
901
1043
|
dependencies: {
|
|
902
1044
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -922,7 +1064,10 @@ export async function makeListener(
|
|
|
922
1064
|
force: Boolean(options.force),
|
|
923
1065
|
dryRun: Boolean(options.dryRun),
|
|
924
1066
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
925
|
-
files:
|
|
1067
|
+
files: [
|
|
1068
|
+
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
1069
|
+
...listenerFiles(names, config),
|
|
1070
|
+
],
|
|
926
1071
|
index: featureArtifactIndexFile("listener", names, config),
|
|
927
1072
|
dependencies: {
|
|
928
1073
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -962,9 +1107,18 @@ export async function makeSchedule(
|
|
|
962
1107
|
requireServer: Boolean(options.route),
|
|
963
1108
|
});
|
|
964
1109
|
|
|
965
|
-
|
|
1110
|
+
// Plan the CRON_SECRET wiring before writing any files so an anchor miss
|
|
1111
|
+
// in lib/env.ts aborts the generator without leaving partial output.
|
|
1112
|
+
const cronSecretPlan = options.route
|
|
1113
|
+
? await planCronSecretWiring(targetDir)
|
|
1114
|
+
: undefined;
|
|
966
1115
|
|
|
967
|
-
|
|
1116
|
+
const files = [
|
|
1117
|
+
...(await missingCapabilityBuilderFiles(targetDir, "schedules", config)),
|
|
1118
|
+
...scheduleFiles(names, config, { route: Boolean(options.route) }),
|
|
1119
|
+
];
|
|
1120
|
+
|
|
1121
|
+
const result = await makeFeatureArtifact({
|
|
968
1122
|
targetDir,
|
|
969
1123
|
config,
|
|
970
1124
|
force: Boolean(options.force),
|
|
@@ -976,6 +1130,139 @@ export async function makeSchedule(
|
|
|
976
1130
|
"@beignet/core": beignetDependencyVersion,
|
|
977
1131
|
},
|
|
978
1132
|
});
|
|
1133
|
+
|
|
1134
|
+
const registryResult = await updateScheduleRegistry(
|
|
1135
|
+
targetDir,
|
|
1136
|
+
names,
|
|
1137
|
+
config,
|
|
1138
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
1139
|
+
);
|
|
1140
|
+
if (registryResult === "created")
|
|
1141
|
+
result.createdFiles.push(config.paths.schedules);
|
|
1142
|
+
if (registryResult === "updated")
|
|
1143
|
+
result.updatedFiles.push(config.paths.schedules);
|
|
1144
|
+
if (registryResult === "skipped")
|
|
1145
|
+
result.skippedFiles.push(config.paths.schedules);
|
|
1146
|
+
if (!result.files.includes(config.paths.schedules)) {
|
|
1147
|
+
result.files.push(config.paths.schedules);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (cronSecretPlan) {
|
|
1151
|
+
const applied = await applyCronSecretWiring(targetDir, cronSecretPlan, {
|
|
1152
|
+
dryRun: Boolean(options.dryRun),
|
|
1153
|
+
});
|
|
1154
|
+
result.createdFiles.push(...applied.createdFiles);
|
|
1155
|
+
result.updatedFiles.push(...applied.updatedFiles);
|
|
1156
|
+
result.skippedFiles.push(...applied.skippedFiles);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const cronSecretEnvFile = "lib/env.ts";
|
|
1163
|
+
const cronSecretEnvEntry = `\t\tCRON_SECRET: z.string().min(1).optional(),`;
|
|
1164
|
+
// The literal APP_URL line every generated lib/env.ts ships with. CRON_SECRET
|
|
1165
|
+
// slots in directly below it, matching the documented env shape.
|
|
1166
|
+
const cronSecretEnvAnchor = `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`;
|
|
1167
|
+
|
|
1168
|
+
type CronSecretWiringPlan = {
|
|
1169
|
+
files: Array<GeneratedFile & { result: "created" | "updated" }>;
|
|
1170
|
+
skippedFiles: string[];
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
async function planCronSecretWiring(
|
|
1174
|
+
targetDir: string,
|
|
1175
|
+
): Promise<CronSecretWiringPlan> {
|
|
1176
|
+
const plan: CronSecretWiringPlan = { files: [], skippedFiles: [] };
|
|
1177
|
+
const manualInstruction = `Add ${cronSecretEnvEntry.trim()} to the createEnv server block in ${cronSecretEnvFile} manually, or restore the generated env file before running make schedule --route.`;
|
|
1178
|
+
|
|
1179
|
+
const envSource = await readOptionalFile(
|
|
1180
|
+
path.join(targetDir, cronSecretEnvFile),
|
|
1181
|
+
);
|
|
1182
|
+
if (envSource === undefined) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`Could not find the generated env file ${cronSecretEnvFile}. ${manualInstruction}`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (/\bCRON_SECRET\b/.test(envSource)) {
|
|
1189
|
+
plan.skippedFiles.push(cronSecretEnvFile);
|
|
1190
|
+
} else if (!envSource.includes(cronSecretEnvAnchor)) {
|
|
1191
|
+
throw new Error(
|
|
1192
|
+
`Could not find the generated APP_URL server entry in ${cronSecretEnvFile}. ${manualInstruction}`,
|
|
1193
|
+
);
|
|
1194
|
+
} else {
|
|
1195
|
+
plan.files.push({
|
|
1196
|
+
path: cronSecretEnvFile,
|
|
1197
|
+
content: envSource.replace(
|
|
1198
|
+
cronSecretEnvAnchor,
|
|
1199
|
+
`${cronSecretEnvAnchor}\n${cronSecretEnvEntry}`,
|
|
1200
|
+
),
|
|
1201
|
+
result: "updated",
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const secretLine = `CRON_SECRET=${randomBytes(16).toString("hex")}`;
|
|
1206
|
+
|
|
1207
|
+
const envExample = await readOptionalFile(
|
|
1208
|
+
path.join(targetDir, ".env.example"),
|
|
1209
|
+
);
|
|
1210
|
+
if (envExample === undefined) {
|
|
1211
|
+
plan.files.push({
|
|
1212
|
+
path: ".env.example",
|
|
1213
|
+
content: `${secretLine}\n`,
|
|
1214
|
+
result: "created",
|
|
1215
|
+
});
|
|
1216
|
+
} else if (/^CRON_SECRET=/m.test(envExample)) {
|
|
1217
|
+
plan.skippedFiles.push(".env.example");
|
|
1218
|
+
} else {
|
|
1219
|
+
plan.files.push({
|
|
1220
|
+
path: ".env.example",
|
|
1221
|
+
content: `${envExample.replace(/\n*$/, "\n")}${secretLine}\n`,
|
|
1222
|
+
result: "updated",
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const envLocal = await readOptionalFile(path.join(targetDir, ".env.local"));
|
|
1227
|
+
if (envLocal !== undefined) {
|
|
1228
|
+
if (/^CRON_SECRET=/m.test(envLocal)) {
|
|
1229
|
+
plan.skippedFiles.push(".env.local");
|
|
1230
|
+
} else {
|
|
1231
|
+
plan.files.push({
|
|
1232
|
+
path: ".env.local",
|
|
1233
|
+
content: `${envLocal.replace(/\n*$/, "\n")}${secretLine}\n`,
|
|
1234
|
+
result: "updated",
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return plan;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async function applyCronSecretWiring(
|
|
1243
|
+
targetDir: string,
|
|
1244
|
+
plan: CronSecretWiringPlan,
|
|
1245
|
+
options: { dryRun: boolean },
|
|
1246
|
+
): Promise<{
|
|
1247
|
+
createdFiles: string[];
|
|
1248
|
+
updatedFiles: string[];
|
|
1249
|
+
skippedFiles: string[];
|
|
1250
|
+
}> {
|
|
1251
|
+
if (!options.dryRun) {
|
|
1252
|
+
for (const file of plan.files) {
|
|
1253
|
+
await writeFile(path.join(targetDir, file.path), file.content);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return {
|
|
1258
|
+
createdFiles: plan.files
|
|
1259
|
+
.filter((file) => file.result === "created")
|
|
1260
|
+
.map((file) => file.path),
|
|
1261
|
+
updatedFiles: plan.files
|
|
1262
|
+
.filter((file) => file.result === "updated")
|
|
1263
|
+
.map((file) => file.path),
|
|
1264
|
+
skippedFiles: plan.skippedFiles,
|
|
1265
|
+
};
|
|
979
1266
|
}
|
|
980
1267
|
|
|
981
1268
|
export async function makeUpload(
|
|
@@ -1003,6 +1290,70 @@ export async function makeUpload(
|
|
|
1003
1290
|
});
|
|
1004
1291
|
}
|
|
1005
1292
|
|
|
1293
|
+
async function makeFeatureUi(
|
|
1294
|
+
options: MakeResourceOptions,
|
|
1295
|
+
): Promise<MakeResult> {
|
|
1296
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
1297
|
+
const names = featureUiNames(options.name);
|
|
1298
|
+
const config = options.config
|
|
1299
|
+
? resolveConfig(options.config)
|
|
1300
|
+
: await loadBeignetConfig(targetDir);
|
|
1301
|
+
|
|
1302
|
+
await assertFeatureUiApp(targetDir, config);
|
|
1303
|
+
|
|
1304
|
+
const componentFiles = featureUiComponentFiles(names, config);
|
|
1305
|
+
const indexFile = featureUiIndexFile(names, config);
|
|
1306
|
+
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
1307
|
+
force: Boolean(options.force),
|
|
1308
|
+
});
|
|
1309
|
+
const createdFiles = plannedFiles
|
|
1310
|
+
.filter((file) => file.result === "created")
|
|
1311
|
+
.map((file) => file.path);
|
|
1312
|
+
const updatedFiles = plannedFiles
|
|
1313
|
+
.filter((file) => file.result === "updated")
|
|
1314
|
+
.map((file) => file.path);
|
|
1315
|
+
const skippedFiles = plannedFiles
|
|
1316
|
+
.filter((file) => file.result === "skipped")
|
|
1317
|
+
.map((file) => file.path);
|
|
1318
|
+
|
|
1319
|
+
if (!options.dryRun) {
|
|
1320
|
+
for (const file of plannedFiles) {
|
|
1321
|
+
await writePlannedGeneratedFile(targetDir, file);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const indexResult = await updateFeatureUiIndex(targetDir, indexFile, {
|
|
1326
|
+
dryRun: Boolean(options.dryRun),
|
|
1327
|
+
});
|
|
1328
|
+
if (indexResult === "created") createdFiles.push(indexFile.path);
|
|
1329
|
+
if (indexResult === "updated") updatedFiles.push(indexFile.path);
|
|
1330
|
+
if (indexResult === "skipped") skippedFiles.push(indexFile.path);
|
|
1331
|
+
|
|
1332
|
+
if (
|
|
1333
|
+
await updatePackageDependencies(
|
|
1334
|
+
targetDir,
|
|
1335
|
+
{
|
|
1336
|
+
"@beignet/react-query": beignetDependencyVersion,
|
|
1337
|
+
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
1338
|
+
},
|
|
1339
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
1340
|
+
)
|
|
1341
|
+
) {
|
|
1342
|
+
updatedFiles.push("package.json");
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return {
|
|
1346
|
+
schemaVersion: 1,
|
|
1347
|
+
name: names.pluralKebab,
|
|
1348
|
+
targetDir,
|
|
1349
|
+
dryRun: Boolean(options.dryRun),
|
|
1350
|
+
files: [...componentFiles.map((file) => file.path), indexFile.path],
|
|
1351
|
+
createdFiles,
|
|
1352
|
+
updatedFiles,
|
|
1353
|
+
skippedFiles,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1006
1357
|
async function assertStandardApp(
|
|
1007
1358
|
targetDir: string,
|
|
1008
1359
|
config: ResolvedBeignetConfig,
|
|
@@ -1131,6 +1482,7 @@ async function assertFeatureArtifactApp(
|
|
|
1131
1482
|
artifact:
|
|
1132
1483
|
| "event"
|
|
1133
1484
|
| "job"
|
|
1485
|
+
| "task"
|
|
1134
1486
|
| "notification"
|
|
1135
1487
|
| "listener"
|
|
1136
1488
|
| "schedule"
|
|
@@ -1153,6 +1505,24 @@ async function assertFeatureArtifactApp(
|
|
|
1153
1505
|
}
|
|
1154
1506
|
}
|
|
1155
1507
|
|
|
1508
|
+
async function assertFeatureUiApp(
|
|
1509
|
+
targetDir: string,
|
|
1510
|
+
config: ResolvedBeignetConfig,
|
|
1511
|
+
): Promise<void> {
|
|
1512
|
+
const clientPath = clientIndexPath(config);
|
|
1513
|
+
const requiredFiles = [config.paths.appContext, clientPath];
|
|
1514
|
+
|
|
1515
|
+
for (const file of requiredFiles) {
|
|
1516
|
+
try {
|
|
1517
|
+
await stat(path.join(targetDir, file));
|
|
1518
|
+
} catch {
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
`beignet make feature --with ui expects a standard Beignet app with React Query client helpers. Missing ${file}.`,
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1156
1526
|
async function assertPersistenceArtifactApp(
|
|
1157
1527
|
targetDir: string,
|
|
1158
1528
|
names: FactoryNames | SeedNames,
|
|
@@ -1264,18 +1634,95 @@ async function readOptionalFile(filePath: string): Promise<string | undefined> {
|
|
|
1264
1634
|
}
|
|
1265
1635
|
}
|
|
1266
1636
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1637
|
+
type CapabilityBuilder =
|
|
1638
|
+
| "listeners"
|
|
1639
|
+
| "jobs"
|
|
1640
|
+
| "schedules"
|
|
1641
|
+
| "notifications"
|
|
1642
|
+
| "tasks";
|
|
1643
|
+
|
|
1644
|
+
const capabilityBuilders: Record<
|
|
1645
|
+
CapabilityBuilder,
|
|
1646
|
+
{ factoryName: string; defineName: string; module: string }
|
|
1647
|
+
> = {
|
|
1648
|
+
listeners: {
|
|
1649
|
+
factoryName: "createListeners",
|
|
1650
|
+
defineName: "defineListener",
|
|
1651
|
+
module: "@beignet/core/events",
|
|
1652
|
+
},
|
|
1653
|
+
jobs: {
|
|
1654
|
+
factoryName: "createJobs",
|
|
1655
|
+
defineName: "defineJob",
|
|
1656
|
+
module: "@beignet/core/jobs",
|
|
1657
|
+
},
|
|
1658
|
+
schedules: {
|
|
1659
|
+
factoryName: "createSchedules",
|
|
1660
|
+
defineName: "defineSchedule",
|
|
1661
|
+
module: "@beignet/core/schedules",
|
|
1662
|
+
},
|
|
1663
|
+
notifications: {
|
|
1664
|
+
factoryName: "createNotifications",
|
|
1665
|
+
defineName: "defineNotification",
|
|
1666
|
+
module: "@beignet/core/notifications",
|
|
1667
|
+
},
|
|
1668
|
+
tasks: {
|
|
1669
|
+
factoryName: "createTasks",
|
|
1670
|
+
defineName: "defineTask",
|
|
1671
|
+
module: "@beignet/core/tasks",
|
|
1672
|
+
},
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
function capabilityBuilderPath(
|
|
1676
|
+
capability: CapabilityBuilder,
|
|
1677
|
+
config: ResolvedBeignetConfig,
|
|
1678
|
+
): string {
|
|
1679
|
+
if (capability === "listeners") return config.paths.listenersBuilder;
|
|
1680
|
+
if (capability === "jobs") return config.paths.jobsBuilder;
|
|
1681
|
+
if (capability === "schedules") return config.paths.schedulesBuilder;
|
|
1682
|
+
if (capability === "notifications") return config.paths.notificationsBuilder;
|
|
1683
|
+
return config.paths.tasksBuilder;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
function capabilityBuilderFile(
|
|
1687
|
+
capability: CapabilityBuilder,
|
|
1688
|
+
config: ResolvedBeignetConfig,
|
|
1689
|
+
): GeneratedFile {
|
|
1690
|
+
const builder = capabilityBuilders[capability];
|
|
1691
|
+
|
|
1692
|
+
return {
|
|
1693
|
+
path: capabilityBuilderPath(capability, config),
|
|
1694
|
+
content: `import { ${builder.factoryName} } from "${builder.module}";
|
|
1695
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
1696
|
+
|
|
1697
|
+
export const { ${builder.defineName} } = ${builder.factoryName}<AppContext>();
|
|
1698
|
+
`,
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
async function missingCapabilityBuilderFiles(
|
|
1703
|
+
targetDir: string,
|
|
1704
|
+
capability: CapabilityBuilder,
|
|
1705
|
+
config: ResolvedBeignetConfig,
|
|
1706
|
+
): Promise<GeneratedFile[]> {
|
|
1707
|
+
const file = capabilityBuilderFile(capability, config);
|
|
1708
|
+
if (await fileExists(path.join(targetDir, file.path))) {
|
|
1709
|
+
return [];
|
|
1710
|
+
}
|
|
1711
|
+
return [file];
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
async function makeFeatureArtifact(options: {
|
|
1715
|
+
targetDir: string;
|
|
1716
|
+
config: ResolvedBeignetConfig;
|
|
1717
|
+
force: boolean;
|
|
1718
|
+
dryRun: boolean;
|
|
1719
|
+
name: string;
|
|
1720
|
+
files: GeneratedFile[];
|
|
1721
|
+
index: FeatureArtifactIndexFile;
|
|
1722
|
+
dependencies: Record<string, (packageJson: PackageJsonLike) => string>;
|
|
1723
|
+
}): Promise<MakeResult> {
|
|
1724
|
+
const plannedFiles = await planGeneratedFiles(
|
|
1725
|
+
options.targetDir,
|
|
1279
1726
|
options.files,
|
|
1280
1727
|
{
|
|
1281
1728
|
force: options.force,
|
|
@@ -1315,6 +1762,7 @@ async function makeFeatureArtifact(options: {
|
|
|
1315
1762
|
}
|
|
1316
1763
|
|
|
1317
1764
|
return {
|
|
1765
|
+
schemaVersion: 1,
|
|
1318
1766
|
name: options.name,
|
|
1319
1767
|
targetDir: options.targetDir,
|
|
1320
1768
|
dryRun: options.dryRun,
|
|
@@ -1358,14 +1806,29 @@ function beignetDependencyVersion(packageJson: PackageJsonLike): string {
|
|
|
1358
1806
|
);
|
|
1359
1807
|
}
|
|
1360
1808
|
|
|
1809
|
+
function tanstackReactQueryDependencyVersion(
|
|
1810
|
+
packageJson: PackageJsonLike,
|
|
1811
|
+
): string {
|
|
1812
|
+
return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1361
1815
|
async function updateResourceWiring(
|
|
1362
1816
|
targetDir: string,
|
|
1363
1817
|
names: ResourceNames,
|
|
1364
1818
|
config: ResolvedBeignetConfig,
|
|
1365
1819
|
persistenceOrOptions:
|
|
1366
1820
|
| ResourcePersistence
|
|
1367
|
-
| {
|
|
1368
|
-
|
|
1821
|
+
| {
|
|
1822
|
+
dryRun: boolean;
|
|
1823
|
+
persistence?: ResourcePersistence;
|
|
1824
|
+
mode?: ResourceGenerationMode;
|
|
1825
|
+
generationOptions?: ResourceGenerationOptions;
|
|
1826
|
+
},
|
|
1827
|
+
maybeOptions?: {
|
|
1828
|
+
dryRun: boolean;
|
|
1829
|
+
mode?: ResourceGenerationMode;
|
|
1830
|
+
generationOptions?: ResourceGenerationOptions;
|
|
1831
|
+
},
|
|
1369
1832
|
): Promise<string[]> {
|
|
1370
1833
|
const persistence =
|
|
1371
1834
|
typeof persistenceOrOptions === "string"
|
|
@@ -1375,6 +1838,16 @@ async function updateResourceWiring(
|
|
|
1375
1838
|
typeof persistenceOrOptions === "string"
|
|
1376
1839
|
? (maybeOptions as { dryRun: boolean })
|
|
1377
1840
|
: persistenceOrOptions;
|
|
1841
|
+
const mode =
|
|
1842
|
+
typeof persistenceOrOptions === "string"
|
|
1843
|
+
? (maybeOptions?.mode ?? "feature")
|
|
1844
|
+
: (persistenceOrOptions.mode ?? "feature");
|
|
1845
|
+
const generationOptions =
|
|
1846
|
+
typeof persistenceOrOptions === "string"
|
|
1847
|
+
? (maybeOptions?.generationOptions ??
|
|
1848
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode))
|
|
1849
|
+
: (persistenceOrOptions.generationOptions ??
|
|
1850
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode));
|
|
1378
1851
|
const updated = new Set<string>();
|
|
1379
1852
|
if (
|
|
1380
1853
|
await updatePackageJson(targetDir, {
|
|
@@ -1384,7 +1857,12 @@ async function updateResourceWiring(
|
|
|
1384
1857
|
) {
|
|
1385
1858
|
updated.add("package.json");
|
|
1386
1859
|
}
|
|
1387
|
-
if (
|
|
1860
|
+
if (
|
|
1861
|
+
await updatePortsIndex(targetDir, names, config, {
|
|
1862
|
+
...options,
|
|
1863
|
+
generationOptions,
|
|
1864
|
+
})
|
|
1865
|
+
) {
|
|
1388
1866
|
updated.add(config.paths.ports);
|
|
1389
1867
|
}
|
|
1390
1868
|
if (
|
|
@@ -1393,6 +1871,18 @@ async function updateResourceWiring(
|
|
|
1393
1871
|
) {
|
|
1394
1872
|
updated.add(config.paths.infrastructurePorts);
|
|
1395
1873
|
}
|
|
1874
|
+
if (
|
|
1875
|
+
generationOptions.auth &&
|
|
1876
|
+
(await updateInfrastructureGatePolicies(targetDir, names, config, options))
|
|
1877
|
+
) {
|
|
1878
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1879
|
+
}
|
|
1880
|
+
if (
|
|
1881
|
+
persistence === "drizzle" &&
|
|
1882
|
+
(await updateInfrastructureDeferredPorts(targetDir, names, config, options))
|
|
1883
|
+
) {
|
|
1884
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1885
|
+
}
|
|
1396
1886
|
if (
|
|
1397
1887
|
persistence === "drizzle" &&
|
|
1398
1888
|
(await updateDrizzleSchemaIndex(targetDir, names, config, options))
|
|
@@ -1411,7 +1901,14 @@ async function updateResourceWiring(
|
|
|
1411
1901
|
) {
|
|
1412
1902
|
updated.add(config.paths.server);
|
|
1413
1903
|
}
|
|
1414
|
-
if (
|
|
1904
|
+
if (
|
|
1905
|
+
await updateSharedErrors(targetDir, names, config, { ...options, mode })
|
|
1906
|
+
) {
|
|
1907
|
+
updated.add(resourceSharedErrorsPath(config));
|
|
1908
|
+
}
|
|
1909
|
+
if (
|
|
1910
|
+
await updateOpenApiRoute(targetDir, names, config, { ...options, mode })
|
|
1911
|
+
) {
|
|
1415
1912
|
updated.add(config.paths.openapiRoute);
|
|
1416
1913
|
}
|
|
1417
1914
|
const routesFile = await updateServerRoutes(
|
|
@@ -1457,7 +1954,7 @@ async function updatePortsIndex(
|
|
|
1457
1954
|
targetDir: string,
|
|
1458
1955
|
names: ResourceNames,
|
|
1459
1956
|
config: ResolvedBeignetConfig,
|
|
1460
|
-
options: { dryRun: boolean },
|
|
1957
|
+
options: { dryRun: boolean; generationOptions?: ResourceGenerationOptions },
|
|
1461
1958
|
): Promise<boolean> {
|
|
1462
1959
|
const filePath = path.join(targetDir, config.paths.ports);
|
|
1463
1960
|
const original = await readFile(filePath, "utf8");
|
|
@@ -1469,6 +1966,30 @@ async function updatePortsIndex(
|
|
|
1469
1966
|
next = insertAfterImports(next, importLine);
|
|
1470
1967
|
}
|
|
1471
1968
|
|
|
1969
|
+
if (options.generationOptions?.auth) {
|
|
1970
|
+
const policyPath = path.join(
|
|
1971
|
+
config.paths.features,
|
|
1972
|
+
names.pluralKebab,
|
|
1973
|
+
"policy.ts",
|
|
1974
|
+
);
|
|
1975
|
+
const policyImportLine = `import type { AuthorizationContext as ${names.singularPascal}AuthorizationContext, ${names.singularCamel}Policy } from "${relativeModule(config.paths.ports, policyPath)}";`;
|
|
1976
|
+
if (!next.includes(policyImportLine)) {
|
|
1977
|
+
next = insertAfterImports(next, policyImportLine);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
next = appendUnionTypeMember(
|
|
1981
|
+
next,
|
|
1982
|
+
"AppAuthorizationContext",
|
|
1983
|
+
`${names.singularPascal}AuthorizationContext`,
|
|
1984
|
+
);
|
|
1985
|
+
next = appendTupleTypeMember(
|
|
1986
|
+
next,
|
|
1987
|
+
"AppGate",
|
|
1988
|
+
`typeof ${names.singularCamel}Policy`,
|
|
1989
|
+
);
|
|
1990
|
+
next = appendGatePolicyMember(next, `typeof ${names.singularCamel}Policy`);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1472
1993
|
const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
|
|
1473
1994
|
if (!next.includes(entry.trim())) {
|
|
1474
1995
|
next = insertTypeProperty(
|
|
@@ -1625,7 +2146,7 @@ async function updateInfrastructurePorts(
|
|
|
1625
2146
|
if (!next.includes(repositoryConst)) {
|
|
1626
2147
|
next = replaceRequired(
|
|
1627
2148
|
next,
|
|
1628
|
-
/(\nexport const appPorts = definePorts(?:<[^>]+>)?\(\{)/,
|
|
2149
|
+
/(\nexport const appPorts = definePorts(?:<[^>]+>)?\((?:\)\()?\{)/,
|
|
1629
2150
|
`\n${repositoryConst}\n$1`,
|
|
1630
2151
|
`Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`,
|
|
1631
2152
|
);
|
|
@@ -1662,6 +2183,37 @@ async function updateInfrastructurePorts(
|
|
|
1662
2183
|
return true;
|
|
1663
2184
|
}
|
|
1664
2185
|
|
|
2186
|
+
async function updateInfrastructureGatePolicies(
|
|
2187
|
+
targetDir: string,
|
|
2188
|
+
names: ResourceNames,
|
|
2189
|
+
config: ResolvedBeignetConfig,
|
|
2190
|
+
options: { dryRun: boolean },
|
|
2191
|
+
): Promise<boolean> {
|
|
2192
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
2193
|
+
const original = await readFile(filePath, "utf8");
|
|
2194
|
+
let next = original;
|
|
2195
|
+
const policyPath = path.join(
|
|
2196
|
+
config.paths.features,
|
|
2197
|
+
names.pluralKebab,
|
|
2198
|
+
"policy.ts",
|
|
2199
|
+
);
|
|
2200
|
+
const importLine = `import { ${names.singularCamel}Policy } from "${relativeModule(config.paths.infrastructurePorts, policyPath)}";`;
|
|
2201
|
+
|
|
2202
|
+
if (!next.includes(importLine)) {
|
|
2203
|
+
next = insertAfterImports(next, importLine);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
next = appendCreateGatePolicy(
|
|
2207
|
+
next,
|
|
2208
|
+
`${names.singularCamel}Policy`,
|
|
2209
|
+
`Could not find createGate({ policies: [...] }) in ${config.paths.infrastructurePorts}. Add ${names.singularCamel}Policy to the gate policies manually, or restore the generated infrastructure ports file before running make resource --auth.`,
|
|
2210
|
+
);
|
|
2211
|
+
|
|
2212
|
+
if (next === original) return false;
|
|
2213
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2214
|
+
return true;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1665
2217
|
async function updateDrizzleSchemaIndex(
|
|
1666
2218
|
targetDir: string,
|
|
1667
2219
|
names: ResourceNames,
|
|
@@ -1719,6 +2271,24 @@ async function updateDrizzleRepositories(
|
|
|
1719
2271
|
return true;
|
|
1720
2272
|
}
|
|
1721
2273
|
|
|
2274
|
+
async function updateInfrastructureDeferredPorts(
|
|
2275
|
+
targetDir: string,
|
|
2276
|
+
names: ResourceNames,
|
|
2277
|
+
config: ResolvedBeignetConfig,
|
|
2278
|
+
options: { dryRun: boolean },
|
|
2279
|
+
): Promise<boolean> {
|
|
2280
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
2281
|
+
const original = await readFile(filePath, "utf8");
|
|
2282
|
+
// Drizzle resources are provided by the app database provider at startup,
|
|
2283
|
+
// so deferred-form ports files defer the new repository key. Identity-form
|
|
2284
|
+
// ports files are left untouched.
|
|
2285
|
+
const next = appendDeferredPortKey(original, names.pluralCamel);
|
|
2286
|
+
|
|
2287
|
+
if (next === original) return false;
|
|
2288
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2289
|
+
return true;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
1722
2292
|
async function updateServerTransactionPorts(
|
|
1723
2293
|
targetDir: string,
|
|
1724
2294
|
names: ResourceNames,
|
|
@@ -1759,7 +2329,7 @@ async function updateOpenApiRoute(
|
|
|
1759
2329
|
targetDir: string,
|
|
1760
2330
|
names: ResourceNames,
|
|
1761
2331
|
config: ResolvedBeignetConfig,
|
|
1762
|
-
options: { dryRun: boolean },
|
|
2332
|
+
options: { dryRun: boolean; mode: ResourceGenerationMode },
|
|
1763
2333
|
): Promise<boolean> {
|
|
1764
2334
|
const filePath = path.join(targetDir, config.paths.openapiRoute);
|
|
1765
2335
|
if (!(await fileExists(filePath))) return false;
|
|
@@ -1768,15 +2338,14 @@ async function updateOpenApiRoute(
|
|
|
1768
2338
|
const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
|
|
1769
2339
|
if (!firstArg?.text.startsWith("[")) return false;
|
|
1770
2340
|
|
|
1771
|
-
const
|
|
1772
|
-
const createName = `create${names.singularPascal}`;
|
|
2341
|
+
const contractNames = resourceContractNames(names, options.mode);
|
|
1773
2342
|
const listedContracts = identifiersFromArrayExpression(firstArg.text);
|
|
1774
|
-
const missingContracts =
|
|
2343
|
+
const missingContracts = contractNames.filter(
|
|
1775
2344
|
(contractName) => !listedContracts.has(contractName),
|
|
1776
2345
|
);
|
|
1777
2346
|
if (missingContracts.length === 0) return false;
|
|
1778
2347
|
|
|
1779
|
-
const importLine = `import {
|
|
2348
|
+
const importLine = `import { ${contractNames.join(", ")} } from "${aliasModule(resourceContractFilePath(names, config))}";`;
|
|
1780
2349
|
const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
|
|
1781
2350
|
let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(
|
|
1782
2351
|
firstArg.end,
|
|
@@ -1791,6 +2360,74 @@ async function updateOpenApiRoute(
|
|
|
1791
2360
|
return true;
|
|
1792
2361
|
}
|
|
1793
2362
|
|
|
2363
|
+
async function updateSharedErrors(
|
|
2364
|
+
targetDir: string,
|
|
2365
|
+
names: ResourceNames,
|
|
2366
|
+
config: ResolvedBeignetConfig,
|
|
2367
|
+
options: { dryRun: boolean; mode: ResourceGenerationMode },
|
|
2368
|
+
): Promise<boolean> {
|
|
2369
|
+
if (options.mode !== "resource") return false;
|
|
2370
|
+
|
|
2371
|
+
const filePath = path.join(targetDir, resourceSharedErrorsPath(config));
|
|
2372
|
+
if (!(await fileExists(filePath))) return false;
|
|
2373
|
+
|
|
2374
|
+
const original = await readFile(filePath, "utf8");
|
|
2375
|
+
const notFoundErrorName = `${names.singularPascal}NotFound`;
|
|
2376
|
+
const conflictErrorName = `${names.singularPascal}Conflict`;
|
|
2377
|
+
const missingErrorNames = [notFoundErrorName, conflictErrorName].filter(
|
|
2378
|
+
(errorName) => !new RegExp(`\\b${errorName}\\b`).test(original),
|
|
2379
|
+
);
|
|
2380
|
+
if (missingErrorNames.length === 0) return false;
|
|
2381
|
+
const expectedErrorNames = missingErrorNames.join(" and ");
|
|
2382
|
+
|
|
2383
|
+
const defineErrorsStart = original.indexOf("defineErrors(");
|
|
2384
|
+
if (defineErrorsStart === -1) {
|
|
2385
|
+
throw new Error(
|
|
2386
|
+
`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`,
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
const openBrace = original.indexOf("{", defineErrorsStart);
|
|
2391
|
+
if (openBrace === -1) {
|
|
2392
|
+
throw new Error(
|
|
2393
|
+
`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`,
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
|
|
2398
|
+
if (closeBrace === -1) {
|
|
2399
|
+
throw new Error(
|
|
2400
|
+
`Could not update ${resourceSharedErrorsPath(config)} because the error catalog object is malformed. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`,
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
const entries = [];
|
|
2405
|
+
if (missingErrorNames.includes(notFoundErrorName)) {
|
|
2406
|
+
entries.push(`${notFoundErrorName}: {
|
|
2407
|
+
\t\tcode: "${constantCase(names.singularKebab)}_NOT_FOUND",
|
|
2408
|
+
\t\tstatus: 404,
|
|
2409
|
+
\t\tmessage: "${names.singularPascal} not found",
|
|
2410
|
+
\t},`);
|
|
2411
|
+
}
|
|
2412
|
+
if (missingErrorNames.includes(conflictErrorName)) {
|
|
2413
|
+
entries.push(`${conflictErrorName}: {
|
|
2414
|
+
\t\tcode: "${constantCase(names.singularKebab)}_CONFLICT",
|
|
2415
|
+
\t\tstatus: 409,
|
|
2416
|
+
\t\tmessage: "${names.singularPascal} was changed by another request",
|
|
2417
|
+
\t},`);
|
|
2418
|
+
}
|
|
2419
|
+
const next = insertBeforeClosingBrace(
|
|
2420
|
+
original,
|
|
2421
|
+
openBrace,
|
|
2422
|
+
closeBrace,
|
|
2423
|
+
entries.join("\n"),
|
|
2424
|
+
);
|
|
2425
|
+
|
|
2426
|
+
if (next === original) return false;
|
|
2427
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2428
|
+
return true;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
1794
2431
|
async function updateServerRoutes(
|
|
1795
2432
|
targetDir: string,
|
|
1796
2433
|
names: ResourceNames,
|
|
@@ -2184,6 +2821,98 @@ function typeBodySource(source: string, typeName: string): string | undefined {
|
|
|
2184
2821
|
return source.slice(openBrace + 1, closeBrace);
|
|
2185
2822
|
}
|
|
2186
2823
|
|
|
2824
|
+
function appendUnionTypeMember(
|
|
2825
|
+
source: string,
|
|
2826
|
+
typeName: string,
|
|
2827
|
+
member: string,
|
|
2828
|
+
): string {
|
|
2829
|
+
const match = new RegExp(
|
|
2830
|
+
`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`,
|
|
2831
|
+
).exec(source);
|
|
2832
|
+
if (!match) return source;
|
|
2833
|
+
|
|
2834
|
+
const declaration = match[0];
|
|
2835
|
+
if (new RegExp(`\\b${member}\\b`).test(declaration)) return source;
|
|
2836
|
+
|
|
2837
|
+
const nextDeclaration = declaration.replace(/;\s*$/, ` | ${member};`);
|
|
2838
|
+
return `${source.slice(0, match.index)}${nextDeclaration}${source.slice(
|
|
2839
|
+
match.index + declaration.length,
|
|
2840
|
+
)}`;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
function appendTupleTypeMember(
|
|
2844
|
+
source: string,
|
|
2845
|
+
typeName: string,
|
|
2846
|
+
member: string,
|
|
2847
|
+
): string {
|
|
2848
|
+
const match = new RegExp(`export\\s+type\\s+${typeName}\\s*=\\s*`).exec(
|
|
2849
|
+
source,
|
|
2850
|
+
);
|
|
2851
|
+
if (!match) return source;
|
|
2852
|
+
|
|
2853
|
+
const tupleStart = source.indexOf("[", match.index);
|
|
2854
|
+
if (tupleStart === -1) return source;
|
|
2855
|
+
|
|
2856
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
2857
|
+
if (tupleEnd === -1) return source;
|
|
2858
|
+
|
|
2859
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
2860
|
+
if (tuple.includes(member)) return source;
|
|
2861
|
+
|
|
2862
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
2863
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(
|
|
2864
|
+
tupleEnd + 1,
|
|
2865
|
+
)}`;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
function appendGatePolicyMember(source: string, member: string): string {
|
|
2869
|
+
const gateMatch = /\bGatePort\s*</.exec(source);
|
|
2870
|
+
if (!gateMatch) return source;
|
|
2871
|
+
|
|
2872
|
+
const tupleStart = source.indexOf("[", gateMatch.index);
|
|
2873
|
+
if (tupleStart === -1) return source;
|
|
2874
|
+
|
|
2875
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
2876
|
+
if (tupleEnd === -1) return source;
|
|
2877
|
+
|
|
2878
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
2879
|
+
if (tuple.includes(member)) return source;
|
|
2880
|
+
|
|
2881
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
2882
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(
|
|
2883
|
+
tupleEnd + 1,
|
|
2884
|
+
)}`;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function appendCreateGatePolicy(
|
|
2888
|
+
source: string,
|
|
2889
|
+
policy: string,
|
|
2890
|
+
failureMessage: string,
|
|
2891
|
+
): string {
|
|
2892
|
+
const policyMatch = /\bpolicies\s*:\s*\[/.exec(source);
|
|
2893
|
+
if (!policyMatch) {
|
|
2894
|
+
throw new Error(failureMessage);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
const arrayStart = source.indexOf("[", policyMatch.index);
|
|
2898
|
+
if (arrayStart === -1) {
|
|
2899
|
+
throw new Error(failureMessage);
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
2903
|
+
if (arrayEnd === -1) {
|
|
2904
|
+
throw new Error(failureMessage);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
const policies = source.slice(arrayStart, arrayEnd + 1);
|
|
2908
|
+
if (policies.includes(policy)) return source;
|
|
2909
|
+
|
|
2910
|
+
const nextPolicies = appendToArrayExpression(policies, [policy]);
|
|
2911
|
+
return `${source.slice(0, arrayStart)}${nextPolicies}${source.slice(
|
|
2912
|
+
arrayEnd + 1,
|
|
2913
|
+
)}`;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2187
2916
|
function insertDefinePortsProperty(
|
|
2188
2917
|
source: string,
|
|
2189
2918
|
property: string,
|
|
@@ -2281,7 +3010,20 @@ function definePortsBodySource(source: string): string | undefined {
|
|
|
2281
3010
|
function definePortsBodyRange(
|
|
2282
3011
|
source: string,
|
|
2283
3012
|
): { openBrace: number; closeBrace: number } | undefined {
|
|
2284
|
-
const
|
|
3013
|
+
const outer = definePortsOuterBodyRange(source);
|
|
3014
|
+
if (!outer) return undefined;
|
|
3015
|
+
|
|
3016
|
+
// The curried `definePorts<AppPorts>()({ bound, deferred })` form binds app
|
|
3017
|
+
// ports inside `bound`; generated bindings belong there.
|
|
3018
|
+
return (
|
|
3019
|
+
topLevelBoundObjectRange(source, outer.openBrace, outer.closeBrace) ?? outer
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
function definePortsOuterBodyRange(
|
|
3024
|
+
source: string,
|
|
3025
|
+
): { openBrace: number; closeBrace: number } | undefined {
|
|
3026
|
+
const match = /definePorts(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?\{/.exec(source);
|
|
2285
3027
|
if (!match) return undefined;
|
|
2286
3028
|
|
|
2287
3029
|
const openBrace = source.indexOf("{", match.index);
|
|
@@ -2291,6 +3033,50 @@ function definePortsBodyRange(
|
|
|
2291
3033
|
return { openBrace, closeBrace };
|
|
2292
3034
|
}
|
|
2293
3035
|
|
|
3036
|
+
function appendDeferredPortKey(source: string, portKey: string): string {
|
|
3037
|
+
const outer = definePortsOuterBodyRange(source);
|
|
3038
|
+
if (!outer) return source;
|
|
3039
|
+
|
|
3040
|
+
const body = source.slice(outer.openBrace + 1, outer.closeBrace);
|
|
3041
|
+
if (!hasTopLevelObjectProperty(body, "deferred")) return source;
|
|
3042
|
+
|
|
3043
|
+
const deferredMatch = /\bdeferred\s*:\s*\[/.exec(body);
|
|
3044
|
+
if (!deferredMatch) return source;
|
|
3045
|
+
|
|
3046
|
+
const arrayStart = source.indexOf(
|
|
3047
|
+
"[",
|
|
3048
|
+
outer.openBrace + 1 + deferredMatch.index,
|
|
3049
|
+
);
|
|
3050
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
3051
|
+
if (arrayEnd === -1) return source;
|
|
3052
|
+
|
|
3053
|
+
const tuple = source.slice(arrayStart, arrayEnd + 1);
|
|
3054
|
+
if (new RegExp(`["']${portKey}["']`).test(tuple)) return source;
|
|
3055
|
+
|
|
3056
|
+
const nextTuple = appendToArrayExpression(tuple, [`"${portKey}"`]);
|
|
3057
|
+
return `${source.slice(0, arrayStart)}${nextTuple}${source.slice(
|
|
3058
|
+
arrayEnd + 1,
|
|
3059
|
+
)}`;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function topLevelBoundObjectRange(
|
|
3063
|
+
source: string,
|
|
3064
|
+
openBrace: number,
|
|
3065
|
+
closeBrace: number,
|
|
3066
|
+
): { openBrace: number; closeBrace: number } | undefined {
|
|
3067
|
+
const body = source.slice(openBrace + 1, closeBrace);
|
|
3068
|
+
if (!hasTopLevelObjectProperty(body, "bound")) return undefined;
|
|
3069
|
+
|
|
3070
|
+
const boundMatch = /\bbound\s*:\s*\{/.exec(body);
|
|
3071
|
+
if (!boundMatch) return undefined;
|
|
3072
|
+
|
|
3073
|
+
const boundOpen = source.indexOf("{", openBrace + 1 + boundMatch.index);
|
|
3074
|
+
const boundClose = matchingBraceIndex(source, boundOpen);
|
|
3075
|
+
if (boundClose === -1) return undefined;
|
|
3076
|
+
|
|
3077
|
+
return { openBrace: boundOpen, closeBrace: boundClose };
|
|
3078
|
+
}
|
|
3079
|
+
|
|
2294
3080
|
function hasTopLevelObjectProperty(
|
|
2295
3081
|
source: string,
|
|
2296
3082
|
propertyName: string,
|
|
@@ -2593,6 +3379,21 @@ function resourceNames(input: string): ResourceNames {
|
|
|
2593
3379
|
return names;
|
|
2594
3380
|
}
|
|
2595
3381
|
|
|
3382
|
+
function featureUiNames(input: string): FeatureUiNames {
|
|
3383
|
+
const names = resourceNames(input);
|
|
3384
|
+
const resolvedNames = {
|
|
3385
|
+
...names,
|
|
3386
|
+
componentExportName: `${names.pluralPascal}Panel`,
|
|
3387
|
+
componentFileName: `${names.pluralKebab}-panel`,
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3390
|
+
assertGeneratedIdentifiers(input, "Feature UI name", [
|
|
3391
|
+
resolvedNames.componentExportName,
|
|
3392
|
+
]);
|
|
3393
|
+
|
|
3394
|
+
return resolvedNames;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
2596
3397
|
function featureResourceNames(names: FeatureArtifactNames): ResourceNames {
|
|
2597
3398
|
return resourceNames(names.feature.kebab);
|
|
2598
3399
|
}
|
|
@@ -2737,6 +3538,21 @@ function jobNames(input: string): JobNames {
|
|
|
2737
3538
|
};
|
|
2738
3539
|
}
|
|
2739
3540
|
|
|
3541
|
+
function taskNames(input: string): TaskNames {
|
|
3542
|
+
const names = featureArtifactNames(input, "Task");
|
|
3543
|
+
const taskExportName = `${names.artifact.camel}Task`;
|
|
3544
|
+
|
|
3545
|
+
assertGeneratedIdentifiers(input, "Task name", [taskExportName]);
|
|
3546
|
+
|
|
3547
|
+
return {
|
|
3548
|
+
...names,
|
|
3549
|
+
taskName: names.stableName,
|
|
3550
|
+
taskExportName,
|
|
3551
|
+
inputSchemaName: `${names.artifact.pascal}TaskInputSchema`,
|
|
3552
|
+
inputTypeName: `${names.artifact.pascal}TaskInput`,
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
|
|
2740
3556
|
function factoryNames(input: string): FactoryNames {
|
|
2741
3557
|
const names = featureArtifactNames(input, "Factory");
|
|
2742
3558
|
const factoryExportName = `${names.artifact.camel}Factory`;
|
|
@@ -2986,10 +3802,21 @@ function pascalCase(value: string): string {
|
|
|
2986
3802
|
return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
2987
3803
|
}
|
|
2988
3804
|
|
|
3805
|
+
function constantCase(value: string): string {
|
|
3806
|
+
return value.replaceAll("-", "_").toUpperCase();
|
|
3807
|
+
}
|
|
3808
|
+
|
|
2989
3809
|
function resourceFiles(
|
|
2990
3810
|
names: ResourceNames,
|
|
2991
3811
|
config: ResolvedBeignetConfig,
|
|
2992
3812
|
persistence: ResourcePersistence = "memory",
|
|
3813
|
+
mode: ResourceGenerationMode = "feature",
|
|
3814
|
+
options: ResourceGenerationOptions = {
|
|
3815
|
+
auth: false,
|
|
3816
|
+
tenant: false,
|
|
3817
|
+
events: false,
|
|
3818
|
+
softDelete: false,
|
|
3819
|
+
},
|
|
2993
3820
|
): GeneratedFile[] {
|
|
2994
3821
|
const useCaseDir = resourceUseCaseDir(names, config);
|
|
2995
3822
|
const infraDir = infrastructureDir(config);
|
|
@@ -2997,27 +3824,63 @@ function resourceFiles(
|
|
|
2997
3824
|
const files: GeneratedFile[] = [
|
|
2998
3825
|
{
|
|
2999
3826
|
path: resourceContractFilePath(names, config),
|
|
3000
|
-
content: contractFile(names, config),
|
|
3827
|
+
content: contractFile(names, config, mode, options),
|
|
3001
3828
|
},
|
|
3002
3829
|
{
|
|
3003
|
-
path: path.join(
|
|
3004
|
-
content: schemasFile(names),
|
|
3830
|
+
path: path.join(featureDir, "schemas.ts"),
|
|
3831
|
+
content: schemasFile(names, mode, options),
|
|
3005
3832
|
},
|
|
3006
3833
|
{
|
|
3007
3834
|
path: path.join(useCaseDir, `list-${names.pluralKebab}.ts`),
|
|
3008
|
-
content: listUseCaseFile(names, config),
|
|
3835
|
+
content: listUseCaseFile(names, config, options),
|
|
3009
3836
|
},
|
|
3010
3837
|
{
|
|
3011
3838
|
path: path.join(useCaseDir, `create-${names.singularKebab}.ts`),
|
|
3012
|
-
content: createUseCaseFile(names, config),
|
|
3839
|
+
content: createUseCaseFile(names, config, options),
|
|
3013
3840
|
},
|
|
3841
|
+
...(mode === "resource"
|
|
3842
|
+
? [
|
|
3843
|
+
{
|
|
3844
|
+
path: path.join(useCaseDir, `get-${names.singularKebab}.ts`),
|
|
3845
|
+
content: getUseCaseFile(names, config, options),
|
|
3846
|
+
},
|
|
3847
|
+
{
|
|
3848
|
+
path: path.join(useCaseDir, `update-${names.singularKebab}.ts`),
|
|
3849
|
+
content: updateUseCaseFile(names, config, options),
|
|
3850
|
+
},
|
|
3851
|
+
{
|
|
3852
|
+
path: path.join(useCaseDir, `delete-${names.singularKebab}.ts`),
|
|
3853
|
+
content: deleteUseCaseFile(names, config, options),
|
|
3854
|
+
},
|
|
3855
|
+
{
|
|
3856
|
+
path: path.join(featureDir, "policy.ts"),
|
|
3857
|
+
content: resourcePolicyFile(names, config, options),
|
|
3858
|
+
},
|
|
3859
|
+
]
|
|
3860
|
+
: []),
|
|
3861
|
+
...(mode === "resource" && options.events
|
|
3862
|
+
? [
|
|
3863
|
+
{
|
|
3864
|
+
path: path.join(featureDir, "domain/events/index.ts"),
|
|
3865
|
+
content: resourceEventsFile(names),
|
|
3866
|
+
},
|
|
3867
|
+
]
|
|
3868
|
+
: []),
|
|
3869
|
+
...(mode === "resource" && options.auth
|
|
3870
|
+
? [
|
|
3871
|
+
{
|
|
3872
|
+
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
3873
|
+
content: resourcePolicyTestFile(names, options),
|
|
3874
|
+
},
|
|
3875
|
+
]
|
|
3876
|
+
: []),
|
|
3014
3877
|
{
|
|
3015
3878
|
path: path.join(useCaseDir, "index.ts"),
|
|
3016
|
-
content: useCasesIndexFile(names),
|
|
3879
|
+
content: useCasesIndexFile(names, mode),
|
|
3017
3880
|
},
|
|
3018
3881
|
{
|
|
3019
3882
|
path: resourcePortFilePath(names, config),
|
|
3020
|
-
content: repositoryPortFile(names, config),
|
|
3883
|
+
content: repositoryPortFile(names, config, mode, options),
|
|
3021
3884
|
},
|
|
3022
3885
|
{
|
|
3023
3886
|
path: path.join(
|
|
@@ -3025,15 +3888,15 @@ function resourceFiles(
|
|
|
3025
3888
|
names.pluralKebab,
|
|
3026
3889
|
`in-memory-${names.singularKebab}-repository.ts`,
|
|
3027
3890
|
),
|
|
3028
|
-
content: inMemoryRepositoryFile(names, config),
|
|
3891
|
+
content: inMemoryRepositoryFile(names, config, mode, options),
|
|
3029
3892
|
},
|
|
3030
3893
|
{
|
|
3031
3894
|
path: path.join(featureDir, "routes.ts"),
|
|
3032
|
-
content: routeGroupFile(names, config),
|
|
3895
|
+
content: routeGroupFile(names, config, mode, options),
|
|
3033
3896
|
},
|
|
3034
3897
|
{
|
|
3035
3898
|
path: resourceTestFilePath(names, config),
|
|
3036
|
-
content: testFile(names, config),
|
|
3899
|
+
content: testFile(names, config, mode, options),
|
|
3037
3900
|
},
|
|
3038
3901
|
];
|
|
3039
3902
|
|
|
@@ -3041,11 +3904,11 @@ function resourceFiles(
|
|
|
3041
3904
|
files.push(
|
|
3042
3905
|
{
|
|
3043
3906
|
path: drizzleResourceSchemaFilePath(names, config),
|
|
3044
|
-
content: drizzleSchemaFile(names),
|
|
3907
|
+
content: drizzleSchemaFile(names, options),
|
|
3045
3908
|
},
|
|
3046
3909
|
{
|
|
3047
3910
|
path: drizzleResourceRepositoryFilePath(names, config),
|
|
3048
|
-
content: drizzleRepositoryFile(names, config),
|
|
3911
|
+
content: drizzleRepositoryFile(names, config, mode, options),
|
|
3049
3912
|
},
|
|
3050
3913
|
);
|
|
3051
3914
|
}
|
|
@@ -3053,6 +3916,31 @@ function resourceFiles(
|
|
|
3053
3916
|
return files;
|
|
3054
3917
|
}
|
|
3055
3918
|
|
|
3919
|
+
function featureUiComponentFiles(
|
|
3920
|
+
names: FeatureUiNames,
|
|
3921
|
+
config: ResolvedBeignetConfig,
|
|
3922
|
+
): GeneratedFile[] {
|
|
3923
|
+
const componentPath = featureUiComponentFilePath(names, config);
|
|
3924
|
+
|
|
3925
|
+
return [
|
|
3926
|
+
{
|
|
3927
|
+
path: componentPath,
|
|
3928
|
+
content: featureUiComponentFile(names, config),
|
|
3929
|
+
},
|
|
3930
|
+
];
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
function featureUiIndexFile(
|
|
3934
|
+
names: FeatureUiNames,
|
|
3935
|
+
config: ResolvedBeignetConfig,
|
|
3936
|
+
): GeneratedFile {
|
|
3937
|
+
return {
|
|
3938
|
+
path: featureUiIndexFilePath(names, config),
|
|
3939
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";
|
|
3940
|
+
`,
|
|
3941
|
+
};
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3056
3944
|
function contractFiles(
|
|
3057
3945
|
names: ResourceNames,
|
|
3058
3946
|
config: ResolvedBeignetConfig,
|
|
@@ -3151,6 +4039,18 @@ function jobFiles(
|
|
|
3151
4039
|
];
|
|
3152
4040
|
}
|
|
3153
4041
|
|
|
4042
|
+
function taskFiles(
|
|
4043
|
+
names: TaskNames,
|
|
4044
|
+
config: ResolvedBeignetConfig,
|
|
4045
|
+
): GeneratedFile[] {
|
|
4046
|
+
return [
|
|
4047
|
+
{
|
|
4048
|
+
path: taskFilePath(names, config),
|
|
4049
|
+
content: taskFile(names, config),
|
|
4050
|
+
},
|
|
4051
|
+
];
|
|
4052
|
+
}
|
|
4053
|
+
|
|
3154
4054
|
function notificationFiles(
|
|
3155
4055
|
names: NotificationNames,
|
|
3156
4056
|
config: ResolvedBeignetConfig,
|
|
@@ -3283,14 +4183,38 @@ function drizzleResourceRepositoryFilePath(
|
|
|
3283
4183
|
);
|
|
3284
4184
|
}
|
|
3285
4185
|
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
4186
|
+
function resourceSharedErrorsPath(config: ResolvedBeignetConfig): string {
|
|
4187
|
+
return path.join(config.paths.features, "shared", "errors.ts");
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
function resourceContractNames(
|
|
4191
|
+
names: ResourceNames,
|
|
4192
|
+
mode: ResourceGenerationMode,
|
|
4193
|
+
): string[] {
|
|
4194
|
+
const contractNames = [
|
|
4195
|
+
`list${names.pluralPascal}`,
|
|
4196
|
+
`create${names.singularPascal}`,
|
|
4197
|
+
];
|
|
4198
|
+
|
|
4199
|
+
if (mode === "resource") {
|
|
4200
|
+
contractNames.push(
|
|
4201
|
+
`get${names.singularPascal}`,
|
|
4202
|
+
`update${names.singularPascal}`,
|
|
4203
|
+
`delete${names.singularPascal}`,
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
return contractNames;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
async function detectResourcePersistence(
|
|
4211
|
+
targetDir: string,
|
|
4212
|
+
config: ResolvedBeignetConfig,
|
|
4213
|
+
): Promise<ResourcePersistence> {
|
|
4214
|
+
return (await fileExists(
|
|
4215
|
+
path.join(targetDir, drizzleRepositoriesPath(config)),
|
|
4216
|
+
))
|
|
4217
|
+
? "drizzle"
|
|
3294
4218
|
: "memory";
|
|
3295
4219
|
}
|
|
3296
4220
|
|
|
@@ -3301,6 +4225,36 @@ function resourceFeatureDir(
|
|
|
3301
4225
|
return path.join(config.paths.features, names.pluralKebab);
|
|
3302
4226
|
}
|
|
3303
4227
|
|
|
4228
|
+
function featureUiDir(
|
|
4229
|
+
names: FeatureUiNames,
|
|
4230
|
+
config: ResolvedBeignetConfig,
|
|
4231
|
+
): string {
|
|
4232
|
+
return path.join(resourceFeatureDir(names, config), "components");
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
function featureUiComponentFilePath(
|
|
4236
|
+
names: FeatureUiNames,
|
|
4237
|
+
config: ResolvedBeignetConfig,
|
|
4238
|
+
): string {
|
|
4239
|
+
return path.join(
|
|
4240
|
+
featureUiDir(names, config),
|
|
4241
|
+
`${names.componentFileName}.tsx`,
|
|
4242
|
+
);
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
function featureUiIndexFilePath(
|
|
4246
|
+
names: FeatureUiNames,
|
|
4247
|
+
config: ResolvedBeignetConfig,
|
|
4248
|
+
): string {
|
|
4249
|
+
return path.join(featureUiDir(names, config), "index.ts");
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
function clientIndexPath(config: ResolvedBeignetConfig): string {
|
|
4253
|
+
const appRoot = path.posix.dirname(config.paths.appContext);
|
|
4254
|
+
|
|
4255
|
+
return path.join(appRoot === "." ? "" : appRoot, "client/index.ts");
|
|
4256
|
+
}
|
|
4257
|
+
|
|
3304
4258
|
function usesFeatureOwnedContracts(config: ResolvedBeignetConfig): boolean {
|
|
3305
4259
|
return (
|
|
3306
4260
|
directoryPath(config.paths.contracts) ===
|
|
@@ -3365,6 +4319,13 @@ function resourceUseCaseIndexPath(
|
|
|
3365
4319
|
return path.join(resourceUseCaseDir(names, config), "index.ts");
|
|
3366
4320
|
}
|
|
3367
4321
|
|
|
4322
|
+
function resourceSchemaFilePath(
|
|
4323
|
+
names: ResourceNames,
|
|
4324
|
+
config: ResolvedBeignetConfig,
|
|
4325
|
+
): string {
|
|
4326
|
+
return path.join(resourceFeatureDir(names, config), "schemas.ts");
|
|
4327
|
+
}
|
|
4328
|
+
|
|
3368
4329
|
function resourcePortFilePath(
|
|
3369
4330
|
names: ResourceNames,
|
|
3370
4331
|
config: ResolvedBeignetConfig,
|
|
@@ -3430,6 +4391,7 @@ function featureArtifactDir(
|
|
|
3430
4391
|
kind:
|
|
3431
4392
|
| "events"
|
|
3432
4393
|
| "factories"
|
|
4394
|
+
| "tasks"
|
|
3433
4395
|
| "jobs"
|
|
3434
4396
|
| "notifications"
|
|
3435
4397
|
| "listeners"
|
|
@@ -3467,6 +4429,13 @@ function jobFilePath(names: JobNames, config: ResolvedBeignetConfig): string {
|
|
|
3467
4429
|
);
|
|
3468
4430
|
}
|
|
3469
4431
|
|
|
4432
|
+
function taskFilePath(names: TaskNames, config: ResolvedBeignetConfig): string {
|
|
4433
|
+
return path.join(
|
|
4434
|
+
featureArtifactDir(names, "tasks", config),
|
|
4435
|
+
`${names.artifact.kebab}.ts`,
|
|
4436
|
+
);
|
|
4437
|
+
}
|
|
4438
|
+
|
|
3470
4439
|
function factoryFilePath(
|
|
3471
4440
|
names: FactoryNames,
|
|
3472
4441
|
config: ResolvedBeignetConfig,
|
|
@@ -3585,11 +4554,36 @@ async function updateUseCaseIndex(
|
|
|
3585
4554
|
return "updated";
|
|
3586
4555
|
}
|
|
3587
4556
|
|
|
4557
|
+
async function updateFeatureUiIndex(
|
|
4558
|
+
targetDir: string,
|
|
4559
|
+
file: GeneratedFile,
|
|
4560
|
+
options: { dryRun: boolean },
|
|
4561
|
+
): Promise<WriteGeneratedFileResult> {
|
|
4562
|
+
const destination = path.join(targetDir, file.path);
|
|
4563
|
+
const existing = await readOptionalFile(destination);
|
|
4564
|
+
if (existing === undefined) {
|
|
4565
|
+
if (!options.dryRun) {
|
|
4566
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
4567
|
+
await writeFile(destination, file.content);
|
|
4568
|
+
}
|
|
4569
|
+
return "created";
|
|
4570
|
+
}
|
|
4571
|
+
|
|
4572
|
+
if (existing.includes(file.content.trim())) {
|
|
4573
|
+
return "skipped";
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
const next = `${existing.trimEnd()}\n${file.content}`;
|
|
4577
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
4578
|
+
return "updated";
|
|
4579
|
+
}
|
|
4580
|
+
|
|
3588
4581
|
type FeatureArtifactIndexFile = {
|
|
3589
4582
|
path: string;
|
|
3590
4583
|
kind:
|
|
3591
4584
|
| "event"
|
|
3592
4585
|
| "job"
|
|
4586
|
+
| "task"
|
|
3593
4587
|
| "factory"
|
|
3594
4588
|
| "seed"
|
|
3595
4589
|
| "notification"
|
|
@@ -3606,6 +4600,7 @@ function featureArtifactIndexFile(
|
|
|
3606
4600
|
kind:
|
|
3607
4601
|
| "event"
|
|
3608
4602
|
| "job"
|
|
4603
|
+
| "task"
|
|
3609
4604
|
| "factory"
|
|
3610
4605
|
| "seed"
|
|
3611
4606
|
| "notification"
|
|
@@ -3615,6 +4610,7 @@ function featureArtifactIndexFile(
|
|
|
3615
4610
|
names:
|
|
3616
4611
|
| EventNames
|
|
3617
4612
|
| JobNames
|
|
4613
|
+
| TaskNames
|
|
3618
4614
|
| FactoryNames
|
|
3619
4615
|
| SeedNames
|
|
3620
4616
|
| NotificationNames
|
|
@@ -3628,49 +4624,55 @@ function featureArtifactIndexFile(
|
|
|
3628
4624
|
? eventFilePath(names as EventNames, config)
|
|
3629
4625
|
: kind === "job"
|
|
3630
4626
|
? jobFilePath(names as JobNames, config)
|
|
3631
|
-
: kind === "
|
|
3632
|
-
?
|
|
3633
|
-
: kind === "
|
|
3634
|
-
?
|
|
3635
|
-
: kind === "
|
|
3636
|
-
?
|
|
3637
|
-
: kind === "
|
|
3638
|
-
?
|
|
3639
|
-
: kind === "
|
|
3640
|
-
?
|
|
3641
|
-
:
|
|
4627
|
+
: kind === "task"
|
|
4628
|
+
? taskFilePath(names as TaskNames, config)
|
|
4629
|
+
: kind === "factory"
|
|
4630
|
+
? factoryFilePath(names as FactoryNames, config)
|
|
4631
|
+
: kind === "seed"
|
|
4632
|
+
? seedFilePath(names as SeedNames, config)
|
|
4633
|
+
: kind === "notification"
|
|
4634
|
+
? notificationFilePath(names as NotificationNames, config)
|
|
4635
|
+
: kind === "listener"
|
|
4636
|
+
? listenerFilePath(names as ListenerNames, config)
|
|
4637
|
+
: kind === "schedule"
|
|
4638
|
+
? scheduleFilePath(names as ScheduleNames, config)
|
|
4639
|
+
: uploadFilePath(names as UploadNames, config);
|
|
3642
4640
|
const registrySuffix =
|
|
3643
4641
|
kind === "event"
|
|
3644
4642
|
? "Events"
|
|
3645
4643
|
: kind === "job"
|
|
3646
4644
|
? "Jobs"
|
|
3647
|
-
: kind === "
|
|
3648
|
-
? "
|
|
3649
|
-
: kind === "
|
|
3650
|
-
? "
|
|
3651
|
-
: kind === "
|
|
3652
|
-
? "
|
|
3653
|
-
: kind === "
|
|
3654
|
-
? "
|
|
3655
|
-
: kind === "
|
|
3656
|
-
? "
|
|
3657
|
-
: "
|
|
4645
|
+
: kind === "task"
|
|
4646
|
+
? "Tasks"
|
|
4647
|
+
: kind === "factory"
|
|
4648
|
+
? "Factories"
|
|
4649
|
+
: kind === "seed"
|
|
4650
|
+
? "Seeds"
|
|
4651
|
+
: kind === "notification"
|
|
4652
|
+
? "Notifications"
|
|
4653
|
+
: kind === "listener"
|
|
4654
|
+
? "Listeners"
|
|
4655
|
+
: kind === "schedule"
|
|
4656
|
+
? "Schedules"
|
|
4657
|
+
: "Uploads";
|
|
3658
4658
|
const importName =
|
|
3659
4659
|
kind === "event"
|
|
3660
4660
|
? (names as EventNames).eventExportName
|
|
3661
4661
|
: kind === "job"
|
|
3662
4662
|
? (names as JobNames).jobExportName
|
|
3663
|
-
: kind === "
|
|
3664
|
-
? (names as
|
|
3665
|
-
: kind === "
|
|
3666
|
-
? (names as
|
|
3667
|
-
: kind === "
|
|
3668
|
-
? (names as
|
|
3669
|
-
: kind === "
|
|
3670
|
-
? (names as
|
|
3671
|
-
: kind === "
|
|
3672
|
-
? (names as
|
|
3673
|
-
:
|
|
4663
|
+
: kind === "task"
|
|
4664
|
+
? (names as TaskNames).taskExportName
|
|
4665
|
+
: kind === "factory"
|
|
4666
|
+
? (names as FactoryNames).factoryExportName
|
|
4667
|
+
: kind === "seed"
|
|
4668
|
+
? (names as SeedNames).seedExportName
|
|
4669
|
+
: kind === "notification"
|
|
4670
|
+
? (names as NotificationNames).notificationExportName
|
|
4671
|
+
: kind === "listener"
|
|
4672
|
+
? (names as ListenerNames).listenerExportName
|
|
4673
|
+
: kind === "schedule"
|
|
4674
|
+
? (names as ScheduleNames).scheduleExportName
|
|
4675
|
+
: (names as UploadNames).uploadExportName;
|
|
3674
4676
|
|
|
3675
4677
|
return {
|
|
3676
4678
|
path: path.join(path.dirname(artifactPath), "index.ts"),
|
|
@@ -3786,6 +4788,150 @@ export const ${file.registryName} = [${file.importName}] as const;
|
|
|
3786
4788
|
return "updated";
|
|
3787
4789
|
}
|
|
3788
4790
|
|
|
4791
|
+
async function updateTaskRegistry(
|
|
4792
|
+
targetDir: string,
|
|
4793
|
+
names: TaskNames,
|
|
4794
|
+
config: ResolvedBeignetConfig,
|
|
4795
|
+
options: { dryRun: boolean },
|
|
4796
|
+
): Promise<WriteGeneratedFileResult> {
|
|
4797
|
+
const destination = path.join(targetDir, config.paths.tasks);
|
|
4798
|
+
const existing = await readOptionalFile(destination);
|
|
4799
|
+
const featureTasksPath = path.join(
|
|
4800
|
+
featureArtifactDir(names, "tasks", config),
|
|
4801
|
+
"index.ts",
|
|
4802
|
+
);
|
|
4803
|
+
const registryName = `${names.featureSingularCamel}Tasks`;
|
|
4804
|
+
const importLine = `import { ${registryName} } from "${relativeModule(
|
|
4805
|
+
config.paths.tasks,
|
|
4806
|
+
featureTasksPath,
|
|
4807
|
+
)}";`;
|
|
4808
|
+
const defineTasksImport = `import { defineTasks } from "@beignet/core/tasks";`;
|
|
4809
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
4810
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
4811
|
+
const serverImport = `import { server } from "${relativeModule(
|
|
4812
|
+
config.paths.tasks,
|
|
4813
|
+
config.paths.server,
|
|
4814
|
+
)}";`;
|
|
4815
|
+
const registryEntry = `...${registryName}`;
|
|
4816
|
+
|
|
4817
|
+
if (existing === undefined) {
|
|
4818
|
+
const content = `${defineTasksImport}
|
|
4819
|
+
${serviceActorImport}
|
|
4820
|
+
${appContextImport}
|
|
4821
|
+
${importLine}
|
|
4822
|
+
${serverImport}
|
|
4823
|
+
|
|
4824
|
+
export const tasks = defineTasks([
|
|
4825
|
+
\t${registryEntry},
|
|
4826
|
+
] as const);
|
|
4827
|
+
|
|
4828
|
+
export async function createTaskContext(): Promise<AppContext> {
|
|
4829
|
+
\treturn server.createServiceContext({
|
|
4830
|
+
\t\tactor: createServiceActor("beignet-cli"),
|
|
4831
|
+
\t});
|
|
4832
|
+
}
|
|
4833
|
+
|
|
4834
|
+
export async function stopTaskContext(): Promise<void> {
|
|
4835
|
+
\tawait server.stop();
|
|
4836
|
+
}
|
|
4837
|
+
`;
|
|
4838
|
+
if (!options.dryRun) {
|
|
4839
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
4840
|
+
await writeFile(destination, content);
|
|
4841
|
+
}
|
|
4842
|
+
return "created";
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
let next = existing;
|
|
4846
|
+
if (!next.includes(defineTasksImport)) {
|
|
4847
|
+
next = insertAfterImports(next, defineTasksImport);
|
|
4848
|
+
}
|
|
4849
|
+
if (!next.includes(importLine)) {
|
|
4850
|
+
next = insertAfterImports(next, importLine);
|
|
4851
|
+
}
|
|
4852
|
+
|
|
4853
|
+
const registry = arrayInitializerInfo(next, "tasks");
|
|
4854
|
+
if (!registry) {
|
|
4855
|
+
next = `${next.trimEnd()}\n\nexport const tasks = defineTasks([\n\t${registryEntry},\n] as const);\n`;
|
|
4856
|
+
} else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
4857
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
4858
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(
|
|
4859
|
+
registry.end,
|
|
4860
|
+
)}`;
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
if (next === existing) return "skipped";
|
|
4864
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
4865
|
+
return "updated";
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
async function updateScheduleRegistry(
|
|
4869
|
+
targetDir: string,
|
|
4870
|
+
names: ScheduleNames,
|
|
4871
|
+
config: ResolvedBeignetConfig,
|
|
4872
|
+
options: { dryRun: boolean },
|
|
4873
|
+
): Promise<WriteGeneratedFileResult> {
|
|
4874
|
+
const destination = path.join(targetDir, config.paths.schedules);
|
|
4875
|
+
const existing = await readOptionalFile(destination);
|
|
4876
|
+
const featureSchedulesPath = path.join(
|
|
4877
|
+
featureArtifactDir(names, "schedules", config),
|
|
4878
|
+
"index.ts",
|
|
4879
|
+
);
|
|
4880
|
+
const registryName = `${names.featureSingularCamel}Schedules`;
|
|
4881
|
+
const importLine = `import { ${registryName} } from "${aliasModule(featureSchedulesPath)}";`;
|
|
4882
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
4883
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
4884
|
+
const serverImport = `import { server } from "${relativeModule(
|
|
4885
|
+
config.paths.schedules,
|
|
4886
|
+
config.paths.server,
|
|
4887
|
+
)}";`;
|
|
4888
|
+
const registryEntry = `...${registryName}`;
|
|
4889
|
+
|
|
4890
|
+
if (existing === undefined) {
|
|
4891
|
+
const content = `${serviceActorImport}
|
|
4892
|
+
${appContextImport}
|
|
4893
|
+
${importLine}
|
|
4894
|
+
${serverImport}
|
|
4895
|
+
|
|
4896
|
+
export const schedules = [${registryEntry}] as const;
|
|
4897
|
+
|
|
4898
|
+
export async function createScheduleContext(): Promise<AppContext> {
|
|
4899
|
+
\treturn server.createServiceContext({
|
|
4900
|
+
\t\tactor: createServiceActor("beignet-schedule"),
|
|
4901
|
+
\t});
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
export async function stopScheduleContext(): Promise<void> {
|
|
4905
|
+
\tawait server.stop();
|
|
4906
|
+
}
|
|
4907
|
+
`;
|
|
4908
|
+
if (!options.dryRun) {
|
|
4909
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
4910
|
+
await writeFile(destination, content);
|
|
4911
|
+
}
|
|
4912
|
+
return "created";
|
|
4913
|
+
}
|
|
4914
|
+
|
|
4915
|
+
let next = existing;
|
|
4916
|
+
if (!next.includes(importLine)) {
|
|
4917
|
+
next = insertAfterImports(next, importLine);
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
const registry = arrayInitializerInfo(next, "schedules");
|
|
4921
|
+
if (!registry) {
|
|
4922
|
+
next = `${next.trimEnd()}\n\nexport const schedules = [${registryEntry}] as const;\n`;
|
|
4923
|
+
} else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
4924
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
4925
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(
|
|
4926
|
+
registry.end,
|
|
4927
|
+
)}`;
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
if (next === existing) return "skipped";
|
|
4931
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
4932
|
+
return "updated";
|
|
4933
|
+
}
|
|
4934
|
+
|
|
3789
4935
|
function arrayInitializerInfo(
|
|
3790
4936
|
source: string,
|
|
3791
4937
|
constName: string,
|
|
@@ -3828,27 +4974,110 @@ function defineUploadsInitializerInfo(
|
|
|
3828
4974
|
};
|
|
3829
4975
|
}
|
|
3830
4976
|
|
|
3831
|
-
function schemasFile(
|
|
4977
|
+
function schemasFile(
|
|
4978
|
+
names: ResourceNames,
|
|
4979
|
+
mode: ResourceGenerationMode,
|
|
4980
|
+
options: ResourceGenerationOptions,
|
|
4981
|
+
): string {
|
|
3832
4982
|
return `import { z } from "zod";
|
|
3833
4983
|
|
|
3834
4984
|
export const ${names.singularPascal}Schema = z.object({
|
|
3835
4985
|
id: z.string().uuid(),
|
|
3836
|
-
name: z.string().min(1),
|
|
4986
|
+
${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
4987
|
+
version: z.number().int().min(1),
|
|
3837
4988
|
createdAt: z.string().datetime(),
|
|
4989
|
+
updatedAt: z.string().datetime(),
|
|
3838
4990
|
});
|
|
3839
4991
|
|
|
3840
|
-
export const
|
|
3841
|
-
|
|
3842
|
-
|
|
4992
|
+
export const ${names.singularPascal}SortBySchema = z.enum(["createdAt", "name"]);
|
|
4993
|
+
export const ${names.singularPascal}SortDirectionSchema = z.enum(["asc", "desc"]);
|
|
4994
|
+
|
|
4995
|
+
const ${names.singularPascal}CursorPayloadSchema = z.object({
|
|
4996
|
+
sortBy: ${names.singularPascal}SortBySchema,
|
|
4997
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema,
|
|
4998
|
+
sortValue: z.string(),
|
|
4999
|
+
id: z.string().uuid(),
|
|
3843
5000
|
});
|
|
3844
5001
|
|
|
5002
|
+
export type ${names.singularPascal}SortBy = z.infer<
|
|
5003
|
+
typeof ${names.singularPascal}SortBySchema
|
|
5004
|
+
>;
|
|
5005
|
+
export type ${names.singularPascal}SortDirection = z.infer<
|
|
5006
|
+
typeof ${names.singularPascal}SortDirectionSchema
|
|
5007
|
+
>;
|
|
5008
|
+
export type ${names.singularPascal}Cursor = z.infer<
|
|
5009
|
+
typeof ${names.singularPascal}CursorPayloadSchema
|
|
5010
|
+
>;
|
|
5011
|
+
|
|
5012
|
+
function encodeBase64Url(value: string): string {
|
|
5013
|
+
const bytes = new TextEncoder().encode(value);
|
|
5014
|
+
let binary = "";
|
|
5015
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
5016
|
+
|
|
5017
|
+
return btoa(binary)
|
|
5018
|
+
.replaceAll("+", "-")
|
|
5019
|
+
.replaceAll("/", "_")
|
|
5020
|
+
.replaceAll("=", "");
|
|
5021
|
+
}
|
|
5022
|
+
|
|
5023
|
+
function decodeBase64Url(value: string): string {
|
|
5024
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
5025
|
+
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
|
5026
|
+
const binary = atob(base64 + padding);
|
|
5027
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
5028
|
+
|
|
5029
|
+
return new TextDecoder().decode(bytes);
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
export function encode${names.singularPascal}Cursor(cursor: ${names.singularPascal}Cursor): string {
|
|
5033
|
+
return encodeBase64Url(JSON.stringify(cursor));
|
|
5034
|
+
}
|
|
5035
|
+
|
|
5036
|
+
export function decode${names.singularPascal}Cursor(cursor: string): ${names.singularPascal}Cursor {
|
|
5037
|
+
return ${names.singularPascal}CursorPayloadSchema.parse(
|
|
5038
|
+
JSON.parse(decodeBase64Url(cursor)),
|
|
5039
|
+
);
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
export const List${names.pluralPascal}InputSchema = z
|
|
5043
|
+
.object({
|
|
5044
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
5045
|
+
cursor: z.string().nullable().default(null),
|
|
5046
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
5047
|
+
sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
|
|
5048
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
|
|
5049
|
+
})
|
|
5050
|
+
.superRefine((input, ctx) => {
|
|
5051
|
+
if (!input.cursor) return;
|
|
5052
|
+
|
|
5053
|
+
try {
|
|
5054
|
+
const cursor = decode${names.singularPascal}Cursor(input.cursor);
|
|
5055
|
+
if (
|
|
5056
|
+
cursor.sortBy !== input.sortBy ||
|
|
5057
|
+
cursor.sortDirection !== input.sortDirection
|
|
5058
|
+
) {
|
|
5059
|
+
ctx.addIssue({
|
|
5060
|
+
code: z.ZodIssueCode.custom,
|
|
5061
|
+
message: "cursor does not match the requested sort.",
|
|
5062
|
+
path: ["cursor"],
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
} catch {
|
|
5066
|
+
ctx.addIssue({
|
|
5067
|
+
code: z.ZodIssueCode.custom,
|
|
5068
|
+
message: "cursor is invalid.",
|
|
5069
|
+
path: ["cursor"],
|
|
5070
|
+
});
|
|
5071
|
+
}
|
|
5072
|
+
});
|
|
5073
|
+
|
|
3845
5074
|
export const List${names.pluralPascal}OutputSchema = z.object({
|
|
3846
5075
|
items: z.array(${names.singularPascal}Schema),
|
|
3847
5076
|
page: z.object({
|
|
3848
|
-
kind: z.literal("
|
|
5077
|
+
kind: z.literal("cursor"),
|
|
3849
5078
|
limit: z.number().int().min(1),
|
|
3850
|
-
|
|
3851
|
-
|
|
5079
|
+
cursor: z.string().nullable(),
|
|
5080
|
+
nextCursor: z.string().nullable(),
|
|
3852
5081
|
hasMore: z.boolean(),
|
|
3853
5082
|
}),
|
|
3854
5083
|
});
|
|
@@ -3856,11 +5085,42 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
3856
5085
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
3857
5086
|
name: z.string().min(1).max(120),
|
|
3858
5087
|
});
|
|
5088
|
+
${
|
|
5089
|
+
mode === "resource"
|
|
5090
|
+
? `
|
|
5091
|
+
export const ${names.singularPascal}IdInputSchema = z.object({
|
|
5092
|
+
id: z.string().uuid(),
|
|
5093
|
+
});
|
|
5094
|
+
|
|
5095
|
+
export const Update${names.singularPascal}BodySchema = z.object({
|
|
5096
|
+
name: z.string().min(1).max(120),
|
|
5097
|
+
version: z.number().int().min(1),
|
|
5098
|
+
});
|
|
3859
5099
|
|
|
5100
|
+
export const Update${names.singularPascal}InputSchema =
|
|
5101
|
+
${names.singularPascal}IdInputSchema.merge(Update${names.singularPascal}BodySchema);
|
|
5102
|
+
`
|
|
5103
|
+
: ""
|
|
5104
|
+
}
|
|
3860
5105
|
export type ${names.singularPascal} = z.infer<typeof ${names.singularPascal}Schema>;
|
|
3861
5106
|
export type Create${names.singularPascal}Input = z.infer<
|
|
3862
5107
|
typeof Create${names.singularPascal}InputSchema
|
|
3863
5108
|
>;
|
|
5109
|
+
${
|
|
5110
|
+
mode === "resource"
|
|
5111
|
+
? `
|
|
5112
|
+
export type ${names.singularPascal}IdInput = z.infer<
|
|
5113
|
+
typeof ${names.singularPascal}IdInputSchema
|
|
5114
|
+
>;
|
|
5115
|
+
export type Update${names.singularPascal}Body = z.infer<
|
|
5116
|
+
typeof Update${names.singularPascal}BodySchema
|
|
5117
|
+
>;
|
|
5118
|
+
export type Update${names.singularPascal}Input = z.infer<
|
|
5119
|
+
typeof Update${names.singularPascal}InputSchema
|
|
5120
|
+
>;
|
|
5121
|
+
`
|
|
5122
|
+
: ""
|
|
5123
|
+
}
|
|
3864
5124
|
export type List${names.pluralPascal}Input = z.infer<
|
|
3865
5125
|
typeof List${names.pluralPascal}InputSchema
|
|
3866
5126
|
>;
|
|
@@ -3870,30 +5130,39 @@ export type List${names.pluralPascal}Input = z.infer<
|
|
|
3870
5130
|
function listUseCaseFile(
|
|
3871
5131
|
names: ResourceNames,
|
|
3872
5132
|
config: ResolvedBeignetConfig,
|
|
5133
|
+
options: ResourceGenerationOptions,
|
|
3873
5134
|
): string {
|
|
3874
5135
|
const filePath = path.join(
|
|
3875
5136
|
resourceUseCaseDir(names, config),
|
|
3876
5137
|
`list-${names.pluralKebab}.ts`,
|
|
3877
5138
|
);
|
|
3878
5139
|
|
|
3879
|
-
return `import {
|
|
5140
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5141
|
+
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
3880
5142
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3881
5143
|
import {
|
|
5144
|
+
decode${names.singularPascal}Cursor,
|
|
3882
5145
|
List${names.pluralPascal}InputSchema,
|
|
3883
5146
|
List${names.pluralPascal}OutputSchema,
|
|
3884
|
-
} from "
|
|
5147
|
+
} from "../schemas";
|
|
3885
5148
|
|
|
3886
5149
|
export const list${names.pluralPascal}UseCase = useCase
|
|
3887
5150
|
.query("${names.pluralCamel}.list")
|
|
3888
5151
|
.input(List${names.pluralPascal}InputSchema)
|
|
3889
5152
|
.output(List${names.pluralPascal}OutputSchema)
|
|
3890
5153
|
.run(async ({ ctx, input }) => {
|
|
3891
|
-
const page =
|
|
5154
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to list ${names.pluralKebab}.");\n\t\t}\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
3892
5155
|
defaultLimit: 20,
|
|
3893
5156
|
maxLimit: 100,
|
|
3894
5157
|
});
|
|
3895
5158
|
|
|
3896
|
-
return ctx.ports.${names.pluralCamel}.list(
|
|
5159
|
+
return ctx.ports.${names.pluralCamel}.list({
|
|
5160
|
+
page,
|
|
5161
|
+
cursor: page.cursor ? decode${names.singularPascal}Cursor(page.cursor) : null,
|
|
5162
|
+
name: input.name,
|
|
5163
|
+
sortBy: input.sortBy,
|
|
5164
|
+
sortDirection: input.sortDirection,
|
|
5165
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3897
5166
|
});
|
|
3898
5167
|
`;
|
|
3899
5168
|
}
|
|
@@ -3901,134 +5170,430 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
3901
5170
|
function createUseCaseFile(
|
|
3902
5171
|
names: ResourceNames,
|
|
3903
5172
|
config: ResolvedBeignetConfig,
|
|
5173
|
+
options: ResourceGenerationOptions,
|
|
3904
5174
|
): string {
|
|
3905
5175
|
const filePath = path.join(
|
|
3906
5176
|
resourceUseCaseDir(names, config),
|
|
3907
5177
|
`create-${names.singularKebab}.ts`,
|
|
3908
5178
|
);
|
|
3909
5179
|
|
|
3910
|
-
return `import {
|
|
5180
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5181
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3911
5182
|
import {
|
|
3912
5183
|
Create${names.singularPascal}InputSchema,
|
|
3913
5184
|
${names.singularPascal}Schema,
|
|
3914
|
-
} from "
|
|
5185
|
+
} from "../schemas";
|
|
5186
|
+
${options.events ? `import { ${names.singularPascal}Created } from "../domain/events";\n` : ""}
|
|
3915
5187
|
|
|
3916
5188
|
export const create${names.singularPascal}UseCase = useCase
|
|
3917
5189
|
.command("${names.pluralCamel}.create")
|
|
3918
5190
|
.input(Create${names.singularPascal}InputSchema)
|
|
3919
5191
|
.output(${names.singularPascal}Schema)
|
|
3920
|
-
.run(async ({ ctx, input }) =>
|
|
5192
|
+
.run(async ({ ctx, input }) => {
|
|
5193
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to create ${names.singularKebab}.");\n\t\t}\n\n` : ""}${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create({
|
|
5194
|
+
...input,
|
|
5195
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
5196
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
5197
|
+
return ${names.singularCamel};
|
|
5198
|
+
});
|
|
5199
|
+
`;
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
function getUseCaseFile(
|
|
5203
|
+
names: ResourceNames,
|
|
5204
|
+
config: ResolvedBeignetConfig,
|
|
5205
|
+
options: ResourceGenerationOptions,
|
|
5206
|
+
): string {
|
|
5207
|
+
const filePath = path.join(
|
|
5208
|
+
resourceUseCaseDir(names, config),
|
|
5209
|
+
`get-${names.singularKebab}.ts`,
|
|
5210
|
+
);
|
|
5211
|
+
|
|
5212
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5213
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5214
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5215
|
+
import {
|
|
5216
|
+
${names.singularPascal}IdInputSchema,
|
|
5217
|
+
${names.singularPascal}Schema,
|
|
5218
|
+
} from "../schemas";
|
|
5219
|
+
|
|
5220
|
+
export const get${names.singularPascal}UseCase = useCase
|
|
5221
|
+
.query("${names.pluralCamel}.get")
|
|
5222
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
5223
|
+
.output(${names.singularPascal}Schema)
|
|
5224
|
+
.run(async ({ ctx, input }) => {
|
|
5225
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to get ${names.singularKebab}.");\n\t\t}\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
5226
|
+
if (!${names.singularCamel}) {
|
|
5227
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5228
|
+
details: { id: input.id },
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5231
|
+
|
|
5232
|
+
return ${names.singularCamel};
|
|
5233
|
+
});
|
|
5234
|
+
`;
|
|
5235
|
+
}
|
|
5236
|
+
|
|
5237
|
+
function updateUseCaseFile(
|
|
5238
|
+
names: ResourceNames,
|
|
5239
|
+
config: ResolvedBeignetConfig,
|
|
5240
|
+
options: ResourceGenerationOptions,
|
|
5241
|
+
): string {
|
|
5242
|
+
const filePath = path.join(
|
|
5243
|
+
resourceUseCaseDir(names, config),
|
|
5244
|
+
`update-${names.singularKebab}.ts`,
|
|
5245
|
+
);
|
|
5246
|
+
|
|
5247
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5248
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5249
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5250
|
+
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
5251
|
+
Update${names.singularPascal}InputSchema,
|
|
5252
|
+
${names.singularPascal}Schema,
|
|
5253
|
+
} from "../schemas";
|
|
5254
|
+
|
|
5255
|
+
export const update${names.singularPascal}UseCase = useCase
|
|
5256
|
+
.command("${names.pluralCamel}.update")
|
|
5257
|
+
.input(Update${names.singularPascal}InputSchema)
|
|
5258
|
+
.output(${names.singularPascal}Schema)
|
|
5259
|
+
.run(async ({ ctx, input }) => {
|
|
5260
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to update ${names.singularKebab}.");\n\t\t}\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
5261
|
+
if (!existing) {
|
|
5262
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5263
|
+
details: { id: input.id },
|
|
5264
|
+
});
|
|
5265
|
+
}
|
|
5266
|
+
${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
5267
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
5268
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
5269
|
+
});
|
|
5270
|
+
}
|
|
5271
|
+
|
|
5272
|
+
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
5273
|
+
...input,
|
|
5274
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
5275
|
+
if (!${names.singularCamel}) {
|
|
5276
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
5277
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
5278
|
+
});
|
|
5279
|
+
}
|
|
5280
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Updated, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
5281
|
+
return ${names.singularCamel};
|
|
5282
|
+
});
|
|
5283
|
+
`;
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
function deleteUseCaseFile(
|
|
5287
|
+
names: ResourceNames,
|
|
5288
|
+
config: ResolvedBeignetConfig,
|
|
5289
|
+
options: ResourceGenerationOptions,
|
|
5290
|
+
): string {
|
|
5291
|
+
const filePath = path.join(
|
|
5292
|
+
resourceUseCaseDir(names, config),
|
|
5293
|
+
`delete-${names.singularKebab}.ts`,
|
|
5294
|
+
);
|
|
5295
|
+
|
|
5296
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5297
|
+
import { z } from "zod";
|
|
5298
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5299
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5300
|
+
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
5301
|
+
|
|
5302
|
+
export const delete${names.singularPascal}UseCase = useCase
|
|
5303
|
+
.command("${names.pluralCamel}.delete")
|
|
5304
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
5305
|
+
.output(z.void())
|
|
5306
|
+
.run(async ({ ctx, input }) => {
|
|
5307
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to delete ${names.singularKebab}.");\n\t\t}\n\n` : ""}${options.auth ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
5308
|
+
if (!deleted) {
|
|
5309
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5310
|
+
details: { id: input.id },
|
|
5311
|
+
});
|
|
5312
|
+
}
|
|
5313
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Deleted, {\n\t\t\tid: input.id,\n\t\t});\n` : ""} });
|
|
3921
5314
|
`;
|
|
3922
5315
|
}
|
|
3923
5316
|
|
|
3924
|
-
function useCasesIndexFile(
|
|
5317
|
+
function useCasesIndexFile(
|
|
5318
|
+
names: ResourceNames,
|
|
5319
|
+
mode: ResourceGenerationMode,
|
|
5320
|
+
): string {
|
|
3925
5321
|
return `export { create${names.singularPascal}UseCase } from "./create-${names.singularKebab}";
|
|
3926
|
-
export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
3927
|
-
export {
|
|
5322
|
+
${mode === "resource" ? `export { delete${names.singularPascal}UseCase } from "./delete-${names.singularKebab}";\nexport { get${names.singularPascal}UseCase } from "./get-${names.singularKebab}";\n` : ""}export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
5323
|
+
${mode === "resource" ? `export { update${names.singularPascal}UseCase } from "./update-${names.singularKebab}";\n` : ""}export {
|
|
3928
5324
|
Create${names.singularPascal}InputSchema,
|
|
3929
|
-
List${names.pluralPascal}InputSchema,
|
|
5325
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n` : ""} List${names.pluralPascal}InputSchema,
|
|
3930
5326
|
List${names.pluralPascal}OutputSchema,
|
|
3931
|
-
${names.singularPascal}Schema,
|
|
5327
|
+
${mode === "resource" ? ` Update${names.singularPascal}BodySchema,\n Update${names.singularPascal}InputSchema,\n` : ""} ${names.singularPascal}Schema,
|
|
3932
5328
|
type Create${names.singularPascal}Input,
|
|
3933
|
-
type List${names.pluralPascal}Input,
|
|
5329
|
+
${mode === "resource" ? ` type ${names.singularPascal}IdInput,\n type Update${names.singularPascal}Body,\n type Update${names.singularPascal}Input,\n` : ""} type List${names.pluralPascal}Input,
|
|
3934
5330
|
type ${names.singularPascal},
|
|
3935
|
-
} from "
|
|
5331
|
+
} from "../schemas";
|
|
3936
5332
|
`;
|
|
3937
5333
|
}
|
|
3938
5334
|
|
|
3939
5335
|
function repositoryPortFile(
|
|
3940
5336
|
names: ResourceNames,
|
|
3941
5337
|
config: ResolvedBeignetConfig,
|
|
5338
|
+
mode: ResourceGenerationMode,
|
|
5339
|
+
options: ResourceGenerationOptions,
|
|
3942
5340
|
): string {
|
|
3943
5341
|
return `import type {
|
|
3944
|
-
|
|
3945
|
-
|
|
5342
|
+
CursorPage,
|
|
5343
|
+
CursorPageInfo,
|
|
3946
5344
|
PageResult,
|
|
5345
|
+
SortDirection,
|
|
3947
5346
|
} from "@beignet/core/pagination";
|
|
3948
5347
|
import type {
|
|
3949
5348
|
Create${names.singularPascal}Input,
|
|
3950
|
-
${names.singularPascal},
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
5349
|
+
${names.singularPascal}Cursor,
|
|
5350
|
+
${names.singularPascal}SortBy,
|
|
5351
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5352
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
5353
|
+
|
|
5354
|
+
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal}, CursorPageInfo>;
|
|
5355
|
+
export type List${names.pluralPascal}Query = {
|
|
5356
|
+
page: CursorPage;
|
|
5357
|
+
cursor: ${names.singularPascal}Cursor | null;
|
|
5358
|
+
name?: string;
|
|
5359
|
+
sortBy: ${names.singularPascal}SortBy;
|
|
5360
|
+
sortDirection: SortDirection;
|
|
5361
|
+
${options.tenant ? `\ttenantId: string;\n` : ""}};
|
|
5362
|
+
${options.tenant ? `export type ${names.singularPascal}TenantFilter = { tenantId: string };\nexport type Create${names.singularPascal}RepositoryInput = Create${names.singularPascal}Input & ${names.singularPascal}TenantFilter;\n${mode === "resource" ? `export type Update${names.singularPascal}RepositoryInput = Update${names.singularPascal}Input & ${names.singularPascal}TenantFilter;\n` : ""}` : ""}
|
|
3954
5363
|
|
|
3955
5364
|
export interface ${names.singularPascal}Repository {
|
|
3956
|
-
list(
|
|
3957
|
-
create(input: Create${names.singularPascal}Input): Promise<${names.singularPascal}>;
|
|
3958
|
-
}
|
|
5365
|
+
list(query: List${names.pluralPascal}Query): Promise<List${names.pluralPascal}Result>;
|
|
5366
|
+
create(input: ${options.tenant ? `Create${names.singularPascal}RepositoryInput` : `Create${names.singularPascal}Input`}): Promise<${names.singularPascal}>;
|
|
5367
|
+
${mode === "resource" ? ` findById(id: string${options.tenant ? `, filter: ${names.singularPascal}TenantFilter` : ""}): Promise<${names.singularPascal} | null>;\n update(input: ${options.tenant ? `Update${names.singularPascal}RepositoryInput` : `Update${names.singularPascal}Input`}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenant ? `, filter: ${names.singularPascal}TenantFilter` : ""}): Promise<boolean>;\n` : ""}}
|
|
3959
5368
|
`;
|
|
3960
5369
|
}
|
|
3961
5370
|
|
|
3962
5371
|
function inMemoryRepositoryFile(
|
|
3963
5372
|
names: ResourceNames,
|
|
3964
5373
|
config: ResolvedBeignetConfig,
|
|
5374
|
+
mode: ResourceGenerationMode,
|
|
5375
|
+
options: ResourceGenerationOptions,
|
|
3965
5376
|
): string {
|
|
3966
5377
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
5378
|
+
const recordType = options.softDelete
|
|
5379
|
+
? `type ${names.singularPascal}Record = ${names.singularPascal} & {
|
|
5380
|
+
\tdeletedAt: string | null;
|
|
5381
|
+
};
|
|
3967
5382
|
|
|
3968
|
-
|
|
5383
|
+
`
|
|
5384
|
+
: "";
|
|
5385
|
+
const mapValueType = options.softDelete
|
|
5386
|
+
? `${names.singularPascal}Record`
|
|
5387
|
+
: names.singularPascal;
|
|
5388
|
+
const activeFilter = options.softDelete
|
|
5389
|
+
? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.deletedAt === null)\n`
|
|
5390
|
+
: "";
|
|
5391
|
+
const newDeletedAtField = options.softDelete
|
|
5392
|
+
? `\t\t\t\tdeletedAt: null,\n`
|
|
5393
|
+
: "";
|
|
5394
|
+
const findDeletedGuard = options.softDelete
|
|
5395
|
+
? `\t\t\tif (${names.singularCamel}?.deletedAt !== null) return null;\n`
|
|
5396
|
+
: "";
|
|
5397
|
+
const updateDeletedGuard = options.softDelete
|
|
5398
|
+
? " || existing.deletedAt !== null"
|
|
5399
|
+
: "";
|
|
5400
|
+
const deleteImplementation = options.softDelete
|
|
5401
|
+
? `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== filter.tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
|
|
5402
|
+
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
5403
|
+
return true;`
|
|
5404
|
+
: `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== filter.tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
|
|
5405
|
+
|
|
5406
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5407
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
3969
5408
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
5409
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3970
5410
|
import type {
|
|
3971
5411
|
Create${names.singularPascal}Input,
|
|
3972
|
-
${names.singularPascal},
|
|
3973
|
-
} from "${aliasModule(
|
|
5412
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5413
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
5414
|
+
|
|
5415
|
+
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
5416
|
+
return {
|
|
5417
|
+
id: ${names.singularCamel}.id,
|
|
5418
|
+
${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
5419
|
+
version: ${names.singularCamel}.version,
|
|
5420
|
+
createdAt: ${names.singularCamel}.createdAt,
|
|
5421
|
+
updatedAt: ${names.singularCamel}.updatedAt,
|
|
5422
|
+
};
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
function compare${names.pluralPascal}(
|
|
5426
|
+
left: ${mapValueType},
|
|
5427
|
+
right: ${mapValueType},
|
|
5428
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5429
|
+
): number {
|
|
5430
|
+
const leftValue = left[query.sortBy];
|
|
5431
|
+
const rightValue = right[query.sortBy];
|
|
5432
|
+
const fieldComparison =
|
|
5433
|
+
leftValue === rightValue ? left.id.localeCompare(right.id) : leftValue.localeCompare(rightValue);
|
|
5434
|
+
|
|
5435
|
+
return query.sortDirection === "asc" ? fieldComparison : -fieldComparison;
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
function isAfter${names.singularPascal}Cursor(
|
|
5439
|
+
${names.singularCamel}: ${mapValueType},
|
|
5440
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5441
|
+
): boolean {
|
|
5442
|
+
if (!query.cursor) return true;
|
|
5443
|
+
|
|
5444
|
+
const sortValue = ${names.singularCamel}[query.sortBy];
|
|
5445
|
+
const fieldComparison =
|
|
5446
|
+
sortValue === query.cursor.sortValue
|
|
5447
|
+
? ${names.singularCamel}.id.localeCompare(query.cursor.id)
|
|
5448
|
+
: sortValue.localeCompare(query.cursor.sortValue);
|
|
5449
|
+
|
|
5450
|
+
return query.sortDirection === "asc" ? fieldComparison > 0 : fieldComparison < 0;
|
|
5451
|
+
}
|
|
5452
|
+
|
|
5453
|
+
function cursorFor${names.singularPascal}(
|
|
5454
|
+
${names.singularCamel}: ${mapValueType},
|
|
5455
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5456
|
+
): string {
|
|
5457
|
+
return encode${names.singularPascal}Cursor({
|
|
5458
|
+
sortBy: query.sortBy,
|
|
5459
|
+
sortDirection: query.sortDirection,
|
|
5460
|
+
sortValue: ${names.singularCamel}[query.sortBy],
|
|
5461
|
+
id: ${names.singularCamel}.id,
|
|
5462
|
+
});
|
|
5463
|
+
}
|
|
3974
5464
|
|
|
3975
5465
|
export function createInMemory${names.singularPascal}Repository(
|
|
3976
5466
|
seed: ${names.singularPascal}[] = [],
|
|
3977
5467
|
): ${names.singularPascal}Repository {
|
|
3978
|
-
const ${names.pluralCamel} = new Map
|
|
5468
|
+
const ${names.pluralCamel} = new Map<string, ${mapValueType}>(
|
|
5469
|
+
seed.map((${names.singularCamel}) => [
|
|
5470
|
+
${names.singularCamel}.id,
|
|
5471
|
+
{ ...${names.singularCamel}${options.softDelete ? ", deletedAt: null" : ""} },
|
|
5472
|
+
]),
|
|
5473
|
+
);
|
|
3979
5474
|
|
|
3980
5475
|
return {
|
|
3981
|
-
async list(
|
|
3982
|
-
const
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
all${names.pluralPascal}.length
|
|
5476
|
+
async list(query) {
|
|
5477
|
+
const name = query.name?.toLocaleLowerCase();
|
|
5478
|
+
const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
5479
|
+
${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === query.tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
|
|
5480
|
+
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
5481
|
+
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
5482
|
+
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
5483
|
+
const nextCursor =
|
|
5484
|
+
all${names.pluralPascal}.length > query.page.limit && pageItems.length > 0
|
|
5485
|
+
? cursorFor${names.singularPascal}(pageItems[pageItems.length - 1]!, query)
|
|
5486
|
+
: null;
|
|
5487
|
+
|
|
5488
|
+
return cursorPageResult(
|
|
5489
|
+
pageItems.map(to${names.singularPascal}),
|
|
5490
|
+
query.page,
|
|
5491
|
+
nextCursor,
|
|
3990
5492
|
);
|
|
3991
5493
|
},
|
|
3992
|
-
async create(input
|
|
5494
|
+
async create(input) {
|
|
5495
|
+
const now = new Date().toISOString();
|
|
3993
5496
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
3994
5497
|
id: crypto.randomUUID(),
|
|
3995
|
-
name: input.name,
|
|
3996
|
-
|
|
5498
|
+
${options.tenant ? `\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
5499
|
+
version: 1,
|
|
5500
|
+
createdAt: now,
|
|
5501
|
+
updatedAt: now,
|
|
3997
5502
|
};
|
|
3998
|
-
${names.pluralCamel}.set(${names.singularCamel}.id,
|
|
5503
|
+
${names.pluralCamel}.set(${names.singularCamel}.id, {
|
|
5504
|
+
...${names.singularCamel},
|
|
5505
|
+
${newDeletedAtField} });
|
|
3999
5506
|
return ${names.singularCamel};
|
|
4000
5507
|
},
|
|
5508
|
+
${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {\n const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenant ? `\t\t\tif (${names.singularCamel}?.tenantId !== filter.tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input) {\n const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenant ? " || existing.tenantId !== input.tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenant ? ", filter" : ""}) {\n${deleteImplementation}\n },\n` : ""}
|
|
4001
5509
|
};
|
|
4002
5510
|
}
|
|
4003
5511
|
`;
|
|
4004
5512
|
}
|
|
4005
5513
|
|
|
4006
|
-
function drizzleSchemaFile(
|
|
4007
|
-
|
|
5514
|
+
function drizzleSchemaFile(
|
|
5515
|
+
names: ResourceNames,
|
|
5516
|
+
options: ResourceGenerationOptions,
|
|
5517
|
+
): string {
|
|
5518
|
+
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
4008
5519
|
|
|
4009
5520
|
export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
4010
5521
|
id: text("id").primaryKey(),
|
|
4011
|
-
name: text("name").notNull(),
|
|
5522
|
+
${options.tenant ? `\ttenantId: text("tenant_id").notNull(),\n` : ""} name: text("name").notNull(),
|
|
5523
|
+
version: integer("version").notNull(),
|
|
4012
5524
|
createdAt: text("created_at").notNull(),
|
|
4013
|
-
|
|
5525
|
+
updatedAt: text("updated_at").notNull(),
|
|
5526
|
+
${options.softDelete ? `\tdeletedAt: text("deleted_at"),\n` : ""}});
|
|
4014
5527
|
`;
|
|
4015
5528
|
}
|
|
4016
5529
|
|
|
5530
|
+
function drizzleResourceWhere(
|
|
5531
|
+
names: ResourceNames,
|
|
5532
|
+
options: ResourceGenerationOptions,
|
|
5533
|
+
predicates: readonly string[],
|
|
5534
|
+
): string {
|
|
5535
|
+
const allPredicates = [
|
|
5536
|
+
...predicates,
|
|
5537
|
+
...(options.softDelete
|
|
5538
|
+
? [`isNull(schema.${names.pluralCamel}.deletedAt)`]
|
|
5539
|
+
: []),
|
|
5540
|
+
];
|
|
5541
|
+
|
|
5542
|
+
if (allPredicates.length === 0) return "";
|
|
5543
|
+
if (allPredicates.length === 1) return allPredicates[0] ?? "";
|
|
5544
|
+
return `and(${allPredicates.join(", ")})`;
|
|
5545
|
+
}
|
|
5546
|
+
|
|
4017
5547
|
function drizzleRepositoryFile(
|
|
4018
5548
|
names: ResourceNames,
|
|
4019
5549
|
config: ResolvedBeignetConfig,
|
|
5550
|
+
mode: ResourceGenerationMode,
|
|
5551
|
+
options: ResourceGenerationOptions,
|
|
4020
5552
|
): string {
|
|
4021
5553
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
4022
5554
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
4023
|
-
|
|
4024
|
-
|
|
5555
|
+
const findWhere = drizzleResourceWhere(names, options, [
|
|
5556
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
5557
|
+
...(options.tenant
|
|
5558
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
5559
|
+
: []),
|
|
5560
|
+
]);
|
|
5561
|
+
const updateWhere = drizzleResourceWhere(names, options, [
|
|
5562
|
+
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
5563
|
+
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
5564
|
+
...(options.tenant
|
|
5565
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, input.tenantId)`]
|
|
5566
|
+
: []),
|
|
5567
|
+
]);
|
|
5568
|
+
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
5569
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
5570
|
+
...(options.tenant
|
|
5571
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
5572
|
+
: []),
|
|
5573
|
+
]);
|
|
5574
|
+
const drizzleImports = [
|
|
5575
|
+
"and",
|
|
5576
|
+
"asc",
|
|
5577
|
+
"desc",
|
|
5578
|
+
"eq",
|
|
5579
|
+
"gt",
|
|
5580
|
+
options.softDelete ? "isNull" : undefined,
|
|
5581
|
+
"lt",
|
|
5582
|
+
"or",
|
|
5583
|
+
"sql",
|
|
5584
|
+
"type SQL",
|
|
5585
|
+
].filter((name): name is string => Boolean(name));
|
|
5586
|
+
|
|
5587
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5588
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
4025
5589
|
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
4026
|
-
import {
|
|
5590
|
+
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
4027
5591
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
5592
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
4028
5593
|
import type {
|
|
4029
5594
|
Create${names.singularPascal}Input,
|
|
4030
|
-
${names.singularPascal},
|
|
4031
|
-
} from "${aliasModule(
|
|
5595
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5596
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
4032
5597
|
import * as schema from "${aliasModule(schemaPath)}";
|
|
4033
5598
|
|
|
4034
5599
|
type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelect;
|
|
@@ -4036,35 +5601,104 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
4036
5601
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
4037
5602
|
return {
|
|
4038
5603
|
id: row.id,
|
|
4039
|
-
name: row.name,
|
|
5604
|
+
${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
5605
|
+
version: row.version,
|
|
4040
5606
|
createdAt: row.createdAt,
|
|
5607
|
+
updatedAt: row.updatedAt,
|
|
4041
5608
|
};
|
|
4042
5609
|
}
|
|
4043
5610
|
|
|
5611
|
+
type List${names.pluralPascal}Query = Parameters<${names.singularPascal}Repository["list"]>[0];
|
|
5612
|
+
|
|
5613
|
+
function ${names.singularCamel}SortColumn(query: List${names.pluralPascal}Query) {
|
|
5614
|
+
return query.sortBy === "name" ? schema.${names.pluralCamel}.name : schema.${names.pluralCamel}.createdAt;
|
|
5615
|
+
}
|
|
5616
|
+
|
|
5617
|
+
function ${names.singularCamel}CursorFilter(
|
|
5618
|
+
query: List${names.pluralPascal}Query,
|
|
5619
|
+
): SQL<unknown> | undefined {
|
|
5620
|
+
if (!query.cursor) return undefined;
|
|
5621
|
+
|
|
5622
|
+
const column = ${names.singularCamel}SortColumn(query);
|
|
5623
|
+
const compare = query.sortDirection === "asc" ? gt : lt;
|
|
5624
|
+
|
|
5625
|
+
return or(
|
|
5626
|
+
compare(column, query.cursor.sortValue),
|
|
5627
|
+
and(
|
|
5628
|
+
eq(column, query.cursor.sortValue),
|
|
5629
|
+
compare(schema.${names.pluralCamel}.id, query.cursor.id),
|
|
5630
|
+
),
|
|
5631
|
+
) as SQL<unknown>;
|
|
5632
|
+
}
|
|
5633
|
+
|
|
5634
|
+
function ${names.singularCamel}ListWhere(
|
|
5635
|
+
query: List${names.pluralPascal}Query,
|
|
5636
|
+
): SQL<unknown> | undefined {
|
|
5637
|
+
const filters: SQL<unknown>[] = [
|
|
5638
|
+
${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, query.tenantId),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
5639
|
+
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
5640
|
+
|
|
5641
|
+
if (query.name) {
|
|
5642
|
+
const pattern = \`%\${query.name.toLocaleLowerCase()}%\`;
|
|
5643
|
+
filters.push(sql\`lower(\${schema.${names.pluralCamel}.name}) like \${pattern}\`);
|
|
5644
|
+
}
|
|
5645
|
+
if (cursor) filters.push(cursor);
|
|
5646
|
+
|
|
5647
|
+
return filters.length > 0 ? and(...filters) : undefined;
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5650
|
+
function ${names.singularCamel}OrderBy(query: List${names.pluralPascal}Query) {
|
|
5651
|
+
const order = query.sortDirection === "asc" ? asc : desc;
|
|
5652
|
+
|
|
5653
|
+
return [order(${names.singularCamel}SortColumn(query)), order(schema.${names.pluralCamel}.id)] as const;
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5656
|
+
function cursorFor${names.singularPascal}(
|
|
5657
|
+
row: ${names.singularPascal}Row,
|
|
5658
|
+
query: List${names.pluralPascal}Query,
|
|
5659
|
+
): string {
|
|
5660
|
+
return encode${names.singularPascal}Cursor({
|
|
5661
|
+
sortBy: query.sortBy,
|
|
5662
|
+
sortDirection: query.sortDirection,
|
|
5663
|
+
sortValue: row[query.sortBy],
|
|
5664
|
+
id: row.id,
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
|
|
4044
5668
|
export function createDrizzle${names.singularPascal}Repository(
|
|
4045
5669
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
4046
5670
|
): ${names.singularPascal}Repository {
|
|
4047
5671
|
return {
|
|
4048
|
-
async list(
|
|
5672
|
+
async list(query) {
|
|
5673
|
+
const where = ${names.singularCamel}ListWhere(query);
|
|
4049
5674
|
const rows = await db
|
|
4050
5675
|
.select()
|
|
4051
5676
|
.from(schema.${names.pluralCamel})
|
|
4052
|
-
.
|
|
4053
|
-
.
|
|
4054
|
-
.
|
|
4055
|
-
const
|
|
4056
|
-
|
|
4057
|
-
.
|
|
4058
|
-
|
|
4059
|
-
|
|
5677
|
+
.where(where)
|
|
5678
|
+
.orderBy(...${names.singularCamel}OrderBy(query))
|
|
5679
|
+
.limit(query.page.limit + 1);
|
|
5680
|
+
const pageRows = rows.slice(0, query.page.limit);
|
|
5681
|
+
const nextCursor =
|
|
5682
|
+
rows.length > query.page.limit && pageRows.length > 0
|
|
5683
|
+
? cursorFor${names.singularPascal}(pageRows[pageRows.length - 1]!, query)
|
|
5684
|
+
: null;
|
|
5685
|
+
|
|
5686
|
+
return cursorPageResult(
|
|
5687
|
+
pageRows.map(to${names.singularPascal}),
|
|
5688
|
+
query.page,
|
|
5689
|
+
nextCursor,
|
|
5690
|
+
);
|
|
4060
5691
|
},
|
|
4061
|
-
async create(input
|
|
5692
|
+
async create(input) {
|
|
5693
|
+
const now = new Date().toISOString();
|
|
4062
5694
|
const [row] = await db
|
|
4063
5695
|
.insert(schema.${names.pluralCamel})
|
|
4064
5696
|
.values({
|
|
4065
5697
|
id: crypto.randomUUID(),
|
|
4066
|
-
name: input.name,
|
|
4067
|
-
|
|
5698
|
+
${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
5699
|
+
version: 1,
|
|
5700
|
+
createdAt: now,
|
|
5701
|
+
updatedAt: now,
|
|
4068
5702
|
})
|
|
4069
5703
|
.returning();
|
|
4070
5704
|
|
|
@@ -4074,6 +5708,7 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
4074
5708
|
|
|
4075
5709
|
return to${names.singularPascal}(row);
|
|
4076
5710
|
},
|
|
5711
|
+
${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {\n const [row] = await db\n .select()\n .from(schema.${names.pluralCamel})\n .where(${findWhere})\n .limit(1);\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async update(input) {\n const [row] = await db\n .update(schema.${names.pluralCamel})\n .set({\n name: input.name,\n version: input.version + 1,\n updatedAt: new Date().toISOString(),\n })\n .where(${updateWhere})\n .returning();\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async delete(id: string${options.tenant ? ", filter" : ""}) {\n${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}\n return rows.length > 0;\n },\n` : ""}
|
|
4077
5712
|
};
|
|
4078
5713
|
}
|
|
4079
5714
|
`;
|
|
@@ -4082,13 +5717,17 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
4082
5717
|
function contractFile(
|
|
4083
5718
|
names: ResourceNames,
|
|
4084
5719
|
config: ResolvedBeignetConfig,
|
|
5720
|
+
mode: ResourceGenerationMode,
|
|
5721
|
+
options: ResourceGenerationOptions,
|
|
4085
5722
|
): string {
|
|
4086
|
-
return `import {
|
|
5723
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
4087
5724
|
import { z } from "zod";
|
|
4088
|
-
import {
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
5725
|
+
${mode === "resource" ? `import { errors } from "${aliasModule(resourceSharedErrorsPath(config))}";\n` : ""}import {
|
|
5726
|
+
Create${names.singularPascal}InputSchema,
|
|
5727
|
+
List${names.pluralPascal}InputSchema,
|
|
5728
|
+
List${names.pluralPascal}OutputSchema,
|
|
5729
|
+
${names.singularPascal}Schema,
|
|
5730
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n Update${names.singularPascal}BodySchema,\n` : ""}} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
4092
5731
|
|
|
4093
5732
|
const ErrorResponseSchema = z.object({
|
|
4094
5733
|
code: z.string(),
|
|
@@ -4096,7 +5735,7 @@ const ErrorResponseSchema = z.object({
|
|
|
4096
5735
|
requestId: z.string().optional(),
|
|
4097
5736
|
});
|
|
4098
5737
|
|
|
4099
|
-
const ${names.pluralCamel} =
|
|
5738
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
4100
5739
|
.namespace("${names.pluralCamel}")
|
|
4101
5740
|
.responses({
|
|
4102
5741
|
500: ErrorResponseSchema,
|
|
@@ -4104,22 +5743,52 @@ const ${names.pluralCamel} = createContractGroup()
|
|
|
4104
5743
|
|
|
4105
5744
|
export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
4106
5745
|
.get("/api/${names.pluralKebab}")
|
|
4107
|
-
.query(
|
|
5746
|
+
.query(List${names.pluralPascal}InputSchema)
|
|
4108
5747
|
.responses({
|
|
4109
|
-
200:
|
|
5748
|
+
200: List${names.pluralPascal}OutputSchema,
|
|
4110
5749
|
});
|
|
4111
5750
|
|
|
4112
5751
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
4113
5752
|
.post("/api/${names.pluralKebab}")
|
|
4114
|
-
.body(
|
|
5753
|
+
.body(Create${names.singularPascal}InputSchema)
|
|
5754
|
+
${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
5755
|
+
201: ${names.singularPascal}Schema,
|
|
5756
|
+
});
|
|
5757
|
+
${
|
|
5758
|
+
mode === "resource"
|
|
5759
|
+
? `
|
|
5760
|
+
export const get${names.singularPascal} = ${names.pluralCamel}
|
|
5761
|
+
.get("/api/${names.pluralKebab}/:id")
|
|
5762
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5763
|
+
.errors({ ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
4115
5764
|
.responses({
|
|
4116
|
-
|
|
5765
|
+
200: ${names.singularPascal}Schema,
|
|
4117
5766
|
});
|
|
5767
|
+
|
|
5768
|
+
export const update${names.singularPascal} = ${names.pluralCamel}
|
|
5769
|
+
.patch("/api/${names.pluralKebab}/:id")
|
|
5770
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5771
|
+
.body(Update${names.singularPascal}BodySchema)
|
|
5772
|
+
${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
|
|
5773
|
+
.responses({
|
|
5774
|
+
200: ${names.singularPascal}Schema,
|
|
5775
|
+
});
|
|
5776
|
+
|
|
5777
|
+
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
5778
|
+
.delete("/api/${names.pluralKebab}/:id")
|
|
5779
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5780
|
+
${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
5781
|
+
.responses({
|
|
5782
|
+
204: null,
|
|
5783
|
+
});
|
|
5784
|
+
`
|
|
5785
|
+
: ""
|
|
5786
|
+
}
|
|
4118
5787
|
`;
|
|
4119
5788
|
}
|
|
4120
5789
|
|
|
4121
5790
|
function standaloneContractFile(names: ResourceNames): string {
|
|
4122
|
-
return `import {
|
|
5791
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
4123
5792
|
import { z } from "zod";
|
|
4124
5793
|
|
|
4125
5794
|
const ErrorResponseSchema = z.object({
|
|
@@ -4128,7 +5797,7 @@ const ErrorResponseSchema = z.object({
|
|
|
4128
5797
|
requestId: z.string().optional(),
|
|
4129
5798
|
});
|
|
4130
5799
|
|
|
4131
|
-
const ${names.pluralCamel} =
|
|
5800
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
4132
5801
|
.namespace("${names.pluralCamel}")
|
|
4133
5802
|
.responses({
|
|
4134
5803
|
500: ErrorResponseSchema,
|
|
@@ -4169,7 +5838,8 @@ function standaloneUseCaseFile(
|
|
|
4169
5838
|
config: ResolvedBeignetConfig,
|
|
4170
5839
|
filePath: string,
|
|
4171
5840
|
): string {
|
|
4172
|
-
return `import {
|
|
5841
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5842
|
+
import { z } from "zod";
|
|
4173
5843
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
4174
5844
|
|
|
4175
5845
|
export const ${names.action.pascal}InputSchema = z.object({});
|
|
@@ -4202,29 +5872,27 @@ function useCaseTestFile(
|
|
|
4202
5872
|
return `import { describe, expect, it } from "bun:test";
|
|
4203
5873
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
4204
5874
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
4205
|
-
import {
|
|
5875
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
5876
|
+
import { createTestAnonymousActor } from "@beignet/core/ports/testing";
|
|
4206
5877
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4207
5878
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
4208
5879
|
import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
|
|
4209
5880
|
|
|
4210
5881
|
describe("${names.exportName}", () => {
|
|
4211
5882
|
it("runs ${names.action.camel}", async () => {
|
|
4212
|
-
const
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
gate: testPorts.gate.bind({ actor, auth: null }),
|
|
4226
|
-
ports: testPorts,
|
|
4227
|
-
}));
|
|
5883
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
5884
|
+
base: appPorts,
|
|
5885
|
+
overrides: {
|
|
5886
|
+
gate: appPorts.gate,
|
|
5887
|
+
devtools: createInMemoryDevtools(),
|
|
5888
|
+
},
|
|
5889
|
+
});
|
|
5890
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
5891
|
+
ports: testFixture.ports,
|
|
5892
|
+
actor: createTestAnonymousActor(),
|
|
5893
|
+
tenant: null,
|
|
5894
|
+
});
|
|
5895
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
4228
5896
|
|
|
4229
5897
|
const result = await tester.run(${names.exportName}, {});
|
|
4230
5898
|
|
|
@@ -4290,6 +5958,144 @@ export const ${names.policyName} = definePolicy({
|
|
|
4290
5958
|
`;
|
|
4291
5959
|
}
|
|
4292
5960
|
|
|
5961
|
+
function resourcePolicyFile(
|
|
5962
|
+
names: ResourceNames,
|
|
5963
|
+
_config: ResolvedBeignetConfig,
|
|
5964
|
+
options: ResourceGenerationOptions,
|
|
5965
|
+
): string {
|
|
5966
|
+
return `import type { ActivityActor, ActivityTenant } from "@beignet/core/ports";
|
|
5967
|
+
import { definePolicy, deny } from "@beignet/core/ports";
|
|
5968
|
+
|
|
5969
|
+
export type AuthorizationContext = {
|
|
5970
|
+
actor: ActivityActor;
|
|
5971
|
+
tenant?: ActivityTenant;
|
|
5972
|
+
};
|
|
5973
|
+
|
|
5974
|
+
type ${names.singularPascal}PolicyResource = {
|
|
5975
|
+
id: string;
|
|
5976
|
+
tenantId?: string;
|
|
5977
|
+
};
|
|
5978
|
+
|
|
5979
|
+
function isAuthenticated(ctx: AuthorizationContext) {
|
|
5980
|
+
return ctx.actor.type === "user" && Boolean(ctx.actor.id);
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5983
|
+
function canWrite(ctx: AuthorizationContext) {
|
|
5984
|
+
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
5985
|
+
${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
5986
|
+
}
|
|
5987
|
+
|
|
5988
|
+
export const ${names.singularCamel}Policy = definePolicy({
|
|
5989
|
+
"${names.pluralCamel}.view": (_ctx: AuthorizationContext) => true,
|
|
5990
|
+
"${names.pluralCamel}.create": (ctx: AuthorizationContext) =>
|
|
5991
|
+
canWrite(ctx),
|
|
5992
|
+
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
5993
|
+
const decision = canWrite(ctx);
|
|
5994
|
+
if (decision !== true) return decision;
|
|
5995
|
+
${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
5996
|
+
return true;
|
|
5997
|
+
},
|
|
5998
|
+
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
5999
|
+
const decision = canWrite(ctx);
|
|
6000
|
+
if (decision !== true) return decision;
|
|
6001
|
+
${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
6002
|
+
return true;
|
|
6003
|
+
},
|
|
6004
|
+
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
6005
|
+
deny("You are not allowed to manage this ${names.singularKebab}."),
|
|
6006
|
+
});
|
|
6007
|
+
`;
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
function resourceEventsFile(names: ResourceNames): string {
|
|
6011
|
+
return `import { defineEvent } from "@beignet/core/events";
|
|
6012
|
+
import { z } from "zod";
|
|
6013
|
+
|
|
6014
|
+
export const ${names.singularPascal}Created = defineEvent("${names.pluralCamel}.created", {
|
|
6015
|
+
payload: z.object({
|
|
6016
|
+
id: z.string().uuid(),
|
|
6017
|
+
}),
|
|
6018
|
+
});
|
|
6019
|
+
|
|
6020
|
+
export const ${names.singularPascal}Updated = defineEvent("${names.pluralCamel}.updated", {
|
|
6021
|
+
payload: z.object({
|
|
6022
|
+
id: z.string().uuid(),
|
|
6023
|
+
}),
|
|
6024
|
+
});
|
|
6025
|
+
|
|
6026
|
+
export const ${names.singularPascal}Deleted = defineEvent("${names.pluralCamel}.deleted", {
|
|
6027
|
+
payload: z.object({
|
|
6028
|
+
id: z.string().uuid(),
|
|
6029
|
+
}),
|
|
6030
|
+
});
|
|
6031
|
+
|
|
6032
|
+
export const ${names.singularCamel}Events = [
|
|
6033
|
+
${names.singularPascal}Created,
|
|
6034
|
+
${names.singularPascal}Updated,
|
|
6035
|
+
${names.singularPascal}Deleted,
|
|
6036
|
+
] as const;
|
|
6037
|
+
`;
|
|
6038
|
+
}
|
|
6039
|
+
|
|
6040
|
+
function resourcePolicyTestFile(
|
|
6041
|
+
names: ResourceNames,
|
|
6042
|
+
options: ResourceGenerationOptions,
|
|
6043
|
+
): string {
|
|
6044
|
+
return `import { describe, expect, it } from "bun:test";
|
|
6045
|
+
import { createPolicyTester } from "@beignet/core/ports/testing";
|
|
6046
|
+
import { ${names.singularCamel}Policy } from "../policy";
|
|
6047
|
+
import type { ${names.singularPascal} } from "../schemas";
|
|
6048
|
+
|
|
6049
|
+
function create${names.singularPascal}(): ${names.singularPascal} {
|
|
6050
|
+
const now = new Date().toISOString();
|
|
6051
|
+
return {
|
|
6052
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
6053
|
+
${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
6054
|
+
version: 1,
|
|
6055
|
+
createdAt: now,
|
|
6056
|
+
updatedAt: now,
|
|
6057
|
+
};
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6060
|
+
describe("${names.singularCamel}Policy", () => {
|
|
6061
|
+
it("documents generated ${names.pluralKebab} authorization rules", async () => {
|
|
6062
|
+
const tester = createPolicyTester({
|
|
6063
|
+
policies: [${names.singularCamel}Policy],
|
|
6064
|
+
});
|
|
6065
|
+
const ${names.singularCamel} = create${names.singularPascal}();
|
|
6066
|
+
const ctx = {
|
|
6067
|
+
actor: { type: "user" as const, id: "user_test" },
|
|
6068
|
+
${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
6069
|
+
|
|
6070
|
+
await expect(
|
|
6071
|
+
tester.assertMatrix([
|
|
6072
|
+
{
|
|
6073
|
+
name: "authenticated actor can create ${names.singularKebab}",
|
|
6074
|
+
ctx,
|
|
6075
|
+
ability: "${names.pluralCamel}.create",
|
|
6076
|
+
expected: "allow",
|
|
6077
|
+
},
|
|
6078
|
+
{
|
|
6079
|
+
name: "authenticated actor can update ${names.singularKebab}",
|
|
6080
|
+
ctx,
|
|
6081
|
+
ability: "${names.pluralCamel}.update",
|
|
6082
|
+
subject: ${names.singularCamel},
|
|
6083
|
+
expected: "allow",
|
|
6084
|
+
},
|
|
6085
|
+
{
|
|
6086
|
+
name: "authenticated actor can delete ${names.singularKebab}",
|
|
6087
|
+
ctx,
|
|
6088
|
+
ability: "${names.pluralCamel}.delete",
|
|
6089
|
+
subject: ${names.singularCamel},
|
|
6090
|
+
expected: "allow",
|
|
6091
|
+
},
|
|
6092
|
+
]),
|
|
6093
|
+
).resolves.toBeUndefined();
|
|
6094
|
+
});
|
|
6095
|
+
});
|
|
6096
|
+
`;
|
|
6097
|
+
}
|
|
6098
|
+
|
|
4293
6099
|
function eventFile(names: EventNames): string {
|
|
4294
6100
|
return `import { defineEvent } from "@beignet/core/events";
|
|
4295
6101
|
import { z } from "zod";
|
|
@@ -4307,11 +6113,9 @@ export const ${names.eventExportName} = defineEvent("${names.eventName}", {
|
|
|
4307
6113
|
}
|
|
4308
6114
|
|
|
4309
6115
|
function jobFile(names: JobNames, config: ResolvedBeignetConfig): string {
|
|
4310
|
-
return `import {
|
|
6116
|
+
return `import { retry } from "@beignet/core/jobs";
|
|
4311
6117
|
import { z } from "zod";
|
|
4312
|
-
import
|
|
4313
|
-
|
|
4314
|
-
const jobs = createJobHandlers<AppContext>();
|
|
6118
|
+
import { defineJob } from "${aliasModule(config.paths.jobsBuilder)}";
|
|
4315
6119
|
|
|
4316
6120
|
export const ${names.payloadSchemaName} = z.object({
|
|
4317
6121
|
\tid: z.string().uuid(),
|
|
@@ -4319,7 +6123,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
4319
6123
|
|
|
4320
6124
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
4321
6125
|
|
|
4322
|
-
export const ${names.jobExportName} =
|
|
6126
|
+
export const ${names.jobExportName} = defineJob("${names.jobName}", {
|
|
4323
6127
|
\tpayload: ${names.payloadSchemaName},
|
|
4324
6128
|
\tretry: retry.exponential({
|
|
4325
6129
|
\t\tattempts: 3,
|
|
@@ -4336,16 +6140,43 @@ export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
|
|
|
4336
6140
|
`;
|
|
4337
6141
|
}
|
|
4338
6142
|
|
|
6143
|
+
function taskFile(names: TaskNames, config: ResolvedBeignetConfig): string {
|
|
6144
|
+
return `import { z } from "zod";
|
|
6145
|
+
import { defineTask } from "${aliasModule(config.paths.tasksBuilder)}";
|
|
6146
|
+
|
|
6147
|
+
export const ${names.inputSchemaName} = z.object({
|
|
6148
|
+
\tdryRun: z.boolean().default(true),
|
|
6149
|
+
});
|
|
6150
|
+
|
|
6151
|
+
export type ${names.inputTypeName} = z.infer<typeof ${names.inputSchemaName}>;
|
|
6152
|
+
|
|
6153
|
+
export const ${names.taskExportName} = defineTask("${names.taskName}", {
|
|
6154
|
+
\tinput: ${names.inputSchemaName},
|
|
6155
|
+
\tdescription: "Operational ${names.feature.kebab} ${names.artifact.kebab} task.",
|
|
6156
|
+
\tasync handle({ input, ctx }) {
|
|
6157
|
+
\t\tctx.ports.logger.info("Task handled", {
|
|
6158
|
+
\t\t\ttaskName: "${names.taskName}",
|
|
6159
|
+
\t\t\tdryRun: input.dryRun,
|
|
6160
|
+
\t\t});
|
|
6161
|
+
|
|
6162
|
+
\t\treturn {
|
|
6163
|
+
\t\t\tdryRun: input.dryRun,
|
|
6164
|
+
\t\t};
|
|
6165
|
+
\t},
|
|
6166
|
+
});
|
|
6167
|
+
`;
|
|
6168
|
+
}
|
|
6169
|
+
|
|
4339
6170
|
function factoryFile(
|
|
4340
6171
|
names: FactoryNames,
|
|
4341
6172
|
config: ResolvedBeignetConfig,
|
|
4342
6173
|
): string {
|
|
4343
6174
|
const resource = featureResourceNames(names);
|
|
4344
6175
|
|
|
4345
|
-
return `import {
|
|
6176
|
+
return `import { createFactory } from "@beignet/core/testing";
|
|
4346
6177
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4347
6178
|
|
|
4348
|
-
export const ${names.factoryExportName} =
|
|
6179
|
+
export const ${names.factoryExportName} = createFactory("${names.factoryName}", {
|
|
4349
6180
|
\tdefaults: ({ sequence }) => ({
|
|
4350
6181
|
\t\tname: "${resource.singularPascal} " + sequence,
|
|
4351
6182
|
\t}),
|
|
@@ -4375,14 +6206,9 @@ function notificationFile(
|
|
|
4375
6206
|
names: NotificationNames,
|
|
4376
6207
|
config: ResolvedBeignetConfig,
|
|
4377
6208
|
): string {
|
|
4378
|
-
return `import {
|
|
4379
|
-
\tcreateNotificationHandlers,
|
|
4380
|
-
\tdefineMailNotificationChannel,
|
|
4381
|
-
} from "@beignet/core/notifications";
|
|
6209
|
+
return `import { defineMailNotificationChannel } from "@beignet/core/notifications";
|
|
4382
6210
|
import { z } from "zod";
|
|
4383
|
-
import
|
|
4384
|
-
|
|
4385
|
-
const notifications = createNotificationHandlers<AppContext>();
|
|
6211
|
+
import { defineNotification } from "${aliasModule(config.paths.notificationsBuilder)}";
|
|
4386
6212
|
|
|
4387
6213
|
export const ${names.payloadSchemaName} = z.object({
|
|
4388
6214
|
\tid: z.string().uuid(),
|
|
@@ -4392,7 +6218,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
4392
6218
|
|
|
4393
6219
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
4394
6220
|
|
|
4395
|
-
export const ${names.notificationExportName} =
|
|
6221
|
+
export const ${names.notificationExportName} = defineNotification(
|
|
4396
6222
|
\t"${names.notificationName}",
|
|
4397
6223
|
\t{
|
|
4398
6224
|
\t\tpayload: ${names.payloadSchemaName},
|
|
@@ -4414,13 +6240,10 @@ function listenerFile(
|
|
|
4414
6240
|
): string {
|
|
4415
6241
|
const filePath = listenerFilePath(names, config);
|
|
4416
6242
|
|
|
4417
|
-
return `import {
|
|
4418
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
6243
|
+
return `import { defineListener } from "${aliasModule(config.paths.listenersBuilder)}";
|
|
4419
6244
|
import { ${names.event.eventExportName} } from "${relativeModule(filePath, eventFilePath(names.event, config))}";
|
|
4420
6245
|
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
export const ${names.listenerExportName} = events.defineListener(${names.event.eventExportName}, {
|
|
6246
|
+
export const ${names.listenerExportName} = defineListener(${names.event.eventExportName}, {
|
|
4424
6247
|
\tname: "${names.listenerName}",
|
|
4425
6248
|
\tasync handle({ payload, ctx }) {
|
|
4426
6249
|
\t\tctx.ports.logger.info("Listener handled", {
|
|
@@ -4436,11 +6259,8 @@ function scheduleFile(
|
|
|
4436
6259
|
names: ScheduleNames,
|
|
4437
6260
|
config: ResolvedBeignetConfig,
|
|
4438
6261
|
): string {
|
|
4439
|
-
return `import {
|
|
4440
|
-
import {
|
|
4441
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4442
|
-
|
|
4443
|
-
const schedules = createScheduleHandlers<AppContext>();
|
|
6262
|
+
return `import { z } from "zod";
|
|
6263
|
+
import { defineSchedule } from "${aliasModule(config.paths.schedulesBuilder)}";
|
|
4444
6264
|
|
|
4445
6265
|
export const ${names.payloadSchemaName} = z.object({
|
|
4446
6266
|
\tdate: z.string(),
|
|
@@ -4448,7 +6268,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
4448
6268
|
|
|
4449
6269
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
4450
6270
|
|
|
4451
|
-
export const ${names.scheduleExportName} =
|
|
6271
|
+
export const ${names.scheduleExportName} = defineSchedule(
|
|
4452
6272
|
\t"${names.scheduleName}",
|
|
4453
6273
|
\t{
|
|
4454
6274
|
\t\tcron: "${names.cron}",
|
|
@@ -4526,111 +6346,119 @@ export const ${names.uploadExportName} = defineUpload<
|
|
|
4526
6346
|
`;
|
|
4527
6347
|
}
|
|
4528
6348
|
|
|
6349
|
+
function featureUiComponentFile(
|
|
6350
|
+
names: FeatureUiNames,
|
|
6351
|
+
config: ResolvedBeignetConfig,
|
|
6352
|
+
): string {
|
|
6353
|
+
return `"use client";
|
|
6354
|
+
|
|
6355
|
+
import { contractErrorMessage } from "@beignet/core/client";
|
|
6356
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6357
|
+
import { useState } from "react";
|
|
6358
|
+
import { rq } from "${aliasModule(clientIndexPath(config))}";
|
|
6359
|
+
import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(
|
|
6360
|
+
resourceContractFilePath(names, config),
|
|
6361
|
+
)}";
|
|
6362
|
+
|
|
6363
|
+
export function ${names.componentExportName}() {
|
|
6364
|
+
\tconst [name, setName] = useState("");
|
|
6365
|
+
\tconst queryClient = useQueryClient();
|
|
6366
|
+
\tconst ${names.pluralCamel}Query = useQuery(
|
|
6367
|
+
\t\trq(list${names.pluralPascal}).queryOptions({ query: {} }),
|
|
6368
|
+
\t);
|
|
6369
|
+
\tconst create${names.singularPascal}Mutation = useMutation(
|
|
6370
|
+
\t\trq(create${names.singularPascal}).mutationOptions({
|
|
6371
|
+
\t\t\tonSuccess: async () => {
|
|
6372
|
+
\t\t\t\tsetName("");
|
|
6373
|
+
\t\t\t\tawait rq(list${names.pluralPascal}).invalidate(queryClient);
|
|
6374
|
+
\t\t\t},
|
|
6375
|
+
\t\t}),
|
|
6376
|
+
\t);
|
|
6377
|
+
|
|
6378
|
+
\treturn (
|
|
6379
|
+
\t\t<section className="${names.pluralKebab}-panel">
|
|
6380
|
+
\t\t\t<form
|
|
6381
|
+
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
6382
|
+
\t\t\t\tonSubmit={(event) => {
|
|
6383
|
+
\t\t\t\t\tevent.preventDefault();
|
|
6384
|
+
\t\t\t\t\tconst trimmedName = name.trim();
|
|
6385
|
+
\t\t\t\t\tif (!trimmedName) return;
|
|
6386
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
6387
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
6388
|
+
\t\t\t\t\t\tbody: { name: trimmedName },
|
|
6389
|
+
\t\t\t\t\t});
|
|
6390
|
+
\t\t\t\t}}
|
|
6391
|
+
\t\t\t>
|
|
6392
|
+
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
6393
|
+
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
6394
|
+
\t\t\t\t\t<input
|
|
6395
|
+
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
6396
|
+
\t\t\t\t\t\tvalue={name}
|
|
6397
|
+
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
6398
|
+
\t\t\t\t\t\tplaceholder="Name"
|
|
6399
|
+
\t\t\t\t\t/>
|
|
6400
|
+
\t\t\t\t\t<button
|
|
6401
|
+
\t\t\t\t\t\ttype="submit"
|
|
6402
|
+
\t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
|
|
6403
|
+
\t\t\t\t\t>
|
|
6404
|
+
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
6405
|
+
\t\t\t\t\t</button>
|
|
6406
|
+
\t\t\t\t</div>
|
|
6407
|
+
\t\t\t\t{create${names.singularPascal}Mutation.isError ? (
|
|
6408
|
+
\t\t\t\t\t<p role="alert">
|
|
6409
|
+
\t\t\t\t\t\t{contractErrorMessage(
|
|
6410
|
+
\t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
|
|
6411
|
+
\t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
|
|
6412
|
+
\t\t\t\t\t\t)}
|
|
6413
|
+
\t\t\t\t\t</p>
|
|
6414
|
+
\t\t\t\t) : null}
|
|
6415
|
+
\t\t\t</form>
|
|
6416
|
+
|
|
6417
|
+
\t\t\t<div className="${names.pluralKebab}-list">
|
|
6418
|
+
\t\t\t\t<div className="${names.pluralKebab}-list-heading">
|
|
6419
|
+
\t\t\t\t\t<h2>${names.pluralPascal}</h2>
|
|
6420
|
+
\t\t\t\t\t<span>{${names.pluralCamel}Query.data?.items.length ?? 0} shown</span>
|
|
6421
|
+
\t\t\t\t</div>
|
|
6422
|
+
\t\t\t\t{${names.pluralCamel}Query.isLoading ? <p>Loading...</p> : null}
|
|
6423
|
+
\t\t\t\t{${names.pluralCamel}Query.isError ? (
|
|
6424
|
+
\t\t\t\t\t<p role="alert">Could not load ${names.pluralKebab}.</p>
|
|
6425
|
+
\t\t\t\t) : null}
|
|
6426
|
+
\t\t\t\t{${names.pluralCamel}Query.data?.items.length === 0 ? (
|
|
6427
|
+
\t\t\t\t\t<p>No ${names.pluralKebab} yet.</p>
|
|
6428
|
+
\t\t\t\t) : null}
|
|
6429
|
+
\t\t\t\t<ul>
|
|
6430
|
+
\t\t\t\t\t{${names.pluralCamel}Query.data?.items.map((${names.singularCamel}) => (
|
|
6431
|
+
\t\t\t\t\t\t<li key={${names.singularCamel}.id}>
|
|
6432
|
+
\t\t\t\t\t\t\t<strong>{${names.singularCamel}.name}</strong>
|
|
6433
|
+
\t\t\t\t\t\t\t<small>{new Date(${names.singularCamel}.createdAt).toLocaleDateString()}</small>
|
|
6434
|
+
\t\t\t\t\t\t</li>
|
|
6435
|
+
\t\t\t\t\t))}
|
|
6436
|
+
\t\t\t\t</ul>
|
|
6437
|
+
\t\t\t</div>
|
|
6438
|
+
\t\t</section>
|
|
6439
|
+
\t);
|
|
6440
|
+
}
|
|
6441
|
+
`;
|
|
6442
|
+
}
|
|
6443
|
+
|
|
4529
6444
|
function scheduleRouteFile(
|
|
4530
6445
|
names: ScheduleNames,
|
|
4531
6446
|
config: ResolvedBeignetConfig,
|
|
4532
6447
|
): string {
|
|
4533
|
-
return `import {
|
|
4534
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
6448
|
+
return `import { createScheduleRoute } from "@beignet/next";
|
|
4535
6449
|
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
4536
6450
|
import { env } from "@/lib/env";
|
|
4537
6451
|
import { server } from "${aliasModule(config.paths.server)}";
|
|
4538
6452
|
|
|
4539
|
-
|
|
4540
|
-
\tconst cronSecret = env.CRON_SECRET;
|
|
6453
|
+
export const runtime = "nodejs";
|
|
4541
6454
|
|
|
4542
|
-
|
|
4543
|
-
\
|
|
4544
|
-
\
|
|
4545
|
-
\
|
|
4546
|
-
\
|
|
4547
|
-
\
|
|
4548
|
-
|
|
4549
|
-
\t\t\t{ status: 500 },
|
|
4550
|
-
\t\t);
|
|
4551
|
-
\t}
|
|
4552
|
-
|
|
4553
|
-
\tif (
|
|
4554
|
-
\t\trequest.headers.get("authorization") !== \`Bearer \${cronSecret}\`
|
|
4555
|
-
\t) {
|
|
4556
|
-
\t\treturn Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
4557
|
-
\t}
|
|
4558
|
-
|
|
4559
|
-
\tconst ctx = await server.createContextFromNext();
|
|
4560
|
-
\tconst runner = createInlineScheduleRunner<AppContext>({
|
|
4561
|
-
\t\tctx,
|
|
4562
|
-
\t\tonStart({ run, schedule }) {
|
|
4563
|
-
\t\t\tctx.ports.devtools.record({
|
|
4564
|
-
\t\t\t\ttype: "schedule",
|
|
4565
|
-
\t\t\t\twatcher: "schedules",
|
|
4566
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
4567
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
4568
|
-
\t\t\t\tstatus: "started",
|
|
4569
|
-
\t\t\t\tcron: schedule.cron,
|
|
4570
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
4571
|
-
\t\t\t\tdetails: {
|
|
4572
|
-
\t\t\t\t\tsource: run.source,
|
|
4573
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
4574
|
-
\t\t\t\t},
|
|
4575
|
-
\t\t\t});
|
|
4576
|
-
\t\t},
|
|
4577
|
-
\t\tonSuccess({ run, schedule }) {
|
|
4578
|
-
\t\t\tctx.ports.devtools.record({
|
|
4579
|
-
\t\t\t\ttype: "schedule",
|
|
4580
|
-
\t\t\t\twatcher: "schedules",
|
|
4581
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
4582
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
4583
|
-
\t\t\t\tstatus: "completed",
|
|
4584
|
-
\t\t\t\tcron: schedule.cron,
|
|
4585
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
4586
|
-
\t\t\t\tdetails: {
|
|
4587
|
-
\t\t\t\t\tsource: run.source,
|
|
4588
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
4589
|
-
\t\t\t\t},
|
|
4590
|
-
\t\t\t});
|
|
4591
|
-
\t\t},
|
|
4592
|
-
\t\tonError({ error, run, schedule }) {
|
|
4593
|
-
\t\t\tctx.ports.devtools.record({
|
|
4594
|
-
\t\t\t\ttype: "schedule",
|
|
4595
|
-
\t\t\t\twatcher: "schedules",
|
|
4596
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
4597
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
4598
|
-
\t\t\t\tstatus: "failed",
|
|
4599
|
-
\t\t\t\tcron: schedule.cron,
|
|
4600
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
4601
|
-
\t\t\t\tdetails: {
|
|
4602
|
-
\t\t\t\t\terror,
|
|
4603
|
-
\t\t\t\t\tsource: run.source,
|
|
4604
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
4605
|
-
\t\t\t\t},
|
|
4606
|
-
\t\t\t});
|
|
4607
|
-
\t\t\tctx.ports.logger.error("Schedule failed", {
|
|
4608
|
-
\t\t\t\terror,
|
|
4609
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
4610
|
-
\t\t\t});
|
|
4611
|
-
\t\t},
|
|
4612
|
-
\t\tonHookError({ error, hook, schedule }) {
|
|
4613
|
-
\t\t\tctx.ports.logger.warn("Schedule lifecycle hook failed", {
|
|
4614
|
-
\t\t\t\terror,
|
|
4615
|
-
\t\t\t\thook,
|
|
4616
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
4617
|
-
\t\t\t});
|
|
4618
|
-
\t\t},
|
|
4619
|
-
\t});
|
|
4620
|
-
|
|
4621
|
-
\ttry {
|
|
4622
|
-
\t\tawait runner.run(${names.scheduleExportName}, {
|
|
4623
|
-
\t\t\tsource: "cron-route",
|
|
4624
|
-
\t\t});
|
|
4625
|
-
\t} catch {
|
|
4626
|
-
\t\treturn Response.json({ error: "Schedule failed" }, { status: 500 });
|
|
4627
|
-
\t}
|
|
4628
|
-
|
|
4629
|
-
\treturn Response.json({ ok: true });
|
|
4630
|
-
}
|
|
4631
|
-
|
|
4632
|
-
export const GET = run${names.artifact.pascal};
|
|
4633
|
-
export const POST = run${names.artifact.pascal};
|
|
6455
|
+
export const { GET, POST } = createScheduleRoute({
|
|
6456
|
+
\tserver,
|
|
6457
|
+
\tschedules: [${names.scheduleExportName}],
|
|
6458
|
+
\tschedule: ${names.scheduleExportName}.name,
|
|
6459
|
+
\tsecret: env.CRON_SECRET,
|
|
6460
|
+
\tsource: "cron-route",
|
|
6461
|
+
});
|
|
4634
6462
|
`;
|
|
4635
6463
|
}
|
|
4636
6464
|
|
|
@@ -4648,84 +6476,161 @@ function useCaseIndexExport(names: UseCaseNames): string {
|
|
|
4648
6476
|
function routeGroupFile(
|
|
4649
6477
|
names: ResourceNames,
|
|
4650
6478
|
config: ResolvedBeignetConfig,
|
|
6479
|
+
mode: ResourceGenerationMode,
|
|
6480
|
+
_options: ResourceGenerationOptions,
|
|
4651
6481
|
): string {
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
6482
|
+
const routeFilePath = path.join(
|
|
6483
|
+
config.paths.features,
|
|
6484
|
+
names.pluralKebab,
|
|
6485
|
+
"routes.ts",
|
|
6486
|
+
);
|
|
6487
|
+
const contractsPath = resourceContractFilePath(names, config);
|
|
6488
|
+
const useCasesPath = resourceUseCaseIndexPath(names, config);
|
|
6489
|
+
|
|
6490
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
6491
|
+
import { defineRouteGroup } from "@beignet/next";
|
|
6492
|
+
import type { AppContext } from "${relativeModule(routeFilePath, config.paths.appContext)}";
|
|
6493
|
+
import {
|
|
6494
|
+
create${names.singularPascal},
|
|
6495
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
6496
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${relativeModule(routeFilePath, contractsPath)}";
|
|
4655
6497
|
import {
|
|
4656
6498
|
create${names.singularPascal}UseCase,
|
|
4657
|
-
list${names.pluralPascal}UseCase,
|
|
4658
|
-
|
|
6499
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
6500
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${relativeModule(routeFilePath, useCasesPath)}";
|
|
4659
6501
|
|
|
4660
6502
|
export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>({
|
|
4661
6503
|
name: "${names.pluralKebab}",
|
|
4662
6504
|
routes: [
|
|
4663
|
-
{
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
status: 200,
|
|
4667
|
-
body: await list${names.pluralPascal}UseCase.run({ ctx, input: query }),
|
|
4668
|
-
}),
|
|
4669
|
-
},
|
|
4670
|
-
{
|
|
4671
|
-
contract: create${names.singularPascal},
|
|
4672
|
-
handle: async ({ ctx, body }) => ({
|
|
4673
|
-
status: 201,
|
|
4674
|
-
body: await create${names.singularPascal}UseCase.run({ ctx, input: body }),
|
|
4675
|
-
}),
|
|
4676
|
-
},
|
|
4677
|
-
],
|
|
6505
|
+
{ contract: list${names.pluralPascal}, useCase: list${names.pluralPascal}UseCase },
|
|
6506
|
+
{ contract: create${names.singularPascal}, useCase: create${names.singularPascal}UseCase },
|
|
6507
|
+
${mode === "resource" ? ` { contract: get${names.singularPascal}, useCase: get${names.singularPascal}UseCase },\n { contract: update${names.singularPascal}, useCase: update${names.singularPascal}UseCase },\n { contract: delete${names.singularPascal}, useCase: delete${names.singularPascal}UseCase },\n` : ""} ],
|
|
4678
6508
|
});
|
|
4679
6509
|
`;
|
|
4680
6510
|
}
|
|
4681
6511
|
|
|
4682
|
-
function testFile(
|
|
6512
|
+
function testFile(
|
|
6513
|
+
names: ResourceNames,
|
|
6514
|
+
config: ResolvedBeignetConfig,
|
|
6515
|
+
mode: ResourceGenerationMode,
|
|
6516
|
+
options: ResourceGenerationOptions,
|
|
6517
|
+
): string {
|
|
4683
6518
|
const repositoryPath = path.join(
|
|
4684
6519
|
path.dirname(config.paths.infrastructurePorts),
|
|
4685
6520
|
names.pluralKebab,
|
|
4686
6521
|
`in-memory-${names.singularKebab}-repository.ts`,
|
|
4687
6522
|
);
|
|
6523
|
+
const serverContextPath = path.join(
|
|
6524
|
+
path.dirname(config.paths.server),
|
|
6525
|
+
"context.ts",
|
|
6526
|
+
);
|
|
6527
|
+
const requestTarget = options.tenant ? "requester" : "app";
|
|
4688
6528
|
|
|
4689
6529
|
return `import { describe, expect, it } from "bun:test";
|
|
4690
6530
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
4691
|
-
import { defineRoutes } from "@beignet/web";
|
|
4692
|
-
import { createTestApp } from "@beignet/web/testing";
|
|
6531
|
+
${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` : ""}import { defineRoutes } from "@beignet/web";
|
|
6532
|
+
import { createTestApp${options.tenant ? ", createTestRequester" : ""} } from "@beignet/web/testing";
|
|
4693
6533
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
4694
|
-
import {
|
|
6534
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
6535
|
+
import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/ports/testing";
|
|
4695
6536
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4696
6537
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
4697
|
-
import {
|
|
6538
|
+
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
6539
|
+
import {
|
|
6540
|
+
create${names.singularPascal},
|
|
6541
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
6542
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
4698
6543
|
import { createInMemory${names.singularPascal}Repository } from "${aliasModule(repositoryPath)}";
|
|
4699
6544
|
import { ${names.singularCamel}Routes } from "${aliasModule(path.join(resourceFeatureDir(names, config), "routes.ts"))}";
|
|
4700
6545
|
import {
|
|
4701
6546
|
create${names.singularPascal}UseCase,
|
|
4702
|
-
list${names.pluralPascal}UseCase,
|
|
4703
|
-
} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
6547
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
6548
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
4704
6549
|
|
|
4705
6550
|
describe("${names.pluralCamel} resource", () => {
|
|
4706
|
-
it("creates and lists ${names.pluralCamel}", async () => {
|
|
4707
|
-
const ${names.
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
6551
|
+
it("${mode === "resource" ? `exercises ${names.pluralCamel} CRUD` : `creates and lists ${names.pluralCamel}`}", async () => {
|
|
6552
|
+
const seed${names.pluralPascal} = [
|
|
6553
|
+
{
|
|
6554
|
+
id: "00000000-0000-4000-8000-000000000101",
|
|
6555
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
6556
|
+
version: 1,
|
|
6557
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
6558
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
6559
|
+
},
|
|
6560
|
+
{
|
|
6561
|
+
id: "00000000-0000-4000-8000-000000000102",
|
|
6562
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
6563
|
+
version: 1,
|
|
6564
|
+
createdAt: "2024-01-02T00:00:00.000Z",
|
|
6565
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
6566
|
+
},
|
|
6567
|
+
{
|
|
6568
|
+
id: "00000000-0000-4000-8000-000000000103",
|
|
6569
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
6570
|
+
version: 1,
|
|
6571
|
+
createdAt: "2024-01-03T00:00:00.000Z",
|
|
6572
|
+
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
6573
|
+
},
|
|
6574
|
+
${options.tenant ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
|
|
6575
|
+
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
6576
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
6577
|
+
base: appPorts,
|
|
6578
|
+
overrides: {
|
|
4713
6579
|
${names.pluralCamel},
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
6580
|
+
${
|
|
6581
|
+
options.auth
|
|
6582
|
+
? `\t\t\t\tauth: {
|
|
6583
|
+
getSession: async () => ({
|
|
6584
|
+
user: { id: "user_test", name: "Test User" },
|
|
6585
|
+
}),
|
|
6586
|
+
},
|
|
6587
|
+
gate: appPorts.gate,\n`
|
|
6588
|
+
: `\t\t\t\tauth: {
|
|
6589
|
+
getSession: async () => null,
|
|
6590
|
+
},\n`
|
|
6591
|
+
} devtools: createInMemoryDevtools(),
|
|
6592
|
+
},
|
|
6593
|
+
transaction: {
|
|
6594
|
+
ports: (ports) => ({
|
|
6595
|
+
...ports,
|
|
6596
|
+
${names.pluralCamel},
|
|
6597
|
+
}),
|
|
6598
|
+
},
|
|
6599
|
+
});
|
|
6600
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
6601
|
+
ports: testFixture.ports,
|
|
6602
|
+
actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
6603
|
+
tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
|
|
4725
6604
|
});
|
|
4726
6605
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
4727
6606
|
|
|
4728
6607
|
const ctx = await tester.ctx();
|
|
6608
|
+
const defaultPage = await tester.run(list${names.pluralPascal}UseCase, {}, { ctx });
|
|
6609
|
+
const filteredPage = await tester.run(
|
|
6610
|
+
list${names.pluralPascal}UseCase,
|
|
6611
|
+
{ name: "alp" },
|
|
6612
|
+
{ ctx },
|
|
6613
|
+
);
|
|
6614
|
+
const firstPage = await tester.run(
|
|
6615
|
+
list${names.pluralPascal}UseCase,
|
|
6616
|
+
{ limit: 2 },
|
|
6617
|
+
{ ctx },
|
|
6618
|
+
);
|
|
6619
|
+
const secondPage = await tester.run(
|
|
6620
|
+
list${names.pluralPascal}UseCase,
|
|
6621
|
+
{ limit: 2, cursor: firstPage.page.nextCursor },
|
|
6622
|
+
{ ctx },
|
|
6623
|
+
);
|
|
6624
|
+
const nameAscendingPage = await tester.run(
|
|
6625
|
+
list${names.pluralPascal}UseCase,
|
|
6626
|
+
{ sortBy: "name", sortDirection: "asc" },
|
|
6627
|
+
{ ctx },
|
|
6628
|
+
);
|
|
6629
|
+
const nameDescendingPage = await tester.run(
|
|
6630
|
+
list${names.pluralPascal}UseCase,
|
|
6631
|
+
{ sortBy: "name", sortDirection: "desc" },
|
|
6632
|
+
{ ctx },
|
|
6633
|
+
);
|
|
4729
6634
|
const created = await tester.run(
|
|
4730
6635
|
create${names.singularPascal}UseCase,
|
|
4731
6636
|
{ name: "First ${names.singularPascal}" },
|
|
@@ -4733,35 +6638,113 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
4733
6638
|
);
|
|
4734
6639
|
const result = await tester.run(
|
|
4735
6640
|
list${names.pluralPascal}UseCase,
|
|
4736
|
-
{
|
|
6641
|
+
{ name: "First" },
|
|
4737
6642
|
{ ctx },
|
|
4738
6643
|
);
|
|
4739
6644
|
|
|
6645
|
+
expect(defaultPage.page).toMatchObject({
|
|
6646
|
+
kind: "cursor",
|
|
6647
|
+
limit: 20,
|
|
6648
|
+
cursor: null,
|
|
6649
|
+
nextCursor: null,
|
|
6650
|
+
hasMore: false,
|
|
6651
|
+
});
|
|
6652
|
+
expect(defaultPage.items.map((item) => item.name)).toEqual([
|
|
6653
|
+
"Gamma ${names.singularPascal}",
|
|
6654
|
+
"Beta ${names.singularPascal}",
|
|
6655
|
+
"Alpha ${names.singularPascal}",
|
|
6656
|
+
]);
|
|
6657
|
+
expect(filteredPage.items.map((item) => item.name)).toEqual([
|
|
6658
|
+
"Alpha ${names.singularPascal}",
|
|
6659
|
+
]);
|
|
6660
|
+
expect(firstPage.items.map((item) => item.name)).toEqual([
|
|
6661
|
+
"Gamma ${names.singularPascal}",
|
|
6662
|
+
"Beta ${names.singularPascal}",
|
|
6663
|
+
]);
|
|
6664
|
+
expect(firstPage.page.nextCursor).toEqual(expect.any(String));
|
|
6665
|
+
expect(secondPage.items.map((item) => item.name)).toEqual([
|
|
6666
|
+
"Alpha ${names.singularPascal}",
|
|
6667
|
+
]);
|
|
6668
|
+
expect(secondPage.page.nextCursor).toBeNull();
|
|
6669
|
+
expect(nameAscendingPage.items.map((item) => item.name)).toEqual([
|
|
6670
|
+
"Alpha ${names.singularPascal}",
|
|
6671
|
+
"Beta ${names.singularPascal}",
|
|
6672
|
+
"Gamma ${names.singularPascal}",
|
|
6673
|
+
]);
|
|
6674
|
+
expect(nameDescendingPage.items.map((item) => item.name)).toEqual([
|
|
6675
|
+
"Gamma ${names.singularPascal}",
|
|
6676
|
+
"Beta ${names.singularPascal}",
|
|
6677
|
+
"Alpha ${names.singularPascal}",
|
|
6678
|
+
]);
|
|
4740
6679
|
expect(created.name).toBe("First ${names.singularPascal}");
|
|
4741
|
-
expect(result.page.total).toBe(1);
|
|
4742
6680
|
expect(result.items).toEqual([created]);
|
|
6681
|
+
${
|
|
6682
|
+
mode === "resource"
|
|
6683
|
+
? `
|
|
6684
|
+
const fetched = await tester.run(
|
|
6685
|
+
get${names.singularPascal}UseCase,
|
|
6686
|
+
{ id: created.id },
|
|
6687
|
+
{ ctx },
|
|
6688
|
+
);
|
|
6689
|
+
const updated = await tester.run(
|
|
6690
|
+
update${names.singularPascal}UseCase,
|
|
6691
|
+
{ id: created.id, name: "Updated ${names.singularPascal}", version: fetched.version },
|
|
6692
|
+
{ ctx },
|
|
6693
|
+
);
|
|
6694
|
+
try {
|
|
6695
|
+
await tester.run(
|
|
6696
|
+
update${names.singularPascal}UseCase,
|
|
6697
|
+
{ id: created.id, name: "Stale ${names.singularPascal}", version: fetched.version },
|
|
6698
|
+
{ ctx },
|
|
6699
|
+
);
|
|
6700
|
+
throw new Error("Expected ${names.singularPascal}Conflict");
|
|
6701
|
+
} catch (error) {
|
|
6702
|
+
expect(isAppError(error)).toBe(true);
|
|
6703
|
+
if (isAppError(error)) {
|
|
6704
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_CONFLICT");
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
await tester.run(delete${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
6708
|
+
const afterDelete = await tester.run(
|
|
6709
|
+
list${names.pluralPascal}UseCase,
|
|
6710
|
+
{ name: "Updated" },
|
|
6711
|
+
{ ctx },
|
|
6712
|
+
);
|
|
4743
6713
|
|
|
4744
|
-
|
|
4745
|
-
|
|
6714
|
+
expect(fetched).toEqual(created);
|
|
6715
|
+
expect(updated.name).toBe("Updated ${names.singularPascal}");
|
|
6716
|
+
expect(updated.version).toBe(fetched.version + 1);
|
|
6717
|
+
expect(afterDelete.items).toEqual([]);
|
|
6718
|
+
|
|
6719
|
+
try {
|
|
6720
|
+
await tester.run(get${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
6721
|
+
throw new Error("Expected ${names.singularPascal}NotFound");
|
|
6722
|
+
} catch (error) {
|
|
6723
|
+
expect(isAppError(error)).toBe(true);
|
|
6724
|
+
if (isAppError(error)) {
|
|
6725
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_NOT_FOUND");
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6728
|
+
`
|
|
6729
|
+
: ""
|
|
6730
|
+
}
|
|
6731
|
+
|
|
6732
|
+
const app = await createTestApp({
|
|
6733
|
+
ports: testFixture.ports,
|
|
4746
6734
|
routes: defineRoutes<AppContext>([${names.singularCamel}Routes]),
|
|
4747
|
-
|
|
4748
|
-
mapUnhandledError: () => ({
|
|
4749
|
-
status: 500,
|
|
4750
|
-
body: { code: "INTERNAL_SERVER_ERROR", message: "Internal server error" },
|
|
4751
|
-
}),
|
|
6735
|
+
context: appContext,
|
|
4752
6736
|
});
|
|
4753
|
-
|
|
4754
|
-
const createdViaRoute = await
|
|
6737
|
+
${options.tenant ? `\t\tconst requester = createTestRequester(app, {\n\t\t\theaders: { "x-tenant-id": "tenant_test" },\n\t\t});\n` : ""}
|
|
6738
|
+
const createdViaRoute = await ${requestTarget}.request(create${names.singularPascal}, {
|
|
4755
6739
|
body: { name: "Second ${names.singularPascal}" },
|
|
4756
6740
|
});
|
|
4757
|
-
const listedViaRoute = await
|
|
4758
|
-
query: {
|
|
6741
|
+
${mode === "resource" ? ` const fetchedViaRoute = await ${requestTarget}.request(get${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n });\n const updatedViaRoute = await ${requestTarget}.request(update${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n body: { name: "Updated via route", version: fetchedViaRoute.version },\n });\n` : ""} const listedViaRoute = await ${requestTarget}.request(list${names.pluralPascal}, {
|
|
6742
|
+
query: { name: "${mode === "resource" ? "Updated via route" : "Second"}" },
|
|
4759
6743
|
});
|
|
4760
|
-
await app.stop();
|
|
6744
|
+
${mode === "resource" ? ` const deletedViaRoute = await ${requestTarget}.request(delete${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n });\n const listedAfterDeleteViaRoute = await ${requestTarget}.request(list${names.pluralPascal}, {\n query: { name: "Updated via route" },\n });\n` : ""} await app.stop();
|
|
4761
6745
|
|
|
4762
6746
|
expect(createdViaRoute.name).toBe("Second ${names.singularPascal}");
|
|
4763
|
-
expect(
|
|
4764
|
-
expect(listedViaRoute.items).toContainEqual(createdViaRoute);
|
|
6747
|
+
${mode === "resource" ? ` expect(fetchedViaRoute).toEqual(createdViaRoute);\n expect(updatedViaRoute.name).toBe("Updated via route");\n expect(updatedViaRoute.version).toBe(fetchedViaRoute.version + 1);\n expect(deletedViaRoute).toBeUndefined();\n expect(listedViaRoute.items).toContainEqual(updatedViaRoute);\n expect(listedAfterDeleteViaRoute.items).toEqual([]);\n` : ` expect(listedViaRoute.items).toContainEqual(createdViaRoute);\n`}
|
|
4765
6748
|
});
|
|
4766
6749
|
});
|
|
4767
6750
|
`;
|