@beignet/cli 0.0.37 → 0.0.39
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 +26 -0
- package/README.md +138 -101
- package/dist/choices.d.ts +17 -21
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +21 -67
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +5 -6
- package/dist/create-prompts.d.ts.map +1 -1
- package/dist/create-prompts.js +15 -15
- package/dist/create-prompts.js.map +1 -1
- package/dist/create.d.ts +11 -2
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +38 -28
- package/dist/create.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -47
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +69 -16
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +27 -6
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.js +10 -10
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts +2 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +18 -14
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +11 -4
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +56 -45
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.js +8 -8
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts +3 -2
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +481 -200
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +15 -11
- package/dist/mcp.js.map +1 -1
- package/dist/operational-error-reporting.d.ts +15 -0
- package/dist/operational-error-reporting.d.ts.map +1 -0
- package/dist/operational-error-reporting.js +42 -0
- package/dist/operational-error-reporting.js.map +1 -0
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +73 -0
- package/dist/outbox.js.map +1 -1
- package/dist/provider-add.d.ts +21 -3
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +117 -81
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.js +3 -3
- package/dist/provider-audit.js.map +1 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +28 -1
- package/dist/schedule.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +38 -5
- package/dist/task.js.map +1 -1
- package/dist/templates/base.d.ts +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +15 -15
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +7 -7
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +22 -18
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +4 -4
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +6 -6
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.js +1 -1
- package/dist/templates/todos.js +6 -6
- package/package.json +4 -4
- package/skills/app-structure/SKILL.md +13 -8
- package/src/choices.ts +38 -88
- package/src/config.ts +2 -2
- package/src/create-prompts.ts +20 -21
- package/src/create.ts +54 -36
- package/src/index.ts +193 -65
- package/src/inspect.ts +116 -18
- package/src/lib.ts +1 -1
- package/src/lint.ts +35 -4
- package/src/make/inbox.ts +10 -10
- package/src/make/payments.ts +26 -18
- package/src/make/shared.ts +72 -50
- package/src/make/tenancy.ts +8 -8
- package/src/make.ts +607 -191
- package/src/mcp.ts +20 -13
- package/src/operational-error-reporting.ts +60 -0
- package/src/outbox.ts +77 -0
- package/src/provider-add.ts +179 -95
- package/src/provider-audit.ts +3 -3
- package/src/schedule.ts +32 -1
- package/src/task.ts +42 -5
- package/src/templates/base.ts +16 -16
- package/src/templates/index.ts +8 -8
- package/src/templates/server.ts +22 -18
- package/src/templates/shared.ts +10 -10
- package/src/templates/testing.ts +1 -1
- package/src/templates/todos.ts +6 -6
- package/src/test-helpers/generated-app.ts +1 -0
package/dist/make.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { clientFormsPath, clientIndexPath, loadBeignetConfig, resolveConfig, } from "./config.js";
|
|
5
6
|
import { adapterFilePath, addNamedImport, addNamedTypeImport, aliasModule, appendDeferredPortKey, appendPickStringLiteralMember, applyPortProviderWiring, assertAdapterApp, assertFeatureArtifactApp, assertFeatureUiApp, assertGeneratedIdentifiers, assertPersistenceArtifactApp, assertPolicyApp, assertPortApp, assertServerRuntimeApp, assertStandardApp, assertTestApp, assertUseCaseApp, beignetDependencyVersion, constantCase, createListenersRegistrySource, defineUploadsInitializerInfo, detectResourceDatabase, detectResourcePersistence, drizzleDialects, drizzleRepositoriesPath, drizzleResourceRepositoryFilePath, drizzleSchemaIndexPath, eventBusPortWiring, eventFilePath, factoryFilePath, featureArtifactDir, featureArtifactIndexFile, featureArtifactNames, featureResourceNames, featureUiClientQueriesFilePath, featureUiComponentFilePath, featureUiIndexFilePath, featureUiNames, fileExists, hasTopLevelObjectProperty, identifierNames, infrastructureDir, insertTypeProperty, jobFilePath, listenerFilePath, listenersFilePath, mergeMakeResultInto, mergeMakeResults, missingCapabilityBuilderFiles, notificationFilePath, notificationPortWirings, planGeneratedFile, planGeneratedFiles, portFilePath, portNames, providersFilePath, readOptionalFile, relativeModule, resourceContractFilePath, resourceFeatureDir, resourceNames, resourcePortFilePath, resourceSharedErrorsPath, scheduleFilePath, seedEntrypointPath, seedFilePath, seedNames, singularize, storagePortWiring, tanstackReactQueryDependencyVersion, taskFilePath, typeBodySource, uniqueStrings, updateDrizzleRepositories, updateDrizzleSchemaIndex, updateFeatureArtifactIndex, updateInfrastructureAdapterWiring, updateInfrastructureDeferredPorts, updateInfrastructureGatePolicies, updateInfrastructurePortStub, updateInfrastructurePorts, updateListenersRegistrySource, updateOpenApiRoute, updatePackageDependencies, updatePackageJson, updatePackageScripts, updatePortIndex, updatePortsIndex, updateSeedEntrypoint, updateServerRoutes, updateServerTransactionPorts, updateSharedErrors, uploadFilePath, useCaseFeatureDir, useCaseFilePath, usesFeatureOwnedPolicies, usesFeatureOwnedTests, wireListenersProviderSource, wirePortProviders, writeGeneratedFile, writePlannedGeneratedFile, } from "./make/shared.js";
|
|
6
7
|
import { appendToArrayExpression, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, } from "./registry-edits.js";
|
|
8
|
+
import { externalVersions } from "./templates/shared.js";
|
|
7
9
|
import { testSupportTemplateFiles } from "./templates/testing.js";
|
|
8
10
|
export { makeFeatureAddonChoices, makeFeatureRecipeChoices, } from "./choices.js";
|
|
9
11
|
export * from "./make/inbox.js";
|
|
@@ -17,7 +19,7 @@ export async function makeFeature(options) {
|
|
|
17
19
|
const addons = normalizeMakeFeatureAddons([
|
|
18
20
|
...makeFeatureRecipeAddons(options.recipe),
|
|
19
21
|
...(options.with ?? []),
|
|
20
|
-
...(options.events ? ["
|
|
22
|
+
...(options.events ? ["event"] : []),
|
|
21
23
|
]);
|
|
22
24
|
const featureOptions = {
|
|
23
25
|
...options,
|
|
@@ -122,30 +124,36 @@ async function makeResourceSlice(options, mode) {
|
|
|
122
124
|
function resourceGenerationOptions(options, mode) {
|
|
123
125
|
if (mode === "feature") {
|
|
124
126
|
return {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
authorization: false,
|
|
128
|
+
tenantScoped: false,
|
|
127
129
|
events: Boolean(options.events),
|
|
128
130
|
softDelete: false,
|
|
129
131
|
};
|
|
130
132
|
}
|
|
131
133
|
if (mode !== "resource") {
|
|
132
|
-
return {
|
|
134
|
+
return {
|
|
135
|
+
authorization: false,
|
|
136
|
+
tenantScoped: false,
|
|
137
|
+
events: false,
|
|
138
|
+
softDelete: false,
|
|
139
|
+
};
|
|
133
140
|
}
|
|
134
141
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
authorization: Boolean(options.authorization),
|
|
143
|
+
tenantScoped: Boolean(options.tenantScoped),
|
|
137
144
|
events: Boolean(options.events),
|
|
138
145
|
softDelete: Boolean(options.softDelete),
|
|
139
146
|
};
|
|
140
147
|
}
|
|
141
148
|
async function makeFeatureSlice(options, addons) {
|
|
142
149
|
let result = await makeFeatureResource(options);
|
|
150
|
+
const includeUploadUi = addons.includes("ui") && addons.includes("upload");
|
|
143
151
|
for (const addon of addons) {
|
|
144
|
-
result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options));
|
|
152
|
+
result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options, includeUploadUi));
|
|
145
153
|
}
|
|
146
154
|
return result;
|
|
147
155
|
}
|
|
148
|
-
async function makeFeatureAddon(addon, feature, options) {
|
|
156
|
+
async function makeFeatureAddon(addon, feature, options, includeUploadUi) {
|
|
149
157
|
const shared = {
|
|
150
158
|
cwd: options.cwd,
|
|
151
159
|
force: options.force,
|
|
@@ -158,69 +166,53 @@ async function makeFeatureAddon(addon, feature, options) {
|
|
|
158
166
|
if (addon === "factory") {
|
|
159
167
|
return makeFactory({
|
|
160
168
|
...shared,
|
|
161
|
-
name: `${feature}
|
|
169
|
+
name: `${feature}.${singularize(feature)}`,
|
|
162
170
|
skipPersistenceAssert: true,
|
|
163
171
|
});
|
|
164
172
|
}
|
|
165
173
|
if (addon === "task") {
|
|
166
|
-
return makeTask({ ...shared, name: `${feature}
|
|
174
|
+
return makeTask({ ...shared, name: `${feature}.backfill` });
|
|
167
175
|
}
|
|
168
176
|
if (addon === "event") {
|
|
169
|
-
return makeEvent({ ...shared, name: `${feature}
|
|
177
|
+
return makeEvent({ ...shared, name: `${feature}.created` });
|
|
170
178
|
}
|
|
171
179
|
if (addon === "job") {
|
|
172
|
-
return makeJob({ ...shared, name: `${feature}
|
|
180
|
+
return makeJob({ ...shared, name: `${feature}.process` });
|
|
173
181
|
}
|
|
174
182
|
if (addon === "listener") {
|
|
175
183
|
return makeListener({
|
|
176
184
|
...shared,
|
|
177
|
-
name: `${feature}
|
|
178
|
-
event: `${feature}
|
|
185
|
+
name: `${feature}.handle-created`,
|
|
186
|
+
event: `${feature}.created`,
|
|
179
187
|
skipEventAssert: true,
|
|
180
188
|
});
|
|
181
189
|
}
|
|
182
190
|
if (addon === "notification") {
|
|
183
|
-
return makeNotification({ ...shared, name: `${feature}
|
|
191
|
+
return makeNotification({ ...shared, name: `${feature}.created` });
|
|
184
192
|
}
|
|
185
193
|
if (addon === "schedule") {
|
|
186
|
-
return makeSchedule({ ...shared, name: `${feature}
|
|
194
|
+
return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
|
|
187
195
|
}
|
|
188
196
|
if (addon === "seed") {
|
|
189
197
|
return makeSeed({
|
|
190
198
|
...shared,
|
|
191
|
-
name: `${feature}
|
|
199
|
+
name: `${feature}.demo-${feature}`,
|
|
192
200
|
skipPersistenceAssert: true,
|
|
193
201
|
});
|
|
194
202
|
}
|
|
195
203
|
if (addon === "ui") {
|
|
196
204
|
return makeFeatureUi({ ...shared, name: feature });
|
|
197
205
|
}
|
|
198
|
-
return makeUpload({
|
|
206
|
+
return makeUpload({
|
|
207
|
+
...shared,
|
|
208
|
+
name: `${feature}.attachment`,
|
|
209
|
+
ui: includeUploadUi,
|
|
210
|
+
});
|
|
199
211
|
}
|
|
200
212
|
function normalizeMakeFeatureAddons(addons) {
|
|
201
213
|
const normalized = new Set();
|
|
202
214
|
for (const addon of addons) {
|
|
203
|
-
if (addon === "
|
|
204
|
-
normalized.add("factory");
|
|
205
|
-
else if (addon === "events")
|
|
206
|
-
normalized.add("event");
|
|
207
|
-
else if (addon === "listeners") {
|
|
208
|
-
normalized.add("event");
|
|
209
|
-
normalized.add("listener");
|
|
210
|
-
}
|
|
211
|
-
else if (addon === "tasks")
|
|
212
|
-
normalized.add("task");
|
|
213
|
-
else if (addon === "jobs")
|
|
214
|
-
normalized.add("job");
|
|
215
|
-
else if (addon === "notifications")
|
|
216
|
-
normalized.add("notification");
|
|
217
|
-
else if (addon === "schedules")
|
|
218
|
-
normalized.add("schedule");
|
|
219
|
-
else if (addon === "seeds")
|
|
220
|
-
normalized.add("seed");
|
|
221
|
-
else if (addon === "uploads")
|
|
222
|
-
normalized.add("upload");
|
|
223
|
-
else if (addon === "listener") {
|
|
215
|
+
if (addon === "listener") {
|
|
224
216
|
normalized.add("event");
|
|
225
217
|
normalized.add("listener");
|
|
226
218
|
}
|
|
@@ -233,16 +225,16 @@ function makeFeatureRecipeAddons(recipe) {
|
|
|
233
225
|
if (recipe === "full-slice") {
|
|
234
226
|
return [
|
|
235
227
|
"policy",
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
228
|
+
"factory",
|
|
229
|
+
"seed",
|
|
230
|
+
"task",
|
|
231
|
+
"event",
|
|
232
|
+
"listener",
|
|
233
|
+
"job",
|
|
234
|
+
"notification",
|
|
235
|
+
"schedule",
|
|
244
236
|
"ui",
|
|
245
|
-
"
|
|
237
|
+
"upload",
|
|
246
238
|
];
|
|
247
239
|
}
|
|
248
240
|
return [];
|
|
@@ -328,7 +320,7 @@ export async function makeUseCase(options) {
|
|
|
328
320
|
skippedFiles.push(indexFile.path);
|
|
329
321
|
return {
|
|
330
322
|
schemaVersion: 1,
|
|
331
|
-
name: `${names.feature.kebab}
|
|
323
|
+
name: `${names.feature.kebab}.${names.action.kebab}`,
|
|
332
324
|
targetDir,
|
|
333
325
|
dryRun: Boolean(options.dryRun),
|
|
334
326
|
files: [...generatedFiles.map((file) => file.path), indexFile.path],
|
|
@@ -367,7 +359,7 @@ export async function makeTest(options) {
|
|
|
367
359
|
}
|
|
368
360
|
return {
|
|
369
361
|
schemaVersion: 1,
|
|
370
|
-
name: `${names.feature.kebab}
|
|
362
|
+
name: `${names.feature.kebab}.${names.action.kebab}`,
|
|
371
363
|
targetDir,
|
|
372
364
|
dryRun: Boolean(options.dryRun),
|
|
373
365
|
files: generatedFiles.map((file) => file.path),
|
|
@@ -415,7 +407,7 @@ export async function makePort(options) {
|
|
|
415
407
|
if (await updateInfrastructurePortStub(targetDir, names, config, {
|
|
416
408
|
dryRun: Boolean(options.dryRun),
|
|
417
409
|
})) {
|
|
418
|
-
updatedFiles.push(config.paths.
|
|
410
|
+
updatedFiles.push(config.paths.portWiring);
|
|
419
411
|
}
|
|
420
412
|
return {
|
|
421
413
|
schemaVersion: 1,
|
|
@@ -454,7 +446,7 @@ export async function makeAdapter(options) {
|
|
|
454
446
|
if (await updateInfrastructureAdapterWiring(targetDir, names, config, {
|
|
455
447
|
dryRun: Boolean(options.dryRun),
|
|
456
448
|
})) {
|
|
457
|
-
updatedFiles.push(config.paths.
|
|
449
|
+
updatedFiles.push(config.paths.portWiring);
|
|
458
450
|
}
|
|
459
451
|
return {
|
|
460
452
|
schemaVersion: 1,
|
|
@@ -521,12 +513,15 @@ export async function makeEvent(options) {
|
|
|
521
513
|
const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
|
|
522
514
|
force: Boolean(options.force),
|
|
523
515
|
});
|
|
516
|
+
await updateOutboxRegistry(targetDir, names, "events", config, {
|
|
517
|
+
dryRun: true,
|
|
518
|
+
});
|
|
524
519
|
const result = await makeFeatureArtifact({
|
|
525
520
|
targetDir,
|
|
526
521
|
config,
|
|
527
522
|
force: Boolean(options.force),
|
|
528
523
|
dryRun: Boolean(options.dryRun),
|
|
529
|
-
name:
|
|
524
|
+
name: names.stableName,
|
|
530
525
|
files: eventFiles(names, config),
|
|
531
526
|
index: featureArtifactIndexFile("event", names, config),
|
|
532
527
|
dependencies: {
|
|
@@ -559,12 +554,15 @@ export async function makeJob(options) {
|
|
|
559
554
|
const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
|
|
560
555
|
force: Boolean(options.force),
|
|
561
556
|
});
|
|
557
|
+
await updateOutboxRegistry(targetDir, names, "jobs", config, {
|
|
558
|
+
dryRun: true,
|
|
559
|
+
});
|
|
562
560
|
const result = await makeFeatureArtifact({
|
|
563
561
|
targetDir,
|
|
564
562
|
config,
|
|
565
563
|
force: Boolean(options.force),
|
|
566
564
|
dryRun: Boolean(options.dryRun),
|
|
567
|
-
name:
|
|
565
|
+
name: names.stableName,
|
|
568
566
|
files: [
|
|
569
567
|
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
570
568
|
...jobFiles(names, config),
|
|
@@ -597,7 +595,7 @@ export async function makeTask(options) {
|
|
|
597
595
|
config,
|
|
598
596
|
force: Boolean(options.force),
|
|
599
597
|
dryRun: Boolean(options.dryRun),
|
|
600
|
-
name:
|
|
598
|
+
name: names.stableName,
|
|
601
599
|
files: [
|
|
602
600
|
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
603
601
|
...taskFiles(names, config),
|
|
@@ -635,7 +633,7 @@ export async function makeFactory(options) {
|
|
|
635
633
|
config,
|
|
636
634
|
force: Boolean(options.force),
|
|
637
635
|
dryRun: Boolean(options.dryRun),
|
|
638
|
-
name:
|
|
636
|
+
name: names.stableName,
|
|
639
637
|
files: factoryFiles(names, config),
|
|
640
638
|
index: featureArtifactIndexFile("factory", names, config),
|
|
641
639
|
dependencies: {
|
|
@@ -657,7 +655,7 @@ export async function makeSeed(options) {
|
|
|
657
655
|
config,
|
|
658
656
|
force: Boolean(options.force),
|
|
659
657
|
dryRun: Boolean(options.dryRun),
|
|
660
|
-
name:
|
|
658
|
+
name: names.stableName,
|
|
661
659
|
files: seedFiles(names, config),
|
|
662
660
|
index: featureArtifactIndexFile("seed", names, config),
|
|
663
661
|
dependencies: {
|
|
@@ -671,7 +669,7 @@ export async function makeSeed(options) {
|
|
|
671
669
|
config,
|
|
672
670
|
force: Boolean(options.force),
|
|
673
671
|
dryRun: Boolean(options.dryRun),
|
|
674
|
-
name:
|
|
672
|
+
name: factoryNamesForSeed.stableName,
|
|
675
673
|
files: factoryFiles(factoryNamesForSeed, config),
|
|
676
674
|
index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
|
|
677
675
|
dependencies: {
|
|
@@ -708,7 +706,7 @@ export async function makeNotification(options) {
|
|
|
708
706
|
// notifications uses ctx.ports.notifications, so the generator wires both
|
|
709
707
|
// ports with dev-default providers. Each port is wired independently and
|
|
710
708
|
// skipped when the app already has it, for example when the resend
|
|
711
|
-
//
|
|
709
|
+
// provider contributed the mailer. Plan the wiring before writing any
|
|
712
710
|
// files so an anchor miss aborts the generator without leaving partial
|
|
713
711
|
// output.
|
|
714
712
|
const notificationWirings = notificationPortWirings("make notification");
|
|
@@ -720,7 +718,7 @@ export async function makeNotification(options) {
|
|
|
720
718
|
config,
|
|
721
719
|
force: Boolean(options.force),
|
|
722
720
|
dryRun: Boolean(options.dryRun),
|
|
723
|
-
name:
|
|
721
|
+
name: names.stableName,
|
|
724
722
|
files: [
|
|
725
723
|
...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
|
|
726
724
|
...notificationFiles(names, config),
|
|
@@ -754,7 +752,7 @@ export async function makeListener(options) {
|
|
|
754
752
|
config,
|
|
755
753
|
force: Boolean(options.force),
|
|
756
754
|
dryRun: Boolean(options.dryRun),
|
|
757
|
-
name:
|
|
755
|
+
name: names.stableName,
|
|
758
756
|
files: [
|
|
759
757
|
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
760
758
|
...listenerFiles(names, config),
|
|
@@ -826,7 +824,7 @@ async function assertListenerEventExists(targetDir, names, config) {
|
|
|
826
824
|
await stat(path.join(targetDir, file));
|
|
827
825
|
}
|
|
828
826
|
catch {
|
|
829
|
-
throw new Error(`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.
|
|
827
|
+
throw new Error(`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`);
|
|
830
828
|
}
|
|
831
829
|
}
|
|
832
830
|
async function updateListenerProviderWiring(targetDir, names, config, options) {
|
|
@@ -878,7 +876,7 @@ export async function makeSchedule(options) {
|
|
|
878
876
|
config,
|
|
879
877
|
force: Boolean(options.force),
|
|
880
878
|
dryRun: Boolean(options.dryRun),
|
|
881
|
-
name:
|
|
879
|
+
name: names.stableName,
|
|
882
880
|
files,
|
|
883
881
|
index: featureArtifactIndexFile("schedule", names, config),
|
|
884
882
|
dependencies: {
|
|
@@ -1003,7 +1001,7 @@ async function updateOutboxPortWiring(targetDir, config, options) {
|
|
|
1003
1001
|
changes.updatedFiles.push(config.paths.ports);
|
|
1004
1002
|
}
|
|
1005
1003
|
if (await updateOutboxInfrastructurePorts(targetDir, config, options)) {
|
|
1006
|
-
changes.updatedFiles.push(config.paths.
|
|
1004
|
+
changes.updatedFiles.push(config.paths.portWiring);
|
|
1007
1005
|
}
|
|
1008
1006
|
if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
|
|
1009
1007
|
changes.updatedFiles.push(path.join(infrastructureDir(config), "db/provider.ts"));
|
|
@@ -1075,7 +1073,7 @@ async function updateOutboxPorts(targetDir, config, options) {
|
|
|
1075
1073
|
return true;
|
|
1076
1074
|
}
|
|
1077
1075
|
async function updateOutboxInfrastructurePorts(targetDir, config, options) {
|
|
1078
|
-
const filePath = path.join(targetDir, config.paths.
|
|
1076
|
+
const filePath = path.join(targetDir, config.paths.portWiring);
|
|
1079
1077
|
const original = await readFile(filePath, "utf8");
|
|
1080
1078
|
let next = appendDeferredPortKey(original, "outbox");
|
|
1081
1079
|
next = appendDeferredPortKey(next, "outboxAdmin");
|
|
@@ -1246,6 +1244,9 @@ export async function makeUpload(options) {
|
|
|
1246
1244
|
? resolveConfig(options.config)
|
|
1247
1245
|
: await loadBeignetConfig(targetDir);
|
|
1248
1246
|
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
1247
|
+
if (options.ui) {
|
|
1248
|
+
await assertUploadUiApp(targetDir, config);
|
|
1249
|
+
}
|
|
1249
1250
|
await wirePortProviders(targetDir, config, [storagePortWiring("make upload")], {
|
|
1250
1251
|
dryRun: true,
|
|
1251
1252
|
});
|
|
@@ -1254,10 +1255,10 @@ export async function makeUpload(options) {
|
|
|
1254
1255
|
config,
|
|
1255
1256
|
force: Boolean(options.force),
|
|
1256
1257
|
dryRun: Boolean(options.dryRun),
|
|
1257
|
-
name:
|
|
1258
|
+
name: names.stableName,
|
|
1258
1259
|
files: [
|
|
1259
1260
|
...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
|
|
1260
|
-
...uploadFiles(names, config),
|
|
1261
|
+
...uploadFiles(names, config, { ui: Boolean(options.ui) }),
|
|
1261
1262
|
],
|
|
1262
1263
|
index: featureArtifactIndexFile("upload", names, config),
|
|
1263
1264
|
dependencies: {
|
|
@@ -1277,6 +1278,31 @@ export async function makeUpload(options) {
|
|
|
1277
1278
|
if (!result.files.includes(uploadRoutePath(config))) {
|
|
1278
1279
|
result.files.push(uploadRoutePath(config));
|
|
1279
1280
|
}
|
|
1281
|
+
if (options.ui) {
|
|
1282
|
+
const componentIndex = uploadUiComponentIndexFile(names, config);
|
|
1283
|
+
const componentIndexResult = await updateFeatureUiIndex(targetDir, componentIndex, { dryRun: Boolean(options.dryRun) });
|
|
1284
|
+
if (componentIndexResult === "created") {
|
|
1285
|
+
result.createdFiles.push(componentIndex.path);
|
|
1286
|
+
}
|
|
1287
|
+
if (componentIndexResult === "updated") {
|
|
1288
|
+
result.updatedFiles.push(componentIndex.path);
|
|
1289
|
+
}
|
|
1290
|
+
if (componentIndexResult === "skipped") {
|
|
1291
|
+
result.skippedFiles.push(componentIndex.path);
|
|
1292
|
+
}
|
|
1293
|
+
if (!result.files.includes(componentIndex.path)) {
|
|
1294
|
+
result.files.push(componentIndex.path);
|
|
1295
|
+
}
|
|
1296
|
+
if (await updatePackageDependencies(targetDir, { "@beignet/react-uploads": beignetDependencyVersion }, { dryRun: Boolean(options.dryRun) })) {
|
|
1297
|
+
result.updatedFiles.push("package.json");
|
|
1298
|
+
if (!result.files.includes("package.json")) {
|
|
1299
|
+
result.files.push("package.json");
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
result.createdFiles = uniqueStrings(result.createdFiles);
|
|
1304
|
+
result.updatedFiles = uniqueStrings(result.updatedFiles);
|
|
1305
|
+
result.skippedFiles = uniqueStrings(result.skippedFiles);
|
|
1280
1306
|
return result;
|
|
1281
1307
|
}
|
|
1282
1308
|
export async function makeOutbox(options = {}) {
|
|
@@ -1325,7 +1351,7 @@ export async function makeOutbox(options = {}) {
|
|
|
1325
1351
|
...cronSecretPlan.files.map((file) => file.path),
|
|
1326
1352
|
...cronSecretPlan.skippedFiles,
|
|
1327
1353
|
config.paths.ports,
|
|
1328
|
-
config.paths.
|
|
1354
|
+
config.paths.portWiring,
|
|
1329
1355
|
path.join(infrastructureDir(config), "db/provider.ts"),
|
|
1330
1356
|
drizzleRepositoriesPath(config),
|
|
1331
1357
|
outboxDrizzleSchemaFilePath(config),
|
|
@@ -1343,7 +1369,10 @@ async function makeFeatureUi(options) {
|
|
|
1343
1369
|
? resolveConfig(options.config)
|
|
1344
1370
|
: await loadBeignetConfig(targetDir);
|
|
1345
1371
|
await assertFeatureUiApp(targetDir, config);
|
|
1346
|
-
const componentFiles =
|
|
1372
|
+
const componentFiles = [
|
|
1373
|
+
...(await missingFeatureUiFormsFiles(targetDir, config)),
|
|
1374
|
+
...featureUiComponentFiles(names, config),
|
|
1375
|
+
];
|
|
1347
1376
|
const indexFile = featureUiIndexFile(names, config);
|
|
1348
1377
|
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
1349
1378
|
force: Boolean(options.force),
|
|
@@ -1372,8 +1401,13 @@ async function makeFeatureUi(options) {
|
|
|
1372
1401
|
if (indexResult === "skipped")
|
|
1373
1402
|
skippedFiles.push(indexFile.path);
|
|
1374
1403
|
if (await updatePackageDependencies(targetDir, {
|
|
1404
|
+
"@beignet/react-hook-form": beignetDependencyVersion,
|
|
1375
1405
|
"@beignet/react-query": beignetDependencyVersion,
|
|
1406
|
+
"@hookform/resolvers": (packageJson) => packageJson.dependencies?.["@hookform/resolvers"] ??
|
|
1407
|
+
externalVersions.hookformResolvers,
|
|
1376
1408
|
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
1409
|
+
"react-hook-form": (packageJson) => packageJson.dependencies?.["react-hook-form"] ??
|
|
1410
|
+
externalVersions.reactHookForm,
|
|
1377
1411
|
}, { dryRun: Boolean(options.dryRun) })) {
|
|
1378
1412
|
updatedFiles.push("package.json");
|
|
1379
1413
|
}
|
|
@@ -1459,15 +1493,15 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
1459
1493
|
}
|
|
1460
1494
|
if (persistence === "memory" &&
|
|
1461
1495
|
(await updateInfrastructurePorts(targetDir, names, config, options))) {
|
|
1462
|
-
updated.add(config.paths.
|
|
1496
|
+
updated.add(config.paths.portWiring);
|
|
1463
1497
|
}
|
|
1464
|
-
if (generationOptions.
|
|
1498
|
+
if (generationOptions.authorization &&
|
|
1465
1499
|
(await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
|
|
1466
|
-
updated.add(config.paths.
|
|
1500
|
+
updated.add(config.paths.portWiring);
|
|
1467
1501
|
}
|
|
1468
1502
|
if (persistence === "drizzle" &&
|
|
1469
1503
|
(await updateInfrastructureDeferredPorts(targetDir, names, config, options))) {
|
|
1470
|
-
updated.add(config.paths.
|
|
1504
|
+
updated.add(config.paths.portWiring);
|
|
1471
1505
|
}
|
|
1472
1506
|
if (persistence === "drizzle" &&
|
|
1473
1507
|
(await updateDrizzleSchemaIndex(targetDir, names, config, options))) {
|
|
@@ -1499,11 +1533,10 @@ function typeHasTopLevelProperty(source, typeName, propertyName) {
|
|
|
1499
1533
|
function useCaseNames(input) {
|
|
1500
1534
|
const parts = input
|
|
1501
1535
|
.trim()
|
|
1502
|
-
.split("
|
|
1503
|
-
.map((part) => part.trim())
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
throw new Error("Use case name must use feature/action format, for example projects/archive-project.");
|
|
1536
|
+
.split(".")
|
|
1537
|
+
.map((part) => part.trim());
|
|
1538
|
+
if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
|
|
1539
|
+
throw new Error("Use case name must use feature.action format, for example projects.archive-project.");
|
|
1507
1540
|
}
|
|
1508
1541
|
const feature = identifierNames(parts[0], "Use case feature");
|
|
1509
1542
|
const action = identifierNames(parts[1], "Use case action");
|
|
@@ -1535,7 +1568,7 @@ function policyNames(input) {
|
|
|
1535
1568
|
return resolvedNames;
|
|
1536
1569
|
}
|
|
1537
1570
|
function eventNames(input) {
|
|
1538
|
-
const names = featureArtifactNames(
|
|
1571
|
+
const names = featureArtifactNames(input, "Event");
|
|
1539
1572
|
const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
|
|
1540
1573
|
assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
|
|
1541
1574
|
return {
|
|
@@ -1581,7 +1614,7 @@ function factoryNames(input) {
|
|
|
1581
1614
|
};
|
|
1582
1615
|
}
|
|
1583
1616
|
function defaultFactoryNamesForSeed(names) {
|
|
1584
|
-
return factoryNames(`${names.feature.kebab}
|
|
1617
|
+
return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
|
|
1585
1618
|
}
|
|
1586
1619
|
function notificationNames(input) {
|
|
1587
1620
|
const names = featureArtifactNames(input, "Notification");
|
|
@@ -1625,24 +1658,26 @@ function scheduleNames(input, options) {
|
|
|
1625
1658
|
function uploadNames(input) {
|
|
1626
1659
|
const names = featureArtifactNames(input, "Upload");
|
|
1627
1660
|
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
1628
|
-
|
|
1661
|
+
const componentExportName = `${names.artifact.pascal}Uploader`;
|
|
1662
|
+
assertGeneratedIdentifiers(input, "Upload name", [
|
|
1663
|
+
uploadExportName,
|
|
1664
|
+
componentExportName,
|
|
1665
|
+
]);
|
|
1629
1666
|
return {
|
|
1630
1667
|
...names,
|
|
1631
1668
|
uploadName: names.stableName,
|
|
1632
1669
|
uploadExportName,
|
|
1633
1670
|
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
1634
1671
|
metadataTypeName: `${uploadExportName}Metadata`,
|
|
1672
|
+
fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
|
|
1673
|
+
manifestExportName: `${names.artifact.camel}UploadManifest`,
|
|
1674
|
+
manifestFileName: `${names.artifact.kebab}-upload-manifest`,
|
|
1675
|
+
reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
|
|
1676
|
+
clientFileName: `${names.artifact.kebab}-upload`,
|
|
1677
|
+
componentExportName,
|
|
1678
|
+
componentFileName: `${names.artifact.kebab}-uploader`,
|
|
1635
1679
|
};
|
|
1636
1680
|
}
|
|
1637
|
-
function normalizeEventReference(input) {
|
|
1638
|
-
const trimmed = input.trim();
|
|
1639
|
-
if (trimmed.includes("/"))
|
|
1640
|
-
return trimmed;
|
|
1641
|
-
const dotIndex = trimmed.indexOf(".");
|
|
1642
|
-
if (dotIndex === -1)
|
|
1643
|
-
return trimmed;
|
|
1644
|
-
return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
|
|
1645
|
-
}
|
|
1646
1681
|
function inferUseCaseKind(action) {
|
|
1647
1682
|
if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
|
|
1648
1683
|
return "query";
|
|
@@ -1650,8 +1685,8 @@ function inferUseCaseKind(action) {
|
|
|
1650
1685
|
return "command";
|
|
1651
1686
|
}
|
|
1652
1687
|
function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
|
|
1653
|
-
|
|
1654
|
-
|
|
1688
|
+
authorization: false,
|
|
1689
|
+
tenantScoped: false,
|
|
1655
1690
|
events: false,
|
|
1656
1691
|
softDelete: false,
|
|
1657
1692
|
}, database = "sqlite") {
|
|
@@ -1704,7 +1739,7 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
|
|
|
1704
1739
|
},
|
|
1705
1740
|
]
|
|
1706
1741
|
: []),
|
|
1707
|
-
...(mode === "resource" && options.
|
|
1742
|
+
...(mode === "resource" && options.authorization
|
|
1708
1743
|
? [
|
|
1709
1744
|
{
|
|
1710
1745
|
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
@@ -1759,6 +1794,59 @@ function featureUiComponentFiles(names, config) {
|
|
|
1759
1794
|
},
|
|
1760
1795
|
];
|
|
1761
1796
|
}
|
|
1797
|
+
async function missingFeatureUiFormsFiles(targetDir, config) {
|
|
1798
|
+
const formsPath = clientFormsPath(config);
|
|
1799
|
+
const existing = await readOptionalFile(path.join(targetDir, formsPath));
|
|
1800
|
+
if (existing === undefined) {
|
|
1801
|
+
return [
|
|
1802
|
+
{
|
|
1803
|
+
path: formsPath,
|
|
1804
|
+
content: `import { createReactHookForm } from "@beignet/react-hook-form";
|
|
1805
|
+
|
|
1806
|
+
export const rhf = createReactHookForm();
|
|
1807
|
+
`,
|
|
1808
|
+
},
|
|
1809
|
+
];
|
|
1810
|
+
}
|
|
1811
|
+
if (!sourceExportsValue(existing, "rhf")) {
|
|
1812
|
+
throw new Error(`beignet make feature --with ui expects ${formsPath} to export rhf. Add \`export const rhf = createReactHookForm();\` from @beignet/react-hook-form, or move the existing helper before generating UI.`);
|
|
1813
|
+
}
|
|
1814
|
+
return [];
|
|
1815
|
+
}
|
|
1816
|
+
async function assertUploadUiApp(targetDir, config) {
|
|
1817
|
+
const clientPath = clientIndexPath(config);
|
|
1818
|
+
const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
|
|
1819
|
+
if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
|
|
1820
|
+
throw new Error(`beignet make upload --ui expects a full-stack Beignet app with ${clientPath} exporting rq. API-only apps stay backend-only; omit --ui or add the full-stack client setup first.`);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
function sourceExportsValue(source, name) {
|
|
1824
|
+
const sourceFile = ts.createSourceFile("client-forms.ts", source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
1825
|
+
for (const statement of sourceFile.statements) {
|
|
1826
|
+
const exported = ts.canHaveModifiers(statement) &&
|
|
1827
|
+
ts
|
|
1828
|
+
.getModifiers(statement)
|
|
1829
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
1830
|
+
if (exported && ts.isVariableStatement(statement)) {
|
|
1831
|
+
if (statement.declarationList.declarations.some((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name)) {
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (exported &&
|
|
1836
|
+
ts.isFunctionDeclaration(statement) &&
|
|
1837
|
+
statement.name?.text === name) {
|
|
1838
|
+
return true;
|
|
1839
|
+
}
|
|
1840
|
+
if (ts.isExportDeclaration(statement) &&
|
|
1841
|
+
!statement.isTypeOnly &&
|
|
1842
|
+
statement.exportClause &&
|
|
1843
|
+
ts.isNamedExports(statement.exportClause) &&
|
|
1844
|
+
statement.exportClause.elements.some((element) => !element.isTypeOnly && element.name.text === name)) {
|
|
1845
|
+
return true;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return false;
|
|
1849
|
+
}
|
|
1762
1850
|
function featureUiIndexFile(names, config) {
|
|
1763
1851
|
return {
|
|
1764
1852
|
path: featureUiIndexFilePath(names, config),
|
|
@@ -1887,12 +1975,54 @@ function scheduleFiles(names, config, options) {
|
|
|
1887
1975
|
}
|
|
1888
1976
|
return files;
|
|
1889
1977
|
}
|
|
1890
|
-
function uploadFiles(names, config) {
|
|
1978
|
+
function uploadFiles(names, config, options) {
|
|
1891
1979
|
return [
|
|
1980
|
+
{
|
|
1981
|
+
path: uploadManifestFilePath(names, config),
|
|
1982
|
+
content: uploadManifestFile(names),
|
|
1983
|
+
},
|
|
1892
1984
|
{
|
|
1893
1985
|
path: uploadFilePath(names, config),
|
|
1894
1986
|
content: uploadFile(names, config),
|
|
1895
1987
|
},
|
|
1988
|
+
...(options.ui ? uploadUiFiles(names, config) : []),
|
|
1989
|
+
];
|
|
1990
|
+
}
|
|
1991
|
+
function uploadFeatureDir(names, config) {
|
|
1992
|
+
return path.join(config.paths.features, names.feature.kebab);
|
|
1993
|
+
}
|
|
1994
|
+
function uploadManifestFilePath(names, config) {
|
|
1995
|
+
return path.join(uploadFeatureDir(names, config), `${names.manifestFileName}.ts`);
|
|
1996
|
+
}
|
|
1997
|
+
function uploadUiClientFilePath(names, config) {
|
|
1998
|
+
return path.join(uploadFeatureDir(names, config), "client", `${names.clientFileName}.ts`);
|
|
1999
|
+
}
|
|
2000
|
+
function uploadUiComponentFilePath(names, config) {
|
|
2001
|
+
return path.join(uploadFeatureDir(names, config), "components", `${names.componentFileName}.tsx`);
|
|
2002
|
+
}
|
|
2003
|
+
function uploadUiComponentTestFilePath(names, config) {
|
|
2004
|
+
return path.join(uploadFeatureDir(names, config), "tests", `${names.componentFileName}.test.tsx`);
|
|
2005
|
+
}
|
|
2006
|
+
function uploadUiComponentIndexFile(names, config) {
|
|
2007
|
+
return {
|
|
2008
|
+
path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
|
|
2009
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
function uploadUiFiles(names, config) {
|
|
2013
|
+
return [
|
|
2014
|
+
{
|
|
2015
|
+
path: uploadUiClientFilePath(names, config),
|
|
2016
|
+
content: uploadUiClientFile(names, config),
|
|
2017
|
+
},
|
|
2018
|
+
{
|
|
2019
|
+
path: uploadUiComponentFilePath(names, config),
|
|
2020
|
+
content: uploadUiComponentFile(names, config),
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
path: uploadUiComponentTestFilePath(names, config),
|
|
2024
|
+
content: uploadUiComponentTestFile(names, config),
|
|
2025
|
+
},
|
|
1896
2026
|
];
|
|
1897
2027
|
}
|
|
1898
2028
|
function outboxFiles(config) {
|
|
@@ -2166,7 +2296,10 @@ async function updateOutboxRegistry(targetDir, names, kind, config, options) {
|
|
|
2166
2296
|
const registryName = `${names.featureSingularCamel}${kind === "events" ? "Events" : "Jobs"}`;
|
|
2167
2297
|
const importLine = `import { ${registryName} } from "${aliasModule(featureIndexPath)}";`;
|
|
2168
2298
|
const appended = appendToOutboxRegistryArray(existing, kind, `...${registryName}`, importLine);
|
|
2169
|
-
if (appended.kind === "missing"
|
|
2299
|
+
if (appended.kind === "missing") {
|
|
2300
|
+
throw new Error(`Could not find the ${kind} array in ${config.paths.outbox}. Register ${registryName} manually, or restore the generated outbox file before running this command.`);
|
|
2301
|
+
}
|
|
2302
|
+
if (appended.kind === "unchanged") {
|
|
2170
2303
|
return "skipped";
|
|
2171
2304
|
}
|
|
2172
2305
|
if (!options.dryRun)
|
|
@@ -2195,7 +2328,7 @@ function schemasFile(names, mode, options) {
|
|
|
2195
2328
|
|
|
2196
2329
|
export const ${names.singularPascal}Schema = z.object({
|
|
2197
2330
|
id: z.string().uuid(),
|
|
2198
|
-
${options.
|
|
2331
|
+
${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
2199
2332
|
version: z.number().int().min(1),
|
|
2200
2333
|
createdAt: z.string().datetime(),
|
|
2201
2334
|
updatedAt: z.string().datetime(),
|
|
@@ -2295,7 +2428,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
2295
2428
|
});
|
|
2296
2429
|
|
|
2297
2430
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
2298
|
-
name: z.string().min(1).max(120),
|
|
2431
|
+
name: z.string().trim().min(1).max(120),
|
|
2299
2432
|
});
|
|
2300
2433
|
${mode === "resource"
|
|
2301
2434
|
? `
|
|
@@ -2304,7 +2437,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
|
|
|
2304
2437
|
});
|
|
2305
2438
|
|
|
2306
2439
|
export const Update${names.singularPascal}BodySchema = z.object({
|
|
2307
|
-
name: z.string().min(1).max(120),
|
|
2440
|
+
name: z.string().trim().min(1).max(120),
|
|
2308
2441
|
version: z.number().int().min(1),
|
|
2309
2442
|
});
|
|
2310
2443
|
|
|
@@ -2338,7 +2471,7 @@ function listUseCaseFile(names, config, options) {
|
|
|
2338
2471
|
const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
|
|
2339
2472
|
return `import "@beignet/core/server-only";
|
|
2340
2473
|
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
2341
|
-
${options.
|
|
2474
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2342
2475
|
import {
|
|
2343
2476
|
decode${names.singularPascal}Cursor,
|
|
2344
2477
|
List${names.pluralPascal}InputSchema,
|
|
@@ -2350,7 +2483,7 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
2350
2483
|
.input(List${names.pluralPascal}InputSchema)
|
|
2351
2484
|
.output(List${names.pluralPascal}OutputSchema)
|
|
2352
2485
|
.run(async ({ ctx, input }) => {
|
|
2353
|
-
${options.
|
|
2486
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
2354
2487
|
defaultLimit: 20,
|
|
2355
2488
|
maxLimit: 100,
|
|
2356
2489
|
});
|
|
@@ -2361,14 +2494,14 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
2361
2494
|
name: input.name,
|
|
2362
2495
|
sortBy: input.sortBy,
|
|
2363
2496
|
sortDirection: input.sortDirection,
|
|
2364
|
-
}${options.
|
|
2497
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
2365
2498
|
});
|
|
2366
2499
|
`;
|
|
2367
2500
|
}
|
|
2368
2501
|
function createUseCaseFile(names, config, options) {
|
|
2369
2502
|
const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
|
|
2370
2503
|
return `import "@beignet/core/server-only";
|
|
2371
|
-
${options.
|
|
2504
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2372
2505
|
import {
|
|
2373
2506
|
Create${names.singularPascal}InputSchema,
|
|
2374
2507
|
${names.singularPascal}Schema,
|
|
@@ -2380,7 +2513,7 @@ export const create${names.singularPascal}UseCase = useCase
|
|
|
2380
2513
|
.input(Create${names.singularPascal}InputSchema)
|
|
2381
2514
|
.output(${names.singularPascal}Schema)
|
|
2382
2515
|
.run(async ({ ctx, input }) => {
|
|
2383
|
-
${options.
|
|
2516
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create(input${options.tenantScoped ? ", scope" : ""});
|
|
2384
2517
|
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
2385
2518
|
return ${names.singularCamel};
|
|
2386
2519
|
});
|
|
@@ -2389,7 +2522,7 @@ ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPasca
|
|
|
2389
2522
|
function getUseCaseFile(names, config, options) {
|
|
2390
2523
|
const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
|
|
2391
2524
|
return `import "@beignet/core/server-only";
|
|
2392
|
-
${options.
|
|
2525
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2393
2526
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2394
2527
|
import {
|
|
2395
2528
|
${names.singularPascal}IdInputSchema,
|
|
@@ -2401,7 +2534,7 @@ export const get${names.singularPascal}UseCase = useCase
|
|
|
2401
2534
|
.input(${names.singularPascal}IdInputSchema)
|
|
2402
2535
|
.output(${names.singularPascal}Schema)
|
|
2403
2536
|
.run(async ({ ctx, input }) => {
|
|
2404
|
-
${options.
|
|
2537
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
2405
2538
|
if (!${names.singularCamel}) {
|
|
2406
2539
|
throw appError("${names.singularPascal}NotFound", {
|
|
2407
2540
|
details: { id: input.id },
|
|
@@ -2415,7 +2548,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
2415
2548
|
function updateUseCaseFile(names, config, options) {
|
|
2416
2549
|
const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
|
|
2417
2550
|
return `import "@beignet/core/server-only";
|
|
2418
|
-
${options.
|
|
2551
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2419
2552
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2420
2553
|
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
2421
2554
|
Update${names.singularPascal}InputSchema,
|
|
@@ -2427,13 +2560,13 @@ export const update${names.singularPascal}UseCase = useCase
|
|
|
2427
2560
|
.input(Update${names.singularPascal}InputSchema)
|
|
2428
2561
|
.output(${names.singularPascal}Schema)
|
|
2429
2562
|
.run(async ({ ctx, input }) => {
|
|
2430
|
-
${options.
|
|
2563
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
2431
2564
|
if (!existing) {
|
|
2432
2565
|
throw appError("${names.singularPascal}NotFound", {
|
|
2433
2566
|
details: { id: input.id },
|
|
2434
2567
|
});
|
|
2435
2568
|
}
|
|
2436
|
-
${options.
|
|
2569
|
+
${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
2437
2570
|
throw appError("${names.singularPascal}Conflict", {
|
|
2438
2571
|
details: { id: input.id, expectedVersion: existing.version },
|
|
2439
2572
|
});
|
|
@@ -2441,7 +2574,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
|
|
|
2441
2574
|
|
|
2442
2575
|
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
2443
2576
|
...input,
|
|
2444
|
-
}${options.
|
|
2577
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
2445
2578
|
if (!${names.singularCamel}) {
|
|
2446
2579
|
throw appError("${names.singularPascal}Conflict", {
|
|
2447
2580
|
details: { id: input.id, expectedVersion: existing.version },
|
|
@@ -2456,7 +2589,7 @@ function deleteUseCaseFile(names, config, options) {
|
|
|
2456
2589
|
const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
|
|
2457
2590
|
return `import "@beignet/core/server-only";
|
|
2458
2591
|
import { z } from "zod";
|
|
2459
|
-
${options.
|
|
2592
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2460
2593
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2461
2594
|
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
2462
2595
|
|
|
@@ -2465,7 +2598,7 @@ export const delete${names.singularPascal}UseCase = useCase
|
|
|
2465
2598
|
.input(${names.singularPascal}IdInputSchema)
|
|
2466
2599
|
.output(z.void())
|
|
2467
2600
|
.run(async ({ ctx, input }) => {
|
|
2468
|
-
${options.
|
|
2601
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});\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.tenantScoped ? ", scope" : ""});
|
|
2469
2602
|
if (!deleted) {
|
|
2470
2603
|
throw appError("${names.singularPascal}NotFound", {
|
|
2471
2604
|
details: { id: input.id },
|
|
@@ -2495,7 +2628,7 @@ function repositoryPortFile(names, config, mode, options) {
|
|
|
2495
2628
|
PageResult,
|
|
2496
2629
|
SortDirection,
|
|
2497
2630
|
} from "@beignet/core/pagination";
|
|
2498
|
-
${options.
|
|
2631
|
+
${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
|
|
2499
2632
|
Create${names.singularPascal}Input,
|
|
2500
2633
|
${names.singularPascal}Cursor,
|
|
2501
2634
|
${names.singularPascal}SortBy,
|
|
@@ -2512,9 +2645,9 @@ export type List${names.pluralPascal}Query = {
|
|
|
2512
2645
|
};
|
|
2513
2646
|
|
|
2514
2647
|
export interface ${names.singularPascal}Repository {
|
|
2515
|
-
list(query: List${names.pluralPascal}Query${options.
|
|
2516
|
-
create(input: Create${names.singularPascal}Input${options.
|
|
2517
|
-
${mode === "resource" ? ` findById(id: string${options.
|
|
2648
|
+
list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
|
|
2649
|
+
create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
|
|
2650
|
+
${mode === "resource" ? ` findById(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n update(input: Update${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<boolean>;\n` : ""}}
|
|
2518
2651
|
`;
|
|
2519
2652
|
}
|
|
2520
2653
|
function inMemoryRepositoryFile(names, config, mode, options) {
|
|
@@ -2542,13 +2675,13 @@ function inMemoryRepositoryFile(names, config, mode, options) {
|
|
|
2542
2675
|
? " || existing.deletedAt !== null"
|
|
2543
2676
|
: "";
|
|
2544
2677
|
const deleteImplementation = options.softDelete
|
|
2545
|
-
? `${options.
|
|
2678
|
+
? `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== 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();
|
|
2546
2679
|
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
2547
2680
|
return true;`
|
|
2548
|
-
: `${options.
|
|
2681
|
+
: `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
|
|
2549
2682
|
return `import "@beignet/core/server-only";
|
|
2550
2683
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2551
|
-
${options.
|
|
2684
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
2552
2685
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2553
2686
|
import type {
|
|
2554
2687
|
Create${names.singularPascal}Input,
|
|
@@ -2558,7 +2691,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
|
|
|
2558
2691
|
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
2559
2692
|
return {
|
|
2560
2693
|
id: ${names.singularCamel}.id,
|
|
2561
|
-
${options.
|
|
2694
|
+
${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
2562
2695
|
version: ${names.singularCamel}.version,
|
|
2563
2696
|
createdAt: ${names.singularCamel}.createdAt,
|
|
2564
2697
|
updatedAt: ${names.singularCamel}.updatedAt,
|
|
@@ -2616,10 +2749,10 @@ export function createInMemory${names.singularPascal}Repository(
|
|
|
2616
2749
|
);
|
|
2617
2750
|
|
|
2618
2751
|
return {
|
|
2619
|
-
async list(query${options.
|
|
2752
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
2620
2753
|
const name = query.name?.toLocaleLowerCase();
|
|
2621
|
-
${options.
|
|
2622
|
-
${activeFilter}${options.
|
|
2754
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
2755
|
+
${activeFilter}${options.tenantScoped ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
|
|
2623
2756
|
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
2624
2757
|
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
2625
2758
|
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
@@ -2634,11 +2767,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
|
|
|
2634
2767
|
nextCursor,
|
|
2635
2768
|
);
|
|
2636
2769
|
},
|
|
2637
|
-
async create(input${options.
|
|
2770
|
+
async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2638
2771
|
const now = new Date().toISOString();
|
|
2639
|
-
${options.
|
|
2772
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2640
2773
|
id: crypto.randomUUID(),
|
|
2641
|
-
${options.
|
|
2774
|
+
${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
2642
2775
|
version: 1,
|
|
2643
2776
|
createdAt: now,
|
|
2644
2777
|
updatedAt: now,
|
|
@@ -2648,7 +2781,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
|
2648
2781
|
${newDeletedAtField} });
|
|
2649
2782
|
return ${names.singularCamel};
|
|
2650
2783
|
},
|
|
2651
|
-
${mode === "resource" ? ` async findById(id: string${options.
|
|
2784
|
+
${mode === "resource" ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenantScoped ? `\t\t\tif (${names.singularCamel}?.tenantId !== tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenantScoped ? " || existing.tenantId !== 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.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""}${deleteImplementation}\n },\n` : ""}
|
|
2652
2785
|
};
|
|
2653
2786
|
}
|
|
2654
2787
|
`;
|
|
@@ -2658,7 +2791,7 @@ function drizzleSchemaFile(names, options, dialect) {
|
|
|
2658
2791
|
|
|
2659
2792
|
export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
2660
2793
|
id: ${dialect.idColumn("id")}.primaryKey(),
|
|
2661
|
-
${options.
|
|
2794
|
+
${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
|
|
2662
2795
|
version: ${dialect.integerColumn("version")}.notNull(),
|
|
2663
2796
|
createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
|
|
2664
2797
|
updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
|
|
@@ -2676,6 +2809,7 @@ export const outboxMessages = mysqlTable(
|
|
|
2676
2809
|
\t\tkind: varchar("kind", { length: 32 }).notNull(),
|
|
2677
2810
|
\t\tname: varchar("name", { length: 255 }).notNull(),
|
|
2678
2811
|
\t\tpayloadJson: longtext("payload_json").notNull(),
|
|
2812
|
+
\t\ttraceContextJson: longtext("trace_context_json"),
|
|
2679
2813
|
\t\tstatus: varchar("status", { length: 32 }).notNull(),
|
|
2680
2814
|
\t\tattempts: int("attempts").notNull().default(0),
|
|
2681
2815
|
\t\tmaxAttempts: int("max_attempts").notNull().default(3),
|
|
@@ -2710,6 +2844,7 @@ export const outboxMessages = ${dialect.tableFunction}(
|
|
|
2710
2844
|
\t\tkind: text("kind").notNull(),
|
|
2711
2845
|
\t\tname: text("name").notNull(),
|
|
2712
2846
|
\t\tpayloadJson: text("payload_json").notNull(),
|
|
2847
|
+
\t\ttraceContextJson: text("trace_context_json"),
|
|
2713
2848
|
\t\tstatus: text("status").notNull(),
|
|
2714
2849
|
\t\tattempts: integer("attempts").notNull().default(0),
|
|
2715
2850
|
\t\tmaxAttempts: integer("max_attempts").notNull().default(3),
|
|
@@ -2753,20 +2888,20 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
|
|
|
2753
2888
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
2754
2889
|
const findWhere = drizzleResourceWhere(names, options, [
|
|
2755
2890
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
2756
|
-
...(options.
|
|
2891
|
+
...(options.tenantScoped
|
|
2757
2892
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2758
2893
|
: []),
|
|
2759
2894
|
]);
|
|
2760
2895
|
const updateWhere = drizzleResourceWhere(names, options, [
|
|
2761
2896
|
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
2762
2897
|
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
2763
|
-
...(options.
|
|
2898
|
+
...(options.tenantScoped
|
|
2764
2899
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2765
2900
|
: []),
|
|
2766
2901
|
]);
|
|
2767
2902
|
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
2768
2903
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
2769
|
-
...(options.
|
|
2904
|
+
...(options.tenantScoped
|
|
2770
2905
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2771
2906
|
: []),
|
|
2772
2907
|
]);
|
|
@@ -2784,7 +2919,7 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
|
|
|
2784
2919
|
].filter((name) => Boolean(name));
|
|
2785
2920
|
// After a version-guarded update succeeds, re-select by identity only —
|
|
2786
2921
|
// matching what `.returning()` yields on the dialects that support it.
|
|
2787
|
-
const updateReselectWhere = options.
|
|
2922
|
+
const updateReselectWhere = options.tenantScoped
|
|
2788
2923
|
? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
|
|
2789
2924
|
: `eq(schema.${names.pluralCamel}.id, input.id)`;
|
|
2790
2925
|
// drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
|
|
@@ -2806,13 +2941,13 @@ function affectedRows(result: unknown): number {
|
|
|
2806
2941
|
`
|
|
2807
2942
|
: "";
|
|
2808
2943
|
const createMethod = dialect.supportsReturning
|
|
2809
|
-
? ` async create(input${options.
|
|
2944
|
+
? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2810
2945
|
const now = new Date().toISOString();
|
|
2811
2946
|
const [row] = await db
|
|
2812
2947
|
.insert(schema.${names.pluralCamel})
|
|
2813
2948
|
.values({
|
|
2814
2949
|
id: crypto.randomUUID(),
|
|
2815
|
-
${options.
|
|
2950
|
+
${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
2816
2951
|
version: 1,
|
|
2817
2952
|
createdAt: now,
|
|
2818
2953
|
updatedAt: now,
|
|
@@ -2826,11 +2961,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
|
|
|
2826
2961
|
return to${names.singularPascal}(row);
|
|
2827
2962
|
},
|
|
2828
2963
|
`
|
|
2829
|
-
: ` async create(input${options.
|
|
2964
|
+
: ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2830
2965
|
const now = new Date().toISOString();
|
|
2831
2966
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2832
2967
|
id: crypto.randomUUID(),
|
|
2833
|
-
${options.
|
|
2968
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
2834
2969
|
version: 1,
|
|
2835
2970
|
createdAt: now,
|
|
2836
2971
|
updatedAt: now,
|
|
@@ -2843,7 +2978,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2843
2978
|
const resourceMethods = mode !== "resource"
|
|
2844
2979
|
? ""
|
|
2845
2980
|
: dialect.supportsReturning
|
|
2846
|
-
? ` async findById(id: string${options.
|
|
2981
|
+
? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2847
2982
|
const [row] = await db
|
|
2848
2983
|
.select()
|
|
2849
2984
|
.from(schema.${names.pluralCamel})
|
|
@@ -2852,7 +2987,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2852
2987
|
|
|
2853
2988
|
return row ? to${names.singularPascal}(row) : null;
|
|
2854
2989
|
},
|
|
2855
|
-
async update(input${options.
|
|
2990
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2856
2991
|
const [row] = await db
|
|
2857
2992
|
.update(schema.${names.pluralCamel})
|
|
2858
2993
|
.set({
|
|
@@ -2865,12 +3000,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2865
3000
|
|
|
2866
3001
|
return row ? to${names.singularPascal}(row) : null;
|
|
2867
3002
|
},
|
|
2868
|
-
async delete(id: string${options.
|
|
3003
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2869
3004
|
${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`}
|
|
2870
3005
|
return rows.length > 0;
|
|
2871
3006
|
},
|
|
2872
3007
|
`
|
|
2873
|
-
: ` async findById(id: string${options.
|
|
3008
|
+
: ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2874
3009
|
const [row] = await db
|
|
2875
3010
|
.select()
|
|
2876
3011
|
.from(schema.${names.pluralCamel})
|
|
@@ -2879,7 +3014,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
2879
3014
|
|
|
2880
3015
|
return row ? to${names.singularPascal}(row) : null;
|
|
2881
3016
|
},
|
|
2882
|
-
async update(input${options.
|
|
3017
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2883
3018
|
const result = await db
|
|
2884
3019
|
.update(schema.${names.pluralCamel})
|
|
2885
3020
|
.set({
|
|
@@ -2899,14 +3034,14 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
2899
3034
|
|
|
2900
3035
|
return row ? to${names.singularPascal}(row) : null;
|
|
2901
3036
|
},
|
|
2902
|
-
async delete(id: string${options.
|
|
3037
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2903
3038
|
${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst result = 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\tconst result = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere});\n`}
|
|
2904
3039
|
return affectedRows(result) > 0;
|
|
2905
3040
|
},
|
|
2906
3041
|
`;
|
|
2907
3042
|
return `import "@beignet/core/server-only";
|
|
2908
3043
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2909
|
-
${options.
|
|
3044
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
|
|
2910
3045
|
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
2911
3046
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
2912
3047
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
@@ -2921,7 +3056,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
2921
3056
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
2922
3057
|
return {
|
|
2923
3058
|
id: row.id,
|
|
2924
|
-
${options.
|
|
3059
|
+
${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
2925
3060
|
version: row.version,
|
|
2926
3061
|
createdAt: row.createdAt,
|
|
2927
3062
|
updatedAt: row.updatedAt,
|
|
@@ -2953,9 +3088,9 @@ function ${names.singularCamel}CursorFilter(
|
|
|
2953
3088
|
|
|
2954
3089
|
function ${names.singularCamel}ListWhere(
|
|
2955
3090
|
query: List${names.pluralPascal}Query,
|
|
2956
|
-
${options.
|
|
3091
|
+
${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
|
|
2957
3092
|
const filters: SQL<unknown>[] = [
|
|
2958
|
-
${options.
|
|
3093
|
+
${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
2959
3094
|
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
2960
3095
|
|
|
2961
3096
|
if (query.name) {
|
|
@@ -2989,8 +3124,8 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
2989
3124
|
db: ${dialect.dbTypeName}<typeof schema>,
|
|
2990
3125
|
): ${names.singularPascal}Repository {
|
|
2991
3126
|
return {
|
|
2992
|
-
async list(query${options.
|
|
2993
|
-
const where = ${names.singularCamel}ListWhere(query${options.
|
|
3127
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
3128
|
+
const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
|
|
2994
3129
|
const rows = await db
|
|
2995
3130
|
.select()
|
|
2996
3131
|
.from(schema.${names.pluralCamel})
|
|
@@ -3046,7 +3181,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
3046
3181
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
3047
3182
|
.post("/api/${names.pluralKebab}")
|
|
3048
3183
|
.body(Create${names.singularPascal}InputSchema)
|
|
3049
|
-
${options.
|
|
3184
|
+
${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
3050
3185
|
201: ${names.singularPascal}Schema,
|
|
3051
3186
|
});
|
|
3052
3187
|
${mode === "resource"
|
|
@@ -3063,7 +3198,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
|
|
|
3063
3198
|
.patch("/api/${names.pluralKebab}/:id")
|
|
3064
3199
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3065
3200
|
.body(Update${names.singularPascal}BodySchema)
|
|
3066
|
-
${options.
|
|
3201
|
+
${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
|
|
3067
3202
|
.responses({
|
|
3068
3203
|
200: ${names.singularPascal}Schema,
|
|
3069
3204
|
});
|
|
@@ -3071,7 +3206,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
|
|
|
3071
3206
|
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
3072
3207
|
.delete("/api/${names.pluralKebab}/:id")
|
|
3073
3208
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3074
|
-
${options.
|
|
3209
|
+
${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
3075
3210
|
.responses({
|
|
3076
3211
|
204: null,
|
|
3077
3212
|
});
|
|
@@ -3157,15 +3292,15 @@ import { createInMemoryDevtools } from "@beignet/devtools";
|
|
|
3157
3292
|
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
3158
3293
|
import { createTestAnonymousActor } from "@beignet/core/testing";
|
|
3159
3294
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3160
|
-
import {
|
|
3295
|
+
import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
|
|
3161
3296
|
import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
|
|
3162
3297
|
|
|
3163
3298
|
describe("${names.exportName}", () => {
|
|
3164
3299
|
it("runs ${names.action.camel}", async () => {
|
|
3165
3300
|
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
3166
|
-
base:
|
|
3301
|
+
base: initialPorts,
|
|
3167
3302
|
overrides: {
|
|
3168
|
-
gate:
|
|
3303
|
+
gate: initialPorts.gate,
|
|
3169
3304
|
devtools: createInMemoryDevtools(),
|
|
3170
3305
|
},
|
|
3171
3306
|
});
|
|
@@ -3255,7 +3390,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
|
|
|
3255
3390
|
|
|
3256
3391
|
function canWrite(ctx: AuthorizationContext) {
|
|
3257
3392
|
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
3258
|
-
${options.
|
|
3393
|
+
${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
3259
3394
|
}
|
|
3260
3395
|
|
|
3261
3396
|
export const ${names.singularCamel}Policy = definePolicy({
|
|
@@ -3265,13 +3400,13 @@ export const ${names.singularCamel}Policy = definePolicy({
|
|
|
3265
3400
|
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3266
3401
|
const decision = canWrite(ctx);
|
|
3267
3402
|
if (decision !== true) return decision;
|
|
3268
|
-
${options.
|
|
3403
|
+
${options.tenantScoped ? `\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` : ""}
|
|
3269
3404
|
return true;
|
|
3270
3405
|
},
|
|
3271
3406
|
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3272
3407
|
const decision = canWrite(ctx);
|
|
3273
3408
|
if (decision !== true) return decision;
|
|
3274
|
-
${options.
|
|
3409
|
+
${options.tenantScoped ? `\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` : ""}
|
|
3275
3410
|
return true;
|
|
3276
3411
|
},
|
|
3277
3412
|
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
@@ -3318,7 +3453,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
|
|
|
3318
3453
|
const now = new Date().toISOString();
|
|
3319
3454
|
return {
|
|
3320
3455
|
id: "00000000-0000-4000-8000-000000000001",
|
|
3321
|
-
${options.
|
|
3456
|
+
${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
3322
3457
|
version: 1,
|
|
3323
3458
|
createdAt: now,
|
|
3324
3459
|
updatedAt: now,
|
|
@@ -3333,7 +3468,7 @@ describe("${names.singularCamel}Policy", () => {
|
|
|
3333
3468
|
const ${names.singularCamel} = create${names.singularPascal}();
|
|
3334
3469
|
const ctx = {
|
|
3335
3470
|
actor: { type: "user" as const, id: "user_test" },
|
|
3336
|
-
${options.
|
|
3471
|
+
${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
3337
3472
|
|
|
3338
3473
|
await expect(
|
|
3339
3474
|
tester.assertMatrix([
|
|
@@ -3535,8 +3670,32 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
3535
3670
|
);
|
|
3536
3671
|
`;
|
|
3537
3672
|
}
|
|
3673
|
+
function uploadManifestFile(names) {
|
|
3674
|
+
return `import type {
|
|
3675
|
+
\tUploadFileConstraints,
|
|
3676
|
+
\tUploadManifestEntry,
|
|
3677
|
+
} from "@beignet/core/uploads";
|
|
3678
|
+
|
|
3679
|
+
export const ${names.fileConstraintsExportName} = {
|
|
3680
|
+
\tcontentTypes: ["application/pdf", "text/plain"],
|
|
3681
|
+
\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
3682
|
+
\tmaxFiles: 1,
|
|
3683
|
+
\tvisibility: "private",
|
|
3684
|
+
\tcacheControl: "private, max-age=0",
|
|
3685
|
+
} satisfies UploadFileConstraints;
|
|
3686
|
+
|
|
3687
|
+
export const ${names.manifestExportName} = [
|
|
3688
|
+
\t{
|
|
3689
|
+
\t\tname: "${names.uploadName}",
|
|
3690
|
+
\t\tdescription: "${names.artifact.pascal} upload",
|
|
3691
|
+
\t\tfile: ${names.fileConstraintsExportName},
|
|
3692
|
+
\t},
|
|
3693
|
+
] satisfies readonly UploadManifestEntry[];
|
|
3694
|
+
`;
|
|
3695
|
+
}
|
|
3538
3696
|
function uploadFile(names, config) {
|
|
3539
3697
|
return `import { z } from "zod";
|
|
3698
|
+
import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
|
|
3540
3699
|
import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
|
|
3541
3700
|
|
|
3542
3701
|
export const ${names.metadataSchemaName} = z.object({
|
|
@@ -3547,13 +3706,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
|
|
|
3547
3706
|
|
|
3548
3707
|
export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
3549
3708
|
\tmetadata: ${names.metadataSchemaName},
|
|
3550
|
-
\tfile: {
|
|
3551
|
-
\t\tcontentTypes: ["application/pdf", "text/plain"],
|
|
3552
|
-
\t\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
3553
|
-
\t\tmaxFiles: 1,
|
|
3554
|
-
\t\tvisibility: "private",
|
|
3555
|
-
\t\tcacheControl: "private, max-age=0",
|
|
3556
|
-
\t},
|
|
3709
|
+
\tfile: ${names.fileConstraintsExportName},
|
|
3557
3710
|
\tauthorize({ ctx }) {
|
|
3558
3711
|
\t\treturn ctx.actor.type === "user";
|
|
3559
3712
|
\t},
|
|
@@ -3583,6 +3736,127 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
|
3583
3736
|
});
|
|
3584
3737
|
`;
|
|
3585
3738
|
}
|
|
3739
|
+
function uploadUiClientFile(names, config) {
|
|
3740
|
+
const clientPath = uploadUiClientFilePath(names, config);
|
|
3741
|
+
const registryPath = path.join(featureArtifactDir(names, "uploads", config), "index.ts");
|
|
3742
|
+
return `import { createUploadClient } from "@beignet/core/uploads/client";
|
|
3743
|
+
import { createReactUploads } from "@beignet/react-uploads";
|
|
3744
|
+
import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
|
|
3745
|
+
import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
|
|
3746
|
+
|
|
3747
|
+
const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
|
|
3748
|
+
\tbaseUrl: "/api/uploads",
|
|
3749
|
+
\tmanifest: ${names.manifestExportName},
|
|
3750
|
+
});
|
|
3751
|
+
|
|
3752
|
+
export const ${names.reactUploadsExportName} = createReactUploads({
|
|
3753
|
+
\tuploads: ${names.artifact.camel}UploadClient,
|
|
3754
|
+
});
|
|
3755
|
+
`;
|
|
3756
|
+
}
|
|
3757
|
+
function uploadUiComponentFile(names, config) {
|
|
3758
|
+
const componentPath = uploadUiComponentFilePath(names, config);
|
|
3759
|
+
return `"use client";
|
|
3760
|
+
|
|
3761
|
+
import { useId } from "react";
|
|
3762
|
+
import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
|
|
3763
|
+
|
|
3764
|
+
export type ${names.componentExportName}Props = {
|
|
3765
|
+
\tresourceId: string;
|
|
3766
|
+
\tdisabled?: boolean;
|
|
3767
|
+
\tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
|
|
3768
|
+
};
|
|
3769
|
+
|
|
3770
|
+
export function ${names.componentExportName}({
|
|
3771
|
+
\tresourceId,
|
|
3772
|
+
\tdisabled = false,
|
|
3773
|
+
\tonComplete,
|
|
3774
|
+
}: ${names.componentExportName}Props) {
|
|
3775
|
+
\tconst inputId = useId();
|
|
3776
|
+
\tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
|
|
3777
|
+
\t\tonSuccess({ result }) {
|
|
3778
|
+
\t\t\treturn onComplete?.(result.objectKeys);
|
|
3779
|
+
\t\t},
|
|
3780
|
+
\t});
|
|
3781
|
+
\tconst maxSizeMb = upload.constraints?.maxSizeBytes
|
|
3782
|
+
\t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
|
|
3783
|
+
\t\t: undefined;
|
|
3784
|
+
|
|
3785
|
+
\treturn (
|
|
3786
|
+
\t\t<section className="${names.artifact.kebab}-uploader">
|
|
3787
|
+
\t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
|
|
3788
|
+
\t\t\t<input
|
|
3789
|
+
\t\t\t\tid={inputId}
|
|
3790
|
+
\t\t\t\ttype="file"
|
|
3791
|
+
\t\t\t\taccept={upload.accept}
|
|
3792
|
+
\t\t\t\tdisabled={disabled || upload.isUploading}
|
|
3793
|
+
\t\t\t\tonChange={(event) => {
|
|
3794
|
+
\t\t\t\t\tconst file = event.currentTarget.files?.[0];
|
|
3795
|
+
\t\t\t\t\tevent.currentTarget.value = "";
|
|
3796
|
+
\t\t\t\t\tif (!file) return;
|
|
3797
|
+
|
|
3798
|
+
\t\t\t\t\tupload.uploadFile(file, {
|
|
3799
|
+
\t\t\t\t\t\tmetadata: { resourceId },
|
|
3800
|
+
\t\t\t\t\t});
|
|
3801
|
+
\t\t\t\t}}
|
|
3802
|
+
\t\t\t/>
|
|
3803
|
+
\t\t\t<p>
|
|
3804
|
+
\t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
|
|
3805
|
+
\t\t\t</p>
|
|
3806
|
+
|
|
3807
|
+
\t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
|
|
3808
|
+
\t\t\t{upload.isUploading ? (
|
|
3809
|
+
\t\t\t\t<div aria-live="polite">
|
|
3810
|
+
\t\t\t\t\t<progress
|
|
3811
|
+
\t\t\t\t\t\taria-label="Upload progress"
|
|
3812
|
+
\t\t\t\t\t\tmax={100}
|
|
3813
|
+
\t\t\t\t\t\tvalue={upload.progress}
|
|
3814
|
+
\t\t\t\t\t/>
|
|
3815
|
+
\t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
|
|
3816
|
+
\t\t\t\t\t<button type="button" onClick={upload.abort}>
|
|
3817
|
+
\t\t\t\t\t\tCancel upload
|
|
3818
|
+
\t\t\t\t\t</button>
|
|
3819
|
+
\t\t\t\t</div>
|
|
3820
|
+
\t\t\t) : null}
|
|
3821
|
+
\t\t\t{upload.isError ? (
|
|
3822
|
+
\t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
|
|
3823
|
+
\t\t\t) : null}
|
|
3824
|
+
\t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
|
|
3825
|
+
\t\t\t{upload.isError || upload.isSuccess ? (
|
|
3826
|
+
\t\t\t\t<button type="button" onClick={upload.reset}>
|
|
3827
|
+
\t\t\t\t\tReset upload
|
|
3828
|
+
\t\t\t\t</button>
|
|
3829
|
+
\t\t\t) : null}
|
|
3830
|
+
\t\t</section>
|
|
3831
|
+
\t);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
function uploadErrorMessage(error: unknown): string {
|
|
3835
|
+
\treturn error instanceof Error ? error.message : "Upload failed.";
|
|
3836
|
+
}
|
|
3837
|
+
`;
|
|
3838
|
+
}
|
|
3839
|
+
function uploadUiComponentTestFile(names, config) {
|
|
3840
|
+
const testPath = uploadUiComponentTestFilePath(names, config);
|
|
3841
|
+
const testHelperPath = path.join(path.dirname(config.paths.useCaseBuilder), "beignet-test.ts");
|
|
3842
|
+
return `import { renderToStaticMarkup } from "react-dom/server";
|
|
3843
|
+
import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
|
|
3844
|
+
import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
|
|
3845
|
+
import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
|
|
3846
|
+
|
|
3847
|
+
describe("${names.componentExportName}", () => {
|
|
3848
|
+
\ttest("renders the generated upload constraints", () => {
|
|
3849
|
+
\t\tconst markup = renderToStaticMarkup(
|
|
3850
|
+
\t\t\t<${names.componentExportName} resourceId="resource-1" />,
|
|
3851
|
+
\t\t);
|
|
3852
|
+
|
|
3853
|
+
\t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
|
|
3854
|
+
\t\texpect(markup).toContain('accept="application/pdf,text/plain"');
|
|
3855
|
+
\t\texpect(markup).toContain("PDF or plain text");
|
|
3856
|
+
\t});
|
|
3857
|
+
});
|
|
3858
|
+
`;
|
|
3859
|
+
}
|
|
3586
3860
|
function uploadRouteFile(names, config) {
|
|
3587
3861
|
const registryName = `${names.featureSingularCamel}Uploads`;
|
|
3588
3862
|
return `import {
|
|
@@ -3671,63 +3945,70 @@ function featureUiComponentFile(names, config) {
|
|
|
3671
3945
|
const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
|
|
3672
3946
|
return `"use client";
|
|
3673
3947
|
|
|
3674
|
-
import {
|
|
3948
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
3675
3949
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3676
|
-
import {
|
|
3950
|
+
import { rhf } from "${aliasModule(clientFormsPath(config))}";
|
|
3951
|
+
import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
|
|
3677
3952
|
import {
|
|
3678
3953
|
\tcreate${names.singularPascal}MutationOptions,
|
|
3679
3954
|
\tinvalidate${names.pluralPascal},
|
|
3680
3955
|
\tlist${names.pluralPascal}QueryOptions,
|
|
3681
3956
|
} from "${relativeModule(componentPath, clientQueriesPath)}";
|
|
3682
3957
|
|
|
3958
|
+
const create${names.singularPascal}Form = rhf(create${names.singularPascal});
|
|
3959
|
+
|
|
3683
3960
|
export function ${names.componentExportName}() {
|
|
3684
|
-
\tconst [name, setName] = useState("");
|
|
3685
3961
|
\tconst queryClient = useQueryClient();
|
|
3686
3962
|
\tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
|
|
3963
|
+
\tconst form = create${names.singularPascal}Form.useForm({
|
|
3964
|
+
\t\tdefaultValues: { name: "" },
|
|
3965
|
+
\t});
|
|
3966
|
+
\tconst name = form.watch("name");
|
|
3687
3967
|
\tconst create${names.singularPascal}Mutation = useMutation({
|
|
3688
3968
|
\t\t...create${names.singularPascal}MutationOptions(),
|
|
3689
3969
|
\t\tonSuccess: async () => {
|
|
3690
|
-
\t\t\
|
|
3970
|
+
\t\t\tform.reset({ name: "" });
|
|
3691
3971
|
\t\t\tawait invalidate${names.pluralPascal}(queryClient);
|
|
3692
3972
|
\t\t},
|
|
3973
|
+
\t\tonError: (error) => {
|
|
3974
|
+
\t\t\tform.setError(
|
|
3975
|
+
\t\t\t\t"root",
|
|
3976
|
+
\t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
|
|
3977
|
+
\t\t\t);
|
|
3978
|
+
\t\t},
|
|
3693
3979
|
\t});
|
|
3694
3980
|
|
|
3695
3981
|
\treturn (
|
|
3696
3982
|
\t\t<section className="${names.pluralKebab}-panel">
|
|
3697
3983
|
\t\t\t<form
|
|
3698
3984
|
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
3699
|
-
\t\t\t\tonSubmit={(
|
|
3700
|
-
\t\t\t\t\
|
|
3701
|
-
\t\t\t\t\tconst trimmedName = name.trim();
|
|
3702
|
-
\t\t\t\t\tif (!trimmedName) return;
|
|
3985
|
+
\t\t\t\tonSubmit={form.handleSubmit((values) => {
|
|
3986
|
+
\t\t\t\t\tform.clearErrors("root");
|
|
3703
3987
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
3704
3988
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
3705
|
-
\t\t\t\t\t\tbody:
|
|
3989
|
+
\t\t\t\t\t\tbody: values,
|
|
3706
3990
|
\t\t\t\t\t});
|
|
3707
|
-
\t\t\t\t}}
|
|
3991
|
+
\t\t\t\t})}
|
|
3708
3992
|
\t\t\t>
|
|
3709
3993
|
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
3710
3994
|
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
3711
3995
|
\t\t\t\t\t<input
|
|
3712
3996
|
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
3713
|
-
\t\t\t\t\t\
|
|
3714
|
-
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
3997
|
+
\t\t\t\t\t\t{...form.register("name")}
|
|
3715
3998
|
\t\t\t\t\t\tplaceholder="Name"
|
|
3716
3999
|
\t\t\t\t\t/>
|
|
3717
4000
|
\t\t\t\t\t<button
|
|
3718
4001
|
\t\t\t\t\t\ttype="submit"
|
|
3719
|
-
\t\t\t\t\t\tdisabled={!name
|
|
4002
|
+
\t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
|
|
3720
4003
|
\t\t\t\t\t>
|
|
3721
4004
|
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
3722
4005
|
\t\t\t\t\t</button>
|
|
3723
4006
|
\t\t\t\t</div>
|
|
3724
|
-
\t\t\t\t{
|
|
3725
|
-
\t\t\t\t\t<p role="alert">
|
|
3726
|
-
\t\t\t\t
|
|
3727
|
-
\t\t\t\t
|
|
3728
|
-
\t\t\t\t\t
|
|
3729
|
-
\t\t\t\t\t\t)}
|
|
3730
|
-
\t\t\t\t\t</p>
|
|
4007
|
+
\t\t\t\t{form.formState.errors.name ? (
|
|
4008
|
+
\t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
|
|
4009
|
+
\t\t\t\t) : null}
|
|
4010
|
+
\t\t\t\t{form.formState.errors.root ? (
|
|
4011
|
+
\t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
|
|
3731
4012
|
\t\t\t\t) : null}
|
|
3732
4013
|
\t\t\t</form>
|
|
3733
4014
|
|
|
@@ -3832,7 +4113,7 @@ ${mode === "resource" ? ` { contract: get${names.singularPascal}, useCase: get$
|
|
|
3832
4113
|
`;
|
|
3833
4114
|
}
|
|
3834
4115
|
function testFile(names, config, mode, options) {
|
|
3835
|
-
const repositoryPath = path.join(path.dirname(config.paths.
|
|
4116
|
+
const repositoryPath = path.join(path.dirname(config.paths.portWiring), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
|
|
3836
4117
|
const serverContextPath = path.join(path.dirname(config.paths.server), "context.ts");
|
|
3837
4118
|
const requestTarget = "app";
|
|
3838
4119
|
return `import { describe, expect, it } from "@/lib/beignet-test";
|
|
@@ -3841,9 +4122,9 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
|
|
|
3841
4122
|
import { createTestApp } from "@beignet/web/testing";
|
|
3842
4123
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3843
4124
|
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
3844
|
-
import { ${options.
|
|
4125
|
+
import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
|
|
3845
4126
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3846
|
-
import {
|
|
4127
|
+
import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
|
|
3847
4128
|
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
3848
4129
|
import {
|
|
3849
4130
|
create${names.singularPascal},
|
|
@@ -3861,39 +4142,39 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
3861
4142
|
const seed${names.pluralPascal} = [
|
|
3862
4143
|
{
|
|
3863
4144
|
id: "00000000-0000-4000-8000-000000000101",
|
|
3864
|
-
${options.
|
|
4145
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
3865
4146
|
version: 1,
|
|
3866
4147
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
3867
4148
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
3868
4149
|
},
|
|
3869
4150
|
{
|
|
3870
4151
|
id: "00000000-0000-4000-8000-000000000102",
|
|
3871
|
-
${options.
|
|
4152
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
3872
4153
|
version: 1,
|
|
3873
4154
|
createdAt: "2024-01-02T00:00:00.000Z",
|
|
3874
4155
|
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
3875
4156
|
},
|
|
3876
4157
|
{
|
|
3877
4158
|
id: "00000000-0000-4000-8000-000000000103",
|
|
3878
|
-
${options.
|
|
4159
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
3879
4160
|
version: 1,
|
|
3880
4161
|
createdAt: "2024-01-03T00:00:00.000Z",
|
|
3881
4162
|
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
3882
4163
|
},
|
|
3883
|
-
${options.
|
|
4164
|
+
${options.tenantScoped ? `\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` : ""} ];
|
|
3884
4165
|
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
3885
4166
|
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
3886
|
-
base:
|
|
4167
|
+
base: initialPorts,
|
|
3887
4168
|
overrides: {
|
|
3888
4169
|
${names.pluralCamel},
|
|
3889
|
-
${options.
|
|
4170
|
+
${options.authorization || options.tenantScoped
|
|
3890
4171
|
? `\t\t\t\tauth: {
|
|
3891
4172
|
getSession: async () => ({
|
|
3892
|
-
user: { id: "user_test", name: "Test User"${options.
|
|
3893
|
-
${options.
|
|
4173
|
+
user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
|
|
4174
|
+
${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
|
|
3894
4175
|
}),
|
|
3895
4176
|
},
|
|
3896
|
-
${options.
|
|
4177
|
+
${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
3897
4178
|
`
|
|
3898
4179
|
: `\t\t\t\tauth: {
|
|
3899
4180
|
getSession: async () => null,
|
|
@@ -3908,8 +4189,8 @@ ${options.auth ? `\t\t\t\tgate: appPorts.gate,\n` : ""}
|
|
|
3908
4189
|
});
|
|
3909
4190
|
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
3910
4191
|
ports: testFixture.ports,
|
|
3911
|
-
actor: ${options.
|
|
3912
|
-
tenant: ${options.
|
|
4192
|
+
actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
4193
|
+
tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
|
|
3913
4194
|
});
|
|
3914
4195
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
3915
4196
|
|