@objectql/studio 1.4.0 → 1.6.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.
package/dist/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ObjectQL Console</title>
7
- <script type="module" crossorigin src="/studio/assets/index-sazuLrgQ.js"></script>
8
- <link rel="stylesheet" crossorigin href="/studio/assets/index-ClGKK4W3.css">
7
+ <script type="module" crossorigin src="/studio/assets/index-BT6-06vQ.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/studio/assets/index-DXr7ziDn.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectql/studio",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Web-based admin studio for ObjectQL database management",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/App.tsx CHANGED
@@ -3,6 +3,7 @@ import { Sidebar } from '@/components/Sidebar';
3
3
  import { Dashboard } from '@/pages/Dashboard';
4
4
  import { ObjectView } from '@/pages/ObjectView';
5
5
  import { SchemaEditor } from '@/pages/SchemaEditor';
6
+ import { MetadataBrowser } from '@/pages/MetadataBrowser';
6
7
  import './index.css';
7
8
 
8
9
  // Wrapper to extract params
@@ -21,6 +22,7 @@ function App() {
21
22
  <Routes>
22
23
  <Route path="/" element={<Dashboard />} />
23
24
  <Route path="/schema" element={<SchemaEditor />} />
25
+ <Route path="/metadata" element={<MetadataBrowser />} />
24
26
  <Route path="/object/:name" element={<ObjectViewWrapper />} />
25
27
  </Routes>
26
28
  </main>
@@ -30,3 +32,4 @@ function App() {
30
32
  }
31
33
 
32
34
  export default App;
35
+
@@ -1,8 +1,9 @@
1
1
  import { NavLink } from 'react-router-dom';
2
2
  import { useMetadata } from '@/hooks/use-metadata';
3
- import { Database, Home, Loader2, Table2, FileCode, BookOpen } from 'lucide-react';
3
+ import { Database, Home, Loader2, Table2, FileCode, BookOpen, Layers } from 'lucide-react';
4
4
  import { cn } from '@/lib/utils';
5
5
 
6
+
6
7
  export function Sidebar() {
7
8
  const { objects, loading, error } = useMetadata();
8
9
 
@@ -33,6 +34,17 @@ export function Sidebar() {
33
34
  </h4>
34
35
  </div>
35
36
 
37
+ <NavLink
38
+ to="/metadata"
39
+ className={({isActive}) => cn(
40
+ "flex items-center space-x-2 px-4 py-2 rounded-md text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
41
+ isActive && "bg-accent/50 text-accent-foreground"
42
+ )}
43
+ >
44
+ <Layers className="h-4 w-4" />
45
+ <span>Metadata Explorer</span>
46
+ </NavLink>
47
+
36
48
  <NavLink
37
49
  to="/schema"
38
50
  className={({isActive}) => cn(
@@ -0,0 +1,200 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Loader2, ArrowLeft, FileJson, Layers, Shield, FileText, Activity, Layout, AlertTriangle, Workflow } from 'lucide-react';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ // Helper to format JSON
8
+ function JsonViewer({ data }: { data: any }) {
9
+ return (
10
+ <pre className="bg-muted p-4 rounded-md overflow-auto text-xs font-mono max-h-[600px]">
11
+ {JSON.stringify(data, null, 2)}
12
+ </pre>
13
+ );
14
+ }
15
+
16
+ const METADATA_TYPES = [
17
+ { id: 'objects', label: 'Objects', icon: Layers },
18
+ { id: 'view', label: 'Views', icon: Layout },
19
+ { id: 'permission', label: 'Permissions', icon: Shield },
20
+ { id: 'report', label: 'Reports', icon: FileText },
21
+ { id: 'validation', label: 'Validations', icon: AlertTriangle },
22
+ { id: 'workflow', label: 'Workflows', icon: Workflow },
23
+ { id: 'form', label: 'Forms', icon: Activity },
24
+ { id: 'app', label: 'Apps', icon: FileJson },
25
+ ];
26
+
27
+ export function MetadataBrowser() {
28
+ // State
29
+ const [selectedType, setSelectedType] = useState<string | null>(null);
30
+ const [selectedItem, setSelectedItem] = useState<string | null>(null);
31
+
32
+ const [items, setItems] = useState<any[]>([]);
33
+ const [itemDetail, setItemDetail] = useState<any>(null);
34
+
35
+ const [loading, setLoading] = useState(false);
36
+ const [error, setError] = useState<string | null>(null);
37
+
38
+ // Fetch list when type changes
39
+ useEffect(() => {
40
+ if (!selectedType) return;
41
+
42
+ setLoading(true);
43
+ setItems([]);
44
+ setSelectedItem(null);
45
+ setError(null);
46
+
47
+ fetch(`/api/metadata/${selectedType}`)
48
+ .then(async res => {
49
+ if (!res.ok) throw new Error(`Failed to fetch ${selectedType}`);
50
+ const data = await res.json();
51
+ // API returns { [type]: [...] }
52
+ const list = data[selectedType] || data.objects || [];
53
+ setItems(list);
54
+ })
55
+ .catch(err => setError(err.message))
56
+ .finally(() => setLoading(false));
57
+
58
+ }, [selectedType]);
59
+
60
+ // Fetch detail when item changes
61
+ useEffect(() => {
62
+ if (!selectedType || !selectedItem) return;
63
+
64
+ setLoading(true);
65
+ setItemDetail(null);
66
+ setError(null);
67
+
68
+ // For objects, the ID is the name. For others, it relies on file structure or id
69
+ fetch(`/api/metadata/${selectedType}/${selectedItem}`)
70
+ .then(async res => {
71
+ if (!res.ok) throw new Error(`Failed to fetch detail for ${selectedItem}`);
72
+ const data = await res.json();
73
+ setItemDetail(data);
74
+ })
75
+ .catch(err => setError(err.message))
76
+ .finally(() => setLoading(false));
77
+
78
+ }, [selectedType, selectedItem]);
79
+
80
+ // --- Render: Main Menu (Type Selection) ---
81
+ if (!selectedType) {
82
+ return (
83
+ <div className="p-8 max-w-6xl mx-auto">
84
+ <h1 className="text-3xl font-bold mb-6">Metadata Registry</h1>
85
+ <p className="text-muted-foreground mb-8">
86
+ Browse the active runtime metadata loaded in the ObjectQL engine.
87
+ </p>
88
+
89
+ <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
90
+ {METADATA_TYPES.map((type) => {
91
+ const Icon = type.icon;
92
+ return (
93
+ <Card
94
+ key={type.id}
95
+ className="cursor-pointer hover:bg-accent/50 transition-colors border-2 hover:border-primary/50"
96
+ onClick={() => setSelectedType(type.id)}
97
+ >
98
+ <CardHeader className="flex flex-row items-center space-y-0 space-x-4">
99
+ <div className="p-2 bg-primary/10 rounded-full text-primary">
100
+ <Icon className="h-6 w-6" />
101
+ </div>
102
+ <CardTitle className="text-xl">{type.label}</CardTitle>
103
+ </CardHeader>
104
+ <CardContent>
105
+ <p className="text-sm text-muted-foreground">
106
+ Browse {type.label.toLowerCase()} definitions
107
+ </p>
108
+ </CardContent>
109
+ </Card>
110
+ );
111
+ })}
112
+ </div>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ // --- Render: List or Detail ---
118
+ return (
119
+ <div className="flex h-screen overflow-hidden">
120
+ {/* Left Panel: List */}
121
+ <div className="w-1/3 border-r bg-card flex flex-col">
122
+ <div className="p-4 border-b flex items-center space-x-2">
123
+ <Button variant="ghost" size="sm" onClick={() => setSelectedType(null)}>
124
+ <ArrowLeft className="h-4 w-4" />
125
+ </Button>
126
+ <h2 className="font-semibold text-lg capitalize">{selectedType} List</h2>
127
+ </div>
128
+
129
+ <div className="flex-1 overflow-auto p-2 space-y-2">
130
+ {error && (
131
+ <div className="p-4 text-sm text-red-500 bg-red-50 rounded mb-2">
132
+ Error: {error}
133
+ </div>
134
+ )}
135
+
136
+ {loading && items.length === 0 && (
137
+ <div className="flex justify-center p-8 text-muted-foreground">
138
+ <Loader2 className="h-6 w-6 animate-spin mr-2" /> Loading...
139
+ </div>
140
+ )}
141
+
142
+ {!loading && items.map((item: any) => {
143
+ const id = item.name || item.id;
144
+ return (
145
+ <div
146
+ key={id}
147
+ onClick={() => setSelectedItem(id)}
148
+ className={cn(
149
+ "p-3 rounded-md cursor-pointer text-sm font-medium transition-colors border",
150
+ selectedItem === id
151
+ ? "bg-primary text-primary-foreground border-primary"
152
+ : "hover:bg-accent border-transparent"
153
+ )}
154
+ >
155
+ <div className="flex justify-between items-center">
156
+ <span>{item.label || item.name}</span>
157
+ {item.name !== item.label && (
158
+ <span className="text-xs opacity-70 ml-2 font-mono">({item.name})</span>
159
+ )}
160
+ </div>
161
+ </div>
162
+ );
163
+ })}
164
+
165
+ {!loading && items.length === 0 && (
166
+ <div className="text-center p-8 text-muted-foreground text-sm">
167
+ No {selectedType} found.
168
+ </div>
169
+ )}
170
+ </div>
171
+ </div>
172
+
173
+ {/* Right Panel: Detail */}
174
+ <div className="flex-1 bg-muted/20 flex flex-col h-full overflow-hidden">
175
+ {!selectedItem ? (
176
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
177
+ Select an item to view details
178
+ </div>
179
+ ) : (
180
+ <div className="flex-1 flex flex-col h-full overflow-hidden">
181
+ <div className="p-4 border-b bg-card">
182
+ <h2 className="text-xl font-bold">{selectedItem}</h2>
183
+ </div>
184
+ <div className="flex-1 overflow-auto p-6">
185
+ {loading && !itemDetail ? (
186
+ <div className="flex items-center text-muted-foreground">
187
+ <Loader2 className="h-4 w-4 animate-spin mr-2" /> Loading details...
188
+ </div>
189
+ ) : (
190
+ <JsonViewer data={itemDetail} />
191
+ )}
192
+ </div>
193
+ </div>
194
+ )}
195
+ </div>
196
+ </div>
197
+ );
198
+ }
199
+
200
+ export default MetadataBrowser;
@@ -13,52 +13,130 @@ interface ObjectViewProps {
13
13
  }
14
14
 
15
15
  export function ObjectView({ objectName }: ObjectViewProps) {
16
- const [rowData, setRowData] = useState<any[]>([]);
16
+ // const [rowData, setRowData] = useState<any[]>([]); // Using Server Side
17
17
  const [columnDefs, setColumnDefs] = useState<ColDef[]>([]);
18
18
  const [loading, setLoading] = useState(false);
19
19
  const [activeTab, setActiveTab] = useState<'data' | 'schema'>('data');
20
20
  const [schemaFile, setSchemaFile] = useState<string | null>(null);
21
21
 
22
- const fetchData = async () => {
22
+ const [gridApi, setGridApi] = useState<any>(null);
23
+
24
+ // Initial load for columns
25
+ useEffect(() => {
26
+ const loadColumns = async () => {
27
+ const metaRes = await fetch('/api/metadata');
28
+ const meta = await metaRes.json();
29
+ const objects = Array.isArray(meta) ? meta : meta.objects;
30
+ const currentObj = objects.find((o: any) => o.name === objectName);
31
+
32
+ if (currentObj) {
33
+ const cols: ColDef[] = [
34
+ { field: 'id', headerName: 'ID', width: 100, pinned: 'left', filter: 'agTextColumnFilter' }
35
+ ];
36
+
37
+ Object.entries(currentObj.fields).forEach(([key, field]: [string, any]) => {
38
+ cols.push({
39
+ field: key,
40
+ headerName: field.label || key,
41
+ flex: 1,
42
+ filter: 'agTextColumnFilter', // Enforce text filter for simplicity in Infinite Model
43
+ filterParams: {
44
+ filterOptions: ['contains', 'equals'],
45
+ suppressAndOrCondition: true
46
+ }
47
+ });
48
+ });
49
+
50
+ setColumnDefs(cols);
51
+ }
52
+ };
53
+ loadColumns();
54
+ }, [objectName]);
55
+
56
+ const onGridReady = (params: any) => {
57
+ setGridApi(params.api);
23
58
  setLoading(true);
24
- try {
25
- // 1. Fetch Schema to build columns
26
- // Ideally we should cache metadata, but fetching it here is safer for now
27
- const metaRes = await fetch('/api/metadata');
28
- const meta = await metaRes.json();
29
- const objects = Array.isArray(meta) ? meta : meta.objects;
30
- const currentObj = objects.find((o: any) => o.name === objectName);
31
-
32
- if (currentObj) {
33
- const cols: ColDef[] = [
34
- { field: 'id', headerName: 'ID', width: 100, pinned: 'left' }
35
- ];
59
+
60
+ const datasource = {
61
+ getRows: async (params: any) => {
62
+ const { startRow, endRow, filterModel, sortModel } = params;
63
+ setLoading(true);
36
64
 
37
- Object.entries(currentObj.fields).forEach(([key, field]: [string, any]) => {
38
- cols.push({
39
- field: key,
40
- headerName: field.label || key,
41
- flex: 1,
42
- filter: true
65
+ try {
66
+ // 1. Convert Filters
67
+ const filters: any[] = [];
68
+ if (filterModel) {
69
+ for (const key of Object.keys(filterModel)) {
70
+ const model = filterModel[key];
71
+ // agTextColumnFilter model: { filterType: 'text', type: 'contains', filter: 'value' }
72
+ if (model.filterType === 'text') {
73
+ if (model.type === 'contains') {
74
+ filters.push([key, 'contains', model.filter]);
75
+ } else if (model.type === 'equals') {
76
+ filters.push([key, '=', model.filter]);
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ // 2. Convert Sort
83
+ const sort = sortModel.map((s: any) => [s.colId, s.sort]);
84
+
85
+ // 3. Fetch Data
86
+ const response = await fetch('/api/objectql', {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({
90
+ op: 'find',
91
+ object: objectName,
92
+ args: {
93
+ skip: startRow,
94
+ limit: endRow - startRow,
95
+ filters: filters.length > 0 ? filters : undefined,
96
+ sort: sort.length > 0 ? sort : undefined
97
+ }
98
+ })
43
99
  });
44
- });
45
-
46
- // Add system fields if not present
47
- if (!cols.find(c => c.field === 'createdAt')) cols.push({ field: 'createdAt', hide: true });
48
- if (!cols.find(c => c.field === 'updatedAt')) cols.push({ field: 'updatedAt', hide: true });
100
+ const resJson = await response.json();
101
+ const rows = resJson.data || resJson; // normalize
102
+
103
+ // 4. Fetch Count (for total pagination)
104
+ // Optimization: Only fetch count if we don't know it or filter changed?
105
+ // For Infinite Scroll simplicity, we fetch it.
106
+ let lastRow = -1;
107
+ if (rows.length < (endRow - startRow)) {
108
+ lastRow = startRow + rows.length;
109
+ } else {
110
+ const countRes = await fetch('/api/objectql', {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json' },
113
+ body: JSON.stringify({
114
+ op: 'count',
115
+ object: objectName,
116
+ args: filters.length > 0 ? filters : {}
117
+ })
118
+ });
119
+ const countJson = await countRes.json();
120
+ lastRow = typeof countJson === 'number' ? countJson : countJson.data;
121
+ }
49
122
 
50
- setColumnDefs(cols);
123
+ params.successCallback(rows, lastRow);
124
+
125
+ } catch (e) {
126
+ console.error('Datasource getRows error:', e);
127
+ params.failCallback();
128
+ } finally {
129
+ setLoading(false);
130
+ }
51
131
  }
132
+ };
52
133
 
53
- // 2. Fetch Data
54
- const res = await fetch(`/api/objectql/${objectName}`);
55
- const data = await res.json();
56
- setRowData(Array.isArray(data) ? data : []);
134
+ params.api.setDatasource(datasource);
135
+ };
57
136
 
58
- } catch (e) {
59
- console.error(e);
60
- } finally {
61
- setLoading(false);
137
+ const refreshData = () => {
138
+ if (gridApi) {
139
+ gridApi.refreshInfiniteCache();
62
140
  }
63
141
  };
64
142
 
@@ -84,18 +162,18 @@ export function ObjectView({ objectName }: ObjectViewProps) {
84
162
  }, [objectName]);
85
163
 
86
164
  useEffect(() => {
87
- if (activeTab === 'data') {
88
- fetchData();
89
- } else {
165
+ if (activeTab === 'schema') {
90
166
  fetchSchemaFile();
91
167
  }
92
168
  }, [objectName, activeTab]);
93
169
 
170
+
94
171
  const defaultColDef = useMemo(() => {
95
172
  return {
96
173
  sortable: true,
97
174
  filter: true,
98
175
  resizable: true,
176
+ floatingFilter: true, // Enable Floating Filter
99
177
  };
100
178
  }, []);
101
179
 
@@ -135,7 +213,7 @@ export function ObjectView({ objectName }: ObjectViewProps) {
135
213
 
136
214
  {activeTab === 'data' && (
137
215
  <div className="flex items-center space-x-2">
138
- <Button variant="outline" size="sm" onClick={fetchData} disabled={loading}>
216
+ <Button variant="outline" size="sm" onClick={refreshData} disabled={loading}>
139
217
  <RefreshCw className={cn("mr-2 h-4 w-4", loading && "animate-spin")} />
140
218
  Refresh
141
219
  </Button>
@@ -150,15 +228,18 @@ export function ObjectView({ objectName }: ObjectViewProps) {
150
228
  {/* Content */}
151
229
  <div className="flex-1 overflow-hidden p-6 bg-muted/20">
152
230
  {activeTab === 'data' ? (
153
- <div className="border rounded-md overflow-hidden bg-card h-full" style={{opacity: loading ? 0.6 : 1}}>
231
+ <div className="rounded-md overflow-hidden bg-card h-full" style={{opacity: loading ? 0.6 : 1}}>
154
232
  <div className="ag-theme-quartz h-full w-full">
155
233
  <AgGridReact
156
- rowData={rowData}
234
+ rowModelType="infinite"
235
+ onGridReady={onGridReady}
157
236
  columnDefs={columnDefs}
158
237
  defaultColDef={defaultColDef}
159
238
  pagination={true}
160
239
  paginationPageSize={20}
240
+ cacheBlockSize={20}
161
241
  rowSelection="multiple"
242
+ maxBlocksInCache={10}
162
243
  />
163
244
  </div>
164
245
  </div>
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "extends": "../../tsconfig.base.json",
2
+ "extends": "../../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "target": "ES2020",
5
5
  "useDefineForClassFields": true,