@supacloud/compiler 0.50.0 → 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 +110 -10
- package/dist/contract-manifest.d.ts +1 -0
- package/dist/index.js +110 -10
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
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
|
|
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
|
}
|
|
@@ -390,6 +434,17 @@ function validateFeatureSpec(spec, module) {
|
|
|
390
434
|
}
|
|
391
435
|
}
|
|
392
436
|
}
|
|
437
|
+
const declaredTags = module ? new Set(module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.tags ?? []))) : new Set;
|
|
438
|
+
if (transition.persistence !== undefined && declaredTags.size > 0 && transition.invalidateTags === undefined) {
|
|
439
|
+
error("feature-invalidation-tags-required", `Persistent transition ${transition.name} must declare invalidateTags when Feature ${spec.name} exposes query tags.`);
|
|
440
|
+
}
|
|
441
|
+
if (transition.invalidateTags !== undefined && declaredTags.size > 0) {
|
|
442
|
+
for (const tag of transition.invalidateTags) {
|
|
443
|
+
if (!declaredTags.has(tag)) {
|
|
444
|
+
error("feature-invalidation-tag-unresolved", `Transition ${transition.name} invalidates '${tag}', but Feature ${spec.name} does not declare that query tag.`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
393
448
|
if (transition.route) {
|
|
394
449
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
395
450
|
if (routes.length !== 1) {
|
|
@@ -1437,15 +1492,26 @@ var init_validate = __esm(() => {
|
|
|
1437
1492
|
"duplicate-feature-transition": { code: "SC6102", docsUrl: "https://supacloud.dev/errors/SC6102" },
|
|
1438
1493
|
"invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
|
|
1439
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" },
|
|
1440
1500
|
"feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
|
|
1441
1501
|
"feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
|
|
1442
1502
|
"feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
|
|
1503
|
+
"feature-invalidation-tag-unresolved": { code: "SC6113", docsUrl: "https://supacloud.dev/errors/SC6113" },
|
|
1443
1504
|
"feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
|
|
1444
|
-
"feature-tenant-boundary-drift": { code: "
|
|
1505
|
+
"feature-tenant-boundary-drift": { code: "SC6127", docsUrl: "https://supacloud.dev/errors/SC6127" },
|
|
1445
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" },
|
|
1446
1510
|
"feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
|
|
1447
1511
|
"feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
|
|
1448
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" },
|
|
1449
1515
|
"feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
|
|
1450
1516
|
"feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
|
|
1451
1517
|
"feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
|
|
@@ -1454,6 +1520,7 @@ var init_validate = __esm(() => {
|
|
|
1454
1520
|
"feature-database-source-invalid": { code: "SC6123", docsUrl: "https://supacloud.dev/errors/SC6123" },
|
|
1455
1521
|
"feature-database-table-invalid": { code: "SC6124", docsUrl: "https://supacloud.dev/errors/SC6124" },
|
|
1456
1522
|
"feature-database-row-type-invalid": { code: "SC6125", docsUrl: "https://supacloud.dev/errors/SC6125" },
|
|
1523
|
+
"feature-invalidation-tags-required": { code: "SC6126", docsUrl: "https://supacloud.dev/errors/SC6126" },
|
|
1457
1524
|
"feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
|
|
1458
1525
|
"feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
|
|
1459
1526
|
"invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
|
|
@@ -1509,10 +1576,13 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1509
1576
|
});
|
|
1510
1577
|
}
|
|
1511
1578
|
for (const query of module.queries) {
|
|
1579
|
+
if (query.permission)
|
|
1580
|
+
permissions.add(query.permission);
|
|
1512
1581
|
queries.push({
|
|
1513
1582
|
name: query.name,
|
|
1514
1583
|
className: query.className,
|
|
1515
1584
|
module: module.name,
|
|
1585
|
+
...query.permission === undefined ? {} : { permission: query.permission },
|
|
1516
1586
|
...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
|
|
1517
1587
|
});
|
|
1518
1588
|
}
|
|
@@ -1536,6 +1606,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1536
1606
|
states: [...module.featureSpec.states],
|
|
1537
1607
|
...module.featureSpec.database === undefined ? {} : { database: module.featureSpec.database },
|
|
1538
1608
|
...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
|
|
1609
|
+
...module.featureSpec.readPermission === undefined ? {} : { readPermission: module.featureSpec.readPermission },
|
|
1539
1610
|
transitions: module.featureSpec.transitions.map((transition) => {
|
|
1540
1611
|
const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
|
|
1541
1612
|
return {
|
|
@@ -1577,8 +1648,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1577
1648
|
const governedCommand = command ?? featureCommand;
|
|
1578
1649
|
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
1579
1650
|
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1580
|
-
|
|
1581
|
-
|
|
1651
|
+
const permission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
|
|
1652
|
+
if (permission)
|
|
1653
|
+
permissions.add(permission);
|
|
1582
1654
|
if (route.contract?.evidence)
|
|
1583
1655
|
fixtures.add(route.contract.evidence);
|
|
1584
1656
|
routes.push({
|
|
@@ -1588,7 +1660,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1588
1660
|
controller: controller.className,
|
|
1589
1661
|
handler: route.handler,
|
|
1590
1662
|
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
1591
|
-
...
|
|
1663
|
+
...permission === undefined ? {} : { permission },
|
|
1592
1664
|
...idempotency === undefined ? {} : { idempotency },
|
|
1593
1665
|
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
1594
1666
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
@@ -2650,6 +2722,7 @@ function renderClient(graph, options) {
|
|
|
2650
2722
|
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
2651
2723
|
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
|
|
2652
2724
|
const governedCommand = command ?? featureCommand;
|
|
2725
|
+
const routePermission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
|
|
2653
2726
|
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
2654
2727
|
const transitionRoute = `${route.method} ${fullPath}`;
|
|
2655
2728
|
const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
|
|
@@ -2676,6 +2749,7 @@ function renderClient(graph, options) {
|
|
|
2676
2749
|
["parse", route.parse],
|
|
2677
2750
|
["allowDeleteBody", route.allowDeleteBody],
|
|
2678
2751
|
["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
|
|
2752
|
+
["permission", routePermission],
|
|
2679
2753
|
["tags", route.tags],
|
|
2680
2754
|
["invalidateTags", invalidationTags]
|
|
2681
2755
|
]) {
|
|
@@ -2717,6 +2791,7 @@ function renderClient(graph, options) {
|
|
|
2717
2791
|
path: fullPath,
|
|
2718
2792
|
controller: controller.className,
|
|
2719
2793
|
handler: route.handler,
|
|
2794
|
+
...routePermission === undefined ? {} : { permission: routePermission },
|
|
2720
2795
|
kind: procedureKind,
|
|
2721
2796
|
idempotency: idempotencyMode,
|
|
2722
2797
|
...route.tags === undefined ? {} : { tags: route.tags },
|
|
@@ -2729,7 +2804,7 @@ function renderClient(graph, options) {
|
|
|
2729
2804
|
handler: route.handler,
|
|
2730
2805
|
kind: procedureKind,
|
|
2731
2806
|
...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
|
|
2732
|
-
...
|
|
2807
|
+
...routePermission === undefined ? {} : { permission: routePermission },
|
|
2733
2808
|
idempotency: idempotencyMode,
|
|
2734
2809
|
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
2735
2810
|
...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
|
|
@@ -2785,6 +2860,7 @@ ${procedureMethods.join(`
|
|
|
2785
2860
|
" controller: string;",
|
|
2786
2861
|
" handler: string;",
|
|
2787
2862
|
" kind: Kind;",
|
|
2863
|
+
" permission?: string;",
|
|
2788
2864
|
" idempotency: Idempotency;",
|
|
2789
2865
|
" tags?: readonly string[];",
|
|
2790
2866
|
" invalidateTags?: readonly string[];",
|
|
@@ -3423,7 +3499,7 @@ function renderOpenApi(graph, options) {
|
|
|
3423
3499
|
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
|
|
3424
3500
|
const governedCommand = command ?? featureCommand;
|
|
3425
3501
|
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
3426
|
-
const permission = governedCommand?.permission ?? featureTransition?.permission;
|
|
3502
|
+
const permission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
|
|
3427
3503
|
const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
|
|
3428
3504
|
const responseKind = route.contract?.response;
|
|
3429
3505
|
const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
|
|
@@ -3876,6 +3952,10 @@ function renderPermissions(graph) {
|
|
|
3876
3952
|
if (command.permission)
|
|
3877
3953
|
permissions.add(command.permission);
|
|
3878
3954
|
}
|
|
3955
|
+
for (const query of module.queries) {
|
|
3956
|
+
if (query.permission)
|
|
3957
|
+
permissions.add(query.permission);
|
|
3958
|
+
}
|
|
3879
3959
|
for (const controller of module.controllers) {
|
|
3880
3960
|
for (const route of controller.routes) {
|
|
3881
3961
|
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
@@ -3883,7 +3963,7 @@ function renderPermissions(graph) {
|
|
|
3883
3963
|
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
3884
3964
|
const governedCommand = command ?? featureCommand;
|
|
3885
3965
|
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
3886
|
-
const perm = governedCommand?.permission ?? featureTransition?.permission;
|
|
3966
|
+
const perm = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
|
|
3887
3967
|
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
3888
3968
|
if (perm)
|
|
3889
3969
|
permissions.add(perm);
|
|
@@ -19575,10 +19655,12 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19575
19655
|
if (queryDec) {
|
|
19576
19656
|
const meta = decoratorObjectArg(queryDec);
|
|
19577
19657
|
if (meta) {
|
|
19658
|
+
const permission = stringLiteralProp(meta, "permission");
|
|
19578
19659
|
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19579
19660
|
queries.push({
|
|
19580
19661
|
className: cls.name?.text ?? "<anonymous>",
|
|
19581
19662
|
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
19663
|
+
...permission === undefined ? {} : { permission },
|
|
19582
19664
|
...tenantScope === undefined ? {} : { tenantScope }
|
|
19583
19665
|
});
|
|
19584
19666
|
}
|
|
@@ -19639,11 +19721,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19639
19721
|
if (databaseExpr !== undefined && database === undefined)
|
|
19640
19722
|
return invalid();
|
|
19641
19723
|
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19724
|
+
const readPermission = stringLiteralProp(input, "readPermission");
|
|
19725
|
+
const requiredAdaptersExpr = getProp(input, "requiredAdapters");
|
|
19726
|
+
const requiredAdapterScopesExpr = getProp(input, "requiredAdapterScopes");
|
|
19642
19727
|
const statesExpr = getProp(input, "states");
|
|
19643
19728
|
const transitionObject = getProp(input, "transitions");
|
|
19644
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))) {
|
|
19645
19730
|
return invalid();
|
|
19646
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();
|
|
19647
19741
|
const states = [];
|
|
19648
19742
|
for (const state of statesExpr.elements) {
|
|
19649
19743
|
if (!ts7.isStringLiteral(state))
|
|
@@ -19699,6 +19793,9 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19699
19793
|
states,
|
|
19700
19794
|
...database === undefined ? {} : { database },
|
|
19701
19795
|
...tenantField === undefined ? {} : { tenantField },
|
|
19796
|
+
...readPermission === undefined ? {} : { readPermission },
|
|
19797
|
+
...requiredAdapters === undefined ? {} : { requiredAdapters },
|
|
19798
|
+
...requiredAdapterScopes === undefined ? {} : { requiredAdapterScopes },
|
|
19702
19799
|
transitions,
|
|
19703
19800
|
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
19704
19801
|
line: lineOf(input)
|
|
@@ -20635,6 +20732,9 @@ function parseController(input, ctx) {
|
|
|
20635
20732
|
const optionsArg = args[1];
|
|
20636
20733
|
const optionsObject = resolveStaticObjectLiteral(optionsArg, ctx);
|
|
20637
20734
|
if (optionsObject) {
|
|
20735
|
+
const permission = stringLiteralProp(optionsObject, "permission");
|
|
20736
|
+
if (permission !== undefined)
|
|
20737
|
+
route.permission = permission;
|
|
20638
20738
|
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
20639
20739
|
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
20640
20740
|
const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
|
|
@@ -24993,10 +25093,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24993
25093
|
import { resolve as resolve18 } from "node:path";
|
|
24994
25094
|
|
|
24995
25095
|
// src/migration-app-version.ts
|
|
24996
|
-
var MIGRATION_APP_VERSION = "0.
|
|
25096
|
+
var MIGRATION_APP_VERSION = "0.32.0";
|
|
24997
25097
|
|
|
24998
25098
|
// src/migration-elysia-version.ts
|
|
24999
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
25099
|
+
var MIGRATION_ELYSIA_VERSION = "0.36.0";
|
|
25000
25100
|
|
|
25001
25101
|
// src/migration-policy.ts
|
|
25002
25102
|
var cachedCompilerVersion;
|
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
|
|
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
|
}
|
|
@@ -389,6 +433,17 @@ function validateFeatureSpec(spec, module) {
|
|
|
389
433
|
}
|
|
390
434
|
}
|
|
391
435
|
}
|
|
436
|
+
const declaredTags = module ? new Set(module.controllers.flatMap((controller) => controller.routes.flatMap((route) => route.tags ?? []))) : new Set;
|
|
437
|
+
if (transition.persistence !== undefined && declaredTags.size > 0 && transition.invalidateTags === undefined) {
|
|
438
|
+
error("feature-invalidation-tags-required", `Persistent transition ${transition.name} must declare invalidateTags when Feature ${spec.name} exposes query tags.`);
|
|
439
|
+
}
|
|
440
|
+
if (transition.invalidateTags !== undefined && declaredTags.size > 0) {
|
|
441
|
+
for (const tag of transition.invalidateTags) {
|
|
442
|
+
if (!declaredTags.has(tag)) {
|
|
443
|
+
error("feature-invalidation-tag-unresolved", `Transition ${transition.name} invalidates '${tag}', but Feature ${spec.name} does not declare that query tag.`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
392
447
|
if (transition.route) {
|
|
393
448
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
394
449
|
if (routes.length !== 1) {
|
|
@@ -1436,15 +1491,26 @@ var init_validate = __esm(() => {
|
|
|
1436
1491
|
"duplicate-feature-transition": { code: "SC6102", docsUrl: "https://supacloud.dev/errors/SC6102" },
|
|
1437
1492
|
"invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
|
|
1438
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" },
|
|
1439
1499
|
"feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
|
|
1440
1500
|
"feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
|
|
1441
1501
|
"feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
|
|
1502
|
+
"feature-invalidation-tag-unresolved": { code: "SC6113", docsUrl: "https://supacloud.dev/errors/SC6113" },
|
|
1442
1503
|
"feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
|
|
1443
|
-
"feature-tenant-boundary-drift": { code: "
|
|
1504
|
+
"feature-tenant-boundary-drift": { code: "SC6127", docsUrl: "https://supacloud.dev/errors/SC6127" },
|
|
1444
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" },
|
|
1445
1509
|
"feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
|
|
1446
1510
|
"feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
|
|
1447
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" },
|
|
1448
1514
|
"feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
|
|
1449
1515
|
"feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
|
|
1450
1516
|
"feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
|
|
@@ -1453,6 +1519,7 @@ var init_validate = __esm(() => {
|
|
|
1453
1519
|
"feature-database-source-invalid": { code: "SC6123", docsUrl: "https://supacloud.dev/errors/SC6123" },
|
|
1454
1520
|
"feature-database-table-invalid": { code: "SC6124", docsUrl: "https://supacloud.dev/errors/SC6124" },
|
|
1455
1521
|
"feature-database-row-type-invalid": { code: "SC6125", docsUrl: "https://supacloud.dev/errors/SC6125" },
|
|
1522
|
+
"feature-invalidation-tags-required": { code: "SC6126", docsUrl: "https://supacloud.dev/errors/SC6126" },
|
|
1456
1523
|
"feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
|
|
1457
1524
|
"feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
|
|
1458
1525
|
"invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
|
|
@@ -1508,10 +1575,13 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1508
1575
|
});
|
|
1509
1576
|
}
|
|
1510
1577
|
for (const query of module.queries) {
|
|
1578
|
+
if (query.permission)
|
|
1579
|
+
permissions.add(query.permission);
|
|
1511
1580
|
queries.push({
|
|
1512
1581
|
name: query.name,
|
|
1513
1582
|
className: query.className,
|
|
1514
1583
|
module: module.name,
|
|
1584
|
+
...query.permission === undefined ? {} : { permission: query.permission },
|
|
1515
1585
|
...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
|
|
1516
1586
|
});
|
|
1517
1587
|
}
|
|
@@ -1535,6 +1605,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1535
1605
|
states: [...module.featureSpec.states],
|
|
1536
1606
|
...module.featureSpec.database === undefined ? {} : { database: module.featureSpec.database },
|
|
1537
1607
|
...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
|
|
1608
|
+
...module.featureSpec.readPermission === undefined ? {} : { readPermission: module.featureSpec.readPermission },
|
|
1538
1609
|
transitions: module.featureSpec.transitions.map((transition) => {
|
|
1539
1610
|
const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
|
|
1540
1611
|
return {
|
|
@@ -1576,8 +1647,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1576
1647
|
const governedCommand = command ?? featureCommand;
|
|
1577
1648
|
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
1578
1649
|
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1579
|
-
|
|
1580
|
-
|
|
1650
|
+
const permission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
|
|
1651
|
+
if (permission)
|
|
1652
|
+
permissions.add(permission);
|
|
1581
1653
|
if (route.contract?.evidence)
|
|
1582
1654
|
fixtures.add(route.contract.evidence);
|
|
1583
1655
|
routes.push({
|
|
@@ -1587,7 +1659,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1587
1659
|
controller: controller.className,
|
|
1588
1660
|
handler: route.handler,
|
|
1589
1661
|
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
1590
|
-
...
|
|
1662
|
+
...permission === undefined ? {} : { permission },
|
|
1591
1663
|
...idempotency === undefined ? {} : { idempotency },
|
|
1592
1664
|
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
1593
1665
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
@@ -2649,6 +2721,7 @@ function renderClient(graph, options) {
|
|
|
2649
2721
|
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
2650
2722
|
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
|
|
2651
2723
|
const governedCommand = command ?? featureCommand;
|
|
2724
|
+
const routePermission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
|
|
2652
2725
|
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
2653
2726
|
const transitionRoute = `${route.method} ${fullPath}`;
|
|
2654
2727
|
const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
|
|
@@ -2675,6 +2748,7 @@ function renderClient(graph, options) {
|
|
|
2675
2748
|
["parse", route.parse],
|
|
2676
2749
|
["allowDeleteBody", route.allowDeleteBody],
|
|
2677
2750
|
["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
|
|
2751
|
+
["permission", routePermission],
|
|
2678
2752
|
["tags", route.tags],
|
|
2679
2753
|
["invalidateTags", invalidationTags]
|
|
2680
2754
|
]) {
|
|
@@ -2716,6 +2790,7 @@ function renderClient(graph, options) {
|
|
|
2716
2790
|
path: fullPath,
|
|
2717
2791
|
controller: controller.className,
|
|
2718
2792
|
handler: route.handler,
|
|
2793
|
+
...routePermission === undefined ? {} : { permission: routePermission },
|
|
2719
2794
|
kind: procedureKind,
|
|
2720
2795
|
idempotency: idempotencyMode,
|
|
2721
2796
|
...route.tags === undefined ? {} : { tags: route.tags },
|
|
@@ -2728,7 +2803,7 @@ function renderClient(graph, options) {
|
|
|
2728
2803
|
handler: route.handler,
|
|
2729
2804
|
kind: procedureKind,
|
|
2730
2805
|
...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
|
|
2731
|
-
...
|
|
2806
|
+
...routePermission === undefined ? {} : { permission: routePermission },
|
|
2732
2807
|
idempotency: idempotencyMode,
|
|
2733
2808
|
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
2734
2809
|
...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
|
|
@@ -2784,6 +2859,7 @@ ${procedureMethods.join(`
|
|
|
2784
2859
|
" controller: string;",
|
|
2785
2860
|
" handler: string;",
|
|
2786
2861
|
" kind: Kind;",
|
|
2862
|
+
" permission?: string;",
|
|
2787
2863
|
" idempotency: Idempotency;",
|
|
2788
2864
|
" tags?: readonly string[];",
|
|
2789
2865
|
" invalidateTags?: readonly string[];",
|
|
@@ -3422,7 +3498,7 @@ function renderOpenApi(graph, options) {
|
|
|
3422
3498
|
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
|
|
3423
3499
|
const governedCommand = command ?? featureCommand;
|
|
3424
3500
|
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
3425
|
-
const permission = governedCommand?.permission ?? featureTransition?.permission;
|
|
3501
|
+
const permission = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
|
|
3426
3502
|
const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
|
|
3427
3503
|
const responseKind = route.contract?.response;
|
|
3428
3504
|
const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
|
|
@@ -3875,6 +3951,10 @@ function renderPermissions(graph) {
|
|
|
3875
3951
|
if (command.permission)
|
|
3876
3952
|
permissions.add(command.permission);
|
|
3877
3953
|
}
|
|
3954
|
+
for (const query of module.queries) {
|
|
3955
|
+
if (query.permission)
|
|
3956
|
+
permissions.add(query.permission);
|
|
3957
|
+
}
|
|
3878
3958
|
for (const controller of module.controllers) {
|
|
3879
3959
|
for (const route of controller.routes) {
|
|
3880
3960
|
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
@@ -3882,7 +3962,7 @@ function renderPermissions(graph) {
|
|
|
3882
3962
|
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
3883
3963
|
const governedCommand = command ?? featureCommand;
|
|
3884
3964
|
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
3885
|
-
const perm = governedCommand?.permission ?? featureTransition?.permission;
|
|
3965
|
+
const perm = route.permission ?? governedCommand?.permission ?? featureTransition?.permission;
|
|
3886
3966
|
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
3887
3967
|
if (perm)
|
|
3888
3968
|
permissions.add(perm);
|
|
@@ -19534,10 +19614,12 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19534
19614
|
if (queryDec) {
|
|
19535
19615
|
const meta = decoratorObjectArg(queryDec);
|
|
19536
19616
|
if (meta) {
|
|
19617
|
+
const permission = stringLiteralProp(meta, "permission");
|
|
19537
19618
|
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19538
19619
|
queries.push({
|
|
19539
19620
|
className: cls.name?.text ?? "<anonymous>",
|
|
19540
19621
|
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
19622
|
+
...permission === undefined ? {} : { permission },
|
|
19541
19623
|
...tenantScope === undefined ? {} : { tenantScope }
|
|
19542
19624
|
});
|
|
19543
19625
|
}
|
|
@@ -19598,11 +19680,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19598
19680
|
if (databaseExpr !== undefined && database === undefined)
|
|
19599
19681
|
return invalid();
|
|
19600
19682
|
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19683
|
+
const readPermission = stringLiteralProp(input, "readPermission");
|
|
19684
|
+
const requiredAdaptersExpr = getProp(input, "requiredAdapters");
|
|
19685
|
+
const requiredAdapterScopesExpr = getProp(input, "requiredAdapterScopes");
|
|
19601
19686
|
const statesExpr = getProp(input, "states");
|
|
19602
19687
|
const transitionObject = getProp(input, "transitions");
|
|
19603
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))) {
|
|
19604
19689
|
return invalid();
|
|
19605
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();
|
|
19606
19700
|
const states = [];
|
|
19607
19701
|
for (const state of statesExpr.elements) {
|
|
19608
19702
|
if (!ts7.isStringLiteral(state))
|
|
@@ -19658,6 +19752,9 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19658
19752
|
states,
|
|
19659
19753
|
...database === undefined ? {} : { database },
|
|
19660
19754
|
...tenantField === undefined ? {} : { tenantField },
|
|
19755
|
+
...readPermission === undefined ? {} : { readPermission },
|
|
19756
|
+
...requiredAdapters === undefined ? {} : { requiredAdapters },
|
|
19757
|
+
...requiredAdapterScopes === undefined ? {} : { requiredAdapterScopes },
|
|
19661
19758
|
transitions,
|
|
19662
19759
|
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
19663
19760
|
line: lineOf(input)
|
|
@@ -20594,6 +20691,9 @@ function parseController(input, ctx) {
|
|
|
20594
20691
|
const optionsArg = args[1];
|
|
20595
20692
|
const optionsObject = resolveStaticObjectLiteral(optionsArg, ctx);
|
|
20596
20693
|
if (optionsObject) {
|
|
20694
|
+
const permission = stringLiteralProp(optionsObject, "permission");
|
|
20695
|
+
if (permission !== undefined)
|
|
20696
|
+
route.permission = permission;
|
|
20597
20697
|
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
20598
20698
|
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
20599
20699
|
const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
|
|
@@ -24675,10 +24775,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24675
24775
|
import { resolve as resolve17 } from "node:path";
|
|
24676
24776
|
|
|
24677
24777
|
// src/migration-app-version.ts
|
|
24678
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24778
|
+
var MIGRATION_APP_VERSION = "0.32.0";
|
|
24679
24779
|
|
|
24680
24780
|
// src/migration-elysia-version.ts
|
|
24681
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24781
|
+
var MIGRATION_ELYSIA_VERSION = "0.36.0";
|
|
24682
24782
|
|
|
24683
24783
|
// src/migration-policy.ts
|
|
24684
24784
|
var cachedCompilerVersion;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MIGRATION_APP_VERSION = "0.
|
|
1
|
+
export declare const MIGRATION_APP_VERSION = "0.32.0";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MIGRATION_ELYSIA_VERSION = "0.
|
|
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.
|
|
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",
|