-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathExpressionProvider.tsx
More file actions
130 lines (120 loc) · 5.1 KB
/
Copy pathExpressionProvider.tsx
File metadata and controls
130 lines (120 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* ExpressionContext Provider
*
* Provides expression evaluation context (user, app, data) to all child components.
* Used by useCondition/useExpression hooks from @object-ui/react to evaluate
* dynamic visibility, disabled, and hidden expressions in navigation items,
* fields, and components.
*
* @example
* ```tsx
* <ExpressionProvider user={currentUser} app={activeApp}>
* <AppSidebar />
* </ExpressionProvider>
* ```
*/
import React, { createContext, useContext, useMemo } from 'react';
import { ExpressionEvaluator } from '@object-ui/core';
import { PredicateScopeProvider } from '@object-ui/react';
export interface ExpressionContextValue {
/** Current authenticated user */
user: Record<string, any>;
/** Active application config */
app: Record<string, any>;
/** Additional data scope */
data: Record<string, any>;
/**
* Deployment-level feature flags surfaced by `/api/v1/auth/config`
* (e.g. `multiOrgEnabled`). Used by CEL/template predicates on
* metadata actions and views to hide entries that would otherwise
* hit a forbidden endpoint. Empty `{}` when auth config hasn't
* loaded yet — predicates should default to "visible" in that case
* (see `sys_organization.create_organization.visible`).
*/
features: Record<string, any>;
/** The evaluator instance (for imperative use) */
evaluator: ExpressionEvaluator;
}
const ExprCtx = createContext<ExpressionContextValue | null>(null);
interface ExpressionProviderProps {
children: React.ReactNode;
user?: Record<string, any>;
app?: Record<string, any>;
data?: Record<string, any>;
features?: Record<string, any>;
}
export function ExpressionProvider({ children, user = {}, app = {}, data = {}, features = {} }: ExpressionProviderProps) {
const value = useMemo(() => {
// ADR-0068: expose the SAME user object under the canonical `current_user`
// plus the back-compat `user` alias, the server-RLS-parity `ctx.user`
// alias, and the server-CEL-parity `os.user` alias (the spec's canonical
// identity scope — `{{os.user.id}}` per @objectstack/spec expression docs),
// so a predicate authored against any one form evaluates identically on
// client, server-formula, and server-RLS (#2358 trap 1).
const context = { current_user: user, user, ctx: { user }, os: { user }, app, data, features };
const evaluator = new ExpressionEvaluator(context);
return { user, app, data, features, evaluator };
}, [user, app, data, features]);
// Also feed the predicate scope used by useCondition/useExpression in
// @object-ui/react so action visibility predicates (e.g. on toolbar
// buttons) can see deployment-level flags like features.multiOrgEnabled.
// Mirror the canonical `current_user`/`user`/`ctx.user`/`os.user` aliases
// here too.
const scope = useMemo(
() => ({ current_user: user, user, ctx: { user }, os: { user }, app, data, features }),
[user, app, data, features],
);
return (
<ExprCtx.Provider value={value}>
<PredicateScopeProvider scope={scope}>{children}</PredicateScopeProvider>
</ExprCtx.Provider>
);
}
/**
* Hook to access the expression context.
* Returns the full context value or a default empty context.
*/
export function useExpressionContext(): ExpressionContextValue {
const ctx = useContext(ExprCtx);
if (!ctx) {
// Return a safe default so components can be used outside the provider
const fallback = { user: {}, app: {}, data: {}, features: {} };
const evalContext = { current_user: {}, ctx: { user: {} }, os: { user: {} }, ...fallback };
return { ...fallback, evaluator: new ExpressionEvaluator(evalContext) };
}
return ctx;
}
/**
* Evaluate a visibility expression.
* Supports:
* - boolean: true/false
* - string "true"/"false"
* - template expression: "${user.role === 'admin'}"
* - `{ dialect: 'cel', source }` envelopes — the shape the spec's
* `ExpressionInputSchema` normalizes every authored `visible` string into,
* which is what nav/area items carry after the server serves the app schema
* - bare expression strings (evaluated as one boolean expression)
*
* Everything non-literal is delegated to `evaluator.evaluateCondition`, which
* routes CEL envelopes to the canonical `@objectstack/formula` engine. The
* envelope and bare-string shapes used to fall through to a blanket
* `return true`, so a constant-false nav `visible` predicate (e.g.
* ``P`'org_admin' in current_user.positions` ``) still rendered for everyone —
* the app author had no working way to hide a menu item by role.
*
* Returns true if the item should be visible (fail-open on evaluation errors,
* matching the evaluator's own default).
*/
export function evaluateVisibility(
expression: string | boolean | { dialect?: string; source?: string } | undefined,
evaluator: ExpressionEvaluator,
): boolean {
if (expression === undefined || expression === null) return true;
if (expression === true || expression === 'true') return true;
if (expression === false || expression === 'false') return false;
try {
return evaluator.evaluateCondition(expression);
} catch {
return true; // Default to visible on error
}
}