@beignet/cli 0.0.38 → 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 +9 -0
- package/README.md +120 -96
- 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/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 +54 -1
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/make/inbox.js +3 -3
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +3 -3
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +10 -3
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +7 -8
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts +3 -2
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +455 -185
- 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/provider-add.d.ts +21 -3
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +101 -65
- package/dist/provider-add.js.map +1 -1
- package/dist/task.js +1 -1
- 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 +5 -5
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +17 -13
- 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/package.json +2 -2
- package/skills/app-structure/SKILL.md +13 -8
- package/src/choices.ts +38 -88
- package/src/create-prompts.ts +20 -21
- package/src/create.ts +54 -36
- package/src/index.ts +193 -65
- package/src/inspect.ts +101 -1
- package/src/lib.ts +1 -1
- package/src/make/inbox.ts +3 -3
- package/src/make/payments.ts +3 -3
- package/src/make/shared.ts +16 -10
- package/src/make.ts +578 -176
- package/src/mcp.ts +20 -13
- package/src/provider-add.ts +159 -79
- package/src/task.ts +1 -1
- package/src/templates/base.ts +16 -16
- package/src/templates/index.ts +6 -6
- package/src/templates/server.ts +17 -13
- package/src/templates/shared.ts +10 -10
- 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),
|
|
@@ -529,7 +521,7 @@ export async function makeEvent(options) {
|
|
|
529
521
|
config,
|
|
530
522
|
force: Boolean(options.force),
|
|
531
523
|
dryRun: Boolean(options.dryRun),
|
|
532
|
-
name:
|
|
524
|
+
name: names.stableName,
|
|
533
525
|
files: eventFiles(names, config),
|
|
534
526
|
index: featureArtifactIndexFile("event", names, config),
|
|
535
527
|
dependencies: {
|
|
@@ -570,7 +562,7 @@ export async function makeJob(options) {
|
|
|
570
562
|
config,
|
|
571
563
|
force: Boolean(options.force),
|
|
572
564
|
dryRun: Boolean(options.dryRun),
|
|
573
|
-
name:
|
|
565
|
+
name: names.stableName,
|
|
574
566
|
files: [
|
|
575
567
|
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
576
568
|
...jobFiles(names, config),
|
|
@@ -603,7 +595,7 @@ export async function makeTask(options) {
|
|
|
603
595
|
config,
|
|
604
596
|
force: Boolean(options.force),
|
|
605
597
|
dryRun: Boolean(options.dryRun),
|
|
606
|
-
name:
|
|
598
|
+
name: names.stableName,
|
|
607
599
|
files: [
|
|
608
600
|
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
609
601
|
...taskFiles(names, config),
|
|
@@ -641,7 +633,7 @@ export async function makeFactory(options) {
|
|
|
641
633
|
config,
|
|
642
634
|
force: Boolean(options.force),
|
|
643
635
|
dryRun: Boolean(options.dryRun),
|
|
644
|
-
name:
|
|
636
|
+
name: names.stableName,
|
|
645
637
|
files: factoryFiles(names, config),
|
|
646
638
|
index: featureArtifactIndexFile("factory", names, config),
|
|
647
639
|
dependencies: {
|
|
@@ -663,7 +655,7 @@ export async function makeSeed(options) {
|
|
|
663
655
|
config,
|
|
664
656
|
force: Boolean(options.force),
|
|
665
657
|
dryRun: Boolean(options.dryRun),
|
|
666
|
-
name:
|
|
658
|
+
name: names.stableName,
|
|
667
659
|
files: seedFiles(names, config),
|
|
668
660
|
index: featureArtifactIndexFile("seed", names, config),
|
|
669
661
|
dependencies: {
|
|
@@ -677,7 +669,7 @@ export async function makeSeed(options) {
|
|
|
677
669
|
config,
|
|
678
670
|
force: Boolean(options.force),
|
|
679
671
|
dryRun: Boolean(options.dryRun),
|
|
680
|
-
name:
|
|
672
|
+
name: factoryNamesForSeed.stableName,
|
|
681
673
|
files: factoryFiles(factoryNamesForSeed, config),
|
|
682
674
|
index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
|
|
683
675
|
dependencies: {
|
|
@@ -714,7 +706,7 @@ export async function makeNotification(options) {
|
|
|
714
706
|
// notifications uses ctx.ports.notifications, so the generator wires both
|
|
715
707
|
// ports with dev-default providers. Each port is wired independently and
|
|
716
708
|
// skipped when the app already has it, for example when the resend
|
|
717
|
-
//
|
|
709
|
+
// provider contributed the mailer. Plan the wiring before writing any
|
|
718
710
|
// files so an anchor miss aborts the generator without leaving partial
|
|
719
711
|
// output.
|
|
720
712
|
const notificationWirings = notificationPortWirings("make notification");
|
|
@@ -726,7 +718,7 @@ export async function makeNotification(options) {
|
|
|
726
718
|
config,
|
|
727
719
|
force: Boolean(options.force),
|
|
728
720
|
dryRun: Boolean(options.dryRun),
|
|
729
|
-
name:
|
|
721
|
+
name: names.stableName,
|
|
730
722
|
files: [
|
|
731
723
|
...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
|
|
732
724
|
...notificationFiles(names, config),
|
|
@@ -760,7 +752,7 @@ export async function makeListener(options) {
|
|
|
760
752
|
config,
|
|
761
753
|
force: Boolean(options.force),
|
|
762
754
|
dryRun: Boolean(options.dryRun),
|
|
763
|
-
name:
|
|
755
|
+
name: names.stableName,
|
|
764
756
|
files: [
|
|
765
757
|
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
766
758
|
...listenerFiles(names, config),
|
|
@@ -832,7 +824,7 @@ async function assertListenerEventExists(targetDir, names, config) {
|
|
|
832
824
|
await stat(path.join(targetDir, file));
|
|
833
825
|
}
|
|
834
826
|
catch {
|
|
835
|
-
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.`);
|
|
836
828
|
}
|
|
837
829
|
}
|
|
838
830
|
async function updateListenerProviderWiring(targetDir, names, config, options) {
|
|
@@ -884,7 +876,7 @@ export async function makeSchedule(options) {
|
|
|
884
876
|
config,
|
|
885
877
|
force: Boolean(options.force),
|
|
886
878
|
dryRun: Boolean(options.dryRun),
|
|
887
|
-
name:
|
|
879
|
+
name: names.stableName,
|
|
888
880
|
files,
|
|
889
881
|
index: featureArtifactIndexFile("schedule", names, config),
|
|
890
882
|
dependencies: {
|
|
@@ -1252,6 +1244,9 @@ export async function makeUpload(options) {
|
|
|
1252
1244
|
? resolveConfig(options.config)
|
|
1253
1245
|
: await loadBeignetConfig(targetDir);
|
|
1254
1246
|
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
1247
|
+
if (options.ui) {
|
|
1248
|
+
await assertUploadUiApp(targetDir, config);
|
|
1249
|
+
}
|
|
1255
1250
|
await wirePortProviders(targetDir, config, [storagePortWiring("make upload")], {
|
|
1256
1251
|
dryRun: true,
|
|
1257
1252
|
});
|
|
@@ -1260,10 +1255,10 @@ export async function makeUpload(options) {
|
|
|
1260
1255
|
config,
|
|
1261
1256
|
force: Boolean(options.force),
|
|
1262
1257
|
dryRun: Boolean(options.dryRun),
|
|
1263
|
-
name:
|
|
1258
|
+
name: names.stableName,
|
|
1264
1259
|
files: [
|
|
1265
1260
|
...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
|
|
1266
|
-
...uploadFiles(names, config),
|
|
1261
|
+
...uploadFiles(names, config, { ui: Boolean(options.ui) }),
|
|
1267
1262
|
],
|
|
1268
1263
|
index: featureArtifactIndexFile("upload", names, config),
|
|
1269
1264
|
dependencies: {
|
|
@@ -1283,6 +1278,31 @@ export async function makeUpload(options) {
|
|
|
1283
1278
|
if (!result.files.includes(uploadRoutePath(config))) {
|
|
1284
1279
|
result.files.push(uploadRoutePath(config));
|
|
1285
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);
|
|
1286
1306
|
return result;
|
|
1287
1307
|
}
|
|
1288
1308
|
export async function makeOutbox(options = {}) {
|
|
@@ -1349,7 +1369,10 @@ async function makeFeatureUi(options) {
|
|
|
1349
1369
|
? resolveConfig(options.config)
|
|
1350
1370
|
: await loadBeignetConfig(targetDir);
|
|
1351
1371
|
await assertFeatureUiApp(targetDir, config);
|
|
1352
|
-
const componentFiles =
|
|
1372
|
+
const componentFiles = [
|
|
1373
|
+
...(await missingFeatureUiFormsFiles(targetDir, config)),
|
|
1374
|
+
...featureUiComponentFiles(names, config),
|
|
1375
|
+
];
|
|
1353
1376
|
const indexFile = featureUiIndexFile(names, config);
|
|
1354
1377
|
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
1355
1378
|
force: Boolean(options.force),
|
|
@@ -1378,8 +1401,13 @@ async function makeFeatureUi(options) {
|
|
|
1378
1401
|
if (indexResult === "skipped")
|
|
1379
1402
|
skippedFiles.push(indexFile.path);
|
|
1380
1403
|
if (await updatePackageDependencies(targetDir, {
|
|
1404
|
+
"@beignet/react-hook-form": beignetDependencyVersion,
|
|
1381
1405
|
"@beignet/react-query": beignetDependencyVersion,
|
|
1406
|
+
"@hookform/resolvers": (packageJson) => packageJson.dependencies?.["@hookform/resolvers"] ??
|
|
1407
|
+
externalVersions.hookformResolvers,
|
|
1382
1408
|
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
1409
|
+
"react-hook-form": (packageJson) => packageJson.dependencies?.["react-hook-form"] ??
|
|
1410
|
+
externalVersions.reactHookForm,
|
|
1383
1411
|
}, { dryRun: Boolean(options.dryRun) })) {
|
|
1384
1412
|
updatedFiles.push("package.json");
|
|
1385
1413
|
}
|
|
@@ -1467,7 +1495,7 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
1467
1495
|
(await updateInfrastructurePorts(targetDir, names, config, options))) {
|
|
1468
1496
|
updated.add(config.paths.portWiring);
|
|
1469
1497
|
}
|
|
1470
|
-
if (generationOptions.
|
|
1498
|
+
if (generationOptions.authorization &&
|
|
1471
1499
|
(await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
|
|
1472
1500
|
updated.add(config.paths.portWiring);
|
|
1473
1501
|
}
|
|
@@ -1505,11 +1533,10 @@ function typeHasTopLevelProperty(source, typeName, propertyName) {
|
|
|
1505
1533
|
function useCaseNames(input) {
|
|
1506
1534
|
const parts = input
|
|
1507
1535
|
.trim()
|
|
1508
|
-
.split("
|
|
1509
|
-
.map((part) => part.trim())
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
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.");
|
|
1513
1540
|
}
|
|
1514
1541
|
const feature = identifierNames(parts[0], "Use case feature");
|
|
1515
1542
|
const action = identifierNames(parts[1], "Use case action");
|
|
@@ -1541,7 +1568,7 @@ function policyNames(input) {
|
|
|
1541
1568
|
return resolvedNames;
|
|
1542
1569
|
}
|
|
1543
1570
|
function eventNames(input) {
|
|
1544
|
-
const names = featureArtifactNames(
|
|
1571
|
+
const names = featureArtifactNames(input, "Event");
|
|
1545
1572
|
const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
|
|
1546
1573
|
assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
|
|
1547
1574
|
return {
|
|
@@ -1587,7 +1614,7 @@ function factoryNames(input) {
|
|
|
1587
1614
|
};
|
|
1588
1615
|
}
|
|
1589
1616
|
function defaultFactoryNamesForSeed(names) {
|
|
1590
|
-
return factoryNames(`${names.feature.kebab}
|
|
1617
|
+
return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
|
|
1591
1618
|
}
|
|
1592
1619
|
function notificationNames(input) {
|
|
1593
1620
|
const names = featureArtifactNames(input, "Notification");
|
|
@@ -1631,24 +1658,26 @@ function scheduleNames(input, options) {
|
|
|
1631
1658
|
function uploadNames(input) {
|
|
1632
1659
|
const names = featureArtifactNames(input, "Upload");
|
|
1633
1660
|
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
1634
|
-
|
|
1661
|
+
const componentExportName = `${names.artifact.pascal}Uploader`;
|
|
1662
|
+
assertGeneratedIdentifiers(input, "Upload name", [
|
|
1663
|
+
uploadExportName,
|
|
1664
|
+
componentExportName,
|
|
1665
|
+
]);
|
|
1635
1666
|
return {
|
|
1636
1667
|
...names,
|
|
1637
1668
|
uploadName: names.stableName,
|
|
1638
1669
|
uploadExportName,
|
|
1639
1670
|
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
1640
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`,
|
|
1641
1679
|
};
|
|
1642
1680
|
}
|
|
1643
|
-
function normalizeEventReference(input) {
|
|
1644
|
-
const trimmed = input.trim();
|
|
1645
|
-
if (trimmed.includes("/"))
|
|
1646
|
-
return trimmed;
|
|
1647
|
-
const dotIndex = trimmed.indexOf(".");
|
|
1648
|
-
if (dotIndex === -1)
|
|
1649
|
-
return trimmed;
|
|
1650
|
-
return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
|
|
1651
|
-
}
|
|
1652
1681
|
function inferUseCaseKind(action) {
|
|
1653
1682
|
if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
|
|
1654
1683
|
return "query";
|
|
@@ -1656,8 +1685,8 @@ function inferUseCaseKind(action) {
|
|
|
1656
1685
|
return "command";
|
|
1657
1686
|
}
|
|
1658
1687
|
function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
|
|
1659
|
-
|
|
1660
|
-
|
|
1688
|
+
authorization: false,
|
|
1689
|
+
tenantScoped: false,
|
|
1661
1690
|
events: false,
|
|
1662
1691
|
softDelete: false,
|
|
1663
1692
|
}, database = "sqlite") {
|
|
@@ -1710,7 +1739,7 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
|
|
|
1710
1739
|
},
|
|
1711
1740
|
]
|
|
1712
1741
|
: []),
|
|
1713
|
-
...(mode === "resource" && options.
|
|
1742
|
+
...(mode === "resource" && options.authorization
|
|
1714
1743
|
? [
|
|
1715
1744
|
{
|
|
1716
1745
|
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
@@ -1765,6 +1794,59 @@ function featureUiComponentFiles(names, config) {
|
|
|
1765
1794
|
},
|
|
1766
1795
|
];
|
|
1767
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
|
+
}
|
|
1768
1850
|
function featureUiIndexFile(names, config) {
|
|
1769
1851
|
return {
|
|
1770
1852
|
path: featureUiIndexFilePath(names, config),
|
|
@@ -1893,12 +1975,54 @@ function scheduleFiles(names, config, options) {
|
|
|
1893
1975
|
}
|
|
1894
1976
|
return files;
|
|
1895
1977
|
}
|
|
1896
|
-
function uploadFiles(names, config) {
|
|
1978
|
+
function uploadFiles(names, config, options) {
|
|
1897
1979
|
return [
|
|
1980
|
+
{
|
|
1981
|
+
path: uploadManifestFilePath(names, config),
|
|
1982
|
+
content: uploadManifestFile(names),
|
|
1983
|
+
},
|
|
1898
1984
|
{
|
|
1899
1985
|
path: uploadFilePath(names, config),
|
|
1900
1986
|
content: uploadFile(names, config),
|
|
1901
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
|
+
},
|
|
1902
2026
|
];
|
|
1903
2027
|
}
|
|
1904
2028
|
function outboxFiles(config) {
|
|
@@ -2204,7 +2328,7 @@ function schemasFile(names, mode, options) {
|
|
|
2204
2328
|
|
|
2205
2329
|
export const ${names.singularPascal}Schema = z.object({
|
|
2206
2330
|
id: z.string().uuid(),
|
|
2207
|
-
${options.
|
|
2331
|
+
${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
2208
2332
|
version: z.number().int().min(1),
|
|
2209
2333
|
createdAt: z.string().datetime(),
|
|
2210
2334
|
updatedAt: z.string().datetime(),
|
|
@@ -2304,7 +2428,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
2304
2428
|
});
|
|
2305
2429
|
|
|
2306
2430
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
2307
|
-
name: z.string().min(1).max(120),
|
|
2431
|
+
name: z.string().trim().min(1).max(120),
|
|
2308
2432
|
});
|
|
2309
2433
|
${mode === "resource"
|
|
2310
2434
|
? `
|
|
@@ -2313,7 +2437,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
|
|
|
2313
2437
|
});
|
|
2314
2438
|
|
|
2315
2439
|
export const Update${names.singularPascal}BodySchema = z.object({
|
|
2316
|
-
name: z.string().min(1).max(120),
|
|
2440
|
+
name: z.string().trim().min(1).max(120),
|
|
2317
2441
|
version: z.number().int().min(1),
|
|
2318
2442
|
});
|
|
2319
2443
|
|
|
@@ -2347,7 +2471,7 @@ function listUseCaseFile(names, config, options) {
|
|
|
2347
2471
|
const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
|
|
2348
2472
|
return `import "@beignet/core/server-only";
|
|
2349
2473
|
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
2350
|
-
${options.
|
|
2474
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2351
2475
|
import {
|
|
2352
2476
|
decode${names.singularPascal}Cursor,
|
|
2353
2477
|
List${names.pluralPascal}InputSchema,
|
|
@@ -2359,7 +2483,7 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
2359
2483
|
.input(List${names.pluralPascal}InputSchema)
|
|
2360
2484
|
.output(List${names.pluralPascal}OutputSchema)
|
|
2361
2485
|
.run(async ({ ctx, input }) => {
|
|
2362
|
-
${options.
|
|
2486
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
2363
2487
|
defaultLimit: 20,
|
|
2364
2488
|
maxLimit: 100,
|
|
2365
2489
|
});
|
|
@@ -2370,14 +2494,14 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
2370
2494
|
name: input.name,
|
|
2371
2495
|
sortBy: input.sortBy,
|
|
2372
2496
|
sortDirection: input.sortDirection,
|
|
2373
|
-
}${options.
|
|
2497
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
2374
2498
|
});
|
|
2375
2499
|
`;
|
|
2376
2500
|
}
|
|
2377
2501
|
function createUseCaseFile(names, config, options) {
|
|
2378
2502
|
const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
|
|
2379
2503
|
return `import "@beignet/core/server-only";
|
|
2380
|
-
${options.
|
|
2504
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2381
2505
|
import {
|
|
2382
2506
|
Create${names.singularPascal}InputSchema,
|
|
2383
2507
|
${names.singularPascal}Schema,
|
|
@@ -2389,7 +2513,7 @@ export const create${names.singularPascal}UseCase = useCase
|
|
|
2389
2513
|
.input(Create${names.singularPascal}InputSchema)
|
|
2390
2514
|
.output(${names.singularPascal}Schema)
|
|
2391
2515
|
.run(async ({ ctx, input }) => {
|
|
2392
|
-
${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" : ""});
|
|
2393
2517
|
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
2394
2518
|
return ${names.singularCamel};
|
|
2395
2519
|
});
|
|
@@ -2398,7 +2522,7 @@ ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPasca
|
|
|
2398
2522
|
function getUseCaseFile(names, config, options) {
|
|
2399
2523
|
const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
|
|
2400
2524
|
return `import "@beignet/core/server-only";
|
|
2401
|
-
${options.
|
|
2525
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2402
2526
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2403
2527
|
import {
|
|
2404
2528
|
${names.singularPascal}IdInputSchema,
|
|
@@ -2410,7 +2534,7 @@ export const get${names.singularPascal}UseCase = useCase
|
|
|
2410
2534
|
.input(${names.singularPascal}IdInputSchema)
|
|
2411
2535
|
.output(${names.singularPascal}Schema)
|
|
2412
2536
|
.run(async ({ ctx, input }) => {
|
|
2413
|
-
${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" : ""});
|
|
2414
2538
|
if (!${names.singularCamel}) {
|
|
2415
2539
|
throw appError("${names.singularPascal}NotFound", {
|
|
2416
2540
|
details: { id: input.id },
|
|
@@ -2424,7 +2548,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
2424
2548
|
function updateUseCaseFile(names, config, options) {
|
|
2425
2549
|
const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
|
|
2426
2550
|
return `import "@beignet/core/server-only";
|
|
2427
|
-
${options.
|
|
2551
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2428
2552
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2429
2553
|
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
2430
2554
|
Update${names.singularPascal}InputSchema,
|
|
@@ -2436,13 +2560,13 @@ export const update${names.singularPascal}UseCase = useCase
|
|
|
2436
2560
|
.input(Update${names.singularPascal}InputSchema)
|
|
2437
2561
|
.output(${names.singularPascal}Schema)
|
|
2438
2562
|
.run(async ({ ctx, input }) => {
|
|
2439
|
-
${options.
|
|
2563
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
2440
2564
|
if (!existing) {
|
|
2441
2565
|
throw appError("${names.singularPascal}NotFound", {
|
|
2442
2566
|
details: { id: input.id },
|
|
2443
2567
|
});
|
|
2444
2568
|
}
|
|
2445
|
-
${options.
|
|
2569
|
+
${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
2446
2570
|
throw appError("${names.singularPascal}Conflict", {
|
|
2447
2571
|
details: { id: input.id, expectedVersion: existing.version },
|
|
2448
2572
|
});
|
|
@@ -2450,7 +2574,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
|
|
|
2450
2574
|
|
|
2451
2575
|
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
2452
2576
|
...input,
|
|
2453
|
-
}${options.
|
|
2577
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
2454
2578
|
if (!${names.singularCamel}) {
|
|
2455
2579
|
throw appError("${names.singularPascal}Conflict", {
|
|
2456
2580
|
details: { id: input.id, expectedVersion: existing.version },
|
|
@@ -2465,7 +2589,7 @@ function deleteUseCaseFile(names, config, options) {
|
|
|
2465
2589
|
const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
|
|
2466
2590
|
return `import "@beignet/core/server-only";
|
|
2467
2591
|
import { z } from "zod";
|
|
2468
|
-
${options.
|
|
2592
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2469
2593
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2470
2594
|
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
2471
2595
|
|
|
@@ -2474,7 +2598,7 @@ export const delete${names.singularPascal}UseCase = useCase
|
|
|
2474
2598
|
.input(${names.singularPascal}IdInputSchema)
|
|
2475
2599
|
.output(z.void())
|
|
2476
2600
|
.run(async ({ ctx, input }) => {
|
|
2477
|
-
${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" : ""});
|
|
2478
2602
|
if (!deleted) {
|
|
2479
2603
|
throw appError("${names.singularPascal}NotFound", {
|
|
2480
2604
|
details: { id: input.id },
|
|
@@ -2504,7 +2628,7 @@ function repositoryPortFile(names, config, mode, options) {
|
|
|
2504
2628
|
PageResult,
|
|
2505
2629
|
SortDirection,
|
|
2506
2630
|
} from "@beignet/core/pagination";
|
|
2507
|
-
${options.
|
|
2631
|
+
${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
|
|
2508
2632
|
Create${names.singularPascal}Input,
|
|
2509
2633
|
${names.singularPascal}Cursor,
|
|
2510
2634
|
${names.singularPascal}SortBy,
|
|
@@ -2521,9 +2645,9 @@ export type List${names.pluralPascal}Query = {
|
|
|
2521
2645
|
};
|
|
2522
2646
|
|
|
2523
2647
|
export interface ${names.singularPascal}Repository {
|
|
2524
|
-
list(query: List${names.pluralPascal}Query${options.
|
|
2525
|
-
create(input: Create${names.singularPascal}Input${options.
|
|
2526
|
-
${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` : ""}}
|
|
2527
2651
|
`;
|
|
2528
2652
|
}
|
|
2529
2653
|
function inMemoryRepositoryFile(names, config, mode, options) {
|
|
@@ -2551,13 +2675,13 @@ function inMemoryRepositoryFile(names, config, mode, options) {
|
|
|
2551
2675
|
? " || existing.deletedAt !== null"
|
|
2552
2676
|
: "";
|
|
2553
2677
|
const deleteImplementation = options.softDelete
|
|
2554
|
-
? `${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();
|
|
2555
2679
|
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
2556
2680
|
return true;`
|
|
2557
|
-
: `${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);`;
|
|
2558
2682
|
return `import "@beignet/core/server-only";
|
|
2559
2683
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2560
|
-
${options.
|
|
2684
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
2561
2685
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2562
2686
|
import type {
|
|
2563
2687
|
Create${names.singularPascal}Input,
|
|
@@ -2567,7 +2691,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
|
|
|
2567
2691
|
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
2568
2692
|
return {
|
|
2569
2693
|
id: ${names.singularCamel}.id,
|
|
2570
|
-
${options.
|
|
2694
|
+
${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
2571
2695
|
version: ${names.singularCamel}.version,
|
|
2572
2696
|
createdAt: ${names.singularCamel}.createdAt,
|
|
2573
2697
|
updatedAt: ${names.singularCamel}.updatedAt,
|
|
@@ -2625,10 +2749,10 @@ export function createInMemory${names.singularPascal}Repository(
|
|
|
2625
2749
|
);
|
|
2626
2750
|
|
|
2627
2751
|
return {
|
|
2628
|
-
async list(query${options.
|
|
2752
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
2629
2753
|
const name = query.name?.toLocaleLowerCase();
|
|
2630
|
-
${options.
|
|
2631
|
-
${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))
|
|
2632
2756
|
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
2633
2757
|
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
2634
2758
|
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
@@ -2643,11 +2767,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
|
|
|
2643
2767
|
nextCursor,
|
|
2644
2768
|
);
|
|
2645
2769
|
},
|
|
2646
|
-
async create(input${options.
|
|
2770
|
+
async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2647
2771
|
const now = new Date().toISOString();
|
|
2648
|
-
${options.
|
|
2772
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2649
2773
|
id: crypto.randomUUID(),
|
|
2650
|
-
${options.
|
|
2774
|
+
${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
2651
2775
|
version: 1,
|
|
2652
2776
|
createdAt: now,
|
|
2653
2777
|
updatedAt: now,
|
|
@@ -2657,7 +2781,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
|
2657
2781
|
${newDeletedAtField} });
|
|
2658
2782
|
return ${names.singularCamel};
|
|
2659
2783
|
},
|
|
2660
|
-
${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` : ""}
|
|
2661
2785
|
};
|
|
2662
2786
|
}
|
|
2663
2787
|
`;
|
|
@@ -2667,7 +2791,7 @@ function drizzleSchemaFile(names, options, dialect) {
|
|
|
2667
2791
|
|
|
2668
2792
|
export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
2669
2793
|
id: ${dialect.idColumn("id")}.primaryKey(),
|
|
2670
|
-
${options.
|
|
2794
|
+
${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
|
|
2671
2795
|
version: ${dialect.integerColumn("version")}.notNull(),
|
|
2672
2796
|
createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
|
|
2673
2797
|
updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
|
|
@@ -2764,20 +2888,20 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
|
|
|
2764
2888
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
2765
2889
|
const findWhere = drizzleResourceWhere(names, options, [
|
|
2766
2890
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
2767
|
-
...(options.
|
|
2891
|
+
...(options.tenantScoped
|
|
2768
2892
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2769
2893
|
: []),
|
|
2770
2894
|
]);
|
|
2771
2895
|
const updateWhere = drizzleResourceWhere(names, options, [
|
|
2772
2896
|
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
2773
2897
|
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
2774
|
-
...(options.
|
|
2898
|
+
...(options.tenantScoped
|
|
2775
2899
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2776
2900
|
: []),
|
|
2777
2901
|
]);
|
|
2778
2902
|
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
2779
2903
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
2780
|
-
...(options.
|
|
2904
|
+
...(options.tenantScoped
|
|
2781
2905
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2782
2906
|
: []),
|
|
2783
2907
|
]);
|
|
@@ -2795,7 +2919,7 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
|
|
|
2795
2919
|
].filter((name) => Boolean(name));
|
|
2796
2920
|
// After a version-guarded update succeeds, re-select by identity only —
|
|
2797
2921
|
// matching what `.returning()` yields on the dialects that support it.
|
|
2798
|
-
const updateReselectWhere = options.
|
|
2922
|
+
const updateReselectWhere = options.tenantScoped
|
|
2799
2923
|
? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
|
|
2800
2924
|
: `eq(schema.${names.pluralCamel}.id, input.id)`;
|
|
2801
2925
|
// drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
|
|
@@ -2817,13 +2941,13 @@ function affectedRows(result: unknown): number {
|
|
|
2817
2941
|
`
|
|
2818
2942
|
: "";
|
|
2819
2943
|
const createMethod = dialect.supportsReturning
|
|
2820
|
-
? ` async create(input${options.
|
|
2944
|
+
? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2821
2945
|
const now = new Date().toISOString();
|
|
2822
2946
|
const [row] = await db
|
|
2823
2947
|
.insert(schema.${names.pluralCamel})
|
|
2824
2948
|
.values({
|
|
2825
2949
|
id: crypto.randomUUID(),
|
|
2826
|
-
${options.
|
|
2950
|
+
${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
2827
2951
|
version: 1,
|
|
2828
2952
|
createdAt: now,
|
|
2829
2953
|
updatedAt: now,
|
|
@@ -2837,11 +2961,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
|
|
|
2837
2961
|
return to${names.singularPascal}(row);
|
|
2838
2962
|
},
|
|
2839
2963
|
`
|
|
2840
|
-
: ` async create(input${options.
|
|
2964
|
+
: ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2841
2965
|
const now = new Date().toISOString();
|
|
2842
2966
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2843
2967
|
id: crypto.randomUUID(),
|
|
2844
|
-
${options.
|
|
2968
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
2845
2969
|
version: 1,
|
|
2846
2970
|
createdAt: now,
|
|
2847
2971
|
updatedAt: now,
|
|
@@ -2854,7 +2978,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2854
2978
|
const resourceMethods = mode !== "resource"
|
|
2855
2979
|
? ""
|
|
2856
2980
|
: dialect.supportsReturning
|
|
2857
|
-
? ` async findById(id: string${options.
|
|
2981
|
+
? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2858
2982
|
const [row] = await db
|
|
2859
2983
|
.select()
|
|
2860
2984
|
.from(schema.${names.pluralCamel})
|
|
@@ -2863,7 +2987,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2863
2987
|
|
|
2864
2988
|
return row ? to${names.singularPascal}(row) : null;
|
|
2865
2989
|
},
|
|
2866
|
-
async update(input${options.
|
|
2990
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2867
2991
|
const [row] = await db
|
|
2868
2992
|
.update(schema.${names.pluralCamel})
|
|
2869
2993
|
.set({
|
|
@@ -2876,12 +3000,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2876
3000
|
|
|
2877
3001
|
return row ? to${names.singularPascal}(row) : null;
|
|
2878
3002
|
},
|
|
2879
|
-
async delete(id: string${options.
|
|
3003
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2880
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`}
|
|
2881
3005
|
return rows.length > 0;
|
|
2882
3006
|
},
|
|
2883
3007
|
`
|
|
2884
|
-
: ` async findById(id: string${options.
|
|
3008
|
+
: ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2885
3009
|
const [row] = await db
|
|
2886
3010
|
.select()
|
|
2887
3011
|
.from(schema.${names.pluralCamel})
|
|
@@ -2890,7 +3014,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
2890
3014
|
|
|
2891
3015
|
return row ? to${names.singularPascal}(row) : null;
|
|
2892
3016
|
},
|
|
2893
|
-
async update(input${options.
|
|
3017
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2894
3018
|
const result = await db
|
|
2895
3019
|
.update(schema.${names.pluralCamel})
|
|
2896
3020
|
.set({
|
|
@@ -2910,14 +3034,14 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
2910
3034
|
|
|
2911
3035
|
return row ? to${names.singularPascal}(row) : null;
|
|
2912
3036
|
},
|
|
2913
|
-
async delete(id: string${options.
|
|
3037
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2914
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`}
|
|
2915
3039
|
return affectedRows(result) > 0;
|
|
2916
3040
|
},
|
|
2917
3041
|
`;
|
|
2918
3042
|
return `import "@beignet/core/server-only";
|
|
2919
3043
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2920
|
-
${options.
|
|
3044
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
|
|
2921
3045
|
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
2922
3046
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
2923
3047
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
@@ -2932,7 +3056,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
2932
3056
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
2933
3057
|
return {
|
|
2934
3058
|
id: row.id,
|
|
2935
|
-
${options.
|
|
3059
|
+
${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
2936
3060
|
version: row.version,
|
|
2937
3061
|
createdAt: row.createdAt,
|
|
2938
3062
|
updatedAt: row.updatedAt,
|
|
@@ -2964,9 +3088,9 @@ function ${names.singularCamel}CursorFilter(
|
|
|
2964
3088
|
|
|
2965
3089
|
function ${names.singularCamel}ListWhere(
|
|
2966
3090
|
query: List${names.pluralPascal}Query,
|
|
2967
|
-
${options.
|
|
3091
|
+
${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
|
|
2968
3092
|
const filters: SQL<unknown>[] = [
|
|
2969
|
-
${options.
|
|
3093
|
+
${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
2970
3094
|
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
2971
3095
|
|
|
2972
3096
|
if (query.name) {
|
|
@@ -3000,8 +3124,8 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
3000
3124
|
db: ${dialect.dbTypeName}<typeof schema>,
|
|
3001
3125
|
): ${names.singularPascal}Repository {
|
|
3002
3126
|
return {
|
|
3003
|
-
async list(query${options.
|
|
3004
|
-
const where = ${names.singularCamel}ListWhere(query${options.
|
|
3127
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
3128
|
+
const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
|
|
3005
3129
|
const rows = await db
|
|
3006
3130
|
.select()
|
|
3007
3131
|
.from(schema.${names.pluralCamel})
|
|
@@ -3057,7 +3181,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
3057
3181
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
3058
3182
|
.post("/api/${names.pluralKebab}")
|
|
3059
3183
|
.body(Create${names.singularPascal}InputSchema)
|
|
3060
|
-
${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({
|
|
3061
3185
|
201: ${names.singularPascal}Schema,
|
|
3062
3186
|
});
|
|
3063
3187
|
${mode === "resource"
|
|
@@ -3074,7 +3198,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
|
|
|
3074
3198
|
.patch("/api/${names.pluralKebab}/:id")
|
|
3075
3199
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3076
3200
|
.body(Update${names.singularPascal}BodySchema)
|
|
3077
|
-
${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 })
|
|
3078
3202
|
.responses({
|
|
3079
3203
|
200: ${names.singularPascal}Schema,
|
|
3080
3204
|
});
|
|
@@ -3082,7 +3206,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
|
|
|
3082
3206
|
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
3083
3207
|
.delete("/api/${names.pluralKebab}/:id")
|
|
3084
3208
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3085
|
-
${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 })
|
|
3086
3210
|
.responses({
|
|
3087
3211
|
204: null,
|
|
3088
3212
|
});
|
|
@@ -3266,7 +3390,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
|
|
|
3266
3390
|
|
|
3267
3391
|
function canWrite(ctx: AuthorizationContext) {
|
|
3268
3392
|
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
3269
|
-
${options.
|
|
3393
|
+
${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
3270
3394
|
}
|
|
3271
3395
|
|
|
3272
3396
|
export const ${names.singularCamel}Policy = definePolicy({
|
|
@@ -3276,13 +3400,13 @@ export const ${names.singularCamel}Policy = definePolicy({
|
|
|
3276
3400
|
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3277
3401
|
const decision = canWrite(ctx);
|
|
3278
3402
|
if (decision !== true) return decision;
|
|
3279
|
-
${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` : ""}
|
|
3280
3404
|
return true;
|
|
3281
3405
|
},
|
|
3282
3406
|
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3283
3407
|
const decision = canWrite(ctx);
|
|
3284
3408
|
if (decision !== true) return decision;
|
|
3285
|
-
${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` : ""}
|
|
3286
3410
|
return true;
|
|
3287
3411
|
},
|
|
3288
3412
|
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
@@ -3329,7 +3453,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
|
|
|
3329
3453
|
const now = new Date().toISOString();
|
|
3330
3454
|
return {
|
|
3331
3455
|
id: "00000000-0000-4000-8000-000000000001",
|
|
3332
|
-
${options.
|
|
3456
|
+
${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
3333
3457
|
version: 1,
|
|
3334
3458
|
createdAt: now,
|
|
3335
3459
|
updatedAt: now,
|
|
@@ -3344,7 +3468,7 @@ describe("${names.singularCamel}Policy", () => {
|
|
|
3344
3468
|
const ${names.singularCamel} = create${names.singularPascal}();
|
|
3345
3469
|
const ctx = {
|
|
3346
3470
|
actor: { type: "user" as const, id: "user_test" },
|
|
3347
|
-
${options.
|
|
3471
|
+
${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
3348
3472
|
|
|
3349
3473
|
await expect(
|
|
3350
3474
|
tester.assertMatrix([
|
|
@@ -3546,8 +3670,32 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
3546
3670
|
);
|
|
3547
3671
|
`;
|
|
3548
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
|
+
}
|
|
3549
3696
|
function uploadFile(names, config) {
|
|
3550
3697
|
return `import { z } from "zod";
|
|
3698
|
+
import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
|
|
3551
3699
|
import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
|
|
3552
3700
|
|
|
3553
3701
|
export const ${names.metadataSchemaName} = z.object({
|
|
@@ -3558,13 +3706,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
|
|
|
3558
3706
|
|
|
3559
3707
|
export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
3560
3708
|
\tmetadata: ${names.metadataSchemaName},
|
|
3561
|
-
\tfile: {
|
|
3562
|
-
\t\tcontentTypes: ["application/pdf", "text/plain"],
|
|
3563
|
-
\t\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
3564
|
-
\t\tmaxFiles: 1,
|
|
3565
|
-
\t\tvisibility: "private",
|
|
3566
|
-
\t\tcacheControl: "private, max-age=0",
|
|
3567
|
-
\t},
|
|
3709
|
+
\tfile: ${names.fileConstraintsExportName},
|
|
3568
3710
|
\tauthorize({ ctx }) {
|
|
3569
3711
|
\t\treturn ctx.actor.type === "user";
|
|
3570
3712
|
\t},
|
|
@@ -3594,6 +3736,127 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
|
3594
3736
|
});
|
|
3595
3737
|
`;
|
|
3596
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
|
+
}
|
|
3597
3860
|
function uploadRouteFile(names, config) {
|
|
3598
3861
|
const registryName = `${names.featureSingularCamel}Uploads`;
|
|
3599
3862
|
return `import {
|
|
@@ -3682,63 +3945,70 @@ function featureUiComponentFile(names, config) {
|
|
|
3682
3945
|
const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
|
|
3683
3946
|
return `"use client";
|
|
3684
3947
|
|
|
3685
|
-
import {
|
|
3948
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
3686
3949
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3687
|
-
import {
|
|
3950
|
+
import { rhf } from "${aliasModule(clientFormsPath(config))}";
|
|
3951
|
+
import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
|
|
3688
3952
|
import {
|
|
3689
3953
|
\tcreate${names.singularPascal}MutationOptions,
|
|
3690
3954
|
\tinvalidate${names.pluralPascal},
|
|
3691
3955
|
\tlist${names.pluralPascal}QueryOptions,
|
|
3692
3956
|
} from "${relativeModule(componentPath, clientQueriesPath)}";
|
|
3693
3957
|
|
|
3958
|
+
const create${names.singularPascal}Form = rhf(create${names.singularPascal});
|
|
3959
|
+
|
|
3694
3960
|
export function ${names.componentExportName}() {
|
|
3695
|
-
\tconst [name, setName] = useState("");
|
|
3696
3961
|
\tconst queryClient = useQueryClient();
|
|
3697
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");
|
|
3698
3967
|
\tconst create${names.singularPascal}Mutation = useMutation({
|
|
3699
3968
|
\t\t...create${names.singularPascal}MutationOptions(),
|
|
3700
3969
|
\t\tonSuccess: async () => {
|
|
3701
|
-
\t\t\
|
|
3970
|
+
\t\t\tform.reset({ name: "" });
|
|
3702
3971
|
\t\t\tawait invalidate${names.pluralPascal}(queryClient);
|
|
3703
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},
|
|
3704
3979
|
\t});
|
|
3705
3980
|
|
|
3706
3981
|
\treturn (
|
|
3707
3982
|
\t\t<section className="${names.pluralKebab}-panel">
|
|
3708
3983
|
\t\t\t<form
|
|
3709
3984
|
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
3710
|
-
\t\t\t\tonSubmit={(
|
|
3711
|
-
\t\t\t\t\
|
|
3712
|
-
\t\t\t\t\tconst trimmedName = name.trim();
|
|
3713
|
-
\t\t\t\t\tif (!trimmedName) return;
|
|
3985
|
+
\t\t\t\tonSubmit={form.handleSubmit((values) => {
|
|
3986
|
+
\t\t\t\t\tform.clearErrors("root");
|
|
3714
3987
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
3715
3988
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
3716
|
-
\t\t\t\t\t\tbody:
|
|
3989
|
+
\t\t\t\t\t\tbody: values,
|
|
3717
3990
|
\t\t\t\t\t});
|
|
3718
|
-
\t\t\t\t}}
|
|
3991
|
+
\t\t\t\t})}
|
|
3719
3992
|
\t\t\t>
|
|
3720
3993
|
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
3721
3994
|
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
3722
3995
|
\t\t\t\t\t<input
|
|
3723
3996
|
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
3724
|
-
\t\t\t\t\t\
|
|
3725
|
-
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
3997
|
+
\t\t\t\t\t\t{...form.register("name")}
|
|
3726
3998
|
\t\t\t\t\t\tplaceholder="Name"
|
|
3727
3999
|
\t\t\t\t\t/>
|
|
3728
4000
|
\t\t\t\t\t<button
|
|
3729
4001
|
\t\t\t\t\t\ttype="submit"
|
|
3730
|
-
\t\t\t\t\t\tdisabled={!name
|
|
4002
|
+
\t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
|
|
3731
4003
|
\t\t\t\t\t>
|
|
3732
4004
|
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
3733
4005
|
\t\t\t\t\t</button>
|
|
3734
4006
|
\t\t\t\t</div>
|
|
3735
|
-
\t\t\t\t{
|
|
3736
|
-
\t\t\t\t\t<p role="alert">
|
|
3737
|
-
\t\t\t\t
|
|
3738
|
-
\t\t\t\t
|
|
3739
|
-
\t\t\t\t\t
|
|
3740
|
-
\t\t\t\t\t\t)}
|
|
3741
|
-
\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>
|
|
3742
4012
|
\t\t\t\t) : null}
|
|
3743
4013
|
\t\t\t</form>
|
|
3744
4014
|
|
|
@@ -3852,7 +4122,7 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
|
|
|
3852
4122
|
import { createTestApp } from "@beignet/web/testing";
|
|
3853
4123
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3854
4124
|
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
3855
|
-
import { ${options.
|
|
4125
|
+
import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
|
|
3856
4126
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3857
4127
|
import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
|
|
3858
4128
|
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
@@ -3872,39 +4142,39 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
3872
4142
|
const seed${names.pluralPascal} = [
|
|
3873
4143
|
{
|
|
3874
4144
|
id: "00000000-0000-4000-8000-000000000101",
|
|
3875
|
-
${options.
|
|
4145
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
3876
4146
|
version: 1,
|
|
3877
4147
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
3878
4148
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
3879
4149
|
},
|
|
3880
4150
|
{
|
|
3881
4151
|
id: "00000000-0000-4000-8000-000000000102",
|
|
3882
|
-
${options.
|
|
4152
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
3883
4153
|
version: 1,
|
|
3884
4154
|
createdAt: "2024-01-02T00:00:00.000Z",
|
|
3885
4155
|
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
3886
4156
|
},
|
|
3887
4157
|
{
|
|
3888
4158
|
id: "00000000-0000-4000-8000-000000000103",
|
|
3889
|
-
${options.
|
|
4159
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
3890
4160
|
version: 1,
|
|
3891
4161
|
createdAt: "2024-01-03T00:00:00.000Z",
|
|
3892
4162
|
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
3893
4163
|
},
|
|
3894
|
-
${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` : ""} ];
|
|
3895
4165
|
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
3896
4166
|
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
3897
4167
|
base: initialPorts,
|
|
3898
4168
|
overrides: {
|
|
3899
4169
|
${names.pluralCamel},
|
|
3900
|
-
${options.
|
|
4170
|
+
${options.authorization || options.tenantScoped
|
|
3901
4171
|
? `\t\t\t\tauth: {
|
|
3902
4172
|
getSession: async () => ({
|
|
3903
|
-
user: { id: "user_test", name: "Test User"${options.
|
|
3904
|
-
${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` : ""}
|
|
3905
4175
|
}),
|
|
3906
4176
|
},
|
|
3907
|
-
${options.
|
|
4177
|
+
${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
3908
4178
|
`
|
|
3909
4179
|
: `\t\t\t\tauth: {
|
|
3910
4180
|
getSession: async () => null,
|
|
@@ -3919,8 +4189,8 @@ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
|
3919
4189
|
});
|
|
3920
4190
|
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
3921
4191
|
ports: testFixture.ports,
|
|
3922
|
-
actor: ${options.
|
|
3923
|
-
tenant: ${options.
|
|
4192
|
+
actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
4193
|
+
tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
|
|
3924
4194
|
});
|
|
3925
4195
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
3926
4196
|
|