@beignet/cli 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +205 -0
- package/README.md +379 -61
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +72 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +88 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +42 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +136 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +4 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +16 -26
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +657 -588
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1756 -394
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates.d.ts +3 -32
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +1002 -527
- package/dist/templates.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +137 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +182 -0
- package/src/create.ts +24 -31
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1067 -803
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2181 -405
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates.ts +1073 -567
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/src/create-bin.ts +0 -11
package/src/make.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import type { MakeFeatureAddon } from "./choices.js";
|
|
3
4
|
import {
|
|
4
5
|
type BeignetConfig,
|
|
5
6
|
directoryPath,
|
|
@@ -15,27 +16,22 @@ type MakeResourceOptions = {
|
|
|
15
16
|
force?: boolean;
|
|
16
17
|
dryRun?: boolean;
|
|
17
18
|
config?: BeignetConfig;
|
|
19
|
+
auth?: boolean;
|
|
20
|
+
tenant?: boolean;
|
|
21
|
+
events?: boolean;
|
|
22
|
+
softDelete?: boolean;
|
|
18
23
|
};
|
|
19
24
|
|
|
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];
|
|
25
|
+
export type { MakeFeatureAddon } from "./choices.js";
|
|
26
|
+
export { makeFeatureAddonChoices } from "./choices.js";
|
|
33
27
|
|
|
34
28
|
type NormalizedMakeFeatureAddon =
|
|
35
29
|
| "policy"
|
|
30
|
+
| "task"
|
|
36
31
|
| "event"
|
|
37
32
|
| "job"
|
|
38
33
|
| "notification"
|
|
34
|
+
| "ui"
|
|
39
35
|
| "upload";
|
|
40
36
|
|
|
41
37
|
type MakeFeatureOptions = MakeResourceOptions & {
|
|
@@ -58,6 +54,14 @@ type MakeUseCaseOptions = {
|
|
|
58
54
|
config?: BeignetConfig;
|
|
59
55
|
};
|
|
60
56
|
|
|
57
|
+
type MakeTaskOptions = {
|
|
58
|
+
name: string;
|
|
59
|
+
cwd?: string;
|
|
60
|
+
force?: boolean;
|
|
61
|
+
dryRun?: boolean;
|
|
62
|
+
config?: BeignetConfig;
|
|
63
|
+
};
|
|
64
|
+
|
|
61
65
|
type MakeTestOptions = {
|
|
62
66
|
name: string;
|
|
63
67
|
cwd?: string;
|
|
@@ -159,6 +163,7 @@ type MakeUploadOptions = {
|
|
|
159
163
|
};
|
|
160
164
|
|
|
161
165
|
type MakeResult = {
|
|
166
|
+
schemaVersion: 1;
|
|
162
167
|
name: string;
|
|
163
168
|
targetDir: string;
|
|
164
169
|
dryRun: boolean;
|
|
@@ -184,6 +189,10 @@ export type MakeContractResult = MakeResult;
|
|
|
184
189
|
* Result returned by `beignet make use-case`.
|
|
185
190
|
*/
|
|
186
191
|
export type MakeUseCaseResult = MakeResult;
|
|
192
|
+
/**
|
|
193
|
+
* Result returned by `beignet make task`.
|
|
194
|
+
*/
|
|
195
|
+
export type MakeTaskResult = MakeResult;
|
|
187
196
|
/**
|
|
188
197
|
* Result returned by `beignet make test`.
|
|
189
198
|
*/
|
|
@@ -243,6 +252,15 @@ type ResourceNames = {
|
|
|
243
252
|
pluralPascal: string;
|
|
244
253
|
};
|
|
245
254
|
|
|
255
|
+
type ResourceGenerationMode = "feature" | "resource";
|
|
256
|
+
|
|
257
|
+
type ResourceGenerationOptions = {
|
|
258
|
+
auth: boolean;
|
|
259
|
+
tenant: boolean;
|
|
260
|
+
events: boolean;
|
|
261
|
+
softDelete: boolean;
|
|
262
|
+
};
|
|
263
|
+
|
|
246
264
|
type GeneratedFile = {
|
|
247
265
|
path: string;
|
|
248
266
|
content: string;
|
|
@@ -300,6 +318,13 @@ type JobNames = FeatureArtifactNames & {
|
|
|
300
318
|
payloadTypeName: string;
|
|
301
319
|
};
|
|
302
320
|
|
|
321
|
+
type TaskNames = FeatureArtifactNames & {
|
|
322
|
+
taskName: string;
|
|
323
|
+
taskExportName: string;
|
|
324
|
+
inputSchemaName: string;
|
|
325
|
+
inputTypeName: string;
|
|
326
|
+
};
|
|
327
|
+
|
|
303
328
|
type FactoryNames = FeatureArtifactNames & {
|
|
304
329
|
factoryName: string;
|
|
305
330
|
factoryExportName: string;
|
|
@@ -339,8 +364,51 @@ type UploadNames = FeatureArtifactNames & {
|
|
|
339
364
|
metadataTypeName: string;
|
|
340
365
|
};
|
|
341
366
|
|
|
367
|
+
type FeatureUiNames = ResourceNames & {
|
|
368
|
+
componentExportName: string;
|
|
369
|
+
componentFileName: string;
|
|
370
|
+
};
|
|
371
|
+
|
|
342
372
|
export async function makeResource(
|
|
343
373
|
options: MakeResourceOptions,
|
|
374
|
+
): Promise<MakeResourceResult> {
|
|
375
|
+
return makeResourceSlice(options, "resource");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function makeFeature(
|
|
379
|
+
options: MakeFeatureOptions,
|
|
380
|
+
): Promise<MakeFeatureResult> {
|
|
381
|
+
const addons = normalizeMakeFeatureAddons(options.with ?? []);
|
|
382
|
+
if (addons.length === 0) {
|
|
383
|
+
return makeFeatureResource(options);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (options.dryRun) {
|
|
387
|
+
return makeFeatureSlice(options, addons);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
391
|
+
return makeFeatureSlice(options, addons);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function makeFeatureResource(
|
|
395
|
+
options: MakeResourceOptions,
|
|
396
|
+
): Promise<MakeResourceResult> {
|
|
397
|
+
try {
|
|
398
|
+
return await makeResourceSlice(options, "feature");
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error instanceof Error && error.message.includes("make resource")) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
error.message.replaceAll("make resource", "make feature"),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function makeResourceSlice(
|
|
410
|
+
options: MakeResourceOptions,
|
|
411
|
+
mode: ResourceGenerationMode,
|
|
344
412
|
): Promise<MakeResourceResult> {
|
|
345
413
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
346
414
|
const names = resourceNames(options.name);
|
|
@@ -349,9 +417,24 @@ export async function makeResource(
|
|
|
349
417
|
: await loadBeignetConfig(targetDir);
|
|
350
418
|
|
|
351
419
|
await assertStandardApp(targetDir, config);
|
|
420
|
+
if (
|
|
421
|
+
mode === "resource" &&
|
|
422
|
+
!(await fileExists(path.join(targetDir, resourceSharedErrorsPath(config))))
|
|
423
|
+
) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`beignet make resource expects an app error catalog. Missing ${resourceSharedErrorsPath(config)}.`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
352
428
|
|
|
353
429
|
const persistence = await detectResourcePersistence(targetDir, config);
|
|
354
|
-
const
|
|
430
|
+
const generationOptions = resourceGenerationOptions(options, mode);
|
|
431
|
+
const generatedFiles = resourceFiles(
|
|
432
|
+
names,
|
|
433
|
+
config,
|
|
434
|
+
persistence,
|
|
435
|
+
mode,
|
|
436
|
+
generationOptions,
|
|
437
|
+
);
|
|
355
438
|
const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
|
|
356
439
|
force: Boolean(options.force),
|
|
357
440
|
});
|
|
@@ -362,6 +445,8 @@ export async function makeResource(
|
|
|
362
445
|
persistence,
|
|
363
446
|
{
|
|
364
447
|
dryRun: true,
|
|
448
|
+
mode,
|
|
449
|
+
generationOptions,
|
|
365
450
|
},
|
|
366
451
|
);
|
|
367
452
|
const createdFiles = plannedFiles
|
|
@@ -385,11 +470,14 @@ export async function makeResource(
|
|
|
385
470
|
...(await updateResourceWiring(targetDir, names, config, {
|
|
386
471
|
persistence,
|
|
387
472
|
dryRun: false,
|
|
473
|
+
mode,
|
|
474
|
+
generationOptions,
|
|
388
475
|
})),
|
|
389
476
|
);
|
|
390
477
|
}
|
|
391
478
|
|
|
392
479
|
return {
|
|
480
|
+
schemaVersion: 1,
|
|
393
481
|
name: names.pluralKebab,
|
|
394
482
|
targetDir,
|
|
395
483
|
dryRun: Boolean(options.dryRun),
|
|
@@ -400,35 +488,20 @@ export async function makeResource(
|
|
|
400
488
|
};
|
|
401
489
|
}
|
|
402
490
|
|
|
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(
|
|
491
|
+
function resourceGenerationOptions(
|
|
420
492
|
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;
|
|
493
|
+
mode: ResourceGenerationMode,
|
|
494
|
+
): ResourceGenerationOptions {
|
|
495
|
+
if (mode !== "resource") {
|
|
496
|
+
return { auth: false, tenant: false, events: false, softDelete: false };
|
|
431
497
|
}
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
auth: Boolean(options.auth),
|
|
501
|
+
tenant: Boolean(options.tenant),
|
|
502
|
+
events: Boolean(options.events),
|
|
503
|
+
softDelete: Boolean(options.softDelete),
|
|
504
|
+
};
|
|
432
505
|
}
|
|
433
506
|
|
|
434
507
|
async function makeFeatureSlice(
|
|
@@ -462,6 +535,9 @@ async function makeFeatureAddon(
|
|
|
462
535
|
if (addon === "policy") {
|
|
463
536
|
return makePolicy({ ...shared, name: feature });
|
|
464
537
|
}
|
|
538
|
+
if (addon === "task") {
|
|
539
|
+
return makeTask({ ...shared, name: `${feature}/backfill` });
|
|
540
|
+
}
|
|
465
541
|
if (addon === "event") {
|
|
466
542
|
return makeEvent({ ...shared, name: `${feature}/created` });
|
|
467
543
|
}
|
|
@@ -471,6 +547,9 @@ async function makeFeatureAddon(
|
|
|
471
547
|
if (addon === "notification") {
|
|
472
548
|
return makeNotification({ ...shared, name: `${feature}/created` });
|
|
473
549
|
}
|
|
550
|
+
if (addon === "ui") {
|
|
551
|
+
return makeFeatureUi({ ...shared, name: feature });
|
|
552
|
+
}
|
|
474
553
|
|
|
475
554
|
return makeUpload({ ...shared, name: `${feature}/attachment` });
|
|
476
555
|
}
|
|
@@ -482,6 +561,7 @@ function normalizeMakeFeatureAddons(
|
|
|
482
561
|
|
|
483
562
|
for (const addon of addons) {
|
|
484
563
|
if (addon === "events") normalized.add("event");
|
|
564
|
+
else if (addon === "tasks") normalized.add("task");
|
|
485
565
|
else if (addon === "jobs") normalized.add("job");
|
|
486
566
|
else if (addon === "notifications") normalized.add("notification");
|
|
487
567
|
else if (addon === "uploads") normalized.add("upload");
|
|
@@ -493,9 +573,11 @@ function normalizeMakeFeatureAddons(
|
|
|
493
573
|
|
|
494
574
|
const featureAddonOrder: readonly NormalizedMakeFeatureAddon[] = [
|
|
495
575
|
"policy",
|
|
576
|
+
"task",
|
|
496
577
|
"event",
|
|
497
578
|
"job",
|
|
498
579
|
"notification",
|
|
580
|
+
"ui",
|
|
499
581
|
"upload",
|
|
500
582
|
];
|
|
501
583
|
|
|
@@ -537,6 +619,7 @@ export async function makeContract(
|
|
|
537
619
|
}
|
|
538
620
|
|
|
539
621
|
return {
|
|
622
|
+
schemaVersion: 1,
|
|
540
623
|
name: names.pluralKebab,
|
|
541
624
|
targetDir,
|
|
542
625
|
dryRun: Boolean(options.dryRun),
|
|
@@ -582,6 +665,7 @@ export async function makeUseCase(
|
|
|
582
665
|
if (indexResult === "skipped") skippedFiles.push(indexFile.path);
|
|
583
666
|
|
|
584
667
|
return {
|
|
668
|
+
schemaVersion: 1,
|
|
585
669
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
586
670
|
targetDir,
|
|
587
671
|
dryRun: Boolean(options.dryRun),
|
|
@@ -627,6 +711,7 @@ export async function makeTest(
|
|
|
627
711
|
}
|
|
628
712
|
|
|
629
713
|
return {
|
|
714
|
+
schemaVersion: 1,
|
|
630
715
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
631
716
|
targetDir,
|
|
632
717
|
dryRun: Boolean(options.dryRun),
|
|
@@ -689,6 +774,7 @@ export async function makePort(
|
|
|
689
774
|
}
|
|
690
775
|
|
|
691
776
|
return {
|
|
777
|
+
schemaVersion: 1,
|
|
692
778
|
name: names.kebab,
|
|
693
779
|
targetDir,
|
|
694
780
|
dryRun: Boolean(options.dryRun),
|
|
@@ -734,6 +820,7 @@ export async function makeAdapter(
|
|
|
734
820
|
}
|
|
735
821
|
|
|
736
822
|
return {
|
|
823
|
+
schemaVersion: 1,
|
|
737
824
|
name: names.kebab,
|
|
738
825
|
targetDir,
|
|
739
826
|
dryRun: Boolean(options.dryRun),
|
|
@@ -771,6 +858,7 @@ export async function makePolicy(
|
|
|
771
858
|
}
|
|
772
859
|
|
|
773
860
|
return {
|
|
861
|
+
schemaVersion: 1,
|
|
774
862
|
name: names.kebab,
|
|
775
863
|
targetDir,
|
|
776
864
|
dryRun: Boolean(options.dryRun),
|
|
@@ -821,7 +909,10 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
821
909
|
force: Boolean(options.force),
|
|
822
910
|
dryRun: Boolean(options.dryRun),
|
|
823
911
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
824
|
-
files:
|
|
912
|
+
files: [
|
|
913
|
+
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
914
|
+
...jobFiles(names, config),
|
|
915
|
+
],
|
|
825
916
|
index: featureArtifactIndexFile("job", names, config),
|
|
826
917
|
dependencies: {
|
|
827
918
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -829,6 +920,49 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
829
920
|
});
|
|
830
921
|
}
|
|
831
922
|
|
|
923
|
+
export async function makeTask(
|
|
924
|
+
options: MakeTaskOptions,
|
|
925
|
+
): Promise<MakeTaskResult> {
|
|
926
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
927
|
+
const names = taskNames(options.name);
|
|
928
|
+
const config = options.config
|
|
929
|
+
? resolveConfig(options.config)
|
|
930
|
+
: await loadBeignetConfig(targetDir);
|
|
931
|
+
|
|
932
|
+
await assertFeatureArtifactApp(targetDir, config, "task");
|
|
933
|
+
|
|
934
|
+
const result = await makeFeatureArtifact({
|
|
935
|
+
targetDir,
|
|
936
|
+
config,
|
|
937
|
+
force: Boolean(options.force),
|
|
938
|
+
dryRun: Boolean(options.dryRun),
|
|
939
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
940
|
+
files: [
|
|
941
|
+
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
942
|
+
...taskFiles(names, config),
|
|
943
|
+
],
|
|
944
|
+
index: featureArtifactIndexFile("task", names, config),
|
|
945
|
+
dependencies: {
|
|
946
|
+
"@beignet/core": beignetDependencyVersion,
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
const registryResult = await updateTaskRegistry(targetDir, names, config, {
|
|
951
|
+
dryRun: Boolean(options.dryRun),
|
|
952
|
+
});
|
|
953
|
+
if (registryResult === "created")
|
|
954
|
+
result.createdFiles.push(config.paths.tasks);
|
|
955
|
+
if (registryResult === "updated")
|
|
956
|
+
result.updatedFiles.push(config.paths.tasks);
|
|
957
|
+
if (registryResult === "skipped")
|
|
958
|
+
result.skippedFiles.push(config.paths.tasks);
|
|
959
|
+
if (!result.files.includes(config.paths.tasks)) {
|
|
960
|
+
result.files.push(config.paths.tasks);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
|
|
832
966
|
export async function makeFactory(
|
|
833
967
|
options: MakeFactoryOptions,
|
|
834
968
|
): Promise<MakeFactoryResult> {
|
|
@@ -896,7 +1030,14 @@ export async function makeNotification(
|
|
|
896
1030
|
force: Boolean(options.force),
|
|
897
1031
|
dryRun: Boolean(options.dryRun),
|
|
898
1032
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
899
|
-
files:
|
|
1033
|
+
files: [
|
|
1034
|
+
...(await missingCapabilityBuilderFiles(
|
|
1035
|
+
targetDir,
|
|
1036
|
+
"notifications",
|
|
1037
|
+
config,
|
|
1038
|
+
)),
|
|
1039
|
+
...notificationFiles(names, config),
|
|
1040
|
+
],
|
|
900
1041
|
index: featureArtifactIndexFile("notification", names, config),
|
|
901
1042
|
dependencies: {
|
|
902
1043
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -922,7 +1063,10 @@ export async function makeListener(
|
|
|
922
1063
|
force: Boolean(options.force),
|
|
923
1064
|
dryRun: Boolean(options.dryRun),
|
|
924
1065
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
925
|
-
files:
|
|
1066
|
+
files: [
|
|
1067
|
+
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
1068
|
+
...listenerFiles(names, config),
|
|
1069
|
+
],
|
|
926
1070
|
index: featureArtifactIndexFile("listener", names, config),
|
|
927
1071
|
dependencies: {
|
|
928
1072
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -962,7 +1106,10 @@ export async function makeSchedule(
|
|
|
962
1106
|
requireServer: Boolean(options.route),
|
|
963
1107
|
});
|
|
964
1108
|
|
|
965
|
-
const files =
|
|
1109
|
+
const files = [
|
|
1110
|
+
...(await missingCapabilityBuilderFiles(targetDir, "schedules", config)),
|
|
1111
|
+
...scheduleFiles(names, config, { route: Boolean(options.route) }),
|
|
1112
|
+
];
|
|
966
1113
|
|
|
967
1114
|
return makeFeatureArtifact({
|
|
968
1115
|
targetDir,
|
|
@@ -1003,6 +1150,70 @@ export async function makeUpload(
|
|
|
1003
1150
|
});
|
|
1004
1151
|
}
|
|
1005
1152
|
|
|
1153
|
+
async function makeFeatureUi(
|
|
1154
|
+
options: MakeResourceOptions,
|
|
1155
|
+
): Promise<MakeResult> {
|
|
1156
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
1157
|
+
const names = featureUiNames(options.name);
|
|
1158
|
+
const config = options.config
|
|
1159
|
+
? resolveConfig(options.config)
|
|
1160
|
+
: await loadBeignetConfig(targetDir);
|
|
1161
|
+
|
|
1162
|
+
await assertFeatureUiApp(targetDir, config);
|
|
1163
|
+
|
|
1164
|
+
const componentFiles = featureUiComponentFiles(names, config);
|
|
1165
|
+
const indexFile = featureUiIndexFile(names, config);
|
|
1166
|
+
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
1167
|
+
force: Boolean(options.force),
|
|
1168
|
+
});
|
|
1169
|
+
const createdFiles = plannedFiles
|
|
1170
|
+
.filter((file) => file.result === "created")
|
|
1171
|
+
.map((file) => file.path);
|
|
1172
|
+
const updatedFiles = plannedFiles
|
|
1173
|
+
.filter((file) => file.result === "updated")
|
|
1174
|
+
.map((file) => file.path);
|
|
1175
|
+
const skippedFiles = plannedFiles
|
|
1176
|
+
.filter((file) => file.result === "skipped")
|
|
1177
|
+
.map((file) => file.path);
|
|
1178
|
+
|
|
1179
|
+
if (!options.dryRun) {
|
|
1180
|
+
for (const file of plannedFiles) {
|
|
1181
|
+
await writePlannedGeneratedFile(targetDir, file);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const indexResult = await updateFeatureUiIndex(targetDir, indexFile, {
|
|
1186
|
+
dryRun: Boolean(options.dryRun),
|
|
1187
|
+
});
|
|
1188
|
+
if (indexResult === "created") createdFiles.push(indexFile.path);
|
|
1189
|
+
if (indexResult === "updated") updatedFiles.push(indexFile.path);
|
|
1190
|
+
if (indexResult === "skipped") skippedFiles.push(indexFile.path);
|
|
1191
|
+
|
|
1192
|
+
if (
|
|
1193
|
+
await updatePackageDependencies(
|
|
1194
|
+
targetDir,
|
|
1195
|
+
{
|
|
1196
|
+
"@beignet/react-query": beignetDependencyVersion,
|
|
1197
|
+
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
1198
|
+
},
|
|
1199
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
1200
|
+
)
|
|
1201
|
+
) {
|
|
1202
|
+
updatedFiles.push("package.json");
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
return {
|
|
1206
|
+
schemaVersion: 1,
|
|
1207
|
+
name: names.pluralKebab,
|
|
1208
|
+
targetDir,
|
|
1209
|
+
dryRun: Boolean(options.dryRun),
|
|
1210
|
+
files: [...componentFiles.map((file) => file.path), indexFile.path],
|
|
1211
|
+
createdFiles,
|
|
1212
|
+
updatedFiles,
|
|
1213
|
+
skippedFiles,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1006
1217
|
async function assertStandardApp(
|
|
1007
1218
|
targetDir: string,
|
|
1008
1219
|
config: ResolvedBeignetConfig,
|
|
@@ -1131,6 +1342,7 @@ async function assertFeatureArtifactApp(
|
|
|
1131
1342
|
artifact:
|
|
1132
1343
|
| "event"
|
|
1133
1344
|
| "job"
|
|
1345
|
+
| "task"
|
|
1134
1346
|
| "notification"
|
|
1135
1347
|
| "listener"
|
|
1136
1348
|
| "schedule"
|
|
@@ -1153,6 +1365,24 @@ async function assertFeatureArtifactApp(
|
|
|
1153
1365
|
}
|
|
1154
1366
|
}
|
|
1155
1367
|
|
|
1368
|
+
async function assertFeatureUiApp(
|
|
1369
|
+
targetDir: string,
|
|
1370
|
+
config: ResolvedBeignetConfig,
|
|
1371
|
+
): Promise<void> {
|
|
1372
|
+
const clientPath = clientIndexPath(config);
|
|
1373
|
+
const requiredFiles = [config.paths.appContext, clientPath];
|
|
1374
|
+
|
|
1375
|
+
for (const file of requiredFiles) {
|
|
1376
|
+
try {
|
|
1377
|
+
await stat(path.join(targetDir, file));
|
|
1378
|
+
} catch {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`beignet make feature --with ui expects a standard Beignet app with React Query client helpers. Missing ${file}.`,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1156
1386
|
async function assertPersistenceArtifactApp(
|
|
1157
1387
|
targetDir: string,
|
|
1158
1388
|
names: FactoryNames | SeedNames,
|
|
@@ -1264,6 +1494,83 @@ async function readOptionalFile(filePath: string): Promise<string | undefined> {
|
|
|
1264
1494
|
}
|
|
1265
1495
|
}
|
|
1266
1496
|
|
|
1497
|
+
type CapabilityBuilder =
|
|
1498
|
+
| "listeners"
|
|
1499
|
+
| "jobs"
|
|
1500
|
+
| "schedules"
|
|
1501
|
+
| "notifications"
|
|
1502
|
+
| "tasks";
|
|
1503
|
+
|
|
1504
|
+
const capabilityBuilders: Record<
|
|
1505
|
+
CapabilityBuilder,
|
|
1506
|
+
{ factoryName: string; defineName: string; module: string }
|
|
1507
|
+
> = {
|
|
1508
|
+
listeners: {
|
|
1509
|
+
factoryName: "createListeners",
|
|
1510
|
+
defineName: "defineListener",
|
|
1511
|
+
module: "@beignet/core/events",
|
|
1512
|
+
},
|
|
1513
|
+
jobs: {
|
|
1514
|
+
factoryName: "createJobs",
|
|
1515
|
+
defineName: "defineJob",
|
|
1516
|
+
module: "@beignet/core/jobs",
|
|
1517
|
+
},
|
|
1518
|
+
schedules: {
|
|
1519
|
+
factoryName: "createSchedules",
|
|
1520
|
+
defineName: "defineSchedule",
|
|
1521
|
+
module: "@beignet/core/schedules",
|
|
1522
|
+
},
|
|
1523
|
+
notifications: {
|
|
1524
|
+
factoryName: "createNotifications",
|
|
1525
|
+
defineName: "defineNotification",
|
|
1526
|
+
module: "@beignet/core/notifications",
|
|
1527
|
+
},
|
|
1528
|
+
tasks: {
|
|
1529
|
+
factoryName: "createTasks",
|
|
1530
|
+
defineName: "defineTask",
|
|
1531
|
+
module: "@beignet/core/tasks",
|
|
1532
|
+
},
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
function capabilityBuilderPath(
|
|
1536
|
+
capability: CapabilityBuilder,
|
|
1537
|
+
config: ResolvedBeignetConfig,
|
|
1538
|
+
): string {
|
|
1539
|
+
if (capability === "listeners") return config.paths.listenersBuilder;
|
|
1540
|
+
if (capability === "jobs") return config.paths.jobsBuilder;
|
|
1541
|
+
if (capability === "schedules") return config.paths.schedulesBuilder;
|
|
1542
|
+
if (capability === "notifications") return config.paths.notificationsBuilder;
|
|
1543
|
+
return config.paths.tasksBuilder;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function capabilityBuilderFile(
|
|
1547
|
+
capability: CapabilityBuilder,
|
|
1548
|
+
config: ResolvedBeignetConfig,
|
|
1549
|
+
): GeneratedFile {
|
|
1550
|
+
const builder = capabilityBuilders[capability];
|
|
1551
|
+
|
|
1552
|
+
return {
|
|
1553
|
+
path: capabilityBuilderPath(capability, config),
|
|
1554
|
+
content: `import { ${builder.factoryName} } from "${builder.module}";
|
|
1555
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
1556
|
+
|
|
1557
|
+
export const { ${builder.defineName} } = ${builder.factoryName}<AppContext>();
|
|
1558
|
+
`,
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async function missingCapabilityBuilderFiles(
|
|
1563
|
+
targetDir: string,
|
|
1564
|
+
capability: CapabilityBuilder,
|
|
1565
|
+
config: ResolvedBeignetConfig,
|
|
1566
|
+
): Promise<GeneratedFile[]> {
|
|
1567
|
+
const file = capabilityBuilderFile(capability, config);
|
|
1568
|
+
if (await fileExists(path.join(targetDir, file.path))) {
|
|
1569
|
+
return [];
|
|
1570
|
+
}
|
|
1571
|
+
return [file];
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1267
1574
|
async function makeFeatureArtifact(options: {
|
|
1268
1575
|
targetDir: string;
|
|
1269
1576
|
config: ResolvedBeignetConfig;
|
|
@@ -1315,6 +1622,7 @@ async function makeFeatureArtifact(options: {
|
|
|
1315
1622
|
}
|
|
1316
1623
|
|
|
1317
1624
|
return {
|
|
1625
|
+
schemaVersion: 1,
|
|
1318
1626
|
name: options.name,
|
|
1319
1627
|
targetDir: options.targetDir,
|
|
1320
1628
|
dryRun: options.dryRun,
|
|
@@ -1358,14 +1666,29 @@ function beignetDependencyVersion(packageJson: PackageJsonLike): string {
|
|
|
1358
1666
|
);
|
|
1359
1667
|
}
|
|
1360
1668
|
|
|
1669
|
+
function tanstackReactQueryDependencyVersion(
|
|
1670
|
+
packageJson: PackageJsonLike,
|
|
1671
|
+
): string {
|
|
1672
|
+
return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1361
1675
|
async function updateResourceWiring(
|
|
1362
1676
|
targetDir: string,
|
|
1363
1677
|
names: ResourceNames,
|
|
1364
1678
|
config: ResolvedBeignetConfig,
|
|
1365
1679
|
persistenceOrOptions:
|
|
1366
1680
|
| ResourcePersistence
|
|
1367
|
-
| {
|
|
1368
|
-
|
|
1681
|
+
| {
|
|
1682
|
+
dryRun: boolean;
|
|
1683
|
+
persistence?: ResourcePersistence;
|
|
1684
|
+
mode?: ResourceGenerationMode;
|
|
1685
|
+
generationOptions?: ResourceGenerationOptions;
|
|
1686
|
+
},
|
|
1687
|
+
maybeOptions?: {
|
|
1688
|
+
dryRun: boolean;
|
|
1689
|
+
mode?: ResourceGenerationMode;
|
|
1690
|
+
generationOptions?: ResourceGenerationOptions;
|
|
1691
|
+
},
|
|
1369
1692
|
): Promise<string[]> {
|
|
1370
1693
|
const persistence =
|
|
1371
1694
|
typeof persistenceOrOptions === "string"
|
|
@@ -1375,6 +1698,16 @@ async function updateResourceWiring(
|
|
|
1375
1698
|
typeof persistenceOrOptions === "string"
|
|
1376
1699
|
? (maybeOptions as { dryRun: boolean })
|
|
1377
1700
|
: persistenceOrOptions;
|
|
1701
|
+
const mode =
|
|
1702
|
+
typeof persistenceOrOptions === "string"
|
|
1703
|
+
? (maybeOptions?.mode ?? "feature")
|
|
1704
|
+
: (persistenceOrOptions.mode ?? "feature");
|
|
1705
|
+
const generationOptions =
|
|
1706
|
+
typeof persistenceOrOptions === "string"
|
|
1707
|
+
? (maybeOptions?.generationOptions ??
|
|
1708
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode))
|
|
1709
|
+
: (persistenceOrOptions.generationOptions ??
|
|
1710
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode));
|
|
1378
1711
|
const updated = new Set<string>();
|
|
1379
1712
|
if (
|
|
1380
1713
|
await updatePackageJson(targetDir, {
|
|
@@ -1384,7 +1717,12 @@ async function updateResourceWiring(
|
|
|
1384
1717
|
) {
|
|
1385
1718
|
updated.add("package.json");
|
|
1386
1719
|
}
|
|
1387
|
-
if (
|
|
1720
|
+
if (
|
|
1721
|
+
await updatePortsIndex(targetDir, names, config, {
|
|
1722
|
+
...options,
|
|
1723
|
+
generationOptions,
|
|
1724
|
+
})
|
|
1725
|
+
) {
|
|
1388
1726
|
updated.add(config.paths.ports);
|
|
1389
1727
|
}
|
|
1390
1728
|
if (
|
|
@@ -1393,6 +1731,18 @@ async function updateResourceWiring(
|
|
|
1393
1731
|
) {
|
|
1394
1732
|
updated.add(config.paths.infrastructurePorts);
|
|
1395
1733
|
}
|
|
1734
|
+
if (
|
|
1735
|
+
generationOptions.auth &&
|
|
1736
|
+
(await updateInfrastructureGatePolicies(targetDir, names, config, options))
|
|
1737
|
+
) {
|
|
1738
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1739
|
+
}
|
|
1740
|
+
if (
|
|
1741
|
+
persistence === "drizzle" &&
|
|
1742
|
+
(await updateInfrastructureDeferredPorts(targetDir, names, config, options))
|
|
1743
|
+
) {
|
|
1744
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1745
|
+
}
|
|
1396
1746
|
if (
|
|
1397
1747
|
persistence === "drizzle" &&
|
|
1398
1748
|
(await updateDrizzleSchemaIndex(targetDir, names, config, options))
|
|
@@ -1411,7 +1761,14 @@ async function updateResourceWiring(
|
|
|
1411
1761
|
) {
|
|
1412
1762
|
updated.add(config.paths.server);
|
|
1413
1763
|
}
|
|
1414
|
-
if (
|
|
1764
|
+
if (
|
|
1765
|
+
await updateSharedErrors(targetDir, names, config, { ...options, mode })
|
|
1766
|
+
) {
|
|
1767
|
+
updated.add(resourceSharedErrorsPath(config));
|
|
1768
|
+
}
|
|
1769
|
+
if (
|
|
1770
|
+
await updateOpenApiRoute(targetDir, names, config, { ...options, mode })
|
|
1771
|
+
) {
|
|
1415
1772
|
updated.add(config.paths.openapiRoute);
|
|
1416
1773
|
}
|
|
1417
1774
|
const routesFile = await updateServerRoutes(
|
|
@@ -1457,7 +1814,7 @@ async function updatePortsIndex(
|
|
|
1457
1814
|
targetDir: string,
|
|
1458
1815
|
names: ResourceNames,
|
|
1459
1816
|
config: ResolvedBeignetConfig,
|
|
1460
|
-
options: { dryRun: boolean },
|
|
1817
|
+
options: { dryRun: boolean; generationOptions?: ResourceGenerationOptions },
|
|
1461
1818
|
): Promise<boolean> {
|
|
1462
1819
|
const filePath = path.join(targetDir, config.paths.ports);
|
|
1463
1820
|
const original = await readFile(filePath, "utf8");
|
|
@@ -1469,6 +1826,30 @@ async function updatePortsIndex(
|
|
|
1469
1826
|
next = insertAfterImports(next, importLine);
|
|
1470
1827
|
}
|
|
1471
1828
|
|
|
1829
|
+
if (options.generationOptions?.auth) {
|
|
1830
|
+
const policyPath = path.join(
|
|
1831
|
+
config.paths.features,
|
|
1832
|
+
names.pluralKebab,
|
|
1833
|
+
"policy.ts",
|
|
1834
|
+
);
|
|
1835
|
+
const policyImportLine = `import type { AuthorizationContext as ${names.singularPascal}AuthorizationContext, ${names.singularCamel}Policy } from "${relativeModule(config.paths.ports, policyPath)}";`;
|
|
1836
|
+
if (!next.includes(policyImportLine)) {
|
|
1837
|
+
next = insertAfterImports(next, policyImportLine);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
next = appendUnionTypeMember(
|
|
1841
|
+
next,
|
|
1842
|
+
"AppAuthorizationContext",
|
|
1843
|
+
`${names.singularPascal}AuthorizationContext`,
|
|
1844
|
+
);
|
|
1845
|
+
next = appendTupleTypeMember(
|
|
1846
|
+
next,
|
|
1847
|
+
"AppGate",
|
|
1848
|
+
`typeof ${names.singularCamel}Policy`,
|
|
1849
|
+
);
|
|
1850
|
+
next = appendGatePolicyMember(next, `typeof ${names.singularCamel}Policy`);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1472
1853
|
const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
|
|
1473
1854
|
if (!next.includes(entry.trim())) {
|
|
1474
1855
|
next = insertTypeProperty(
|
|
@@ -1625,7 +2006,7 @@ async function updateInfrastructurePorts(
|
|
|
1625
2006
|
if (!next.includes(repositoryConst)) {
|
|
1626
2007
|
next = replaceRequired(
|
|
1627
2008
|
next,
|
|
1628
|
-
/(\nexport const appPorts = definePorts(?:<[^>]+>)?\(\{)/,
|
|
2009
|
+
/(\nexport const appPorts = definePorts(?:<[^>]+>)?\((?:\)\()?\{)/,
|
|
1629
2010
|
`\n${repositoryConst}\n$1`,
|
|
1630
2011
|
`Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`,
|
|
1631
2012
|
);
|
|
@@ -1662,23 +2043,54 @@ async function updateInfrastructurePorts(
|
|
|
1662
2043
|
return true;
|
|
1663
2044
|
}
|
|
1664
2045
|
|
|
1665
|
-
async function
|
|
2046
|
+
async function updateInfrastructureGatePolicies(
|
|
1666
2047
|
targetDir: string,
|
|
1667
2048
|
names: ResourceNames,
|
|
1668
2049
|
config: ResolvedBeignetConfig,
|
|
1669
2050
|
options: { dryRun: boolean },
|
|
1670
2051
|
): Promise<boolean> {
|
|
1671
|
-
const filePath = path.join(targetDir,
|
|
1672
|
-
if (!(await fileExists(filePath))) return false;
|
|
1673
|
-
|
|
2052
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
1674
2053
|
const original = await readFile(filePath, "utf8");
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
}
|
|
2054
|
+
let next = original;
|
|
2055
|
+
const policyPath = path.join(
|
|
2056
|
+
config.paths.features,
|
|
2057
|
+
names.pluralKebab,
|
|
2058
|
+
"policy.ts",
|
|
2059
|
+
);
|
|
2060
|
+
const importLine = `import { ${names.singularCamel}Policy } from "${relativeModule(config.paths.infrastructurePorts, policyPath)}";`;
|
|
2061
|
+
|
|
2062
|
+
if (!next.includes(importLine)) {
|
|
2063
|
+
next = insertAfterImports(next, importLine);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
next = appendCreateGatePolicy(
|
|
2067
|
+
next,
|
|
2068
|
+
`${names.singularCamel}Policy`,
|
|
2069
|
+
`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.`,
|
|
2070
|
+
);
|
|
2071
|
+
|
|
2072
|
+
if (next === original) return false;
|
|
2073
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
async function updateDrizzleSchemaIndex(
|
|
2078
|
+
targetDir: string,
|
|
2079
|
+
names: ResourceNames,
|
|
2080
|
+
config: ResolvedBeignetConfig,
|
|
2081
|
+
options: { dryRun: boolean },
|
|
2082
|
+
): Promise<boolean> {
|
|
2083
|
+
const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
|
|
2084
|
+
if (!(await fileExists(filePath))) return false;
|
|
2085
|
+
|
|
2086
|
+
const original = await readFile(filePath, "utf8");
|
|
2087
|
+
const exportLine = `export { ${names.pluralCamel} } from "./${names.pluralKebab}";`;
|
|
2088
|
+
if (original.includes(exportLine)) return false;
|
|
2089
|
+
|
|
2090
|
+
const next = `${original.trimEnd()}\n${exportLine}\n`;
|
|
2091
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2092
|
+
return true;
|
|
2093
|
+
}
|
|
1682
2094
|
|
|
1683
2095
|
async function updateDrizzleRepositories(
|
|
1684
2096
|
targetDir: string,
|
|
@@ -1719,6 +2131,24 @@ async function updateDrizzleRepositories(
|
|
|
1719
2131
|
return true;
|
|
1720
2132
|
}
|
|
1721
2133
|
|
|
2134
|
+
async function updateInfrastructureDeferredPorts(
|
|
2135
|
+
targetDir: string,
|
|
2136
|
+
names: ResourceNames,
|
|
2137
|
+
config: ResolvedBeignetConfig,
|
|
2138
|
+
options: { dryRun: boolean },
|
|
2139
|
+
): Promise<boolean> {
|
|
2140
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
2141
|
+
const original = await readFile(filePath, "utf8");
|
|
2142
|
+
// Drizzle resources are provided by the app database provider at startup,
|
|
2143
|
+
// so deferred-form ports files defer the new repository key. Identity-form
|
|
2144
|
+
// ports files are left untouched.
|
|
2145
|
+
const next = appendDeferredPortKey(original, names.pluralCamel);
|
|
2146
|
+
|
|
2147
|
+
if (next === original) return false;
|
|
2148
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
1722
2152
|
async function updateServerTransactionPorts(
|
|
1723
2153
|
targetDir: string,
|
|
1724
2154
|
names: ResourceNames,
|
|
@@ -1759,7 +2189,7 @@ async function updateOpenApiRoute(
|
|
|
1759
2189
|
targetDir: string,
|
|
1760
2190
|
names: ResourceNames,
|
|
1761
2191
|
config: ResolvedBeignetConfig,
|
|
1762
|
-
options: { dryRun: boolean },
|
|
2192
|
+
options: { dryRun: boolean; mode: ResourceGenerationMode },
|
|
1763
2193
|
): Promise<boolean> {
|
|
1764
2194
|
const filePath = path.join(targetDir, config.paths.openapiRoute);
|
|
1765
2195
|
if (!(await fileExists(filePath))) return false;
|
|
@@ -1768,15 +2198,14 @@ async function updateOpenApiRoute(
|
|
|
1768
2198
|
const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
|
|
1769
2199
|
if (!firstArg?.text.startsWith("[")) return false;
|
|
1770
2200
|
|
|
1771
|
-
const
|
|
1772
|
-
const createName = `create${names.singularPascal}`;
|
|
2201
|
+
const contractNames = resourceContractNames(names, options.mode);
|
|
1773
2202
|
const listedContracts = identifiersFromArrayExpression(firstArg.text);
|
|
1774
|
-
const missingContracts =
|
|
2203
|
+
const missingContracts = contractNames.filter(
|
|
1775
2204
|
(contractName) => !listedContracts.has(contractName),
|
|
1776
2205
|
);
|
|
1777
2206
|
if (missingContracts.length === 0) return false;
|
|
1778
2207
|
|
|
1779
|
-
const importLine = `import {
|
|
2208
|
+
const importLine = `import { ${contractNames.join(", ")} } from "${aliasModule(resourceContractFilePath(names, config))}";`;
|
|
1780
2209
|
const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
|
|
1781
2210
|
let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(
|
|
1782
2211
|
firstArg.end,
|
|
@@ -1791,6 +2220,74 @@ async function updateOpenApiRoute(
|
|
|
1791
2220
|
return true;
|
|
1792
2221
|
}
|
|
1793
2222
|
|
|
2223
|
+
async function updateSharedErrors(
|
|
2224
|
+
targetDir: string,
|
|
2225
|
+
names: ResourceNames,
|
|
2226
|
+
config: ResolvedBeignetConfig,
|
|
2227
|
+
options: { dryRun: boolean; mode: ResourceGenerationMode },
|
|
2228
|
+
): Promise<boolean> {
|
|
2229
|
+
if (options.mode !== "resource") return false;
|
|
2230
|
+
|
|
2231
|
+
const filePath = path.join(targetDir, resourceSharedErrorsPath(config));
|
|
2232
|
+
if (!(await fileExists(filePath))) return false;
|
|
2233
|
+
|
|
2234
|
+
const original = await readFile(filePath, "utf8");
|
|
2235
|
+
const notFoundErrorName = `${names.singularPascal}NotFound`;
|
|
2236
|
+
const conflictErrorName = `${names.singularPascal}Conflict`;
|
|
2237
|
+
const missingErrorNames = [notFoundErrorName, conflictErrorName].filter(
|
|
2238
|
+
(errorName) => !new RegExp(`\\b${errorName}\\b`).test(original),
|
|
2239
|
+
);
|
|
2240
|
+
if (missingErrorNames.length === 0) return false;
|
|
2241
|
+
const expectedErrorNames = missingErrorNames.join(" and ");
|
|
2242
|
+
|
|
2243
|
+
const defineErrorsStart = original.indexOf("defineErrors(");
|
|
2244
|
+
if (defineErrorsStart === -1) {
|
|
2245
|
+
throw new Error(
|
|
2246
|
+
`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`,
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const openBrace = original.indexOf("{", defineErrorsStart);
|
|
2251
|
+
if (openBrace === -1) {
|
|
2252
|
+
throw new Error(
|
|
2253
|
+
`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`,
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
|
|
2258
|
+
if (closeBrace === -1) {
|
|
2259
|
+
throw new Error(
|
|
2260
|
+
`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.`,
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
const entries = [];
|
|
2265
|
+
if (missingErrorNames.includes(notFoundErrorName)) {
|
|
2266
|
+
entries.push(`${notFoundErrorName}: {
|
|
2267
|
+
\t\tcode: "${constantCase(names.singularKebab)}_NOT_FOUND",
|
|
2268
|
+
\t\tstatus: 404,
|
|
2269
|
+
\t\tmessage: "${names.singularPascal} not found",
|
|
2270
|
+
\t},`);
|
|
2271
|
+
}
|
|
2272
|
+
if (missingErrorNames.includes(conflictErrorName)) {
|
|
2273
|
+
entries.push(`${conflictErrorName}: {
|
|
2274
|
+
\t\tcode: "${constantCase(names.singularKebab)}_CONFLICT",
|
|
2275
|
+
\t\tstatus: 409,
|
|
2276
|
+
\t\tmessage: "${names.singularPascal} was changed by another request",
|
|
2277
|
+
\t},`);
|
|
2278
|
+
}
|
|
2279
|
+
const next = insertBeforeClosingBrace(
|
|
2280
|
+
original,
|
|
2281
|
+
openBrace,
|
|
2282
|
+
closeBrace,
|
|
2283
|
+
entries.join("\n"),
|
|
2284
|
+
);
|
|
2285
|
+
|
|
2286
|
+
if (next === original) return false;
|
|
2287
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2288
|
+
return true;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
1794
2291
|
async function updateServerRoutes(
|
|
1795
2292
|
targetDir: string,
|
|
1796
2293
|
names: ResourceNames,
|
|
@@ -2184,6 +2681,98 @@ function typeBodySource(source: string, typeName: string): string | undefined {
|
|
|
2184
2681
|
return source.slice(openBrace + 1, closeBrace);
|
|
2185
2682
|
}
|
|
2186
2683
|
|
|
2684
|
+
function appendUnionTypeMember(
|
|
2685
|
+
source: string,
|
|
2686
|
+
typeName: string,
|
|
2687
|
+
member: string,
|
|
2688
|
+
): string {
|
|
2689
|
+
const match = new RegExp(
|
|
2690
|
+
`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`,
|
|
2691
|
+
).exec(source);
|
|
2692
|
+
if (!match) return source;
|
|
2693
|
+
|
|
2694
|
+
const declaration = match[0];
|
|
2695
|
+
if (new RegExp(`\\b${member}\\b`).test(declaration)) return source;
|
|
2696
|
+
|
|
2697
|
+
const nextDeclaration = declaration.replace(/;\s*$/, ` | ${member};`);
|
|
2698
|
+
return `${source.slice(0, match.index)}${nextDeclaration}${source.slice(
|
|
2699
|
+
match.index + declaration.length,
|
|
2700
|
+
)}`;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
function appendTupleTypeMember(
|
|
2704
|
+
source: string,
|
|
2705
|
+
typeName: string,
|
|
2706
|
+
member: string,
|
|
2707
|
+
): string {
|
|
2708
|
+
const match = new RegExp(`export\\s+type\\s+${typeName}\\s*=\\s*`).exec(
|
|
2709
|
+
source,
|
|
2710
|
+
);
|
|
2711
|
+
if (!match) return source;
|
|
2712
|
+
|
|
2713
|
+
const tupleStart = source.indexOf("[", match.index);
|
|
2714
|
+
if (tupleStart === -1) return source;
|
|
2715
|
+
|
|
2716
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
2717
|
+
if (tupleEnd === -1) return source;
|
|
2718
|
+
|
|
2719
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
2720
|
+
if (tuple.includes(member)) return source;
|
|
2721
|
+
|
|
2722
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
2723
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(
|
|
2724
|
+
tupleEnd + 1,
|
|
2725
|
+
)}`;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
function appendGatePolicyMember(source: string, member: string): string {
|
|
2729
|
+
const gateMatch = /\bGatePort\s*</.exec(source);
|
|
2730
|
+
if (!gateMatch) return source;
|
|
2731
|
+
|
|
2732
|
+
const tupleStart = source.indexOf("[", gateMatch.index);
|
|
2733
|
+
if (tupleStart === -1) return source;
|
|
2734
|
+
|
|
2735
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
2736
|
+
if (tupleEnd === -1) return source;
|
|
2737
|
+
|
|
2738
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
2739
|
+
if (tuple.includes(member)) return source;
|
|
2740
|
+
|
|
2741
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
2742
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(
|
|
2743
|
+
tupleEnd + 1,
|
|
2744
|
+
)}`;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function appendCreateGatePolicy(
|
|
2748
|
+
source: string,
|
|
2749
|
+
policy: string,
|
|
2750
|
+
failureMessage: string,
|
|
2751
|
+
): string {
|
|
2752
|
+
const policyMatch = /\bpolicies\s*:\s*\[/.exec(source);
|
|
2753
|
+
if (!policyMatch) {
|
|
2754
|
+
throw new Error(failureMessage);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
const arrayStart = source.indexOf("[", policyMatch.index);
|
|
2758
|
+
if (arrayStart === -1) {
|
|
2759
|
+
throw new Error(failureMessage);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
2763
|
+
if (arrayEnd === -1) {
|
|
2764
|
+
throw new Error(failureMessage);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
const policies = source.slice(arrayStart, arrayEnd + 1);
|
|
2768
|
+
if (policies.includes(policy)) return source;
|
|
2769
|
+
|
|
2770
|
+
const nextPolicies = appendToArrayExpression(policies, [policy]);
|
|
2771
|
+
return `${source.slice(0, arrayStart)}${nextPolicies}${source.slice(
|
|
2772
|
+
arrayEnd + 1,
|
|
2773
|
+
)}`;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2187
2776
|
function insertDefinePortsProperty(
|
|
2188
2777
|
source: string,
|
|
2189
2778
|
property: string,
|
|
@@ -2281,7 +2870,20 @@ function definePortsBodySource(source: string): string | undefined {
|
|
|
2281
2870
|
function definePortsBodyRange(
|
|
2282
2871
|
source: string,
|
|
2283
2872
|
): { openBrace: number; closeBrace: number } | undefined {
|
|
2284
|
-
const
|
|
2873
|
+
const outer = definePortsOuterBodyRange(source);
|
|
2874
|
+
if (!outer) return undefined;
|
|
2875
|
+
|
|
2876
|
+
// The curried `definePorts<AppPorts>()({ bound, deferred })` form binds app
|
|
2877
|
+
// ports inside `bound`; generated bindings belong there.
|
|
2878
|
+
return (
|
|
2879
|
+
topLevelBoundObjectRange(source, outer.openBrace, outer.closeBrace) ?? outer
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function definePortsOuterBodyRange(
|
|
2884
|
+
source: string,
|
|
2885
|
+
): { openBrace: number; closeBrace: number } | undefined {
|
|
2886
|
+
const match = /definePorts(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?\{/.exec(source);
|
|
2285
2887
|
if (!match) return undefined;
|
|
2286
2888
|
|
|
2287
2889
|
const openBrace = source.indexOf("{", match.index);
|
|
@@ -2291,6 +2893,50 @@ function definePortsBodyRange(
|
|
|
2291
2893
|
return { openBrace, closeBrace };
|
|
2292
2894
|
}
|
|
2293
2895
|
|
|
2896
|
+
function appendDeferredPortKey(source: string, portKey: string): string {
|
|
2897
|
+
const outer = definePortsOuterBodyRange(source);
|
|
2898
|
+
if (!outer) return source;
|
|
2899
|
+
|
|
2900
|
+
const body = source.slice(outer.openBrace + 1, outer.closeBrace);
|
|
2901
|
+
if (!hasTopLevelObjectProperty(body, "deferred")) return source;
|
|
2902
|
+
|
|
2903
|
+
const deferredMatch = /\bdeferred\s*:\s*\[/.exec(body);
|
|
2904
|
+
if (!deferredMatch) return source;
|
|
2905
|
+
|
|
2906
|
+
const arrayStart = source.indexOf(
|
|
2907
|
+
"[",
|
|
2908
|
+
outer.openBrace + 1 + deferredMatch.index,
|
|
2909
|
+
);
|
|
2910
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
2911
|
+
if (arrayEnd === -1) return source;
|
|
2912
|
+
|
|
2913
|
+
const tuple = source.slice(arrayStart, arrayEnd + 1);
|
|
2914
|
+
if (new RegExp(`["']${portKey}["']`).test(tuple)) return source;
|
|
2915
|
+
|
|
2916
|
+
const nextTuple = appendToArrayExpression(tuple, [`"${portKey}"`]);
|
|
2917
|
+
return `${source.slice(0, arrayStart)}${nextTuple}${source.slice(
|
|
2918
|
+
arrayEnd + 1,
|
|
2919
|
+
)}`;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
function topLevelBoundObjectRange(
|
|
2923
|
+
source: string,
|
|
2924
|
+
openBrace: number,
|
|
2925
|
+
closeBrace: number,
|
|
2926
|
+
): { openBrace: number; closeBrace: number } | undefined {
|
|
2927
|
+
const body = source.slice(openBrace + 1, closeBrace);
|
|
2928
|
+
if (!hasTopLevelObjectProperty(body, "bound")) return undefined;
|
|
2929
|
+
|
|
2930
|
+
const boundMatch = /\bbound\s*:\s*\{/.exec(body);
|
|
2931
|
+
if (!boundMatch) return undefined;
|
|
2932
|
+
|
|
2933
|
+
const boundOpen = source.indexOf("{", openBrace + 1 + boundMatch.index);
|
|
2934
|
+
const boundClose = matchingBraceIndex(source, boundOpen);
|
|
2935
|
+
if (boundClose === -1) return undefined;
|
|
2936
|
+
|
|
2937
|
+
return { openBrace: boundOpen, closeBrace: boundClose };
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2294
2940
|
function hasTopLevelObjectProperty(
|
|
2295
2941
|
source: string,
|
|
2296
2942
|
propertyName: string,
|
|
@@ -2593,6 +3239,21 @@ function resourceNames(input: string): ResourceNames {
|
|
|
2593
3239
|
return names;
|
|
2594
3240
|
}
|
|
2595
3241
|
|
|
3242
|
+
function featureUiNames(input: string): FeatureUiNames {
|
|
3243
|
+
const names = resourceNames(input);
|
|
3244
|
+
const resolvedNames = {
|
|
3245
|
+
...names,
|
|
3246
|
+
componentExportName: `${names.pluralPascal}Panel`,
|
|
3247
|
+
componentFileName: `${names.pluralKebab}-panel`,
|
|
3248
|
+
};
|
|
3249
|
+
|
|
3250
|
+
assertGeneratedIdentifiers(input, "Feature UI name", [
|
|
3251
|
+
resolvedNames.componentExportName,
|
|
3252
|
+
]);
|
|
3253
|
+
|
|
3254
|
+
return resolvedNames;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
2596
3257
|
function featureResourceNames(names: FeatureArtifactNames): ResourceNames {
|
|
2597
3258
|
return resourceNames(names.feature.kebab);
|
|
2598
3259
|
}
|
|
@@ -2737,6 +3398,21 @@ function jobNames(input: string): JobNames {
|
|
|
2737
3398
|
};
|
|
2738
3399
|
}
|
|
2739
3400
|
|
|
3401
|
+
function taskNames(input: string): TaskNames {
|
|
3402
|
+
const names = featureArtifactNames(input, "Task");
|
|
3403
|
+
const taskExportName = `${names.artifact.camel}Task`;
|
|
3404
|
+
|
|
3405
|
+
assertGeneratedIdentifiers(input, "Task name", [taskExportName]);
|
|
3406
|
+
|
|
3407
|
+
return {
|
|
3408
|
+
...names,
|
|
3409
|
+
taskName: names.stableName,
|
|
3410
|
+
taskExportName,
|
|
3411
|
+
inputSchemaName: `${names.artifact.pascal}TaskInputSchema`,
|
|
3412
|
+
inputTypeName: `${names.artifact.pascal}TaskInput`,
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
|
|
2740
3416
|
function factoryNames(input: string): FactoryNames {
|
|
2741
3417
|
const names = featureArtifactNames(input, "Factory");
|
|
2742
3418
|
const factoryExportName = `${names.artifact.camel}Factory`;
|
|
@@ -2986,10 +3662,21 @@ function pascalCase(value: string): string {
|
|
|
2986
3662
|
return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
2987
3663
|
}
|
|
2988
3664
|
|
|
3665
|
+
function constantCase(value: string): string {
|
|
3666
|
+
return value.replaceAll("-", "_").toUpperCase();
|
|
3667
|
+
}
|
|
3668
|
+
|
|
2989
3669
|
function resourceFiles(
|
|
2990
3670
|
names: ResourceNames,
|
|
2991
3671
|
config: ResolvedBeignetConfig,
|
|
2992
3672
|
persistence: ResourcePersistence = "memory",
|
|
3673
|
+
mode: ResourceGenerationMode = "feature",
|
|
3674
|
+
options: ResourceGenerationOptions = {
|
|
3675
|
+
auth: false,
|
|
3676
|
+
tenant: false,
|
|
3677
|
+
events: false,
|
|
3678
|
+
softDelete: false,
|
|
3679
|
+
},
|
|
2993
3680
|
): GeneratedFile[] {
|
|
2994
3681
|
const useCaseDir = resourceUseCaseDir(names, config);
|
|
2995
3682
|
const infraDir = infrastructureDir(config);
|
|
@@ -2997,27 +3684,63 @@ function resourceFiles(
|
|
|
2997
3684
|
const files: GeneratedFile[] = [
|
|
2998
3685
|
{
|
|
2999
3686
|
path: resourceContractFilePath(names, config),
|
|
3000
|
-
content: contractFile(names, config),
|
|
3687
|
+
content: contractFile(names, config, mode, options),
|
|
3001
3688
|
},
|
|
3002
3689
|
{
|
|
3003
|
-
path: path.join(
|
|
3004
|
-
content: schemasFile(names),
|
|
3690
|
+
path: path.join(featureDir, "schemas.ts"),
|
|
3691
|
+
content: schemasFile(names, mode, options),
|
|
3005
3692
|
},
|
|
3006
3693
|
{
|
|
3007
3694
|
path: path.join(useCaseDir, `list-${names.pluralKebab}.ts`),
|
|
3008
|
-
content: listUseCaseFile(names, config),
|
|
3695
|
+
content: listUseCaseFile(names, config, options),
|
|
3009
3696
|
},
|
|
3010
3697
|
{
|
|
3011
3698
|
path: path.join(useCaseDir, `create-${names.singularKebab}.ts`),
|
|
3012
|
-
content: createUseCaseFile(names, config),
|
|
3699
|
+
content: createUseCaseFile(names, config, options),
|
|
3013
3700
|
},
|
|
3701
|
+
...(mode === "resource"
|
|
3702
|
+
? [
|
|
3703
|
+
{
|
|
3704
|
+
path: path.join(useCaseDir, `get-${names.singularKebab}.ts`),
|
|
3705
|
+
content: getUseCaseFile(names, config, options),
|
|
3706
|
+
},
|
|
3707
|
+
{
|
|
3708
|
+
path: path.join(useCaseDir, `update-${names.singularKebab}.ts`),
|
|
3709
|
+
content: updateUseCaseFile(names, config, options),
|
|
3710
|
+
},
|
|
3711
|
+
{
|
|
3712
|
+
path: path.join(useCaseDir, `delete-${names.singularKebab}.ts`),
|
|
3713
|
+
content: deleteUseCaseFile(names, config, options),
|
|
3714
|
+
},
|
|
3715
|
+
{
|
|
3716
|
+
path: path.join(featureDir, "policy.ts"),
|
|
3717
|
+
content: resourcePolicyFile(names, config, options),
|
|
3718
|
+
},
|
|
3719
|
+
]
|
|
3720
|
+
: []),
|
|
3721
|
+
...(mode === "resource" && options.events
|
|
3722
|
+
? [
|
|
3723
|
+
{
|
|
3724
|
+
path: path.join(featureDir, "domain/events/index.ts"),
|
|
3725
|
+
content: resourceEventsFile(names),
|
|
3726
|
+
},
|
|
3727
|
+
]
|
|
3728
|
+
: []),
|
|
3729
|
+
...(mode === "resource" && options.auth
|
|
3730
|
+
? [
|
|
3731
|
+
{
|
|
3732
|
+
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
3733
|
+
content: resourcePolicyTestFile(names, options),
|
|
3734
|
+
},
|
|
3735
|
+
]
|
|
3736
|
+
: []),
|
|
3014
3737
|
{
|
|
3015
3738
|
path: path.join(useCaseDir, "index.ts"),
|
|
3016
|
-
content: useCasesIndexFile(names),
|
|
3739
|
+
content: useCasesIndexFile(names, mode),
|
|
3017
3740
|
},
|
|
3018
3741
|
{
|
|
3019
3742
|
path: resourcePortFilePath(names, config),
|
|
3020
|
-
content: repositoryPortFile(names, config),
|
|
3743
|
+
content: repositoryPortFile(names, config, mode, options),
|
|
3021
3744
|
},
|
|
3022
3745
|
{
|
|
3023
3746
|
path: path.join(
|
|
@@ -3025,15 +3748,15 @@ function resourceFiles(
|
|
|
3025
3748
|
names.pluralKebab,
|
|
3026
3749
|
`in-memory-${names.singularKebab}-repository.ts`,
|
|
3027
3750
|
),
|
|
3028
|
-
content: inMemoryRepositoryFile(names, config),
|
|
3751
|
+
content: inMemoryRepositoryFile(names, config, mode, options),
|
|
3029
3752
|
},
|
|
3030
3753
|
{
|
|
3031
3754
|
path: path.join(featureDir, "routes.ts"),
|
|
3032
|
-
content: routeGroupFile(names, config),
|
|
3755
|
+
content: routeGroupFile(names, config, mode, options),
|
|
3033
3756
|
},
|
|
3034
3757
|
{
|
|
3035
3758
|
path: resourceTestFilePath(names, config),
|
|
3036
|
-
content: testFile(names, config),
|
|
3759
|
+
content: testFile(names, config, mode, options),
|
|
3037
3760
|
},
|
|
3038
3761
|
];
|
|
3039
3762
|
|
|
@@ -3041,11 +3764,11 @@ function resourceFiles(
|
|
|
3041
3764
|
files.push(
|
|
3042
3765
|
{
|
|
3043
3766
|
path: drizzleResourceSchemaFilePath(names, config),
|
|
3044
|
-
content: drizzleSchemaFile(names),
|
|
3767
|
+
content: drizzleSchemaFile(names, options),
|
|
3045
3768
|
},
|
|
3046
3769
|
{
|
|
3047
3770
|
path: drizzleResourceRepositoryFilePath(names, config),
|
|
3048
|
-
content: drizzleRepositoryFile(names, config),
|
|
3771
|
+
content: drizzleRepositoryFile(names, config, mode, options),
|
|
3049
3772
|
},
|
|
3050
3773
|
);
|
|
3051
3774
|
}
|
|
@@ -3053,6 +3776,31 @@ function resourceFiles(
|
|
|
3053
3776
|
return files;
|
|
3054
3777
|
}
|
|
3055
3778
|
|
|
3779
|
+
function featureUiComponentFiles(
|
|
3780
|
+
names: FeatureUiNames,
|
|
3781
|
+
config: ResolvedBeignetConfig,
|
|
3782
|
+
): GeneratedFile[] {
|
|
3783
|
+
const componentPath = featureUiComponentFilePath(names, config);
|
|
3784
|
+
|
|
3785
|
+
return [
|
|
3786
|
+
{
|
|
3787
|
+
path: componentPath,
|
|
3788
|
+
content: featureUiComponentFile(names, config),
|
|
3789
|
+
},
|
|
3790
|
+
];
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
function featureUiIndexFile(
|
|
3794
|
+
names: FeatureUiNames,
|
|
3795
|
+
config: ResolvedBeignetConfig,
|
|
3796
|
+
): GeneratedFile {
|
|
3797
|
+
return {
|
|
3798
|
+
path: featureUiIndexFilePath(names, config),
|
|
3799
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";
|
|
3800
|
+
`,
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3056
3804
|
function contractFiles(
|
|
3057
3805
|
names: ResourceNames,
|
|
3058
3806
|
config: ResolvedBeignetConfig,
|
|
@@ -3151,6 +3899,18 @@ function jobFiles(
|
|
|
3151
3899
|
];
|
|
3152
3900
|
}
|
|
3153
3901
|
|
|
3902
|
+
function taskFiles(
|
|
3903
|
+
names: TaskNames,
|
|
3904
|
+
config: ResolvedBeignetConfig,
|
|
3905
|
+
): GeneratedFile[] {
|
|
3906
|
+
return [
|
|
3907
|
+
{
|
|
3908
|
+
path: taskFilePath(names, config),
|
|
3909
|
+
content: taskFile(names, config),
|
|
3910
|
+
},
|
|
3911
|
+
];
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3154
3914
|
function notificationFiles(
|
|
3155
3915
|
names: NotificationNames,
|
|
3156
3916
|
config: ResolvedBeignetConfig,
|
|
@@ -3283,6 +4043,30 @@ function drizzleResourceRepositoryFilePath(
|
|
|
3283
4043
|
);
|
|
3284
4044
|
}
|
|
3285
4045
|
|
|
4046
|
+
function resourceSharedErrorsPath(config: ResolvedBeignetConfig): string {
|
|
4047
|
+
return path.join(config.paths.features, "shared", "errors.ts");
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
function resourceContractNames(
|
|
4051
|
+
names: ResourceNames,
|
|
4052
|
+
mode: ResourceGenerationMode,
|
|
4053
|
+
): string[] {
|
|
4054
|
+
const contractNames = [
|
|
4055
|
+
`list${names.pluralPascal}`,
|
|
4056
|
+
`create${names.singularPascal}`,
|
|
4057
|
+
];
|
|
4058
|
+
|
|
4059
|
+
if (mode === "resource") {
|
|
4060
|
+
contractNames.push(
|
|
4061
|
+
`get${names.singularPascal}`,
|
|
4062
|
+
`update${names.singularPascal}`,
|
|
4063
|
+
`delete${names.singularPascal}`,
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
return contractNames;
|
|
4068
|
+
}
|
|
4069
|
+
|
|
3286
4070
|
async function detectResourcePersistence(
|
|
3287
4071
|
targetDir: string,
|
|
3288
4072
|
config: ResolvedBeignetConfig,
|
|
@@ -3301,6 +4085,36 @@ function resourceFeatureDir(
|
|
|
3301
4085
|
return path.join(config.paths.features, names.pluralKebab);
|
|
3302
4086
|
}
|
|
3303
4087
|
|
|
4088
|
+
function featureUiDir(
|
|
4089
|
+
names: FeatureUiNames,
|
|
4090
|
+
config: ResolvedBeignetConfig,
|
|
4091
|
+
): string {
|
|
4092
|
+
return path.join(resourceFeatureDir(names, config), "components");
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
function featureUiComponentFilePath(
|
|
4096
|
+
names: FeatureUiNames,
|
|
4097
|
+
config: ResolvedBeignetConfig,
|
|
4098
|
+
): string {
|
|
4099
|
+
return path.join(
|
|
4100
|
+
featureUiDir(names, config),
|
|
4101
|
+
`${names.componentFileName}.tsx`,
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
function featureUiIndexFilePath(
|
|
4106
|
+
names: FeatureUiNames,
|
|
4107
|
+
config: ResolvedBeignetConfig,
|
|
4108
|
+
): string {
|
|
4109
|
+
return path.join(featureUiDir(names, config), "index.ts");
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
function clientIndexPath(config: ResolvedBeignetConfig): string {
|
|
4113
|
+
const appRoot = path.posix.dirname(config.paths.appContext);
|
|
4114
|
+
|
|
4115
|
+
return path.join(appRoot === "." ? "" : appRoot, "client/index.ts");
|
|
4116
|
+
}
|
|
4117
|
+
|
|
3304
4118
|
function usesFeatureOwnedContracts(config: ResolvedBeignetConfig): boolean {
|
|
3305
4119
|
return (
|
|
3306
4120
|
directoryPath(config.paths.contracts) ===
|
|
@@ -3365,6 +4179,13 @@ function resourceUseCaseIndexPath(
|
|
|
3365
4179
|
return path.join(resourceUseCaseDir(names, config), "index.ts");
|
|
3366
4180
|
}
|
|
3367
4181
|
|
|
4182
|
+
function resourceSchemaFilePath(
|
|
4183
|
+
names: ResourceNames,
|
|
4184
|
+
config: ResolvedBeignetConfig,
|
|
4185
|
+
): string {
|
|
4186
|
+
return path.join(resourceFeatureDir(names, config), "schemas.ts");
|
|
4187
|
+
}
|
|
4188
|
+
|
|
3368
4189
|
function resourcePortFilePath(
|
|
3369
4190
|
names: ResourceNames,
|
|
3370
4191
|
config: ResolvedBeignetConfig,
|
|
@@ -3430,6 +4251,7 @@ function featureArtifactDir(
|
|
|
3430
4251
|
kind:
|
|
3431
4252
|
| "events"
|
|
3432
4253
|
| "factories"
|
|
4254
|
+
| "tasks"
|
|
3433
4255
|
| "jobs"
|
|
3434
4256
|
| "notifications"
|
|
3435
4257
|
| "listeners"
|
|
@@ -3467,6 +4289,13 @@ function jobFilePath(names: JobNames, config: ResolvedBeignetConfig): string {
|
|
|
3467
4289
|
);
|
|
3468
4290
|
}
|
|
3469
4291
|
|
|
4292
|
+
function taskFilePath(names: TaskNames, config: ResolvedBeignetConfig): string {
|
|
4293
|
+
return path.join(
|
|
4294
|
+
featureArtifactDir(names, "tasks", config),
|
|
4295
|
+
`${names.artifact.kebab}.ts`,
|
|
4296
|
+
);
|
|
4297
|
+
}
|
|
4298
|
+
|
|
3470
4299
|
function factoryFilePath(
|
|
3471
4300
|
names: FactoryNames,
|
|
3472
4301
|
config: ResolvedBeignetConfig,
|
|
@@ -3585,11 +4414,36 @@ async function updateUseCaseIndex(
|
|
|
3585
4414
|
return "updated";
|
|
3586
4415
|
}
|
|
3587
4416
|
|
|
4417
|
+
async function updateFeatureUiIndex(
|
|
4418
|
+
targetDir: string,
|
|
4419
|
+
file: GeneratedFile,
|
|
4420
|
+
options: { dryRun: boolean },
|
|
4421
|
+
): Promise<WriteGeneratedFileResult> {
|
|
4422
|
+
const destination = path.join(targetDir, file.path);
|
|
4423
|
+
const existing = await readOptionalFile(destination);
|
|
4424
|
+
if (existing === undefined) {
|
|
4425
|
+
if (!options.dryRun) {
|
|
4426
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
4427
|
+
await writeFile(destination, file.content);
|
|
4428
|
+
}
|
|
4429
|
+
return "created";
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
if (existing.includes(file.content.trim())) {
|
|
4433
|
+
return "skipped";
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
const next = `${existing.trimEnd()}\n${file.content}`;
|
|
4437
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
4438
|
+
return "updated";
|
|
4439
|
+
}
|
|
4440
|
+
|
|
3588
4441
|
type FeatureArtifactIndexFile = {
|
|
3589
4442
|
path: string;
|
|
3590
4443
|
kind:
|
|
3591
4444
|
| "event"
|
|
3592
4445
|
| "job"
|
|
4446
|
+
| "task"
|
|
3593
4447
|
| "factory"
|
|
3594
4448
|
| "seed"
|
|
3595
4449
|
| "notification"
|
|
@@ -3606,6 +4460,7 @@ function featureArtifactIndexFile(
|
|
|
3606
4460
|
kind:
|
|
3607
4461
|
| "event"
|
|
3608
4462
|
| "job"
|
|
4463
|
+
| "task"
|
|
3609
4464
|
| "factory"
|
|
3610
4465
|
| "seed"
|
|
3611
4466
|
| "notification"
|
|
@@ -3615,6 +4470,7 @@ function featureArtifactIndexFile(
|
|
|
3615
4470
|
names:
|
|
3616
4471
|
| EventNames
|
|
3617
4472
|
| JobNames
|
|
4473
|
+
| TaskNames
|
|
3618
4474
|
| FactoryNames
|
|
3619
4475
|
| SeedNames
|
|
3620
4476
|
| NotificationNames
|
|
@@ -3628,49 +4484,55 @@ function featureArtifactIndexFile(
|
|
|
3628
4484
|
? eventFilePath(names as EventNames, config)
|
|
3629
4485
|
: kind === "job"
|
|
3630
4486
|
? jobFilePath(names as JobNames, config)
|
|
3631
|
-
: kind === "
|
|
3632
|
-
?
|
|
3633
|
-
: kind === "
|
|
3634
|
-
?
|
|
3635
|
-
: kind === "
|
|
3636
|
-
?
|
|
3637
|
-
: kind === "
|
|
3638
|
-
?
|
|
3639
|
-
: kind === "
|
|
3640
|
-
?
|
|
3641
|
-
:
|
|
4487
|
+
: kind === "task"
|
|
4488
|
+
? taskFilePath(names as TaskNames, config)
|
|
4489
|
+
: kind === "factory"
|
|
4490
|
+
? factoryFilePath(names as FactoryNames, config)
|
|
4491
|
+
: kind === "seed"
|
|
4492
|
+
? seedFilePath(names as SeedNames, config)
|
|
4493
|
+
: kind === "notification"
|
|
4494
|
+
? notificationFilePath(names as NotificationNames, config)
|
|
4495
|
+
: kind === "listener"
|
|
4496
|
+
? listenerFilePath(names as ListenerNames, config)
|
|
4497
|
+
: kind === "schedule"
|
|
4498
|
+
? scheduleFilePath(names as ScheduleNames, config)
|
|
4499
|
+
: uploadFilePath(names as UploadNames, config);
|
|
3642
4500
|
const registrySuffix =
|
|
3643
4501
|
kind === "event"
|
|
3644
4502
|
? "Events"
|
|
3645
4503
|
: kind === "job"
|
|
3646
4504
|
? "Jobs"
|
|
3647
|
-
: kind === "
|
|
3648
|
-
? "
|
|
3649
|
-
: kind === "
|
|
3650
|
-
? "
|
|
3651
|
-
: kind === "
|
|
3652
|
-
? "
|
|
3653
|
-
: kind === "
|
|
3654
|
-
? "
|
|
3655
|
-
: kind === "
|
|
3656
|
-
? "
|
|
3657
|
-
: "
|
|
4505
|
+
: kind === "task"
|
|
4506
|
+
? "Tasks"
|
|
4507
|
+
: kind === "factory"
|
|
4508
|
+
? "Factories"
|
|
4509
|
+
: kind === "seed"
|
|
4510
|
+
? "Seeds"
|
|
4511
|
+
: kind === "notification"
|
|
4512
|
+
? "Notifications"
|
|
4513
|
+
: kind === "listener"
|
|
4514
|
+
? "Listeners"
|
|
4515
|
+
: kind === "schedule"
|
|
4516
|
+
? "Schedules"
|
|
4517
|
+
: "Uploads";
|
|
3658
4518
|
const importName =
|
|
3659
4519
|
kind === "event"
|
|
3660
4520
|
? (names as EventNames).eventExportName
|
|
3661
4521
|
: kind === "job"
|
|
3662
4522
|
? (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
|
-
:
|
|
4523
|
+
: kind === "task"
|
|
4524
|
+
? (names as TaskNames).taskExportName
|
|
4525
|
+
: kind === "factory"
|
|
4526
|
+
? (names as FactoryNames).factoryExportName
|
|
4527
|
+
: kind === "seed"
|
|
4528
|
+
? (names as SeedNames).seedExportName
|
|
4529
|
+
: kind === "notification"
|
|
4530
|
+
? (names as NotificationNames).notificationExportName
|
|
4531
|
+
: kind === "listener"
|
|
4532
|
+
? (names as ListenerNames).listenerExportName
|
|
4533
|
+
: kind === "schedule"
|
|
4534
|
+
? (names as ScheduleNames).scheduleExportName
|
|
4535
|
+
: (names as UploadNames).uploadExportName;
|
|
3674
4536
|
|
|
3675
4537
|
return {
|
|
3676
4538
|
path: path.join(path.dirname(artifactPath), "index.ts"),
|
|
@@ -3786,15 +4648,92 @@ export const ${file.registryName} = [${file.importName}] as const;
|
|
|
3786
4648
|
return "updated";
|
|
3787
4649
|
}
|
|
3788
4650
|
|
|
3789
|
-
function
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
const
|
|
3797
|
-
|
|
4651
|
+
async function updateTaskRegistry(
|
|
4652
|
+
targetDir: string,
|
|
4653
|
+
names: TaskNames,
|
|
4654
|
+
config: ResolvedBeignetConfig,
|
|
4655
|
+
options: { dryRun: boolean },
|
|
4656
|
+
): Promise<WriteGeneratedFileResult> {
|
|
4657
|
+
const destination = path.join(targetDir, config.paths.tasks);
|
|
4658
|
+
const existing = await readOptionalFile(destination);
|
|
4659
|
+
const featureTasksPath = path.join(
|
|
4660
|
+
featureArtifactDir(names, "tasks", config),
|
|
4661
|
+
"index.ts",
|
|
4662
|
+
);
|
|
4663
|
+
const registryName = `${names.featureSingularCamel}Tasks`;
|
|
4664
|
+
const importLine = `import { ${registryName} } from "${relativeModule(
|
|
4665
|
+
config.paths.tasks,
|
|
4666
|
+
featureTasksPath,
|
|
4667
|
+
)}";`;
|
|
4668
|
+
const defineTasksImport = `import { defineTasks } from "@beignet/core/tasks";`;
|
|
4669
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
4670
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
4671
|
+
const serverImport = `import { server } from "${relativeModule(
|
|
4672
|
+
config.paths.tasks,
|
|
4673
|
+
config.paths.server,
|
|
4674
|
+
)}";`;
|
|
4675
|
+
const registryEntry = `...${registryName}`;
|
|
4676
|
+
|
|
4677
|
+
if (existing === undefined) {
|
|
4678
|
+
const content = `${defineTasksImport}
|
|
4679
|
+
${serviceActorImport}
|
|
4680
|
+
${appContextImport}
|
|
4681
|
+
${importLine}
|
|
4682
|
+
${serverImport}
|
|
4683
|
+
|
|
4684
|
+
export const tasks = defineTasks([
|
|
4685
|
+
\t${registryEntry},
|
|
4686
|
+
] as const);
|
|
4687
|
+
|
|
4688
|
+
export async function createTaskContext(): Promise<AppContext> {
|
|
4689
|
+
\treturn server.createServiceContext({
|
|
4690
|
+
\t\tactor: createServiceActor("beignet-cli"),
|
|
4691
|
+
\t});
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
export async function stopTaskContext(): Promise<void> {
|
|
4695
|
+
\tawait server.stop();
|
|
4696
|
+
}
|
|
4697
|
+
`;
|
|
4698
|
+
if (!options.dryRun) {
|
|
4699
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
4700
|
+
await writeFile(destination, content);
|
|
4701
|
+
}
|
|
4702
|
+
return "created";
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
let next = existing;
|
|
4706
|
+
if (!next.includes(defineTasksImport)) {
|
|
4707
|
+
next = insertAfterImports(next, defineTasksImport);
|
|
4708
|
+
}
|
|
4709
|
+
if (!next.includes(importLine)) {
|
|
4710
|
+
next = insertAfterImports(next, importLine);
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
const registry = arrayInitializerInfo(next, "tasks");
|
|
4714
|
+
if (!registry) {
|
|
4715
|
+
next = `${next.trimEnd()}\n\nexport const tasks = defineTasks([\n\t${registryEntry},\n] as const);\n`;
|
|
4716
|
+
} else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
4717
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
4718
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(
|
|
4719
|
+
registry.end,
|
|
4720
|
+
)}`;
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
if (next === existing) return "skipped";
|
|
4724
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
4725
|
+
return "updated";
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
function arrayInitializerInfo(
|
|
4729
|
+
source: string,
|
|
4730
|
+
constName: string,
|
|
4731
|
+
): { text: string; start: number; end: number } | undefined {
|
|
4732
|
+
const match = new RegExp(`\\bconst\\s+${constName}\\s*=`).exec(source);
|
|
4733
|
+
if (!match) return undefined;
|
|
4734
|
+
|
|
4735
|
+
const openBracket = source.indexOf("[", match.index);
|
|
4736
|
+
if (openBracket === -1) return undefined;
|
|
3798
4737
|
|
|
3799
4738
|
const closeBracket = matchingDelimiterIndex(source, openBracket, "[", "]");
|
|
3800
4739
|
if (closeBracket === -1) return undefined;
|
|
@@ -3828,27 +4767,110 @@ function defineUploadsInitializerInfo(
|
|
|
3828
4767
|
};
|
|
3829
4768
|
}
|
|
3830
4769
|
|
|
3831
|
-
function schemasFile(
|
|
4770
|
+
function schemasFile(
|
|
4771
|
+
names: ResourceNames,
|
|
4772
|
+
mode: ResourceGenerationMode,
|
|
4773
|
+
options: ResourceGenerationOptions,
|
|
4774
|
+
): string {
|
|
3832
4775
|
return `import { z } from "zod";
|
|
3833
4776
|
|
|
3834
4777
|
export const ${names.singularPascal}Schema = z.object({
|
|
3835
4778
|
id: z.string().uuid(),
|
|
3836
|
-
name: z.string().min(1),
|
|
4779
|
+
${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
4780
|
+
version: z.number().int().min(1),
|
|
3837
4781
|
createdAt: z.string().datetime(),
|
|
4782
|
+
updatedAt: z.string().datetime(),
|
|
3838
4783
|
});
|
|
3839
4784
|
|
|
3840
|
-
export const
|
|
3841
|
-
|
|
3842
|
-
|
|
4785
|
+
export const ${names.singularPascal}SortBySchema = z.enum(["createdAt", "name"]);
|
|
4786
|
+
export const ${names.singularPascal}SortDirectionSchema = z.enum(["asc", "desc"]);
|
|
4787
|
+
|
|
4788
|
+
const ${names.singularPascal}CursorPayloadSchema = z.object({
|
|
4789
|
+
sortBy: ${names.singularPascal}SortBySchema,
|
|
4790
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema,
|
|
4791
|
+
sortValue: z.string(),
|
|
4792
|
+
id: z.string().uuid(),
|
|
3843
4793
|
});
|
|
3844
4794
|
|
|
4795
|
+
export type ${names.singularPascal}SortBy = z.infer<
|
|
4796
|
+
typeof ${names.singularPascal}SortBySchema
|
|
4797
|
+
>;
|
|
4798
|
+
export type ${names.singularPascal}SortDirection = z.infer<
|
|
4799
|
+
typeof ${names.singularPascal}SortDirectionSchema
|
|
4800
|
+
>;
|
|
4801
|
+
export type ${names.singularPascal}Cursor = z.infer<
|
|
4802
|
+
typeof ${names.singularPascal}CursorPayloadSchema
|
|
4803
|
+
>;
|
|
4804
|
+
|
|
4805
|
+
function encodeBase64Url(value: string): string {
|
|
4806
|
+
const bytes = new TextEncoder().encode(value);
|
|
4807
|
+
let binary = "";
|
|
4808
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
4809
|
+
|
|
4810
|
+
return btoa(binary)
|
|
4811
|
+
.replaceAll("+", "-")
|
|
4812
|
+
.replaceAll("/", "_")
|
|
4813
|
+
.replaceAll("=", "");
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4816
|
+
function decodeBase64Url(value: string): string {
|
|
4817
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
4818
|
+
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
|
4819
|
+
const binary = atob(base64 + padding);
|
|
4820
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
4821
|
+
|
|
4822
|
+
return new TextDecoder().decode(bytes);
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
export function encode${names.singularPascal}Cursor(cursor: ${names.singularPascal}Cursor): string {
|
|
4826
|
+
return encodeBase64Url(JSON.stringify(cursor));
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
export function decode${names.singularPascal}Cursor(cursor: string): ${names.singularPascal}Cursor {
|
|
4830
|
+
return ${names.singularPascal}CursorPayloadSchema.parse(
|
|
4831
|
+
JSON.parse(decodeBase64Url(cursor)),
|
|
4832
|
+
);
|
|
4833
|
+
}
|
|
4834
|
+
|
|
4835
|
+
export const List${names.pluralPascal}InputSchema = z
|
|
4836
|
+
.object({
|
|
4837
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
4838
|
+
cursor: z.string().nullable().default(null),
|
|
4839
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
4840
|
+
sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
|
|
4841
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
|
|
4842
|
+
})
|
|
4843
|
+
.superRefine((input, ctx) => {
|
|
4844
|
+
if (!input.cursor) return;
|
|
4845
|
+
|
|
4846
|
+
try {
|
|
4847
|
+
const cursor = decode${names.singularPascal}Cursor(input.cursor);
|
|
4848
|
+
if (
|
|
4849
|
+
cursor.sortBy !== input.sortBy ||
|
|
4850
|
+
cursor.sortDirection !== input.sortDirection
|
|
4851
|
+
) {
|
|
4852
|
+
ctx.addIssue({
|
|
4853
|
+
code: z.ZodIssueCode.custom,
|
|
4854
|
+
message: "cursor does not match the requested sort.",
|
|
4855
|
+
path: ["cursor"],
|
|
4856
|
+
});
|
|
4857
|
+
}
|
|
4858
|
+
} catch {
|
|
4859
|
+
ctx.addIssue({
|
|
4860
|
+
code: z.ZodIssueCode.custom,
|
|
4861
|
+
message: "cursor is invalid.",
|
|
4862
|
+
path: ["cursor"],
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
});
|
|
4866
|
+
|
|
3845
4867
|
export const List${names.pluralPascal}OutputSchema = z.object({
|
|
3846
4868
|
items: z.array(${names.singularPascal}Schema),
|
|
3847
4869
|
page: z.object({
|
|
3848
|
-
kind: z.literal("
|
|
4870
|
+
kind: z.literal("cursor"),
|
|
3849
4871
|
limit: z.number().int().min(1),
|
|
3850
|
-
|
|
3851
|
-
|
|
4872
|
+
cursor: z.string().nullable(),
|
|
4873
|
+
nextCursor: z.string().nullable(),
|
|
3852
4874
|
hasMore: z.boolean(),
|
|
3853
4875
|
}),
|
|
3854
4876
|
});
|
|
@@ -3856,11 +4878,42 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
3856
4878
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
3857
4879
|
name: z.string().min(1).max(120),
|
|
3858
4880
|
});
|
|
4881
|
+
${
|
|
4882
|
+
mode === "resource"
|
|
4883
|
+
? `
|
|
4884
|
+
export const ${names.singularPascal}IdInputSchema = z.object({
|
|
4885
|
+
id: z.string().uuid(),
|
|
4886
|
+
});
|
|
4887
|
+
|
|
4888
|
+
export const Update${names.singularPascal}BodySchema = z.object({
|
|
4889
|
+
name: z.string().min(1).max(120),
|
|
4890
|
+
version: z.number().int().min(1),
|
|
4891
|
+
});
|
|
3859
4892
|
|
|
4893
|
+
export const Update${names.singularPascal}InputSchema =
|
|
4894
|
+
${names.singularPascal}IdInputSchema.merge(Update${names.singularPascal}BodySchema);
|
|
4895
|
+
`
|
|
4896
|
+
: ""
|
|
4897
|
+
}
|
|
3860
4898
|
export type ${names.singularPascal} = z.infer<typeof ${names.singularPascal}Schema>;
|
|
3861
4899
|
export type Create${names.singularPascal}Input = z.infer<
|
|
3862
4900
|
typeof Create${names.singularPascal}InputSchema
|
|
3863
4901
|
>;
|
|
4902
|
+
${
|
|
4903
|
+
mode === "resource"
|
|
4904
|
+
? `
|
|
4905
|
+
export type ${names.singularPascal}IdInput = z.infer<
|
|
4906
|
+
typeof ${names.singularPascal}IdInputSchema
|
|
4907
|
+
>;
|
|
4908
|
+
export type Update${names.singularPascal}Body = z.infer<
|
|
4909
|
+
typeof Update${names.singularPascal}BodySchema
|
|
4910
|
+
>;
|
|
4911
|
+
export type Update${names.singularPascal}Input = z.infer<
|
|
4912
|
+
typeof Update${names.singularPascal}InputSchema
|
|
4913
|
+
>;
|
|
4914
|
+
`
|
|
4915
|
+
: ""
|
|
4916
|
+
}
|
|
3864
4917
|
export type List${names.pluralPascal}Input = z.infer<
|
|
3865
4918
|
typeof List${names.pluralPascal}InputSchema
|
|
3866
4919
|
>;
|
|
@@ -3870,30 +4923,39 @@ export type List${names.pluralPascal}Input = z.infer<
|
|
|
3870
4923
|
function listUseCaseFile(
|
|
3871
4924
|
names: ResourceNames,
|
|
3872
4925
|
config: ResolvedBeignetConfig,
|
|
4926
|
+
options: ResourceGenerationOptions,
|
|
3873
4927
|
): string {
|
|
3874
4928
|
const filePath = path.join(
|
|
3875
4929
|
resourceUseCaseDir(names, config),
|
|
3876
4930
|
`list-${names.pluralKebab}.ts`,
|
|
3877
4931
|
);
|
|
3878
4932
|
|
|
3879
|
-
return `import {
|
|
4933
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
4934
|
+
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
3880
4935
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3881
4936
|
import {
|
|
4937
|
+
decode${names.singularPascal}Cursor,
|
|
3882
4938
|
List${names.pluralPascal}InputSchema,
|
|
3883
4939
|
List${names.pluralPascal}OutputSchema,
|
|
3884
|
-
} from "
|
|
4940
|
+
} from "../schemas";
|
|
3885
4941
|
|
|
3886
4942
|
export const list${names.pluralPascal}UseCase = useCase
|
|
3887
4943
|
.query("${names.pluralCamel}.list")
|
|
3888
4944
|
.input(List${names.pluralPascal}InputSchema)
|
|
3889
4945
|
.output(List${names.pluralPascal}OutputSchema)
|
|
3890
4946
|
.run(async ({ ctx, input }) => {
|
|
3891
|
-
const page =
|
|
4947
|
+
${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
4948
|
defaultLimit: 20,
|
|
3893
4949
|
maxLimit: 100,
|
|
3894
4950
|
});
|
|
3895
4951
|
|
|
3896
|
-
return ctx.ports.${names.pluralCamel}.list(
|
|
4952
|
+
return ctx.ports.${names.pluralCamel}.list({
|
|
4953
|
+
page,
|
|
4954
|
+
cursor: page.cursor ? decode${names.singularPascal}Cursor(page.cursor) : null,
|
|
4955
|
+
name: input.name,
|
|
4956
|
+
sortBy: input.sortBy,
|
|
4957
|
+
sortDirection: input.sortDirection,
|
|
4958
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3897
4959
|
});
|
|
3898
4960
|
`;
|
|
3899
4961
|
}
|
|
@@ -3901,134 +4963,430 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
3901
4963
|
function createUseCaseFile(
|
|
3902
4964
|
names: ResourceNames,
|
|
3903
4965
|
config: ResolvedBeignetConfig,
|
|
4966
|
+
options: ResourceGenerationOptions,
|
|
3904
4967
|
): string {
|
|
3905
4968
|
const filePath = path.join(
|
|
3906
4969
|
resourceUseCaseDir(names, config),
|
|
3907
4970
|
`create-${names.singularKebab}.ts`,
|
|
3908
4971
|
);
|
|
3909
4972
|
|
|
3910
|
-
return `import {
|
|
4973
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
4974
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3911
4975
|
import {
|
|
3912
4976
|
Create${names.singularPascal}InputSchema,
|
|
3913
4977
|
${names.singularPascal}Schema,
|
|
3914
|
-
} from "
|
|
4978
|
+
} from "../schemas";
|
|
4979
|
+
${options.events ? `import { ${names.singularPascal}Created } from "../domain/events";\n` : ""}
|
|
3915
4980
|
|
|
3916
4981
|
export const create${names.singularPascal}UseCase = useCase
|
|
3917
4982
|
.command("${names.pluralCamel}.create")
|
|
3918
4983
|
.input(Create${names.singularPascal}InputSchema)
|
|
3919
4984
|
.output(${names.singularPascal}Schema)
|
|
3920
|
-
.run(async ({ ctx, input }) =>
|
|
4985
|
+
.run(async ({ ctx, input }) => {
|
|
4986
|
+
${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({
|
|
4987
|
+
...input,
|
|
4988
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
4989
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
4990
|
+
return ${names.singularCamel};
|
|
4991
|
+
});
|
|
4992
|
+
`;
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
function getUseCaseFile(
|
|
4996
|
+
names: ResourceNames,
|
|
4997
|
+
config: ResolvedBeignetConfig,
|
|
4998
|
+
options: ResourceGenerationOptions,
|
|
4999
|
+
): string {
|
|
5000
|
+
const filePath = path.join(
|
|
5001
|
+
resourceUseCaseDir(names, config),
|
|
5002
|
+
`get-${names.singularKebab}.ts`,
|
|
5003
|
+
);
|
|
5004
|
+
|
|
5005
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5006
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5007
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5008
|
+
import {
|
|
5009
|
+
${names.singularPascal}IdInputSchema,
|
|
5010
|
+
${names.singularPascal}Schema,
|
|
5011
|
+
} from "../schemas";
|
|
5012
|
+
|
|
5013
|
+
export const get${names.singularPascal}UseCase = useCase
|
|
5014
|
+
.query("${names.pluralCamel}.get")
|
|
5015
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
5016
|
+
.output(${names.singularPascal}Schema)
|
|
5017
|
+
.run(async ({ ctx, input }) => {
|
|
5018
|
+
${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 }" : ""});
|
|
5019
|
+
if (!${names.singularCamel}) {
|
|
5020
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5021
|
+
details: { id: input.id },
|
|
5022
|
+
});
|
|
5023
|
+
}
|
|
5024
|
+
|
|
5025
|
+
return ${names.singularCamel};
|
|
5026
|
+
});
|
|
3921
5027
|
`;
|
|
3922
5028
|
}
|
|
3923
5029
|
|
|
3924
|
-
function
|
|
5030
|
+
function updateUseCaseFile(
|
|
5031
|
+
names: ResourceNames,
|
|
5032
|
+
config: ResolvedBeignetConfig,
|
|
5033
|
+
options: ResourceGenerationOptions,
|
|
5034
|
+
): string {
|
|
5035
|
+
const filePath = path.join(
|
|
5036
|
+
resourceUseCaseDir(names, config),
|
|
5037
|
+
`update-${names.singularKebab}.ts`,
|
|
5038
|
+
);
|
|
5039
|
+
|
|
5040
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5041
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5042
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5043
|
+
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
5044
|
+
Update${names.singularPascal}InputSchema,
|
|
5045
|
+
${names.singularPascal}Schema,
|
|
5046
|
+
} from "../schemas";
|
|
5047
|
+
|
|
5048
|
+
export const update${names.singularPascal}UseCase = useCase
|
|
5049
|
+
.command("${names.pluralCamel}.update")
|
|
5050
|
+
.input(Update${names.singularPascal}InputSchema)
|
|
5051
|
+
.output(${names.singularPascal}Schema)
|
|
5052
|
+
.run(async ({ ctx, input }) => {
|
|
5053
|
+
${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 }" : ""});
|
|
5054
|
+
if (!existing) {
|
|
5055
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5056
|
+
details: { id: input.id },
|
|
5057
|
+
});
|
|
5058
|
+
}
|
|
5059
|
+
${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
5060
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
5061
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
5062
|
+
});
|
|
5063
|
+
}
|
|
5064
|
+
|
|
5065
|
+
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
5066
|
+
...input,
|
|
5067
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
5068
|
+
if (!${names.singularCamel}) {
|
|
5069
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
5070
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Updated, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
5074
|
+
return ${names.singularCamel};
|
|
5075
|
+
});
|
|
5076
|
+
`;
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
function deleteUseCaseFile(
|
|
5080
|
+
names: ResourceNames,
|
|
5081
|
+
config: ResolvedBeignetConfig,
|
|
5082
|
+
options: ResourceGenerationOptions,
|
|
5083
|
+
): string {
|
|
5084
|
+
const filePath = path.join(
|
|
5085
|
+
resourceUseCaseDir(names, config),
|
|
5086
|
+
`delete-${names.singularKebab}.ts`,
|
|
5087
|
+
);
|
|
5088
|
+
|
|
5089
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5090
|
+
import { z } from "zod";
|
|
5091
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5092
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5093
|
+
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
5094
|
+
|
|
5095
|
+
export const delete${names.singularPascal}UseCase = useCase
|
|
5096
|
+
.command("${names.pluralCamel}.delete")
|
|
5097
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
5098
|
+
.output(z.void())
|
|
5099
|
+
.run(async ({ ctx, input }) => {
|
|
5100
|
+
${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 }" : ""});
|
|
5101
|
+
if (!deleted) {
|
|
5102
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5103
|
+
details: { id: input.id },
|
|
5104
|
+
});
|
|
5105
|
+
}
|
|
5106
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Deleted, {\n\t\t\tid: input.id,\n\t\t});\n` : ""} });
|
|
5107
|
+
`;
|
|
5108
|
+
}
|
|
5109
|
+
|
|
5110
|
+
function useCasesIndexFile(
|
|
5111
|
+
names: ResourceNames,
|
|
5112
|
+
mode: ResourceGenerationMode,
|
|
5113
|
+
): string {
|
|
3925
5114
|
return `export { create${names.singularPascal}UseCase } from "./create-${names.singularKebab}";
|
|
3926
|
-
export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
3927
|
-
export {
|
|
5115
|
+
${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}";
|
|
5116
|
+
${mode === "resource" ? `export { update${names.singularPascal}UseCase } from "./update-${names.singularKebab}";\n` : ""}export {
|
|
3928
5117
|
Create${names.singularPascal}InputSchema,
|
|
3929
|
-
List${names.pluralPascal}InputSchema,
|
|
5118
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n` : ""} List${names.pluralPascal}InputSchema,
|
|
3930
5119
|
List${names.pluralPascal}OutputSchema,
|
|
3931
|
-
${names.singularPascal}Schema,
|
|
5120
|
+
${mode === "resource" ? ` Update${names.singularPascal}BodySchema,\n Update${names.singularPascal}InputSchema,\n` : ""} ${names.singularPascal}Schema,
|
|
3932
5121
|
type Create${names.singularPascal}Input,
|
|
3933
|
-
type List${names.pluralPascal}Input,
|
|
5122
|
+
${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
5123
|
type ${names.singularPascal},
|
|
3935
|
-
} from "
|
|
5124
|
+
} from "../schemas";
|
|
3936
5125
|
`;
|
|
3937
5126
|
}
|
|
3938
5127
|
|
|
3939
5128
|
function repositoryPortFile(
|
|
3940
5129
|
names: ResourceNames,
|
|
3941
5130
|
config: ResolvedBeignetConfig,
|
|
5131
|
+
mode: ResourceGenerationMode,
|
|
5132
|
+
options: ResourceGenerationOptions,
|
|
3942
5133
|
): string {
|
|
3943
5134
|
return `import type {
|
|
3944
|
-
|
|
3945
|
-
|
|
5135
|
+
CursorPage,
|
|
5136
|
+
CursorPageInfo,
|
|
3946
5137
|
PageResult,
|
|
5138
|
+
SortDirection,
|
|
3947
5139
|
} from "@beignet/core/pagination";
|
|
3948
5140
|
import type {
|
|
3949
5141
|
Create${names.singularPascal}Input,
|
|
3950
|
-
${names.singularPascal},
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
5142
|
+
${names.singularPascal}Cursor,
|
|
5143
|
+
${names.singularPascal}SortBy,
|
|
5144
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5145
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
5146
|
+
|
|
5147
|
+
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal}, CursorPageInfo>;
|
|
5148
|
+
export type List${names.pluralPascal}Query = {
|
|
5149
|
+
page: CursorPage;
|
|
5150
|
+
cursor: ${names.singularPascal}Cursor | null;
|
|
5151
|
+
name?: string;
|
|
5152
|
+
sortBy: ${names.singularPascal}SortBy;
|
|
5153
|
+
sortDirection: SortDirection;
|
|
5154
|
+
${options.tenant ? `\ttenantId: string;\n` : ""}};
|
|
5155
|
+
${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
5156
|
|
|
3955
5157
|
export interface ${names.singularPascal}Repository {
|
|
3956
|
-
list(
|
|
3957
|
-
create(input: Create${names.singularPascal}Input): Promise<${names.singularPascal}>;
|
|
3958
|
-
}
|
|
5158
|
+
list(query: List${names.pluralPascal}Query): Promise<List${names.pluralPascal}Result>;
|
|
5159
|
+
create(input: ${options.tenant ? `Create${names.singularPascal}RepositoryInput` : `Create${names.singularPascal}Input`}): Promise<${names.singularPascal}>;
|
|
5160
|
+
${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
5161
|
`;
|
|
3960
5162
|
}
|
|
3961
5163
|
|
|
3962
5164
|
function inMemoryRepositoryFile(
|
|
3963
5165
|
names: ResourceNames,
|
|
3964
5166
|
config: ResolvedBeignetConfig,
|
|
5167
|
+
mode: ResourceGenerationMode,
|
|
5168
|
+
options: ResourceGenerationOptions,
|
|
3965
5169
|
): string {
|
|
3966
5170
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
5171
|
+
const recordType = options.softDelete
|
|
5172
|
+
? `type ${names.singularPascal}Record = ${names.singularPascal} & {
|
|
5173
|
+
\tdeletedAt: string | null;
|
|
5174
|
+
};
|
|
3967
5175
|
|
|
3968
|
-
|
|
5176
|
+
`
|
|
5177
|
+
: "";
|
|
5178
|
+
const mapValueType = options.softDelete
|
|
5179
|
+
? `${names.singularPascal}Record`
|
|
5180
|
+
: names.singularPascal;
|
|
5181
|
+
const activeFilter = options.softDelete
|
|
5182
|
+
? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.deletedAt === null)\n`
|
|
5183
|
+
: "";
|
|
5184
|
+
const newDeletedAtField = options.softDelete
|
|
5185
|
+
? `\t\t\t\tdeletedAt: null,\n`
|
|
5186
|
+
: "";
|
|
5187
|
+
const findDeletedGuard = options.softDelete
|
|
5188
|
+
? `\t\t\tif (${names.singularCamel}?.deletedAt !== null) return null;\n`
|
|
5189
|
+
: "";
|
|
5190
|
+
const updateDeletedGuard = options.softDelete
|
|
5191
|
+
? " || existing.deletedAt !== null"
|
|
5192
|
+
: "";
|
|
5193
|
+
const deleteImplementation = options.softDelete
|
|
5194
|
+
? `${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();
|
|
5195
|
+
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
5196
|
+
return true;`
|
|
5197
|
+
: `${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);`;
|
|
5198
|
+
|
|
5199
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5200
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
3969
5201
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
5202
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3970
5203
|
import type {
|
|
3971
5204
|
Create${names.singularPascal}Input,
|
|
3972
|
-
${names.singularPascal},
|
|
3973
|
-
} from "${aliasModule(
|
|
5205
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5206
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
5207
|
+
|
|
5208
|
+
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
5209
|
+
return {
|
|
5210
|
+
id: ${names.singularCamel}.id,
|
|
5211
|
+
${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
5212
|
+
version: ${names.singularCamel}.version,
|
|
5213
|
+
createdAt: ${names.singularCamel}.createdAt,
|
|
5214
|
+
updatedAt: ${names.singularCamel}.updatedAt,
|
|
5215
|
+
};
|
|
5216
|
+
}
|
|
5217
|
+
|
|
5218
|
+
function compare${names.pluralPascal}(
|
|
5219
|
+
left: ${mapValueType},
|
|
5220
|
+
right: ${mapValueType},
|
|
5221
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5222
|
+
): number {
|
|
5223
|
+
const leftValue = left[query.sortBy];
|
|
5224
|
+
const rightValue = right[query.sortBy];
|
|
5225
|
+
const fieldComparison =
|
|
5226
|
+
leftValue === rightValue ? left.id.localeCompare(right.id) : leftValue.localeCompare(rightValue);
|
|
5227
|
+
|
|
5228
|
+
return query.sortDirection === "asc" ? fieldComparison : -fieldComparison;
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
function isAfter${names.singularPascal}Cursor(
|
|
5232
|
+
${names.singularCamel}: ${mapValueType},
|
|
5233
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5234
|
+
): boolean {
|
|
5235
|
+
if (!query.cursor) return true;
|
|
5236
|
+
|
|
5237
|
+
const sortValue = ${names.singularCamel}[query.sortBy];
|
|
5238
|
+
const fieldComparison =
|
|
5239
|
+
sortValue === query.cursor.sortValue
|
|
5240
|
+
? ${names.singularCamel}.id.localeCompare(query.cursor.id)
|
|
5241
|
+
: sortValue.localeCompare(query.cursor.sortValue);
|
|
5242
|
+
|
|
5243
|
+
return query.sortDirection === "asc" ? fieldComparison > 0 : fieldComparison < 0;
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
function cursorFor${names.singularPascal}(
|
|
5247
|
+
${names.singularCamel}: ${mapValueType},
|
|
5248
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5249
|
+
): string {
|
|
5250
|
+
return encode${names.singularPascal}Cursor({
|
|
5251
|
+
sortBy: query.sortBy,
|
|
5252
|
+
sortDirection: query.sortDirection,
|
|
5253
|
+
sortValue: ${names.singularCamel}[query.sortBy],
|
|
5254
|
+
id: ${names.singularCamel}.id,
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
3974
5257
|
|
|
3975
5258
|
export function createInMemory${names.singularPascal}Repository(
|
|
3976
5259
|
seed: ${names.singularPascal}[] = [],
|
|
3977
5260
|
): ${names.singularPascal}Repository {
|
|
3978
|
-
const ${names.pluralCamel} = new Map
|
|
5261
|
+
const ${names.pluralCamel} = new Map<string, ${mapValueType}>(
|
|
5262
|
+
seed.map((${names.singularCamel}) => [
|
|
5263
|
+
${names.singularCamel}.id,
|
|
5264
|
+
{ ...${names.singularCamel}${options.softDelete ? ", deletedAt: null" : ""} },
|
|
5265
|
+
]),
|
|
5266
|
+
);
|
|
3979
5267
|
|
|
3980
5268
|
return {
|
|
3981
|
-
async list(
|
|
3982
|
-
const
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
all${names.pluralPascal}.length
|
|
5269
|
+
async list(query) {
|
|
5270
|
+
const name = query.name?.toLocaleLowerCase();
|
|
5271
|
+
const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
5272
|
+
${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))
|
|
5273
|
+
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
5274
|
+
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
5275
|
+
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
5276
|
+
const nextCursor =
|
|
5277
|
+
all${names.pluralPascal}.length > query.page.limit && pageItems.length > 0
|
|
5278
|
+
? cursorFor${names.singularPascal}(pageItems[pageItems.length - 1]!, query)
|
|
5279
|
+
: null;
|
|
5280
|
+
|
|
5281
|
+
return cursorPageResult(
|
|
5282
|
+
pageItems.map(to${names.singularPascal}),
|
|
5283
|
+
query.page,
|
|
5284
|
+
nextCursor,
|
|
3990
5285
|
);
|
|
3991
5286
|
},
|
|
3992
|
-
async create(input
|
|
5287
|
+
async create(input) {
|
|
5288
|
+
const now = new Date().toISOString();
|
|
3993
5289
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
3994
5290
|
id: crypto.randomUUID(),
|
|
3995
|
-
name: input.name,
|
|
3996
|
-
|
|
5291
|
+
${options.tenant ? `\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
5292
|
+
version: 1,
|
|
5293
|
+
createdAt: now,
|
|
5294
|
+
updatedAt: now,
|
|
3997
5295
|
};
|
|
3998
|
-
${names.pluralCamel}.set(${names.singularCamel}.id,
|
|
5296
|
+
${names.pluralCamel}.set(${names.singularCamel}.id, {
|
|
5297
|
+
...${names.singularCamel},
|
|
5298
|
+
${newDeletedAtField} });
|
|
3999
5299
|
return ${names.singularCamel};
|
|
4000
5300
|
},
|
|
5301
|
+
${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
5302
|
};
|
|
4002
5303
|
}
|
|
4003
5304
|
`;
|
|
4004
5305
|
}
|
|
4005
5306
|
|
|
4006
|
-
function drizzleSchemaFile(
|
|
4007
|
-
|
|
5307
|
+
function drizzleSchemaFile(
|
|
5308
|
+
names: ResourceNames,
|
|
5309
|
+
options: ResourceGenerationOptions,
|
|
5310
|
+
): string {
|
|
5311
|
+
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
4008
5312
|
|
|
4009
5313
|
export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
4010
5314
|
id: text("id").primaryKey(),
|
|
4011
|
-
name: text("name").notNull(),
|
|
5315
|
+
${options.tenant ? `\ttenantId: text("tenant_id").notNull(),\n` : ""} name: text("name").notNull(),
|
|
5316
|
+
version: integer("version").notNull(),
|
|
4012
5317
|
createdAt: text("created_at").notNull(),
|
|
4013
|
-
|
|
5318
|
+
updatedAt: text("updated_at").notNull(),
|
|
5319
|
+
${options.softDelete ? `\tdeletedAt: text("deleted_at"),\n` : ""}});
|
|
4014
5320
|
`;
|
|
4015
5321
|
}
|
|
4016
5322
|
|
|
5323
|
+
function drizzleResourceWhere(
|
|
5324
|
+
names: ResourceNames,
|
|
5325
|
+
options: ResourceGenerationOptions,
|
|
5326
|
+
predicates: readonly string[],
|
|
5327
|
+
): string {
|
|
5328
|
+
const allPredicates = [
|
|
5329
|
+
...predicates,
|
|
5330
|
+
...(options.softDelete
|
|
5331
|
+
? [`isNull(schema.${names.pluralCamel}.deletedAt)`]
|
|
5332
|
+
: []),
|
|
5333
|
+
];
|
|
5334
|
+
|
|
5335
|
+
if (allPredicates.length === 0) return "";
|
|
5336
|
+
if (allPredicates.length === 1) return allPredicates[0] ?? "";
|
|
5337
|
+
return `and(${allPredicates.join(", ")})`;
|
|
5338
|
+
}
|
|
5339
|
+
|
|
4017
5340
|
function drizzleRepositoryFile(
|
|
4018
5341
|
names: ResourceNames,
|
|
4019
5342
|
config: ResolvedBeignetConfig,
|
|
5343
|
+
mode: ResourceGenerationMode,
|
|
5344
|
+
options: ResourceGenerationOptions,
|
|
4020
5345
|
): string {
|
|
4021
5346
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
4022
5347
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
4023
|
-
|
|
4024
|
-
|
|
5348
|
+
const findWhere = drizzleResourceWhere(names, options, [
|
|
5349
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
5350
|
+
...(options.tenant
|
|
5351
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
5352
|
+
: []),
|
|
5353
|
+
]);
|
|
5354
|
+
const updateWhere = drizzleResourceWhere(names, options, [
|
|
5355
|
+
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
5356
|
+
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
5357
|
+
...(options.tenant
|
|
5358
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, input.tenantId)`]
|
|
5359
|
+
: []),
|
|
5360
|
+
]);
|
|
5361
|
+
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
5362
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
5363
|
+
...(options.tenant
|
|
5364
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
5365
|
+
: []),
|
|
5366
|
+
]);
|
|
5367
|
+
const drizzleImports = [
|
|
5368
|
+
"and",
|
|
5369
|
+
"asc",
|
|
5370
|
+
"desc",
|
|
5371
|
+
"eq",
|
|
5372
|
+
"gt",
|
|
5373
|
+
options.softDelete ? "isNull" : undefined,
|
|
5374
|
+
"lt",
|
|
5375
|
+
"or",
|
|
5376
|
+
"sql",
|
|
5377
|
+
"type SQL",
|
|
5378
|
+
].filter((name): name is string => Boolean(name));
|
|
5379
|
+
|
|
5380
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5381
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
4025
5382
|
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
4026
|
-
import {
|
|
5383
|
+
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
4027
5384
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
5385
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
4028
5386
|
import type {
|
|
4029
5387
|
Create${names.singularPascal}Input,
|
|
4030
|
-
${names.singularPascal},
|
|
4031
|
-
} from "${aliasModule(
|
|
5388
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5389
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
4032
5390
|
import * as schema from "${aliasModule(schemaPath)}";
|
|
4033
5391
|
|
|
4034
5392
|
type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelect;
|
|
@@ -4036,35 +5394,104 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
4036
5394
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
4037
5395
|
return {
|
|
4038
5396
|
id: row.id,
|
|
4039
|
-
name: row.name,
|
|
5397
|
+
${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
5398
|
+
version: row.version,
|
|
4040
5399
|
createdAt: row.createdAt,
|
|
5400
|
+
updatedAt: row.updatedAt,
|
|
4041
5401
|
};
|
|
4042
5402
|
}
|
|
4043
5403
|
|
|
5404
|
+
type List${names.pluralPascal}Query = Parameters<${names.singularPascal}Repository["list"]>[0];
|
|
5405
|
+
|
|
5406
|
+
function ${names.singularCamel}SortColumn(query: List${names.pluralPascal}Query) {
|
|
5407
|
+
return query.sortBy === "name" ? schema.${names.pluralCamel}.name : schema.${names.pluralCamel}.createdAt;
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
function ${names.singularCamel}CursorFilter(
|
|
5411
|
+
query: List${names.pluralPascal}Query,
|
|
5412
|
+
): SQL<unknown> | undefined {
|
|
5413
|
+
if (!query.cursor) return undefined;
|
|
5414
|
+
|
|
5415
|
+
const column = ${names.singularCamel}SortColumn(query);
|
|
5416
|
+
const compare = query.sortDirection === "asc" ? gt : lt;
|
|
5417
|
+
|
|
5418
|
+
return or(
|
|
5419
|
+
compare(column, query.cursor.sortValue),
|
|
5420
|
+
and(
|
|
5421
|
+
eq(column, query.cursor.sortValue),
|
|
5422
|
+
compare(schema.${names.pluralCamel}.id, query.cursor.id),
|
|
5423
|
+
),
|
|
5424
|
+
) as SQL<unknown>;
|
|
5425
|
+
}
|
|
5426
|
+
|
|
5427
|
+
function ${names.singularCamel}ListWhere(
|
|
5428
|
+
query: List${names.pluralPascal}Query,
|
|
5429
|
+
): SQL<unknown> | undefined {
|
|
5430
|
+
const filters: SQL<unknown>[] = [
|
|
5431
|
+
${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, query.tenantId),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
5432
|
+
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
5433
|
+
|
|
5434
|
+
if (query.name) {
|
|
5435
|
+
const pattern = \`%\${query.name.toLocaleLowerCase()}%\`;
|
|
5436
|
+
filters.push(sql\`lower(\${schema.${names.pluralCamel}.name}) like \${pattern}\`);
|
|
5437
|
+
}
|
|
5438
|
+
if (cursor) filters.push(cursor);
|
|
5439
|
+
|
|
5440
|
+
return filters.length > 0 ? and(...filters) : undefined;
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
function ${names.singularCamel}OrderBy(query: List${names.pluralPascal}Query) {
|
|
5444
|
+
const order = query.sortDirection === "asc" ? asc : desc;
|
|
5445
|
+
|
|
5446
|
+
return [order(${names.singularCamel}SortColumn(query)), order(schema.${names.pluralCamel}.id)] as const;
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
function cursorFor${names.singularPascal}(
|
|
5450
|
+
row: ${names.singularPascal}Row,
|
|
5451
|
+
query: List${names.pluralPascal}Query,
|
|
5452
|
+
): string {
|
|
5453
|
+
return encode${names.singularPascal}Cursor({
|
|
5454
|
+
sortBy: query.sortBy,
|
|
5455
|
+
sortDirection: query.sortDirection,
|
|
5456
|
+
sortValue: row[query.sortBy],
|
|
5457
|
+
id: row.id,
|
|
5458
|
+
});
|
|
5459
|
+
}
|
|
5460
|
+
|
|
4044
5461
|
export function createDrizzle${names.singularPascal}Repository(
|
|
4045
5462
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
4046
5463
|
): ${names.singularPascal}Repository {
|
|
4047
5464
|
return {
|
|
4048
|
-
async list(
|
|
5465
|
+
async list(query) {
|
|
5466
|
+
const where = ${names.singularCamel}ListWhere(query);
|
|
4049
5467
|
const rows = await db
|
|
4050
5468
|
.select()
|
|
4051
5469
|
.from(schema.${names.pluralCamel})
|
|
4052
|
-
.
|
|
4053
|
-
.
|
|
4054
|
-
.
|
|
4055
|
-
const
|
|
4056
|
-
|
|
4057
|
-
.
|
|
4058
|
-
|
|
4059
|
-
|
|
5470
|
+
.where(where)
|
|
5471
|
+
.orderBy(...${names.singularCamel}OrderBy(query))
|
|
5472
|
+
.limit(query.page.limit + 1);
|
|
5473
|
+
const pageRows = rows.slice(0, query.page.limit);
|
|
5474
|
+
const nextCursor =
|
|
5475
|
+
rows.length > query.page.limit && pageRows.length > 0
|
|
5476
|
+
? cursorFor${names.singularPascal}(pageRows[pageRows.length - 1]!, query)
|
|
5477
|
+
: null;
|
|
5478
|
+
|
|
5479
|
+
return cursorPageResult(
|
|
5480
|
+
pageRows.map(to${names.singularPascal}),
|
|
5481
|
+
query.page,
|
|
5482
|
+
nextCursor,
|
|
5483
|
+
);
|
|
4060
5484
|
},
|
|
4061
|
-
async create(input
|
|
5485
|
+
async create(input) {
|
|
5486
|
+
const now = new Date().toISOString();
|
|
4062
5487
|
const [row] = await db
|
|
4063
5488
|
.insert(schema.${names.pluralCamel})
|
|
4064
5489
|
.values({
|
|
4065
5490
|
id: crypto.randomUUID(),
|
|
4066
|
-
name: input.name,
|
|
4067
|
-
|
|
5491
|
+
${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
5492
|
+
version: 1,
|
|
5493
|
+
createdAt: now,
|
|
5494
|
+
updatedAt: now,
|
|
4068
5495
|
})
|
|
4069
5496
|
.returning();
|
|
4070
5497
|
|
|
@@ -4074,6 +5501,7 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
4074
5501
|
|
|
4075
5502
|
return to${names.singularPascal}(row);
|
|
4076
5503
|
},
|
|
5504
|
+
${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
5505
|
};
|
|
4078
5506
|
}
|
|
4079
5507
|
`;
|
|
@@ -4082,13 +5510,17 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
4082
5510
|
function contractFile(
|
|
4083
5511
|
names: ResourceNames,
|
|
4084
5512
|
config: ResolvedBeignetConfig,
|
|
5513
|
+
mode: ResourceGenerationMode,
|
|
5514
|
+
options: ResourceGenerationOptions,
|
|
4085
5515
|
): string {
|
|
4086
|
-
return `import {
|
|
5516
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
4087
5517
|
import { z } from "zod";
|
|
4088
|
-
import {
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
5518
|
+
${mode === "resource" ? `import { errors } from "${aliasModule(resourceSharedErrorsPath(config))}";\n` : ""}import {
|
|
5519
|
+
Create${names.singularPascal}InputSchema,
|
|
5520
|
+
List${names.pluralPascal}InputSchema,
|
|
5521
|
+
List${names.pluralPascal}OutputSchema,
|
|
5522
|
+
${names.singularPascal}Schema,
|
|
5523
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n Update${names.singularPascal}BodySchema,\n` : ""}} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
4092
5524
|
|
|
4093
5525
|
const ErrorResponseSchema = z.object({
|
|
4094
5526
|
code: z.string(),
|
|
@@ -4096,7 +5528,7 @@ const ErrorResponseSchema = z.object({
|
|
|
4096
5528
|
requestId: z.string().optional(),
|
|
4097
5529
|
});
|
|
4098
5530
|
|
|
4099
|
-
const ${names.pluralCamel} =
|
|
5531
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
4100
5532
|
.namespace("${names.pluralCamel}")
|
|
4101
5533
|
.responses({
|
|
4102
5534
|
500: ErrorResponseSchema,
|
|
@@ -4104,22 +5536,52 @@ const ${names.pluralCamel} = createContractGroup()
|
|
|
4104
5536
|
|
|
4105
5537
|
export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
4106
5538
|
.get("/api/${names.pluralKebab}")
|
|
4107
|
-
.query(
|
|
5539
|
+
.query(List${names.pluralPascal}InputSchema)
|
|
4108
5540
|
.responses({
|
|
4109
|
-
200:
|
|
5541
|
+
200: List${names.pluralPascal}OutputSchema,
|
|
4110
5542
|
});
|
|
4111
5543
|
|
|
4112
5544
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
4113
5545
|
.post("/api/${names.pluralKebab}")
|
|
4114
|
-
.body(
|
|
5546
|
+
.body(Create${names.singularPascal}InputSchema)
|
|
5547
|
+
${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
5548
|
+
201: ${names.singularPascal}Schema,
|
|
5549
|
+
});
|
|
5550
|
+
${
|
|
5551
|
+
mode === "resource"
|
|
5552
|
+
? `
|
|
5553
|
+
export const get${names.singularPascal} = ${names.pluralCamel}
|
|
5554
|
+
.get("/api/${names.pluralKebab}/:id")
|
|
5555
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5556
|
+
.errors({ ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
5557
|
+
.responses({
|
|
5558
|
+
200: ${names.singularPascal}Schema,
|
|
5559
|
+
});
|
|
5560
|
+
|
|
5561
|
+
export const update${names.singularPascal} = ${names.pluralCamel}
|
|
5562
|
+
.patch("/api/${names.pluralKebab}/:id")
|
|
5563
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5564
|
+
.body(Update${names.singularPascal}BodySchema)
|
|
5565
|
+
${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 })
|
|
4115
5566
|
.responses({
|
|
4116
|
-
|
|
5567
|
+
200: ${names.singularPascal}Schema,
|
|
4117
5568
|
});
|
|
5569
|
+
|
|
5570
|
+
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
5571
|
+
.delete("/api/${names.pluralKebab}/:id")
|
|
5572
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5573
|
+
${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 })
|
|
5574
|
+
.responses({
|
|
5575
|
+
204: null,
|
|
5576
|
+
});
|
|
5577
|
+
`
|
|
5578
|
+
: ""
|
|
5579
|
+
}
|
|
4118
5580
|
`;
|
|
4119
5581
|
}
|
|
4120
5582
|
|
|
4121
5583
|
function standaloneContractFile(names: ResourceNames): string {
|
|
4122
|
-
return `import {
|
|
5584
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
4123
5585
|
import { z } from "zod";
|
|
4124
5586
|
|
|
4125
5587
|
const ErrorResponseSchema = z.object({
|
|
@@ -4128,7 +5590,7 @@ const ErrorResponseSchema = z.object({
|
|
|
4128
5590
|
requestId: z.string().optional(),
|
|
4129
5591
|
});
|
|
4130
5592
|
|
|
4131
|
-
const ${names.pluralCamel} =
|
|
5593
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
4132
5594
|
.namespace("${names.pluralCamel}")
|
|
4133
5595
|
.responses({
|
|
4134
5596
|
500: ErrorResponseSchema,
|
|
@@ -4169,7 +5631,8 @@ function standaloneUseCaseFile(
|
|
|
4169
5631
|
config: ResolvedBeignetConfig,
|
|
4170
5632
|
filePath: string,
|
|
4171
5633
|
): string {
|
|
4172
|
-
return `import {
|
|
5634
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5635
|
+
import { z } from "zod";
|
|
4173
5636
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
4174
5637
|
|
|
4175
5638
|
export const ${names.action.pascal}InputSchema = z.object({});
|
|
@@ -4202,29 +5665,27 @@ function useCaseTestFile(
|
|
|
4202
5665
|
return `import { describe, expect, it } from "bun:test";
|
|
4203
5666
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
4204
5667
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
4205
|
-
import {
|
|
5668
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
5669
|
+
import { createTestAnonymousActor } from "@beignet/core/ports/testing";
|
|
4206
5670
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4207
5671
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
4208
5672
|
import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
|
|
4209
5673
|
|
|
4210
5674
|
describe("${names.exportName}", () => {
|
|
4211
5675
|
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
|
-
}));
|
|
5676
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
5677
|
+
base: appPorts,
|
|
5678
|
+
overrides: {
|
|
5679
|
+
gate: appPorts.gate,
|
|
5680
|
+
devtools: createInMemoryDevtools(),
|
|
5681
|
+
},
|
|
5682
|
+
});
|
|
5683
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
5684
|
+
ports: testFixture.ports,
|
|
5685
|
+
actor: createTestAnonymousActor(),
|
|
5686
|
+
tenant: null,
|
|
5687
|
+
});
|
|
5688
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
4228
5689
|
|
|
4229
5690
|
const result = await tester.run(${names.exportName}, {});
|
|
4230
5691
|
|
|
@@ -4290,6 +5751,144 @@ export const ${names.policyName} = definePolicy({
|
|
|
4290
5751
|
`;
|
|
4291
5752
|
}
|
|
4292
5753
|
|
|
5754
|
+
function resourcePolicyFile(
|
|
5755
|
+
names: ResourceNames,
|
|
5756
|
+
_config: ResolvedBeignetConfig,
|
|
5757
|
+
options: ResourceGenerationOptions,
|
|
5758
|
+
): string {
|
|
5759
|
+
return `import type { ActivityActor, ActivityTenant } from "@beignet/core/ports";
|
|
5760
|
+
import { definePolicy, deny } from "@beignet/core/ports";
|
|
5761
|
+
|
|
5762
|
+
export type AuthorizationContext = {
|
|
5763
|
+
actor: ActivityActor;
|
|
5764
|
+
tenant?: ActivityTenant;
|
|
5765
|
+
};
|
|
5766
|
+
|
|
5767
|
+
type ${names.singularPascal}PolicyResource = {
|
|
5768
|
+
id: string;
|
|
5769
|
+
tenantId?: string;
|
|
5770
|
+
};
|
|
5771
|
+
|
|
5772
|
+
function isAuthenticated(ctx: AuthorizationContext) {
|
|
5773
|
+
return ctx.actor.type === "user" && Boolean(ctx.actor.id);
|
|
5774
|
+
}
|
|
5775
|
+
|
|
5776
|
+
function canWrite(ctx: AuthorizationContext) {
|
|
5777
|
+
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
5778
|
+
${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
export const ${names.singularCamel}Policy = definePolicy({
|
|
5782
|
+
"${names.pluralCamel}.view": (_ctx: AuthorizationContext) => true,
|
|
5783
|
+
"${names.pluralCamel}.create": (ctx: AuthorizationContext) =>
|
|
5784
|
+
canWrite(ctx),
|
|
5785
|
+
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
5786
|
+
const decision = canWrite(ctx);
|
|
5787
|
+
if (decision !== true) return decision;
|
|
5788
|
+
${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` : ""}
|
|
5789
|
+
return true;
|
|
5790
|
+
},
|
|
5791
|
+
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
5792
|
+
const decision = canWrite(ctx);
|
|
5793
|
+
if (decision !== true) return decision;
|
|
5794
|
+
${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` : ""}
|
|
5795
|
+
return true;
|
|
5796
|
+
},
|
|
5797
|
+
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
5798
|
+
deny("You are not allowed to manage this ${names.singularKebab}."),
|
|
5799
|
+
});
|
|
5800
|
+
`;
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
function resourceEventsFile(names: ResourceNames): string {
|
|
5804
|
+
return `import { defineEvent } from "@beignet/core/events";
|
|
5805
|
+
import { z } from "zod";
|
|
5806
|
+
|
|
5807
|
+
export const ${names.singularPascal}Created = defineEvent("${names.pluralCamel}.created", {
|
|
5808
|
+
payload: z.object({
|
|
5809
|
+
id: z.string().uuid(),
|
|
5810
|
+
}),
|
|
5811
|
+
});
|
|
5812
|
+
|
|
5813
|
+
export const ${names.singularPascal}Updated = defineEvent("${names.pluralCamel}.updated", {
|
|
5814
|
+
payload: z.object({
|
|
5815
|
+
id: z.string().uuid(),
|
|
5816
|
+
}),
|
|
5817
|
+
});
|
|
5818
|
+
|
|
5819
|
+
export const ${names.singularPascal}Deleted = defineEvent("${names.pluralCamel}.deleted", {
|
|
5820
|
+
payload: z.object({
|
|
5821
|
+
id: z.string().uuid(),
|
|
5822
|
+
}),
|
|
5823
|
+
});
|
|
5824
|
+
|
|
5825
|
+
export const ${names.singularCamel}Events = [
|
|
5826
|
+
${names.singularPascal}Created,
|
|
5827
|
+
${names.singularPascal}Updated,
|
|
5828
|
+
${names.singularPascal}Deleted,
|
|
5829
|
+
] as const;
|
|
5830
|
+
`;
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
function resourcePolicyTestFile(
|
|
5834
|
+
names: ResourceNames,
|
|
5835
|
+
options: ResourceGenerationOptions,
|
|
5836
|
+
): string {
|
|
5837
|
+
return `import { describe, expect, it } from "bun:test";
|
|
5838
|
+
import { createPolicyTester } from "@beignet/core/ports/testing";
|
|
5839
|
+
import { ${names.singularCamel}Policy } from "../policy";
|
|
5840
|
+
import type { ${names.singularPascal} } from "../schemas";
|
|
5841
|
+
|
|
5842
|
+
function create${names.singularPascal}(): ${names.singularPascal} {
|
|
5843
|
+
const now = new Date().toISOString();
|
|
5844
|
+
return {
|
|
5845
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
5846
|
+
${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
5847
|
+
version: 1,
|
|
5848
|
+
createdAt: now,
|
|
5849
|
+
updatedAt: now,
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
|
|
5853
|
+
describe("${names.singularCamel}Policy", () => {
|
|
5854
|
+
it("documents generated ${names.pluralKebab} authorization rules", async () => {
|
|
5855
|
+
const tester = createPolicyTester({
|
|
5856
|
+
policies: [${names.singularCamel}Policy],
|
|
5857
|
+
});
|
|
5858
|
+
const ${names.singularCamel} = create${names.singularPascal}();
|
|
5859
|
+
const ctx = {
|
|
5860
|
+
actor: { type: "user" as const, id: "user_test" },
|
|
5861
|
+
${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
5862
|
+
|
|
5863
|
+
await expect(
|
|
5864
|
+
tester.assertMatrix([
|
|
5865
|
+
{
|
|
5866
|
+
name: "authenticated actor can create ${names.singularKebab}",
|
|
5867
|
+
ctx,
|
|
5868
|
+
ability: "${names.pluralCamel}.create",
|
|
5869
|
+
expected: "allow",
|
|
5870
|
+
},
|
|
5871
|
+
{
|
|
5872
|
+
name: "authenticated actor can update ${names.singularKebab}",
|
|
5873
|
+
ctx,
|
|
5874
|
+
ability: "${names.pluralCamel}.update",
|
|
5875
|
+
subject: ${names.singularCamel},
|
|
5876
|
+
expected: "allow",
|
|
5877
|
+
},
|
|
5878
|
+
{
|
|
5879
|
+
name: "authenticated actor can delete ${names.singularKebab}",
|
|
5880
|
+
ctx,
|
|
5881
|
+
ability: "${names.pluralCamel}.delete",
|
|
5882
|
+
subject: ${names.singularCamel},
|
|
5883
|
+
expected: "allow",
|
|
5884
|
+
},
|
|
5885
|
+
]),
|
|
5886
|
+
).resolves.toBeUndefined();
|
|
5887
|
+
});
|
|
5888
|
+
});
|
|
5889
|
+
`;
|
|
5890
|
+
}
|
|
5891
|
+
|
|
4293
5892
|
function eventFile(names: EventNames): string {
|
|
4294
5893
|
return `import { defineEvent } from "@beignet/core/events";
|
|
4295
5894
|
import { z } from "zod";
|
|
@@ -4307,11 +5906,9 @@ export const ${names.eventExportName} = defineEvent("${names.eventName}", {
|
|
|
4307
5906
|
}
|
|
4308
5907
|
|
|
4309
5908
|
function jobFile(names: JobNames, config: ResolvedBeignetConfig): string {
|
|
4310
|
-
return `import {
|
|
5909
|
+
return `import { retry } from "@beignet/core/jobs";
|
|
4311
5910
|
import { z } from "zod";
|
|
4312
|
-
import
|
|
4313
|
-
|
|
4314
|
-
const jobs = createJobHandlers<AppContext>();
|
|
5911
|
+
import { defineJob } from "${aliasModule(config.paths.jobsBuilder)}";
|
|
4315
5912
|
|
|
4316
5913
|
export const ${names.payloadSchemaName} = z.object({
|
|
4317
5914
|
\tid: z.string().uuid(),
|
|
@@ -4319,7 +5916,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
4319
5916
|
|
|
4320
5917
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
4321
5918
|
|
|
4322
|
-
export const ${names.jobExportName} =
|
|
5919
|
+
export const ${names.jobExportName} = defineJob("${names.jobName}", {
|
|
4323
5920
|
\tpayload: ${names.payloadSchemaName},
|
|
4324
5921
|
\tretry: retry.exponential({
|
|
4325
5922
|
\t\tattempts: 3,
|
|
@@ -4336,16 +5933,43 @@ export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
|
|
|
4336
5933
|
`;
|
|
4337
5934
|
}
|
|
4338
5935
|
|
|
5936
|
+
function taskFile(names: TaskNames, config: ResolvedBeignetConfig): string {
|
|
5937
|
+
return `import { z } from "zod";
|
|
5938
|
+
import { defineTask } from "${aliasModule(config.paths.tasksBuilder)}";
|
|
5939
|
+
|
|
5940
|
+
export const ${names.inputSchemaName} = z.object({
|
|
5941
|
+
\tdryRun: z.boolean().default(true),
|
|
5942
|
+
});
|
|
5943
|
+
|
|
5944
|
+
export type ${names.inputTypeName} = z.infer<typeof ${names.inputSchemaName}>;
|
|
5945
|
+
|
|
5946
|
+
export const ${names.taskExportName} = defineTask("${names.taskName}", {
|
|
5947
|
+
\tinput: ${names.inputSchemaName},
|
|
5948
|
+
\tdescription: "Operational ${names.feature.kebab} ${names.artifact.kebab} task.",
|
|
5949
|
+
\tasync handle({ input, ctx }) {
|
|
5950
|
+
\t\tctx.ports.logger.info("Task handled", {
|
|
5951
|
+
\t\t\ttaskName: "${names.taskName}",
|
|
5952
|
+
\t\t\tdryRun: input.dryRun,
|
|
5953
|
+
\t\t});
|
|
5954
|
+
|
|
5955
|
+
\t\treturn {
|
|
5956
|
+
\t\t\tdryRun: input.dryRun,
|
|
5957
|
+
\t\t};
|
|
5958
|
+
\t},
|
|
5959
|
+
});
|
|
5960
|
+
`;
|
|
5961
|
+
}
|
|
5962
|
+
|
|
4339
5963
|
function factoryFile(
|
|
4340
5964
|
names: FactoryNames,
|
|
4341
5965
|
config: ResolvedBeignetConfig,
|
|
4342
5966
|
): string {
|
|
4343
5967
|
const resource = featureResourceNames(names);
|
|
4344
5968
|
|
|
4345
|
-
return `import {
|
|
5969
|
+
return `import { createFactory } from "@beignet/core/testing";
|
|
4346
5970
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4347
5971
|
|
|
4348
|
-
export const ${names.factoryExportName} =
|
|
5972
|
+
export const ${names.factoryExportName} = createFactory("${names.factoryName}", {
|
|
4349
5973
|
\tdefaults: ({ sequence }) => ({
|
|
4350
5974
|
\t\tname: "${resource.singularPascal} " + sequence,
|
|
4351
5975
|
\t}),
|
|
@@ -4375,14 +5999,9 @@ function notificationFile(
|
|
|
4375
5999
|
names: NotificationNames,
|
|
4376
6000
|
config: ResolvedBeignetConfig,
|
|
4377
6001
|
): string {
|
|
4378
|
-
return `import {
|
|
4379
|
-
\tcreateNotificationHandlers,
|
|
4380
|
-
\tdefineMailNotificationChannel,
|
|
4381
|
-
} from "@beignet/core/notifications";
|
|
6002
|
+
return `import { defineMailNotificationChannel } from "@beignet/core/notifications";
|
|
4382
6003
|
import { z } from "zod";
|
|
4383
|
-
import
|
|
4384
|
-
|
|
4385
|
-
const notifications = createNotificationHandlers<AppContext>();
|
|
6004
|
+
import { defineNotification } from "${aliasModule(config.paths.notificationsBuilder)}";
|
|
4386
6005
|
|
|
4387
6006
|
export const ${names.payloadSchemaName} = z.object({
|
|
4388
6007
|
\tid: z.string().uuid(),
|
|
@@ -4392,7 +6011,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
4392
6011
|
|
|
4393
6012
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
4394
6013
|
|
|
4395
|
-
export const ${names.notificationExportName} =
|
|
6014
|
+
export const ${names.notificationExportName} = defineNotification(
|
|
4396
6015
|
\t"${names.notificationName}",
|
|
4397
6016
|
\t{
|
|
4398
6017
|
\t\tpayload: ${names.payloadSchemaName},
|
|
@@ -4414,13 +6033,10 @@ function listenerFile(
|
|
|
4414
6033
|
): string {
|
|
4415
6034
|
const filePath = listenerFilePath(names, config);
|
|
4416
6035
|
|
|
4417
|
-
return `import {
|
|
4418
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
6036
|
+
return `import { defineListener } from "${aliasModule(config.paths.listenersBuilder)}";
|
|
4419
6037
|
import { ${names.event.eventExportName} } from "${relativeModule(filePath, eventFilePath(names.event, config))}";
|
|
4420
6038
|
|
|
4421
|
-
const
|
|
4422
|
-
|
|
4423
|
-
export const ${names.listenerExportName} = events.defineListener(${names.event.eventExportName}, {
|
|
6039
|
+
export const ${names.listenerExportName} = defineListener(${names.event.eventExportName}, {
|
|
4424
6040
|
\tname: "${names.listenerName}",
|
|
4425
6041
|
\tasync handle({ payload, ctx }) {
|
|
4426
6042
|
\t\tctx.ports.logger.info("Listener handled", {
|
|
@@ -4436,11 +6052,8 @@ function scheduleFile(
|
|
|
4436
6052
|
names: ScheduleNames,
|
|
4437
6053
|
config: ResolvedBeignetConfig,
|
|
4438
6054
|
): string {
|
|
4439
|
-
return `import {
|
|
4440
|
-
import {
|
|
4441
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4442
|
-
|
|
4443
|
-
const schedules = createScheduleHandlers<AppContext>();
|
|
6055
|
+
return `import { z } from "zod";
|
|
6056
|
+
import { defineSchedule } from "${aliasModule(config.paths.schedulesBuilder)}";
|
|
4444
6057
|
|
|
4445
6058
|
export const ${names.payloadSchemaName} = z.object({
|
|
4446
6059
|
\tdate: z.string(),
|
|
@@ -4448,7 +6061,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
4448
6061
|
|
|
4449
6062
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
4450
6063
|
|
|
4451
|
-
export const ${names.scheduleExportName} =
|
|
6064
|
+
export const ${names.scheduleExportName} = defineSchedule(
|
|
4452
6065
|
\t"${names.scheduleName}",
|
|
4453
6066
|
\t{
|
|
4454
6067
|
\t\tcron: "${names.cron}",
|
|
@@ -4526,111 +6139,119 @@ export const ${names.uploadExportName} = defineUpload<
|
|
|
4526
6139
|
`;
|
|
4527
6140
|
}
|
|
4528
6141
|
|
|
6142
|
+
function featureUiComponentFile(
|
|
6143
|
+
names: FeatureUiNames,
|
|
6144
|
+
config: ResolvedBeignetConfig,
|
|
6145
|
+
): string {
|
|
6146
|
+
return `"use client";
|
|
6147
|
+
|
|
6148
|
+
import { contractErrorMessage } from "@beignet/core/client";
|
|
6149
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6150
|
+
import { useState } from "react";
|
|
6151
|
+
import { rq } from "${aliasModule(clientIndexPath(config))}";
|
|
6152
|
+
import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(
|
|
6153
|
+
resourceContractFilePath(names, config),
|
|
6154
|
+
)}";
|
|
6155
|
+
|
|
6156
|
+
export function ${names.componentExportName}() {
|
|
6157
|
+
\tconst [name, setName] = useState("");
|
|
6158
|
+
\tconst queryClient = useQueryClient();
|
|
6159
|
+
\tconst ${names.pluralCamel}Query = useQuery(
|
|
6160
|
+
\t\trq(list${names.pluralPascal}).queryOptions({ query: {} }),
|
|
6161
|
+
\t);
|
|
6162
|
+
\tconst create${names.singularPascal}Mutation = useMutation(
|
|
6163
|
+
\t\trq(create${names.singularPascal}).mutationOptions({
|
|
6164
|
+
\t\t\tonSuccess: async () => {
|
|
6165
|
+
\t\t\t\tsetName("");
|
|
6166
|
+
\t\t\t\tawait rq(list${names.pluralPascal}).invalidate(queryClient);
|
|
6167
|
+
\t\t\t},
|
|
6168
|
+
\t\t}),
|
|
6169
|
+
\t);
|
|
6170
|
+
|
|
6171
|
+
\treturn (
|
|
6172
|
+
\t\t<section className="${names.pluralKebab}-panel">
|
|
6173
|
+
\t\t\t<form
|
|
6174
|
+
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
6175
|
+
\t\t\t\tonSubmit={(event) => {
|
|
6176
|
+
\t\t\t\t\tevent.preventDefault();
|
|
6177
|
+
\t\t\t\t\tconst trimmedName = name.trim();
|
|
6178
|
+
\t\t\t\t\tif (!trimmedName) return;
|
|
6179
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
6180
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
6181
|
+
\t\t\t\t\t\tbody: { name: trimmedName },
|
|
6182
|
+
\t\t\t\t\t});
|
|
6183
|
+
\t\t\t\t}}
|
|
6184
|
+
\t\t\t>
|
|
6185
|
+
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
6186
|
+
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
6187
|
+
\t\t\t\t\t<input
|
|
6188
|
+
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
6189
|
+
\t\t\t\t\t\tvalue={name}
|
|
6190
|
+
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
6191
|
+
\t\t\t\t\t\tplaceholder="Name"
|
|
6192
|
+
\t\t\t\t\t/>
|
|
6193
|
+
\t\t\t\t\t<button
|
|
6194
|
+
\t\t\t\t\t\ttype="submit"
|
|
6195
|
+
\t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
|
|
6196
|
+
\t\t\t\t\t>
|
|
6197
|
+
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
6198
|
+
\t\t\t\t\t</button>
|
|
6199
|
+
\t\t\t\t</div>
|
|
6200
|
+
\t\t\t\t{create${names.singularPascal}Mutation.isError ? (
|
|
6201
|
+
\t\t\t\t\t<p role="alert">
|
|
6202
|
+
\t\t\t\t\t\t{contractErrorMessage(
|
|
6203
|
+
\t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
|
|
6204
|
+
\t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
|
|
6205
|
+
\t\t\t\t\t\t)}
|
|
6206
|
+
\t\t\t\t\t</p>
|
|
6207
|
+
\t\t\t\t) : null}
|
|
6208
|
+
\t\t\t</form>
|
|
6209
|
+
|
|
6210
|
+
\t\t\t<div className="${names.pluralKebab}-list">
|
|
6211
|
+
\t\t\t\t<div className="${names.pluralKebab}-list-heading">
|
|
6212
|
+
\t\t\t\t\t<h2>${names.pluralPascal}</h2>
|
|
6213
|
+
\t\t\t\t\t<span>{${names.pluralCamel}Query.data?.items.length ?? 0} shown</span>
|
|
6214
|
+
\t\t\t\t</div>
|
|
6215
|
+
\t\t\t\t{${names.pluralCamel}Query.isLoading ? <p>Loading...</p> : null}
|
|
6216
|
+
\t\t\t\t{${names.pluralCamel}Query.isError ? (
|
|
6217
|
+
\t\t\t\t\t<p role="alert">Could not load ${names.pluralKebab}.</p>
|
|
6218
|
+
\t\t\t\t) : null}
|
|
6219
|
+
\t\t\t\t{${names.pluralCamel}Query.data?.items.length === 0 ? (
|
|
6220
|
+
\t\t\t\t\t<p>No ${names.pluralKebab} yet.</p>
|
|
6221
|
+
\t\t\t\t) : null}
|
|
6222
|
+
\t\t\t\t<ul>
|
|
6223
|
+
\t\t\t\t\t{${names.pluralCamel}Query.data?.items.map((${names.singularCamel}) => (
|
|
6224
|
+
\t\t\t\t\t\t<li key={${names.singularCamel}.id}>
|
|
6225
|
+
\t\t\t\t\t\t\t<strong>{${names.singularCamel}.name}</strong>
|
|
6226
|
+
\t\t\t\t\t\t\t<small>{new Date(${names.singularCamel}.createdAt).toLocaleDateString()}</small>
|
|
6227
|
+
\t\t\t\t\t\t</li>
|
|
6228
|
+
\t\t\t\t\t))}
|
|
6229
|
+
\t\t\t\t</ul>
|
|
6230
|
+
\t\t\t</div>
|
|
6231
|
+
\t\t</section>
|
|
6232
|
+
\t);
|
|
6233
|
+
}
|
|
6234
|
+
`;
|
|
6235
|
+
}
|
|
6236
|
+
|
|
4529
6237
|
function scheduleRouteFile(
|
|
4530
6238
|
names: ScheduleNames,
|
|
4531
6239
|
config: ResolvedBeignetConfig,
|
|
4532
6240
|
): string {
|
|
4533
|
-
return `import {
|
|
4534
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
6241
|
+
return `import { createScheduleRoute } from "@beignet/next";
|
|
4535
6242
|
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
4536
6243
|
import { env } from "@/lib/env";
|
|
4537
6244
|
import { server } from "${aliasModule(config.paths.server)}";
|
|
4538
6245
|
|
|
4539
|
-
|
|
4540
|
-
\tconst cronSecret = env.CRON_SECRET;
|
|
4541
|
-
|
|
4542
|
-
\tif (!cronSecret) {
|
|
4543
|
-
\t\treturn Response.json(
|
|
4544
|
-
\t\t\t{
|
|
4545
|
-
\t\t\t\tok: false,
|
|
4546
|
-
\t\t\t\terror: "CRON_SECRET is not configured.",
|
|
4547
|
-
\t\t\t\tscheduleName: ${names.scheduleExportName}.name,
|
|
4548
|
-
\t\t\t},
|
|
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});
|
|
6246
|
+
export const runtime = "nodejs";
|
|
4620
6247
|
|
|
4621
|
-
|
|
4622
|
-
\
|
|
4623
|
-
\
|
|
4624
|
-
\
|
|
4625
|
-
\
|
|
4626
|
-
\
|
|
4627
|
-
|
|
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};
|
|
6248
|
+
export const { GET, POST } = createScheduleRoute({
|
|
6249
|
+
\tserver,
|
|
6250
|
+
\tschedules: [${names.scheduleExportName}],
|
|
6251
|
+
\tschedule: ${names.scheduleExportName}.name,
|
|
6252
|
+
\tsecret: env.CRON_SECRET,
|
|
6253
|
+
\tsource: "cron-route",
|
|
6254
|
+
});
|
|
4634
6255
|
`;
|
|
4635
6256
|
}
|
|
4636
6257
|
|
|
@@ -4648,84 +6269,161 @@ function useCaseIndexExport(names: UseCaseNames): string {
|
|
|
4648
6269
|
function routeGroupFile(
|
|
4649
6270
|
names: ResourceNames,
|
|
4650
6271
|
config: ResolvedBeignetConfig,
|
|
6272
|
+
mode: ResourceGenerationMode,
|
|
6273
|
+
_options: ResourceGenerationOptions,
|
|
4651
6274
|
): string {
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
6275
|
+
const routeFilePath = path.join(
|
|
6276
|
+
config.paths.features,
|
|
6277
|
+
names.pluralKebab,
|
|
6278
|
+
"routes.ts",
|
|
6279
|
+
);
|
|
6280
|
+
const contractsPath = resourceContractFilePath(names, config);
|
|
6281
|
+
const useCasesPath = resourceUseCaseIndexPath(names, config);
|
|
6282
|
+
|
|
6283
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
6284
|
+
import { defineRouteGroup } from "@beignet/next";
|
|
6285
|
+
import type { AppContext } from "${relativeModule(routeFilePath, config.paths.appContext)}";
|
|
6286
|
+
import {
|
|
6287
|
+
create${names.singularPascal},
|
|
6288
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
6289
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${relativeModule(routeFilePath, contractsPath)}";
|
|
4655
6290
|
import {
|
|
4656
6291
|
create${names.singularPascal}UseCase,
|
|
4657
|
-
list${names.pluralPascal}UseCase,
|
|
4658
|
-
|
|
6292
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
6293
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${relativeModule(routeFilePath, useCasesPath)}";
|
|
4659
6294
|
|
|
4660
6295
|
export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>({
|
|
4661
6296
|
name: "${names.pluralKebab}",
|
|
4662
6297
|
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
|
-
],
|
|
6298
|
+
{ contract: list${names.pluralPascal}, useCase: list${names.pluralPascal}UseCase },
|
|
6299
|
+
{ contract: create${names.singularPascal}, useCase: create${names.singularPascal}UseCase },
|
|
6300
|
+
${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
6301
|
});
|
|
4679
6302
|
`;
|
|
4680
6303
|
}
|
|
4681
6304
|
|
|
4682
|
-
function testFile(
|
|
6305
|
+
function testFile(
|
|
6306
|
+
names: ResourceNames,
|
|
6307
|
+
config: ResolvedBeignetConfig,
|
|
6308
|
+
mode: ResourceGenerationMode,
|
|
6309
|
+
options: ResourceGenerationOptions,
|
|
6310
|
+
): string {
|
|
4683
6311
|
const repositoryPath = path.join(
|
|
4684
6312
|
path.dirname(config.paths.infrastructurePorts),
|
|
4685
6313
|
names.pluralKebab,
|
|
4686
6314
|
`in-memory-${names.singularKebab}-repository.ts`,
|
|
4687
6315
|
);
|
|
6316
|
+
const serverContextPath = path.join(
|
|
6317
|
+
path.dirname(config.paths.server),
|
|
6318
|
+
"context.ts",
|
|
6319
|
+
);
|
|
6320
|
+
const requestTarget = options.tenant ? "requester" : "app";
|
|
4688
6321
|
|
|
4689
6322
|
return `import { describe, expect, it } from "bun:test";
|
|
4690
6323
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
4691
|
-
import { defineRoutes } from "@beignet/web";
|
|
4692
|
-
import { createTestApp } from "@beignet/web/testing";
|
|
6324
|
+
${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` : ""}import { defineRoutes } from "@beignet/web";
|
|
6325
|
+
import { createTestApp${options.tenant ? ", createTestRequester" : ""} } from "@beignet/web/testing";
|
|
4693
6326
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
4694
|
-
import {
|
|
6327
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
6328
|
+
import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/ports/testing";
|
|
4695
6329
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4696
6330
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
4697
|
-
import {
|
|
6331
|
+
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
6332
|
+
import {
|
|
6333
|
+
create${names.singularPascal},
|
|
6334
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
6335
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
4698
6336
|
import { createInMemory${names.singularPascal}Repository } from "${aliasModule(repositoryPath)}";
|
|
4699
6337
|
import { ${names.singularCamel}Routes } from "${aliasModule(path.join(resourceFeatureDir(names, config), "routes.ts"))}";
|
|
4700
6338
|
import {
|
|
4701
6339
|
create${names.singularPascal}UseCase,
|
|
4702
|
-
list${names.pluralPascal}UseCase,
|
|
4703
|
-
} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
6340
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
6341
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
4704
6342
|
|
|
4705
6343
|
describe("${names.pluralCamel} resource", () => {
|
|
4706
|
-
it("creates and lists ${names.pluralCamel}", async () => {
|
|
4707
|
-
const ${names.
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
6344
|
+
it("${mode === "resource" ? `exercises ${names.pluralCamel} CRUD` : `creates and lists ${names.pluralCamel}`}", async () => {
|
|
6345
|
+
const seed${names.pluralPascal} = [
|
|
6346
|
+
{
|
|
6347
|
+
id: "00000000-0000-4000-8000-000000000101",
|
|
6348
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
6349
|
+
version: 1,
|
|
6350
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
6351
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
6352
|
+
},
|
|
6353
|
+
{
|
|
6354
|
+
id: "00000000-0000-4000-8000-000000000102",
|
|
6355
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
6356
|
+
version: 1,
|
|
6357
|
+
createdAt: "2024-01-02T00:00:00.000Z",
|
|
6358
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
6359
|
+
},
|
|
6360
|
+
{
|
|
6361
|
+
id: "00000000-0000-4000-8000-000000000103",
|
|
6362
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
6363
|
+
version: 1,
|
|
6364
|
+
createdAt: "2024-01-03T00:00:00.000Z",
|
|
6365
|
+
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
6366
|
+
},
|
|
6367
|
+
${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` : ""} ];
|
|
6368
|
+
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
6369
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
6370
|
+
base: appPorts,
|
|
6371
|
+
overrides: {
|
|
4713
6372
|
${names.pluralCamel},
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
6373
|
+
${
|
|
6374
|
+
options.auth
|
|
6375
|
+
? `\t\t\t\tauth: {
|
|
6376
|
+
getSession: async () => ({
|
|
6377
|
+
user: { id: "user_test", name: "Test User" },
|
|
6378
|
+
}),
|
|
6379
|
+
},
|
|
6380
|
+
gate: appPorts.gate,\n`
|
|
6381
|
+
: `\t\t\t\tauth: {
|
|
6382
|
+
getSession: async () => null,
|
|
6383
|
+
},\n`
|
|
6384
|
+
} devtools: createInMemoryDevtools(),
|
|
6385
|
+
},
|
|
6386
|
+
transaction: {
|
|
6387
|
+
ports: (ports) => ({
|
|
6388
|
+
...ports,
|
|
6389
|
+
${names.pluralCamel},
|
|
6390
|
+
}),
|
|
6391
|
+
},
|
|
6392
|
+
});
|
|
6393
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
6394
|
+
ports: testFixture.ports,
|
|
6395
|
+
actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
6396
|
+
tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
|
|
4725
6397
|
});
|
|
4726
6398
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
4727
6399
|
|
|
4728
6400
|
const ctx = await tester.ctx();
|
|
6401
|
+
const defaultPage = await tester.run(list${names.pluralPascal}UseCase, {}, { ctx });
|
|
6402
|
+
const filteredPage = await tester.run(
|
|
6403
|
+
list${names.pluralPascal}UseCase,
|
|
6404
|
+
{ name: "alp" },
|
|
6405
|
+
{ ctx },
|
|
6406
|
+
);
|
|
6407
|
+
const firstPage = await tester.run(
|
|
6408
|
+
list${names.pluralPascal}UseCase,
|
|
6409
|
+
{ limit: 2 },
|
|
6410
|
+
{ ctx },
|
|
6411
|
+
);
|
|
6412
|
+
const secondPage = await tester.run(
|
|
6413
|
+
list${names.pluralPascal}UseCase,
|
|
6414
|
+
{ limit: 2, cursor: firstPage.page.nextCursor },
|
|
6415
|
+
{ ctx },
|
|
6416
|
+
);
|
|
6417
|
+
const nameAscendingPage = await tester.run(
|
|
6418
|
+
list${names.pluralPascal}UseCase,
|
|
6419
|
+
{ sortBy: "name", sortDirection: "asc" },
|
|
6420
|
+
{ ctx },
|
|
6421
|
+
);
|
|
6422
|
+
const nameDescendingPage = await tester.run(
|
|
6423
|
+
list${names.pluralPascal}UseCase,
|
|
6424
|
+
{ sortBy: "name", sortDirection: "desc" },
|
|
6425
|
+
{ ctx },
|
|
6426
|
+
);
|
|
4729
6427
|
const created = await tester.run(
|
|
4730
6428
|
create${names.singularPascal}UseCase,
|
|
4731
6429
|
{ name: "First ${names.singularPascal}" },
|
|
@@ -4733,35 +6431,113 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
4733
6431
|
);
|
|
4734
6432
|
const result = await tester.run(
|
|
4735
6433
|
list${names.pluralPascal}UseCase,
|
|
4736
|
-
{
|
|
6434
|
+
{ name: "First" },
|
|
4737
6435
|
{ ctx },
|
|
4738
6436
|
);
|
|
4739
6437
|
|
|
6438
|
+
expect(defaultPage.page).toMatchObject({
|
|
6439
|
+
kind: "cursor",
|
|
6440
|
+
limit: 20,
|
|
6441
|
+
cursor: null,
|
|
6442
|
+
nextCursor: null,
|
|
6443
|
+
hasMore: false,
|
|
6444
|
+
});
|
|
6445
|
+
expect(defaultPage.items.map((item) => item.name)).toEqual([
|
|
6446
|
+
"Gamma ${names.singularPascal}",
|
|
6447
|
+
"Beta ${names.singularPascal}",
|
|
6448
|
+
"Alpha ${names.singularPascal}",
|
|
6449
|
+
]);
|
|
6450
|
+
expect(filteredPage.items.map((item) => item.name)).toEqual([
|
|
6451
|
+
"Alpha ${names.singularPascal}",
|
|
6452
|
+
]);
|
|
6453
|
+
expect(firstPage.items.map((item) => item.name)).toEqual([
|
|
6454
|
+
"Gamma ${names.singularPascal}",
|
|
6455
|
+
"Beta ${names.singularPascal}",
|
|
6456
|
+
]);
|
|
6457
|
+
expect(firstPage.page.nextCursor).toEqual(expect.any(String));
|
|
6458
|
+
expect(secondPage.items.map((item) => item.name)).toEqual([
|
|
6459
|
+
"Alpha ${names.singularPascal}",
|
|
6460
|
+
]);
|
|
6461
|
+
expect(secondPage.page.nextCursor).toBeNull();
|
|
6462
|
+
expect(nameAscendingPage.items.map((item) => item.name)).toEqual([
|
|
6463
|
+
"Alpha ${names.singularPascal}",
|
|
6464
|
+
"Beta ${names.singularPascal}",
|
|
6465
|
+
"Gamma ${names.singularPascal}",
|
|
6466
|
+
]);
|
|
6467
|
+
expect(nameDescendingPage.items.map((item) => item.name)).toEqual([
|
|
6468
|
+
"Gamma ${names.singularPascal}",
|
|
6469
|
+
"Beta ${names.singularPascal}",
|
|
6470
|
+
"Alpha ${names.singularPascal}",
|
|
6471
|
+
]);
|
|
4740
6472
|
expect(created.name).toBe("First ${names.singularPascal}");
|
|
4741
|
-
expect(result.page.total).toBe(1);
|
|
4742
6473
|
expect(result.items).toEqual([created]);
|
|
6474
|
+
${
|
|
6475
|
+
mode === "resource"
|
|
6476
|
+
? `
|
|
6477
|
+
const fetched = await tester.run(
|
|
6478
|
+
get${names.singularPascal}UseCase,
|
|
6479
|
+
{ id: created.id },
|
|
6480
|
+
{ ctx },
|
|
6481
|
+
);
|
|
6482
|
+
const updated = await tester.run(
|
|
6483
|
+
update${names.singularPascal}UseCase,
|
|
6484
|
+
{ id: created.id, name: "Updated ${names.singularPascal}", version: fetched.version },
|
|
6485
|
+
{ ctx },
|
|
6486
|
+
);
|
|
6487
|
+
try {
|
|
6488
|
+
await tester.run(
|
|
6489
|
+
update${names.singularPascal}UseCase,
|
|
6490
|
+
{ id: created.id, name: "Stale ${names.singularPascal}", version: fetched.version },
|
|
6491
|
+
{ ctx },
|
|
6492
|
+
);
|
|
6493
|
+
throw new Error("Expected ${names.singularPascal}Conflict");
|
|
6494
|
+
} catch (error) {
|
|
6495
|
+
expect(isAppError(error)).toBe(true);
|
|
6496
|
+
if (isAppError(error)) {
|
|
6497
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_CONFLICT");
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6500
|
+
await tester.run(delete${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
6501
|
+
const afterDelete = await tester.run(
|
|
6502
|
+
list${names.pluralPascal}UseCase,
|
|
6503
|
+
{ name: "Updated" },
|
|
6504
|
+
{ ctx },
|
|
6505
|
+
);
|
|
6506
|
+
|
|
6507
|
+
expect(fetched).toEqual(created);
|
|
6508
|
+
expect(updated.name).toBe("Updated ${names.singularPascal}");
|
|
6509
|
+
expect(updated.version).toBe(fetched.version + 1);
|
|
6510
|
+
expect(afterDelete.items).toEqual([]);
|
|
6511
|
+
|
|
6512
|
+
try {
|
|
6513
|
+
await tester.run(get${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
6514
|
+
throw new Error("Expected ${names.singularPascal}NotFound");
|
|
6515
|
+
} catch (error) {
|
|
6516
|
+
expect(isAppError(error)).toBe(true);
|
|
6517
|
+
if (isAppError(error)) {
|
|
6518
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_NOT_FOUND");
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
`
|
|
6522
|
+
: ""
|
|
6523
|
+
}
|
|
4743
6524
|
|
|
4744
|
-
const app = await createTestApp
|
|
4745
|
-
ports:
|
|
6525
|
+
const app = await createTestApp({
|
|
6526
|
+
ports: testFixture.ports,
|
|
4746
6527
|
routes: defineRoutes<AppContext>([${names.singularCamel}Routes]),
|
|
4747
|
-
|
|
4748
|
-
mapUnhandledError: () => ({
|
|
4749
|
-
status: 500,
|
|
4750
|
-
body: { code: "INTERNAL_SERVER_ERROR", message: "Internal server error" },
|
|
4751
|
-
}),
|
|
6528
|
+
context: appContext,
|
|
4752
6529
|
});
|
|
4753
|
-
|
|
4754
|
-
const createdViaRoute = await
|
|
6530
|
+
${options.tenant ? `\t\tconst requester = createTestRequester(app, {\n\t\t\theaders: { "x-tenant-id": "tenant_test" },\n\t\t});\n` : ""}
|
|
6531
|
+
const createdViaRoute = await ${requestTarget}.request(create${names.singularPascal}, {
|
|
4755
6532
|
body: { name: "Second ${names.singularPascal}" },
|
|
4756
6533
|
});
|
|
4757
|
-
const listedViaRoute = await
|
|
4758
|
-
query: {
|
|
6534
|
+
${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}, {
|
|
6535
|
+
query: { name: "${mode === "resource" ? "Updated via route" : "Second"}" },
|
|
4759
6536
|
});
|
|
4760
|
-
await app.stop();
|
|
6537
|
+
${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
6538
|
|
|
4762
6539
|
expect(createdViaRoute.name).toBe("Second ${names.singularPascal}");
|
|
4763
|
-
expect(
|
|
4764
|
-
expect(listedViaRoute.items).toContainEqual(createdViaRoute);
|
|
6540
|
+
${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
6541
|
});
|
|
4766
6542
|
});
|
|
4767
6543
|
`;
|