@geekmidas/studio 0.2.0 → 0.4.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/{DataBrowser-hGwiTffZ.d.cts → DataBrowser-B-jz8KBR.d.mts} +5 -2
- package/dist/DataBrowser-B-jz8KBR.d.mts.map +1 -0
- package/dist/{DataBrowser-SOcqmZb2.d.mts → DataBrowser-BTe9HWJy.d.cts} +5 -2
- package/dist/DataBrowser-BTe9HWJy.d.cts.map +1 -0
- package/dist/{DataBrowser-c-Gs6PZB.cjs → DataBrowser-D8c_pBf4.cjs} +4 -4
- package/dist/DataBrowser-D8c_pBf4.cjs.map +1 -0
- package/dist/{DataBrowser-DQ3-ZxdV.mjs → DataBrowser-kgcI9ApJ.mjs} +4 -4
- package/dist/DataBrowser-kgcI9ApJ.mjs.map +1 -0
- package/dist/Studio-CYzz3wD2.d.cts +152 -0
- package/dist/Studio-CYzz3wD2.d.cts.map +1 -0
- package/dist/Studio-D5yGscb8.d.mts +152 -0
- package/dist/Studio-D5yGscb8.d.mts.map +1 -0
- package/dist/data/index.cjs +1 -1
- package/dist/data/index.d.cts +1 -1
- package/dist/data/index.d.mts +1 -1
- package/dist/data/index.mjs +1 -1
- package/dist/index.cjs +33 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -131
- package/dist/index.d.mts +4 -131
- package/dist/index.mjs +33 -3
- package/dist/index.mjs.map +1 -1
- package/dist/server/hono.cjs +168 -21
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.d.cts +13 -2
- package/dist/server/hono.d.cts.map +1 -0
- package/dist/server/hono.d.mts +13 -2
- package/dist/server/hono.d.mts.map +1 -0
- package/dist/server/hono.mjs +168 -21
- package/dist/server/hono.mjs.map +1 -1
- package/dist/types-BZv87Ikv.mjs.map +1 -1
- package/dist/types-CMttUZYk.cjs.map +1 -1
- package/package.json +5 -5
- package/src/Studio.ts +341 -292
- package/src/__tests__/Studio.spec.ts +447 -0
- package/src/data/DataBrowser.ts +147 -143
- package/src/data/__tests__/DataBrowser.integration.spec.ts +404 -404
- package/src/data/__tests__/filtering.integration.spec.ts +726 -726
- package/src/data/__tests__/introspection.integration.spec.ts +340 -340
- package/src/data/__tests__/pagination.spec.ts +123 -0
- package/src/data/filtering.ts +154 -154
- package/src/data/introspection.ts +141 -141
- package/src/data/pagination.ts +15 -15
- package/src/index.ts +22 -24
- package/src/server/__tests__/hono.integration.spec.ts +605 -347
- package/src/server/hono.ts +392 -190
- package/src/types.ts +138 -138
- package/src/ui-assets.ts +10 -13
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +9 -9
- package/ui/package.json +28 -22
- package/ui/src/App.tsx +95 -235
- package/ui/src/api.ts +184 -42
- package/ui/src/components/FilterPanel.tsx +198 -198
- package/ui/src/components/NavRail.tsx +183 -0
- package/ui/src/components/RowDetail.tsx +106 -106
- package/ui/src/components/StudioHeader.tsx +109 -0
- package/ui/src/components/TableList.tsx +49 -49
- package/ui/src/components/TableView.tsx +530 -485
- package/ui/src/main.tsx +3 -3
- package/ui/src/pages/DashboardPage.tsx +500 -0
- package/ui/src/pages/DatabasePage.tsx +226 -0
- package/ui/src/pages/EndpointDetailsPage.tsx +288 -0
- package/ui/src/pages/ExceptionsPage.tsx +268 -0
- package/ui/src/pages/LogsPage.tsx +228 -0
- package/ui/src/pages/MonitoringPage.tsx +46 -0
- package/ui/src/pages/PerformancePage.tsx +307 -0
- package/ui/src/pages/RequestsPage.tsx +379 -0
- package/ui/src/providers/StudioProvider.tsx +194 -0
- package/ui/src/styles.css +53 -142
- package/ui/src/types.ts +154 -30
- package/ui/tsconfig.tsbuildinfo +1 -1
- package/ui/vite.config.ts +6 -6
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +0 -1
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +0 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { InMemoryStorage } from '@geekmidas/telescope';
|
|
2
|
+
import {
|
|
3
|
+
CamelCasePlugin,
|
|
4
|
+
type Generated,
|
|
5
|
+
Kysely,
|
|
6
|
+
PostgresDialect,
|
|
7
|
+
} from 'kysely';
|
|
8
|
+
import pg from 'pg';
|
|
9
|
+
import {
|
|
10
|
+
afterAll,
|
|
11
|
+
afterEach,
|
|
12
|
+
beforeAll,
|
|
13
|
+
beforeEach,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
it,
|
|
17
|
+
vi,
|
|
18
|
+
} from 'vitest';
|
|
19
|
+
import { TEST_DATABASE_CONFIG } from '../../../testkit/test/globalSetup';
|
|
20
|
+
import { Studio } from '../Studio';
|
|
21
|
+
import { Direction } from '../types';
|
|
22
|
+
|
|
23
|
+
interface TestDatabase {
|
|
24
|
+
studioTestUsers: {
|
|
25
|
+
id: Generated<number>;
|
|
26
|
+
name: string;
|
|
27
|
+
email: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('Studio', () => {
|
|
32
|
+
let db: Kysely<TestDatabase>;
|
|
33
|
+
let studio: Studio<TestDatabase>;
|
|
34
|
+
let storage: InMemoryStorage;
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
db = new Kysely<TestDatabase>({
|
|
38
|
+
dialect: new PostgresDialect({
|
|
39
|
+
pool: new pg.Pool({
|
|
40
|
+
...TEST_DATABASE_CONFIG,
|
|
41
|
+
database: 'postgres',
|
|
42
|
+
}),
|
|
43
|
+
}),
|
|
44
|
+
plugins: [new CamelCasePlugin()],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Create test table
|
|
48
|
+
await db.schema
|
|
49
|
+
.createTable('studio_test_users')
|
|
50
|
+
.ifNotExists()
|
|
51
|
+
.addColumn('id', 'serial', (col) => col.primaryKey())
|
|
52
|
+
.addColumn('name', 'varchar(255)', (col) => col.notNull())
|
|
53
|
+
.addColumn('email', 'varchar(255)', (col) => col.notNull())
|
|
54
|
+
.execute();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
storage = new InMemoryStorage();
|
|
59
|
+
studio = new Studio({
|
|
60
|
+
monitoring: {
|
|
61
|
+
storage,
|
|
62
|
+
},
|
|
63
|
+
data: {
|
|
64
|
+
db,
|
|
65
|
+
cursor: { field: 'id', direction: Direction.Desc },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(async () => {
|
|
71
|
+
studio.destroy();
|
|
72
|
+
await db.deleteFrom('studioTestUsers').execute();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
await db.schema.dropTable('studio_test_users').ifExists().execute();
|
|
77
|
+
await db.destroy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('configuration', () => {
|
|
81
|
+
it('should expose path property with default value', () => {
|
|
82
|
+
expect(studio.path).toBe('/__studio');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should allow custom path', () => {
|
|
86
|
+
const customStudio = new Studio({
|
|
87
|
+
monitoring: { storage },
|
|
88
|
+
data: { db, cursor: { field: 'id', direction: Direction.Desc } },
|
|
89
|
+
path: '/custom-studio',
|
|
90
|
+
});
|
|
91
|
+
expect(customStudio.path).toBe('/custom-studio');
|
|
92
|
+
customStudio.destroy();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should expose enabled property', () => {
|
|
96
|
+
expect(studio.enabled).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should allow disabling studio', () => {
|
|
100
|
+
const disabledStudio = new Studio({
|
|
101
|
+
monitoring: { storage },
|
|
102
|
+
data: { db, cursor: { field: 'id', direction: Direction.Desc } },
|
|
103
|
+
enabled: false,
|
|
104
|
+
});
|
|
105
|
+
expect(disabledStudio.enabled).toBe(false);
|
|
106
|
+
disabledStudio.destroy();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should expose recordBody property', () => {
|
|
110
|
+
expect(studio.recordBody).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should expose maxBodySize property', () => {
|
|
114
|
+
expect(studio.maxBodySize).toBe(64 * 1024); // 64KB default
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should expose data browser instance', () => {
|
|
118
|
+
expect(studio.data).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('monitoring - recording', () => {
|
|
123
|
+
it('should record requests', async () => {
|
|
124
|
+
const requestId = await studio.recordRequest({
|
|
125
|
+
method: 'GET',
|
|
126
|
+
path: '/api/users',
|
|
127
|
+
status: 200,
|
|
128
|
+
duration: 100,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(requestId).toBeDefined();
|
|
132
|
+
expect(typeof requestId).toBe('string');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should record log entries in batch', async () => {
|
|
136
|
+
await studio.log([
|
|
137
|
+
{ level: 'info', message: 'Test log 1' },
|
|
138
|
+
{ level: 'error', message: 'Test log 2' },
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const logs = await studio.getLogs();
|
|
142
|
+
expect(logs.length).toBeGreaterThanOrEqual(2);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should log debug messages', async () => {
|
|
146
|
+
await studio.debug('Debug message', { foo: 'bar' });
|
|
147
|
+
|
|
148
|
+
const logs = await studio.getLogs({ level: 'debug' });
|
|
149
|
+
expect(logs.some((l) => l.message === 'Debug message')).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should log info messages', async () => {
|
|
153
|
+
await studio.info('Info message', { key: 'value' });
|
|
154
|
+
|
|
155
|
+
const logs = await studio.getLogs({ level: 'info' });
|
|
156
|
+
expect(logs.some((l) => l.message === 'Info message')).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should log warning messages', async () => {
|
|
160
|
+
await studio.warn('Warning message');
|
|
161
|
+
|
|
162
|
+
const logs = await studio.getLogs({ level: 'warn' });
|
|
163
|
+
expect(logs.some((l) => l.message === 'Warning message')).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should log error messages', async () => {
|
|
167
|
+
await studio.error('Error message');
|
|
168
|
+
|
|
169
|
+
const logs = await studio.getLogs({ level: 'error' });
|
|
170
|
+
expect(logs.some((l) => l.message === 'Error message')).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should record exceptions', async () => {
|
|
174
|
+
const error = new Error('Test error');
|
|
175
|
+
await studio.exception(error);
|
|
176
|
+
|
|
177
|
+
const exceptions = await studio.getExceptions();
|
|
178
|
+
expect(exceptions.some((e) => e.message === 'Test error')).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should record exceptions with request ID', async () => {
|
|
182
|
+
const requestId = await studio.recordRequest({
|
|
183
|
+
method: 'GET',
|
|
184
|
+
path: '/api/users',
|
|
185
|
+
status: 500,
|
|
186
|
+
duration: 50,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await studio.exception(new Error('Request error'), requestId);
|
|
190
|
+
|
|
191
|
+
const exceptions = await studio.getExceptions();
|
|
192
|
+
expect(exceptions.some((e) => e.requestId === requestId)).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('monitoring - querying', () => {
|
|
197
|
+
beforeEach(async () => {
|
|
198
|
+
// Record some test data
|
|
199
|
+
await studio.recordRequest({
|
|
200
|
+
method: 'GET',
|
|
201
|
+
path: '/api/users',
|
|
202
|
+
status: 200,
|
|
203
|
+
duration: 100,
|
|
204
|
+
});
|
|
205
|
+
await studio.recordRequest({
|
|
206
|
+
method: 'POST',
|
|
207
|
+
path: '/api/users',
|
|
208
|
+
status: 201,
|
|
209
|
+
duration: 150,
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should get all requests', async () => {
|
|
214
|
+
const requests = await studio.getRequests();
|
|
215
|
+
expect(requests.length).toBeGreaterThanOrEqual(2);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should get requests with limit', async () => {
|
|
219
|
+
const requests = await studio.getRequests({ limit: 1 });
|
|
220
|
+
expect(requests.length).toBe(1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should get a single request by ID', async () => {
|
|
224
|
+
const requestId = await studio.recordRequest({
|
|
225
|
+
method: 'DELETE',
|
|
226
|
+
path: '/api/users/123',
|
|
227
|
+
status: 204,
|
|
228
|
+
duration: 50,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const request = await studio.getRequest(requestId);
|
|
232
|
+
expect(request).toBeDefined();
|
|
233
|
+
expect(request?.method).toBe('DELETE');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should return null for non-existent request', async () => {
|
|
237
|
+
const request = await studio.getRequest('non-existent-id');
|
|
238
|
+
expect(request).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should get exceptions', async () => {
|
|
242
|
+
await studio.exception(new Error('Test exception'));
|
|
243
|
+
const exceptions = await studio.getExceptions();
|
|
244
|
+
expect(exceptions.length).toBeGreaterThanOrEqual(1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should get a single exception by ID', async () => {
|
|
248
|
+
await studio.exception(new Error('Another exception'));
|
|
249
|
+
const exceptions = await studio.getExceptions();
|
|
250
|
+
const first = exceptions[0];
|
|
251
|
+
|
|
252
|
+
const exception = await studio.getException(first.id);
|
|
253
|
+
expect(exception).toBeDefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should return null for non-existent exception', async () => {
|
|
257
|
+
const exception = await studio.getException('non-existent-id');
|
|
258
|
+
expect(exception).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should get logs', async () => {
|
|
262
|
+
await studio.info('Log entry');
|
|
263
|
+
const logs = await studio.getLogs();
|
|
264
|
+
expect(logs.length).toBeGreaterThanOrEqual(1);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should get storage stats', async () => {
|
|
268
|
+
const stats = await studio.getStats();
|
|
269
|
+
expect(stats).toBeDefined();
|
|
270
|
+
expect(typeof stats.requests).toBe('number');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('metrics', () => {
|
|
275
|
+
it('should get aggregated metrics', () => {
|
|
276
|
+
const metrics = studio.getMetrics();
|
|
277
|
+
expect(metrics).toBeDefined();
|
|
278
|
+
expect(typeof metrics.totalRequests).toBe('number');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should get endpoint metrics', () => {
|
|
282
|
+
const endpoints = studio.getEndpointMetrics();
|
|
283
|
+
expect(Array.isArray(endpoints)).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should get status distribution', () => {
|
|
287
|
+
const distribution = studio.getStatusDistribution();
|
|
288
|
+
expect(distribution).toBeDefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should reset metrics', () => {
|
|
292
|
+
// Just ensure it doesn't throw
|
|
293
|
+
studio.resetMetrics();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('path ignoring', () => {
|
|
298
|
+
it('should ignore studio paths', () => {
|
|
299
|
+
expect(studio.shouldIgnore('/__studio')).toBe(true);
|
|
300
|
+
expect(studio.shouldIgnore('/__studio/api/requests')).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should not ignore other paths', () => {
|
|
304
|
+
expect(studio.shouldIgnore('/api/users')).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should respect custom ignore patterns', () => {
|
|
308
|
+
const studioWithPatterns = new Studio({
|
|
309
|
+
monitoring: {
|
|
310
|
+
storage,
|
|
311
|
+
ignorePatterns: ['/health', '/metrics'],
|
|
312
|
+
},
|
|
313
|
+
data: {
|
|
314
|
+
db,
|
|
315
|
+
cursor: { field: 'id', direction: Direction.Desc },
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(studioWithPatterns.shouldIgnore('/health')).toBe(true);
|
|
320
|
+
expect(studioWithPatterns.shouldIgnore('/api/users')).toBe(false);
|
|
321
|
+
|
|
322
|
+
studioWithPatterns.destroy();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('pruning', () => {
|
|
327
|
+
it('should prune old entries', async () => {
|
|
328
|
+
// Record some data
|
|
329
|
+
await studio.recordRequest({
|
|
330
|
+
method: 'GET',
|
|
331
|
+
path: '/old-request',
|
|
332
|
+
status: 200,
|
|
333
|
+
duration: 100,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Prune entries older than now (should remove all)
|
|
337
|
+
const futureDate = new Date(Date.now() + 1000);
|
|
338
|
+
const pruned = await studio.prune(futureDate);
|
|
339
|
+
|
|
340
|
+
expect(typeof pruned).toBe('number');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('WebSocket management', () => {
|
|
345
|
+
it('should add WebSocket clients', () => {
|
|
346
|
+
const mockWs = {
|
|
347
|
+
send: vi.fn(),
|
|
348
|
+
readyState: 1, // WebSocket.OPEN
|
|
349
|
+
} as any;
|
|
350
|
+
|
|
351
|
+
// Should not throw
|
|
352
|
+
studio.addWsClient(mockWs);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should remove WebSocket clients', () => {
|
|
356
|
+
const mockWs = {
|
|
357
|
+
send: vi.fn(),
|
|
358
|
+
readyState: 1,
|
|
359
|
+
} as any;
|
|
360
|
+
|
|
361
|
+
studio.addWsClient(mockWs);
|
|
362
|
+
studio.removeWsClient(mockWs);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should broadcast events to connected clients', () => {
|
|
366
|
+
const mockWs = {
|
|
367
|
+
send: vi.fn(),
|
|
368
|
+
readyState: 1,
|
|
369
|
+
} as any;
|
|
370
|
+
|
|
371
|
+
studio.addWsClient(mockWs);
|
|
372
|
+
|
|
373
|
+
studio.broadcast({
|
|
374
|
+
type: 'request',
|
|
375
|
+
payload: { id: '123' },
|
|
376
|
+
timestamp: Date.now(),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(mockWs.send).toHaveBeenCalled();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should remove clients that fail to receive', () => {
|
|
383
|
+
const mockWs = {
|
|
384
|
+
send: vi.fn().mockImplementation(() => {
|
|
385
|
+
throw new Error('Connection closed');
|
|
386
|
+
}),
|
|
387
|
+
readyState: 1,
|
|
388
|
+
} as any;
|
|
389
|
+
|
|
390
|
+
studio.addWsClient(mockWs);
|
|
391
|
+
|
|
392
|
+
// Broadcast should not throw
|
|
393
|
+
studio.broadcast({
|
|
394
|
+
type: 'log',
|
|
395
|
+
payload: { message: 'test' },
|
|
396
|
+
timestamp: Date.now(),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Client should be removed after failed send
|
|
400
|
+
// (internal state, hard to verify directly)
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe('destroy', () => {
|
|
405
|
+
it('should clean up resources', () => {
|
|
406
|
+
const s = new Studio({
|
|
407
|
+
monitoring: { storage },
|
|
408
|
+
data: {
|
|
409
|
+
db,
|
|
410
|
+
cursor: { field: 'id', direction: Direction.Desc },
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Add a WebSocket client
|
|
415
|
+
const mockWs = { send: vi.fn() } as any;
|
|
416
|
+
s.addWsClient(mockWs);
|
|
417
|
+
|
|
418
|
+
// Should not throw
|
|
419
|
+
s.destroy();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe('data browser integration', () => {
|
|
424
|
+
it('should access the underlying database through data browser', async () => {
|
|
425
|
+
// Insert test data
|
|
426
|
+
await db
|
|
427
|
+
.insertInto('studioTestUsers')
|
|
428
|
+
.values({ name: 'Test User', email: 'test@example.com' })
|
|
429
|
+
.execute();
|
|
430
|
+
|
|
431
|
+
// Query through data browser
|
|
432
|
+
const result = await studio.data.query({
|
|
433
|
+
table: 'studio_test_users',
|
|
434
|
+
pageSize: 10,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(result.rows.length).toBeGreaterThanOrEqual(1);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should get schema through data browser', async () => {
|
|
441
|
+
const schema = await studio.data.getSchema();
|
|
442
|
+
|
|
443
|
+
const tableNames = schema.tables.map((t) => t.name);
|
|
444
|
+
expect(tableNames).toContain('studio_test_users');
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
});
|