@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/actions/ActionRunner.d.ts +40 -0
  3. package/dist/actions/ActionRunner.js +160 -0
  4. package/dist/actions/index.d.ts +8 -0
  5. package/dist/actions/index.js +8 -0
  6. package/dist/adapters/index.d.ts +7 -0
  7. package/dist/adapters/index.js +10 -0
  8. package/dist/builder/schema-builder.d.ts +7 -0
  9. package/dist/builder/schema-builder.js +4 -6
  10. package/dist/evaluator/ExpressionContext.d.ts +51 -0
  11. package/dist/evaluator/ExpressionContext.js +110 -0
  12. package/dist/evaluator/ExpressionEvaluator.d.ts +99 -0
  13. package/dist/evaluator/ExpressionEvaluator.js +200 -0
  14. package/dist/evaluator/index.d.ts +9 -0
  15. package/dist/evaluator/index.js +9 -0
  16. package/dist/index.d.ts +10 -0
  17. package/dist/index.js +10 -1
  18. package/dist/registry/Registry.d.ts +7 -0
  19. package/dist/registry/Registry.js +7 -0
  20. package/dist/types/index.d.ts +7 -0
  21. package/dist/types/index.js +7 -0
  22. package/dist/utils/filter-converter.d.ts +57 -0
  23. package/dist/utils/filter-converter.js +100 -0
  24. package/dist/validation/schema-validator.d.ts +7 -0
  25. package/dist/validation/schema-validator.js +4 -6
  26. package/package.json +16 -5
  27. package/src/actions/ActionRunner.ts +195 -0
  28. package/src/actions/index.ts +9 -0
  29. package/src/adapters/README.md +180 -0
  30. package/src/adapters/index.d.ts +8 -0
  31. package/src/adapters/index.js +10 -0
  32. package/src/adapters/index.ts +10 -0
  33. package/src/builder/schema-builder.d.ts +7 -0
  34. package/src/builder/schema-builder.js +4 -6
  35. package/src/builder/schema-builder.ts +8 -0
  36. package/src/evaluator/ExpressionContext.ts +118 -0
  37. package/src/evaluator/ExpressionEvaluator.ts +248 -0
  38. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
  39. package/src/evaluator/index.ts +10 -0
  40. package/src/index.d.ts +9 -0
  41. package/src/index.js +10 -1
  42. package/src/index.test.ts +8 -0
  43. package/src/index.ts +11 -1
  44. package/src/registry/Registry.d.ts +7 -0
  45. package/src/registry/Registry.js +7 -0
  46. package/src/registry/Registry.ts +8 -0
  47. package/src/types/index.d.ts +7 -0
  48. package/src/types/index.js +7 -0
  49. package/src/types/index.ts +8 -0
  50. package/src/utils/__tests__/filter-converter.test.ts +118 -0
  51. package/src/utils/filter-converter.d.ts +57 -0
  52. package/src/utils/filter-converter.js +100 -0
  53. package/src/utils/filter-converter.ts +133 -0
  54. package/src/validation/schema-validator.d.ts +7 -0
  55. package/src/validation/schema-validator.js +4 -6
  56. package/src/validation/schema-validator.ts +8 -0
  57. 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,9 @@
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 * from './ActionRunner';
@@ -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,3 +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
+ */
1
8
  /**
2
9
  * @object-ui/core - Schema Builder
3
10
  *
@@ -1,11 +1,9 @@
1
1
  /**
2
- * @object-ui/core - Schema Builder
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
3
4
  *
4
- * Fluent API for building schemas programmatically.
5
- * Provides type-safe builder functions for common schema patterns.
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
@@ -1,3 +1,11 @@
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
+
1
9
  /**
2
10
  * @object-ui/core - Schema Builder
3
11
  *
@@ -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
+ }