@object-ui/core 0.3.0 → 0.3.1
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/CHANGELOG.md +8 -0
- package/dist/actions/ActionRunner.d.ts +40 -0
- package/dist/actions/ActionRunner.js +160 -0
- package/dist/actions/index.d.ts +8 -0
- package/dist/actions/index.js +8 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +10 -0
- package/dist/builder/schema-builder.d.ts +7 -0
- package/dist/builder/schema-builder.js +4 -6
- package/dist/evaluator/ExpressionContext.d.ts +51 -0
- package/dist/evaluator/ExpressionContext.js +110 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +99 -0
- package/dist/evaluator/ExpressionEvaluator.js +200 -0
- package/dist/evaluator/index.d.ts +9 -0
- package/dist/evaluator/index.js +9 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -1
- package/dist/registry/Registry.d.ts +7 -0
- package/dist/registry/Registry.js +7 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/filter-converter.d.ts +57 -0
- package/dist/utils/filter-converter.js +100 -0
- package/dist/validation/schema-validator.d.ts +7 -0
- package/dist/validation/schema-validator.js +4 -6
- package/package.json +16 -5
- package/src/actions/ActionRunner.ts +195 -0
- package/src/actions/index.ts +9 -0
- package/src/adapters/README.md +180 -0
- package/src/adapters/index.d.ts +8 -0
- package/src/adapters/index.js +10 -0
- package/src/adapters/index.ts +10 -0
- package/src/builder/schema-builder.d.ts +7 -0
- package/src/builder/schema-builder.js +4 -6
- package/src/builder/schema-builder.ts +8 -0
- package/src/evaluator/ExpressionContext.ts +118 -0
- package/src/evaluator/ExpressionEvaluator.ts +248 -0
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
- package/src/evaluator/index.ts +10 -0
- package/src/index.d.ts +9 -0
- package/src/index.js +10 -1
- package/src/index.test.ts +8 -0
- package/src/index.ts +11 -1
- package/src/registry/Registry.d.ts +7 -0
- package/src/registry/Registry.js +7 -0
- package/src/registry/Registry.ts +8 -0
- package/src/types/index.d.ts +7 -0
- package/src/types/index.js +7 -0
- package/src/types/index.ts +8 -0
- package/src/utils/__tests__/filter-converter.test.ts +118 -0
- package/src/utils/filter-converter.d.ts +57 -0
- package/src/utils/filter-converter.js +100 -0
- package/src/utils/filter-converter.ts +133 -0
- package/src/validation/schema-validator.d.ts +7 -0
- package/src/validation/schema-validator.js +4 -6
- package/src/validation/schema-validator.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @object-ui/core - Action Runner
|
|
11
|
+
*
|
|
12
|
+
* Executes actions defined in ActionSchema and EventHandler.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator';
|
|
16
|
+
|
|
17
|
+
export interface ActionResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
data?: any;
|
|
20
|
+
error?: string;
|
|
21
|
+
reload?: boolean;
|
|
22
|
+
close?: boolean;
|
|
23
|
+
redirect?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ActionContext {
|
|
27
|
+
data?: Record<string, any>;
|
|
28
|
+
record?: any;
|
|
29
|
+
user?: any;
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ActionHandler = (
|
|
34
|
+
action: any,
|
|
35
|
+
context: ActionContext
|
|
36
|
+
) => Promise<ActionResult> | ActionResult;
|
|
37
|
+
|
|
38
|
+
export class ActionRunner {
|
|
39
|
+
private handlers = new Map<string, ActionHandler>();
|
|
40
|
+
private evaluator: ExpressionEvaluator;
|
|
41
|
+
private context: ActionContext;
|
|
42
|
+
|
|
43
|
+
constructor(context: ActionContext = {}) {
|
|
44
|
+
this.context = context;
|
|
45
|
+
this.evaluator = new ExpressionEvaluator(context);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
registerHandler(actionName: string, handler: ActionHandler): void {
|
|
49
|
+
this.handlers.set(actionName, handler);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async execute(action: any): Promise<ActionResult> {
|
|
53
|
+
try {
|
|
54
|
+
if (action.condition) {
|
|
55
|
+
const shouldExecute = this.evaluator.evaluateCondition(action.condition);
|
|
56
|
+
if (!shouldExecute) {
|
|
57
|
+
return { success: false, error: 'Action condition not met' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (action.disabled) {
|
|
62
|
+
const isDisabled = typeof action.disabled === 'string'
|
|
63
|
+
? this.evaluator.evaluateCondition(action.disabled)
|
|
64
|
+
: action.disabled;
|
|
65
|
+
|
|
66
|
+
if (isDisabled) {
|
|
67
|
+
return { success: false, error: 'Action is disabled' };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (action.type === 'action' || action.actionType) {
|
|
72
|
+
return await this.executeActionSchema(action);
|
|
73
|
+
} else if (action.type === 'navigation' || action.navigate) {
|
|
74
|
+
return await this.executeNavigation(action);
|
|
75
|
+
} else if (action.type === 'api' || action.api) {
|
|
76
|
+
return await this.executeAPI(action);
|
|
77
|
+
} else if (action.onClick) {
|
|
78
|
+
await action.onClick();
|
|
79
|
+
return { success: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { success: false, error: 'Unknown action type' };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return { success: false, error: (error as Error).message };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async executeActionSchema(action: any): Promise<ActionResult> {
|
|
89
|
+
const result: ActionResult = { success: true };
|
|
90
|
+
|
|
91
|
+
if (action.confirmText) {
|
|
92
|
+
const confirmed = await this.showConfirmation(action.confirmText);
|
|
93
|
+
if (!confirmed) {
|
|
94
|
+
return { success: false, error: 'Action cancelled by user' };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (action.api) {
|
|
99
|
+
const apiResult = await this.executeAPI(action);
|
|
100
|
+
if (!apiResult.success) return apiResult;
|
|
101
|
+
result.data = apiResult.data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (action.onClick) {
|
|
105
|
+
await action.onClick();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
result.reload = action.reload !== false;
|
|
109
|
+
result.close = action.close !== false;
|
|
110
|
+
|
|
111
|
+
if (action.redirect) {
|
|
112
|
+
result.redirect = this.evaluator.evaluate(action.redirect) as string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Execute navigation action
|
|
120
|
+
*/
|
|
121
|
+
private async executeNavigation(action: any): Promise<ActionResult> {
|
|
122
|
+
const nav = action.navigate || action;
|
|
123
|
+
const to = this.evaluator.evaluate(nav.to) as string;
|
|
124
|
+
|
|
125
|
+
// Validate URL to prevent javascript: or data: schemes
|
|
126
|
+
const isValidUrl = typeof to === 'string' && (
|
|
127
|
+
to.startsWith('http://') ||
|
|
128
|
+
to.startsWith('https://') ||
|
|
129
|
+
to.startsWith('/') ||
|
|
130
|
+
to.startsWith('./')
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (!isValidUrl) {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: 'Invalid URL scheme. Only http://, https://, and relative URLs are allowed.'
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (nav.external) {
|
|
141
|
+
window.open(to, '_blank', 'noopener,noreferrer');
|
|
142
|
+
} else {
|
|
143
|
+
return { success: true, redirect: to };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { success: true };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async executeAPI(action: any): Promise<ActionResult> {
|
|
150
|
+
const apiConfig = action.api;
|
|
151
|
+
|
|
152
|
+
if (typeof apiConfig === 'string') {
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(apiConfig, {
|
|
155
|
+
method: action.method || 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify(this.context.data || {})
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const data = await response.json();
|
|
165
|
+
return { success: true, data };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return { success: false, error: (error as Error).message };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { success: false, error: 'Complex API configuration not yet implemented' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async showConfirmation(message: string): Promise<boolean> {
|
|
175
|
+
const evaluatedMessage = this.evaluator.evaluate(message) as string;
|
|
176
|
+
return window.confirm(evaluatedMessage);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
updateContext(newContext: Partial<ActionContext>): void {
|
|
180
|
+
this.context = { ...this.context, ...newContext };
|
|
181
|
+
this.evaluator.updateContext(newContext);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getContext(): ActionContext {
|
|
185
|
+
return this.context;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function executeAction(
|
|
190
|
+
action: any,
|
|
191
|
+
context: ActionContext = {}
|
|
192
|
+
): Promise<ActionResult> {
|
|
193
|
+
const runner = new ActionRunner(context);
|
|
194
|
+
return await runner.execute(action);
|
|
195
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Data Source Adapters
|
|
2
|
+
|
|
3
|
+
This directory contains data source adapters that bridge various backend protocols with the ObjectUI DataSource interface.
|
|
4
|
+
|
|
5
|
+
## ObjectStack Adapter
|
|
6
|
+
|
|
7
|
+
The `ObjectStackAdapter` provides seamless integration with ObjectStack Protocol servers.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- ✅ Full CRUD operations (find, findOne, create, update, delete)
|
|
12
|
+
- ✅ Bulk operations (createMany, updateMany, deleteMany)
|
|
13
|
+
- ✅ Auto-discovery of server capabilities
|
|
14
|
+
- ✅ Query parameter translation (OData-style → ObjectStack)
|
|
15
|
+
- ✅ Proper error handling
|
|
16
|
+
- ✅ TypeScript types
|
|
17
|
+
|
|
18
|
+
### Usage
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { createObjectStackAdapter } from '@object-ui/core';
|
|
22
|
+
|
|
23
|
+
// Create the adapter
|
|
24
|
+
const dataSource = createObjectStackAdapter({
|
|
25
|
+
baseUrl: 'https://api.example.com',
|
|
26
|
+
token: 'your-auth-token', // Optional
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Use it with ObjectUI components
|
|
30
|
+
const schema = {
|
|
31
|
+
type: 'data-table',
|
|
32
|
+
dataSource,
|
|
33
|
+
resource: 'users',
|
|
34
|
+
columns: [
|
|
35
|
+
{ header: 'Name', accessorKey: 'name' },
|
|
36
|
+
{ header: 'Email', accessorKey: 'email' },
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Advanced Usage
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { ObjectStackAdapter } from '@object-ui/core';
|
|
45
|
+
|
|
46
|
+
const adapter = new ObjectStackAdapter({
|
|
47
|
+
baseUrl: 'https://api.example.com',
|
|
48
|
+
token: process.env.API_TOKEN,
|
|
49
|
+
fetch: customFetch // Optional: use custom fetch (e.g., Next.js fetch)
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Manually connect (optional, auto-connects on first request)
|
|
53
|
+
await adapter.connect();
|
|
54
|
+
|
|
55
|
+
// Query with filters (MongoDB-like operators)
|
|
56
|
+
const result = await adapter.find('tasks', {
|
|
57
|
+
$filter: {
|
|
58
|
+
status: 'active',
|
|
59
|
+
priority: { $gte: 2 }
|
|
60
|
+
},
|
|
61
|
+
$orderby: { createdAt: 'desc' },
|
|
62
|
+
$top: 20,
|
|
63
|
+
$skip: 0
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Access the underlying client for advanced operations
|
|
67
|
+
const client = adapter.getClient();
|
|
68
|
+
const metadata = await client.meta.getObject('task');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Filter Conversion
|
|
72
|
+
|
|
73
|
+
The adapter automatically converts MongoDB-like filter operators to **ObjectStack FilterNode AST format**. This ensures compatibility with the latest ObjectStack Protocol (v0.1.2+).
|
|
74
|
+
|
|
75
|
+
#### Supported Filter Operators
|
|
76
|
+
|
|
77
|
+
| MongoDB Operator | ObjectStack Operator | Example |
|
|
78
|
+
|------------------|---------------------|---------|
|
|
79
|
+
| `$eq` or simple value | `=` | `{ status: 'active' }` → `['status', '=', 'active']` |
|
|
80
|
+
| `$ne` | `!=` | `{ status: { $ne: 'archived' } }` → `['status', '!=', 'archived']` |
|
|
81
|
+
| `$gt` | `>` | `{ age: { $gt: 18 } }` → `['age', '>', 18]` |
|
|
82
|
+
| `$gte` | `>=` | `{ age: { $gte: 18 } }` → `['age', '>=', 18]` |
|
|
83
|
+
| `$lt` | `<` | `{ age: { $lt: 65 } }` → `['age', '<', 65]` |
|
|
84
|
+
| `$lte` | `<=` | `{ age: { $lte: 65 } }` → `['age', '<=', 65]` |
|
|
85
|
+
| `$in` | `in` | `{ status: { $in: ['active', 'pending'] } }` → `['status', 'in', ['active', 'pending']]` |
|
|
86
|
+
| `$nin` / `$notin` | `notin` | `{ status: { $nin: ['archived'] } }` → `['status', 'notin', ['archived']]` |
|
|
87
|
+
| `$contains` / `$regex` | `contains` | `{ name: { $contains: 'John' } }` → `['name', 'contains', 'John']` |
|
|
88
|
+
| `$startswith` | `startswith` | `{ email: { $startswith: 'admin' } }` → `['email', 'startswith', 'admin']` |
|
|
89
|
+
| `$between` | `between` | `{ age: { $between: [18, 65] } }` → `['age', 'between', [18, 65]]` |
|
|
90
|
+
|
|
91
|
+
#### Complex Filter Examples
|
|
92
|
+
|
|
93
|
+
**Multiple conditions** are combined with `'and'`:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Input
|
|
97
|
+
$filter: {
|
|
98
|
+
age: { $gte: 18, $lte: 65 },
|
|
99
|
+
status: 'active'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Converted to AST
|
|
103
|
+
['and',
|
|
104
|
+
['age', '>=', 18],
|
|
105
|
+
['age', '<=', 65],
|
|
106
|
+
['status', '=', 'active']
|
|
107
|
+
]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Query Parameter Mapping
|
|
111
|
+
|
|
112
|
+
The adapter automatically converts ObjectUI query parameters (OData-style) to ObjectStack protocol:
|
|
113
|
+
|
|
114
|
+
| ObjectUI ($) | ObjectStack | Description |
|
|
115
|
+
|--------------|-------------|-------------|
|
|
116
|
+
| `$select` | `select` | Field selection |
|
|
117
|
+
| `$filter` | `filters` (AST) | Filter conditions (converted to FilterNode AST) |
|
|
118
|
+
| `$orderby` | `sort` | Sort order |
|
|
119
|
+
| `$skip` | `skip` | Pagination offset |
|
|
120
|
+
| `$top` | `top` | Limit records |
|
|
121
|
+
|
|
122
|
+
### Example with Sorting
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// OData-style
|
|
126
|
+
await dataSource.find('users', {
|
|
127
|
+
$orderby: {
|
|
128
|
+
createdAt: 'desc',
|
|
129
|
+
name: 'asc'
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Converted to ObjectStack: ['-createdAt', 'name']
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Creating Custom Adapters
|
|
137
|
+
|
|
138
|
+
To create a custom adapter, implement the `DataSource<T>` interface:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import type { DataSource, QueryParams, QueryResult } from '@object-ui/types';
|
|
142
|
+
|
|
143
|
+
export class MyCustomAdapter<T = any> implements DataSource<T> {
|
|
144
|
+
async find(resource: string, params?: QueryParams): Promise<QueryResult<T>> {
|
|
145
|
+
// Your implementation
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async findOne(resource: string, id: string | number): Promise<T | null> {
|
|
149
|
+
// Your implementation
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async create(resource: string, data: Partial<T>): Promise<T> {
|
|
153
|
+
// Your implementation
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async update(resource: string, id: string | number, data: Partial<T>): Promise<T> {
|
|
157
|
+
// Your implementation
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async delete(resource: string, id: string | number): Promise<boolean> {
|
|
161
|
+
// Your implementation
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Optional: bulk operations
|
|
165
|
+
async bulk?(resource: string, operation: string, data: Partial<T>[]): Promise<T[]> {
|
|
166
|
+
// Your implementation
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Available Adapters
|
|
172
|
+
|
|
173
|
+
- **ObjectStackAdapter** - For ObjectStack Protocol servers
|
|
174
|
+
- More adapters coming soon (REST, GraphQL, Supabase, Firebase, etc.)
|
|
175
|
+
|
|
176
|
+
## Related Packages
|
|
177
|
+
|
|
178
|
+
- `@objectstack/client` - ObjectStack Client SDK
|
|
179
|
+
- `@objectstack/spec` - ObjectStack Protocol Specification
|
|
180
|
+
- `@object-ui/types` - ObjectUI Type Definitions
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
export { ObjectStackAdapter, createObjectStackAdapter } from './objectstack-adapter';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// export { ObjectStackAdapter, createObjectStackAdapter } from './objectstack-adapter';
|
|
10
|
+
// Adapters have been moved to separate packages (e.g. @object-ui/data-objectstack)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// export { ObjectStackAdapter, createObjectStackAdapter } from './objectstack-adapter';
|
|
10
|
+
// Adapters have been moved to separate packages (e.g. @object-ui/data-objectstack)
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* @module builder
|
|
8
|
-
* @packageDocumentation
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
9
7
|
*/
|
|
10
8
|
/**
|
|
11
9
|
* Base builder class
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @object-ui/core - Expression Context
|
|
11
|
+
*
|
|
12
|
+
* Manages variable scope and data context for expression evaluation.
|
|
13
|
+
*
|
|
14
|
+
* @module evaluator
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Expression context for variable resolution
|
|
20
|
+
*/
|
|
21
|
+
export class ExpressionContext {
|
|
22
|
+
private scopes: Map<string, any>[] = [];
|
|
23
|
+
|
|
24
|
+
constructor(initialData: Record<string, any> = {}) {
|
|
25
|
+
this.scopes.push(new Map(Object.entries(initialData)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Push a new scope onto the context stack
|
|
30
|
+
*/
|
|
31
|
+
pushScope(data: Record<string, any>): void {
|
|
32
|
+
this.scopes.push(new Map(Object.entries(data)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pop the current scope from the context stack
|
|
37
|
+
*/
|
|
38
|
+
popScope(): void {
|
|
39
|
+
if (this.scopes.length > 1) {
|
|
40
|
+
this.scopes.pop();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get a variable value from the context
|
|
46
|
+
* Searches from innermost to outermost scope
|
|
47
|
+
*/
|
|
48
|
+
get(path: string): any {
|
|
49
|
+
// Split path by dots for nested access
|
|
50
|
+
const parts = path.split('.');
|
|
51
|
+
const varName = parts[0];
|
|
52
|
+
|
|
53
|
+
// Search scopes from innermost to outermost
|
|
54
|
+
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
|
55
|
+
if (this.scopes[i].has(varName)) {
|
|
56
|
+
let value = this.scopes[i].get(varName);
|
|
57
|
+
|
|
58
|
+
// Navigate nested path
|
|
59
|
+
for (let j = 1; j < parts.length; j++) {
|
|
60
|
+
if (value && typeof value === 'object') {
|
|
61
|
+
value = value[parts[j]];
|
|
62
|
+
} else {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set a variable value in the current scope
|
|
76
|
+
*/
|
|
77
|
+
set(name: string, value: any): void {
|
|
78
|
+
if (this.scopes.length > 0) {
|
|
79
|
+
this.scopes[this.scopes.length - 1].set(name, value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a variable exists in any scope
|
|
85
|
+
*/
|
|
86
|
+
has(name: string): boolean {
|
|
87
|
+
const varName = name.split('.')[0];
|
|
88
|
+
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
|
89
|
+
if (this.scopes[i].has(varName)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get all variables from all scopes as a flat object
|
|
98
|
+
*/
|
|
99
|
+
toObject(): Record<string, any> {
|
|
100
|
+
const result: Record<string, any> = {};
|
|
101
|
+
// Merge from outermost to innermost (later scopes override earlier ones)
|
|
102
|
+
for (const scope of this.scopes) {
|
|
103
|
+
for (const [key, value] of scope.entries()) {
|
|
104
|
+
result[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a child context with additional data
|
|
112
|
+
*/
|
|
113
|
+
createChild(data: Record<string, any> = {}): ExpressionContext {
|
|
114
|
+
const child = new ExpressionContext();
|
|
115
|
+
child.scopes = [...this.scopes, new Map(Object.entries(data))];
|
|
116
|
+
return child;
|
|
117
|
+
}
|
|
118
|
+
}
|