@supacloud/compiler 0.50.1 → 0.51.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/dist/cli.js CHANGED
@@ -270,6 +270,16 @@ function validateFeatureSpec(spec, module) {
270
270
  if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
271
271
  error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
272
272
  }
273
+ if (spec.requiredAdapters !== undefined) {
274
+ if (spec.requiredAdapters.length === 0 || spec.requiredAdapters.some((token) => !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(token)) || new Set(spec.requiredAdapters).size !== spec.requiredAdapters.length) {
275
+ error("feature-adapter-requirements-invalid", `Feature ${spec.name} must declare unique, non-empty adapter token identifiers.`);
276
+ }
277
+ }
278
+ if (spec.requiredAdapterScopes !== undefined) {
279
+ if (spec.requiredAdapters === undefined || Object.keys(spec.requiredAdapterScopes).some((token) => !spec.requiredAdapters?.includes(token))) {
280
+ error("feature-adapter-requirements-invalid", `Feature ${spec.name} must scope only declared required adapters.`);
281
+ }
282
+ }
273
283
  const databaseTransition = spec.transitions.some((transition) => transition.persistence?.boundary === "database");
274
284
  if (databaseTransition && spec.database === undefined && spec.file !== undefined) {
275
285
  error("feature-database-source-required", `Database Feature ${spec.name} must declare its database source, table and rowType.`);
@@ -286,6 +296,19 @@ function validateFeatureSpec(spec, module) {
286
296
  }
287
297
  }
288
298
  const names = new Set;
299
+ if (module && spec.requiredAdapters !== undefined) {
300
+ for (const token of spec.requiredAdapters) {
301
+ const provider = module.providers.find((candidate) => candidate.token === token || candidate.useClass === token);
302
+ if (!provider) {
303
+ error("feature-adapter-unresolved", `Feature ${spec.name} requires adapter ${token}, but module ${module.name} does not register a provider for it.`);
304
+ } else {
305
+ const expectedScope = spec.requiredAdapterScopes?.[token];
306
+ if (expectedScope !== undefined && provider.scope !== expectedScope) {
307
+ error("feature-adapter-provider-scope", `Feature ${spec.name} requires adapter ${token} to use ${expectedScope} scope, but module ${module.name} registers it with ${provider.scope} scope.`);
308
+ }
309
+ }
310
+ }
311
+ }
289
312
  for (const transition of spec.transitions) {
290
313
  if (!transition.name.trim() || names.has(transition.name)) {
291
314
  error("duplicate-feature-transition", `Feature ${spec.name} has duplicate/empty transition '${transition.name}'.`);
@@ -325,11 +348,20 @@ function validateFeatureSpec(spec, module) {
325
348
  }
326
349
  if (!module)
327
350
  continue;
328
- if (transition.persistence?.boundary === "database") {
351
+ if (transition.persistence !== undefined) {
329
352
  for (const query of module.queries) {
353
+ const queryProvider = module.providers.find((provider) => provider.token === query.className || provider.useClass === query.className);
354
+ if (!queryProvider) {
355
+ error("feature-query-provider-unresolved", `Feature query ${query.className} is not registered as a runtime provider in module ${module.name}.`);
356
+ } else if (queryProvider.scope !== "request") {
357
+ error("feature-query-provider-scope", `Feature query ${query.className} must use request scope, but module ${module.name} registers it with ${queryProvider.scope} scope.`);
358
+ }
330
359
  if (query.tenantScope !== "required") {
331
360
  error("feature-query-tenant-boundary", `Query ${query.className} in Feature ${spec.name} must declare tenantScope: "required".`);
332
361
  }
362
+ if (spec.readPermission !== undefined && query.permission !== spec.readPermission) {
363
+ error("feature-query-permission", `Query ${query.className} in Feature ${spec.name} must declare read permission '${spec.readPermission}'.`);
364
+ }
333
365
  }
334
366
  for (const job of module.jobs ?? []) {
335
367
  if (job.tenantScope !== "required") {
@@ -346,6 +378,12 @@ function validateFeatureSpec(spec, module) {
346
378
  error("feature-command-unresolved", `Transition ${transition.name} declares governance without a command binding.`);
347
379
  }
348
380
  if (command) {
381
+ const commandProvider = module.providers.find((provider) => provider.token === command.className || provider.useClass === command.className);
382
+ if (!commandProvider) {
383
+ error("feature-command-provider-unresolved", `Feature transition ${transition.name} references command ${command.className}, but the command is not registered as a provider in module ${module.name}.`);
384
+ } else if (commandProvider.scope !== "request") {
385
+ error("feature-command-provider-scope", `Feature command ${command.className} must use request scope, but module ${module.name} registers it with ${commandProvider.scope} scope.`);
386
+ }
349
387
  for (const key of ["permission", "transaction", "idempotency", "audit"]) {
350
388
  if (transition[key] !== undefined && transition[key] !== command[key]) {
351
389
  error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
@@ -372,6 +410,12 @@ function validateFeatureSpec(spec, module) {
372
410
  error("feature-job-unresolved", `Transition ${transition.name} must reference exactly one Job in module ${module.name}.`);
373
411
  } else {
374
412
  const job = jobs[0];
413
+ const jobProvider = module.providers.find((provider) => provider.token === job.className || provider.useClass === job.className);
414
+ if (!jobProvider) {
415
+ error("feature-job-provider-unresolved", `Feature transition ${transition.name} references Job ${job.className}, but the Job is not registered as a runtime provider in module ${module.name}.`);
416
+ } else if (jobProvider.scope !== "job") {
417
+ error("feature-job-provider-scope", `Feature Job ${job.className} must use job scope, but module ${module.name} registers it with ${jobProvider.scope} scope.`);
418
+ }
375
419
  if (job.tenantScope !== "required") {
376
420
  error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
377
421
  }
@@ -1448,6 +1492,11 @@ var init_validate = __esm(() => {
1448
1492
  "duplicate-feature-transition": { code: "SC6102", docsUrl: "https://supacloud.dev/errors/SC6102" },
1449
1493
  "invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
1450
1494
  "feature-command-unresolved": { code: "SC6104", docsUrl: "https://supacloud.dev/errors/SC6104" },
1495
+ "feature-command-provider-unresolved": { code: "SC6128", docsUrl: "https://supacloud.dev/errors/SC6128" },
1496
+ "feature-command-provider-scope": { code: "SC6134", docsUrl: "https://supacloud.dev/errors/SC6134" },
1497
+ "feature-adapter-unresolved": { code: "SC6135", docsUrl: "https://supacloud.dev/errors/SC6135" },
1498
+ "feature-adapter-requirements-invalid": { code: "SC6136", docsUrl: "https://supacloud.dev/errors/SC6136" },
1499
+ "feature-adapter-provider-scope": { code: "SC6137", docsUrl: "https://supacloud.dev/errors/SC6137" },
1451
1500
  "feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
1452
1501
  "feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
1453
1502
  "feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
@@ -1455,9 +1504,14 @@ var init_validate = __esm(() => {
1455
1504
  "feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
1456
1505
  "feature-tenant-boundary-drift": { code: "SC6127", docsUrl: "https://supacloud.dev/errors/SC6127" },
1457
1506
  "feature-query-tenant-boundary": { code: "SC6114", docsUrl: "https://supacloud.dev/errors/SC6114" },
1507
+ "feature-query-provider-unresolved": { code: "SC6130", docsUrl: "https://supacloud.dev/errors/SC6130" },
1508
+ "feature-query-provider-scope": { code: "SC6131", docsUrl: "https://supacloud.dev/errors/SC6131" },
1509
+ "feature-query-permission": { code: "SC6138", docsUrl: "https://supacloud.dev/errors/SC6138" },
1458
1510
  "feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
1459
1511
  "feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
1460
1512
  "feature-job-unresolved": { code: "SC6117", docsUrl: "https://supacloud.dev/errors/SC6117" },
1513
+ "feature-job-provider-unresolved": { code: "SC6129", docsUrl: "https://supacloud.dev/errors/SC6129" },
1514
+ "feature-job-provider-scope": { code: "SC6132", docsUrl: "https://supacloud.dev/errors/SC6132" },
1461
1515
  "feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
1462
1516
  "feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
1463
1517
  "feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
@@ -1522,10 +1576,13 @@ function buildContractManifest(graph, artifacts) {
1522
1576
  });
1523
1577
  }
1524
1578
  for (const query of module.queries) {
1579
+ if (query.permission)
1580
+ permissions.add(query.permission);
1525
1581
  queries.push({
1526
1582
  name: query.name,
1527
1583
  className: query.className,
1528
1584
  module: module.name,
1585
+ ...query.permission === undefined ? {} : { permission: query.permission },
1529
1586
  ...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
1530
1587
  });
1531
1588
  }
@@ -1549,6 +1606,7 @@ function buildContractManifest(graph, artifacts) {
1549
1606
  states: [...module.featureSpec.states],
1550
1607
  ...module.featureSpec.database === undefined ? {} : { database: module.featureSpec.database },
1551
1608
  ...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
1609
+ ...module.featureSpec.readPermission === undefined ? {} : { readPermission: module.featureSpec.readPermission },
1552
1610
  transitions: module.featureSpec.transitions.map((transition) => {
1553
1611
  const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
1554
1612
  return {
@@ -1590,8 +1648,9 @@ function buildContractManifest(graph, artifacts) {
1590
1648
  const governedCommand = command ?? featureCommand;
1591
1649
  const effectiveCommand = route.command ?? featureTransition?.command;
1592
1650
  const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
1593
- if (governedCommand?.permission)
1594
- permissions.add(governedCommand.permission);
1651
+ const permission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
1652
+ if (permission)
1653
+ permissions.add(permission);
1595
1654
  if (route.contract?.evidence)
1596
1655
  fixtures.add(route.contract.evidence);
1597
1656
  routes.push({
@@ -1601,7 +1660,7 @@ function buildContractManifest(graph, artifacts) {
1601
1660
  controller: controller.className,
1602
1661
  handler: route.handler,
1603
1662
  ...effectiveCommand === undefined ? {} : { command: effectiveCommand },
1604
- ...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
1663
+ ...permission === undefined ? {} : { permission },
1605
1664
  ...idempotency === undefined ? {} : { idempotency },
1606
1665
  ...route.tags === undefined ? {} : { tags: [...route.tags] },
1607
1666
  requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
@@ -2663,6 +2722,7 @@ function renderClient(graph, options) {
2663
2722
  const featureTransition = featureTransitionForRoute(module, controller, route);
2664
2723
  const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
2665
2724
  const governedCommand = command ?? featureCommand;
2725
+ const routePermission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
2666
2726
  const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
2667
2727
  const transitionRoute = `${route.method} ${fullPath}`;
2668
2728
  const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
@@ -2689,6 +2749,7 @@ function renderClient(graph, options) {
2689
2749
  ["parse", route.parse],
2690
2750
  ["allowDeleteBody", route.allowDeleteBody],
2691
2751
  ["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
2752
+ ["permission", routePermission],
2692
2753
  ["tags", route.tags],
2693
2754
  ["invalidateTags", invalidationTags]
2694
2755
  ]) {
@@ -2730,6 +2791,7 @@ function renderClient(graph, options) {
2730
2791
  path: fullPath,
2731
2792
  controller: controller.className,
2732
2793
  handler: route.handler,
2794
+ ...routePermission === undefined ? {} : { permission: routePermission },
2733
2795
  kind: procedureKind,
2734
2796
  idempotency: idempotencyMode,
2735
2797
  ...route.tags === undefined ? {} : { tags: route.tags },
@@ -2742,7 +2804,7 @@ function renderClient(graph, options) {
2742
2804
  handler: route.handler,
2743
2805
  kind: procedureKind,
2744
2806
  ...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
2745
- ...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
2807
+ ...routePermission === undefined ? {} : { permission: routePermission },
2746
2808
  idempotency: idempotencyMode,
2747
2809
  ...route.tags === undefined ? {} : { tags: [...route.tags] },
2748
2810
  ...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
@@ -2798,6 +2860,7 @@ ${procedureMethods.join(`
2798
2860
  " controller: string;",
2799
2861
  " handler: string;",
2800
2862
  " kind: Kind;",
2863
+ " permission?: string;",
2801
2864
  " idempotency: Idempotency;",
2802
2865
  " tags?: readonly string[];",
2803
2866
  " invalidateTags?: readonly string[];",
@@ -3436,7 +3499,7 @@ function renderOpenApi(graph, options) {
3436
3499
  const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
3437
3500
  const governedCommand = command ?? featureCommand;
3438
3501
  const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
3439
- const permission = governedCommand?.permission ?? featureTransition?.permission;
3502
+ const permission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
3440
3503
  const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
3441
3504
  const responseKind = route.contract?.response;
3442
3505
  const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
@@ -3889,6 +3952,10 @@ function renderPermissions(graph) {
3889
3952
  if (command.permission)
3890
3953
  permissions.add(command.permission);
3891
3954
  }
3955
+ for (const query of module.queries) {
3956
+ if (query.permission)
3957
+ permissions.add(query.permission);
3958
+ }
3892
3959
  for (const controller of module.controllers) {
3893
3960
  for (const route of controller.routes) {
3894
3961
  const featureTransition = featureTransitionForRoute(module, controller, route);
@@ -3896,7 +3963,7 @@ function renderPermissions(graph) {
3896
3963
  const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
3897
3964
  const governedCommand = command ?? featureCommand;
3898
3965
  const effectiveCommand = route.command ?? featureTransition?.command;
3899
- const perm = governedCommand?.permission ?? featureTransition?.permission;
3966
+ const perm = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
3900
3967
  const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
3901
3968
  if (perm)
3902
3969
  permissions.add(perm);
@@ -19588,10 +19655,12 @@ function parseModule(candidate, nameByNode, ctx) {
19588
19655
  if (queryDec) {
19589
19656
  const meta = decoratorObjectArg(queryDec);
19590
19657
  if (meta) {
19658
+ const permission = stringLiteralProp(meta, "permission");
19591
19659
  const tenantScope = commandModeProp(meta, "tenantScope");
19592
19660
  queries.push({
19593
19661
  className: cls.name?.text ?? "<anonymous>",
19594
19662
  name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
19663
+ ...permission === undefined ? {} : { permission },
19595
19664
  ...tenantScope === undefined ? {} : { tenantScope }
19596
19665
  });
19597
19666
  }
@@ -19652,11 +19721,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19652
19721
  if (databaseExpr !== undefined && database === undefined)
19653
19722
  return invalid();
19654
19723
  const tenantField = stringLiteralProp(input, "tenantField");
19724
+ const readPermission = stringLiteralProp(input, "readPermission");
19725
+ const requiredAdaptersExpr = getProp(input, "requiredAdapters");
19726
+ const requiredAdapterScopesExpr = getProp(input, "requiredAdapterScopes");
19655
19727
  const statesExpr = getProp(input, "states");
19656
19728
  const transitionObject = getProp(input, "transitions");
19657
19729
  if (!name || !statesExpr || !ts7.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts7.isStringLiteral(state)) || !transitionObject || !ts7.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts7.isPropertyAssignment(property))) {
19658
19730
  return invalid();
19659
19731
  }
19732
+ const requiredAdapters = requiredAdaptersExpr === undefined ? undefined : ts7.isArrayLiteralExpression(requiredAdaptersExpr) && requiredAdaptersExpr.elements.every((element) => ts7.isStringLiteral(element) && element.text.trim().length > 0) ? requiredAdaptersExpr.elements.map((element) => element.text) : undefined;
19733
+ if (requiredAdaptersExpr !== undefined && requiredAdapters === undefined)
19734
+ return invalid();
19735
+ const requiredAdapterScopes = requiredAdapterScopesExpr === undefined ? undefined : ts7.isObjectLiteralExpression(requiredAdapterScopesExpr) && requiredAdapterScopesExpr.properties.every((property) => ts7.isPropertyAssignment(property) && !ts7.isComputedPropertyName(property.name) && ts7.isStringLiteral(property.initializer) && isScope(property.initializer.text)) ? Object.fromEntries(requiredAdapterScopesExpr.properties.map((property) => [
19736
+ propertyName(property.name),
19737
+ property.initializer.getText().slice(1, -1)
19738
+ ])) : undefined;
19739
+ if (requiredAdapterScopesExpr !== undefined && requiredAdapterScopes === undefined)
19740
+ return invalid();
19660
19741
  const states = [];
19661
19742
  for (const state of statesExpr.elements) {
19662
19743
  if (!ts7.isStringLiteral(state))
@@ -19712,6 +19793,9 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19712
19793
  states,
19713
19794
  ...database === undefined ? {} : { database },
19714
19795
  ...tenantField === undefined ? {} : { tenantField },
19796
+ ...readPermission === undefined ? {} : { readPermission },
19797
+ ...requiredAdapters === undefined ? {} : { requiredAdapters },
19798
+ ...requiredAdapterScopes === undefined ? {} : { requiredAdapterScopes },
19715
19799
  transitions,
19716
19800
  file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
19717
19801
  line: lineOf(input)
@@ -20648,6 +20732,9 @@ function parseController(input, ctx) {
20648
20732
  const optionsArg = args[1];
20649
20733
  const optionsObject = resolveStaticObjectLiteral(optionsArg, ctx);
20650
20734
  if (optionsObject) {
20735
+ const permission = stringLiteralProp(optionsObject, "permission");
20736
+ if (permission !== undefined)
20737
+ route.permission = permission;
20651
20738
  const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
20652
20739
  const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
20653
20740
  const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
@@ -25006,10 +25093,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
25006
25093
  import { resolve as resolve18 } from "node:path";
25007
25094
 
25008
25095
  // src/migration-app-version.ts
25009
- var MIGRATION_APP_VERSION = "0.31.1";
25096
+ var MIGRATION_APP_VERSION = "0.32.0";
25010
25097
 
25011
25098
  // src/migration-elysia-version.ts
25012
- var MIGRATION_ELYSIA_VERSION = "0.35.2";
25099
+ var MIGRATION_ELYSIA_VERSION = "0.36.0";
25013
25100
 
25014
25101
  // src/migration-policy.ts
25015
25102
  var cachedCompilerVersion;
@@ -24,6 +24,7 @@ export interface ContractManifest {
24
24
  name: string;
25
25
  className: string;
26
26
  module: string;
27
+ permission?: string;
27
28
  tenantScope?: "required" | "none";
28
29
  }>;
29
30
  jobs: Array<{
package/dist/index.js CHANGED
@@ -269,6 +269,16 @@ function validateFeatureSpec(spec, module) {
269
269
  if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
270
270
  error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
271
271
  }
272
+ if (spec.requiredAdapters !== undefined) {
273
+ if (spec.requiredAdapters.length === 0 || spec.requiredAdapters.some((token) => !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(token)) || new Set(spec.requiredAdapters).size !== spec.requiredAdapters.length) {
274
+ error("feature-adapter-requirements-invalid", `Feature ${spec.name} must declare unique, non-empty adapter token identifiers.`);
275
+ }
276
+ }
277
+ if (spec.requiredAdapterScopes !== undefined) {
278
+ if (spec.requiredAdapters === undefined || Object.keys(spec.requiredAdapterScopes).some((token) => !spec.requiredAdapters?.includes(token))) {
279
+ error("feature-adapter-requirements-invalid", `Feature ${spec.name} must scope only declared required adapters.`);
280
+ }
281
+ }
272
282
  const databaseTransition = spec.transitions.some((transition) => transition.persistence?.boundary === "database");
273
283
  if (databaseTransition && spec.database === undefined && spec.file !== undefined) {
274
284
  error("feature-database-source-required", `Database Feature ${spec.name} must declare its database source, table and rowType.`);
@@ -285,6 +295,19 @@ function validateFeatureSpec(spec, module) {
285
295
  }
286
296
  }
287
297
  const names = new Set;
298
+ if (module && spec.requiredAdapters !== undefined) {
299
+ for (const token of spec.requiredAdapters) {
300
+ const provider = module.providers.find((candidate) => candidate.token === token || candidate.useClass === token);
301
+ if (!provider) {
302
+ error("feature-adapter-unresolved", `Feature ${spec.name} requires adapter ${token}, but module ${module.name} does not register a provider for it.`);
303
+ } else {
304
+ const expectedScope = spec.requiredAdapterScopes?.[token];
305
+ if (expectedScope !== undefined && provider.scope !== expectedScope) {
306
+ error("feature-adapter-provider-scope", `Feature ${spec.name} requires adapter ${token} to use ${expectedScope} scope, but module ${module.name} registers it with ${provider.scope} scope.`);
307
+ }
308
+ }
309
+ }
310
+ }
288
311
  for (const transition of spec.transitions) {
289
312
  if (!transition.name.trim() || names.has(transition.name)) {
290
313
  error("duplicate-feature-transition", `Feature ${spec.name} has duplicate/empty transition '${transition.name}'.`);
@@ -324,11 +347,20 @@ function validateFeatureSpec(spec, module) {
324
347
  }
325
348
  if (!module)
326
349
  continue;
327
- if (transition.persistence?.boundary === "database") {
350
+ if (transition.persistence !== undefined) {
328
351
  for (const query of module.queries) {
352
+ const queryProvider = module.providers.find((provider) => provider.token === query.className || provider.useClass === query.className);
353
+ if (!queryProvider) {
354
+ error("feature-query-provider-unresolved", `Feature query ${query.className} is not registered as a runtime provider in module ${module.name}.`);
355
+ } else if (queryProvider.scope !== "request") {
356
+ error("feature-query-provider-scope", `Feature query ${query.className} must use request scope, but module ${module.name} registers it with ${queryProvider.scope} scope.`);
357
+ }
329
358
  if (query.tenantScope !== "required") {
330
359
  error("feature-query-tenant-boundary", `Query ${query.className} in Feature ${spec.name} must declare tenantScope: "required".`);
331
360
  }
361
+ if (spec.readPermission !== undefined && query.permission !== spec.readPermission) {
362
+ error("feature-query-permission", `Query ${query.className} in Feature ${spec.name} must declare read permission '${spec.readPermission}'.`);
363
+ }
332
364
  }
333
365
  for (const job of module.jobs ?? []) {
334
366
  if (job.tenantScope !== "required") {
@@ -345,6 +377,12 @@ function validateFeatureSpec(spec, module) {
345
377
  error("feature-command-unresolved", `Transition ${transition.name} declares governance without a command binding.`);
346
378
  }
347
379
  if (command) {
380
+ const commandProvider = module.providers.find((provider) => provider.token === command.className || provider.useClass === command.className);
381
+ if (!commandProvider) {
382
+ error("feature-command-provider-unresolved", `Feature transition ${transition.name} references command ${command.className}, but the command is not registered as a provider in module ${module.name}.`);
383
+ } else if (commandProvider.scope !== "request") {
384
+ error("feature-command-provider-scope", `Feature command ${command.className} must use request scope, but module ${module.name} registers it with ${commandProvider.scope} scope.`);
385
+ }
348
386
  for (const key of ["permission", "transaction", "idempotency", "audit"]) {
349
387
  if (transition[key] !== undefined && transition[key] !== command[key]) {
350
388
  error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
@@ -371,6 +409,12 @@ function validateFeatureSpec(spec, module) {
371
409
  error("feature-job-unresolved", `Transition ${transition.name} must reference exactly one Job in module ${module.name}.`);
372
410
  } else {
373
411
  const job = jobs[0];
412
+ const jobProvider = module.providers.find((provider) => provider.token === job.className || provider.useClass === job.className);
413
+ if (!jobProvider) {
414
+ error("feature-job-provider-unresolved", `Feature transition ${transition.name} references Job ${job.className}, but the Job is not registered as a runtime provider in module ${module.name}.`);
415
+ } else if (jobProvider.scope !== "job") {
416
+ error("feature-job-provider-scope", `Feature Job ${job.className} must use job scope, but module ${module.name} registers it with ${jobProvider.scope} scope.`);
417
+ }
374
418
  if (job.tenantScope !== "required") {
375
419
  error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
376
420
  }
@@ -1447,6 +1491,11 @@ var init_validate = __esm(() => {
1447
1491
  "duplicate-feature-transition": { code: "SC6102", docsUrl: "https://supacloud.dev/errors/SC6102" },
1448
1492
  "invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
1449
1493
  "feature-command-unresolved": { code: "SC6104", docsUrl: "https://supacloud.dev/errors/SC6104" },
1494
+ "feature-command-provider-unresolved": { code: "SC6128", docsUrl: "https://supacloud.dev/errors/SC6128" },
1495
+ "feature-command-provider-scope": { code: "SC6134", docsUrl: "https://supacloud.dev/errors/SC6134" },
1496
+ "feature-adapter-unresolved": { code: "SC6135", docsUrl: "https://supacloud.dev/errors/SC6135" },
1497
+ "feature-adapter-requirements-invalid": { code: "SC6136", docsUrl: "https://supacloud.dev/errors/SC6136" },
1498
+ "feature-adapter-provider-scope": { code: "SC6137", docsUrl: "https://supacloud.dev/errors/SC6137" },
1450
1499
  "feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
1451
1500
  "feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
1452
1501
  "feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
@@ -1454,9 +1503,14 @@ var init_validate = __esm(() => {
1454
1503
  "feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
1455
1504
  "feature-tenant-boundary-drift": { code: "SC6127", docsUrl: "https://supacloud.dev/errors/SC6127" },
1456
1505
  "feature-query-tenant-boundary": { code: "SC6114", docsUrl: "https://supacloud.dev/errors/SC6114" },
1506
+ "feature-query-provider-unresolved": { code: "SC6130", docsUrl: "https://supacloud.dev/errors/SC6130" },
1507
+ "feature-query-provider-scope": { code: "SC6131", docsUrl: "https://supacloud.dev/errors/SC6131" },
1508
+ "feature-query-permission": { code: "SC6138", docsUrl: "https://supacloud.dev/errors/SC6138" },
1457
1509
  "feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
1458
1510
  "feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
1459
1511
  "feature-job-unresolved": { code: "SC6117", docsUrl: "https://supacloud.dev/errors/SC6117" },
1512
+ "feature-job-provider-unresolved": { code: "SC6129", docsUrl: "https://supacloud.dev/errors/SC6129" },
1513
+ "feature-job-provider-scope": { code: "SC6132", docsUrl: "https://supacloud.dev/errors/SC6132" },
1460
1514
  "feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
1461
1515
  "feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
1462
1516
  "feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
@@ -1521,10 +1575,13 @@ function buildContractManifest(graph, artifacts) {
1521
1575
  });
1522
1576
  }
1523
1577
  for (const query of module.queries) {
1578
+ if (query.permission)
1579
+ permissions.add(query.permission);
1524
1580
  queries.push({
1525
1581
  name: query.name,
1526
1582
  className: query.className,
1527
1583
  module: module.name,
1584
+ ...query.permission === undefined ? {} : { permission: query.permission },
1528
1585
  ...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
1529
1586
  });
1530
1587
  }
@@ -1548,6 +1605,7 @@ function buildContractManifest(graph, artifacts) {
1548
1605
  states: [...module.featureSpec.states],
1549
1606
  ...module.featureSpec.database === undefined ? {} : { database: module.featureSpec.database },
1550
1607
  ...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
1608
+ ...module.featureSpec.readPermission === undefined ? {} : { readPermission: module.featureSpec.readPermission },
1551
1609
  transitions: module.featureSpec.transitions.map((transition) => {
1552
1610
  const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
1553
1611
  return {
@@ -1589,8 +1647,9 @@ function buildContractManifest(graph, artifacts) {
1589
1647
  const governedCommand = command ?? featureCommand;
1590
1648
  const effectiveCommand = route.command ?? featureTransition?.command;
1591
1649
  const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
1592
- if (governedCommand?.permission)
1593
- permissions.add(governedCommand.permission);
1650
+ const permission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
1651
+ if (permission)
1652
+ permissions.add(permission);
1594
1653
  if (route.contract?.evidence)
1595
1654
  fixtures.add(route.contract.evidence);
1596
1655
  routes.push({
@@ -1600,7 +1659,7 @@ function buildContractManifest(graph, artifacts) {
1600
1659
  controller: controller.className,
1601
1660
  handler: route.handler,
1602
1661
  ...effectiveCommand === undefined ? {} : { command: effectiveCommand },
1603
- ...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
1662
+ ...permission === undefined ? {} : { permission },
1604
1663
  ...idempotency === undefined ? {} : { idempotency },
1605
1664
  ...route.tags === undefined ? {} : { tags: [...route.tags] },
1606
1665
  requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
@@ -2662,6 +2721,7 @@ function renderClient(graph, options) {
2662
2721
  const featureTransition = featureTransitionForRoute(module, controller, route);
2663
2722
  const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
2664
2723
  const governedCommand = command ?? featureCommand;
2724
+ const routePermission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
2665
2725
  const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
2666
2726
  const transitionRoute = `${route.method} ${fullPath}`;
2667
2727
  const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
@@ -2688,6 +2748,7 @@ function renderClient(graph, options) {
2688
2748
  ["parse", route.parse],
2689
2749
  ["allowDeleteBody", route.allowDeleteBody],
2690
2750
  ["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
2751
+ ["permission", routePermission],
2691
2752
  ["tags", route.tags],
2692
2753
  ["invalidateTags", invalidationTags]
2693
2754
  ]) {
@@ -2729,6 +2790,7 @@ function renderClient(graph, options) {
2729
2790
  path: fullPath,
2730
2791
  controller: controller.className,
2731
2792
  handler: route.handler,
2793
+ ...routePermission === undefined ? {} : { permission: routePermission },
2732
2794
  kind: procedureKind,
2733
2795
  idempotency: idempotencyMode,
2734
2796
  ...route.tags === undefined ? {} : { tags: route.tags },
@@ -2741,7 +2803,7 @@ function renderClient(graph, options) {
2741
2803
  handler: route.handler,
2742
2804
  kind: procedureKind,
2743
2805
  ...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
2744
- ...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
2806
+ ...routePermission === undefined ? {} : { permission: routePermission },
2745
2807
  idempotency: idempotencyMode,
2746
2808
  ...route.tags === undefined ? {} : { tags: [...route.tags] },
2747
2809
  ...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
@@ -2797,6 +2859,7 @@ ${procedureMethods.join(`
2797
2859
  " controller: string;",
2798
2860
  " handler: string;",
2799
2861
  " kind: Kind;",
2862
+ " permission?: string;",
2800
2863
  " idempotency: Idempotency;",
2801
2864
  " tags?: readonly string[];",
2802
2865
  " invalidateTags?: readonly string[];",
@@ -3435,7 +3498,7 @@ function renderOpenApi(graph, options) {
3435
3498
  const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
3436
3499
  const governedCommand = command ?? featureCommand;
3437
3500
  const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
3438
- const permission = governedCommand?.permission ?? featureTransition?.permission;
3501
+ const permission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
3439
3502
  const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
3440
3503
  const responseKind = route.contract?.response;
3441
3504
  const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
@@ -3888,6 +3951,10 @@ function renderPermissions(graph) {
3888
3951
  if (command.permission)
3889
3952
  permissions.add(command.permission);
3890
3953
  }
3954
+ for (const query of module.queries) {
3955
+ if (query.permission)
3956
+ permissions.add(query.permission);
3957
+ }
3891
3958
  for (const controller of module.controllers) {
3892
3959
  for (const route of controller.routes) {
3893
3960
  const featureTransition = featureTransitionForRoute(module, controller, route);
@@ -3895,7 +3962,7 @@ function renderPermissions(graph) {
3895
3962
  const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
3896
3963
  const governedCommand = command ?? featureCommand;
3897
3964
  const effectiveCommand = route.command ?? featureTransition?.command;
3898
- const perm = governedCommand?.permission ?? featureTransition?.permission;
3965
+ const perm = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
3899
3966
  const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
3900
3967
  if (perm)
3901
3968
  permissions.add(perm);
@@ -19547,10 +19614,12 @@ function parseModule(candidate, nameByNode, ctx) {
19547
19614
  if (queryDec) {
19548
19615
  const meta = decoratorObjectArg(queryDec);
19549
19616
  if (meta) {
19617
+ const permission = stringLiteralProp(meta, "permission");
19550
19618
  const tenantScope = commandModeProp(meta, "tenantScope");
19551
19619
  queries.push({
19552
19620
  className: cls.name?.text ?? "<anonymous>",
19553
19621
  name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
19622
+ ...permission === undefined ? {} : { permission },
19554
19623
  ...tenantScope === undefined ? {} : { tenantScope }
19555
19624
  });
19556
19625
  }
@@ -19611,11 +19680,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19611
19680
  if (databaseExpr !== undefined && database === undefined)
19612
19681
  return invalid();
19613
19682
  const tenantField = stringLiteralProp(input, "tenantField");
19683
+ const readPermission = stringLiteralProp(input, "readPermission");
19684
+ const requiredAdaptersExpr = getProp(input, "requiredAdapters");
19685
+ const requiredAdapterScopesExpr = getProp(input, "requiredAdapterScopes");
19614
19686
  const statesExpr = getProp(input, "states");
19615
19687
  const transitionObject = getProp(input, "transitions");
19616
19688
  if (!name || !statesExpr || !ts7.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts7.isStringLiteral(state)) || !transitionObject || !ts7.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts7.isPropertyAssignment(property))) {
19617
19689
  return invalid();
19618
19690
  }
19691
+ const requiredAdapters = requiredAdaptersExpr === undefined ? undefined : ts7.isArrayLiteralExpression(requiredAdaptersExpr) && requiredAdaptersExpr.elements.every((element) => ts7.isStringLiteral(element) && element.text.trim().length > 0) ? requiredAdaptersExpr.elements.map((element) => element.text) : undefined;
19692
+ if (requiredAdaptersExpr !== undefined && requiredAdapters === undefined)
19693
+ return invalid();
19694
+ const requiredAdapterScopes = requiredAdapterScopesExpr === undefined ? undefined : ts7.isObjectLiteralExpression(requiredAdapterScopesExpr) && requiredAdapterScopesExpr.properties.every((property) => ts7.isPropertyAssignment(property) && !ts7.isComputedPropertyName(property.name) && ts7.isStringLiteral(property.initializer) && isScope(property.initializer.text)) ? Object.fromEntries(requiredAdapterScopesExpr.properties.map((property) => [
19695
+ propertyName(property.name),
19696
+ property.initializer.getText().slice(1, -1)
19697
+ ])) : undefined;
19698
+ if (requiredAdapterScopesExpr !== undefined && requiredAdapterScopes === undefined)
19699
+ return invalid();
19619
19700
  const states = [];
19620
19701
  for (const state of statesExpr.elements) {
19621
19702
  if (!ts7.isStringLiteral(state))
@@ -19671,6 +19752,9 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
19671
19752
  states,
19672
19753
  ...database === undefined ? {} : { database },
19673
19754
  ...tenantField === undefined ? {} : { tenantField },
19755
+ ...readPermission === undefined ? {} : { readPermission },
19756
+ ...requiredAdapters === undefined ? {} : { requiredAdapters },
19757
+ ...requiredAdapterScopes === undefined ? {} : { requiredAdapterScopes },
19674
19758
  transitions,
19675
19759
  file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
19676
19760
  line: lineOf(input)
@@ -20607,6 +20691,9 @@ function parseController(input, ctx) {
20607
20691
  const optionsArg = args[1];
20608
20692
  const optionsObject = resolveStaticObjectLiteral(optionsArg, ctx);
20609
20693
  if (optionsObject) {
20694
+ const permission = stringLiteralProp(optionsObject, "permission");
20695
+ if (permission !== undefined)
20696
+ route.permission = permission;
20610
20697
  const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
20611
20698
  const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
20612
20699
  const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
@@ -24688,10 +24775,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
24688
24775
  import { resolve as resolve17 } from "node:path";
24689
24776
 
24690
24777
  // src/migration-app-version.ts
24691
- var MIGRATION_APP_VERSION = "0.31.1";
24778
+ var MIGRATION_APP_VERSION = "0.32.0";
24692
24779
 
24693
24780
  // src/migration-elysia-version.ts
24694
- var MIGRATION_ELYSIA_VERSION = "0.35.2";
24781
+ var MIGRATION_ELYSIA_VERSION = "0.36.0";
24695
24782
 
24696
24783
  // src/migration-policy.ts
24697
24784
  var cachedCompilerVersion;
@@ -1 +1 @@
1
- export declare const MIGRATION_APP_VERSION = "0.31.1";
1
+ export declare const MIGRATION_APP_VERSION = "0.32.0";
@@ -1 +1 @@
1
- export declare const MIGRATION_ELYSIA_VERSION = "0.35.2";
1
+ export declare const MIGRATION_ELYSIA_VERSION = "0.36.0";
package/dist/types.d.ts CHANGED
@@ -201,6 +201,7 @@ export interface EffectRouteNode {
201
201
  timeoutMs?: number;
202
202
  }
203
203
  export interface RouteNode {
204
+ permission?: string;
204
205
  /** Transport-level idempotency contract, independent of route command binding. */
205
206
  idempotency?: "required" | "none";
206
207
  /** Explicit query cache tags exposed to generated clients. */
@@ -344,6 +345,7 @@ export interface JobNode {
344
345
  export interface QueryNode {
345
346
  className: string;
346
347
  name: string;
348
+ permission?: string;
347
349
  tenantScope?: "required" | "none";
348
350
  }
349
351
  export interface FeatureTransitionNode {
@@ -382,6 +384,9 @@ export interface FeatureSpecNode {
382
384
  states: string[];
383
385
  database?: FeatureDatabaseSourceNode;
384
386
  tenantField?: string;
387
+ readPermission?: string;
388
+ requiredAdapters?: string[];
389
+ requiredAdapterScopes?: Record<string, Scope>;
385
390
  transitions: FeatureTransitionNode[];
386
391
  file?: string;
387
392
  line?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.50.1",
3
+ "version": "0.51.0",
4
4
  "description": "Static compiler for @supacloud/app metadata: builds the application graph from AST, validates it, and generates reflection-free factory code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",