@beignet/cli 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +219 -0
- package/README.md +379 -61
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +49 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +53 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +41 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +78 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +10 -5
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +28 -47
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -603
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1896 -382
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates/base.d.ts +13 -0
- package/dist/templates/base.d.ts.map +1 -0
- package/dist/templates/base.js +300 -0
- package/dist/templates/base.js.map +1 -0
- package/dist/templates/db.d.ts +14 -0
- package/dist/templates/db.d.ts.map +1 -0
- package/dist/templates/db.js +962 -0
- package/dist/templates/db.js.map +1 -0
- package/dist/templates/index.d.ts +14 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +123 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/server.d.ts +26 -0
- package/dist/templates/server.d.ts.map +1 -0
- package/dist/templates/server.js +520 -0
- package/dist/templates/server.js.map +1 -0
- package/dist/templates/shadcn.d.ts +5 -0
- package/dist/templates/shadcn.d.ts.map +1 -0
- package/dist/templates/shadcn.js +555 -0
- package/dist/templates/shadcn.js.map +1 -0
- package/dist/templates/shared.d.ts +49 -0
- package/dist/templates/shared.d.ts.map +1 -0
- package/dist/templates/shared.js +57 -0
- package/dist/templates/shared.js.map +1 -0
- package/dist/templates/shell.d.ts +5 -0
- package/dist/templates/shell.d.ts.map +1 -0
- package/dist/templates/shell.js +1190 -0
- package/dist/templates/shell.js.map +1 -0
- package/dist/templates/todos.d.ts +17 -0
- package/dist/templates/todos.d.ts.map +1 -0
- package/dist/templates/todos.js +607 -0
- package/dist/templates/todos.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +83 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +114 -0
- package/src/create.ts +40 -64
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1075 -817
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2389 -406
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates/base.ts +377 -0
- package/src/templates/db.ts +963 -0
- package/src/templates/index.ts +148 -0
- package/src/templates/server.ts +528 -0
- package/src/templates/shadcn.ts +570 -0
- package/src/templates/shared.ts +90 -0
- package/src/templates/shell.ts +1219 -0
- package/src/templates/todos.ts +607 -0
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/dist/templates.d.ts +0 -55
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -3520
- package/dist/templates.js.map +0 -1
- package/src/create-bin.ts +0 -11
- package/src/templates.ts +0 -3774
package/dist/make.js
CHANGED
|
@@ -1,31 +1,54 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
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
|
-
];
|
|
5
|
+
export { makeFeatureAddonChoices } from "./choices.js";
|
|
15
6
|
export async function makeResource(options) {
|
|
7
|
+
return makeResourceSlice(options, "resource");
|
|
8
|
+
}
|
|
9
|
+
export async function makeFeature(options) {
|
|
10
|
+
const addons = normalizeMakeFeatureAddons(options.with ?? []);
|
|
11
|
+
if (addons.length === 0) {
|
|
12
|
+
return makeFeatureResource(options);
|
|
13
|
+
}
|
|
14
|
+
if (options.dryRun) {
|
|
15
|
+
return makeFeatureSlice(options, addons);
|
|
16
|
+
}
|
|
17
|
+
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
18
|
+
return makeFeatureSlice(options, addons);
|
|
19
|
+
}
|
|
20
|
+
async function makeFeatureResource(options) {
|
|
21
|
+
try {
|
|
22
|
+
return await makeResourceSlice(options, "feature");
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error instanceof Error && error.message.includes("make resource")) {
|
|
26
|
+
throw new Error(error.message.replaceAll("make resource", "make feature"));
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function makeResourceSlice(options, mode) {
|
|
16
32
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
17
33
|
const names = resourceNames(options.name);
|
|
18
34
|
const config = options.config
|
|
19
35
|
? resolveConfig(options.config)
|
|
20
36
|
: await loadBeignetConfig(targetDir);
|
|
21
37
|
await assertStandardApp(targetDir, config);
|
|
38
|
+
if (mode === "resource" &&
|
|
39
|
+
!(await fileExists(path.join(targetDir, resourceSharedErrorsPath(config))))) {
|
|
40
|
+
throw new Error(`beignet make resource expects an app error catalog. Missing ${resourceSharedErrorsPath(config)}.`);
|
|
41
|
+
}
|
|
22
42
|
const persistence = await detectResourcePersistence(targetDir, config);
|
|
23
|
-
const
|
|
43
|
+
const generationOptions = resourceGenerationOptions(options, mode);
|
|
44
|
+
const generatedFiles = resourceFiles(names, config, persistence, mode, generationOptions);
|
|
24
45
|
const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
|
|
25
46
|
force: Boolean(options.force),
|
|
26
47
|
});
|
|
27
48
|
const plannedWiringUpdates = await updateResourceWiring(targetDir, names, config, persistence, {
|
|
28
49
|
dryRun: true,
|
|
50
|
+
mode,
|
|
51
|
+
generationOptions,
|
|
29
52
|
});
|
|
30
53
|
const createdFiles = plannedFiles
|
|
31
54
|
.filter((file) => file.result === "created")
|
|
@@ -46,9 +69,12 @@ export async function makeResource(options) {
|
|
|
46
69
|
updatedFiles.push(...(await updateResourceWiring(targetDir, names, config, {
|
|
47
70
|
persistence,
|
|
48
71
|
dryRun: false,
|
|
72
|
+
mode,
|
|
73
|
+
generationOptions,
|
|
49
74
|
})));
|
|
50
75
|
}
|
|
51
76
|
return {
|
|
77
|
+
schemaVersion: 1,
|
|
52
78
|
name: names.pluralKebab,
|
|
53
79
|
targetDir,
|
|
54
80
|
dryRun: Boolean(options.dryRun),
|
|
@@ -58,27 +84,16 @@ export async function makeResource(options) {
|
|
|
58
84
|
skippedFiles,
|
|
59
85
|
};
|
|
60
86
|
}
|
|
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;
|
|
87
|
+
function resourceGenerationOptions(options, mode) {
|
|
88
|
+
if (mode !== "resource") {
|
|
89
|
+
return { auth: false, tenant: false, events: false, softDelete: false };
|
|
81
90
|
}
|
|
91
|
+
return {
|
|
92
|
+
auth: Boolean(options.auth),
|
|
93
|
+
tenant: Boolean(options.tenant),
|
|
94
|
+
events: Boolean(options.events),
|
|
95
|
+
softDelete: Boolean(options.softDelete),
|
|
96
|
+
};
|
|
82
97
|
}
|
|
83
98
|
async function makeFeatureSlice(options, addons) {
|
|
84
99
|
let result = await makeFeatureResource(options);
|
|
@@ -97,6 +112,9 @@ async function makeFeatureAddon(addon, feature, options) {
|
|
|
97
112
|
if (addon === "policy") {
|
|
98
113
|
return makePolicy({ ...shared, name: feature });
|
|
99
114
|
}
|
|
115
|
+
if (addon === "task") {
|
|
116
|
+
return makeTask({ ...shared, name: `${feature}/backfill` });
|
|
117
|
+
}
|
|
100
118
|
if (addon === "event") {
|
|
101
119
|
return makeEvent({ ...shared, name: `${feature}/created` });
|
|
102
120
|
}
|
|
@@ -106,6 +124,9 @@ async function makeFeatureAddon(addon, feature, options) {
|
|
|
106
124
|
if (addon === "notification") {
|
|
107
125
|
return makeNotification({ ...shared, name: `${feature}/created` });
|
|
108
126
|
}
|
|
127
|
+
if (addon === "ui") {
|
|
128
|
+
return makeFeatureUi({ ...shared, name: feature });
|
|
129
|
+
}
|
|
109
130
|
return makeUpload({ ...shared, name: `${feature}/attachment` });
|
|
110
131
|
}
|
|
111
132
|
function normalizeMakeFeatureAddons(addons) {
|
|
@@ -113,6 +134,8 @@ function normalizeMakeFeatureAddons(addons) {
|
|
|
113
134
|
for (const addon of addons) {
|
|
114
135
|
if (addon === "events")
|
|
115
136
|
normalized.add("event");
|
|
137
|
+
else if (addon === "tasks")
|
|
138
|
+
normalized.add("task");
|
|
116
139
|
else if (addon === "jobs")
|
|
117
140
|
normalized.add("job");
|
|
118
141
|
else if (addon === "notifications")
|
|
@@ -126,9 +149,11 @@ function normalizeMakeFeatureAddons(addons) {
|
|
|
126
149
|
}
|
|
127
150
|
const featureAddonOrder = [
|
|
128
151
|
"policy",
|
|
152
|
+
"task",
|
|
129
153
|
"event",
|
|
130
154
|
"job",
|
|
131
155
|
"notification",
|
|
156
|
+
"ui",
|
|
132
157
|
"upload",
|
|
133
158
|
];
|
|
134
159
|
function mergeMakeResults(left, right) {
|
|
@@ -166,6 +191,7 @@ export async function makeContract(options) {
|
|
|
166
191
|
skippedFiles.push(file.path);
|
|
167
192
|
}
|
|
168
193
|
return {
|
|
194
|
+
schemaVersion: 1,
|
|
169
195
|
name: names.pluralKebab,
|
|
170
196
|
targetDir,
|
|
171
197
|
dryRun: Boolean(options.dryRun),
|
|
@@ -209,6 +235,7 @@ export async function makeUseCase(options) {
|
|
|
209
235
|
if (indexResult === "skipped")
|
|
210
236
|
skippedFiles.push(indexFile.path);
|
|
211
237
|
return {
|
|
238
|
+
schemaVersion: 1,
|
|
212
239
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
213
240
|
targetDir,
|
|
214
241
|
dryRun: Boolean(options.dryRun),
|
|
@@ -247,6 +274,7 @@ export async function makeTest(options) {
|
|
|
247
274
|
updatedFiles.push("package.json");
|
|
248
275
|
}
|
|
249
276
|
return {
|
|
277
|
+
schemaVersion: 1,
|
|
250
278
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
251
279
|
targetDir,
|
|
252
280
|
dryRun: Boolean(options.dryRun),
|
|
@@ -298,6 +326,7 @@ export async function makePort(options) {
|
|
|
298
326
|
updatedFiles.push(config.paths.infrastructurePorts);
|
|
299
327
|
}
|
|
300
328
|
return {
|
|
329
|
+
schemaVersion: 1,
|
|
301
330
|
name: names.kebab,
|
|
302
331
|
targetDir,
|
|
303
332
|
dryRun: Boolean(options.dryRun),
|
|
@@ -336,6 +365,7 @@ export async function makeAdapter(options) {
|
|
|
336
365
|
updatedFiles.push(config.paths.infrastructurePorts);
|
|
337
366
|
}
|
|
338
367
|
return {
|
|
368
|
+
schemaVersion: 1,
|
|
339
369
|
name: names.kebab,
|
|
340
370
|
targetDir,
|
|
341
371
|
dryRun: Boolean(options.dryRun),
|
|
@@ -369,6 +399,7 @@ export async function makePolicy(options) {
|
|
|
369
399
|
skippedFiles.push(file.path);
|
|
370
400
|
}
|
|
371
401
|
return {
|
|
402
|
+
schemaVersion: 1,
|
|
372
403
|
name: names.kebab,
|
|
373
404
|
targetDir,
|
|
374
405
|
dryRun: Boolean(options.dryRun),
|
|
@@ -411,13 +442,52 @@ export async function makeJob(options) {
|
|
|
411
442
|
force: Boolean(options.force),
|
|
412
443
|
dryRun: Boolean(options.dryRun),
|
|
413
444
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
414
|
-
files:
|
|
445
|
+
files: [
|
|
446
|
+
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
447
|
+
...jobFiles(names, config),
|
|
448
|
+
],
|
|
415
449
|
index: featureArtifactIndexFile("job", names, config),
|
|
416
450
|
dependencies: {
|
|
417
451
|
"@beignet/core": beignetDependencyVersion,
|
|
418
452
|
},
|
|
419
453
|
});
|
|
420
454
|
}
|
|
455
|
+
export async function makeTask(options) {
|
|
456
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
457
|
+
const names = taskNames(options.name);
|
|
458
|
+
const config = options.config
|
|
459
|
+
? resolveConfig(options.config)
|
|
460
|
+
: await loadBeignetConfig(targetDir);
|
|
461
|
+
await assertFeatureArtifactApp(targetDir, config, "task");
|
|
462
|
+
const result = await makeFeatureArtifact({
|
|
463
|
+
targetDir,
|
|
464
|
+
config,
|
|
465
|
+
force: Boolean(options.force),
|
|
466
|
+
dryRun: Boolean(options.dryRun),
|
|
467
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
468
|
+
files: [
|
|
469
|
+
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
470
|
+
...taskFiles(names, config),
|
|
471
|
+
],
|
|
472
|
+
index: featureArtifactIndexFile("task", names, config),
|
|
473
|
+
dependencies: {
|
|
474
|
+
"@beignet/core": beignetDependencyVersion,
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
const registryResult = await updateTaskRegistry(targetDir, names, config, {
|
|
478
|
+
dryRun: Boolean(options.dryRun),
|
|
479
|
+
});
|
|
480
|
+
if (registryResult === "created")
|
|
481
|
+
result.createdFiles.push(config.paths.tasks);
|
|
482
|
+
if (registryResult === "updated")
|
|
483
|
+
result.updatedFiles.push(config.paths.tasks);
|
|
484
|
+
if (registryResult === "skipped")
|
|
485
|
+
result.skippedFiles.push(config.paths.tasks);
|
|
486
|
+
if (!result.files.includes(config.paths.tasks)) {
|
|
487
|
+
result.files.push(config.paths.tasks);
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
421
491
|
export async function makeFactory(options) {
|
|
422
492
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
423
493
|
const names = factoryNames(options.name);
|
|
@@ -471,7 +541,10 @@ export async function makeNotification(options) {
|
|
|
471
541
|
force: Boolean(options.force),
|
|
472
542
|
dryRun: Boolean(options.dryRun),
|
|
473
543
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
474
|
-
files:
|
|
544
|
+
files: [
|
|
545
|
+
...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
|
|
546
|
+
...notificationFiles(names, config),
|
|
547
|
+
],
|
|
475
548
|
index: featureArtifactIndexFile("notification", names, config),
|
|
476
549
|
dependencies: {
|
|
477
550
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -492,7 +565,10 @@ export async function makeListener(options) {
|
|
|
492
565
|
force: Boolean(options.force),
|
|
493
566
|
dryRun: Boolean(options.dryRun),
|
|
494
567
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
495
|
-
files:
|
|
568
|
+
files: [
|
|
569
|
+
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
570
|
+
...listenerFiles(names, config),
|
|
571
|
+
],
|
|
496
572
|
index: featureArtifactIndexFile("listener", names, config),
|
|
497
573
|
dependencies: {
|
|
498
574
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -520,8 +596,16 @@ export async function makeSchedule(options) {
|
|
|
520
596
|
await assertFeatureArtifactApp(targetDir, config, "schedule", {
|
|
521
597
|
requireServer: Boolean(options.route),
|
|
522
598
|
});
|
|
523
|
-
|
|
524
|
-
|
|
599
|
+
// Plan the CRON_SECRET wiring before writing any files so an anchor miss
|
|
600
|
+
// in lib/env.ts aborts the generator without leaving partial output.
|
|
601
|
+
const cronSecretPlan = options.route
|
|
602
|
+
? await planCronSecretWiring(targetDir)
|
|
603
|
+
: undefined;
|
|
604
|
+
const files = [
|
|
605
|
+
...(await missingCapabilityBuilderFiles(targetDir, "schedules", config)),
|
|
606
|
+
...scheduleFiles(names, config, { route: Boolean(options.route) }),
|
|
607
|
+
];
|
|
608
|
+
const result = await makeFeatureArtifact({
|
|
525
609
|
targetDir,
|
|
526
610
|
config,
|
|
527
611
|
force: Boolean(options.force),
|
|
@@ -533,6 +617,100 @@ export async function makeSchedule(options) {
|
|
|
533
617
|
"@beignet/core": beignetDependencyVersion,
|
|
534
618
|
},
|
|
535
619
|
});
|
|
620
|
+
const registryResult = await updateScheduleRegistry(targetDir, names, config, { dryRun: Boolean(options.dryRun) });
|
|
621
|
+
if (registryResult === "created")
|
|
622
|
+
result.createdFiles.push(config.paths.schedules);
|
|
623
|
+
if (registryResult === "updated")
|
|
624
|
+
result.updatedFiles.push(config.paths.schedules);
|
|
625
|
+
if (registryResult === "skipped")
|
|
626
|
+
result.skippedFiles.push(config.paths.schedules);
|
|
627
|
+
if (!result.files.includes(config.paths.schedules)) {
|
|
628
|
+
result.files.push(config.paths.schedules);
|
|
629
|
+
}
|
|
630
|
+
if (cronSecretPlan) {
|
|
631
|
+
const applied = await applyCronSecretWiring(targetDir, cronSecretPlan, {
|
|
632
|
+
dryRun: Boolean(options.dryRun),
|
|
633
|
+
});
|
|
634
|
+
result.createdFiles.push(...applied.createdFiles);
|
|
635
|
+
result.updatedFiles.push(...applied.updatedFiles);
|
|
636
|
+
result.skippedFiles.push(...applied.skippedFiles);
|
|
637
|
+
}
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
const cronSecretEnvFile = "lib/env.ts";
|
|
641
|
+
const cronSecretEnvEntry = `\t\tCRON_SECRET: z.string().min(1).optional(),`;
|
|
642
|
+
// The literal APP_URL line every generated lib/env.ts ships with. CRON_SECRET
|
|
643
|
+
// slots in directly below it, matching the documented env shape.
|
|
644
|
+
const cronSecretEnvAnchor = `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`;
|
|
645
|
+
async function planCronSecretWiring(targetDir) {
|
|
646
|
+
const plan = { files: [], skippedFiles: [] };
|
|
647
|
+
const manualInstruction = `Add ${cronSecretEnvEntry.trim()} to the createEnv server block in ${cronSecretEnvFile} manually, or restore the generated env file before running make schedule --route.`;
|
|
648
|
+
const envSource = await readOptionalFile(path.join(targetDir, cronSecretEnvFile));
|
|
649
|
+
if (envSource === undefined) {
|
|
650
|
+
throw new Error(`Could not find the generated env file ${cronSecretEnvFile}. ${manualInstruction}`);
|
|
651
|
+
}
|
|
652
|
+
if (/\bCRON_SECRET\b/.test(envSource)) {
|
|
653
|
+
plan.skippedFiles.push(cronSecretEnvFile);
|
|
654
|
+
}
|
|
655
|
+
else if (!envSource.includes(cronSecretEnvAnchor)) {
|
|
656
|
+
throw new Error(`Could not find the generated APP_URL server entry in ${cronSecretEnvFile}. ${manualInstruction}`);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
plan.files.push({
|
|
660
|
+
path: cronSecretEnvFile,
|
|
661
|
+
content: envSource.replace(cronSecretEnvAnchor, `${cronSecretEnvAnchor}\n${cronSecretEnvEntry}`),
|
|
662
|
+
result: "updated",
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
const secretLine = `CRON_SECRET=${randomBytes(16).toString("hex")}`;
|
|
666
|
+
const envExample = await readOptionalFile(path.join(targetDir, ".env.example"));
|
|
667
|
+
if (envExample === undefined) {
|
|
668
|
+
plan.files.push({
|
|
669
|
+
path: ".env.example",
|
|
670
|
+
content: `${secretLine}\n`,
|
|
671
|
+
result: "created",
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
else if (/^CRON_SECRET=/m.test(envExample)) {
|
|
675
|
+
plan.skippedFiles.push(".env.example");
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
plan.files.push({
|
|
679
|
+
path: ".env.example",
|
|
680
|
+
content: `${envExample.replace(/\n*$/, "\n")}${secretLine}\n`,
|
|
681
|
+
result: "updated",
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const envLocal = await readOptionalFile(path.join(targetDir, ".env.local"));
|
|
685
|
+
if (envLocal !== undefined) {
|
|
686
|
+
if (/^CRON_SECRET=/m.test(envLocal)) {
|
|
687
|
+
plan.skippedFiles.push(".env.local");
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
plan.files.push({
|
|
691
|
+
path: ".env.local",
|
|
692
|
+
content: `${envLocal.replace(/\n*$/, "\n")}${secretLine}\n`,
|
|
693
|
+
result: "updated",
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return plan;
|
|
698
|
+
}
|
|
699
|
+
async function applyCronSecretWiring(targetDir, plan, options) {
|
|
700
|
+
if (!options.dryRun) {
|
|
701
|
+
for (const file of plan.files) {
|
|
702
|
+
await writeFile(path.join(targetDir, file.path), file.content);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
createdFiles: plan.files
|
|
707
|
+
.filter((file) => file.result === "created")
|
|
708
|
+
.map((file) => file.path),
|
|
709
|
+
updatedFiles: plan.files
|
|
710
|
+
.filter((file) => file.result === "updated")
|
|
711
|
+
.map((file) => file.path),
|
|
712
|
+
skippedFiles: plan.skippedFiles,
|
|
713
|
+
};
|
|
536
714
|
}
|
|
537
715
|
export async function makeUpload(options) {
|
|
538
716
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
@@ -554,6 +732,58 @@ export async function makeUpload(options) {
|
|
|
554
732
|
},
|
|
555
733
|
});
|
|
556
734
|
}
|
|
735
|
+
async function makeFeatureUi(options) {
|
|
736
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
737
|
+
const names = featureUiNames(options.name);
|
|
738
|
+
const config = options.config
|
|
739
|
+
? resolveConfig(options.config)
|
|
740
|
+
: await loadBeignetConfig(targetDir);
|
|
741
|
+
await assertFeatureUiApp(targetDir, config);
|
|
742
|
+
const componentFiles = featureUiComponentFiles(names, config);
|
|
743
|
+
const indexFile = featureUiIndexFile(names, config);
|
|
744
|
+
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
745
|
+
force: Boolean(options.force),
|
|
746
|
+
});
|
|
747
|
+
const createdFiles = plannedFiles
|
|
748
|
+
.filter((file) => file.result === "created")
|
|
749
|
+
.map((file) => file.path);
|
|
750
|
+
const updatedFiles = plannedFiles
|
|
751
|
+
.filter((file) => file.result === "updated")
|
|
752
|
+
.map((file) => file.path);
|
|
753
|
+
const skippedFiles = plannedFiles
|
|
754
|
+
.filter((file) => file.result === "skipped")
|
|
755
|
+
.map((file) => file.path);
|
|
756
|
+
if (!options.dryRun) {
|
|
757
|
+
for (const file of plannedFiles) {
|
|
758
|
+
await writePlannedGeneratedFile(targetDir, file);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const indexResult = await updateFeatureUiIndex(targetDir, indexFile, {
|
|
762
|
+
dryRun: Boolean(options.dryRun),
|
|
763
|
+
});
|
|
764
|
+
if (indexResult === "created")
|
|
765
|
+
createdFiles.push(indexFile.path);
|
|
766
|
+
if (indexResult === "updated")
|
|
767
|
+
updatedFiles.push(indexFile.path);
|
|
768
|
+
if (indexResult === "skipped")
|
|
769
|
+
skippedFiles.push(indexFile.path);
|
|
770
|
+
if (await updatePackageDependencies(targetDir, {
|
|
771
|
+
"@beignet/react-query": beignetDependencyVersion,
|
|
772
|
+
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
773
|
+
}, { dryRun: Boolean(options.dryRun) })) {
|
|
774
|
+
updatedFiles.push("package.json");
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
schemaVersion: 1,
|
|
778
|
+
name: names.pluralKebab,
|
|
779
|
+
targetDir,
|
|
780
|
+
dryRun: Boolean(options.dryRun),
|
|
781
|
+
files: [...componentFiles.map((file) => file.path), indexFile.path],
|
|
782
|
+
createdFiles,
|
|
783
|
+
updatedFiles,
|
|
784
|
+
skippedFiles,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
557
787
|
async function assertStandardApp(targetDir, config) {
|
|
558
788
|
const requiredFiles = [
|
|
559
789
|
config.paths.appContext,
|
|
@@ -646,6 +876,18 @@ async function assertFeatureArtifactApp(targetDir, config, artifact, options = {
|
|
|
646
876
|
}
|
|
647
877
|
}
|
|
648
878
|
}
|
|
879
|
+
async function assertFeatureUiApp(targetDir, config) {
|
|
880
|
+
const clientPath = clientIndexPath(config);
|
|
881
|
+
const requiredFiles = [config.paths.appContext, clientPath];
|
|
882
|
+
for (const file of requiredFiles) {
|
|
883
|
+
try {
|
|
884
|
+
await stat(path.join(targetDir, file));
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
throw new Error(`beignet make feature --with ui expects a standard Beignet app with React Query client helpers. Missing ${file}.`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
649
891
|
async function assertPersistenceArtifactApp(targetDir, names, config, artifact) {
|
|
650
892
|
const requiredFiles = [
|
|
651
893
|
config.paths.appContext,
|
|
@@ -712,6 +954,62 @@ async function readOptionalFile(filePath) {
|
|
|
712
954
|
return undefined;
|
|
713
955
|
}
|
|
714
956
|
}
|
|
957
|
+
const capabilityBuilders = {
|
|
958
|
+
listeners: {
|
|
959
|
+
factoryName: "createListeners",
|
|
960
|
+
defineName: "defineListener",
|
|
961
|
+
module: "@beignet/core/events",
|
|
962
|
+
},
|
|
963
|
+
jobs: {
|
|
964
|
+
factoryName: "createJobs",
|
|
965
|
+
defineName: "defineJob",
|
|
966
|
+
module: "@beignet/core/jobs",
|
|
967
|
+
},
|
|
968
|
+
schedules: {
|
|
969
|
+
factoryName: "createSchedules",
|
|
970
|
+
defineName: "defineSchedule",
|
|
971
|
+
module: "@beignet/core/schedules",
|
|
972
|
+
},
|
|
973
|
+
notifications: {
|
|
974
|
+
factoryName: "createNotifications",
|
|
975
|
+
defineName: "defineNotification",
|
|
976
|
+
module: "@beignet/core/notifications",
|
|
977
|
+
},
|
|
978
|
+
tasks: {
|
|
979
|
+
factoryName: "createTasks",
|
|
980
|
+
defineName: "defineTask",
|
|
981
|
+
module: "@beignet/core/tasks",
|
|
982
|
+
},
|
|
983
|
+
};
|
|
984
|
+
function capabilityBuilderPath(capability, config) {
|
|
985
|
+
if (capability === "listeners")
|
|
986
|
+
return config.paths.listenersBuilder;
|
|
987
|
+
if (capability === "jobs")
|
|
988
|
+
return config.paths.jobsBuilder;
|
|
989
|
+
if (capability === "schedules")
|
|
990
|
+
return config.paths.schedulesBuilder;
|
|
991
|
+
if (capability === "notifications")
|
|
992
|
+
return config.paths.notificationsBuilder;
|
|
993
|
+
return config.paths.tasksBuilder;
|
|
994
|
+
}
|
|
995
|
+
function capabilityBuilderFile(capability, config) {
|
|
996
|
+
const builder = capabilityBuilders[capability];
|
|
997
|
+
return {
|
|
998
|
+
path: capabilityBuilderPath(capability, config),
|
|
999
|
+
content: `import { ${builder.factoryName} } from "${builder.module}";
|
|
1000
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
1001
|
+
|
|
1002
|
+
export const { ${builder.defineName} } = ${builder.factoryName}<AppContext>();
|
|
1003
|
+
`,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
async function missingCapabilityBuilderFiles(targetDir, capability, config) {
|
|
1007
|
+
const file = capabilityBuilderFile(capability, config);
|
|
1008
|
+
if (await fileExists(path.join(targetDir, file.path))) {
|
|
1009
|
+
return [];
|
|
1010
|
+
}
|
|
1011
|
+
return [file];
|
|
1012
|
+
}
|
|
715
1013
|
async function makeFeatureArtifact(options) {
|
|
716
1014
|
const plannedFiles = await planGeneratedFiles(options.targetDir, options.files, {
|
|
717
1015
|
force: options.force,
|
|
@@ -743,6 +1041,7 @@ async function makeFeatureArtifact(options) {
|
|
|
743
1041
|
updatedFiles.push("package.json");
|
|
744
1042
|
}
|
|
745
1043
|
return {
|
|
1044
|
+
schemaVersion: 1,
|
|
746
1045
|
name: options.name,
|
|
747
1046
|
targetDir: options.targetDir,
|
|
748
1047
|
dryRun: options.dryRun,
|
|
@@ -772,6 +1071,9 @@ function beignetDependencyVersion(packageJson) {
|
|
|
772
1071
|
packageJson.dependencies?.["@beignet/next"] ??
|
|
773
1072
|
"*");
|
|
774
1073
|
}
|
|
1074
|
+
function tanstackReactQueryDependencyVersion(packageJson) {
|
|
1075
|
+
return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
|
|
1076
|
+
}
|
|
775
1077
|
async function updateResourceWiring(targetDir, names, config, persistenceOrOptions, maybeOptions) {
|
|
776
1078
|
const persistence = typeof persistenceOrOptions === "string"
|
|
777
1079
|
? persistenceOrOptions
|
|
@@ -779,6 +1081,14 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
779
1081
|
const options = typeof persistenceOrOptions === "string"
|
|
780
1082
|
? maybeOptions
|
|
781
1083
|
: persistenceOrOptions;
|
|
1084
|
+
const mode = typeof persistenceOrOptions === "string"
|
|
1085
|
+
? (maybeOptions?.mode ?? "feature")
|
|
1086
|
+
: (persistenceOrOptions.mode ?? "feature");
|
|
1087
|
+
const generationOptions = typeof persistenceOrOptions === "string"
|
|
1088
|
+
? (maybeOptions?.generationOptions ??
|
|
1089
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode))
|
|
1090
|
+
: (persistenceOrOptions.generationOptions ??
|
|
1091
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode));
|
|
782
1092
|
const updated = new Set();
|
|
783
1093
|
if (await updatePackageJson(targetDir, {
|
|
784
1094
|
...options,
|
|
@@ -786,13 +1096,24 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
786
1096
|
})) {
|
|
787
1097
|
updated.add("package.json");
|
|
788
1098
|
}
|
|
789
|
-
if (await updatePortsIndex(targetDir, names, config,
|
|
1099
|
+
if (await updatePortsIndex(targetDir, names, config, {
|
|
1100
|
+
...options,
|
|
1101
|
+
generationOptions,
|
|
1102
|
+
})) {
|
|
790
1103
|
updated.add(config.paths.ports);
|
|
791
1104
|
}
|
|
792
1105
|
if (persistence === "memory" &&
|
|
793
1106
|
(await updateInfrastructurePorts(targetDir, names, config, options))) {
|
|
794
1107
|
updated.add(config.paths.infrastructurePorts);
|
|
795
1108
|
}
|
|
1109
|
+
if (generationOptions.auth &&
|
|
1110
|
+
(await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
|
|
1111
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1112
|
+
}
|
|
1113
|
+
if (persistence === "drizzle" &&
|
|
1114
|
+
(await updateInfrastructureDeferredPorts(targetDir, names, config, options))) {
|
|
1115
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1116
|
+
}
|
|
796
1117
|
if (persistence === "drizzle" &&
|
|
797
1118
|
(await updateDrizzleSchemaIndex(targetDir, names, config, options))) {
|
|
798
1119
|
updated.add(drizzleSchemaIndexPath(config));
|
|
@@ -805,7 +1126,10 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
805
1126
|
(await updateServerTransactionPorts(targetDir, names, config, options))) {
|
|
806
1127
|
updated.add(config.paths.server);
|
|
807
1128
|
}
|
|
808
|
-
if (await
|
|
1129
|
+
if (await updateSharedErrors(targetDir, names, config, { ...options, mode })) {
|
|
1130
|
+
updated.add(resourceSharedErrorsPath(config));
|
|
1131
|
+
}
|
|
1132
|
+
if (await updateOpenApiRoute(targetDir, names, config, { ...options, mode })) {
|
|
809
1133
|
updated.add(config.paths.openapiRoute);
|
|
810
1134
|
}
|
|
811
1135
|
const routesFile = await updateServerRoutes(targetDir, names, config, options);
|
|
@@ -840,6 +1164,16 @@ async function updatePortsIndex(targetDir, names, config, options) {
|
|
|
840
1164
|
if (!next.includes(importLine)) {
|
|
841
1165
|
next = insertAfterImports(next, importLine);
|
|
842
1166
|
}
|
|
1167
|
+
if (options.generationOptions?.auth) {
|
|
1168
|
+
const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
|
|
1169
|
+
const policyImportLine = `import type { AuthorizationContext as ${names.singularPascal}AuthorizationContext, ${names.singularCamel}Policy } from "${relativeModule(config.paths.ports, policyPath)}";`;
|
|
1170
|
+
if (!next.includes(policyImportLine)) {
|
|
1171
|
+
next = insertAfterImports(next, policyImportLine);
|
|
1172
|
+
}
|
|
1173
|
+
next = appendUnionTypeMember(next, "AppAuthorizationContext", `${names.singularPascal}AuthorizationContext`);
|
|
1174
|
+
next = appendTupleTypeMember(next, "AppGate", `typeof ${names.singularCamel}Policy`);
|
|
1175
|
+
next = appendGatePolicyMember(next, `typeof ${names.singularCamel}Policy`);
|
|
1176
|
+
}
|
|
843
1177
|
const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
|
|
844
1178
|
if (!next.includes(entry.trim())) {
|
|
845
1179
|
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 +1268,7 @@ async function updateInfrastructurePorts(targetDir, names, config, options) {
|
|
|
934
1268
|
}
|
|
935
1269
|
const repositoryConst = `const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository();`;
|
|
936
1270
|
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.`);
|
|
1271
|
+
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
1272
|
}
|
|
939
1273
|
const entry = `\t${names.pluralCamel},`;
|
|
940
1274
|
const definePortsBody = definePortsBodySource(next);
|
|
@@ -954,6 +1288,22 @@ async function updateInfrastructurePorts(targetDir, names, config, options) {
|
|
|
954
1288
|
await writeFile(filePath, next);
|
|
955
1289
|
return true;
|
|
956
1290
|
}
|
|
1291
|
+
async function updateInfrastructureGatePolicies(targetDir, names, config, options) {
|
|
1292
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
1293
|
+
const original = await readFile(filePath, "utf8");
|
|
1294
|
+
let next = original;
|
|
1295
|
+
const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
|
|
1296
|
+
const importLine = `import { ${names.singularCamel}Policy } from "${relativeModule(config.paths.infrastructurePorts, policyPath)}";`;
|
|
1297
|
+
if (!next.includes(importLine)) {
|
|
1298
|
+
next = insertAfterImports(next, importLine);
|
|
1299
|
+
}
|
|
1300
|
+
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.`);
|
|
1301
|
+
if (next === original)
|
|
1302
|
+
return false;
|
|
1303
|
+
if (!options.dryRun)
|
|
1304
|
+
await writeFile(filePath, next);
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
957
1307
|
async function updateDrizzleSchemaIndex(targetDir, names, config, options) {
|
|
958
1308
|
const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
|
|
959
1309
|
if (!(await fileExists(filePath)))
|
|
@@ -992,6 +1342,19 @@ async function updateDrizzleRepositories(targetDir, names, config, options) {
|
|
|
992
1342
|
await writeFile(filePath, next);
|
|
993
1343
|
return true;
|
|
994
1344
|
}
|
|
1345
|
+
async function updateInfrastructureDeferredPorts(targetDir, names, config, options) {
|
|
1346
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
1347
|
+
const original = await readFile(filePath, "utf8");
|
|
1348
|
+
// Drizzle resources are provided by the app database provider at startup,
|
|
1349
|
+
// so deferred-form ports files defer the new repository key. Identity-form
|
|
1350
|
+
// ports files are left untouched.
|
|
1351
|
+
const next = appendDeferredPortKey(original, names.pluralCamel);
|
|
1352
|
+
if (next === original)
|
|
1353
|
+
return false;
|
|
1354
|
+
if (!options.dryRun)
|
|
1355
|
+
await writeFile(filePath, next);
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
995
1358
|
async function updateServerTransactionPorts(targetDir, names, config, options) {
|
|
996
1359
|
const filePath = path.join(targetDir, config.paths.server);
|
|
997
1360
|
const original = await readFile(filePath, "utf8");
|
|
@@ -1020,13 +1383,12 @@ async function updateOpenApiRoute(targetDir, names, config, options) {
|
|
|
1020
1383
|
const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
|
|
1021
1384
|
if (!firstArg?.text.startsWith("["))
|
|
1022
1385
|
return false;
|
|
1023
|
-
const
|
|
1024
|
-
const createName = `create${names.singularPascal}`;
|
|
1386
|
+
const contractNames = resourceContractNames(names, options.mode);
|
|
1025
1387
|
const listedContracts = identifiersFromArrayExpression(firstArg.text);
|
|
1026
|
-
const missingContracts =
|
|
1388
|
+
const missingContracts = contractNames.filter((contractName) => !listedContracts.has(contractName));
|
|
1027
1389
|
if (missingContracts.length === 0)
|
|
1028
1390
|
return false;
|
|
1029
|
-
const importLine = `import {
|
|
1391
|
+
const importLine = `import { ${contractNames.join(", ")} } from "${aliasModule(resourceContractFilePath(names, config))}";`;
|
|
1030
1392
|
const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
|
|
1031
1393
|
let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(firstArg.end)}`;
|
|
1032
1394
|
if (!next.includes(importLine)) {
|
|
@@ -1038,6 +1400,53 @@ async function updateOpenApiRoute(targetDir, names, config, options) {
|
|
|
1038
1400
|
await writeFile(filePath, next);
|
|
1039
1401
|
return true;
|
|
1040
1402
|
}
|
|
1403
|
+
async function updateSharedErrors(targetDir, names, config, options) {
|
|
1404
|
+
if (options.mode !== "resource")
|
|
1405
|
+
return false;
|
|
1406
|
+
const filePath = path.join(targetDir, resourceSharedErrorsPath(config));
|
|
1407
|
+
if (!(await fileExists(filePath)))
|
|
1408
|
+
return false;
|
|
1409
|
+
const original = await readFile(filePath, "utf8");
|
|
1410
|
+
const notFoundErrorName = `${names.singularPascal}NotFound`;
|
|
1411
|
+
const conflictErrorName = `${names.singularPascal}Conflict`;
|
|
1412
|
+
const missingErrorNames = [notFoundErrorName, conflictErrorName].filter((errorName) => !new RegExp(`\\b${errorName}\\b`).test(original));
|
|
1413
|
+
if (missingErrorNames.length === 0)
|
|
1414
|
+
return false;
|
|
1415
|
+
const expectedErrorNames = missingErrorNames.join(" and ");
|
|
1416
|
+
const defineErrorsStart = original.indexOf("defineErrors(");
|
|
1417
|
+
if (defineErrorsStart === -1) {
|
|
1418
|
+
throw new Error(`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
|
|
1419
|
+
}
|
|
1420
|
+
const openBrace = original.indexOf("{", defineErrorsStart);
|
|
1421
|
+
if (openBrace === -1) {
|
|
1422
|
+
throw new Error(`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
|
|
1423
|
+
}
|
|
1424
|
+
const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
|
|
1425
|
+
if (closeBrace === -1) {
|
|
1426
|
+
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.`);
|
|
1427
|
+
}
|
|
1428
|
+
const entries = [];
|
|
1429
|
+
if (missingErrorNames.includes(notFoundErrorName)) {
|
|
1430
|
+
entries.push(`${notFoundErrorName}: {
|
|
1431
|
+
\t\tcode: "${constantCase(names.singularKebab)}_NOT_FOUND",
|
|
1432
|
+
\t\tstatus: 404,
|
|
1433
|
+
\t\tmessage: "${names.singularPascal} not found",
|
|
1434
|
+
\t},`);
|
|
1435
|
+
}
|
|
1436
|
+
if (missingErrorNames.includes(conflictErrorName)) {
|
|
1437
|
+
entries.push(`${conflictErrorName}: {
|
|
1438
|
+
\t\tcode: "${constantCase(names.singularKebab)}_CONFLICT",
|
|
1439
|
+
\t\tstatus: 409,
|
|
1440
|
+
\t\tmessage: "${names.singularPascal} was changed by another request",
|
|
1441
|
+
\t},`);
|
|
1442
|
+
}
|
|
1443
|
+
const next = insertBeforeClosingBrace(original, openBrace, closeBrace, entries.join("\n"));
|
|
1444
|
+
if (next === original)
|
|
1445
|
+
return false;
|
|
1446
|
+
if (!options.dryRun)
|
|
1447
|
+
await writeFile(filePath, next);
|
|
1448
|
+
return true;
|
|
1449
|
+
}
|
|
1041
1450
|
async function updateServerRoutes(targetDir, names, config, options) {
|
|
1042
1451
|
const filePath = path.join(targetDir, config.paths.server);
|
|
1043
1452
|
const original = await readFile(filePath, "utf8");
|
|
@@ -1299,6 +1708,67 @@ function typeBodySource(source, typeName) {
|
|
|
1299
1708
|
return undefined;
|
|
1300
1709
|
return source.slice(openBrace + 1, closeBrace);
|
|
1301
1710
|
}
|
|
1711
|
+
function appendUnionTypeMember(source, typeName, member) {
|
|
1712
|
+
const match = new RegExp(`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`).exec(source);
|
|
1713
|
+
if (!match)
|
|
1714
|
+
return source;
|
|
1715
|
+
const declaration = match[0];
|
|
1716
|
+
if (new RegExp(`\\b${member}\\b`).test(declaration))
|
|
1717
|
+
return source;
|
|
1718
|
+
const nextDeclaration = declaration.replace(/;\s*$/, ` | ${member};`);
|
|
1719
|
+
return `${source.slice(0, match.index)}${nextDeclaration}${source.slice(match.index + declaration.length)}`;
|
|
1720
|
+
}
|
|
1721
|
+
function appendTupleTypeMember(source, typeName, member) {
|
|
1722
|
+
const match = new RegExp(`export\\s+type\\s+${typeName}\\s*=\\s*`).exec(source);
|
|
1723
|
+
if (!match)
|
|
1724
|
+
return source;
|
|
1725
|
+
const tupleStart = source.indexOf("[", match.index);
|
|
1726
|
+
if (tupleStart === -1)
|
|
1727
|
+
return source;
|
|
1728
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
1729
|
+
if (tupleEnd === -1)
|
|
1730
|
+
return source;
|
|
1731
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
1732
|
+
if (tuple.includes(member))
|
|
1733
|
+
return source;
|
|
1734
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
1735
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
|
|
1736
|
+
}
|
|
1737
|
+
function appendGatePolicyMember(source, member) {
|
|
1738
|
+
const gateMatch = /\bGatePort\s*</.exec(source);
|
|
1739
|
+
if (!gateMatch)
|
|
1740
|
+
return source;
|
|
1741
|
+
const tupleStart = source.indexOf("[", gateMatch.index);
|
|
1742
|
+
if (tupleStart === -1)
|
|
1743
|
+
return source;
|
|
1744
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
1745
|
+
if (tupleEnd === -1)
|
|
1746
|
+
return source;
|
|
1747
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
1748
|
+
if (tuple.includes(member))
|
|
1749
|
+
return source;
|
|
1750
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
1751
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
|
|
1752
|
+
}
|
|
1753
|
+
function appendCreateGatePolicy(source, policy, failureMessage) {
|
|
1754
|
+
const policyMatch = /\bpolicies\s*:\s*\[/.exec(source);
|
|
1755
|
+
if (!policyMatch) {
|
|
1756
|
+
throw new Error(failureMessage);
|
|
1757
|
+
}
|
|
1758
|
+
const arrayStart = source.indexOf("[", policyMatch.index);
|
|
1759
|
+
if (arrayStart === -1) {
|
|
1760
|
+
throw new Error(failureMessage);
|
|
1761
|
+
}
|
|
1762
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
1763
|
+
if (arrayEnd === -1) {
|
|
1764
|
+
throw new Error(failureMessage);
|
|
1765
|
+
}
|
|
1766
|
+
const policies = source.slice(arrayStart, arrayEnd + 1);
|
|
1767
|
+
if (policies.includes(policy))
|
|
1768
|
+
return source;
|
|
1769
|
+
const nextPolicies = appendToArrayExpression(policies, [policy]);
|
|
1770
|
+
return `${source.slice(0, arrayStart)}${nextPolicies}${source.slice(arrayEnd + 1)}`;
|
|
1771
|
+
}
|
|
1302
1772
|
function insertDefinePortsProperty(source, property, message) {
|
|
1303
1773
|
const body = definePortsBodyRange(source);
|
|
1304
1774
|
if (!body)
|
|
@@ -1348,7 +1818,15 @@ function definePortsBodySource(source) {
|
|
|
1348
1818
|
return source.slice(body.openBrace + 1, body.closeBrace);
|
|
1349
1819
|
}
|
|
1350
1820
|
function definePortsBodyRange(source) {
|
|
1351
|
-
const
|
|
1821
|
+
const outer = definePortsOuterBodyRange(source);
|
|
1822
|
+
if (!outer)
|
|
1823
|
+
return undefined;
|
|
1824
|
+
// The curried `definePorts<AppPorts>()({ bound, deferred })` form binds app
|
|
1825
|
+
// ports inside `bound`; generated bindings belong there.
|
|
1826
|
+
return (topLevelBoundObjectRange(source, outer.openBrace, outer.closeBrace) ?? outer);
|
|
1827
|
+
}
|
|
1828
|
+
function definePortsOuterBodyRange(source) {
|
|
1829
|
+
const match = /definePorts(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?\{/.exec(source);
|
|
1352
1830
|
if (!match)
|
|
1353
1831
|
return undefined;
|
|
1354
1832
|
const openBrace = source.indexOf("{", match.index);
|
|
@@ -1357,6 +1835,39 @@ function definePortsBodyRange(source) {
|
|
|
1357
1835
|
return undefined;
|
|
1358
1836
|
return { openBrace, closeBrace };
|
|
1359
1837
|
}
|
|
1838
|
+
function appendDeferredPortKey(source, portKey) {
|
|
1839
|
+
const outer = definePortsOuterBodyRange(source);
|
|
1840
|
+
if (!outer)
|
|
1841
|
+
return source;
|
|
1842
|
+
const body = source.slice(outer.openBrace + 1, outer.closeBrace);
|
|
1843
|
+
if (!hasTopLevelObjectProperty(body, "deferred"))
|
|
1844
|
+
return source;
|
|
1845
|
+
const deferredMatch = /\bdeferred\s*:\s*\[/.exec(body);
|
|
1846
|
+
if (!deferredMatch)
|
|
1847
|
+
return source;
|
|
1848
|
+
const arrayStart = source.indexOf("[", outer.openBrace + 1 + deferredMatch.index);
|
|
1849
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
1850
|
+
if (arrayEnd === -1)
|
|
1851
|
+
return source;
|
|
1852
|
+
const tuple = source.slice(arrayStart, arrayEnd + 1);
|
|
1853
|
+
if (new RegExp(`["']${portKey}["']`).test(tuple))
|
|
1854
|
+
return source;
|
|
1855
|
+
const nextTuple = appendToArrayExpression(tuple, [`"${portKey}"`]);
|
|
1856
|
+
return `${source.slice(0, arrayStart)}${nextTuple}${source.slice(arrayEnd + 1)}`;
|
|
1857
|
+
}
|
|
1858
|
+
function topLevelBoundObjectRange(source, openBrace, closeBrace) {
|
|
1859
|
+
const body = source.slice(openBrace + 1, closeBrace);
|
|
1860
|
+
if (!hasTopLevelObjectProperty(body, "bound"))
|
|
1861
|
+
return undefined;
|
|
1862
|
+
const boundMatch = /\bbound\s*:\s*\{/.exec(body);
|
|
1863
|
+
if (!boundMatch)
|
|
1864
|
+
return undefined;
|
|
1865
|
+
const boundOpen = source.indexOf("{", openBrace + 1 + boundMatch.index);
|
|
1866
|
+
const boundClose = matchingBraceIndex(source, boundOpen);
|
|
1867
|
+
if (boundClose === -1)
|
|
1868
|
+
return undefined;
|
|
1869
|
+
return { openBrace: boundOpen, closeBrace: boundClose };
|
|
1870
|
+
}
|
|
1360
1871
|
function hasTopLevelObjectProperty(source, propertyName) {
|
|
1361
1872
|
let depth = 0;
|
|
1362
1873
|
let inString;
|
|
@@ -1586,6 +2097,18 @@ function resourceNames(input) {
|
|
|
1586
2097
|
]);
|
|
1587
2098
|
return names;
|
|
1588
2099
|
}
|
|
2100
|
+
function featureUiNames(input) {
|
|
2101
|
+
const names = resourceNames(input);
|
|
2102
|
+
const resolvedNames = {
|
|
2103
|
+
...names,
|
|
2104
|
+
componentExportName: `${names.pluralPascal}Panel`,
|
|
2105
|
+
componentFileName: `${names.pluralKebab}-panel`,
|
|
2106
|
+
};
|
|
2107
|
+
assertGeneratedIdentifiers(input, "Feature UI name", [
|
|
2108
|
+
resolvedNames.componentExportName,
|
|
2109
|
+
]);
|
|
2110
|
+
return resolvedNames;
|
|
2111
|
+
}
|
|
1589
2112
|
function featureResourceNames(names) {
|
|
1590
2113
|
return resourceNames(names.feature.kebab);
|
|
1591
2114
|
}
|
|
@@ -1703,6 +2226,18 @@ function jobNames(input) {
|
|
|
1703
2226
|
payloadTypeName: `${jobExportName}Payload`,
|
|
1704
2227
|
};
|
|
1705
2228
|
}
|
|
2229
|
+
function taskNames(input) {
|
|
2230
|
+
const names = featureArtifactNames(input, "Task");
|
|
2231
|
+
const taskExportName = `${names.artifact.camel}Task`;
|
|
2232
|
+
assertGeneratedIdentifiers(input, "Task name", [taskExportName]);
|
|
2233
|
+
return {
|
|
2234
|
+
...names,
|
|
2235
|
+
taskName: names.stableName,
|
|
2236
|
+
taskExportName,
|
|
2237
|
+
inputSchemaName: `${names.artifact.pascal}TaskInputSchema`,
|
|
2238
|
+
inputTypeName: `${names.artifact.pascal}TaskInput`,
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
1706
2241
|
function factoryNames(input) {
|
|
1707
2242
|
const names = featureArtifactNames(input, "Factory");
|
|
1708
2243
|
const factoryExportName = `${names.artifact.camel}Factory`;
|
|
@@ -1901,59 +2436,119 @@ function pascalCase(value) {
|
|
|
1901
2436
|
const camel = camelCase(value);
|
|
1902
2437
|
return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
1903
2438
|
}
|
|
1904
|
-
function
|
|
2439
|
+
function constantCase(value) {
|
|
2440
|
+
return value.replaceAll("-", "_").toUpperCase();
|
|
2441
|
+
}
|
|
2442
|
+
function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
|
|
2443
|
+
auth: false,
|
|
2444
|
+
tenant: false,
|
|
2445
|
+
events: false,
|
|
2446
|
+
softDelete: false,
|
|
2447
|
+
}) {
|
|
1905
2448
|
const useCaseDir = resourceUseCaseDir(names, config);
|
|
1906
2449
|
const infraDir = infrastructureDir(config);
|
|
1907
2450
|
const featureDir = resourceFeatureDir(names, config);
|
|
1908
2451
|
const files = [
|
|
1909
2452
|
{
|
|
1910
2453
|
path: resourceContractFilePath(names, config),
|
|
1911
|
-
content: contractFile(names, config),
|
|
2454
|
+
content: contractFile(names, config, mode, options),
|
|
1912
2455
|
},
|
|
1913
2456
|
{
|
|
1914
|
-
path: path.join(
|
|
1915
|
-
content: schemasFile(names),
|
|
2457
|
+
path: path.join(featureDir, "schemas.ts"),
|
|
2458
|
+
content: schemasFile(names, mode, options),
|
|
1916
2459
|
},
|
|
1917
2460
|
{
|
|
1918
2461
|
path: path.join(useCaseDir, `list-${names.pluralKebab}.ts`),
|
|
1919
|
-
content: listUseCaseFile(names, config),
|
|
2462
|
+
content: listUseCaseFile(names, config, options),
|
|
1920
2463
|
},
|
|
1921
2464
|
{
|
|
1922
2465
|
path: path.join(useCaseDir, `create-${names.singularKebab}.ts`),
|
|
1923
|
-
content: createUseCaseFile(names, config),
|
|
2466
|
+
content: createUseCaseFile(names, config, options),
|
|
1924
2467
|
},
|
|
2468
|
+
...(mode === "resource"
|
|
2469
|
+
? [
|
|
2470
|
+
{
|
|
2471
|
+
path: path.join(useCaseDir, `get-${names.singularKebab}.ts`),
|
|
2472
|
+
content: getUseCaseFile(names, config, options),
|
|
2473
|
+
},
|
|
2474
|
+
{
|
|
2475
|
+
path: path.join(useCaseDir, `update-${names.singularKebab}.ts`),
|
|
2476
|
+
content: updateUseCaseFile(names, config, options),
|
|
2477
|
+
},
|
|
2478
|
+
{
|
|
2479
|
+
path: path.join(useCaseDir, `delete-${names.singularKebab}.ts`),
|
|
2480
|
+
content: deleteUseCaseFile(names, config, options),
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
path: path.join(featureDir, "policy.ts"),
|
|
2484
|
+
content: resourcePolicyFile(names, config, options),
|
|
2485
|
+
},
|
|
2486
|
+
]
|
|
2487
|
+
: []),
|
|
2488
|
+
...(mode === "resource" && options.events
|
|
2489
|
+
? [
|
|
2490
|
+
{
|
|
2491
|
+
path: path.join(featureDir, "domain/events/index.ts"),
|
|
2492
|
+
content: resourceEventsFile(names),
|
|
2493
|
+
},
|
|
2494
|
+
]
|
|
2495
|
+
: []),
|
|
2496
|
+
...(mode === "resource" && options.auth
|
|
2497
|
+
? [
|
|
2498
|
+
{
|
|
2499
|
+
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
2500
|
+
content: resourcePolicyTestFile(names, options),
|
|
2501
|
+
},
|
|
2502
|
+
]
|
|
2503
|
+
: []),
|
|
1925
2504
|
{
|
|
1926
2505
|
path: path.join(useCaseDir, "index.ts"),
|
|
1927
|
-
content: useCasesIndexFile(names),
|
|
2506
|
+
content: useCasesIndexFile(names, mode),
|
|
1928
2507
|
},
|
|
1929
2508
|
{
|
|
1930
2509
|
path: resourcePortFilePath(names, config),
|
|
1931
|
-
content: repositoryPortFile(names, config),
|
|
2510
|
+
content: repositoryPortFile(names, config, mode, options),
|
|
1932
2511
|
},
|
|
1933
2512
|
{
|
|
1934
2513
|
path: path.join(infraDir, names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`),
|
|
1935
|
-
content: inMemoryRepositoryFile(names, config),
|
|
2514
|
+
content: inMemoryRepositoryFile(names, config, mode, options),
|
|
1936
2515
|
},
|
|
1937
2516
|
{
|
|
1938
2517
|
path: path.join(featureDir, "routes.ts"),
|
|
1939
|
-
content: routeGroupFile(names, config),
|
|
2518
|
+
content: routeGroupFile(names, config, mode, options),
|
|
1940
2519
|
},
|
|
1941
2520
|
{
|
|
1942
2521
|
path: resourceTestFilePath(names, config),
|
|
1943
|
-
content: testFile(names, config),
|
|
2522
|
+
content: testFile(names, config, mode, options),
|
|
1944
2523
|
},
|
|
1945
2524
|
];
|
|
1946
2525
|
if (persistence === "drizzle") {
|
|
1947
2526
|
files.push({
|
|
1948
2527
|
path: drizzleResourceSchemaFilePath(names, config),
|
|
1949
|
-
content: drizzleSchemaFile(names),
|
|
2528
|
+
content: drizzleSchemaFile(names, options),
|
|
1950
2529
|
}, {
|
|
1951
2530
|
path: drizzleResourceRepositoryFilePath(names, config),
|
|
1952
|
-
content: drizzleRepositoryFile(names, config),
|
|
2531
|
+
content: drizzleRepositoryFile(names, config, mode, options),
|
|
1953
2532
|
});
|
|
1954
2533
|
}
|
|
1955
2534
|
return files;
|
|
1956
2535
|
}
|
|
2536
|
+
function featureUiComponentFiles(names, config) {
|
|
2537
|
+
const componentPath = featureUiComponentFilePath(names, config);
|
|
2538
|
+
return [
|
|
2539
|
+
{
|
|
2540
|
+
path: componentPath,
|
|
2541
|
+
content: featureUiComponentFile(names, config),
|
|
2542
|
+
},
|
|
2543
|
+
];
|
|
2544
|
+
}
|
|
2545
|
+
function featureUiIndexFile(names, config) {
|
|
2546
|
+
return {
|
|
2547
|
+
path: featureUiIndexFilePath(names, config),
|
|
2548
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";
|
|
2549
|
+
`,
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
1957
2552
|
function contractFiles(names, config) {
|
|
1958
2553
|
return [
|
|
1959
2554
|
{
|
|
@@ -2019,6 +2614,14 @@ function jobFiles(names, config) {
|
|
|
2019
2614
|
},
|
|
2020
2615
|
];
|
|
2021
2616
|
}
|
|
2617
|
+
function taskFiles(names, config) {
|
|
2618
|
+
return [
|
|
2619
|
+
{
|
|
2620
|
+
path: taskFilePath(names, config),
|
|
2621
|
+
content: taskFile(names, config),
|
|
2622
|
+
},
|
|
2623
|
+
];
|
|
2624
|
+
}
|
|
2022
2625
|
function notificationFiles(names, config) {
|
|
2023
2626
|
return [
|
|
2024
2627
|
{
|
|
@@ -2095,6 +2698,19 @@ function drizzleResourceSchemaFilePath(names, config) {
|
|
|
2095
2698
|
function drizzleResourceRepositoryFilePath(names, config) {
|
|
2096
2699
|
return path.join(infrastructureDir(config), names.pluralKebab, `drizzle-${names.singularKebab}-repository.ts`);
|
|
2097
2700
|
}
|
|
2701
|
+
function resourceSharedErrorsPath(config) {
|
|
2702
|
+
return path.join(config.paths.features, "shared", "errors.ts");
|
|
2703
|
+
}
|
|
2704
|
+
function resourceContractNames(names, mode) {
|
|
2705
|
+
const contractNames = [
|
|
2706
|
+
`list${names.pluralPascal}`,
|
|
2707
|
+
`create${names.singularPascal}`,
|
|
2708
|
+
];
|
|
2709
|
+
if (mode === "resource") {
|
|
2710
|
+
contractNames.push(`get${names.singularPascal}`, `update${names.singularPascal}`, `delete${names.singularPascal}`);
|
|
2711
|
+
}
|
|
2712
|
+
return contractNames;
|
|
2713
|
+
}
|
|
2098
2714
|
async function detectResourcePersistence(targetDir, config) {
|
|
2099
2715
|
return (await fileExists(path.join(targetDir, drizzleRepositoriesPath(config))))
|
|
2100
2716
|
? "drizzle"
|
|
@@ -2103,6 +2719,19 @@ async function detectResourcePersistence(targetDir, config) {
|
|
|
2103
2719
|
function resourceFeatureDir(names, config) {
|
|
2104
2720
|
return path.join(config.paths.features, names.pluralKebab);
|
|
2105
2721
|
}
|
|
2722
|
+
function featureUiDir(names, config) {
|
|
2723
|
+
return path.join(resourceFeatureDir(names, config), "components");
|
|
2724
|
+
}
|
|
2725
|
+
function featureUiComponentFilePath(names, config) {
|
|
2726
|
+
return path.join(featureUiDir(names, config), `${names.componentFileName}.tsx`);
|
|
2727
|
+
}
|
|
2728
|
+
function featureUiIndexFilePath(names, config) {
|
|
2729
|
+
return path.join(featureUiDir(names, config), "index.ts");
|
|
2730
|
+
}
|
|
2731
|
+
function clientIndexPath(config) {
|
|
2732
|
+
const appRoot = path.posix.dirname(config.paths.appContext);
|
|
2733
|
+
return path.join(appRoot === "." ? "" : appRoot, "client/index.ts");
|
|
2734
|
+
}
|
|
2106
2735
|
function usesFeatureOwnedContracts(config) {
|
|
2107
2736
|
return (directoryPath(config.paths.contracts) ===
|
|
2108
2737
|
directoryPath(config.paths.features));
|
|
@@ -2137,6 +2766,9 @@ function resourceUseCaseDir(names, config) {
|
|
|
2137
2766
|
function resourceUseCaseIndexPath(names, config) {
|
|
2138
2767
|
return path.join(resourceUseCaseDir(names, config), "index.ts");
|
|
2139
2768
|
}
|
|
2769
|
+
function resourceSchemaFilePath(names, config) {
|
|
2770
|
+
return path.join(resourceFeatureDir(names, config), "schemas.ts");
|
|
2771
|
+
}
|
|
2140
2772
|
function resourcePortFilePath(names, config) {
|
|
2141
2773
|
if (usesFeatureOwnedContracts(config)) {
|
|
2142
2774
|
return path.join(resourceFeatureDir(names, config), "ports.ts");
|
|
@@ -2177,6 +2809,9 @@ function eventFilePath(names, config) {
|
|
|
2177
2809
|
function jobFilePath(names, config) {
|
|
2178
2810
|
return path.join(featureArtifactDir(names, "jobs", config), `${names.artifact.kebab}.ts`);
|
|
2179
2811
|
}
|
|
2812
|
+
function taskFilePath(names, config) {
|
|
2813
|
+
return path.join(featureArtifactDir(names, "tasks", config), `${names.artifact.kebab}.ts`);
|
|
2814
|
+
}
|
|
2180
2815
|
function factoryFilePath(names, config) {
|
|
2181
2816
|
return path.join(featureArtifactDir(names, "factories", config), `${names.artifact.kebab}.ts`);
|
|
2182
2817
|
}
|
|
@@ -2226,52 +2861,76 @@ async function updateUseCaseIndex(targetDir, file, options) {
|
|
|
2226
2861
|
await writeFile(destination, next);
|
|
2227
2862
|
return "updated";
|
|
2228
2863
|
}
|
|
2229
|
-
function
|
|
2230
|
-
const
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
:
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2864
|
+
async function updateFeatureUiIndex(targetDir, file, options) {
|
|
2865
|
+
const destination = path.join(targetDir, file.path);
|
|
2866
|
+
const existing = await readOptionalFile(destination);
|
|
2867
|
+
if (existing === undefined) {
|
|
2868
|
+
if (!options.dryRun) {
|
|
2869
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
2870
|
+
await writeFile(destination, file.content);
|
|
2871
|
+
}
|
|
2872
|
+
return "created";
|
|
2873
|
+
}
|
|
2874
|
+
if (existing.includes(file.content.trim())) {
|
|
2875
|
+
return "skipped";
|
|
2876
|
+
}
|
|
2877
|
+
const next = `${existing.trimEnd()}\n${file.content}`;
|
|
2878
|
+
if (!options.dryRun)
|
|
2879
|
+
await writeFile(destination, next);
|
|
2880
|
+
return "updated";
|
|
2881
|
+
}
|
|
2882
|
+
function featureArtifactIndexFile(kind, names, config) {
|
|
2883
|
+
const artifactPath = kind === "event"
|
|
2884
|
+
? eventFilePath(names, config)
|
|
2885
|
+
: kind === "job"
|
|
2886
|
+
? jobFilePath(names, config)
|
|
2887
|
+
: kind === "task"
|
|
2888
|
+
? taskFilePath(names, config)
|
|
2889
|
+
: kind === "factory"
|
|
2890
|
+
? factoryFilePath(names, config)
|
|
2891
|
+
: kind === "seed"
|
|
2892
|
+
? seedFilePath(names, config)
|
|
2893
|
+
: kind === "notification"
|
|
2894
|
+
? notificationFilePath(names, config)
|
|
2895
|
+
: kind === "listener"
|
|
2896
|
+
? listenerFilePath(names, config)
|
|
2897
|
+
: kind === "schedule"
|
|
2898
|
+
? scheduleFilePath(names, config)
|
|
2899
|
+
: uploadFilePath(names, config);
|
|
2245
2900
|
const registrySuffix = kind === "event"
|
|
2246
2901
|
? "Events"
|
|
2247
2902
|
: kind === "job"
|
|
2248
2903
|
? "Jobs"
|
|
2249
|
-
: kind === "
|
|
2250
|
-
? "
|
|
2251
|
-
: kind === "
|
|
2252
|
-
? "
|
|
2253
|
-
: kind === "
|
|
2254
|
-
? "
|
|
2255
|
-
: kind === "
|
|
2256
|
-
? "
|
|
2257
|
-
: kind === "
|
|
2258
|
-
? "
|
|
2259
|
-
: "
|
|
2904
|
+
: kind === "task"
|
|
2905
|
+
? "Tasks"
|
|
2906
|
+
: kind === "factory"
|
|
2907
|
+
? "Factories"
|
|
2908
|
+
: kind === "seed"
|
|
2909
|
+
? "Seeds"
|
|
2910
|
+
: kind === "notification"
|
|
2911
|
+
? "Notifications"
|
|
2912
|
+
: kind === "listener"
|
|
2913
|
+
? "Listeners"
|
|
2914
|
+
: kind === "schedule"
|
|
2915
|
+
? "Schedules"
|
|
2916
|
+
: "Uploads";
|
|
2260
2917
|
const importName = kind === "event"
|
|
2261
2918
|
? names.eventExportName
|
|
2262
2919
|
: kind === "job"
|
|
2263
2920
|
? names.jobExportName
|
|
2264
|
-
: kind === "
|
|
2265
|
-
? names.
|
|
2266
|
-
: kind === "
|
|
2267
|
-
? names.
|
|
2268
|
-
: kind === "
|
|
2269
|
-
? names.
|
|
2270
|
-
: kind === "
|
|
2271
|
-
? names.
|
|
2272
|
-
: kind === "
|
|
2273
|
-
? names.
|
|
2274
|
-
:
|
|
2921
|
+
: kind === "task"
|
|
2922
|
+
? names.taskExportName
|
|
2923
|
+
: kind === "factory"
|
|
2924
|
+
? names.factoryExportName
|
|
2925
|
+
: kind === "seed"
|
|
2926
|
+
? names.seedExportName
|
|
2927
|
+
: kind === "notification"
|
|
2928
|
+
? names.notificationExportName
|
|
2929
|
+
: kind === "listener"
|
|
2930
|
+
? names.listenerExportName
|
|
2931
|
+
: kind === "schedule"
|
|
2932
|
+
? names.scheduleExportName
|
|
2933
|
+
: names.uploadExportName;
|
|
2275
2934
|
return {
|
|
2276
2935
|
path: path.join(path.dirname(artifactPath), "index.ts"),
|
|
2277
2936
|
kind,
|
|
@@ -2365,6 +3024,117 @@ export const ${file.registryName} = [${file.importName}] as const;
|
|
|
2365
3024
|
await writeFile(destination, next);
|
|
2366
3025
|
return "updated";
|
|
2367
3026
|
}
|
|
3027
|
+
async function updateTaskRegistry(targetDir, names, config, options) {
|
|
3028
|
+
const destination = path.join(targetDir, config.paths.tasks);
|
|
3029
|
+
const existing = await readOptionalFile(destination);
|
|
3030
|
+
const featureTasksPath = path.join(featureArtifactDir(names, "tasks", config), "index.ts");
|
|
3031
|
+
const registryName = `${names.featureSingularCamel}Tasks`;
|
|
3032
|
+
const importLine = `import { ${registryName} } from "${relativeModule(config.paths.tasks, featureTasksPath)}";`;
|
|
3033
|
+
const defineTasksImport = `import { defineTasks } from "@beignet/core/tasks";`;
|
|
3034
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
3035
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
3036
|
+
const serverImport = `import { server } from "${relativeModule(config.paths.tasks, config.paths.server)}";`;
|
|
3037
|
+
const registryEntry = `...${registryName}`;
|
|
3038
|
+
if (existing === undefined) {
|
|
3039
|
+
const content = `${defineTasksImport}
|
|
3040
|
+
${serviceActorImport}
|
|
3041
|
+
${appContextImport}
|
|
3042
|
+
${importLine}
|
|
3043
|
+
${serverImport}
|
|
3044
|
+
|
|
3045
|
+
export const tasks = defineTasks([
|
|
3046
|
+
\t${registryEntry},
|
|
3047
|
+
] as const);
|
|
3048
|
+
|
|
3049
|
+
export async function createTaskContext(): Promise<AppContext> {
|
|
3050
|
+
\treturn server.createServiceContext({
|
|
3051
|
+
\t\tactor: createServiceActor("beignet-cli"),
|
|
3052
|
+
\t});
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
export async function stopTaskContext(): Promise<void> {
|
|
3056
|
+
\tawait server.stop();
|
|
3057
|
+
}
|
|
3058
|
+
`;
|
|
3059
|
+
if (!options.dryRun) {
|
|
3060
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
3061
|
+
await writeFile(destination, content);
|
|
3062
|
+
}
|
|
3063
|
+
return "created";
|
|
3064
|
+
}
|
|
3065
|
+
let next = existing;
|
|
3066
|
+
if (!next.includes(defineTasksImport)) {
|
|
3067
|
+
next = insertAfterImports(next, defineTasksImport);
|
|
3068
|
+
}
|
|
3069
|
+
if (!next.includes(importLine)) {
|
|
3070
|
+
next = insertAfterImports(next, importLine);
|
|
3071
|
+
}
|
|
3072
|
+
const registry = arrayInitializerInfo(next, "tasks");
|
|
3073
|
+
if (!registry) {
|
|
3074
|
+
next = `${next.trimEnd()}\n\nexport const tasks = defineTasks([\n\t${registryEntry},\n] as const);\n`;
|
|
3075
|
+
}
|
|
3076
|
+
else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
3077
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
3078
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
|
|
3079
|
+
}
|
|
3080
|
+
if (next === existing)
|
|
3081
|
+
return "skipped";
|
|
3082
|
+
if (!options.dryRun)
|
|
3083
|
+
await writeFile(destination, next);
|
|
3084
|
+
return "updated";
|
|
3085
|
+
}
|
|
3086
|
+
async function updateScheduleRegistry(targetDir, names, config, options) {
|
|
3087
|
+
const destination = path.join(targetDir, config.paths.schedules);
|
|
3088
|
+
const existing = await readOptionalFile(destination);
|
|
3089
|
+
const featureSchedulesPath = path.join(featureArtifactDir(names, "schedules", config), "index.ts");
|
|
3090
|
+
const registryName = `${names.featureSingularCamel}Schedules`;
|
|
3091
|
+
const importLine = `import { ${registryName} } from "${aliasModule(featureSchedulesPath)}";`;
|
|
3092
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
3093
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
3094
|
+
const serverImport = `import { server } from "${relativeModule(config.paths.schedules, config.paths.server)}";`;
|
|
3095
|
+
const registryEntry = `...${registryName}`;
|
|
3096
|
+
if (existing === undefined) {
|
|
3097
|
+
const content = `${serviceActorImport}
|
|
3098
|
+
${appContextImport}
|
|
3099
|
+
${importLine}
|
|
3100
|
+
${serverImport}
|
|
3101
|
+
|
|
3102
|
+
export const schedules = [${registryEntry}] as const;
|
|
3103
|
+
|
|
3104
|
+
export async function createScheduleContext(): Promise<AppContext> {
|
|
3105
|
+
\treturn server.createServiceContext({
|
|
3106
|
+
\t\tactor: createServiceActor("beignet-schedule"),
|
|
3107
|
+
\t});
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
export async function stopScheduleContext(): Promise<void> {
|
|
3111
|
+
\tawait server.stop();
|
|
3112
|
+
}
|
|
3113
|
+
`;
|
|
3114
|
+
if (!options.dryRun) {
|
|
3115
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
3116
|
+
await writeFile(destination, content);
|
|
3117
|
+
}
|
|
3118
|
+
return "created";
|
|
3119
|
+
}
|
|
3120
|
+
let next = existing;
|
|
3121
|
+
if (!next.includes(importLine)) {
|
|
3122
|
+
next = insertAfterImports(next, importLine);
|
|
3123
|
+
}
|
|
3124
|
+
const registry = arrayInitializerInfo(next, "schedules");
|
|
3125
|
+
if (!registry) {
|
|
3126
|
+
next = `${next.trimEnd()}\n\nexport const schedules = [${registryEntry}] as const;\n`;
|
|
3127
|
+
}
|
|
3128
|
+
else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
3129
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
3130
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
|
|
3131
|
+
}
|
|
3132
|
+
if (next === existing)
|
|
3133
|
+
return "skipped";
|
|
3134
|
+
if (!options.dryRun)
|
|
3135
|
+
await writeFile(destination, next);
|
|
3136
|
+
return "updated";
|
|
3137
|
+
}
|
|
2368
3138
|
function arrayInitializerInfo(source, constName) {
|
|
2369
3139
|
const match = new RegExp(`\\bconst\\s+${constName}\\s*=`).exec(source);
|
|
2370
3140
|
if (!match)
|
|
@@ -2397,27 +3167,106 @@ function defineUploadsInitializerInfo(source, constName) {
|
|
|
2397
3167
|
end: closeBrace,
|
|
2398
3168
|
};
|
|
2399
3169
|
}
|
|
2400
|
-
function schemasFile(names) {
|
|
3170
|
+
function schemasFile(names, mode, options) {
|
|
2401
3171
|
return `import { z } from "zod";
|
|
2402
3172
|
|
|
2403
3173
|
export const ${names.singularPascal}Schema = z.object({
|
|
2404
3174
|
id: z.string().uuid(),
|
|
2405
|
-
name: z.string().min(1),
|
|
3175
|
+
${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
3176
|
+
version: z.number().int().min(1),
|
|
2406
3177
|
createdAt: z.string().datetime(),
|
|
3178
|
+
updatedAt: z.string().datetime(),
|
|
2407
3179
|
});
|
|
2408
3180
|
|
|
2409
|
-
export const
|
|
2410
|
-
|
|
2411
|
-
|
|
3181
|
+
export const ${names.singularPascal}SortBySchema = z.enum(["createdAt", "name"]);
|
|
3182
|
+
export const ${names.singularPascal}SortDirectionSchema = z.enum(["asc", "desc"]);
|
|
3183
|
+
|
|
3184
|
+
const ${names.singularPascal}CursorPayloadSchema = z.object({
|
|
3185
|
+
sortBy: ${names.singularPascal}SortBySchema,
|
|
3186
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema,
|
|
3187
|
+
sortValue: z.string(),
|
|
3188
|
+
id: z.string().uuid(),
|
|
2412
3189
|
});
|
|
2413
3190
|
|
|
3191
|
+
export type ${names.singularPascal}SortBy = z.infer<
|
|
3192
|
+
typeof ${names.singularPascal}SortBySchema
|
|
3193
|
+
>;
|
|
3194
|
+
export type ${names.singularPascal}SortDirection = z.infer<
|
|
3195
|
+
typeof ${names.singularPascal}SortDirectionSchema
|
|
3196
|
+
>;
|
|
3197
|
+
export type ${names.singularPascal}Cursor = z.infer<
|
|
3198
|
+
typeof ${names.singularPascal}CursorPayloadSchema
|
|
3199
|
+
>;
|
|
3200
|
+
|
|
3201
|
+
function encodeBase64Url(value: string): string {
|
|
3202
|
+
const bytes = new TextEncoder().encode(value);
|
|
3203
|
+
let binary = "";
|
|
3204
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3205
|
+
|
|
3206
|
+
return btoa(binary)
|
|
3207
|
+
.replaceAll("+", "-")
|
|
3208
|
+
.replaceAll("/", "_")
|
|
3209
|
+
.replaceAll("=", "");
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
function decodeBase64Url(value: string): string {
|
|
3213
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
3214
|
+
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
|
3215
|
+
const binary = atob(base64 + padding);
|
|
3216
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
3217
|
+
|
|
3218
|
+
return new TextDecoder().decode(bytes);
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
export function encode${names.singularPascal}Cursor(cursor: ${names.singularPascal}Cursor): string {
|
|
3222
|
+
return encodeBase64Url(JSON.stringify(cursor));
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
export function decode${names.singularPascal}Cursor(cursor: string): ${names.singularPascal}Cursor {
|
|
3226
|
+
return ${names.singularPascal}CursorPayloadSchema.parse(
|
|
3227
|
+
JSON.parse(decodeBase64Url(cursor)),
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
export const List${names.pluralPascal}InputSchema = z
|
|
3232
|
+
.object({
|
|
3233
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
3234
|
+
cursor: z.string().nullable().default(null),
|
|
3235
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
3236
|
+
sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
|
|
3237
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
|
|
3238
|
+
})
|
|
3239
|
+
.superRefine((input, ctx) => {
|
|
3240
|
+
if (!input.cursor) return;
|
|
3241
|
+
|
|
3242
|
+
try {
|
|
3243
|
+
const cursor = decode${names.singularPascal}Cursor(input.cursor);
|
|
3244
|
+
if (
|
|
3245
|
+
cursor.sortBy !== input.sortBy ||
|
|
3246
|
+
cursor.sortDirection !== input.sortDirection
|
|
3247
|
+
) {
|
|
3248
|
+
ctx.addIssue({
|
|
3249
|
+
code: z.ZodIssueCode.custom,
|
|
3250
|
+
message: "cursor does not match the requested sort.",
|
|
3251
|
+
path: ["cursor"],
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
} catch {
|
|
3255
|
+
ctx.addIssue({
|
|
3256
|
+
code: z.ZodIssueCode.custom,
|
|
3257
|
+
message: "cursor is invalid.",
|
|
3258
|
+
path: ["cursor"],
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
});
|
|
3262
|
+
|
|
2414
3263
|
export const List${names.pluralPascal}OutputSchema = z.object({
|
|
2415
3264
|
items: z.array(${names.singularPascal}Schema),
|
|
2416
3265
|
page: z.object({
|
|
2417
|
-
kind: z.literal("
|
|
3266
|
+
kind: z.literal("cursor"),
|
|
2418
3267
|
limit: z.number().int().min(1),
|
|
2419
|
-
|
|
2420
|
-
|
|
3268
|
+
cursor: z.string().nullable(),
|
|
3269
|
+
nextCursor: z.string().nullable(),
|
|
2421
3270
|
hasMore: z.boolean(),
|
|
2422
3271
|
}),
|
|
2423
3272
|
});
|
|
@@ -2425,147 +3274,434 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
2425
3274
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
2426
3275
|
name: z.string().min(1).max(120),
|
|
2427
3276
|
});
|
|
3277
|
+
${mode === "resource"
|
|
3278
|
+
? `
|
|
3279
|
+
export const ${names.singularPascal}IdInputSchema = z.object({
|
|
3280
|
+
id: z.string().uuid(),
|
|
3281
|
+
});
|
|
2428
3282
|
|
|
3283
|
+
export const Update${names.singularPascal}BodySchema = z.object({
|
|
3284
|
+
name: z.string().min(1).max(120),
|
|
3285
|
+
version: z.number().int().min(1),
|
|
3286
|
+
});
|
|
3287
|
+
|
|
3288
|
+
export const Update${names.singularPascal}InputSchema =
|
|
3289
|
+
${names.singularPascal}IdInputSchema.merge(Update${names.singularPascal}BodySchema);
|
|
3290
|
+
`
|
|
3291
|
+
: ""}
|
|
2429
3292
|
export type ${names.singularPascal} = z.infer<typeof ${names.singularPascal}Schema>;
|
|
2430
3293
|
export type Create${names.singularPascal}Input = z.infer<
|
|
2431
3294
|
typeof Create${names.singularPascal}InputSchema
|
|
2432
3295
|
>;
|
|
3296
|
+
${mode === "resource"
|
|
3297
|
+
? `
|
|
3298
|
+
export type ${names.singularPascal}IdInput = z.infer<
|
|
3299
|
+
typeof ${names.singularPascal}IdInputSchema
|
|
3300
|
+
>;
|
|
3301
|
+
export type Update${names.singularPascal}Body = z.infer<
|
|
3302
|
+
typeof Update${names.singularPascal}BodySchema
|
|
3303
|
+
>;
|
|
3304
|
+
export type Update${names.singularPascal}Input = z.infer<
|
|
3305
|
+
typeof Update${names.singularPascal}InputSchema
|
|
3306
|
+
>;
|
|
3307
|
+
`
|
|
3308
|
+
: ""}
|
|
2433
3309
|
export type List${names.pluralPascal}Input = z.infer<
|
|
2434
3310
|
typeof List${names.pluralPascal}InputSchema
|
|
2435
3311
|
>;
|
|
2436
3312
|
`;
|
|
2437
3313
|
}
|
|
2438
|
-
function listUseCaseFile(names, config) {
|
|
3314
|
+
function listUseCaseFile(names, config, options) {
|
|
2439
3315
|
const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
|
|
2440
|
-
return `import {
|
|
3316
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3317
|
+
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
2441
3318
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2442
3319
|
import {
|
|
3320
|
+
decode${names.singularPascal}Cursor,
|
|
2443
3321
|
List${names.pluralPascal}InputSchema,
|
|
2444
3322
|
List${names.pluralPascal}OutputSchema,
|
|
2445
|
-
} from "
|
|
3323
|
+
} from "../schemas";
|
|
2446
3324
|
|
|
2447
3325
|
export const list${names.pluralPascal}UseCase = useCase
|
|
2448
3326
|
.query("${names.pluralCamel}.list")
|
|
2449
3327
|
.input(List${names.pluralPascal}InputSchema)
|
|
2450
3328
|
.output(List${names.pluralPascal}OutputSchema)
|
|
2451
3329
|
.run(async ({ ctx, input }) => {
|
|
2452
|
-
const page =
|
|
3330
|
+
${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
3331
|
defaultLimit: 20,
|
|
2454
3332
|
maxLimit: 100,
|
|
2455
3333
|
});
|
|
2456
3334
|
|
|
2457
|
-
return ctx.ports.${names.pluralCamel}.list(
|
|
3335
|
+
return ctx.ports.${names.pluralCamel}.list({
|
|
3336
|
+
page,
|
|
3337
|
+
cursor: page.cursor ? decode${names.singularPascal}Cursor(page.cursor) : null,
|
|
3338
|
+
name: input.name,
|
|
3339
|
+
sortBy: input.sortBy,
|
|
3340
|
+
sortDirection: input.sortDirection,
|
|
3341
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
2458
3342
|
});
|
|
2459
3343
|
`;
|
|
2460
3344
|
}
|
|
2461
|
-
function createUseCaseFile(names, config) {
|
|
3345
|
+
function createUseCaseFile(names, config, options) {
|
|
2462
3346
|
const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
|
|
2463
|
-
return `import {
|
|
3347
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3348
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2464
3349
|
import {
|
|
2465
3350
|
Create${names.singularPascal}InputSchema,
|
|
2466
3351
|
${names.singularPascal}Schema,
|
|
2467
|
-
} from "
|
|
3352
|
+
} from "../schemas";
|
|
3353
|
+
${options.events ? `import { ${names.singularPascal}Created } from "../domain/events";\n` : ""}
|
|
2468
3354
|
|
|
2469
3355
|
export const create${names.singularPascal}UseCase = useCase
|
|
2470
3356
|
.command("${names.pluralCamel}.create")
|
|
2471
3357
|
.input(Create${names.singularPascal}InputSchema)
|
|
2472
3358
|
.output(${names.singularPascal}Schema)
|
|
2473
|
-
.run(async ({ ctx, input }) =>
|
|
3359
|
+
.run(async ({ ctx, input }) => {
|
|
3360
|
+
${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({
|
|
3361
|
+
...input,
|
|
3362
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3363
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3364
|
+
return ${names.singularCamel};
|
|
3365
|
+
});
|
|
3366
|
+
`;
|
|
3367
|
+
}
|
|
3368
|
+
function getUseCaseFile(names, config, options) {
|
|
3369
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
|
|
3370
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3371
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3372
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3373
|
+
import {
|
|
3374
|
+
${names.singularPascal}IdInputSchema,
|
|
3375
|
+
${names.singularPascal}Schema,
|
|
3376
|
+
} from "../schemas";
|
|
3377
|
+
|
|
3378
|
+
export const get${names.singularPascal}UseCase = useCase
|
|
3379
|
+
.query("${names.pluralCamel}.get")
|
|
3380
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
3381
|
+
.output(${names.singularPascal}Schema)
|
|
3382
|
+
.run(async ({ ctx, input }) => {
|
|
3383
|
+
${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 }" : ""});
|
|
3384
|
+
if (!${names.singularCamel}) {
|
|
3385
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3386
|
+
details: { id: input.id },
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
return ${names.singularCamel};
|
|
3391
|
+
});
|
|
3392
|
+
`;
|
|
3393
|
+
}
|
|
3394
|
+
function updateUseCaseFile(names, config, options) {
|
|
3395
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
|
|
3396
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3397
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3398
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3399
|
+
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
3400
|
+
Update${names.singularPascal}InputSchema,
|
|
3401
|
+
${names.singularPascal}Schema,
|
|
3402
|
+
} from "../schemas";
|
|
3403
|
+
|
|
3404
|
+
export const update${names.singularPascal}UseCase = useCase
|
|
3405
|
+
.command("${names.pluralCamel}.update")
|
|
3406
|
+
.input(Update${names.singularPascal}InputSchema)
|
|
3407
|
+
.output(${names.singularPascal}Schema)
|
|
3408
|
+
.run(async ({ ctx, input }) => {
|
|
3409
|
+
${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 }" : ""});
|
|
3410
|
+
if (!existing) {
|
|
3411
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3412
|
+
details: { id: input.id },
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
3416
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
3417
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
3422
|
+
...input,
|
|
3423
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3424
|
+
if (!${names.singularCamel}) {
|
|
3425
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
3426
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Updated, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3430
|
+
return ${names.singularCamel};
|
|
3431
|
+
});
|
|
2474
3432
|
`;
|
|
2475
3433
|
}
|
|
2476
|
-
function
|
|
3434
|
+
function deleteUseCaseFile(names, config, options) {
|
|
3435
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
|
|
3436
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3437
|
+
import { z } from "zod";
|
|
3438
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3439
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3440
|
+
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
3441
|
+
|
|
3442
|
+
export const delete${names.singularPascal}UseCase = useCase
|
|
3443
|
+
.command("${names.pluralCamel}.delete")
|
|
3444
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
3445
|
+
.output(z.void())
|
|
3446
|
+
.run(async ({ ctx, input }) => {
|
|
3447
|
+
${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 }" : ""});
|
|
3448
|
+
if (!deleted) {
|
|
3449
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3450
|
+
details: { id: input.id },
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Deleted, {\n\t\t\tid: input.id,\n\t\t});\n` : ""} });
|
|
3454
|
+
`;
|
|
3455
|
+
}
|
|
3456
|
+
function useCasesIndexFile(names, mode) {
|
|
2477
3457
|
return `export { create${names.singularPascal}UseCase } from "./create-${names.singularKebab}";
|
|
2478
|
-
export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
2479
|
-
export {
|
|
3458
|
+
${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}";
|
|
3459
|
+
${mode === "resource" ? `export { update${names.singularPascal}UseCase } from "./update-${names.singularKebab}";\n` : ""}export {
|
|
2480
3460
|
Create${names.singularPascal}InputSchema,
|
|
2481
|
-
List${names.pluralPascal}InputSchema,
|
|
3461
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n` : ""} List${names.pluralPascal}InputSchema,
|
|
2482
3462
|
List${names.pluralPascal}OutputSchema,
|
|
2483
|
-
${names.singularPascal}Schema,
|
|
3463
|
+
${mode === "resource" ? ` Update${names.singularPascal}BodySchema,\n Update${names.singularPascal}InputSchema,\n` : ""} ${names.singularPascal}Schema,
|
|
2484
3464
|
type Create${names.singularPascal}Input,
|
|
2485
|
-
type List${names.pluralPascal}Input,
|
|
3465
|
+
${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
3466
|
type ${names.singularPascal},
|
|
2487
|
-
} from "
|
|
3467
|
+
} from "../schemas";
|
|
2488
3468
|
`;
|
|
2489
3469
|
}
|
|
2490
|
-
function repositoryPortFile(names, config) {
|
|
3470
|
+
function repositoryPortFile(names, config, mode, options) {
|
|
2491
3471
|
return `import type {
|
|
2492
|
-
|
|
2493
|
-
|
|
3472
|
+
CursorPage,
|
|
3473
|
+
CursorPageInfo,
|
|
2494
3474
|
PageResult,
|
|
3475
|
+
SortDirection,
|
|
2495
3476
|
} from "@beignet/core/pagination";
|
|
2496
3477
|
import type {
|
|
2497
3478
|
Create${names.singularPascal}Input,
|
|
2498
|
-
${names.singularPascal},
|
|
2499
|
-
|
|
3479
|
+
${names.singularPascal}Cursor,
|
|
3480
|
+
${names.singularPascal}SortBy,
|
|
3481
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3482
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2500
3483
|
|
|
2501
|
-
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal},
|
|
3484
|
+
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal}, CursorPageInfo>;
|
|
3485
|
+
export type List${names.pluralPascal}Query = {
|
|
3486
|
+
page: CursorPage;
|
|
3487
|
+
cursor: ${names.singularPascal}Cursor | null;
|
|
3488
|
+
name?: string;
|
|
3489
|
+
sortBy: ${names.singularPascal}SortBy;
|
|
3490
|
+
sortDirection: SortDirection;
|
|
3491
|
+
${options.tenant ? `\ttenantId: string;\n` : ""}};
|
|
3492
|
+
${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
3493
|
|
|
2503
3494
|
export interface ${names.singularPascal}Repository {
|
|
2504
|
-
list(
|
|
2505
|
-
create(input: Create${names.singularPascal}Input): Promise<${names.singularPascal}>;
|
|
2506
|
-
}
|
|
3495
|
+
list(query: List${names.pluralPascal}Query): Promise<List${names.pluralPascal}Result>;
|
|
3496
|
+
create(input: ${options.tenant ? `Create${names.singularPascal}RepositoryInput` : `Create${names.singularPascal}Input`}): Promise<${names.singularPascal}>;
|
|
3497
|
+
${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
3498
|
`;
|
|
2508
3499
|
}
|
|
2509
|
-
function inMemoryRepositoryFile(names, config) {
|
|
3500
|
+
function inMemoryRepositoryFile(names, config, mode, options) {
|
|
2510
3501
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
2511
|
-
|
|
3502
|
+
const recordType = options.softDelete
|
|
3503
|
+
? `type ${names.singularPascal}Record = ${names.singularPascal} & {
|
|
3504
|
+
\tdeletedAt: string | null;
|
|
3505
|
+
};
|
|
3506
|
+
|
|
3507
|
+
`
|
|
3508
|
+
: "";
|
|
3509
|
+
const mapValueType = options.softDelete
|
|
3510
|
+
? `${names.singularPascal}Record`
|
|
3511
|
+
: names.singularPascal;
|
|
3512
|
+
const activeFilter = options.softDelete
|
|
3513
|
+
? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.deletedAt === null)\n`
|
|
3514
|
+
: "";
|
|
3515
|
+
const newDeletedAtField = options.softDelete
|
|
3516
|
+
? `\t\t\t\tdeletedAt: null,\n`
|
|
3517
|
+
: "";
|
|
3518
|
+
const findDeletedGuard = options.softDelete
|
|
3519
|
+
? `\t\t\tif (${names.singularCamel}?.deletedAt !== null) return null;\n`
|
|
3520
|
+
: "";
|
|
3521
|
+
const updateDeletedGuard = options.softDelete
|
|
3522
|
+
? " || existing.deletedAt !== null"
|
|
3523
|
+
: "";
|
|
3524
|
+
const deleteImplementation = options.softDelete
|
|
3525
|
+
? `${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();
|
|
3526
|
+
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
3527
|
+
return true;`
|
|
3528
|
+
: `${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);`;
|
|
3529
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3530
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2512
3531
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3532
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2513
3533
|
import type {
|
|
2514
3534
|
Create${names.singularPascal}Input,
|
|
2515
|
-
${names.singularPascal},
|
|
2516
|
-
} from "${aliasModule(
|
|
3535
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3536
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3537
|
+
|
|
3538
|
+
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
3539
|
+
return {
|
|
3540
|
+
id: ${names.singularCamel}.id,
|
|
3541
|
+
${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
3542
|
+
version: ${names.singularCamel}.version,
|
|
3543
|
+
createdAt: ${names.singularCamel}.createdAt,
|
|
3544
|
+
updatedAt: ${names.singularCamel}.updatedAt,
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
function compare${names.pluralPascal}(
|
|
3549
|
+
left: ${mapValueType},
|
|
3550
|
+
right: ${mapValueType},
|
|
3551
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3552
|
+
): number {
|
|
3553
|
+
const leftValue = left[query.sortBy];
|
|
3554
|
+
const rightValue = right[query.sortBy];
|
|
3555
|
+
const fieldComparison =
|
|
3556
|
+
leftValue === rightValue ? left.id.localeCompare(right.id) : leftValue.localeCompare(rightValue);
|
|
3557
|
+
|
|
3558
|
+
return query.sortDirection === "asc" ? fieldComparison : -fieldComparison;
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
function isAfter${names.singularPascal}Cursor(
|
|
3562
|
+
${names.singularCamel}: ${mapValueType},
|
|
3563
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3564
|
+
): boolean {
|
|
3565
|
+
if (!query.cursor) return true;
|
|
3566
|
+
|
|
3567
|
+
const sortValue = ${names.singularCamel}[query.sortBy];
|
|
3568
|
+
const fieldComparison =
|
|
3569
|
+
sortValue === query.cursor.sortValue
|
|
3570
|
+
? ${names.singularCamel}.id.localeCompare(query.cursor.id)
|
|
3571
|
+
: sortValue.localeCompare(query.cursor.sortValue);
|
|
3572
|
+
|
|
3573
|
+
return query.sortDirection === "asc" ? fieldComparison > 0 : fieldComparison < 0;
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
function cursorFor${names.singularPascal}(
|
|
3577
|
+
${names.singularCamel}: ${mapValueType},
|
|
3578
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3579
|
+
): string {
|
|
3580
|
+
return encode${names.singularPascal}Cursor({
|
|
3581
|
+
sortBy: query.sortBy,
|
|
3582
|
+
sortDirection: query.sortDirection,
|
|
3583
|
+
sortValue: ${names.singularCamel}[query.sortBy],
|
|
3584
|
+
id: ${names.singularCamel}.id,
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
2517
3587
|
|
|
2518
3588
|
export function createInMemory${names.singularPascal}Repository(
|
|
2519
3589
|
seed: ${names.singularPascal}[] = [],
|
|
2520
3590
|
): ${names.singularPascal}Repository {
|
|
2521
|
-
const ${names.pluralCamel} = new Map
|
|
3591
|
+
const ${names.pluralCamel} = new Map<string, ${mapValueType}>(
|
|
3592
|
+
seed.map((${names.singularCamel}) => [
|
|
3593
|
+
${names.singularCamel}.id,
|
|
3594
|
+
{ ...${names.singularCamel}${options.softDelete ? ", deletedAt: null" : ""} },
|
|
3595
|
+
]),
|
|
3596
|
+
);
|
|
2522
3597
|
|
|
2523
3598
|
return {
|
|
2524
|
-
async list(
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
|
|
3599
|
+
async list(query) {
|
|
3600
|
+
const name = query.name?.toLocaleLowerCase();
|
|
3601
|
+
const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
3602
|
+
${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))
|
|
3603
|
+
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
3604
|
+
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
3605
|
+
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
3606
|
+
const nextCursor =
|
|
3607
|
+
all${names.pluralPascal}.length > query.page.limit && pageItems.length > 0
|
|
3608
|
+
? cursorFor${names.singularPascal}(pageItems[pageItems.length - 1]!, query)
|
|
3609
|
+
: null;
|
|
2528
3610
|
|
|
2529
|
-
return
|
|
2530
|
-
|
|
2531
|
-
page,
|
|
2532
|
-
|
|
3611
|
+
return cursorPageResult(
|
|
3612
|
+
pageItems.map(to${names.singularPascal}),
|
|
3613
|
+
query.page,
|
|
3614
|
+
nextCursor,
|
|
2533
3615
|
);
|
|
2534
3616
|
},
|
|
2535
|
-
async create(input
|
|
3617
|
+
async create(input) {
|
|
3618
|
+
const now = new Date().toISOString();
|
|
2536
3619
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2537
3620
|
id: crypto.randomUUID(),
|
|
2538
|
-
name: input.name,
|
|
2539
|
-
|
|
3621
|
+
${options.tenant ? `\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
3622
|
+
version: 1,
|
|
3623
|
+
createdAt: now,
|
|
3624
|
+
updatedAt: now,
|
|
2540
3625
|
};
|
|
2541
|
-
${names.pluralCamel}.set(${names.singularCamel}.id,
|
|
3626
|
+
${names.pluralCamel}.set(${names.singularCamel}.id, {
|
|
3627
|
+
...${names.singularCamel},
|
|
3628
|
+
${newDeletedAtField} });
|
|
2542
3629
|
return ${names.singularCamel};
|
|
2543
3630
|
},
|
|
3631
|
+
${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
3632
|
};
|
|
2545
3633
|
}
|
|
2546
3634
|
`;
|
|
2547
3635
|
}
|
|
2548
|
-
function drizzleSchemaFile(names) {
|
|
2549
|
-
return `import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
3636
|
+
function drizzleSchemaFile(names, options) {
|
|
3637
|
+
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2550
3638
|
|
|
2551
3639
|
export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
2552
3640
|
id: text("id").primaryKey(),
|
|
2553
|
-
name: text("name").notNull(),
|
|
3641
|
+
${options.tenant ? `\ttenantId: text("tenant_id").notNull(),\n` : ""} name: text("name").notNull(),
|
|
3642
|
+
version: integer("version").notNull(),
|
|
2554
3643
|
createdAt: text("created_at").notNull(),
|
|
2555
|
-
|
|
3644
|
+
updatedAt: text("updated_at").notNull(),
|
|
3645
|
+
${options.softDelete ? `\tdeletedAt: text("deleted_at"),\n` : ""}});
|
|
2556
3646
|
`;
|
|
2557
3647
|
}
|
|
2558
|
-
function
|
|
3648
|
+
function drizzleResourceWhere(names, options, predicates) {
|
|
3649
|
+
const allPredicates = [
|
|
3650
|
+
...predicates,
|
|
3651
|
+
...(options.softDelete
|
|
3652
|
+
? [`isNull(schema.${names.pluralCamel}.deletedAt)`]
|
|
3653
|
+
: []),
|
|
3654
|
+
];
|
|
3655
|
+
if (allPredicates.length === 0)
|
|
3656
|
+
return "";
|
|
3657
|
+
if (allPredicates.length === 1)
|
|
3658
|
+
return allPredicates[0] ?? "";
|
|
3659
|
+
return `and(${allPredicates.join(", ")})`;
|
|
3660
|
+
}
|
|
3661
|
+
function drizzleRepositoryFile(names, config, mode, options) {
|
|
2559
3662
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
2560
3663
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
2561
|
-
|
|
3664
|
+
const findWhere = drizzleResourceWhere(names, options, [
|
|
3665
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
3666
|
+
...(options.tenant
|
|
3667
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
3668
|
+
: []),
|
|
3669
|
+
]);
|
|
3670
|
+
const updateWhere = drizzleResourceWhere(names, options, [
|
|
3671
|
+
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
3672
|
+
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
3673
|
+
...(options.tenant
|
|
3674
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, input.tenantId)`]
|
|
3675
|
+
: []),
|
|
3676
|
+
]);
|
|
3677
|
+
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
3678
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
3679
|
+
...(options.tenant
|
|
3680
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
3681
|
+
: []),
|
|
3682
|
+
]);
|
|
3683
|
+
const drizzleImports = [
|
|
3684
|
+
"and",
|
|
3685
|
+
"asc",
|
|
3686
|
+
"desc",
|
|
3687
|
+
"eq",
|
|
3688
|
+
"gt",
|
|
3689
|
+
options.softDelete ? "isNull" : undefined,
|
|
3690
|
+
"lt",
|
|
3691
|
+
"or",
|
|
3692
|
+
"sql",
|
|
3693
|
+
"type SQL",
|
|
3694
|
+
].filter((name) => Boolean(name));
|
|
3695
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3696
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2562
3697
|
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
2563
|
-
import {
|
|
3698
|
+
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
2564
3699
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3700
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2565
3701
|
import type {
|
|
2566
3702
|
Create${names.singularPascal}Input,
|
|
2567
|
-
${names.singularPascal},
|
|
2568
|
-
} from "${aliasModule(
|
|
3703
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3704
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2569
3705
|
import * as schema from "${aliasModule(schemaPath)}";
|
|
2570
3706
|
|
|
2571
3707
|
type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelect;
|
|
@@ -2573,35 +3709,104 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
2573
3709
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
2574
3710
|
return {
|
|
2575
3711
|
id: row.id,
|
|
2576
|
-
name: row.name,
|
|
3712
|
+
${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
3713
|
+
version: row.version,
|
|
2577
3714
|
createdAt: row.createdAt,
|
|
3715
|
+
updatedAt: row.updatedAt,
|
|
2578
3716
|
};
|
|
2579
3717
|
}
|
|
2580
3718
|
|
|
3719
|
+
type List${names.pluralPascal}Query = Parameters<${names.singularPascal}Repository["list"]>[0];
|
|
3720
|
+
|
|
3721
|
+
function ${names.singularCamel}SortColumn(query: List${names.pluralPascal}Query) {
|
|
3722
|
+
return query.sortBy === "name" ? schema.${names.pluralCamel}.name : schema.${names.pluralCamel}.createdAt;
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
function ${names.singularCamel}CursorFilter(
|
|
3726
|
+
query: List${names.pluralPascal}Query,
|
|
3727
|
+
): SQL<unknown> | undefined {
|
|
3728
|
+
if (!query.cursor) return undefined;
|
|
3729
|
+
|
|
3730
|
+
const column = ${names.singularCamel}SortColumn(query);
|
|
3731
|
+
const compare = query.sortDirection === "asc" ? gt : lt;
|
|
3732
|
+
|
|
3733
|
+
return or(
|
|
3734
|
+
compare(column, query.cursor.sortValue),
|
|
3735
|
+
and(
|
|
3736
|
+
eq(column, query.cursor.sortValue),
|
|
3737
|
+
compare(schema.${names.pluralCamel}.id, query.cursor.id),
|
|
3738
|
+
),
|
|
3739
|
+
) as SQL<unknown>;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
function ${names.singularCamel}ListWhere(
|
|
3743
|
+
query: List${names.pluralPascal}Query,
|
|
3744
|
+
): SQL<unknown> | undefined {
|
|
3745
|
+
const filters: SQL<unknown>[] = [
|
|
3746
|
+
${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, query.tenantId),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
3747
|
+
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
3748
|
+
|
|
3749
|
+
if (query.name) {
|
|
3750
|
+
const pattern = \`%\${query.name.toLocaleLowerCase()}%\`;
|
|
3751
|
+
filters.push(sql\`lower(\${schema.${names.pluralCamel}.name}) like \${pattern}\`);
|
|
3752
|
+
}
|
|
3753
|
+
if (cursor) filters.push(cursor);
|
|
3754
|
+
|
|
3755
|
+
return filters.length > 0 ? and(...filters) : undefined;
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
function ${names.singularCamel}OrderBy(query: List${names.pluralPascal}Query) {
|
|
3759
|
+
const order = query.sortDirection === "asc" ? asc : desc;
|
|
3760
|
+
|
|
3761
|
+
return [order(${names.singularCamel}SortColumn(query)), order(schema.${names.pluralCamel}.id)] as const;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
function cursorFor${names.singularPascal}(
|
|
3765
|
+
row: ${names.singularPascal}Row,
|
|
3766
|
+
query: List${names.pluralPascal}Query,
|
|
3767
|
+
): string {
|
|
3768
|
+
return encode${names.singularPascal}Cursor({
|
|
3769
|
+
sortBy: query.sortBy,
|
|
3770
|
+
sortDirection: query.sortDirection,
|
|
3771
|
+
sortValue: row[query.sortBy],
|
|
3772
|
+
id: row.id,
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
|
|
2581
3776
|
export function createDrizzle${names.singularPascal}Repository(
|
|
2582
3777
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
2583
3778
|
): ${names.singularPascal}Repository {
|
|
2584
3779
|
return {
|
|
2585
|
-
async list(
|
|
3780
|
+
async list(query) {
|
|
3781
|
+
const where = ${names.singularCamel}ListWhere(query);
|
|
2586
3782
|
const rows = await db
|
|
2587
3783
|
.select()
|
|
2588
3784
|
.from(schema.${names.pluralCamel})
|
|
2589
|
-
.
|
|
2590
|
-
.
|
|
2591
|
-
.
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
.
|
|
2595
|
-
|
|
2596
|
-
|
|
3785
|
+
.where(where)
|
|
3786
|
+
.orderBy(...${names.singularCamel}OrderBy(query))
|
|
3787
|
+
.limit(query.page.limit + 1);
|
|
3788
|
+
const pageRows = rows.slice(0, query.page.limit);
|
|
3789
|
+
const nextCursor =
|
|
3790
|
+
rows.length > query.page.limit && pageRows.length > 0
|
|
3791
|
+
? cursorFor${names.singularPascal}(pageRows[pageRows.length - 1]!, query)
|
|
3792
|
+
: null;
|
|
3793
|
+
|
|
3794
|
+
return cursorPageResult(
|
|
3795
|
+
pageRows.map(to${names.singularPascal}),
|
|
3796
|
+
query.page,
|
|
3797
|
+
nextCursor,
|
|
3798
|
+
);
|
|
2597
3799
|
},
|
|
2598
|
-
async create(input
|
|
3800
|
+
async create(input) {
|
|
3801
|
+
const now = new Date().toISOString();
|
|
2599
3802
|
const [row] = await db
|
|
2600
3803
|
.insert(schema.${names.pluralCamel})
|
|
2601
3804
|
.values({
|
|
2602
3805
|
id: crypto.randomUUID(),
|
|
2603
|
-
name: input.name,
|
|
2604
|
-
|
|
3806
|
+
${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
3807
|
+
version: 1,
|
|
3808
|
+
createdAt: now,
|
|
3809
|
+
updatedAt: now,
|
|
2605
3810
|
})
|
|
2606
3811
|
.returning();
|
|
2607
3812
|
|
|
@@ -2611,17 +3816,20 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
2611
3816
|
|
|
2612
3817
|
return to${names.singularPascal}(row);
|
|
2613
3818
|
},
|
|
3819
|
+
${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
3820
|
};
|
|
2615
3821
|
}
|
|
2616
3822
|
`;
|
|
2617
3823
|
}
|
|
2618
|
-
function contractFile(names, config) {
|
|
2619
|
-
return `import {
|
|
3824
|
+
function contractFile(names, config, mode, options) {
|
|
3825
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
2620
3826
|
import { z } from "zod";
|
|
2621
|
-
import {
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
3827
|
+
${mode === "resource" ? `import { errors } from "${aliasModule(resourceSharedErrorsPath(config))}";\n` : ""}import {
|
|
3828
|
+
Create${names.singularPascal}InputSchema,
|
|
3829
|
+
List${names.pluralPascal}InputSchema,
|
|
3830
|
+
List${names.pluralPascal}OutputSchema,
|
|
3831
|
+
${names.singularPascal}Schema,
|
|
3832
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n Update${names.singularPascal}BodySchema,\n` : ""}} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2625
3833
|
|
|
2626
3834
|
const ErrorResponseSchema = z.object({
|
|
2627
3835
|
code: z.string(),
|
|
@@ -2629,7 +3837,7 @@ const ErrorResponseSchema = z.object({
|
|
|
2629
3837
|
requestId: z.string().optional(),
|
|
2630
3838
|
});
|
|
2631
3839
|
|
|
2632
|
-
const ${names.pluralCamel} =
|
|
3840
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
2633
3841
|
.namespace("${names.pluralCamel}")
|
|
2634
3842
|
.responses({
|
|
2635
3843
|
500: ErrorResponseSchema,
|
|
@@ -2637,21 +3845,49 @@ const ${names.pluralCamel} = createContractGroup()
|
|
|
2637
3845
|
|
|
2638
3846
|
export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
2639
3847
|
.get("/api/${names.pluralKebab}")
|
|
2640
|
-
.query(
|
|
3848
|
+
.query(List${names.pluralPascal}InputSchema)
|
|
2641
3849
|
.responses({
|
|
2642
|
-
200:
|
|
3850
|
+
200: List${names.pluralPascal}OutputSchema,
|
|
2643
3851
|
});
|
|
2644
3852
|
|
|
2645
3853
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
2646
3854
|
.post("/api/${names.pluralKebab}")
|
|
2647
|
-
.body(
|
|
3855
|
+
.body(Create${names.singularPascal}InputSchema)
|
|
3856
|
+
${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
3857
|
+
201: ${names.singularPascal}Schema,
|
|
3858
|
+
});
|
|
3859
|
+
${mode === "resource"
|
|
3860
|
+
? `
|
|
3861
|
+
export const get${names.singularPascal} = ${names.pluralCamel}
|
|
3862
|
+
.get("/api/${names.pluralKebab}/:id")
|
|
3863
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3864
|
+
.errors({ ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
3865
|
+
.responses({
|
|
3866
|
+
200: ${names.singularPascal}Schema,
|
|
3867
|
+
});
|
|
3868
|
+
|
|
3869
|
+
export const update${names.singularPascal} = ${names.pluralCamel}
|
|
3870
|
+
.patch("/api/${names.pluralKebab}/:id")
|
|
3871
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3872
|
+
.body(Update${names.singularPascal}BodySchema)
|
|
3873
|
+
${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 })
|
|
3874
|
+
.responses({
|
|
3875
|
+
200: ${names.singularPascal}Schema,
|
|
3876
|
+
});
|
|
3877
|
+
|
|
3878
|
+
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
3879
|
+
.delete("/api/${names.pluralKebab}/:id")
|
|
3880
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3881
|
+
${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 })
|
|
2648
3882
|
.responses({
|
|
2649
|
-
|
|
3883
|
+
204: null,
|
|
2650
3884
|
});
|
|
3885
|
+
`
|
|
3886
|
+
: ""}
|
|
2651
3887
|
`;
|
|
2652
3888
|
}
|
|
2653
3889
|
function standaloneContractFile(names) {
|
|
2654
|
-
return `import {
|
|
3890
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
2655
3891
|
import { z } from "zod";
|
|
2656
3892
|
|
|
2657
3893
|
const ErrorResponseSchema = z.object({
|
|
@@ -2660,7 +3896,7 @@ const ErrorResponseSchema = z.object({
|
|
|
2660
3896
|
requestId: z.string().optional(),
|
|
2661
3897
|
});
|
|
2662
3898
|
|
|
2663
|
-
const ${names.pluralCamel} =
|
|
3899
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
2664
3900
|
.namespace("${names.pluralCamel}")
|
|
2665
3901
|
.responses({
|
|
2666
3902
|
500: ErrorResponseSchema,
|
|
@@ -2696,7 +3932,8 @@ export const ${names.pluralCamel}Contracts = [list${names.pluralPascal}];
|
|
|
2696
3932
|
`;
|
|
2697
3933
|
}
|
|
2698
3934
|
function standaloneUseCaseFile(names, config, filePath) {
|
|
2699
|
-
return `import {
|
|
3935
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3936
|
+
import { z } from "zod";
|
|
2700
3937
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2701
3938
|
|
|
2702
3939
|
export const ${names.action.pascal}InputSchema = z.object({});
|
|
@@ -2724,29 +3961,27 @@ function useCaseTestFile(names, config) {
|
|
|
2724
3961
|
return `import { describe, expect, it } from "bun:test";
|
|
2725
3962
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
2726
3963
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
2727
|
-
import {
|
|
3964
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
3965
|
+
import { createTestAnonymousActor } from "@beignet/core/ports/testing";
|
|
2728
3966
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2729
3967
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
2730
3968
|
import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
|
|
2731
3969
|
|
|
2732
3970
|
describe("${names.exportName}", () => {
|
|
2733
3971
|
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
|
-
}));
|
|
3972
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
3973
|
+
base: appPorts,
|
|
3974
|
+
overrides: {
|
|
3975
|
+
gate: appPorts.gate,
|
|
3976
|
+
devtools: createInMemoryDevtools(),
|
|
3977
|
+
},
|
|
3978
|
+
});
|
|
3979
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
3980
|
+
ports: testFixture.ports,
|
|
3981
|
+
actor: createTestAnonymousActor(),
|
|
3982
|
+
tenant: null,
|
|
3983
|
+
});
|
|
3984
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
2750
3985
|
|
|
2751
3986
|
const result = await tester.run(${names.exportName}, {});
|
|
2752
3987
|
|
|
@@ -2807,6 +4042,134 @@ export const ${names.policyName} = definePolicy({
|
|
|
2807
4042
|
});
|
|
2808
4043
|
`;
|
|
2809
4044
|
}
|
|
4045
|
+
function resourcePolicyFile(names, _config, options) {
|
|
4046
|
+
return `import type { ActivityActor, ActivityTenant } from "@beignet/core/ports";
|
|
4047
|
+
import { definePolicy, deny } from "@beignet/core/ports";
|
|
4048
|
+
|
|
4049
|
+
export type AuthorizationContext = {
|
|
4050
|
+
actor: ActivityActor;
|
|
4051
|
+
tenant?: ActivityTenant;
|
|
4052
|
+
};
|
|
4053
|
+
|
|
4054
|
+
type ${names.singularPascal}PolicyResource = {
|
|
4055
|
+
id: string;
|
|
4056
|
+
tenantId?: string;
|
|
4057
|
+
};
|
|
4058
|
+
|
|
4059
|
+
function isAuthenticated(ctx: AuthorizationContext) {
|
|
4060
|
+
return ctx.actor.type === "user" && Boolean(ctx.actor.id);
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
function canWrite(ctx: AuthorizationContext) {
|
|
4064
|
+
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
4065
|
+
${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
export const ${names.singularCamel}Policy = definePolicy({
|
|
4069
|
+
"${names.pluralCamel}.view": (_ctx: AuthorizationContext) => true,
|
|
4070
|
+
"${names.pluralCamel}.create": (ctx: AuthorizationContext) =>
|
|
4071
|
+
canWrite(ctx),
|
|
4072
|
+
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
4073
|
+
const decision = canWrite(ctx);
|
|
4074
|
+
if (decision !== true) return decision;
|
|
4075
|
+
${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` : ""}
|
|
4076
|
+
return true;
|
|
4077
|
+
},
|
|
4078
|
+
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
4079
|
+
const decision = canWrite(ctx);
|
|
4080
|
+
if (decision !== true) return decision;
|
|
4081
|
+
${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` : ""}
|
|
4082
|
+
return true;
|
|
4083
|
+
},
|
|
4084
|
+
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
4085
|
+
deny("You are not allowed to manage this ${names.singularKebab}."),
|
|
4086
|
+
});
|
|
4087
|
+
`;
|
|
4088
|
+
}
|
|
4089
|
+
function resourceEventsFile(names) {
|
|
4090
|
+
return `import { defineEvent } from "@beignet/core/events";
|
|
4091
|
+
import { z } from "zod";
|
|
4092
|
+
|
|
4093
|
+
export const ${names.singularPascal}Created = defineEvent("${names.pluralCamel}.created", {
|
|
4094
|
+
payload: z.object({
|
|
4095
|
+
id: z.string().uuid(),
|
|
4096
|
+
}),
|
|
4097
|
+
});
|
|
4098
|
+
|
|
4099
|
+
export const ${names.singularPascal}Updated = defineEvent("${names.pluralCamel}.updated", {
|
|
4100
|
+
payload: z.object({
|
|
4101
|
+
id: z.string().uuid(),
|
|
4102
|
+
}),
|
|
4103
|
+
});
|
|
4104
|
+
|
|
4105
|
+
export const ${names.singularPascal}Deleted = defineEvent("${names.pluralCamel}.deleted", {
|
|
4106
|
+
payload: z.object({
|
|
4107
|
+
id: z.string().uuid(),
|
|
4108
|
+
}),
|
|
4109
|
+
});
|
|
4110
|
+
|
|
4111
|
+
export const ${names.singularCamel}Events = [
|
|
4112
|
+
${names.singularPascal}Created,
|
|
4113
|
+
${names.singularPascal}Updated,
|
|
4114
|
+
${names.singularPascal}Deleted,
|
|
4115
|
+
] as const;
|
|
4116
|
+
`;
|
|
4117
|
+
}
|
|
4118
|
+
function resourcePolicyTestFile(names, options) {
|
|
4119
|
+
return `import { describe, expect, it } from "bun:test";
|
|
4120
|
+
import { createPolicyTester } from "@beignet/core/ports/testing";
|
|
4121
|
+
import { ${names.singularCamel}Policy } from "../policy";
|
|
4122
|
+
import type { ${names.singularPascal} } from "../schemas";
|
|
4123
|
+
|
|
4124
|
+
function create${names.singularPascal}(): ${names.singularPascal} {
|
|
4125
|
+
const now = new Date().toISOString();
|
|
4126
|
+
return {
|
|
4127
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
4128
|
+
${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
4129
|
+
version: 1,
|
|
4130
|
+
createdAt: now,
|
|
4131
|
+
updatedAt: now,
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
describe("${names.singularCamel}Policy", () => {
|
|
4136
|
+
it("documents generated ${names.pluralKebab} authorization rules", async () => {
|
|
4137
|
+
const tester = createPolicyTester({
|
|
4138
|
+
policies: [${names.singularCamel}Policy],
|
|
4139
|
+
});
|
|
4140
|
+
const ${names.singularCamel} = create${names.singularPascal}();
|
|
4141
|
+
const ctx = {
|
|
4142
|
+
actor: { type: "user" as const, id: "user_test" },
|
|
4143
|
+
${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
4144
|
+
|
|
4145
|
+
await expect(
|
|
4146
|
+
tester.assertMatrix([
|
|
4147
|
+
{
|
|
4148
|
+
name: "authenticated actor can create ${names.singularKebab}",
|
|
4149
|
+
ctx,
|
|
4150
|
+
ability: "${names.pluralCamel}.create",
|
|
4151
|
+
expected: "allow",
|
|
4152
|
+
},
|
|
4153
|
+
{
|
|
4154
|
+
name: "authenticated actor can update ${names.singularKebab}",
|
|
4155
|
+
ctx,
|
|
4156
|
+
ability: "${names.pluralCamel}.update",
|
|
4157
|
+
subject: ${names.singularCamel},
|
|
4158
|
+
expected: "allow",
|
|
4159
|
+
},
|
|
4160
|
+
{
|
|
4161
|
+
name: "authenticated actor can delete ${names.singularKebab}",
|
|
4162
|
+
ctx,
|
|
4163
|
+
ability: "${names.pluralCamel}.delete",
|
|
4164
|
+
subject: ${names.singularCamel},
|
|
4165
|
+
expected: "allow",
|
|
4166
|
+
},
|
|
4167
|
+
]),
|
|
4168
|
+
).resolves.toBeUndefined();
|
|
4169
|
+
});
|
|
4170
|
+
});
|
|
4171
|
+
`;
|
|
4172
|
+
}
|
|
2810
4173
|
function eventFile(names) {
|
|
2811
4174
|
return `import { defineEvent } from "@beignet/core/events";
|
|
2812
4175
|
import { z } from "zod";
|
|
@@ -2823,11 +4186,9 @@ export const ${names.eventExportName} = defineEvent("${names.eventName}", {
|
|
|
2823
4186
|
`;
|
|
2824
4187
|
}
|
|
2825
4188
|
function jobFile(names, config) {
|
|
2826
|
-
return `import {
|
|
4189
|
+
return `import { retry } from "@beignet/core/jobs";
|
|
2827
4190
|
import { z } from "zod";
|
|
2828
|
-
import
|
|
2829
|
-
|
|
2830
|
-
const jobs = createJobHandlers<AppContext>();
|
|
4191
|
+
import { defineJob } from "${aliasModule(config.paths.jobsBuilder)}";
|
|
2831
4192
|
|
|
2832
4193
|
export const ${names.payloadSchemaName} = z.object({
|
|
2833
4194
|
\tid: z.string().uuid(),
|
|
@@ -2835,7 +4196,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
2835
4196
|
|
|
2836
4197
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2837
4198
|
|
|
2838
|
-
export const ${names.jobExportName} =
|
|
4199
|
+
export const ${names.jobExportName} = defineJob("${names.jobName}", {
|
|
2839
4200
|
\tpayload: ${names.payloadSchemaName},
|
|
2840
4201
|
\tretry: retry.exponential({
|
|
2841
4202
|
\t\tattempts: 3,
|
|
@@ -2851,12 +4212,38 @@ export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
|
|
|
2851
4212
|
});
|
|
2852
4213
|
`;
|
|
2853
4214
|
}
|
|
4215
|
+
function taskFile(names, config) {
|
|
4216
|
+
return `import { z } from "zod";
|
|
4217
|
+
import { defineTask } from "${aliasModule(config.paths.tasksBuilder)}";
|
|
4218
|
+
|
|
4219
|
+
export const ${names.inputSchemaName} = z.object({
|
|
4220
|
+
\tdryRun: z.boolean().default(true),
|
|
4221
|
+
});
|
|
4222
|
+
|
|
4223
|
+
export type ${names.inputTypeName} = z.infer<typeof ${names.inputSchemaName}>;
|
|
4224
|
+
|
|
4225
|
+
export const ${names.taskExportName} = defineTask("${names.taskName}", {
|
|
4226
|
+
\tinput: ${names.inputSchemaName},
|
|
4227
|
+
\tdescription: "Operational ${names.feature.kebab} ${names.artifact.kebab} task.",
|
|
4228
|
+
\tasync handle({ input, ctx }) {
|
|
4229
|
+
\t\tctx.ports.logger.info("Task handled", {
|
|
4230
|
+
\t\t\ttaskName: "${names.taskName}",
|
|
4231
|
+
\t\t\tdryRun: input.dryRun,
|
|
4232
|
+
\t\t});
|
|
4233
|
+
|
|
4234
|
+
\t\treturn {
|
|
4235
|
+
\t\t\tdryRun: input.dryRun,
|
|
4236
|
+
\t\t};
|
|
4237
|
+
\t},
|
|
4238
|
+
});
|
|
4239
|
+
`;
|
|
4240
|
+
}
|
|
2854
4241
|
function factoryFile(names, config) {
|
|
2855
4242
|
const resource = featureResourceNames(names);
|
|
2856
|
-
return `import {
|
|
4243
|
+
return `import { createFactory } from "@beignet/core/testing";
|
|
2857
4244
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2858
4245
|
|
|
2859
|
-
export const ${names.factoryExportName} =
|
|
4246
|
+
export const ${names.factoryExportName} = createFactory("${names.factoryName}", {
|
|
2860
4247
|
\tdefaults: ({ sequence }) => ({
|
|
2861
4248
|
\t\tname: "${resource.singularPascal} " + sequence,
|
|
2862
4249
|
\t}),
|
|
@@ -2880,14 +4267,9 @@ export const ${names.seedExportName} = defineSeed("${names.seedName}", {
|
|
|
2880
4267
|
`;
|
|
2881
4268
|
}
|
|
2882
4269
|
function notificationFile(names, config) {
|
|
2883
|
-
return `import {
|
|
2884
|
-
\tcreateNotificationHandlers,
|
|
2885
|
-
\tdefineMailNotificationChannel,
|
|
2886
|
-
} from "@beignet/core/notifications";
|
|
4270
|
+
return `import { defineMailNotificationChannel } from "@beignet/core/notifications";
|
|
2887
4271
|
import { z } from "zod";
|
|
2888
|
-
import
|
|
2889
|
-
|
|
2890
|
-
const notifications = createNotificationHandlers<AppContext>();
|
|
4272
|
+
import { defineNotification } from "${aliasModule(config.paths.notificationsBuilder)}";
|
|
2891
4273
|
|
|
2892
4274
|
export const ${names.payloadSchemaName} = z.object({
|
|
2893
4275
|
\tid: z.string().uuid(),
|
|
@@ -2897,7 +4279,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
2897
4279
|
|
|
2898
4280
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2899
4281
|
|
|
2900
|
-
export const ${names.notificationExportName} =
|
|
4282
|
+
export const ${names.notificationExportName} = defineNotification(
|
|
2901
4283
|
\t"${names.notificationName}",
|
|
2902
4284
|
\t{
|
|
2903
4285
|
\t\tpayload: ${names.payloadSchemaName},
|
|
@@ -2914,13 +4296,10 @@ export const ${names.notificationExportName} = notifications.defineNotification(
|
|
|
2914
4296
|
}
|
|
2915
4297
|
function listenerFile(names, config) {
|
|
2916
4298
|
const filePath = listenerFilePath(names, config);
|
|
2917
|
-
return `import {
|
|
2918
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4299
|
+
return `import { defineListener } from "${aliasModule(config.paths.listenersBuilder)}";
|
|
2919
4300
|
import { ${names.event.eventExportName} } from "${relativeModule(filePath, eventFilePath(names.event, config))}";
|
|
2920
4301
|
|
|
2921
|
-
const
|
|
2922
|
-
|
|
2923
|
-
export const ${names.listenerExportName} = events.defineListener(${names.event.eventExportName}, {
|
|
4302
|
+
export const ${names.listenerExportName} = defineListener(${names.event.eventExportName}, {
|
|
2924
4303
|
\tname: "${names.listenerName}",
|
|
2925
4304
|
\tasync handle({ payload, ctx }) {
|
|
2926
4305
|
\t\tctx.ports.logger.info("Listener handled", {
|
|
@@ -2932,11 +4311,8 @@ export const ${names.listenerExportName} = events.defineListener(${names.event.e
|
|
|
2932
4311
|
`;
|
|
2933
4312
|
}
|
|
2934
4313
|
function scheduleFile(names, config) {
|
|
2935
|
-
return `import {
|
|
2936
|
-
import {
|
|
2937
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2938
|
-
|
|
2939
|
-
const schedules = createScheduleHandlers<AppContext>();
|
|
4314
|
+
return `import { z } from "zod";
|
|
4315
|
+
import { defineSchedule } from "${aliasModule(config.paths.schedulesBuilder)}";
|
|
2940
4316
|
|
|
2941
4317
|
export const ${names.payloadSchemaName} = z.object({
|
|
2942
4318
|
\tdate: z.string(),
|
|
@@ -2944,7 +4320,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
2944
4320
|
|
|
2945
4321
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2946
4322
|
|
|
2947
|
-
export const ${names.scheduleExportName} =
|
|
4323
|
+
export const ${names.scheduleExportName} = defineSchedule(
|
|
2948
4324
|
\t"${names.scheduleName}",
|
|
2949
4325
|
\t{
|
|
2950
4326
|
\t\tcron: "${names.cron}",
|
|
@@ -3020,108 +4396,110 @@ export const ${names.uploadExportName} = defineUpload<
|
|
|
3020
4396
|
});
|
|
3021
4397
|
`;
|
|
3022
4398
|
}
|
|
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)}";
|
|
4399
|
+
function featureUiComponentFile(names, config) {
|
|
4400
|
+
return `"use client";
|
|
3029
4401
|
|
|
3030
|
-
|
|
3031
|
-
|
|
4402
|
+
import { contractErrorMessage } from "@beignet/core/client";
|
|
4403
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4404
|
+
import { useState } from "react";
|
|
4405
|
+
import { rq } from "${aliasModule(clientIndexPath(config))}";
|
|
4406
|
+
import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
3032
4407
|
|
|
3033
|
-
|
|
3034
|
-
\
|
|
3035
|
-
\
|
|
3036
|
-
\
|
|
3037
|
-
\t\
|
|
3038
|
-
\t
|
|
4408
|
+
export function ${names.componentExportName}() {
|
|
4409
|
+
\tconst [name, setName] = useState("");
|
|
4410
|
+
\tconst queryClient = useQueryClient();
|
|
4411
|
+
\tconst ${names.pluralCamel}Query = useQuery(
|
|
4412
|
+
\t\trq(list${names.pluralPascal}).queryOptions({ query: {} }),
|
|
4413
|
+
\t);
|
|
4414
|
+
\tconst create${names.singularPascal}Mutation = useMutation(
|
|
4415
|
+
\t\trq(create${names.singularPascal}).mutationOptions({
|
|
4416
|
+
\t\t\tonSuccess: async () => {
|
|
4417
|
+
\t\t\t\tsetName("");
|
|
4418
|
+
\t\t\t\tawait rq(list${names.pluralPascal}).invalidate(queryClient);
|
|
3039
4419
|
\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});
|
|
4420
|
+
\t\t}),
|
|
4421
|
+
\t);
|
|
3111
4422
|
|
|
3112
|
-
\
|
|
3113
|
-
\t\
|
|
3114
|
-
\t\t\
|
|
3115
|
-
\t\t}
|
|
3116
|
-
\t
|
|
3117
|
-
\t\
|
|
3118
|
-
\t
|
|
4423
|
+
\treturn (
|
|
4424
|
+
\t\t<section className="${names.pluralKebab}-panel">
|
|
4425
|
+
\t\t\t<form
|
|
4426
|
+
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
4427
|
+
\t\t\t\tonSubmit={(event) => {
|
|
4428
|
+
\t\t\t\t\tevent.preventDefault();
|
|
4429
|
+
\t\t\t\t\tconst trimmedName = name.trim();
|
|
4430
|
+
\t\t\t\t\tif (!trimmedName) return;
|
|
4431
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
4432
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
4433
|
+
\t\t\t\t\t\tbody: { name: trimmedName },
|
|
4434
|
+
\t\t\t\t\t});
|
|
4435
|
+
\t\t\t\t}}
|
|
4436
|
+
\t\t\t>
|
|
4437
|
+
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
4438
|
+
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
4439
|
+
\t\t\t\t\t<input
|
|
4440
|
+
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
4441
|
+
\t\t\t\t\t\tvalue={name}
|
|
4442
|
+
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
4443
|
+
\t\t\t\t\t\tplaceholder="Name"
|
|
4444
|
+
\t\t\t\t\t/>
|
|
4445
|
+
\t\t\t\t\t<button
|
|
4446
|
+
\t\t\t\t\t\ttype="submit"
|
|
4447
|
+
\t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
|
|
4448
|
+
\t\t\t\t\t>
|
|
4449
|
+
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
4450
|
+
\t\t\t\t\t</button>
|
|
4451
|
+
\t\t\t\t</div>
|
|
4452
|
+
\t\t\t\t{create${names.singularPascal}Mutation.isError ? (
|
|
4453
|
+
\t\t\t\t\t<p role="alert">
|
|
4454
|
+
\t\t\t\t\t\t{contractErrorMessage(
|
|
4455
|
+
\t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
|
|
4456
|
+
\t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
|
|
4457
|
+
\t\t\t\t\t\t)}
|
|
4458
|
+
\t\t\t\t\t</p>
|
|
4459
|
+
\t\t\t\t) : null}
|
|
4460
|
+
\t\t\t</form>
|
|
3119
4461
|
|
|
3120
|
-
\
|
|
4462
|
+
\t\t\t<div className="${names.pluralKebab}-list">
|
|
4463
|
+
\t\t\t\t<div className="${names.pluralKebab}-list-heading">
|
|
4464
|
+
\t\t\t\t\t<h2>${names.pluralPascal}</h2>
|
|
4465
|
+
\t\t\t\t\t<span>{${names.pluralCamel}Query.data?.items.length ?? 0} shown</span>
|
|
4466
|
+
\t\t\t\t</div>
|
|
4467
|
+
\t\t\t\t{${names.pluralCamel}Query.isLoading ? <p>Loading...</p> : null}
|
|
4468
|
+
\t\t\t\t{${names.pluralCamel}Query.isError ? (
|
|
4469
|
+
\t\t\t\t\t<p role="alert">Could not load ${names.pluralKebab}.</p>
|
|
4470
|
+
\t\t\t\t) : null}
|
|
4471
|
+
\t\t\t\t{${names.pluralCamel}Query.data?.items.length === 0 ? (
|
|
4472
|
+
\t\t\t\t\t<p>No ${names.pluralKebab} yet.</p>
|
|
4473
|
+
\t\t\t\t) : null}
|
|
4474
|
+
\t\t\t\t<ul>
|
|
4475
|
+
\t\t\t\t\t{${names.pluralCamel}Query.data?.items.map((${names.singularCamel}) => (
|
|
4476
|
+
\t\t\t\t\t\t<li key={${names.singularCamel}.id}>
|
|
4477
|
+
\t\t\t\t\t\t\t<strong>{${names.singularCamel}.name}</strong>
|
|
4478
|
+
\t\t\t\t\t\t\t<small>{new Date(${names.singularCamel}.createdAt).toLocaleDateString()}</small>
|
|
4479
|
+
\t\t\t\t\t\t</li>
|
|
4480
|
+
\t\t\t\t\t))}
|
|
4481
|
+
\t\t\t\t</ul>
|
|
4482
|
+
\t\t\t</div>
|
|
4483
|
+
\t\t</section>
|
|
4484
|
+
\t);
|
|
4485
|
+
}
|
|
4486
|
+
`;
|
|
3121
4487
|
}
|
|
4488
|
+
function scheduleRouteFile(names, config) {
|
|
4489
|
+
return `import { createScheduleRoute } from "@beignet/next";
|
|
4490
|
+
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
4491
|
+
import { env } from "@/lib/env";
|
|
4492
|
+
import { server } from "${aliasModule(config.paths.server)}";
|
|
3122
4493
|
|
|
3123
|
-
export const
|
|
3124
|
-
|
|
4494
|
+
export const runtime = "nodejs";
|
|
4495
|
+
|
|
4496
|
+
export const { GET, POST } = createScheduleRoute({
|
|
4497
|
+
\tserver,
|
|
4498
|
+
\tschedules: [${names.scheduleExportName}],
|
|
4499
|
+
\tschedule: ${names.scheduleExportName}.name,
|
|
4500
|
+
\tsecret: env.CRON_SECRET,
|
|
4501
|
+
\tsource: "cron-route",
|
|
4502
|
+
});
|
|
3125
4503
|
`;
|
|
3126
4504
|
}
|
|
3127
4505
|
function useCaseIndexExport(names) {
|
|
@@ -3134,78 +4512,138 @@ function useCaseIndexExport(names) {
|
|
|
3134
4512
|
} from "./${names.action.kebab}";
|
|
3135
4513
|
`;
|
|
3136
4514
|
}
|
|
3137
|
-
function routeGroupFile(names, config) {
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
4515
|
+
function routeGroupFile(names, config, mode, _options) {
|
|
4516
|
+
const routeFilePath = path.join(config.paths.features, names.pluralKebab, "routes.ts");
|
|
4517
|
+
const contractsPath = resourceContractFilePath(names, config);
|
|
4518
|
+
const useCasesPath = resourceUseCaseIndexPath(names, config);
|
|
4519
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
4520
|
+
import { defineRouteGroup } from "@beignet/next";
|
|
4521
|
+
import type { AppContext } from "${relativeModule(routeFilePath, config.paths.appContext)}";
|
|
4522
|
+
import {
|
|
4523
|
+
create${names.singularPascal},
|
|
4524
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
4525
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${relativeModule(routeFilePath, contractsPath)}";
|
|
3141
4526
|
import {
|
|
3142
4527
|
create${names.singularPascal}UseCase,
|
|
3143
|
-
list${names.pluralPascal}UseCase,
|
|
3144
|
-
|
|
4528
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
4529
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${relativeModule(routeFilePath, useCasesPath)}";
|
|
3145
4530
|
|
|
3146
4531
|
export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>({
|
|
3147
4532
|
name: "${names.pluralKebab}",
|
|
3148
4533
|
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
|
-
],
|
|
4534
|
+
{ contract: list${names.pluralPascal}, useCase: list${names.pluralPascal}UseCase },
|
|
4535
|
+
{ contract: create${names.singularPascal}, useCase: create${names.singularPascal}UseCase },
|
|
4536
|
+
${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
4537
|
});
|
|
3165
4538
|
`;
|
|
3166
4539
|
}
|
|
3167
|
-
function testFile(names, config) {
|
|
4540
|
+
function testFile(names, config, mode, options) {
|
|
3168
4541
|
const repositoryPath = path.join(path.dirname(config.paths.infrastructurePorts), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
|
|
4542
|
+
const serverContextPath = path.join(path.dirname(config.paths.server), "context.ts");
|
|
4543
|
+
const requestTarget = options.tenant ? "requester" : "app";
|
|
3169
4544
|
return `import { describe, expect, it } from "bun:test";
|
|
3170
4545
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
3171
|
-
import { defineRoutes } from "@beignet/web";
|
|
3172
|
-
import { createTestApp } from "@beignet/web/testing";
|
|
4546
|
+
${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` : ""}import { defineRoutes } from "@beignet/web";
|
|
4547
|
+
import { createTestApp${options.tenant ? ", createTestRequester" : ""} } from "@beignet/web/testing";
|
|
3173
4548
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3174
|
-
import {
|
|
4549
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
4550
|
+
import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/ports/testing";
|
|
3175
4551
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3176
4552
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
3177
|
-
import {
|
|
4553
|
+
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
4554
|
+
import {
|
|
4555
|
+
create${names.singularPascal},
|
|
4556
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
4557
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
3178
4558
|
import { createInMemory${names.singularPascal}Repository } from "${aliasModule(repositoryPath)}";
|
|
3179
4559
|
import { ${names.singularCamel}Routes } from "${aliasModule(path.join(resourceFeatureDir(names, config), "routes.ts"))}";
|
|
3180
4560
|
import {
|
|
3181
4561
|
create${names.singularPascal}UseCase,
|
|
3182
|
-
list${names.pluralPascal}UseCase,
|
|
3183
|
-
} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
4562
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
4563
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
3184
4564
|
|
|
3185
4565
|
describe("${names.pluralCamel} resource", () => {
|
|
3186
|
-
it("creates and lists ${names.pluralCamel}", async () => {
|
|
3187
|
-
const ${names.
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
4566
|
+
it("${mode === "resource" ? `exercises ${names.pluralCamel} CRUD` : `creates and lists ${names.pluralCamel}`}", async () => {
|
|
4567
|
+
const seed${names.pluralPascal} = [
|
|
4568
|
+
{
|
|
4569
|
+
id: "00000000-0000-4000-8000-000000000101",
|
|
4570
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
4571
|
+
version: 1,
|
|
4572
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
4573
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
4574
|
+
},
|
|
4575
|
+
{
|
|
4576
|
+
id: "00000000-0000-4000-8000-000000000102",
|
|
4577
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
4578
|
+
version: 1,
|
|
4579
|
+
createdAt: "2024-01-02T00:00:00.000Z",
|
|
4580
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
4581
|
+
},
|
|
4582
|
+
{
|
|
4583
|
+
id: "00000000-0000-4000-8000-000000000103",
|
|
4584
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
4585
|
+
version: 1,
|
|
4586
|
+
createdAt: "2024-01-03T00:00:00.000Z",
|
|
4587
|
+
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
4588
|
+
},
|
|
4589
|
+
${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` : ""} ];
|
|
4590
|
+
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
4591
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
4592
|
+
base: appPorts,
|
|
4593
|
+
overrides: {
|
|
3193
4594
|
${names.pluralCamel},
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
4595
|
+
${options.auth
|
|
4596
|
+
? `\t\t\t\tauth: {
|
|
4597
|
+
getSession: async () => ({
|
|
4598
|
+
user: { id: "user_test", name: "Test User" },
|
|
4599
|
+
}),
|
|
4600
|
+
},
|
|
4601
|
+
gate: appPorts.gate,\n`
|
|
4602
|
+
: `\t\t\t\tauth: {
|
|
4603
|
+
getSession: async () => null,
|
|
4604
|
+
},\n`} devtools: createInMemoryDevtools(),
|
|
4605
|
+
},
|
|
4606
|
+
transaction: {
|
|
4607
|
+
ports: (ports) => ({
|
|
4608
|
+
...ports,
|
|
4609
|
+
${names.pluralCamel},
|
|
4610
|
+
}),
|
|
4611
|
+
},
|
|
4612
|
+
});
|
|
4613
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
4614
|
+
ports: testFixture.ports,
|
|
4615
|
+
actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
4616
|
+
tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
|
|
3205
4617
|
});
|
|
3206
4618
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
3207
4619
|
|
|
3208
4620
|
const ctx = await tester.ctx();
|
|
4621
|
+
const defaultPage = await tester.run(list${names.pluralPascal}UseCase, {}, { ctx });
|
|
4622
|
+
const filteredPage = await tester.run(
|
|
4623
|
+
list${names.pluralPascal}UseCase,
|
|
4624
|
+
{ name: "alp" },
|
|
4625
|
+
{ ctx },
|
|
4626
|
+
);
|
|
4627
|
+
const firstPage = await tester.run(
|
|
4628
|
+
list${names.pluralPascal}UseCase,
|
|
4629
|
+
{ limit: 2 },
|
|
4630
|
+
{ ctx },
|
|
4631
|
+
);
|
|
4632
|
+
const secondPage = await tester.run(
|
|
4633
|
+
list${names.pluralPascal}UseCase,
|
|
4634
|
+
{ limit: 2, cursor: firstPage.page.nextCursor },
|
|
4635
|
+
{ ctx },
|
|
4636
|
+
);
|
|
4637
|
+
const nameAscendingPage = await tester.run(
|
|
4638
|
+
list${names.pluralPascal}UseCase,
|
|
4639
|
+
{ sortBy: "name", sortDirection: "asc" },
|
|
4640
|
+
{ ctx },
|
|
4641
|
+
);
|
|
4642
|
+
const nameDescendingPage = await tester.run(
|
|
4643
|
+
list${names.pluralPascal}UseCase,
|
|
4644
|
+
{ sortBy: "name", sortDirection: "desc" },
|
|
4645
|
+
{ ctx },
|
|
4646
|
+
);
|
|
3209
4647
|
const created = await tester.run(
|
|
3210
4648
|
create${names.singularPascal}UseCase,
|
|
3211
4649
|
{ name: "First ${names.singularPascal}" },
|
|
@@ -3213,35 +4651,111 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
3213
4651
|
);
|
|
3214
4652
|
const result = await tester.run(
|
|
3215
4653
|
list${names.pluralPascal}UseCase,
|
|
3216
|
-
{
|
|
4654
|
+
{ name: "First" },
|
|
3217
4655
|
{ ctx },
|
|
3218
4656
|
);
|
|
3219
4657
|
|
|
4658
|
+
expect(defaultPage.page).toMatchObject({
|
|
4659
|
+
kind: "cursor",
|
|
4660
|
+
limit: 20,
|
|
4661
|
+
cursor: null,
|
|
4662
|
+
nextCursor: null,
|
|
4663
|
+
hasMore: false,
|
|
4664
|
+
});
|
|
4665
|
+
expect(defaultPage.items.map((item) => item.name)).toEqual([
|
|
4666
|
+
"Gamma ${names.singularPascal}",
|
|
4667
|
+
"Beta ${names.singularPascal}",
|
|
4668
|
+
"Alpha ${names.singularPascal}",
|
|
4669
|
+
]);
|
|
4670
|
+
expect(filteredPage.items.map((item) => item.name)).toEqual([
|
|
4671
|
+
"Alpha ${names.singularPascal}",
|
|
4672
|
+
]);
|
|
4673
|
+
expect(firstPage.items.map((item) => item.name)).toEqual([
|
|
4674
|
+
"Gamma ${names.singularPascal}",
|
|
4675
|
+
"Beta ${names.singularPascal}",
|
|
4676
|
+
]);
|
|
4677
|
+
expect(firstPage.page.nextCursor).toEqual(expect.any(String));
|
|
4678
|
+
expect(secondPage.items.map((item) => item.name)).toEqual([
|
|
4679
|
+
"Alpha ${names.singularPascal}",
|
|
4680
|
+
]);
|
|
4681
|
+
expect(secondPage.page.nextCursor).toBeNull();
|
|
4682
|
+
expect(nameAscendingPage.items.map((item) => item.name)).toEqual([
|
|
4683
|
+
"Alpha ${names.singularPascal}",
|
|
4684
|
+
"Beta ${names.singularPascal}",
|
|
4685
|
+
"Gamma ${names.singularPascal}",
|
|
4686
|
+
]);
|
|
4687
|
+
expect(nameDescendingPage.items.map((item) => item.name)).toEqual([
|
|
4688
|
+
"Gamma ${names.singularPascal}",
|
|
4689
|
+
"Beta ${names.singularPascal}",
|
|
4690
|
+
"Alpha ${names.singularPascal}",
|
|
4691
|
+
]);
|
|
3220
4692
|
expect(created.name).toBe("First ${names.singularPascal}");
|
|
3221
|
-
expect(result.page.total).toBe(1);
|
|
3222
4693
|
expect(result.items).toEqual([created]);
|
|
4694
|
+
${mode === "resource"
|
|
4695
|
+
? `
|
|
4696
|
+
const fetched = await tester.run(
|
|
4697
|
+
get${names.singularPascal}UseCase,
|
|
4698
|
+
{ id: created.id },
|
|
4699
|
+
{ ctx },
|
|
4700
|
+
);
|
|
4701
|
+
const updated = await tester.run(
|
|
4702
|
+
update${names.singularPascal}UseCase,
|
|
4703
|
+
{ id: created.id, name: "Updated ${names.singularPascal}", version: fetched.version },
|
|
4704
|
+
{ ctx },
|
|
4705
|
+
);
|
|
4706
|
+
try {
|
|
4707
|
+
await tester.run(
|
|
4708
|
+
update${names.singularPascal}UseCase,
|
|
4709
|
+
{ id: created.id, name: "Stale ${names.singularPascal}", version: fetched.version },
|
|
4710
|
+
{ ctx },
|
|
4711
|
+
);
|
|
4712
|
+
throw new Error("Expected ${names.singularPascal}Conflict");
|
|
4713
|
+
} catch (error) {
|
|
4714
|
+
expect(isAppError(error)).toBe(true);
|
|
4715
|
+
if (isAppError(error)) {
|
|
4716
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_CONFLICT");
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
await tester.run(delete${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
4720
|
+
const afterDelete = await tester.run(
|
|
4721
|
+
list${names.pluralPascal}UseCase,
|
|
4722
|
+
{ name: "Updated" },
|
|
4723
|
+
{ ctx },
|
|
4724
|
+
);
|
|
4725
|
+
|
|
4726
|
+
expect(fetched).toEqual(created);
|
|
4727
|
+
expect(updated.name).toBe("Updated ${names.singularPascal}");
|
|
4728
|
+
expect(updated.version).toBe(fetched.version + 1);
|
|
4729
|
+
expect(afterDelete.items).toEqual([]);
|
|
3223
4730
|
|
|
3224
|
-
|
|
3225
|
-
|
|
4731
|
+
try {
|
|
4732
|
+
await tester.run(get${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
4733
|
+
throw new Error("Expected ${names.singularPascal}NotFound");
|
|
4734
|
+
} catch (error) {
|
|
4735
|
+
expect(isAppError(error)).toBe(true);
|
|
4736
|
+
if (isAppError(error)) {
|
|
4737
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_NOT_FOUND");
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
`
|
|
4741
|
+
: ""}
|
|
4742
|
+
|
|
4743
|
+
const app = await createTestApp({
|
|
4744
|
+
ports: testFixture.ports,
|
|
3226
4745
|
routes: defineRoutes<AppContext>([${names.singularCamel}Routes]),
|
|
3227
|
-
|
|
3228
|
-
mapUnhandledError: () => ({
|
|
3229
|
-
status: 500,
|
|
3230
|
-
body: { code: "INTERNAL_SERVER_ERROR", message: "Internal server error" },
|
|
3231
|
-
}),
|
|
4746
|
+
context: appContext,
|
|
3232
4747
|
});
|
|
3233
|
-
|
|
3234
|
-
const createdViaRoute = await
|
|
4748
|
+
${options.tenant ? `\t\tconst requester = createTestRequester(app, {\n\t\t\theaders: { "x-tenant-id": "tenant_test" },\n\t\t});\n` : ""}
|
|
4749
|
+
const createdViaRoute = await ${requestTarget}.request(create${names.singularPascal}, {
|
|
3235
4750
|
body: { name: "Second ${names.singularPascal}" },
|
|
3236
4751
|
});
|
|
3237
|
-
const listedViaRoute = await
|
|
3238
|
-
query: {
|
|
4752
|
+
${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}, {
|
|
4753
|
+
query: { name: "${mode === "resource" ? "Updated via route" : "Second"}" },
|
|
3239
4754
|
});
|
|
3240
|
-
await app.stop();
|
|
4755
|
+
${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
4756
|
|
|
3242
4757
|
expect(createdViaRoute.name).toBe("Second ${names.singularPascal}");
|
|
3243
|
-
expect(
|
|
3244
|
-
expect(listedViaRoute.items).toContainEqual(createdViaRoute);
|
|
4758
|
+
${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
4759
|
});
|
|
3246
4760
|
});
|
|
3247
4761
|
`;
|