@supacloud/compiler 0.47.3 → 0.49.0
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/DELIVERY.md +1 -1
- package/dist/application-development.d.ts +1 -0
- package/dist/cli.js +775 -65
- package/dist/contract-manifest.d.ts +56 -1
- package/dist/delivery-bundle-worker.js +3 -1
- package/dist/feature.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +779 -69
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-assess.d.ts +2 -0
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +32 -0
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -249,6 +249,10 @@ function findClosestMatch(target, candidates) {
|
|
|
249
249
|
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context", JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
250
250
|
|
|
251
251
|
// src/feature.ts
|
|
252
|
+
function featureTransitionForRoute(module, controller, route) {
|
|
253
|
+
const routePath = `${route.method} ${joinRoutePaths(controller.path, route.path)}`;
|
|
254
|
+
return module.featureSpec?.transitions.find((transition) => transition.route === routePath);
|
|
255
|
+
}
|
|
252
256
|
function validateFeatureSpec(spec, module) {
|
|
253
257
|
const diagnostics = [];
|
|
254
258
|
const error = (code, message) => {
|
|
@@ -263,6 +267,9 @@ function validateFeatureSpec(spec, module) {
|
|
|
263
267
|
if (!spec.name.trim() || spec.states.length === 0 || spec.states.some((state) => !state.trim()) || new Set(spec.states).size !== spec.states.length) {
|
|
264
268
|
error("invalid-feature-states", "Feature name and states must be non-empty; states must be unique.");
|
|
265
269
|
}
|
|
270
|
+
if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
|
|
271
|
+
error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
|
|
272
|
+
}
|
|
266
273
|
const names = new Set;
|
|
267
274
|
for (const transition of spec.transitions) {
|
|
268
275
|
if (!transition.name.trim() || names.has(transition.name)) {
|
|
@@ -275,8 +282,46 @@ function validateFeatureSpec(spec, module) {
|
|
|
275
282
|
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
276
283
|
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
277
284
|
}
|
|
285
|
+
if (transition.invalidateTags !== undefined) {
|
|
286
|
+
if (transition.invalidateTags.length === 0 || transition.invalidateTags.some((tag) => !tag.trim()) || new Set(transition.invalidateTags).size !== transition.invalidateTags.length) {
|
|
287
|
+
error("feature-invalidation-tags-invalid", `Transition ${transition.name} must declare unique, non-empty invalidateTags.`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (transition.persistence) {
|
|
291
|
+
const persistence = transition.persistence;
|
|
292
|
+
if (persistence.boundary === "database" && (persistence.optimisticLock !== "required" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.versionField ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.tenantField ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.stateField ?? "") || new Set([persistence.tenantField, persistence.versionField, persistence.stateField]).size !== 3 || persistence.receipt !== "required" || persistence.reconciliation !== "none")) {
|
|
293
|
+
error("feature-persistence-invalid", `Database transition ${transition.name} requires distinct safe tenant/version/state fields, optimistic locking, and a receipt, without reconciliation.`);
|
|
294
|
+
}
|
|
295
|
+
if (persistence.boundary === "database" && spec.tenantField !== persistence.tenantField) {
|
|
296
|
+
error("feature-tenant-boundary-drift", `Database transition ${transition.name} tenant field must match Feature ${spec.name} tenantField.`);
|
|
297
|
+
}
|
|
298
|
+
if (persistence.boundary === "external" && (persistence.optimisticLock !== "none" || persistence.versionField !== undefined || persistence.tenantField !== undefined || persistence.stateField !== undefined || persistence.receipt !== "required" || persistence.reconciliation !== "required")) {
|
|
299
|
+
error("feature-persistence-invalid", `External transition ${transition.name} requires a receipt and reconciliation, without database optimistic locking.`);
|
|
300
|
+
}
|
|
301
|
+
if (persistence.boundary === "external" && !transition.job) {
|
|
302
|
+
error("feature-job-unresolved", `External transition ${transition.name} must bind a reconciliation Job.`);
|
|
303
|
+
}
|
|
304
|
+
if (!transition.command) {
|
|
305
|
+
error("feature-command-unresolved", `Persistent transition ${transition.name} must reference a command.`);
|
|
306
|
+
}
|
|
307
|
+
if (!transition.permission?.trim()) {
|
|
308
|
+
error("feature-governance-drift", `Persistent transition ${transition.name} must declare a permission.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
278
311
|
if (!module)
|
|
279
312
|
continue;
|
|
313
|
+
if (transition.persistence?.boundary === "database") {
|
|
314
|
+
for (const query of module.queries) {
|
|
315
|
+
if (query.tenantScope !== "required") {
|
|
316
|
+
error("feature-query-tenant-boundary", `Query ${query.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const job of module.jobs ?? []) {
|
|
320
|
+
if (job.tenantScope !== "required") {
|
|
321
|
+
error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
280
325
|
const commands = module.commands.filter((command) => command.className === transition.command || command.name === transition.command);
|
|
281
326
|
const command = commands[0];
|
|
282
327
|
if (transition.command && commands.length !== 1) {
|
|
@@ -291,23 +336,59 @@ function validateFeatureSpec(spec, module) {
|
|
|
291
336
|
error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
|
|
292
337
|
}
|
|
293
338
|
}
|
|
339
|
+
if (transition.persistence) {
|
|
340
|
+
if (transition.persistence.boundary === "database" && command.transaction !== "required") {
|
|
341
|
+
error("feature-persistence-drift", `Database transition ${transition.name} requires command ${command.className} to declare transaction: "required".`);
|
|
342
|
+
}
|
|
343
|
+
if (command.tenantScope !== "required") {
|
|
344
|
+
error("feature-command-tenant-boundary", `Command ${command.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
345
|
+
}
|
|
346
|
+
if (transition.persistence.boundary === "external" && command.transaction !== "none") {
|
|
347
|
+
error("feature-persistence-drift", `External transition ${transition.name} requires command ${command.className} to declare transaction: "none".`);
|
|
348
|
+
}
|
|
349
|
+
if (command.idempotency !== "required" || !command.audit) {
|
|
350
|
+
error("feature-persistence-drift", `Persistent transition ${transition.name} requires command ${command.className} to declare idempotency and audit.`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (transition.job) {
|
|
355
|
+
const jobs = (module.jobs ?? []).filter((job) => job.className === transition.job || job.name === transition.job);
|
|
356
|
+
if (jobs.length !== 1) {
|
|
357
|
+
error("feature-job-unresolved", `Transition ${transition.name} must reference exactly one Job in module ${module.name}.`);
|
|
358
|
+
} else {
|
|
359
|
+
const job = jobs[0];
|
|
360
|
+
if (job.tenantScope !== "required") {
|
|
361
|
+
error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
362
|
+
}
|
|
363
|
+
if (job.idempotency !== "required") {
|
|
364
|
+
error("feature-job-idempotency", `Job ${job.className} in Feature ${spec.name} must declare idempotency: "required".`);
|
|
365
|
+
}
|
|
366
|
+
if (job.mode === undefined) {
|
|
367
|
+
error("feature-job-mode", `Job ${job.className} in Feature ${spec.name} must declare mode: "task" or "workflow".`);
|
|
368
|
+
}
|
|
369
|
+
if (job.input === undefined || job.schemaKinds?.input !== "declared") {
|
|
370
|
+
error("feature-job-contract", `Job ${job.className} in Feature ${spec.name} must declare a non-opaque input schema.`);
|
|
371
|
+
}
|
|
372
|
+
const provider = module.providers.find((candidate) => candidate.token === job.className || candidate.useClass === job.className);
|
|
373
|
+
if (job.tenantScope === "required" && provider && !provider.deps.some((token) => isJobContextToken(token))) {
|
|
374
|
+
error("feature-job-context", `Job ${job.className} in Feature ${spec.name} must inject JOB_CONTEXT for trusted tenant execution.`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
294
377
|
}
|
|
295
378
|
if (transition.route) {
|
|
296
379
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
297
380
|
if (routes.length !== 1) {
|
|
298
381
|
error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
|
|
299
|
-
} else if (command && routes[0]?.command !== command.className) {
|
|
382
|
+
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className && routes[0]?.command !== command.name) {
|
|
300
383
|
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
384
|
+
} else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
|
|
385
|
+
error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
|
|
301
386
|
}
|
|
302
387
|
}
|
|
303
388
|
}
|
|
304
389
|
return diagnostics;
|
|
305
390
|
}
|
|
306
391
|
function generateFeatureSource(spec) {
|
|
307
|
-
const errors = validateFeatureSpec(spec);
|
|
308
|
-
if (errors.length)
|
|
309
|
-
throw new Error(errors.map((error) => error.message).join(`
|
|
310
|
-
`));
|
|
311
392
|
if (spec.transitions.some((transition) => !transition.permission)) {
|
|
312
393
|
throw new Error("Spec-to-Code requires an explicit permission for every transition.");
|
|
313
394
|
}
|
|
@@ -315,13 +396,34 @@ function generateFeatureSource(spec) {
|
|
|
315
396
|
...transition,
|
|
316
397
|
command: `Transition${index + 1}Command`
|
|
317
398
|
}));
|
|
399
|
+
const errors = validateFeatureSpec({ ...spec, transitions });
|
|
400
|
+
if (errors.length)
|
|
401
|
+
throw new Error(errors.map((error) => error.message).join(`
|
|
402
|
+
`));
|
|
318
403
|
if (transitions.some((transition) => transition.route)) {
|
|
319
404
|
throw new Error("Generate the command slice first; existing HTTP routes require explicit schema and handler implementations.");
|
|
320
405
|
}
|
|
321
|
-
const sourceSpec = {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
406
|
+
const sourceSpec = {
|
|
407
|
+
name: spec.name,
|
|
408
|
+
states: spec.states,
|
|
409
|
+
tenantField: spec.tenantField ?? "tenant_id",
|
|
410
|
+
transitions: Object.fromEntries(transitions.map((transition) => {
|
|
411
|
+
const { name, ...options } = transition;
|
|
412
|
+
const persistence = transition.persistence ?? {
|
|
413
|
+
boundary: "database",
|
|
414
|
+
optimisticLock: "required",
|
|
415
|
+
versionField: "version",
|
|
416
|
+
tenantField: "tenant_id",
|
|
417
|
+
stateField: "state",
|
|
418
|
+
receipt: "required",
|
|
419
|
+
reconciliation: "none"
|
|
420
|
+
};
|
|
421
|
+
return [name, {
|
|
422
|
+
...options,
|
|
423
|
+
persistence
|
|
424
|
+
}];
|
|
425
|
+
}))
|
|
426
|
+
};
|
|
325
427
|
return [
|
|
326
428
|
'import { Command, defineFeatureSlice, defineFeatureSpec } from "@supacloud/app";',
|
|
327
429
|
"",
|
|
@@ -332,9 +434,10 @@ function generateFeatureSource(spec) {
|
|
|
332
434
|
`@Command(${JSON.stringify({
|
|
333
435
|
name: `${spec.name}.${transition.name}`,
|
|
334
436
|
permission: transition.permission,
|
|
335
|
-
transaction: transition.transaction ?? "none",
|
|
336
|
-
idempotency: transition.idempotency ?? "
|
|
337
|
-
audit: transition.audit
|
|
437
|
+
transaction: transition.transaction ?? (transition.persistence?.boundary === "external" ? "none" : "required"),
|
|
438
|
+
idempotency: transition.idempotency ?? "required",
|
|
439
|
+
audit: transition.audit ?? `${spec.name}.${transition.name}`,
|
|
440
|
+
tenantScope: "required"
|
|
338
441
|
})})`,
|
|
339
442
|
`export class ${transition.command} {`,
|
|
340
443
|
` execute(state: FeatureState): never {`,
|
|
@@ -521,13 +624,17 @@ function validateGraph(graph, options = false) {
|
|
|
521
624
|
handler: route.handler,
|
|
522
625
|
...route.redirectTo === undefined ? {} : { redirectTo: route.redirectTo }
|
|
523
626
|
});
|
|
524
|
-
|
|
525
|
-
|
|
627
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
628
|
+
const effectiveCommandRef = route.command ?? featureTransition?.command;
|
|
629
|
+
const effectiveCommand = effectiveCommandRef === undefined ? undefined : module.commands.find((command) => command.className === effectiveCommandRef || command.name === effectiveCommandRef);
|
|
630
|
+
const effectiveIdempotency = route.idempotency ?? featureTransition?.idempotency;
|
|
631
|
+
if (route.command && !effectiveCommand) {
|
|
632
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
526
633
|
}
|
|
527
|
-
if ((route.method === "GET" || route.method === "HEAD") &&
|
|
528
|
-
const boundCommand =
|
|
634
|
+
if ((route.method === "GET" || route.method === "HEAD") && effectiveCommand) {
|
|
635
|
+
const boundCommand = effectiveCommand;
|
|
529
636
|
if (boundCommand && boundCommand.transaction === "required") {
|
|
530
|
-
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${
|
|
637
|
+
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${boundCommand.name}' with transaction: 'required'. Mutating transactions are not permitted on read-only HTTP GET requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for mutating command routes, or set transaction: 'none'.`);
|
|
531
638
|
}
|
|
532
639
|
}
|
|
533
640
|
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
@@ -598,6 +705,9 @@ function validateGraph(graph, options = false) {
|
|
|
598
705
|
if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
|
|
599
706
|
error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
|
|
600
707
|
}
|
|
708
|
+
if (effectiveIdempotency === "required" && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
709
|
+
error("invalid-route-idempotency", `Route ${controller.className}.${route.handler} cannot require idempotency on HTTP ${route.method}.`, controller.file, undefined, `Use idempotency: "required" only on a mutation route.`);
|
|
710
|
+
}
|
|
601
711
|
if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
|
|
602
712
|
error("invalid-raw-body-contract", `Route ${controller.className}.${route.handler} parse: "none" requires domain body ownership and evidence, without a parsed body schema or @Body binding.`, controller.file);
|
|
603
713
|
}
|
|
@@ -802,13 +912,40 @@ function validateGraph(graph, options = false) {
|
|
|
802
912
|
permission: `${module.name}.${command.name}`
|
|
803
913
|
});
|
|
804
914
|
}
|
|
915
|
+
if (command.rpc && command.branches) {
|
|
916
|
+
error("command-rpc-branches-conflict", `Command ${command.name} cannot declare both rpc and branches.`, module.file, module.line);
|
|
917
|
+
}
|
|
918
|
+
const branchAdapters = command.branches ?? [];
|
|
919
|
+
for (const branch of branchAdapters) {
|
|
920
|
+
if (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, branch.rpc)) {
|
|
921
|
+
error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${branch.rpc}'.`, module.file, module.line);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
805
924
|
if (command.rpc && (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, command.rpc))) {
|
|
806
925
|
error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${command.rpc}'.`, module.file, module.line);
|
|
807
926
|
}
|
|
808
927
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
809
928
|
const hostCaps = options.commandCapabilities;
|
|
810
929
|
const rpcCaps = command.rpc && Object.hasOwn(hostCaps.rpc ?? {}, command.rpc) ? hostCaps.rpc?.[command.rpc] : undefined;
|
|
811
|
-
|
|
930
|
+
const branchCaps = branchAdapters.map((branch) => ({
|
|
931
|
+
branch,
|
|
932
|
+
capabilities: Object.hasOwn(hostCaps.rpc ?? {}, branch.rpc) ? hostCaps.rpc?.[branch.rpc] : undefined
|
|
933
|
+
}));
|
|
934
|
+
for (const { branch, capabilities } of branchCaps) {
|
|
935
|
+
if (capabilities?.boundary === "external" && (branch.transaction === "required" || capabilities.transaction === true)) {
|
|
936
|
+
error("command-external-transaction", `Command ${command.name} branch '${branch.when}' calls an external service; use transaction: 'none'.`, module.file, module.line, "Use transaction: 'none' with durable intent and reconciliation.");
|
|
937
|
+
}
|
|
938
|
+
if (branch.audit && capabilities?.audit !== true) {
|
|
939
|
+
error("command-audit-unsupported", `Command ${command.name} branch '${branch.when}' declares audit, but runtime audit persistence is unavailable.`, module.file, module.line);
|
|
940
|
+
}
|
|
941
|
+
if (branch.idempotency === "required" && capabilities?.idempotency !== true) {
|
|
942
|
+
error("command-idempotency-unsupported", `Command ${command.name} branch '${branch.when}' declares idempotency, but receipt persistence is unavailable.`, module.file, module.line);
|
|
943
|
+
}
|
|
944
|
+
if (branch.transaction === "required" && capabilities?.transaction !== true) {
|
|
945
|
+
error("command-transaction-unsupported", `Command ${command.name} branch '${branch.when}' declares transaction, but transaction support is unavailable.`, module.file, module.line);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (hostCaps.requirePersistentAdapters && (command.rpc !== undefined && (!rpcCaps?.boundary || rpcCaps.audit !== true || rpcCaps.idempotency !== true) || branchCaps.length > 0 && branchCaps.some(({ branch, capabilities }) => !capabilities?.boundary || capabilities.audit !== true || capabilities.idempotency !== true || capabilities.boundary === "database" && (capabilities.transaction !== true || branch.transaction !== "required")) || hostCaps.permission !== true || !command.permission || branchCaps.length === 0 && (!command.audit || command.idempotency !== "required") || branchCaps.length > 0 && branchCaps.some(({ branch }) => !branch.audit || branch.idempotency !== "required") || branchCaps.length > 0 && branchCaps.some(({ capabilities }) => capabilities?.boundary === "external" && capabilities.reconcile !== true) || command.rpc !== undefined && rpcCaps?.boundary === "database" && (rpcCaps.transaction !== true || command.transaction !== "required") || command.rpc === undefined && branchCaps.length === 0 && (hostCaps.audit !== true || hostCaps.idempotency !== true || hostCaps.transaction !== true || command.transaction !== "required") || command.rpc !== undefined && rpcCaps?.boundary === "external" && rpcCaps.reconcile !== true)) {
|
|
812
949
|
error("command-persistence-required", `Command ${command.name} requires an explicit persistent adapter, permission, audit and idempotency policy.`, module.file, module.line, "Register a named database/external adapter, enable permission checks and declare permission, audit and required idempotency on the command.");
|
|
813
950
|
}
|
|
814
951
|
if (rpcCaps?.boundary === "external" && (command.transaction === "required" || rpcCaps.transaction === true)) {
|
|
@@ -1245,6 +1382,7 @@ var init_validate = __esm(() => {
|
|
|
1245
1382
|
"command-transaction-unsupported": { code: "SC4026", docsUrl: "https://supacloud.dev/errors/SC4026" },
|
|
1246
1383
|
"command-reconcile-required": { code: "SC4028", docsUrl: "https://supacloud.dev/errors/SC4028" },
|
|
1247
1384
|
"invalid-route-contract": { code: "SC3020", docsUrl: "https://supacloud.dev/errors/SC3020" },
|
|
1385
|
+
"invalid-route-idempotency": { code: "SC3028", docsUrl: "https://supacloud.dev/errors/SC3028" },
|
|
1248
1386
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
1249
1387
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
1250
1388
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
@@ -1284,6 +1422,18 @@ var init_validate = __esm(() => {
|
|
|
1284
1422
|
"invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
|
|
1285
1423
|
"feature-command-unresolved": { code: "SC6104", docsUrl: "https://supacloud.dev/errors/SC6104" },
|
|
1286
1424
|
"feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
|
|
1425
|
+
"feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
|
|
1426
|
+
"feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
|
|
1427
|
+
"feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
|
|
1428
|
+
"feature-tenant-boundary-drift": { code: "SC6113", docsUrl: "https://supacloud.dev/errors/SC6113" },
|
|
1429
|
+
"feature-query-tenant-boundary": { code: "SC6114", docsUrl: "https://supacloud.dev/errors/SC6114" },
|
|
1430
|
+
"feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
|
|
1431
|
+
"feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
|
|
1432
|
+
"feature-job-unresolved": { code: "SC6117", docsUrl: "https://supacloud.dev/errors/SC6117" },
|
|
1433
|
+
"feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
|
|
1434
|
+
"feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
|
|
1435
|
+
"feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
|
|
1436
|
+
"feature-job-context": { code: "SC6121", docsUrl: "https://supacloud.dev/errors/SC6121" },
|
|
1287
1437
|
"feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
|
|
1288
1438
|
"feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
|
|
1289
1439
|
"invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
|
|
@@ -1294,6 +1444,8 @@ var init_validate = __esm(() => {
|
|
|
1294
1444
|
function buildContractManifest(graph, artifacts) {
|
|
1295
1445
|
const commands = [];
|
|
1296
1446
|
const queries = [];
|
|
1447
|
+
const jobs = [];
|
|
1448
|
+
const features = [];
|
|
1297
1449
|
const routes = [];
|
|
1298
1450
|
const permissions = new Set;
|
|
1299
1451
|
const rpc = [];
|
|
@@ -1305,27 +1457,107 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1305
1457
|
permissions.add(command.permission);
|
|
1306
1458
|
if (command.rpc)
|
|
1307
1459
|
rpc.push({ command: command.name, adapter: command.rpc });
|
|
1460
|
+
for (const branch of command.branches ?? []) {
|
|
1461
|
+
rpc.push({ command: `${command.name} [${branch.when}]`, adapter: branch.rpc });
|
|
1462
|
+
}
|
|
1308
1463
|
if (command.audit)
|
|
1309
1464
|
events.push({ name: command.audit, source: "command.audit", command: command.name });
|
|
1465
|
+
for (const branch of command.branches ?? []) {
|
|
1466
|
+
if (branch.audit)
|
|
1467
|
+
events.push({ name: branch.audit, source: "command.branch.audit", command: command.name });
|
|
1468
|
+
}
|
|
1310
1469
|
commands.push({
|
|
1311
1470
|
name: command.name,
|
|
1312
1471
|
className: command.className,
|
|
1313
1472
|
module: module.name,
|
|
1314
1473
|
...command.permission === undefined ? {} : { permission: command.permission },
|
|
1315
1474
|
...command.rpc === undefined ? {} : { rpc: command.rpc },
|
|
1475
|
+
...command.branches === undefined ? {} : {
|
|
1476
|
+
branches: command.branches.map((branch) => ({
|
|
1477
|
+
when: branch.when,
|
|
1478
|
+
adapter: branch.rpc,
|
|
1479
|
+
transaction: branch.transaction,
|
|
1480
|
+
idempotency: branch.idempotency,
|
|
1481
|
+
...branch.audit === undefined ? {} : { auditEvent: branch.audit },
|
|
1482
|
+
...branch.boundary === undefined ? {} : { boundary: branch.boundary }
|
|
1483
|
+
}))
|
|
1484
|
+
},
|
|
1316
1485
|
transaction: command.transaction,
|
|
1317
1486
|
idempotency: command.idempotency,
|
|
1487
|
+
...command.tenantScope === undefined ? {} : { tenantScope: command.tenantScope },
|
|
1318
1488
|
...command.audit === undefined ? {} : { auditEvent: command.audit }
|
|
1319
1489
|
});
|
|
1320
1490
|
}
|
|
1321
1491
|
for (const query of module.queries) {
|
|
1322
|
-
queries.push({
|
|
1492
|
+
queries.push({
|
|
1493
|
+
name: query.name,
|
|
1494
|
+
className: query.className,
|
|
1495
|
+
module: module.name,
|
|
1496
|
+
...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
for (const job of module.jobs ?? []) {
|
|
1500
|
+
jobs.push({
|
|
1501
|
+
name: job.name,
|
|
1502
|
+
className: job.className,
|
|
1503
|
+
module: module.name,
|
|
1504
|
+
...job.input === undefined ? {} : { input: job.input },
|
|
1505
|
+
...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
|
|
1506
|
+
...job.output === undefined ? {} : { output: job.output },
|
|
1507
|
+
...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
|
|
1508
|
+
...job.mode === undefined ? {} : { mode: job.mode },
|
|
1509
|
+
...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
|
|
1510
|
+
...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
if (module.featureSpec) {
|
|
1514
|
+
features.push({
|
|
1515
|
+
name: module.featureSpec.name,
|
|
1516
|
+
states: [...module.featureSpec.states],
|
|
1517
|
+
...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
|
|
1518
|
+
transitions: module.featureSpec.transitions.map((transition) => {
|
|
1519
|
+
const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
|
|
1520
|
+
return {
|
|
1521
|
+
name: transition.name,
|
|
1522
|
+
from: transition.from,
|
|
1523
|
+
to: transition.to,
|
|
1524
|
+
...transition.permission === undefined ? {} : { permission: transition.permission },
|
|
1525
|
+
...transition.command === undefined ? {} : { command: transition.command },
|
|
1526
|
+
...transition.route === undefined ? {} : { route: transition.route },
|
|
1527
|
+
...transition.routeBinding === undefined ? {} : { routeBinding: transition.routeBinding },
|
|
1528
|
+
...transition.transaction === undefined ? {} : { transaction: transition.transaction },
|
|
1529
|
+
...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
|
|
1530
|
+
...transition.audit === undefined ? {} : { audit: transition.audit },
|
|
1531
|
+
...transition.job === undefined ? {} : { job: transition.job },
|
|
1532
|
+
...transition.invalidateTags === undefined ? {} : { invalidateTags: [...transition.invalidateTags] },
|
|
1533
|
+
...job === undefined ? {} : {
|
|
1534
|
+
jobDetails: {
|
|
1535
|
+
name: job.name,
|
|
1536
|
+
className: job.className,
|
|
1537
|
+
...job.input === undefined ? {} : { input: job.input },
|
|
1538
|
+
...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
|
|
1539
|
+
...job.output === undefined ? {} : { output: job.output },
|
|
1540
|
+
...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
|
|
1541
|
+
...job.mode === undefined ? {} : { mode: job.mode },
|
|
1542
|
+
...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
|
|
1543
|
+
...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
...transition.persistence === undefined ? {} : { persistence: transition.persistence }
|
|
1547
|
+
};
|
|
1548
|
+
})
|
|
1549
|
+
});
|
|
1323
1550
|
}
|
|
1324
1551
|
for (const controller of module.controllers) {
|
|
1325
1552
|
for (const route of controller.routes) {
|
|
1326
|
-
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
|
|
1327
|
-
|
|
1328
|
-
|
|
1553
|
+
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command) : undefined;
|
|
1554
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
1555
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
1556
|
+
const governedCommand = command ?? featureCommand;
|
|
1557
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
1558
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1559
|
+
if (governedCommand?.permission)
|
|
1560
|
+
permissions.add(governedCommand.permission);
|
|
1329
1561
|
if (route.contract?.evidence)
|
|
1330
1562
|
fixtures.add(route.contract.evidence);
|
|
1331
1563
|
routes.push({
|
|
@@ -1334,8 +1566,10 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1334
1566
|
module: module.name,
|
|
1335
1567
|
controller: controller.className,
|
|
1336
1568
|
handler: route.handler,
|
|
1337
|
-
...
|
|
1338
|
-
...
|
|
1569
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
1570
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
1571
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
1572
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
1339
1573
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
1340
1574
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
1341
1575
|
...route.responses === undefined ? {} : {
|
|
@@ -1350,6 +1584,8 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1350
1584
|
}
|
|
1351
1585
|
commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
1352
1586
|
queries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1587
|
+
jobs.sort((left, right) => left.name.localeCompare(right.name));
|
|
1588
|
+
features.sort((left, right) => left.name.localeCompare(right.name));
|
|
1353
1589
|
routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
|
|
1354
1590
|
rpc.sort((left, right) => left.command.localeCompare(right.command));
|
|
1355
1591
|
events.sort((left, right) => left.name.localeCompare(right.name));
|
|
@@ -1357,6 +1593,8 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1357
1593
|
version: 1,
|
|
1358
1594
|
commands,
|
|
1359
1595
|
queries,
|
|
1596
|
+
jobs,
|
|
1597
|
+
features,
|
|
1360
1598
|
routes,
|
|
1361
1599
|
permissions: [...permissions].sort(),
|
|
1362
1600
|
rpc,
|
|
@@ -1370,7 +1608,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1370
1608
|
}
|
|
1371
1609
|
};
|
|
1372
1610
|
}
|
|
1373
|
-
var init_contract_manifest = () => {
|
|
1611
|
+
var init_contract_manifest = __esm(() => {
|
|
1612
|
+
init_feature();
|
|
1613
|
+
});
|
|
1374
1614
|
|
|
1375
1615
|
// src/generate.ts
|
|
1376
1616
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -1504,6 +1744,7 @@ function renderApplication(graph, options) {
|
|
|
1504
1744
|
name: command.name,
|
|
1505
1745
|
permission: command.permission ?? null,
|
|
1506
1746
|
rpc: command.rpc ?? null,
|
|
1747
|
+
branches: command.branches ?? [],
|
|
1507
1748
|
transaction: command.transaction ?? null,
|
|
1508
1749
|
audit: command.audit ?? null,
|
|
1509
1750
|
idempotency: command.idempotency ?? null
|
|
@@ -1781,6 +2022,9 @@ class ModuleGenerator {
|
|
|
1781
2022
|
return "[]";
|
|
1782
2023
|
const items = this.module.controllers.map((controller) => {
|
|
1783
2024
|
const routes = controller.routes.map((route) => {
|
|
2025
|
+
const command = route.command === undefined ? undefined : this.module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
2026
|
+
const featureTransition = featureTransitionForRoute(this.module, controller, route);
|
|
2027
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1784
2028
|
const responseEntries = normalizedResponseEntries(route.responses);
|
|
1785
2029
|
const hasResponseMap = responseEntries.length > 0;
|
|
1786
2030
|
const fields = [
|
|
@@ -1807,6 +2051,9 @@ class ModuleGenerator {
|
|
|
1807
2051
|
if (route.effect !== undefined) {
|
|
1808
2052
|
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
1809
2053
|
}
|
|
2054
|
+
if (idempotency !== undefined) {
|
|
2055
|
+
fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
|
|
2056
|
+
}
|
|
1810
2057
|
if (route.command)
|
|
1811
2058
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
1812
2059
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -1930,7 +2177,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1930
2177
|
`transaction: ${JSON.stringify(command.transaction)}`,
|
|
1931
2178
|
...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
|
|
1932
2179
|
`idempotency: ${JSON.stringify(command.idempotency)}`,
|
|
2180
|
+
...command.tenantScope ? [`tenantScope: ${JSON.stringify(command.tenantScope)}`] : [],
|
|
1933
2181
|
...command.rpc ? [`rpc: ${JSON.stringify(command.rpc)}`] : [],
|
|
2182
|
+
...command.branches && command.branches.length > 0 ? [`branches: ${JSON.stringify(command.branches)}`] : [],
|
|
1934
2183
|
...command.standalone ? ["standalone: true"] : [],
|
|
1935
2184
|
...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`, `aspectPipeline: ${this.renderAspectPipeline(command.aspects)}`] : []
|
|
1936
2185
|
];
|
|
@@ -1963,6 +2212,8 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1963
2212
|
fields.push(`maxAttempts: ${job.maxAttempts}`);
|
|
1964
2213
|
if (job.idempotency !== undefined)
|
|
1965
2214
|
fields.push(`idempotency: ${JSON.stringify(job.idempotency)}`);
|
|
2215
|
+
if (job.tenantScope !== undefined)
|
|
2216
|
+
fields.push(`tenantScope: ${JSON.stringify(job.tenantScope)}`);
|
|
1966
2217
|
if (job.aspects && job.aspects.length > 0) {
|
|
1967
2218
|
fields.push(`aspects: ${this.renderAspects(job.aspects)}`);
|
|
1968
2219
|
fields.push(`aspectPipeline: ${this.renderAspectPipeline(job.aspects)}`);
|
|
@@ -2285,7 +2536,18 @@ function renderClient(graph, options) {
|
|
|
2285
2536
|
const usedTypeNames = new Set;
|
|
2286
2537
|
const allRoutes = [];
|
|
2287
2538
|
const allProcedures = [];
|
|
2288
|
-
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.
|
|
2539
|
+
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
2540
|
+
[command.className, command],
|
|
2541
|
+
[command.name, command]
|
|
2542
|
+
])));
|
|
2543
|
+
const featureInvalidationTags = new Map;
|
|
2544
|
+
for (const module of graph.modules) {
|
|
2545
|
+
for (const transition of module.featureSpec?.transitions ?? []) {
|
|
2546
|
+
if (transition.route && transition.invalidateTags) {
|
|
2547
|
+
featureInvalidationTags.set(`${module.name}:${transition.route}`, [...transition.invalidateTags]);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2289
2551
|
let usesStatic = false;
|
|
2290
2552
|
let usesValue = false;
|
|
2291
2553
|
const uniqueTypeName = (candidate) => {
|
|
@@ -2363,6 +2625,13 @@ function renderClient(graph, options) {
|
|
|
2363
2625
|
const requestType = `ClientRequestOptions<${paramsType}, ${queryType}, ${bodyType}, ${headersType}, ${cookieType}>${requiredRequestFields.length > 0 ? ` & { ${requiredRequestFields.join("; ")} }` : ""}`;
|
|
2364
2626
|
routeTypes.push(`export type ${requestTypeName} = ${requestType};`);
|
|
2365
2627
|
routeTypes.push(`export type ${responseTypeName} = ${responseType === "never" ? "unknown" : responseType};`);
|
|
2628
|
+
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2629
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
2630
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
|
|
2631
|
+
const governedCommand = command ?? featureCommand;
|
|
2632
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
2633
|
+
const transitionRoute = `${route.method} ${fullPath}`;
|
|
2634
|
+
const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
|
|
2366
2635
|
const routeFields = [
|
|
2367
2636
|
`method: ${JSON.stringify(route.method)}`,
|
|
2368
2637
|
`path: ${JSON.stringify(fullPath)}`,
|
|
@@ -2370,7 +2639,7 @@ function renderClient(graph, options) {
|
|
|
2370
2639
|
`handler: ${JSON.stringify(route.handler)}`
|
|
2371
2640
|
];
|
|
2372
2641
|
for (const [key, value] of [
|
|
2373
|
-
["command", route.command],
|
|
2642
|
+
["command", route.command ?? featureTransition?.command],
|
|
2374
2643
|
["guards", route.guards],
|
|
2375
2644
|
["canMatch", route.canMatch],
|
|
2376
2645
|
["canDeactivate", route.canDeactivate],
|
|
@@ -2384,7 +2653,10 @@ function renderClient(graph, options) {
|
|
|
2384
2653
|
["title", route.title],
|
|
2385
2654
|
["data", route.data],
|
|
2386
2655
|
["parse", route.parse],
|
|
2387
|
-
["allowDeleteBody", route.allowDeleteBody]
|
|
2656
|
+
["allowDeleteBody", route.allowDeleteBody],
|
|
2657
|
+
["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
|
|
2658
|
+
["tags", route.tags],
|
|
2659
|
+
["invalidateTags", invalidationTags]
|
|
2388
2660
|
]) {
|
|
2389
2661
|
if (value !== undefined)
|
|
2390
2662
|
routeFields.push(`${key}: ${JSON.stringify(value)}`);
|
|
@@ -2417,9 +2689,7 @@ function renderClient(graph, options) {
|
|
|
2417
2689
|
const responseSchemaEntries = hasResponseMap ? Object.entries(responseSchemas).map(([status, ref]) => [status, ref.local]) : responseSchema ? [["200", responseSchema.local]] : [];
|
|
2418
2690
|
const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
|
|
2419
2691
|
const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
|
|
2420
|
-
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2421
2692
|
const procedureKind = route.method === "GET" || route.method === "HEAD" ? "query" : "mutation";
|
|
2422
|
-
const idempotencyMode = command?.idempotency ?? "none";
|
|
2423
2693
|
const procedureMetadata = {
|
|
2424
2694
|
key: `${controller.className}.${route.handler}`,
|
|
2425
2695
|
method: route.method,
|
|
@@ -2427,7 +2697,9 @@ function renderClient(graph, options) {
|
|
|
2427
2697
|
controller: controller.className,
|
|
2428
2698
|
handler: route.handler,
|
|
2429
2699
|
kind: procedureKind,
|
|
2430
|
-
idempotency: idempotencyMode
|
|
2700
|
+
idempotency: idempotencyMode,
|
|
2701
|
+
...route.tags === undefined ? {} : { tags: route.tags },
|
|
2702
|
+
...invalidationTags === undefined ? {} : { invalidateTags: invalidationTags }
|
|
2431
2703
|
};
|
|
2432
2704
|
allProcedures.push({
|
|
2433
2705
|
method: route.method,
|
|
@@ -2435,9 +2707,11 @@ function renderClient(graph, options) {
|
|
|
2435
2707
|
controller: controller.className,
|
|
2436
2708
|
handler: route.handler,
|
|
2437
2709
|
kind: procedureKind,
|
|
2438
|
-
...
|
|
2439
|
-
...
|
|
2440
|
-
idempotency: idempotencyMode
|
|
2710
|
+
...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
|
|
2711
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
2712
|
+
idempotency: idempotencyMode,
|
|
2713
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
2714
|
+
...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
|
|
2441
2715
|
});
|
|
2442
2716
|
routeMethods.push(`
|
|
2443
2717
|
${route.handler}: makeRoute<${requestTypeName}, ${responseType === "never" ? "never" : responseTypeName}, ${JSON.stringify(procedureKind)}, ${JSON.stringify(idempotencyMode)}>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, ${responseSchemaArgument}, ${responseKindArgument}, ${route.parse === "none"}, ${JSON.stringify(procedureKind)}, ${JSON.stringify(idempotencyMode)}, ${JSON.stringify(procedureMetadata)}),`);
|
|
@@ -2491,6 +2765,8 @@ ${procedureMethods.join(`
|
|
|
2491
2765
|
" handler: string;",
|
|
2492
2766
|
" kind: Kind;",
|
|
2493
2767
|
" idempotency: Idempotency;",
|
|
2768
|
+
" tags?: readonly string[];",
|
|
2769
|
+
" invalidateTags?: readonly string[];",
|
|
2494
2770
|
"}",
|
|
2495
2771
|
"",
|
|
2496
2772
|
"export interface ProcedureExecutionOptions {",
|
|
@@ -2596,6 +2872,8 @@ ${procedureMethods.join(`
|
|
|
2596
2872
|
" command?: string;",
|
|
2597
2873
|
" permission?: string;",
|
|
2598
2874
|
" idempotency: IdempotencyMode;",
|
|
2875
|
+
" tags?: readonly string[];",
|
|
2876
|
+
" invalidateTags?: readonly string[];",
|
|
2599
2877
|
"}",
|
|
2600
2878
|
"",
|
|
2601
2879
|
...routeTypes,
|
|
@@ -3095,7 +3373,10 @@ function renderOpenApi(graph, options) {
|
|
|
3095
3373
|
}
|
|
3096
3374
|
return schemaComponents.get(key);
|
|
3097
3375
|
};
|
|
3098
|
-
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.
|
|
3376
|
+
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
3377
|
+
[command.className, command],
|
|
3378
|
+
[command.name, command]
|
|
3379
|
+
])));
|
|
3099
3380
|
for (const module of graph.modules) {
|
|
3100
3381
|
for (const controller of module.controllers) {
|
|
3101
3382
|
for (const route of controller.routes) {
|
|
@@ -3117,7 +3398,12 @@ function renderOpenApi(graph, options) {
|
|
|
3117
3398
|
const operationBase = pascalName(`${module.name}_${controller.className}_${route.handler}`);
|
|
3118
3399
|
const operationId = uniqueName(operationBase, operationIds);
|
|
3119
3400
|
const command = route.command ? commandByClass.get(route.command) : undefined;
|
|
3120
|
-
const
|
|
3401
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3402
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
|
|
3403
|
+
const governedCommand = command ?? featureCommand;
|
|
3404
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
3405
|
+
const permission = governedCommand?.permission ?? featureTransition?.permission;
|
|
3406
|
+
const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
|
|
3121
3407
|
const responseKind = route.contract?.response;
|
|
3122
3408
|
const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
|
|
3123
3409
|
const definition = {
|
|
@@ -3142,11 +3428,12 @@ function renderOpenApi(graph, options) {
|
|
|
3142
3428
|
responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
|
|
3143
3429
|
...route.parse === undefined ? {} : { parse: route.parse },
|
|
3144
3430
|
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
|
|
3431
|
+
...idempotencyMode === "none" ? {} : { idempotency: idempotencyMode },
|
|
3145
3432
|
module: module.name,
|
|
3146
3433
|
controller: controller.className,
|
|
3147
3434
|
handler: route.handler,
|
|
3148
|
-
...route.command === undefined ? {} : { command: route.command },
|
|
3149
|
-
...
|
|
3435
|
+
...route.command === undefined && featureTransition?.command === undefined ? {} : { command: route.command ?? featureTransition?.command },
|
|
3436
|
+
...permission === undefined ? {} : { permission },
|
|
3150
3437
|
...route.guards && route.guards.length > 0 ? { guards: route.guards } : {},
|
|
3151
3438
|
...route.canMatch && route.canMatch.length > 0 ? { canMatch: route.canMatch } : {},
|
|
3152
3439
|
...route.canDeactivate && route.canDeactivate.length > 0 ? { canDeactivate: route.canDeactivate } : {},
|
|
@@ -3221,6 +3508,7 @@ function renderOpenApi(graph, options) {
|
|
|
3221
3508
|
" handler: string;",
|
|
3222
3509
|
" command?: string;",
|
|
3223
3510
|
" permission?: string;",
|
|
3511
|
+
' idempotency?: "required" | "none";',
|
|
3224
3512
|
" guards?: readonly string[];",
|
|
3225
3513
|
" canMatch?: readonly string[];",
|
|
3226
3514
|
" canDeactivate?: readonly string[];",
|
|
@@ -3473,6 +3761,18 @@ function renderOpenApi(graph, options) {
|
|
|
3473
3761
|
" for (const name of headersSchema ? schemaPropertyNames(headersSchema) : []) parameters.push({",
|
|
3474
3762
|
' name, in: "header", required: schemaRequired(headersSchema, name), schema: schemaProperty(headersSchema, name),',
|
|
3475
3763
|
" });",
|
|
3764
|
+
' if (definition.idempotency === "required") {',
|
|
3765
|
+
' const keySchema = { type: "string", minLength: 1, maxLength: 512, pattern: "^[A-Za-z0-9._:-]{1,512}$" };',
|
|
3766
|
+
' const declaredHeaders = parameters.filter((parameter) => parameter["in"] === "header" && typeof parameter["name"] === "string" && parameter["name"].toLowerCase() === "idempotency-key");',
|
|
3767
|
+
" const header = declaredHeaders[0];",
|
|
3768
|
+
" if (header) {",
|
|
3769
|
+
' header["required"] = true;',
|
|
3770
|
+
' header["schema"] = { allOf: [...declaredHeaders.map((parameter) => parameter["schema"]), keySchema] };',
|
|
3771
|
+
" for (const duplicate of declaredHeaders.slice(1)) parameters.splice(parameters.indexOf(duplicate), 1);",
|
|
3772
|
+
" } else {",
|
|
3773
|
+
' parameters.push({ name: "Idempotency-Key", in: "header", required: true, schema: keySchema });',
|
|
3774
|
+
" }",
|
|
3775
|
+
" }",
|
|
3476
3776
|
" const cookieSchema = definition.cookieSchema ? OPENAPI_SCHEMA_REGISTRY[definition.cookieSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
3477
3777
|
" for (const name of cookieSchema ? schemaPropertyNames(cookieSchema) : []) parameters.push({",
|
|
3478
3778
|
' name, in: "cookie", required: schemaRequired(cookieSchema, name), schema: schemaProperty(cookieSchema, name),',
|
|
@@ -3495,6 +3795,7 @@ function renderOpenApi(graph, options) {
|
|
|
3495
3795
|
' responses["500"] = { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/SupaCloudError" } } } };',
|
|
3496
3796
|
' responses["200"] = { description: "Successful response" };',
|
|
3497
3797
|
" }",
|
|
3798
|
+
' if (definition.idempotency === "required" && (!definition.responseSchemas || !responseMapCoversStatus(definition.responseSchemas, 400))) responses["400"] = { description: "Idempotency-Key is missing or invalid", content: { "application/json": { schema: { $ref: "#/components/schemas/SupaCloudError" } } } };',
|
|
3498
3799
|
" const operation: Record<string, unknown> = {",
|
|
3499
3800
|
" operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
|
|
3500
3801
|
" ...(parameters.length === 0 ? {} : { parameters }),",
|
|
@@ -3505,6 +3806,7 @@ function renderOpenApi(graph, options) {
|
|
|
3505
3806
|
" module: definition.module, controller: definition.controller, handler: definition.handler,",
|
|
3506
3807
|
" ...(definition.command ? { command: definition.command } : {}),",
|
|
3507
3808
|
" ...(definition.permission ? { permission: definition.permission } : {}),",
|
|
3809
|
+
" ...(definition.idempotency ? { idempotency: definition.idempotency } : {}),",
|
|
3508
3810
|
" ...(definition.guards ? { guards: [...definition.guards] } : {}),",
|
|
3509
3811
|
" ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
|
|
3510
3812
|
" ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
|
|
@@ -3555,11 +3857,13 @@ function renderPermissions(graph) {
|
|
|
3555
3857
|
}
|
|
3556
3858
|
for (const controller of module.controllers) {
|
|
3557
3859
|
for (const route of controller.routes) {
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3860
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3861
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
3862
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
3863
|
+
const governedCommand = command ?? featureCommand;
|
|
3864
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
3865
|
+
const perm = governedCommand?.permission ?? featureTransition?.permission;
|
|
3866
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
3563
3867
|
if (perm)
|
|
3564
3868
|
permissions.add(perm);
|
|
3565
3869
|
bindings.push({
|
|
@@ -3567,8 +3871,9 @@ function renderPermissions(graph) {
|
|
|
3567
3871
|
path: joinRoutePaths(controller.path, route.path),
|
|
3568
3872
|
controller: controller.className,
|
|
3569
3873
|
handler: route.handler,
|
|
3570
|
-
...
|
|
3571
|
-
...perm === undefined ? {} : { permission: perm }
|
|
3874
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
3875
|
+
...perm === undefined ? {} : { permission: perm },
|
|
3876
|
+
...idempotency === undefined ? {} : { idempotency }
|
|
3572
3877
|
});
|
|
3573
3878
|
}
|
|
3574
3879
|
}
|
|
@@ -3594,6 +3899,7 @@ function renderPermissions(graph) {
|
|
|
3594
3899
|
" handler: string;",
|
|
3595
3900
|
" command?: string;",
|
|
3596
3901
|
" permission?: string;",
|
|
3902
|
+
' idempotency?: "required" | "none";',
|
|
3597
3903
|
"}",
|
|
3598
3904
|
"",
|
|
3599
3905
|
"export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
|
|
@@ -3637,6 +3943,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3637
3943
|
/** True when the handler returns a native Response rather than a framework value. */
|
|
3638
3944
|
nativeResponse?: boolean;
|
|
3639
3945
|
command?: string;
|
|
3946
|
+
/** Transport-level idempotency contract, independent of route command binding. */
|
|
3947
|
+
idempotency?: "required" | "none";
|
|
3640
3948
|
guards?: string[];
|
|
3641
3949
|
canMatch?: string[];
|
|
3642
3950
|
canDeactivate?: string[];
|
|
@@ -3666,17 +3974,28 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3666
3974
|
|
|
3667
3975
|
export interface CompiledCommand {
|
|
3668
3976
|
rpc?: string;
|
|
3977
|
+
branches?: readonly CompiledCommandBranch[];
|
|
3669
3978
|
className: string;
|
|
3670
3979
|
name: string;
|
|
3671
3980
|
permission: string;
|
|
3672
3981
|
transaction: "required" | "none";
|
|
3673
3982
|
audit?: string;
|
|
3674
3983
|
idempotency: "required" | "none";
|
|
3984
|
+
tenantScope?: "required" | "none";
|
|
3675
3985
|
standalone?: boolean;
|
|
3676
3986
|
aspects?: CompiledAspect[];
|
|
3677
3987
|
aspectPipeline?: CompiledAspectPipeline;
|
|
3678
3988
|
}
|
|
3679
3989
|
|
|
3990
|
+
export interface CompiledCommandBranch {
|
|
3991
|
+
when: string;
|
|
3992
|
+
rpc: string;
|
|
3993
|
+
transaction?: "required" | "none" | string;
|
|
3994
|
+
audit?: string;
|
|
3995
|
+
idempotency?: "required" | "none" | string;
|
|
3996
|
+
boundary?: "database" | "external";
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3680
3999
|
export interface CompiledJob {
|
|
3681
4000
|
className: string;
|
|
3682
4001
|
name: string;
|
|
@@ -3688,6 +4007,7 @@ export interface CompiledJob {
|
|
|
3688
4007
|
timeoutSec?: number;
|
|
3689
4008
|
maxAttempts?: number;
|
|
3690
4009
|
idempotency?: "required" | "none";
|
|
4010
|
+
tenantScope?: "required" | "none";
|
|
3691
4011
|
aspects?: CompiledAspect[];
|
|
3692
4012
|
aspectPipeline?: CompiledAspectPipeline;
|
|
3693
4013
|
}
|
|
@@ -3862,6 +4182,7 @@ async function destroyServiceInstances(
|
|
|
3862
4182
|
`;
|
|
3863
4183
|
var init_generate = __esm(() => {
|
|
3864
4184
|
init_contract_manifest();
|
|
4185
|
+
init_feature();
|
|
3865
4186
|
});
|
|
3866
4187
|
|
|
3867
4188
|
// src/route-contracts.ts
|
|
@@ -4715,7 +5036,9 @@ function createExecutionPlans(graph) {
|
|
|
4715
5036
|
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
4716
5037
|
return graph.modules.flatMap((module) => [
|
|
4717
5038
|
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
4718
|
-
const
|
|
5039
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
5040
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
5041
|
+
const command = module.commands.find((item) => item.className === commandRef || item.name === commandRef);
|
|
4719
5042
|
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
4720
5043
|
return {
|
|
4721
5044
|
module: module.name,
|
|
@@ -4865,6 +5188,7 @@ function exportGraphDot(graph) {
|
|
|
4865
5188
|
}
|
|
4866
5189
|
var init_inspect = __esm(() => {
|
|
4867
5190
|
init_route_contracts();
|
|
5191
|
+
init_feature();
|
|
4868
5192
|
});
|
|
4869
5193
|
|
|
4870
5194
|
// node_modules/typebox/build/system/memory/metrics.mjs
|
|
@@ -15432,6 +15756,7 @@ var init_application_development = __esm(() => {
|
|
|
15432
15756
|
controller: text2,
|
|
15433
15757
|
handler: text2,
|
|
15434
15758
|
command: Optional(text2),
|
|
15759
|
+
idempotency: Optional(mode),
|
|
15435
15760
|
aspects: texts,
|
|
15436
15761
|
schemaKinds: Optional(_Object_({
|
|
15437
15762
|
body: schemaKind,
|
|
@@ -15554,7 +15879,11 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
15554
15879
|
for (const controller of module.controllers) {
|
|
15555
15880
|
for (const route of controller.routes) {
|
|
15556
15881
|
const candidates = module.commands.filter((command) => command.className === route.command || command.name === route.command);
|
|
15557
|
-
const
|
|
15882
|
+
const commandNode = candidates.length === 1 ? candidates[0] : undefined;
|
|
15883
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
15884
|
+
const featureCommandNode = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
15885
|
+
const command = commandNode?.name ?? featureCommandNode?.name;
|
|
15886
|
+
const idempotency = route.idempotency ?? commandNode?.idempotency ?? featureTransition?.idempotency;
|
|
15558
15887
|
const kinds = schemaKinds(route);
|
|
15559
15888
|
routes.push({
|
|
15560
15889
|
module: module.name,
|
|
@@ -15563,6 +15892,7 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
15563
15892
|
controller: controller.className,
|
|
15564
15893
|
handler: route.handler,
|
|
15565
15894
|
...command === undefined ? {} : { command },
|
|
15895
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
15566
15896
|
aspects: (route.aspects ?? []).map((aspect) => aspect.name),
|
|
15567
15897
|
...kinds === undefined ? {} : { schemaKinds: kinds }
|
|
15568
15898
|
});
|
|
@@ -15683,6 +16013,7 @@ var init_application_development2 = __esm(() => {
|
|
|
15683
16013
|
init_application_development();
|
|
15684
16014
|
init_inspect();
|
|
15685
16015
|
init_execution_snapshot();
|
|
16016
|
+
init_feature();
|
|
15686
16017
|
APPLICATION_DEVELOPMENT_LIMITS2 = APPLICATION_DEVELOPMENT_LIMITS;
|
|
15687
16018
|
ApplicationDevelopmentError = class ApplicationDevelopmentError extends Error {
|
|
15688
16019
|
code;
|
|
@@ -15784,7 +16115,7 @@ function parseDeliveryPlanResult(value) {
|
|
|
15784
16115
|
throw new Error("Invalid delivery plan result.");
|
|
15785
16116
|
return value;
|
|
15786
16117
|
}
|
|
15787
|
-
var objectOptions, reference2, references, kind2, isolation, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
16118
|
+
var objectOptions, reference2, references, kind2, isolation, mode2, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
15788
16119
|
var init_delivery_schema = __esm(() => {
|
|
15789
16120
|
init_build();
|
|
15790
16121
|
init_value2();
|
|
@@ -15794,6 +16125,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
15794
16125
|
references = _Array_(reference2, { uniqueItems: true });
|
|
15795
16126
|
kind2 = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
|
|
15796
16127
|
isolation = Union([Literal("shared"), Literal("process")]);
|
|
16128
|
+
mode2 = Union([Literal("required"), Literal("none")]);
|
|
15797
16129
|
DeliveryFilePathSchema = String2({
|
|
15798
16130
|
maxLength: 512,
|
|
15799
16131
|
pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
|
|
@@ -15867,6 +16199,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
15867
16199
|
]),
|
|
15868
16200
|
path: String2(),
|
|
15869
16201
|
command: Optional(String2()),
|
|
16202
|
+
idempotency: Optional(mode2),
|
|
15870
16203
|
reason: Union([Literal("default-http"), Literal("explicit-module")])
|
|
15871
16204
|
}, objectOptions);
|
|
15872
16205
|
jobSchema = _Object_({
|
|
@@ -16125,7 +16458,12 @@ function parseDeliveryBuildManifest(value) {
|
|
|
16125
16458
|
throw new Error("Invalid delivery object inventory.");
|
|
16126
16459
|
}
|
|
16127
16460
|
}
|
|
16128
|
-
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
16461
|
+
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
16462
|
+
method: route.method,
|
|
16463
|
+
path: route.path,
|
|
16464
|
+
target: target.name,
|
|
16465
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
16466
|
+
})));
|
|
16129
16467
|
const jobs = value.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })));
|
|
16130
16468
|
if (canonical2(value.routes) !== canonical2(routes) || canonical2(value.jobs) !== canonical2(jobs)) {
|
|
16131
16469
|
throw new Error("Delivery mappings do not match planned ownership.");
|
|
@@ -16176,7 +16514,8 @@ var init_delivery_build_schema = __esm(() => {
|
|
|
16176
16514
|
routes: _Array_(_Object_({
|
|
16177
16515
|
method: String2(),
|
|
16178
16516
|
path: String2(),
|
|
16179
|
-
target: String2()
|
|
16517
|
+
target: String2(),
|
|
16518
|
+
idempotency: Optional(Union([Literal("required"), Literal("none")]))
|
|
16180
16519
|
}, closed4)),
|
|
16181
16520
|
jobs: _Array_(_Object_({ name: String2(), target: String2() }, closed4))
|
|
16182
16521
|
}, closed4);
|
|
@@ -16219,7 +16558,7 @@ async function readDeliverySelection(manifestPath, target) {
|
|
|
16219
16558
|
return { root, object, planned };
|
|
16220
16559
|
}
|
|
16221
16560
|
async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
16222
|
-
if (object.files.length > 1024 || object.files.some((file) => file.bytes >
|
|
16561
|
+
if (object.files.length > 1024 || object.files.some((file) => file.bytes > DELIVERY_ARTIFACT_MAX_FILE_BYTES) || object.files.reduce((bytes, file) => bytes + file.bytes, 0) > DELIVERY_ARTIFACT_MAX_TOTAL_BYTES) {
|
|
16223
16562
|
throw new Error("Artifact inventory exceeds limits.");
|
|
16224
16563
|
}
|
|
16225
16564
|
const objectRoot = join7(root, "objects", object.objectId);
|
|
@@ -16258,10 +16597,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
|
16258
16597
|
}
|
|
16259
16598
|
return retained;
|
|
16260
16599
|
}
|
|
16261
|
-
var JSON_LIMIT = 1048576;
|
|
16600
|
+
var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
|
|
16262
16601
|
var init_delivery_artifact = __esm(() => {
|
|
16263
16602
|
init_delivery_build_schema();
|
|
16264
16603
|
init_delivery_files();
|
|
16604
|
+
DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
|
|
16605
|
+
DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
|
|
16265
16606
|
});
|
|
16266
16607
|
|
|
16267
16608
|
// src/delivery-artifact.ts
|
|
@@ -17426,6 +17767,7 @@ var init_delivery_migration_plan = () => {};
|
|
|
17426
17767
|
|
|
17427
17768
|
// node_modules/@supacloud/delivery/src/index.ts
|
|
17428
17769
|
var init_src = __esm(() => {
|
|
17770
|
+
init_delivery_artifact();
|
|
17429
17771
|
init_delivery_artifact();
|
|
17430
17772
|
init_worker_execution();
|
|
17431
17773
|
init_worker_execution();
|
|
@@ -18807,6 +19149,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18807
19149
|
if (meta && booleanProp(meta, "standalone")) {
|
|
18808
19150
|
const permission = stringLiteralProp(meta, "permission");
|
|
18809
19151
|
const rpc = checkedRpc(meta, ctx);
|
|
19152
|
+
const branches = checkedBranches(meta, ctx);
|
|
18810
19153
|
const audit = stringLiteralProp(meta, "audit");
|
|
18811
19154
|
const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
|
|
18812
19155
|
const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
|
|
@@ -18816,6 +19159,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18816
19159
|
name: standaloneName,
|
|
18817
19160
|
...permission === undefined ? {} : { permission },
|
|
18818
19161
|
...rpc === undefined ? {} : { rpc },
|
|
19162
|
+
...branches === undefined ? {} : { branches },
|
|
18819
19163
|
transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
18820
19164
|
...audit === undefined ? {} : { audit },
|
|
18821
19165
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
@@ -19147,18 +19491,22 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19147
19491
|
const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
|
|
19148
19492
|
const permission = stringLiteralProp(meta, "permission");
|
|
19149
19493
|
const rpc = checkedRpc(meta, ctx);
|
|
19494
|
+
const branches = checkedBranches(meta, ctx);
|
|
19150
19495
|
const audit = stringLiteralProp(meta, "audit");
|
|
19151
19496
|
const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
|
|
19152
19497
|
const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19153
19498
|
rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19499
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19154
19500
|
commands.push({
|
|
19155
19501
|
className: cls.name?.text ?? "<anonymous>",
|
|
19156
19502
|
name: commandName,
|
|
19157
19503
|
...permission === undefined ? {} : { permission },
|
|
19158
19504
|
...rpc === undefined ? {} : { rpc },
|
|
19505
|
+
...branches === undefined ? {} : { branches },
|
|
19159
19506
|
transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19160
19507
|
...audit === undefined ? {} : { audit },
|
|
19161
19508
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19509
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19162
19510
|
...booleanProp(meta, "standalone") ? { standalone: true } : {},
|
|
19163
19511
|
...uses.length > 0 ? { uses } : {},
|
|
19164
19512
|
...aspects.length > 0 ? { aspects } : {}
|
|
@@ -19189,12 +19537,14 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19189
19537
|
const contract = parseJobOptions(meta, className, ctx);
|
|
19190
19538
|
const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19191
19539
|
rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19540
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19192
19541
|
jobs.push({
|
|
19193
19542
|
className,
|
|
19194
19543
|
name: stringLiteralProp(meta, "name") ?? className,
|
|
19195
19544
|
serviceKey: camelName(provider?.token ?? className),
|
|
19196
19545
|
scope,
|
|
19197
19546
|
...contract,
|
|
19547
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19198
19548
|
...jobUses.length > 0 ? { uses: jobUses } : {},
|
|
19199
19549
|
...aspects.length > 0 ? { aspects } : {}
|
|
19200
19550
|
});
|
|
@@ -19204,9 +19554,11 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19204
19554
|
if (queryDec) {
|
|
19205
19555
|
const meta = decoratorObjectArg(queryDec);
|
|
19206
19556
|
if (meta) {
|
|
19557
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19207
19558
|
queries.push({
|
|
19208
19559
|
className: cls.name?.text ?? "<anonymous>",
|
|
19209
|
-
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
|
|
19560
|
+
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
19561
|
+
...tenantScope === undefined ? {} : { tenantScope }
|
|
19210
19562
|
});
|
|
19211
19563
|
}
|
|
19212
19564
|
}
|
|
@@ -19261,6 +19613,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19261
19613
|
if (!ts7.isObjectLiteralExpression(input))
|
|
19262
19614
|
return invalid();
|
|
19263
19615
|
const name = stringLiteralProp(input, "name");
|
|
19616
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19264
19617
|
const statesExpr = getProp(input, "states");
|
|
19265
19618
|
const transitionObject = getProp(input, "transitions");
|
|
19266
19619
|
if (!name || !statesExpr || !ts7.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts7.isStringLiteral(state)) || !transitionObject || !ts7.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts7.isPropertyAssignment(property))) {
|
|
@@ -19279,15 +19632,27 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19279
19632
|
const options = property.initializer;
|
|
19280
19633
|
const from = stringLiteralProp(options, "from");
|
|
19281
19634
|
const to = stringLiteralProp(options, "to");
|
|
19282
|
-
if (!from || !to || options.properties.some((prop) => !ts7.isPropertyAssignment(prop)) || ["permission", "command", "route", "audit"].some((key) => getProp(options, key) && !stringLiteralProp(options, key)) || ["transaction", "idempotency"].some((key) => getProp(options, key) && !commandModeProp(options, key))) {
|
|
19635
|
+
if (!from || !to || options.properties.some((prop) => !ts7.isPropertyAssignment(prop)) || ["permission", "command", "route", "routeBinding", "audit", "job"].some((key) => getProp(options, key) && !stringLiteralProp(options, key)) || ["transaction", "idempotency"].some((key) => getProp(options, key) && !commandModeProp(options, key))) {
|
|
19283
19636
|
return invalid();
|
|
19284
19637
|
}
|
|
19285
19638
|
const permission = stringLiteralProp(options, "permission");
|
|
19286
19639
|
const command = stringLiteralProp(options, "command");
|
|
19287
19640
|
const route = stringLiteralProp(options, "route");
|
|
19641
|
+
const routeBinding = stringLiteralProp(options, "routeBinding");
|
|
19642
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
19643
|
+
return invalid();
|
|
19288
19644
|
const transaction = commandModeProp(options, "transaction");
|
|
19289
19645
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19290
19646
|
const audit = stringLiteralProp(options, "audit");
|
|
19647
|
+
const job = stringLiteralProp(options, "job");
|
|
19648
|
+
const invalidateTagsExpr = getProp(options, "invalidateTags");
|
|
19649
|
+
const invalidateTags = invalidateTagsExpr === undefined ? undefined : ts7.isArrayLiteralExpression(invalidateTagsExpr) && invalidateTagsExpr.elements.every((element) => ts7.isStringLiteral(element) && element.text.trim().length > 0) ? invalidateTagsExpr.elements.map((element) => element.text) : undefined;
|
|
19650
|
+
if (invalidateTagsExpr !== undefined && invalidateTags === undefined)
|
|
19651
|
+
return invalid();
|
|
19652
|
+
const persistenceExpr = getProp(options, "persistence");
|
|
19653
|
+
const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
|
|
19654
|
+
if (persistenceExpr && !persistence)
|
|
19655
|
+
return invalid();
|
|
19291
19656
|
transitions.push({
|
|
19292
19657
|
name: propertyName(property.name),
|
|
19293
19658
|
from,
|
|
@@ -19295,12 +19660,45 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19295
19660
|
...permission === undefined ? {} : { permission },
|
|
19296
19661
|
...command === undefined ? {} : { command },
|
|
19297
19662
|
...route === undefined ? {} : { route },
|
|
19663
|
+
...routeBinding === undefined ? {} : { routeBinding },
|
|
19298
19664
|
...transaction === undefined ? {} : { transaction },
|
|
19299
19665
|
...idempotency === undefined ? {} : { idempotency },
|
|
19300
|
-
...audit === undefined ? {} : { audit }
|
|
19666
|
+
...audit === undefined ? {} : { audit },
|
|
19667
|
+
...job === undefined ? {} : { job },
|
|
19668
|
+
...invalidateTags === undefined ? {} : { invalidateTags },
|
|
19669
|
+
...persistence === undefined ? {} : { persistence }
|
|
19301
19670
|
});
|
|
19302
19671
|
}
|
|
19303
|
-
return {
|
|
19672
|
+
return {
|
|
19673
|
+
name,
|
|
19674
|
+
states,
|
|
19675
|
+
...tenantField === undefined ? {} : { tenantField },
|
|
19676
|
+
transitions,
|
|
19677
|
+
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
19678
|
+
line: lineOf(input)
|
|
19679
|
+
};
|
|
19680
|
+
}
|
|
19681
|
+
function parseFeaturePersistence(input) {
|
|
19682
|
+
if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
|
|
19683
|
+
return;
|
|
19684
|
+
const boundary = stringLiteralProp(input, "boundary");
|
|
19685
|
+
const optimisticLock = commandModeProp(input, "optimisticLock");
|
|
19686
|
+
const versionField = stringLiteralProp(input, "versionField");
|
|
19687
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19688
|
+
const stateField = stringLiteralProp(input, "stateField");
|
|
19689
|
+
const receipt = commandModeProp(input, "receipt");
|
|
19690
|
+
const reconciliation = commandModeProp(input, "reconciliation");
|
|
19691
|
+
if (boundary !== "database" && boundary !== "external" || optimisticLock !== "required" && optimisticLock !== "none" || receipt !== "required" && receipt !== "none" || reconciliation !== "required" && reconciliation !== "none")
|
|
19692
|
+
return;
|
|
19693
|
+
return {
|
|
19694
|
+
boundary,
|
|
19695
|
+
optimisticLock,
|
|
19696
|
+
...versionField === undefined ? {} : { versionField },
|
|
19697
|
+
...tenantField === undefined ? {} : { tenantField },
|
|
19698
|
+
...stateField === undefined ? {} : { stateField },
|
|
19699
|
+
receipt,
|
|
19700
|
+
reconciliation
|
|
19701
|
+
};
|
|
19304
19702
|
}
|
|
19305
19703
|
function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
|
|
19306
19704
|
if (!input || seen.has(input))
|
|
@@ -19370,6 +19768,54 @@ function checkedCommandMode(object, property, ctx, command) {
|
|
|
19370
19768
|
}
|
|
19371
19769
|
return value;
|
|
19372
19770
|
}
|
|
19771
|
+
function checkedRouteIdempotency(object, ctx, route) {
|
|
19772
|
+
const expression = getProp(object, "idempotency");
|
|
19773
|
+
const value = stringLiteralProp(object, "idempotency");
|
|
19774
|
+
if (expression && value !== "required" && value !== "none") {
|
|
19775
|
+
const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
|
|
19776
|
+
ctx.diagnostics.push({
|
|
19777
|
+
severity: "error",
|
|
19778
|
+
code: "invalid-route-idempotency",
|
|
19779
|
+
errorCode: "SC3028",
|
|
19780
|
+
docsUrl: "https://supacloud.dev/errors/SC3028",
|
|
19781
|
+
message: `${route}.idempotency must be the explicit literal "required" or "none".`,
|
|
19782
|
+
file,
|
|
19783
|
+
line: lineOf(expression),
|
|
19784
|
+
suggestion: `Choose the intended idempotency policy explicitly; use "required" for replayable mutations or "none" for routes without that contract.`
|
|
19785
|
+
});
|
|
19786
|
+
}
|
|
19787
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
19788
|
+
}
|
|
19789
|
+
function checkedRouteTags(expression, ctx, route) {
|
|
19790
|
+
if (expression === undefined)
|
|
19791
|
+
return;
|
|
19792
|
+
if (!ts7.isArrayLiteralExpression(expression) || expression.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
19793
|
+
ctx.diagnostics.push({
|
|
19794
|
+
severity: "error",
|
|
19795
|
+
code: "invalid-route-tags",
|
|
19796
|
+
errorCode: "SC3029",
|
|
19797
|
+
docsUrl: "https://supacloud.dev/errors/SC3029",
|
|
19798
|
+
message: `${route}.tags must be a static array of non-empty string literals.`,
|
|
19799
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19800
|
+
line: lineOf(expression),
|
|
19801
|
+
suggestion: 'Declare stable cache tags explicitly, for example tags: ["cases.list"].'
|
|
19802
|
+
});
|
|
19803
|
+
return;
|
|
19804
|
+
}
|
|
19805
|
+
const tags = expression.elements.map((element) => element.text);
|
|
19806
|
+
if (new Set(tags).size !== tags.length) {
|
|
19807
|
+
ctx.diagnostics.push({
|
|
19808
|
+
severity: "error",
|
|
19809
|
+
code: "duplicate-route-tags",
|
|
19810
|
+
errorCode: "SC3030",
|
|
19811
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
19812
|
+
message: `${route}.tags must not contain duplicate values.`,
|
|
19813
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19814
|
+
line: lineOf(expression)
|
|
19815
|
+
});
|
|
19816
|
+
}
|
|
19817
|
+
return tags;
|
|
19818
|
+
}
|
|
19373
19819
|
function parseProvider(el, exportsSet, ctx) {
|
|
19374
19820
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
|
|
19375
19821
|
const line = lineOf(el);
|
|
@@ -20149,7 +20595,11 @@ function parseController(input, ctx) {
|
|
|
20149
20595
|
if (optionsObject) {
|
|
20150
20596
|
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
20151
20597
|
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
20152
|
-
|
|
20598
|
+
const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
|
|
20599
|
+
if (idempotency !== undefined)
|
|
20600
|
+
route.idempotency = idempotency;
|
|
20601
|
+
const tagsPolicy = routePolicyProperty(optionsObject, "tags", ctx);
|
|
20602
|
+
if (parsePolicy.unresolved || deletePolicy.unresolved || tagsPolicy.unresolved) {
|
|
20153
20603
|
ctx.diagnostics.push({
|
|
20154
20604
|
severity: "error",
|
|
20155
20605
|
code: "unresolved-route-body-policy",
|
|
@@ -20157,6 +20607,9 @@ function parseController(input, ctx) {
|
|
|
20157
20607
|
message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
|
|
20158
20608
|
});
|
|
20159
20609
|
}
|
|
20610
|
+
const tags = checkedRouteTags(tagsPolicy.value, ctx, route.handler);
|
|
20611
|
+
if (tags !== undefined)
|
|
20612
|
+
route.tags = tags;
|
|
20160
20613
|
const parse = parsePolicy.value;
|
|
20161
20614
|
if (parse) {
|
|
20162
20615
|
if (ts7.isStringLiteral(parse) && parse.text === "none")
|
|
@@ -20727,6 +21180,75 @@ function checkedRpc(meta, ctx) {
|
|
|
20727
21180
|
});
|
|
20728
21181
|
return;
|
|
20729
21182
|
}
|
|
21183
|
+
function checkedBranches(meta, ctx) {
|
|
21184
|
+
const expression = getProp(meta, "branches");
|
|
21185
|
+
if (!expression)
|
|
21186
|
+
return;
|
|
21187
|
+
if (!ts7.isArrayLiteralExpression(expression)) {
|
|
21188
|
+
ctx.diagnostics.push({
|
|
21189
|
+
severity: "error",
|
|
21190
|
+
code: "invalid-command-branches",
|
|
21191
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
21192
|
+
line: lineOf(expression),
|
|
21193
|
+
message: "Command branches must be an explicit object array.",
|
|
21194
|
+
suggestion: "Declare each branch as { when, rpc, transaction, audit, idempotency, boundary }."
|
|
21195
|
+
});
|
|
21196
|
+
return;
|
|
21197
|
+
}
|
|
21198
|
+
const branches = [];
|
|
21199
|
+
for (const item of expression.elements) {
|
|
21200
|
+
if (!ts7.isObjectLiteralExpression(item)) {
|
|
21201
|
+
ctx.diagnostics.push({
|
|
21202
|
+
severity: "error",
|
|
21203
|
+
code: "invalid-command-branches",
|
|
21204
|
+
file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
|
|
21205
|
+
line: lineOf(item),
|
|
21206
|
+
message: "Command branches entries must be object literals."
|
|
21207
|
+
});
|
|
21208
|
+
continue;
|
|
21209
|
+
}
|
|
21210
|
+
const when = stringLiteralProp(item, "when");
|
|
21211
|
+
const rpc = stringLiteralProp(item, "rpc");
|
|
21212
|
+
const transaction = checkedCommandMode(item, "transaction", ctx, "command branch") ?? "none";
|
|
21213
|
+
const audit = stringLiteralProp(item, "audit");
|
|
21214
|
+
const idempotency = checkedCommandMode(item, "idempotency", ctx, "command branch") ?? "none";
|
|
21215
|
+
const boundary = stringLiteralProp(item, "boundary");
|
|
21216
|
+
const validBoundary = boundary === undefined || boundary === "database" || boundary === "external";
|
|
21217
|
+
const validWhen = when !== undefined && /^(?:[A-Za-z_$][\w$]*)(?:\.[A-Za-z_$][\w$]*)*\s*===\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(when);
|
|
21218
|
+
if (!when || !validWhen || !rpc?.trim() || !validBoundary) {
|
|
21219
|
+
ctx.diagnostics.push({
|
|
21220
|
+
severity: "error",
|
|
21221
|
+
code: "invalid-command-branches",
|
|
21222
|
+
file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
|
|
21223
|
+
line: lineOf(item),
|
|
21224
|
+
message: "Command branches require a restricted equality selector, a non-empty rpc, and a valid boundary.",
|
|
21225
|
+
suggestion: `Use when: 'resource === "value"' (or body.resource/params.resource/query.resource), plus a named RPC adapter.`
|
|
21226
|
+
});
|
|
21227
|
+
continue;
|
|
21228
|
+
}
|
|
21229
|
+
branches.push({
|
|
21230
|
+
when,
|
|
21231
|
+
rpc,
|
|
21232
|
+
transaction,
|
|
21233
|
+
...audit === undefined ? {} : { audit },
|
|
21234
|
+
idempotency,
|
|
21235
|
+
...boundary === undefined ? {} : { boundary }
|
|
21236
|
+
});
|
|
21237
|
+
}
|
|
21238
|
+
if (branches.length === 0)
|
|
21239
|
+
return [];
|
|
21240
|
+
if (new Set(branches.map((branch) => branch.when)).size !== branches.length) {
|
|
21241
|
+
ctx.diagnostics.push({
|
|
21242
|
+
severity: "error",
|
|
21243
|
+
code: "duplicate-command-branch",
|
|
21244
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
21245
|
+
line: lineOf(expression),
|
|
21246
|
+
message: "Command branches must use distinct selectors.",
|
|
21247
|
+
suggestion: "Make each branch selector mutually distinct."
|
|
21248
|
+
});
|
|
21249
|
+
}
|
|
21250
|
+
return branches;
|
|
21251
|
+
}
|
|
20730
21252
|
function classDeps(cls, ctx) {
|
|
20731
21253
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
20732
21254
|
const ctor = cls.members.find(ts7.isConstructorDeclaration);
|
|
@@ -21936,6 +22458,7 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
21936
22458
|
|
|
21937
22459
|
// src/effect-governance.ts
|
|
21938
22460
|
init_validate();
|
|
22461
|
+
init_feature();
|
|
21939
22462
|
function diagnostic(code, message, file, suggestion) {
|
|
21940
22463
|
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
21941
22464
|
return {
|
|
@@ -21973,8 +22496,10 @@ function validateEffectPolicies(graph, options = {}) {
|
|
|
21973
22496
|
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
21974
22497
|
diagnostics.push(diagnostic("invalid-effect-retry", `Route ${label} enables explicit Effect retry without maxAttempts.`, controller.file, "Declare a positive maxAttempts value including the initial attempt."));
|
|
21975
22498
|
}
|
|
21976
|
-
if (
|
|
21977
|
-
const
|
|
22499
|
+
if (effect.retry === "explicit") {
|
|
22500
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22501
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
22502
|
+
const command = commandRef === undefined ? undefined : commands.get(commandRef) ?? [...commands.values()].find((candidate) => candidate.name === commandRef);
|
|
21978
22503
|
if (command && command.idempotency !== "required") {
|
|
21979
22504
|
diagnostics.push(diagnostic("effect-command-retry-forbidden", `Route ${label} enables explicit Effect retry for non-idempotent command "${command.name}".`, controller.file, 'Set command idempotency to "required" or use effect.retry: "none".'));
|
|
21980
22505
|
}
|
|
@@ -23191,6 +23716,7 @@ init_graphql_options();
|
|
|
23191
23716
|
|
|
23192
23717
|
// src/delivery-plan.ts
|
|
23193
23718
|
import { createHash as createHash10 } from "node:crypto";
|
|
23719
|
+
init_feature();
|
|
23194
23720
|
init_validate();
|
|
23195
23721
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
23196
23722
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
@@ -23310,6 +23836,10 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
23310
23836
|
fail("delivery-route-conflict", `Public route "${key}" is owned by both "${existing}" and "${target.name}".`, "Give public routes unambiguous method/path contracts before splitting targets.");
|
|
23311
23837
|
}
|
|
23312
23838
|
publicRoutes.set(key, target.name);
|
|
23839
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
23840
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
23841
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
23842
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
23313
23843
|
target.roots.push(module.name);
|
|
23314
23844
|
target.routes.push({
|
|
23315
23845
|
module: module.name,
|
|
@@ -23317,7 +23847,8 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
23317
23847
|
handler: route.handler,
|
|
23318
23848
|
method: route.method,
|
|
23319
23849
|
path,
|
|
23320
|
-
...
|
|
23850
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
23851
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
23321
23852
|
reason: owner === undefined ? "default-http" : "explicit-module"
|
|
23322
23853
|
});
|
|
23323
23854
|
}
|
|
@@ -24349,7 +24880,12 @@ async function buildDeliveryProject(options, delivery) {
|
|
|
24349
24880
|
deploymentReady: false,
|
|
24350
24881
|
plan: planned.plan,
|
|
24351
24882
|
objects,
|
|
24352
|
-
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
24883
|
+
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
24884
|
+
method: route.method,
|
|
24885
|
+
path: route.path,
|
|
24886
|
+
target: target.name,
|
|
24887
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
24888
|
+
}))),
|
|
24353
24889
|
jobs: planned.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })))
|
|
24354
24890
|
});
|
|
24355
24891
|
phase = "artifact-integrity";
|
|
@@ -24415,10 +24951,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24415
24951
|
import { resolve as resolve18 } from "node:path";
|
|
24416
24952
|
|
|
24417
24953
|
// src/migration-app-version.ts
|
|
24418
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24954
|
+
var MIGRATION_APP_VERSION = "0.30.0";
|
|
24419
24955
|
|
|
24420
24956
|
// src/migration-elysia-version.ts
|
|
24421
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24957
|
+
var MIGRATION_ELYSIA_VERSION = "0.35.0";
|
|
24422
24958
|
|
|
24423
24959
|
// src/migration-policy.ts
|
|
24424
24960
|
var cachedCompilerVersion;
|
|
@@ -25388,6 +25924,175 @@ function isRecord2(value) {
|
|
|
25388
25924
|
function arrayLength(value) {
|
|
25389
25925
|
return Array.isArray(value) ? value.length : undefined;
|
|
25390
25926
|
}
|
|
25927
|
+
function safeField(value) {
|
|
25928
|
+
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
25929
|
+
}
|
|
25930
|
+
function validFeaturePersistence(value) {
|
|
25931
|
+
if (!isRecord2(value))
|
|
25932
|
+
return false;
|
|
25933
|
+
const boundary = value["boundary"];
|
|
25934
|
+
const receipt = value["receipt"];
|
|
25935
|
+
const reconciliation = value["reconciliation"];
|
|
25936
|
+
if (boundary === "database") {
|
|
25937
|
+
const fields = [value["tenantField"], value["versionField"], value["stateField"]];
|
|
25938
|
+
return value["optimisticLock"] === "required" && fields.every(safeField) && new Set(fields).size === 3 && receipt === "required" && reconciliation === "none";
|
|
25939
|
+
}
|
|
25940
|
+
if (boundary === "external") {
|
|
25941
|
+
return value["optimisticLock"] === "none" && value["versionField"] === undefined && value["tenantField"] === undefined && value["stateField"] === undefined && receipt === "required" && reconciliation === "required";
|
|
25942
|
+
}
|
|
25943
|
+
return false;
|
|
25944
|
+
}
|
|
25945
|
+
function validCommandInventory(value) {
|
|
25946
|
+
const names = new Set;
|
|
25947
|
+
const classes = new Set;
|
|
25948
|
+
return Array.isArray(value) && value.every((command) => {
|
|
25949
|
+
if (!isRecord2(command) || typeof command["name"] !== "string" || command["name"].trim() === "" || typeof command["className"] !== "string" || command["className"].trim() === "" || typeof command["module"] !== "string" || command["module"].trim() === "" || typeof command["permission"] !== "string" || command["permission"].trim() === "" || command["transaction"] !== "required" && command["transaction"] !== "none" || command["idempotency"] !== "required" && command["idempotency"] !== "none" || command["tenantScope"] !== undefined && command["tenantScope"] !== "required" && command["tenantScope"] !== "none")
|
|
25950
|
+
return false;
|
|
25951
|
+
if (names.has(command["name"]) || classes.has(command["className"]))
|
|
25952
|
+
return false;
|
|
25953
|
+
if (command["rpc"] !== undefined && (typeof command["rpc"] !== "string" || command["rpc"].trim() === ""))
|
|
25954
|
+
return false;
|
|
25955
|
+
if (command["auditEvent"] !== undefined && (typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === ""))
|
|
25956
|
+
return false;
|
|
25957
|
+
names.add(command["name"]);
|
|
25958
|
+
classes.add(command["className"]);
|
|
25959
|
+
return true;
|
|
25960
|
+
});
|
|
25961
|
+
}
|
|
25962
|
+
function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
25963
|
+
const featureNames = new Set;
|
|
25964
|
+
return Array.isArray(value) && value.every((feature) => {
|
|
25965
|
+
if (!isRecord2(feature) || typeof feature["name"] !== "string" || feature["name"].trim() === "")
|
|
25966
|
+
return false;
|
|
25967
|
+
if (featureNames.has(feature["name"]))
|
|
25968
|
+
return false;
|
|
25969
|
+
featureNames.add(feature["name"]);
|
|
25970
|
+
if (feature["tenantField"] !== undefined && !safeField(feature["tenantField"]))
|
|
25971
|
+
return false;
|
|
25972
|
+
const states = feature["states"];
|
|
25973
|
+
const transitions = feature["transitions"];
|
|
25974
|
+
if (!Array.isArray(states) || states.length === 0 || states.some((state) => typeof state !== "string" || state.trim() === "") || new Set(states).size !== states.length || !Array.isArray(transitions))
|
|
25975
|
+
return false;
|
|
25976
|
+
const transitionNames = new Set;
|
|
25977
|
+
return transitions.every((transition) => {
|
|
25978
|
+
if (!isRecord2(transition) || typeof transition["name"] !== "string" || transition["name"].trim() === "" || typeof transition["from"] !== "string" || !states.includes(transition["from"]) || typeof transition["to"] !== "string" || !states.includes(transition["to"]))
|
|
25979
|
+
return false;
|
|
25980
|
+
if (transitionNames.has(transition["name"]))
|
|
25981
|
+
return false;
|
|
25982
|
+
transitionNames.add(transition["name"]);
|
|
25983
|
+
const persistence = transition["persistence"];
|
|
25984
|
+
if (persistence !== undefined) {
|
|
25985
|
+
if (!isRecord2(persistence) || !validFeaturePersistence(persistence))
|
|
25986
|
+
return false;
|
|
25987
|
+
if (typeof transition["command"] !== "string" || transition["command"].trim() === "" || typeof transition["permission"] !== "string" || transition["permission"].trim() === "")
|
|
25988
|
+
return false;
|
|
25989
|
+
if (persistence["boundary"] === "database" && feature["tenantField"] !== persistence["tenantField"])
|
|
25990
|
+
return false;
|
|
25991
|
+
}
|
|
25992
|
+
const commandName = transition["command"];
|
|
25993
|
+
const command = commandName === undefined ? undefined : commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25994
|
+
if (commandName !== undefined) {
|
|
25995
|
+
if (!isRecord2(command))
|
|
25996
|
+
return false;
|
|
25997
|
+
if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
|
|
25998
|
+
return false;
|
|
25999
|
+
if (transition["transaction"] !== undefined && command["transaction"] !== transition["transaction"])
|
|
26000
|
+
return false;
|
|
26001
|
+
if (transition["idempotency"] !== undefined && command["idempotency"] !== transition["idempotency"])
|
|
26002
|
+
return false;
|
|
26003
|
+
if (transition["audit"] !== undefined && command["auditEvent"] !== transition["audit"])
|
|
26004
|
+
return false;
|
|
26005
|
+
if (persistence !== undefined) {
|
|
26006
|
+
const adapter = command["rpc"];
|
|
26007
|
+
if (typeof adapter !== "string" || adapter.trim() === "" || !rpc.some((candidate) => isRecord2(candidate) && candidate["command"] === command["name"] && candidate["adapter"] === adapter) || command["idempotency"] !== "required" || command["tenantScope"] !== "required" || typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === "")
|
|
26008
|
+
return false;
|
|
26009
|
+
const expectedTransaction = persistence["boundary"] === "database" ? "required" : "none";
|
|
26010
|
+
if (command["transaction"] !== expectedTransaction)
|
|
26011
|
+
return false;
|
|
26012
|
+
}
|
|
26013
|
+
}
|
|
26014
|
+
const routeName = transition["route"];
|
|
26015
|
+
const routeBinding = transition["routeBinding"];
|
|
26016
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
26017
|
+
return false;
|
|
26018
|
+
if (routeName !== undefined) {
|
|
26019
|
+
const route = routes.find((candidate) => isRecord2(candidate) && `${candidate["method"]} ${candidate["path"]}` === routeName);
|
|
26020
|
+
if (!isRecord2(route))
|
|
26021
|
+
return false;
|
|
26022
|
+
if (routeBinding === "controller") {
|
|
26023
|
+
if (route["command"] !== undefined)
|
|
26024
|
+
return false;
|
|
26025
|
+
} else {
|
|
26026
|
+
if (typeof route["command"] !== "string")
|
|
26027
|
+
return false;
|
|
26028
|
+
const routeCommand = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === route["command"] || candidate["className"] === route["command"]));
|
|
26029
|
+
if (!isRecord2(routeCommand) || !isRecord2(command) || routeCommand["name"] !== command["name"])
|
|
26030
|
+
return false;
|
|
26031
|
+
}
|
|
26032
|
+
if (transition["permission"] !== undefined && route["permission"] !== transition["permission"])
|
|
26033
|
+
return false;
|
|
26034
|
+
if (transition["idempotency"] !== undefined && route["idempotency"] !== transition["idempotency"])
|
|
26035
|
+
return false;
|
|
26036
|
+
if (persistence !== undefined && route["idempotency"] !== "required")
|
|
26037
|
+
return false;
|
|
26038
|
+
if (route["idempotency"] === "required" && (route["method"] === "GET" || route["method"] === "HEAD" || route["method"] === "OPTIONS"))
|
|
26039
|
+
return false;
|
|
26040
|
+
}
|
|
26041
|
+
const job = transition["job"];
|
|
26042
|
+
if (job === undefined)
|
|
26043
|
+
return true;
|
|
26044
|
+
const details = transition["jobDetails"];
|
|
26045
|
+
const inventory = jobs.find((candidate) => isRecord2(candidate) && (candidate["name"] === job || candidate["className"] === job));
|
|
26046
|
+
const contractFields = ["input", "inputKind", "output", "outputKind", "mode", "idempotency", "tenantScope"];
|
|
26047
|
+
return typeof job === "string" && job.trim() !== "" && isRecord2(details) && isRecord2(inventory) && (details["name"] === job || details["className"] === job) && typeof details["className"] === "string" && details["className"].trim() !== "" && details["inputKind"] === "declared" && (details["mode"] === "task" || details["mode"] === "workflow") && details["idempotency"] === "required" && details["tenantScope"] === "required" && contractFields.every((field) => details[field] === inventory[field]);
|
|
26048
|
+
});
|
|
26049
|
+
});
|
|
26050
|
+
}
|
|
26051
|
+
function validJobInventory(value) {
|
|
26052
|
+
const names = new Set;
|
|
26053
|
+
const classes = new Set;
|
|
26054
|
+
return Array.isArray(value) && value.every((job) => {
|
|
26055
|
+
if (!isRecord2(job) || typeof job["name"] !== "string" || job["name"].trim() === "" || typeof job["className"] !== "string" || job["className"].trim() === "" || typeof job["module"] !== "string" || job["module"].trim() === "")
|
|
26056
|
+
return false;
|
|
26057
|
+
if (names.has(job["name"]) || classes.has(job["className"]))
|
|
26058
|
+
return false;
|
|
26059
|
+
names.add(job["name"]);
|
|
26060
|
+
classes.add(job["className"]);
|
|
26061
|
+
if (job["input"] !== undefined && (typeof job["input"] !== "string" || job["input"].trim() === ""))
|
|
26062
|
+
return false;
|
|
26063
|
+
if (job["inputKind"] !== undefined && job["inputKind"] !== "opaque" && job["inputKind"] !== "declared")
|
|
26064
|
+
return false;
|
|
26065
|
+
if (job["output"] !== undefined && (typeof job["output"] !== "string" || job["output"].trim() === ""))
|
|
26066
|
+
return false;
|
|
26067
|
+
if (job["outputKind"] !== undefined && job["outputKind"] !== "opaque" && job["outputKind"] !== "declared")
|
|
26068
|
+
return false;
|
|
26069
|
+
if (job["mode"] !== undefined && job["mode"] !== "task" && job["mode"] !== "workflow")
|
|
26070
|
+
return false;
|
|
26071
|
+
if (job["idempotency"] !== undefined && job["idempotency"] !== "required" && job["idempotency"] !== "none")
|
|
26072
|
+
return false;
|
|
26073
|
+
return job["tenantScope"] === undefined || job["tenantScope"] === "required" || job["tenantScope"] === "none";
|
|
26074
|
+
});
|
|
26075
|
+
}
|
|
26076
|
+
function validRpcInventory(value, commandInventory) {
|
|
26077
|
+
const commandNames = new Set;
|
|
26078
|
+
const adapters = new Set;
|
|
26079
|
+
return Array.isArray(value) && value.every((rpc) => {
|
|
26080
|
+
if (!isRecord2(rpc) || typeof rpc["command"] !== "string" || rpc["command"].trim() === "" || typeof rpc["adapter"] !== "string" || rpc["adapter"].trim() === "")
|
|
26081
|
+
return false;
|
|
26082
|
+
if (commandNames.has(rpc["command"]) || adapters.has(rpc["adapter"]))
|
|
26083
|
+
return false;
|
|
26084
|
+
const command = commandInventory.find((candidate) => isRecord2(candidate) && candidate["name"] === rpc["command"] && candidate["rpc"] === rpc["adapter"]);
|
|
26085
|
+
if (!isRecord2(command))
|
|
26086
|
+
return false;
|
|
26087
|
+
commandNames.add(rpc["command"]);
|
|
26088
|
+
adapters.add(rpc["adapter"]);
|
|
26089
|
+
return true;
|
|
26090
|
+
}) && commandInventory.every((command) => {
|
|
26091
|
+
if (!isRecord2(command) || command["rpc"] === undefined)
|
|
26092
|
+
return true;
|
|
26093
|
+
return value.some((rpc) => isRecord2(rpc) && rpc["command"] === command["name"] && rpc["adapter"] === command["rpc"]);
|
|
26094
|
+
});
|
|
26095
|
+
}
|
|
25391
26096
|
async function sha2566(path) {
|
|
25392
26097
|
return createHash11("sha256").update(await readFile11(path)).digest("hex");
|
|
25393
26098
|
}
|
|
@@ -25457,19 +26162,24 @@ async function assessMigration(options) {
|
|
|
25457
26162
|
};
|
|
25458
26163
|
try {
|
|
25459
26164
|
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
25460
|
-
if (!isRecord2(parsed) || parsed["version"] !== 1)
|
|
26165
|
+
if (!isRecord2(parsed) || parsed["version"] !== 1 || !validJobInventory(parsed["jobs"]) || !validCommandInventory(parsed["commands"]) || !Array.isArray(parsed["routes"]) || !validRpcInventory(parsed["rpc"], parsed["commands"]) || !validFeatureInventory(parsed["features"], parsed["jobs"], parsed["commands"], parsed["routes"], parsed["rpc"])) {
|
|
25461
26166
|
throw new Error("invalid manifest");
|
|
26167
|
+
}
|
|
25462
26168
|
const commands = arrayLength(parsed["commands"]);
|
|
26169
|
+
const jobs = arrayLength(parsed["jobs"]);
|
|
25463
26170
|
const routes = arrayLength(parsed["routes"]);
|
|
25464
26171
|
const permissions = arrayLength(parsed["permissions"]);
|
|
26172
|
+
const features = arrayLength(parsed["features"]);
|
|
25465
26173
|
contractsManifest = {
|
|
25466
26174
|
path: contractsManifestPath,
|
|
25467
26175
|
present: true,
|
|
25468
26176
|
sha256: await sha2566(contractsManifestPath),
|
|
25469
26177
|
version: 1,
|
|
25470
26178
|
...commands === undefined ? {} : { commands },
|
|
26179
|
+
...jobs === undefined ? {} : { jobs },
|
|
25471
26180
|
...routes === undefined ? {} : { routes },
|
|
25472
|
-
...permissions === undefined ? {} : { permissions }
|
|
26181
|
+
...permissions === undefined ? {} : { permissions },
|
|
26182
|
+
...features === undefined ? {} : { features }
|
|
25473
26183
|
};
|
|
25474
26184
|
} catch {
|
|
25475
26185
|
findings.push({
|