@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/src/make.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import type { MakeFeatureAddon } from "./choices.js";
|
|
3
4
|
import {
|
|
4
5
|
type BeignetConfig,
|
|
5
6
|
directoryPath,
|
|
@@ -15,6 +16,26 @@ type MakeResourceOptions = {
|
|
|
15
16
|
force?: boolean;
|
|
16
17
|
dryRun?: boolean;
|
|
17
18
|
config?: BeignetConfig;
|
|
19
|
+
auth?: boolean;
|
|
20
|
+
tenant?: boolean;
|
|
21
|
+
events?: boolean;
|
|
22
|
+
softDelete?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type { MakeFeatureAddon } from "./choices.js";
|
|
26
|
+
export { makeFeatureAddonChoices } from "./choices.js";
|
|
27
|
+
|
|
28
|
+
type NormalizedMakeFeatureAddon =
|
|
29
|
+
| "policy"
|
|
30
|
+
| "task"
|
|
31
|
+
| "event"
|
|
32
|
+
| "job"
|
|
33
|
+
| "notification"
|
|
34
|
+
| "ui"
|
|
35
|
+
| "upload";
|
|
36
|
+
|
|
37
|
+
type MakeFeatureOptions = MakeResourceOptions & {
|
|
38
|
+
with?: readonly MakeFeatureAddon[];
|
|
18
39
|
};
|
|
19
40
|
|
|
20
41
|
type MakeContractOptions = {
|
|
@@ -33,6 +54,14 @@ type MakeUseCaseOptions = {
|
|
|
33
54
|
config?: BeignetConfig;
|
|
34
55
|
};
|
|
35
56
|
|
|
57
|
+
type MakeTaskOptions = {
|
|
58
|
+
name: string;
|
|
59
|
+
cwd?: string;
|
|
60
|
+
force?: boolean;
|
|
61
|
+
dryRun?: boolean;
|
|
62
|
+
config?: BeignetConfig;
|
|
63
|
+
};
|
|
64
|
+
|
|
36
65
|
type MakeTestOptions = {
|
|
37
66
|
name: string;
|
|
38
67
|
cwd?: string;
|
|
@@ -81,6 +110,30 @@ type MakeJobOptions = {
|
|
|
81
110
|
config?: BeignetConfig;
|
|
82
111
|
};
|
|
83
112
|
|
|
113
|
+
type MakeFactoryOptions = {
|
|
114
|
+
name: string;
|
|
115
|
+
cwd?: string;
|
|
116
|
+
force?: boolean;
|
|
117
|
+
dryRun?: boolean;
|
|
118
|
+
config?: BeignetConfig;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type MakeSeedOptions = {
|
|
122
|
+
name: string;
|
|
123
|
+
cwd?: string;
|
|
124
|
+
force?: boolean;
|
|
125
|
+
dryRun?: boolean;
|
|
126
|
+
config?: BeignetConfig;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
type MakeNotificationOptions = {
|
|
130
|
+
name: string;
|
|
131
|
+
cwd?: string;
|
|
132
|
+
force?: boolean;
|
|
133
|
+
dryRun?: boolean;
|
|
134
|
+
config?: BeignetConfig;
|
|
135
|
+
};
|
|
136
|
+
|
|
84
137
|
type MakeListenerOptions = {
|
|
85
138
|
name: string;
|
|
86
139
|
event: string;
|
|
@@ -101,7 +154,16 @@ type MakeScheduleOptions = {
|
|
|
101
154
|
route?: boolean;
|
|
102
155
|
};
|
|
103
156
|
|
|
157
|
+
type MakeUploadOptions = {
|
|
158
|
+
name: string;
|
|
159
|
+
cwd?: string;
|
|
160
|
+
force?: boolean;
|
|
161
|
+
dryRun?: boolean;
|
|
162
|
+
config?: BeignetConfig;
|
|
163
|
+
};
|
|
164
|
+
|
|
104
165
|
type MakeResult = {
|
|
166
|
+
schemaVersion: 1;
|
|
105
167
|
name: string;
|
|
106
168
|
targetDir: string;
|
|
107
169
|
dryRun: boolean;
|
|
@@ -111,17 +173,74 @@ type MakeResult = {
|
|
|
111
173
|
skippedFiles: string[];
|
|
112
174
|
};
|
|
113
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Result returned by `beignet make resource`.
|
|
178
|
+
*/
|
|
114
179
|
export type MakeResourceResult = MakeResult;
|
|
180
|
+
/**
|
|
181
|
+
* Result returned by `beignet make feature`.
|
|
182
|
+
*/
|
|
183
|
+
export type MakeFeatureResult = MakeResult;
|
|
184
|
+
/**
|
|
185
|
+
* Result returned by `beignet make contract`.
|
|
186
|
+
*/
|
|
115
187
|
export type MakeContractResult = MakeResult;
|
|
188
|
+
/**
|
|
189
|
+
* Result returned by `beignet make use-case`.
|
|
190
|
+
*/
|
|
116
191
|
export type MakeUseCaseResult = MakeResult;
|
|
192
|
+
/**
|
|
193
|
+
* Result returned by `beignet make task`.
|
|
194
|
+
*/
|
|
195
|
+
export type MakeTaskResult = MakeResult;
|
|
196
|
+
/**
|
|
197
|
+
* Result returned by `beignet make test`.
|
|
198
|
+
*/
|
|
117
199
|
export type MakeTestResult = MakeResult;
|
|
200
|
+
/**
|
|
201
|
+
* Result returned by `beignet make port`.
|
|
202
|
+
*/
|
|
118
203
|
export type MakePortResult = MakeResult;
|
|
204
|
+
/**
|
|
205
|
+
* Result returned by `beignet make adapter`.
|
|
206
|
+
*/
|
|
119
207
|
export type MakeAdapterResult = MakeResult;
|
|
208
|
+
/**
|
|
209
|
+
* Result returned by `beignet make policy`.
|
|
210
|
+
*/
|
|
120
211
|
export type MakePolicyResult = MakeResult;
|
|
212
|
+
/**
|
|
213
|
+
* Result returned by `beignet make event`.
|
|
214
|
+
*/
|
|
121
215
|
export type MakeEventResult = MakeResult;
|
|
216
|
+
/**
|
|
217
|
+
* Result returned by `beignet make job`.
|
|
218
|
+
*/
|
|
122
219
|
export type MakeJobResult = MakeResult;
|
|
220
|
+
/**
|
|
221
|
+
* Result returned by `beignet make factory`.
|
|
222
|
+
*/
|
|
223
|
+
export type MakeFactoryResult = MakeResult;
|
|
224
|
+
/**
|
|
225
|
+
* Result returned by `beignet make seed`.
|
|
226
|
+
*/
|
|
227
|
+
export type MakeSeedResult = MakeResult;
|
|
228
|
+
/**
|
|
229
|
+
* Result returned by `beignet make notification`.
|
|
230
|
+
*/
|
|
231
|
+
export type MakeNotificationResult = MakeResult;
|
|
232
|
+
/**
|
|
233
|
+
* Result returned by `beignet make listener`.
|
|
234
|
+
*/
|
|
123
235
|
export type MakeListenerResult = MakeResult;
|
|
236
|
+
/**
|
|
237
|
+
* Result returned by `beignet make schedule`.
|
|
238
|
+
*/
|
|
124
239
|
export type MakeScheduleResult = MakeResult;
|
|
240
|
+
/**
|
|
241
|
+
* Result returned by `beignet make upload`.
|
|
242
|
+
*/
|
|
243
|
+
export type MakeUploadResult = MakeResult;
|
|
125
244
|
|
|
126
245
|
type ResourceNames = {
|
|
127
246
|
input: string;
|
|
@@ -133,6 +252,15 @@ type ResourceNames = {
|
|
|
133
252
|
pluralPascal: string;
|
|
134
253
|
};
|
|
135
254
|
|
|
255
|
+
type ResourceGenerationMode = "feature" | "resource";
|
|
256
|
+
|
|
257
|
+
type ResourceGenerationOptions = {
|
|
258
|
+
auth: boolean;
|
|
259
|
+
tenant: boolean;
|
|
260
|
+
events: boolean;
|
|
261
|
+
softDelete: boolean;
|
|
262
|
+
};
|
|
263
|
+
|
|
136
264
|
type GeneratedFile = {
|
|
137
265
|
path: string;
|
|
138
266
|
content: string;
|
|
@@ -190,6 +318,30 @@ type JobNames = FeatureArtifactNames & {
|
|
|
190
318
|
payloadTypeName: string;
|
|
191
319
|
};
|
|
192
320
|
|
|
321
|
+
type TaskNames = FeatureArtifactNames & {
|
|
322
|
+
taskName: string;
|
|
323
|
+
taskExportName: string;
|
|
324
|
+
inputSchemaName: string;
|
|
325
|
+
inputTypeName: string;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
type FactoryNames = FeatureArtifactNames & {
|
|
329
|
+
factoryName: string;
|
|
330
|
+
factoryExportName: string;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
type SeedNames = FeatureArtifactNames & {
|
|
334
|
+
seedName: string;
|
|
335
|
+
seedExportName: string;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
type NotificationNames = FeatureArtifactNames & {
|
|
339
|
+
notificationName: string;
|
|
340
|
+
notificationExportName: string;
|
|
341
|
+
payloadSchemaName: string;
|
|
342
|
+
payloadTypeName: string;
|
|
343
|
+
};
|
|
344
|
+
|
|
193
345
|
type ListenerNames = FeatureArtifactNames & {
|
|
194
346
|
listenerName: string;
|
|
195
347
|
listenerExportName: string;
|
|
@@ -205,8 +357,58 @@ type ScheduleNames = FeatureArtifactNames & {
|
|
|
205
357
|
timezone?: string;
|
|
206
358
|
};
|
|
207
359
|
|
|
360
|
+
type UploadNames = FeatureArtifactNames & {
|
|
361
|
+
uploadName: string;
|
|
362
|
+
uploadExportName: string;
|
|
363
|
+
metadataSchemaName: string;
|
|
364
|
+
metadataTypeName: string;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
type FeatureUiNames = ResourceNames & {
|
|
368
|
+
componentExportName: string;
|
|
369
|
+
componentFileName: string;
|
|
370
|
+
};
|
|
371
|
+
|
|
208
372
|
export async function makeResource(
|
|
209
373
|
options: MakeResourceOptions,
|
|
374
|
+
): Promise<MakeResourceResult> {
|
|
375
|
+
return makeResourceSlice(options, "resource");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function makeFeature(
|
|
379
|
+
options: MakeFeatureOptions,
|
|
380
|
+
): Promise<MakeFeatureResult> {
|
|
381
|
+
const addons = normalizeMakeFeatureAddons(options.with ?? []);
|
|
382
|
+
if (addons.length === 0) {
|
|
383
|
+
return makeFeatureResource(options);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (options.dryRun) {
|
|
387
|
+
return makeFeatureSlice(options, addons);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
391
|
+
return makeFeatureSlice(options, addons);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function makeFeatureResource(
|
|
395
|
+
options: MakeResourceOptions,
|
|
396
|
+
): Promise<MakeResourceResult> {
|
|
397
|
+
try {
|
|
398
|
+
return await makeResourceSlice(options, "feature");
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error instanceof Error && error.message.includes("make resource")) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
error.message.replaceAll("make resource", "make feature"),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function makeResourceSlice(
|
|
410
|
+
options: MakeResourceOptions,
|
|
411
|
+
mode: ResourceGenerationMode,
|
|
210
412
|
): Promise<MakeResourceResult> {
|
|
211
413
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
212
414
|
const names = resourceNames(options.name);
|
|
@@ -215,9 +417,24 @@ export async function makeResource(
|
|
|
215
417
|
: await loadBeignetConfig(targetDir);
|
|
216
418
|
|
|
217
419
|
await assertStandardApp(targetDir, config);
|
|
420
|
+
if (
|
|
421
|
+
mode === "resource" &&
|
|
422
|
+
!(await fileExists(path.join(targetDir, resourceSharedErrorsPath(config))))
|
|
423
|
+
) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`beignet make resource expects an app error catalog. Missing ${resourceSharedErrorsPath(config)}.`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
218
428
|
|
|
219
429
|
const persistence = await detectResourcePersistence(targetDir, config);
|
|
220
|
-
const
|
|
430
|
+
const generationOptions = resourceGenerationOptions(options, mode);
|
|
431
|
+
const generatedFiles = resourceFiles(
|
|
432
|
+
names,
|
|
433
|
+
config,
|
|
434
|
+
persistence,
|
|
435
|
+
mode,
|
|
436
|
+
generationOptions,
|
|
437
|
+
);
|
|
221
438
|
const plannedFiles = await planGeneratedFiles(targetDir, generatedFiles, {
|
|
222
439
|
force: Boolean(options.force),
|
|
223
440
|
});
|
|
@@ -228,6 +445,8 @@ export async function makeResource(
|
|
|
228
445
|
persistence,
|
|
229
446
|
{
|
|
230
447
|
dryRun: true,
|
|
448
|
+
mode,
|
|
449
|
+
generationOptions,
|
|
231
450
|
},
|
|
232
451
|
);
|
|
233
452
|
const createdFiles = plannedFiles
|
|
@@ -251,11 +470,14 @@ export async function makeResource(
|
|
|
251
470
|
...(await updateResourceWiring(targetDir, names, config, {
|
|
252
471
|
persistence,
|
|
253
472
|
dryRun: false,
|
|
473
|
+
mode,
|
|
474
|
+
generationOptions,
|
|
254
475
|
})),
|
|
255
476
|
);
|
|
256
477
|
}
|
|
257
478
|
|
|
258
479
|
return {
|
|
480
|
+
schemaVersion: 1,
|
|
259
481
|
name: names.pluralKebab,
|
|
260
482
|
targetDir,
|
|
261
483
|
dryRun: Boolean(options.dryRun),
|
|
@@ -266,6 +488,113 @@ export async function makeResource(
|
|
|
266
488
|
};
|
|
267
489
|
}
|
|
268
490
|
|
|
491
|
+
function resourceGenerationOptions(
|
|
492
|
+
options: MakeResourceOptions,
|
|
493
|
+
mode: ResourceGenerationMode,
|
|
494
|
+
): ResourceGenerationOptions {
|
|
495
|
+
if (mode !== "resource") {
|
|
496
|
+
return { auth: false, tenant: false, events: false, softDelete: false };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
auth: Boolean(options.auth),
|
|
501
|
+
tenant: Boolean(options.tenant),
|
|
502
|
+
events: Boolean(options.events),
|
|
503
|
+
softDelete: Boolean(options.softDelete),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function makeFeatureSlice(
|
|
508
|
+
options: MakeFeatureOptions,
|
|
509
|
+
addons: readonly NormalizedMakeFeatureAddon[],
|
|
510
|
+
): Promise<MakeFeatureResult> {
|
|
511
|
+
let result = await makeFeatureResource(options);
|
|
512
|
+
|
|
513
|
+
for (const addon of addons) {
|
|
514
|
+
result = mergeMakeResults(
|
|
515
|
+
result,
|
|
516
|
+
await makeFeatureAddon(addon, result.name, options),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function makeFeatureAddon(
|
|
524
|
+
addon: NormalizedMakeFeatureAddon,
|
|
525
|
+
feature: string,
|
|
526
|
+
options: MakeFeatureOptions,
|
|
527
|
+
): Promise<MakeResult> {
|
|
528
|
+
const shared = {
|
|
529
|
+
cwd: options.cwd,
|
|
530
|
+
force: options.force,
|
|
531
|
+
dryRun: options.dryRun,
|
|
532
|
+
config: options.config,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
if (addon === "policy") {
|
|
536
|
+
return makePolicy({ ...shared, name: feature });
|
|
537
|
+
}
|
|
538
|
+
if (addon === "task") {
|
|
539
|
+
return makeTask({ ...shared, name: `${feature}/backfill` });
|
|
540
|
+
}
|
|
541
|
+
if (addon === "event") {
|
|
542
|
+
return makeEvent({ ...shared, name: `${feature}/created` });
|
|
543
|
+
}
|
|
544
|
+
if (addon === "job") {
|
|
545
|
+
return makeJob({ ...shared, name: `${feature}/process` });
|
|
546
|
+
}
|
|
547
|
+
if (addon === "notification") {
|
|
548
|
+
return makeNotification({ ...shared, name: `${feature}/created` });
|
|
549
|
+
}
|
|
550
|
+
if (addon === "ui") {
|
|
551
|
+
return makeFeatureUi({ ...shared, name: feature });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return makeUpload({ ...shared, name: `${feature}/attachment` });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function normalizeMakeFeatureAddons(
|
|
558
|
+
addons: readonly MakeFeatureAddon[],
|
|
559
|
+
): NormalizedMakeFeatureAddon[] {
|
|
560
|
+
const normalized = new Set<NormalizedMakeFeatureAddon>();
|
|
561
|
+
|
|
562
|
+
for (const addon of addons) {
|
|
563
|
+
if (addon === "events") normalized.add("event");
|
|
564
|
+
else if (addon === "tasks") normalized.add("task");
|
|
565
|
+
else if (addon === "jobs") normalized.add("job");
|
|
566
|
+
else if (addon === "notifications") normalized.add("notification");
|
|
567
|
+
else if (addon === "uploads") normalized.add("upload");
|
|
568
|
+
else normalized.add(addon);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return featureAddonOrder.filter((addon) => normalized.has(addon));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const featureAddonOrder: readonly NormalizedMakeFeatureAddon[] = [
|
|
575
|
+
"policy",
|
|
576
|
+
"task",
|
|
577
|
+
"event",
|
|
578
|
+
"job",
|
|
579
|
+
"notification",
|
|
580
|
+
"ui",
|
|
581
|
+
"upload",
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
function mergeMakeResults(left: MakeResult, right: MakeResult): MakeResult {
|
|
585
|
+
return {
|
|
586
|
+
...left,
|
|
587
|
+
files: uniqueStrings([...left.files, ...right.files]),
|
|
588
|
+
createdFiles: uniqueStrings([...left.createdFiles, ...right.createdFiles]),
|
|
589
|
+
updatedFiles: uniqueStrings([...left.updatedFiles, ...right.updatedFiles]),
|
|
590
|
+
skippedFiles: uniqueStrings([...left.skippedFiles, ...right.skippedFiles]),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function uniqueStrings(values: readonly string[]): string[] {
|
|
595
|
+
return [...new Set(values)];
|
|
596
|
+
}
|
|
597
|
+
|
|
269
598
|
export async function makeContract(
|
|
270
599
|
options: MakeContractOptions,
|
|
271
600
|
): Promise<MakeContractResult> {
|
|
@@ -290,6 +619,7 @@ export async function makeContract(
|
|
|
290
619
|
}
|
|
291
620
|
|
|
292
621
|
return {
|
|
622
|
+
schemaVersion: 1,
|
|
293
623
|
name: names.pluralKebab,
|
|
294
624
|
targetDir,
|
|
295
625
|
dryRun: Boolean(options.dryRun),
|
|
@@ -335,6 +665,7 @@ export async function makeUseCase(
|
|
|
335
665
|
if (indexResult === "skipped") skippedFiles.push(indexFile.path);
|
|
336
666
|
|
|
337
667
|
return {
|
|
668
|
+
schemaVersion: 1,
|
|
338
669
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
339
670
|
targetDir,
|
|
340
671
|
dryRun: Boolean(options.dryRun),
|
|
@@ -371,11 +702,16 @@ export async function makeTest(
|
|
|
371
702
|
if (result === "skipped") skippedFiles.push(file.path);
|
|
372
703
|
}
|
|
373
704
|
|
|
374
|
-
if (
|
|
705
|
+
if (
|
|
706
|
+
await updatePackageJson(targetDir, {
|
|
707
|
+
dryRun: Boolean(options.dryRun),
|
|
708
|
+
})
|
|
709
|
+
) {
|
|
375
710
|
updatedFiles.push("package.json");
|
|
376
711
|
}
|
|
377
712
|
|
|
378
713
|
return {
|
|
714
|
+
schemaVersion: 1,
|
|
379
715
|
name: `${names.feature.kebab}/${names.action.kebab}`,
|
|
380
716
|
targetDir,
|
|
381
717
|
dryRun: Boolean(options.dryRun),
|
|
@@ -438,6 +774,7 @@ export async function makePort(
|
|
|
438
774
|
}
|
|
439
775
|
|
|
440
776
|
return {
|
|
777
|
+
schemaVersion: 1,
|
|
441
778
|
name: names.kebab,
|
|
442
779
|
targetDir,
|
|
443
780
|
dryRun: Boolean(options.dryRun),
|
|
@@ -483,6 +820,7 @@ export async function makeAdapter(
|
|
|
483
820
|
}
|
|
484
821
|
|
|
485
822
|
return {
|
|
823
|
+
schemaVersion: 1,
|
|
486
824
|
name: names.kebab,
|
|
487
825
|
targetDir,
|
|
488
826
|
dryRun: Boolean(options.dryRun),
|
|
@@ -520,6 +858,7 @@ export async function makePolicy(
|
|
|
520
858
|
}
|
|
521
859
|
|
|
522
860
|
return {
|
|
861
|
+
schemaVersion: 1,
|
|
523
862
|
name: names.kebab,
|
|
524
863
|
targetDir,
|
|
525
864
|
dryRun: Boolean(options.dryRun),
|
|
@@ -570,7 +909,10 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
570
909
|
force: Boolean(options.force),
|
|
571
910
|
dryRun: Boolean(options.dryRun),
|
|
572
911
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
573
|
-
files:
|
|
912
|
+
files: [
|
|
913
|
+
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
914
|
+
...jobFiles(names, config),
|
|
915
|
+
],
|
|
574
916
|
index: featureArtifactIndexFile("job", names, config),
|
|
575
917
|
dependencies: {
|
|
576
918
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -578,6 +920,131 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
578
920
|
});
|
|
579
921
|
}
|
|
580
922
|
|
|
923
|
+
export async function makeTask(
|
|
924
|
+
options: MakeTaskOptions,
|
|
925
|
+
): Promise<MakeTaskResult> {
|
|
926
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
927
|
+
const names = taskNames(options.name);
|
|
928
|
+
const config = options.config
|
|
929
|
+
? resolveConfig(options.config)
|
|
930
|
+
: await loadBeignetConfig(targetDir);
|
|
931
|
+
|
|
932
|
+
await assertFeatureArtifactApp(targetDir, config, "task");
|
|
933
|
+
|
|
934
|
+
const result = await makeFeatureArtifact({
|
|
935
|
+
targetDir,
|
|
936
|
+
config,
|
|
937
|
+
force: Boolean(options.force),
|
|
938
|
+
dryRun: Boolean(options.dryRun),
|
|
939
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
940
|
+
files: [
|
|
941
|
+
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
942
|
+
...taskFiles(names, config),
|
|
943
|
+
],
|
|
944
|
+
index: featureArtifactIndexFile("task", names, config),
|
|
945
|
+
dependencies: {
|
|
946
|
+
"@beignet/core": beignetDependencyVersion,
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
const registryResult = await updateTaskRegistry(targetDir, names, config, {
|
|
951
|
+
dryRun: Boolean(options.dryRun),
|
|
952
|
+
});
|
|
953
|
+
if (registryResult === "created")
|
|
954
|
+
result.createdFiles.push(config.paths.tasks);
|
|
955
|
+
if (registryResult === "updated")
|
|
956
|
+
result.updatedFiles.push(config.paths.tasks);
|
|
957
|
+
if (registryResult === "skipped")
|
|
958
|
+
result.skippedFiles.push(config.paths.tasks);
|
|
959
|
+
if (!result.files.includes(config.paths.tasks)) {
|
|
960
|
+
result.files.push(config.paths.tasks);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export async function makeFactory(
|
|
967
|
+
options: MakeFactoryOptions,
|
|
968
|
+
): Promise<MakeFactoryResult> {
|
|
969
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
970
|
+
const names = factoryNames(options.name);
|
|
971
|
+
const config = options.config
|
|
972
|
+
? resolveConfig(options.config)
|
|
973
|
+
: await loadBeignetConfig(targetDir);
|
|
974
|
+
|
|
975
|
+
await assertPersistenceArtifactApp(targetDir, names, config, "factory");
|
|
976
|
+
|
|
977
|
+
return makeFeatureArtifact({
|
|
978
|
+
targetDir,
|
|
979
|
+
config,
|
|
980
|
+
force: Boolean(options.force),
|
|
981
|
+
dryRun: Boolean(options.dryRun),
|
|
982
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
983
|
+
files: factoryFiles(names, config),
|
|
984
|
+
index: featureArtifactIndexFile("factory", names, config),
|
|
985
|
+
dependencies: {
|
|
986
|
+
"@beignet/core": beignetDependencyVersion,
|
|
987
|
+
},
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export async function makeSeed(
|
|
992
|
+
options: MakeSeedOptions,
|
|
993
|
+
): Promise<MakeSeedResult> {
|
|
994
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
995
|
+
const names = seedNames(options.name);
|
|
996
|
+
const config = options.config
|
|
997
|
+
? resolveConfig(options.config)
|
|
998
|
+
: await loadBeignetConfig(targetDir);
|
|
999
|
+
|
|
1000
|
+
await assertPersistenceArtifactApp(targetDir, names, config, "seed");
|
|
1001
|
+
|
|
1002
|
+
return makeFeatureArtifact({
|
|
1003
|
+
targetDir,
|
|
1004
|
+
config,
|
|
1005
|
+
force: Boolean(options.force),
|
|
1006
|
+
dryRun: Boolean(options.dryRun),
|
|
1007
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
1008
|
+
files: seedFiles(names, config),
|
|
1009
|
+
index: featureArtifactIndexFile("seed", names, config),
|
|
1010
|
+
dependencies: {
|
|
1011
|
+
"@beignet/core": beignetDependencyVersion,
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export async function makeNotification(
|
|
1017
|
+
options: MakeNotificationOptions,
|
|
1018
|
+
): Promise<MakeNotificationResult> {
|
|
1019
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
1020
|
+
const names = notificationNames(options.name);
|
|
1021
|
+
const config = options.config
|
|
1022
|
+
? resolveConfig(options.config)
|
|
1023
|
+
: await loadBeignetConfig(targetDir);
|
|
1024
|
+
|
|
1025
|
+
await assertFeatureArtifactApp(targetDir, config, "notification");
|
|
1026
|
+
|
|
1027
|
+
return makeFeatureArtifact({
|
|
1028
|
+
targetDir,
|
|
1029
|
+
config,
|
|
1030
|
+
force: Boolean(options.force),
|
|
1031
|
+
dryRun: Boolean(options.dryRun),
|
|
1032
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
1033
|
+
files: [
|
|
1034
|
+
...(await missingCapabilityBuilderFiles(
|
|
1035
|
+
targetDir,
|
|
1036
|
+
"notifications",
|
|
1037
|
+
config,
|
|
1038
|
+
)),
|
|
1039
|
+
...notificationFiles(names, config),
|
|
1040
|
+
],
|
|
1041
|
+
index: featureArtifactIndexFile("notification", names, config),
|
|
1042
|
+
dependencies: {
|
|
1043
|
+
"@beignet/core": beignetDependencyVersion,
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
581
1048
|
export async function makeListener(
|
|
582
1049
|
options: MakeListenerOptions,
|
|
583
1050
|
): Promise<MakeListenerResult> {
|
|
@@ -596,7 +1063,10 @@ export async function makeListener(
|
|
|
596
1063
|
force: Boolean(options.force),
|
|
597
1064
|
dryRun: Boolean(options.dryRun),
|
|
598
1065
|
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
599
|
-
files:
|
|
1066
|
+
files: [
|
|
1067
|
+
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
1068
|
+
...listenerFiles(names, config),
|
|
1069
|
+
],
|
|
600
1070
|
index: featureArtifactIndexFile("listener", names, config),
|
|
601
1071
|
dependencies: {
|
|
602
1072
|
"@beignet/core": beignetDependencyVersion,
|
|
@@ -636,7 +1106,10 @@ export async function makeSchedule(
|
|
|
636
1106
|
requireServer: Boolean(options.route),
|
|
637
1107
|
});
|
|
638
1108
|
|
|
639
|
-
const files =
|
|
1109
|
+
const files = [
|
|
1110
|
+
...(await missingCapabilityBuilderFiles(targetDir, "schedules", config)),
|
|
1111
|
+
...scheduleFiles(names, config, { route: Boolean(options.route) }),
|
|
1112
|
+
];
|
|
640
1113
|
|
|
641
1114
|
return makeFeatureArtifact({
|
|
642
1115
|
targetDir,
|
|
@@ -652,47 +1125,136 @@ export async function makeSchedule(
|
|
|
652
1125
|
});
|
|
653
1126
|
}
|
|
654
1127
|
|
|
655
|
-
async function
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
config
|
|
662
|
-
|
|
663
|
-
config.paths.server,
|
|
664
|
-
config.paths.useCaseBuilder,
|
|
665
|
-
];
|
|
1128
|
+
export async function makeUpload(
|
|
1129
|
+
options: MakeUploadOptions,
|
|
1130
|
+
): Promise<MakeUploadResult> {
|
|
1131
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
1132
|
+
const names = uploadNames(options.name);
|
|
1133
|
+
const config = options.config
|
|
1134
|
+
? resolveConfig(options.config)
|
|
1135
|
+
: await loadBeignetConfig(targetDir);
|
|
666
1136
|
|
|
667
|
-
|
|
668
|
-
try {
|
|
669
|
-
await stat(path.join(targetDir, file));
|
|
670
|
-
} catch {
|
|
671
|
-
throw new Error(
|
|
672
|
-
`beignet make resource expects a standard Beignet app. Missing ${file}.`,
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
1137
|
+
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
677
1138
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1139
|
+
return makeFeatureArtifact({
|
|
1140
|
+
targetDir,
|
|
1141
|
+
config,
|
|
1142
|
+
force: Boolean(options.force),
|
|
1143
|
+
dryRun: Boolean(options.dryRun),
|
|
1144
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
1145
|
+
files: uploadFiles(names, config),
|
|
1146
|
+
index: featureArtifactIndexFile("upload", names, config),
|
|
1147
|
+
dependencies: {
|
|
1148
|
+
"@beignet/core": beignetDependencyVersion,
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
689
1151
|
}
|
|
690
1152
|
|
|
691
|
-
async function
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1153
|
+
async function makeFeatureUi(
|
|
1154
|
+
options: MakeResourceOptions,
|
|
1155
|
+
): Promise<MakeResult> {
|
|
1156
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
1157
|
+
const names = featureUiNames(options.name);
|
|
1158
|
+
const config = options.config
|
|
1159
|
+
? resolveConfig(options.config)
|
|
1160
|
+
: await loadBeignetConfig(targetDir);
|
|
1161
|
+
|
|
1162
|
+
await assertFeatureUiApp(targetDir, config);
|
|
1163
|
+
|
|
1164
|
+
const componentFiles = featureUiComponentFiles(names, config);
|
|
1165
|
+
const indexFile = featureUiIndexFile(names, config);
|
|
1166
|
+
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
1167
|
+
force: Boolean(options.force),
|
|
1168
|
+
});
|
|
1169
|
+
const createdFiles = plannedFiles
|
|
1170
|
+
.filter((file) => file.result === "created")
|
|
1171
|
+
.map((file) => file.path);
|
|
1172
|
+
const updatedFiles = plannedFiles
|
|
1173
|
+
.filter((file) => file.result === "updated")
|
|
1174
|
+
.map((file) => file.path);
|
|
1175
|
+
const skippedFiles = plannedFiles
|
|
1176
|
+
.filter((file) => file.result === "skipped")
|
|
1177
|
+
.map((file) => file.path);
|
|
1178
|
+
|
|
1179
|
+
if (!options.dryRun) {
|
|
1180
|
+
for (const file of plannedFiles) {
|
|
1181
|
+
await writePlannedGeneratedFile(targetDir, file);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const indexResult = await updateFeatureUiIndex(targetDir, indexFile, {
|
|
1186
|
+
dryRun: Boolean(options.dryRun),
|
|
1187
|
+
});
|
|
1188
|
+
if (indexResult === "created") createdFiles.push(indexFile.path);
|
|
1189
|
+
if (indexResult === "updated") updatedFiles.push(indexFile.path);
|
|
1190
|
+
if (indexResult === "skipped") skippedFiles.push(indexFile.path);
|
|
1191
|
+
|
|
1192
|
+
if (
|
|
1193
|
+
await updatePackageDependencies(
|
|
1194
|
+
targetDir,
|
|
1195
|
+
{
|
|
1196
|
+
"@beignet/react-query": beignetDependencyVersion,
|
|
1197
|
+
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
1198
|
+
},
|
|
1199
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
1200
|
+
)
|
|
1201
|
+
) {
|
|
1202
|
+
updatedFiles.push("package.json");
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
return {
|
|
1206
|
+
schemaVersion: 1,
|
|
1207
|
+
name: names.pluralKebab,
|
|
1208
|
+
targetDir,
|
|
1209
|
+
dryRun: Boolean(options.dryRun),
|
|
1210
|
+
files: [...componentFiles.map((file) => file.path), indexFile.path],
|
|
1211
|
+
createdFiles,
|
|
1212
|
+
updatedFiles,
|
|
1213
|
+
skippedFiles,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async function assertStandardApp(
|
|
1218
|
+
targetDir: string,
|
|
1219
|
+
config: ResolvedBeignetConfig,
|
|
1220
|
+
): Promise<void> {
|
|
1221
|
+
const requiredFiles = [
|
|
1222
|
+
config.paths.appContext,
|
|
1223
|
+
config.paths.infrastructurePorts,
|
|
1224
|
+
config.paths.ports,
|
|
1225
|
+
config.paths.server,
|
|
1226
|
+
config.paths.useCaseBuilder,
|
|
1227
|
+
];
|
|
1228
|
+
|
|
1229
|
+
for (const file of requiredFiles) {
|
|
1230
|
+
try {
|
|
1231
|
+
await stat(path.join(targetDir, file));
|
|
1232
|
+
} catch {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`beignet make resource expects a standard Beignet app. Missing ${file}.`,
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function assertUseCaseApp(
|
|
1241
|
+
targetDir: string,
|
|
1242
|
+
config: ResolvedBeignetConfig,
|
|
1243
|
+
): Promise<void> {
|
|
1244
|
+
try {
|
|
1245
|
+
await stat(path.join(targetDir, config.paths.useCaseBuilder));
|
|
1246
|
+
} catch {
|
|
1247
|
+
throw new Error(
|
|
1248
|
+
`beignet make use-case expects a standard Beignet app. Missing ${config.paths.useCaseBuilder}.`,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
async function assertTestApp(
|
|
1254
|
+
targetDir: string,
|
|
1255
|
+
names: UseCaseNames,
|
|
1256
|
+
config: ResolvedBeignetConfig,
|
|
1257
|
+
): Promise<void> {
|
|
696
1258
|
const requiredFiles = [
|
|
697
1259
|
config.paths.appContext,
|
|
698
1260
|
config.paths.infrastructurePorts,
|
|
@@ -777,7 +1339,14 @@ async function assertPolicyApp(
|
|
|
777
1339
|
async function assertFeatureArtifactApp(
|
|
778
1340
|
targetDir: string,
|
|
779
1341
|
config: ResolvedBeignetConfig,
|
|
780
|
-
artifact:
|
|
1342
|
+
artifact:
|
|
1343
|
+
| "event"
|
|
1344
|
+
| "job"
|
|
1345
|
+
| "task"
|
|
1346
|
+
| "notification"
|
|
1347
|
+
| "listener"
|
|
1348
|
+
| "schedule"
|
|
1349
|
+
| "upload",
|
|
781
1350
|
options: { requireServer?: boolean } = {},
|
|
782
1351
|
): Promise<void> {
|
|
783
1352
|
const requiredFiles = [
|
|
@@ -796,6 +1365,47 @@ async function assertFeatureArtifactApp(
|
|
|
796
1365
|
}
|
|
797
1366
|
}
|
|
798
1367
|
|
|
1368
|
+
async function assertFeatureUiApp(
|
|
1369
|
+
targetDir: string,
|
|
1370
|
+
config: ResolvedBeignetConfig,
|
|
1371
|
+
): Promise<void> {
|
|
1372
|
+
const clientPath = clientIndexPath(config);
|
|
1373
|
+
const requiredFiles = [config.paths.appContext, clientPath];
|
|
1374
|
+
|
|
1375
|
+
for (const file of requiredFiles) {
|
|
1376
|
+
try {
|
|
1377
|
+
await stat(path.join(targetDir, file));
|
|
1378
|
+
} catch {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`beignet make feature --with ui expects a standard Beignet app with React Query client helpers. Missing ${file}.`,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
async function assertPersistenceArtifactApp(
|
|
1387
|
+
targetDir: string,
|
|
1388
|
+
names: FactoryNames | SeedNames,
|
|
1389
|
+
config: ResolvedBeignetConfig,
|
|
1390
|
+
artifact: "factory" | "seed",
|
|
1391
|
+
): Promise<void> {
|
|
1392
|
+
const requiredFiles = [
|
|
1393
|
+
config.paths.appContext,
|
|
1394
|
+
config.paths.ports,
|
|
1395
|
+
resourcePortFilePath(featureResourceNames(names), config),
|
|
1396
|
+
];
|
|
1397
|
+
|
|
1398
|
+
for (const file of requiredFiles) {
|
|
1399
|
+
try {
|
|
1400
|
+
await stat(path.join(targetDir, file));
|
|
1401
|
+
} catch {
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
`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.`,
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
799
1409
|
type WriteGeneratedFileOptions = {
|
|
800
1410
|
force: boolean;
|
|
801
1411
|
dryRun: boolean;
|
|
@@ -884,6 +1494,83 @@ async function readOptionalFile(filePath: string): Promise<string | undefined> {
|
|
|
884
1494
|
}
|
|
885
1495
|
}
|
|
886
1496
|
|
|
1497
|
+
type CapabilityBuilder =
|
|
1498
|
+
| "listeners"
|
|
1499
|
+
| "jobs"
|
|
1500
|
+
| "schedules"
|
|
1501
|
+
| "notifications"
|
|
1502
|
+
| "tasks";
|
|
1503
|
+
|
|
1504
|
+
const capabilityBuilders: Record<
|
|
1505
|
+
CapabilityBuilder,
|
|
1506
|
+
{ factoryName: string; defineName: string; module: string }
|
|
1507
|
+
> = {
|
|
1508
|
+
listeners: {
|
|
1509
|
+
factoryName: "createListeners",
|
|
1510
|
+
defineName: "defineListener",
|
|
1511
|
+
module: "@beignet/core/events",
|
|
1512
|
+
},
|
|
1513
|
+
jobs: {
|
|
1514
|
+
factoryName: "createJobs",
|
|
1515
|
+
defineName: "defineJob",
|
|
1516
|
+
module: "@beignet/core/jobs",
|
|
1517
|
+
},
|
|
1518
|
+
schedules: {
|
|
1519
|
+
factoryName: "createSchedules",
|
|
1520
|
+
defineName: "defineSchedule",
|
|
1521
|
+
module: "@beignet/core/schedules",
|
|
1522
|
+
},
|
|
1523
|
+
notifications: {
|
|
1524
|
+
factoryName: "createNotifications",
|
|
1525
|
+
defineName: "defineNotification",
|
|
1526
|
+
module: "@beignet/core/notifications",
|
|
1527
|
+
},
|
|
1528
|
+
tasks: {
|
|
1529
|
+
factoryName: "createTasks",
|
|
1530
|
+
defineName: "defineTask",
|
|
1531
|
+
module: "@beignet/core/tasks",
|
|
1532
|
+
},
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
function capabilityBuilderPath(
|
|
1536
|
+
capability: CapabilityBuilder,
|
|
1537
|
+
config: ResolvedBeignetConfig,
|
|
1538
|
+
): string {
|
|
1539
|
+
if (capability === "listeners") return config.paths.listenersBuilder;
|
|
1540
|
+
if (capability === "jobs") return config.paths.jobsBuilder;
|
|
1541
|
+
if (capability === "schedules") return config.paths.schedulesBuilder;
|
|
1542
|
+
if (capability === "notifications") return config.paths.notificationsBuilder;
|
|
1543
|
+
return config.paths.tasksBuilder;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function capabilityBuilderFile(
|
|
1547
|
+
capability: CapabilityBuilder,
|
|
1548
|
+
config: ResolvedBeignetConfig,
|
|
1549
|
+
): GeneratedFile {
|
|
1550
|
+
const builder = capabilityBuilders[capability];
|
|
1551
|
+
|
|
1552
|
+
return {
|
|
1553
|
+
path: capabilityBuilderPath(capability, config),
|
|
1554
|
+
content: `import { ${builder.factoryName} } from "${builder.module}";
|
|
1555
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
1556
|
+
|
|
1557
|
+
export const { ${builder.defineName} } = ${builder.factoryName}<AppContext>();
|
|
1558
|
+
`,
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async function missingCapabilityBuilderFiles(
|
|
1563
|
+
targetDir: string,
|
|
1564
|
+
capability: CapabilityBuilder,
|
|
1565
|
+
config: ResolvedBeignetConfig,
|
|
1566
|
+
): Promise<GeneratedFile[]> {
|
|
1567
|
+
const file = capabilityBuilderFile(capability, config);
|
|
1568
|
+
if (await fileExists(path.join(targetDir, file.path))) {
|
|
1569
|
+
return [];
|
|
1570
|
+
}
|
|
1571
|
+
return [file];
|
|
1572
|
+
}
|
|
1573
|
+
|
|
887
1574
|
async function makeFeatureArtifact(options: {
|
|
888
1575
|
targetDir: string;
|
|
889
1576
|
config: ResolvedBeignetConfig;
|
|
@@ -935,6 +1622,7 @@ async function makeFeatureArtifact(options: {
|
|
|
935
1622
|
}
|
|
936
1623
|
|
|
937
1624
|
return {
|
|
1625
|
+
schemaVersion: 1,
|
|
938
1626
|
name: options.name,
|
|
939
1627
|
targetDir: options.targetDir,
|
|
940
1628
|
dryRun: options.dryRun,
|
|
@@ -978,14 +1666,29 @@ function beignetDependencyVersion(packageJson: PackageJsonLike): string {
|
|
|
978
1666
|
);
|
|
979
1667
|
}
|
|
980
1668
|
|
|
1669
|
+
function tanstackReactQueryDependencyVersion(
|
|
1670
|
+
packageJson: PackageJsonLike,
|
|
1671
|
+
): string {
|
|
1672
|
+
return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
|
|
1673
|
+
}
|
|
1674
|
+
|
|
981
1675
|
async function updateResourceWiring(
|
|
982
1676
|
targetDir: string,
|
|
983
1677
|
names: ResourceNames,
|
|
984
1678
|
config: ResolvedBeignetConfig,
|
|
985
1679
|
persistenceOrOptions:
|
|
986
1680
|
| ResourcePersistence
|
|
987
|
-
| {
|
|
988
|
-
|
|
1681
|
+
| {
|
|
1682
|
+
dryRun: boolean;
|
|
1683
|
+
persistence?: ResourcePersistence;
|
|
1684
|
+
mode?: ResourceGenerationMode;
|
|
1685
|
+
generationOptions?: ResourceGenerationOptions;
|
|
1686
|
+
},
|
|
1687
|
+
maybeOptions?: {
|
|
1688
|
+
dryRun: boolean;
|
|
1689
|
+
mode?: ResourceGenerationMode;
|
|
1690
|
+
generationOptions?: ResourceGenerationOptions;
|
|
1691
|
+
},
|
|
989
1692
|
): Promise<string[]> {
|
|
990
1693
|
const persistence =
|
|
991
1694
|
typeof persistenceOrOptions === "string"
|
|
@@ -995,12 +1698,49 @@ async function updateResourceWiring(
|
|
|
995
1698
|
typeof persistenceOrOptions === "string"
|
|
996
1699
|
? (maybeOptions as { dryRun: boolean })
|
|
997
1700
|
: persistenceOrOptions;
|
|
1701
|
+
const mode =
|
|
1702
|
+
typeof persistenceOrOptions === "string"
|
|
1703
|
+
? (maybeOptions?.mode ?? "feature")
|
|
1704
|
+
: (persistenceOrOptions.mode ?? "feature");
|
|
1705
|
+
const generationOptions =
|
|
1706
|
+
typeof persistenceOrOptions === "string"
|
|
1707
|
+
? (maybeOptions?.generationOptions ??
|
|
1708
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode))
|
|
1709
|
+
: (persistenceOrOptions.generationOptions ??
|
|
1710
|
+
resourceGenerationOptions({ name: names.pluralKebab }, mode));
|
|
998
1711
|
const updated = new Set<string>();
|
|
999
|
-
if (
|
|
1000
|
-
|
|
1712
|
+
if (
|
|
1713
|
+
await updatePackageJson(targetDir, {
|
|
1714
|
+
...options,
|
|
1715
|
+
webTestingDependency: true,
|
|
1716
|
+
})
|
|
1717
|
+
) {
|
|
1718
|
+
updated.add("package.json");
|
|
1719
|
+
}
|
|
1720
|
+
if (
|
|
1721
|
+
await updatePortsIndex(targetDir, names, config, {
|
|
1722
|
+
...options,
|
|
1723
|
+
generationOptions,
|
|
1724
|
+
})
|
|
1725
|
+
) {
|
|
1001
1726
|
updated.add(config.paths.ports);
|
|
1002
1727
|
}
|
|
1003
|
-
if (
|
|
1728
|
+
if (
|
|
1729
|
+
persistence === "memory" &&
|
|
1730
|
+
(await updateInfrastructurePorts(targetDir, names, config, options))
|
|
1731
|
+
) {
|
|
1732
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1733
|
+
}
|
|
1734
|
+
if (
|
|
1735
|
+
generationOptions.auth &&
|
|
1736
|
+
(await updateInfrastructureGatePolicies(targetDir, names, config, options))
|
|
1737
|
+
) {
|
|
1738
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1739
|
+
}
|
|
1740
|
+
if (
|
|
1741
|
+
persistence === "drizzle" &&
|
|
1742
|
+
(await updateInfrastructureDeferredPorts(targetDir, names, config, options))
|
|
1743
|
+
) {
|
|
1004
1744
|
updated.add(config.paths.infrastructurePorts);
|
|
1005
1745
|
}
|
|
1006
1746
|
if (
|
|
@@ -1021,7 +1761,14 @@ async function updateResourceWiring(
|
|
|
1021
1761
|
) {
|
|
1022
1762
|
updated.add(config.paths.server);
|
|
1023
1763
|
}
|
|
1024
|
-
if (
|
|
1764
|
+
if (
|
|
1765
|
+
await updateSharedErrors(targetDir, names, config, { ...options, mode })
|
|
1766
|
+
) {
|
|
1767
|
+
updated.add(resourceSharedErrorsPath(config));
|
|
1768
|
+
}
|
|
1769
|
+
if (
|
|
1770
|
+
await updateOpenApiRoute(targetDir, names, config, { ...options, mode })
|
|
1771
|
+
) {
|
|
1025
1772
|
updated.add(config.paths.openapiRoute);
|
|
1026
1773
|
}
|
|
1027
1774
|
const routesFile = await updateServerRoutes(
|
|
@@ -1037,17 +1784,27 @@ async function updateResourceWiring(
|
|
|
1037
1784
|
|
|
1038
1785
|
async function updatePackageJson(
|
|
1039
1786
|
targetDir: string,
|
|
1040
|
-
options: { dryRun: boolean },
|
|
1787
|
+
options: { dryRun: boolean; webTestingDependency?: boolean },
|
|
1041
1788
|
): Promise<boolean> {
|
|
1042
1789
|
const filePath = path.join(targetDir, "package.json");
|
|
1043
1790
|
const original = await readFile(filePath, "utf8");
|
|
1044
1791
|
const packageJson = JSON.parse(original) as {
|
|
1045
1792
|
scripts?: Record<string, string>;
|
|
1793
|
+
dependencies?: Record<string, string>;
|
|
1794
|
+
devDependencies?: Record<string, string>;
|
|
1046
1795
|
};
|
|
1047
1796
|
packageJson.scripts = packageJson.scripts ?? {};
|
|
1048
|
-
if (packageJson.scripts.test) return false;
|
|
1049
1797
|
|
|
1050
|
-
packageJson.scripts.test
|
|
1798
|
+
packageJson.scripts.test ??= "bun test";
|
|
1799
|
+
|
|
1800
|
+
if (options.webTestingDependency) {
|
|
1801
|
+
packageJson.devDependencies = packageJson.devDependencies ?? {};
|
|
1802
|
+
packageJson.devDependencies["@beignet/web"] ??=
|
|
1803
|
+
packageJson.dependencies?.["@beignet/core"] ??
|
|
1804
|
+
packageJson.dependencies?.["@beignet/next"] ??
|
|
1805
|
+
"*";
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1051
1808
|
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
1052
1809
|
if (!options.dryRun) await writeFile(filePath, next);
|
|
1053
1810
|
return next !== original;
|
|
@@ -1057,7 +1814,7 @@ async function updatePortsIndex(
|
|
|
1057
1814
|
targetDir: string,
|
|
1058
1815
|
names: ResourceNames,
|
|
1059
1816
|
config: ResolvedBeignetConfig,
|
|
1060
|
-
options: { dryRun: boolean },
|
|
1817
|
+
options: { dryRun: boolean; generationOptions?: ResourceGenerationOptions },
|
|
1061
1818
|
): Promise<boolean> {
|
|
1062
1819
|
const filePath = path.join(targetDir, config.paths.ports);
|
|
1063
1820
|
const original = await readFile(filePath, "utf8");
|
|
@@ -1069,6 +1826,30 @@ async function updatePortsIndex(
|
|
|
1069
1826
|
next = insertAfterImports(next, importLine);
|
|
1070
1827
|
}
|
|
1071
1828
|
|
|
1829
|
+
if (options.generationOptions?.auth) {
|
|
1830
|
+
const policyPath = path.join(
|
|
1831
|
+
config.paths.features,
|
|
1832
|
+
names.pluralKebab,
|
|
1833
|
+
"policy.ts",
|
|
1834
|
+
);
|
|
1835
|
+
const policyImportLine = `import type { AuthorizationContext as ${names.singularPascal}AuthorizationContext, ${names.singularCamel}Policy } from "${relativeModule(config.paths.ports, policyPath)}";`;
|
|
1836
|
+
if (!next.includes(policyImportLine)) {
|
|
1837
|
+
next = insertAfterImports(next, policyImportLine);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
next = appendUnionTypeMember(
|
|
1841
|
+
next,
|
|
1842
|
+
"AppAuthorizationContext",
|
|
1843
|
+
`${names.singularPascal}AuthorizationContext`,
|
|
1844
|
+
);
|
|
1845
|
+
next = appendTupleTypeMember(
|
|
1846
|
+
next,
|
|
1847
|
+
"AppGate",
|
|
1848
|
+
`typeof ${names.singularCamel}Policy`,
|
|
1849
|
+
);
|
|
1850
|
+
next = appendGatePolicyMember(next, `typeof ${names.singularCamel}Policy`);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1072
1853
|
const entry = `\t${names.pluralCamel}: ${names.singularPascal}Repository;`;
|
|
1073
1854
|
if (!next.includes(entry.trim())) {
|
|
1074
1855
|
next = insertTypeProperty(
|
|
@@ -1225,7 +2006,7 @@ async function updateInfrastructurePorts(
|
|
|
1225
2006
|
if (!next.includes(repositoryConst)) {
|
|
1226
2007
|
next = replaceRequired(
|
|
1227
2008
|
next,
|
|
1228
|
-
/(\nexport const appPorts = definePorts(?:<[^>]+>)?\(\{)/,
|
|
2009
|
+
/(\nexport const appPorts = definePorts(?:<[^>]+>)?\((?:\)\()?\{)/,
|
|
1229
2010
|
`\n${repositoryConst}\n$1`,
|
|
1230
2011
|
`Could not find definePorts({ in ${config.paths.infrastructurePorts}. Add ${repositoryConst} manually, or restore the generated infrastructure ports file before running make resource.`,
|
|
1231
2012
|
);
|
|
@@ -1262,6 +2043,37 @@ async function updateInfrastructurePorts(
|
|
|
1262
2043
|
return true;
|
|
1263
2044
|
}
|
|
1264
2045
|
|
|
2046
|
+
async function updateInfrastructureGatePolicies(
|
|
2047
|
+
targetDir: string,
|
|
2048
|
+
names: ResourceNames,
|
|
2049
|
+
config: ResolvedBeignetConfig,
|
|
2050
|
+
options: { dryRun: boolean },
|
|
2051
|
+
): Promise<boolean> {
|
|
2052
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
2053
|
+
const original = await readFile(filePath, "utf8");
|
|
2054
|
+
let next = original;
|
|
2055
|
+
const policyPath = path.join(
|
|
2056
|
+
config.paths.features,
|
|
2057
|
+
names.pluralKebab,
|
|
2058
|
+
"policy.ts",
|
|
2059
|
+
);
|
|
2060
|
+
const importLine = `import { ${names.singularCamel}Policy } from "${relativeModule(config.paths.infrastructurePorts, policyPath)}";`;
|
|
2061
|
+
|
|
2062
|
+
if (!next.includes(importLine)) {
|
|
2063
|
+
next = insertAfterImports(next, importLine);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
next = appendCreateGatePolicy(
|
|
2067
|
+
next,
|
|
2068
|
+
`${names.singularCamel}Policy`,
|
|
2069
|
+
`Could not find createGate({ policies: [...] }) in ${config.paths.infrastructurePorts}. Add ${names.singularCamel}Policy to the gate policies manually, or restore the generated infrastructure ports file before running make resource --auth.`,
|
|
2070
|
+
);
|
|
2071
|
+
|
|
2072
|
+
if (next === original) return false;
|
|
2073
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1265
2077
|
async function updateDrizzleSchemaIndex(
|
|
1266
2078
|
targetDir: string,
|
|
1267
2079
|
names: ResourceNames,
|
|
@@ -1319,6 +2131,24 @@ async function updateDrizzleRepositories(
|
|
|
1319
2131
|
return true;
|
|
1320
2132
|
}
|
|
1321
2133
|
|
|
2134
|
+
async function updateInfrastructureDeferredPorts(
|
|
2135
|
+
targetDir: string,
|
|
2136
|
+
names: ResourceNames,
|
|
2137
|
+
config: ResolvedBeignetConfig,
|
|
2138
|
+
options: { dryRun: boolean },
|
|
2139
|
+
): Promise<boolean> {
|
|
2140
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
2141
|
+
const original = await readFile(filePath, "utf8");
|
|
2142
|
+
// Drizzle resources are provided by the app database provider at startup,
|
|
2143
|
+
// so deferred-form ports files defer the new repository key. Identity-form
|
|
2144
|
+
// ports files are left untouched.
|
|
2145
|
+
const next = appendDeferredPortKey(original, names.pluralCamel);
|
|
2146
|
+
|
|
2147
|
+
if (next === original) return false;
|
|
2148
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
1322
2152
|
async function updateServerTransactionPorts(
|
|
1323
2153
|
targetDir: string,
|
|
1324
2154
|
names: ResourceNames,
|
|
@@ -1359,7 +2189,7 @@ async function updateOpenApiRoute(
|
|
|
1359
2189
|
targetDir: string,
|
|
1360
2190
|
names: ResourceNames,
|
|
1361
2191
|
config: ResolvedBeignetConfig,
|
|
1362
|
-
options: { dryRun: boolean },
|
|
2192
|
+
options: { dryRun: boolean; mode: ResourceGenerationMode },
|
|
1363
2193
|
): Promise<boolean> {
|
|
1364
2194
|
const filePath = path.join(targetDir, config.paths.openapiRoute);
|
|
1365
2195
|
if (!(await fileExists(filePath))) return false;
|
|
@@ -1368,15 +2198,14 @@ async function updateOpenApiRoute(
|
|
|
1368
2198
|
const firstArg = firstCreateOpenAPIHandlerArgInfo(original);
|
|
1369
2199
|
if (!firstArg?.text.startsWith("[")) return false;
|
|
1370
2200
|
|
|
1371
|
-
const
|
|
1372
|
-
const createName = `create${names.singularPascal}`;
|
|
2201
|
+
const contractNames = resourceContractNames(names, options.mode);
|
|
1373
2202
|
const listedContracts = identifiersFromArrayExpression(firstArg.text);
|
|
1374
|
-
const missingContracts =
|
|
2203
|
+
const missingContracts = contractNames.filter(
|
|
1375
2204
|
(contractName) => !listedContracts.has(contractName),
|
|
1376
2205
|
);
|
|
1377
2206
|
if (missingContracts.length === 0) return false;
|
|
1378
2207
|
|
|
1379
|
-
const importLine = `import {
|
|
2208
|
+
const importLine = `import { ${contractNames.join(", ")} } from "${aliasModule(resourceContractFilePath(names, config))}";`;
|
|
1380
2209
|
const nextArray = appendToArrayExpression(firstArg.text, missingContracts);
|
|
1381
2210
|
let next = `${original.slice(0, firstArg.start)}${nextArray}${original.slice(
|
|
1382
2211
|
firstArg.end,
|
|
@@ -1391,38 +2220,106 @@ async function updateOpenApiRoute(
|
|
|
1391
2220
|
return true;
|
|
1392
2221
|
}
|
|
1393
2222
|
|
|
1394
|
-
async function
|
|
2223
|
+
async function updateSharedErrors(
|
|
1395
2224
|
targetDir: string,
|
|
1396
2225
|
names: ResourceNames,
|
|
1397
2226
|
config: ResolvedBeignetConfig,
|
|
1398
|
-
options: { dryRun: boolean },
|
|
1399
|
-
): Promise<
|
|
1400
|
-
|
|
2227
|
+
options: { dryRun: boolean; mode: ResourceGenerationMode },
|
|
2228
|
+
): Promise<boolean> {
|
|
2229
|
+
if (options.mode !== "resource") return false;
|
|
2230
|
+
|
|
2231
|
+
const filePath = path.join(targetDir, resourceSharedErrorsPath(config));
|
|
2232
|
+
if (!(await fileExists(filePath))) return false;
|
|
2233
|
+
|
|
1401
2234
|
const original = await readFile(filePath, "utf8");
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
2235
|
+
const notFoundErrorName = `${names.singularPascal}NotFound`;
|
|
2236
|
+
const conflictErrorName = `${names.singularPascal}Conflict`;
|
|
2237
|
+
const missingErrorNames = [notFoundErrorName, conflictErrorName].filter(
|
|
2238
|
+
(errorName) => !new RegExp(`\\b${errorName}\\b`).test(original),
|
|
1405
2239
|
);
|
|
2240
|
+
if (missingErrorNames.length === 0) return false;
|
|
2241
|
+
const expectedErrorNames = missingErrorNames.join(" and ");
|
|
1406
2242
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
routeRegistryPath,
|
|
1412
|
-
names,
|
|
1413
|
-
config,
|
|
1414
|
-
options,
|
|
2243
|
+
const defineErrorsStart = original.indexOf("defineErrors(");
|
|
2244
|
+
if (defineErrorsStart === -1) {
|
|
2245
|
+
throw new Error(
|
|
2246
|
+
`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`,
|
|
1415
2247
|
);
|
|
1416
2248
|
}
|
|
1417
2249
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
}
|
|
2250
|
+
const openBrace = original.indexOf("{", defineErrorsStart);
|
|
2251
|
+
if (openBrace === -1) {
|
|
2252
|
+
throw new Error(
|
|
2253
|
+
`Could not find defineErrors({ in ${resourceSharedErrorsPath(config)}. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`,
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
const closeBrace = matchingDelimiterIndex(original, openBrace, "{", "}");
|
|
2258
|
+
if (closeBrace === -1) {
|
|
2259
|
+
throw new Error(
|
|
2260
|
+
`Could not update ${resourceSharedErrorsPath(config)} because the error catalog object is malformed. Add ${expectedErrorNames} manually, or restore the generated shared error catalog before running make resource.`,
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
const entries = [];
|
|
2265
|
+
if (missingErrorNames.includes(notFoundErrorName)) {
|
|
2266
|
+
entries.push(`${notFoundErrorName}: {
|
|
2267
|
+
\t\tcode: "${constantCase(names.singularKebab)}_NOT_FOUND",
|
|
2268
|
+
\t\tstatus: 404,
|
|
2269
|
+
\t\tmessage: "${names.singularPascal} not found",
|
|
2270
|
+
\t},`);
|
|
2271
|
+
}
|
|
2272
|
+
if (missingErrorNames.includes(conflictErrorName)) {
|
|
2273
|
+
entries.push(`${conflictErrorName}: {
|
|
2274
|
+
\t\tcode: "${constantCase(names.singularKebab)}_CONFLICT",
|
|
2275
|
+
\t\tstatus: 409,
|
|
2276
|
+
\t\tmessage: "${names.singularPascal} was changed by another request",
|
|
2277
|
+
\t},`);
|
|
2278
|
+
}
|
|
2279
|
+
const next = insertBeforeClosingBrace(
|
|
2280
|
+
original,
|
|
2281
|
+
openBrace,
|
|
2282
|
+
closeBrace,
|
|
2283
|
+
entries.join("\n"),
|
|
2284
|
+
);
|
|
2285
|
+
|
|
2286
|
+
if (next === original) return false;
|
|
2287
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2288
|
+
return true;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
async function updateServerRoutes(
|
|
2292
|
+
targetDir: string,
|
|
2293
|
+
names: ResourceNames,
|
|
2294
|
+
config: ResolvedBeignetConfig,
|
|
2295
|
+
options: { dryRun: boolean },
|
|
2296
|
+
): Promise<string | undefined> {
|
|
2297
|
+
const filePath = path.join(targetDir, config.paths.server);
|
|
2298
|
+
const original = await readFile(filePath, "utf8");
|
|
2299
|
+
const routeRegistryPath = routeRegistryFileFromServerSource(
|
|
2300
|
+
original,
|
|
2301
|
+
config.paths.server,
|
|
2302
|
+
);
|
|
2303
|
+
|
|
2304
|
+
if (routeRegistryPath) {
|
|
2305
|
+
await assertImportedRoutesFileExists(targetDir, routeRegistryPath);
|
|
2306
|
+
return updateDefineRoutesFile(
|
|
2307
|
+
targetDir,
|
|
2308
|
+
routeRegistryPath,
|
|
2309
|
+
names,
|
|
2310
|
+
config,
|
|
2311
|
+
options,
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
return updateDefineRoutesFile(
|
|
2316
|
+
targetDir,
|
|
2317
|
+
config.paths.server,
|
|
2318
|
+
names,
|
|
2319
|
+
config,
|
|
2320
|
+
options,
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
1426
2323
|
|
|
1427
2324
|
async function updateDefineRoutesFile(
|
|
1428
2325
|
targetDir: string,
|
|
@@ -1784,6 +2681,98 @@ function typeBodySource(source: string, typeName: string): string | undefined {
|
|
|
1784
2681
|
return source.slice(openBrace + 1, closeBrace);
|
|
1785
2682
|
}
|
|
1786
2683
|
|
|
2684
|
+
function appendUnionTypeMember(
|
|
2685
|
+
source: string,
|
|
2686
|
+
typeName: string,
|
|
2687
|
+
member: string,
|
|
2688
|
+
): string {
|
|
2689
|
+
const match = new RegExp(
|
|
2690
|
+
`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`,
|
|
2691
|
+
).exec(source);
|
|
2692
|
+
if (!match) return source;
|
|
2693
|
+
|
|
2694
|
+
const declaration = match[0];
|
|
2695
|
+
if (new RegExp(`\\b${member}\\b`).test(declaration)) return source;
|
|
2696
|
+
|
|
2697
|
+
const nextDeclaration = declaration.replace(/;\s*$/, ` | ${member};`);
|
|
2698
|
+
return `${source.slice(0, match.index)}${nextDeclaration}${source.slice(
|
|
2699
|
+
match.index + declaration.length,
|
|
2700
|
+
)}`;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
function appendTupleTypeMember(
|
|
2704
|
+
source: string,
|
|
2705
|
+
typeName: string,
|
|
2706
|
+
member: string,
|
|
2707
|
+
): string {
|
|
2708
|
+
const match = new RegExp(`export\\s+type\\s+${typeName}\\s*=\\s*`).exec(
|
|
2709
|
+
source,
|
|
2710
|
+
);
|
|
2711
|
+
if (!match) return source;
|
|
2712
|
+
|
|
2713
|
+
const tupleStart = source.indexOf("[", match.index);
|
|
2714
|
+
if (tupleStart === -1) return source;
|
|
2715
|
+
|
|
2716
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
2717
|
+
if (tupleEnd === -1) return source;
|
|
2718
|
+
|
|
2719
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
2720
|
+
if (tuple.includes(member)) return source;
|
|
2721
|
+
|
|
2722
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
2723
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(
|
|
2724
|
+
tupleEnd + 1,
|
|
2725
|
+
)}`;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
function appendGatePolicyMember(source: string, member: string): string {
|
|
2729
|
+
const gateMatch = /\bGatePort\s*</.exec(source);
|
|
2730
|
+
if (!gateMatch) return source;
|
|
2731
|
+
|
|
2732
|
+
const tupleStart = source.indexOf("[", gateMatch.index);
|
|
2733
|
+
if (tupleStart === -1) return source;
|
|
2734
|
+
|
|
2735
|
+
const tupleEnd = matchingDelimiterIndex(source, tupleStart, "[", "]");
|
|
2736
|
+
if (tupleEnd === -1) return source;
|
|
2737
|
+
|
|
2738
|
+
const tuple = source.slice(tupleStart, tupleEnd + 1);
|
|
2739
|
+
if (tuple.includes(member)) return source;
|
|
2740
|
+
|
|
2741
|
+
const nextTuple = appendToArrayExpression(tuple, [member]);
|
|
2742
|
+
return `${source.slice(0, tupleStart)}${nextTuple}${source.slice(
|
|
2743
|
+
tupleEnd + 1,
|
|
2744
|
+
)}`;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function appendCreateGatePolicy(
|
|
2748
|
+
source: string,
|
|
2749
|
+
policy: string,
|
|
2750
|
+
failureMessage: string,
|
|
2751
|
+
): string {
|
|
2752
|
+
const policyMatch = /\bpolicies\s*:\s*\[/.exec(source);
|
|
2753
|
+
if (!policyMatch) {
|
|
2754
|
+
throw new Error(failureMessage);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
const arrayStart = source.indexOf("[", policyMatch.index);
|
|
2758
|
+
if (arrayStart === -1) {
|
|
2759
|
+
throw new Error(failureMessage);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
2763
|
+
if (arrayEnd === -1) {
|
|
2764
|
+
throw new Error(failureMessage);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
const policies = source.slice(arrayStart, arrayEnd + 1);
|
|
2768
|
+
if (policies.includes(policy)) return source;
|
|
2769
|
+
|
|
2770
|
+
const nextPolicies = appendToArrayExpression(policies, [policy]);
|
|
2771
|
+
return `${source.slice(0, arrayStart)}${nextPolicies}${source.slice(
|
|
2772
|
+
arrayEnd + 1,
|
|
2773
|
+
)}`;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
1787
2776
|
function insertDefinePortsProperty(
|
|
1788
2777
|
source: string,
|
|
1789
2778
|
property: string,
|
|
@@ -1881,7 +2870,20 @@ function definePortsBodySource(source: string): string | undefined {
|
|
|
1881
2870
|
function definePortsBodyRange(
|
|
1882
2871
|
source: string,
|
|
1883
2872
|
): { openBrace: number; closeBrace: number } | undefined {
|
|
1884
|
-
const
|
|
2873
|
+
const outer = definePortsOuterBodyRange(source);
|
|
2874
|
+
if (!outer) return undefined;
|
|
2875
|
+
|
|
2876
|
+
// The curried `definePorts<AppPorts>()({ bound, deferred })` form binds app
|
|
2877
|
+
// ports inside `bound`; generated bindings belong there.
|
|
2878
|
+
return (
|
|
2879
|
+
topLevelBoundObjectRange(source, outer.openBrace, outer.closeBrace) ?? outer
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function definePortsOuterBodyRange(
|
|
2884
|
+
source: string,
|
|
2885
|
+
): { openBrace: number; closeBrace: number } | undefined {
|
|
2886
|
+
const match = /definePorts(?:<[^>]+>)?\(\s*(?:\)\s*\(\s*)?\{/.exec(source);
|
|
1885
2887
|
if (!match) return undefined;
|
|
1886
2888
|
|
|
1887
2889
|
const openBrace = source.indexOf("{", match.index);
|
|
@@ -1891,6 +2893,50 @@ function definePortsBodyRange(
|
|
|
1891
2893
|
return { openBrace, closeBrace };
|
|
1892
2894
|
}
|
|
1893
2895
|
|
|
2896
|
+
function appendDeferredPortKey(source: string, portKey: string): string {
|
|
2897
|
+
const outer = definePortsOuterBodyRange(source);
|
|
2898
|
+
if (!outer) return source;
|
|
2899
|
+
|
|
2900
|
+
const body = source.slice(outer.openBrace + 1, outer.closeBrace);
|
|
2901
|
+
if (!hasTopLevelObjectProperty(body, "deferred")) return source;
|
|
2902
|
+
|
|
2903
|
+
const deferredMatch = /\bdeferred\s*:\s*\[/.exec(body);
|
|
2904
|
+
if (!deferredMatch) return source;
|
|
2905
|
+
|
|
2906
|
+
const arrayStart = source.indexOf(
|
|
2907
|
+
"[",
|
|
2908
|
+
outer.openBrace + 1 + deferredMatch.index,
|
|
2909
|
+
);
|
|
2910
|
+
const arrayEnd = matchingDelimiterIndex(source, arrayStart, "[", "]");
|
|
2911
|
+
if (arrayEnd === -1) return source;
|
|
2912
|
+
|
|
2913
|
+
const tuple = source.slice(arrayStart, arrayEnd + 1);
|
|
2914
|
+
if (new RegExp(`["']${portKey}["']`).test(tuple)) return source;
|
|
2915
|
+
|
|
2916
|
+
const nextTuple = appendToArrayExpression(tuple, [`"${portKey}"`]);
|
|
2917
|
+
return `${source.slice(0, arrayStart)}${nextTuple}${source.slice(
|
|
2918
|
+
arrayEnd + 1,
|
|
2919
|
+
)}`;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
function topLevelBoundObjectRange(
|
|
2923
|
+
source: string,
|
|
2924
|
+
openBrace: number,
|
|
2925
|
+
closeBrace: number,
|
|
2926
|
+
): { openBrace: number; closeBrace: number } | undefined {
|
|
2927
|
+
const body = source.slice(openBrace + 1, closeBrace);
|
|
2928
|
+
if (!hasTopLevelObjectProperty(body, "bound")) return undefined;
|
|
2929
|
+
|
|
2930
|
+
const boundMatch = /\bbound\s*:\s*\{/.exec(body);
|
|
2931
|
+
if (!boundMatch) return undefined;
|
|
2932
|
+
|
|
2933
|
+
const boundOpen = source.indexOf("{", openBrace + 1 + boundMatch.index);
|
|
2934
|
+
const boundClose = matchingBraceIndex(source, boundOpen);
|
|
2935
|
+
if (boundClose === -1) return undefined;
|
|
2936
|
+
|
|
2937
|
+
return { openBrace: boundOpen, closeBrace: boundClose };
|
|
2938
|
+
}
|
|
2939
|
+
|
|
1894
2940
|
function hasTopLevelObjectProperty(
|
|
1895
2941
|
source: string,
|
|
1896
2942
|
propertyName: string,
|
|
@@ -2193,6 +3239,25 @@ function resourceNames(input: string): ResourceNames {
|
|
|
2193
3239
|
return names;
|
|
2194
3240
|
}
|
|
2195
3241
|
|
|
3242
|
+
function featureUiNames(input: string): FeatureUiNames {
|
|
3243
|
+
const names = resourceNames(input);
|
|
3244
|
+
const resolvedNames = {
|
|
3245
|
+
...names,
|
|
3246
|
+
componentExportName: `${names.pluralPascal}Panel`,
|
|
3247
|
+
componentFileName: `${names.pluralKebab}-panel`,
|
|
3248
|
+
};
|
|
3249
|
+
|
|
3250
|
+
assertGeneratedIdentifiers(input, "Feature UI name", [
|
|
3251
|
+
resolvedNames.componentExportName,
|
|
3252
|
+
]);
|
|
3253
|
+
|
|
3254
|
+
return resolvedNames;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
function featureResourceNames(names: FeatureArtifactNames): ResourceNames {
|
|
3258
|
+
return resourceNames(names.feature.kebab);
|
|
3259
|
+
}
|
|
3260
|
+
|
|
2196
3261
|
function useCaseNames(input: string): UseCaseNames {
|
|
2197
3262
|
const parts = input
|
|
2198
3263
|
.trim()
|
|
@@ -2333,6 +3398,64 @@ function jobNames(input: string): JobNames {
|
|
|
2333
3398
|
};
|
|
2334
3399
|
}
|
|
2335
3400
|
|
|
3401
|
+
function taskNames(input: string): TaskNames {
|
|
3402
|
+
const names = featureArtifactNames(input, "Task");
|
|
3403
|
+
const taskExportName = `${names.artifact.camel}Task`;
|
|
3404
|
+
|
|
3405
|
+
assertGeneratedIdentifiers(input, "Task name", [taskExportName]);
|
|
3406
|
+
|
|
3407
|
+
return {
|
|
3408
|
+
...names,
|
|
3409
|
+
taskName: names.stableName,
|
|
3410
|
+
taskExportName,
|
|
3411
|
+
inputSchemaName: `${names.artifact.pascal}TaskInputSchema`,
|
|
3412
|
+
inputTypeName: `${names.artifact.pascal}TaskInput`,
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
function factoryNames(input: string): FactoryNames {
|
|
3417
|
+
const names = featureArtifactNames(input, "Factory");
|
|
3418
|
+
const factoryExportName = `${names.artifact.camel}Factory`;
|
|
3419
|
+
|
|
3420
|
+
assertGeneratedIdentifiers(input, "Factory name", [factoryExportName]);
|
|
3421
|
+
|
|
3422
|
+
return {
|
|
3423
|
+
...names,
|
|
3424
|
+
factoryName: names.stableName,
|
|
3425
|
+
factoryExportName,
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
function seedNames(input: string): SeedNames {
|
|
3430
|
+
const names = featureArtifactNames(input, "Seed");
|
|
3431
|
+
const seedExportName = `${names.artifact.camel}Seed`;
|
|
3432
|
+
|
|
3433
|
+
assertGeneratedIdentifiers(input, "Seed name", [seedExportName]);
|
|
3434
|
+
|
|
3435
|
+
return {
|
|
3436
|
+
...names,
|
|
3437
|
+
seedName: names.stableName,
|
|
3438
|
+
seedExportName,
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
function notificationNames(input: string): NotificationNames {
|
|
3443
|
+
const names = featureArtifactNames(input, "Notification");
|
|
3444
|
+
const notificationExportName = `${names.featureSingularPascal}${names.artifact.pascal}Notification`;
|
|
3445
|
+
|
|
3446
|
+
assertGeneratedIdentifiers(input, "Notification name", [
|
|
3447
|
+
notificationExportName,
|
|
3448
|
+
]);
|
|
3449
|
+
|
|
3450
|
+
return {
|
|
3451
|
+
...names,
|
|
3452
|
+
notificationName: names.stableName,
|
|
3453
|
+
notificationExportName,
|
|
3454
|
+
payloadSchemaName: `${notificationExportName}PayloadSchema`,
|
|
3455
|
+
payloadTypeName: `${notificationExportName}Payload`,
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
|
|
2336
3459
|
function listenerNames(input: string, eventInput: string): ListenerNames {
|
|
2337
3460
|
const names = featureArtifactNames(input, "Listener");
|
|
2338
3461
|
const listenerExportName = names.artifact.camel;
|
|
@@ -2367,6 +3490,21 @@ function scheduleNames(
|
|
|
2367
3490
|
};
|
|
2368
3491
|
}
|
|
2369
3492
|
|
|
3493
|
+
function uploadNames(input: string): UploadNames {
|
|
3494
|
+
const names = featureArtifactNames(input, "Upload");
|
|
3495
|
+
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
3496
|
+
|
|
3497
|
+
assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
|
|
3498
|
+
|
|
3499
|
+
return {
|
|
3500
|
+
...names,
|
|
3501
|
+
uploadName: names.stableName,
|
|
3502
|
+
uploadExportName,
|
|
3503
|
+
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
3504
|
+
metadataTypeName: `${uploadExportName}Metadata`,
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
|
|
2370
3508
|
function normalizeEventReference(input: string): string {
|
|
2371
3509
|
const trimmed = input.trim();
|
|
2372
3510
|
if (trimmed.includes("/")) return trimmed;
|
|
@@ -2524,10 +3662,21 @@ function pascalCase(value: string): string {
|
|
|
2524
3662
|
return `${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
2525
3663
|
}
|
|
2526
3664
|
|
|
3665
|
+
function constantCase(value: string): string {
|
|
3666
|
+
return value.replaceAll("-", "_").toUpperCase();
|
|
3667
|
+
}
|
|
3668
|
+
|
|
2527
3669
|
function resourceFiles(
|
|
2528
3670
|
names: ResourceNames,
|
|
2529
3671
|
config: ResolvedBeignetConfig,
|
|
2530
3672
|
persistence: ResourcePersistence = "memory",
|
|
3673
|
+
mode: ResourceGenerationMode = "feature",
|
|
3674
|
+
options: ResourceGenerationOptions = {
|
|
3675
|
+
auth: false,
|
|
3676
|
+
tenant: false,
|
|
3677
|
+
events: false,
|
|
3678
|
+
softDelete: false,
|
|
3679
|
+
},
|
|
2531
3680
|
): GeneratedFile[] {
|
|
2532
3681
|
const useCaseDir = resourceUseCaseDir(names, config);
|
|
2533
3682
|
const infraDir = infrastructureDir(config);
|
|
@@ -2535,27 +3684,63 @@ function resourceFiles(
|
|
|
2535
3684
|
const files: GeneratedFile[] = [
|
|
2536
3685
|
{
|
|
2537
3686
|
path: resourceContractFilePath(names, config),
|
|
2538
|
-
content: contractFile(names, config),
|
|
3687
|
+
content: contractFile(names, config, mode, options),
|
|
2539
3688
|
},
|
|
2540
3689
|
{
|
|
2541
|
-
path: path.join(
|
|
2542
|
-
content: schemasFile(names),
|
|
3690
|
+
path: path.join(featureDir, "schemas.ts"),
|
|
3691
|
+
content: schemasFile(names, mode, options),
|
|
2543
3692
|
},
|
|
2544
3693
|
{
|
|
2545
3694
|
path: path.join(useCaseDir, `list-${names.pluralKebab}.ts`),
|
|
2546
|
-
content: listUseCaseFile(names, config),
|
|
3695
|
+
content: listUseCaseFile(names, config, options),
|
|
2547
3696
|
},
|
|
2548
3697
|
{
|
|
2549
3698
|
path: path.join(useCaseDir, `create-${names.singularKebab}.ts`),
|
|
2550
|
-
content: createUseCaseFile(names, config),
|
|
3699
|
+
content: createUseCaseFile(names, config, options),
|
|
2551
3700
|
},
|
|
3701
|
+
...(mode === "resource"
|
|
3702
|
+
? [
|
|
3703
|
+
{
|
|
3704
|
+
path: path.join(useCaseDir, `get-${names.singularKebab}.ts`),
|
|
3705
|
+
content: getUseCaseFile(names, config, options),
|
|
3706
|
+
},
|
|
3707
|
+
{
|
|
3708
|
+
path: path.join(useCaseDir, `update-${names.singularKebab}.ts`),
|
|
3709
|
+
content: updateUseCaseFile(names, config, options),
|
|
3710
|
+
},
|
|
3711
|
+
{
|
|
3712
|
+
path: path.join(useCaseDir, `delete-${names.singularKebab}.ts`),
|
|
3713
|
+
content: deleteUseCaseFile(names, config, options),
|
|
3714
|
+
},
|
|
3715
|
+
{
|
|
3716
|
+
path: path.join(featureDir, "policy.ts"),
|
|
3717
|
+
content: resourcePolicyFile(names, config, options),
|
|
3718
|
+
},
|
|
3719
|
+
]
|
|
3720
|
+
: []),
|
|
3721
|
+
...(mode === "resource" && options.events
|
|
3722
|
+
? [
|
|
3723
|
+
{
|
|
3724
|
+
path: path.join(featureDir, "domain/events/index.ts"),
|
|
3725
|
+
content: resourceEventsFile(names),
|
|
3726
|
+
},
|
|
3727
|
+
]
|
|
3728
|
+
: []),
|
|
3729
|
+
...(mode === "resource" && options.auth
|
|
3730
|
+
? [
|
|
3731
|
+
{
|
|
3732
|
+
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
3733
|
+
content: resourcePolicyTestFile(names, options),
|
|
3734
|
+
},
|
|
3735
|
+
]
|
|
3736
|
+
: []),
|
|
2552
3737
|
{
|
|
2553
3738
|
path: path.join(useCaseDir, "index.ts"),
|
|
2554
|
-
content: useCasesIndexFile(names),
|
|
3739
|
+
content: useCasesIndexFile(names, mode),
|
|
2555
3740
|
},
|
|
2556
3741
|
{
|
|
2557
3742
|
path: resourcePortFilePath(names, config),
|
|
2558
|
-
content: repositoryPortFile(names, config),
|
|
3743
|
+
content: repositoryPortFile(names, config, mode, options),
|
|
2559
3744
|
},
|
|
2560
3745
|
{
|
|
2561
3746
|
path: path.join(
|
|
@@ -2563,15 +3748,15 @@ function resourceFiles(
|
|
|
2563
3748
|
names.pluralKebab,
|
|
2564
3749
|
`in-memory-${names.singularKebab}-repository.ts`,
|
|
2565
3750
|
),
|
|
2566
|
-
content: inMemoryRepositoryFile(names, config),
|
|
3751
|
+
content: inMemoryRepositoryFile(names, config, mode, options),
|
|
2567
3752
|
},
|
|
2568
3753
|
{
|
|
2569
3754
|
path: path.join(featureDir, "routes.ts"),
|
|
2570
|
-
content: routeGroupFile(names, config),
|
|
3755
|
+
content: routeGroupFile(names, config, mode, options),
|
|
2571
3756
|
},
|
|
2572
3757
|
{
|
|
2573
3758
|
path: resourceTestFilePath(names, config),
|
|
2574
|
-
content: testFile(names, config),
|
|
3759
|
+
content: testFile(names, config, mode, options),
|
|
2575
3760
|
},
|
|
2576
3761
|
];
|
|
2577
3762
|
|
|
@@ -2579,11 +3764,11 @@ function resourceFiles(
|
|
|
2579
3764
|
files.push(
|
|
2580
3765
|
{
|
|
2581
3766
|
path: drizzleResourceSchemaFilePath(names, config),
|
|
2582
|
-
content: drizzleSchemaFile(names),
|
|
3767
|
+
content: drizzleSchemaFile(names, options),
|
|
2583
3768
|
},
|
|
2584
3769
|
{
|
|
2585
3770
|
path: drizzleResourceRepositoryFilePath(names, config),
|
|
2586
|
-
content: drizzleRepositoryFile(names, config),
|
|
3771
|
+
content: drizzleRepositoryFile(names, config, mode, options),
|
|
2587
3772
|
},
|
|
2588
3773
|
);
|
|
2589
3774
|
}
|
|
@@ -2591,6 +3776,31 @@ function resourceFiles(
|
|
|
2591
3776
|
return files;
|
|
2592
3777
|
}
|
|
2593
3778
|
|
|
3779
|
+
function featureUiComponentFiles(
|
|
3780
|
+
names: FeatureUiNames,
|
|
3781
|
+
config: ResolvedBeignetConfig,
|
|
3782
|
+
): GeneratedFile[] {
|
|
3783
|
+
const componentPath = featureUiComponentFilePath(names, config);
|
|
3784
|
+
|
|
3785
|
+
return [
|
|
3786
|
+
{
|
|
3787
|
+
path: componentPath,
|
|
3788
|
+
content: featureUiComponentFile(names, config),
|
|
3789
|
+
},
|
|
3790
|
+
];
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
function featureUiIndexFile(
|
|
3794
|
+
names: FeatureUiNames,
|
|
3795
|
+
config: ResolvedBeignetConfig,
|
|
3796
|
+
): GeneratedFile {
|
|
3797
|
+
return {
|
|
3798
|
+
path: featureUiIndexFilePath(names, config),
|
|
3799
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";
|
|
3800
|
+
`,
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
|
|
2594
3804
|
function contractFiles(
|
|
2595
3805
|
names: ResourceNames,
|
|
2596
3806
|
config: ResolvedBeignetConfig,
|
|
@@ -2689,6 +3899,54 @@ function jobFiles(
|
|
|
2689
3899
|
];
|
|
2690
3900
|
}
|
|
2691
3901
|
|
|
3902
|
+
function taskFiles(
|
|
3903
|
+
names: TaskNames,
|
|
3904
|
+
config: ResolvedBeignetConfig,
|
|
3905
|
+
): GeneratedFile[] {
|
|
3906
|
+
return [
|
|
3907
|
+
{
|
|
3908
|
+
path: taskFilePath(names, config),
|
|
3909
|
+
content: taskFile(names, config),
|
|
3910
|
+
},
|
|
3911
|
+
];
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
function notificationFiles(
|
|
3915
|
+
names: NotificationNames,
|
|
3916
|
+
config: ResolvedBeignetConfig,
|
|
3917
|
+
): GeneratedFile[] {
|
|
3918
|
+
return [
|
|
3919
|
+
{
|
|
3920
|
+
path: notificationFilePath(names, config),
|
|
3921
|
+
content: notificationFile(names, config),
|
|
3922
|
+
},
|
|
3923
|
+
];
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
function factoryFiles(
|
|
3927
|
+
names: FactoryNames,
|
|
3928
|
+
config: ResolvedBeignetConfig,
|
|
3929
|
+
): GeneratedFile[] {
|
|
3930
|
+
return [
|
|
3931
|
+
{
|
|
3932
|
+
path: factoryFilePath(names, config),
|
|
3933
|
+
content: factoryFile(names, config),
|
|
3934
|
+
},
|
|
3935
|
+
];
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
function seedFiles(
|
|
3939
|
+
names: SeedNames,
|
|
3940
|
+
config: ResolvedBeignetConfig,
|
|
3941
|
+
): GeneratedFile[] {
|
|
3942
|
+
return [
|
|
3943
|
+
{
|
|
3944
|
+
path: seedFilePath(names, config),
|
|
3945
|
+
content: seedFile(names, config),
|
|
3946
|
+
},
|
|
3947
|
+
];
|
|
3948
|
+
}
|
|
3949
|
+
|
|
2692
3950
|
function listenerFiles(
|
|
2693
3951
|
names: ListenerNames,
|
|
2694
3952
|
config: ResolvedBeignetConfig,
|
|
@@ -2723,6 +3981,18 @@ function scheduleFiles(
|
|
|
2723
3981
|
return files;
|
|
2724
3982
|
}
|
|
2725
3983
|
|
|
3984
|
+
function uploadFiles(
|
|
3985
|
+
names: UploadNames,
|
|
3986
|
+
config: ResolvedBeignetConfig,
|
|
3987
|
+
): GeneratedFile[] {
|
|
3988
|
+
return [
|
|
3989
|
+
{
|
|
3990
|
+
path: uploadFilePath(names, config),
|
|
3991
|
+
content: uploadFile(names, config),
|
|
3992
|
+
},
|
|
3993
|
+
];
|
|
3994
|
+
}
|
|
3995
|
+
|
|
2726
3996
|
function portFilePath(names: PortNames, config: ResolvedBeignetConfig): string {
|
|
2727
3997
|
return path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
|
|
2728
3998
|
}
|
|
@@ -2773,6 +4043,30 @@ function drizzleResourceRepositoryFilePath(
|
|
|
2773
4043
|
);
|
|
2774
4044
|
}
|
|
2775
4045
|
|
|
4046
|
+
function resourceSharedErrorsPath(config: ResolvedBeignetConfig): string {
|
|
4047
|
+
return path.join(config.paths.features, "shared", "errors.ts");
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
function resourceContractNames(
|
|
4051
|
+
names: ResourceNames,
|
|
4052
|
+
mode: ResourceGenerationMode,
|
|
4053
|
+
): string[] {
|
|
4054
|
+
const contractNames = [
|
|
4055
|
+
`list${names.pluralPascal}`,
|
|
4056
|
+
`create${names.singularPascal}`,
|
|
4057
|
+
];
|
|
4058
|
+
|
|
4059
|
+
if (mode === "resource") {
|
|
4060
|
+
contractNames.push(
|
|
4061
|
+
`get${names.singularPascal}`,
|
|
4062
|
+
`update${names.singularPascal}`,
|
|
4063
|
+
`delete${names.singularPascal}`,
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
return contractNames;
|
|
4068
|
+
}
|
|
4069
|
+
|
|
2776
4070
|
async function detectResourcePersistence(
|
|
2777
4071
|
targetDir: string,
|
|
2778
4072
|
config: ResolvedBeignetConfig,
|
|
@@ -2791,6 +4085,36 @@ function resourceFeatureDir(
|
|
|
2791
4085
|
return path.join(config.paths.features, names.pluralKebab);
|
|
2792
4086
|
}
|
|
2793
4087
|
|
|
4088
|
+
function featureUiDir(
|
|
4089
|
+
names: FeatureUiNames,
|
|
4090
|
+
config: ResolvedBeignetConfig,
|
|
4091
|
+
): string {
|
|
4092
|
+
return path.join(resourceFeatureDir(names, config), "components");
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
function featureUiComponentFilePath(
|
|
4096
|
+
names: FeatureUiNames,
|
|
4097
|
+
config: ResolvedBeignetConfig,
|
|
4098
|
+
): string {
|
|
4099
|
+
return path.join(
|
|
4100
|
+
featureUiDir(names, config),
|
|
4101
|
+
`${names.componentFileName}.tsx`,
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
function featureUiIndexFilePath(
|
|
4106
|
+
names: FeatureUiNames,
|
|
4107
|
+
config: ResolvedBeignetConfig,
|
|
4108
|
+
): string {
|
|
4109
|
+
return path.join(featureUiDir(names, config), "index.ts");
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
function clientIndexPath(config: ResolvedBeignetConfig): string {
|
|
4113
|
+
const appRoot = path.posix.dirname(config.paths.appContext);
|
|
4114
|
+
|
|
4115
|
+
return path.join(appRoot === "." ? "" : appRoot, "client/index.ts");
|
|
4116
|
+
}
|
|
4117
|
+
|
|
2794
4118
|
function usesFeatureOwnedContracts(config: ResolvedBeignetConfig): boolean {
|
|
2795
4119
|
return (
|
|
2796
4120
|
directoryPath(config.paths.contracts) ===
|
|
@@ -2855,6 +4179,13 @@ function resourceUseCaseIndexPath(
|
|
|
2855
4179
|
return path.join(resourceUseCaseDir(names, config), "index.ts");
|
|
2856
4180
|
}
|
|
2857
4181
|
|
|
4182
|
+
function resourceSchemaFilePath(
|
|
4183
|
+
names: ResourceNames,
|
|
4184
|
+
config: ResolvedBeignetConfig,
|
|
4185
|
+
): string {
|
|
4186
|
+
return path.join(resourceFeatureDir(names, config), "schemas.ts");
|
|
4187
|
+
}
|
|
4188
|
+
|
|
2858
4189
|
function resourcePortFilePath(
|
|
2859
4190
|
names: ResourceNames,
|
|
2860
4191
|
config: ResolvedBeignetConfig,
|
|
@@ -2917,7 +4248,16 @@ function policyFilePath(
|
|
|
2917
4248
|
|
|
2918
4249
|
function featureArtifactDir(
|
|
2919
4250
|
names: FeatureArtifactNames,
|
|
2920
|
-
kind:
|
|
4251
|
+
kind:
|
|
4252
|
+
| "events"
|
|
4253
|
+
| "factories"
|
|
4254
|
+
| "tasks"
|
|
4255
|
+
| "jobs"
|
|
4256
|
+
| "notifications"
|
|
4257
|
+
| "listeners"
|
|
4258
|
+
| "seeds"
|
|
4259
|
+
| "schedules"
|
|
4260
|
+
| "uploads",
|
|
2921
4261
|
config: ResolvedBeignetConfig,
|
|
2922
4262
|
): string {
|
|
2923
4263
|
const featureDir = path.join(config.paths.features, names.feature.kebab);
|
|
@@ -2925,6 +4265,9 @@ function featureArtifactDir(
|
|
|
2925
4265
|
if (kind === "events") {
|
|
2926
4266
|
return path.join(featureDir, "domain", "events");
|
|
2927
4267
|
}
|
|
4268
|
+
if (kind === "factories") {
|
|
4269
|
+
return path.join(featureDir, "tests", "factories");
|
|
4270
|
+
}
|
|
2928
4271
|
|
|
2929
4272
|
return path.join(featureDir, kind);
|
|
2930
4273
|
}
|
|
@@ -2946,10 +4289,44 @@ function jobFilePath(names: JobNames, config: ResolvedBeignetConfig): string {
|
|
|
2946
4289
|
);
|
|
2947
4290
|
}
|
|
2948
4291
|
|
|
2949
|
-
function
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
4292
|
+
function taskFilePath(names: TaskNames, config: ResolvedBeignetConfig): string {
|
|
4293
|
+
return path.join(
|
|
4294
|
+
featureArtifactDir(names, "tasks", config),
|
|
4295
|
+
`${names.artifact.kebab}.ts`,
|
|
4296
|
+
);
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4299
|
+
function factoryFilePath(
|
|
4300
|
+
names: FactoryNames,
|
|
4301
|
+
config: ResolvedBeignetConfig,
|
|
4302
|
+
): string {
|
|
4303
|
+
return path.join(
|
|
4304
|
+
featureArtifactDir(names, "factories", config),
|
|
4305
|
+
`${names.artifact.kebab}.ts`,
|
|
4306
|
+
);
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
function seedFilePath(names: SeedNames, config: ResolvedBeignetConfig): string {
|
|
4310
|
+
return path.join(
|
|
4311
|
+
featureArtifactDir(names, "seeds", config),
|
|
4312
|
+
`${names.artifact.kebab}.ts`,
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
function notificationFilePath(
|
|
4317
|
+
names: NotificationNames,
|
|
4318
|
+
config: ResolvedBeignetConfig,
|
|
4319
|
+
): string {
|
|
4320
|
+
return path.join(
|
|
4321
|
+
featureArtifactDir(names, "notifications", config),
|
|
4322
|
+
`${names.artifact.kebab}.ts`,
|
|
4323
|
+
);
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
function listenerFilePath(
|
|
4327
|
+
names: ListenerNames,
|
|
4328
|
+
config: ResolvedBeignetConfig,
|
|
4329
|
+
): string {
|
|
2953
4330
|
return path.join(
|
|
2954
4331
|
featureArtifactDir(names, "listeners", config),
|
|
2955
4332
|
`${names.artifact.kebab}.ts`,
|
|
@@ -2966,6 +4343,16 @@ function scheduleFilePath(
|
|
|
2966
4343
|
);
|
|
2967
4344
|
}
|
|
2968
4345
|
|
|
4346
|
+
function uploadFilePath(
|
|
4347
|
+
names: UploadNames,
|
|
4348
|
+
config: ResolvedBeignetConfig,
|
|
4349
|
+
): string {
|
|
4350
|
+
return path.join(
|
|
4351
|
+
featureArtifactDir(names, "uploads", config),
|
|
4352
|
+
`${names.artifact.kebab}.ts`,
|
|
4353
|
+
);
|
|
4354
|
+
}
|
|
4355
|
+
|
|
2969
4356
|
function scheduleRouteFilePath(
|
|
2970
4357
|
names: ScheduleNames,
|
|
2971
4358
|
config: ResolvedBeignetConfig,
|
|
@@ -3027,16 +4414,69 @@ async function updateUseCaseIndex(
|
|
|
3027
4414
|
return "updated";
|
|
3028
4415
|
}
|
|
3029
4416
|
|
|
4417
|
+
async function updateFeatureUiIndex(
|
|
4418
|
+
targetDir: string,
|
|
4419
|
+
file: GeneratedFile,
|
|
4420
|
+
options: { dryRun: boolean },
|
|
4421
|
+
): Promise<WriteGeneratedFileResult> {
|
|
4422
|
+
const destination = path.join(targetDir, file.path);
|
|
4423
|
+
const existing = await readOptionalFile(destination);
|
|
4424
|
+
if (existing === undefined) {
|
|
4425
|
+
if (!options.dryRun) {
|
|
4426
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
4427
|
+
await writeFile(destination, file.content);
|
|
4428
|
+
}
|
|
4429
|
+
return "created";
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
if (existing.includes(file.content.trim())) {
|
|
4433
|
+
return "skipped";
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
const next = `${existing.trimEnd()}\n${file.content}`;
|
|
4437
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
4438
|
+
return "updated";
|
|
4439
|
+
}
|
|
4440
|
+
|
|
3030
4441
|
type FeatureArtifactIndexFile = {
|
|
3031
4442
|
path: string;
|
|
4443
|
+
kind:
|
|
4444
|
+
| "event"
|
|
4445
|
+
| "job"
|
|
4446
|
+
| "task"
|
|
4447
|
+
| "factory"
|
|
4448
|
+
| "seed"
|
|
4449
|
+
| "notification"
|
|
4450
|
+
| "listener"
|
|
4451
|
+
| "schedule"
|
|
4452
|
+
| "upload";
|
|
3032
4453
|
importName: string;
|
|
3033
4454
|
artifactPath: string;
|
|
3034
4455
|
registryName: string;
|
|
4456
|
+
registryEntryName: string;
|
|
3035
4457
|
};
|
|
3036
4458
|
|
|
3037
4459
|
function featureArtifactIndexFile(
|
|
3038
|
-
kind:
|
|
3039
|
-
|
|
4460
|
+
kind:
|
|
4461
|
+
| "event"
|
|
4462
|
+
| "job"
|
|
4463
|
+
| "task"
|
|
4464
|
+
| "factory"
|
|
4465
|
+
| "seed"
|
|
4466
|
+
| "notification"
|
|
4467
|
+
| "listener"
|
|
4468
|
+
| "schedule"
|
|
4469
|
+
| "upload",
|
|
4470
|
+
names:
|
|
4471
|
+
| EventNames
|
|
4472
|
+
| JobNames
|
|
4473
|
+
| TaskNames
|
|
4474
|
+
| FactoryNames
|
|
4475
|
+
| SeedNames
|
|
4476
|
+
| NotificationNames
|
|
4477
|
+
| ListenerNames
|
|
4478
|
+
| ScheduleNames
|
|
4479
|
+
| UploadNames,
|
|
3040
4480
|
config: ResolvedBeignetConfig,
|
|
3041
4481
|
): FeatureArtifactIndexFile {
|
|
3042
4482
|
const artifactPath =
|
|
@@ -3044,31 +4484,63 @@ function featureArtifactIndexFile(
|
|
|
3044
4484
|
? eventFilePath(names as EventNames, config)
|
|
3045
4485
|
: kind === "job"
|
|
3046
4486
|
? jobFilePath(names as JobNames, config)
|
|
3047
|
-
: kind === "
|
|
3048
|
-
?
|
|
3049
|
-
:
|
|
4487
|
+
: kind === "task"
|
|
4488
|
+
? taskFilePath(names as TaskNames, config)
|
|
4489
|
+
: kind === "factory"
|
|
4490
|
+
? factoryFilePath(names as FactoryNames, config)
|
|
4491
|
+
: kind === "seed"
|
|
4492
|
+
? seedFilePath(names as SeedNames, config)
|
|
4493
|
+
: kind === "notification"
|
|
4494
|
+
? notificationFilePath(names as NotificationNames, config)
|
|
4495
|
+
: kind === "listener"
|
|
4496
|
+
? listenerFilePath(names as ListenerNames, config)
|
|
4497
|
+
: kind === "schedule"
|
|
4498
|
+
? scheduleFilePath(names as ScheduleNames, config)
|
|
4499
|
+
: uploadFilePath(names as UploadNames, config);
|
|
3050
4500
|
const registrySuffix =
|
|
3051
4501
|
kind === "event"
|
|
3052
4502
|
? "Events"
|
|
3053
4503
|
: kind === "job"
|
|
3054
4504
|
? "Jobs"
|
|
3055
|
-
: kind === "
|
|
3056
|
-
? "
|
|
3057
|
-
: "
|
|
4505
|
+
: kind === "task"
|
|
4506
|
+
? "Tasks"
|
|
4507
|
+
: kind === "factory"
|
|
4508
|
+
? "Factories"
|
|
4509
|
+
: kind === "seed"
|
|
4510
|
+
? "Seeds"
|
|
4511
|
+
: kind === "notification"
|
|
4512
|
+
? "Notifications"
|
|
4513
|
+
: kind === "listener"
|
|
4514
|
+
? "Listeners"
|
|
4515
|
+
: kind === "schedule"
|
|
4516
|
+
? "Schedules"
|
|
4517
|
+
: "Uploads";
|
|
3058
4518
|
const importName =
|
|
3059
4519
|
kind === "event"
|
|
3060
4520
|
? (names as EventNames).eventExportName
|
|
3061
4521
|
: kind === "job"
|
|
3062
4522
|
? (names as JobNames).jobExportName
|
|
3063
|
-
: kind === "
|
|
3064
|
-
? (names as
|
|
3065
|
-
:
|
|
4523
|
+
: kind === "task"
|
|
4524
|
+
? (names as TaskNames).taskExportName
|
|
4525
|
+
: kind === "factory"
|
|
4526
|
+
? (names as FactoryNames).factoryExportName
|
|
4527
|
+
: kind === "seed"
|
|
4528
|
+
? (names as SeedNames).seedExportName
|
|
4529
|
+
: kind === "notification"
|
|
4530
|
+
? (names as NotificationNames).notificationExportName
|
|
4531
|
+
: kind === "listener"
|
|
4532
|
+
? (names as ListenerNames).listenerExportName
|
|
4533
|
+
: kind === "schedule"
|
|
4534
|
+
? (names as ScheduleNames).scheduleExportName
|
|
4535
|
+
: (names as UploadNames).uploadExportName;
|
|
3066
4536
|
|
|
3067
4537
|
return {
|
|
3068
4538
|
path: path.join(path.dirname(artifactPath), "index.ts"),
|
|
4539
|
+
kind,
|
|
3069
4540
|
importName,
|
|
3070
4541
|
artifactPath,
|
|
3071
4542
|
registryName: `${names.featureSingularCamel}${registrySuffix}`,
|
|
4543
|
+
registryEntryName: names.artifact.camel,
|
|
3072
4544
|
};
|
|
3073
4545
|
}
|
|
3074
4546
|
|
|
@@ -3081,10 +4553,22 @@ async function updateFeatureArtifactIndex(
|
|
|
3081
4553
|
const existing = await readOptionalFile(destination);
|
|
3082
4554
|
const specifier = relativeModule(file.path, file.artifactPath);
|
|
3083
4555
|
const importLine = `import { ${file.importName} } from "${specifier}";`;
|
|
4556
|
+
const defineUploadsImport = `import { defineUploads } from "@beignet/core/uploads";`;
|
|
3084
4557
|
const exportLine = `export { ${file.importName} } from "${specifier}";`;
|
|
3085
4558
|
|
|
3086
4559
|
if (existing === undefined) {
|
|
3087
|
-
const content =
|
|
4560
|
+
const content =
|
|
4561
|
+
file.kind === "upload"
|
|
4562
|
+
? `${defineUploadsImport}
|
|
4563
|
+
${importLine}
|
|
4564
|
+
|
|
4565
|
+
${exportLine}
|
|
4566
|
+
|
|
4567
|
+
export const ${file.registryName} = defineUploads({
|
|
4568
|
+
\t${file.registryEntryName}: ${file.importName},
|
|
4569
|
+
});
|
|
4570
|
+
`
|
|
4571
|
+
: `${importLine}
|
|
3088
4572
|
|
|
3089
4573
|
${exportLine}
|
|
3090
4574
|
|
|
@@ -3114,13 +4598,123 @@ export const ${file.registryName} = [${file.importName}] as const;
|
|
|
3114
4598
|
: `${next.trimEnd()}\n${exportLine}\n`;
|
|
3115
4599
|
}
|
|
3116
4600
|
|
|
3117
|
-
|
|
4601
|
+
if (file.kind === "upload") {
|
|
4602
|
+
const registry = defineUploadsInitializerInfo(next, file.registryName);
|
|
4603
|
+
const arrayRegistry = arrayInitializerInfo(next, file.registryName);
|
|
4604
|
+
if (registry) {
|
|
4605
|
+
if (!next.includes(defineUploadsImport)) {
|
|
4606
|
+
next = insertAfterImports(next, defineUploadsImport);
|
|
4607
|
+
}
|
|
4608
|
+
if (!registry.text.includes(`${file.registryEntryName}:`)) {
|
|
4609
|
+
next = `${next.slice(0, registry.end)}\t${file.registryEntryName}: ${file.importName},\n${next.slice(
|
|
4610
|
+
registry.end,
|
|
4611
|
+
)}`;
|
|
4612
|
+
}
|
|
4613
|
+
} else if (arrayRegistry) {
|
|
4614
|
+
if (
|
|
4615
|
+
!identifiersFromArrayExpression(arrayRegistry.text).has(file.importName)
|
|
4616
|
+
) {
|
|
4617
|
+
const nextArray = appendToArrayExpression(arrayRegistry.text, [
|
|
4618
|
+
file.importName,
|
|
4619
|
+
]);
|
|
4620
|
+
next = `${next.slice(0, arrayRegistry.start)}${nextArray}${next.slice(
|
|
4621
|
+
arrayRegistry.end,
|
|
4622
|
+
)}`;
|
|
4623
|
+
}
|
|
4624
|
+
} else {
|
|
4625
|
+
if (!next.includes(defineUploadsImport)) {
|
|
4626
|
+
next = insertAfterImports(next, defineUploadsImport);
|
|
4627
|
+
}
|
|
4628
|
+
next = `${next.trimEnd()}\n\nexport const ${file.registryName} = defineUploads({\n\t${file.registryEntryName}: ${file.importName},\n});\n`;
|
|
4629
|
+
}
|
|
4630
|
+
} else {
|
|
4631
|
+
const registry = arrayInitializerInfo(next, file.registryName);
|
|
4632
|
+
if (!registry) {
|
|
4633
|
+
next = `${next.trimEnd()}\n\nexport const ${file.registryName} = [${file.importName}] as const;\n`;
|
|
4634
|
+
} else if (
|
|
4635
|
+
!identifiersFromArrayExpression(registry.text).has(file.importName)
|
|
4636
|
+
) {
|
|
4637
|
+
const nextArray = appendToArrayExpression(registry.text, [
|
|
4638
|
+
file.importName,
|
|
4639
|
+
]);
|
|
4640
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(
|
|
4641
|
+
registry.end,
|
|
4642
|
+
)}`;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
if (next === existing) return "skipped";
|
|
4647
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
4648
|
+
return "updated";
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
async function updateTaskRegistry(
|
|
4652
|
+
targetDir: string,
|
|
4653
|
+
names: TaskNames,
|
|
4654
|
+
config: ResolvedBeignetConfig,
|
|
4655
|
+
options: { dryRun: boolean },
|
|
4656
|
+
): Promise<WriteGeneratedFileResult> {
|
|
4657
|
+
const destination = path.join(targetDir, config.paths.tasks);
|
|
4658
|
+
const existing = await readOptionalFile(destination);
|
|
4659
|
+
const featureTasksPath = path.join(
|
|
4660
|
+
featureArtifactDir(names, "tasks", config),
|
|
4661
|
+
"index.ts",
|
|
4662
|
+
);
|
|
4663
|
+
const registryName = `${names.featureSingularCamel}Tasks`;
|
|
4664
|
+
const importLine = `import { ${registryName} } from "${relativeModule(
|
|
4665
|
+
config.paths.tasks,
|
|
4666
|
+
featureTasksPath,
|
|
4667
|
+
)}";`;
|
|
4668
|
+
const defineTasksImport = `import { defineTasks } from "@beignet/core/tasks";`;
|
|
4669
|
+
const serviceActorImport = `import { createServiceActor } from "@beignet/core/ports";`;
|
|
4670
|
+
const appContextImport = `import type { AppContext } from "${aliasModule(config.paths.appContext)}";`;
|
|
4671
|
+
const serverImport = `import { server } from "${relativeModule(
|
|
4672
|
+
config.paths.tasks,
|
|
4673
|
+
config.paths.server,
|
|
4674
|
+
)}";`;
|
|
4675
|
+
const registryEntry = `...${registryName}`;
|
|
4676
|
+
|
|
4677
|
+
if (existing === undefined) {
|
|
4678
|
+
const content = `${defineTasksImport}
|
|
4679
|
+
${serviceActorImport}
|
|
4680
|
+
${appContextImport}
|
|
4681
|
+
${importLine}
|
|
4682
|
+
${serverImport}
|
|
4683
|
+
|
|
4684
|
+
export const tasks = defineTasks([
|
|
4685
|
+
\t${registryEntry},
|
|
4686
|
+
] as const);
|
|
4687
|
+
|
|
4688
|
+
export async function createTaskContext(): Promise<AppContext> {
|
|
4689
|
+
\treturn server.createServiceContext({
|
|
4690
|
+
\t\tactor: createServiceActor("beignet-cli"),
|
|
4691
|
+
\t});
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
export async function stopTaskContext(): Promise<void> {
|
|
4695
|
+
\tawait server.stop();
|
|
4696
|
+
}
|
|
4697
|
+
`;
|
|
4698
|
+
if (!options.dryRun) {
|
|
4699
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
4700
|
+
await writeFile(destination, content);
|
|
4701
|
+
}
|
|
4702
|
+
return "created";
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
let next = existing;
|
|
4706
|
+
if (!next.includes(defineTasksImport)) {
|
|
4707
|
+
next = insertAfterImports(next, defineTasksImport);
|
|
4708
|
+
}
|
|
4709
|
+
if (!next.includes(importLine)) {
|
|
4710
|
+
next = insertAfterImports(next, importLine);
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
const registry = arrayInitializerInfo(next, "tasks");
|
|
3118
4714
|
if (!registry) {
|
|
3119
|
-
next = `${next.trimEnd()}\n\nexport const
|
|
3120
|
-
} else if (
|
|
3121
|
-
|
|
3122
|
-
) {
|
|
3123
|
-
const nextArray = appendToArrayExpression(registry.text, [file.importName]);
|
|
4715
|
+
next = `${next.trimEnd()}\n\nexport const tasks = defineTasks([\n\t${registryEntry},\n] as const);\n`;
|
|
4716
|
+
} else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
4717
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
3124
4718
|
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(
|
|
3125
4719
|
registry.end,
|
|
3126
4720
|
)}`;
|
|
@@ -3151,35 +4745,175 @@ function arrayInitializerInfo(
|
|
|
3151
4745
|
};
|
|
3152
4746
|
}
|
|
3153
4747
|
|
|
3154
|
-
function
|
|
4748
|
+
function defineUploadsInitializerInfo(
|
|
4749
|
+
source: string,
|
|
4750
|
+
constName: string,
|
|
4751
|
+
): { text: string; start: number; end: number } | undefined {
|
|
4752
|
+
const match = new RegExp(
|
|
4753
|
+
`\\bconst\\s+${constName}\\s*=\\s*defineUploads\\s*\\(`,
|
|
4754
|
+
).exec(source);
|
|
4755
|
+
if (!match) return undefined;
|
|
4756
|
+
|
|
4757
|
+
const openBrace = source.indexOf("{", match.index);
|
|
4758
|
+
if (openBrace === -1) return undefined;
|
|
4759
|
+
|
|
4760
|
+
const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
|
|
4761
|
+
if (closeBrace === -1) return undefined;
|
|
4762
|
+
|
|
4763
|
+
return {
|
|
4764
|
+
text: source.slice(openBrace, closeBrace + 1),
|
|
4765
|
+
start: openBrace,
|
|
4766
|
+
end: closeBrace,
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
function schemasFile(
|
|
4771
|
+
names: ResourceNames,
|
|
4772
|
+
mode: ResourceGenerationMode,
|
|
4773
|
+
options: ResourceGenerationOptions,
|
|
4774
|
+
): string {
|
|
3155
4775
|
return `import { z } from "zod";
|
|
3156
4776
|
|
|
3157
4777
|
export const ${names.singularPascal}Schema = z.object({
|
|
3158
4778
|
id: z.string().uuid(),
|
|
3159
|
-
name: z.string().min(1),
|
|
4779
|
+
${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
4780
|
+
version: z.number().int().min(1),
|
|
3160
4781
|
createdAt: z.string().datetime(),
|
|
4782
|
+
updatedAt: z.string().datetime(),
|
|
3161
4783
|
});
|
|
3162
4784
|
|
|
3163
|
-
export const
|
|
3164
|
-
|
|
3165
|
-
|
|
4785
|
+
export const ${names.singularPascal}SortBySchema = z.enum(["createdAt", "name"]);
|
|
4786
|
+
export const ${names.singularPascal}SortDirectionSchema = z.enum(["asc", "desc"]);
|
|
4787
|
+
|
|
4788
|
+
const ${names.singularPascal}CursorPayloadSchema = z.object({
|
|
4789
|
+
sortBy: ${names.singularPascal}SortBySchema,
|
|
4790
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema,
|
|
4791
|
+
sortValue: z.string(),
|
|
4792
|
+
id: z.string().uuid(),
|
|
3166
4793
|
});
|
|
3167
4794
|
|
|
4795
|
+
export type ${names.singularPascal}SortBy = z.infer<
|
|
4796
|
+
typeof ${names.singularPascal}SortBySchema
|
|
4797
|
+
>;
|
|
4798
|
+
export type ${names.singularPascal}SortDirection = z.infer<
|
|
4799
|
+
typeof ${names.singularPascal}SortDirectionSchema
|
|
4800
|
+
>;
|
|
4801
|
+
export type ${names.singularPascal}Cursor = z.infer<
|
|
4802
|
+
typeof ${names.singularPascal}CursorPayloadSchema
|
|
4803
|
+
>;
|
|
4804
|
+
|
|
4805
|
+
function encodeBase64Url(value: string): string {
|
|
4806
|
+
const bytes = new TextEncoder().encode(value);
|
|
4807
|
+
let binary = "";
|
|
4808
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
4809
|
+
|
|
4810
|
+
return btoa(binary)
|
|
4811
|
+
.replaceAll("+", "-")
|
|
4812
|
+
.replaceAll("/", "_")
|
|
4813
|
+
.replaceAll("=", "");
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4816
|
+
function decodeBase64Url(value: string): string {
|
|
4817
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
4818
|
+
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
|
|
4819
|
+
const binary = atob(base64 + padding);
|
|
4820
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
4821
|
+
|
|
4822
|
+
return new TextDecoder().decode(bytes);
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
export function encode${names.singularPascal}Cursor(cursor: ${names.singularPascal}Cursor): string {
|
|
4826
|
+
return encodeBase64Url(JSON.stringify(cursor));
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
export function decode${names.singularPascal}Cursor(cursor: string): ${names.singularPascal}Cursor {
|
|
4830
|
+
return ${names.singularPascal}CursorPayloadSchema.parse(
|
|
4831
|
+
JSON.parse(decodeBase64Url(cursor)),
|
|
4832
|
+
);
|
|
4833
|
+
}
|
|
4834
|
+
|
|
4835
|
+
export const List${names.pluralPascal}InputSchema = z
|
|
4836
|
+
.object({
|
|
4837
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
4838
|
+
cursor: z.string().nullable().default(null),
|
|
4839
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
4840
|
+
sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
|
|
4841
|
+
sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
|
|
4842
|
+
})
|
|
4843
|
+
.superRefine((input, ctx) => {
|
|
4844
|
+
if (!input.cursor) return;
|
|
4845
|
+
|
|
4846
|
+
try {
|
|
4847
|
+
const cursor = decode${names.singularPascal}Cursor(input.cursor);
|
|
4848
|
+
if (
|
|
4849
|
+
cursor.sortBy !== input.sortBy ||
|
|
4850
|
+
cursor.sortDirection !== input.sortDirection
|
|
4851
|
+
) {
|
|
4852
|
+
ctx.addIssue({
|
|
4853
|
+
code: z.ZodIssueCode.custom,
|
|
4854
|
+
message: "cursor does not match the requested sort.",
|
|
4855
|
+
path: ["cursor"],
|
|
4856
|
+
});
|
|
4857
|
+
}
|
|
4858
|
+
} catch {
|
|
4859
|
+
ctx.addIssue({
|
|
4860
|
+
code: z.ZodIssueCode.custom,
|
|
4861
|
+
message: "cursor is invalid.",
|
|
4862
|
+
path: ["cursor"],
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
});
|
|
4866
|
+
|
|
3168
4867
|
export const List${names.pluralPascal}OutputSchema = z.object({
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
4868
|
+
items: z.array(${names.singularPascal}Schema),
|
|
4869
|
+
page: z.object({
|
|
4870
|
+
kind: z.literal("cursor"),
|
|
4871
|
+
limit: z.number().int().min(1),
|
|
4872
|
+
cursor: z.string().nullable(),
|
|
4873
|
+
nextCursor: z.string().nullable(),
|
|
4874
|
+
hasMore: z.boolean(),
|
|
4875
|
+
}),
|
|
3173
4876
|
});
|
|
3174
4877
|
|
|
3175
4878
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
3176
4879
|
name: z.string().min(1).max(120),
|
|
3177
4880
|
});
|
|
4881
|
+
${
|
|
4882
|
+
mode === "resource"
|
|
4883
|
+
? `
|
|
4884
|
+
export const ${names.singularPascal}IdInputSchema = z.object({
|
|
4885
|
+
id: z.string().uuid(),
|
|
4886
|
+
});
|
|
4887
|
+
|
|
4888
|
+
export const Update${names.singularPascal}BodySchema = z.object({
|
|
4889
|
+
name: z.string().min(1).max(120),
|
|
4890
|
+
version: z.number().int().min(1),
|
|
4891
|
+
});
|
|
3178
4892
|
|
|
4893
|
+
export const Update${names.singularPascal}InputSchema =
|
|
4894
|
+
${names.singularPascal}IdInputSchema.merge(Update${names.singularPascal}BodySchema);
|
|
4895
|
+
`
|
|
4896
|
+
: ""
|
|
4897
|
+
}
|
|
3179
4898
|
export type ${names.singularPascal} = z.infer<typeof ${names.singularPascal}Schema>;
|
|
3180
4899
|
export type Create${names.singularPascal}Input = z.infer<
|
|
3181
4900
|
typeof Create${names.singularPascal}InputSchema
|
|
3182
4901
|
>;
|
|
4902
|
+
${
|
|
4903
|
+
mode === "resource"
|
|
4904
|
+
? `
|
|
4905
|
+
export type ${names.singularPascal}IdInput = z.infer<
|
|
4906
|
+
typeof ${names.singularPascal}IdInputSchema
|
|
4907
|
+
>;
|
|
4908
|
+
export type Update${names.singularPascal}Body = z.infer<
|
|
4909
|
+
typeof Update${names.singularPascal}BodySchema
|
|
4910
|
+
>;
|
|
4911
|
+
export type Update${names.singularPascal}Input = z.infer<
|
|
4912
|
+
typeof Update${names.singularPascal}InputSchema
|
|
4913
|
+
>;
|
|
4914
|
+
`
|
|
4915
|
+
: ""
|
|
4916
|
+
}
|
|
3183
4917
|
export type List${names.pluralPascal}Input = z.infer<
|
|
3184
4918
|
typeof List${names.pluralPascal}InputSchema
|
|
3185
4919
|
>;
|
|
@@ -3189,30 +4923,39 @@ export type List${names.pluralPascal}Input = z.infer<
|
|
|
3189
4923
|
function listUseCaseFile(
|
|
3190
4924
|
names: ResourceNames,
|
|
3191
4925
|
config: ResolvedBeignetConfig,
|
|
4926
|
+
options: ResourceGenerationOptions,
|
|
3192
4927
|
): string {
|
|
3193
4928
|
const filePath = path.join(
|
|
3194
4929
|
resourceUseCaseDir(names, config),
|
|
3195
4930
|
`list-${names.pluralKebab}.ts`,
|
|
3196
4931
|
);
|
|
3197
4932
|
|
|
3198
|
-
return `import {
|
|
4933
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
4934
|
+
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
4935
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3199
4936
|
import {
|
|
4937
|
+
decode${names.singularPascal}Cursor,
|
|
3200
4938
|
List${names.pluralPascal}InputSchema,
|
|
3201
4939
|
List${names.pluralPascal}OutputSchema,
|
|
3202
|
-
} from "
|
|
4940
|
+
} from "../schemas";
|
|
3203
4941
|
|
|
3204
4942
|
export const list${names.pluralPascal}UseCase = useCase
|
|
3205
4943
|
.query("${names.pluralCamel}.list")
|
|
3206
4944
|
.input(List${names.pluralPascal}InputSchema)
|
|
3207
4945
|
.output(List${names.pluralPascal}OutputSchema)
|
|
3208
4946
|
.run(async ({ ctx, input }) => {
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
4947
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to list ${names.pluralKebab}.");\n\t\t}\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
4948
|
+
defaultLimit: 20,
|
|
4949
|
+
maxLimit: 100,
|
|
4950
|
+
});
|
|
4951
|
+
|
|
4952
|
+
return ctx.ports.${names.pluralCamel}.list({
|
|
4953
|
+
page,
|
|
4954
|
+
cursor: page.cursor ? decode${names.singularPascal}Cursor(page.cursor) : null,
|
|
4955
|
+
name: input.name,
|
|
4956
|
+
sortBy: input.sortBy,
|
|
4957
|
+
sortDirection: input.sortDirection,
|
|
4958
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
3216
4959
|
});
|
|
3217
4960
|
`;
|
|
3218
4961
|
}
|
|
@@ -3220,135 +4963,430 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
3220
4963
|
function createUseCaseFile(
|
|
3221
4964
|
names: ResourceNames,
|
|
3222
4965
|
config: ResolvedBeignetConfig,
|
|
4966
|
+
options: ResourceGenerationOptions,
|
|
3223
4967
|
): string {
|
|
3224
4968
|
const filePath = path.join(
|
|
3225
4969
|
resourceUseCaseDir(names, config),
|
|
3226
4970
|
`create-${names.singularKebab}.ts`,
|
|
3227
4971
|
);
|
|
3228
4972
|
|
|
3229
|
-
return `import {
|
|
4973
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
4974
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3230
4975
|
import {
|
|
3231
4976
|
Create${names.singularPascal}InputSchema,
|
|
3232
4977
|
${names.singularPascal}Schema,
|
|
3233
|
-
} from "
|
|
4978
|
+
} from "../schemas";
|
|
4979
|
+
${options.events ? `import { ${names.singularPascal}Created } from "../domain/events";\n` : ""}
|
|
3234
4980
|
|
|
3235
4981
|
export const create${names.singularPascal}UseCase = useCase
|
|
3236
4982
|
.command("${names.pluralCamel}.create")
|
|
3237
4983
|
.input(Create${names.singularPascal}InputSchema)
|
|
3238
4984
|
.output(${names.singularPascal}Schema)
|
|
3239
|
-
.run(async ({ ctx, input }) =>
|
|
4985
|
+
.run(async ({ ctx, input }) => {
|
|
4986
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to create ${names.singularKebab}.");\n\t\t}\n\n` : ""}${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create({
|
|
4987
|
+
...input,
|
|
4988
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
4989
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
4990
|
+
return ${names.singularCamel};
|
|
4991
|
+
});
|
|
4992
|
+
`;
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
function getUseCaseFile(
|
|
4996
|
+
names: ResourceNames,
|
|
4997
|
+
config: ResolvedBeignetConfig,
|
|
4998
|
+
options: ResourceGenerationOptions,
|
|
4999
|
+
): string {
|
|
5000
|
+
const filePath = path.join(
|
|
5001
|
+
resourceUseCaseDir(names, config),
|
|
5002
|
+
`get-${names.singularKebab}.ts`,
|
|
5003
|
+
);
|
|
5004
|
+
|
|
5005
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5006
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5007
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5008
|
+
import {
|
|
5009
|
+
${names.singularPascal}IdInputSchema,
|
|
5010
|
+
${names.singularPascal}Schema,
|
|
5011
|
+
} from "../schemas";
|
|
5012
|
+
|
|
5013
|
+
export const get${names.singularPascal}UseCase = useCase
|
|
5014
|
+
.query("${names.pluralCamel}.get")
|
|
5015
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
5016
|
+
.output(${names.singularPascal}Schema)
|
|
5017
|
+
.run(async ({ ctx, input }) => {
|
|
5018
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to get ${names.singularKebab}.");\n\t\t}\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
5019
|
+
if (!${names.singularCamel}) {
|
|
5020
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5021
|
+
details: { id: input.id },
|
|
5022
|
+
});
|
|
5023
|
+
}
|
|
5024
|
+
|
|
5025
|
+
return ${names.singularCamel};
|
|
5026
|
+
});
|
|
5027
|
+
`;
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
function updateUseCaseFile(
|
|
5031
|
+
names: ResourceNames,
|
|
5032
|
+
config: ResolvedBeignetConfig,
|
|
5033
|
+
options: ResourceGenerationOptions,
|
|
5034
|
+
): string {
|
|
5035
|
+
const filePath = path.join(
|
|
5036
|
+
resourceUseCaseDir(names, config),
|
|
5037
|
+
`update-${names.singularKebab}.ts`,
|
|
5038
|
+
);
|
|
5039
|
+
|
|
5040
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5041
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5042
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5043
|
+
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
5044
|
+
Update${names.singularPascal}InputSchema,
|
|
5045
|
+
${names.singularPascal}Schema,
|
|
5046
|
+
} from "../schemas";
|
|
5047
|
+
|
|
5048
|
+
export const update${names.singularPascal}UseCase = useCase
|
|
5049
|
+
.command("${names.pluralCamel}.update")
|
|
5050
|
+
.input(Update${names.singularPascal}InputSchema)
|
|
5051
|
+
.output(${names.singularPascal}Schema)
|
|
5052
|
+
.run(async ({ ctx, input }) => {
|
|
5053
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to update ${names.singularKebab}.");\n\t\t}\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
5054
|
+
if (!existing) {
|
|
5055
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5056
|
+
details: { id: input.id },
|
|
5057
|
+
});
|
|
5058
|
+
}
|
|
5059
|
+
${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
5060
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
5061
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
5062
|
+
});
|
|
5063
|
+
}
|
|
5064
|
+
|
|
5065
|
+
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
5066
|
+
...input,
|
|
5067
|
+
${options.tenant ? `\t\t\ttenantId,\n` : ""} });
|
|
5068
|
+
if (!${names.singularCamel}) {
|
|
5069
|
+
throw appError("${names.singularPascal}Conflict", {
|
|
5070
|
+
details: { id: input.id, expectedVersion: existing.version },
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Updated, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
5074
|
+
return ${names.singularCamel};
|
|
5075
|
+
});
|
|
5076
|
+
`;
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
function deleteUseCaseFile(
|
|
5080
|
+
names: ResourceNames,
|
|
5081
|
+
config: ResolvedBeignetConfig,
|
|
5082
|
+
options: ResourceGenerationOptions,
|
|
5083
|
+
): string {
|
|
5084
|
+
const filePath = path.join(
|
|
5085
|
+
resourceUseCaseDir(names, config),
|
|
5086
|
+
`delete-${names.singularKebab}.ts`,
|
|
5087
|
+
);
|
|
5088
|
+
|
|
5089
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5090
|
+
import { z } from "zod";
|
|
5091
|
+
import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
5092
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
5093
|
+
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
5094
|
+
|
|
5095
|
+
export const delete${names.singularPascal}UseCase = useCase
|
|
5096
|
+
.command("${names.pluralCamel}.delete")
|
|
5097
|
+
.input(${names.singularPascal}IdInputSchema)
|
|
5098
|
+
.output(z.void())
|
|
5099
|
+
.run(async ({ ctx, input }) => {
|
|
5100
|
+
${options.tenant ? `\t\tconst tenantId = ctx.tenant?.id;\n\t\tif (!tenantId) {\n\t\t\tthrow new Error("Tenant is required to delete ${names.singularKebab}.");\n\t\t}\n\n` : ""}${options.auth ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", { tenantId }" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenant ? ", { tenantId }" : ""});
|
|
5101
|
+
if (!deleted) {
|
|
5102
|
+
throw appError("${names.singularPascal}NotFound", {
|
|
5103
|
+
details: { id: input.id },
|
|
5104
|
+
});
|
|
5105
|
+
}
|
|
5106
|
+
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Deleted, {\n\t\t\tid: input.id,\n\t\t});\n` : ""} });
|
|
3240
5107
|
`;
|
|
3241
5108
|
}
|
|
3242
5109
|
|
|
3243
|
-
function useCasesIndexFile(
|
|
5110
|
+
function useCasesIndexFile(
|
|
5111
|
+
names: ResourceNames,
|
|
5112
|
+
mode: ResourceGenerationMode,
|
|
5113
|
+
): string {
|
|
3244
5114
|
return `export { create${names.singularPascal}UseCase } from "./create-${names.singularKebab}";
|
|
3245
|
-
export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
3246
|
-
export {
|
|
5115
|
+
${mode === "resource" ? `export { delete${names.singularPascal}UseCase } from "./delete-${names.singularKebab}";\nexport { get${names.singularPascal}UseCase } from "./get-${names.singularKebab}";\n` : ""}export { list${names.pluralPascal}UseCase } from "./list-${names.pluralKebab}";
|
|
5116
|
+
${mode === "resource" ? `export { update${names.singularPascal}UseCase } from "./update-${names.singularKebab}";\n` : ""}export {
|
|
3247
5117
|
Create${names.singularPascal}InputSchema,
|
|
3248
|
-
List${names.pluralPascal}InputSchema,
|
|
5118
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n` : ""} List${names.pluralPascal}InputSchema,
|
|
3249
5119
|
List${names.pluralPascal}OutputSchema,
|
|
3250
|
-
${names.singularPascal}Schema,
|
|
5120
|
+
${mode === "resource" ? ` Update${names.singularPascal}BodySchema,\n Update${names.singularPascal}InputSchema,\n` : ""} ${names.singularPascal}Schema,
|
|
3251
5121
|
type Create${names.singularPascal}Input,
|
|
3252
|
-
type List${names.pluralPascal}Input,
|
|
5122
|
+
${mode === "resource" ? ` type ${names.singularPascal}IdInput,\n type Update${names.singularPascal}Body,\n type Update${names.singularPascal}Input,\n` : ""} type List${names.pluralPascal}Input,
|
|
3253
5123
|
type ${names.singularPascal},
|
|
3254
|
-
} from "
|
|
5124
|
+
} from "../schemas";
|
|
3255
5125
|
`;
|
|
3256
5126
|
}
|
|
3257
5127
|
|
|
3258
5128
|
function repositoryPortFile(
|
|
3259
5129
|
names: ResourceNames,
|
|
3260
5130
|
config: ResolvedBeignetConfig,
|
|
5131
|
+
mode: ResourceGenerationMode,
|
|
5132
|
+
options: ResourceGenerationOptions,
|
|
3261
5133
|
): string {
|
|
3262
5134
|
return `import type {
|
|
5135
|
+
CursorPage,
|
|
5136
|
+
CursorPageInfo,
|
|
5137
|
+
PageResult,
|
|
5138
|
+
SortDirection,
|
|
5139
|
+
} from "@beignet/core/pagination";
|
|
5140
|
+
import type {
|
|
3263
5141
|
Create${names.singularPascal}Input,
|
|
3264
|
-
|
|
3265
|
-
${names.singularPascal},
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
5142
|
+
${names.singularPascal}Cursor,
|
|
5143
|
+
${names.singularPascal}SortBy,
|
|
5144
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5145
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
5146
|
+
|
|
5147
|
+
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal}, CursorPageInfo>;
|
|
5148
|
+
export type List${names.pluralPascal}Query = {
|
|
5149
|
+
page: CursorPage;
|
|
5150
|
+
cursor: ${names.singularPascal}Cursor | null;
|
|
5151
|
+
name?: string;
|
|
5152
|
+
sortBy: ${names.singularPascal}SortBy;
|
|
5153
|
+
sortDirection: SortDirection;
|
|
5154
|
+
${options.tenant ? `\ttenantId: string;\n` : ""}};
|
|
5155
|
+
${options.tenant ? `export type ${names.singularPascal}TenantFilter = { tenantId: string };\nexport type Create${names.singularPascal}RepositoryInput = Create${names.singularPascal}Input & ${names.singularPascal}TenantFilter;\n${mode === "resource" ? `export type Update${names.singularPascal}RepositoryInput = Update${names.singularPascal}Input & ${names.singularPascal}TenantFilter;\n` : ""}` : ""}
|
|
3272
5156
|
|
|
3273
5157
|
export interface ${names.singularPascal}Repository {
|
|
3274
|
-
list(
|
|
3275
|
-
create(input: Create${names.singularPascal}Input): Promise<${names.singularPascal}>;
|
|
3276
|
-
}
|
|
5158
|
+
list(query: List${names.pluralPascal}Query): Promise<List${names.pluralPascal}Result>;
|
|
5159
|
+
create(input: ${options.tenant ? `Create${names.singularPascal}RepositoryInput` : `Create${names.singularPascal}Input`}): Promise<${names.singularPascal}>;
|
|
5160
|
+
${mode === "resource" ? ` findById(id: string${options.tenant ? `, filter: ${names.singularPascal}TenantFilter` : ""}): Promise<${names.singularPascal} | null>;\n update(input: ${options.tenant ? `Update${names.singularPascal}RepositoryInput` : `Update${names.singularPascal}Input`}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenant ? `, filter: ${names.singularPascal}TenantFilter` : ""}): Promise<boolean>;\n` : ""}}
|
|
3277
5161
|
`;
|
|
3278
5162
|
}
|
|
3279
5163
|
|
|
3280
5164
|
function inMemoryRepositoryFile(
|
|
3281
5165
|
names: ResourceNames,
|
|
3282
5166
|
config: ResolvedBeignetConfig,
|
|
5167
|
+
mode: ResourceGenerationMode,
|
|
5168
|
+
options: ResourceGenerationOptions,
|
|
3283
5169
|
): string {
|
|
3284
5170
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
5171
|
+
const recordType = options.softDelete
|
|
5172
|
+
? `type ${names.singularPascal}Record = ${names.singularPascal} & {
|
|
5173
|
+
\tdeletedAt: string | null;
|
|
5174
|
+
};
|
|
3285
5175
|
|
|
3286
|
-
|
|
5176
|
+
`
|
|
5177
|
+
: "";
|
|
5178
|
+
const mapValueType = options.softDelete
|
|
5179
|
+
? `${names.singularPascal}Record`
|
|
5180
|
+
: names.singularPascal;
|
|
5181
|
+
const activeFilter = options.softDelete
|
|
5182
|
+
? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.deletedAt === null)\n`
|
|
5183
|
+
: "";
|
|
5184
|
+
const newDeletedAtField = options.softDelete
|
|
5185
|
+
? `\t\t\t\tdeletedAt: null,\n`
|
|
5186
|
+
: "";
|
|
5187
|
+
const findDeletedGuard = options.softDelete
|
|
5188
|
+
? `\t\t\tif (${names.singularCamel}?.deletedAt !== null) return null;\n`
|
|
5189
|
+
: "";
|
|
5190
|
+
const updateDeletedGuard = options.softDelete
|
|
5191
|
+
? " || existing.deletedAt !== null"
|
|
5192
|
+
: "";
|
|
5193
|
+
const deleteImplementation = options.softDelete
|
|
5194
|
+
? `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== filter.tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
|
|
5195
|
+
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
5196
|
+
return true;`
|
|
5197
|
+
: `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== filter.tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
|
|
5198
|
+
|
|
5199
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5200
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
5201
|
+
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
5202
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3287
5203
|
import type {
|
|
3288
5204
|
Create${names.singularPascal}Input,
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
5205
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5206
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
5207
|
+
|
|
5208
|
+
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
5209
|
+
return {
|
|
5210
|
+
id: ${names.singularCamel}.id,
|
|
5211
|
+
${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
5212
|
+
version: ${names.singularCamel}.version,
|
|
5213
|
+
createdAt: ${names.singularCamel}.createdAt,
|
|
5214
|
+
updatedAt: ${names.singularCamel}.updatedAt,
|
|
5215
|
+
};
|
|
5216
|
+
}
|
|
5217
|
+
|
|
5218
|
+
function compare${names.pluralPascal}(
|
|
5219
|
+
left: ${mapValueType},
|
|
5220
|
+
right: ${mapValueType},
|
|
5221
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5222
|
+
): number {
|
|
5223
|
+
const leftValue = left[query.sortBy];
|
|
5224
|
+
const rightValue = right[query.sortBy];
|
|
5225
|
+
const fieldComparison =
|
|
5226
|
+
leftValue === rightValue ? left.id.localeCompare(right.id) : leftValue.localeCompare(rightValue);
|
|
5227
|
+
|
|
5228
|
+
return query.sortDirection === "asc" ? fieldComparison : -fieldComparison;
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
function isAfter${names.singularPascal}Cursor(
|
|
5232
|
+
${names.singularCamel}: ${mapValueType},
|
|
5233
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5234
|
+
): boolean {
|
|
5235
|
+
if (!query.cursor) return true;
|
|
5236
|
+
|
|
5237
|
+
const sortValue = ${names.singularCamel}[query.sortBy];
|
|
5238
|
+
const fieldComparison =
|
|
5239
|
+
sortValue === query.cursor.sortValue
|
|
5240
|
+
? ${names.singularCamel}.id.localeCompare(query.cursor.id)
|
|
5241
|
+
: sortValue.localeCompare(query.cursor.sortValue);
|
|
5242
|
+
|
|
5243
|
+
return query.sortDirection === "asc" ? fieldComparison > 0 : fieldComparison < 0;
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
function cursorFor${names.singularPascal}(
|
|
5247
|
+
${names.singularCamel}: ${mapValueType},
|
|
5248
|
+
query: Parameters<${names.singularPascal}Repository["list"]>[0],
|
|
5249
|
+
): string {
|
|
5250
|
+
return encode${names.singularPascal}Cursor({
|
|
5251
|
+
sortBy: query.sortBy,
|
|
5252
|
+
sortDirection: query.sortDirection,
|
|
5253
|
+
sortValue: ${names.singularCamel}[query.sortBy],
|
|
5254
|
+
id: ${names.singularCamel}.id,
|
|
5255
|
+
});
|
|
5256
|
+
}
|
|
3292
5257
|
|
|
3293
5258
|
export function createInMemory${names.singularPascal}Repository(
|
|
3294
5259
|
seed: ${names.singularPascal}[] = [],
|
|
3295
5260
|
): ${names.singularPascal}Repository {
|
|
3296
|
-
const ${names.pluralCamel} = new Map
|
|
5261
|
+
const ${names.pluralCamel} = new Map<string, ${mapValueType}>(
|
|
5262
|
+
seed.map((${names.singularCamel}) => [
|
|
5263
|
+
${names.singularCamel}.id,
|
|
5264
|
+
{ ...${names.singularCamel}${options.softDelete ? ", deletedAt: null" : ""} },
|
|
5265
|
+
]),
|
|
5266
|
+
);
|
|
3297
5267
|
|
|
3298
5268
|
return {
|
|
3299
|
-
async list(
|
|
3300
|
-
const
|
|
3301
|
-
|
|
5269
|
+
async list(query) {
|
|
5270
|
+
const name = query.name?.toLocaleLowerCase();
|
|
5271
|
+
const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
5272
|
+
${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === query.tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
|
|
5273
|
+
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
5274
|
+
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
5275
|
+
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
5276
|
+
const nextCursor =
|
|
5277
|
+
all${names.pluralPascal}.length > query.page.limit && pageItems.length > 0
|
|
5278
|
+
? cursorFor${names.singularPascal}(pageItems[pageItems.length - 1]!, query)
|
|
5279
|
+
: null;
|
|
5280
|
+
|
|
5281
|
+
return cursorPageResult(
|
|
5282
|
+
pageItems.map(to${names.singularPascal}),
|
|
5283
|
+
query.page,
|
|
5284
|
+
nextCursor,
|
|
3302
5285
|
);
|
|
3303
|
-
|
|
3304
|
-
return {
|
|
3305
|
-
${names.pluralCamel}: all${names.pluralPascal}.slice(
|
|
3306
|
-
input.offset,
|
|
3307
|
-
input.offset + input.limit,
|
|
3308
|
-
),
|
|
3309
|
-
total: all${names.pluralPascal}.length,
|
|
3310
|
-
};
|
|
3311
5286
|
},
|
|
3312
|
-
async create(input
|
|
5287
|
+
async create(input) {
|
|
5288
|
+
const now = new Date().toISOString();
|
|
3313
5289
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
3314
5290
|
id: crypto.randomUUID(),
|
|
3315
|
-
name: input.name,
|
|
3316
|
-
|
|
5291
|
+
${options.tenant ? `\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
5292
|
+
version: 1,
|
|
5293
|
+
createdAt: now,
|
|
5294
|
+
updatedAt: now,
|
|
3317
5295
|
};
|
|
3318
|
-
${names.pluralCamel}.set(${names.singularCamel}.id,
|
|
5296
|
+
${names.pluralCamel}.set(${names.singularCamel}.id, {
|
|
5297
|
+
...${names.singularCamel},
|
|
5298
|
+
${newDeletedAtField} });
|
|
3319
5299
|
return ${names.singularCamel};
|
|
3320
5300
|
},
|
|
5301
|
+
${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {\n const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenant ? `\t\t\tif (${names.singularCamel}?.tenantId !== filter.tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input) {\n const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenant ? " || existing.tenantId !== input.tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenant ? ", filter" : ""}) {\n${deleteImplementation}\n },\n` : ""}
|
|
3321
5302
|
};
|
|
3322
5303
|
}
|
|
3323
5304
|
`;
|
|
3324
5305
|
}
|
|
3325
5306
|
|
|
3326
|
-
function drizzleSchemaFile(
|
|
3327
|
-
|
|
5307
|
+
function drizzleSchemaFile(
|
|
5308
|
+
names: ResourceNames,
|
|
5309
|
+
options: ResourceGenerationOptions,
|
|
5310
|
+
): string {
|
|
5311
|
+
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
3328
5312
|
|
|
3329
5313
|
export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
3330
5314
|
id: text("id").primaryKey(),
|
|
3331
|
-
name: text("name").notNull(),
|
|
5315
|
+
${options.tenant ? `\ttenantId: text("tenant_id").notNull(),\n` : ""} name: text("name").notNull(),
|
|
5316
|
+
version: integer("version").notNull(),
|
|
3332
5317
|
createdAt: text("created_at").notNull(),
|
|
3333
|
-
|
|
5318
|
+
updatedAt: text("updated_at").notNull(),
|
|
5319
|
+
${options.softDelete ? `\tdeletedAt: text("deleted_at"),\n` : ""}});
|
|
3334
5320
|
`;
|
|
3335
5321
|
}
|
|
3336
5322
|
|
|
5323
|
+
function drizzleResourceWhere(
|
|
5324
|
+
names: ResourceNames,
|
|
5325
|
+
options: ResourceGenerationOptions,
|
|
5326
|
+
predicates: readonly string[],
|
|
5327
|
+
): string {
|
|
5328
|
+
const allPredicates = [
|
|
5329
|
+
...predicates,
|
|
5330
|
+
...(options.softDelete
|
|
5331
|
+
? [`isNull(schema.${names.pluralCamel}.deletedAt)`]
|
|
5332
|
+
: []),
|
|
5333
|
+
];
|
|
5334
|
+
|
|
5335
|
+
if (allPredicates.length === 0) return "";
|
|
5336
|
+
if (allPredicates.length === 1) return allPredicates[0] ?? "";
|
|
5337
|
+
return `and(${allPredicates.join(", ")})`;
|
|
5338
|
+
}
|
|
5339
|
+
|
|
3337
5340
|
function drizzleRepositoryFile(
|
|
3338
5341
|
names: ResourceNames,
|
|
3339
5342
|
config: ResolvedBeignetConfig,
|
|
5343
|
+
mode: ResourceGenerationMode,
|
|
5344
|
+
options: ResourceGenerationOptions,
|
|
3340
5345
|
): string {
|
|
3341
5346
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
3342
5347
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
5348
|
+
const findWhere = drizzleResourceWhere(names, options, [
|
|
5349
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
5350
|
+
...(options.tenant
|
|
5351
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
5352
|
+
: []),
|
|
5353
|
+
]);
|
|
5354
|
+
const updateWhere = drizzleResourceWhere(names, options, [
|
|
5355
|
+
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
5356
|
+
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
5357
|
+
...(options.tenant
|
|
5358
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, input.tenantId)`]
|
|
5359
|
+
: []),
|
|
5360
|
+
]);
|
|
5361
|
+
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
5362
|
+
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
5363
|
+
...(options.tenant
|
|
5364
|
+
? [`eq(schema.${names.pluralCamel}.tenantId, filter.tenantId)`]
|
|
5365
|
+
: []),
|
|
5366
|
+
]);
|
|
5367
|
+
const drizzleImports = [
|
|
5368
|
+
"and",
|
|
5369
|
+
"asc",
|
|
5370
|
+
"desc",
|
|
5371
|
+
"eq",
|
|
5372
|
+
"gt",
|
|
5373
|
+
options.softDelete ? "isNull" : undefined,
|
|
5374
|
+
"lt",
|
|
5375
|
+
"or",
|
|
5376
|
+
"sql",
|
|
5377
|
+
"type SQL",
|
|
5378
|
+
].filter((name): name is string => Boolean(name));
|
|
5379
|
+
|
|
5380
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5381
|
+
import { cursorPageResult } from "@beignet/core/pagination";
|
|
5382
|
+
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
5383
|
+
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
3346
5384
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
5385
|
+
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3347
5386
|
import type {
|
|
3348
5387
|
Create${names.singularPascal}Input,
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
} from "${aliasModule(path.join(resourceUseCaseDir(names, config), "schemas.ts"))}";
|
|
5388
|
+
${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.singularPascal},
|
|
5389
|
+
} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3352
5390
|
import * as schema from "${aliasModule(schemaPath)}";
|
|
3353
5391
|
|
|
3354
5392
|
type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelect;
|
|
@@ -3356,38 +5394,104 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
3356
5394
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
3357
5395
|
return {
|
|
3358
5396
|
id: row.id,
|
|
3359
|
-
name: row.name,
|
|
5397
|
+
${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
5398
|
+
version: row.version,
|
|
3360
5399
|
createdAt: row.createdAt,
|
|
5400
|
+
updatedAt: row.updatedAt,
|
|
3361
5401
|
};
|
|
3362
5402
|
}
|
|
3363
5403
|
|
|
5404
|
+
type List${names.pluralPascal}Query = Parameters<${names.singularPascal}Repository["list"]>[0];
|
|
5405
|
+
|
|
5406
|
+
function ${names.singularCamel}SortColumn(query: List${names.pluralPascal}Query) {
|
|
5407
|
+
return query.sortBy === "name" ? schema.${names.pluralCamel}.name : schema.${names.pluralCamel}.createdAt;
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
function ${names.singularCamel}CursorFilter(
|
|
5411
|
+
query: List${names.pluralPascal}Query,
|
|
5412
|
+
): SQL<unknown> | undefined {
|
|
5413
|
+
if (!query.cursor) return undefined;
|
|
5414
|
+
|
|
5415
|
+
const column = ${names.singularCamel}SortColumn(query);
|
|
5416
|
+
const compare = query.sortDirection === "asc" ? gt : lt;
|
|
5417
|
+
|
|
5418
|
+
return or(
|
|
5419
|
+
compare(column, query.cursor.sortValue),
|
|
5420
|
+
and(
|
|
5421
|
+
eq(column, query.cursor.sortValue),
|
|
5422
|
+
compare(schema.${names.pluralCamel}.id, query.cursor.id),
|
|
5423
|
+
),
|
|
5424
|
+
) as SQL<unknown>;
|
|
5425
|
+
}
|
|
5426
|
+
|
|
5427
|
+
function ${names.singularCamel}ListWhere(
|
|
5428
|
+
query: List${names.pluralPascal}Query,
|
|
5429
|
+
): SQL<unknown> | undefined {
|
|
5430
|
+
const filters: SQL<unknown>[] = [
|
|
5431
|
+
${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, query.tenantId),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
5432
|
+
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
5433
|
+
|
|
5434
|
+
if (query.name) {
|
|
5435
|
+
const pattern = \`%\${query.name.toLocaleLowerCase()}%\`;
|
|
5436
|
+
filters.push(sql\`lower(\${schema.${names.pluralCamel}.name}) like \${pattern}\`);
|
|
5437
|
+
}
|
|
5438
|
+
if (cursor) filters.push(cursor);
|
|
5439
|
+
|
|
5440
|
+
return filters.length > 0 ? and(...filters) : undefined;
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
function ${names.singularCamel}OrderBy(query: List${names.pluralPascal}Query) {
|
|
5444
|
+
const order = query.sortDirection === "asc" ? asc : desc;
|
|
5445
|
+
|
|
5446
|
+
return [order(${names.singularCamel}SortColumn(query)), order(schema.${names.pluralCamel}.id)] as const;
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
function cursorFor${names.singularPascal}(
|
|
5450
|
+
row: ${names.singularPascal}Row,
|
|
5451
|
+
query: List${names.pluralPascal}Query,
|
|
5452
|
+
): string {
|
|
5453
|
+
return encode${names.singularPascal}Cursor({
|
|
5454
|
+
sortBy: query.sortBy,
|
|
5455
|
+
sortDirection: query.sortDirection,
|
|
5456
|
+
sortValue: row[query.sortBy],
|
|
5457
|
+
id: row.id,
|
|
5458
|
+
});
|
|
5459
|
+
}
|
|
5460
|
+
|
|
3364
5461
|
export function createDrizzle${names.singularPascal}Repository(
|
|
3365
5462
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
3366
5463
|
): ${names.singularPascal}Repository {
|
|
3367
5464
|
return {
|
|
3368
|
-
async list(
|
|
5465
|
+
async list(query) {
|
|
5466
|
+
const where = ${names.singularCamel}ListWhere(query);
|
|
3369
5467
|
const rows = await db
|
|
3370
5468
|
.select()
|
|
3371
5469
|
.from(schema.${names.pluralCamel})
|
|
3372
|
-
.
|
|
3373
|
-
.
|
|
3374
|
-
.
|
|
3375
|
-
const
|
|
3376
|
-
|
|
3377
|
-
.
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
5470
|
+
.where(where)
|
|
5471
|
+
.orderBy(...${names.singularCamel}OrderBy(query))
|
|
5472
|
+
.limit(query.page.limit + 1);
|
|
5473
|
+
const pageRows = rows.slice(0, query.page.limit);
|
|
5474
|
+
const nextCursor =
|
|
5475
|
+
rows.length > query.page.limit && pageRows.length > 0
|
|
5476
|
+
? cursorFor${names.singularPascal}(pageRows[pageRows.length - 1]!, query)
|
|
5477
|
+
: null;
|
|
5478
|
+
|
|
5479
|
+
return cursorPageResult(
|
|
5480
|
+
pageRows.map(to${names.singularPascal}),
|
|
5481
|
+
query.page,
|
|
5482
|
+
nextCursor,
|
|
5483
|
+
);
|
|
3383
5484
|
},
|
|
3384
|
-
async create(input
|
|
5485
|
+
async create(input) {
|
|
5486
|
+
const now = new Date().toISOString();
|
|
3385
5487
|
const [row] = await db
|
|
3386
5488
|
.insert(schema.${names.pluralCamel})
|
|
3387
5489
|
.values({
|
|
3388
5490
|
id: crypto.randomUUID(),
|
|
3389
|
-
name: input.name,
|
|
3390
|
-
|
|
5491
|
+
${options.tenant ? `\t\t\t\t\ttenantId: input.tenantId,\n` : ""} name: input.name,
|
|
5492
|
+
version: 1,
|
|
5493
|
+
createdAt: now,
|
|
5494
|
+
updatedAt: now,
|
|
3391
5495
|
})
|
|
3392
5496
|
.returning();
|
|
3393
5497
|
|
|
@@ -3397,6 +5501,7 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
3397
5501
|
|
|
3398
5502
|
return to${names.singularPascal}(row);
|
|
3399
5503
|
},
|
|
5504
|
+
${mode === "resource" ? ` async findById(id: string${options.tenant ? ", filter" : ""}) {\n const [row] = await db\n .select()\n .from(schema.${names.pluralCamel})\n .where(${findWhere})\n .limit(1);\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async update(input) {\n const [row] = await db\n .update(schema.${names.pluralCamel})\n .set({\n name: input.name,\n version: input.version + 1,\n updatedAt: new Date().toISOString(),\n })\n .where(${updateWhere})\n .returning();\n\n return row ? to${names.singularPascal}(row) : null;\n },\n async delete(id: string${options.tenant ? ", filter" : ""}) {\n${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}\n return rows.length > 0;\n },\n` : ""}
|
|
3400
5505
|
};
|
|
3401
5506
|
}
|
|
3402
5507
|
`;
|
|
@@ -3405,13 +5510,17 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
3405
5510
|
function contractFile(
|
|
3406
5511
|
names: ResourceNames,
|
|
3407
5512
|
config: ResolvedBeignetConfig,
|
|
5513
|
+
mode: ResourceGenerationMode,
|
|
5514
|
+
options: ResourceGenerationOptions,
|
|
3408
5515
|
): string {
|
|
3409
|
-
return `import {
|
|
5516
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
3410
5517
|
import { z } from "zod";
|
|
3411
|
-
import {
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
5518
|
+
${mode === "resource" ? `import { errors } from "${aliasModule(resourceSharedErrorsPath(config))}";\n` : ""}import {
|
|
5519
|
+
Create${names.singularPascal}InputSchema,
|
|
5520
|
+
List${names.pluralPascal}InputSchema,
|
|
5521
|
+
List${names.pluralPascal}OutputSchema,
|
|
5522
|
+
${names.singularPascal}Schema,
|
|
5523
|
+
${mode === "resource" ? ` ${names.singularPascal}IdInputSchema,\n Update${names.singularPascal}BodySchema,\n` : ""}} from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3415
5524
|
|
|
3416
5525
|
const ErrorResponseSchema = z.object({
|
|
3417
5526
|
code: z.string(),
|
|
@@ -3419,7 +5528,7 @@ const ErrorResponseSchema = z.object({
|
|
|
3419
5528
|
requestId: z.string().optional(),
|
|
3420
5529
|
});
|
|
3421
5530
|
|
|
3422
|
-
const ${names.pluralCamel} =
|
|
5531
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
3423
5532
|
.namespace("${names.pluralCamel}")
|
|
3424
5533
|
.responses({
|
|
3425
5534
|
500: ErrorResponseSchema,
|
|
@@ -3427,22 +5536,52 @@ const ${names.pluralCamel} = createContractGroup()
|
|
|
3427
5536
|
|
|
3428
5537
|
export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
3429
5538
|
.get("/api/${names.pluralKebab}")
|
|
3430
|
-
.query(
|
|
5539
|
+
.query(List${names.pluralPascal}InputSchema)
|
|
3431
5540
|
.responses({
|
|
3432
|
-
200:
|
|
5541
|
+
200: List${names.pluralPascal}OutputSchema,
|
|
3433
5542
|
});
|
|
3434
5543
|
|
|
3435
5544
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
3436
5545
|
.post("/api/${names.pluralKebab}")
|
|
3437
|
-
.body(
|
|
5546
|
+
.body(Create${names.singularPascal}InputSchema)
|
|
5547
|
+
${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
5548
|
+
201: ${names.singularPascal}Schema,
|
|
5549
|
+
});
|
|
5550
|
+
${
|
|
5551
|
+
mode === "resource"
|
|
5552
|
+
? `
|
|
5553
|
+
export const get${names.singularPascal} = ${names.pluralCamel}
|
|
5554
|
+
.get("/api/${names.pluralKebab}/:id")
|
|
5555
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5556
|
+
.errors({ ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
5557
|
+
.responses({
|
|
5558
|
+
200: ${names.singularPascal}Schema,
|
|
5559
|
+
});
|
|
5560
|
+
|
|
5561
|
+
export const update${names.singularPascal} = ${names.pluralCamel}
|
|
5562
|
+
.patch("/api/${names.pluralKebab}/:id")
|
|
5563
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5564
|
+
.body(Update${names.singularPascal}BodySchema)
|
|
5565
|
+
${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
|
|
3438
5566
|
.responses({
|
|
3439
|
-
|
|
5567
|
+
200: ${names.singularPascal}Schema,
|
|
3440
5568
|
});
|
|
5569
|
+
|
|
5570
|
+
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
5571
|
+
.delete("/api/${names.pluralKebab}/:id")
|
|
5572
|
+
.pathParams(${names.singularPascal}IdInputSchema)
|
|
5573
|
+
${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
5574
|
+
.responses({
|
|
5575
|
+
204: null,
|
|
5576
|
+
});
|
|
5577
|
+
`
|
|
5578
|
+
: ""
|
|
5579
|
+
}
|
|
3441
5580
|
`;
|
|
3442
5581
|
}
|
|
3443
5582
|
|
|
3444
5583
|
function standaloneContractFile(names: ResourceNames): string {
|
|
3445
|
-
return `import {
|
|
5584
|
+
return `import { defineContractGroup } from "@beignet/core/contracts";
|
|
3446
5585
|
import { z } from "zod";
|
|
3447
5586
|
|
|
3448
5587
|
const ErrorResponseSchema = z.object({
|
|
@@ -3451,7 +5590,7 @@ const ErrorResponseSchema = z.object({
|
|
|
3451
5590
|
requestId: z.string().optional(),
|
|
3452
5591
|
});
|
|
3453
5592
|
|
|
3454
|
-
const ${names.pluralCamel} =
|
|
5593
|
+
const ${names.pluralCamel} = defineContractGroup()
|
|
3455
5594
|
.namespace("${names.pluralCamel}")
|
|
3456
5595
|
.responses({
|
|
3457
5596
|
500: ErrorResponseSchema,
|
|
@@ -3472,10 +5611,14 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
3472
5611
|
)
|
|
3473
5612
|
.responses({
|
|
3474
5613
|
200: z.object({
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
5614
|
+
items: z.array(${names.singularCamel}Schema),
|
|
5615
|
+
page: z.object({
|
|
5616
|
+
kind: z.literal("offset"),
|
|
5617
|
+
limit: z.number().int().min(1),
|
|
5618
|
+
offset: z.number().int().min(0),
|
|
5619
|
+
total: z.number().int().min(0),
|
|
5620
|
+
hasMore: z.boolean(),
|
|
5621
|
+
}),
|
|
3479
5622
|
}),
|
|
3480
5623
|
});
|
|
3481
5624
|
|
|
@@ -3488,7 +5631,8 @@ function standaloneUseCaseFile(
|
|
|
3488
5631
|
config: ResolvedBeignetConfig,
|
|
3489
5632
|
filePath: string,
|
|
3490
5633
|
): string {
|
|
3491
|
-
return `import {
|
|
5634
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
5635
|
+
import { z } from "zod";
|
|
3492
5636
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3493
5637
|
|
|
3494
5638
|
export const ${names.action.pascal}InputSchema = z.object({});
|
|
@@ -3521,29 +5665,27 @@ function useCaseTestFile(
|
|
|
3521
5665
|
return `import { describe, expect, it } from "bun:test";
|
|
3522
5666
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
3523
5667
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3524
|
-
import {
|
|
5668
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
5669
|
+
import { createTestAnonymousActor } from "@beignet/core/ports/testing";
|
|
3525
5670
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3526
5671
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
3527
5672
|
import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
|
|
3528
5673
|
|
|
3529
5674
|
describe("${names.exportName}", () => {
|
|
3530
5675
|
it("runs ${names.action.camel}", async () => {
|
|
3531
|
-
const
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
gate: testPorts.gate.bind({ actor, auth: null }),
|
|
3545
|
-
ports: testPorts,
|
|
3546
|
-
}));
|
|
5676
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
5677
|
+
base: appPorts,
|
|
5678
|
+
overrides: {
|
|
5679
|
+
gate: appPorts.gate,
|
|
5680
|
+
devtools: createInMemoryDevtools(),
|
|
5681
|
+
},
|
|
5682
|
+
});
|
|
5683
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
5684
|
+
ports: testFixture.ports,
|
|
5685
|
+
actor: createTestAnonymousActor(),
|
|
5686
|
+
tenant: null,
|
|
5687
|
+
});
|
|
5688
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
3547
5689
|
|
|
3548
5690
|
const result = await tester.run(${names.exportName}, {});
|
|
3549
5691
|
|
|
@@ -3609,6 +5751,144 @@ export const ${names.policyName} = definePolicy({
|
|
|
3609
5751
|
`;
|
|
3610
5752
|
}
|
|
3611
5753
|
|
|
5754
|
+
function resourcePolicyFile(
|
|
5755
|
+
names: ResourceNames,
|
|
5756
|
+
_config: ResolvedBeignetConfig,
|
|
5757
|
+
options: ResourceGenerationOptions,
|
|
5758
|
+
): string {
|
|
5759
|
+
return `import type { ActivityActor, ActivityTenant } from "@beignet/core/ports";
|
|
5760
|
+
import { definePolicy, deny } from "@beignet/core/ports";
|
|
5761
|
+
|
|
5762
|
+
export type AuthorizationContext = {
|
|
5763
|
+
actor: ActivityActor;
|
|
5764
|
+
tenant?: ActivityTenant;
|
|
5765
|
+
};
|
|
5766
|
+
|
|
5767
|
+
type ${names.singularPascal}PolicyResource = {
|
|
5768
|
+
id: string;
|
|
5769
|
+
tenantId?: string;
|
|
5770
|
+
};
|
|
5771
|
+
|
|
5772
|
+
function isAuthenticated(ctx: AuthorizationContext) {
|
|
5773
|
+
return ctx.actor.type === "user" && Boolean(ctx.actor.id);
|
|
5774
|
+
}
|
|
5775
|
+
|
|
5776
|
+
function canWrite(ctx: AuthorizationContext) {
|
|
5777
|
+
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
5778
|
+
${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
export const ${names.singularCamel}Policy = definePolicy({
|
|
5782
|
+
"${names.pluralCamel}.view": (_ctx: AuthorizationContext) => true,
|
|
5783
|
+
"${names.pluralCamel}.create": (ctx: AuthorizationContext) =>
|
|
5784
|
+
canWrite(ctx),
|
|
5785
|
+
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
5786
|
+
const decision = canWrite(ctx);
|
|
5787
|
+
if (decision !== true) return decision;
|
|
5788
|
+
${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
5789
|
+
return true;
|
|
5790
|
+
},
|
|
5791
|
+
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
5792
|
+
const decision = canWrite(ctx);
|
|
5793
|
+
if (decision !== true) return decision;
|
|
5794
|
+
${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
5795
|
+
return true;
|
|
5796
|
+
},
|
|
5797
|
+
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
5798
|
+
deny("You are not allowed to manage this ${names.singularKebab}."),
|
|
5799
|
+
});
|
|
5800
|
+
`;
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
function resourceEventsFile(names: ResourceNames): string {
|
|
5804
|
+
return `import { defineEvent } from "@beignet/core/events";
|
|
5805
|
+
import { z } from "zod";
|
|
5806
|
+
|
|
5807
|
+
export const ${names.singularPascal}Created = defineEvent("${names.pluralCamel}.created", {
|
|
5808
|
+
payload: z.object({
|
|
5809
|
+
id: z.string().uuid(),
|
|
5810
|
+
}),
|
|
5811
|
+
});
|
|
5812
|
+
|
|
5813
|
+
export const ${names.singularPascal}Updated = defineEvent("${names.pluralCamel}.updated", {
|
|
5814
|
+
payload: z.object({
|
|
5815
|
+
id: z.string().uuid(),
|
|
5816
|
+
}),
|
|
5817
|
+
});
|
|
5818
|
+
|
|
5819
|
+
export const ${names.singularPascal}Deleted = defineEvent("${names.pluralCamel}.deleted", {
|
|
5820
|
+
payload: z.object({
|
|
5821
|
+
id: z.string().uuid(),
|
|
5822
|
+
}),
|
|
5823
|
+
});
|
|
5824
|
+
|
|
5825
|
+
export const ${names.singularCamel}Events = [
|
|
5826
|
+
${names.singularPascal}Created,
|
|
5827
|
+
${names.singularPascal}Updated,
|
|
5828
|
+
${names.singularPascal}Deleted,
|
|
5829
|
+
] as const;
|
|
5830
|
+
`;
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
function resourcePolicyTestFile(
|
|
5834
|
+
names: ResourceNames,
|
|
5835
|
+
options: ResourceGenerationOptions,
|
|
5836
|
+
): string {
|
|
5837
|
+
return `import { describe, expect, it } from "bun:test";
|
|
5838
|
+
import { createPolicyTester } from "@beignet/core/ports/testing";
|
|
5839
|
+
import { ${names.singularCamel}Policy } from "../policy";
|
|
5840
|
+
import type { ${names.singularPascal} } from "../schemas";
|
|
5841
|
+
|
|
5842
|
+
function create${names.singularPascal}(): ${names.singularPascal} {
|
|
5843
|
+
const now = new Date().toISOString();
|
|
5844
|
+
return {
|
|
5845
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
5846
|
+
${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
5847
|
+
version: 1,
|
|
5848
|
+
createdAt: now,
|
|
5849
|
+
updatedAt: now,
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
|
|
5853
|
+
describe("${names.singularCamel}Policy", () => {
|
|
5854
|
+
it("documents generated ${names.pluralKebab} authorization rules", async () => {
|
|
5855
|
+
const tester = createPolicyTester({
|
|
5856
|
+
policies: [${names.singularCamel}Policy],
|
|
5857
|
+
});
|
|
5858
|
+
const ${names.singularCamel} = create${names.singularPascal}();
|
|
5859
|
+
const ctx = {
|
|
5860
|
+
actor: { type: "user" as const, id: "user_test" },
|
|
5861
|
+
${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
5862
|
+
|
|
5863
|
+
await expect(
|
|
5864
|
+
tester.assertMatrix([
|
|
5865
|
+
{
|
|
5866
|
+
name: "authenticated actor can create ${names.singularKebab}",
|
|
5867
|
+
ctx,
|
|
5868
|
+
ability: "${names.pluralCamel}.create",
|
|
5869
|
+
expected: "allow",
|
|
5870
|
+
},
|
|
5871
|
+
{
|
|
5872
|
+
name: "authenticated actor can update ${names.singularKebab}",
|
|
5873
|
+
ctx,
|
|
5874
|
+
ability: "${names.pluralCamel}.update",
|
|
5875
|
+
subject: ${names.singularCamel},
|
|
5876
|
+
expected: "allow",
|
|
5877
|
+
},
|
|
5878
|
+
{
|
|
5879
|
+
name: "authenticated actor can delete ${names.singularKebab}",
|
|
5880
|
+
ctx,
|
|
5881
|
+
ability: "${names.pluralCamel}.delete",
|
|
5882
|
+
subject: ${names.singularCamel},
|
|
5883
|
+
expected: "allow",
|
|
5884
|
+
},
|
|
5885
|
+
]),
|
|
5886
|
+
).resolves.toBeUndefined();
|
|
5887
|
+
});
|
|
5888
|
+
});
|
|
5889
|
+
`;
|
|
5890
|
+
}
|
|
5891
|
+
|
|
3612
5892
|
function eventFile(names: EventNames): string {
|
|
3613
5893
|
return `import { defineEvent } from "@beignet/core/events";
|
|
3614
5894
|
import { z } from "zod";
|
|
@@ -3626,11 +5906,9 @@ export const ${names.eventExportName} = defineEvent("${names.eventName}", {
|
|
|
3626
5906
|
}
|
|
3627
5907
|
|
|
3628
5908
|
function jobFile(names: JobNames, config: ResolvedBeignetConfig): string {
|
|
3629
|
-
return `import {
|
|
5909
|
+
return `import { retry } from "@beignet/core/jobs";
|
|
3630
5910
|
import { z } from "zod";
|
|
3631
|
-
import
|
|
3632
|
-
|
|
3633
|
-
const jobs = createJobHandlers<AppContext>();
|
|
5911
|
+
import { defineJob } from "${aliasModule(config.paths.jobsBuilder)}";
|
|
3634
5912
|
|
|
3635
5913
|
export const ${names.payloadSchemaName} = z.object({
|
|
3636
5914
|
\tid: z.string().uuid(),
|
|
@@ -3638,11 +5916,13 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
3638
5916
|
|
|
3639
5917
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
3640
5918
|
|
|
3641
|
-
export const ${names.jobExportName} =
|
|
5919
|
+
export const ${names.jobExportName} = defineJob("${names.jobName}", {
|
|
3642
5920
|
\tpayload: ${names.payloadSchemaName},
|
|
3643
|
-
\tretry: {
|
|
5921
|
+
\tretry: retry.exponential({
|
|
3644
5922
|
\t\tattempts: 3,
|
|
3645
|
-
\t
|
|
5923
|
+
\t\tinitialDelay: "1s",
|
|
5924
|
+
\t\tmaxDelay: "1m",
|
|
5925
|
+
\t}),
|
|
3646
5926
|
\tasync handle({ payload, ctx }) {
|
|
3647
5927
|
\t\tctx.ports.logger.info("Job handled", {
|
|
3648
5928
|
\t\t\tjobName: "${names.jobName}",
|
|
@@ -3653,19 +5933,110 @@ export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
|
|
|
3653
5933
|
`;
|
|
3654
5934
|
}
|
|
3655
5935
|
|
|
5936
|
+
function taskFile(names: TaskNames, config: ResolvedBeignetConfig): string {
|
|
5937
|
+
return `import { z } from "zod";
|
|
5938
|
+
import { defineTask } from "${aliasModule(config.paths.tasksBuilder)}";
|
|
5939
|
+
|
|
5940
|
+
export const ${names.inputSchemaName} = z.object({
|
|
5941
|
+
\tdryRun: z.boolean().default(true),
|
|
5942
|
+
});
|
|
5943
|
+
|
|
5944
|
+
export type ${names.inputTypeName} = z.infer<typeof ${names.inputSchemaName}>;
|
|
5945
|
+
|
|
5946
|
+
export const ${names.taskExportName} = defineTask("${names.taskName}", {
|
|
5947
|
+
\tinput: ${names.inputSchemaName},
|
|
5948
|
+
\tdescription: "Operational ${names.feature.kebab} ${names.artifact.kebab} task.",
|
|
5949
|
+
\tasync handle({ input, ctx }) {
|
|
5950
|
+
\t\tctx.ports.logger.info("Task handled", {
|
|
5951
|
+
\t\t\ttaskName: "${names.taskName}",
|
|
5952
|
+
\t\t\tdryRun: input.dryRun,
|
|
5953
|
+
\t\t});
|
|
5954
|
+
|
|
5955
|
+
\t\treturn {
|
|
5956
|
+
\t\t\tdryRun: input.dryRun,
|
|
5957
|
+
\t\t};
|
|
5958
|
+
\t},
|
|
5959
|
+
});
|
|
5960
|
+
`;
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5963
|
+
function factoryFile(
|
|
5964
|
+
names: FactoryNames,
|
|
5965
|
+
config: ResolvedBeignetConfig,
|
|
5966
|
+
): string {
|
|
5967
|
+
const resource = featureResourceNames(names);
|
|
5968
|
+
|
|
5969
|
+
return `import { createFactory } from "@beignet/core/testing";
|
|
5970
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
5971
|
+
|
|
5972
|
+
export const ${names.factoryExportName} = createFactory("${names.factoryName}", {
|
|
5973
|
+
\tdefaults: ({ sequence }) => ({
|
|
5974
|
+
\t\tname: "${resource.singularPascal} " + sequence,
|
|
5975
|
+
\t}),
|
|
5976
|
+
\tpersist: (ctx: AppContext, input) => ctx.ports.${resource.pluralCamel}.create(input),
|
|
5977
|
+
});
|
|
5978
|
+
`;
|
|
5979
|
+
}
|
|
5980
|
+
|
|
5981
|
+
function seedFile(names: SeedNames, config: ResolvedBeignetConfig): string {
|
|
5982
|
+
const resource = featureResourceNames(names);
|
|
5983
|
+
|
|
5984
|
+
return `import { defineSeed } from "@beignet/core/testing";
|
|
5985
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
5986
|
+
|
|
5987
|
+
export const ${names.seedExportName} = defineSeed("${names.seedName}", {
|
|
5988
|
+
\tdescription: "Creates starter ${names.feature.kebab} data.",
|
|
5989
|
+
\trun: async (ctx: AppContext) => {
|
|
5990
|
+
\t\tawait ctx.ports.${resource.pluralCamel}.create({
|
|
5991
|
+
\t\t\tname: "Demo ${resource.singularPascal}",
|
|
5992
|
+
\t\t});
|
|
5993
|
+
\t},
|
|
5994
|
+
});
|
|
5995
|
+
`;
|
|
5996
|
+
}
|
|
5997
|
+
|
|
5998
|
+
function notificationFile(
|
|
5999
|
+
names: NotificationNames,
|
|
6000
|
+
config: ResolvedBeignetConfig,
|
|
6001
|
+
): string {
|
|
6002
|
+
return `import { defineMailNotificationChannel } from "@beignet/core/notifications";
|
|
6003
|
+
import { z } from "zod";
|
|
6004
|
+
import { defineNotification } from "${aliasModule(config.paths.notificationsBuilder)}";
|
|
6005
|
+
|
|
6006
|
+
export const ${names.payloadSchemaName} = z.object({
|
|
6007
|
+
\tid: z.string().uuid(),
|
|
6008
|
+
\tsubject: z.string(),
|
|
6009
|
+
\trecipientEmail: z.string().email(),
|
|
6010
|
+
});
|
|
6011
|
+
|
|
6012
|
+
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
6013
|
+
|
|
6014
|
+
export const ${names.notificationExportName} = defineNotification(
|
|
6015
|
+
\t"${names.notificationName}",
|
|
6016
|
+
\t{
|
|
6017
|
+
\t\tpayload: ${names.payloadSchemaName},
|
|
6018
|
+
\t\tchannels: {
|
|
6019
|
+
\t\t\temail: defineMailNotificationChannel(({ payload }) => ({
|
|
6020
|
+
\t\t\t\tto: payload.recipientEmail,
|
|
6021
|
+
\t\t\t\tsubject: payload.subject,
|
|
6022
|
+
\t\t\t\ttext: \`${names.notificationName} notification for \${payload.id}.\`,
|
|
6023
|
+
\t\t\t})),
|
|
6024
|
+
\t\t},
|
|
6025
|
+
\t},
|
|
6026
|
+
);
|
|
6027
|
+
`;
|
|
6028
|
+
}
|
|
6029
|
+
|
|
3656
6030
|
function listenerFile(
|
|
3657
6031
|
names: ListenerNames,
|
|
3658
6032
|
config: ResolvedBeignetConfig,
|
|
3659
6033
|
): string {
|
|
3660
6034
|
const filePath = listenerFilePath(names, config);
|
|
3661
6035
|
|
|
3662
|
-
return `import {
|
|
3663
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
6036
|
+
return `import { defineListener } from "${aliasModule(config.paths.listenersBuilder)}";
|
|
3664
6037
|
import { ${names.event.eventExportName} } from "${relativeModule(filePath, eventFilePath(names.event, config))}";
|
|
3665
6038
|
|
|
3666
|
-
const
|
|
3667
|
-
|
|
3668
|
-
export const ${names.listenerExportName} = events.defineListener(${names.event.eventExportName}, {
|
|
6039
|
+
export const ${names.listenerExportName} = defineListener(${names.event.eventExportName}, {
|
|
3669
6040
|
\tname: "${names.listenerName}",
|
|
3670
6041
|
\tasync handle({ payload, ctx }) {
|
|
3671
6042
|
\t\tctx.ports.logger.info("Listener handled", {
|
|
@@ -3681,11 +6052,8 @@ function scheduleFile(
|
|
|
3681
6052
|
names: ScheduleNames,
|
|
3682
6053
|
config: ResolvedBeignetConfig,
|
|
3683
6054
|
): string {
|
|
3684
|
-
return `import {
|
|
3685
|
-
import {
|
|
3686
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3687
|
-
|
|
3688
|
-
const schedules = createScheduleHandlers<AppContext>();
|
|
6055
|
+
return `import { z } from "zod";
|
|
6056
|
+
import { defineSchedule } from "${aliasModule(config.paths.schedulesBuilder)}";
|
|
3689
6057
|
|
|
3690
6058
|
export const ${names.payloadSchemaName} = z.object({
|
|
3691
6059
|
\tdate: z.string(),
|
|
@@ -3693,7 +6061,7 @@ export const ${names.payloadSchemaName} = z.object({
|
|
|
3693
6061
|
|
|
3694
6062
|
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
3695
6063
|
|
|
3696
|
-
export const ${names.scheduleExportName} =
|
|
6064
|
+
export const ${names.scheduleExportName} = defineSchedule(
|
|
3697
6065
|
\t"${names.scheduleName}",
|
|
3698
6066
|
\t{
|
|
3699
6067
|
\t\tcron: "${names.cron}",
|
|
@@ -3716,111 +6084,174 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
3716
6084
|
`;
|
|
3717
6085
|
}
|
|
3718
6086
|
|
|
6087
|
+
function uploadFile(names: UploadNames, config: ResolvedBeignetConfig): string {
|
|
6088
|
+
return `import { defineUpload } from "@beignet/core/uploads";
|
|
6089
|
+
import { z } from "zod";
|
|
6090
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
6091
|
+
|
|
6092
|
+
export const ${names.metadataSchemaName} = z.object({
|
|
6093
|
+
\tresourceId: z.string().min(1),
|
|
6094
|
+
});
|
|
6095
|
+
|
|
6096
|
+
export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaName}>;
|
|
6097
|
+
|
|
6098
|
+
export const ${names.uploadExportName} = defineUpload<
|
|
6099
|
+
\t"${names.uploadName}",
|
|
6100
|
+
\ttypeof ${names.metadataSchemaName},
|
|
6101
|
+
\tAppContext,
|
|
6102
|
+
\t{ objectKeys: string[] }
|
|
6103
|
+
>("${names.uploadName}", {
|
|
6104
|
+
\tmetadata: ${names.metadataSchemaName},
|
|
6105
|
+
\tfile: {
|
|
6106
|
+
\t\tcontentTypes: ["application/pdf", "text/plain"],
|
|
6107
|
+
\t\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
6108
|
+
\t\tmaxFiles: 1,
|
|
6109
|
+
\t\tvisibility: "private",
|
|
6110
|
+
\t\tcacheControl: "private, max-age=0",
|
|
6111
|
+
\t},
|
|
6112
|
+
\tauthorize({ ctx }) {
|
|
6113
|
+
\t\treturn ctx.actor.type === "user";
|
|
6114
|
+
\t},
|
|
6115
|
+
\tkey({ ctx, metadata, uploadId, file }) {
|
|
6116
|
+
\t\tconst tenantId = ctx.tenant?.id ?? "default";
|
|
6117
|
+
\t\tconst extension = file.name.includes(".") ? file.name.split(".").pop() : undefined;
|
|
6118
|
+
\t\tconst suffix = extension ? \`.\${extension}\` : "";
|
|
6119
|
+
\t\treturn "${names.feature.kebab}/" + tenantId + "/" + metadata.resourceId + "/" + uploadId + suffix;
|
|
6120
|
+
\t},
|
|
6121
|
+
\tstorageMetadata({ ctx, metadata }) {
|
|
6122
|
+
\t\treturn {
|
|
6123
|
+
\t\t\ttenantId: ctx.tenant?.id ?? "default",
|
|
6124
|
+
\t\t\tresourceId: metadata.resourceId,
|
|
6125
|
+
\t\t};
|
|
6126
|
+
\t},
|
|
6127
|
+
\tasync onComplete({ ctx, metadata, files }) {
|
|
6128
|
+
\t\tctx.ports.logger.info("Upload completed", {
|
|
6129
|
+
\t\t\tuploadName: "${names.uploadName}",
|
|
6130
|
+
\t\t\tresourceId: metadata.resourceId,
|
|
6131
|
+
\t\t\tobjectKeys: files.map((file) => file.key),
|
|
6132
|
+
\t\t});
|
|
6133
|
+
|
|
6134
|
+
\t\treturn {
|
|
6135
|
+
\t\t\tobjectKeys: files.map((file) => file.key),
|
|
6136
|
+
\t\t};
|
|
6137
|
+
\t},
|
|
6138
|
+
});
|
|
6139
|
+
`;
|
|
6140
|
+
}
|
|
6141
|
+
|
|
6142
|
+
function featureUiComponentFile(
|
|
6143
|
+
names: FeatureUiNames,
|
|
6144
|
+
config: ResolvedBeignetConfig,
|
|
6145
|
+
): string {
|
|
6146
|
+
return `"use client";
|
|
6147
|
+
|
|
6148
|
+
import { contractErrorMessage } from "@beignet/core/client";
|
|
6149
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6150
|
+
import { useState } from "react";
|
|
6151
|
+
import { rq } from "${aliasModule(clientIndexPath(config))}";
|
|
6152
|
+
import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(
|
|
6153
|
+
resourceContractFilePath(names, config),
|
|
6154
|
+
)}";
|
|
6155
|
+
|
|
6156
|
+
export function ${names.componentExportName}() {
|
|
6157
|
+
\tconst [name, setName] = useState("");
|
|
6158
|
+
\tconst queryClient = useQueryClient();
|
|
6159
|
+
\tconst ${names.pluralCamel}Query = useQuery(
|
|
6160
|
+
\t\trq(list${names.pluralPascal}).queryOptions({ query: {} }),
|
|
6161
|
+
\t);
|
|
6162
|
+
\tconst create${names.singularPascal}Mutation = useMutation(
|
|
6163
|
+
\t\trq(create${names.singularPascal}).mutationOptions({
|
|
6164
|
+
\t\t\tonSuccess: async () => {
|
|
6165
|
+
\t\t\t\tsetName("");
|
|
6166
|
+
\t\t\t\tawait rq(list${names.pluralPascal}).invalidate(queryClient);
|
|
6167
|
+
\t\t\t},
|
|
6168
|
+
\t\t}),
|
|
6169
|
+
\t);
|
|
6170
|
+
|
|
6171
|
+
\treturn (
|
|
6172
|
+
\t\t<section className="${names.pluralKebab}-panel">
|
|
6173
|
+
\t\t\t<form
|
|
6174
|
+
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
6175
|
+
\t\t\t\tonSubmit={(event) => {
|
|
6176
|
+
\t\t\t\t\tevent.preventDefault();
|
|
6177
|
+
\t\t\t\t\tconst trimmedName = name.trim();
|
|
6178
|
+
\t\t\t\t\tif (!trimmedName) return;
|
|
6179
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
6180
|
+
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
6181
|
+
\t\t\t\t\t\tbody: { name: trimmedName },
|
|
6182
|
+
\t\t\t\t\t});
|
|
6183
|
+
\t\t\t\t}}
|
|
6184
|
+
\t\t\t>
|
|
6185
|
+
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
6186
|
+
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
6187
|
+
\t\t\t\t\t<input
|
|
6188
|
+
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
6189
|
+
\t\t\t\t\t\tvalue={name}
|
|
6190
|
+
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
6191
|
+
\t\t\t\t\t\tplaceholder="Name"
|
|
6192
|
+
\t\t\t\t\t/>
|
|
6193
|
+
\t\t\t\t\t<button
|
|
6194
|
+
\t\t\t\t\t\ttype="submit"
|
|
6195
|
+
\t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
|
|
6196
|
+
\t\t\t\t\t>
|
|
6197
|
+
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
6198
|
+
\t\t\t\t\t</button>
|
|
6199
|
+
\t\t\t\t</div>
|
|
6200
|
+
\t\t\t\t{create${names.singularPascal}Mutation.isError ? (
|
|
6201
|
+
\t\t\t\t\t<p role="alert">
|
|
6202
|
+
\t\t\t\t\t\t{contractErrorMessage(
|
|
6203
|
+
\t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
|
|
6204
|
+
\t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
|
|
6205
|
+
\t\t\t\t\t\t)}
|
|
6206
|
+
\t\t\t\t\t</p>
|
|
6207
|
+
\t\t\t\t) : null}
|
|
6208
|
+
\t\t\t</form>
|
|
6209
|
+
|
|
6210
|
+
\t\t\t<div className="${names.pluralKebab}-list">
|
|
6211
|
+
\t\t\t\t<div className="${names.pluralKebab}-list-heading">
|
|
6212
|
+
\t\t\t\t\t<h2>${names.pluralPascal}</h2>
|
|
6213
|
+
\t\t\t\t\t<span>{${names.pluralCamel}Query.data?.items.length ?? 0} shown</span>
|
|
6214
|
+
\t\t\t\t</div>
|
|
6215
|
+
\t\t\t\t{${names.pluralCamel}Query.isLoading ? <p>Loading...</p> : null}
|
|
6216
|
+
\t\t\t\t{${names.pluralCamel}Query.isError ? (
|
|
6217
|
+
\t\t\t\t\t<p role="alert">Could not load ${names.pluralKebab}.</p>
|
|
6218
|
+
\t\t\t\t) : null}
|
|
6219
|
+
\t\t\t\t{${names.pluralCamel}Query.data?.items.length === 0 ? (
|
|
6220
|
+
\t\t\t\t\t<p>No ${names.pluralKebab} yet.</p>
|
|
6221
|
+
\t\t\t\t) : null}
|
|
6222
|
+
\t\t\t\t<ul>
|
|
6223
|
+
\t\t\t\t\t{${names.pluralCamel}Query.data?.items.map((${names.singularCamel}) => (
|
|
6224
|
+
\t\t\t\t\t\t<li key={${names.singularCamel}.id}>
|
|
6225
|
+
\t\t\t\t\t\t\t<strong>{${names.singularCamel}.name}</strong>
|
|
6226
|
+
\t\t\t\t\t\t\t<small>{new Date(${names.singularCamel}.createdAt).toLocaleDateString()}</small>
|
|
6227
|
+
\t\t\t\t\t\t</li>
|
|
6228
|
+
\t\t\t\t\t))}
|
|
6229
|
+
\t\t\t\t</ul>
|
|
6230
|
+
\t\t\t</div>
|
|
6231
|
+
\t\t</section>
|
|
6232
|
+
\t);
|
|
6233
|
+
}
|
|
6234
|
+
`;
|
|
6235
|
+
}
|
|
6236
|
+
|
|
3719
6237
|
function scheduleRouteFile(
|
|
3720
6238
|
names: ScheduleNames,
|
|
3721
6239
|
config: ResolvedBeignetConfig,
|
|
3722
6240
|
): string {
|
|
3723
|
-
return `import {
|
|
3724
|
-
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
6241
|
+
return `import { createScheduleRoute } from "@beignet/next";
|
|
3725
6242
|
import { ${names.scheduleExportName} } from "${aliasModule(scheduleFilePath(names, config))}";
|
|
3726
6243
|
import { env } from "@/lib/env";
|
|
3727
6244
|
import { server } from "${aliasModule(config.paths.server)}";
|
|
3728
6245
|
|
|
3729
|
-
|
|
3730
|
-
\tconst cronSecret = env.CRON_SECRET;
|
|
6246
|
+
export const runtime = "nodejs";
|
|
3731
6247
|
|
|
3732
|
-
|
|
3733
|
-
\
|
|
3734
|
-
\
|
|
3735
|
-
\
|
|
3736
|
-
\
|
|
3737
|
-
\
|
|
3738
|
-
|
|
3739
|
-
\t\t\t{ status: 500 },
|
|
3740
|
-
\t\t);
|
|
3741
|
-
\t}
|
|
3742
|
-
|
|
3743
|
-
\tif (
|
|
3744
|
-
\t\trequest.headers.get("authorization") !== \`Bearer \${cronSecret}\`
|
|
3745
|
-
\t) {
|
|
3746
|
-
\t\treturn Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
3747
|
-
\t}
|
|
3748
|
-
|
|
3749
|
-
\tconst ctx = await server.createContextFromNext();
|
|
3750
|
-
\tconst runner = createInlineScheduleRunner<AppContext>({
|
|
3751
|
-
\t\tctx,
|
|
3752
|
-
\t\tonStart({ run, schedule }) {
|
|
3753
|
-
\t\t\tctx.ports.devtools.record({
|
|
3754
|
-
\t\t\t\ttype: "schedule",
|
|
3755
|
-
\t\t\t\twatcher: "schedules",
|
|
3756
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
3757
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3758
|
-
\t\t\t\tstatus: "started",
|
|
3759
|
-
\t\t\t\tcron: schedule.cron,
|
|
3760
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
3761
|
-
\t\t\t\tdetails: {
|
|
3762
|
-
\t\t\t\t\tsource: run.source,
|
|
3763
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
3764
|
-
\t\t\t\t},
|
|
3765
|
-
\t\t\t});
|
|
3766
|
-
\t\t},
|
|
3767
|
-
\t\tonSuccess({ run, schedule }) {
|
|
3768
|
-
\t\t\tctx.ports.devtools.record({
|
|
3769
|
-
\t\t\t\ttype: "schedule",
|
|
3770
|
-
\t\t\t\twatcher: "schedules",
|
|
3771
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
3772
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3773
|
-
\t\t\t\tstatus: "completed",
|
|
3774
|
-
\t\t\t\tcron: schedule.cron,
|
|
3775
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
3776
|
-
\t\t\t\tdetails: {
|
|
3777
|
-
\t\t\t\t\tsource: run.source,
|
|
3778
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
3779
|
-
\t\t\t\t},
|
|
3780
|
-
\t\t\t});
|
|
3781
|
-
\t\t},
|
|
3782
|
-
\t\tonError({ error, run, schedule }) {
|
|
3783
|
-
\t\t\tctx.ports.devtools.record({
|
|
3784
|
-
\t\t\t\ttype: "schedule",
|
|
3785
|
-
\t\t\t\twatcher: "schedules",
|
|
3786
|
-
\t\t\t\trequestId: ctx.requestId,
|
|
3787
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3788
|
-
\t\t\t\tstatus: "failed",
|
|
3789
|
-
\t\t\t\tcron: schedule.cron,
|
|
3790
|
-
\t\t\t\ttimezone: schedule.timezone,
|
|
3791
|
-
\t\t\t\tdetails: {
|
|
3792
|
-
\t\t\t\t\terror,
|
|
3793
|
-
\t\t\t\t\tsource: run.source,
|
|
3794
|
-
\t\t\t\t\tscheduledAt: run.scheduledAt?.toISOString(),
|
|
3795
|
-
\t\t\t\t},
|
|
3796
|
-
\t\t\t});
|
|
3797
|
-
\t\t\tctx.ports.logger.error("Schedule failed", {
|
|
3798
|
-
\t\t\t\terror,
|
|
3799
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3800
|
-
\t\t\t});
|
|
3801
|
-
\t\t},
|
|
3802
|
-
\t\tonHookError({ error, hook, schedule }) {
|
|
3803
|
-
\t\t\tctx.ports.logger.warn("Schedule lifecycle hook failed", {
|
|
3804
|
-
\t\t\t\terror,
|
|
3805
|
-
\t\t\t\thook,
|
|
3806
|
-
\t\t\t\tscheduleName: schedule.name,
|
|
3807
|
-
\t\t\t});
|
|
3808
|
-
\t\t},
|
|
3809
|
-
\t});
|
|
3810
|
-
|
|
3811
|
-
\ttry {
|
|
3812
|
-
\t\tawait runner.run(${names.scheduleExportName}, {
|
|
3813
|
-
\t\t\tsource: "cron-route",
|
|
3814
|
-
\t\t});
|
|
3815
|
-
\t} catch {
|
|
3816
|
-
\t\treturn Response.json({ error: "Schedule failed" }, { status: 500 });
|
|
3817
|
-
\t}
|
|
3818
|
-
|
|
3819
|
-
\treturn Response.json({ ok: true });
|
|
3820
|
-
}
|
|
3821
|
-
|
|
3822
|
-
export const GET = run${names.artifact.pascal};
|
|
3823
|
-
export const POST = run${names.artifact.pascal};
|
|
6248
|
+
export const { GET, POST } = createScheduleRoute({
|
|
6249
|
+
\tserver,
|
|
6250
|
+
\tschedules: [${names.scheduleExportName}],
|
|
6251
|
+
\tschedule: ${names.scheduleExportName}.name,
|
|
6252
|
+
\tsecret: env.CRON_SECRET,
|
|
6253
|
+
\tsource: "cron-route",
|
|
6254
|
+
});
|
|
3824
6255
|
`;
|
|
3825
6256
|
}
|
|
3826
6257
|
|
|
@@ -3838,79 +6269,161 @@ function useCaseIndexExport(names: UseCaseNames): string {
|
|
|
3838
6269
|
function routeGroupFile(
|
|
3839
6270
|
names: ResourceNames,
|
|
3840
6271
|
config: ResolvedBeignetConfig,
|
|
6272
|
+
mode: ResourceGenerationMode,
|
|
6273
|
+
_options: ResourceGenerationOptions,
|
|
3841
6274
|
): string {
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
6275
|
+
const routeFilePath = path.join(
|
|
6276
|
+
config.paths.features,
|
|
6277
|
+
names.pluralKebab,
|
|
6278
|
+
"routes.ts",
|
|
6279
|
+
);
|
|
6280
|
+
const contractsPath = resourceContractFilePath(names, config);
|
|
6281
|
+
const useCasesPath = resourceUseCaseIndexPath(names, config);
|
|
6282
|
+
|
|
6283
|
+
return `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
6284
|
+
import { defineRouteGroup } from "@beignet/next";
|
|
6285
|
+
import type { AppContext } from "${relativeModule(routeFilePath, config.paths.appContext)}";
|
|
6286
|
+
import {
|
|
6287
|
+
create${names.singularPascal},
|
|
6288
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
6289
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${relativeModule(routeFilePath, contractsPath)}";
|
|
3845
6290
|
import {
|
|
3846
6291
|
create${names.singularPascal}UseCase,
|
|
3847
|
-
list${names.pluralPascal}UseCase,
|
|
3848
|
-
|
|
6292
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
6293
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${relativeModule(routeFilePath, useCasesPath)}";
|
|
3849
6294
|
|
|
3850
6295
|
export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>({
|
|
3851
6296
|
name: "${names.pluralKebab}",
|
|
3852
6297
|
routes: [
|
|
3853
|
-
{
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
status: 200,
|
|
3857
|
-
body: await list${names.pluralPascal}UseCase.run({ ctx, input: query }),
|
|
3858
|
-
}),
|
|
3859
|
-
},
|
|
3860
|
-
{
|
|
3861
|
-
contract: create${names.singularPascal},
|
|
3862
|
-
handle: async ({ ctx, body }) => ({
|
|
3863
|
-
status: 201,
|
|
3864
|
-
body: await create${names.singularPascal}UseCase.run({ ctx, input: body }),
|
|
3865
|
-
}),
|
|
3866
|
-
},
|
|
3867
|
-
],
|
|
6298
|
+
{ contract: list${names.pluralPascal}, useCase: list${names.pluralPascal}UseCase },
|
|
6299
|
+
{ contract: create${names.singularPascal}, useCase: create${names.singularPascal}UseCase },
|
|
6300
|
+
${mode === "resource" ? ` { contract: get${names.singularPascal}, useCase: get${names.singularPascal}UseCase },\n { contract: update${names.singularPascal}, useCase: update${names.singularPascal}UseCase },\n { contract: delete${names.singularPascal}, useCase: delete${names.singularPascal}UseCase },\n` : ""} ],
|
|
3868
6301
|
});
|
|
3869
6302
|
`;
|
|
3870
6303
|
}
|
|
3871
6304
|
|
|
3872
|
-
function testFile(
|
|
6305
|
+
function testFile(
|
|
6306
|
+
names: ResourceNames,
|
|
6307
|
+
config: ResolvedBeignetConfig,
|
|
6308
|
+
mode: ResourceGenerationMode,
|
|
6309
|
+
options: ResourceGenerationOptions,
|
|
6310
|
+
): string {
|
|
3873
6311
|
const repositoryPath = path.join(
|
|
3874
6312
|
path.dirname(config.paths.infrastructurePorts),
|
|
3875
6313
|
names.pluralKebab,
|
|
3876
6314
|
`in-memory-${names.singularKebab}-repository.ts`,
|
|
3877
6315
|
);
|
|
6316
|
+
const serverContextPath = path.join(
|
|
6317
|
+
path.dirname(config.paths.server),
|
|
6318
|
+
"context.ts",
|
|
6319
|
+
);
|
|
6320
|
+
const requestTarget = options.tenant ? "requester" : "app";
|
|
3878
6321
|
|
|
3879
6322
|
return `import { describe, expect, it } from "bun:test";
|
|
3880
6323
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
6324
|
+
${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` : ""}import { defineRoutes } from "@beignet/web";
|
|
6325
|
+
import { createTestApp${options.tenant ? ", createTestRequester" : ""} } from "@beignet/web/testing";
|
|
3881
6326
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3882
|
-
import {
|
|
6327
|
+
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
6328
|
+
import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/ports/testing";
|
|
3883
6329
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3884
6330
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
6331
|
+
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
6332
|
+
import {
|
|
6333
|
+
create${names.singularPascal},
|
|
6334
|
+
${mode === "resource" ? ` delete${names.singularPascal},\n get${names.singularPascal},\n` : ""} list${names.pluralPascal},
|
|
6335
|
+
${mode === "resource" ? ` update${names.singularPascal},\n` : ""}} from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
3885
6336
|
import { createInMemory${names.singularPascal}Repository } from "${aliasModule(repositoryPath)}";
|
|
6337
|
+
import { ${names.singularCamel}Routes } from "${aliasModule(path.join(resourceFeatureDir(names, config), "routes.ts"))}";
|
|
3886
6338
|
import {
|
|
3887
6339
|
create${names.singularPascal}UseCase,
|
|
3888
|
-
list${names.pluralPascal}UseCase,
|
|
3889
|
-
} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
6340
|
+
${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
|
|
6341
|
+
${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${aliasModule(resourceUseCaseIndexPath(names, config))}";
|
|
3890
6342
|
|
|
3891
6343
|
describe("${names.pluralCamel} resource", () => {
|
|
3892
|
-
it("creates and lists ${names.pluralCamel}", async () => {
|
|
3893
|
-
const ${names.
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
6344
|
+
it("${mode === "resource" ? `exercises ${names.pluralCamel} CRUD` : `creates and lists ${names.pluralCamel}`}", async () => {
|
|
6345
|
+
const seed${names.pluralPascal} = [
|
|
6346
|
+
{
|
|
6347
|
+
id: "00000000-0000-4000-8000-000000000101",
|
|
6348
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
6349
|
+
version: 1,
|
|
6350
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
6351
|
+
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
6352
|
+
},
|
|
6353
|
+
{
|
|
6354
|
+
id: "00000000-0000-4000-8000-000000000102",
|
|
6355
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
6356
|
+
version: 1,
|
|
6357
|
+
createdAt: "2024-01-02T00:00:00.000Z",
|
|
6358
|
+
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
6359
|
+
},
|
|
6360
|
+
{
|
|
6361
|
+
id: "00000000-0000-4000-8000-000000000103",
|
|
6362
|
+
${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
6363
|
+
version: 1,
|
|
6364
|
+
createdAt: "2024-01-03T00:00:00.000Z",
|
|
6365
|
+
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
6366
|
+
},
|
|
6367
|
+
${options.tenant ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
|
|
6368
|
+
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
6369
|
+
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
6370
|
+
base: appPorts,
|
|
6371
|
+
overrides: {
|
|
3899
6372
|
${names.pluralCamel},
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
6373
|
+
${
|
|
6374
|
+
options.auth
|
|
6375
|
+
? `\t\t\t\tauth: {
|
|
6376
|
+
getSession: async () => ({
|
|
6377
|
+
user: { id: "user_test", name: "Test User" },
|
|
6378
|
+
}),
|
|
6379
|
+
},
|
|
6380
|
+
gate: appPorts.gate,\n`
|
|
6381
|
+
: `\t\t\t\tauth: {
|
|
6382
|
+
getSession: async () => null,
|
|
6383
|
+
},\n`
|
|
6384
|
+
} devtools: createInMemoryDevtools(),
|
|
6385
|
+
},
|
|
6386
|
+
transaction: {
|
|
6387
|
+
ports: (ports) => ({
|
|
6388
|
+
...ports,
|
|
6389
|
+
${names.pluralCamel},
|
|
6390
|
+
}),
|
|
6391
|
+
},
|
|
6392
|
+
});
|
|
6393
|
+
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
6394
|
+
ports: testFixture.ports,
|
|
6395
|
+
actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
6396
|
+
tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
|
|
6397
|
+
});
|
|
6398
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
3912
6399
|
|
|
3913
6400
|
const ctx = await tester.ctx();
|
|
6401
|
+
const defaultPage = await tester.run(list${names.pluralPascal}UseCase, {}, { ctx });
|
|
6402
|
+
const filteredPage = await tester.run(
|
|
6403
|
+
list${names.pluralPascal}UseCase,
|
|
6404
|
+
{ name: "alp" },
|
|
6405
|
+
{ ctx },
|
|
6406
|
+
);
|
|
6407
|
+
const firstPage = await tester.run(
|
|
6408
|
+
list${names.pluralPascal}UseCase,
|
|
6409
|
+
{ limit: 2 },
|
|
6410
|
+
{ ctx },
|
|
6411
|
+
);
|
|
6412
|
+
const secondPage = await tester.run(
|
|
6413
|
+
list${names.pluralPascal}UseCase,
|
|
6414
|
+
{ limit: 2, cursor: firstPage.page.nextCursor },
|
|
6415
|
+
{ ctx },
|
|
6416
|
+
);
|
|
6417
|
+
const nameAscendingPage = await tester.run(
|
|
6418
|
+
list${names.pluralPascal}UseCase,
|
|
6419
|
+
{ sortBy: "name", sortDirection: "asc" },
|
|
6420
|
+
{ ctx },
|
|
6421
|
+
);
|
|
6422
|
+
const nameDescendingPage = await tester.run(
|
|
6423
|
+
list${names.pluralPascal}UseCase,
|
|
6424
|
+
{ sortBy: "name", sortDirection: "desc" },
|
|
6425
|
+
{ ctx },
|
|
6426
|
+
);
|
|
3914
6427
|
const created = await tester.run(
|
|
3915
6428
|
create${names.singularPascal}UseCase,
|
|
3916
6429
|
{ name: "First ${names.singularPascal}" },
|
|
@@ -3918,13 +6431,113 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
3918
6431
|
);
|
|
3919
6432
|
const result = await tester.run(
|
|
3920
6433
|
list${names.pluralPascal}UseCase,
|
|
3921
|
-
{
|
|
6434
|
+
{ name: "First" },
|
|
3922
6435
|
{ ctx },
|
|
3923
6436
|
);
|
|
3924
6437
|
|
|
6438
|
+
expect(defaultPage.page).toMatchObject({
|
|
6439
|
+
kind: "cursor",
|
|
6440
|
+
limit: 20,
|
|
6441
|
+
cursor: null,
|
|
6442
|
+
nextCursor: null,
|
|
6443
|
+
hasMore: false,
|
|
6444
|
+
});
|
|
6445
|
+
expect(defaultPage.items.map((item) => item.name)).toEqual([
|
|
6446
|
+
"Gamma ${names.singularPascal}",
|
|
6447
|
+
"Beta ${names.singularPascal}",
|
|
6448
|
+
"Alpha ${names.singularPascal}",
|
|
6449
|
+
]);
|
|
6450
|
+
expect(filteredPage.items.map((item) => item.name)).toEqual([
|
|
6451
|
+
"Alpha ${names.singularPascal}",
|
|
6452
|
+
]);
|
|
6453
|
+
expect(firstPage.items.map((item) => item.name)).toEqual([
|
|
6454
|
+
"Gamma ${names.singularPascal}",
|
|
6455
|
+
"Beta ${names.singularPascal}",
|
|
6456
|
+
]);
|
|
6457
|
+
expect(firstPage.page.nextCursor).toEqual(expect.any(String));
|
|
6458
|
+
expect(secondPage.items.map((item) => item.name)).toEqual([
|
|
6459
|
+
"Alpha ${names.singularPascal}",
|
|
6460
|
+
]);
|
|
6461
|
+
expect(secondPage.page.nextCursor).toBeNull();
|
|
6462
|
+
expect(nameAscendingPage.items.map((item) => item.name)).toEqual([
|
|
6463
|
+
"Alpha ${names.singularPascal}",
|
|
6464
|
+
"Beta ${names.singularPascal}",
|
|
6465
|
+
"Gamma ${names.singularPascal}",
|
|
6466
|
+
]);
|
|
6467
|
+
expect(nameDescendingPage.items.map((item) => item.name)).toEqual([
|
|
6468
|
+
"Gamma ${names.singularPascal}",
|
|
6469
|
+
"Beta ${names.singularPascal}",
|
|
6470
|
+
"Alpha ${names.singularPascal}",
|
|
6471
|
+
]);
|
|
3925
6472
|
expect(created.name).toBe("First ${names.singularPascal}");
|
|
3926
|
-
expect(result.
|
|
3927
|
-
|
|
6473
|
+
expect(result.items).toEqual([created]);
|
|
6474
|
+
${
|
|
6475
|
+
mode === "resource"
|
|
6476
|
+
? `
|
|
6477
|
+
const fetched = await tester.run(
|
|
6478
|
+
get${names.singularPascal}UseCase,
|
|
6479
|
+
{ id: created.id },
|
|
6480
|
+
{ ctx },
|
|
6481
|
+
);
|
|
6482
|
+
const updated = await tester.run(
|
|
6483
|
+
update${names.singularPascal}UseCase,
|
|
6484
|
+
{ id: created.id, name: "Updated ${names.singularPascal}", version: fetched.version },
|
|
6485
|
+
{ ctx },
|
|
6486
|
+
);
|
|
6487
|
+
try {
|
|
6488
|
+
await tester.run(
|
|
6489
|
+
update${names.singularPascal}UseCase,
|
|
6490
|
+
{ id: created.id, name: "Stale ${names.singularPascal}", version: fetched.version },
|
|
6491
|
+
{ ctx },
|
|
6492
|
+
);
|
|
6493
|
+
throw new Error("Expected ${names.singularPascal}Conflict");
|
|
6494
|
+
} catch (error) {
|
|
6495
|
+
expect(isAppError(error)).toBe(true);
|
|
6496
|
+
if (isAppError(error)) {
|
|
6497
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_CONFLICT");
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6500
|
+
await tester.run(delete${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
6501
|
+
const afterDelete = await tester.run(
|
|
6502
|
+
list${names.pluralPascal}UseCase,
|
|
6503
|
+
{ name: "Updated" },
|
|
6504
|
+
{ ctx },
|
|
6505
|
+
);
|
|
6506
|
+
|
|
6507
|
+
expect(fetched).toEqual(created);
|
|
6508
|
+
expect(updated.name).toBe("Updated ${names.singularPascal}");
|
|
6509
|
+
expect(updated.version).toBe(fetched.version + 1);
|
|
6510
|
+
expect(afterDelete.items).toEqual([]);
|
|
6511
|
+
|
|
6512
|
+
try {
|
|
6513
|
+
await tester.run(get${names.singularPascal}UseCase, { id: created.id }, { ctx });
|
|
6514
|
+
throw new Error("Expected ${names.singularPascal}NotFound");
|
|
6515
|
+
} catch (error) {
|
|
6516
|
+
expect(isAppError(error)).toBe(true);
|
|
6517
|
+
if (isAppError(error)) {
|
|
6518
|
+
expect(error.code).toBe("${constantCase(names.singularKebab)}_NOT_FOUND");
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
`
|
|
6522
|
+
: ""
|
|
6523
|
+
}
|
|
6524
|
+
|
|
6525
|
+
const app = await createTestApp({
|
|
6526
|
+
ports: testFixture.ports,
|
|
6527
|
+
routes: defineRoutes<AppContext>([${names.singularCamel}Routes]),
|
|
6528
|
+
context: appContext,
|
|
6529
|
+
});
|
|
6530
|
+
${options.tenant ? `\t\tconst requester = createTestRequester(app, {\n\t\t\theaders: { "x-tenant-id": "tenant_test" },\n\t\t});\n` : ""}
|
|
6531
|
+
const createdViaRoute = await ${requestTarget}.request(create${names.singularPascal}, {
|
|
6532
|
+
body: { name: "Second ${names.singularPascal}" },
|
|
6533
|
+
});
|
|
6534
|
+
${mode === "resource" ? ` const fetchedViaRoute = await ${requestTarget}.request(get${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n });\n const updatedViaRoute = await ${requestTarget}.request(update${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n body: { name: "Updated via route", version: fetchedViaRoute.version },\n });\n` : ""} const listedViaRoute = await ${requestTarget}.request(list${names.pluralPascal}, {
|
|
6535
|
+
query: { name: "${mode === "resource" ? "Updated via route" : "Second"}" },
|
|
6536
|
+
});
|
|
6537
|
+
${mode === "resource" ? ` const deletedViaRoute = await ${requestTarget}.request(delete${names.singularPascal}, {\n path: { id: createdViaRoute.id },\n });\n const listedAfterDeleteViaRoute = await ${requestTarget}.request(list${names.pluralPascal}, {\n query: { name: "Updated via route" },\n });\n` : ""} await app.stop();
|
|
6538
|
+
|
|
6539
|
+
expect(createdViaRoute.name).toBe("Second ${names.singularPascal}");
|
|
6540
|
+
${mode === "resource" ? ` expect(fetchedViaRoute).toEqual(createdViaRoute);\n expect(updatedViaRoute.name).toBe("Updated via route");\n expect(updatedViaRoute.version).toBe(fetchedViaRoute.version + 1);\n expect(deletedViaRoute).toBeUndefined();\n expect(listedViaRoute.items).toContainEqual(updatedViaRoute);\n expect(listedAfterDeleteViaRoute.items).toEqual([]);\n` : ` expect(listedViaRoute.items).toContainEqual(createdViaRoute);\n`}
|
|
3928
6541
|
});
|
|
3929
6542
|
});
|
|
3930
6543
|
`;
|