Skip to content

Commit 6e0aa55

Browse files
authored
Merge pull request #432 from objectstack-ai/copilot/complete-development-tasks
2 parents 329eede + f97746b commit 6e0aa55

17 files changed

Lines changed: 932 additions & 50 deletions

File tree

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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 - DataScope Manager
11+
*
12+
* Runtime implementation of the DataContext interface for managing
13+
* named data scopes. Provides row-level data access control and
14+
* reactive data state management within the UI component tree.
15+
*
16+
* @module data-scope
17+
* @packageDocumentation
18+
*/
19+
20+
import type { DataScope, DataContext, DataSource } from '@object-ui/types';
21+
22+
/**
23+
* Row-level filter for restricting data access within a scope
24+
*/
25+
export interface RowLevelFilter {
26+
/** Field to filter on */
27+
field: string;
28+
/** Filter operator */
29+
operator: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'in' | 'nin' | 'contains';
30+
/** Filter value */
31+
value: any;
32+
}
33+
34+
/**
35+
* Configuration for creating a data scope
36+
*/
37+
export interface DataScopeConfig {
38+
/** Data source instance */
39+
dataSource?: DataSource;
40+
/** Initial data */
41+
data?: any;
42+
/** Row-level filters to apply */
43+
filters?: RowLevelFilter[];
44+
/** Whether this scope is read-only */
45+
readOnly?: boolean;
46+
}
47+
48+
/**
49+
* DataScopeManager — Runtime implementation of DataContext.
50+
*
51+
* Manages named data scopes for the component tree, providing:
52+
* - Scope registration and lookup
53+
* - Row-level security via filters
54+
* - Data state management (data, loading, error)
55+
*
56+
* @example
57+
* ```ts
58+
* const manager = new DataScopeManager();
59+
* manager.registerScope('contacts', {
60+
* dataSource: myDataSource,
61+
* data: [],
62+
* });
63+
* const scope = manager.getScope('contacts');
64+
* ```
65+
*/
66+
export class DataScopeManager implements DataContext {
67+
scopes: Record<string, DataScope> = {};
68+
private filters: Record<string, RowLevelFilter[]> = {};
69+
private readOnlyScopes: Set<string> = new Set();
70+
private listeners: Map<string, Array<(scope: DataScope) => void>> = new Map();
71+
72+
/**
73+
* Register a data scope
74+
*/
75+
registerScope(name: string, scope: DataScope): void {
76+
this.scopes[name] = scope;
77+
this.notifyListeners(name, scope);
78+
}
79+
80+
/**
81+
* Register a data scope with configuration
82+
*/
83+
registerScopeWithConfig(name: string, config: DataScopeConfig): void {
84+
const scope: DataScope = {
85+
dataSource: config.dataSource,
86+
data: config.data,
87+
loading: false,
88+
error: null,
89+
};
90+
91+
if (config.filters) {
92+
this.filters[name] = config.filters;
93+
}
94+
95+
if (config.readOnly) {
96+
this.readOnlyScopes.add(name);
97+
}
98+
99+
this.scopes[name] = scope;
100+
this.notifyListeners(name, scope);
101+
}
102+
103+
/**
104+
* Get a data scope by name
105+
*/
106+
getScope(name: string): DataScope | undefined {
107+
return this.scopes[name];
108+
}
109+
110+
/**
111+
* Remove a data scope
112+
*/
113+
removeScope(name: string): void {
114+
delete this.scopes[name];
115+
delete this.filters[name];
116+
this.readOnlyScopes.delete(name);
117+
this.listeners.delete(name);
118+
}
119+
120+
/**
121+
* Check if a scope is read-only
122+
*/
123+
isReadOnly(name: string): boolean {
124+
return this.readOnlyScopes.has(name);
125+
}
126+
127+
/**
128+
* Get row-level filters for a scope
129+
*/
130+
getFilters(name: string): RowLevelFilter[] {
131+
return this.filters[name] || [];
132+
}
133+
134+
/**
135+
* Set row-level filters for a scope
136+
*/
137+
setFilters(name: string, filters: RowLevelFilter[]): void {
138+
this.filters[name] = filters;
139+
}
140+
141+
/**
142+
* Apply row-level filters to a dataset
143+
*/
144+
applyFilters(name: string, data: any[]): any[] {
145+
const scopeFilters = this.filters[name];
146+
if (!scopeFilters || scopeFilters.length === 0) {
147+
return data;
148+
}
149+
150+
return data.filter(row => {
151+
return scopeFilters.every(filter => {
152+
const fieldValue = row[filter.field];
153+
return evaluateFilter(fieldValue, filter.operator, filter.value);
154+
});
155+
});
156+
}
157+
158+
/**
159+
* Update data in a scope
160+
*/
161+
updateScopeData(name: string, data: any): void {
162+
const scope = this.scopes[name];
163+
if (!scope) return;
164+
165+
if (this.readOnlyScopes.has(name)) {
166+
throw new Error(`Cannot update read-only scope: ${name}`);
167+
}
168+
169+
scope.data = data;
170+
this.notifyListeners(name, scope);
171+
}
172+
173+
/**
174+
* Update loading state for a scope
175+
*/
176+
updateScopeLoading(name: string, loading: boolean): void {
177+
const scope = this.scopes[name];
178+
if (!scope) return;
179+
180+
scope.loading = loading;
181+
this.notifyListeners(name, scope);
182+
}
183+
184+
/**
185+
* Update error state for a scope
186+
*/
187+
updateScopeError(name: string, error: Error | string | null): void {
188+
const scope = this.scopes[name];
189+
if (!scope) return;
190+
191+
scope.error = error;
192+
this.notifyListeners(name, scope);
193+
}
194+
195+
/**
196+
* Subscribe to scope changes
197+
*/
198+
onScopeChange(name: string, listener: (scope: DataScope) => void): () => void {
199+
if (!this.listeners.has(name)) {
200+
this.listeners.set(name, []);
201+
}
202+
this.listeners.get(name)!.push(listener);
203+
204+
return () => {
205+
const arr = this.listeners.get(name);
206+
if (arr) {
207+
const idx = arr.indexOf(listener);
208+
if (idx >= 0) arr.splice(idx, 1);
209+
}
210+
};
211+
}
212+
213+
/**
214+
* Get all registered scope names
215+
*/
216+
getScopeNames(): string[] {
217+
return Object.keys(this.scopes);
218+
}
219+
220+
/**
221+
* Clear all scopes
222+
*/
223+
clear(): void {
224+
this.scopes = {};
225+
this.filters = {};
226+
this.readOnlyScopes.clear();
227+
this.listeners.clear();
228+
}
229+
230+
private notifyListeners(name: string, scope: DataScope): void {
231+
const arr = this.listeners.get(name);
232+
if (arr) {
233+
arr.forEach(listener => listener(scope));
234+
}
235+
}
236+
}
237+
238+
/**
239+
* Evaluate a single filter condition against a field value
240+
*/
241+
function evaluateFilter(fieldValue: any, operator: RowLevelFilter['operator'], filterValue: any): boolean {
242+
switch (operator) {
243+
case 'eq':
244+
return fieldValue === filterValue;
245+
case 'ne':
246+
return fieldValue !== filterValue;
247+
case 'gt':
248+
return fieldValue > filterValue;
249+
case 'lt':
250+
return fieldValue < filterValue;
251+
case 'gte':
252+
return fieldValue >= filterValue;
253+
case 'lte':
254+
return fieldValue <= filterValue;
255+
case 'in':
256+
return Array.isArray(filterValue) && filterValue.includes(fieldValue);
257+
case 'nin':
258+
return Array.isArray(filterValue) && !filterValue.includes(fieldValue);
259+
case 'contains':
260+
return typeof fieldValue === 'string' && fieldValue.includes(String(filterValue));
261+
default:
262+
return true;
263+
}
264+
}
265+
266+
/**
267+
* Default DataScopeManager instance
268+
*/
269+
export const defaultDataScopeManager = new DataScopeManager();

0 commit comments

Comments
 (0)