@beignet/cli 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +205 -0
- package/README.md +379 -61
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +72 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +88 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +42 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +136 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +4 -1
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +16 -26
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +657 -588
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1756 -394
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates.d.ts +3 -32
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +1002 -527
- package/dist/templates.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +137 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +182 -0
- package/src/create.ts +24 -31
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1067 -803
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2181 -405
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates.ts +1073 -567
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/src/create-bin.ts +0 -11
package/dist/make.js
CHANGED
|
@@ -1,31 +1,53 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
|
|
4
|
-
export
|
|
5
|
-
"policy",
|
|
6
|
-
"event",
|
|
7
|
-
"events",
|
|
8
|
-
"job",
|
|
9
|
-
"jobs",
|
|
10
|
-
"notification",
|
|
11
|
-
"notifications",
|
|
12
|
-
"upload",
|
|
13
|
-
"uploads",
|
|
14
|
-
];
|
|
4
|
+
export { makeFeatureAddonChoices } from "./choices.js";
|
|
15
5
|
export async function makeResource(options) {
|
|
6
|
+
return makeResourceSlice(options, "resource");
|
|
7
|
+
}
|
|
8
|
+
export async function makeFeature(options) {
|
|
9
|
+
const addons = normalizeMakeFeatureAddons(options.with ?? []);
|
|
10
|
+
if (addons.length === 0) {
|
|
11
|
+
return makeFeatureResource(options);
|
|
12
|
+
}
|
|
13
|
+
if (options.dryRun) {
|
|
14
|
+
return makeFeatureSlice(options, addons);
|
|
15
|
+
}
|
|
16
|
+
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
17
|
+
return makeFeatureSlice(options, addons);
|
|
18
|
+
}
|
|
19
|
+
async function makeFeatureResource(options) {
|
|
20
|
+
try {
|
|
21
|
+
return await makeResourceSlice(options, "feature");
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof Error && error.message.includes("make resource")) {
|
|
25
|
+
throw new Error(error.message.replaceAll("make resource", "make feature"));
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function makeResourceSlice(options, mode) {
|
|
16
31
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
17
32
|
const names = resourceNames(options.name);
|
|
18
33
|
const config = options.config
|
|
19
34
|
? resolveConfig(options.config)
|
|
20
35
|
: await loadBeignetConfig(targetDir);
|
|
21
36
|
await assertStandardApp(targetDir, config);
|
|
37
|
+
if (mode === "resource" &&
|
|
38
|
+
!(await fileExists(path.join(targetDir, resourceSharedErrorsPath(config))))) {
|
|
39
|
+
throw new Error(`beignet make resource expects an app error catalog. Missing ${resourceSharedErrorsPath(config)}.`);
|
|
40
|
+
}
|
|
22
41
|
const persistence = await detectResourcePersistence(targetDir, config);
|
|
23
|
-
const
|
|
42
|
+
const generationOptions = resourceGenerationOptions(options, mode);
|
|
43
|
+
const generatedFiles = resourceFiles(names, config, persistence, mode, generationOptions);
|
|
24
44
|
const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
|
|
25
45
|
force: Boolean(options.force),
|
|
26
46
|
});
|
|
27
47
|
const plannedWiringUpdates = await updateResourceWiring(targetDir, names, config, persistence, {
|
|
28
48
|
dryRun: true,
|
|
49
|
+
mode,
|
|
50
|
+
generationOptions,
|
|
29
51
|
});
|
|
30
52
|
const createdFiles = plannedFiles
|
|
31
53
|
.filter((file) => file.result === "created")
|
|
@@ -46,9 +68,12 @@ export async function makeResource(options) {
|
|
|
46
68
|
updatedFiles.push(...(await updateResourceWiring(targetDir, names, config, {
|
|
47
69
|
persistence,
|
|
48
70
|
dryRun: false,
|
|
71
|
+
mode,
|
|
72
|
+
generationOptions,
|
|
49
73
|
})));
|
|
50
74
|
}
|
|
51
75
|
return {
|
|
76
|
+
schemaVersion: 1,
|
|
52
77
|
name: names.pluralKebab,
|
|
53
78
|
targetDir,
|
|
54
79
|
dryRun: Boolean(options.dryRun),
|
|
@@ -58,27 +83,16 @@ export async function makeResource(options) {
|
|
|
58
83
|
skippedFiles,
|
|
59
84
|
};
|
|
60
85
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return makeFeatureResource(options);
|
|
65
|
-
}
|
|
66
|
-
if (options.dryRun) {
|
|
67
|
-
return makeFeatureSlice(options, addons);
|
|
68
|
-
}
|
|
69
|
-
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
70
|
-
return makeFeatureSlice(options, addons);
|
|
71
|
-
}
|
|
72
|
-
async function makeFeatureResource(options) {
|
|
73
|
-
try {
|
|
74
|
-
return await makeResource(options);
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
if (error instanceof Error && error.message.includes("make resource")) {
|
|
78
|
-
throw new Error(error.message.replaceAll("make resource", "make feature"));
|
|
79
|
-
}
|
|
80
|
-
throw error;
|
|
86
|
+
function resourceGenerationOptions(options, mode) {
|
|
87
|
+
if (mode !== "resource") {
|
|
88
|
+
return { auth: false, tenant: false, events: false, softDelete: false };
|
|
81
89
|
}
|
|
90
|
+
return {
|
|
91
|
+
auth: Boolean(options.auth),
|
|
92
|
+
tenant: Boolean(options.tenant),
|
|
93
|
+
events: Boolean(options.events),
|
|
94
|
+
softDelete: Boolean(options.softDelete),
|
|
95
|
+
};
|
|
82
96
|
}
|
|
83
97
|
async function makeFeatureSlice(options, addons) {
|
|
84
98
|
let result = await makeFeatureResource(options);
|
|
@@ -97,6 +111,9 @@ async function makeFeatureAddon(addon, feature, options) {
|
|
|
97
111
|
if (addon === "policy") {
|
|
98
112
|
return makePolicy({ ...shared, name: feature });
|
|
99
113
|
}
|
|
114
|
+
if (addon === "task") {
|
|
115
|
+
return makeTask({ ...shared, name: `${feature}/backfill` });
|
|
116
|
+
}
|
|
100
117
|
if (addon === "event") {
|
|
101
118
|
return makeEvent({ ...shared, name: `${feature}/created` });
|
|
102
119
|
}
|
|
@@ -106,6 +123,9 @@ async function makeFeatureAddon(addon, feature, options) {
|
|
|
106
123
|
if (addon === "notification") {
|
|
107
124
|
return makeNotification({ ...shared, name: `${feature}/created` });
|
|
108
125
|
}
|
|
126
|
+
if (addon === "ui") {
|
|
127
|
+
return makeFeatureUi({ ...shared, name: feature });
|
|
128
|
+
}
|
|
109
129
|
return makeUpload({ ...shared, name: `${feature}/attachment` });
|
|
110
130
|
}
|
|
111
131
|
function normalizeMakeFeatureAddons(addons) {
|
|
@@ -113,6 +133,8 @@ function normalizeMakeFeatureAddons(addons) {
|
|
|
113
133
|
for (const addon of addons) {
|
|
114
134
|
if (addon === "events")
|
|
115
135
|
normalized.add("event");
|
|
136
|
+
else if (addon === "tasks")
|
|
137
|
+
normalized.add("task");
|
|
116
138
|
else if (addon === "jobs")
|
|
117
139
|
normalized.add("job");
|
|
118
140
|
else if (addon === "notifications")
|
|
@@ -126,9 +148,11 @@ function normalizeMakeFeatureAddons(addons) {
|
|
|
126
148
|
}
|
|
127
149
|
const featureAddonOrder = [
|
|
128
150
|
"policy",
|
|
151
|
+
"task",
|
|
129
152
|
"event",
|
|
130
153
|
"job",
|
|
131
154
|
"notification",
|
|
155
|
+
"ui",
|
|
132
156
|
"upload",
|
|
133
157
|
];
|
|
134
158
|
function mergeMakeResults(left, right) {
|
|
@@ -166,6 +190,7 @@ export async function makeContract(options) {
|
|
|
166
190
|
skippedFiles.push(file.path);
|
|
167
191
|
}
|
|
168
192
|
return {
|
|
193
|
+
schemaVersion: 1,
|
|
169
194
|
name: names.pluralKebab,
|
|
170
195
|
targetDir,
|
|
171
196
|
dryRun: Boolean(options.dryRun),
|
|
@@ -209,6 +234,7 @@ export async function makeUseCase(options) {
|
|
|
209
234
|
if (indexResult === "skipped")
|
|
210
235
|
skippedFiles.push(indexFile.path);
|
|
211
236
|
return {
|
|
237
|
+
schemaVersion: 1,
|
|
212
238
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
213
239
|
targetDir,
|
|
214
240
|
dryRun: Boolean(options.dryRun),
|
|
@@ -247,6 +273,7 @@ export async function makeTest(options) {
|
|
|
247
273
|
updatedFiles.push("package.json");
|
|
248
274
|
}
|
|
249
275
|
return {
|
|
276
|
+
schemaVersion: 1,
|
|
250
277
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
251
278
|
targetDir,
|
|
252
279
|
dryRun: Boolean(options.dryRun),
|
|
@@ -298,6 +325,7 @@ export async function makePort(options) {
|
|
|
298
325
|
updatedFiles.push(config.paths.infrastructurePorts);
|
|
299
326
|
}
|
|
300
327
|
return {
|
|
328
|
+
schemaVersion: 1,
|
|
301
329
|
name: names.kebab,
|
|
302
330
|
targetDir,
|
|
303
331
|
dryRun: Boolean(options.dryRun),
|
|
@@ -336,6 +364,7 @@ export async function makeAdapter(options) {
|
|
|
336
364
|
updatedFiles.push(config.paths.infrastructurePorts);
|
|
337
365
|
}
|
|
338
366
|
return {
|
|
367
|
+
schemaVersion: 1,
|
|
339
368
|
name: names.kebab,
|
|
340
369
|
targetDir,
|
|
341
370
|
dryRun: Boolean(options.dryRun),
|
|
@@ -369,6 +398,7 @@ export async function makePolicy(options) {
|
|
|
369
398
|
skippedFiles.push(file.path);
|
|
370
399
|
}
|
|
371
400
|
return {
|
|
401
|
+
schemaVersion: 1,
|
|
372
402
|
name: names.kebab,
|
|
373
403
|
targetDir,
|
|
374
404
|
dryRun: Boolean(options.dryRun),
|
|
@@ -411,13 +441,52 @@ export async function makeJob(options) {
|
|
|
411
441
|
force: Boolean(options.force),
|
|
412
442
|
dryRun: Boolean(options.dryRun),
|
|
413
443
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
414
|
-
files:
|
|
444
|
+
files: [
|
|
445
|
+
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
446
|
+
...jobFiles(names, config),
|
|
447
|
+
],
|
|
415
448
|
index: featureArtifactIndexFile("job", names, config),
|
|
416
449
|
dependencies: {
|
|
417
450
|
"@beignet/core": beignetDependencyVersion,
|
|
418
451
|
},
|
|
419
452
|
});
|
|
420
453
|
}
|
|
454
|
+
export async function makeTask(options) {
|
|
455
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
456
|
+
const names = taskNames(options.name);
|
|
457
|
+
const config = options.config
|
|
458
|
+
? resolveConfig(options.config)
|
|
459
|
+
: await loadBeignetConfig(targetDir);
|
|
460
|
+
await assertFeatureArtifactApp(targetDir, config, "task");
|
|
461
|
+
const result = await makeFeatureArtifact({
|
|
462
|
+
targetDir,
|
|
463
|
+
config,
|
|
464
|
+
force: Boolean(options.force),
|
|
465
|
+
dryRun: Boolean(options.dryRun),
|
|
466
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
467
|
+
files: [
|
|
468
|
+
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
469
|
+
...taskFiles(names, config),
|
|
470
|
+
],
|
|
471
|
+
index: featureArtifactIndexFile("task", names, config),
|
|
472
|
+
dependencies: {
|
|
473
|
+
"@beignet/core": beignetDependencyVersion,
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
const registryResult = await updateTaskRegistry(targetDir, names, config, {
|
|
477
|
+
dryRun: Boolean(options.dryRun),
|
|
478
|
+
});
|
|
479
|
+
if (registryResult === "created")
|
|
480
|
+
result.createdFiles.push(config.paths.tasks);
|
|
481
|
+
if (registryResult === "updated")
|
|
482
|
+
result.updatedFiles.push(config.paths.tasks);
|
|
483
|
+
if (registryResult === "skipped")
|
|
484
|
+
result.skippedFiles.push(config.paths.tasks);
|
|
485
|
+
if (!result.files.includes(config.paths.tasks)) {
|
|
486
|
+
result.files.push(config.paths.tasks);
|
|
487
|
+
}
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
421
490
|
export async function makeFactory(options) {
|
|
422
491
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
423
492
|
const names = factoryNames(options.name);
|
|
@@ -471,7 +540,10 @@ export async function makeNotification(options) {
|
|
|
471
540
|
force: Boolean(options.force),
|
|
472
541
|
dryRun: Boolean(options.dryRun),
|
|
473
542
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
474
|
-
files:
|
|
543
|
+
files: [
|
|
544
|
+
...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
|
|
545
|
+
...notificationFiles(names, config),
|
|
546
|
+
],
|
|
475
547
|
index: featureArtifactIndexFile("notification", names, config),
|
|
476
548
|
dependencies: {
|
|
477
549
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -492,7 +564,10 @@ export async function makeListener(options) {
|
|
|
492
564
|
force: Boolean(options.force),
|
|
493
565
|
dryRun: Boolean(options.dryRun),
|
|
494
566
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
495
|
-
files:
|
|
567
|
+
files: [
|
|
568
|
+
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
569
|
+
...listenerFiles(names, config),
|
|
570
|
+
],
|
|
496
571
|
index: featureArtifactIndexFile("listener", names, config),
|
|
497
572
|
dependencies: {
|
|
498
573
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -520,7 +595,10 @@ export async function makeSchedule(options) {
|
|
|
520
595
|
await assertFeatureArtifactApp(targetDir, config, "schedule", {
|
|
521
596
|
requireServer: Boolean(options.route),
|
|
522
597
|
});
|
|
523
|
-
const files =
|
|
598
|
+
const files = [
|
|
599
|
+
...(await missingCapabilityBuilderFiles(targetDir, "schedules", config)),
|
|
600
|
+
...scheduleFiles(names, config, { route: Boolean(options.route) }),
|
|
601
|
+
];
|
|
524
602
|
return makeFeatureArtifact({
|
|
525
603
|
targetDir,
|
|
526
604
|
config,
|
|
@@ -554,6 +632,58 @@ export async function makeUpload(options) {
|
|
|
554
632
|
},
|
|
555
633
|
});
|
|
556
634
|
}
|
|
635
|
+
async function makeFeatureUi(options) {
|
|
636
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
637
|
+
const names = featureUiNames(options.name);
|
|
638
|
+
const config = options.config
|
|
639
|
+
? resolveConfig(options.config)
|
|
640
|
+
: await loadBeignetConfig(targetDir);
|
|
641
|
+
await assertFeatureUiApp(targetDir, config);
|
|
642
|
+
const componentFiles = featureUiComponentFiles(names, config);
|
|
643
|
+
const indexFile = featureUiIndexFile(names, config);
|
|
644
|
+
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
645
|
+
force: Boolean(options.force),
|
|
646
|
+
});
|
|
647
|
+
const createdFiles = plannedFiles
|
|
648
|
+
.filter((file) => file.result === "created")
|
|
649
|
+
.map((file) => file.path);
|
|
650
|
+
const updatedFiles = plannedFiles
|
|
651
|
+
.filter((file) => file.result === "updated")
|
|
652
|
+
.map((file) => file.path);
|
|
653
|
+
const skippedFiles = plannedFiles
|
|
654
|
+
.filter((file) => file.result === "skipped")
|
|
655
|
+
.map((file) => file.path);
|
|
656
|
+
if (!options.dryRun) {
|
|
657
|
+
for (const file of plannedFiles) {
|
|
658
|
+
await writePlannedGeneratedFile(targetDir, file);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const indexResult = await updateFeatureUiIndex(targetDir, indexFile, {
|
|
662
|
+
dryRun: Boolean(options.dryRun),
|
|
663
|
+
});
|
|
664
|
+
if (indexResult === "created")
|
|
665
|
+
createdFiles.push(indexFile.path);
|
|
666
|
+
if (indexResult === "updated")
|
|
667
|
+
updatedFiles.push(indexFile.path);
|
|
668
|
+
if (indexResult === "skipped")
|
|
669
|
+
skippedFiles.push(indexFile.path);
|
|
670
|
+
if (await updatePackageDependencies(targetDir, {
|
|
671
|
+
"@beignet/react-query": beignetDependencyVersion,
|
|
672
|
+
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
673
|
+
}, { dryRun: Boolean(options.dryRun) })) {
|
|
674
|
+
updatedFiles.push("package.json");
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
schemaVersion: 1,
|
|
678
|
+
name: names.pluralKebab,
|
|
679
|
+
targetDir,
|
|
680
|
+
dryRun: Boolean(options.dryRun),
|
|
681
|
+
files: [...componentFiles.map((file) => file.path), indexFile.path],
|
|
682
|
+
createdFiles,
|
|
683
|
+
updatedFiles,
|
|
684
|
+
skippedFiles,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
557
687
|
async function assertStandardApp(targetDir, config) {
|
|
558
688
|
const requiredFiles = [
|
|
559
689
|
config.paths.appContext,
|
|
@@ -646,6 +776,18 @@ async function assertFeatureArtifactApp(targetDir, config, artifact, options = {
|
|
|
646
776
|
}
|
|
647
777
|
}
|
|
648
778
|
}
|
|
779
|
+
async function assertFeatureUiApp(targetDir, config) {
|
|
780
|
+
const clientPath = clientIndexPath(config);
|
|
781
|
+
const requiredFiles = [config.paths.appContext, clientPath];
|
|
782
|
+
for (const file of requiredFiles) {
|
|
783
|
+
try {
|
|
784
|
+
await stat(path.join(targetDir, file));
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
throw new Error(`beignet make feature --with ui expects a standard Beignet app with React Query client helpers. Missing ${file}.`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
649
791
|
async function assertPersistenceArtifactApp(targetDir, names, config, artifact) {
|
|
650
792
|
const requiredFiles = [
|
|
651
793
|
config.paths.appContext,
|
|
@@ -712,6 +854,62 @@ async function readOptionalFile(filePath) {
|
|
|
712
854
|
return undefined;
|
|
713
855
|
}
|
|
714
856
|
}
|
|
857
|
+
const capabilityBuilders = {
|
|
858
|
+
listeners: {
|
|
859
|
+
factoryName: "createListeners",
|
|
860
|
+
defineName: "defineListener",
|
|
861
|
+
module: "@beignet/core/events",
|
|
862
|
+
},
|
|
863
|
+
jobs: {
|
|
864
|
+
factoryName: "createJobs",
|
|
865
|
+
defineName: "defineJob",
|
|
866
|
+
module: "@beignet/core/jobs",
|
|
867
|
+
},
|
|
868
|
+
schedules: {
|
|
869
|
+
factoryName: "createSchedules",
|
|
870
|
+
defineName: "defineSchedule",
|
|
871
|
+
module: "@beignet/core/schedules",
|
|
872
|
+
},
|
|
873
|
+
notifications: {
|
|
874
|
+
factoryName: "createNotifications",
|
|
875
|
+
defineName: "defineNotification",
|
|
876
|
+
module: "@beignet/core/notifications",
|
|
877
|
+
},
|
|
878
|
+
tasks: {
|
|
879
|
+
factoryName: "createTasks",
|
|
880
|
+
defineName: "defineTask",
|
|
881
|
+
module: "@beignet/core/tasks",
|
|
882
|
+
},
|
|
883
|
+
};
|
|
884
|
+
function capabilityBuilderPath(capability, config) {
|
|
885
|
+
if (capability === "listeners")
|
|
886
|
+
return config.paths.listenersBuilder;
|
|
887
|
+
if (capability === "jobs")
|
|
888
|
+
return config.paths.jobsBuilder;
|
|
889
|
+
if (capability === "schedules")
|
|
890
|
+
return config.paths.schedulesBuilder;
|
|
891
|
+
if (capability === "notifications")
|
|
892
|
+
return config.paths.notificationsBuilder;
|
|
893
|
+
return config.paths.tasksBuilder;
|
|
894
|
+
}
|
|
895
|
+
function capabilityBuilderFile(capability, config) {
|
|
896
|
+
const builder = capabilityBuilders[capability];
|
|
897
|
+
return {
|
|
898
|
+
path: capabilityBuilderPath(capability, config),
|
|
899
|
+
content: `import { ${builder.factoryName} } from "${builder.module}";
|
|
900
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
901
|
+
|
|
902
|
+
export const { ${builder.defineName} } = ${builder.factoryName}<AppContext>();
|
|
903
|
+
`,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
async function missingCapabilityBuilderFiles(targetDir, capability, config) {
|
|
907
|
+
const file = capabilityBuilderFile(capability, config);
|
|
908
|
+
if (await fileExists(path.join(targetDir, file.path))) {
|
|
909
|
+
return [];
|
|
910
|
+
}
|
|
911
|
+
return [file];
|
|
912
|
+
}
|
|
715
913
|
async function makeFeatureArtifact(options) {
|
|
716
914
|
const plannedFiles = await planGeneratedFiles(options.targetDir, options.files, {
|
|
717
915
|
force: options.force,
|
|
@@ -743,6 +941,7 @@ async function makeFeatureArtifact(options) {
|
|
|
743
941
|
updatedFiles.push("package.json");
|
|
744
942
|
}
|
|
745
943
|
return {
|
|
944
|
+
schemaVersion: 1,
|
|
746
945
|
name: options.name,
|
|
747
946
|
targetDir: options.targetDir,
|
|
748
947
|
dryRun: options.dryRun,
|
|
@@ -772,6 +971,9 @@ function beignetDependencyVersion(packageJson) {
|
|
|
772
971
|
packageJson.dependencies?.["@beignet/next"] ??
|
|
773
972
|
"*");
|
|
774
973
|
}
|
|
974
|
+
function tanstackReactQueryDependencyVersion(packageJson) {
|
|
975
|
+
return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
|
|
976
|
+
}
|
|
775
977
|
async function updateResourceWiring(targetDir, names, config, persistenceOrOptions, maybeOptions) {
|
|
776
978
|
const persistence = typeof persistenceOrOptions === "string"
|
|
777
979
|
? persistenceOrOptions
|
|
@@ -779,6 +981,14 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
779
981
|
const options = typeof persistenceOrOptions === "string"
|
|
780
982
|
? maybeOptions
|
|
781
983
|
: persistenceOrOptions;
|
|
984
|
+
const mode = typeof persistenceOrOptions === "string"
|
|
985
|
+
? (maybeOptions?.mode ?? "feature")
|
|
986
|
+
: (persistenceOrOptions.mode ?? "feature");
|
|
987
|
+
const generationOptions = typeof persistenceOrOptions === "string"
|
|
988
|
+
? (maybeOptions?.generationOptions ??
|
|
989
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode))
|
|
990
|
+
: (persistenceOrOptions.generationOptions ??
|
|
991
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode));
|
|
782
992
|
const updated = new Set();
|
|
783
993
|
if (await updatePackageJson(targetDir, {
|
|
784
994
|
...options,
|
|
@@ -786,13 +996,24 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
786
996
|
})) {
|
|
787
997
|
updated.add("package.json");
|
|
788
998
|
}
|
|
789
|
-
if (await updatePortsIndex(targetDir, names, config,
|
|
999
|
+
if (await updatePortsIndex(targetDir, names, config, {
|
|
1000
|
+
...options,
|
|
1001
|
+
generationOptions,
|
|
1002
|
+
})) {
|
|
790
1003
|
updated.add(config.paths.ports);
|
|
791
1004
|
}
|
|
792
1005
|
if (persistence === "memory" &&
|
|
793
1006
|
(await updateInfrastructurePorts(targetDir, names, config, options))) {
|
|
794
1007
|
updated.add(config.paths.infrastructurePorts);
|
|
795
1008
|
}
|
|
1009
|
+
if (generationOptions.auth &&
|
|
1010
|
+
(await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
|
|
1011
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1012
|
+
}
|
|
1013
|
+
if (persistence === "drizzle" &&
|
|
1014
|
+
(await updateInfrastructureDeferredPorts(targetDir, names, config, options))) {
|
|
1015
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1016
|
+
}
|
|
796
1017
|
if (persistence === "drizzle" &&
|
|
797
1018
|
(await updateDrizzleSchemaIndex(targetDir, names, config, options))) {
|
|
798
1019
|
updated.add(drizzleSchemaIndexPath(config));
|
|
@@ -805,7 +1026,10 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
805
1026
|
(await updateServerTransactionPorts(targetDir, names, config, options))) {
|
|
806
1027
|
updated.add(config.paths.server);
|
|
807
1028
|
}
|
|
808
|
-
if (await
|
|
1029
|
+
if (await updateSharedErrors(targetDir, names, config, { ...options, mode })) {
|
|
1030
|
+
updated.add(resourceSharedErrorsPath(config));
|
|
1031
|
+
}
|
|
1032
|
+
if (await updateOpenApiRoute(targetDir, names, config, { ...options, mode })) {
|
|
809
1033
|
updated.add(config.paths.openapiRoute);
|
|
810
1034
|
}
|
|
811
1035
|
const routesFile = await updateServerRoutes(targetDir, names, config, options);
|
|
@@ -840,6 +1064,16 @@ async function updatePortsIndex(targetDir, names, config, options) {
|
|
|
840
1064
|
if (!next.includes(importLine)) {
|
|
841
1065
|
next = insertAfterImports(next, importLine);
|
|
842
1066
|
}
|
|
1067
|
+
if (options.generationOptions?.auth) {
|
|
1068
|
+
const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
|
|
1069
|
+
const policyImportLine = `import type { AuthorizationContext as ${names.singularPascal}AuthorizationContext, ${names.singularCamel}Policy } from "${relativeModule(config.paths.ports, policyPath)}";`;
|
|
1070
|
+
if (!next.includes(policyImportLine)) {
|
|
1071
|
+
next = insertAfterImports(next, policyImportLine);
|
|
1072
|
+
}
|
|
1073
|
+
next = appendUnionTypeMember(next, "AppAuthorizationContext", `${names.singularPascal}AuthorizationContext`);
|
|
1074
|
+
next = appendTupleTypeMember(next, "AppGate", `typeof ${names.singularCamel}Policy`);
|
|
1075
|
+
next = appendGatePolicyMember(next, `typeof ${names.singularCamel}Policy`);
|
|
1076
|
+
}
|
|
843
1077
|
const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
|
|
844
1078
|
if (!next.includes(entry.trim())) {
|
|
845
1079
|
next = insertTypeProperty(next, "AppPorts", entry.trim(), `Could not find AppPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make resource.`);
|
|
@@ -934,7 +1168,7 @@ async function updateInfrastructurePorts(targetDir, names, config, options) {
|
|
|
934
1168
|
}
|
|
935
1169
|
const repositoryConst = `const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository();`;
|
|
936
1170
|
if (!next.includes(repositoryConst)) {
|
|
937
|
-
next = replaceRequired(next, /(\nexport const appPorts = definePorts(?:<[^>]+>)?\(\{)/, `\n${repositoryConst}\n$1`, `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`);
|
|
1171
|
+
next = replaceRequired(next, /(\nexport const appPorts = definePorts(?:<[^>]+>)?\((?:\)\()?\{)/, `\n${repositoryConst}\n$1`, `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`);
|
|
938
1172
|
}
|
|
939
1173
|
const entry = `\t${names.pluralCamel},`;
|
|
940
1174
|
const definePortsBody = definePortsBodySource(next);
|
|
@@ -954,6 +1188,22 @@ async function updateInfrastructurePorts(targetDir, names, config, options) {
|
|
|
954
1188
|
await writeFile(filePath, next);
|
|
955
1189
|
return true;
|
|
956
1190
|
}
|
|
1191
|
+
async function updateInfrastructureGatePolicies(targetDir, names, config, options) {
|
|
1192
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
1193
|
+
const original = await readFile(filePath, "utf8");
|
|
1194
|
+
let next = original;
|
|
1195
|
+
const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
|
|
1196
|
+
const importLine = `import { ${names.singularCamel}Policy } from "${relativeModule(config.paths.infrastructurePorts, policyPath)}";`;
|
|
1197
|
+
if (!next.includes(importLine)) {
|
|
1198
|
+
next = insertAfterImports(next, importLine);
|
|
1199
|
+
}
|
|
1200
|
+
next = appendCreateGatePolicy(next, `${names.singularCamel}Policy`, `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.`);
|
|
1201
|
+
if (next === original)
|
|
1202
|
+
return false;
|
|
1203
|
+
if (!options.dryRun)
|
|
1204
|
+
await writeFile(filePath, next);
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
957
1207
|
async function updateDrizzleSchemaIndex(targetDir, names, config, options) {
|
|
958
1208
|
const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
|
|
959
1209
|
if (!(await fileExists(filePath)))
|
|
@@ -992,6 +1242,19 @@ async function updateDrizzleRepositories(targetDir, names, config, options) {
|
|
|
992
1242
|
await writeFile(filePath, next);
|
|
993
1243
|
return true;
|
|
994
1244
|
}
|
|
1245
|
+
async function updateInfrastructureDeferredPorts(targetDir, names, config, options) {
|
|
1246
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
1247
|
+
const original = await readFile(filePath, "utf8");
|
|
1248
|
+
// Drizzle resources are provided by the app database provider at startup,
|
|
1249
|
+
// so deferred-form ports files defer the new repository key. Identity-form
|
|
1250
|
+
// ports files are left untouched.
|
|
1251
|
+
const next = appendDeferredPortKey(original, names.pluralCamel);
|
|
1252
|
+
if (next === original)
|
|
1253
|
+
return false;
|
|
1254
|
+
if (!options.dryRun)
|
|
1255
|
+
await writeFile(filePath, next);
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
995
1258
|
async function updateServerTransactionPorts(targetDir, names, config, options) {
|
|
996
1259
|
const filePath = path.join(targetDir, config.paths.server);
|
|
997
1260
|
const original = await readFile(filePath, "utf8");
|
|
@@ -1020,13 +1283,12 @@ async function updateOpenApiRoute(targetDir, names, config, options) {
|
|
|
1020
1283
|
const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
|
|
1021
1284
|
if (!firstArg?.text.startsWith("["))
|
|
1022
1285
|
return false;
|
|
1023
|
-
const
|
|
1024
|
-
const createName = `create${names.singularPascal}`;
|
|
1286
|
+
const contractNames = resourceContractNames(names, options.mode);
|
|
1025
1287
|
const listedContracts = identifiersFromArrayExpression(firstArg.text);
|
|
1026
|
-
const missingContracts =
|
|
1288
|
+
const missingContracts = contractNames.filter((contractName) => !listedContracts.has(contractName));
|
|
1027
1289
|
if (missingContracts.length === 0)
|
|
1028
1290
|
return false;
|
|
1029
|
-
const importLine = `import {
|
|
1291
|
+
const importLine = `import { ${contractNames.join(", ")} } from "${aliasModule(resourceContractFilePath(names, config))}";`;
|
|
1030
1292
|
const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
|
|
1031
1293
|
let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(firstArg.end)}`;
|
|
1032
1294
|
if (!next.includes(importLine)) {
|
|
@@ -1038,6 +1300,53 @@ async function updateOpenApiRoute(targetDir, names, config, options) {
|
|
|
1038
1300
|
await writeFile(filePath, next);
|
|
1039
1301
|
return true;
|
|
1040
1302
|
}
|
|
1303
|
+
async function updateSharedErrors(targetDir, names, config, options) {
|
|
1304
|
+
if (options.mode !== "resource")
|
|
1305
|
+
return false;
|
|
1306
|
+
const filePath = path.join(targetDir, resourceSharedErrorsPath(config));
|
|
1307
|
+
if (!(await fileExists(filePath)))
|
|
1308
|
+
return false;
|
|
1309
|
+
const original = await readFile(filePath, "utf8");
|
|
1310
|
+
const notFoundErrorName = `${names.singularPascal}NotFound`;
|
|
1311
|
+
const conflictErrorName = `${names.singularPascal}Conflict`;
|
|
1312
|
+
const missingErrorNames = [notFoundErrorName, conflictErrorName].filter((errorName) => !new RegExp(`\\b${errorName}\\b`).test(original));
|
|
1313
|
+
if (missingErrorNames.length === 0)
|
|
1314
|
+
return false;
|
|
1315
|
+
const expectedErrorNames = missingErrorNames.join(" and ");
|
|
1316
|
+
const defineErrorsStart = original.indexOf("defineErrors(");
|
|
1317
|
+
if (defineErrorsStart === -1) {
|
|
1318
|
+
throw new Error(`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
|
|
1319
|
+
}
|
|
1320
|
+
const openBrace = original.indexOf("{", defineErrorsStart);
|
|
1321
|
+
if (openBrace === -1) {
|
|
1322
|
+
throw new Error(`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
|
|
1323
|
+
}
|
|
1324
|
+
const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
|
|
1325
|
+
if (closeBrace === -1) {
|
|
1326
|
+
throw new Error(`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.`);
|
|
1327
|
+
}
|
|
1328
|
+
const entries = [];
|
|
1329
|
+
if (missingErrorNames.includes(notFoundErrorName)) {
|
|
1330
|
+
entries.push(`${notFoundErrorName}: {
|
|
1331
|
+
\t\tcode: "${constantCase(names.singularKebab)}_NOT_FOUND",
|
|
1332
|
+
\t\tstatus: 404,
|
|
1333
|
+
\t\tmessage: "${names.singularPascal} not found",
|
|
1334
|
+
\t},`);
|
|
1335
|
+
}
|
|
1336
|
+
if (missingErrorNames.includes(conflictErrorName)) {
|
|
1337
|
+
entries.push(`${conflictErrorName}: {
|
|
1338
|
+
\t\tcode: "${constantCase(names.singularKebab)}_CONFLICT",
|
|
1339
|
+
\t\tstatus: 409,
|
|
1340
|
+
\t\tmessage: "${names.singularPascal} was changed by another request",
|
|
1341
|
+
\t},`);
|
|
1342
|
+
}
|
|
1343
|
+
const next = insertBeforeClosingBrace(original, openBrace, closeBrace, entries.join("\n"));
|
|
1344
|
+
if (next === original)
|
|
1345
|
+
return false;
|
|
1346
|
+
if (!options.dryRun)
|
|
1347
|
+
await writeFile(filePath, next);
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1041
1350
|
async function updateServerRoutes(targetDir, names, config, options) {
|
|
1042
1351
|
const filePath = path.join(targetDir, config.paths.server);
|
|
1043
1352
|
const original = await readFile(filePath, "utf8");
|
|
@@ -1299,6 +1608,67 @@ function typeBodySource(source, typeName) {
|
|
|
1299
1608
|
return undefined;
|
|
1300
1609
|
return source.slice(openBrace + 1, closeBrace);
|
|
1301
1610
|
}
|
|
1611
|
+
function appendUnionTypeMember(source, typeName, member) {
|
|
1612
|
+
const match = new RegExp(`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`).exec(source);
|
|
1613
|
+
if (!match)
|
|
1614
|
+
return source;
|
|
1615
|
+
const declaration = match[0];
|
|
1616
|
+
if (new RegExp(`\\b${member}\\b`).test(declaration))
|
|
1617
|
+
return source;
|
|
1618
|
+
const nextDeclaration = declaration.replace(/;\s*$/, ` | ${member};`);
|
|
1619
|
+
return `${source.slice(0, match.index)}${nextDeclaration}${source.slice(match.index + declaration.length)}`;
|
|
1620
|
+
}
|
|
1621
|
+
function appendTupleTypeMember(source, typeName, member) {
|
|
1622
|
+
const match = new RegExp(`export\\s+type\\s+${typeName}\\s*=\\s*`).exec(source);
|
|
1623
|
+
if (!match)
|
|
1624
|
+
return source;
|
|
1625
|
+
const tupleStart = source.indexOf("[", match.index);
|
|
1626
|
+
if (tupleStart === -1)
|
|
1627
|
+
return source;
|
|
1628
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
1629
|
+
if (tupleEnd === -1)
|
|
1630
|
+
return source;
|
|
1631
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
1632
|
+
if (tuple.includes(member))
|
|
1633
|
+
return source;
|
|
1634
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
1635
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
|
|
1636
|
+
}
|
|
1637
|
+
function appendGatePolicyMember(source, member) {
|
|
1638
|
+
const gateMatch = /\bGatePort\s*</.exec(source);
|
|
1639
|
+
if (!gateMatch)
|
|
1640
|
+
return source;
|
|
1641
|
+
const tupleStart = source.indexOf("[", gateMatch.index);
|
|
1642
|
+
if (tupleStart === -1)
|
|
1643
|
+
return source;
|
|
1644
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
1645
|
+
if (tupleEnd === -1)
|
|
1646
|
+
return source;
|
|
1647
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
1648
|
+
if (tuple.includes(member))
|
|
1649
|
+
return source;
|
|
1650
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
1651
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
|
|
1652
|
+
}
|
|
1653
|
+
function appendCreateGatePolicy(source, policy, failureMessage) {
|
|
1654
|
+
const policyMatch = /\bpolicies\s*:\s*\[/.exec(source);
|
|
1655
|
+
if (!policyMatch) {
|
|
1656
|
+
throw new Error(failureMessage);
|
|
1657
|
+
}
|
|
1658
|
+
const arrayStart = source.indexOf("[", policyMatch.index);
|
|
1659
|
+
if (arrayStart === -1) {
|
|
1660
|
+
throw new Error(failureMessage);
|
|
1661
|
+
}
|
|
1662
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
1663
|
+
if (arrayEnd === -1) {
|
|
1664
|
+
throw new Error(failureMessage);
|
|
1665
|
+
}
|
|
1666
|
+
const policies = source.slice(arrayStart, arrayEnd + 1);
|
|
1667
|
+
if (policies.includes(policy))
|
|
1668
|
+
return source;
|
|
1669
|
+
const nextPolicies = appendToArrayExpression(policies, [policy]);
|
|
1670
|
+
return `${source.slice(0, arrayStart)}${nextPolicies}${source.slice(arrayEnd + 1)}`;
|
|
1671
|
+
}
|
|
1302
1672
|
function insertDefinePortsProperty(source, property, message) {
|
|
1303
1673
|
const body = definePortsBodyRange(source);
|
|
1304
1674
|
if (!body)
|
|
@@ -1348,7 +1718,15 @@ function definePortsBodySource(source) {
|
|
|
1348
1718
|
return source.slice(body.openBrace + 1, body.closeBrace);
|
|
1349
1719
|
}
|
|
1350
1720
|
function definePortsBodyRange(source) {
|
|
1351
|
-
const
|
|
1721
|
+
const outer = definePortsOuterBodyRange(source);
|
|
1722
|
+
if (!outer)
|
|
1723
|
+
return undefined;
|
|
1724
|
+
// The curried `definePorts<AppPorts>()({ bound, deferred })` form binds app
|
|
1725
|
+
// ports inside `bound`; generated bindings belong there.
|
|
1726
|
+
return (topLevelBoundObjectRange(source, outer.openBrace, outer.closeBrace) ?? outer);
|
|
1727
|
+
}
|
|
1728
|
+
function definePortsOuterBodyRange(source) {
|
|
1729
|
+
const match = /definePorts(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?\{/.exec(source);
|
|
1352
1730
|
if (!match)
|
|
1353
1731
|
return undefined;
|
|
1354
1732
|
const openBrace = source.indexOf("{", match.index);
|
|
@@ -1357,6 +1735,39 @@ function definePortsBodyRange(source) {
|
|
|
1357
1735
|
return undefined;
|
|
1358
1736
|
return { openBrace, closeBrace };
|
|
1359
1737
|
}
|
|
1738
|
+
function appendDeferredPortKey(source, portKey) {
|
|
1739
|
+
const outer = definePortsOuterBodyRange(source);
|
|
1740
|
+
if (!outer)
|
|
1741
|
+
return source;
|
|
1742
|
+
const body = source.slice(outer.openBrace + 1, outer.closeBrace);
|
|
1743
|
+
if (!hasTopLevelObjectProperty(body, "deferred"))
|
|
1744
|
+
return source;
|
|
1745
|
+
const deferredMatch = /\bdeferred\s*:\s*\[/.exec(body);
|
|
1746
|
+
if (!deferredMatch)
|
|
1747
|
+
return source;
|
|
1748
|
+
const arrayStart = source.indexOf("[", outer.openBrace + 1 + deferredMatch.index);
|
|
1749
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
1750
|
+
if (arrayEnd === -1)
|
|
1751
|
+
return source;
|
|
1752
|
+
const tuple = source.slice(arrayStart, arrayEnd + 1);
|
|
1753
|
+
if (new RegExp(`["']${portKey}["']`).test(tuple))
|
|
1754
|
+
return source;
|
|
1755
|
+
const nextTuple = appendToArrayExpression(tuple, [`"${portKey}"`]);
|
|
1756
|
+
return `${source.slice(0, arrayStart)}${nextTuple}${source.slice(arrayEnd + 1)}`;
|
|
1757
|
+
}
|
|
1758
|
+
function topLevelBoundObjectRange(source, openBrace, closeBrace) {
|
|
1759
|
+
const body = source.slice(openBrace + 1, closeBrace);
|
|
1760
|
+
if (!hasTopLevelObjectProperty(body, "bound"))
|
|
1761
|
+
return undefined;
|
|
1762
|
+
const boundMatch = /\bbound\s*:\s*\{/.exec(body);
|
|
1763
|
+
if (!boundMatch)
|
|
1764
|
+
return undefined;
|
|
1765
|
+
const boundOpen = source.indexOf("{", openBrace + 1 + boundMatch.index);
|
|
1766
|
+
const boundClose = matchingBraceIndex(source, boundOpen);
|
|
1767
|
+
if (boundClose === -1)
|
|
1768
|
+
return undefined;
|
|
1769
|
+
return { openBrace: boundOpen, closeBrace: boundClose };
|
|
1770
|
+
}
|
|
1360
1771
|
function hasTopLevelObjectProperty(source, propertyName) {
|
|
1361
1772
|
let depth = 0;
|
|
1362
1773
|
let inString;
|
|
@@ -1586,6 +1997,18 @@ function resourceNames(input) {
|
|
|
1586
1997
|
]);
|
|
1587
1998
|
return names;
|
|
1588
1999
|
}
|
|
2000
|
+
function featureUiNames(input) {
|
|
2001
|
+
const names = resourceNames(input);
|
|
2002
|
+
const resolvedNames = {
|
|
2003
|
+
...names,
|
|
2004
|
+
componentExportName: `${names.pluralPascal}Panel`,
|
|
2005
|
+
componentFileName: `${names.pluralKebab}-panel`,
|
|
2006
|
+
};
|
|
2007
|
+
assertGeneratedIdentifiers(input, "Feature UI name", [
|
|
2008
|
+
resolvedNames.componentExportName,
|
|
2009
|
+
]);
|
|
2010
|
+
return resolvedNames;
|
|
2011
|
+
}
|
|
1589
2012
|
function featureResourceNames(names) {
|
|
1590
2013
|
return resourceNames(names.feature.kebab);
|
|
1591
2014
|
}
|
|
@@ -1703,6 +2126,18 @@ function jobNames(input) {
|
|
|
1703
2126
|
payloadTypeName: `${jobExportName}Payload`,
|
|
1704
2127
|
};
|
|
1705
2128
|
}
|
|
2129
|
+
function taskNames(input) {
|
|
2130
|
+
const names = featureArtifactNames(input, "Task");
|
|
2131
|
+
const taskExportName = `${names.artifact.camel}Task`;
|
|
2132
|
+
assertGeneratedIdentifiers(input, "Task name", [taskExportName]);
|
|
2133
|
+
return {
|
|
2134
|
+
...names,
|
|
2135
|
+
taskName: names.stableName,
|
|
2136
|
+
taskExportName,
|
|
2137
|
+
inputSchemaName: `${names.artifact.pascal}TaskInputSchema`,
|
|
2138
|
+
inputTypeName: `${names.artifact.pascal}TaskInput`,
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
1706
2141
|
function factoryNames(input) {
|
|
1707
2142
|
const names = featureArtifactNames(input, "Factory");
|
|
1708
2143
|
const factoryExportName = `${names.artifact.camel}Factory`;
|
|
@@ -1901,59 +2336,119 @@ function pascalCase(value) {
|
|
|
1901
2336
|
const camel = camelCase(value);
|
|
1902
2337
|
return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
1903
2338
|
}
|
|
1904
|
-
function
|
|
2339
|
+
function constantCase(value) {
|
|
2340
|
+
return value.replaceAll("-", "_").toUpperCase();
|
|
2341
|
+
}
|
|
2342
|
+
function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
|
|
2343
|
+
auth: false,
|
|
2344
|
+
tenant: false,
|
|
2345
|
+
events: false,
|
|
2346
|
+
softDelete: false,
|
|
2347
|
+
}) {
|
|
1905
2348
|
const useCaseDir = resourceUseCaseDir(names, config);
|
|
1906
2349
|
const infraDir = infrastructureDir(config);
|
|
1907
2350
|
const featureDir = resourceFeatureDir(names, config);
|
|
1908
2351
|
const files = [
|
|
1909
2352
|
{
|
|
1910
2353
|
path: resourceContractFilePath(names, config),
|
|
1911
|
-
content: contractFile(names, config),
|
|
2354
|
+
content: contractFile(names, config, mode, options),
|
|
1912
2355
|
},
|
|
1913
2356
|
{
|
|
1914
|
-
path: path.join(
|
|
1915
|
-
content: schemasFile(names),
|
|
2357
|
+
path: path.join(featureDir, "schemas.ts"),
|
|
2358
|
+
content: schemasFile(names, mode, options),
|
|
1916
2359
|
},
|
|
1917
2360
|
{
|
|
1918
2361
|
path: path.join(useCaseDir, `list-${names.pluralKebab}.ts`),
|
|
1919
|
-
content: listUseCaseFile(names, config),
|
|
2362
|
+
content: listUseCaseFile(names, config, options),
|
|
1920
2363
|
},
|
|
1921
2364
|
{
|
|
1922
2365
|
path: path.join(useCaseDir, `create-${names.singularKebab}.ts`),
|
|
1923
|
-
content: createUseCaseFile(names, config),
|
|
2366
|
+
content: createUseCaseFile(names, config, options),
|
|
1924
2367
|
},
|
|
2368
|
+
...(mode === "resource"
|
|
2369
|
+
? [
|
|
2370
|
+
{
|
|
2371
|
+
path: path.join(useCaseDir, `get-${names.singularKebab}.ts`),
|
|
2372
|
+
content: getUseCaseFile(names, config, options),
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
path: path.join(useCaseDir, `update-${names.singularKebab}.ts`),
|
|
2376
|
+
content: updateUseCaseFile(names, config, options),
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
path: path.join(useCaseDir, `delete-${names.singularKebab}.ts`),
|
|
2380
|
+
content: deleteUseCaseFile(names, config, options),
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
path: path.join(featureDir, "policy.ts"),
|
|
2384
|
+
content: resourcePolicyFile(names, config, options),
|
|
2385
|
+
},
|
|
2386
|
+
]
|
|
2387
|
+
: []),
|
|
2388
|
+
...(mode === "resource" && options.events
|
|
2389
|
+
? [
|
|
2390
|
+
{
|
|
2391
|
+
path: path.join(featureDir, "domain/events/index.ts"),
|
|
2392
|
+
content: resourceEventsFile(names),
|
|
2393
|
+
},
|
|
2394
|
+
]
|
|
2395
|
+
: []),
|
|
2396
|
+
...(mode === "resource" && options.auth
|
|
2397
|
+
? [
|
|
2398
|
+
{
|
|
2399
|
+
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
2400
|
+
content: resourcePolicyTestFile(names, options),
|
|
2401
|
+
},
|
|
2402
|
+
]
|
|
2403
|
+
: []),
|
|
1925
2404
|
{
|
|
1926
2405
|
path: path.join(useCaseDir, "index.ts"),
|
|
1927
|
-
content: useCasesIndexFile(names),
|
|
2406
|
+
content: useCasesIndexFile(names, mode),
|
|
1928
2407
|
},
|
|
1929
2408
|
{
|
|
1930
2409
|
path: resourcePortFilePath(names, config),
|
|
1931
|
-
content: repositoryPortFile(names, config),
|
|
2410
|
+
content: repositoryPortFile(names, config, mode, options),
|
|
1932
2411
|
},
|
|
1933
2412
|
{
|
|
1934
2413
|
path: path.join(infraDir, names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`),
|
|
1935
|
-
content: inMemoryRepositoryFile(names, config),
|
|
2414
|
+
content: inMemoryRepositoryFile(names, config, mode, options),
|
|
1936
2415
|
},
|
|
1937
2416
|
{
|
|
1938
2417
|
path: path.join(featureDir, "routes.ts"),
|
|
1939
|
-
content: routeGroupFile(names, config),
|
|
2418
|
+
content: routeGroupFile(names, config, mode, options),
|
|
1940
2419
|
},
|
|
1941
2420
|
{
|
|
1942
2421
|
path: resourceTestFilePath(names, config),
|
|
1943
|
-
content: testFile(names, config),
|
|
2422
|
+
content: testFile(names, config, mode, options),
|
|
1944
2423
|
},
|
|
1945
2424
|
];
|
|
1946
2425
|
if (persistence === "drizzle") {
|
|
1947
2426
|
files.push({
|
|
1948
2427
|
path: drizzleResourceSchemaFilePath(names, config),
|
|
1949
|
-
content: drizzleSchemaFile(names),
|
|
2428
|
+
content: drizzleSchemaFile(names, options),
|
|
1950
2429
|
}, {
|
|
1951
2430
|
path: drizzleResourceRepositoryFilePath(names, config),
|
|
1952
|
-
content: drizzleRepositoryFile(names, config),
|
|
2431
|
+
content: drizzleRepositoryFile(names, config, mode, options),
|
|
1953
2432
|
});
|
|
1954
2433
|
}
|
|
1955
2434
|
return files;
|
|
1956
2435
|
}
|
|
2436
|
+
function featureUiComponentFiles(names, config) {
|
|
2437
|
+
const componentPath = featureUiComponentFilePath(names, config);
|
|
2438
|
+
return [
|
|
2439
|
+
{
|
|
2440
|
+
path: componentPath,
|
|
2441
|
+
content: featureUiComponentFile(names, config),
|
|
2442
|
+
},
|
|
2443
|
+
];
|
|
2444
|
+
}
|
|
2445
|
+
function featureUiIndexFile(names, config) {
|
|
2446
|
+
return {
|
|
2447
|
+
path: featureUiIndexFilePath(names, config),
|
|
2448
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";
|
|
2449
|
+
`,
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
1957
2452
|
function contractFiles(names, config) {
|
|
1958
2453
|
return [
|
|
1959
2454
|
{
|
|
@@ -2019,6 +2514,14 @@ function jobFiles(names, config) {
|
|
|
2019
2514
|
},
|
|
2020
2515
|
];
|
|
2021
2516
|
}
|
|
2517
|
+
function taskFiles(names, config) {
|
|
2518
|
+
return [
|
|
2519
|
+
{
|
|
2520
|
+
path: taskFilePath(names, config),
|
|
2521
|
+
content: taskFile(names, config),
|
|
2522
|
+
},
|
|
2523
|
+
];
|
|
2524
|
+
}
|
|
2022
2525
|
function notificationFiles(names, config) {
|
|
2023
2526
|
return [
|
|
2024
2527
|
{
|
|
@@ -2095,6 +2598,19 @@ function drizzleResourceSchemaFilePath(names, config) {
|
|
|
2095
2598
|
function drizzleResourceRepositoryFilePath(names, config) {
|
|
2096
2599
|
return path.join(infrastructureDir(config), names.pluralKebab, `drizzle-${names.singularKebab}-repository.ts`);
|
|
2097
2600
|
}
|
|
2601
|
+
function resourceSharedErrorsPath(config) {
|
|
2602
|
+
return path.join(config.paths.features, "shared", "errors.ts");
|
|
2603
|
+
}
|
|
2604
|
+
function resourceContractNames(names, mode) {
|
|
2605
|
+
const contractNames = [
|
|
2606
|
+
`list${names.pluralPascal}`,
|
|
2607
|
+
`create${names.singularPascal}`,
|
|
2608
|
+
];
|
|
2609
|
+
if (mode === "resource") {
|
|
2610
|
+
contractNames.push(`get${names.singularPascal}`, `update${names.singularPascal}`, `delete${names.singularPascal}`);
|
|
2611
|
+
}
|
|
2612
|
+
return contractNames;
|
|
2613
|
+
}
|
|
2098
2614
|
async function detectResourcePersistence(targetDir, config) {
|
|
2099
2615
|
return (await fileExists(path.join(targetDir, drizzleRepositoriesPath(config))))
|
|
2100
2616
|
? "drizzle"
|
|
@@ -2103,6 +2619,19 @@ async function detectResourcePersistence(targetDir, config) {
|
|
|
2103
2619
|
function resourceFeatureDir(names, config) {
|
|
2104
2620
|
return path.join(config.paths.features, names.pluralKebab);
|
|
2105
2621
|
}
|
|
2622
|
+
function featureUiDir(names, config) {
|
|
2623
|
+
return path.join(resourceFeatureDir(names, config), "components");
|
|
2624
|
+
}
|
|
2625
|
+
function featureUiComponentFilePath(names, config) {
|
|
2626
|
+
return path.join(featureUiDir(names, config), `${names.componentFileName}.tsx`);
|
|
2627
|
+
}
|
|
2628
|
+
function featureUiIndexFilePath(names, config) {
|
|
2629
|
+
return path.join(featureUiDir(names, config), "index.ts");
|
|
2630
|
+
}
|
|
2631
|
+
function clientIndexPath(config) {
|
|
2632
|
+
const appRoot = path.posix.dirname(config.paths.appContext);
|
|
2633
|
+
return path.join(appRoot === "." ? "" : appRoot, "client/index.ts");
|
|
2634
|
+
}
|
|
2106
2635
|
function usesFeatureOwnedContracts(config) {
|
|
2107
2636
|
return (directoryPath(config.paths.contracts) ===
|
|
2108
2637
|
directoryPath(config.paths.features));
|
|
@@ -2137,6 +2666,9 @@ function resourceUseCaseDir(names, config) {
|
|
|
2137
2666
|
function resourceUseCaseIndexPath(names, config) {
|
|
2138
2667
|
return path.join(resourceUseCaseDir(names, config), "index.ts");
|
|
2139
2668
|
}
|
|
2669
|
+
function resourceSchemaFilePath(names, config) {
|
|
2670
|
+
return path.join(resourceFeatureDir(names, config), "schemas.ts");
|
|
2671
|
+
}
|
|
2140
2672
|
function resourcePortFilePath(names, config) {
|
|
2141
2673
|
if (usesFeatureOwnedContracts(config)) {
|
|
2142
2674
|
return path.join(resourceFeatureDir(names, config), "ports.ts");
|
|
@@ -2177,6 +2709,9 @@ function eventFilePath(names, config) {
|
|
|
2177
2709
|
function jobFilePath(names, config) {
|
|
2178
2710
|
return path.join(featureArtifactDir(names, "jobs", config), `${names.artifact.kebab}.ts`);
|
|
2179
2711
|
}
|
|
2712
|
+
function taskFilePath(names, config) {
|
|
2713
|
+
return path.join(featureArtifactDir(names, "tasks", config), `${names.artifact.kebab}.ts`);
|
|
2714
|
+
}
|
|
2180
2715
|
function factoryFilePath(names, config) {
|
|
2181
2716
|
return path.join(featureArtifactDir(names, "factories", config), `${names.artifact.kebab}.ts`);
|
|
2182
2717
|
}
|
|
@@ -2226,52 +2761,76 @@ async function updateUseCaseIndex(targetDir, file, options) {
|
|
|
2226
2761
|
await writeFile(destination, next);
|
|
2227
2762
|
return "updated";
|
|
2228
2763
|
}
|
|
2764
|
+
async function updateFeatureUiIndex(targetDir, file, options) {
|
|
2765
|
+
const destination = path.join(targetDir, file.path);
|
|
2766
|
+
const existing = await readOptionalFile(destination);
|
|
2767
|
+
if (existing === undefined) {
|
|
2768
|
+
if (!options.dryRun) {
|
|
2769
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
2770
|
+
await writeFile(destination, file.content);
|
|
2771
|
+
}
|
|
2772
|
+
return "created";
|
|
2773
|
+
}
|
|
2774
|
+
if (existing.includes(file.content.trim())) {
|
|
2775
|
+
return "skipped";
|
|
2776
|
+
}
|
|
2777
|
+
const next = `${existing.trimEnd()}\n${file.content}`;
|
|
2778
|
+
if (!options.dryRun)
|
|
2779
|
+
await writeFile(destination, next);
|
|
2780
|
+
return "updated";
|
|
2781
|
+
}
|
|
2229
2782
|
function featureArtifactIndexFile(kind, names, config) {
|
|
2230
2783
|
const artifactPath = kind === "event"
|
|
2231
2784
|
? eventFilePath(names, config)
|
|
2232
2785
|
: kind === "job"
|
|
2233
2786
|
? jobFilePath(names, config)
|
|
2234
|
-
: kind === "
|
|
2235
|
-
?
|
|
2236
|
-
: kind === "
|
|
2237
|
-
?
|
|
2238
|
-
: kind === "
|
|
2239
|
-
?
|
|
2240
|
-
: kind === "
|
|
2241
|
-
?
|
|
2242
|
-
: kind === "
|
|
2243
|
-
?
|
|
2244
|
-
:
|
|
2787
|
+
: kind === "task"
|
|
2788
|
+
? taskFilePath(names, config)
|
|
2789
|
+
: kind === "factory"
|
|
2790
|
+
? factoryFilePath(names, config)
|
|
2791
|
+
: kind === "seed"
|
|
2792
|
+
? seedFilePath(names, config)
|
|
2793
|
+
: kind === "notification"
|
|
2794
|
+
? notificationFilePath(names, config)
|
|
2795
|
+
: kind === "listener"
|
|
2796
|
+
? listenerFilePath(names, config)
|
|
2797
|
+
: kind === "schedule"
|
|
2798
|
+
? scheduleFilePath(names, config)
|
|
2799
|
+
: uploadFilePath(names, config);
|
|
2245
2800
|
const registrySuffix = kind === "event"
|
|
2246
2801
|
? "Events"
|
|
2247
2802
|
: kind === "job"
|
|
2248
2803
|
? "Jobs"
|
|
2249
|
-
: kind === "
|
|
2250
|
-
? "
|
|
2251
|
-
: kind === "
|
|
2252
|
-
? "
|
|
2253
|
-
: kind === "
|
|
2254
|
-
? "
|
|
2255
|
-
: kind === "
|
|
2256
|
-
? "
|
|
2257
|
-
: kind === "
|
|
2258
|
-
? "
|
|
2259
|
-
: "
|
|
2804
|
+
: kind === "task"
|
|
2805
|
+
? "Tasks"
|
|
2806
|
+
: kind === "factory"
|
|
2807
|
+
? "Factories"
|
|
2808
|
+
: kind === "seed"
|
|
2809
|
+
? "Seeds"
|
|
2810
|
+
: kind === "notification"
|
|
2811
|
+
? "Notifications"
|
|
2812
|
+
: kind === "listener"
|
|
2813
|
+
? "Listeners"
|
|
2814
|
+
: kind === "schedule"
|
|
2815
|
+
? "Schedules"
|
|
2816
|
+
: "Uploads";
|
|
2260
2817
|
const importName = kind === "event"
|
|
2261
2818
|
? names.eventExportName
|
|
2262
2819
|
: kind === "job"
|
|
2263
2820
|
? names.jobExportName
|
|
2264
|
-
: kind === "
|
|
2265
|
-
? names.
|
|
2266
|
-
: kind === "
|
|
2267
|
-
? names.
|
|
2268
|
-
: kind === "
|
|
2269
|
-
? names.
|
|
2270
|
-
: kind === "
|
|
2271
|
-
? names.
|
|
2272
|
-
: kind === "
|
|
2273
|
-
? names.
|
|
2274
|
-
:
|
|
2821
|
+
: kind === "task"
|
|
2822
|
+
? names.taskExportName
|
|
2823
|
+
: kind === "factory"
|
|
2824
|
+
? names.factoryExportName
|
|
2825
|
+
: kind === "seed"
|
|
2826
|
+
? names.seedExportName
|
|
2827
|
+
: kind === "notification"
|
|
2828
|
+
? names.notificationExportName
|
|
2829
|
+
: kind === "listener"
|
|
2830
|
+
? names.listenerExportName
|
|
2831
|
+
: kind === "schedule"
|
|
2832
|
+
? names.scheduleExportName
|
|
2833
|
+
: names.uploadExportName;
|
|
2275
2834
|
return {
|
|
2276
2835
|
path: path.join(path.dirname(artifactPath), "index.ts"),
|
|
2277
2836
|
kind,
|
|
@@ -2365,24 +2924,83 @@ export const ${file.registryName} = [${file.importName}] as const;
|
|
|
2365
2924
|
await writeFile(destination, next);
|
|
2366
2925
|
return "updated";
|
|
2367
2926
|
}
|
|
2368
|
-
function
|
|
2369
|
-
const
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
const
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2927
|
+
async function updateTaskRegistry(targetDir, names, config, options) {
|
|
2928
|
+
const destination = path.join(targetDir, config.paths.tasks);
|
|
2929
|
+
const existing = await readOptionalFile(destination);
|
|
2930
|
+
const featureTasksPath = path.join(featureArtifactDir(names, "tasks", config), "index.ts");
|
|
2931
|
+
const registryName = `${names.featureSingularCamel}Tasks`;
|
|
2932
|
+
const importLine = `import { ${registryName} } from "${relativeModule(config.paths.tasks, featureTasksPath)}";`;
|
|
2933
|
+
const defineTasksImport = `import { defineTasks } from "@beignet/core/tasks";`;
|
|
2934
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
2935
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
2936
|
+
const serverImport = `import { server } from "${relativeModule(config.paths.tasks, config.paths.server)}";`;
|
|
2937
|
+
const registryEntry = `...${registryName}`;
|
|
2938
|
+
if (existing === undefined) {
|
|
2939
|
+
const content = `${defineTasksImport}
|
|
2940
|
+
${serviceActorImport}
|
|
2941
|
+
${appContextImport}
|
|
2942
|
+
${importLine}
|
|
2943
|
+
${serverImport}
|
|
2944
|
+
|
|
2945
|
+
export const tasks = defineTasks([
|
|
2946
|
+
\t${registryEntry},
|
|
2947
|
+
] as const);
|
|
2948
|
+
|
|
2949
|
+
export async function createTaskContext(): Promise<AppContext> {
|
|
2950
|
+
\treturn server.createServiceContext({
|
|
2951
|
+
\t\tactor: createServiceActor("beignet-cli"),
|
|
2952
|
+
\t});
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
export async function stopTaskContext(): Promise<void> {
|
|
2956
|
+
\tawait server.stop();
|
|
2957
|
+
}
|
|
2958
|
+
`;
|
|
2959
|
+
if (!options.dryRun) {
|
|
2960
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
2961
|
+
await writeFile(destination, content);
|
|
2962
|
+
}
|
|
2963
|
+
return "created";
|
|
2964
|
+
}
|
|
2965
|
+
let next = existing;
|
|
2966
|
+
if (!next.includes(defineTasksImport)) {
|
|
2967
|
+
next = insertAfterImports(next, defineTasksImport);
|
|
2968
|
+
}
|
|
2969
|
+
if (!next.includes(importLine)) {
|
|
2970
|
+
next = insertAfterImports(next, importLine);
|
|
2971
|
+
}
|
|
2972
|
+
const registry = arrayInitializerInfo(next, "tasks");
|
|
2973
|
+
if (!registry) {
|
|
2974
|
+
next = `${next.trimEnd()}\n\nexport const tasks = defineTasks([\n\t${registryEntry},\n] as const);\n`;
|
|
2975
|
+
}
|
|
2976
|
+
else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
2977
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
2978
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
|
|
2979
|
+
}
|
|
2980
|
+
if (next === existing)
|
|
2981
|
+
return "skipped";
|
|
2982
|
+
if (!options.dryRun)
|
|
2983
|
+
await writeFile(destination, next);
|
|
2984
|
+
return "updated";
|
|
2985
|
+
}
|
|
2986
|
+
function arrayInitializerInfo(source, constName) {
|
|
2987
|
+
const match = new RegExp(`\\bconst\\s+${constName}\\s*=`).exec(source);
|
|
2988
|
+
if (!match)
|
|
2989
|
+
return undefined;
|
|
2990
|
+
const openBracket = source.indexOf("[", match.index);
|
|
2991
|
+
if (openBracket === -1)
|
|
2992
|
+
return undefined;
|
|
2993
|
+
const closeBracket = matchingDelimiterIndex(source, openBracket, "[", "]");
|
|
2994
|
+
if (closeBracket === -1)
|
|
2995
|
+
return undefined;
|
|
2996
|
+
return {
|
|
2997
|
+
text: source.slice(openBracket, closeBracket + 1),
|
|
2998
|
+
start: openBracket,
|
|
2999
|
+
end: closeBracket + 1,
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
function defineUploadsInitializerInfo(source, constName) {
|
|
3003
|
+
const match = new RegExp(`\\bconst\\s+${constName}\\s*=\\s*defineUploads\\s*\\(`).exec(source);
|
|
2386
3004
|
if (!match)
|
|
2387
3005
|
return undefined;
|
|
2388
3006
|
const openBrace = source.indexOf("{", match.index);
|
|
@@ -2397,27 +3015,106 @@ function defineUploadsInitializerInfo(source, constName) {
|
|
|
2397
3015
|
end: closeBrace,
|
|
2398
3016
|
};
|
|
2399
3017
|
}
|
|
2400
|
-
function schemasFile(names) {
|
|
3018
|
+
function schemasFile(names, mode, options) {
|
|
2401
3019
|
return `import { z } from "zod";
|
|
2402
3020
|
|
|
2403
3021
|
export const ${names.singularPascal}Schema = z.object({
|
|
2404
3022
|
id: z.string().uuid(),
|
|
2405
|
-
name: z.string().min(1),
|
|
3023
|
+
${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
3024
|
+
version: z.number().int().min(1),
|
|
2406
3025
|
createdAt: z.string().datetime(),
|
|
3026
|
+
updatedAt: z.string().datetime(),
|
|
2407
3027
|
});
|
|
2408
3028
|
|
|
2409
|
-
export const
|
|
2410
|
-
|
|
2411
|
-
|
|
3029
|
+
export const ${names.singularPascal}SortBySchema = z.enum(["createdAt", "name"]);
|
|
3030
|
+
export const ${names.singularPascal}SortDirectionSchema = z.enum(["asc", "desc"]);
|
|
3031
|
+
|
|
3032
|
+
const ${names.singularPascal}CursorPayloadSchema = z.object({
|
|
3033
|
+
sortBy: ${names.singularPascal}SortBySchema,
|
|
3034
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema,
|
|
3035
|
+
sortValue: z.string(),
|
|
3036
|
+
id: z.string().uuid(),
|
|
2412
3037
|
});
|
|
2413
3038
|
|
|
3039
|
+
export type ${names.singularPascal}SortBy = z.infer<
|
|
3040
|
+
typeof ${names.singularPascal}SortBySchema
|
|
3041
|
+
>;
|
|
3042
|
+
export type ${names.singularPascal}SortDirection = z.infer<
|
|
3043
|
+
typeof ${names.singularPascal}SortDirectionSchema
|
|
3044
|
+
>;
|
|
3045
|
+
export type ${names.singularPascal}Cursor = z.infer<
|
|
3046
|
+
typeof ${names.singularPascal}CursorPayloadSchema
|
|
3047
|
+
>;
|
|
3048
|
+
|
|
3049
|
+
function encodeBase64Url(value: string): string {
|
|
3050
|
+
const bytes = new TextEncoder().encode(value);
|
|
3051
|
+
let binary = "";
|
|
3052
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3053
|
+
|
|
3054
|
+
return btoa(binary)
|
|
3055
|
+
.replaceAll("+", "-")
|
|
3056
|
+
.replaceAll("/", "_")
|
|
3057
|
+
.replaceAll("=", "");
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function decodeBase64Url(value: string): string {
|
|
3061
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
3062
|
+
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
|
3063
|
+
const binary = atob(base64 + padding);
|
|
3064
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
3065
|
+
|
|
3066
|
+
return new TextDecoder().decode(bytes);
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
export function encode${names.singularPascal}Cursor(cursor: ${names.singularPascal}Cursor): string {
|
|
3070
|
+
return encodeBase64Url(JSON.stringify(cursor));
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
export function decode${names.singularPascal}Cursor(cursor: string): ${names.singularPascal}Cursor {
|
|
3074
|
+
return ${names.singularPascal}CursorPayloadSchema.parse(
|
|
3075
|
+
JSON.parse(decodeBase64Url(cursor)),
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
export const List${names.pluralPascal}InputSchema = z
|
|
3080
|
+
.object({
|
|
3081
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
3082
|
+
cursor: z.string().nullable().default(null),
|
|
3083
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
3084
|
+
sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
|
|
3085
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
|
|
3086
|
+
})
|
|
3087
|
+
.superRefine((input, ctx) => {
|
|
3088
|
+
if (!input.cursor) return;
|
|
3089
|
+
|
|
3090
|
+
try {
|
|
3091
|
+
const cursor = decode${names.singularPascal}Cursor(input.cursor);
|
|
3092
|
+
if (
|
|
3093
|
+
cursor.sortBy !== input.sortBy ||
|
|
3094
|
+
cursor.sortDirection !== input.sortDirection
|
|
3095
|
+
) {
|
|
3096
|
+
ctx.addIssue({
|
|
3097
|
+
code: z.ZodIssueCode.custom,
|
|
3098
|
+
message: "cursor does not match the requested sort.",
|
|
3099
|
+
path: ["cursor"],
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
} catch {
|
|
3103
|
+
ctx.addIssue({
|
|
3104
|
+
code: z.ZodIssueCode.custom,
|
|
3105
|
+
message: "cursor is invalid.",
|
|
3106
|
+
path: ["cursor"],
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
3110
|
+
|
|
2414
3111
|
export const List${names.pluralPascal}OutputSchema = z.object({
|
|
2415
3112
|
items: z.array(${names.singularPascal}Schema),
|
|
2416
3113
|
page: z.object({
|
|
2417
|
-
kind: z.literal("
|
|
3114
|
+
kind: z.literal("cursor"),
|
|
2418
3115
|
limit: z.number().int().min(1),
|
|
2419
|
-
|
|
2420
|
-
|
|
3116
|
+
cursor: z.string().nullable(),
|
|
3117
|
+
nextCursor: z.string().nullable(),
|
|
2421
3118
|
hasMore: z.boolean(),
|
|
2422
3119
|
}),
|
|
2423
3120
|
});
|
|
@@ -2425,147 +3122,434 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
2425
3122
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
2426
3123
|
name: z.string().min(1).max(120),
|
|
2427
3124
|
});
|
|
3125
|
+
${mode === "resource"
|
|
3126
|
+
? `
|
|
3127
|
+
export const ${names.singularPascal}IdInputSchema = z.object({
|
|
3128
|
+
id: z.string().uuid(),
|
|
3129
|
+
});
|
|
2428
3130
|
|
|
3131
|
+
export const Update${names.singularPascal}BodySchema = z.object({
|
|
3132
|
+
name: z.string().min(1).max(120),
|
|
3133
|
+
version: z.number().int().min(1),
|
|
3134
|
+
});
|
|
3135
|
+
|
|
3136
|
+
export const Update${names.singularPascal}InputSchema =
|
|
3137
|
+
${names.singularPascal}IdInputSchema.merge(Update${names.singularPascal}BodySchema);
|
|
3138
|
+
`
|
|
3139
|
+
: ""}
|
|
2429
3140
|
export type ${names.singularPascal} = z.infer<typeof ${names.singularPascal}Schema>;
|
|
2430
3141
|
export type Create${names.singularPascal}Input = z.infer<
|
|
2431
3142
|
typeof Create${names.singularPascal}InputSchema
|
|
2432
3143
|
>;
|
|
3144
|
+
${mode === "resource"
|
|
3145
|
+
? `
|
|
3146
|
+
export type ${names.singularPascal}IdInput = z.infer<
|
|
3147
|
+
typeof ${names.singularPascal}IdInputSchema
|
|
3148
|
+
>;
|
|
3149
|
+
export type Update${names.singularPascal}Body = z.infer<
|
|
3150
|
+
typeof Update${names.singularPascal}BodySchema
|
|
3151
|
+
>;
|
|
3152
|
+
export type Update${names.singularPascal}Input = z.infer<
|
|
3153
|
+
typeof Update${names.singularPascal}InputSchema
|
|
3154
|
+
>;
|
|
3155
|
+
`
|
|
3156
|
+
: ""}
|
|
2433
3157
|
export type List${names.pluralPascal}Input = z.infer<
|
|
2434
3158
|
typeof List${names.pluralPascal}InputSchema
|
|
2435
3159
|
>;
|
|
2436
3160
|
`;
|
|
2437
3161
|
}
|
|
2438
|
-
function listUseCaseFile(names, config) {
|
|
3162
|
+
function listUseCaseFile(names, config, options) {
|
|
2439
3163
|
const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
|
|
2440
|
-
return `import {
|
|
3164
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3165
|
+
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
2441
3166
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2442
3167
|
import {
|
|
3168
|
+
decode${names.singularPascal}Cursor,
|
|
2443
3169
|
List${names.pluralPascal}InputSchema,
|
|
2444
3170
|
List${names.pluralPascal}OutputSchema,
|
|
2445
|
-
} from "
|
|
3171
|
+
} from "../schemas";
|
|
2446
3172
|
|
|
2447
3173
|
export const list${names.pluralPascal}UseCase = useCase
|
|
2448
3174
|
.query("${names.pluralCamel}.list")
|
|
2449
3175
|
.input(List${names.pluralPascal}InputSchema)
|
|
2450
3176
|
.output(List${names.pluralPascal}OutputSchema)
|
|
2451
3177
|
.run(async ({ ctx, input }) => {
|
|
2452
|
-
const page =
|
|
3178
|
+
${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, {
|
|
2453
3179
|
defaultLimit: 20,
|
|
2454
3180
|
maxLimit: 100,
|
|
2455
3181
|
});
|
|
2456
3182
|
|
|
2457
|
-
return ctx.ports.${names.pluralCamel}.list(
|
|
3183
|
+
return ctx.ports.${names.pluralCamel}.list({
|
|
3184
|
+
page,
|
|
3185
|
+
cursor: page.cursor ? decode${names.singularPascal}Cursor(page.cursor) : null,
|
|
3186
|
+
name: input.name,
|
|
3187
|
+
sortBy: input.sortBy,
|
|
3188
|
+
sortDirection: input.sortDirection,
|
|
3189
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
2458
3190
|
});
|
|
2459
3191
|
`;
|
|
2460
3192
|
}
|
|
2461
|
-
function createUseCaseFile(names, config) {
|
|
3193
|
+
function createUseCaseFile(names, config, options) {
|
|
2462
3194
|
const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
|
|
2463
|
-
return `import {
|
|
3195
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3196
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2464
3197
|
import {
|
|
2465
3198
|
Create${names.singularPascal}InputSchema,
|
|
2466
3199
|
${names.singularPascal}Schema,
|
|
2467
|
-
} from "
|
|
3200
|
+
} from "../schemas";
|
|
3201
|
+
${options.events ? `import { ${names.singularPascal}Created } from "../domain/events";\n` : ""}
|
|
2468
3202
|
|
|
2469
3203
|
export const create${names.singularPascal}UseCase = useCase
|
|
2470
3204
|
.command("${names.pluralCamel}.create")
|
|
2471
3205
|
.input(Create${names.singularPascal}InputSchema)
|
|
2472
3206
|
.output(${names.singularPascal}Schema)
|
|
2473
|
-
.run(async ({ ctx, input }) =>
|
|
3207
|
+
.run(async ({ ctx, input }) => {
|
|
3208
|
+
${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({
|
|
3209
|
+
...input,
|
|
3210
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3211
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3212
|
+
return ${names.singularCamel};
|
|
3213
|
+
});
|
|
2474
3214
|
`;
|
|
2475
3215
|
}
|
|
2476
|
-
function
|
|
3216
|
+
function getUseCaseFile(names, config, options) {
|
|
3217
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
|
|
3218
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3219
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3220
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3221
|
+
import {
|
|
3222
|
+
${names.singularPascal}IdInputSchema,
|
|
3223
|
+
${names.singularPascal}Schema,
|
|
3224
|
+
} from "../schemas";
|
|
3225
|
+
|
|
3226
|
+
export const get${names.singularPascal}UseCase = useCase
|
|
3227
|
+
.query("${names.pluralCamel}.get")
|
|
3228
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
3229
|
+
.output(${names.singularPascal}Schema)
|
|
3230
|
+
.run(async ({ ctx, input }) => {
|
|
3231
|
+
${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 }" : ""});
|
|
3232
|
+
if (!${names.singularCamel}) {
|
|
3233
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3234
|
+
details: { id: input.id },
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
return ${names.singularCamel};
|
|
3239
|
+
});
|
|
3240
|
+
`;
|
|
3241
|
+
}
|
|
3242
|
+
function updateUseCaseFile(names, config, options) {
|
|
3243
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
|
|
3244
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3245
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3246
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3247
|
+
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
3248
|
+
Update${names.singularPascal}InputSchema,
|
|
3249
|
+
${names.singularPascal}Schema,
|
|
3250
|
+
} from "../schemas";
|
|
3251
|
+
|
|
3252
|
+
export const update${names.singularPascal}UseCase = useCase
|
|
3253
|
+
.command("${names.pluralCamel}.update")
|
|
3254
|
+
.input(Update${names.singularPascal}InputSchema)
|
|
3255
|
+
.output(${names.singularPascal}Schema)
|
|
3256
|
+
.run(async ({ ctx, input }) => {
|
|
3257
|
+
${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 }" : ""});
|
|
3258
|
+
if (!existing) {
|
|
3259
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3260
|
+
details: { id: input.id },
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3263
|
+
${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
3264
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
3265
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
3270
|
+
...input,
|
|
3271
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3272
|
+
if (!${names.singularCamel}) {
|
|
3273
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
3274
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Updated, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3278
|
+
return ${names.singularCamel};
|
|
3279
|
+
});
|
|
3280
|
+
`;
|
|
3281
|
+
}
|
|
3282
|
+
function deleteUseCaseFile(names, config, options) {
|
|
3283
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
|
|
3284
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3285
|
+
import { z } from "zod";
|
|
3286
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3287
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3288
|
+
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
3289
|
+
|
|
3290
|
+
export const delete${names.singularPascal}UseCase = useCase
|
|
3291
|
+
.command("${names.pluralCamel}.delete")
|
|
3292
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
3293
|
+
.output(z.void())
|
|
3294
|
+
.run(async ({ ctx, input }) => {
|
|
3295
|
+
${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 }" : ""});
|
|
3296
|
+
if (!deleted) {
|
|
3297
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3298
|
+
details: { id: input.id },
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Deleted, {\n\t\t\tid: input.id,\n\t\t});\n` : ""} });
|
|
3302
|
+
`;
|
|
3303
|
+
}
|
|
3304
|
+
function useCasesIndexFile(names, mode) {
|
|
2477
3305
|
return `export { create${names.singularPascal}UseCase } from "./create-${names.singularKebab}";
|
|
2478
|
-
export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
2479
|
-
export {
|
|
3306
|
+
${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}";
|
|
3307
|
+
${mode === "resource" ? `export { update${names.singularPascal}UseCase } from "./update-${names.singularKebab}";\n` : ""}export {
|
|
2480
3308
|
Create${names.singularPascal}InputSchema,
|
|
2481
|
-
List${names.pluralPascal}InputSchema,
|
|
3309
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n` : ""} List${names.pluralPascal}InputSchema,
|
|
2482
3310
|
List${names.pluralPascal}OutputSchema,
|
|
2483
|
-
${names.singularPascal}Schema,
|
|
3311
|
+
${mode === "resource" ? ` Update${names.singularPascal}BodySchema,\n Update${names.singularPascal}InputSchema,\n` : ""} ${names.singularPascal}Schema,
|
|
2484
3312
|
type Create${names.singularPascal}Input,
|
|
2485
|
-
type List${names.pluralPascal}Input,
|
|
3313
|
+
${mode === "resource" ? ` type ${names.singularPascal}IdInput,\n type Update${names.singularPascal}Body,\n type Update${names.singularPascal}Input,\n` : ""} type List${names.pluralPascal}Input,
|
|
2486
3314
|
type ${names.singularPascal},
|
|
2487
|
-
} from "
|
|
3315
|
+
} from "../schemas";
|
|
2488
3316
|
`;
|
|
2489
3317
|
}
|
|
2490
|
-
function repositoryPortFile(names, config) {
|
|
3318
|
+
function repositoryPortFile(names, config, mode, options) {
|
|
2491
3319
|
return `import type {
|
|
2492
|
-
|
|
2493
|
-
|
|
3320
|
+
CursorPage,
|
|
3321
|
+
CursorPageInfo,
|
|
2494
3322
|
PageResult,
|
|
3323
|
+
SortDirection,
|
|
2495
3324
|
} from "@beignet/core/pagination";
|
|
2496
3325
|
import type {
|
|
2497
3326
|
Create${names.singularPascal}Input,
|
|
2498
|
-
${names.singularPascal},
|
|
2499
|
-
|
|
3327
|
+
${names.singularPascal}Cursor,
|
|
3328
|
+
${names.singularPascal}SortBy,
|
|
3329
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3330
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2500
3331
|
|
|
2501
|
-
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal},
|
|
3332
|
+
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal}, CursorPageInfo>;
|
|
3333
|
+
export type List${names.pluralPascal}Query = {
|
|
3334
|
+
page: CursorPage;
|
|
3335
|
+
cursor: ${names.singularPascal}Cursor | null;
|
|
3336
|
+
name?: string;
|
|
3337
|
+
sortBy: ${names.singularPascal}SortBy;
|
|
3338
|
+
sortDirection: SortDirection;
|
|
3339
|
+
${options.tenant ? `\ttenantId: string;\n` : ""}};
|
|
3340
|
+
${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` : ""}` : ""}
|
|
2502
3341
|
|
|
2503
3342
|
export interface ${names.singularPascal}Repository {
|
|
2504
|
-
list(
|
|
2505
|
-
create(input: Create${names.singularPascal}Input): Promise<${names.singularPascal}>;
|
|
2506
|
-
}
|
|
3343
|
+
list(query: List${names.pluralPascal}Query): Promise<List${names.pluralPascal}Result>;
|
|
3344
|
+
create(input: ${options.tenant ? `Create${names.singularPascal}RepositoryInput` : `Create${names.singularPascal}Input`}): Promise<${names.singularPascal}>;
|
|
3345
|
+
${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` : ""}}
|
|
2507
3346
|
`;
|
|
2508
3347
|
}
|
|
2509
|
-
function inMemoryRepositoryFile(names, config) {
|
|
3348
|
+
function inMemoryRepositoryFile(names, config, mode, options) {
|
|
2510
3349
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
2511
|
-
|
|
3350
|
+
const recordType = options.softDelete
|
|
3351
|
+
? `type ${names.singularPascal}Record = ${names.singularPascal} & {
|
|
3352
|
+
\tdeletedAt: string | null;
|
|
3353
|
+
};
|
|
3354
|
+
|
|
3355
|
+
`
|
|
3356
|
+
: "";
|
|
3357
|
+
const mapValueType = options.softDelete
|
|
3358
|
+
? `${names.singularPascal}Record`
|
|
3359
|
+
: names.singularPascal;
|
|
3360
|
+
const activeFilter = options.softDelete
|
|
3361
|
+
? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.deletedAt === null)\n`
|
|
3362
|
+
: "";
|
|
3363
|
+
const newDeletedAtField = options.softDelete
|
|
3364
|
+
? `\t\t\t\tdeletedAt: null,\n`
|
|
3365
|
+
: "";
|
|
3366
|
+
const findDeletedGuard = options.softDelete
|
|
3367
|
+
? `\t\t\tif (${names.singularCamel}?.deletedAt !== null) return null;\n`
|
|
3368
|
+
: "";
|
|
3369
|
+
const updateDeletedGuard = options.softDelete
|
|
3370
|
+
? " || existing.deletedAt !== null"
|
|
3371
|
+
: "";
|
|
3372
|
+
const deleteImplementation = options.softDelete
|
|
3373
|
+
? `${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();
|
|
3374
|
+
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
3375
|
+
return true;`
|
|
3376
|
+
: `${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);`;
|
|
3377
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3378
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2512
3379
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3380
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2513
3381
|
import type {
|
|
2514
3382
|
Create${names.singularPascal}Input,
|
|
2515
|
-
${names.singularPascal},
|
|
2516
|
-
} from "${aliasModule(
|
|
3383
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3384
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3385
|
+
|
|
3386
|
+
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
3387
|
+
return {
|
|
3388
|
+
id: ${names.singularCamel}.id,
|
|
3389
|
+
${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
3390
|
+
version: ${names.singularCamel}.version,
|
|
3391
|
+
createdAt: ${names.singularCamel}.createdAt,
|
|
3392
|
+
updatedAt: ${names.singularCamel}.updatedAt,
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
function compare${names.pluralPascal}(
|
|
3397
|
+
left: ${mapValueType},
|
|
3398
|
+
right: ${mapValueType},
|
|
3399
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3400
|
+
): number {
|
|
3401
|
+
const leftValue = left[query.sortBy];
|
|
3402
|
+
const rightValue = right[query.sortBy];
|
|
3403
|
+
const fieldComparison =
|
|
3404
|
+
leftValue === rightValue ? left.id.localeCompare(right.id) : leftValue.localeCompare(rightValue);
|
|
3405
|
+
|
|
3406
|
+
return query.sortDirection === "asc" ? fieldComparison : -fieldComparison;
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
function isAfter${names.singularPascal}Cursor(
|
|
3410
|
+
${names.singularCamel}: ${mapValueType},
|
|
3411
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3412
|
+
): boolean {
|
|
3413
|
+
if (!query.cursor) return true;
|
|
3414
|
+
|
|
3415
|
+
const sortValue = ${names.singularCamel}[query.sortBy];
|
|
3416
|
+
const fieldComparison =
|
|
3417
|
+
sortValue === query.cursor.sortValue
|
|
3418
|
+
? ${names.singularCamel}.id.localeCompare(query.cursor.id)
|
|
3419
|
+
: sortValue.localeCompare(query.cursor.sortValue);
|
|
3420
|
+
|
|
3421
|
+
return query.sortDirection === "asc" ? fieldComparison > 0 : fieldComparison < 0;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function cursorFor${names.singularPascal}(
|
|
3425
|
+
${names.singularCamel}: ${mapValueType},
|
|
3426
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3427
|
+
): string {
|
|
3428
|
+
return encode${names.singularPascal}Cursor({
|
|
3429
|
+
sortBy: query.sortBy,
|
|
3430
|
+
sortDirection: query.sortDirection,
|
|
3431
|
+
sortValue: ${names.singularCamel}[query.sortBy],
|
|
3432
|
+
id: ${names.singularCamel}.id,
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
2517
3435
|
|
|
2518
3436
|
export function createInMemory${names.singularPascal}Repository(
|
|
2519
3437
|
seed: ${names.singularPascal}[] = [],
|
|
2520
3438
|
): ${names.singularPascal}Repository {
|
|
2521
|
-
const ${names.pluralCamel} = new Map
|
|
3439
|
+
const ${names.pluralCamel} = new Map<string, ${mapValueType}>(
|
|
3440
|
+
seed.map((${names.singularCamel}) => [
|
|
3441
|
+
${names.singularCamel}.id,
|
|
3442
|
+
{ ...${names.singularCamel}${options.softDelete ? ", deletedAt: null" : ""} },
|
|
3443
|
+
]),
|
|
3444
|
+
);
|
|
2522
3445
|
|
|
2523
3446
|
return {
|
|
2524
|
-
async list(
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
|
|
3447
|
+
async list(query) {
|
|
3448
|
+
const name = query.name?.toLocaleLowerCase();
|
|
3449
|
+
const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
3450
|
+
${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))
|
|
3451
|
+
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
3452
|
+
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
3453
|
+
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
3454
|
+
const nextCursor =
|
|
3455
|
+
all${names.pluralPascal}.length > query.page.limit && pageItems.length > 0
|
|
3456
|
+
? cursorFor${names.singularPascal}(pageItems[pageItems.length - 1]!, query)
|
|
3457
|
+
: null;
|
|
2528
3458
|
|
|
2529
|
-
return
|
|
2530
|
-
|
|
2531
|
-
page,
|
|
2532
|
-
|
|
3459
|
+
return cursorPageResult(
|
|
3460
|
+
pageItems.map(to${names.singularPascal}),
|
|
3461
|
+
query.page,
|
|
3462
|
+
nextCursor,
|
|
2533
3463
|
);
|
|
2534
3464
|
},
|
|
2535
|
-
async create(input
|
|
3465
|
+
async create(input) {
|
|
3466
|
+
const now = new Date().toISOString();
|
|
2536
3467
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2537
3468
|
id: crypto.randomUUID(),
|
|
2538
|
-
name: input.name,
|
|
2539
|
-
|
|
3469
|
+
${options.tenant ? `\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
3470
|
+
version: 1,
|
|
3471
|
+
createdAt: now,
|
|
3472
|
+
updatedAt: now,
|
|
2540
3473
|
};
|
|
2541
|
-
${names.pluralCamel}.set(${names.singularCamel}.id,
|
|
3474
|
+
${names.pluralCamel}.set(${names.singularCamel}.id, {
|
|
3475
|
+
...${names.singularCamel},
|
|
3476
|
+
${newDeletedAtField} });
|
|
2542
3477
|
return ${names.singularCamel};
|
|
2543
3478
|
},
|
|
3479
|
+
${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` : ""}
|
|
2544
3480
|
};
|
|
2545
3481
|
}
|
|
2546
3482
|
`;
|
|
2547
3483
|
}
|
|
2548
|
-
function drizzleSchemaFile(names) {
|
|
2549
|
-
return `import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
3484
|
+
function drizzleSchemaFile(names, options) {
|
|
3485
|
+
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2550
3486
|
|
|
2551
3487
|
export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
2552
3488
|
id: text("id").primaryKey(),
|
|
2553
|
-
name: text("name").notNull(),
|
|
3489
|
+
${options.tenant ? `\ttenantId: text("tenant_id").notNull(),\n` : ""} name: text("name").notNull(),
|
|
3490
|
+
version: integer("version").notNull(),
|
|
2554
3491
|
createdAt: text("created_at").notNull(),
|
|
2555
|
-
|
|
3492
|
+
updatedAt: text("updated_at").notNull(),
|
|
3493
|
+
${options.softDelete ? `\tdeletedAt: text("deleted_at"),\n` : ""}});
|
|
2556
3494
|
`;
|
|
2557
3495
|
}
|
|
2558
|
-
function
|
|
3496
|
+
function drizzleResourceWhere(names, options, predicates) {
|
|
3497
|
+
const allPredicates = [
|
|
3498
|
+
...predicates,
|
|
3499
|
+
...(options.softDelete
|
|
3500
|
+
? [`isNull(schema.${names.pluralCamel}.deletedAt)`]
|
|
3501
|
+
: []),
|
|
3502
|
+
];
|
|
3503
|
+
if (allPredicates.length === 0)
|
|
3504
|
+
return "";
|
|
3505
|
+
if (allPredicates.length === 1)
|
|
3506
|
+
return allPredicates[0] ?? "";
|
|
3507
|
+
return `and(${allPredicates.join(", ")})`;
|
|
3508
|
+
}
|
|
3509
|
+
function drizzleRepositoryFile(names, config, mode, options) {
|
|
2559
3510
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
2560
3511
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
2561
|
-
|
|
3512
|
+
const findWhere = drizzleResourceWhere(names, options, [
|
|
3513
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
3514
|
+
...(options.tenant
|
|
3515
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
3516
|
+
: []),
|
|
3517
|
+
]);
|
|
3518
|
+
const updateWhere = drizzleResourceWhere(names, options, [
|
|
3519
|
+
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
3520
|
+
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
3521
|
+
...(options.tenant
|
|
3522
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, input.tenantId)`]
|
|
3523
|
+
: []),
|
|
3524
|
+
]);
|
|
3525
|
+
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
3526
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
3527
|
+
...(options.tenant
|
|
3528
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
3529
|
+
: []),
|
|
3530
|
+
]);
|
|
3531
|
+
const drizzleImports = [
|
|
3532
|
+
"and",
|
|
3533
|
+
"asc",
|
|
3534
|
+
"desc",
|
|
3535
|
+
"eq",
|
|
3536
|
+
"gt",
|
|
3537
|
+
options.softDelete ? "isNull" : undefined,
|
|
3538
|
+
"lt",
|
|
3539
|
+
"or",
|
|
3540
|
+
"sql",
|
|
3541
|
+
"type SQL",
|
|
3542
|
+
].filter((name) => Boolean(name));
|
|
3543
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3544
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2562
3545
|
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
2563
|
-
import {
|
|
3546
|
+
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
2564
3547
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3548
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2565
3549
|
import type {
|
|
2566
3550
|
Create${names.singularPascal}Input,
|
|
2567
|
-
${names.singularPascal},
|
|
2568
|
-
} from "${aliasModule(
|
|
3551
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3552
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2569
3553
|
import * as schema from "${aliasModule(schemaPath)}";
|
|
2570
3554
|
|
|
2571
3555
|
type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelect;
|
|
@@ -2573,35 +3557,104 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
2573
3557
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
2574
3558
|
return {
|
|
2575
3559
|
id: row.id,
|
|
2576
|
-
name: row.name,
|
|
3560
|
+
${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
3561
|
+
version: row.version,
|
|
2577
3562
|
createdAt: row.createdAt,
|
|
3563
|
+
updatedAt: row.updatedAt,
|
|
2578
3564
|
};
|
|
2579
3565
|
}
|
|
2580
3566
|
|
|
3567
|
+
type List${names.pluralPascal}Query = Parameters<${names.singularPascal}Repository["list"]>[0];
|
|
3568
|
+
|
|
3569
|
+
function ${names.singularCamel}SortColumn(query: List${names.pluralPascal}Query) {
|
|
3570
|
+
return query.sortBy === "name" ? schema.${names.pluralCamel}.name : schema.${names.pluralCamel}.createdAt;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
function ${names.singularCamel}CursorFilter(
|
|
3574
|
+
query: List${names.pluralPascal}Query,
|
|
3575
|
+
): SQL<unknown> | undefined {
|
|
3576
|
+
if (!query.cursor) return undefined;
|
|
3577
|
+
|
|
3578
|
+
const column = ${names.singularCamel}SortColumn(query);
|
|
3579
|
+
const compare = query.sortDirection === "asc" ? gt : lt;
|
|
3580
|
+
|
|
3581
|
+
return or(
|
|
3582
|
+
compare(column, query.cursor.sortValue),
|
|
3583
|
+
and(
|
|
3584
|
+
eq(column, query.cursor.sortValue),
|
|
3585
|
+
compare(schema.${names.pluralCamel}.id, query.cursor.id),
|
|
3586
|
+
),
|
|
3587
|
+
) as SQL<unknown>;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
function ${names.singularCamel}ListWhere(
|
|
3591
|
+
query: List${names.pluralPascal}Query,
|
|
3592
|
+
): SQL<unknown> | undefined {
|
|
3593
|
+
const filters: SQL<unknown>[] = [
|
|
3594
|
+
${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, query.tenantId),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
3595
|
+
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
3596
|
+
|
|
3597
|
+
if (query.name) {
|
|
3598
|
+
const pattern = \`%\${query.name.toLocaleLowerCase()}%\`;
|
|
3599
|
+
filters.push(sql\`lower(\${schema.${names.pluralCamel}.name}) like \${pattern}\`);
|
|
3600
|
+
}
|
|
3601
|
+
if (cursor) filters.push(cursor);
|
|
3602
|
+
|
|
3603
|
+
return filters.length > 0 ? and(...filters) : undefined;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
function ${names.singularCamel}OrderBy(query: List${names.pluralPascal}Query) {
|
|
3607
|
+
const order = query.sortDirection === "asc" ? asc : desc;
|
|
3608
|
+
|
|
3609
|
+
return [order(${names.singularCamel}SortColumn(query)), order(schema.${names.pluralCamel}.id)] as const;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
function cursorFor${names.singularPascal}(
|
|
3613
|
+
row: ${names.singularPascal}Row,
|
|
3614
|
+
query: List${names.pluralPascal}Query,
|
|
3615
|
+
): string {
|
|
3616
|
+
return encode${names.singularPascal}Cursor({
|
|
3617
|
+
sortBy: query.sortBy,
|
|
3618
|
+
sortDirection: query.sortDirection,
|
|
3619
|
+
sortValue: row[query.sortBy],
|
|
3620
|
+
id: row.id,
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
|
|
2581
3624
|
export function createDrizzle${names.singularPascal}Repository(
|
|
2582
3625
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
2583
3626
|
): ${names.singularPascal}Repository {
|
|
2584
3627
|
return {
|
|
2585
|
-
async list(
|
|
3628
|
+
async list(query) {
|
|
3629
|
+
const where = ${names.singularCamel}ListWhere(query);
|
|
2586
3630
|
const rows = await db
|
|
2587
3631
|
.select()
|
|
2588
3632
|
.from(schema.${names.pluralCamel})
|
|
2589
|
-
.
|
|
2590
|
-
.
|
|
2591
|
-
.
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
.
|
|
2595
|
-
|
|
2596
|
-
|
|
3633
|
+
.where(where)
|
|
3634
|
+
.orderBy(...${names.singularCamel}OrderBy(query))
|
|
3635
|
+
.limit(query.page.limit + 1);
|
|
3636
|
+
const pageRows = rows.slice(0, query.page.limit);
|
|
3637
|
+
const nextCursor =
|
|
3638
|
+
rows.length > query.page.limit && pageRows.length > 0
|
|
3639
|
+
? cursorFor${names.singularPascal}(pageRows[pageRows.length - 1]!, query)
|
|
3640
|
+
: null;
|
|
3641
|
+
|
|
3642
|
+
return cursorPageResult(
|
|
3643
|
+
pageRows.map(to${names.singularPascal}),
|
|
3644
|
+
query.page,
|
|
3645
|
+
nextCursor,
|
|
3646
|
+
);
|
|
2597
3647
|
},
|
|
2598
|
-
async create(input
|
|
3648
|
+
async create(input) {
|
|
3649
|
+
const now = new Date().toISOString();
|
|
2599
3650
|
const [row] = await db
|
|
2600
3651
|
.insert(schema.${names.pluralCamel})
|
|
2601
3652
|
.values({
|
|
2602
3653
|
id: crypto.randomUUID(),
|
|
2603
|
-
name: input.name,
|
|
2604
|
-
|
|
3654
|
+
${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
3655
|
+
version: 1,
|
|
3656
|
+
createdAt: now,
|
|
3657
|
+
updatedAt: now,
|
|
2605
3658
|
})
|
|
2606
3659
|
.returning();
|
|
2607
3660
|
|
|
@@ -2611,17 +3664,20 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
2611
3664
|
|
|
2612
3665
|
return to${names.singularPascal}(row);
|
|
2613
3666
|
},
|
|
3667
|
+
${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` : ""}
|
|
2614
3668
|
};
|
|
2615
3669
|
}
|
|
2616
3670
|
`;
|
|
2617
3671
|
}
|
|
2618
|
-
function contractFile(names, config) {
|
|
2619
|
-
return `import {
|
|
3672
|
+
function contractFile(names, config, mode, options) {
|
|
3673
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
2620
3674
|
import { z } from "zod";
|
|
2621
|
-
import {
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
3675
|
+
${mode === "resource" ? `import { errors } from "${aliasModule(resourceSharedErrorsPath(config))}";\n` : ""}import {
|
|
3676
|
+
Create${names.singularPascal}InputSchema,
|
|
3677
|
+
List${names.pluralPascal}InputSchema,
|
|
3678
|
+
List${names.pluralPascal}OutputSchema,
|
|
3679
|
+
${names.singularPascal}Schema,
|
|
3680
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n Update${names.singularPascal}BodySchema,\n` : ""}} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2625
3681
|
|
|
2626
3682
|
const ErrorResponseSchema = z.object({
|
|
2627
3683
|
code: z.string(),
|
|
@@ -2629,7 +3685,7 @@ const ErrorResponseSchema = z.object({
|
|
|
2629
3685
|
requestId: z.string().optional(),
|
|
2630
3686
|
});
|
|
2631
3687
|
|
|
2632
|
-
const ${names.pluralCamel} =
|
|
3688
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
2633
3689
|
.namespace("${names.pluralCamel}")
|
|
2634
3690
|
.responses({
|
|
2635
3691
|
500: ErrorResponseSchema,
|
|
@@ -2637,21 +3693,49 @@ const ${names.pluralCamel} = createContractGroup()
|
|
|
2637
3693
|
|
|
2638
3694
|
export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
2639
3695
|
.get("/api/${names.pluralKebab}")
|
|
2640
|
-
.query(
|
|
3696
|
+
.query(List${names.pluralPascal}InputSchema)
|
|
2641
3697
|
.responses({
|
|
2642
|
-
200:
|
|
3698
|
+
200: List${names.pluralPascal}OutputSchema,
|
|
2643
3699
|
});
|
|
2644
3700
|
|
|
2645
3701
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
2646
3702
|
.post("/api/${names.pluralKebab}")
|
|
2647
|
-
.body(
|
|
3703
|
+
.body(Create${names.singularPascal}InputSchema)
|
|
3704
|
+
${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
3705
|
+
201: ${names.singularPascal}Schema,
|
|
3706
|
+
});
|
|
3707
|
+
${mode === "resource"
|
|
3708
|
+
? `
|
|
3709
|
+
export const get${names.singularPascal} = ${names.pluralCamel}
|
|
3710
|
+
.get("/api/${names.pluralKebab}/:id")
|
|
3711
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3712
|
+
.errors({ ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
2648
3713
|
.responses({
|
|
2649
|
-
|
|
3714
|
+
200: ${names.singularPascal}Schema,
|
|
2650
3715
|
});
|
|
3716
|
+
|
|
3717
|
+
export const update${names.singularPascal} = ${names.pluralCamel}
|
|
3718
|
+
.patch("/api/${names.pluralKebab}/:id")
|
|
3719
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3720
|
+
.body(Update${names.singularPascal}BodySchema)
|
|
3721
|
+
${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 })
|
|
3722
|
+
.responses({
|
|
3723
|
+
200: ${names.singularPascal}Schema,
|
|
3724
|
+
});
|
|
3725
|
+
|
|
3726
|
+
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
3727
|
+
.delete("/api/${names.pluralKebab}/:id")
|
|
3728
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3729
|
+
${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 })
|
|
3730
|
+
.responses({
|
|
3731
|
+
204: null,
|
|
3732
|
+
});
|
|
3733
|
+
`
|
|
3734
|
+
: ""}
|
|
2651
3735
|
`;
|
|
2652
3736
|
}
|
|
2653
3737
|
function standaloneContractFile(names) {
|
|
2654
|
-
return `import {
|
|
3738
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
2655
3739
|
import { z } from "zod";
|
|
2656
3740
|
|
|
2657
3741
|
const ErrorResponseSchema = z.object({
|
|
@@ -2660,7 +3744,7 @@ const ErrorResponseSchema = z.object({
|
|
|
2660
3744
|
requestId: z.string().optional(),
|
|
2661
3745
|
});
|
|
2662
3746
|
|
|
2663
|
-
const ${names.pluralCamel} =
|
|
3747
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
2664
3748
|
.namespace("${names.pluralCamel}")
|
|
2665
3749
|
.responses({
|
|
2666
3750
|
500: ErrorResponseSchema,
|
|
@@ -2696,7 +3780,8 @@ export const ${names.pluralCamel}Contracts = [list${names.pluralPascal}];
|
|
|
2696
3780
|
`;
|
|
2697
3781
|
}
|
|
2698
3782
|
function standaloneUseCaseFile(names, config, filePath) {
|
|
2699
|
-
return `import {
|
|
3783
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3784
|
+
import { z } from "zod";
|
|
2700
3785
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2701
3786
|
|
|
2702
3787
|
export const ${names.action.pascal}InputSchema = z.object({});
|
|
@@ -2724,29 +3809,27 @@ function useCaseTestFile(names, config) {
|
|
|
2724
3809
|
return `import { describe, expect, it } from "bun:test";
|
|
2725
3810
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
2726
3811
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
2727
|
-
import {
|
|
3812
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
3813
|
+
import { createTestAnonymousActor } from "@beignet/core/ports/testing";
|
|
2728
3814
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2729
3815
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
2730
3816
|
import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
|
|
2731
3817
|
|
|
2732
3818
|
describe("${names.exportName}", () => {
|
|
2733
3819
|
it("runs ${names.action.camel}", async () => {
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
gate: testPorts.gate.bind({ actor, auth: null }),
|
|
2748
|
-
ports: testPorts,
|
|
2749
|
-
}));
|
|
3820
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
3821
|
+
base: appPorts,
|
|
3822
|
+
overrides: {
|
|
3823
|
+
gate: appPorts.gate,
|
|
3824
|
+
devtools: createInMemoryDevtools(),
|
|
3825
|
+
},
|
|
3826
|
+
});
|
|
3827
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
3828
|
+
ports: testFixture.ports,
|
|
3829
|
+
actor: createTestAnonymousActor(),
|
|
3830
|
+
tenant: null,
|
|
3831
|
+
});
|
|
3832
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
2750
3833
|
|
|
2751
3834
|
const result = await tester.run(${names.exportName}, {});
|
|
2752
3835
|
|
|
@@ -2807,6 +3890,134 @@ export const ${names.policyName} = definePolicy({
|
|
|
2807
3890
|
});
|
|
2808
3891
|
`;
|
|
2809
3892
|
}
|
|
3893
|
+
function resourcePolicyFile(names, _config, options) {
|
|
3894
|
+
return `import type { ActivityActor, ActivityTenant } from "@beignet/core/ports";
|
|
3895
|
+
import { definePolicy, deny } from "@beignet/core/ports";
|
|
3896
|
+
|
|
3897
|
+
export type AuthorizationContext = {
|
|
3898
|
+
actor: ActivityActor;
|
|
3899
|
+
tenant?: ActivityTenant;
|
|
3900
|
+
};
|
|
3901
|
+
|
|
3902
|
+
type ${names.singularPascal}PolicyResource = {
|
|
3903
|
+
id: string;
|
|
3904
|
+
tenantId?: string;
|
|
3905
|
+
};
|
|
3906
|
+
|
|
3907
|
+
function isAuthenticated(ctx: AuthorizationContext) {
|
|
3908
|
+
return ctx.actor.type === "user" && Boolean(ctx.actor.id);
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
function canWrite(ctx: AuthorizationContext) {
|
|
3912
|
+
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
3913
|
+
${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
export const ${names.singularCamel}Policy = definePolicy({
|
|
3917
|
+
"${names.pluralCamel}.view": (_ctx: AuthorizationContext) => true,
|
|
3918
|
+
"${names.pluralCamel}.create": (ctx: AuthorizationContext) =>
|
|
3919
|
+
canWrite(ctx),
|
|
3920
|
+
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3921
|
+
const decision = canWrite(ctx);
|
|
3922
|
+
if (decision !== true) return decision;
|
|
3923
|
+
${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` : ""}
|
|
3924
|
+
return true;
|
|
3925
|
+
},
|
|
3926
|
+
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3927
|
+
const decision = canWrite(ctx);
|
|
3928
|
+
if (decision !== true) return decision;
|
|
3929
|
+
${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` : ""}
|
|
3930
|
+
return true;
|
|
3931
|
+
},
|
|
3932
|
+
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
3933
|
+
deny("You are not allowed to manage this ${names.singularKebab}."),
|
|
3934
|
+
});
|
|
3935
|
+
`;
|
|
3936
|
+
}
|
|
3937
|
+
function resourceEventsFile(names) {
|
|
3938
|
+
return `import { defineEvent } from "@beignet/core/events";
|
|
3939
|
+
import { z } from "zod";
|
|
3940
|
+
|
|
3941
|
+
export const ${names.singularPascal}Created = defineEvent("${names.pluralCamel}.created", {
|
|
3942
|
+
payload: z.object({
|
|
3943
|
+
id: z.string().uuid(),
|
|
3944
|
+
}),
|
|
3945
|
+
});
|
|
3946
|
+
|
|
3947
|
+
export const ${names.singularPascal}Updated = defineEvent("${names.pluralCamel}.updated", {
|
|
3948
|
+
payload: z.object({
|
|
3949
|
+
id: z.string().uuid(),
|
|
3950
|
+
}),
|
|
3951
|
+
});
|
|
3952
|
+
|
|
3953
|
+
export const ${names.singularPascal}Deleted = defineEvent("${names.pluralCamel}.deleted", {
|
|
3954
|
+
payload: z.object({
|
|
3955
|
+
id: z.string().uuid(),
|
|
3956
|
+
}),
|
|
3957
|
+
});
|
|
3958
|
+
|
|
3959
|
+
export const ${names.singularCamel}Events = [
|
|
3960
|
+
${names.singularPascal}Created,
|
|
3961
|
+
${names.singularPascal}Updated,
|
|
3962
|
+
${names.singularPascal}Deleted,
|
|
3963
|
+
] as const;
|
|
3964
|
+
`;
|
|
3965
|
+
}
|
|
3966
|
+
function resourcePolicyTestFile(names, options) {
|
|
3967
|
+
return `import { describe, expect, it } from "bun:test";
|
|
3968
|
+
import { createPolicyTester } from "@beignet/core/ports/testing";
|
|
3969
|
+
import { ${names.singularCamel}Policy } from "../policy";
|
|
3970
|
+
import type { ${names.singularPascal} } from "../schemas";
|
|
3971
|
+
|
|
3972
|
+
function create${names.singularPascal}(): ${names.singularPascal} {
|
|
3973
|
+
const now = new Date().toISOString();
|
|
3974
|
+
return {
|
|
3975
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
3976
|
+
${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
3977
|
+
version: 1,
|
|
3978
|
+
createdAt: now,
|
|
3979
|
+
updatedAt: now,
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
describe("${names.singularCamel}Policy", () => {
|
|
3984
|
+
it("documents generated ${names.pluralKebab} authorization rules", async () => {
|
|
3985
|
+
const tester = createPolicyTester({
|
|
3986
|
+
policies: [${names.singularCamel}Policy],
|
|
3987
|
+
});
|
|
3988
|
+
const ${names.singularCamel} = create${names.singularPascal}();
|
|
3989
|
+
const ctx = {
|
|
3990
|
+
actor: { type: "user" as const, id: "user_test" },
|
|
3991
|
+
${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
3992
|
+
|
|
3993
|
+
await expect(
|
|
3994
|
+
tester.assertMatrix([
|
|
3995
|
+
{
|
|
3996
|
+
name: "authenticated actor can create ${names.singularKebab}",
|
|
3997
|
+
ctx,
|
|
3998
|
+
ability: "${names.pluralCamel}.create",
|
|
3999
|
+
expected: "allow",
|
|
4000
|
+
},
|
|
4001
|
+
{
|
|
4002
|
+
name: "authenticated actor can update ${names.singularKebab}",
|
|
4003
|
+
ctx,
|
|
4004
|
+
ability: "${names.pluralCamel}.update",
|
|
4005
|
+
subject: ${names.singularCamel},
|
|
4006
|
+
expected: "allow",
|
|
4007
|
+
},
|
|
4008
|
+
{
|
|
4009
|
+
name: "authenticated actor can delete ${names.singularKebab}",
|
|
4010
|
+
ctx,
|
|
4011
|
+
ability: "${names.pluralCamel}.delete",
|
|
4012
|
+
subject: ${names.singularCamel},
|
|
4013
|
+
expected: "allow",
|
|
4014
|
+
},
|
|
4015
|
+
]),
|
|
4016
|
+
).resolves.toBeUndefined();
|
|
4017
|
+
});
|
|
4018
|
+
});
|
|
4019
|
+
`;
|
|
4020
|
+
}
|
|
2810
4021
|
function eventFile(names) {
|
|
2811
4022
|
return `import { defineEvent } from "@beignet/core/events";
|
|
2812
4023
|
import { z } from "zod";
|
|
@@ -2823,11 +4034,9 @@ export const ${names.eventExportName} = defineEvent("${names.eventName}", {
|
|
|
2823
4034
|
`;
|
|
2824
4035
|
}
|
|
2825
4036
|
function jobFile(names, config) {
|
|
2826
|
-
return `import {
|
|
4037
|
+
return `import { retry } from "@beignet/core/jobs";
|
|
2827
4038
|
import { z } from "zod";
|
|
2828
|
-
import
|
|
2829
|
-
|
|
2830
|
-
const jobs = createJobHandlers<AppContext>();
|
|
4039
|
+
import { defineJob } from "${aliasModule(config.paths.jobsBuilder)}";
|
|
2831
4040
|
|
|
2832
4041
|
export const ${names.payloadSchemaName} = z.object({
|
|
2833
4042
|
\tid: z.string().uuid(),
|
|
@@ -2835,7 +4044,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
2835
4044
|
|
|
2836
4045
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2837
4046
|
|
|
2838
|
-
export const ${names.jobExportName} =
|
|
4047
|
+
export const ${names.jobExportName} = defineJob("${names.jobName}", {
|
|
2839
4048
|
\tpayload: ${names.payloadSchemaName},
|
|
2840
4049
|
\tretry: retry.exponential({
|
|
2841
4050
|
\t\tattempts: 3,
|
|
@@ -2851,12 +4060,38 @@ export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
|
|
|
2851
4060
|
});
|
|
2852
4061
|
`;
|
|
2853
4062
|
}
|
|
4063
|
+
function taskFile(names, config) {
|
|
4064
|
+
return `import { z } from "zod";
|
|
4065
|
+
import { defineTask } from "${aliasModule(config.paths.tasksBuilder)}";
|
|
4066
|
+
|
|
4067
|
+
export const ${names.inputSchemaName} = z.object({
|
|
4068
|
+
\tdryRun: z.boolean().default(true),
|
|
4069
|
+
});
|
|
4070
|
+
|
|
4071
|
+
export type ${names.inputTypeName} = z.infer<typeof ${names.inputSchemaName}>;
|
|
4072
|
+
|
|
4073
|
+
export const ${names.taskExportName} = defineTask("${names.taskName}", {
|
|
4074
|
+
\tinput: ${names.inputSchemaName},
|
|
4075
|
+
\tdescription: "Operational ${names.feature.kebab} ${names.artifact.kebab} task.",
|
|
4076
|
+
\tasync handle({ input, ctx }) {
|
|
4077
|
+
\t\tctx.ports.logger.info("Task handled", {
|
|
4078
|
+
\t\t\ttaskName: "${names.taskName}",
|
|
4079
|
+
\t\t\tdryRun: input.dryRun,
|
|
4080
|
+
\t\t});
|
|
4081
|
+
|
|
4082
|
+
\t\treturn {
|
|
4083
|
+
\t\t\tdryRun: input.dryRun,
|
|
4084
|
+
\t\t};
|
|
4085
|
+
\t},
|
|
4086
|
+
});
|
|
4087
|
+
`;
|
|
4088
|
+
}
|
|
2854
4089
|
function factoryFile(names, config) {
|
|
2855
4090
|
const resource = featureResourceNames(names);
|
|
2856
|
-
return `import {
|
|
4091
|
+
return `import { createFactory } from "@beignet/core/testing";
|
|
2857
4092
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2858
4093
|
|
|
2859
|
-
export const ${names.factoryExportName} =
|
|
4094
|
+
export const ${names.factoryExportName} = createFactory("${names.factoryName}", {
|
|
2860
4095
|
\tdefaults: ({ sequence }) => ({
|
|
2861
4096
|
\t\tname: "${resource.singularPascal} " + sequence,
|
|
2862
4097
|
\t}),
|
|
@@ -2880,14 +4115,9 @@ export const ${names.seedExportName} = defineSeed("${names.seedName}", {
|
|
|
2880
4115
|
`;
|
|
2881
4116
|
}
|
|
2882
4117
|
function notificationFile(names, config) {
|
|
2883
|
-
return `import {
|
|
2884
|
-
\tcreateNotificationHandlers,
|
|
2885
|
-
\tdefineMailNotificationChannel,
|
|
2886
|
-
} from "@beignet/core/notifications";
|
|
4118
|
+
return `import { defineMailNotificationChannel } from "@beignet/core/notifications";
|
|
2887
4119
|
import { z } from "zod";
|
|
2888
|
-
import
|
|
2889
|
-
|
|
2890
|
-
const notifications = createNotificationHandlers<AppContext>();
|
|
4120
|
+
import { defineNotification } from "${aliasModule(config.paths.notificationsBuilder)}";
|
|
2891
4121
|
|
|
2892
4122
|
export const ${names.payloadSchemaName} = z.object({
|
|
2893
4123
|
\tid: z.string().uuid(),
|
|
@@ -2897,7 +4127,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
2897
4127
|
|
|
2898
4128
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2899
4129
|
|
|
2900
|
-
export const ${names.notificationExportName} =
|
|
4130
|
+
export const ${names.notificationExportName} = defineNotification(
|
|
2901
4131
|
\t"${names.notificationName}",
|
|
2902
4132
|
\t{
|
|
2903
4133
|
\t\tpayload: ${names.payloadSchemaName},
|
|
@@ -2914,13 +4144,10 @@ export const ${names.notificationExportName} = notifications.defineNotification(
|
|
|
2914
4144
|
}
|
|
2915
4145
|
function listenerFile(names, config) {
|
|
2916
4146
|
const filePath = listenerFilePath(names, config);
|
|
2917
|
-
return `import {
|
|
2918
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4147
|
+
return `import { defineListener } from "${aliasModule(config.paths.listenersBuilder)}";
|
|
2919
4148
|
import { ${names.event.eventExportName} } from "${relativeModule(filePath, eventFilePath(names.event, config))}";
|
|
2920
4149
|
|
|
2921
|
-
const
|
|
2922
|
-
|
|
2923
|
-
export const ${names.listenerExportName} = events.defineListener(${names.event.eventExportName}, {
|
|
4150
|
+
export const ${names.listenerExportName} = defineListener(${names.event.eventExportName}, {
|
|
2924
4151
|
\tname: "${names.listenerName}",
|
|
2925
4152
|
\tasync handle({ payload, ctx }) {
|
|
2926
4153
|
\t\tctx.ports.logger.info("Listener handled", {
|
|
@@ -2932,11 +4159,8 @@ export const ${names.listenerExportName} = events.defineListener(${names.event.e
|
|
|
2932
4159
|
`;
|
|
2933
4160
|
}
|
|
2934
4161
|
function scheduleFile(names, config) {
|
|
2935
|
-
return `import {
|
|
2936
|
-
import {
|
|
2937
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2938
|
-
|
|
2939
|
-
const schedules = createScheduleHandlers<AppContext>();
|
|
4162
|
+
return `import { z } from "zod";
|
|
4163
|
+
import { defineSchedule } from "${aliasModule(config.paths.schedulesBuilder)}";
|
|
2940
4164
|
|
|
2941
4165
|
export const ${names.payloadSchemaName} = z.object({
|
|
2942
4166
|
\tdate: z.string(),
|
|
@@ -2944,7 +4168,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
2944
4168
|
|
|
2945
4169
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2946
4170
|
|
|
2947
|
-
export const ${names.scheduleExportName} =
|
|
4171
|
+
export const ${names.scheduleExportName} = defineSchedule(
|
|
2948
4172
|
\t"${names.scheduleName}",
|
|
2949
4173
|
\t{
|
|
2950
4174
|
\t\tcron: "${names.cron}",
|
|
@@ -3020,108 +4244,110 @@ export const ${names.uploadExportName} = defineUpload<
|
|
|
3020
4244
|
});
|
|
3021
4245
|
`;
|
|
3022
4246
|
}
|
|
3023
|
-
function
|
|
3024
|
-
return `
|
|
3025
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3026
|
-
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
3027
|
-
import { env } from "@/lib/env";
|
|
3028
|
-
import { server } from "${aliasModule(config.paths.server)}";
|
|
4247
|
+
function featureUiComponentFile(names, config) {
|
|
4248
|
+
return `"use client";
|
|
3029
4249
|
|
|
3030
|
-
|
|
3031
|
-
|
|
4250
|
+
import { contractErrorMessage } from "@beignet/core/client";
|
|
4251
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4252
|
+
import { useState } from "react";
|
|
4253
|
+
import { rq } from "${aliasModule(clientIndexPath(config))}";
|
|
4254
|
+
import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
3032
4255
|
|
|
3033
|
-
|
|
3034
|
-
\
|
|
3035
|
-
\
|
|
3036
|
-
\
|
|
3037
|
-
\t\
|
|
3038
|
-
\t
|
|
4256
|
+
export function ${names.componentExportName}() {
|
|
4257
|
+
\tconst [name, setName] = useState("");
|
|
4258
|
+
\tconst queryClient = useQueryClient();
|
|
4259
|
+
\tconst ${names.pluralCamel}Query = useQuery(
|
|
4260
|
+
\t\trq(list${names.pluralPascal}).queryOptions({ query: {} }),
|
|
4261
|
+
\t);
|
|
4262
|
+
\tconst create${names.singularPascal}Mutation = useMutation(
|
|
4263
|
+
\t\trq(create${names.singularPascal}).mutationOptions({
|
|
4264
|
+
\t\t\tonSuccess: async () => {
|
|
4265
|
+
\t\t\t\tsetName("");
|
|
4266
|
+
\t\t\t\tawait rq(list${names.pluralPascal}).invalidate(queryClient);
|
|
3039
4267
|
\t\t\t},
|
|
3040
|
-
\t\t
|
|
3041
|
-
\t
|
|
3042
|
-
\t}
|
|
3043
|
-
|
|
3044
|
-
\tif (
|
|
3045
|
-
\t\trequest.headers.get("authorization") !== \`Bearer \${cronSecret}\`
|
|
3046
|
-
\t) {
|
|
3047
|
-
\t\treturn Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
3048
|
-
\t}
|
|
3049
|
-
|
|
3050
|
-
\tconst ctx = await server.createContextFromNext();
|
|
3051
|
-
\tconst runner = createInlineScheduleRunner<AppContext>({
|
|
3052
|
-
\t\tctx,
|
|
3053
|
-
\t\tonStart({ run, schedule }) {
|
|
3054
|
-
\t\t\tctx.ports.devtools.record({
|
|
3055
|
-
\t\t\t\ttype: "schedule",
|
|
3056
|
-
\t\t\t\twatcher: "schedules",
|
|
3057
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
3058
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3059
|
-
\t\t\t\tstatus: "started",
|
|
3060
|
-
\t\t\t\tcron: schedule.cron,
|
|
3061
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
3062
|
-
\t\t\t\tdetails: {
|
|
3063
|
-
\t\t\t\t\tsource: run.source,
|
|
3064
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
3065
|
-
\t\t\t\t},
|
|
3066
|
-
\t\t\t});
|
|
3067
|
-
\t\t},
|
|
3068
|
-
\t\tonSuccess({ run, schedule }) {
|
|
3069
|
-
\t\t\tctx.ports.devtools.record({
|
|
3070
|
-
\t\t\t\ttype: "schedule",
|
|
3071
|
-
\t\t\t\twatcher: "schedules",
|
|
3072
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
3073
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3074
|
-
\t\t\t\tstatus: "completed",
|
|
3075
|
-
\t\t\t\tcron: schedule.cron,
|
|
3076
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
3077
|
-
\t\t\t\tdetails: {
|
|
3078
|
-
\t\t\t\t\tsource: run.source,
|
|
3079
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
3080
|
-
\t\t\t\t},
|
|
3081
|
-
\t\t\t});
|
|
3082
|
-
\t\t},
|
|
3083
|
-
\t\tonError({ error, run, schedule }) {
|
|
3084
|
-
\t\t\tctx.ports.devtools.record({
|
|
3085
|
-
\t\t\t\ttype: "schedule",
|
|
3086
|
-
\t\t\t\twatcher: "schedules",
|
|
3087
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
3088
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3089
|
-
\t\t\t\tstatus: "failed",
|
|
3090
|
-
\t\t\t\tcron: schedule.cron,
|
|
3091
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
3092
|
-
\t\t\t\tdetails: {
|
|
3093
|
-
\t\t\t\t\terror,
|
|
3094
|
-
\t\t\t\t\tsource: run.source,
|
|
3095
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
3096
|
-
\t\t\t\t},
|
|
3097
|
-
\t\t\t});
|
|
3098
|
-
\t\t\tctx.ports.logger.error("Schedule failed", {
|
|
3099
|
-
\t\t\t\terror,
|
|
3100
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3101
|
-
\t\t\t});
|
|
3102
|
-
\t\t},
|
|
3103
|
-
\t\tonHookError({ error, hook, schedule }) {
|
|
3104
|
-
\t\t\tctx.ports.logger.warn("Schedule lifecycle hook failed", {
|
|
3105
|
-
\t\t\t\terror,
|
|
3106
|
-
\t\t\t\thook,
|
|
3107
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3108
|
-
\t\t\t});
|
|
3109
|
-
\t\t},
|
|
3110
|
-
\t});
|
|
4268
|
+
\t\t}),
|
|
4269
|
+
\t);
|
|
3111
4270
|
|
|
3112
|
-
\
|
|
3113
|
-
\t\
|
|
3114
|
-
\t\t\
|
|
3115
|
-
\t\t}
|
|
3116
|
-
\t
|
|
3117
|
-
\t\
|
|
3118
|
-
\t
|
|
4271
|
+
\treturn (
|
|
4272
|
+
\t\t<section className="${names.pluralKebab}-panel">
|
|
4273
|
+
\t\t\t<form
|
|
4274
|
+
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
4275
|
+
\t\t\t\tonSubmit={(event) => {
|
|
4276
|
+
\t\t\t\t\tevent.preventDefault();
|
|
4277
|
+
\t\t\t\t\tconst trimmedName = name.trim();
|
|
4278
|
+
\t\t\t\t\tif (!trimmedName) return;
|
|
4279
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
4280
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
4281
|
+
\t\t\t\t\t\tbody: { name: trimmedName },
|
|
4282
|
+
\t\t\t\t\t});
|
|
4283
|
+
\t\t\t\t}}
|
|
4284
|
+
\t\t\t>
|
|
4285
|
+
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
4286
|
+
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
4287
|
+
\t\t\t\t\t<input
|
|
4288
|
+
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
4289
|
+
\t\t\t\t\t\tvalue={name}
|
|
4290
|
+
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
4291
|
+
\t\t\t\t\t\tplaceholder="Name"
|
|
4292
|
+
\t\t\t\t\t/>
|
|
4293
|
+
\t\t\t\t\t<button
|
|
4294
|
+
\t\t\t\t\t\ttype="submit"
|
|
4295
|
+
\t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
|
|
4296
|
+
\t\t\t\t\t>
|
|
4297
|
+
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
4298
|
+
\t\t\t\t\t</button>
|
|
4299
|
+
\t\t\t\t</div>
|
|
4300
|
+
\t\t\t\t{create${names.singularPascal}Mutation.isError ? (
|
|
4301
|
+
\t\t\t\t\t<p role="alert">
|
|
4302
|
+
\t\t\t\t\t\t{contractErrorMessage(
|
|
4303
|
+
\t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
|
|
4304
|
+
\t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
|
|
4305
|
+
\t\t\t\t\t\t)}
|
|
4306
|
+
\t\t\t\t\t</p>
|
|
4307
|
+
\t\t\t\t) : null}
|
|
4308
|
+
\t\t\t</form>
|
|
3119
4309
|
|
|
3120
|
-
\
|
|
4310
|
+
\t\t\t<div className="${names.pluralKebab}-list">
|
|
4311
|
+
\t\t\t\t<div className="${names.pluralKebab}-list-heading">
|
|
4312
|
+
\t\t\t\t\t<h2>${names.pluralPascal}</h2>
|
|
4313
|
+
\t\t\t\t\t<span>{${names.pluralCamel}Query.data?.items.length ?? 0} shown</span>
|
|
4314
|
+
\t\t\t\t</div>
|
|
4315
|
+
\t\t\t\t{${names.pluralCamel}Query.isLoading ? <p>Loading...</p> : null}
|
|
4316
|
+
\t\t\t\t{${names.pluralCamel}Query.isError ? (
|
|
4317
|
+
\t\t\t\t\t<p role="alert">Could not load ${names.pluralKebab}.</p>
|
|
4318
|
+
\t\t\t\t) : null}
|
|
4319
|
+
\t\t\t\t{${names.pluralCamel}Query.data?.items.length === 0 ? (
|
|
4320
|
+
\t\t\t\t\t<p>No ${names.pluralKebab} yet.</p>
|
|
4321
|
+
\t\t\t\t) : null}
|
|
4322
|
+
\t\t\t\t<ul>
|
|
4323
|
+
\t\t\t\t\t{${names.pluralCamel}Query.data?.items.map((${names.singularCamel}) => (
|
|
4324
|
+
\t\t\t\t\t\t<li key={${names.singularCamel}.id}>
|
|
4325
|
+
\t\t\t\t\t\t\t<strong>{${names.singularCamel}.name}</strong>
|
|
4326
|
+
\t\t\t\t\t\t\t<small>{new Date(${names.singularCamel}.createdAt).toLocaleDateString()}</small>
|
|
4327
|
+
\t\t\t\t\t\t</li>
|
|
4328
|
+
\t\t\t\t\t))}
|
|
4329
|
+
\t\t\t\t</ul>
|
|
4330
|
+
\t\t\t</div>
|
|
4331
|
+
\t\t</section>
|
|
4332
|
+
\t);
|
|
4333
|
+
}
|
|
4334
|
+
`;
|
|
3121
4335
|
}
|
|
4336
|
+
function scheduleRouteFile(names, config) {
|
|
4337
|
+
return `import { createScheduleRoute } from "@beignet/next";
|
|
4338
|
+
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
4339
|
+
import { env } from "@/lib/env";
|
|
4340
|
+
import { server } from "${aliasModule(config.paths.server)}";
|
|
3122
4341
|
|
|
3123
|
-
export const
|
|
3124
|
-
|
|
4342
|
+
export const runtime = "nodejs";
|
|
4343
|
+
|
|
4344
|
+
export const { GET, POST } = createScheduleRoute({
|
|
4345
|
+
\tserver,
|
|
4346
|
+
\tschedules: [${names.scheduleExportName}],
|
|
4347
|
+
\tschedule: ${names.scheduleExportName}.name,
|
|
4348
|
+
\tsecret: env.CRON_SECRET,
|
|
4349
|
+
\tsource: "cron-route",
|
|
4350
|
+
});
|
|
3125
4351
|
`;
|
|
3126
4352
|
}
|
|
3127
4353
|
function useCaseIndexExport(names) {
|
|
@@ -3134,78 +4360,138 @@ function useCaseIndexExport(names) {
|
|
|
3134
4360
|
} from "./${names.action.kebab}";
|
|
3135
4361
|
`;
|
|
3136
4362
|
}
|
|
3137
|
-
function routeGroupFile(names, config) {
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
4363
|
+
function routeGroupFile(names, config, mode, _options) {
|
|
4364
|
+
const routeFilePath = path.join(config.paths.features, names.pluralKebab, "routes.ts");
|
|
4365
|
+
const contractsPath = resourceContractFilePath(names, config);
|
|
4366
|
+
const useCasesPath = resourceUseCaseIndexPath(names, config);
|
|
4367
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
4368
|
+
import { defineRouteGroup } from "@beignet/next";
|
|
4369
|
+
import type { AppContext } from "${relativeModule(routeFilePath, config.paths.appContext)}";
|
|
4370
|
+
import {
|
|
4371
|
+
create${names.singularPascal},
|
|
4372
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
4373
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${relativeModule(routeFilePath, contractsPath)}";
|
|
3141
4374
|
import {
|
|
3142
4375
|
create${names.singularPascal}UseCase,
|
|
3143
|
-
list${names.pluralPascal}UseCase,
|
|
3144
|
-
|
|
4376
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
4377
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${relativeModule(routeFilePath, useCasesPath)}";
|
|
3145
4378
|
|
|
3146
4379
|
export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>({
|
|
3147
4380
|
name: "${names.pluralKebab}",
|
|
3148
4381
|
routes: [
|
|
3149
|
-
{
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
status: 200,
|
|
3153
|
-
body: await list${names.pluralPascal}UseCase.run({ ctx, input: query }),
|
|
3154
|
-
}),
|
|
3155
|
-
},
|
|
3156
|
-
{
|
|
3157
|
-
contract: create${names.singularPascal},
|
|
3158
|
-
handle: async ({ ctx, body }) => ({
|
|
3159
|
-
status: 201,
|
|
3160
|
-
body: await create${names.singularPascal}UseCase.run({ ctx, input: body }),
|
|
3161
|
-
}),
|
|
3162
|
-
},
|
|
3163
|
-
],
|
|
4382
|
+
{ contract: list${names.pluralPascal}, useCase: list${names.pluralPascal}UseCase },
|
|
4383
|
+
{ contract: create${names.singularPascal}, useCase: create${names.singularPascal}UseCase },
|
|
4384
|
+
${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` : ""} ],
|
|
3164
4385
|
});
|
|
3165
4386
|
`;
|
|
3166
4387
|
}
|
|
3167
|
-
function testFile(names, config) {
|
|
4388
|
+
function testFile(names, config, mode, options) {
|
|
3168
4389
|
const repositoryPath = path.join(path.dirname(config.paths.infrastructurePorts), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
|
|
4390
|
+
const serverContextPath = path.join(path.dirname(config.paths.server), "context.ts");
|
|
4391
|
+
const requestTarget = options.tenant ? "requester" : "app";
|
|
3169
4392
|
return `import { describe, expect, it } from "bun:test";
|
|
3170
4393
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
3171
|
-
import { defineRoutes } from "@beignet/web";
|
|
3172
|
-
import { createTestApp } from "@beignet/web/testing";
|
|
4394
|
+
${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` : ""}import { defineRoutes } from "@beignet/web";
|
|
4395
|
+
import { createTestApp${options.tenant ? ", createTestRequester" : ""} } from "@beignet/web/testing";
|
|
3173
4396
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3174
|
-
import {
|
|
4397
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
4398
|
+
import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/ports/testing";
|
|
3175
4399
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3176
4400
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
3177
|
-
import {
|
|
4401
|
+
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
4402
|
+
import {
|
|
4403
|
+
create${names.singularPascal},
|
|
4404
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
4405
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
3178
4406
|
import { createInMemory${names.singularPascal}Repository } from "${aliasModule(repositoryPath)}";
|
|
3179
4407
|
import { ${names.singularCamel}Routes } from "${aliasModule(path.join(resourceFeatureDir(names, config), "routes.ts"))}";
|
|
3180
4408
|
import {
|
|
3181
4409
|
create${names.singularPascal}UseCase,
|
|
3182
|
-
list${names.pluralPascal}UseCase,
|
|
3183
|
-
} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
4410
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
4411
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
3184
4412
|
|
|
3185
4413
|
describe("${names.pluralCamel} resource", () => {
|
|
3186
|
-
it("creates and lists ${names.pluralCamel}", async () => {
|
|
3187
|
-
const ${names.
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
4414
|
+
it("${mode === "resource" ? `exercises ${names.pluralCamel} CRUD` : `creates and lists ${names.pluralCamel}`}", async () => {
|
|
4415
|
+
const seed${names.pluralPascal} = [
|
|
4416
|
+
{
|
|
4417
|
+
id: "00000000-0000-4000-8000-000000000101",
|
|
4418
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
4419
|
+
version: 1,
|
|
4420
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
4421
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
4422
|
+
},
|
|
4423
|
+
{
|
|
4424
|
+
id: "00000000-0000-4000-8000-000000000102",
|
|
4425
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
4426
|
+
version: 1,
|
|
4427
|
+
createdAt: "2024-01-02T00:00:00.000Z",
|
|
4428
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
4429
|
+
},
|
|
4430
|
+
{
|
|
4431
|
+
id: "00000000-0000-4000-8000-000000000103",
|
|
4432
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
4433
|
+
version: 1,
|
|
4434
|
+
createdAt: "2024-01-03T00:00:00.000Z",
|
|
4435
|
+
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
4436
|
+
},
|
|
4437
|
+
${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` : ""} ];
|
|
4438
|
+
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
4439
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
4440
|
+
base: appPorts,
|
|
4441
|
+
overrides: {
|
|
3193
4442
|
${names.pluralCamel},
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
4443
|
+
${options.auth
|
|
4444
|
+
? `\t\t\t\tauth: {
|
|
4445
|
+
getSession: async () => ({
|
|
4446
|
+
user: { id: "user_test", name: "Test User" },
|
|
4447
|
+
}),
|
|
4448
|
+
},
|
|
4449
|
+
gate: appPorts.gate,\n`
|
|
4450
|
+
: `\t\t\t\tauth: {
|
|
4451
|
+
getSession: async () => null,
|
|
4452
|
+
},\n`} devtools: createInMemoryDevtools(),
|
|
4453
|
+
},
|
|
4454
|
+
transaction: {
|
|
4455
|
+
ports: (ports) => ({
|
|
4456
|
+
...ports,
|
|
4457
|
+
${names.pluralCamel},
|
|
4458
|
+
}),
|
|
4459
|
+
},
|
|
4460
|
+
});
|
|
4461
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
4462
|
+
ports: testFixture.ports,
|
|
4463
|
+
actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
4464
|
+
tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
|
|
3205
4465
|
});
|
|
3206
4466
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
3207
4467
|
|
|
3208
4468
|
const ctx = await tester.ctx();
|
|
4469
|
+
const defaultPage = await tester.run(list${names.pluralPascal}UseCase, {}, { ctx });
|
|
4470
|
+
const filteredPage = await tester.run(
|
|
4471
|
+
list${names.pluralPascal}UseCase,
|
|
4472
|
+
{ name: "alp" },
|
|
4473
|
+
{ ctx },
|
|
4474
|
+
);
|
|
4475
|
+
const firstPage = await tester.run(
|
|
4476
|
+
list${names.pluralPascal}UseCase,
|
|
4477
|
+
{ limit: 2 },
|
|
4478
|
+
{ ctx },
|
|
4479
|
+
);
|
|
4480
|
+
const secondPage = await tester.run(
|
|
4481
|
+
list${names.pluralPascal}UseCase,
|
|
4482
|
+
{ limit: 2, cursor: firstPage.page.nextCursor },
|
|
4483
|
+
{ ctx },
|
|
4484
|
+
);
|
|
4485
|
+
const nameAscendingPage = await tester.run(
|
|
4486
|
+
list${names.pluralPascal}UseCase,
|
|
4487
|
+
{ sortBy: "name", sortDirection: "asc" },
|
|
4488
|
+
{ ctx },
|
|
4489
|
+
);
|
|
4490
|
+
const nameDescendingPage = await tester.run(
|
|
4491
|
+
list${names.pluralPascal}UseCase,
|
|
4492
|
+
{ sortBy: "name", sortDirection: "desc" },
|
|
4493
|
+
{ ctx },
|
|
4494
|
+
);
|
|
3209
4495
|
const created = await tester.run(
|
|
3210
4496
|
create${names.singularPascal}UseCase,
|
|
3211
4497
|
{ name: "First ${names.singularPascal}" },
|
|
@@ -3213,35 +4499,111 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
3213
4499
|
);
|
|
3214
4500
|
const result = await tester.run(
|
|
3215
4501
|
list${names.pluralPascal}UseCase,
|
|
3216
|
-
{
|
|
4502
|
+
{ name: "First" },
|
|
3217
4503
|
{ ctx },
|
|
3218
4504
|
);
|
|
3219
4505
|
|
|
4506
|
+
expect(defaultPage.page).toMatchObject({
|
|
4507
|
+
kind: "cursor",
|
|
4508
|
+
limit: 20,
|
|
4509
|
+
cursor: null,
|
|
4510
|
+
nextCursor: null,
|
|
4511
|
+
hasMore: false,
|
|
4512
|
+
});
|
|
4513
|
+
expect(defaultPage.items.map((item) => item.name)).toEqual([
|
|
4514
|
+
"Gamma ${names.singularPascal}",
|
|
4515
|
+
"Beta ${names.singularPascal}",
|
|
4516
|
+
"Alpha ${names.singularPascal}",
|
|
4517
|
+
]);
|
|
4518
|
+
expect(filteredPage.items.map((item) => item.name)).toEqual([
|
|
4519
|
+
"Alpha ${names.singularPascal}",
|
|
4520
|
+
]);
|
|
4521
|
+
expect(firstPage.items.map((item) => item.name)).toEqual([
|
|
4522
|
+
"Gamma ${names.singularPascal}",
|
|
4523
|
+
"Beta ${names.singularPascal}",
|
|
4524
|
+
]);
|
|
4525
|
+
expect(firstPage.page.nextCursor).toEqual(expect.any(String));
|
|
4526
|
+
expect(secondPage.items.map((item) => item.name)).toEqual([
|
|
4527
|
+
"Alpha ${names.singularPascal}",
|
|
4528
|
+
]);
|
|
4529
|
+
expect(secondPage.page.nextCursor).toBeNull();
|
|
4530
|
+
expect(nameAscendingPage.items.map((item) => item.name)).toEqual([
|
|
4531
|
+
"Alpha ${names.singularPascal}",
|
|
4532
|
+
"Beta ${names.singularPascal}",
|
|
4533
|
+
"Gamma ${names.singularPascal}",
|
|
4534
|
+
]);
|
|
4535
|
+
expect(nameDescendingPage.items.map((item) => item.name)).toEqual([
|
|
4536
|
+
"Gamma ${names.singularPascal}",
|
|
4537
|
+
"Beta ${names.singularPascal}",
|
|
4538
|
+
"Alpha ${names.singularPascal}",
|
|
4539
|
+
]);
|
|
3220
4540
|
expect(created.name).toBe("First ${names.singularPascal}");
|
|
3221
|
-
expect(result.page.total).toBe(1);
|
|
3222
4541
|
expect(result.items).toEqual([created]);
|
|
4542
|
+
${mode === "resource"
|
|
4543
|
+
? `
|
|
4544
|
+
const fetched = await tester.run(
|
|
4545
|
+
get${names.singularPascal}UseCase,
|
|
4546
|
+
{ id: created.id },
|
|
4547
|
+
{ ctx },
|
|
4548
|
+
);
|
|
4549
|
+
const updated = await tester.run(
|
|
4550
|
+
update${names.singularPascal}UseCase,
|
|
4551
|
+
{ id: created.id, name: "Updated ${names.singularPascal}", version: fetched.version },
|
|
4552
|
+
{ ctx },
|
|
4553
|
+
);
|
|
4554
|
+
try {
|
|
4555
|
+
await tester.run(
|
|
4556
|
+
update${names.singularPascal}UseCase,
|
|
4557
|
+
{ id: created.id, name: "Stale ${names.singularPascal}", version: fetched.version },
|
|
4558
|
+
{ ctx },
|
|
4559
|
+
);
|
|
4560
|
+
throw new Error("Expected ${names.singularPascal}Conflict");
|
|
4561
|
+
} catch (error) {
|
|
4562
|
+
expect(isAppError(error)).toBe(true);
|
|
4563
|
+
if (isAppError(error)) {
|
|
4564
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_CONFLICT");
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
await tester.run(delete${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
4568
|
+
const afterDelete = await tester.run(
|
|
4569
|
+
list${names.pluralPascal}UseCase,
|
|
4570
|
+
{ name: "Updated" },
|
|
4571
|
+
{ ctx },
|
|
4572
|
+
);
|
|
3223
4573
|
|
|
3224
|
-
|
|
3225
|
-
|
|
4574
|
+
expect(fetched).toEqual(created);
|
|
4575
|
+
expect(updated.name).toBe("Updated ${names.singularPascal}");
|
|
4576
|
+
expect(updated.version).toBe(fetched.version + 1);
|
|
4577
|
+
expect(afterDelete.items).toEqual([]);
|
|
4578
|
+
|
|
4579
|
+
try {
|
|
4580
|
+
await tester.run(get${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
4581
|
+
throw new Error("Expected ${names.singularPascal}NotFound");
|
|
4582
|
+
} catch (error) {
|
|
4583
|
+
expect(isAppError(error)).toBe(true);
|
|
4584
|
+
if (isAppError(error)) {
|
|
4585
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_NOT_FOUND");
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
`
|
|
4589
|
+
: ""}
|
|
4590
|
+
|
|
4591
|
+
const app = await createTestApp({
|
|
4592
|
+
ports: testFixture.ports,
|
|
3226
4593
|
routes: defineRoutes<AppContext>([${names.singularCamel}Routes]),
|
|
3227
|
-
|
|
3228
|
-
mapUnhandledError: () => ({
|
|
3229
|
-
status: 500,
|
|
3230
|
-
body: { code: "INTERNAL_SERVER_ERROR", message: "Internal server error" },
|
|
3231
|
-
}),
|
|
4594
|
+
context: appContext,
|
|
3232
4595
|
});
|
|
3233
|
-
|
|
3234
|
-
const createdViaRoute = await
|
|
4596
|
+
${options.tenant ? `\t\tconst requester = createTestRequester(app, {\n\t\t\theaders: { "x-tenant-id": "tenant_test" },\n\t\t});\n` : ""}
|
|
4597
|
+
const createdViaRoute = await ${requestTarget}.request(create${names.singularPascal}, {
|
|
3235
4598
|
body: { name: "Second ${names.singularPascal}" },
|
|
3236
4599
|
});
|
|
3237
|
-
const listedViaRoute = await
|
|
3238
|
-
query: {
|
|
4600
|
+
${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}, {
|
|
4601
|
+
query: { name: "${mode === "resource" ? "Updated via route" : "Second"}" },
|
|
3239
4602
|
});
|
|
3240
|
-
await app.stop();
|
|
4603
|
+
${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();
|
|
3241
4604
|
|
|
3242
4605
|
expect(createdViaRoute.name).toBe("Second ${names.singularPascal}");
|
|
3243
|
-
expect(
|
|
3244
|
-
expect(listedViaRoute.items).toContainEqual(createdViaRoute);
|
|
4606
|
+
${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`}
|
|
3245
4607
|
});
|
|
3246
4608
|
});
|
|
3247
4609
|
`;
|