@object-ui/core 3.0.3 → 3.1.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.
Files changed (78) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/actions/ActionEngine.d.ts +98 -0
  3. package/dist/actions/ActionEngine.js +222 -0
  4. package/dist/actions/UndoManager.d.ts +80 -0
  5. package/dist/actions/UndoManager.js +193 -0
  6. package/dist/actions/index.d.ts +2 -0
  7. package/dist/actions/index.js +2 -0
  8. package/dist/adapters/ApiDataSource.d.ts +2 -1
  9. package/dist/adapters/ApiDataSource.js +25 -0
  10. package/dist/adapters/ValueDataSource.d.ts +2 -1
  11. package/dist/adapters/ValueDataSource.js +99 -3
  12. package/dist/data-scope/ViewDataProvider.d.ts +143 -0
  13. package/dist/data-scope/ViewDataProvider.js +153 -0
  14. package/dist/data-scope/index.d.ts +1 -0
  15. package/dist/data-scope/index.js +1 -0
  16. package/dist/evaluator/ExpressionEvaluator.d.ts +7 -0
  17. package/dist/evaluator/ExpressionEvaluator.js +19 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.js +5 -0
  20. package/dist/protocols/DndProtocol.d.ts +84 -0
  21. package/dist/protocols/DndProtocol.js +113 -0
  22. package/dist/protocols/KeyboardProtocol.d.ts +93 -0
  23. package/dist/protocols/KeyboardProtocol.js +108 -0
  24. package/dist/protocols/NotificationProtocol.d.ts +71 -0
  25. package/dist/protocols/NotificationProtocol.js +99 -0
  26. package/dist/protocols/ResponsiveProtocol.d.ts +73 -0
  27. package/dist/protocols/ResponsiveProtocol.js +158 -0
  28. package/dist/protocols/SharingProtocol.d.ts +71 -0
  29. package/dist/protocols/SharingProtocol.js +124 -0
  30. package/dist/protocols/index.d.ts +12 -0
  31. package/dist/protocols/index.js +12 -0
  32. package/dist/utils/debug-collector.d.ts +59 -0
  33. package/dist/utils/debug-collector.js +73 -0
  34. package/dist/utils/debug.d.ts +37 -2
  35. package/dist/utils/debug.js +62 -3
  36. package/dist/utils/expand-fields.d.ts +40 -0
  37. package/dist/utils/expand-fields.js +68 -0
  38. package/dist/utils/extract-records.d.ts +16 -0
  39. package/dist/utils/extract-records.js +32 -0
  40. package/dist/utils/normalize-quick-filter.d.ts +29 -0
  41. package/dist/utils/normalize-quick-filter.js +66 -0
  42. package/package.json +3 -3
  43. package/src/__tests__/protocols/DndProtocol.test.ts +186 -0
  44. package/src/__tests__/protocols/KeyboardProtocol.test.ts +177 -0
  45. package/src/__tests__/protocols/NotificationProtocol.test.ts +142 -0
  46. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +176 -0
  47. package/src/__tests__/protocols/SharingProtocol.test.ts +188 -0
  48. package/src/actions/ActionEngine.ts +268 -0
  49. package/src/actions/UndoManager.ts +215 -0
  50. package/src/actions/__tests__/ActionEngine.test.ts +206 -0
  51. package/src/actions/__tests__/UndoManager.test.ts +320 -0
  52. package/src/actions/index.ts +2 -0
  53. package/src/adapters/ApiDataSource.ts +27 -0
  54. package/src/adapters/ValueDataSource.ts +109 -3
  55. package/src/adapters/__tests__/ValueDataSource.test.ts +147 -0
  56. package/src/data-scope/ViewDataProvider.ts +282 -0
  57. package/src/data-scope/__tests__/ViewDataProvider.test.ts +270 -0
  58. package/src/data-scope/index.ts +8 -0
  59. package/src/evaluator/ExpressionEvaluator.ts +22 -0
  60. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +31 -1
  61. package/src/index.ts +5 -0
  62. package/src/protocols/DndProtocol.ts +184 -0
  63. package/src/protocols/KeyboardProtocol.ts +185 -0
  64. package/src/protocols/NotificationProtocol.ts +159 -0
  65. package/src/protocols/ResponsiveProtocol.ts +210 -0
  66. package/src/protocols/SharingProtocol.ts +185 -0
  67. package/src/protocols/index.ts +13 -0
  68. package/src/utils/__tests__/debug-collector.test.ts +102 -0
  69. package/src/utils/__tests__/debug.test.ts +52 -1
  70. package/src/utils/__tests__/expand-fields.test.ts +120 -0
  71. package/src/utils/__tests__/extract-records.test.ts +50 -0
  72. package/src/utils/__tests__/normalize-quick-filter.test.ts +123 -0
  73. package/src/utils/debug-collector.ts +100 -0
  74. package/src/utils/debug.ts +87 -6
  75. package/src/utils/expand-fields.ts +76 -0
  76. package/src/utils/extract-records.ts +33 -0
  77. package/src/utils/normalize-quick-filter.ts +78 -0
  78. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,282 @@
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 - View Data Provider
11
+ *
12
+ * Resolves @objectstack/spec ViewData discriminated union into
13
+ * a unified data access interface. Supports three data modes:
14
+ *
15
+ * 1. 'object' — Metadata-driven via ObjectStack API (object name → fields/records)
16
+ * 2. 'api' — Custom REST API endpoints (read/write URLs)
17
+ * 3. 'value' — Static data (pre-loaded items array)
18
+ *
19
+ * @module data-scope
20
+ * @packageDocumentation
21
+ */
22
+
23
+ /** ViewData union — matches @objectstack/spec ViewDataSchema */
24
+ export type ViewDataConfig =
25
+ | { provider: 'object'; object: string }
26
+ | {
27
+ provider: 'api';
28
+ read: { url: string; method?: string; headers?: Record<string, string> };
29
+ write?: { url: string; method?: string; headers?: Record<string, string> };
30
+ }
31
+ | { provider: 'value'; items: any[] };
32
+
33
+ /** Element-level data source — matches @objectstack/spec ElementDataSourceSchema */
34
+ export interface ElementDataSourceConfig {
35
+ object: string;
36
+ view?: string;
37
+ filter?: Record<string, any>;
38
+ sort?: Array<{ field: string; order: 'asc' | 'desc' }>;
39
+ limit?: number;
40
+ }
41
+
42
+ /** Fetcher interface — consumers provide the actual fetch implementation */
43
+ export interface DataFetcher {
44
+ /** Fetch records for an object */
45
+ fetchRecords(
46
+ object: string,
47
+ options?: {
48
+ filter?: Record<string, any>;
49
+ sort?: Array<{ field: string; order: 'asc' | 'desc' }>;
50
+ limit?: number;
51
+ offset?: number;
52
+ fields?: string[];
53
+ },
54
+ ): Promise<{ records: any[]; total: number }>;
55
+
56
+ /** Fetch object metadata (fields, etc.) */
57
+ fetchMetadata?(object: string): Promise<{
58
+ name: string;
59
+ label?: string;
60
+ fields: Array<{
61
+ name: string;
62
+ label?: string;
63
+ type: string;
64
+ required?: boolean;
65
+ }>;
66
+ }>;
67
+
68
+ /** Fetch from a custom API URL */
69
+ fetchUrl?(
70
+ url: string,
71
+ options?: {
72
+ method?: string;
73
+ headers?: Record<string, string>;
74
+ body?: any;
75
+ },
76
+ ): Promise<any>;
77
+ }
78
+
79
+ /** Resolved data result */
80
+ export interface ResolvedData {
81
+ /** Data records */
82
+ records: any[];
83
+ /** Total record count (for pagination) */
84
+ total: number;
85
+ /** Object metadata (if available) */
86
+ metadata?: {
87
+ name: string;
88
+ label?: string;
89
+ fields: Array<{
90
+ name: string;
91
+ label?: string;
92
+ type: string;
93
+ required?: boolean;
94
+ }>;
95
+ };
96
+ /** Provider type */
97
+ provider: 'object' | 'api' | 'value';
98
+ /** Loading state */
99
+ loading: boolean;
100
+ /** Error */
101
+ error?: string;
102
+ }
103
+
104
+ /** Extract records array from various common API response shapes */
105
+ function extractRecords(data: any): any[] {
106
+ if (Array.isArray(data)) return data;
107
+ if (Array.isArray(data?.records)) return data.records;
108
+ if (Array.isArray(data?.data)) return data.data;
109
+ if (Array.isArray(data?.items)) return data.items;
110
+ return [];
111
+ }
112
+
113
+ /**
114
+ * Resolves ViewData configuration into actual data via a pluggable fetcher.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const provider = new ViewDataProvider(myFetcher);
119
+ * const data = await provider.resolve({ provider: 'object', object: 'Account' });
120
+ * ```
121
+ */
122
+ export class ViewDataProvider {
123
+ private fetcher: DataFetcher | null = null;
124
+
125
+ constructor(fetcher?: DataFetcher) {
126
+ this.fetcher = fetcher ?? null;
127
+ }
128
+
129
+ /** Set the data fetcher implementation */
130
+ setFetcher(fetcher: DataFetcher): void {
131
+ this.fetcher = fetcher;
132
+ }
133
+
134
+ /** Resolve ViewData config into actual data */
135
+ async resolve(
136
+ config: ViewDataConfig,
137
+ options?: {
138
+ filter?: Record<string, any>;
139
+ sort?: Array<{ field: string; order: 'asc' | 'desc' }>;
140
+ limit?: number;
141
+ offset?: number;
142
+ fields?: string[];
143
+ },
144
+ ): Promise<ResolvedData> {
145
+ switch (config.provider) {
146
+ case 'value':
147
+ return this.resolveValue(config);
148
+ case 'api':
149
+ return this.resolveApi(config);
150
+ case 'object':
151
+ return this.resolveObject(config, options);
152
+ default:
153
+ return {
154
+ records: [],
155
+ total: 0,
156
+ provider: 'value' as const,
157
+ loading: false,
158
+ error: `Unknown data provider: ${(config as any).provider}`,
159
+ };
160
+ }
161
+ }
162
+
163
+ /** Resolve static value data */
164
+ private resolveValue(config: {
165
+ provider: 'value';
166
+ items: any[];
167
+ }): ResolvedData {
168
+ const items = Array.isArray(config.items) ? config.items : [];
169
+ return {
170
+ records: items,
171
+ total: items.length,
172
+ provider: 'value',
173
+ loading: false,
174
+ };
175
+ }
176
+
177
+ /** Resolve API-based data */
178
+ private async resolveApi(config: {
179
+ provider: 'api';
180
+ read: { url: string; method?: string; headers?: Record<string, string> };
181
+ }): Promise<ResolvedData> {
182
+ if (!this.fetcher?.fetchUrl) {
183
+ return {
184
+ records: [],
185
+ total: 0,
186
+ provider: 'api',
187
+ loading: false,
188
+ error: 'No fetchUrl implementation available for API data provider',
189
+ };
190
+ }
191
+
192
+ try {
193
+ const data = await this.fetcher.fetchUrl(config.read.url, {
194
+ method: config.read.method,
195
+ headers: config.read.headers,
196
+ });
197
+
198
+ // Handle common response shapes
199
+ const records = extractRecords(data);
200
+ const total = data?.total ?? data?.count ?? records.length;
201
+
202
+ return {
203
+ records,
204
+ total,
205
+ provider: 'api',
206
+ loading: false,
207
+ };
208
+ } catch (error) {
209
+ return {
210
+ records: [],
211
+ total: 0,
212
+ provider: 'api',
213
+ loading: false,
214
+ error: (error as Error).message,
215
+ };
216
+ }
217
+ }
218
+
219
+ /** Resolve object-based data (metadata-driven) */
220
+ private async resolveObject(
221
+ config: { provider: 'object'; object: string },
222
+ options?: {
223
+ filter?: Record<string, any>;
224
+ sort?: Array<{ field: string; order: 'asc' | 'desc' }>;
225
+ limit?: number;
226
+ offset?: number;
227
+ fields?: string[];
228
+ },
229
+ ): Promise<ResolvedData> {
230
+ if (!this.fetcher) {
231
+ return {
232
+ records: [],
233
+ total: 0,
234
+ provider: 'object',
235
+ loading: false,
236
+ error: 'No data fetcher configured for object data provider',
237
+ };
238
+ }
239
+
240
+ try {
241
+ // Fetch metadata if available
242
+ let metadata: ResolvedData['metadata'];
243
+ if (this.fetcher.fetchMetadata) {
244
+ metadata = await this.fetcher.fetchMetadata(config.object);
245
+ }
246
+
247
+ // Fetch records
248
+ const result = await this.fetcher.fetchRecords(config.object, options);
249
+
250
+ return {
251
+ records: result.records,
252
+ total: result.total,
253
+ metadata,
254
+ provider: 'object',
255
+ loading: false,
256
+ };
257
+ } catch (error) {
258
+ return {
259
+ records: [],
260
+ total: 0,
261
+ provider: 'object',
262
+ loading: false,
263
+ error: (error as Error).message,
264
+ };
265
+ }
266
+ }
267
+
268
+ /** Resolve element-level data source */
269
+ async resolveElementDataSource(
270
+ config: ElementDataSourceConfig,
271
+ ): Promise<ResolvedData> {
272
+ return this.resolve(
273
+ { provider: 'object', object: config.object },
274
+ {
275
+ filter: config.filter,
276
+ sort: config.sort,
277
+ limit: config.limit,
278
+ },
279
+ );
280
+ }
281
+
282
+ }
@@ -0,0 +1,270 @@
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
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
10
+ import {
11
+ ViewDataProvider,
12
+ type DataFetcher,
13
+ type ViewDataConfig,
14
+ } from '../ViewDataProvider';
15
+
16
+ describe('ViewDataProvider', () => {
17
+ let provider: ViewDataProvider;
18
+
19
+ beforeEach(() => {
20
+ provider = new ViewDataProvider();
21
+ });
22
+
23
+ // ===== Value Provider =====
24
+ describe('value provider', () => {
25
+ it('resolves static items', async () => {
26
+ const config: ViewDataConfig = {
27
+ provider: 'value',
28
+ items: [
29
+ { id: 1, name: 'Alice' },
30
+ { id: 2, name: 'Bob' },
31
+ ],
32
+ };
33
+
34
+ const result = await provider.resolve(config);
35
+
36
+ expect(result.provider).toBe('value');
37
+ expect(result.records).toHaveLength(2);
38
+ expect(result.total).toBe(2);
39
+ expect(result.loading).toBe(false);
40
+ expect(result.error).toBeUndefined();
41
+ });
42
+
43
+ it('handles empty items', async () => {
44
+ const result = await provider.resolve({
45
+ provider: 'value',
46
+ items: [],
47
+ });
48
+ expect(result.records).toHaveLength(0);
49
+ expect(result.total).toBe(0);
50
+ });
51
+
52
+ it('handles non-array items gracefully', async () => {
53
+ const result = await provider.resolve({
54
+ provider: 'value',
55
+ items: null as any,
56
+ });
57
+ expect(result.records).toHaveLength(0);
58
+ });
59
+ });
60
+
61
+ // ===== API Provider =====
62
+ describe('api provider', () => {
63
+ it('returns error when no fetchUrl implementation', async () => {
64
+ const config: ViewDataConfig = {
65
+ provider: 'api',
66
+ read: { url: 'https://api.example.com/records' },
67
+ };
68
+
69
+ const result = await provider.resolve(config);
70
+ expect(result.error).toContain('No fetchUrl implementation');
71
+ });
72
+
73
+ it('fetches data from API URL', async () => {
74
+ const fetcher: DataFetcher = {
75
+ fetchRecords: vi.fn(),
76
+ fetchUrl: vi.fn().mockResolvedValue({
77
+ records: [{ id: 1 }],
78
+ total: 1,
79
+ }),
80
+ };
81
+ provider.setFetcher(fetcher);
82
+
83
+ const result = await provider.resolve({
84
+ provider: 'api',
85
+ read: { url: 'https://api.example.com/data', method: 'GET' },
86
+ });
87
+
88
+ expect(result.records).toHaveLength(1);
89
+ expect(result.total).toBe(1);
90
+ expect(fetcher.fetchUrl).toHaveBeenCalledWith(
91
+ 'https://api.example.com/data',
92
+ {
93
+ method: 'GET',
94
+ headers: undefined,
95
+ },
96
+ );
97
+ });
98
+
99
+ it('handles array response shape', async () => {
100
+ const fetcher: DataFetcher = {
101
+ fetchRecords: vi.fn(),
102
+ fetchUrl: vi.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]),
103
+ };
104
+ provider.setFetcher(fetcher);
105
+
106
+ const result = await provider.resolve({
107
+ provider: 'api',
108
+ read: { url: 'https://api.example.com/data' },
109
+ });
110
+
111
+ expect(result.records).toHaveLength(2);
112
+ expect(result.total).toBe(2);
113
+ });
114
+
115
+ it('handles { data: [] } response shape', async () => {
116
+ const fetcher: DataFetcher = {
117
+ fetchRecords: vi.fn(),
118
+ fetchUrl: vi.fn().mockResolvedValue({ data: [{ id: 1 }] }),
119
+ };
120
+ provider.setFetcher(fetcher);
121
+
122
+ const result = await provider.resolve({
123
+ provider: 'api',
124
+ read: { url: 'https://api.example.com/data' },
125
+ });
126
+
127
+ expect(result.records).toHaveLength(1);
128
+ });
129
+
130
+ it('handles API errors gracefully', async () => {
131
+ const fetcher: DataFetcher = {
132
+ fetchRecords: vi.fn(),
133
+ fetchUrl: vi.fn().mockRejectedValue(new Error('Network error')),
134
+ };
135
+ provider.setFetcher(fetcher);
136
+
137
+ const result = await provider.resolve({
138
+ provider: 'api',
139
+ read: { url: 'https://api.example.com/data' },
140
+ });
141
+
142
+ expect(result.error).toBe('Network error');
143
+ expect(result.records).toHaveLength(0);
144
+ });
145
+ });
146
+
147
+ // ===== Object Provider =====
148
+ describe('object provider', () => {
149
+ it('returns error when no fetcher configured', async () => {
150
+ const result = await provider.resolve({
151
+ provider: 'object',
152
+ object: 'Account',
153
+ });
154
+ expect(result.error).toContain('No data fetcher configured');
155
+ });
156
+
157
+ it('fetches records for object', async () => {
158
+ const fetcher: DataFetcher = {
159
+ fetchRecords: vi.fn().mockResolvedValue({
160
+ records: [{ id: '1', name: 'Acme Corp' }],
161
+ total: 1,
162
+ }),
163
+ };
164
+ provider.setFetcher(fetcher);
165
+
166
+ const result = await provider.resolve({
167
+ provider: 'object',
168
+ object: 'Account',
169
+ });
170
+
171
+ expect(result.records).toHaveLength(1);
172
+ expect(result.total).toBe(1);
173
+ expect(fetcher.fetchRecords).toHaveBeenCalledWith('Account', undefined);
174
+ });
175
+
176
+ it('passes filter/sort/limit options', async () => {
177
+ const fetcher: DataFetcher = {
178
+ fetchRecords: vi.fn().mockResolvedValue({ records: [], total: 0 }),
179
+ };
180
+ provider.setFetcher(fetcher);
181
+
182
+ const options = {
183
+ filter: { status: 'active' },
184
+ sort: [{ field: 'name', order: 'asc' as const }],
185
+ limit: 10,
186
+ };
187
+
188
+ await provider.resolve(
189
+ { provider: 'object', object: 'Account' },
190
+ options,
191
+ );
192
+
193
+ expect(fetcher.fetchRecords).toHaveBeenCalledWith('Account', options);
194
+ });
195
+
196
+ it('fetches metadata when available', async () => {
197
+ const fetcher: DataFetcher = {
198
+ fetchRecords: vi.fn().mockResolvedValue({ records: [], total: 0 }),
199
+ fetchMetadata: vi.fn().mockResolvedValue({
200
+ name: 'Account',
201
+ label: 'Accounts',
202
+ fields: [{ name: 'name', type: 'string', label: 'Name' }],
203
+ }),
204
+ };
205
+ provider.setFetcher(fetcher);
206
+
207
+ const result = await provider.resolve({
208
+ provider: 'object',
209
+ object: 'Account',
210
+ });
211
+
212
+ expect(result.metadata).toBeDefined();
213
+ expect(result.metadata?.name).toBe('Account');
214
+ expect(result.metadata?.fields).toHaveLength(1);
215
+ });
216
+
217
+ it('handles fetch errors gracefully', async () => {
218
+ const fetcher: DataFetcher = {
219
+ fetchRecords: vi
220
+ .fn()
221
+ .mockRejectedValue(new Error('Connection failed')),
222
+ };
223
+ provider.setFetcher(fetcher);
224
+
225
+ const result = await provider.resolve({
226
+ provider: 'object',
227
+ object: 'Account',
228
+ });
229
+
230
+ expect(result.error).toBe('Connection failed');
231
+ expect(result.records).toHaveLength(0);
232
+ });
233
+ });
234
+
235
+ // ===== Element Data Source =====
236
+ describe('resolveElementDataSource', () => {
237
+ it('resolves element-level data source', async () => {
238
+ const fetcher: DataFetcher = {
239
+ fetchRecords: vi
240
+ .fn()
241
+ .mockResolvedValue({ records: [{ id: '1' }], total: 1 }),
242
+ };
243
+ provider.setFetcher(fetcher);
244
+
245
+ const result = await provider.resolveElementDataSource({
246
+ object: 'Contact',
247
+ filter: { accountId: '123' },
248
+ sort: [{ field: 'name', order: 'asc' }],
249
+ limit: 5,
250
+ });
251
+
252
+ expect(result.records).toHaveLength(1);
253
+ expect(fetcher.fetchRecords).toHaveBeenCalledWith('Contact', {
254
+ filter: { accountId: '123' },
255
+ sort: [{ field: 'name', order: 'asc' }],
256
+ limit: 5,
257
+ });
258
+ });
259
+ });
260
+
261
+ // ===== Unknown Provider =====
262
+ describe('unknown provider', () => {
263
+ it('returns error for unknown provider type', async () => {
264
+ const result = await provider.resolve({
265
+ provider: 'unknown' as any,
266
+ } as ViewDataConfig);
267
+ expect(result.error).toContain('Unknown data provider');
268
+ });
269
+ });
270
+ });
@@ -14,3 +14,11 @@ export {
14
14
  type RowLevelFilter,
15
15
  type DataScopeConfig,
16
16
  } from './DataScopeManager.js';
17
+
18
+ export {
19
+ ViewDataProvider,
20
+ type ViewDataConfig,
21
+ type ElementDataSourceConfig,
22
+ type DataFetcher,
23
+ type ResolvedData,
24
+ } from './ViewDataProvider.js';
@@ -291,3 +291,25 @@ export function evaluateCondition(
291
291
  const evaluator = new ExpressionEvaluator(context, globalCache, globalFormulas);
292
292
  return evaluator.evaluateCondition(condition);
293
293
  }
294
+
295
+ /**
296
+ * Convenience function to evaluate a plain condition string against a data record.
297
+ * Supports both template expressions (e.g., '${data.amount > 1000}') and
298
+ * plain expressions (e.g., "status == 'overdue'").
299
+ * Record fields are available both directly (status) and namespaced (data.status).
300
+ */
301
+ export function evaluatePlainCondition(
302
+ condition: string,
303
+ record: Record<string, any>
304
+ ): boolean {
305
+ const evaluator = new ExpressionEvaluator({ ...record, data: record }, globalCache, globalFormulas);
306
+ try {
307
+ const isTemplate = /\$\{/.test(condition);
308
+ const result = isTemplate
309
+ ? evaluator.evaluate(condition, { throwOnError: true })
310
+ : evaluator.evaluateExpression(condition);
311
+ return result === true;
312
+ } catch {
313
+ return false;
314
+ }
315
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
- import { ExpressionEvaluator, evaluateExpression, evaluateCondition } from '../ExpressionEvaluator';
10
+ import { ExpressionEvaluator, evaluateExpression, evaluateCondition, evaluatePlainCondition } from '../ExpressionEvaluator';
11
11
  import { ExpressionContext } from '../ExpressionContext';
12
12
 
13
13
  describe('ExpressionContext', () => {
@@ -99,3 +99,33 @@ describe('ExpressionEvaluator', () => {
99
99
  });
100
100
  });
101
101
  });
102
+
103
+ describe('evaluatePlainCondition', () => {
104
+ it('should evaluate a plain condition with direct field references', () => {
105
+ expect(evaluatePlainCondition("status == 'overdue'", { status: 'overdue' })).toBe(true);
106
+ expect(evaluatePlainCondition("status == 'overdue'", { status: 'active' })).toBe(false);
107
+ });
108
+
109
+ it('should evaluate numeric comparisons', () => {
110
+ expect(evaluatePlainCondition('amount > 1000', { amount: 2500 })).toBe(true);
111
+ expect(evaluatePlainCondition('amount > 1000', { amount: 500 })).toBe(false);
112
+ });
113
+
114
+ it('should evaluate compound conditions', () => {
115
+ expect(evaluatePlainCondition("amount > 1000 && status === 'urgent'", { amount: 2000, status: 'urgent' })).toBe(true);
116
+ expect(evaluatePlainCondition("amount > 1000 && status === 'urgent'", { amount: 2000, status: 'normal' })).toBe(false);
117
+ });
118
+
119
+ it('should support data.field references in template expressions', () => {
120
+ expect(evaluatePlainCondition('${data.amount > 1000}', { amount: 2000 })).toBe(true);
121
+ expect(evaluatePlainCondition('${data.amount > 1000}', { amount: 500 })).toBe(false);
122
+ });
123
+
124
+ it('should return false for invalid expressions', () => {
125
+ expect(evaluatePlainCondition('!!!invalidSyntax', { status: 'ok' })).toBe(false);
126
+ });
127
+
128
+ it('should return false for non-boolean results', () => {
129
+ expect(evaluatePlainCondition('status', { status: 'active' })).toBe(false);
130
+ });
131
+ });
package/src/index.ts CHANGED
@@ -14,6 +14,9 @@ export * from './registry/WidgetRegistry.js';
14
14
  export * from './validation/index.js';
15
15
  export * from './builder/schema-builder.js';
16
16
  export * from './utils/filter-converter.js';
17
+ export * from './utils/normalize-quick-filter.js';
18
+ export * from './utils/extract-records.js';
19
+ export * from './utils/expand-fields.js';
17
20
  export * from './evaluator/index.js';
18
21
  export * from './actions/index.js';
19
22
  export * from './query/index.js';
@@ -22,3 +25,5 @@ export * from './theme/index.js';
22
25
  export * from './data-scope/index.js';
23
26
  export * from './errors/index.js';
24
27
  export * from './utils/debug.js';
28
+ export * from './utils/debug-collector.js';
29
+ export * from './protocols/index.js';