@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/index.js
CHANGED
|
@@ -248,6 +248,10 @@ function findClosestMatch(target, candidates) {
|
|
|
248
248
|
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context", JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
249
249
|
|
|
250
250
|
// src/feature.ts
|
|
251
|
+
function featureTransitionForRoute(module, controller, route) {
|
|
252
|
+
const routePath = `${route.method} ${joinRoutePaths(controller.path, route.path)}`;
|
|
253
|
+
return module.featureSpec?.transitions.find((transition) => transition.route === routePath);
|
|
254
|
+
}
|
|
251
255
|
function validateFeatureSpec(spec, module) {
|
|
252
256
|
const diagnostics = [];
|
|
253
257
|
const error = (code, message) => {
|
|
@@ -262,6 +266,9 @@ function validateFeatureSpec(spec, module) {
|
|
|
262
266
|
if (!spec.name.trim() || spec.states.length === 0 || spec.states.some((state) => !state.trim()) || new Set(spec.states).size !== spec.states.length) {
|
|
263
267
|
error("invalid-feature-states", "Feature name and states must be non-empty; states must be unique.");
|
|
264
268
|
}
|
|
269
|
+
if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
|
|
270
|
+
error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
|
|
271
|
+
}
|
|
265
272
|
const names = new Set;
|
|
266
273
|
for (const transition of spec.transitions) {
|
|
267
274
|
if (!transition.name.trim() || names.has(transition.name)) {
|
|
@@ -274,8 +281,46 @@ function validateFeatureSpec(spec, module) {
|
|
|
274
281
|
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
275
282
|
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
276
283
|
}
|
|
284
|
+
if (transition.invalidateTags !== undefined) {
|
|
285
|
+
if (transition.invalidateTags.length === 0 || transition.invalidateTags.some((tag) => !tag.trim()) || new Set(transition.invalidateTags).size !== transition.invalidateTags.length) {
|
|
286
|
+
error("feature-invalidation-tags-invalid", `Transition ${transition.name} must declare unique, non-empty invalidateTags.`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (transition.persistence) {
|
|
290
|
+
const persistence = transition.persistence;
|
|
291
|
+
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")) {
|
|
292
|
+
error("feature-persistence-invalid", `Database transition ${transition.name} requires distinct safe tenant/version/state fields, optimistic locking, and a receipt, without reconciliation.`);
|
|
293
|
+
}
|
|
294
|
+
if (persistence.boundary === "database" && spec.tenantField !== persistence.tenantField) {
|
|
295
|
+
error("feature-tenant-boundary-drift", `Database transition ${transition.name} tenant field must match Feature ${spec.name} tenantField.`);
|
|
296
|
+
}
|
|
297
|
+
if (persistence.boundary === "external" && (persistence.optimisticLock !== "none" || persistence.versionField !== undefined || persistence.tenantField !== undefined || persistence.stateField !== undefined || persistence.receipt !== "required" || persistence.reconciliation !== "required")) {
|
|
298
|
+
error("feature-persistence-invalid", `External transition ${transition.name} requires a receipt and reconciliation, without database optimistic locking.`);
|
|
299
|
+
}
|
|
300
|
+
if (persistence.boundary === "external" && !transition.job) {
|
|
301
|
+
error("feature-job-unresolved", `External transition ${transition.name} must bind a reconciliation Job.`);
|
|
302
|
+
}
|
|
303
|
+
if (!transition.command) {
|
|
304
|
+
error("feature-command-unresolved", `Persistent transition ${transition.name} must reference a command.`);
|
|
305
|
+
}
|
|
306
|
+
if (!transition.permission?.trim()) {
|
|
307
|
+
error("feature-governance-drift", `Persistent transition ${transition.name} must declare a permission.`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
277
310
|
if (!module)
|
|
278
311
|
continue;
|
|
312
|
+
if (transition.persistence?.boundary === "database") {
|
|
313
|
+
for (const query of module.queries) {
|
|
314
|
+
if (query.tenantScope !== "required") {
|
|
315
|
+
error("feature-query-tenant-boundary", `Query ${query.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const job of module.jobs ?? []) {
|
|
319
|
+
if (job.tenantScope !== "required") {
|
|
320
|
+
error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
279
324
|
const commands = module.commands.filter((command) => command.className === transition.command || command.name === transition.command);
|
|
280
325
|
const command = commands[0];
|
|
281
326
|
if (transition.command && commands.length !== 1) {
|
|
@@ -290,23 +335,59 @@ function validateFeatureSpec(spec, module) {
|
|
|
290
335
|
error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
|
|
291
336
|
}
|
|
292
337
|
}
|
|
338
|
+
if (transition.persistence) {
|
|
339
|
+
if (transition.persistence.boundary === "database" && command.transaction !== "required") {
|
|
340
|
+
error("feature-persistence-drift", `Database transition ${transition.name} requires command ${command.className} to declare transaction: "required".`);
|
|
341
|
+
}
|
|
342
|
+
if (command.tenantScope !== "required") {
|
|
343
|
+
error("feature-command-tenant-boundary", `Command ${command.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
344
|
+
}
|
|
345
|
+
if (transition.persistence.boundary === "external" && command.transaction !== "none") {
|
|
346
|
+
error("feature-persistence-drift", `External transition ${transition.name} requires command ${command.className} to declare transaction: "none".`);
|
|
347
|
+
}
|
|
348
|
+
if (command.idempotency !== "required" || !command.audit) {
|
|
349
|
+
error("feature-persistence-drift", `Persistent transition ${transition.name} requires command ${command.className} to declare idempotency and audit.`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (transition.job) {
|
|
354
|
+
const jobs = (module.jobs ?? []).filter((job) => job.className === transition.job || job.name === transition.job);
|
|
355
|
+
if (jobs.length !== 1) {
|
|
356
|
+
error("feature-job-unresolved", `Transition ${transition.name} must reference exactly one Job in module ${module.name}.`);
|
|
357
|
+
} else {
|
|
358
|
+
const job = jobs[0];
|
|
359
|
+
if (job.tenantScope !== "required") {
|
|
360
|
+
error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
361
|
+
}
|
|
362
|
+
if (job.idempotency !== "required") {
|
|
363
|
+
error("feature-job-idempotency", `Job ${job.className} in Feature ${spec.name} must declare idempotency: "required".`);
|
|
364
|
+
}
|
|
365
|
+
if (job.mode === undefined) {
|
|
366
|
+
error("feature-job-mode", `Job ${job.className} in Feature ${spec.name} must declare mode: "task" or "workflow".`);
|
|
367
|
+
}
|
|
368
|
+
if (job.input === undefined || job.schemaKinds?.input !== "declared") {
|
|
369
|
+
error("feature-job-contract", `Job ${job.className} in Feature ${spec.name} must declare a non-opaque input schema.`);
|
|
370
|
+
}
|
|
371
|
+
const provider = module.providers.find((candidate) => candidate.token === job.className || candidate.useClass === job.className);
|
|
372
|
+
if (job.tenantScope === "required" && provider && !provider.deps.some((token) => isJobContextToken(token))) {
|
|
373
|
+
error("feature-job-context", `Job ${job.className} in Feature ${spec.name} must inject JOB_CONTEXT for trusted tenant execution.`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
293
376
|
}
|
|
294
377
|
if (transition.route) {
|
|
295
378
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
296
379
|
if (routes.length !== 1) {
|
|
297
380
|
error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
|
|
298
|
-
} else if (command && routes[0]?.command !== command.className) {
|
|
381
|
+
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className && routes[0]?.command !== command.name) {
|
|
299
382
|
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
383
|
+
} else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
|
|
384
|
+
error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
|
|
300
385
|
}
|
|
301
386
|
}
|
|
302
387
|
}
|
|
303
388
|
return diagnostics;
|
|
304
389
|
}
|
|
305
390
|
function generateFeatureSource(spec) {
|
|
306
|
-
const errors = validateFeatureSpec(spec);
|
|
307
|
-
if (errors.length)
|
|
308
|
-
throw new Error(errors.map((error) => error.message).join(`
|
|
309
|
-
`));
|
|
310
391
|
if (spec.transitions.some((transition) => !transition.permission)) {
|
|
311
392
|
throw new Error("Spec-to-Code requires an explicit permission for every transition.");
|
|
312
393
|
}
|
|
@@ -314,13 +395,34 @@ function generateFeatureSource(spec) {
|
|
|
314
395
|
...transition,
|
|
315
396
|
command: `Transition${index + 1}Command`
|
|
316
397
|
}));
|
|
398
|
+
const errors = validateFeatureSpec({ ...spec, transitions });
|
|
399
|
+
if (errors.length)
|
|
400
|
+
throw new Error(errors.map((error) => error.message).join(`
|
|
401
|
+
`));
|
|
317
402
|
if (transitions.some((transition) => transition.route)) {
|
|
318
403
|
throw new Error("Generate the command slice first; existing HTTP routes require explicit schema and handler implementations.");
|
|
319
404
|
}
|
|
320
|
-
const sourceSpec = {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
405
|
+
const sourceSpec = {
|
|
406
|
+
name: spec.name,
|
|
407
|
+
states: spec.states,
|
|
408
|
+
tenantField: spec.tenantField ?? "tenant_id",
|
|
409
|
+
transitions: Object.fromEntries(transitions.map((transition) => {
|
|
410
|
+
const { name, ...options } = transition;
|
|
411
|
+
const persistence = transition.persistence ?? {
|
|
412
|
+
boundary: "database",
|
|
413
|
+
optimisticLock: "required",
|
|
414
|
+
versionField: "version",
|
|
415
|
+
tenantField: "tenant_id",
|
|
416
|
+
stateField: "state",
|
|
417
|
+
receipt: "required",
|
|
418
|
+
reconciliation: "none"
|
|
419
|
+
};
|
|
420
|
+
return [name, {
|
|
421
|
+
...options,
|
|
422
|
+
persistence
|
|
423
|
+
}];
|
|
424
|
+
}))
|
|
425
|
+
};
|
|
324
426
|
return [
|
|
325
427
|
'import { Command, defineFeatureSlice, defineFeatureSpec } from "@supacloud/app";',
|
|
326
428
|
"",
|
|
@@ -331,9 +433,10 @@ function generateFeatureSource(spec) {
|
|
|
331
433
|
`@Command(${JSON.stringify({
|
|
332
434
|
name: `${spec.name}.${transition.name}`,
|
|
333
435
|
permission: transition.permission,
|
|
334
|
-
transaction: transition.transaction ?? "none",
|
|
335
|
-
idempotency: transition.idempotency ?? "
|
|
336
|
-
audit: transition.audit
|
|
436
|
+
transaction: transition.transaction ?? (transition.persistence?.boundary === "external" ? "none" : "required"),
|
|
437
|
+
idempotency: transition.idempotency ?? "required",
|
|
438
|
+
audit: transition.audit ?? `${spec.name}.${transition.name}`,
|
|
439
|
+
tenantScope: "required"
|
|
337
440
|
})})`,
|
|
338
441
|
`export class ${transition.command} {`,
|
|
339
442
|
` execute(state: FeatureState): never {`,
|
|
@@ -520,13 +623,17 @@ function validateGraph(graph, options = false) {
|
|
|
520
623
|
handler: route.handler,
|
|
521
624
|
...route.redirectTo === undefined ? {} : { redirectTo: route.redirectTo }
|
|
522
625
|
});
|
|
523
|
-
|
|
524
|
-
|
|
626
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
627
|
+
const effectiveCommandRef = route.command ?? featureTransition?.command;
|
|
628
|
+
const effectiveCommand = effectiveCommandRef === undefined ? undefined : module.commands.find((command) => command.className === effectiveCommandRef || command.name === effectiveCommandRef);
|
|
629
|
+
const effectiveIdempotency = route.idempotency ?? featureTransition?.idempotency;
|
|
630
|
+
if (route.command && !effectiveCommand) {
|
|
631
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
525
632
|
}
|
|
526
|
-
if ((route.method === "GET" || route.method === "HEAD") &&
|
|
527
|
-
const boundCommand =
|
|
633
|
+
if ((route.method === "GET" || route.method === "HEAD") && effectiveCommand) {
|
|
634
|
+
const boundCommand = effectiveCommand;
|
|
528
635
|
if (boundCommand && boundCommand.transaction === "required") {
|
|
529
|
-
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${
|
|
636
|
+
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'.`);
|
|
530
637
|
}
|
|
531
638
|
}
|
|
532
639
|
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
@@ -597,6 +704,9 @@ function validateGraph(graph, options = false) {
|
|
|
597
704
|
if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
|
|
598
705
|
error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
|
|
599
706
|
}
|
|
707
|
+
if (effectiveIdempotency === "required" && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
708
|
+
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.`);
|
|
709
|
+
}
|
|
600
710
|
if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
|
|
601
711
|
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);
|
|
602
712
|
}
|
|
@@ -801,13 +911,40 @@ function validateGraph(graph, options = false) {
|
|
|
801
911
|
permission: `${module.name}.${command.name}`
|
|
802
912
|
});
|
|
803
913
|
}
|
|
914
|
+
if (command.rpc && command.branches) {
|
|
915
|
+
error("command-rpc-branches-conflict", `Command ${command.name} cannot declare both rpc and branches.`, module.file, module.line);
|
|
916
|
+
}
|
|
917
|
+
const branchAdapters = command.branches ?? [];
|
|
918
|
+
for (const branch of branchAdapters) {
|
|
919
|
+
if (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, branch.rpc)) {
|
|
920
|
+
error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${branch.rpc}'.`, module.file, module.line);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
804
923
|
if (command.rpc && (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, command.rpc))) {
|
|
805
924
|
error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${command.rpc}'.`, module.file, module.line);
|
|
806
925
|
}
|
|
807
926
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
808
927
|
const hostCaps = options.commandCapabilities;
|
|
809
928
|
const rpcCaps = command.rpc && Object.hasOwn(hostCaps.rpc ?? {}, command.rpc) ? hostCaps.rpc?.[command.rpc] : undefined;
|
|
810
|
-
|
|
929
|
+
const branchCaps = branchAdapters.map((branch) => ({
|
|
930
|
+
branch,
|
|
931
|
+
capabilities: Object.hasOwn(hostCaps.rpc ?? {}, branch.rpc) ? hostCaps.rpc?.[branch.rpc] : undefined
|
|
932
|
+
}));
|
|
933
|
+
for (const { branch, capabilities } of branchCaps) {
|
|
934
|
+
if (capabilities?.boundary === "external" && (branch.transaction === "required" || capabilities.transaction === true)) {
|
|
935
|
+
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.");
|
|
936
|
+
}
|
|
937
|
+
if (branch.audit && capabilities?.audit !== true) {
|
|
938
|
+
error("command-audit-unsupported", `Command ${command.name} branch '${branch.when}' declares audit, but runtime audit persistence is unavailable.`, module.file, module.line);
|
|
939
|
+
}
|
|
940
|
+
if (branch.idempotency === "required" && capabilities?.idempotency !== true) {
|
|
941
|
+
error("command-idempotency-unsupported", `Command ${command.name} branch '${branch.when}' declares idempotency, but receipt persistence is unavailable.`, module.file, module.line);
|
|
942
|
+
}
|
|
943
|
+
if (branch.transaction === "required" && capabilities?.transaction !== true) {
|
|
944
|
+
error("command-transaction-unsupported", `Command ${command.name} branch '${branch.when}' declares transaction, but transaction support is unavailable.`, module.file, module.line);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
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)) {
|
|
811
948
|
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.");
|
|
812
949
|
}
|
|
813
950
|
if (rpcCaps?.boundary === "external" && (command.transaction === "required" || rpcCaps.transaction === true)) {
|
|
@@ -1244,6 +1381,7 @@ var init_validate = __esm(() => {
|
|
|
1244
1381
|
"command-transaction-unsupported": { code: "SC4026", docsUrl: "https://supacloud.dev/errors/SC4026" },
|
|
1245
1382
|
"command-reconcile-required": { code: "SC4028", docsUrl: "https://supacloud.dev/errors/SC4028" },
|
|
1246
1383
|
"invalid-route-contract": { code: "SC3020", docsUrl: "https://supacloud.dev/errors/SC3020" },
|
|
1384
|
+
"invalid-route-idempotency": { code: "SC3028", docsUrl: "https://supacloud.dev/errors/SC3028" },
|
|
1247
1385
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
1248
1386
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
1249
1387
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
@@ -1283,6 +1421,18 @@ var init_validate = __esm(() => {
|
|
|
1283
1421
|
"invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
|
|
1284
1422
|
"feature-command-unresolved": { code: "SC6104", docsUrl: "https://supacloud.dev/errors/SC6104" },
|
|
1285
1423
|
"feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
|
|
1424
|
+
"feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
|
|
1425
|
+
"feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
|
|
1426
|
+
"feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
|
|
1427
|
+
"feature-tenant-boundary-drift": { code: "SC6113", docsUrl: "https://supacloud.dev/errors/SC6113" },
|
|
1428
|
+
"feature-query-tenant-boundary": { code: "SC6114", docsUrl: "https://supacloud.dev/errors/SC6114" },
|
|
1429
|
+
"feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
|
|
1430
|
+
"feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
|
|
1431
|
+
"feature-job-unresolved": { code: "SC6117", docsUrl: "https://supacloud.dev/errors/SC6117" },
|
|
1432
|
+
"feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
|
|
1433
|
+
"feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
|
|
1434
|
+
"feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
|
|
1435
|
+
"feature-job-context": { code: "SC6121", docsUrl: "https://supacloud.dev/errors/SC6121" },
|
|
1286
1436
|
"feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
|
|
1287
1437
|
"feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
|
|
1288
1438
|
"invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
|
|
@@ -1293,6 +1443,8 @@ var init_validate = __esm(() => {
|
|
|
1293
1443
|
function buildContractManifest(graph, artifacts) {
|
|
1294
1444
|
const commands = [];
|
|
1295
1445
|
const queries = [];
|
|
1446
|
+
const jobs = [];
|
|
1447
|
+
const features = [];
|
|
1296
1448
|
const routes = [];
|
|
1297
1449
|
const permissions = new Set;
|
|
1298
1450
|
const rpc = [];
|
|
@@ -1304,27 +1456,107 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1304
1456
|
permissions.add(command.permission);
|
|
1305
1457
|
if (command.rpc)
|
|
1306
1458
|
rpc.push({ command: command.name, adapter: command.rpc });
|
|
1459
|
+
for (const branch of command.branches ?? []) {
|
|
1460
|
+
rpc.push({ command: `${command.name} [${branch.when}]`, adapter: branch.rpc });
|
|
1461
|
+
}
|
|
1307
1462
|
if (command.audit)
|
|
1308
1463
|
events.push({ name: command.audit, source: "command.audit", command: command.name });
|
|
1464
|
+
for (const branch of command.branches ?? []) {
|
|
1465
|
+
if (branch.audit)
|
|
1466
|
+
events.push({ name: branch.audit, source: "command.branch.audit", command: command.name });
|
|
1467
|
+
}
|
|
1309
1468
|
commands.push({
|
|
1310
1469
|
name: command.name,
|
|
1311
1470
|
className: command.className,
|
|
1312
1471
|
module: module.name,
|
|
1313
1472
|
...command.permission === undefined ? {} : { permission: command.permission },
|
|
1314
1473
|
...command.rpc === undefined ? {} : { rpc: command.rpc },
|
|
1474
|
+
...command.branches === undefined ? {} : {
|
|
1475
|
+
branches: command.branches.map((branch) => ({
|
|
1476
|
+
when: branch.when,
|
|
1477
|
+
adapter: branch.rpc,
|
|
1478
|
+
transaction: branch.transaction,
|
|
1479
|
+
idempotency: branch.idempotency,
|
|
1480
|
+
...branch.audit === undefined ? {} : { auditEvent: branch.audit },
|
|
1481
|
+
...branch.boundary === undefined ? {} : { boundary: branch.boundary }
|
|
1482
|
+
}))
|
|
1483
|
+
},
|
|
1315
1484
|
transaction: command.transaction,
|
|
1316
1485
|
idempotency: command.idempotency,
|
|
1486
|
+
...command.tenantScope === undefined ? {} : { tenantScope: command.tenantScope },
|
|
1317
1487
|
...command.audit === undefined ? {} : { auditEvent: command.audit }
|
|
1318
1488
|
});
|
|
1319
1489
|
}
|
|
1320
1490
|
for (const query of module.queries) {
|
|
1321
|
-
queries.push({
|
|
1491
|
+
queries.push({
|
|
1492
|
+
name: query.name,
|
|
1493
|
+
className: query.className,
|
|
1494
|
+
module: module.name,
|
|
1495
|
+
...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
for (const job of module.jobs ?? []) {
|
|
1499
|
+
jobs.push({
|
|
1500
|
+
name: job.name,
|
|
1501
|
+
className: job.className,
|
|
1502
|
+
module: module.name,
|
|
1503
|
+
...job.input === undefined ? {} : { input: job.input },
|
|
1504
|
+
...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
|
|
1505
|
+
...job.output === undefined ? {} : { output: job.output },
|
|
1506
|
+
...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
|
|
1507
|
+
...job.mode === undefined ? {} : { mode: job.mode },
|
|
1508
|
+
...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
|
|
1509
|
+
...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
if (module.featureSpec) {
|
|
1513
|
+
features.push({
|
|
1514
|
+
name: module.featureSpec.name,
|
|
1515
|
+
states: [...module.featureSpec.states],
|
|
1516
|
+
...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
|
|
1517
|
+
transitions: module.featureSpec.transitions.map((transition) => {
|
|
1518
|
+
const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
|
|
1519
|
+
return {
|
|
1520
|
+
name: transition.name,
|
|
1521
|
+
from: transition.from,
|
|
1522
|
+
to: transition.to,
|
|
1523
|
+
...transition.permission === undefined ? {} : { permission: transition.permission },
|
|
1524
|
+
...transition.command === undefined ? {} : { command: transition.command },
|
|
1525
|
+
...transition.route === undefined ? {} : { route: transition.route },
|
|
1526
|
+
...transition.routeBinding === undefined ? {} : { routeBinding: transition.routeBinding },
|
|
1527
|
+
...transition.transaction === undefined ? {} : { transaction: transition.transaction },
|
|
1528
|
+
...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
|
|
1529
|
+
...transition.audit === undefined ? {} : { audit: transition.audit },
|
|
1530
|
+
...transition.job === undefined ? {} : { job: transition.job },
|
|
1531
|
+
...transition.invalidateTags === undefined ? {} : { invalidateTags: [...transition.invalidateTags] },
|
|
1532
|
+
...job === undefined ? {} : {
|
|
1533
|
+
jobDetails: {
|
|
1534
|
+
name: job.name,
|
|
1535
|
+
className: job.className,
|
|
1536
|
+
...job.input === undefined ? {} : { input: job.input },
|
|
1537
|
+
...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
|
|
1538
|
+
...job.output === undefined ? {} : { output: job.output },
|
|
1539
|
+
...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
|
|
1540
|
+
...job.mode === undefined ? {} : { mode: job.mode },
|
|
1541
|
+
...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
|
|
1542
|
+
...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
...transition.persistence === undefined ? {} : { persistence: transition.persistence }
|
|
1546
|
+
};
|
|
1547
|
+
})
|
|
1548
|
+
});
|
|
1322
1549
|
}
|
|
1323
1550
|
for (const controller of module.controllers) {
|
|
1324
1551
|
for (const route of controller.routes) {
|
|
1325
|
-
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
|
|
1326
|
-
|
|
1327
|
-
|
|
1552
|
+
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command) : undefined;
|
|
1553
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
1554
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
1555
|
+
const governedCommand = command ?? featureCommand;
|
|
1556
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
1557
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1558
|
+
if (governedCommand?.permission)
|
|
1559
|
+
permissions.add(governedCommand.permission);
|
|
1328
1560
|
if (route.contract?.evidence)
|
|
1329
1561
|
fixtures.add(route.contract.evidence);
|
|
1330
1562
|
routes.push({
|
|
@@ -1333,8 +1565,10 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1333
1565
|
module: module.name,
|
|
1334
1566
|
controller: controller.className,
|
|
1335
1567
|
handler: route.handler,
|
|
1336
|
-
...
|
|
1337
|
-
...
|
|
1568
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
1569
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
1570
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
1571
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
1338
1572
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
1339
1573
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
1340
1574
|
...route.responses === undefined ? {} : {
|
|
@@ -1349,6 +1583,8 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1349
1583
|
}
|
|
1350
1584
|
commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
1351
1585
|
queries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1586
|
+
jobs.sort((left, right) => left.name.localeCompare(right.name));
|
|
1587
|
+
features.sort((left, right) => left.name.localeCompare(right.name));
|
|
1352
1588
|
routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
|
|
1353
1589
|
rpc.sort((left, right) => left.command.localeCompare(right.command));
|
|
1354
1590
|
events.sort((left, right) => left.name.localeCompare(right.name));
|
|
@@ -1356,6 +1592,8 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1356
1592
|
version: 1,
|
|
1357
1593
|
commands,
|
|
1358
1594
|
queries,
|
|
1595
|
+
jobs,
|
|
1596
|
+
features,
|
|
1359
1597
|
routes,
|
|
1360
1598
|
permissions: [...permissions].sort(),
|
|
1361
1599
|
rpc,
|
|
@@ -1369,7 +1607,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1369
1607
|
}
|
|
1370
1608
|
};
|
|
1371
1609
|
}
|
|
1372
|
-
var init_contract_manifest = () => {
|
|
1610
|
+
var init_contract_manifest = __esm(() => {
|
|
1611
|
+
init_feature();
|
|
1612
|
+
});
|
|
1373
1613
|
|
|
1374
1614
|
// src/generate.ts
|
|
1375
1615
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -1503,6 +1743,7 @@ function renderApplication(graph, options) {
|
|
|
1503
1743
|
name: command.name,
|
|
1504
1744
|
permission: command.permission ?? null,
|
|
1505
1745
|
rpc: command.rpc ?? null,
|
|
1746
|
+
branches: command.branches ?? [],
|
|
1506
1747
|
transaction: command.transaction ?? null,
|
|
1507
1748
|
audit: command.audit ?? null,
|
|
1508
1749
|
idempotency: command.idempotency ?? null
|
|
@@ -1780,6 +2021,9 @@ class ModuleGenerator {
|
|
|
1780
2021
|
return "[]";
|
|
1781
2022
|
const items = this.module.controllers.map((controller) => {
|
|
1782
2023
|
const routes = controller.routes.map((route) => {
|
|
2024
|
+
const command = route.command === undefined ? undefined : this.module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
2025
|
+
const featureTransition = featureTransitionForRoute(this.module, controller, route);
|
|
2026
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1783
2027
|
const responseEntries = normalizedResponseEntries(route.responses);
|
|
1784
2028
|
const hasResponseMap = responseEntries.length > 0;
|
|
1785
2029
|
const fields = [
|
|
@@ -1806,6 +2050,9 @@ class ModuleGenerator {
|
|
|
1806
2050
|
if (route.effect !== undefined) {
|
|
1807
2051
|
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
1808
2052
|
}
|
|
2053
|
+
if (idempotency !== undefined) {
|
|
2054
|
+
fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
|
|
2055
|
+
}
|
|
1809
2056
|
if (route.command)
|
|
1810
2057
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
1811
2058
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -1929,7 +2176,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1929
2176
|
`transaction: ${JSON.stringify(command.transaction)}`,
|
|
1930
2177
|
...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
|
|
1931
2178
|
`idempotency: ${JSON.stringify(command.idempotency)}`,
|
|
2179
|
+
...command.tenantScope ? [`tenantScope: ${JSON.stringify(command.tenantScope)}`] : [],
|
|
1932
2180
|
...command.rpc ? [`rpc: ${JSON.stringify(command.rpc)}`] : [],
|
|
2181
|
+
...command.branches && command.branches.length > 0 ? [`branches: ${JSON.stringify(command.branches)}`] : [],
|
|
1933
2182
|
...command.standalone ? ["standalone: true"] : [],
|
|
1934
2183
|
...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`, `aspectPipeline: ${this.renderAspectPipeline(command.aspects)}`] : []
|
|
1935
2184
|
];
|
|
@@ -1962,6 +2211,8 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1962
2211
|
fields.push(`maxAttempts: ${job.maxAttempts}`);
|
|
1963
2212
|
if (job.idempotency !== undefined)
|
|
1964
2213
|
fields.push(`idempotency: ${JSON.stringify(job.idempotency)}`);
|
|
2214
|
+
if (job.tenantScope !== undefined)
|
|
2215
|
+
fields.push(`tenantScope: ${JSON.stringify(job.tenantScope)}`);
|
|
1965
2216
|
if (job.aspects && job.aspects.length > 0) {
|
|
1966
2217
|
fields.push(`aspects: ${this.renderAspects(job.aspects)}`);
|
|
1967
2218
|
fields.push(`aspectPipeline: ${this.renderAspectPipeline(job.aspects)}`);
|
|
@@ -2284,7 +2535,18 @@ function renderClient(graph, options) {
|
|
|
2284
2535
|
const usedTypeNames = new Set;
|
|
2285
2536
|
const allRoutes = [];
|
|
2286
2537
|
const allProcedures = [];
|
|
2287
|
-
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.
|
|
2538
|
+
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
2539
|
+
[command.className, command],
|
|
2540
|
+
[command.name, command]
|
|
2541
|
+
])));
|
|
2542
|
+
const featureInvalidationTags = new Map;
|
|
2543
|
+
for (const module of graph.modules) {
|
|
2544
|
+
for (const transition of module.featureSpec?.transitions ?? []) {
|
|
2545
|
+
if (transition.route && transition.invalidateTags) {
|
|
2546
|
+
featureInvalidationTags.set(`${module.name}:${transition.route}`, [...transition.invalidateTags]);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2288
2550
|
let usesStatic = false;
|
|
2289
2551
|
let usesValue = false;
|
|
2290
2552
|
const uniqueTypeName = (candidate) => {
|
|
@@ -2362,6 +2624,13 @@ function renderClient(graph, options) {
|
|
|
2362
2624
|
const requestType = `ClientRequestOptions<${paramsType}, ${queryType}, ${bodyType}, ${headersType}, ${cookieType}>${requiredRequestFields.length > 0 ? ` & { ${requiredRequestFields.join("; ")} }` : ""}`;
|
|
2363
2625
|
routeTypes.push(`export type ${requestTypeName} = ${requestType};`);
|
|
2364
2626
|
routeTypes.push(`export type ${responseTypeName} = ${responseType === "never" ? "unknown" : responseType};`);
|
|
2627
|
+
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2628
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
2629
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
|
|
2630
|
+
const governedCommand = command ?? featureCommand;
|
|
2631
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
2632
|
+
const transitionRoute = `${route.method} ${fullPath}`;
|
|
2633
|
+
const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
|
|
2365
2634
|
const routeFields = [
|
|
2366
2635
|
`method: ${JSON.stringify(route.method)}`,
|
|
2367
2636
|
`path: ${JSON.stringify(fullPath)}`,
|
|
@@ -2369,7 +2638,7 @@ function renderClient(graph, options) {
|
|
|
2369
2638
|
`handler: ${JSON.stringify(route.handler)}`
|
|
2370
2639
|
];
|
|
2371
2640
|
for (const [key, value] of [
|
|
2372
|
-
["command", route.command],
|
|
2641
|
+
["command", route.command ?? featureTransition?.command],
|
|
2373
2642
|
["guards", route.guards],
|
|
2374
2643
|
["canMatch", route.canMatch],
|
|
2375
2644
|
["canDeactivate", route.canDeactivate],
|
|
@@ -2383,7 +2652,10 @@ function renderClient(graph, options) {
|
|
|
2383
2652
|
["title", route.title],
|
|
2384
2653
|
["data", route.data],
|
|
2385
2654
|
["parse", route.parse],
|
|
2386
|
-
["allowDeleteBody", route.allowDeleteBody]
|
|
2655
|
+
["allowDeleteBody", route.allowDeleteBody],
|
|
2656
|
+
["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
|
|
2657
|
+
["tags", route.tags],
|
|
2658
|
+
["invalidateTags", invalidationTags]
|
|
2387
2659
|
]) {
|
|
2388
2660
|
if (value !== undefined)
|
|
2389
2661
|
routeFields.push(`${key}: ${JSON.stringify(value)}`);
|
|
@@ -2416,9 +2688,7 @@ function renderClient(graph, options) {
|
|
|
2416
2688
|
const responseSchemaEntries = hasResponseMap ? Object.entries(responseSchemas).map(([status, ref]) => [status, ref.local]) : responseSchema ? [["200", responseSchema.local]] : [];
|
|
2417
2689
|
const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
|
|
2418
2690
|
const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
|
|
2419
|
-
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2420
2691
|
const procedureKind = route.method === "GET" || route.method === "HEAD" ? "query" : "mutation";
|
|
2421
|
-
const idempotencyMode = command?.idempotency ?? "none";
|
|
2422
2692
|
const procedureMetadata = {
|
|
2423
2693
|
key: `${controller.className}.${route.handler}`,
|
|
2424
2694
|
method: route.method,
|
|
@@ -2426,7 +2696,9 @@ function renderClient(graph, options) {
|
|
|
2426
2696
|
controller: controller.className,
|
|
2427
2697
|
handler: route.handler,
|
|
2428
2698
|
kind: procedureKind,
|
|
2429
|
-
idempotency: idempotencyMode
|
|
2699
|
+
idempotency: idempotencyMode,
|
|
2700
|
+
...route.tags === undefined ? {} : { tags: route.tags },
|
|
2701
|
+
...invalidationTags === undefined ? {} : { invalidateTags: invalidationTags }
|
|
2430
2702
|
};
|
|
2431
2703
|
allProcedures.push({
|
|
2432
2704
|
method: route.method,
|
|
@@ -2434,9 +2706,11 @@ function renderClient(graph, options) {
|
|
|
2434
2706
|
controller: controller.className,
|
|
2435
2707
|
handler: route.handler,
|
|
2436
2708
|
kind: procedureKind,
|
|
2437
|
-
...
|
|
2438
|
-
...
|
|
2439
|
-
idempotency: idempotencyMode
|
|
2709
|
+
...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
|
|
2710
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
2711
|
+
idempotency: idempotencyMode,
|
|
2712
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
2713
|
+
...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
|
|
2440
2714
|
});
|
|
2441
2715
|
routeMethods.push(`
|
|
2442
2716
|
${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)}),`);
|
|
@@ -2490,6 +2764,8 @@ ${procedureMethods.join(`
|
|
|
2490
2764
|
" handler: string;",
|
|
2491
2765
|
" kind: Kind;",
|
|
2492
2766
|
" idempotency: Idempotency;",
|
|
2767
|
+
" tags?: readonly string[];",
|
|
2768
|
+
" invalidateTags?: readonly string[];",
|
|
2493
2769
|
"}",
|
|
2494
2770
|
"",
|
|
2495
2771
|
"export interface ProcedureExecutionOptions {",
|
|
@@ -2595,6 +2871,8 @@ ${procedureMethods.join(`
|
|
|
2595
2871
|
" command?: string;",
|
|
2596
2872
|
" permission?: string;",
|
|
2597
2873
|
" idempotency: IdempotencyMode;",
|
|
2874
|
+
" tags?: readonly string[];",
|
|
2875
|
+
" invalidateTags?: readonly string[];",
|
|
2598
2876
|
"}",
|
|
2599
2877
|
"",
|
|
2600
2878
|
...routeTypes,
|
|
@@ -3094,7 +3372,10 @@ function renderOpenApi(graph, options) {
|
|
|
3094
3372
|
}
|
|
3095
3373
|
return schemaComponents.get(key);
|
|
3096
3374
|
};
|
|
3097
|
-
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.
|
|
3375
|
+
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
3376
|
+
[command.className, command],
|
|
3377
|
+
[command.name, command]
|
|
3378
|
+
])));
|
|
3098
3379
|
for (const module of graph.modules) {
|
|
3099
3380
|
for (const controller of module.controllers) {
|
|
3100
3381
|
for (const route of controller.routes) {
|
|
@@ -3116,7 +3397,12 @@ function renderOpenApi(graph, options) {
|
|
|
3116
3397
|
const operationBase = pascalName(`${module.name}_${controller.className}_${route.handler}`);
|
|
3117
3398
|
const operationId = uniqueName(operationBase, operationIds);
|
|
3118
3399
|
const command = route.command ? commandByClass.get(route.command) : undefined;
|
|
3119
|
-
const
|
|
3400
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3401
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
|
|
3402
|
+
const governedCommand = command ?? featureCommand;
|
|
3403
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
3404
|
+
const permission = governedCommand?.permission ?? featureTransition?.permission;
|
|
3405
|
+
const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
|
|
3120
3406
|
const responseKind = route.contract?.response;
|
|
3121
3407
|
const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
|
|
3122
3408
|
const definition = {
|
|
@@ -3141,11 +3427,12 @@ function renderOpenApi(graph, options) {
|
|
|
3141
3427
|
responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
|
|
3142
3428
|
...route.parse === undefined ? {} : { parse: route.parse },
|
|
3143
3429
|
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
|
|
3430
|
+
...idempotencyMode === "none" ? {} : { idempotency: idempotencyMode },
|
|
3144
3431
|
module: module.name,
|
|
3145
3432
|
controller: controller.className,
|
|
3146
3433
|
handler: route.handler,
|
|
3147
|
-
...route.command === undefined ? {} : { command: route.command },
|
|
3148
|
-
...
|
|
3434
|
+
...route.command === undefined && featureTransition?.command === undefined ? {} : { command: route.command ?? featureTransition?.command },
|
|
3435
|
+
...permission === undefined ? {} : { permission },
|
|
3149
3436
|
...route.guards && route.guards.length > 0 ? { guards: route.guards } : {},
|
|
3150
3437
|
...route.canMatch && route.canMatch.length > 0 ? { canMatch: route.canMatch } : {},
|
|
3151
3438
|
...route.canDeactivate && route.canDeactivate.length > 0 ? { canDeactivate: route.canDeactivate } : {},
|
|
@@ -3220,6 +3507,7 @@ function renderOpenApi(graph, options) {
|
|
|
3220
3507
|
" handler: string;",
|
|
3221
3508
|
" command?: string;",
|
|
3222
3509
|
" permission?: string;",
|
|
3510
|
+
' idempotency?: "required" | "none";',
|
|
3223
3511
|
" guards?: readonly string[];",
|
|
3224
3512
|
" canMatch?: readonly string[];",
|
|
3225
3513
|
" canDeactivate?: readonly string[];",
|
|
@@ -3472,6 +3760,18 @@ function renderOpenApi(graph, options) {
|
|
|
3472
3760
|
" for (const name of headersSchema ? schemaPropertyNames(headersSchema) : []) parameters.push({",
|
|
3473
3761
|
' name, in: "header", required: schemaRequired(headersSchema, name), schema: schemaProperty(headersSchema, name),',
|
|
3474
3762
|
" });",
|
|
3763
|
+
' if (definition.idempotency === "required") {',
|
|
3764
|
+
' const keySchema = { type: "string", minLength: 1, maxLength: 512, pattern: "^[A-Za-z0-9._:-]{1,512}$" };',
|
|
3765
|
+
' const declaredHeaders = parameters.filter((parameter) => parameter["in"] === "header" && typeof parameter["name"] === "string" && parameter["name"].toLowerCase() === "idempotency-key");',
|
|
3766
|
+
" const header = declaredHeaders[0];",
|
|
3767
|
+
" if (header) {",
|
|
3768
|
+
' header["required"] = true;',
|
|
3769
|
+
' header["schema"] = { allOf: [...declaredHeaders.map((parameter) => parameter["schema"]), keySchema] };',
|
|
3770
|
+
" for (const duplicate of declaredHeaders.slice(1)) parameters.splice(parameters.indexOf(duplicate), 1);",
|
|
3771
|
+
" } else {",
|
|
3772
|
+
' parameters.push({ name: "Idempotency-Key", in: "header", required: true, schema: keySchema });',
|
|
3773
|
+
" }",
|
|
3774
|
+
" }",
|
|
3475
3775
|
" const cookieSchema = definition.cookieSchema ? OPENAPI_SCHEMA_REGISTRY[definition.cookieSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
3476
3776
|
" for (const name of cookieSchema ? schemaPropertyNames(cookieSchema) : []) parameters.push({",
|
|
3477
3777
|
' name, in: "cookie", required: schemaRequired(cookieSchema, name), schema: schemaProperty(cookieSchema, name),',
|
|
@@ -3494,6 +3794,7 @@ function renderOpenApi(graph, options) {
|
|
|
3494
3794
|
' responses["500"] = { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/SupaCloudError" } } } };',
|
|
3495
3795
|
' responses["200"] = { description: "Successful response" };',
|
|
3496
3796
|
" }",
|
|
3797
|
+
' 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" } } } };',
|
|
3497
3798
|
" const operation: Record<string, unknown> = {",
|
|
3498
3799
|
" operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
|
|
3499
3800
|
" ...(parameters.length === 0 ? {} : { parameters }),",
|
|
@@ -3504,6 +3805,7 @@ function renderOpenApi(graph, options) {
|
|
|
3504
3805
|
" module: definition.module, controller: definition.controller, handler: definition.handler,",
|
|
3505
3806
|
" ...(definition.command ? { command: definition.command } : {}),",
|
|
3506
3807
|
" ...(definition.permission ? { permission: definition.permission } : {}),",
|
|
3808
|
+
" ...(definition.idempotency ? { idempotency: definition.idempotency } : {}),",
|
|
3507
3809
|
" ...(definition.guards ? { guards: [...definition.guards] } : {}),",
|
|
3508
3810
|
" ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
|
|
3509
3811
|
" ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
|
|
@@ -3554,11 +3856,13 @@ function renderPermissions(graph) {
|
|
|
3554
3856
|
}
|
|
3555
3857
|
for (const controller of module.controllers) {
|
|
3556
3858
|
for (const route of controller.routes) {
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3859
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3860
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
3861
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
3862
|
+
const governedCommand = command ?? featureCommand;
|
|
3863
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
3864
|
+
const perm = governedCommand?.permission ?? featureTransition?.permission;
|
|
3865
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
3562
3866
|
if (perm)
|
|
3563
3867
|
permissions.add(perm);
|
|
3564
3868
|
bindings.push({
|
|
@@ -3566,8 +3870,9 @@ function renderPermissions(graph) {
|
|
|
3566
3870
|
path: joinRoutePaths(controller.path, route.path),
|
|
3567
3871
|
controller: controller.className,
|
|
3568
3872
|
handler: route.handler,
|
|
3569
|
-
...
|
|
3570
|
-
...perm === undefined ? {} : { permission: perm }
|
|
3873
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
3874
|
+
...perm === undefined ? {} : { permission: perm },
|
|
3875
|
+
...idempotency === undefined ? {} : { idempotency }
|
|
3571
3876
|
});
|
|
3572
3877
|
}
|
|
3573
3878
|
}
|
|
@@ -3593,6 +3898,7 @@ function renderPermissions(graph) {
|
|
|
3593
3898
|
" handler: string;",
|
|
3594
3899
|
" command?: string;",
|
|
3595
3900
|
" permission?: string;",
|
|
3901
|
+
' idempotency?: "required" | "none";',
|
|
3596
3902
|
"}",
|
|
3597
3903
|
"",
|
|
3598
3904
|
"export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
|
|
@@ -3636,6 +3942,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3636
3942
|
/** True when the handler returns a native Response rather than a framework value. */
|
|
3637
3943
|
nativeResponse?: boolean;
|
|
3638
3944
|
command?: string;
|
|
3945
|
+
/** Transport-level idempotency contract, independent of route command binding. */
|
|
3946
|
+
idempotency?: "required" | "none";
|
|
3639
3947
|
guards?: string[];
|
|
3640
3948
|
canMatch?: string[];
|
|
3641
3949
|
canDeactivate?: string[];
|
|
@@ -3665,17 +3973,28 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3665
3973
|
|
|
3666
3974
|
export interface CompiledCommand {
|
|
3667
3975
|
rpc?: string;
|
|
3976
|
+
branches?: readonly CompiledCommandBranch[];
|
|
3668
3977
|
className: string;
|
|
3669
3978
|
name: string;
|
|
3670
3979
|
permission: string;
|
|
3671
3980
|
transaction: "required" | "none";
|
|
3672
3981
|
audit?: string;
|
|
3673
3982
|
idempotency: "required" | "none";
|
|
3983
|
+
tenantScope?: "required" | "none";
|
|
3674
3984
|
standalone?: boolean;
|
|
3675
3985
|
aspects?: CompiledAspect[];
|
|
3676
3986
|
aspectPipeline?: CompiledAspectPipeline;
|
|
3677
3987
|
}
|
|
3678
3988
|
|
|
3989
|
+
export interface CompiledCommandBranch {
|
|
3990
|
+
when: string;
|
|
3991
|
+
rpc: string;
|
|
3992
|
+
transaction?: "required" | "none" | string;
|
|
3993
|
+
audit?: string;
|
|
3994
|
+
idempotency?: "required" | "none" | string;
|
|
3995
|
+
boundary?: "database" | "external";
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3679
3998
|
export interface CompiledJob {
|
|
3680
3999
|
className: string;
|
|
3681
4000
|
name: string;
|
|
@@ -3687,6 +4006,7 @@ export interface CompiledJob {
|
|
|
3687
4006
|
timeoutSec?: number;
|
|
3688
4007
|
maxAttempts?: number;
|
|
3689
4008
|
idempotency?: "required" | "none";
|
|
4009
|
+
tenantScope?: "required" | "none";
|
|
3690
4010
|
aspects?: CompiledAspect[];
|
|
3691
4011
|
aspectPipeline?: CompiledAspectPipeline;
|
|
3692
4012
|
}
|
|
@@ -3861,6 +4181,7 @@ async function destroyServiceInstances(
|
|
|
3861
4181
|
`;
|
|
3862
4182
|
var init_generate = __esm(() => {
|
|
3863
4183
|
init_contract_manifest();
|
|
4184
|
+
init_feature();
|
|
3864
4185
|
});
|
|
3865
4186
|
|
|
3866
4187
|
// src/route-contracts.ts
|
|
@@ -14583,7 +14904,7 @@ function parseDeliveryPlanResult(value) {
|
|
|
14583
14904
|
throw new Error("Invalid delivery plan result.");
|
|
14584
14905
|
return value;
|
|
14585
14906
|
}
|
|
14586
|
-
var objectOptions, reference, references, kind, isolation, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
14907
|
+
var objectOptions, reference, references, kind, isolation, mode, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
14587
14908
|
var init_delivery_schema = __esm(() => {
|
|
14588
14909
|
init_build();
|
|
14589
14910
|
init_value2();
|
|
@@ -14593,6 +14914,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
14593
14914
|
references = _Array_(reference, { uniqueItems: true });
|
|
14594
14915
|
kind = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
|
|
14595
14916
|
isolation = Union([Literal("shared"), Literal("process")]);
|
|
14917
|
+
mode = Union([Literal("required"), Literal("none")]);
|
|
14596
14918
|
DeliveryFilePathSchema = String2({
|
|
14597
14919
|
maxLength: 512,
|
|
14598
14920
|
pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
|
|
@@ -14666,6 +14988,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
14666
14988
|
]),
|
|
14667
14989
|
path: String2(),
|
|
14668
14990
|
command: Optional(String2()),
|
|
14991
|
+
idempotency: Optional(mode),
|
|
14669
14992
|
reason: Union([Literal("default-http"), Literal("explicit-module")])
|
|
14670
14993
|
}, objectOptions);
|
|
14671
14994
|
jobSchema = _Object_({
|
|
@@ -14924,7 +15247,12 @@ function parseDeliveryBuildManifest(value) {
|
|
|
14924
15247
|
throw new Error("Invalid delivery object inventory.");
|
|
14925
15248
|
}
|
|
14926
15249
|
}
|
|
14927
|
-
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
15250
|
+
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
15251
|
+
method: route.method,
|
|
15252
|
+
path: route.path,
|
|
15253
|
+
target: target.name,
|
|
15254
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
15255
|
+
})));
|
|
14928
15256
|
const jobs = value.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })));
|
|
14929
15257
|
if (canonical2(value.routes) !== canonical2(routes) || canonical2(value.jobs) !== canonical2(jobs)) {
|
|
14930
15258
|
throw new Error("Delivery mappings do not match planned ownership.");
|
|
@@ -14975,7 +15303,8 @@ var init_delivery_build_schema = __esm(() => {
|
|
|
14975
15303
|
routes: _Array_(_Object_({
|
|
14976
15304
|
method: String2(),
|
|
14977
15305
|
path: String2(),
|
|
14978
|
-
target: String2()
|
|
15306
|
+
target: String2(),
|
|
15307
|
+
idempotency: Optional(Union([Literal("required"), Literal("none")]))
|
|
14979
15308
|
}, closed2)),
|
|
14980
15309
|
jobs: _Array_(_Object_({ name: String2(), target: String2() }, closed2))
|
|
14981
15310
|
}, closed2);
|
|
@@ -15194,7 +15523,9 @@ function createExecutionPlans(graph) {
|
|
|
15194
15523
|
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
15195
15524
|
return graph.modules.flatMap((module) => [
|
|
15196
15525
|
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
15197
|
-
const
|
|
15526
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
15527
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
15528
|
+
const command = module.commands.find((item) => item.className === commandRef || item.name === commandRef);
|
|
15198
15529
|
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
15199
15530
|
return {
|
|
15200
15531
|
module: module.name,
|
|
@@ -15344,6 +15675,7 @@ function exportGraphDot(graph) {
|
|
|
15344
15675
|
}
|
|
15345
15676
|
var init_inspect = __esm(() => {
|
|
15346
15677
|
init_route_contracts();
|
|
15678
|
+
init_feature();
|
|
15347
15679
|
});
|
|
15348
15680
|
|
|
15349
15681
|
// src/repair-plan.ts
|
|
@@ -15597,7 +15929,7 @@ function parseDevelopmentContext(input) {
|
|
|
15597
15929
|
throw invalid();
|
|
15598
15930
|
}
|
|
15599
15931
|
}
|
|
15600
|
-
var APPLICATION_DEVELOPMENT_SCHEMA = "supacloud.application-development.v1", APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES = 524288, APPLICATION_DEVELOPMENT_LIMITS, DevelopmentContractError, closed4, text2, texts, count,
|
|
15932
|
+
var APPLICATION_DEVELOPMENT_SCHEMA = "supacloud.application-development.v1", APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES = 524288, APPLICATION_DEVELOPMENT_LIMITS, DevelopmentContractError, closed4, text2, texts, count, mode2, operations2, resourceUse, schemaKind, stage, ContextSchema;
|
|
15601
15933
|
var init_application_development = __esm(() => {
|
|
15602
15934
|
init_build();
|
|
15603
15935
|
init_value2();
|
|
@@ -15625,7 +15957,7 @@ var init_application_development = __esm(() => {
|
|
|
15625
15957
|
text2 = String2({ minLength: 1, maxLength: 512, pattern: "^[^\\u0000-\\u001f\\u007f]+$" });
|
|
15626
15958
|
texts = _Array_(text2);
|
|
15627
15959
|
count = Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
15628
|
-
|
|
15960
|
+
mode2 = Union([Literal("required"), Literal("none")]);
|
|
15629
15961
|
operations2 = _Array_(Union([Literal("read"), Literal("write"), Literal("publish"), Literal("consume")]));
|
|
15630
15962
|
resourceUse = _Object_({ resource: text2, operations: operations2 }, closed4);
|
|
15631
15963
|
schemaKind = Optional(Union([Literal("opaque"), Literal("declared")]));
|
|
@@ -15660,6 +15992,7 @@ var init_application_development = __esm(() => {
|
|
|
15660
15992
|
controller: text2,
|
|
15661
15993
|
handler: text2,
|
|
15662
15994
|
command: Optional(text2),
|
|
15995
|
+
idempotency: Optional(mode2),
|
|
15663
15996
|
aspects: texts,
|
|
15664
15997
|
schemaKinds: Optional(_Object_({
|
|
15665
15998
|
body: schemaKind,
|
|
@@ -15674,8 +16007,8 @@ var init_application_development = __esm(() => {
|
|
|
15674
16007
|
module: text2,
|
|
15675
16008
|
name: text2,
|
|
15676
16009
|
permission: Optional(text2),
|
|
15677
|
-
transaction:
|
|
15678
|
-
idempotency:
|
|
16010
|
+
transaction: mode2,
|
|
16011
|
+
idempotency: mode2,
|
|
15679
16012
|
audit: Optional(text2),
|
|
15680
16013
|
resources: _Array_(resourceUse)
|
|
15681
16014
|
}, closed4), { maxItems: APPLICATION_DEVELOPMENT_LIMITS.commands }),
|
|
@@ -15782,7 +16115,11 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
15782
16115
|
for (const controller of module.controllers) {
|
|
15783
16116
|
for (const route of controller.routes) {
|
|
15784
16117
|
const candidates = module.commands.filter((command) => command.className === route.command || command.name === route.command);
|
|
15785
|
-
const
|
|
16118
|
+
const commandNode = candidates.length === 1 ? candidates[0] : undefined;
|
|
16119
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
16120
|
+
const featureCommandNode = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
16121
|
+
const command = commandNode?.name ?? featureCommandNode?.name;
|
|
16122
|
+
const idempotency = route.idempotency ?? commandNode?.idempotency ?? featureTransition?.idempotency;
|
|
15786
16123
|
const kinds = schemaKinds(route);
|
|
15787
16124
|
routes.push({
|
|
15788
16125
|
module: module.name,
|
|
@@ -15791,6 +16128,7 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
15791
16128
|
controller: controller.className,
|
|
15792
16129
|
handler: route.handler,
|
|
15793
16130
|
...command === undefined ? {} : { command },
|
|
16131
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
15794
16132
|
aspects: (route.aspects ?? []).map((aspect) => aspect.name),
|
|
15795
16133
|
...kinds === undefined ? {} : { schemaKinds: kinds }
|
|
15796
16134
|
});
|
|
@@ -15911,6 +16249,7 @@ var init_application_development2 = __esm(() => {
|
|
|
15911
16249
|
init_application_development();
|
|
15912
16250
|
init_inspect();
|
|
15913
16251
|
init_execution_snapshot();
|
|
16252
|
+
init_feature();
|
|
15914
16253
|
APPLICATION_DEVELOPMENT_LIMITS2 = APPLICATION_DEVELOPMENT_LIMITS;
|
|
15915
16254
|
ApplicationDevelopmentError = class ApplicationDevelopmentError extends Error {
|
|
15916
16255
|
code;
|
|
@@ -15943,7 +16282,7 @@ async function readDeliverySelection(manifestPath, target) {
|
|
|
15943
16282
|
return { root, object, planned };
|
|
15944
16283
|
}
|
|
15945
16284
|
async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
15946
|
-
if (object.files.length > 1024 || object.files.some((file) => file.bytes >
|
|
16285
|
+
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) {
|
|
15947
16286
|
throw new Error("Artifact inventory exceeds limits.");
|
|
15948
16287
|
}
|
|
15949
16288
|
const objectRoot = join8(root, "objects", object.objectId);
|
|
@@ -15982,10 +16321,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
|
15982
16321
|
}
|
|
15983
16322
|
return retained;
|
|
15984
16323
|
}
|
|
15985
|
-
var JSON_LIMIT = 1048576;
|
|
16324
|
+
var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
|
|
15986
16325
|
var init_delivery_artifact = __esm(() => {
|
|
15987
16326
|
init_delivery_build_schema();
|
|
15988
16327
|
init_delivery_files();
|
|
16328
|
+
DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
|
|
16329
|
+
DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
|
|
15989
16330
|
});
|
|
15990
16331
|
|
|
15991
16332
|
// node_modules/@supacloud/delivery/src/application-release.ts
|
|
@@ -16844,6 +17185,7 @@ var init_delivery_migration_plan = () => {};
|
|
|
16844
17185
|
|
|
16845
17186
|
// node_modules/@supacloud/delivery/src/index.ts
|
|
16846
17187
|
var init_src = __esm(() => {
|
|
17188
|
+
init_delivery_artifact();
|
|
16847
17189
|
init_delivery_artifact();
|
|
16848
17190
|
init_worker_execution();
|
|
16849
17191
|
init_worker_execution();
|
|
@@ -18766,6 +19108,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18766
19108
|
if (meta && booleanProp(meta, "standalone")) {
|
|
18767
19109
|
const permission = stringLiteralProp(meta, "permission");
|
|
18768
19110
|
const rpc = checkedRpc(meta, ctx);
|
|
19111
|
+
const branches = checkedBranches(meta, ctx);
|
|
18769
19112
|
const audit = stringLiteralProp(meta, "audit");
|
|
18770
19113
|
const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
|
|
18771
19114
|
const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
|
|
@@ -18775,6 +19118,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18775
19118
|
name: standaloneName,
|
|
18776
19119
|
...permission === undefined ? {} : { permission },
|
|
18777
19120
|
...rpc === undefined ? {} : { rpc },
|
|
19121
|
+
...branches === undefined ? {} : { branches },
|
|
18778
19122
|
transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
18779
19123
|
...audit === undefined ? {} : { audit },
|
|
18780
19124
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
@@ -19106,18 +19450,22 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19106
19450
|
const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
|
|
19107
19451
|
const permission = stringLiteralProp(meta, "permission");
|
|
19108
19452
|
const rpc = checkedRpc(meta, ctx);
|
|
19453
|
+
const branches = checkedBranches(meta, ctx);
|
|
19109
19454
|
const audit = stringLiteralProp(meta, "audit");
|
|
19110
19455
|
const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
|
|
19111
19456
|
const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19112
19457
|
rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19458
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19113
19459
|
commands.push({
|
|
19114
19460
|
className: cls.name?.text ?? "<anonymous>",
|
|
19115
19461
|
name: commandName,
|
|
19116
19462
|
...permission === undefined ? {} : { permission },
|
|
19117
19463
|
...rpc === undefined ? {} : { rpc },
|
|
19464
|
+
...branches === undefined ? {} : { branches },
|
|
19118
19465
|
transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19119
19466
|
...audit === undefined ? {} : { audit },
|
|
19120
19467
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19468
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19121
19469
|
...booleanProp(meta, "standalone") ? { standalone: true } : {},
|
|
19122
19470
|
...uses.length > 0 ? { uses } : {},
|
|
19123
19471
|
...aspects.length > 0 ? { aspects } : {}
|
|
@@ -19148,12 +19496,14 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19148
19496
|
const contract = parseJobOptions(meta, className, ctx);
|
|
19149
19497
|
const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19150
19498
|
rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19499
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19151
19500
|
jobs.push({
|
|
19152
19501
|
className,
|
|
19153
19502
|
name: stringLiteralProp(meta, "name") ?? className,
|
|
19154
19503
|
serviceKey: camelName(provider?.token ?? className),
|
|
19155
19504
|
scope,
|
|
19156
19505
|
...contract,
|
|
19506
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19157
19507
|
...jobUses.length > 0 ? { uses: jobUses } : {},
|
|
19158
19508
|
...aspects.length > 0 ? { aspects } : {}
|
|
19159
19509
|
});
|
|
@@ -19163,9 +19513,11 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19163
19513
|
if (queryDec) {
|
|
19164
19514
|
const meta = decoratorObjectArg(queryDec);
|
|
19165
19515
|
if (meta) {
|
|
19516
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19166
19517
|
queries.push({
|
|
19167
19518
|
className: cls.name?.text ?? "<anonymous>",
|
|
19168
|
-
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
|
|
19519
|
+
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
19520
|
+
...tenantScope === undefined ? {} : { tenantScope }
|
|
19169
19521
|
});
|
|
19170
19522
|
}
|
|
19171
19523
|
}
|
|
@@ -19220,6 +19572,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19220
19572
|
if (!ts7.isObjectLiteralExpression(input))
|
|
19221
19573
|
return invalid();
|
|
19222
19574
|
const name = stringLiteralProp(input, "name");
|
|
19575
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19223
19576
|
const statesExpr = getProp(input, "states");
|
|
19224
19577
|
const transitionObject = getProp(input, "transitions");
|
|
19225
19578
|
if (!name || !statesExpr || !ts7.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts7.isStringLiteral(state)) || !transitionObject || !ts7.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts7.isPropertyAssignment(property))) {
|
|
@@ -19238,15 +19591,27 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19238
19591
|
const options = property.initializer;
|
|
19239
19592
|
const from = stringLiteralProp(options, "from");
|
|
19240
19593
|
const to = stringLiteralProp(options, "to");
|
|
19241
|
-
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))) {
|
|
19594
|
+
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))) {
|
|
19242
19595
|
return invalid();
|
|
19243
19596
|
}
|
|
19244
19597
|
const permission = stringLiteralProp(options, "permission");
|
|
19245
19598
|
const command = stringLiteralProp(options, "command");
|
|
19246
19599
|
const route = stringLiteralProp(options, "route");
|
|
19600
|
+
const routeBinding = stringLiteralProp(options, "routeBinding");
|
|
19601
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
19602
|
+
return invalid();
|
|
19247
19603
|
const transaction = commandModeProp(options, "transaction");
|
|
19248
19604
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19249
19605
|
const audit = stringLiteralProp(options, "audit");
|
|
19606
|
+
const job = stringLiteralProp(options, "job");
|
|
19607
|
+
const invalidateTagsExpr = getProp(options, "invalidateTags");
|
|
19608
|
+
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;
|
|
19609
|
+
if (invalidateTagsExpr !== undefined && invalidateTags === undefined)
|
|
19610
|
+
return invalid();
|
|
19611
|
+
const persistenceExpr = getProp(options, "persistence");
|
|
19612
|
+
const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
|
|
19613
|
+
if (persistenceExpr && !persistence)
|
|
19614
|
+
return invalid();
|
|
19250
19615
|
transitions.push({
|
|
19251
19616
|
name: propertyName(property.name),
|
|
19252
19617
|
from,
|
|
@@ -19254,12 +19619,45 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19254
19619
|
...permission === undefined ? {} : { permission },
|
|
19255
19620
|
...command === undefined ? {} : { command },
|
|
19256
19621
|
...route === undefined ? {} : { route },
|
|
19622
|
+
...routeBinding === undefined ? {} : { routeBinding },
|
|
19257
19623
|
...transaction === undefined ? {} : { transaction },
|
|
19258
19624
|
...idempotency === undefined ? {} : { idempotency },
|
|
19259
|
-
...audit === undefined ? {} : { audit }
|
|
19625
|
+
...audit === undefined ? {} : { audit },
|
|
19626
|
+
...job === undefined ? {} : { job },
|
|
19627
|
+
...invalidateTags === undefined ? {} : { invalidateTags },
|
|
19628
|
+
...persistence === undefined ? {} : { persistence }
|
|
19260
19629
|
});
|
|
19261
19630
|
}
|
|
19262
|
-
return {
|
|
19631
|
+
return {
|
|
19632
|
+
name,
|
|
19633
|
+
states,
|
|
19634
|
+
...tenantField === undefined ? {} : { tenantField },
|
|
19635
|
+
transitions,
|
|
19636
|
+
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
19637
|
+
line: lineOf(input)
|
|
19638
|
+
};
|
|
19639
|
+
}
|
|
19640
|
+
function parseFeaturePersistence(input) {
|
|
19641
|
+
if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
|
|
19642
|
+
return;
|
|
19643
|
+
const boundary = stringLiteralProp(input, "boundary");
|
|
19644
|
+
const optimisticLock = commandModeProp(input, "optimisticLock");
|
|
19645
|
+
const versionField = stringLiteralProp(input, "versionField");
|
|
19646
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19647
|
+
const stateField = stringLiteralProp(input, "stateField");
|
|
19648
|
+
const receipt = commandModeProp(input, "receipt");
|
|
19649
|
+
const reconciliation = commandModeProp(input, "reconciliation");
|
|
19650
|
+
if (boundary !== "database" && boundary !== "external" || optimisticLock !== "required" && optimisticLock !== "none" || receipt !== "required" && receipt !== "none" || reconciliation !== "required" && reconciliation !== "none")
|
|
19651
|
+
return;
|
|
19652
|
+
return {
|
|
19653
|
+
boundary,
|
|
19654
|
+
optimisticLock,
|
|
19655
|
+
...versionField === undefined ? {} : { versionField },
|
|
19656
|
+
...tenantField === undefined ? {} : { tenantField },
|
|
19657
|
+
...stateField === undefined ? {} : { stateField },
|
|
19658
|
+
receipt,
|
|
19659
|
+
reconciliation
|
|
19660
|
+
};
|
|
19263
19661
|
}
|
|
19264
19662
|
function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
|
|
19265
19663
|
if (!input || seen.has(input))
|
|
@@ -19329,6 +19727,54 @@ function checkedCommandMode(object, property, ctx, command) {
|
|
|
19329
19727
|
}
|
|
19330
19728
|
return value;
|
|
19331
19729
|
}
|
|
19730
|
+
function checkedRouteIdempotency(object, ctx, route) {
|
|
19731
|
+
const expression = getProp(object, "idempotency");
|
|
19732
|
+
const value = stringLiteralProp(object, "idempotency");
|
|
19733
|
+
if (expression && value !== "required" && value !== "none") {
|
|
19734
|
+
const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
|
|
19735
|
+
ctx.diagnostics.push({
|
|
19736
|
+
severity: "error",
|
|
19737
|
+
code: "invalid-route-idempotency",
|
|
19738
|
+
errorCode: "SC3028",
|
|
19739
|
+
docsUrl: "https://supacloud.dev/errors/SC3028",
|
|
19740
|
+
message: `${route}.idempotency must be the explicit literal "required" or "none".`,
|
|
19741
|
+
file,
|
|
19742
|
+
line: lineOf(expression),
|
|
19743
|
+
suggestion: `Choose the intended idempotency policy explicitly; use "required" for replayable mutations or "none" for routes without that contract.`
|
|
19744
|
+
});
|
|
19745
|
+
}
|
|
19746
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
19747
|
+
}
|
|
19748
|
+
function checkedRouteTags(expression, ctx, route) {
|
|
19749
|
+
if (expression === undefined)
|
|
19750
|
+
return;
|
|
19751
|
+
if (!ts7.isArrayLiteralExpression(expression) || expression.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
19752
|
+
ctx.diagnostics.push({
|
|
19753
|
+
severity: "error",
|
|
19754
|
+
code: "invalid-route-tags",
|
|
19755
|
+
errorCode: "SC3029",
|
|
19756
|
+
docsUrl: "https://supacloud.dev/errors/SC3029",
|
|
19757
|
+
message: `${route}.tags must be a static array of non-empty string literals.`,
|
|
19758
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19759
|
+
line: lineOf(expression),
|
|
19760
|
+
suggestion: 'Declare stable cache tags explicitly, for example tags: ["cases.list"].'
|
|
19761
|
+
});
|
|
19762
|
+
return;
|
|
19763
|
+
}
|
|
19764
|
+
const tags = expression.elements.map((element) => element.text);
|
|
19765
|
+
if (new Set(tags).size !== tags.length) {
|
|
19766
|
+
ctx.diagnostics.push({
|
|
19767
|
+
severity: "error",
|
|
19768
|
+
code: "duplicate-route-tags",
|
|
19769
|
+
errorCode: "SC3030",
|
|
19770
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
19771
|
+
message: `${route}.tags must not contain duplicate values.`,
|
|
19772
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19773
|
+
line: lineOf(expression)
|
|
19774
|
+
});
|
|
19775
|
+
}
|
|
19776
|
+
return tags;
|
|
19777
|
+
}
|
|
19332
19778
|
function parseProvider(el, exportsSet, ctx) {
|
|
19333
19779
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
|
|
19334
19780
|
const line = lineOf(el);
|
|
@@ -20108,7 +20554,11 @@ function parseController(input, ctx) {
|
|
|
20108
20554
|
if (optionsObject) {
|
|
20109
20555
|
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
20110
20556
|
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
20111
|
-
|
|
20557
|
+
const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
|
|
20558
|
+
if (idempotency !== undefined)
|
|
20559
|
+
route.idempotency = idempotency;
|
|
20560
|
+
const tagsPolicy = routePolicyProperty(optionsObject, "tags", ctx);
|
|
20561
|
+
if (parsePolicy.unresolved || deletePolicy.unresolved || tagsPolicy.unresolved) {
|
|
20112
20562
|
ctx.diagnostics.push({
|
|
20113
20563
|
severity: "error",
|
|
20114
20564
|
code: "unresolved-route-body-policy",
|
|
@@ -20116,6 +20566,9 @@ function parseController(input, ctx) {
|
|
|
20116
20566
|
message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
|
|
20117
20567
|
});
|
|
20118
20568
|
}
|
|
20569
|
+
const tags = checkedRouteTags(tagsPolicy.value, ctx, route.handler);
|
|
20570
|
+
if (tags !== undefined)
|
|
20571
|
+
route.tags = tags;
|
|
20119
20572
|
const parse = parsePolicy.value;
|
|
20120
20573
|
if (parse) {
|
|
20121
20574
|
if (ts7.isStringLiteral(parse) && parse.text === "none")
|
|
@@ -20686,6 +21139,75 @@ function checkedRpc(meta, ctx) {
|
|
|
20686
21139
|
});
|
|
20687
21140
|
return;
|
|
20688
21141
|
}
|
|
21142
|
+
function checkedBranches(meta, ctx) {
|
|
21143
|
+
const expression = getProp(meta, "branches");
|
|
21144
|
+
if (!expression)
|
|
21145
|
+
return;
|
|
21146
|
+
if (!ts7.isArrayLiteralExpression(expression)) {
|
|
21147
|
+
ctx.diagnostics.push({
|
|
21148
|
+
severity: "error",
|
|
21149
|
+
code: "invalid-command-branches",
|
|
21150
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
21151
|
+
line: lineOf(expression),
|
|
21152
|
+
message: "Command branches must be an explicit object array.",
|
|
21153
|
+
suggestion: "Declare each branch as { when, rpc, transaction, audit, idempotency, boundary }."
|
|
21154
|
+
});
|
|
21155
|
+
return;
|
|
21156
|
+
}
|
|
21157
|
+
const branches = [];
|
|
21158
|
+
for (const item of expression.elements) {
|
|
21159
|
+
if (!ts7.isObjectLiteralExpression(item)) {
|
|
21160
|
+
ctx.diagnostics.push({
|
|
21161
|
+
severity: "error",
|
|
21162
|
+
code: "invalid-command-branches",
|
|
21163
|
+
file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
|
|
21164
|
+
line: lineOf(item),
|
|
21165
|
+
message: "Command branches entries must be object literals."
|
|
21166
|
+
});
|
|
21167
|
+
continue;
|
|
21168
|
+
}
|
|
21169
|
+
const when = stringLiteralProp(item, "when");
|
|
21170
|
+
const rpc = stringLiteralProp(item, "rpc");
|
|
21171
|
+
const transaction = checkedCommandMode(item, "transaction", ctx, "command branch") ?? "none";
|
|
21172
|
+
const audit = stringLiteralProp(item, "audit");
|
|
21173
|
+
const idempotency = checkedCommandMode(item, "idempotency", ctx, "command branch") ?? "none";
|
|
21174
|
+
const boundary = stringLiteralProp(item, "boundary");
|
|
21175
|
+
const validBoundary = boundary === undefined || boundary === "database" || boundary === "external";
|
|
21176
|
+
const validWhen = when !== undefined && /^(?:[A-Za-z_$][\w$]*)(?:\.[A-Za-z_$][\w$]*)*\s*===\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(when);
|
|
21177
|
+
if (!when || !validWhen || !rpc?.trim() || !validBoundary) {
|
|
21178
|
+
ctx.diagnostics.push({
|
|
21179
|
+
severity: "error",
|
|
21180
|
+
code: "invalid-command-branches",
|
|
21181
|
+
file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
|
|
21182
|
+
line: lineOf(item),
|
|
21183
|
+
message: "Command branches require a restricted equality selector, a non-empty rpc, and a valid boundary.",
|
|
21184
|
+
suggestion: `Use when: 'resource === "value"' (or body.resource/params.resource/query.resource), plus a named RPC adapter.`
|
|
21185
|
+
});
|
|
21186
|
+
continue;
|
|
21187
|
+
}
|
|
21188
|
+
branches.push({
|
|
21189
|
+
when,
|
|
21190
|
+
rpc,
|
|
21191
|
+
transaction,
|
|
21192
|
+
...audit === undefined ? {} : { audit },
|
|
21193
|
+
idempotency,
|
|
21194
|
+
...boundary === undefined ? {} : { boundary }
|
|
21195
|
+
});
|
|
21196
|
+
}
|
|
21197
|
+
if (branches.length === 0)
|
|
21198
|
+
return [];
|
|
21199
|
+
if (new Set(branches.map((branch) => branch.when)).size !== branches.length) {
|
|
21200
|
+
ctx.diagnostics.push({
|
|
21201
|
+
severity: "error",
|
|
21202
|
+
code: "duplicate-command-branch",
|
|
21203
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
21204
|
+
line: lineOf(expression),
|
|
21205
|
+
message: "Command branches must use distinct selectors.",
|
|
21206
|
+
suggestion: "Make each branch selector mutually distinct."
|
|
21207
|
+
});
|
|
21208
|
+
}
|
|
21209
|
+
return branches;
|
|
21210
|
+
}
|
|
20689
21211
|
function classDeps(cls, ctx) {
|
|
20690
21212
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
20691
21213
|
const ctor = cls.members.find(ts7.isConstructorDeclaration);
|
|
@@ -21897,6 +22419,7 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
21897
22419
|
|
|
21898
22420
|
// src/effect-governance.ts
|
|
21899
22421
|
init_validate();
|
|
22422
|
+
init_feature();
|
|
21900
22423
|
function diagnostic(code, message, file, suggestion) {
|
|
21901
22424
|
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
21902
22425
|
return {
|
|
@@ -21934,8 +22457,10 @@ function validateEffectPolicies(graph, options = {}) {
|
|
|
21934
22457
|
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
21935
22458
|
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."));
|
|
21936
22459
|
}
|
|
21937
|
-
if (
|
|
21938
|
-
const
|
|
22460
|
+
if (effect.retry === "explicit") {
|
|
22461
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22462
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
22463
|
+
const command = commandRef === undefined ? undefined : commands.get(commandRef) ?? [...commands.values()].find((candidate) => candidate.name === commandRef);
|
|
21939
22464
|
if (command && command.idempotency !== "required") {
|
|
21940
22465
|
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".'));
|
|
21941
22466
|
}
|
|
@@ -22190,6 +22715,7 @@ function resolveTypeSafety(options) {
|
|
|
22190
22715
|
init_delivery_schema();
|
|
22191
22716
|
|
|
22192
22717
|
// src/delivery-plan.ts
|
|
22718
|
+
init_feature();
|
|
22193
22719
|
init_validate();
|
|
22194
22720
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
22195
22721
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
@@ -22309,6 +22835,10 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
22309
22835
|
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.");
|
|
22310
22836
|
}
|
|
22311
22837
|
publicRoutes.set(key, target.name);
|
|
22838
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
22839
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22840
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
22841
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
22312
22842
|
target.roots.push(module.name);
|
|
22313
22843
|
target.routes.push({
|
|
22314
22844
|
module: module.name,
|
|
@@ -22316,7 +22846,8 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
22316
22846
|
handler: route.handler,
|
|
22317
22847
|
method: route.method,
|
|
22318
22848
|
path,
|
|
22319
|
-
...
|
|
22849
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
22850
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
22320
22851
|
reason: owner === undefined ? "default-http" : "explicit-module"
|
|
22321
22852
|
});
|
|
22322
22853
|
}
|
|
@@ -23347,7 +23878,12 @@ async function buildDeliveryProject(options, delivery) {
|
|
|
23347
23878
|
deploymentReady: false,
|
|
23348
23879
|
plan: planned.plan,
|
|
23349
23880
|
objects,
|
|
23350
|
-
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
23881
|
+
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
23882
|
+
method: route.method,
|
|
23883
|
+
path: route.path,
|
|
23884
|
+
target: target.name,
|
|
23885
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
23886
|
+
}))),
|
|
23351
23887
|
jobs: planned.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })))
|
|
23352
23888
|
});
|
|
23353
23889
|
phase = "artifact-integrity";
|
|
@@ -24097,10 +24633,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24097
24633
|
import { resolve as resolve17 } from "node:path";
|
|
24098
24634
|
|
|
24099
24635
|
// src/migration-app-version.ts
|
|
24100
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24636
|
+
var MIGRATION_APP_VERSION = "0.30.0";
|
|
24101
24637
|
|
|
24102
24638
|
// src/migration-elysia-version.ts
|
|
24103
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24639
|
+
var MIGRATION_ELYSIA_VERSION = "0.35.0";
|
|
24104
24640
|
|
|
24105
24641
|
// src/migration-policy.ts
|
|
24106
24642
|
var cachedCompilerVersion;
|
|
@@ -25069,6 +25605,175 @@ function isRecord2(value) {
|
|
|
25069
25605
|
function arrayLength(value) {
|
|
25070
25606
|
return Array.isArray(value) ? value.length : undefined;
|
|
25071
25607
|
}
|
|
25608
|
+
function safeField(value) {
|
|
25609
|
+
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
25610
|
+
}
|
|
25611
|
+
function validFeaturePersistence(value) {
|
|
25612
|
+
if (!isRecord2(value))
|
|
25613
|
+
return false;
|
|
25614
|
+
const boundary = value["boundary"];
|
|
25615
|
+
const receipt = value["receipt"];
|
|
25616
|
+
const reconciliation = value["reconciliation"];
|
|
25617
|
+
if (boundary === "database") {
|
|
25618
|
+
const fields = [value["tenantField"], value["versionField"], value["stateField"]];
|
|
25619
|
+
return value["optimisticLock"] === "required" && fields.every(safeField) && new Set(fields).size === 3 && receipt === "required" && reconciliation === "none";
|
|
25620
|
+
}
|
|
25621
|
+
if (boundary === "external") {
|
|
25622
|
+
return value["optimisticLock"] === "none" && value["versionField"] === undefined && value["tenantField"] === undefined && value["stateField"] === undefined && receipt === "required" && reconciliation === "required";
|
|
25623
|
+
}
|
|
25624
|
+
return false;
|
|
25625
|
+
}
|
|
25626
|
+
function validCommandInventory(value) {
|
|
25627
|
+
const names = new Set;
|
|
25628
|
+
const classes = new Set;
|
|
25629
|
+
return Array.isArray(value) && value.every((command) => {
|
|
25630
|
+
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")
|
|
25631
|
+
return false;
|
|
25632
|
+
if (names.has(command["name"]) || classes.has(command["className"]))
|
|
25633
|
+
return false;
|
|
25634
|
+
if (command["rpc"] !== undefined && (typeof command["rpc"] !== "string" || command["rpc"].trim() === ""))
|
|
25635
|
+
return false;
|
|
25636
|
+
if (command["auditEvent"] !== undefined && (typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === ""))
|
|
25637
|
+
return false;
|
|
25638
|
+
names.add(command["name"]);
|
|
25639
|
+
classes.add(command["className"]);
|
|
25640
|
+
return true;
|
|
25641
|
+
});
|
|
25642
|
+
}
|
|
25643
|
+
function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
25644
|
+
const featureNames = new Set;
|
|
25645
|
+
return Array.isArray(value) && value.every((feature) => {
|
|
25646
|
+
if (!isRecord2(feature) || typeof feature["name"] !== "string" || feature["name"].trim() === "")
|
|
25647
|
+
return false;
|
|
25648
|
+
if (featureNames.has(feature["name"]))
|
|
25649
|
+
return false;
|
|
25650
|
+
featureNames.add(feature["name"]);
|
|
25651
|
+
if (feature["tenantField"] !== undefined && !safeField(feature["tenantField"]))
|
|
25652
|
+
return false;
|
|
25653
|
+
const states = feature["states"];
|
|
25654
|
+
const transitions = feature["transitions"];
|
|
25655
|
+
if (!Array.isArray(states) || states.length === 0 || states.some((state) => typeof state !== "string" || state.trim() === "") || new Set(states).size !== states.length || !Array.isArray(transitions))
|
|
25656
|
+
return false;
|
|
25657
|
+
const transitionNames = new Set;
|
|
25658
|
+
return transitions.every((transition) => {
|
|
25659
|
+
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"]))
|
|
25660
|
+
return false;
|
|
25661
|
+
if (transitionNames.has(transition["name"]))
|
|
25662
|
+
return false;
|
|
25663
|
+
transitionNames.add(transition["name"]);
|
|
25664
|
+
const persistence = transition["persistence"];
|
|
25665
|
+
if (persistence !== undefined) {
|
|
25666
|
+
if (!isRecord2(persistence) || !validFeaturePersistence(persistence))
|
|
25667
|
+
return false;
|
|
25668
|
+
if (typeof transition["command"] !== "string" || transition["command"].trim() === "" || typeof transition["permission"] !== "string" || transition["permission"].trim() === "")
|
|
25669
|
+
return false;
|
|
25670
|
+
if (persistence["boundary"] === "database" && feature["tenantField"] !== persistence["tenantField"])
|
|
25671
|
+
return false;
|
|
25672
|
+
}
|
|
25673
|
+
const commandName = transition["command"];
|
|
25674
|
+
const command = commandName === undefined ? undefined : commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25675
|
+
if (commandName !== undefined) {
|
|
25676
|
+
if (!isRecord2(command))
|
|
25677
|
+
return false;
|
|
25678
|
+
if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
|
|
25679
|
+
return false;
|
|
25680
|
+
if (transition["transaction"] !== undefined && command["transaction"] !== transition["transaction"])
|
|
25681
|
+
return false;
|
|
25682
|
+
if (transition["idempotency"] !== undefined && command["idempotency"] !== transition["idempotency"])
|
|
25683
|
+
return false;
|
|
25684
|
+
if (transition["audit"] !== undefined && command["auditEvent"] !== transition["audit"])
|
|
25685
|
+
return false;
|
|
25686
|
+
if (persistence !== undefined) {
|
|
25687
|
+
const adapter = command["rpc"];
|
|
25688
|
+
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() === "")
|
|
25689
|
+
return false;
|
|
25690
|
+
const expectedTransaction = persistence["boundary"] === "database" ? "required" : "none";
|
|
25691
|
+
if (command["transaction"] !== expectedTransaction)
|
|
25692
|
+
return false;
|
|
25693
|
+
}
|
|
25694
|
+
}
|
|
25695
|
+
const routeName = transition["route"];
|
|
25696
|
+
const routeBinding = transition["routeBinding"];
|
|
25697
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
25698
|
+
return false;
|
|
25699
|
+
if (routeName !== undefined) {
|
|
25700
|
+
const route = routes.find((candidate) => isRecord2(candidate) && `${candidate["method"]} ${candidate["path"]}` === routeName);
|
|
25701
|
+
if (!isRecord2(route))
|
|
25702
|
+
return false;
|
|
25703
|
+
if (routeBinding === "controller") {
|
|
25704
|
+
if (route["command"] !== undefined)
|
|
25705
|
+
return false;
|
|
25706
|
+
} else {
|
|
25707
|
+
if (typeof route["command"] !== "string")
|
|
25708
|
+
return false;
|
|
25709
|
+
const routeCommand = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === route["command"] || candidate["className"] === route["command"]));
|
|
25710
|
+
if (!isRecord2(routeCommand) || !isRecord2(command) || routeCommand["name"] !== command["name"])
|
|
25711
|
+
return false;
|
|
25712
|
+
}
|
|
25713
|
+
if (transition["permission"] !== undefined && route["permission"] !== transition["permission"])
|
|
25714
|
+
return false;
|
|
25715
|
+
if (transition["idempotency"] !== undefined && route["idempotency"] !== transition["idempotency"])
|
|
25716
|
+
return false;
|
|
25717
|
+
if (persistence !== undefined && route["idempotency"] !== "required")
|
|
25718
|
+
return false;
|
|
25719
|
+
if (route["idempotency"] === "required" && (route["method"] === "GET" || route["method"] === "HEAD" || route["method"] === "OPTIONS"))
|
|
25720
|
+
return false;
|
|
25721
|
+
}
|
|
25722
|
+
const job = transition["job"];
|
|
25723
|
+
if (job === undefined)
|
|
25724
|
+
return true;
|
|
25725
|
+
const details = transition["jobDetails"];
|
|
25726
|
+
const inventory = jobs.find((candidate) => isRecord2(candidate) && (candidate["name"] === job || candidate["className"] === job));
|
|
25727
|
+
const contractFields = ["input", "inputKind", "output", "outputKind", "mode", "idempotency", "tenantScope"];
|
|
25728
|
+
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]);
|
|
25729
|
+
});
|
|
25730
|
+
});
|
|
25731
|
+
}
|
|
25732
|
+
function validJobInventory(value) {
|
|
25733
|
+
const names = new Set;
|
|
25734
|
+
const classes = new Set;
|
|
25735
|
+
return Array.isArray(value) && value.every((job) => {
|
|
25736
|
+
if (!isRecord2(job) || typeof job["name"] !== "string" || job["name"].trim() === "" || typeof job["className"] !== "string" || job["className"].trim() === "" || typeof job["module"] !== "string" || job["module"].trim() === "")
|
|
25737
|
+
return false;
|
|
25738
|
+
if (names.has(job["name"]) || classes.has(job["className"]))
|
|
25739
|
+
return false;
|
|
25740
|
+
names.add(job["name"]);
|
|
25741
|
+
classes.add(job["className"]);
|
|
25742
|
+
if (job["input"] !== undefined && (typeof job["input"] !== "string" || job["input"].trim() === ""))
|
|
25743
|
+
return false;
|
|
25744
|
+
if (job["inputKind"] !== undefined && job["inputKind"] !== "opaque" && job["inputKind"] !== "declared")
|
|
25745
|
+
return false;
|
|
25746
|
+
if (job["output"] !== undefined && (typeof job["output"] !== "string" || job["output"].trim() === ""))
|
|
25747
|
+
return false;
|
|
25748
|
+
if (job["outputKind"] !== undefined && job["outputKind"] !== "opaque" && job["outputKind"] !== "declared")
|
|
25749
|
+
return false;
|
|
25750
|
+
if (job["mode"] !== undefined && job["mode"] !== "task" && job["mode"] !== "workflow")
|
|
25751
|
+
return false;
|
|
25752
|
+
if (job["idempotency"] !== undefined && job["idempotency"] !== "required" && job["idempotency"] !== "none")
|
|
25753
|
+
return false;
|
|
25754
|
+
return job["tenantScope"] === undefined || job["tenantScope"] === "required" || job["tenantScope"] === "none";
|
|
25755
|
+
});
|
|
25756
|
+
}
|
|
25757
|
+
function validRpcInventory(value, commandInventory) {
|
|
25758
|
+
const commandNames = new Set;
|
|
25759
|
+
const adapters = new Set;
|
|
25760
|
+
return Array.isArray(value) && value.every((rpc) => {
|
|
25761
|
+
if (!isRecord2(rpc) || typeof rpc["command"] !== "string" || rpc["command"].trim() === "" || typeof rpc["adapter"] !== "string" || rpc["adapter"].trim() === "")
|
|
25762
|
+
return false;
|
|
25763
|
+
if (commandNames.has(rpc["command"]) || adapters.has(rpc["adapter"]))
|
|
25764
|
+
return false;
|
|
25765
|
+
const command = commandInventory.find((candidate) => isRecord2(candidate) && candidate["name"] === rpc["command"] && candidate["rpc"] === rpc["adapter"]);
|
|
25766
|
+
if (!isRecord2(command))
|
|
25767
|
+
return false;
|
|
25768
|
+
commandNames.add(rpc["command"]);
|
|
25769
|
+
adapters.add(rpc["adapter"]);
|
|
25770
|
+
return true;
|
|
25771
|
+
}) && commandInventory.every((command) => {
|
|
25772
|
+
if (!isRecord2(command) || command["rpc"] === undefined)
|
|
25773
|
+
return true;
|
|
25774
|
+
return value.some((rpc) => isRecord2(rpc) && rpc["command"] === command["name"] && rpc["adapter"] === command["rpc"]);
|
|
25775
|
+
});
|
|
25776
|
+
}
|
|
25072
25777
|
async function sha2566(path) {
|
|
25073
25778
|
return createHash11("sha256").update(await readFile11(path)).digest("hex");
|
|
25074
25779
|
}
|
|
@@ -25138,19 +25843,24 @@ async function assessMigration(options) {
|
|
|
25138
25843
|
};
|
|
25139
25844
|
try {
|
|
25140
25845
|
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
25141
|
-
if (!isRecord2(parsed) || parsed["version"] !== 1)
|
|
25846
|
+
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"])) {
|
|
25142
25847
|
throw new Error("invalid manifest");
|
|
25848
|
+
}
|
|
25143
25849
|
const commands = arrayLength(parsed["commands"]);
|
|
25850
|
+
const jobs = arrayLength(parsed["jobs"]);
|
|
25144
25851
|
const routes = arrayLength(parsed["routes"]);
|
|
25145
25852
|
const permissions = arrayLength(parsed["permissions"]);
|
|
25853
|
+
const features = arrayLength(parsed["features"]);
|
|
25146
25854
|
contractsManifest = {
|
|
25147
25855
|
path: contractsManifestPath,
|
|
25148
25856
|
present: true,
|
|
25149
25857
|
sha256: await sha2566(contractsManifestPath),
|
|
25150
25858
|
version: 1,
|
|
25151
25859
|
...commands === undefined ? {} : { commands },
|
|
25860
|
+
...jobs === undefined ? {} : { jobs },
|
|
25152
25861
|
...routes === undefined ? {} : { routes },
|
|
25153
|
-
...permissions === undefined ? {} : { permissions }
|
|
25862
|
+
...permissions === undefined ? {} : { permissions },
|
|
25863
|
+
...features === undefined ? {} : { features }
|
|
25154
25864
|
};
|
|
25155
25865
|
} catch {
|
|
25156
25866
|
findings.push({
|