@i4e/invest4edu-access-core 0.32.0 → 0.34.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/README.md +126 -126
- package/package.json +58 -56
- package/src/access-config.js +68 -68
- package/src/access-resolver.js +269 -269
- package/src/access-schema.js +174 -174
- package/src/credits.js +128 -128
- package/src/entitlement-schema.js +194 -194
- package/src/entitlement-store.d.ts +99 -99
- package/src/entitlement-store.js +610 -610
- package/src/entitlement.js +300 -300
- package/src/grid-schema.js +231 -231
- package/src/index.js +84 -74
- package/src/proration.js +155 -155
- package/src/report-audience.js +226 -0
- package/src/reportee-tree.js +129 -129
- package/src/role-capabilities.js +93 -93
- package/src/route-features.js +292 -292
- package/src/route-screen.js +45 -0
- package/src/subscription-lifecycle.js +106 -106
- package/src/tenant-context.js +26 -26
- package/src/tenant-plugin.js +177 -177
- package/src/visible-when.js +108 -108
package/src/visible-when.js
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `visibleWhen` — ONE predicate language, ONE evaluator.
|
|
3
|
-
* @i4e/invest4edu-access-core (unified access framework, P1)
|
|
4
|
-
*
|
|
5
|
-
* The same function gates nav items, screen actions, grid columns, filters, row/bulk actions,
|
|
6
|
-
* exports and summary metrics. That is what makes "driven by product and roles" true across the
|
|
7
|
-
* whole app instead of per-screen — and it means a rule is written once, in data, not in 139
|
|
8
|
-
* hardcoded predicates.
|
|
9
|
-
*
|
|
10
|
-
* Client-side evaluation is UX ONLY. Servers MUST re-evaluate before returning data, projecting
|
|
11
|
-
* columns, or building an export/summary — never trust a client-supplied column list.
|
|
12
|
-
*
|
|
13
|
-
* Design: TDD-unified-access-and-subscription.md §8.3
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { hasFeature, isUsable } from "./access-resolver.js";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Shape (every key optional; all present keys must pass — AND):
|
|
20
|
-
* {
|
|
21
|
-
* features: ["ORDERS.LIST"] // caller must hold all of these
|
|
22
|
-
* actions: ["ORDERS.LIST.CANCEL"] // alias of features, reads better on actions
|
|
23
|
-
* entitled: "TOOLS.AI_REPORT" // must be allowed AND entitlement-unlocked
|
|
24
|
-
* products: ["IVP003"] // caller's product scope must intersect
|
|
25
|
-
* distributorLevel: ["D1","D2"]
|
|
26
|
-
* screenType: "openorders" | ["a","b"]
|
|
27
|
-
* roles: ["BROKER_ADMIN"] // LEGACY escape hatch — lint-flagged, being removed
|
|
28
|
-
* anyOf: [ {...}, {...} ] // OR of nested predicates
|
|
29
|
-
* not: { ... } // negation
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* @param {Object|null|undefined} rule absent/empty ⇒ visible (opt-in gating, not opt-out)
|
|
33
|
-
* @param {Object} ctx { snapshot, productScope?, distributorLevel?, screenType?, roles? }
|
|
34
|
-
*/
|
|
35
|
-
export function evaluateVisibleWhen(rule, ctx = {}) {
|
|
36
|
-
if (!rule || typeof rule !== "object") return true;
|
|
37
|
-
|
|
38
|
-
const snapshot = ctx.snapshot || null;
|
|
39
|
-
// A system admin (D27) resolves a fully-open snapshot, so feature/entitlement checks below
|
|
40
|
-
// pass naturally. Product/level predicates still evaluate — an admin seeing a D1-only column
|
|
41
|
-
// labelled for D1 would be misleading, not permissive.
|
|
42
|
-
const productScope = new Set(
|
|
43
|
-
(ctx.productScope || snapshot?.productScope || []).map(String),
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const asArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
47
|
-
|
|
48
|
-
if (rule.features && !asArray(rule.features).every((c) => hasFeature(snapshot, c))) return false;
|
|
49
|
-
if (rule.actions && !asArray(rule.actions).every((c) => hasFeature(snapshot, c))) return false;
|
|
50
|
-
if (rule.entitled && !asArray(rule.entitled).every((c) => isUsable(snapshot, c))) return false;
|
|
51
|
-
|
|
52
|
-
if (rule.products) {
|
|
53
|
-
const want = asArray(rule.products).map(String);
|
|
54
|
-
if (want.length && !want.some((p) => productScope.has(p))) return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (rule.distributorLevel) {
|
|
58
|
-
const lvl = ctx.distributorLevel ? String(ctx.distributorLevel) : null;
|
|
59
|
-
if (!lvl || !asArray(rule.distributorLevel).map(String).includes(lvl)) return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (rule.screenType) {
|
|
63
|
-
const st = ctx.screenType ? String(ctx.screenType) : null;
|
|
64
|
-
if (!st || !asArray(rule.screenType).map(String).includes(st)) return false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// LEGACY: kept only so migration can proceed screen-by-screen. New configs must not use it.
|
|
68
|
-
if (rule.roles) {
|
|
69
|
-
const held = new Set(asArray(ctx.roles || snapshot?.resolved_for?.roles).map(String));
|
|
70
|
-
if (!asArray(rule.roles).map(String).some((r) => held.has(r))) return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (rule.anyOf) {
|
|
74
|
-
const branches = asArray(rule.anyOf);
|
|
75
|
-
if (branches.length && !branches.some((b) => evaluateVisibleWhen(b, ctx))) return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (rule.not && evaluateVisibleWhen(rule.not, ctx)) return false;
|
|
79
|
-
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Filter a list of `{visibleWhen}`-bearing config items (columns, metrics, actions, filters). */
|
|
84
|
-
export function filterVisible(items = [], ctx = {}) {
|
|
85
|
-
return items.filter((i) => evaluateVisibleWhen(i?.visibleWhen, ctx));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Apply `relabelWhen` overrides to a grid column — absorbs the imperative relabelling in
|
|
90
|
-
* order-constants (e.g. d1/d2/d3_payout → "Net Income" for the matching distributor level).
|
|
91
|
-
* First matching rule wins; the column is returned unchanged when nothing matches.
|
|
92
|
-
*/
|
|
93
|
-
export function applyRelabel(column, ctx = {}) {
|
|
94
|
-
if (!column?.relabelWhen?.length) return column;
|
|
95
|
-
for (const r of column.relabelWhen) {
|
|
96
|
-
if (evaluateVisibleWhen(r?.when, ctx)) {
|
|
97
|
-
return { ...column, header: r.header ?? column.header, label: r.label ?? column.label };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return column;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Resolve a full column list: drop hidden ones, apply relabelling, preserve order. */
|
|
104
|
-
export function resolveColumns(columns = [], ctx = {}) {
|
|
105
|
-
return filterVisible(columns, ctx).map((c) => applyRelabel(c, ctx));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export default { evaluateVisibleWhen, filterVisible, applyRelabel, resolveColumns };
|
|
1
|
+
/**
|
|
2
|
+
* `visibleWhen` — ONE predicate language, ONE evaluator.
|
|
3
|
+
* @i4e/invest4edu-access-core (unified access framework, P1)
|
|
4
|
+
*
|
|
5
|
+
* The same function gates nav items, screen actions, grid columns, filters, row/bulk actions,
|
|
6
|
+
* exports and summary metrics. That is what makes "driven by product and roles" true across the
|
|
7
|
+
* whole app instead of per-screen — and it means a rule is written once, in data, not in 139
|
|
8
|
+
* hardcoded predicates.
|
|
9
|
+
*
|
|
10
|
+
* Client-side evaluation is UX ONLY. Servers MUST re-evaluate before returning data, projecting
|
|
11
|
+
* columns, or building an export/summary — never trust a client-supplied column list.
|
|
12
|
+
*
|
|
13
|
+
* Design: TDD-unified-access-and-subscription.md §8.3
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { hasFeature, isUsable } from "./access-resolver.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Shape (every key optional; all present keys must pass — AND):
|
|
20
|
+
* {
|
|
21
|
+
* features: ["ORDERS.LIST"] // caller must hold all of these
|
|
22
|
+
* actions: ["ORDERS.LIST.CANCEL"] // alias of features, reads better on actions
|
|
23
|
+
* entitled: "TOOLS.AI_REPORT" // must be allowed AND entitlement-unlocked
|
|
24
|
+
* products: ["IVP003"] // caller's product scope must intersect
|
|
25
|
+
* distributorLevel: ["D1","D2"]
|
|
26
|
+
* screenType: "openorders" | ["a","b"]
|
|
27
|
+
* roles: ["BROKER_ADMIN"] // LEGACY escape hatch — lint-flagged, being removed
|
|
28
|
+
* anyOf: [ {...}, {...} ] // OR of nested predicates
|
|
29
|
+
* not: { ... } // negation
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* @param {Object|null|undefined} rule absent/empty ⇒ visible (opt-in gating, not opt-out)
|
|
33
|
+
* @param {Object} ctx { snapshot, productScope?, distributorLevel?, screenType?, roles? }
|
|
34
|
+
*/
|
|
35
|
+
export function evaluateVisibleWhen(rule, ctx = {}) {
|
|
36
|
+
if (!rule || typeof rule !== "object") return true;
|
|
37
|
+
|
|
38
|
+
const snapshot = ctx.snapshot || null;
|
|
39
|
+
// A system admin (D27) resolves a fully-open snapshot, so feature/entitlement checks below
|
|
40
|
+
// pass naturally. Product/level predicates still evaluate — an admin seeing a D1-only column
|
|
41
|
+
// labelled for D1 would be misleading, not permissive.
|
|
42
|
+
const productScope = new Set(
|
|
43
|
+
(ctx.productScope || snapshot?.productScope || []).map(String),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const asArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
47
|
+
|
|
48
|
+
if (rule.features && !asArray(rule.features).every((c) => hasFeature(snapshot, c))) return false;
|
|
49
|
+
if (rule.actions && !asArray(rule.actions).every((c) => hasFeature(snapshot, c))) return false;
|
|
50
|
+
if (rule.entitled && !asArray(rule.entitled).every((c) => isUsable(snapshot, c))) return false;
|
|
51
|
+
|
|
52
|
+
if (rule.products) {
|
|
53
|
+
const want = asArray(rule.products).map(String);
|
|
54
|
+
if (want.length && !want.some((p) => productScope.has(p))) return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (rule.distributorLevel) {
|
|
58
|
+
const lvl = ctx.distributorLevel ? String(ctx.distributorLevel) : null;
|
|
59
|
+
if (!lvl || !asArray(rule.distributorLevel).map(String).includes(lvl)) return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (rule.screenType) {
|
|
63
|
+
const st = ctx.screenType ? String(ctx.screenType) : null;
|
|
64
|
+
if (!st || !asArray(rule.screenType).map(String).includes(st)) return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// LEGACY: kept only so migration can proceed screen-by-screen. New configs must not use it.
|
|
68
|
+
if (rule.roles) {
|
|
69
|
+
const held = new Set(asArray(ctx.roles || snapshot?.resolved_for?.roles).map(String));
|
|
70
|
+
if (!asArray(rule.roles).map(String).some((r) => held.has(r))) return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (rule.anyOf) {
|
|
74
|
+
const branches = asArray(rule.anyOf);
|
|
75
|
+
if (branches.length && !branches.some((b) => evaluateVisibleWhen(b, ctx))) return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (rule.not && evaluateVisibleWhen(rule.not, ctx)) return false;
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Filter a list of `{visibleWhen}`-bearing config items (columns, metrics, actions, filters). */
|
|
84
|
+
export function filterVisible(items = [], ctx = {}) {
|
|
85
|
+
return items.filter((i) => evaluateVisibleWhen(i?.visibleWhen, ctx));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Apply `relabelWhen` overrides to a grid column — absorbs the imperative relabelling in
|
|
90
|
+
* order-constants (e.g. d1/d2/d3_payout → "Net Income" for the matching distributor level).
|
|
91
|
+
* First matching rule wins; the column is returned unchanged when nothing matches.
|
|
92
|
+
*/
|
|
93
|
+
export function applyRelabel(column, ctx = {}) {
|
|
94
|
+
if (!column?.relabelWhen?.length) return column;
|
|
95
|
+
for (const r of column.relabelWhen) {
|
|
96
|
+
if (evaluateVisibleWhen(r?.when, ctx)) {
|
|
97
|
+
return { ...column, header: r.header ?? column.header, label: r.label ?? column.label };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return column;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Resolve a full column list: drop hidden ones, apply relabelling, preserve order. */
|
|
104
|
+
export function resolveColumns(columns = [], ctx = {}) {
|
|
105
|
+
return filterVisible(columns, ctx).map((c) => applyRelabel(c, ctx));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default { evaluateVisibleWhen, filterVisible, applyRelabel, resolveColumns };
|