@beignet/cli 0.0.1 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +231 -0
- package/README.md +497 -90
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +72 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +88 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +42 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +136 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +12 -0
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +19 -24
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +37 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +146 -0
- package/dist/db.js.map +1 -0
- 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 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +710 -400
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +45 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +2191 -100
- 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 +22 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +370 -38
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +109 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +2225 -333
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates.d.ts +12 -8
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +2144 -385
- package/dist/templates.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +137 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +47 -0
- package/src/create-prompts.ts +182 -0
- package/src/create.ts +32 -28
- package/src/db.ts +222 -0
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1119 -535
- package/src/inspect.ts +3372 -134
- package/src/lib.ts +45 -0
- package/src/lint.ts +533 -45
- package/src/make.ts +3010 -397
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates.ts +2282 -462
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/src/create-bin.ts +0 -11
package/dist/make.js
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
|
|
4
|
+
export { makeFeatureAddonChoices } from "./choices.js";
|
|
4
5
|
export async function makeResource(options) {
|
|
6
|
+
return makeResourceSlice(options, "resource");
|
|
7
|
+
}
|
|
8
|
+
export async function makeFeature(options) {
|
|
9
|
+
const addons = normalizeMakeFeatureAddons(options.with ?? []);
|
|
10
|
+
if (addons.length === 0) {
|
|
11
|
+
return makeFeatureResource(options);
|
|
12
|
+
}
|
|
13
|
+
if (options.dryRun) {
|
|
14
|
+
return makeFeatureSlice(options, addons);
|
|
15
|
+
}
|
|
16
|
+
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
17
|
+
return makeFeatureSlice(options, addons);
|
|
18
|
+
}
|
|
19
|
+
async function makeFeatureResource(options) {
|
|
20
|
+
try {
|
|
21
|
+
return await makeResourceSlice(options, "feature");
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof Error && error.message.includes("make resource")) {
|
|
25
|
+
throw new Error(error.message.replaceAll("make resource", "make feature"));
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function makeResourceSlice(options, mode) {
|
|
5
31
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
6
32
|
const names = resourceNames(options.name);
|
|
7
33
|
const config = options.config
|
|
8
34
|
? resolveConfig(options.config)
|
|
9
35
|
: await loadBeignetConfig(targetDir);
|
|
10
36
|
await assertStandardApp(targetDir, config);
|
|
37
|
+
if (mode === "resource" &&
|
|
38
|
+
!(await fileExists(path.join(targetDir, resourceSharedErrorsPath(config))))) {
|
|
39
|
+
throw new Error(`beignet make resource expects an app error catalog. Missing ${resourceSharedErrorsPath(config)}.`);
|
|
40
|
+
}
|
|
11
41
|
const persistence = await detectResourcePersistence(targetDir, config);
|
|
12
|
-
const
|
|
42
|
+
const generationOptions = resourceGenerationOptions(options, mode);
|
|
43
|
+
const generatedFiles = resourceFiles(names, config, persistence, mode, generationOptions);
|
|
13
44
|
const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
|
|
14
45
|
force: Boolean(options.force),
|
|
15
46
|
});
|
|
16
47
|
const plannedWiringUpdates = await updateResourceWiring(targetDir, names, config, persistence, {
|
|
17
48
|
dryRun: true,
|
|
49
|
+
mode,
|
|
50
|
+
generationOptions,
|
|
18
51
|
});
|
|
19
52
|
const createdFiles = plannedFiles
|
|
20
53
|
.filter((file) => file.result === "created")
|
|
@@ -35,9 +68,12 @@ export async function makeResource(options) {
|
|
|
35
68
|
updatedFiles.push(...(await updateResourceWiring(targetDir, names, config, {
|
|
36
69
|
persistence,
|
|
37
70
|
dryRun: false,
|
|
71
|
+
mode,
|
|
72
|
+
generationOptions,
|
|
38
73
|
})));
|
|
39
74
|
}
|
|
40
75
|
return {
|
|
76
|
+
schemaVersion: 1,
|
|
41
77
|
name: names.pluralKebab,
|
|
42
78
|
targetDir,
|
|
43
79
|
dryRun: Boolean(options.dryRun),
|
|
@@ -47,6 +83,90 @@ export async function makeResource(options) {
|
|
|
47
83
|
skippedFiles,
|
|
48
84
|
};
|
|
49
85
|
}
|
|
86
|
+
function resourceGenerationOptions(options, mode) {
|
|
87
|
+
if (mode !== "resource") {
|
|
88
|
+
return { auth: false, tenant: false, events: false, softDelete: false };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
auth: Boolean(options.auth),
|
|
92
|
+
tenant: Boolean(options.tenant),
|
|
93
|
+
events: Boolean(options.events),
|
|
94
|
+
softDelete: Boolean(options.softDelete),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function makeFeatureSlice(options, addons) {
|
|
98
|
+
let result = await makeFeatureResource(options);
|
|
99
|
+
for (const addon of addons) {
|
|
100
|
+
result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options));
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
async function makeFeatureAddon(addon, feature, options) {
|
|
105
|
+
const shared = {
|
|
106
|
+
cwd: options.cwd,
|
|
107
|
+
force: options.force,
|
|
108
|
+
dryRun: options.dryRun,
|
|
109
|
+
config: options.config,
|
|
110
|
+
};
|
|
111
|
+
if (addon === "policy") {
|
|
112
|
+
return makePolicy({ ...shared, name: feature });
|
|
113
|
+
}
|
|
114
|
+
if (addon === "task") {
|
|
115
|
+
return makeTask({ ...shared, name: `${feature}/backfill` });
|
|
116
|
+
}
|
|
117
|
+
if (addon === "event") {
|
|
118
|
+
return makeEvent({ ...shared, name: `${feature}/created` });
|
|
119
|
+
}
|
|
120
|
+
if (addon === "job") {
|
|
121
|
+
return makeJob({ ...shared, name: `${feature}/process` });
|
|
122
|
+
}
|
|
123
|
+
if (addon === "notification") {
|
|
124
|
+
return makeNotification({ ...shared, name: `${feature}/created` });
|
|
125
|
+
}
|
|
126
|
+
if (addon === "ui") {
|
|
127
|
+
return makeFeatureUi({ ...shared, name: feature });
|
|
128
|
+
}
|
|
129
|
+
return makeUpload({ ...shared, name: `${feature}/attachment` });
|
|
130
|
+
}
|
|
131
|
+
function normalizeMakeFeatureAddons(addons) {
|
|
132
|
+
const normalized = new Set();
|
|
133
|
+
for (const addon of addons) {
|
|
134
|
+
if (addon === "events")
|
|
135
|
+
normalized.add("event");
|
|
136
|
+
else if (addon === "tasks")
|
|
137
|
+
normalized.add("task");
|
|
138
|
+
else if (addon === "jobs")
|
|
139
|
+
normalized.add("job");
|
|
140
|
+
else if (addon === "notifications")
|
|
141
|
+
normalized.add("notification");
|
|
142
|
+
else if (addon === "uploads")
|
|
143
|
+
normalized.add("upload");
|
|
144
|
+
else
|
|
145
|
+
normalized.add(addon);
|
|
146
|
+
}
|
|
147
|
+
return featureAddonOrder.filter((addon) => normalized.has(addon));
|
|
148
|
+
}
|
|
149
|
+
const featureAddonOrder = [
|
|
150
|
+
"policy",
|
|
151
|
+
"task",
|
|
152
|
+
"event",
|
|
153
|
+
"job",
|
|
154
|
+
"notification",
|
|
155
|
+
"ui",
|
|
156
|
+
"upload",
|
|
157
|
+
];
|
|
158
|
+
function mergeMakeResults(left, right) {
|
|
159
|
+
return {
|
|
160
|
+
...left,
|
|
161
|
+
files: uniqueStrings([...left.files, ...right.files]),
|
|
162
|
+
createdFiles: uniqueStrings([...left.createdFiles, ...right.createdFiles]),
|
|
163
|
+
updatedFiles: uniqueStrings([...left.updatedFiles, ...right.updatedFiles]),
|
|
164
|
+
skippedFiles: uniqueStrings([...left.skippedFiles, ...right.skippedFiles]),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function uniqueStrings(values) {
|
|
168
|
+
return [...new Set(values)];
|
|
169
|
+
}
|
|
50
170
|
export async function makeContract(options) {
|
|
51
171
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
52
172
|
const names = resourceNames(options.name);
|
|
@@ -70,6 +190,7 @@ export async function makeContract(options) {
|
|
|
70
190
|
skippedFiles.push(file.path);
|
|
71
191
|
}
|
|
72
192
|
return {
|
|
193
|
+
schemaVersion: 1,
|
|
73
194
|
name: names.pluralKebab,
|
|
74
195
|
targetDir,
|
|
75
196
|
dryRun: Boolean(options.dryRun),
|
|
@@ -113,6 +234,7 @@ export async function makeUseCase(options) {
|
|
|
113
234
|
if (indexResult === "skipped")
|
|
114
235
|
skippedFiles.push(indexFile.path);
|
|
115
236
|
return {
|
|
237
|
+
schemaVersion: 1,
|
|
116
238
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
117
239
|
targetDir,
|
|
118
240
|
dryRun: Boolean(options.dryRun),
|
|
@@ -145,10 +267,13 @@ export async function makeTest(options) {
|
|
|
145
267
|
if (result === "skipped")
|
|
146
268
|
skippedFiles.push(file.path);
|
|
147
269
|
}
|
|
148
|
-
if (await updatePackageJson(targetDir, {
|
|
270
|
+
if (await updatePackageJson(targetDir, {
|
|
271
|
+
dryRun: Boolean(options.dryRun),
|
|
272
|
+
})) {
|
|
149
273
|
updatedFiles.push("package.json");
|
|
150
274
|
}
|
|
151
275
|
return {
|
|
276
|
+
schemaVersion: 1,
|
|
152
277
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
153
278
|
targetDir,
|
|
154
279
|
dryRun: Boolean(options.dryRun),
|
|
@@ -200,6 +325,7 @@ export async function makePort(options) {
|
|
|
200
325
|
updatedFiles.push(config.paths.infrastructurePorts);
|
|
201
326
|
}
|
|
202
327
|
return {
|
|
328
|
+
schemaVersion: 1,
|
|
203
329
|
name: names.kebab,
|
|
204
330
|
targetDir,
|
|
205
331
|
dryRun: Boolean(options.dryRun),
|
|
@@ -238,6 +364,7 @@ export async function makeAdapter(options) {
|
|
|
238
364
|
updatedFiles.push(config.paths.infrastructurePorts);
|
|
239
365
|
}
|
|
240
366
|
return {
|
|
367
|
+
schemaVersion: 1,
|
|
241
368
|
name: names.kebab,
|
|
242
369
|
targetDir,
|
|
243
370
|
dryRun: Boolean(options.dryRun),
|
|
@@ -271,6 +398,7 @@ export async function makePolicy(options) {
|
|
|
271
398
|
skippedFiles.push(file.path);
|
|
272
399
|
}
|
|
273
400
|
return {
|
|
401
|
+
schemaVersion: 1,
|
|
274
402
|
name: names.kebab,
|
|
275
403
|
targetDir,
|
|
276
404
|
dryRun: Boolean(options.dryRun),
|
|
@@ -313,13 +441,115 @@ export async function makeJob(options) {
|
|
|
313
441
|
force: Boolean(options.force),
|
|
314
442
|
dryRun: Boolean(options.dryRun),
|
|
315
443
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
316
|
-
files:
|
|
444
|
+
files: [
|
|
445
|
+
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
446
|
+
...jobFiles(names, config),
|
|
447
|
+
],
|
|
317
448
|
index: featureArtifactIndexFile("job", names, config),
|
|
318
449
|
dependencies: {
|
|
319
450
|
"@beignet/core": beignetDependencyVersion,
|
|
320
451
|
},
|
|
321
452
|
});
|
|
322
453
|
}
|
|
454
|
+
export async function makeTask(options) {
|
|
455
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
456
|
+
const names = taskNames(options.name);
|
|
457
|
+
const config = options.config
|
|
458
|
+
? resolveConfig(options.config)
|
|
459
|
+
: await loadBeignetConfig(targetDir);
|
|
460
|
+
await assertFeatureArtifactApp(targetDir, config, "task");
|
|
461
|
+
const result = await makeFeatureArtifact({
|
|
462
|
+
targetDir,
|
|
463
|
+
config,
|
|
464
|
+
force: Boolean(options.force),
|
|
465
|
+
dryRun: Boolean(options.dryRun),
|
|
466
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
467
|
+
files: [
|
|
468
|
+
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
469
|
+
...taskFiles(names, config),
|
|
470
|
+
],
|
|
471
|
+
index: featureArtifactIndexFile("task", names, config),
|
|
472
|
+
dependencies: {
|
|
473
|
+
"@beignet/core": beignetDependencyVersion,
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
const registryResult = await updateTaskRegistry(targetDir, names, config, {
|
|
477
|
+
dryRun: Boolean(options.dryRun),
|
|
478
|
+
});
|
|
479
|
+
if (registryResult === "created")
|
|
480
|
+
result.createdFiles.push(config.paths.tasks);
|
|
481
|
+
if (registryResult === "updated")
|
|
482
|
+
result.updatedFiles.push(config.paths.tasks);
|
|
483
|
+
if (registryResult === "skipped")
|
|
484
|
+
result.skippedFiles.push(config.paths.tasks);
|
|
485
|
+
if (!result.files.includes(config.paths.tasks)) {
|
|
486
|
+
result.files.push(config.paths.tasks);
|
|
487
|
+
}
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
export async function makeFactory(options) {
|
|
491
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
492
|
+
const names = factoryNames(options.name);
|
|
493
|
+
const config = options.config
|
|
494
|
+
? resolveConfig(options.config)
|
|
495
|
+
: await loadBeignetConfig(targetDir);
|
|
496
|
+
await assertPersistenceArtifactApp(targetDir, names, config, "factory");
|
|
497
|
+
return makeFeatureArtifact({
|
|
498
|
+
targetDir,
|
|
499
|
+
config,
|
|
500
|
+
force: Boolean(options.force),
|
|
501
|
+
dryRun: Boolean(options.dryRun),
|
|
502
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
503
|
+
files: factoryFiles(names, config),
|
|
504
|
+
index: featureArtifactIndexFile("factory", names, config),
|
|
505
|
+
dependencies: {
|
|
506
|
+
"@beignet/core": beignetDependencyVersion,
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
export async function makeSeed(options) {
|
|
511
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
512
|
+
const names = seedNames(options.name);
|
|
513
|
+
const config = options.config
|
|
514
|
+
? resolveConfig(options.config)
|
|
515
|
+
: await loadBeignetConfig(targetDir);
|
|
516
|
+
await assertPersistenceArtifactApp(targetDir, names, config, "seed");
|
|
517
|
+
return makeFeatureArtifact({
|
|
518
|
+
targetDir,
|
|
519
|
+
config,
|
|
520
|
+
force: Boolean(options.force),
|
|
521
|
+
dryRun: Boolean(options.dryRun),
|
|
522
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
523
|
+
files: seedFiles(names, config),
|
|
524
|
+
index: featureArtifactIndexFile("seed", names, config),
|
|
525
|
+
dependencies: {
|
|
526
|
+
"@beignet/core": beignetDependencyVersion,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
export async function makeNotification(options) {
|
|
531
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
532
|
+
const names = notificationNames(options.name);
|
|
533
|
+
const config = options.config
|
|
534
|
+
? resolveConfig(options.config)
|
|
535
|
+
: await loadBeignetConfig(targetDir);
|
|
536
|
+
await assertFeatureArtifactApp(targetDir, config, "notification");
|
|
537
|
+
return makeFeatureArtifact({
|
|
538
|
+
targetDir,
|
|
539
|
+
config,
|
|
540
|
+
force: Boolean(options.force),
|
|
541
|
+
dryRun: Boolean(options.dryRun),
|
|
542
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
543
|
+
files: [
|
|
544
|
+
...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
|
|
545
|
+
...notificationFiles(names, config),
|
|
546
|
+
],
|
|
547
|
+
index: featureArtifactIndexFile("notification", names, config),
|
|
548
|
+
dependencies: {
|
|
549
|
+
"@beignet/core": beignetDependencyVersion,
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
}
|
|
323
553
|
export async function makeListener(options) {
|
|
324
554
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
325
555
|
const names = listenerNames(options.name, options.event);
|
|
@@ -334,7 +564,10 @@ export async function makeListener(options) {
|
|
|
334
564
|
force: Boolean(options.force),
|
|
335
565
|
dryRun: Boolean(options.dryRun),
|
|
336
566
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
337
|
-
files:
|
|
567
|
+
files: [
|
|
568
|
+
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
569
|
+
...listenerFiles(names, config),
|
|
570
|
+
],
|
|
338
571
|
index: featureArtifactIndexFile("listener", names, config),
|
|
339
572
|
dependencies: {
|
|
340
573
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -362,7 +595,10 @@ export async function makeSchedule(options) {
|
|
|
362
595
|
await assertFeatureArtifactApp(targetDir, config, "schedule", {
|
|
363
596
|
requireServer: Boolean(options.route),
|
|
364
597
|
});
|
|
365
|
-
const files =
|
|
598
|
+
const files = [
|
|
599
|
+
...(await missingCapabilityBuilderFiles(targetDir, "schedules", config)),
|
|
600
|
+
...scheduleFiles(names, config, { route: Boolean(options.route) }),
|
|
601
|
+
];
|
|
366
602
|
return makeFeatureArtifact({
|
|
367
603
|
targetDir,
|
|
368
604
|
config,
|
|
@@ -376,6 +612,78 @@ export async function makeSchedule(options) {
|
|
|
376
612
|
},
|
|
377
613
|
});
|
|
378
614
|
}
|
|
615
|
+
export async function makeUpload(options) {
|
|
616
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
617
|
+
const names = uploadNames(options.name);
|
|
618
|
+
const config = options.config
|
|
619
|
+
? resolveConfig(options.config)
|
|
620
|
+
: await loadBeignetConfig(targetDir);
|
|
621
|
+
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
622
|
+
return makeFeatureArtifact({
|
|
623
|
+
targetDir,
|
|
624
|
+
config,
|
|
625
|
+
force: Boolean(options.force),
|
|
626
|
+
dryRun: Boolean(options.dryRun),
|
|
627
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
628
|
+
files: uploadFiles(names, config),
|
|
629
|
+
index: featureArtifactIndexFile("upload", names, config),
|
|
630
|
+
dependencies: {
|
|
631
|
+
"@beignet/core": beignetDependencyVersion,
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
async function makeFeatureUi(options) {
|
|
636
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
637
|
+
const names = featureUiNames(options.name);
|
|
638
|
+
const config = options.config
|
|
639
|
+
? resolveConfig(options.config)
|
|
640
|
+
: await loadBeignetConfig(targetDir);
|
|
641
|
+
await assertFeatureUiApp(targetDir, config);
|
|
642
|
+
const componentFiles = featureUiComponentFiles(names, config);
|
|
643
|
+
const indexFile = featureUiIndexFile(names, config);
|
|
644
|
+
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
645
|
+
force: Boolean(options.force),
|
|
646
|
+
});
|
|
647
|
+
const createdFiles = plannedFiles
|
|
648
|
+
.filter((file) => file.result === "created")
|
|
649
|
+
.map((file) => file.path);
|
|
650
|
+
const updatedFiles = plannedFiles
|
|
651
|
+
.filter((file) => file.result === "updated")
|
|
652
|
+
.map((file) => file.path);
|
|
653
|
+
const skippedFiles = plannedFiles
|
|
654
|
+
.filter((file) => file.result === "skipped")
|
|
655
|
+
.map((file) => file.path);
|
|
656
|
+
if (!options.dryRun) {
|
|
657
|
+
for (const file of plannedFiles) {
|
|
658
|
+
await writePlannedGeneratedFile(targetDir, file);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const indexResult = await updateFeatureUiIndex(targetDir, indexFile, {
|
|
662
|
+
dryRun: Boolean(options.dryRun),
|
|
663
|
+
});
|
|
664
|
+
if (indexResult === "created")
|
|
665
|
+
createdFiles.push(indexFile.path);
|
|
666
|
+
if (indexResult === "updated")
|
|
667
|
+
updatedFiles.push(indexFile.path);
|
|
668
|
+
if (indexResult === "skipped")
|
|
669
|
+
skippedFiles.push(indexFile.path);
|
|
670
|
+
if (await updatePackageDependencies(targetDir, {
|
|
671
|
+
"@beignet/react-query": beignetDependencyVersion,
|
|
672
|
+
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
673
|
+
}, { dryRun: Boolean(options.dryRun) })) {
|
|
674
|
+
updatedFiles.push("package.json");
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
schemaVersion: 1,
|
|
678
|
+
name: names.pluralKebab,
|
|
679
|
+
targetDir,
|
|
680
|
+
dryRun: Boolean(options.dryRun),
|
|
681
|
+
files: [...componentFiles.map((file) => file.path), indexFile.path],
|
|
682
|
+
createdFiles,
|
|
683
|
+
updatedFiles,
|
|
684
|
+
skippedFiles,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
379
687
|
async function assertStandardApp(targetDir, config) {
|
|
380
688
|
const requiredFiles = [
|
|
381
689
|
config.paths.appContext,
|
|
@@ -468,6 +776,33 @@ async function assertFeatureArtifactApp(targetDir, config, artifact, options = {
|
|
|
468
776
|
}
|
|
469
777
|
}
|
|
470
778
|
}
|
|
779
|
+
async function assertFeatureUiApp(targetDir, config) {
|
|
780
|
+
const clientPath = clientIndexPath(config);
|
|
781
|
+
const requiredFiles = [config.paths.appContext, clientPath];
|
|
782
|
+
for (const file of requiredFiles) {
|
|
783
|
+
try {
|
|
784
|
+
await stat(path.join(targetDir, file));
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
throw new Error(`beignet make feature --with ui expects a standard Beignet app with React Query client helpers. Missing ${file}.`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async function assertPersistenceArtifactApp(targetDir, names, config, artifact) {
|
|
792
|
+
const requiredFiles = [
|
|
793
|
+
config.paths.appContext,
|
|
794
|
+
config.paths.ports,
|
|
795
|
+
resourcePortFilePath(featureResourceNames(names), config),
|
|
796
|
+
];
|
|
797
|
+
for (const file of requiredFiles) {
|
|
798
|
+
try {
|
|
799
|
+
await stat(path.join(targetDir, file));
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
throw new Error(`beignet make ${artifact} expects a feature with a repository port. Missing ${file}. Run beignet make feature ${names.feature.kebab} first, or add the repository port before generating ${artifact}s.`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
471
806
|
async function writeGeneratedFile(targetDir, file, options) {
|
|
472
807
|
const plannedFile = await planGeneratedFile(targetDir, file, {
|
|
473
808
|
force: options.force,
|
|
@@ -519,6 +854,62 @@ async function readOptionalFile(filePath) {
|
|
|
519
854
|
return undefined;
|
|
520
855
|
}
|
|
521
856
|
}
|
|
857
|
+
const capabilityBuilders = {
|
|
858
|
+
listeners: {
|
|
859
|
+
factoryName: "createListeners",
|
|
860
|
+
defineName: "defineListener",
|
|
861
|
+
module: "@beignet/core/events",
|
|
862
|
+
},
|
|
863
|
+
jobs: {
|
|
864
|
+
factoryName: "createJobs",
|
|
865
|
+
defineName: "defineJob",
|
|
866
|
+
module: "@beignet/core/jobs",
|
|
867
|
+
},
|
|
868
|
+
schedules: {
|
|
869
|
+
factoryName: "createSchedules",
|
|
870
|
+
defineName: "defineSchedule",
|
|
871
|
+
module: "@beignet/core/schedules",
|
|
872
|
+
},
|
|
873
|
+
notifications: {
|
|
874
|
+
factoryName: "createNotifications",
|
|
875
|
+
defineName: "defineNotification",
|
|
876
|
+
module: "@beignet/core/notifications",
|
|
877
|
+
},
|
|
878
|
+
tasks: {
|
|
879
|
+
factoryName: "createTasks",
|
|
880
|
+
defineName: "defineTask",
|
|
881
|
+
module: "@beignet/core/tasks",
|
|
882
|
+
},
|
|
883
|
+
};
|
|
884
|
+
function capabilityBuilderPath(capability, config) {
|
|
885
|
+
if (capability === "listeners")
|
|
886
|
+
return config.paths.listenersBuilder;
|
|
887
|
+
if (capability === "jobs")
|
|
888
|
+
return config.paths.jobsBuilder;
|
|
889
|
+
if (capability === "schedules")
|
|
890
|
+
return config.paths.schedulesBuilder;
|
|
891
|
+
if (capability === "notifications")
|
|
892
|
+
return config.paths.notificationsBuilder;
|
|
893
|
+
return config.paths.tasksBuilder;
|
|
894
|
+
}
|
|
895
|
+
function capabilityBuilderFile(capability, config) {
|
|
896
|
+
const builder = capabilityBuilders[capability];
|
|
897
|
+
return {
|
|
898
|
+
path: capabilityBuilderPath(capability, config),
|
|
899
|
+
content: `import { ${builder.factoryName} } from "${builder.module}";
|
|
900
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
901
|
+
|
|
902
|
+
export const { ${builder.defineName} } = ${builder.factoryName}<AppContext>();
|
|
903
|
+
`,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
async function missingCapabilityBuilderFiles(targetDir, capability, config) {
|
|
907
|
+
const file = capabilityBuilderFile(capability, config);
|
|
908
|
+
if (await fileExists(path.join(targetDir, file.path))) {
|
|
909
|
+
return [];
|
|
910
|
+
}
|
|
911
|
+
return [file];
|
|
912
|
+
}
|
|
522
913
|
async function makeFeatureArtifact(options) {
|
|
523
914
|
const plannedFiles = await planGeneratedFiles(options.targetDir, options.files, {
|
|
524
915
|
force: options.force,
|
|
@@ -550,6 +941,7 @@ async function makeFeatureArtifact(options) {
|
|
|
550
941
|
updatedFiles.push("package.json");
|
|
551
942
|
}
|
|
552
943
|
return {
|
|
944
|
+
schemaVersion: 1,
|
|
553
945
|
name: options.name,
|
|
554
946
|
targetDir: options.targetDir,
|
|
555
947
|
dryRun: options.dryRun,
|
|
@@ -579,6 +971,9 @@ function beignetDependencyVersion(packageJson) {
|
|
|
579
971
|
packageJson.dependencies?.["@beignet/next"] ??
|
|
580
972
|
"*");
|
|
581
973
|
}
|
|
974
|
+
function tanstackReactQueryDependencyVersion(packageJson) {
|
|
975
|
+
return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
|
|
976
|
+
}
|
|
582
977
|
async function updateResourceWiring(targetDir, names, config, persistenceOrOptions, maybeOptions) {
|
|
583
978
|
const persistence = typeof persistenceOrOptions === "string"
|
|
584
979
|
? persistenceOrOptions
|
|
@@ -586,13 +981,37 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
586
981
|
const options = typeof persistenceOrOptions === "string"
|
|
587
982
|
? maybeOptions
|
|
588
983
|
: persistenceOrOptions;
|
|
984
|
+
const mode = typeof persistenceOrOptions === "string"
|
|
985
|
+
? (maybeOptions?.mode ?? "feature")
|
|
986
|
+
: (persistenceOrOptions.mode ?? "feature");
|
|
987
|
+
const generationOptions = typeof persistenceOrOptions === "string"
|
|
988
|
+
? (maybeOptions?.generationOptions ??
|
|
989
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode))
|
|
990
|
+
: (persistenceOrOptions.generationOptions ??
|
|
991
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode));
|
|
589
992
|
const updated = new Set();
|
|
590
|
-
if (await updatePackageJson(targetDir,
|
|
993
|
+
if (await updatePackageJson(targetDir, {
|
|
994
|
+
...options,
|
|
995
|
+
webTestingDependency: true,
|
|
996
|
+
})) {
|
|
591
997
|
updated.add("package.json");
|
|
592
|
-
|
|
998
|
+
}
|
|
999
|
+
if (await updatePortsIndex(targetDir, names, config, {
|
|
1000
|
+
...options,
|
|
1001
|
+
generationOptions,
|
|
1002
|
+
})) {
|
|
593
1003
|
updated.add(config.paths.ports);
|
|
594
1004
|
}
|
|
595
|
-
if (
|
|
1005
|
+
if (persistence === "memory" &&
|
|
1006
|
+
(await updateInfrastructurePorts(targetDir, names, config, options))) {
|
|
1007
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1008
|
+
}
|
|
1009
|
+
if (generationOptions.auth &&
|
|
1010
|
+
(await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
|
|
1011
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1012
|
+
}
|
|
1013
|
+
if (persistence === "drizzle" &&
|
|
1014
|
+
(await updateInfrastructureDeferredPorts(targetDir, names, config, options))) {
|
|
596
1015
|
updated.add(config.paths.infrastructurePorts);
|
|
597
1016
|
}
|
|
598
1017
|
if (persistence === "drizzle" &&
|
|
@@ -607,7 +1026,10 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
607
1026
|
(await updateServerTransactionPorts(targetDir, names, config, options))) {
|
|
608
1027
|
updated.add(config.paths.server);
|
|
609
1028
|
}
|
|
610
|
-
if (await
|
|
1029
|
+
if (await updateSharedErrors(targetDir, names, config, { ...options, mode })) {
|
|
1030
|
+
updated.add(resourceSharedErrorsPath(config));
|
|
1031
|
+
}
|
|
1032
|
+
if (await updateOpenApiRoute(targetDir, names, config, { ...options, mode })) {
|
|
611
1033
|
updated.add(config.paths.openapiRoute);
|
|
612
1034
|
}
|
|
613
1035
|
const routesFile = await updateServerRoutes(targetDir, names, config, options);
|
|
@@ -620,9 +1042,14 @@ async function updatePackageJson(targetDir, options) {
|
|
|
620
1042
|
const original = await readFile(filePath, "utf8");
|
|
621
1043
|
const packageJson = JSON.parse(original);
|
|
622
1044
|
packageJson.scripts = packageJson.scripts ?? {};
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1045
|
+
packageJson.scripts.test ??= "bun test";
|
|
1046
|
+
if (options.webTestingDependency) {
|
|
1047
|
+
packageJson.devDependencies = packageJson.devDependencies ?? {};
|
|
1048
|
+
packageJson.devDependencies["@beignet/web"] ??=
|
|
1049
|
+
packageJson.dependencies?.["@beignet/core"] ??
|
|
1050
|
+
packageJson.dependencies?.["@beignet/next"] ??
|
|
1051
|
+
"*";
|
|
1052
|
+
}
|
|
626
1053
|
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
627
1054
|
if (!options.dryRun)
|
|
628
1055
|
await writeFile(filePath, next);
|
|
@@ -637,6 +1064,16 @@ async function updatePortsIndex(targetDir, names, config, options) {
|
|
|
637
1064
|
if (!next.includes(importLine)) {
|
|
638
1065
|
next = insertAfterImports(next, importLine);
|
|
639
1066
|
}
|
|
1067
|
+
if (options.generationOptions?.auth) {
|
|
1068
|
+
const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
|
|
1069
|
+
const policyImportLine = `import type { AuthorizationContext as ${names.singularPascal}AuthorizationContext, ${names.singularCamel}Policy } from "${relativeModule(config.paths.ports, policyPath)}";`;
|
|
1070
|
+
if (!next.includes(policyImportLine)) {
|
|
1071
|
+
next = insertAfterImports(next, policyImportLine);
|
|
1072
|
+
}
|
|
1073
|
+
next = appendUnionTypeMember(next, "AppAuthorizationContext", `${names.singularPascal}AuthorizationContext`);
|
|
1074
|
+
next = appendTupleTypeMember(next, "AppGate", `typeof ${names.singularCamel}Policy`);
|
|
1075
|
+
next = appendGatePolicyMember(next, `typeof ${names.singularCamel}Policy`);
|
|
1076
|
+
}
|
|
640
1077
|
const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
|
|
641
1078
|
if (!next.includes(entry.trim())) {
|
|
642
1079
|
next = insertTypeProperty(next, "AppPorts", entry.trim(), `Could not find AppPorts in ${config.paths.ports}. Add ${entry.trim()} manually, or restore the generated ports file before running make resource.`);
|
|
@@ -731,7 +1168,7 @@ async function updateInfrastructurePorts(targetDir, names, config, options) {
|
|
|
731
1168
|
}
|
|
732
1169
|
const repositoryConst = `const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository();`;
|
|
733
1170
|
if (!next.includes(repositoryConst)) {
|
|
734
|
-
next = replaceRequired(next, /(\nexport const appPorts = definePorts(?:<[^>]+>)?\(\{)/, `\n${repositoryConst}\n$1`, `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`);
|
|
1171
|
+
next = replaceRequired(next, /(\nexport const appPorts = definePorts(?:<[^>]+>)?\((?:\)\()?\{)/, `\n${repositoryConst}\n$1`, `Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`);
|
|
735
1172
|
}
|
|
736
1173
|
const entry = `\t${names.pluralCamel},`;
|
|
737
1174
|
const definePortsBody = definePortsBodySource(next);
|
|
@@ -751,6 +1188,22 @@ async function updateInfrastructurePorts(targetDir, names, config, options) {
|
|
|
751
1188
|
await writeFile(filePath, next);
|
|
752
1189
|
return true;
|
|
753
1190
|
}
|
|
1191
|
+
async function updateInfrastructureGatePolicies(targetDir, names, config, options) {
|
|
1192
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
1193
|
+
const original = await readFile(filePath, "utf8");
|
|
1194
|
+
let next = original;
|
|
1195
|
+
const policyPath = path.join(config.paths.features, names.pluralKebab, "policy.ts");
|
|
1196
|
+
const importLine = `import { ${names.singularCamel}Policy } from "${relativeModule(config.paths.infrastructurePorts, policyPath)}";`;
|
|
1197
|
+
if (!next.includes(importLine)) {
|
|
1198
|
+
next = insertAfterImports(next, importLine);
|
|
1199
|
+
}
|
|
1200
|
+
next = appendCreateGatePolicy(next, `${names.singularCamel}Policy`, `Could not find createGate({ policies: [...] }) in ${config.paths.infrastructurePorts}. Add ${names.singularCamel}Policy to the gate policies manually, or restore the generated infrastructure ports file before running make resource --auth.`);
|
|
1201
|
+
if (next === original)
|
|
1202
|
+
return false;
|
|
1203
|
+
if (!options.dryRun)
|
|
1204
|
+
await writeFile(filePath, next);
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
754
1207
|
async function updateDrizzleSchemaIndex(targetDir, names, config, options) {
|
|
755
1208
|
const filePath = path.join(targetDir, drizzleSchemaIndexPath(config));
|
|
756
1209
|
if (!(await fileExists(filePath)))
|
|
@@ -789,6 +1242,19 @@ async function updateDrizzleRepositories(targetDir, names, config, options) {
|
|
|
789
1242
|
await writeFile(filePath, next);
|
|
790
1243
|
return true;
|
|
791
1244
|
}
|
|
1245
|
+
async function updateInfrastructureDeferredPorts(targetDir, names, config, options) {
|
|
1246
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
1247
|
+
const original = await readFile(filePath, "utf8");
|
|
1248
|
+
// Drizzle resources are provided by the app database provider at startup,
|
|
1249
|
+
// so deferred-form ports files defer the new repository key. Identity-form
|
|
1250
|
+
// ports files are left untouched.
|
|
1251
|
+
const next = appendDeferredPortKey(original, names.pluralCamel);
|
|
1252
|
+
if (next === original)
|
|
1253
|
+
return false;
|
|
1254
|
+
if (!options.dryRun)
|
|
1255
|
+
await writeFile(filePath, next);
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
792
1258
|
async function updateServerTransactionPorts(targetDir, names, config, options) {
|
|
793
1259
|
const filePath = path.join(targetDir, config.paths.server);
|
|
794
1260
|
const original = await readFile(filePath, "utf8");
|
|
@@ -817,13 +1283,12 @@ async function updateOpenApiRoute(targetDir, names, config, options) {
|
|
|
817
1283
|
const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
|
|
818
1284
|
if (!firstArg?.text.startsWith("["))
|
|
819
1285
|
return false;
|
|
820
|
-
const
|
|
821
|
-
const createName = `create${names.singularPascal}`;
|
|
1286
|
+
const contractNames = resourceContractNames(names, options.mode);
|
|
822
1287
|
const listedContracts = identifiersFromArrayExpression(firstArg.text);
|
|
823
|
-
const missingContracts =
|
|
1288
|
+
const missingContracts = contractNames.filter((contractName) => !listedContracts.has(contractName));
|
|
824
1289
|
if (missingContracts.length === 0)
|
|
825
1290
|
return false;
|
|
826
|
-
const importLine = `import {
|
|
1291
|
+
const importLine = `import { ${contractNames.join(", ")} } from "${aliasModule(resourceContractFilePath(names, config))}";`;
|
|
827
1292
|
const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
|
|
828
1293
|
let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(firstArg.end)}`;
|
|
829
1294
|
if (!next.includes(importLine)) {
|
|
@@ -835,6 +1300,53 @@ async function updateOpenApiRoute(targetDir, names, config, options) {
|
|
|
835
1300
|
await writeFile(filePath, next);
|
|
836
1301
|
return true;
|
|
837
1302
|
}
|
|
1303
|
+
async function updateSharedErrors(targetDir, names, config, options) {
|
|
1304
|
+
if (options.mode !== "resource")
|
|
1305
|
+
return false;
|
|
1306
|
+
const filePath = path.join(targetDir, resourceSharedErrorsPath(config));
|
|
1307
|
+
if (!(await fileExists(filePath)))
|
|
1308
|
+
return false;
|
|
1309
|
+
const original = await readFile(filePath, "utf8");
|
|
1310
|
+
const notFoundErrorName = `${names.singularPascal}NotFound`;
|
|
1311
|
+
const conflictErrorName = `${names.singularPascal}Conflict`;
|
|
1312
|
+
const missingErrorNames = [notFoundErrorName, conflictErrorName].filter((errorName) => !new RegExp(`\\b${errorName}\\b`).test(original));
|
|
1313
|
+
if (missingErrorNames.length === 0)
|
|
1314
|
+
return false;
|
|
1315
|
+
const expectedErrorNames = missingErrorNames.join(" and ");
|
|
1316
|
+
const defineErrorsStart = original.indexOf("defineErrors(");
|
|
1317
|
+
if (defineErrorsStart === -1) {
|
|
1318
|
+
throw new Error(`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
|
|
1319
|
+
}
|
|
1320
|
+
const openBrace = original.indexOf("{", defineErrorsStart);
|
|
1321
|
+
if (openBrace === -1) {
|
|
1322
|
+
throw new Error(`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
|
|
1323
|
+
}
|
|
1324
|
+
const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
|
|
1325
|
+
if (closeBrace === -1) {
|
|
1326
|
+
throw new Error(`Could not update ${resourceSharedErrorsPath(config)} because the error catalog object is malformed. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`);
|
|
1327
|
+
}
|
|
1328
|
+
const entries = [];
|
|
1329
|
+
if (missingErrorNames.includes(notFoundErrorName)) {
|
|
1330
|
+
entries.push(`${notFoundErrorName}: {
|
|
1331
|
+
\t\tcode: "${constantCase(names.singularKebab)}_NOT_FOUND",
|
|
1332
|
+
\t\tstatus: 404,
|
|
1333
|
+
\t\tmessage: "${names.singularPascal} not found",
|
|
1334
|
+
\t},`);
|
|
1335
|
+
}
|
|
1336
|
+
if (missingErrorNames.includes(conflictErrorName)) {
|
|
1337
|
+
entries.push(`${conflictErrorName}: {
|
|
1338
|
+
\t\tcode: "${constantCase(names.singularKebab)}_CONFLICT",
|
|
1339
|
+
\t\tstatus: 409,
|
|
1340
|
+
\t\tmessage: "${names.singularPascal} was changed by another request",
|
|
1341
|
+
\t},`);
|
|
1342
|
+
}
|
|
1343
|
+
const next = insertBeforeClosingBrace(original, openBrace, closeBrace, entries.join("\n"));
|
|
1344
|
+
if (next === original)
|
|
1345
|
+
return false;
|
|
1346
|
+
if (!options.dryRun)
|
|
1347
|
+
await writeFile(filePath, next);
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
838
1350
|
async function updateServerRoutes(targetDir, names, config, options) {
|
|
839
1351
|
const filePath = path.join(targetDir, config.paths.server);
|
|
840
1352
|
const original = await readFile(filePath, "utf8");
|
|
@@ -1096,6 +1608,67 @@ function typeBodySource(source, typeName) {
|
|
|
1096
1608
|
return undefined;
|
|
1097
1609
|
return source.slice(openBrace + 1, closeBrace);
|
|
1098
1610
|
}
|
|
1611
|
+
function appendUnionTypeMember(source, typeName, member) {
|
|
1612
|
+
const match = new RegExp(`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`).exec(source);
|
|
1613
|
+
if (!match)
|
|
1614
|
+
return source;
|
|
1615
|
+
const declaration = match[0];
|
|
1616
|
+
if (new RegExp(`\\b${member}\\b`).test(declaration))
|
|
1617
|
+
return source;
|
|
1618
|
+
const nextDeclaration = declaration.replace(/;\s*$/, ` | ${member};`);
|
|
1619
|
+
return `${source.slice(0, match.index)}${nextDeclaration}${source.slice(match.index + declaration.length)}`;
|
|
1620
|
+
}
|
|
1621
|
+
function appendTupleTypeMember(source, typeName, member) {
|
|
1622
|
+
const match = new RegExp(`export\\s+type\\s+${typeName}\\s*=\\s*`).exec(source);
|
|
1623
|
+
if (!match)
|
|
1624
|
+
return source;
|
|
1625
|
+
const tupleStart = source.indexOf("[", match.index);
|
|
1626
|
+
if (tupleStart === -1)
|
|
1627
|
+
return source;
|
|
1628
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
1629
|
+
if (tupleEnd === -1)
|
|
1630
|
+
return source;
|
|
1631
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
1632
|
+
if (tuple.includes(member))
|
|
1633
|
+
return source;
|
|
1634
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
1635
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
|
|
1636
|
+
}
|
|
1637
|
+
function appendGatePolicyMember(source, member) {
|
|
1638
|
+
const gateMatch = /\bGatePort\s*</.exec(source);
|
|
1639
|
+
if (!gateMatch)
|
|
1640
|
+
return source;
|
|
1641
|
+
const tupleStart = source.indexOf("[", gateMatch.index);
|
|
1642
|
+
if (tupleStart === -1)
|
|
1643
|
+
return source;
|
|
1644
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
1645
|
+
if (tupleEnd === -1)
|
|
1646
|
+
return source;
|
|
1647
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
1648
|
+
if (tuple.includes(member))
|
|
1649
|
+
return source;
|
|
1650
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
1651
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(tupleEnd + 1)}`;
|
|
1652
|
+
}
|
|
1653
|
+
function appendCreateGatePolicy(source, policy, failureMessage) {
|
|
1654
|
+
const policyMatch = /\bpolicies\s*:\s*\[/.exec(source);
|
|
1655
|
+
if (!policyMatch) {
|
|
1656
|
+
throw new Error(failureMessage);
|
|
1657
|
+
}
|
|
1658
|
+
const arrayStart = source.indexOf("[", policyMatch.index);
|
|
1659
|
+
if (arrayStart === -1) {
|
|
1660
|
+
throw new Error(failureMessage);
|
|
1661
|
+
}
|
|
1662
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
1663
|
+
if (arrayEnd === -1) {
|
|
1664
|
+
throw new Error(failureMessage);
|
|
1665
|
+
}
|
|
1666
|
+
const policies = source.slice(arrayStart, arrayEnd + 1);
|
|
1667
|
+
if (policies.includes(policy))
|
|
1668
|
+
return source;
|
|
1669
|
+
const nextPolicies = appendToArrayExpression(policies, [policy]);
|
|
1670
|
+
return `${source.slice(0, arrayStart)}${nextPolicies}${source.slice(arrayEnd + 1)}`;
|
|
1671
|
+
}
|
|
1099
1672
|
function insertDefinePortsProperty(source, property, message) {
|
|
1100
1673
|
const body = definePortsBodyRange(source);
|
|
1101
1674
|
if (!body)
|
|
@@ -1145,7 +1718,15 @@ function definePortsBodySource(source) {
|
|
|
1145
1718
|
return source.slice(body.openBrace + 1, body.closeBrace);
|
|
1146
1719
|
}
|
|
1147
1720
|
function definePortsBodyRange(source) {
|
|
1148
|
-
const
|
|
1721
|
+
const outer = definePortsOuterBodyRange(source);
|
|
1722
|
+
if (!outer)
|
|
1723
|
+
return undefined;
|
|
1724
|
+
// The curried `definePorts<AppPorts>()({ bound, deferred })` form binds app
|
|
1725
|
+
// ports inside `bound`; generated bindings belong there.
|
|
1726
|
+
return (topLevelBoundObjectRange(source, outer.openBrace, outer.closeBrace) ?? outer);
|
|
1727
|
+
}
|
|
1728
|
+
function definePortsOuterBodyRange(source) {
|
|
1729
|
+
const match = /definePorts(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?\{/.exec(source);
|
|
1149
1730
|
if (!match)
|
|
1150
1731
|
return undefined;
|
|
1151
1732
|
const openBrace = source.indexOf("{", match.index);
|
|
@@ -1154,6 +1735,39 @@ function definePortsBodyRange(source) {
|
|
|
1154
1735
|
return undefined;
|
|
1155
1736
|
return { openBrace, closeBrace };
|
|
1156
1737
|
}
|
|
1738
|
+
function appendDeferredPortKey(source, portKey) {
|
|
1739
|
+
const outer = definePortsOuterBodyRange(source);
|
|
1740
|
+
if (!outer)
|
|
1741
|
+
return source;
|
|
1742
|
+
const body = source.slice(outer.openBrace + 1, outer.closeBrace);
|
|
1743
|
+
if (!hasTopLevelObjectProperty(body, "deferred"))
|
|
1744
|
+
return source;
|
|
1745
|
+
const deferredMatch = /\bdeferred\s*:\s*\[/.exec(body);
|
|
1746
|
+
if (!deferredMatch)
|
|
1747
|
+
return source;
|
|
1748
|
+
const arrayStart = source.indexOf("[", outer.openBrace + 1 + deferredMatch.index);
|
|
1749
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
1750
|
+
if (arrayEnd === -1)
|
|
1751
|
+
return source;
|
|
1752
|
+
const tuple = source.slice(arrayStart, arrayEnd + 1);
|
|
1753
|
+
if (new RegExp(`["']${portKey}["']`).test(tuple))
|
|
1754
|
+
return source;
|
|
1755
|
+
const nextTuple = appendToArrayExpression(tuple, [`"${portKey}"`]);
|
|
1756
|
+
return `${source.slice(0, arrayStart)}${nextTuple}${source.slice(arrayEnd + 1)}`;
|
|
1757
|
+
}
|
|
1758
|
+
function topLevelBoundObjectRange(source, openBrace, closeBrace) {
|
|
1759
|
+
const body = source.slice(openBrace + 1, closeBrace);
|
|
1760
|
+
if (!hasTopLevelObjectProperty(body, "bound"))
|
|
1761
|
+
return undefined;
|
|
1762
|
+
const boundMatch = /\bbound\s*:\s*\{/.exec(body);
|
|
1763
|
+
if (!boundMatch)
|
|
1764
|
+
return undefined;
|
|
1765
|
+
const boundOpen = source.indexOf("{", openBrace + 1 + boundMatch.index);
|
|
1766
|
+
const boundClose = matchingBraceIndex(source, boundOpen);
|
|
1767
|
+
if (boundClose === -1)
|
|
1768
|
+
return undefined;
|
|
1769
|
+
return { openBrace: boundOpen, closeBrace: boundClose };
|
|
1770
|
+
}
|
|
1157
1771
|
function hasTopLevelObjectProperty(source, propertyName) {
|
|
1158
1772
|
let depth = 0;
|
|
1159
1773
|
let inString;
|
|
@@ -1383,6 +1997,21 @@ function resourceNames(input) {
|
|
|
1383
1997
|
]);
|
|
1384
1998
|
return names;
|
|
1385
1999
|
}
|
|
2000
|
+
function featureUiNames(input) {
|
|
2001
|
+
const names = resourceNames(input);
|
|
2002
|
+
const resolvedNames = {
|
|
2003
|
+
...names,
|
|
2004
|
+
componentExportName: `${names.pluralPascal}Panel`,
|
|
2005
|
+
componentFileName: `${names.pluralKebab}-panel`,
|
|
2006
|
+
};
|
|
2007
|
+
assertGeneratedIdentifiers(input, "Feature UI name", [
|
|
2008
|
+
resolvedNames.componentExportName,
|
|
2009
|
+
]);
|
|
2010
|
+
return resolvedNames;
|
|
2011
|
+
}
|
|
2012
|
+
function featureResourceNames(names) {
|
|
2013
|
+
return resourceNames(names.feature.kebab);
|
|
2014
|
+
}
|
|
1386
2015
|
function useCaseNames(input) {
|
|
1387
2016
|
const parts = input
|
|
1388
2017
|
.trim()
|
|
@@ -1497,14 +2126,60 @@ function jobNames(input) {
|
|
|
1497
2126
|
payloadTypeName: `${jobExportName}Payload`,
|
|
1498
2127
|
};
|
|
1499
2128
|
}
|
|
1500
|
-
function
|
|
1501
|
-
const names = featureArtifactNames(input, "
|
|
1502
|
-
const
|
|
1503
|
-
assertGeneratedIdentifiers(input, "
|
|
2129
|
+
function taskNames(input) {
|
|
2130
|
+
const names = featureArtifactNames(input, "Task");
|
|
2131
|
+
const taskExportName = `${names.artifact.camel}Task`;
|
|
2132
|
+
assertGeneratedIdentifiers(input, "Task name", [taskExportName]);
|
|
1504
2133
|
return {
|
|
1505
2134
|
...names,
|
|
1506
|
-
|
|
1507
|
-
|
|
2135
|
+
taskName: names.stableName,
|
|
2136
|
+
taskExportName,
|
|
2137
|
+
inputSchemaName: `${names.artifact.pascal}TaskInputSchema`,
|
|
2138
|
+
inputTypeName: `${names.artifact.pascal}TaskInput`,
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
function factoryNames(input) {
|
|
2142
|
+
const names = featureArtifactNames(input, "Factory");
|
|
2143
|
+
const factoryExportName = `${names.artifact.camel}Factory`;
|
|
2144
|
+
assertGeneratedIdentifiers(input, "Factory name", [factoryExportName]);
|
|
2145
|
+
return {
|
|
2146
|
+
...names,
|
|
2147
|
+
factoryName: names.stableName,
|
|
2148
|
+
factoryExportName,
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
function seedNames(input) {
|
|
2152
|
+
const names = featureArtifactNames(input, "Seed");
|
|
2153
|
+
const seedExportName = `${names.artifact.camel}Seed`;
|
|
2154
|
+
assertGeneratedIdentifiers(input, "Seed name", [seedExportName]);
|
|
2155
|
+
return {
|
|
2156
|
+
...names,
|
|
2157
|
+
seedName: names.stableName,
|
|
2158
|
+
seedExportName,
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function notificationNames(input) {
|
|
2162
|
+
const names = featureArtifactNames(input, "Notification");
|
|
2163
|
+
const notificationExportName = `${names.featureSingularPascal}${names.artifact.pascal}Notification`;
|
|
2164
|
+
assertGeneratedIdentifiers(input, "Notification name", [
|
|
2165
|
+
notificationExportName,
|
|
2166
|
+
]);
|
|
2167
|
+
return {
|
|
2168
|
+
...names,
|
|
2169
|
+
notificationName: names.stableName,
|
|
2170
|
+
notificationExportName,
|
|
2171
|
+
payloadSchemaName: `${notificationExportName}PayloadSchema`,
|
|
2172
|
+
payloadTypeName: `${notificationExportName}Payload`,
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
function listenerNames(input, eventInput) {
|
|
2176
|
+
const names = featureArtifactNames(input, "Listener");
|
|
2177
|
+
const listenerExportName = names.artifact.camel;
|
|
2178
|
+
assertGeneratedIdentifiers(input, "Listener name", [listenerExportName]);
|
|
2179
|
+
return {
|
|
2180
|
+
...names,
|
|
2181
|
+
listenerName: names.stableName,
|
|
2182
|
+
listenerExportName,
|
|
1508
2183
|
event: eventNames(eventInput),
|
|
1509
2184
|
};
|
|
1510
2185
|
}
|
|
@@ -1522,6 +2197,18 @@ function scheduleNames(input, options) {
|
|
|
1522
2197
|
timezone: options.timezone?.trim() || undefined,
|
|
1523
2198
|
};
|
|
1524
2199
|
}
|
|
2200
|
+
function uploadNames(input) {
|
|
2201
|
+
const names = featureArtifactNames(input, "Upload");
|
|
2202
|
+
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
2203
|
+
assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
|
|
2204
|
+
return {
|
|
2205
|
+
...names,
|
|
2206
|
+
uploadName: names.stableName,
|
|
2207
|
+
uploadExportName,
|
|
2208
|
+
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
2209
|
+
metadataTypeName: `${uploadExportName}Metadata`,
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
1525
2212
|
function normalizeEventReference(input) {
|
|
1526
2213
|
const trimmed = input.trim();
|
|
1527
2214
|
if (trimmed.includes("/"))
|
|
@@ -1649,59 +2336,119 @@ function pascalCase(value) {
|
|
|
1649
2336
|
const camel = camelCase(value);
|
|
1650
2337
|
return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
1651
2338
|
}
|
|
1652
|
-
function
|
|
2339
|
+
function constantCase(value) {
|
|
2340
|
+
return value.replaceAll("-", "_").toUpperCase();
|
|
2341
|
+
}
|
|
2342
|
+
function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
|
|
2343
|
+
auth: false,
|
|
2344
|
+
tenant: false,
|
|
2345
|
+
events: false,
|
|
2346
|
+
softDelete: false,
|
|
2347
|
+
}) {
|
|
1653
2348
|
const useCaseDir = resourceUseCaseDir(names, config);
|
|
1654
2349
|
const infraDir = infrastructureDir(config);
|
|
1655
2350
|
const featureDir = resourceFeatureDir(names, config);
|
|
1656
2351
|
const files = [
|
|
1657
2352
|
{
|
|
1658
2353
|
path: resourceContractFilePath(names, config),
|
|
1659
|
-
content: contractFile(names, config),
|
|
2354
|
+
content: contractFile(names, config, mode, options),
|
|
1660
2355
|
},
|
|
1661
2356
|
{
|
|
1662
|
-
path: path.join(
|
|
1663
|
-
content: schemasFile(names),
|
|
2357
|
+
path: path.join(featureDir, "schemas.ts"),
|
|
2358
|
+
content: schemasFile(names, mode, options),
|
|
1664
2359
|
},
|
|
1665
2360
|
{
|
|
1666
2361
|
path: path.join(useCaseDir, `list-${names.pluralKebab}.ts`),
|
|
1667
|
-
content: listUseCaseFile(names, config),
|
|
2362
|
+
content: listUseCaseFile(names, config, options),
|
|
1668
2363
|
},
|
|
1669
2364
|
{
|
|
1670
2365
|
path: path.join(useCaseDir, `create-${names.singularKebab}.ts`),
|
|
1671
|
-
content: createUseCaseFile(names, config),
|
|
2366
|
+
content: createUseCaseFile(names, config, options),
|
|
1672
2367
|
},
|
|
2368
|
+
...(mode === "resource"
|
|
2369
|
+
? [
|
|
2370
|
+
{
|
|
2371
|
+
path: path.join(useCaseDir, `get-${names.singularKebab}.ts`),
|
|
2372
|
+
content: getUseCaseFile(names, config, options),
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
path: path.join(useCaseDir, `update-${names.singularKebab}.ts`),
|
|
2376
|
+
content: updateUseCaseFile(names, config, options),
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
path: path.join(useCaseDir, `delete-${names.singularKebab}.ts`),
|
|
2380
|
+
content: deleteUseCaseFile(names, config, options),
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
path: path.join(featureDir, "policy.ts"),
|
|
2384
|
+
content: resourcePolicyFile(names, config, options),
|
|
2385
|
+
},
|
|
2386
|
+
]
|
|
2387
|
+
: []),
|
|
2388
|
+
...(mode === "resource" && options.events
|
|
2389
|
+
? [
|
|
2390
|
+
{
|
|
2391
|
+
path: path.join(featureDir, "domain/events/index.ts"),
|
|
2392
|
+
content: resourceEventsFile(names),
|
|
2393
|
+
},
|
|
2394
|
+
]
|
|
2395
|
+
: []),
|
|
2396
|
+
...(mode === "resource" && options.auth
|
|
2397
|
+
? [
|
|
2398
|
+
{
|
|
2399
|
+
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
2400
|
+
content: resourcePolicyTestFile(names, options),
|
|
2401
|
+
},
|
|
2402
|
+
]
|
|
2403
|
+
: []),
|
|
1673
2404
|
{
|
|
1674
2405
|
path: path.join(useCaseDir, "index.ts"),
|
|
1675
|
-
content: useCasesIndexFile(names),
|
|
2406
|
+
content: useCasesIndexFile(names, mode),
|
|
1676
2407
|
},
|
|
1677
2408
|
{
|
|
1678
2409
|
path: resourcePortFilePath(names, config),
|
|
1679
|
-
content: repositoryPortFile(names, config),
|
|
2410
|
+
content: repositoryPortFile(names, config, mode, options),
|
|
1680
2411
|
},
|
|
1681
2412
|
{
|
|
1682
2413
|
path: path.join(infraDir, names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`),
|
|
1683
|
-
content: inMemoryRepositoryFile(names, config),
|
|
2414
|
+
content: inMemoryRepositoryFile(names, config, mode, options),
|
|
1684
2415
|
},
|
|
1685
2416
|
{
|
|
1686
2417
|
path: path.join(featureDir, "routes.ts"),
|
|
1687
|
-
content: routeGroupFile(names, config),
|
|
2418
|
+
content: routeGroupFile(names, config, mode, options),
|
|
1688
2419
|
},
|
|
1689
2420
|
{
|
|
1690
2421
|
path: resourceTestFilePath(names, config),
|
|
1691
|
-
content: testFile(names, config),
|
|
2422
|
+
content: testFile(names, config, mode, options),
|
|
1692
2423
|
},
|
|
1693
2424
|
];
|
|
1694
2425
|
if (persistence === "drizzle") {
|
|
1695
2426
|
files.push({
|
|
1696
2427
|
path: drizzleResourceSchemaFilePath(names, config),
|
|
1697
|
-
content: drizzleSchemaFile(names),
|
|
2428
|
+
content: drizzleSchemaFile(names, options),
|
|
1698
2429
|
}, {
|
|
1699
2430
|
path: drizzleResourceRepositoryFilePath(names, config),
|
|
1700
|
-
content: drizzleRepositoryFile(names, config),
|
|
2431
|
+
content: drizzleRepositoryFile(names, config, mode, options),
|
|
1701
2432
|
});
|
|
1702
2433
|
}
|
|
1703
2434
|
return files;
|
|
1704
2435
|
}
|
|
2436
|
+
function featureUiComponentFiles(names, config) {
|
|
2437
|
+
const componentPath = featureUiComponentFilePath(names, config);
|
|
2438
|
+
return [
|
|
2439
|
+
{
|
|
2440
|
+
path: componentPath,
|
|
2441
|
+
content: featureUiComponentFile(names, config),
|
|
2442
|
+
},
|
|
2443
|
+
];
|
|
2444
|
+
}
|
|
2445
|
+
function featureUiIndexFile(names, config) {
|
|
2446
|
+
return {
|
|
2447
|
+
path: featureUiIndexFilePath(names, config),
|
|
2448
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";
|
|
2449
|
+
`,
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
1705
2452
|
function contractFiles(names, config) {
|
|
1706
2453
|
return [
|
|
1707
2454
|
{
|
|
@@ -1767,6 +2514,38 @@ function jobFiles(names, config) {
|
|
|
1767
2514
|
},
|
|
1768
2515
|
];
|
|
1769
2516
|
}
|
|
2517
|
+
function taskFiles(names, config) {
|
|
2518
|
+
return [
|
|
2519
|
+
{
|
|
2520
|
+
path: taskFilePath(names, config),
|
|
2521
|
+
content: taskFile(names, config),
|
|
2522
|
+
},
|
|
2523
|
+
];
|
|
2524
|
+
}
|
|
2525
|
+
function notificationFiles(names, config) {
|
|
2526
|
+
return [
|
|
2527
|
+
{
|
|
2528
|
+
path: notificationFilePath(names, config),
|
|
2529
|
+
content: notificationFile(names, config),
|
|
2530
|
+
},
|
|
2531
|
+
];
|
|
2532
|
+
}
|
|
2533
|
+
function factoryFiles(names, config) {
|
|
2534
|
+
return [
|
|
2535
|
+
{
|
|
2536
|
+
path: factoryFilePath(names, config),
|
|
2537
|
+
content: factoryFile(names, config),
|
|
2538
|
+
},
|
|
2539
|
+
];
|
|
2540
|
+
}
|
|
2541
|
+
function seedFiles(names, config) {
|
|
2542
|
+
return [
|
|
2543
|
+
{
|
|
2544
|
+
path: seedFilePath(names, config),
|
|
2545
|
+
content: seedFile(names, config),
|
|
2546
|
+
},
|
|
2547
|
+
];
|
|
2548
|
+
}
|
|
1770
2549
|
function listenerFiles(names, config) {
|
|
1771
2550
|
return [
|
|
1772
2551
|
{
|
|
@@ -1790,6 +2569,14 @@ function scheduleFiles(names, config, options) {
|
|
|
1790
2569
|
}
|
|
1791
2570
|
return files;
|
|
1792
2571
|
}
|
|
2572
|
+
function uploadFiles(names, config) {
|
|
2573
|
+
return [
|
|
2574
|
+
{
|
|
2575
|
+
path: uploadFilePath(names, config),
|
|
2576
|
+
content: uploadFile(names, config),
|
|
2577
|
+
},
|
|
2578
|
+
];
|
|
2579
|
+
}
|
|
1793
2580
|
function portFilePath(names, config) {
|
|
1794
2581
|
return path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
|
|
1795
2582
|
}
|
|
@@ -1811,6 +2598,19 @@ function drizzleResourceSchemaFilePath(names, config) {
|
|
|
1811
2598
|
function drizzleResourceRepositoryFilePath(names, config) {
|
|
1812
2599
|
return path.join(infrastructureDir(config), names.pluralKebab, `drizzle-${names.singularKebab}-repository.ts`);
|
|
1813
2600
|
}
|
|
2601
|
+
function resourceSharedErrorsPath(config) {
|
|
2602
|
+
return path.join(config.paths.features, "shared", "errors.ts");
|
|
2603
|
+
}
|
|
2604
|
+
function resourceContractNames(names, mode) {
|
|
2605
|
+
const contractNames = [
|
|
2606
|
+
`list${names.pluralPascal}`,
|
|
2607
|
+
`create${names.singularPascal}`,
|
|
2608
|
+
];
|
|
2609
|
+
if (mode === "resource") {
|
|
2610
|
+
contractNames.push(`get${names.singularPascal}`, `update${names.singularPascal}`, `delete${names.singularPascal}`);
|
|
2611
|
+
}
|
|
2612
|
+
return contractNames;
|
|
2613
|
+
}
|
|
1814
2614
|
async function detectResourcePersistence(targetDir, config) {
|
|
1815
2615
|
return (await fileExists(path.join(targetDir, drizzleRepositoriesPath(config))))
|
|
1816
2616
|
? "drizzle"
|
|
@@ -1819,6 +2619,19 @@ async function detectResourcePersistence(targetDir, config) {
|
|
|
1819
2619
|
function resourceFeatureDir(names, config) {
|
|
1820
2620
|
return path.join(config.paths.features, names.pluralKebab);
|
|
1821
2621
|
}
|
|
2622
|
+
function featureUiDir(names, config) {
|
|
2623
|
+
return path.join(resourceFeatureDir(names, config), "components");
|
|
2624
|
+
}
|
|
2625
|
+
function featureUiComponentFilePath(names, config) {
|
|
2626
|
+
return path.join(featureUiDir(names, config), `${names.componentFileName}.tsx`);
|
|
2627
|
+
}
|
|
2628
|
+
function featureUiIndexFilePath(names, config) {
|
|
2629
|
+
return path.join(featureUiDir(names, config), "index.ts");
|
|
2630
|
+
}
|
|
2631
|
+
function clientIndexPath(config) {
|
|
2632
|
+
const appRoot = path.posix.dirname(config.paths.appContext);
|
|
2633
|
+
return path.join(appRoot === "." ? "" : appRoot, "client/index.ts");
|
|
2634
|
+
}
|
|
1822
2635
|
function usesFeatureOwnedContracts(config) {
|
|
1823
2636
|
return (directoryPath(config.paths.contracts) ===
|
|
1824
2637
|
directoryPath(config.paths.features));
|
|
@@ -1853,6 +2666,9 @@ function resourceUseCaseDir(names, config) {
|
|
|
1853
2666
|
function resourceUseCaseIndexPath(names, config) {
|
|
1854
2667
|
return path.join(resourceUseCaseDir(names, config), "index.ts");
|
|
1855
2668
|
}
|
|
2669
|
+
function resourceSchemaFilePath(names, config) {
|
|
2670
|
+
return path.join(resourceFeatureDir(names, config), "schemas.ts");
|
|
2671
|
+
}
|
|
1856
2672
|
function resourcePortFilePath(names, config) {
|
|
1857
2673
|
if (usesFeatureOwnedContracts(config)) {
|
|
1858
2674
|
return path.join(resourceFeatureDir(names, config), "ports.ts");
|
|
@@ -1882,6 +2698,9 @@ function featureArtifactDir(names, kind, config) {
|
|
|
1882
2698
|
if (kind === "events") {
|
|
1883
2699
|
return path.join(featureDir, "domain", "events");
|
|
1884
2700
|
}
|
|
2701
|
+
if (kind === "factories") {
|
|
2702
|
+
return path.join(featureDir, "tests", "factories");
|
|
2703
|
+
}
|
|
1885
2704
|
return path.join(featureDir, kind);
|
|
1886
2705
|
}
|
|
1887
2706
|
function eventFilePath(names, config) {
|
|
@@ -1890,12 +2709,27 @@ function eventFilePath(names, config) {
|
|
|
1890
2709
|
function jobFilePath(names, config) {
|
|
1891
2710
|
return path.join(featureArtifactDir(names, "jobs", config), `${names.artifact.kebab}.ts`);
|
|
1892
2711
|
}
|
|
2712
|
+
function taskFilePath(names, config) {
|
|
2713
|
+
return path.join(featureArtifactDir(names, "tasks", config), `${names.artifact.kebab}.ts`);
|
|
2714
|
+
}
|
|
2715
|
+
function factoryFilePath(names, config) {
|
|
2716
|
+
return path.join(featureArtifactDir(names, "factories", config), `${names.artifact.kebab}.ts`);
|
|
2717
|
+
}
|
|
2718
|
+
function seedFilePath(names, config) {
|
|
2719
|
+
return path.join(featureArtifactDir(names, "seeds", config), `${names.artifact.kebab}.ts`);
|
|
2720
|
+
}
|
|
2721
|
+
function notificationFilePath(names, config) {
|
|
2722
|
+
return path.join(featureArtifactDir(names, "notifications", config), `${names.artifact.kebab}.ts`);
|
|
2723
|
+
}
|
|
1893
2724
|
function listenerFilePath(names, config) {
|
|
1894
2725
|
return path.join(featureArtifactDir(names, "listeners", config), `${names.artifact.kebab}.ts`);
|
|
1895
2726
|
}
|
|
1896
2727
|
function scheduleFilePath(names, config) {
|
|
1897
2728
|
return path.join(featureArtifactDir(names, "schedules", config), `${names.artifact.kebab}.ts`);
|
|
1898
2729
|
}
|
|
2730
|
+
function uploadFilePath(names, config) {
|
|
2731
|
+
return path.join(featureArtifactDir(names, "uploads", config), `${names.artifact.kebab}.ts`);
|
|
2732
|
+
}
|
|
1899
2733
|
function scheduleRouteFilePath(names, config) {
|
|
1900
2734
|
return path.join(config.paths.routes, "cron", names.feature.kebab, names.artifact.kebab, "route.ts");
|
|
1901
2735
|
}
|
|
@@ -1927,33 +2761,83 @@ async function updateUseCaseIndex(targetDir, file, options) {
|
|
|
1927
2761
|
await writeFile(destination, next);
|
|
1928
2762
|
return "updated";
|
|
1929
2763
|
}
|
|
2764
|
+
async function updateFeatureUiIndex(targetDir, file, options) {
|
|
2765
|
+
const destination = path.join(targetDir, file.path);
|
|
2766
|
+
const existing = await readOptionalFile(destination);
|
|
2767
|
+
if (existing === undefined) {
|
|
2768
|
+
if (!options.dryRun) {
|
|
2769
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
2770
|
+
await writeFile(destination, file.content);
|
|
2771
|
+
}
|
|
2772
|
+
return "created";
|
|
2773
|
+
}
|
|
2774
|
+
if (existing.includes(file.content.trim())) {
|
|
2775
|
+
return "skipped";
|
|
2776
|
+
}
|
|
2777
|
+
const next = `${existing.trimEnd()}\n${file.content}`;
|
|
2778
|
+
if (!options.dryRun)
|
|
2779
|
+
await writeFile(destination, next);
|
|
2780
|
+
return "updated";
|
|
2781
|
+
}
|
|
1930
2782
|
function featureArtifactIndexFile(kind, names, config) {
|
|
1931
2783
|
const artifactPath = kind === "event"
|
|
1932
2784
|
? eventFilePath(names, config)
|
|
1933
2785
|
: kind === "job"
|
|
1934
2786
|
? jobFilePath(names, config)
|
|
1935
|
-
: kind === "
|
|
1936
|
-
?
|
|
1937
|
-
:
|
|
2787
|
+
: kind === "task"
|
|
2788
|
+
? taskFilePath(names, config)
|
|
2789
|
+
: kind === "factory"
|
|
2790
|
+
? factoryFilePath(names, config)
|
|
2791
|
+
: kind === "seed"
|
|
2792
|
+
? seedFilePath(names, config)
|
|
2793
|
+
: kind === "notification"
|
|
2794
|
+
? notificationFilePath(names, config)
|
|
2795
|
+
: kind === "listener"
|
|
2796
|
+
? listenerFilePath(names, config)
|
|
2797
|
+
: kind === "schedule"
|
|
2798
|
+
? scheduleFilePath(names, config)
|
|
2799
|
+
: uploadFilePath(names, config);
|
|
1938
2800
|
const registrySuffix = kind === "event"
|
|
1939
2801
|
? "Events"
|
|
1940
2802
|
: kind === "job"
|
|
1941
2803
|
? "Jobs"
|
|
1942
|
-
: kind === "
|
|
1943
|
-
? "
|
|
1944
|
-
: "
|
|
2804
|
+
: kind === "task"
|
|
2805
|
+
? "Tasks"
|
|
2806
|
+
: kind === "factory"
|
|
2807
|
+
? "Factories"
|
|
2808
|
+
: kind === "seed"
|
|
2809
|
+
? "Seeds"
|
|
2810
|
+
: kind === "notification"
|
|
2811
|
+
? "Notifications"
|
|
2812
|
+
: kind === "listener"
|
|
2813
|
+
? "Listeners"
|
|
2814
|
+
: kind === "schedule"
|
|
2815
|
+
? "Schedules"
|
|
2816
|
+
: "Uploads";
|
|
1945
2817
|
const importName = kind === "event"
|
|
1946
2818
|
? names.eventExportName
|
|
1947
2819
|
: kind === "job"
|
|
1948
2820
|
? names.jobExportName
|
|
1949
|
-
: kind === "
|
|
1950
|
-
? names.
|
|
1951
|
-
:
|
|
2821
|
+
: kind === "task"
|
|
2822
|
+
? names.taskExportName
|
|
2823
|
+
: kind === "factory"
|
|
2824
|
+
? names.factoryExportName
|
|
2825
|
+
: kind === "seed"
|
|
2826
|
+
? names.seedExportName
|
|
2827
|
+
: kind === "notification"
|
|
2828
|
+
? names.notificationExportName
|
|
2829
|
+
: kind === "listener"
|
|
2830
|
+
? names.listenerExportName
|
|
2831
|
+
: kind === "schedule"
|
|
2832
|
+
? names.scheduleExportName
|
|
2833
|
+
: names.uploadExportName;
|
|
1952
2834
|
return {
|
|
1953
2835
|
path: path.join(path.dirname(artifactPath), "index.ts"),
|
|
2836
|
+
kind,
|
|
1954
2837
|
importName,
|
|
1955
2838
|
artifactPath,
|
|
1956
2839
|
registryName: `${names.featureSingularCamel}${registrySuffix}`,
|
|
2840
|
+
registryEntryName: names.artifact.camel,
|
|
1957
2841
|
};
|
|
1958
2842
|
}
|
|
1959
2843
|
async function updateFeatureArtifactIndex(targetDir, file, options) {
|
|
@@ -1961,9 +2845,20 @@ async function updateFeatureArtifactIndex(targetDir, file, options) {
|
|
|
1961
2845
|
const existing = await readOptionalFile(destination);
|
|
1962
2846
|
const specifier = relativeModule(file.path, file.artifactPath);
|
|
1963
2847
|
const importLine = `import { ${file.importName} } from "${specifier}";`;
|
|
2848
|
+
const defineUploadsImport = `import { defineUploads } from "@beignet/core/uploads";`;
|
|
1964
2849
|
const exportLine = `export { ${file.importName} } from "${specifier}";`;
|
|
1965
2850
|
if (existing === undefined) {
|
|
1966
|
-
const content =
|
|
2851
|
+
const content = file.kind === "upload"
|
|
2852
|
+
? `${defineUploadsImport}
|
|
2853
|
+
${importLine}
|
|
2854
|
+
|
|
2855
|
+
${exportLine}
|
|
2856
|
+
|
|
2857
|
+
export const ${file.registryName} = defineUploads({
|
|
2858
|
+
\t${file.registryEntryName}: ${file.importName},
|
|
2859
|
+
});
|
|
2860
|
+
`
|
|
2861
|
+
: `${importLine}
|
|
1967
2862
|
|
|
1968
2863
|
${exportLine}
|
|
1969
2864
|
|
|
@@ -1985,12 +2880,101 @@ export const ${file.registryName} = [${file.importName}] as const;
|
|
|
1985
2880
|
? `${next.slice(0, registryExport.index).trimEnd()}\n${exportLine}\n\n${next.slice(registryExport.index)}`
|
|
1986
2881
|
: `${next.trimEnd()}\n${exportLine}\n`;
|
|
1987
2882
|
}
|
|
1988
|
-
|
|
2883
|
+
if (file.kind === "upload") {
|
|
2884
|
+
const registry = defineUploadsInitializerInfo(next, file.registryName);
|
|
2885
|
+
const arrayRegistry = arrayInitializerInfo(next, file.registryName);
|
|
2886
|
+
if (registry) {
|
|
2887
|
+
if (!next.includes(defineUploadsImport)) {
|
|
2888
|
+
next = insertAfterImports(next, defineUploadsImport);
|
|
2889
|
+
}
|
|
2890
|
+
if (!registry.text.includes(`${file.registryEntryName}:`)) {
|
|
2891
|
+
next = `${next.slice(0, registry.end)}\t${file.registryEntryName}: ${file.importName},\n${next.slice(registry.end)}`;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
else if (arrayRegistry) {
|
|
2895
|
+
if (!identifiersFromArrayExpression(arrayRegistry.text).has(file.importName)) {
|
|
2896
|
+
const nextArray = appendToArrayExpression(arrayRegistry.text, [
|
|
2897
|
+
file.importName,
|
|
2898
|
+
]);
|
|
2899
|
+
next = `${next.slice(0, arrayRegistry.start)}${nextArray}${next.slice(arrayRegistry.end)}`;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
else {
|
|
2903
|
+
if (!next.includes(defineUploadsImport)) {
|
|
2904
|
+
next = insertAfterImports(next, defineUploadsImport);
|
|
2905
|
+
}
|
|
2906
|
+
next = `${next.trimEnd()}\n\nexport const ${file.registryName} = defineUploads({\n\t${file.registryEntryName}: ${file.importName},\n});\n`;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
else {
|
|
2910
|
+
const registry = arrayInitializerInfo(next, file.registryName);
|
|
2911
|
+
if (!registry) {
|
|
2912
|
+
next = `${next.trimEnd()}\n\nexport const ${file.registryName} = [${file.importName}] as const;\n`;
|
|
2913
|
+
}
|
|
2914
|
+
else if (!identifiersFromArrayExpression(registry.text).has(file.importName)) {
|
|
2915
|
+
const nextArray = appendToArrayExpression(registry.text, [
|
|
2916
|
+
file.importName,
|
|
2917
|
+
]);
|
|
2918
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
if (next === existing)
|
|
2922
|
+
return "skipped";
|
|
2923
|
+
if (!options.dryRun)
|
|
2924
|
+
await writeFile(destination, next);
|
|
2925
|
+
return "updated";
|
|
2926
|
+
}
|
|
2927
|
+
async function updateTaskRegistry(targetDir, names, config, options) {
|
|
2928
|
+
const destination = path.join(targetDir, config.paths.tasks);
|
|
2929
|
+
const existing = await readOptionalFile(destination);
|
|
2930
|
+
const featureTasksPath = path.join(featureArtifactDir(names, "tasks", config), "index.ts");
|
|
2931
|
+
const registryName = `${names.featureSingularCamel}Tasks`;
|
|
2932
|
+
const importLine = `import { ${registryName} } from "${relativeModule(config.paths.tasks, featureTasksPath)}";`;
|
|
2933
|
+
const defineTasksImport = `import { defineTasks } from "@beignet/core/tasks";`;
|
|
2934
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
2935
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
2936
|
+
const serverImport = `import { server } from "${relativeModule(config.paths.tasks, config.paths.server)}";`;
|
|
2937
|
+
const registryEntry = `...${registryName}`;
|
|
2938
|
+
if (existing === undefined) {
|
|
2939
|
+
const content = `${defineTasksImport}
|
|
2940
|
+
${serviceActorImport}
|
|
2941
|
+
${appContextImport}
|
|
2942
|
+
${importLine}
|
|
2943
|
+
${serverImport}
|
|
2944
|
+
|
|
2945
|
+
export const tasks = defineTasks([
|
|
2946
|
+
\t${registryEntry},
|
|
2947
|
+
] as const);
|
|
2948
|
+
|
|
2949
|
+
export async function createTaskContext(): Promise<AppContext> {
|
|
2950
|
+
\treturn server.createServiceContext({
|
|
2951
|
+
\t\tactor: createServiceActor("beignet-cli"),
|
|
2952
|
+
\t});
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
export async function stopTaskContext(): Promise<void> {
|
|
2956
|
+
\tawait server.stop();
|
|
2957
|
+
}
|
|
2958
|
+
`;
|
|
2959
|
+
if (!options.dryRun) {
|
|
2960
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
2961
|
+
await writeFile(destination, content);
|
|
2962
|
+
}
|
|
2963
|
+
return "created";
|
|
2964
|
+
}
|
|
2965
|
+
let next = existing;
|
|
2966
|
+
if (!next.includes(defineTasksImport)) {
|
|
2967
|
+
next = insertAfterImports(next, defineTasksImport);
|
|
2968
|
+
}
|
|
2969
|
+
if (!next.includes(importLine)) {
|
|
2970
|
+
next = insertAfterImports(next, importLine);
|
|
2971
|
+
}
|
|
2972
|
+
const registry = arrayInitializerInfo(next, "tasks");
|
|
1989
2973
|
if (!registry) {
|
|
1990
|
-
next = `${next.trimEnd()}\n\nexport const
|
|
2974
|
+
next = `${next.trimEnd()}\n\nexport const tasks = defineTasks([\n\t${registryEntry},\n] as const);\n`;
|
|
1991
2975
|
}
|
|
1992
|
-
else if (!identifiersFromArrayExpression(registry.text).has(
|
|
1993
|
-
const nextArray = appendToArrayExpression(registry.text, [
|
|
2976
|
+
else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
2977
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
1994
2978
|
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
|
|
1995
2979
|
}
|
|
1996
2980
|
if (next === existing)
|
|
@@ -2015,172 +2999,557 @@ function arrayInitializerInfo(source, constName) {
|
|
|
2015
2999
|
end: closeBracket + 1,
|
|
2016
3000
|
};
|
|
2017
3001
|
}
|
|
2018
|
-
function
|
|
3002
|
+
function defineUploadsInitializerInfo(source, constName) {
|
|
3003
|
+
const match = new RegExp(`\\bconst\\s+${constName}\\s*=\\s*defineUploads\\s*\\(`).exec(source);
|
|
3004
|
+
if (!match)
|
|
3005
|
+
return undefined;
|
|
3006
|
+
const openBrace = source.indexOf("{", match.index);
|
|
3007
|
+
if (openBrace === -1)
|
|
3008
|
+
return undefined;
|
|
3009
|
+
const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
|
|
3010
|
+
if (closeBrace === -1)
|
|
3011
|
+
return undefined;
|
|
3012
|
+
return {
|
|
3013
|
+
text: source.slice(openBrace, closeBrace + 1),
|
|
3014
|
+
start: openBrace,
|
|
3015
|
+
end: closeBrace,
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
function schemasFile(names, mode, options) {
|
|
2019
3019
|
return `import { z } from "zod";
|
|
2020
3020
|
|
|
2021
3021
|
export const ${names.singularPascal}Schema = z.object({
|
|
2022
3022
|
id: z.string().uuid(),
|
|
2023
|
-
name: z.string().min(1),
|
|
3023
|
+
${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
3024
|
+
version: z.number().int().min(1),
|
|
2024
3025
|
createdAt: z.string().datetime(),
|
|
3026
|
+
updatedAt: z.string().datetime(),
|
|
2025
3027
|
});
|
|
2026
3028
|
|
|
2027
|
-
export const
|
|
2028
|
-
|
|
2029
|
-
|
|
3029
|
+
export const ${names.singularPascal}SortBySchema = z.enum(["createdAt", "name"]);
|
|
3030
|
+
export const ${names.singularPascal}SortDirectionSchema = z.enum(["asc", "desc"]);
|
|
3031
|
+
|
|
3032
|
+
const ${names.singularPascal}CursorPayloadSchema = z.object({
|
|
3033
|
+
sortBy: ${names.singularPascal}SortBySchema,
|
|
3034
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema,
|
|
3035
|
+
sortValue: z.string(),
|
|
3036
|
+
id: z.string().uuid(),
|
|
2030
3037
|
});
|
|
2031
3038
|
|
|
3039
|
+
export type ${names.singularPascal}SortBy = z.infer<
|
|
3040
|
+
typeof ${names.singularPascal}SortBySchema
|
|
3041
|
+
>;
|
|
3042
|
+
export type ${names.singularPascal}SortDirection = z.infer<
|
|
3043
|
+
typeof ${names.singularPascal}SortDirectionSchema
|
|
3044
|
+
>;
|
|
3045
|
+
export type ${names.singularPascal}Cursor = z.infer<
|
|
3046
|
+
typeof ${names.singularPascal}CursorPayloadSchema
|
|
3047
|
+
>;
|
|
3048
|
+
|
|
3049
|
+
function encodeBase64Url(value: string): string {
|
|
3050
|
+
const bytes = new TextEncoder().encode(value);
|
|
3051
|
+
let binary = "";
|
|
3052
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
3053
|
+
|
|
3054
|
+
return btoa(binary)
|
|
3055
|
+
.replaceAll("+", "-")
|
|
3056
|
+
.replaceAll("/", "_")
|
|
3057
|
+
.replaceAll("=", "");
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function decodeBase64Url(value: string): string {
|
|
3061
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
3062
|
+
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
|
3063
|
+
const binary = atob(base64 + padding);
|
|
3064
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
3065
|
+
|
|
3066
|
+
return new TextDecoder().decode(bytes);
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
export function encode${names.singularPascal}Cursor(cursor: ${names.singularPascal}Cursor): string {
|
|
3070
|
+
return encodeBase64Url(JSON.stringify(cursor));
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
export function decode${names.singularPascal}Cursor(cursor: string): ${names.singularPascal}Cursor {
|
|
3074
|
+
return ${names.singularPascal}CursorPayloadSchema.parse(
|
|
3075
|
+
JSON.parse(decodeBase64Url(cursor)),
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
export const List${names.pluralPascal}InputSchema = z
|
|
3080
|
+
.object({
|
|
3081
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
3082
|
+
cursor: z.string().nullable().default(null),
|
|
3083
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
3084
|
+
sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
|
|
3085
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
|
|
3086
|
+
})
|
|
3087
|
+
.superRefine((input, ctx) => {
|
|
3088
|
+
if (!input.cursor) return;
|
|
3089
|
+
|
|
3090
|
+
try {
|
|
3091
|
+
const cursor = decode${names.singularPascal}Cursor(input.cursor);
|
|
3092
|
+
if (
|
|
3093
|
+
cursor.sortBy !== input.sortBy ||
|
|
3094
|
+
cursor.sortDirection !== input.sortDirection
|
|
3095
|
+
) {
|
|
3096
|
+
ctx.addIssue({
|
|
3097
|
+
code: z.ZodIssueCode.custom,
|
|
3098
|
+
message: "cursor does not match the requested sort.",
|
|
3099
|
+
path: ["cursor"],
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
} catch {
|
|
3103
|
+
ctx.addIssue({
|
|
3104
|
+
code: z.ZodIssueCode.custom,
|
|
3105
|
+
message: "cursor is invalid.",
|
|
3106
|
+
path: ["cursor"],
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
3110
|
+
|
|
2032
3111
|
export const List${names.pluralPascal}OutputSchema = z.object({
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
3112
|
+
items: z.array(${names.singularPascal}Schema),
|
|
3113
|
+
page: z.object({
|
|
3114
|
+
kind: z.literal("cursor"),
|
|
3115
|
+
limit: z.number().int().min(1),
|
|
3116
|
+
cursor: z.string().nullable(),
|
|
3117
|
+
nextCursor: z.string().nullable(),
|
|
3118
|
+
hasMore: z.boolean(),
|
|
3119
|
+
}),
|
|
2037
3120
|
});
|
|
2038
3121
|
|
|
2039
3122
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
2040
3123
|
name: z.string().min(1).max(120),
|
|
2041
3124
|
});
|
|
3125
|
+
${mode === "resource"
|
|
3126
|
+
? `
|
|
3127
|
+
export const ${names.singularPascal}IdInputSchema = z.object({
|
|
3128
|
+
id: z.string().uuid(),
|
|
3129
|
+
});
|
|
2042
3130
|
|
|
3131
|
+
export const Update${names.singularPascal}BodySchema = z.object({
|
|
3132
|
+
name: z.string().min(1).max(120),
|
|
3133
|
+
version: z.number().int().min(1),
|
|
3134
|
+
});
|
|
3135
|
+
|
|
3136
|
+
export const Update${names.singularPascal}InputSchema =
|
|
3137
|
+
${names.singularPascal}IdInputSchema.merge(Update${names.singularPascal}BodySchema);
|
|
3138
|
+
`
|
|
3139
|
+
: ""}
|
|
2043
3140
|
export type ${names.singularPascal} = z.infer<typeof ${names.singularPascal}Schema>;
|
|
2044
3141
|
export type Create${names.singularPascal}Input = z.infer<
|
|
2045
3142
|
typeof Create${names.singularPascal}InputSchema
|
|
2046
3143
|
>;
|
|
3144
|
+
${mode === "resource"
|
|
3145
|
+
? `
|
|
3146
|
+
export type ${names.singularPascal}IdInput = z.infer<
|
|
3147
|
+
typeof ${names.singularPascal}IdInputSchema
|
|
3148
|
+
>;
|
|
3149
|
+
export type Update${names.singularPascal}Body = z.infer<
|
|
3150
|
+
typeof Update${names.singularPascal}BodySchema
|
|
3151
|
+
>;
|
|
3152
|
+
export type Update${names.singularPascal}Input = z.infer<
|
|
3153
|
+
typeof Update${names.singularPascal}InputSchema
|
|
3154
|
+
>;
|
|
3155
|
+
`
|
|
3156
|
+
: ""}
|
|
2047
3157
|
export type List${names.pluralPascal}Input = z.infer<
|
|
2048
3158
|
typeof List${names.pluralPascal}InputSchema
|
|
2049
3159
|
>;
|
|
2050
3160
|
`;
|
|
2051
3161
|
}
|
|
2052
|
-
function listUseCaseFile(names, config) {
|
|
3162
|
+
function listUseCaseFile(names, config, options) {
|
|
2053
3163
|
const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
|
|
2054
|
-
return `import {
|
|
3164
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3165
|
+
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
3166
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2055
3167
|
import {
|
|
3168
|
+
decode${names.singularPascal}Cursor,
|
|
2056
3169
|
List${names.pluralPascal}InputSchema,
|
|
2057
3170
|
List${names.pluralPascal}OutputSchema,
|
|
2058
|
-
} from "
|
|
3171
|
+
} from "../schemas";
|
|
2059
3172
|
|
|
2060
3173
|
export const list${names.pluralPascal}UseCase = useCase
|
|
2061
3174
|
.query("${names.pluralCamel}.list")
|
|
2062
3175
|
.input(List${names.pluralPascal}InputSchema)
|
|
2063
3176
|
.output(List${names.pluralPascal}OutputSchema)
|
|
2064
3177
|
.run(async ({ ctx, input }) => {
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
3178
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to list ${names.pluralKebab}.");\n\t\t}\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
3179
|
+
defaultLimit: 20,
|
|
3180
|
+
maxLimit: 100,
|
|
3181
|
+
});
|
|
3182
|
+
|
|
3183
|
+
return ctx.ports.${names.pluralCamel}.list({
|
|
3184
|
+
page,
|
|
3185
|
+
cursor: page.cursor ? decode${names.singularPascal}Cursor(page.cursor) : null,
|
|
3186
|
+
name: input.name,
|
|
3187
|
+
sortBy: input.sortBy,
|
|
3188
|
+
sortDirection: input.sortDirection,
|
|
3189
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
2072
3190
|
});
|
|
2073
3191
|
`;
|
|
2074
3192
|
}
|
|
2075
|
-
function createUseCaseFile(names, config) {
|
|
3193
|
+
function createUseCaseFile(names, config, options) {
|
|
2076
3194
|
const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
|
|
2077
|
-
return `import {
|
|
3195
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3196
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2078
3197
|
import {
|
|
2079
3198
|
Create${names.singularPascal}InputSchema,
|
|
2080
3199
|
${names.singularPascal}Schema,
|
|
2081
|
-
} from "
|
|
3200
|
+
} from "../schemas";
|
|
3201
|
+
${options.events ? `import { ${names.singularPascal}Created } from "../domain/events";\n` : ""}
|
|
2082
3202
|
|
|
2083
3203
|
export const create${names.singularPascal}UseCase = useCase
|
|
2084
3204
|
.command("${names.pluralCamel}.create")
|
|
2085
3205
|
.input(Create${names.singularPascal}InputSchema)
|
|
2086
3206
|
.output(${names.singularPascal}Schema)
|
|
2087
|
-
.run(async ({ ctx, input }) =>
|
|
3207
|
+
.run(async ({ ctx, input }) => {
|
|
3208
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to create ${names.singularKebab}.");\n\t\t}\n\n` : ""}${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create({
|
|
3209
|
+
...input,
|
|
3210
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3211
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3212
|
+
return ${names.singularCamel};
|
|
3213
|
+
});
|
|
2088
3214
|
`;
|
|
2089
3215
|
}
|
|
2090
|
-
function
|
|
3216
|
+
function getUseCaseFile(names, config, options) {
|
|
3217
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
|
|
3218
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3219
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3220
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3221
|
+
import {
|
|
3222
|
+
${names.singularPascal}IdInputSchema,
|
|
3223
|
+
${names.singularPascal}Schema,
|
|
3224
|
+
} from "../schemas";
|
|
3225
|
+
|
|
3226
|
+
export const get${names.singularPascal}UseCase = useCase
|
|
3227
|
+
.query("${names.pluralCamel}.get")
|
|
3228
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
3229
|
+
.output(${names.singularPascal}Schema)
|
|
3230
|
+
.run(async ({ ctx, input }) => {
|
|
3231
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to get ${names.singularKebab}.");\n\t\t}\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
3232
|
+
if (!${names.singularCamel}) {
|
|
3233
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3234
|
+
details: { id: input.id },
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
return ${names.singularCamel};
|
|
3239
|
+
});
|
|
3240
|
+
`;
|
|
3241
|
+
}
|
|
3242
|
+
function updateUseCaseFile(names, config, options) {
|
|
3243
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
|
|
3244
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3245
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3246
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3247
|
+
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
3248
|
+
Update${names.singularPascal}InputSchema,
|
|
3249
|
+
${names.singularPascal}Schema,
|
|
3250
|
+
} from "../schemas";
|
|
3251
|
+
|
|
3252
|
+
export const update${names.singularPascal}UseCase = useCase
|
|
3253
|
+
.command("${names.pluralCamel}.update")
|
|
3254
|
+
.input(Update${names.singularPascal}InputSchema)
|
|
3255
|
+
.output(${names.singularPascal}Schema)
|
|
3256
|
+
.run(async ({ ctx, input }) => {
|
|
3257
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to update ${names.singularKebab}.");\n\t\t}\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
3258
|
+
if (!existing) {
|
|
3259
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3260
|
+
details: { id: input.id },
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3263
|
+
${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
3264
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
3265
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
3270
|
+
...input,
|
|
3271
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3272
|
+
if (!${names.singularCamel}) {
|
|
3273
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
3274
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Updated, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3278
|
+
return ${names.singularCamel};
|
|
3279
|
+
});
|
|
3280
|
+
`;
|
|
3281
|
+
}
|
|
3282
|
+
function deleteUseCaseFile(names, config, options) {
|
|
3283
|
+
const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
|
|
3284
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3285
|
+
import { z } from "zod";
|
|
3286
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3287
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3288
|
+
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
3289
|
+
|
|
3290
|
+
export const delete${names.singularPascal}UseCase = useCase
|
|
3291
|
+
.command("${names.pluralCamel}.delete")
|
|
3292
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
3293
|
+
.output(z.void())
|
|
3294
|
+
.run(async ({ ctx, input }) => {
|
|
3295
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to delete ${names.singularKebab}.");\n\t\t}\n\n` : ""}${options.auth ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
3296
|
+
if (!deleted) {
|
|
3297
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
3298
|
+
details: { id: input.id },
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Deleted, {\n\t\t\tid: input.id,\n\t\t});\n` : ""} });
|
|
3302
|
+
`;
|
|
3303
|
+
}
|
|
3304
|
+
function useCasesIndexFile(names, mode) {
|
|
2091
3305
|
return `export { create${names.singularPascal}UseCase } from "./create-${names.singularKebab}";
|
|
2092
|
-
export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
2093
|
-
export {
|
|
3306
|
+
${mode === "resource" ? `export { delete${names.singularPascal}UseCase } from "./delete-${names.singularKebab}";\nexport { get${names.singularPascal}UseCase } from "./get-${names.singularKebab}";\n` : ""}export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
3307
|
+
${mode === "resource" ? `export { update${names.singularPascal}UseCase } from "./update-${names.singularKebab}";\n` : ""}export {
|
|
2094
3308
|
Create${names.singularPascal}InputSchema,
|
|
2095
|
-
List${names.pluralPascal}InputSchema,
|
|
3309
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n` : ""} List${names.pluralPascal}InputSchema,
|
|
2096
3310
|
List${names.pluralPascal}OutputSchema,
|
|
2097
|
-
${names.singularPascal}Schema,
|
|
3311
|
+
${mode === "resource" ? ` Update${names.singularPascal}BodySchema,\n Update${names.singularPascal}InputSchema,\n` : ""} ${names.singularPascal}Schema,
|
|
2098
3312
|
type Create${names.singularPascal}Input,
|
|
2099
|
-
type List${names.pluralPascal}Input,
|
|
3313
|
+
${mode === "resource" ? ` type ${names.singularPascal}IdInput,\n type Update${names.singularPascal}Body,\n type Update${names.singularPascal}Input,\n` : ""} type List${names.pluralPascal}Input,
|
|
2100
3314
|
type ${names.singularPascal},
|
|
2101
|
-
} from "
|
|
3315
|
+
} from "../schemas";
|
|
2102
3316
|
`;
|
|
2103
3317
|
}
|
|
2104
|
-
function repositoryPortFile(names, config) {
|
|
3318
|
+
function repositoryPortFile(names, config, mode, options) {
|
|
2105
3319
|
return `import type {
|
|
3320
|
+
CursorPage,
|
|
3321
|
+
CursorPageInfo,
|
|
3322
|
+
PageResult,
|
|
3323
|
+
SortDirection,
|
|
3324
|
+
} from "@beignet/core/pagination";
|
|
3325
|
+
import type {
|
|
2106
3326
|
Create${names.singularPascal}Input,
|
|
2107
|
-
|
|
2108
|
-
${names.singularPascal},
|
|
2109
|
-
|
|
3327
|
+
${names.singularPascal}Cursor,
|
|
3328
|
+
${names.singularPascal}SortBy,
|
|
3329
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3330
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2110
3331
|
|
|
2111
|
-
export type List${names.pluralPascal}Result = {
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
};
|
|
3332
|
+
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal}, CursorPageInfo>;
|
|
3333
|
+
export type List${names.pluralPascal}Query = {
|
|
3334
|
+
page: CursorPage;
|
|
3335
|
+
cursor: ${names.singularPascal}Cursor | null;
|
|
3336
|
+
name?: string;
|
|
3337
|
+
sortBy: ${names.singularPascal}SortBy;
|
|
3338
|
+
sortDirection: SortDirection;
|
|
3339
|
+
${options.tenant ? `\ttenantId: string;\n` : ""}};
|
|
3340
|
+
${options.tenant ? `export type ${names.singularPascal}TenantFilter = { tenantId: string };\nexport type Create${names.singularPascal}RepositoryInput = Create${names.singularPascal}Input & ${names.singularPascal}TenantFilter;\n${mode === "resource" ? `export type Update${names.singularPascal}RepositoryInput = Update${names.singularPascal}Input & ${names.singularPascal}TenantFilter;\n` : ""}` : ""}
|
|
2115
3341
|
|
|
2116
3342
|
export interface ${names.singularPascal}Repository {
|
|
2117
|
-
list(
|
|
2118
|
-
create(input: Create${names.singularPascal}Input): Promise<${names.singularPascal}>;
|
|
2119
|
-
}
|
|
3343
|
+
list(query: List${names.pluralPascal}Query): Promise<List${names.pluralPascal}Result>;
|
|
3344
|
+
create(input: ${options.tenant ? `Create${names.singularPascal}RepositoryInput` : `Create${names.singularPascal}Input`}): Promise<${names.singularPascal}>;
|
|
3345
|
+
${mode === "resource" ? ` findById(id: string${options.tenant ? `, filter: ${names.singularPascal}TenantFilter` : ""}): Promise<${names.singularPascal} | null>;\n update(input: ${options.tenant ? `Update${names.singularPascal}RepositoryInput` : `Update${names.singularPascal}Input`}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenant ? `, filter: ${names.singularPascal}TenantFilter` : ""}): Promise<boolean>;\n` : ""}}
|
|
2120
3346
|
`;
|
|
2121
3347
|
}
|
|
2122
|
-
function inMemoryRepositoryFile(names, config) {
|
|
3348
|
+
function inMemoryRepositoryFile(names, config, mode, options) {
|
|
2123
3349
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
2124
|
-
|
|
3350
|
+
const recordType = options.softDelete
|
|
3351
|
+
? `type ${names.singularPascal}Record = ${names.singularPascal} & {
|
|
3352
|
+
\tdeletedAt: string | null;
|
|
3353
|
+
};
|
|
3354
|
+
|
|
3355
|
+
`
|
|
3356
|
+
: "";
|
|
3357
|
+
const mapValueType = options.softDelete
|
|
3358
|
+
? `${names.singularPascal}Record`
|
|
3359
|
+
: names.singularPascal;
|
|
3360
|
+
const activeFilter = options.softDelete
|
|
3361
|
+
? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.deletedAt === null)\n`
|
|
3362
|
+
: "";
|
|
3363
|
+
const newDeletedAtField = options.softDelete
|
|
3364
|
+
? `\t\t\t\tdeletedAt: null,\n`
|
|
3365
|
+
: "";
|
|
3366
|
+
const findDeletedGuard = options.softDelete
|
|
3367
|
+
? `\t\t\tif (${names.singularCamel}?.deletedAt !== null) return null;\n`
|
|
3368
|
+
: "";
|
|
3369
|
+
const updateDeletedGuard = options.softDelete
|
|
3370
|
+
? " || existing.deletedAt !== null"
|
|
3371
|
+
: "";
|
|
3372
|
+
const deleteImplementation = options.softDelete
|
|
3373
|
+
? `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== filter.tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
|
|
3374
|
+
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
3375
|
+
return true;`
|
|
3376
|
+
: `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== filter.tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
|
|
3377
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3378
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
3379
|
+
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3380
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2125
3381
|
import type {
|
|
2126
3382
|
Create${names.singularPascal}Input,
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
3383
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3384
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3385
|
+
|
|
3386
|
+
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
3387
|
+
return {
|
|
3388
|
+
id: ${names.singularCamel}.id,
|
|
3389
|
+
${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
3390
|
+
version: ${names.singularCamel}.version,
|
|
3391
|
+
createdAt: ${names.singularCamel}.createdAt,
|
|
3392
|
+
updatedAt: ${names.singularCamel}.updatedAt,
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
function compare${names.pluralPascal}(
|
|
3397
|
+
left: ${mapValueType},
|
|
3398
|
+
right: ${mapValueType},
|
|
3399
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3400
|
+
): number {
|
|
3401
|
+
const leftValue = left[query.sortBy];
|
|
3402
|
+
const rightValue = right[query.sortBy];
|
|
3403
|
+
const fieldComparison =
|
|
3404
|
+
leftValue === rightValue ? left.id.localeCompare(right.id) : leftValue.localeCompare(rightValue);
|
|
3405
|
+
|
|
3406
|
+
return query.sortDirection === "asc" ? fieldComparison : -fieldComparison;
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
function isAfter${names.singularPascal}Cursor(
|
|
3410
|
+
${names.singularCamel}: ${mapValueType},
|
|
3411
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3412
|
+
): boolean {
|
|
3413
|
+
if (!query.cursor) return true;
|
|
3414
|
+
|
|
3415
|
+
const sortValue = ${names.singularCamel}[query.sortBy];
|
|
3416
|
+
const fieldComparison =
|
|
3417
|
+
sortValue === query.cursor.sortValue
|
|
3418
|
+
? ${names.singularCamel}.id.localeCompare(query.cursor.id)
|
|
3419
|
+
: sortValue.localeCompare(query.cursor.sortValue);
|
|
3420
|
+
|
|
3421
|
+
return query.sortDirection === "asc" ? fieldComparison > 0 : fieldComparison < 0;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
function cursorFor${names.singularPascal}(
|
|
3425
|
+
${names.singularCamel}: ${mapValueType},
|
|
3426
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
3427
|
+
): string {
|
|
3428
|
+
return encode${names.singularPascal}Cursor({
|
|
3429
|
+
sortBy: query.sortBy,
|
|
3430
|
+
sortDirection: query.sortDirection,
|
|
3431
|
+
sortValue: ${names.singularCamel}[query.sortBy],
|
|
3432
|
+
id: ${names.singularCamel}.id,
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
2130
3435
|
|
|
2131
3436
|
export function createInMemory${names.singularPascal}Repository(
|
|
2132
3437
|
seed: ${names.singularPascal}[] = [],
|
|
2133
3438
|
): ${names.singularPascal}Repository {
|
|
2134
|
-
const ${names.pluralCamel} = new Map
|
|
3439
|
+
const ${names.pluralCamel} = new Map<string, ${mapValueType}>(
|
|
3440
|
+
seed.map((${names.singularCamel}) => [
|
|
3441
|
+
${names.singularCamel}.id,
|
|
3442
|
+
{ ...${names.singularCamel}${options.softDelete ? ", deletedAt: null" : ""} },
|
|
3443
|
+
]),
|
|
3444
|
+
);
|
|
2135
3445
|
|
|
2136
3446
|
return {
|
|
2137
|
-
async list(
|
|
2138
|
-
const
|
|
2139
|
-
|
|
2140
|
-
|
|
3447
|
+
async list(query) {
|
|
3448
|
+
const name = query.name?.toLocaleLowerCase();
|
|
3449
|
+
const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
3450
|
+
${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === query.tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
|
|
3451
|
+
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
3452
|
+
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
3453
|
+
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
3454
|
+
const nextCursor =
|
|
3455
|
+
all${names.pluralPascal}.length > query.page.limit && pageItems.length > 0
|
|
3456
|
+
? cursorFor${names.singularPascal}(pageItems[pageItems.length - 1]!, query)
|
|
3457
|
+
: null;
|
|
2141
3458
|
|
|
2142
|
-
return
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
total: all${names.pluralPascal}.length,
|
|
2148
|
-
};
|
|
3459
|
+
return cursorPageResult(
|
|
3460
|
+
pageItems.map(to${names.singularPascal}),
|
|
3461
|
+
query.page,
|
|
3462
|
+
nextCursor,
|
|
3463
|
+
);
|
|
2149
3464
|
},
|
|
2150
|
-
async create(input
|
|
3465
|
+
async create(input) {
|
|
3466
|
+
const now = new Date().toISOString();
|
|
2151
3467
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2152
3468
|
id: crypto.randomUUID(),
|
|
2153
|
-
name: input.name,
|
|
2154
|
-
|
|
3469
|
+
${options.tenant ? `\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
3470
|
+
version: 1,
|
|
3471
|
+
createdAt: now,
|
|
3472
|
+
updatedAt: now,
|
|
2155
3473
|
};
|
|
2156
|
-
${names.pluralCamel}.set(${names.singularCamel}.id,
|
|
3474
|
+
${names.pluralCamel}.set(${names.singularCamel}.id, {
|
|
3475
|
+
...${names.singularCamel},
|
|
3476
|
+
${newDeletedAtField} });
|
|
2157
3477
|
return ${names.singularCamel};
|
|
2158
3478
|
},
|
|
3479
|
+
${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {\n const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenant ? `\t\t\tif (${names.singularCamel}?.tenantId !== filter.tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input) {\n const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenant ? " || existing.tenantId !== input.tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenant ? ", filter" : ""}) {\n${deleteImplementation}\n },\n` : ""}
|
|
2159
3480
|
};
|
|
2160
3481
|
}
|
|
2161
3482
|
`;
|
|
2162
3483
|
}
|
|
2163
|
-
function drizzleSchemaFile(names) {
|
|
2164
|
-
return `import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
3484
|
+
function drizzleSchemaFile(names, options) {
|
|
3485
|
+
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2165
3486
|
|
|
2166
3487
|
export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
2167
3488
|
id: text("id").primaryKey(),
|
|
2168
|
-
name: text("name").notNull(),
|
|
3489
|
+
${options.tenant ? `\ttenantId: text("tenant_id").notNull(),\n` : ""} name: text("name").notNull(),
|
|
3490
|
+
version: integer("version").notNull(),
|
|
2169
3491
|
createdAt: text("created_at").notNull(),
|
|
2170
|
-
|
|
3492
|
+
updatedAt: text("updated_at").notNull(),
|
|
3493
|
+
${options.softDelete ? `\tdeletedAt: text("deleted_at"),\n` : ""}});
|
|
2171
3494
|
`;
|
|
2172
3495
|
}
|
|
2173
|
-
function
|
|
3496
|
+
function drizzleResourceWhere(names, options, predicates) {
|
|
3497
|
+
const allPredicates = [
|
|
3498
|
+
...predicates,
|
|
3499
|
+
...(options.softDelete
|
|
3500
|
+
? [`isNull(schema.${names.pluralCamel}.deletedAt)`]
|
|
3501
|
+
: []),
|
|
3502
|
+
];
|
|
3503
|
+
if (allPredicates.length === 0)
|
|
3504
|
+
return "";
|
|
3505
|
+
if (allPredicates.length === 1)
|
|
3506
|
+
return allPredicates[0] ?? "";
|
|
3507
|
+
return `and(${allPredicates.join(", ")})`;
|
|
3508
|
+
}
|
|
3509
|
+
function drizzleRepositoryFile(names, config, mode, options) {
|
|
2174
3510
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
2175
3511
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
2176
|
-
|
|
2177
|
-
|
|
3512
|
+
const findWhere = drizzleResourceWhere(names, options, [
|
|
3513
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
3514
|
+
...(options.tenant
|
|
3515
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
3516
|
+
: []),
|
|
3517
|
+
]);
|
|
3518
|
+
const updateWhere = drizzleResourceWhere(names, options, [
|
|
3519
|
+
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
3520
|
+
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
3521
|
+
...(options.tenant
|
|
3522
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, input.tenantId)`]
|
|
3523
|
+
: []),
|
|
3524
|
+
]);
|
|
3525
|
+
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
3526
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
3527
|
+
...(options.tenant
|
|
3528
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
3529
|
+
: []),
|
|
3530
|
+
]);
|
|
3531
|
+
const drizzleImports = [
|
|
3532
|
+
"and",
|
|
3533
|
+
"asc",
|
|
3534
|
+
"desc",
|
|
3535
|
+
"eq",
|
|
3536
|
+
"gt",
|
|
3537
|
+
options.softDelete ? "isNull" : undefined,
|
|
3538
|
+
"lt",
|
|
3539
|
+
"or",
|
|
3540
|
+
"sql",
|
|
3541
|
+
"type SQL",
|
|
3542
|
+
].filter((name) => Boolean(name));
|
|
3543
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3544
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
3545
|
+
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
3546
|
+
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
2178
3547
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3548
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2179
3549
|
import type {
|
|
2180
3550
|
Create${names.singularPascal}Input,
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
} from "${aliasModule(path.join(resourceUseCaseDir(names, config), "schemas.ts"))}";
|
|
3551
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
3552
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2184
3553
|
import * as schema from "${aliasModule(schemaPath)}";
|
|
2185
3554
|
|
|
2186
3555
|
type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelect;
|
|
@@ -2188,38 +3557,104 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
2188
3557
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
2189
3558
|
return {
|
|
2190
3559
|
id: row.id,
|
|
2191
|
-
name: row.name,
|
|
3560
|
+
${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
3561
|
+
version: row.version,
|
|
2192
3562
|
createdAt: row.createdAt,
|
|
3563
|
+
updatedAt: row.updatedAt,
|
|
2193
3564
|
};
|
|
2194
3565
|
}
|
|
2195
3566
|
|
|
3567
|
+
type List${names.pluralPascal}Query = Parameters<${names.singularPascal}Repository["list"]>[0];
|
|
3568
|
+
|
|
3569
|
+
function ${names.singularCamel}SortColumn(query: List${names.pluralPascal}Query) {
|
|
3570
|
+
return query.sortBy === "name" ? schema.${names.pluralCamel}.name : schema.${names.pluralCamel}.createdAt;
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
function ${names.singularCamel}CursorFilter(
|
|
3574
|
+
query: List${names.pluralPascal}Query,
|
|
3575
|
+
): SQL<unknown> | undefined {
|
|
3576
|
+
if (!query.cursor) return undefined;
|
|
3577
|
+
|
|
3578
|
+
const column = ${names.singularCamel}SortColumn(query);
|
|
3579
|
+
const compare = query.sortDirection === "asc" ? gt : lt;
|
|
3580
|
+
|
|
3581
|
+
return or(
|
|
3582
|
+
compare(column, query.cursor.sortValue),
|
|
3583
|
+
and(
|
|
3584
|
+
eq(column, query.cursor.sortValue),
|
|
3585
|
+
compare(schema.${names.pluralCamel}.id, query.cursor.id),
|
|
3586
|
+
),
|
|
3587
|
+
) as SQL<unknown>;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
function ${names.singularCamel}ListWhere(
|
|
3591
|
+
query: List${names.pluralPascal}Query,
|
|
3592
|
+
): SQL<unknown> | undefined {
|
|
3593
|
+
const filters: SQL<unknown>[] = [
|
|
3594
|
+
${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, query.tenantId),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
3595
|
+
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
3596
|
+
|
|
3597
|
+
if (query.name) {
|
|
3598
|
+
const pattern = \`%\${query.name.toLocaleLowerCase()}%\`;
|
|
3599
|
+
filters.push(sql\`lower(\${schema.${names.pluralCamel}.name}) like \${pattern}\`);
|
|
3600
|
+
}
|
|
3601
|
+
if (cursor) filters.push(cursor);
|
|
3602
|
+
|
|
3603
|
+
return filters.length > 0 ? and(...filters) : undefined;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
function ${names.singularCamel}OrderBy(query: List${names.pluralPascal}Query) {
|
|
3607
|
+
const order = query.sortDirection === "asc" ? asc : desc;
|
|
3608
|
+
|
|
3609
|
+
return [order(${names.singularCamel}SortColumn(query)), order(schema.${names.pluralCamel}.id)] as const;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
function cursorFor${names.singularPascal}(
|
|
3613
|
+
row: ${names.singularPascal}Row,
|
|
3614
|
+
query: List${names.pluralPascal}Query,
|
|
3615
|
+
): string {
|
|
3616
|
+
return encode${names.singularPascal}Cursor({
|
|
3617
|
+
sortBy: query.sortBy,
|
|
3618
|
+
sortDirection: query.sortDirection,
|
|
3619
|
+
sortValue: row[query.sortBy],
|
|
3620
|
+
id: row.id,
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
|
|
2196
3624
|
export function createDrizzle${names.singularPascal}Repository(
|
|
2197
3625
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
2198
3626
|
): ${names.singularPascal}Repository {
|
|
2199
3627
|
return {
|
|
2200
|
-
async list(
|
|
3628
|
+
async list(query) {
|
|
3629
|
+
const where = ${names.singularCamel}ListWhere(query);
|
|
2201
3630
|
const rows = await db
|
|
2202
3631
|
.select()
|
|
2203
3632
|
.from(schema.${names.pluralCamel})
|
|
2204
|
-
.
|
|
2205
|
-
.
|
|
2206
|
-
.
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
.
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
3633
|
+
.where(where)
|
|
3634
|
+
.orderBy(...${names.singularCamel}OrderBy(query))
|
|
3635
|
+
.limit(query.page.limit + 1);
|
|
3636
|
+
const pageRows = rows.slice(0, query.page.limit);
|
|
3637
|
+
const nextCursor =
|
|
3638
|
+
rows.length > query.page.limit && pageRows.length > 0
|
|
3639
|
+
? cursorFor${names.singularPascal}(pageRows[pageRows.length - 1]!, query)
|
|
3640
|
+
: null;
|
|
3641
|
+
|
|
3642
|
+
return cursorPageResult(
|
|
3643
|
+
pageRows.map(to${names.singularPascal}),
|
|
3644
|
+
query.page,
|
|
3645
|
+
nextCursor,
|
|
3646
|
+
);
|
|
2215
3647
|
},
|
|
2216
|
-
async create(input
|
|
3648
|
+
async create(input) {
|
|
3649
|
+
const now = new Date().toISOString();
|
|
2217
3650
|
const [row] = await db
|
|
2218
3651
|
.insert(schema.${names.pluralCamel})
|
|
2219
3652
|
.values({
|
|
2220
3653
|
id: crypto.randomUUID(),
|
|
2221
|
-
name: input.name,
|
|
2222
|
-
|
|
3654
|
+
${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
3655
|
+
version: 1,
|
|
3656
|
+
createdAt: now,
|
|
3657
|
+
updatedAt: now,
|
|
2223
3658
|
})
|
|
2224
3659
|
.returning();
|
|
2225
3660
|
|
|
@@ -2229,17 +3664,20 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
2229
3664
|
|
|
2230
3665
|
return to${names.singularPascal}(row);
|
|
2231
3666
|
},
|
|
3667
|
+
${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {\n const [row] = await db\n .select()\n .from(schema.${names.pluralCamel})\n .where(${findWhere})\n .limit(1);\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async update(input) {\n const [row] = await db\n .update(schema.${names.pluralCamel})\n .set({\n name: input.name,\n version: input.version + 1,\n updatedAt: new Date().toISOString(),\n })\n .where(${updateWhere})\n .returning();\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async delete(id: string${options.tenant ? ", filter" : ""}) {\n${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}\n return rows.length > 0;\n },\n` : ""}
|
|
2232
3668
|
};
|
|
2233
3669
|
}
|
|
2234
3670
|
`;
|
|
2235
3671
|
}
|
|
2236
|
-
function contractFile(names, config) {
|
|
2237
|
-
return `import {
|
|
3672
|
+
function contractFile(names, config, mode, options) {
|
|
3673
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
2238
3674
|
import { z } from "zod";
|
|
2239
|
-
import {
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
3675
|
+
${mode === "resource" ? `import { errors } from "${aliasModule(resourceSharedErrorsPath(config))}";\n` : ""}import {
|
|
3676
|
+
Create${names.singularPascal}InputSchema,
|
|
3677
|
+
List${names.pluralPascal}InputSchema,
|
|
3678
|
+
List${names.pluralPascal}OutputSchema,
|
|
3679
|
+
${names.singularPascal}Schema,
|
|
3680
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n Update${names.singularPascal}BodySchema,\n` : ""}} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2243
3681
|
|
|
2244
3682
|
const ErrorResponseSchema = z.object({
|
|
2245
3683
|
code: z.string(),
|
|
@@ -2247,7 +3685,7 @@ const ErrorResponseSchema = z.object({
|
|
|
2247
3685
|
requestId: z.string().optional(),
|
|
2248
3686
|
});
|
|
2249
3687
|
|
|
2250
|
-
const ${names.pluralCamel} =
|
|
3688
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
2251
3689
|
.namespace("${names.pluralCamel}")
|
|
2252
3690
|
.responses({
|
|
2253
3691
|
500: ErrorResponseSchema,
|
|
@@ -2255,21 +3693,49 @@ const ${names.pluralCamel} = createContractGroup()
|
|
|
2255
3693
|
|
|
2256
3694
|
export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
2257
3695
|
.get("/api/${names.pluralKebab}")
|
|
2258
|
-
.query(
|
|
3696
|
+
.query(List${names.pluralPascal}InputSchema)
|
|
2259
3697
|
.responses({
|
|
2260
|
-
200:
|
|
3698
|
+
200: List${names.pluralPascal}OutputSchema,
|
|
2261
3699
|
});
|
|
2262
3700
|
|
|
2263
3701
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
2264
3702
|
.post("/api/${names.pluralKebab}")
|
|
2265
|
-
.body(
|
|
3703
|
+
.body(Create${names.singularPascal}InputSchema)
|
|
3704
|
+
${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
3705
|
+
201: ${names.singularPascal}Schema,
|
|
3706
|
+
});
|
|
3707
|
+
${mode === "resource"
|
|
3708
|
+
? `
|
|
3709
|
+
export const get${names.singularPascal} = ${names.pluralCamel}
|
|
3710
|
+
.get("/api/${names.pluralKebab}/:id")
|
|
3711
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3712
|
+
.errors({ ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
3713
|
+
.responses({
|
|
3714
|
+
200: ${names.singularPascal}Schema,
|
|
3715
|
+
});
|
|
3716
|
+
|
|
3717
|
+
export const update${names.singularPascal} = ${names.pluralCamel}
|
|
3718
|
+
.patch("/api/${names.pluralKebab}/:id")
|
|
3719
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3720
|
+
.body(Update${names.singularPascal}BodySchema)
|
|
3721
|
+
${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
|
|
2266
3722
|
.responses({
|
|
2267
|
-
|
|
3723
|
+
200: ${names.singularPascal}Schema,
|
|
2268
3724
|
});
|
|
3725
|
+
|
|
3726
|
+
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
3727
|
+
.delete("/api/${names.pluralKebab}/:id")
|
|
3728
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3729
|
+
${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
3730
|
+
.responses({
|
|
3731
|
+
204: null,
|
|
3732
|
+
});
|
|
3733
|
+
`
|
|
3734
|
+
: ""}
|
|
2269
3735
|
`;
|
|
2270
3736
|
}
|
|
2271
3737
|
function standaloneContractFile(names) {
|
|
2272
|
-
return `import {
|
|
3738
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
2273
3739
|
import { z } from "zod";
|
|
2274
3740
|
|
|
2275
3741
|
const ErrorResponseSchema = z.object({
|
|
@@ -2278,7 +3744,7 @@ const ErrorResponseSchema = z.object({
|
|
|
2278
3744
|
requestId: z.string().optional(),
|
|
2279
3745
|
});
|
|
2280
3746
|
|
|
2281
|
-
const ${names.pluralCamel} =
|
|
3747
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
2282
3748
|
.namespace("${names.pluralCamel}")
|
|
2283
3749
|
.responses({
|
|
2284
3750
|
500: ErrorResponseSchema,
|
|
@@ -2299,10 +3765,14 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
2299
3765
|
)
|
|
2300
3766
|
.responses({
|
|
2301
3767
|
200: z.object({
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
3768
|
+
items: z.array(${names.singularCamel}Schema),
|
|
3769
|
+
page: z.object({
|
|
3770
|
+
kind: z.literal("offset"),
|
|
3771
|
+
limit: z.number().int().min(1),
|
|
3772
|
+
offset: z.number().int().min(0),
|
|
3773
|
+
total: z.number().int().min(0),
|
|
3774
|
+
hasMore: z.boolean(),
|
|
3775
|
+
}),
|
|
2306
3776
|
}),
|
|
2307
3777
|
});
|
|
2308
3778
|
|
|
@@ -2310,7 +3780,8 @@ export const ${names.pluralCamel}Contracts = [list${names.pluralPascal}];
|
|
|
2310
3780
|
`;
|
|
2311
3781
|
}
|
|
2312
3782
|
function standaloneUseCaseFile(names, config, filePath) {
|
|
2313
|
-
return `import {
|
|
3783
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
3784
|
+
import { z } from "zod";
|
|
2314
3785
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2315
3786
|
|
|
2316
3787
|
export const ${names.action.pascal}InputSchema = z.object({});
|
|
@@ -2338,29 +3809,27 @@ function useCaseTestFile(names, config) {
|
|
|
2338
3809
|
return `import { describe, expect, it } from "bun:test";
|
|
2339
3810
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
2340
3811
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
2341
|
-
import {
|
|
3812
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
3813
|
+
import { createTestAnonymousActor } from "@beignet/core/ports/testing";
|
|
2342
3814
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2343
3815
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
2344
3816
|
import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
|
|
2345
3817
|
|
|
2346
3818
|
describe("${names.exportName}", () => {
|
|
2347
3819
|
it("runs ${names.action.camel}", async () => {
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
gate: testPorts.gate.bind({ actor, auth: null }),
|
|
2362
|
-
ports: testPorts,
|
|
2363
|
-
}));
|
|
3820
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
3821
|
+
base: appPorts,
|
|
3822
|
+
overrides: {
|
|
3823
|
+
gate: appPorts.gate,
|
|
3824
|
+
devtools: createInMemoryDevtools(),
|
|
3825
|
+
},
|
|
3826
|
+
});
|
|
3827
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
3828
|
+
ports: testFixture.ports,
|
|
3829
|
+
actor: createTestAnonymousActor(),
|
|
3830
|
+
tenant: null,
|
|
3831
|
+
});
|
|
3832
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
2364
3833
|
|
|
2365
3834
|
const result = await tester.run(${names.exportName}, {});
|
|
2366
3835
|
|
|
@@ -2421,6 +3890,134 @@ export const ${names.policyName} = definePolicy({
|
|
|
2421
3890
|
});
|
|
2422
3891
|
`;
|
|
2423
3892
|
}
|
|
3893
|
+
function resourcePolicyFile(names, _config, options) {
|
|
3894
|
+
return `import type { ActivityActor, ActivityTenant } from "@beignet/core/ports";
|
|
3895
|
+
import { definePolicy, deny } from "@beignet/core/ports";
|
|
3896
|
+
|
|
3897
|
+
export type AuthorizationContext = {
|
|
3898
|
+
actor: ActivityActor;
|
|
3899
|
+
tenant?: ActivityTenant;
|
|
3900
|
+
};
|
|
3901
|
+
|
|
3902
|
+
type ${names.singularPascal}PolicyResource = {
|
|
3903
|
+
id: string;
|
|
3904
|
+
tenantId?: string;
|
|
3905
|
+
};
|
|
3906
|
+
|
|
3907
|
+
function isAuthenticated(ctx: AuthorizationContext) {
|
|
3908
|
+
return ctx.actor.type === "user" && Boolean(ctx.actor.id);
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
function canWrite(ctx: AuthorizationContext) {
|
|
3912
|
+
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
3913
|
+
${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
export const ${names.singularCamel}Policy = definePolicy({
|
|
3917
|
+
"${names.pluralCamel}.view": (_ctx: AuthorizationContext) => true,
|
|
3918
|
+
"${names.pluralCamel}.create": (ctx: AuthorizationContext) =>
|
|
3919
|
+
canWrite(ctx),
|
|
3920
|
+
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3921
|
+
const decision = canWrite(ctx);
|
|
3922
|
+
if (decision !== true) return decision;
|
|
3923
|
+
${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
3924
|
+
return true;
|
|
3925
|
+
},
|
|
3926
|
+
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3927
|
+
const decision = canWrite(ctx);
|
|
3928
|
+
if (decision !== true) return decision;
|
|
3929
|
+
${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
3930
|
+
return true;
|
|
3931
|
+
},
|
|
3932
|
+
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
3933
|
+
deny("You are not allowed to manage this ${names.singularKebab}."),
|
|
3934
|
+
});
|
|
3935
|
+
`;
|
|
3936
|
+
}
|
|
3937
|
+
function resourceEventsFile(names) {
|
|
3938
|
+
return `import { defineEvent } from "@beignet/core/events";
|
|
3939
|
+
import { z } from "zod";
|
|
3940
|
+
|
|
3941
|
+
export const ${names.singularPascal}Created = defineEvent("${names.pluralCamel}.created", {
|
|
3942
|
+
payload: z.object({
|
|
3943
|
+
id: z.string().uuid(),
|
|
3944
|
+
}),
|
|
3945
|
+
});
|
|
3946
|
+
|
|
3947
|
+
export const ${names.singularPascal}Updated = defineEvent("${names.pluralCamel}.updated", {
|
|
3948
|
+
payload: z.object({
|
|
3949
|
+
id: z.string().uuid(),
|
|
3950
|
+
}),
|
|
3951
|
+
});
|
|
3952
|
+
|
|
3953
|
+
export const ${names.singularPascal}Deleted = defineEvent("${names.pluralCamel}.deleted", {
|
|
3954
|
+
payload: z.object({
|
|
3955
|
+
id: z.string().uuid(),
|
|
3956
|
+
}),
|
|
3957
|
+
});
|
|
3958
|
+
|
|
3959
|
+
export const ${names.singularCamel}Events = [
|
|
3960
|
+
${names.singularPascal}Created,
|
|
3961
|
+
${names.singularPascal}Updated,
|
|
3962
|
+
${names.singularPascal}Deleted,
|
|
3963
|
+
] as const;
|
|
3964
|
+
`;
|
|
3965
|
+
}
|
|
3966
|
+
function resourcePolicyTestFile(names, options) {
|
|
3967
|
+
return `import { describe, expect, it } from "bun:test";
|
|
3968
|
+
import { createPolicyTester } from "@beignet/core/ports/testing";
|
|
3969
|
+
import { ${names.singularCamel}Policy } from "../policy";
|
|
3970
|
+
import type { ${names.singularPascal} } from "../schemas";
|
|
3971
|
+
|
|
3972
|
+
function create${names.singularPascal}(): ${names.singularPascal} {
|
|
3973
|
+
const now = new Date().toISOString();
|
|
3974
|
+
return {
|
|
3975
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
3976
|
+
${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
3977
|
+
version: 1,
|
|
3978
|
+
createdAt: now,
|
|
3979
|
+
updatedAt: now,
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
describe("${names.singularCamel}Policy", () => {
|
|
3984
|
+
it("documents generated ${names.pluralKebab} authorization rules", async () => {
|
|
3985
|
+
const tester = createPolicyTester({
|
|
3986
|
+
policies: [${names.singularCamel}Policy],
|
|
3987
|
+
});
|
|
3988
|
+
const ${names.singularCamel} = create${names.singularPascal}();
|
|
3989
|
+
const ctx = {
|
|
3990
|
+
actor: { type: "user" as const, id: "user_test" },
|
|
3991
|
+
${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
3992
|
+
|
|
3993
|
+
await expect(
|
|
3994
|
+
tester.assertMatrix([
|
|
3995
|
+
{
|
|
3996
|
+
name: "authenticated actor can create ${names.singularKebab}",
|
|
3997
|
+
ctx,
|
|
3998
|
+
ability: "${names.pluralCamel}.create",
|
|
3999
|
+
expected: "allow",
|
|
4000
|
+
},
|
|
4001
|
+
{
|
|
4002
|
+
name: "authenticated actor can update ${names.singularKebab}",
|
|
4003
|
+
ctx,
|
|
4004
|
+
ability: "${names.pluralCamel}.update",
|
|
4005
|
+
subject: ${names.singularCamel},
|
|
4006
|
+
expected: "allow",
|
|
4007
|
+
},
|
|
4008
|
+
{
|
|
4009
|
+
name: "authenticated actor can delete ${names.singularKebab}",
|
|
4010
|
+
ctx,
|
|
4011
|
+
ability: "${names.pluralCamel}.delete",
|
|
4012
|
+
subject: ${names.singularCamel},
|
|
4013
|
+
expected: "allow",
|
|
4014
|
+
},
|
|
4015
|
+
]),
|
|
4016
|
+
).resolves.toBeUndefined();
|
|
4017
|
+
});
|
|
4018
|
+
});
|
|
4019
|
+
`;
|
|
4020
|
+
}
|
|
2424
4021
|
function eventFile(names) {
|
|
2425
4022
|
return `import { defineEvent } from "@beignet/core/events";
|
|
2426
4023
|
import { z } from "zod";
|
|
@@ -2437,11 +4034,9 @@ export const ${names.eventExportName} = defineEvent("${names.eventName}", {
|
|
|
2437
4034
|
`;
|
|
2438
4035
|
}
|
|
2439
4036
|
function jobFile(names, config) {
|
|
2440
|
-
return `import {
|
|
4037
|
+
return `import { retry } from "@beignet/core/jobs";
|
|
2441
4038
|
import { z } from "zod";
|
|
2442
|
-
import
|
|
2443
|
-
|
|
2444
|
-
const jobs = createJobHandlers<AppContext>();
|
|
4039
|
+
import { defineJob } from "${aliasModule(config.paths.jobsBuilder)}";
|
|
2445
4040
|
|
|
2446
4041
|
export const ${names.payloadSchemaName} = z.object({
|
|
2447
4042
|
\tid: z.string().uuid(),
|
|
@@ -2449,11 +4044,13 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
2449
4044
|
|
|
2450
4045
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2451
4046
|
|
|
2452
|
-
export const ${names.jobExportName} =
|
|
4047
|
+
export const ${names.jobExportName} = defineJob("${names.jobName}", {
|
|
2453
4048
|
\tpayload: ${names.payloadSchemaName},
|
|
2454
|
-
\tretry: {
|
|
4049
|
+
\tretry: retry.exponential({
|
|
2455
4050
|
\t\tattempts: 3,
|
|
2456
|
-
\t
|
|
4051
|
+
\t\tinitialDelay: "1s",
|
|
4052
|
+
\t\tmaxDelay: "1m",
|
|
4053
|
+
\t}),
|
|
2457
4054
|
\tasync handle({ payload, ctx }) {
|
|
2458
4055
|
\t\tctx.ports.logger.info("Job handled", {
|
|
2459
4056
|
\t\t\tjobName: "${names.jobName}",
|
|
@@ -2463,15 +4060,94 @@ export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
|
|
|
2463
4060
|
});
|
|
2464
4061
|
`;
|
|
2465
4062
|
}
|
|
4063
|
+
function taskFile(names, config) {
|
|
4064
|
+
return `import { z } from "zod";
|
|
4065
|
+
import { defineTask } from "${aliasModule(config.paths.tasksBuilder)}";
|
|
4066
|
+
|
|
4067
|
+
export const ${names.inputSchemaName} = z.object({
|
|
4068
|
+
\tdryRun: z.boolean().default(true),
|
|
4069
|
+
});
|
|
4070
|
+
|
|
4071
|
+
export type ${names.inputTypeName} = z.infer<typeof ${names.inputSchemaName}>;
|
|
4072
|
+
|
|
4073
|
+
export const ${names.taskExportName} = defineTask("${names.taskName}", {
|
|
4074
|
+
\tinput: ${names.inputSchemaName},
|
|
4075
|
+
\tdescription: "Operational ${names.feature.kebab} ${names.artifact.kebab} task.",
|
|
4076
|
+
\tasync handle({ input, ctx }) {
|
|
4077
|
+
\t\tctx.ports.logger.info("Task handled", {
|
|
4078
|
+
\t\t\ttaskName: "${names.taskName}",
|
|
4079
|
+
\t\t\tdryRun: input.dryRun,
|
|
4080
|
+
\t\t});
|
|
4081
|
+
|
|
4082
|
+
\t\treturn {
|
|
4083
|
+
\t\t\tdryRun: input.dryRun,
|
|
4084
|
+
\t\t};
|
|
4085
|
+
\t},
|
|
4086
|
+
});
|
|
4087
|
+
`;
|
|
4088
|
+
}
|
|
4089
|
+
function factoryFile(names, config) {
|
|
4090
|
+
const resource = featureResourceNames(names);
|
|
4091
|
+
return `import { createFactory } from "@beignet/core/testing";
|
|
4092
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4093
|
+
|
|
4094
|
+
export const ${names.factoryExportName} = createFactory("${names.factoryName}", {
|
|
4095
|
+
\tdefaults: ({ sequence }) => ({
|
|
4096
|
+
\t\tname: "${resource.singularPascal} " + sequence,
|
|
4097
|
+
\t}),
|
|
4098
|
+
\tpersist: (ctx: AppContext, input) => ctx.ports.${resource.pluralCamel}.create(input),
|
|
4099
|
+
});
|
|
4100
|
+
`;
|
|
4101
|
+
}
|
|
4102
|
+
function seedFile(names, config) {
|
|
4103
|
+
const resource = featureResourceNames(names);
|
|
4104
|
+
return `import { defineSeed } from "@beignet/core/testing";
|
|
4105
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4106
|
+
|
|
4107
|
+
export const ${names.seedExportName} = defineSeed("${names.seedName}", {
|
|
4108
|
+
\tdescription: "Creates starter ${names.feature.kebab} data.",
|
|
4109
|
+
\trun: async (ctx: AppContext) => {
|
|
4110
|
+
\t\tawait ctx.ports.${resource.pluralCamel}.create({
|
|
4111
|
+
\t\t\tname: "Demo ${resource.singularPascal}",
|
|
4112
|
+
\t\t});
|
|
4113
|
+
\t},
|
|
4114
|
+
});
|
|
4115
|
+
`;
|
|
4116
|
+
}
|
|
4117
|
+
function notificationFile(names, config) {
|
|
4118
|
+
return `import { defineMailNotificationChannel } from "@beignet/core/notifications";
|
|
4119
|
+
import { z } from "zod";
|
|
4120
|
+
import { defineNotification } from "${aliasModule(config.paths.notificationsBuilder)}";
|
|
4121
|
+
|
|
4122
|
+
export const ${names.payloadSchemaName} = z.object({
|
|
4123
|
+
\tid: z.string().uuid(),
|
|
4124
|
+
\tsubject: z.string(),
|
|
4125
|
+
\trecipientEmail: z.string().email(),
|
|
4126
|
+
});
|
|
4127
|
+
|
|
4128
|
+
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
4129
|
+
|
|
4130
|
+
export const ${names.notificationExportName} = defineNotification(
|
|
4131
|
+
\t"${names.notificationName}",
|
|
4132
|
+
\t{
|
|
4133
|
+
\t\tpayload: ${names.payloadSchemaName},
|
|
4134
|
+
\t\tchannels: {
|
|
4135
|
+
\t\t\temail: defineMailNotificationChannel(({ payload }) => ({
|
|
4136
|
+
\t\t\t\tto: payload.recipientEmail,
|
|
4137
|
+
\t\t\t\tsubject: payload.subject,
|
|
4138
|
+
\t\t\t\ttext: \`${names.notificationName} notification for \${payload.id}.\`,
|
|
4139
|
+
\t\t\t})),
|
|
4140
|
+
\t\t},
|
|
4141
|
+
\t},
|
|
4142
|
+
);
|
|
4143
|
+
`;
|
|
4144
|
+
}
|
|
2466
4145
|
function listenerFile(names, config) {
|
|
2467
4146
|
const filePath = listenerFilePath(names, config);
|
|
2468
|
-
return `import {
|
|
2469
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4147
|
+
return `import { defineListener } from "${aliasModule(config.paths.listenersBuilder)}";
|
|
2470
4148
|
import { ${names.event.eventExportName} } from "${relativeModule(filePath, eventFilePath(names.event, config))}";
|
|
2471
4149
|
|
|
2472
|
-
const
|
|
2473
|
-
|
|
2474
|
-
export const ${names.listenerExportName} = events.defineListener(${names.event.eventExportName}, {
|
|
4150
|
+
export const ${names.listenerExportName} = defineListener(${names.event.eventExportName}, {
|
|
2475
4151
|
\tname: "${names.listenerName}",
|
|
2476
4152
|
\tasync handle({ payload, ctx }) {
|
|
2477
4153
|
\t\tctx.ports.logger.info("Listener handled", {
|
|
@@ -2483,11 +4159,8 @@ export const ${names.listenerExportName} = events.defineListener(${names.event.e
|
|
|
2483
4159
|
`;
|
|
2484
4160
|
}
|
|
2485
4161
|
function scheduleFile(names, config) {
|
|
2486
|
-
return `import {
|
|
2487
|
-
import {
|
|
2488
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2489
|
-
|
|
2490
|
-
const schedules = createScheduleHandlers<AppContext>();
|
|
4162
|
+
return `import { z } from "zod";
|
|
4163
|
+
import { defineSchedule } from "${aliasModule(config.paths.schedulesBuilder)}";
|
|
2491
4164
|
|
|
2492
4165
|
export const ${names.payloadSchemaName} = z.object({
|
|
2493
4166
|
\tdate: z.string(),
|
|
@@ -2495,7 +4168,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
2495
4168
|
|
|
2496
4169
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2497
4170
|
|
|
2498
|
-
export const ${names.scheduleExportName} =
|
|
4171
|
+
export const ${names.scheduleExportName} = defineSchedule(
|
|
2499
4172
|
\t"${names.scheduleName}",
|
|
2500
4173
|
\t{
|
|
2501
4174
|
\t\tcron: "${names.cron}",
|
|
@@ -2517,108 +4190,164 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
2517
4190
|
);
|
|
2518
4191
|
`;
|
|
2519
4192
|
}
|
|
2520
|
-
function
|
|
2521
|
-
return `import {
|
|
4193
|
+
function uploadFile(names, config) {
|
|
4194
|
+
return `import { defineUpload } from "@beignet/core/uploads";
|
|
4195
|
+
import { z } from "zod";
|
|
2522
4196
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2523
|
-
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
2524
|
-
import { env } from "@/lib/env";
|
|
2525
|
-
import { server } from "${aliasModule(config.paths.server)}";
|
|
2526
4197
|
|
|
2527
|
-
|
|
2528
|
-
\
|
|
4198
|
+
export const ${names.metadataSchemaName} = z.object({
|
|
4199
|
+
\tresourceId: z.string().min(1),
|
|
4200
|
+
});
|
|
2529
4201
|
|
|
2530
|
-
|
|
2531
|
-
\t\treturn Response.json(
|
|
2532
|
-
\t\t\t{
|
|
2533
|
-
\t\t\t\tok: false,
|
|
2534
|
-
\t\t\t\terror: "CRON_SECRET is not configured.",
|
|
2535
|
-
\t\t\t\tscheduleName: ${names.scheduleExportName}.name,
|
|
2536
|
-
\t\t\t},
|
|
2537
|
-
\t\t\t{ status: 500 },
|
|
2538
|
-
\t\t);
|
|
2539
|
-
\t}
|
|
2540
|
-
|
|
2541
|
-
\tif (
|
|
2542
|
-
\t\trequest.headers.get("authorization") !== \`Bearer \${cronSecret}\`
|
|
2543
|
-
\t) {
|
|
2544
|
-
\t\treturn Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2545
|
-
\t}
|
|
2546
|
-
|
|
2547
|
-
\tconst ctx = await server.createContextFromNext();
|
|
2548
|
-
\tconst runner = createInlineScheduleRunner<AppContext>({
|
|
2549
|
-
\t\tctx,
|
|
2550
|
-
\t\tonStart({ run, schedule }) {
|
|
2551
|
-
\t\t\tctx.ports.devtools.record({
|
|
2552
|
-
\t\t\t\ttype: "schedule",
|
|
2553
|
-
\t\t\t\twatcher: "schedules",
|
|
2554
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
2555
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
2556
|
-
\t\t\t\tstatus: "started",
|
|
2557
|
-
\t\t\t\tcron: schedule.cron,
|
|
2558
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
2559
|
-
\t\t\t\tdetails: {
|
|
2560
|
-
\t\t\t\t\tsource: run.source,
|
|
2561
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
2562
|
-
\t\t\t\t},
|
|
2563
|
-
\t\t\t});
|
|
2564
|
-
\t\t},
|
|
2565
|
-
\t\tonSuccess({ run, schedule }) {
|
|
2566
|
-
\t\t\tctx.ports.devtools.record({
|
|
2567
|
-
\t\t\t\ttype: "schedule",
|
|
2568
|
-
\t\t\t\twatcher: "schedules",
|
|
2569
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
2570
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
2571
|
-
\t\t\t\tstatus: "completed",
|
|
2572
|
-
\t\t\t\tcron: schedule.cron,
|
|
2573
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
2574
|
-
\t\t\t\tdetails: {
|
|
2575
|
-
\t\t\t\t\tsource: run.source,
|
|
2576
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
2577
|
-
\t\t\t\t},
|
|
2578
|
-
\t\t\t});
|
|
2579
|
-
\t\t},
|
|
2580
|
-
\t\tonError({ error, run, schedule }) {
|
|
2581
|
-
\t\t\tctx.ports.devtools.record({
|
|
2582
|
-
\t\t\t\ttype: "schedule",
|
|
2583
|
-
\t\t\t\twatcher: "schedules",
|
|
2584
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
2585
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
2586
|
-
\t\t\t\tstatus: "failed",
|
|
2587
|
-
\t\t\t\tcron: schedule.cron,
|
|
2588
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
2589
|
-
\t\t\t\tdetails: {
|
|
2590
|
-
\t\t\t\t\terror,
|
|
2591
|
-
\t\t\t\t\tsource: run.source,
|
|
2592
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
2593
|
-
\t\t\t\t},
|
|
2594
|
-
\t\t\t});
|
|
2595
|
-
\t\t\tctx.ports.logger.error("Schedule failed", {
|
|
2596
|
-
\t\t\t\terror,
|
|
2597
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
2598
|
-
\t\t\t});
|
|
2599
|
-
\t\t},
|
|
2600
|
-
\t\tonHookError({ error, hook, schedule }) {
|
|
2601
|
-
\t\t\tctx.ports.logger.warn("Schedule lifecycle hook failed", {
|
|
2602
|
-
\t\t\t\terror,
|
|
2603
|
-
\t\t\t\thook,
|
|
2604
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
2605
|
-
\t\t\t});
|
|
2606
|
-
\t\t},
|
|
2607
|
-
\t});
|
|
4202
|
+
export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaName}>;
|
|
2608
4203
|
|
|
2609
|
-
|
|
2610
|
-
\t
|
|
2611
|
-
\
|
|
4204
|
+
export const ${names.uploadExportName} = defineUpload<
|
|
4205
|
+
\t"${names.uploadName}",
|
|
4206
|
+
\ttypeof ${names.metadataSchemaName},
|
|
4207
|
+
\tAppContext,
|
|
4208
|
+
\t{ objectKeys: string[] }
|
|
4209
|
+
>("${names.uploadName}", {
|
|
4210
|
+
\tmetadata: ${names.metadataSchemaName},
|
|
4211
|
+
\tfile: {
|
|
4212
|
+
\t\tcontentTypes: ["application/pdf", "text/plain"],
|
|
4213
|
+
\t\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
4214
|
+
\t\tmaxFiles: 1,
|
|
4215
|
+
\t\tvisibility: "private",
|
|
4216
|
+
\t\tcacheControl: "private, max-age=0",
|
|
4217
|
+
\t},
|
|
4218
|
+
\tauthorize({ ctx }) {
|
|
4219
|
+
\t\treturn ctx.actor.type === "user";
|
|
4220
|
+
\t},
|
|
4221
|
+
\tkey({ ctx, metadata, uploadId, file }) {
|
|
4222
|
+
\t\tconst tenantId = ctx.tenant?.id ?? "default";
|
|
4223
|
+
\t\tconst extension = file.name.includes(".") ? file.name.split(".").pop() : undefined;
|
|
4224
|
+
\t\tconst suffix = extension ? \`.\${extension}\` : "";
|
|
4225
|
+
\t\treturn "${names.feature.kebab}/" + tenantId + "/" + metadata.resourceId + "/" + uploadId + suffix;
|
|
4226
|
+
\t},
|
|
4227
|
+
\tstorageMetadata({ ctx, metadata }) {
|
|
4228
|
+
\t\treturn {
|
|
4229
|
+
\t\t\ttenantId: ctx.tenant?.id ?? "default",
|
|
4230
|
+
\t\t\tresourceId: metadata.resourceId,
|
|
4231
|
+
\t\t};
|
|
4232
|
+
\t},
|
|
4233
|
+
\tasync onComplete({ ctx, metadata, files }) {
|
|
4234
|
+
\t\tctx.ports.logger.info("Upload completed", {
|
|
4235
|
+
\t\t\tuploadName: "${names.uploadName}",
|
|
4236
|
+
\t\t\tresourceId: metadata.resourceId,
|
|
4237
|
+
\t\t\tobjectKeys: files.map((file) => file.key),
|
|
2612
4238
|
\t\t});
|
|
2613
|
-
\t} catch {
|
|
2614
|
-
\t\treturn Response.json({ error: "Schedule failed" }, { status: 500 });
|
|
2615
|
-
\t}
|
|
2616
4239
|
|
|
2617
|
-
\treturn
|
|
4240
|
+
\t\treturn {
|
|
4241
|
+
\t\t\tobjectKeys: files.map((file) => file.key),
|
|
4242
|
+
\t\t};
|
|
4243
|
+
\t},
|
|
4244
|
+
});
|
|
4245
|
+
`;
|
|
2618
4246
|
}
|
|
4247
|
+
function featureUiComponentFile(names, config) {
|
|
4248
|
+
return `"use client";
|
|
4249
|
+
|
|
4250
|
+
import { contractErrorMessage } from "@beignet/core/client";
|
|
4251
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4252
|
+
import { useState } from "react";
|
|
4253
|
+
import { rq } from "${aliasModule(clientIndexPath(config))}";
|
|
4254
|
+
import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
4255
|
+
|
|
4256
|
+
export function ${names.componentExportName}() {
|
|
4257
|
+
\tconst [name, setName] = useState("");
|
|
4258
|
+
\tconst queryClient = useQueryClient();
|
|
4259
|
+
\tconst ${names.pluralCamel}Query = useQuery(
|
|
4260
|
+
\t\trq(list${names.pluralPascal}).queryOptions({ query: {} }),
|
|
4261
|
+
\t);
|
|
4262
|
+
\tconst create${names.singularPascal}Mutation = useMutation(
|
|
4263
|
+
\t\trq(create${names.singularPascal}).mutationOptions({
|
|
4264
|
+
\t\t\tonSuccess: async () => {
|
|
4265
|
+
\t\t\t\tsetName("");
|
|
4266
|
+
\t\t\t\tawait rq(list${names.pluralPascal}).invalidate(queryClient);
|
|
4267
|
+
\t\t\t},
|
|
4268
|
+
\t\t}),
|
|
4269
|
+
\t);
|
|
2619
4270
|
|
|
2620
|
-
|
|
2621
|
-
|
|
4271
|
+
\treturn (
|
|
4272
|
+
\t\t<section className="${names.pluralKebab}-panel">
|
|
4273
|
+
\t\t\t<form
|
|
4274
|
+
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
4275
|
+
\t\t\t\tonSubmit={(event) => {
|
|
4276
|
+
\t\t\t\t\tevent.preventDefault();
|
|
4277
|
+
\t\t\t\t\tconst trimmedName = name.trim();
|
|
4278
|
+
\t\t\t\t\tif (!trimmedName) return;
|
|
4279
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
4280
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
4281
|
+
\t\t\t\t\t\tbody: { name: trimmedName },
|
|
4282
|
+
\t\t\t\t\t});
|
|
4283
|
+
\t\t\t\t}}
|
|
4284
|
+
\t\t\t>
|
|
4285
|
+
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
4286
|
+
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
4287
|
+
\t\t\t\t\t<input
|
|
4288
|
+
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
4289
|
+
\t\t\t\t\t\tvalue={name}
|
|
4290
|
+
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
4291
|
+
\t\t\t\t\t\tplaceholder="Name"
|
|
4292
|
+
\t\t\t\t\t/>
|
|
4293
|
+
\t\t\t\t\t<button
|
|
4294
|
+
\t\t\t\t\t\ttype="submit"
|
|
4295
|
+
\t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
|
|
4296
|
+
\t\t\t\t\t>
|
|
4297
|
+
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
4298
|
+
\t\t\t\t\t</button>
|
|
4299
|
+
\t\t\t\t</div>
|
|
4300
|
+
\t\t\t\t{create${names.singularPascal}Mutation.isError ? (
|
|
4301
|
+
\t\t\t\t\t<p role="alert">
|
|
4302
|
+
\t\t\t\t\t\t{contractErrorMessage(
|
|
4303
|
+
\t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
|
|
4304
|
+
\t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
|
|
4305
|
+
\t\t\t\t\t\t)}
|
|
4306
|
+
\t\t\t\t\t</p>
|
|
4307
|
+
\t\t\t\t) : null}
|
|
4308
|
+
\t\t\t</form>
|
|
4309
|
+
|
|
4310
|
+
\t\t\t<div className="${names.pluralKebab}-list">
|
|
4311
|
+
\t\t\t\t<div className="${names.pluralKebab}-list-heading">
|
|
4312
|
+
\t\t\t\t\t<h2>${names.pluralPascal}</h2>
|
|
4313
|
+
\t\t\t\t\t<span>{${names.pluralCamel}Query.data?.items.length ?? 0} shown</span>
|
|
4314
|
+
\t\t\t\t</div>
|
|
4315
|
+
\t\t\t\t{${names.pluralCamel}Query.isLoading ? <p>Loading...</p> : null}
|
|
4316
|
+
\t\t\t\t{${names.pluralCamel}Query.isError ? (
|
|
4317
|
+
\t\t\t\t\t<p role="alert">Could not load ${names.pluralKebab}.</p>
|
|
4318
|
+
\t\t\t\t) : null}
|
|
4319
|
+
\t\t\t\t{${names.pluralCamel}Query.data?.items.length === 0 ? (
|
|
4320
|
+
\t\t\t\t\t<p>No ${names.pluralKebab} yet.</p>
|
|
4321
|
+
\t\t\t\t) : null}
|
|
4322
|
+
\t\t\t\t<ul>
|
|
4323
|
+
\t\t\t\t\t{${names.pluralCamel}Query.data?.items.map((${names.singularCamel}) => (
|
|
4324
|
+
\t\t\t\t\t\t<li key={${names.singularCamel}.id}>
|
|
4325
|
+
\t\t\t\t\t\t\t<strong>{${names.singularCamel}.name}</strong>
|
|
4326
|
+
\t\t\t\t\t\t\t<small>{new Date(${names.singularCamel}.createdAt).toLocaleDateString()}</small>
|
|
4327
|
+
\t\t\t\t\t\t</li>
|
|
4328
|
+
\t\t\t\t\t))}
|
|
4329
|
+
\t\t\t\t</ul>
|
|
4330
|
+
\t\t\t</div>
|
|
4331
|
+
\t\t</section>
|
|
4332
|
+
\t);
|
|
4333
|
+
}
|
|
4334
|
+
`;
|
|
4335
|
+
}
|
|
4336
|
+
function scheduleRouteFile(names, config) {
|
|
4337
|
+
return `import { createScheduleRoute } from "@beignet/next";
|
|
4338
|
+
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
4339
|
+
import { env } from "@/lib/env";
|
|
4340
|
+
import { server } from "${aliasModule(config.paths.server)}";
|
|
4341
|
+
|
|
4342
|
+
export const runtime = "nodejs";
|
|
4343
|
+
|
|
4344
|
+
export const { GET, POST } = createScheduleRoute({
|
|
4345
|
+
\tserver,
|
|
4346
|
+
\tschedules: [${names.scheduleExportName}],
|
|
4347
|
+
\tschedule: ${names.scheduleExportName}.name,
|
|
4348
|
+
\tsecret: env.CRON_SECRET,
|
|
4349
|
+
\tsource: "cron-route",
|
|
4350
|
+
});
|
|
2622
4351
|
`;
|
|
2623
4352
|
}
|
|
2624
4353
|
function useCaseIndexExport(names) {
|
|
@@ -2631,73 +4360,138 @@ function useCaseIndexExport(names) {
|
|
|
2631
4360
|
} from "./${names.action.kebab}";
|
|
2632
4361
|
`;
|
|
2633
4362
|
}
|
|
2634
|
-
function routeGroupFile(names, config) {
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
4363
|
+
function routeGroupFile(names, config, mode, _options) {
|
|
4364
|
+
const routeFilePath = path.join(config.paths.features, names.pluralKebab, "routes.ts");
|
|
4365
|
+
const contractsPath = resourceContractFilePath(names, config);
|
|
4366
|
+
const useCasesPath = resourceUseCaseIndexPath(names, config);
|
|
4367
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
4368
|
+
import { defineRouteGroup } from "@beignet/next";
|
|
4369
|
+
import type { AppContext } from "${relativeModule(routeFilePath, config.paths.appContext)}";
|
|
4370
|
+
import {
|
|
4371
|
+
create${names.singularPascal},
|
|
4372
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
4373
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${relativeModule(routeFilePath, contractsPath)}";
|
|
2638
4374
|
import {
|
|
2639
4375
|
create${names.singularPascal}UseCase,
|
|
2640
|
-
list${names.pluralPascal}UseCase,
|
|
2641
|
-
|
|
4376
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
4377
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${relativeModule(routeFilePath, useCasesPath)}";
|
|
2642
4378
|
|
|
2643
4379
|
export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>({
|
|
2644
4380
|
name: "${names.pluralKebab}",
|
|
2645
4381
|
routes: [
|
|
2646
|
-
{
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
status: 200,
|
|
2650
|
-
body: await list${names.pluralPascal}UseCase.run({ ctx, input: query }),
|
|
2651
|
-
}),
|
|
2652
|
-
},
|
|
2653
|
-
{
|
|
2654
|
-
contract: create${names.singularPascal},
|
|
2655
|
-
handle: async ({ ctx, body }) => ({
|
|
2656
|
-
status: 201,
|
|
2657
|
-
body: await create${names.singularPascal}UseCase.run({ ctx, input: body }),
|
|
2658
|
-
}),
|
|
2659
|
-
},
|
|
2660
|
-
],
|
|
4382
|
+
{ contract: list${names.pluralPascal}, useCase: list${names.pluralPascal}UseCase },
|
|
4383
|
+
{ contract: create${names.singularPascal}, useCase: create${names.singularPascal}UseCase },
|
|
4384
|
+
${mode === "resource" ? ` { contract: get${names.singularPascal}, useCase: get${names.singularPascal}UseCase },\n { contract: update${names.singularPascal}, useCase: update${names.singularPascal}UseCase },\n { contract: delete${names.singularPascal}, useCase: delete${names.singularPascal}UseCase },\n` : ""} ],
|
|
2661
4385
|
});
|
|
2662
4386
|
`;
|
|
2663
4387
|
}
|
|
2664
|
-
function testFile(names, config) {
|
|
4388
|
+
function testFile(names, config, mode, options) {
|
|
2665
4389
|
const repositoryPath = path.join(path.dirname(config.paths.infrastructurePorts), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
|
|
4390
|
+
const serverContextPath = path.join(path.dirname(config.paths.server), "context.ts");
|
|
4391
|
+
const requestTarget = options.tenant ? "requester" : "app";
|
|
2666
4392
|
return `import { describe, expect, it } from "bun:test";
|
|
2667
4393
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
4394
|
+
${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` : ""}import { defineRoutes } from "@beignet/web";
|
|
4395
|
+
import { createTestApp${options.tenant ? ", createTestRequester" : ""} } from "@beignet/web/testing";
|
|
2668
4396
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
2669
|
-
import {
|
|
4397
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
4398
|
+
import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/ports/testing";
|
|
2670
4399
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2671
4400
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
4401
|
+
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
4402
|
+
import {
|
|
4403
|
+
create${names.singularPascal},
|
|
4404
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
4405
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
2672
4406
|
import { createInMemory${names.singularPascal}Repository } from "${aliasModule(repositoryPath)}";
|
|
4407
|
+
import { ${names.singularCamel}Routes } from "${aliasModule(path.join(resourceFeatureDir(names, config), "routes.ts"))}";
|
|
2673
4408
|
import {
|
|
2674
4409
|
create${names.singularPascal}UseCase,
|
|
2675
|
-
list${names.pluralPascal}UseCase,
|
|
2676
|
-
} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
4410
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
4411
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
2677
4412
|
|
|
2678
4413
|
describe("${names.pluralCamel} resource", () => {
|
|
2679
|
-
it("creates and lists ${names.pluralCamel}", async () => {
|
|
2680
|
-
const ${names.
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
4414
|
+
it("${mode === "resource" ? `exercises ${names.pluralCamel} CRUD` : `creates and lists ${names.pluralCamel}`}", async () => {
|
|
4415
|
+
const seed${names.pluralPascal} = [
|
|
4416
|
+
{
|
|
4417
|
+
id: "00000000-0000-4000-8000-000000000101",
|
|
4418
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
4419
|
+
version: 1,
|
|
4420
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
4421
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
4422
|
+
},
|
|
4423
|
+
{
|
|
4424
|
+
id: "00000000-0000-4000-8000-000000000102",
|
|
4425
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
4426
|
+
version: 1,
|
|
4427
|
+
createdAt: "2024-01-02T00:00:00.000Z",
|
|
4428
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
4429
|
+
},
|
|
4430
|
+
{
|
|
4431
|
+
id: "00000000-0000-4000-8000-000000000103",
|
|
4432
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
4433
|
+
version: 1,
|
|
4434
|
+
createdAt: "2024-01-03T00:00:00.000Z",
|
|
4435
|
+
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
4436
|
+
},
|
|
4437
|
+
${options.tenant ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
|
|
4438
|
+
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
4439
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
4440
|
+
base: appPorts,
|
|
4441
|
+
overrides: {
|
|
2686
4442
|
${names.pluralCamel},
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
4443
|
+
${options.auth
|
|
4444
|
+
? `\t\t\t\tauth: {
|
|
4445
|
+
getSession: async () => ({
|
|
4446
|
+
user: { id: "user_test", name: "Test User" },
|
|
4447
|
+
}),
|
|
4448
|
+
},
|
|
4449
|
+
gate: appPorts.gate,\n`
|
|
4450
|
+
: `\t\t\t\tauth: {
|
|
4451
|
+
getSession: async () => null,
|
|
4452
|
+
},\n`} devtools: createInMemoryDevtools(),
|
|
4453
|
+
},
|
|
4454
|
+
transaction: {
|
|
4455
|
+
ports: (ports) => ({
|
|
4456
|
+
...ports,
|
|
4457
|
+
${names.pluralCamel},
|
|
4458
|
+
}),
|
|
4459
|
+
},
|
|
4460
|
+
});
|
|
4461
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
4462
|
+
ports: testFixture.ports,
|
|
4463
|
+
actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
4464
|
+
tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
|
|
4465
|
+
});
|
|
4466
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
2699
4467
|
|
|
2700
4468
|
const ctx = await tester.ctx();
|
|
4469
|
+
const defaultPage = await tester.run(list${names.pluralPascal}UseCase, {}, { ctx });
|
|
4470
|
+
const filteredPage = await tester.run(
|
|
4471
|
+
list${names.pluralPascal}UseCase,
|
|
4472
|
+
{ name: "alp" },
|
|
4473
|
+
{ ctx },
|
|
4474
|
+
);
|
|
4475
|
+
const firstPage = await tester.run(
|
|
4476
|
+
list${names.pluralPascal}UseCase,
|
|
4477
|
+
{ limit: 2 },
|
|
4478
|
+
{ ctx },
|
|
4479
|
+
);
|
|
4480
|
+
const secondPage = await tester.run(
|
|
4481
|
+
list${names.pluralPascal}UseCase,
|
|
4482
|
+
{ limit: 2, cursor: firstPage.page.nextCursor },
|
|
4483
|
+
{ ctx },
|
|
4484
|
+
);
|
|
4485
|
+
const nameAscendingPage = await tester.run(
|
|
4486
|
+
list${names.pluralPascal}UseCase,
|
|
4487
|
+
{ sortBy: "name", sortDirection: "asc" },
|
|
4488
|
+
{ ctx },
|
|
4489
|
+
);
|
|
4490
|
+
const nameDescendingPage = await tester.run(
|
|
4491
|
+
list${names.pluralPascal}UseCase,
|
|
4492
|
+
{ sortBy: "name", sortDirection: "desc" },
|
|
4493
|
+
{ ctx },
|
|
4494
|
+
);
|
|
2701
4495
|
const created = await tester.run(
|
|
2702
4496
|
create${names.singularPascal}UseCase,
|
|
2703
4497
|
{ name: "First ${names.singularPascal}" },
|
|
@@ -2705,13 +4499,111 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
2705
4499
|
);
|
|
2706
4500
|
const result = await tester.run(
|
|
2707
4501
|
list${names.pluralPascal}UseCase,
|
|
2708
|
-
{
|
|
4502
|
+
{ name: "First" },
|
|
2709
4503
|
{ ctx },
|
|
2710
4504
|
);
|
|
2711
4505
|
|
|
4506
|
+
expect(defaultPage.page).toMatchObject({
|
|
4507
|
+
kind: "cursor",
|
|
4508
|
+
limit: 20,
|
|
4509
|
+
cursor: null,
|
|
4510
|
+
nextCursor: null,
|
|
4511
|
+
hasMore: false,
|
|
4512
|
+
});
|
|
4513
|
+
expect(defaultPage.items.map((item) => item.name)).toEqual([
|
|
4514
|
+
"Gamma ${names.singularPascal}",
|
|
4515
|
+
"Beta ${names.singularPascal}",
|
|
4516
|
+
"Alpha ${names.singularPascal}",
|
|
4517
|
+
]);
|
|
4518
|
+
expect(filteredPage.items.map((item) => item.name)).toEqual([
|
|
4519
|
+
"Alpha ${names.singularPascal}",
|
|
4520
|
+
]);
|
|
4521
|
+
expect(firstPage.items.map((item) => item.name)).toEqual([
|
|
4522
|
+
"Gamma ${names.singularPascal}",
|
|
4523
|
+
"Beta ${names.singularPascal}",
|
|
4524
|
+
]);
|
|
4525
|
+
expect(firstPage.page.nextCursor).toEqual(expect.any(String));
|
|
4526
|
+
expect(secondPage.items.map((item) => item.name)).toEqual([
|
|
4527
|
+
"Alpha ${names.singularPascal}",
|
|
4528
|
+
]);
|
|
4529
|
+
expect(secondPage.page.nextCursor).toBeNull();
|
|
4530
|
+
expect(nameAscendingPage.items.map((item) => item.name)).toEqual([
|
|
4531
|
+
"Alpha ${names.singularPascal}",
|
|
4532
|
+
"Beta ${names.singularPascal}",
|
|
4533
|
+
"Gamma ${names.singularPascal}",
|
|
4534
|
+
]);
|
|
4535
|
+
expect(nameDescendingPage.items.map((item) => item.name)).toEqual([
|
|
4536
|
+
"Gamma ${names.singularPascal}",
|
|
4537
|
+
"Beta ${names.singularPascal}",
|
|
4538
|
+
"Alpha ${names.singularPascal}",
|
|
4539
|
+
]);
|
|
2712
4540
|
expect(created.name).toBe("First ${names.singularPascal}");
|
|
2713
|
-
expect(result.
|
|
2714
|
-
|
|
4541
|
+
expect(result.items).toEqual([created]);
|
|
4542
|
+
${mode === "resource"
|
|
4543
|
+
? `
|
|
4544
|
+
const fetched = await tester.run(
|
|
4545
|
+
get${names.singularPascal}UseCase,
|
|
4546
|
+
{ id: created.id },
|
|
4547
|
+
{ ctx },
|
|
4548
|
+
);
|
|
4549
|
+
const updated = await tester.run(
|
|
4550
|
+
update${names.singularPascal}UseCase,
|
|
4551
|
+
{ id: created.id, name: "Updated ${names.singularPascal}", version: fetched.version },
|
|
4552
|
+
{ ctx },
|
|
4553
|
+
);
|
|
4554
|
+
try {
|
|
4555
|
+
await tester.run(
|
|
4556
|
+
update${names.singularPascal}UseCase,
|
|
4557
|
+
{ id: created.id, name: "Stale ${names.singularPascal}", version: fetched.version },
|
|
4558
|
+
{ ctx },
|
|
4559
|
+
);
|
|
4560
|
+
throw new Error("Expected ${names.singularPascal}Conflict");
|
|
4561
|
+
} catch (error) {
|
|
4562
|
+
expect(isAppError(error)).toBe(true);
|
|
4563
|
+
if (isAppError(error)) {
|
|
4564
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_CONFLICT");
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
await tester.run(delete${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
4568
|
+
const afterDelete = await tester.run(
|
|
4569
|
+
list${names.pluralPascal}UseCase,
|
|
4570
|
+
{ name: "Updated" },
|
|
4571
|
+
{ ctx },
|
|
4572
|
+
);
|
|
4573
|
+
|
|
4574
|
+
expect(fetched).toEqual(created);
|
|
4575
|
+
expect(updated.name).toBe("Updated ${names.singularPascal}");
|
|
4576
|
+
expect(updated.version).toBe(fetched.version + 1);
|
|
4577
|
+
expect(afterDelete.items).toEqual([]);
|
|
4578
|
+
|
|
4579
|
+
try {
|
|
4580
|
+
await tester.run(get${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
4581
|
+
throw new Error("Expected ${names.singularPascal}NotFound");
|
|
4582
|
+
} catch (error) {
|
|
4583
|
+
expect(isAppError(error)).toBe(true);
|
|
4584
|
+
if (isAppError(error)) {
|
|
4585
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_NOT_FOUND");
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
`
|
|
4589
|
+
: ""}
|
|
4590
|
+
|
|
4591
|
+
const app = await createTestApp({
|
|
4592
|
+
ports: testFixture.ports,
|
|
4593
|
+
routes: defineRoutes<AppContext>([${names.singularCamel}Routes]),
|
|
4594
|
+
context: appContext,
|
|
4595
|
+
});
|
|
4596
|
+
${options.tenant ? `\t\tconst requester = createTestRequester(app, {\n\t\t\theaders: { "x-tenant-id": "tenant_test" },\n\t\t});\n` : ""}
|
|
4597
|
+
const createdViaRoute = await ${requestTarget}.request(create${names.singularPascal}, {
|
|
4598
|
+
body: { name: "Second ${names.singularPascal}" },
|
|
4599
|
+
});
|
|
4600
|
+
${mode === "resource" ? ` const fetchedViaRoute = await ${requestTarget}.request(get${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n });\n const updatedViaRoute = await ${requestTarget}.request(update${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n body: { name: "Updated via route", version: fetchedViaRoute.version },\n });\n` : ""} const listedViaRoute = await ${requestTarget}.request(list${names.pluralPascal}, {
|
|
4601
|
+
query: { name: "${mode === "resource" ? "Updated via route" : "Second"}" },
|
|
4602
|
+
});
|
|
4603
|
+
${mode === "resource" ? ` const deletedViaRoute = await ${requestTarget}.request(delete${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n });\n const listedAfterDeleteViaRoute = await ${requestTarget}.request(list${names.pluralPascal}, {\n query: { name: "Updated via route" },\n });\n` : ""} await app.stop();
|
|
4604
|
+
|
|
4605
|
+
expect(createdViaRoute.name).toBe("Second ${names.singularPascal}");
|
|
4606
|
+
${mode === "resource" ? ` expect(fetchedViaRoute).toEqual(createdViaRoute);\n expect(updatedViaRoute.name).toBe("Updated via route");\n expect(updatedViaRoute.version).toBe(fetchedViaRoute.version + 1);\n expect(deletedViaRoute).toBeUndefined();\n expect(listedViaRoute.items).toContainEqual(updatedViaRoute);\n expect(listedAfterDeleteViaRoute.items).toEqual([]);\n` : ` expect(listedViaRoute.items).toContainEqual(createdViaRoute);\n`}
|
|
2715
4607
|
});
|
|
2716
4608
|
});
|
|
2717
4609
|
`;
|