@objectql/studio 1.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.
@@ -0,0 +1,137 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+
4
+ interface ObjectInfo {
5
+ name: string;
6
+ label?: string;
7
+ icon?: string;
8
+ recordCount?: number;
9
+ }
10
+
11
+ function ObjectList() {
12
+ const [objects, setObjects] = useState<ObjectInfo[]>([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [error, setError] = useState<string | null>(null);
15
+
16
+ useEffect(() => {
17
+ fetchObjects();
18
+ }, []);
19
+
20
+ const fetchObjects = async () => {
21
+ try {
22
+ setLoading(true);
23
+ const response = await fetch('/api/metadata/objects');
24
+ if (!response.ok) {
25
+ throw new Error(`HTTP error! status: ${response.status}`);
26
+ }
27
+ const data = await response.json();
28
+ setObjects(data.objects || []);
29
+ } catch (e: any) {
30
+ console.error('Failed to fetch objects:', e);
31
+ setError(e.message);
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ };
36
+
37
+ if (loading) {
38
+ return (
39
+ <div className="loading">
40
+ <div className="spinner"></div>
41
+ <p>Loading objects...</p>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ if (error) {
47
+ return (
48
+ <div className="alert alert-error">
49
+ <strong>Error:</strong> {error}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ if (objects.length === 0) {
55
+ return (
56
+ <div className="card">
57
+ <h2 className="card-title">No Objects Found</h2>
58
+ <p>No objects are registered in the ObjectQL instance.</p>
59
+ <p className="alert alert-info" style={{ marginTop: '1rem' }}>
60
+ Make sure your ObjectQL server has loaded object definitions.
61
+ </p>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <div>
68
+ <div className="card">
69
+ <h2 className="card-title">Database Objects</h2>
70
+ <p style={{ color: '#718096', marginBottom: '1.5rem' }}>
71
+ Browse and manage data across all registered objects
72
+ </p>
73
+
74
+ <div className="grid grid-2">
75
+ {objects.map((obj) => (
76
+ <Link
77
+ key={obj.name}
78
+ to={`/object/${obj.name}`}
79
+ style={{ textDecoration: 'none', color: 'inherit' }}
80
+ >
81
+ <div
82
+ className="card"
83
+ style={{
84
+ cursor: 'pointer',
85
+ transition: 'all 0.2s',
86
+ border: '1px solid #e2e8f0',
87
+ }}
88
+ onMouseOver={(e) => {
89
+ e.currentTarget.style.borderColor = '#667eea';
90
+ e.currentTarget.style.transform = 'translateY(-2px)';
91
+ e.currentTarget.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
92
+ }}
93
+ onMouseOut={(e) => {
94
+ e.currentTarget.style.borderColor = '#e2e8f0';
95
+ e.currentTarget.style.transform = 'translateY(0)';
96
+ e.currentTarget.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.1)';
97
+ }}
98
+ >
99
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
100
+ <div
101
+ style={{
102
+ width: '48px',
103
+ height: '48px',
104
+ borderRadius: '8px',
105
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
106
+ display: 'flex',
107
+ alignItems: 'center',
108
+ justifyContent: 'center',
109
+ color: 'white',
110
+ fontSize: '1.5rem',
111
+ fontWeight: 'bold',
112
+ }}
113
+ >
114
+ {obj.icon || obj.name.charAt(0).toUpperCase()}
115
+ </div>
116
+ <div style={{ flex: 1 }}>
117
+ <h3 style={{ fontSize: '1.125rem', fontWeight: '600', marginBottom: '0.25rem' }}>
118
+ {obj.label || obj.name}
119
+ </h3>
120
+ <p style={{ fontSize: '0.875rem', color: '#718096' }}>
121
+ {obj.name}
122
+ {typeof obj.recordCount === 'number' && (
123
+ <span> • {obj.recordCount} records</span>
124
+ )}
125
+ </p>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </Link>
130
+ ))}
131
+ </div>
132
+ </div>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ export default ObjectList;
@@ -0,0 +1,227 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useParams, Link, useNavigate } from 'react-router-dom';
3
+
4
+ interface Record {
5
+ _id?: string;
6
+ id?: string;
7
+ [key: string]: any;
8
+ }
9
+
10
+ function RecordDetail() {
11
+ const { objectName, recordId } = useParams<{ objectName: string; recordId: string }>();
12
+ const navigate = useNavigate();
13
+ const [record, setRecord] = useState<Record | null>(null);
14
+ const [loading, setLoading] = useState(true);
15
+ const [error, setError] = useState<string | null>(null);
16
+ const [editing, setEditing] = useState(false);
17
+ const [formData, setFormData] = useState<Record>({});
18
+
19
+ useEffect(() => {
20
+ fetchRecord();
21
+ }, [objectName, recordId]);
22
+
23
+ const fetchRecord = async () => {
24
+ try {
25
+ setLoading(true);
26
+ const response = await fetch('/api/objectql', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({
30
+ op: 'findOne',
31
+ object: objectName,
32
+ args: {
33
+ filters: [['_id', '=', recordId]].concat(
34
+ recordId ? [['id', '=', recordId]] : []
35
+ )
36
+ }
37
+ })
38
+ });
39
+
40
+ if (!response.ok) {
41
+ throw new Error(`HTTP error! status: ${response.status}`);
42
+ }
43
+
44
+ const result = await response.json();
45
+ setRecord(result.data);
46
+ setFormData(result.data || {});
47
+ } catch (e: any) {
48
+ console.error('Failed to fetch record:', e);
49
+ setError(e.message);
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ };
54
+
55
+ const handleUpdate = async () => {
56
+ try {
57
+ const response = await fetch('/api/objectql', {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ op: 'update',
62
+ object: objectName,
63
+ args: {
64
+ id: recordId,
65
+ data: formData
66
+ }
67
+ })
68
+ });
69
+
70
+ if (!response.ok) {
71
+ throw new Error('Failed to update record');
72
+ }
73
+
74
+ setEditing(false);
75
+ fetchRecord();
76
+ } catch (e: any) {
77
+ alert(`Error: ${e.message}`);
78
+ }
79
+ };
80
+
81
+ const handleDelete = async () => {
82
+ if (!confirm('Are you sure you want to delete this record?')) {
83
+ return;
84
+ }
85
+
86
+ try {
87
+ const response = await fetch('/api/objectql', {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({
91
+ op: 'delete',
92
+ object: objectName,
93
+ args: { id: recordId }
94
+ })
95
+ });
96
+
97
+ if (!response.ok) {
98
+ throw new Error('Failed to delete record');
99
+ }
100
+
101
+ navigate(`/object/${objectName}`);
102
+ } catch (e: any) {
103
+ alert(`Error: ${e.message}`);
104
+ }
105
+ };
106
+
107
+ const formatValue = (value: any): string => {
108
+ if (value === null || value === undefined) return '-';
109
+ if (typeof value === 'object') return JSON.stringify(value, null, 2);
110
+ if (typeof value === 'boolean') return value ? 'Yes' : 'No';
111
+ return String(value);
112
+ };
113
+
114
+ if (loading) {
115
+ return (
116
+ <div className="loading">
117
+ <div className="spinner"></div>
118
+ <p>Loading record...</p>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ if (error || !record) {
124
+ return (
125
+ <div>
126
+ <div className="breadcrumb">
127
+ <Link to="/">Objects</Link>
128
+ <span className="breadcrumb-separator">/</span>
129
+ <Link to={`/object/${objectName}`}>{objectName}</Link>
130
+ <span className="breadcrumb-separator">/</span>
131
+ <span>{recordId}</span>
132
+ </div>
133
+ <div className="alert alert-error">
134
+ <strong>Error:</strong> {error || 'Record not found'}
135
+ </div>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ return (
141
+ <div>
142
+ <div className="breadcrumb">
143
+ <Link to="/">Objects</Link>
144
+ <span className="breadcrumb-separator">/</span>
145
+ <Link to={`/object/${objectName}`}>{objectName}</Link>
146
+ <span className="breadcrumb-separator">/</span>
147
+ <span>{recordId}</span>
148
+ </div>
149
+
150
+ <div className="card">
151
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
152
+ <h2 className="card-title" style={{ marginBottom: 0 }}>Record Detail</h2>
153
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
154
+ {editing ? (
155
+ <>
156
+ <button className="btn btn-primary" onClick={handleUpdate}>Save</button>
157
+ <button className="btn btn-secondary" onClick={() => {
158
+ setEditing(false);
159
+ setFormData(record);
160
+ }}>Cancel</button>
161
+ </>
162
+ ) : (
163
+ <>
164
+ <button className="btn btn-primary" onClick={() => setEditing(true)}>Edit</button>
165
+ <button className="btn btn-danger" onClick={handleDelete}>Delete</button>
166
+ </>
167
+ )}
168
+ </div>
169
+ </div>
170
+
171
+ {editing ? (
172
+ <div>
173
+ {Object.entries(formData).map(([key, value]) => (
174
+ <div key={key} className="form-group">
175
+ <label className="form-label">{key}</label>
176
+ {typeof value === 'object' && value !== null ? (
177
+ <textarea
178
+ className="form-textarea"
179
+ value={JSON.stringify(value, null, 2)}
180
+ onChange={(e) => {
181
+ try {
182
+ setFormData({ ...formData, [key]: JSON.parse(e.target.value) });
183
+ } catch {
184
+ // Invalid JSON, keep as string
185
+ }
186
+ }}
187
+ />
188
+ ) : (
189
+ <input
190
+ type="text"
191
+ className="form-input"
192
+ value={String(value || '')}
193
+ onChange={(e) => setFormData({ ...formData, [key]: e.target.value })}
194
+ disabled={key === '_id' || key === 'id'}
195
+ />
196
+ )}
197
+ </div>
198
+ ))}
199
+ </div>
200
+ ) : (
201
+ <div className="table-container">
202
+ <table className="table">
203
+ <thead>
204
+ <tr>
205
+ <th style={{ width: '30%' }}>Field</th>
206
+ <th>Value</th>
207
+ </tr>
208
+ </thead>
209
+ <tbody>
210
+ {Object.entries(record).map(([key, value]) => (
211
+ <tr key={key}>
212
+ <td><strong>{key}</strong></td>
213
+ <td style={{ whiteSpace: 'pre-wrap', fontFamily: typeof value === 'object' ? 'monospace' : 'inherit' }}>
214
+ {formatValue(value)}
215
+ </td>
216
+ </tr>
217
+ ))}
218
+ </tbody>
219
+ </table>
220
+ </div>
221
+ )}
222
+ </div>
223
+ </div>
224
+ );
225
+ }
226
+
227
+ export default RecordDetail;
@@ -0,0 +1,204 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useParams, Link } from 'react-router-dom';
3
+
4
+ interface Field {
5
+ name: string;
6
+ type?: string;
7
+ label?: string;
8
+ required?: boolean;
9
+ defaultValue?: any;
10
+ }
11
+
12
+ interface ObjectMetadata {
13
+ name: string;
14
+ label?: string;
15
+ fields?: Field[];
16
+ [key: string]: any;
17
+ }
18
+
19
+ function SchemaInspector() {
20
+ const { objectName } = useParams<{ objectName?: string }>();
21
+ const [objects, setObjects] = useState<ObjectMetadata[]>([]);
22
+ const [selectedObject, setSelectedObject] = useState<ObjectMetadata | null>(null);
23
+ const [loading, setLoading] = useState(true);
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ fetchObjects();
28
+ }, []);
29
+
30
+ useEffect(() => {
31
+ if (objectName) {
32
+ fetchObjectDetails(objectName);
33
+ }
34
+ }, [objectName]);
35
+
36
+ const fetchObjects = async () => {
37
+ try {
38
+ setLoading(true);
39
+ const response = await fetch('/api/metadata/objects');
40
+ if (!response.ok) {
41
+ throw new Error(`HTTP error! status: ${response.status}`);
42
+ }
43
+ const data = await response.json();
44
+ setObjects(data.objects || []);
45
+ } catch (e: any) {
46
+ console.error('Failed to fetch objects:', e);
47
+ setError(e.message);
48
+ } finally {
49
+ setLoading(false);
50
+ }
51
+ };
52
+
53
+ const fetchObjectDetails = async (name: string) => {
54
+ try {
55
+ const response = await fetch(`/api/metadata/objects/${name}`);
56
+ if (!response.ok) {
57
+ throw new Error(`HTTP error! status: ${response.status}`);
58
+ }
59
+ const data = await response.json();
60
+ setSelectedObject(data);
61
+ } catch (e: any) {
62
+ console.error('Failed to fetch object details:', e);
63
+ setError(e.message);
64
+ }
65
+ };
66
+
67
+ if (loading) {
68
+ return (
69
+ <div className="loading">
70
+ <div className="spinner"></div>
71
+ <p>Loading schema...</p>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ if (error) {
77
+ return (
78
+ <div className="alert alert-error">
79
+ <strong>Error:</strong> {error}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ return (
85
+ <div>
86
+ {objectName && (
87
+ <div className="breadcrumb">
88
+ <Link to="/schema">Schema</Link>
89
+ <span className="breadcrumb-separator">/</span>
90
+ <span>{objectName}</span>
91
+ </div>
92
+ )}
93
+
94
+ <div className="card">
95
+ <h2 className="card-title">Schema Inspector</h2>
96
+ <p style={{ color: '#718096', marginBottom: '1.5rem' }}>
97
+ View object definitions and field metadata
98
+ </p>
99
+
100
+ {!objectName ? (
101
+ <div className="grid grid-2">
102
+ {objects.map((obj) => (
103
+ <Link
104
+ key={obj.name}
105
+ to={`/schema/${obj.name}`}
106
+ style={{ textDecoration: 'none', color: 'inherit' }}
107
+ >
108
+ <div
109
+ className="card"
110
+ style={{
111
+ cursor: 'pointer',
112
+ transition: 'all 0.2s',
113
+ border: '1px solid #e2e8f0',
114
+ }}
115
+ onMouseOver={(e) => {
116
+ e.currentTarget.style.borderColor = '#667eea';
117
+ e.currentTarget.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
118
+ }}
119
+ onMouseOut={(e) => {
120
+ e.currentTarget.style.borderColor = '#e2e8f0';
121
+ e.currentTarget.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.1)';
122
+ }}
123
+ >
124
+ <h3 style={{ fontSize: '1.125rem', fontWeight: '600', marginBottom: '0.25rem' }}>
125
+ {obj.label || obj.name}
126
+ </h3>
127
+ <p style={{ fontSize: '0.875rem', color: '#718096' }}>
128
+ {obj.name}
129
+ </p>
130
+ </div>
131
+ </Link>
132
+ ))}
133
+ </div>
134
+ ) : selectedObject ? (
135
+ <div>
136
+ <div style={{ marginBottom: '2rem' }}>
137
+ <h3 style={{ fontSize: '1.25rem', fontWeight: '600', marginBottom: '0.5rem' }}>
138
+ {selectedObject.label || selectedObject.name}
139
+ </h3>
140
+ <p style={{ color: '#718096' }}>API Name: <code>{selectedObject.name}</code></p>
141
+ </div>
142
+
143
+ {selectedObject.fields && selectedObject.fields.length > 0 ? (
144
+ <div>
145
+ <h4 style={{ fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }}>Fields</h4>
146
+ <div className="table-container">
147
+ <table className="table">
148
+ <thead>
149
+ <tr>
150
+ <th>Name</th>
151
+ <th>Label</th>
152
+ <th>Type</th>
153
+ <th>Required</th>
154
+ <th>Default Value</th>
155
+ </tr>
156
+ </thead>
157
+ <tbody>
158
+ {selectedObject.fields.map((field) => (
159
+ <tr key={field.name}>
160
+ <td><code>{field.name}</code></td>
161
+ <td>{field.label || '-'}</td>
162
+ <td>
163
+ <span className="badge badge-primary">{field.type || 'text'}</span>
164
+ </td>
165
+ <td>{field.required ? 'Yes' : 'No'}</td>
166
+ <td>{field.defaultValue !== undefined ? String(field.defaultValue) : '-'}</td>
167
+ </tr>
168
+ ))}
169
+ </tbody>
170
+ </table>
171
+ </div>
172
+ </div>
173
+ ) : (
174
+ <div className="alert alert-info">
175
+ No field metadata available for this object.
176
+ </div>
177
+ )}
178
+
179
+ <div style={{ marginTop: '2rem' }}>
180
+ <h4 style={{ fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }}>Raw Definition</h4>
181
+ <pre style={{
182
+ background: '#f7fafc',
183
+ padding: '1rem',
184
+ borderRadius: '6px',
185
+ overflow: 'auto',
186
+ fontSize: '0.875rem',
187
+ border: '1px solid #e2e8f0'
188
+ }}>
189
+ {JSON.stringify(selectedObject, null, 2)}
190
+ </pre>
191
+ </div>
192
+ </div>
193
+ ) : (
194
+ <div className="loading">
195
+ <div className="spinner"></div>
196
+ <p>Loading object details...</p>
197
+ </div>
198
+ )}
199
+ </div>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ export default SchemaInspector;
package/src/index.css ADDED
@@ -0,0 +1,23 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
9
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
10
+ sans-serif;
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ background: #f5f5f5;
14
+ }
15
+
16
+ code {
17
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
18
+ monospace;
19
+ }
20
+
21
+ #root {
22
+ min-height: 100vh;
23
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ "strict": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true
19
+ },
20
+ "include": ["src"]
21
+ }