@geekmidas/studio 9.0.2 → 10.0.0-alpha.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.
Files changed (50) hide show
  1. package/package.json +8 -5
  2. package/CHANGELOG.md +0 -82
  3. package/src/Studio.ts +0 -367
  4. package/src/__tests__/Studio.spec.ts +0 -447
  5. package/src/data/DataBrowser.ts +0 -170
  6. package/src/data/__tests__/DataBrowser.integration.spec.ts +0 -418
  7. package/src/data/__tests__/filtering.integration.spec.ts +0 -741
  8. package/src/data/__tests__/introspection.integration.spec.ts +0 -352
  9. package/src/data/__tests__/pagination.spec.ts +0 -123
  10. package/src/data/filtering.ts +0 -191
  11. package/src/data/index.ts +0 -1
  12. package/src/data/introspection.ts +0 -220
  13. package/src/data/pagination.ts +0 -33
  14. package/src/index.ts +0 -29
  15. package/src/server/__tests__/hono.integration.spec.ts +0 -619
  16. package/src/server/hono.ts +0 -427
  17. package/src/types.ts +0 -278
  18. package/src/ui-assets.ts +0 -37
  19. package/tsconfig.json +0 -9
  20. package/tsdown.config.ts +0 -13
  21. package/ui/CHANGELOG.md +0 -26
  22. package/ui/index.html +0 -12
  23. package/ui/node_modules/.bin/tsc +0 -21
  24. package/ui/node_modules/.bin/tsserver +0 -21
  25. package/ui/node_modules/.bin/vite +0 -21
  26. package/ui/package.json +0 -30
  27. package/ui/src/App.tsx +0 -100
  28. package/ui/src/api.ts +0 -213
  29. package/ui/src/components/FilterPanel.tsx +0 -213
  30. package/ui/src/components/NavRail.tsx +0 -183
  31. package/ui/src/components/RowDetail.tsx +0 -119
  32. package/ui/src/components/StudioHeader.tsx +0 -109
  33. package/ui/src/components/TableList.tsx +0 -58
  34. package/ui/src/components/TableView.tsx +0 -564
  35. package/ui/src/main.tsx +0 -10
  36. package/ui/src/pages/DashboardPage.tsx +0 -500
  37. package/ui/src/pages/DatabasePage.tsx +0 -226
  38. package/ui/src/pages/EndpointDetailsPage.tsx +0 -288
  39. package/ui/src/pages/ExceptionsPage.tsx +0 -268
  40. package/ui/src/pages/LogsPage.tsx +0 -228
  41. package/ui/src/pages/MonitoringPage.tsx +0 -46
  42. package/ui/src/pages/PerformancePage.tsx +0 -307
  43. package/ui/src/pages/RequestsPage.tsx +0 -379
  44. package/ui/src/providers/StudioProvider.tsx +0 -194
  45. package/ui/src/styles.css +0 -105
  46. package/ui/src/types.ts +0 -174
  47. package/ui/src/vite-env.d.ts +0 -1
  48. package/ui/tsconfig.json +0 -21
  49. package/ui/tsconfig.tsbuildinfo +0 -1
  50. package/ui/vite.config.ts +0 -12
@@ -1,619 +0,0 @@
1
- import { InMemoryStorage } from '@geekmidas/telescope';
2
- import {
3
- CamelCasePlugin,
4
- type Generated,
5
- Kysely,
6
- PostgresDialect,
7
- sql,
8
- } from 'kysely';
9
- import pg from 'pg';
10
- import {
11
- afterAll,
12
- afterEach,
13
- beforeAll,
14
- beforeEach,
15
- describe,
16
- expect,
17
- it,
18
- } from 'vitest';
19
- import { TEST_DATABASE_CONFIG } from '../../../../testkit/test/globalSetup';
20
- import { Studio } from '../../Studio';
21
- import { Direction } from '../../types';
22
- import { createStudioApp } from '../hono';
23
-
24
- interface TestDatabase {
25
- studioHonoProducts: {
26
- id: Generated<number>;
27
- name: string;
28
- price: number;
29
- category: string;
30
- inStock: boolean;
31
- createdAt: Generated<Date>;
32
- };
33
- }
34
-
35
- describe('Hono Server Adapter Integration Tests', () => {
36
- let db: Kysely<TestDatabase>;
37
- let studio: Studio<TestDatabase>;
38
- let storage: InMemoryStorage;
39
- let app: ReturnType<typeof createStudioApp>;
40
-
41
- beforeAll(async () => {
42
- db = new Kysely<TestDatabase>({
43
- dialect: new PostgresDialect({
44
- pool: new pg.Pool({
45
- ...TEST_DATABASE_CONFIG,
46
- database: 'postgres',
47
- }),
48
- }),
49
- plugins: [new CamelCasePlugin()],
50
- });
51
-
52
- // Create test table
53
- await db.schema
54
- .createTable('studio_hono_products')
55
- .ifNotExists()
56
- .addColumn('id', 'serial', (col) => col.primaryKey())
57
- .addColumn('name', 'varchar(255)', (col) => col.notNull())
58
- .addColumn('price', 'numeric(10, 2)', (col) => col.notNull())
59
- .addColumn('category', 'varchar(100)', (col) => col.notNull())
60
- .addColumn('in_stock', 'boolean', (col) => col.notNull().defaultTo(true))
61
- .addColumn('created_at', 'timestamptz', (col) =>
62
- col.defaultTo(sql`now()`).notNull(),
63
- )
64
- .execute();
65
- });
66
-
67
- beforeEach(async () => {
68
- storage = new InMemoryStorage();
69
- studio = new Studio({
70
- monitoring: { storage },
71
- data: {
72
- db,
73
- cursor: { field: 'id', direction: Direction.Asc },
74
- },
75
- });
76
- app = createStudioApp(studio);
77
-
78
- // Insert test data for database browsing
79
- await db
80
- .insertInto('studioHonoProducts')
81
- .values([
82
- {
83
- name: 'Laptop',
84
- price: 999.99,
85
- category: 'electronics',
86
- inStock: true,
87
- },
88
- {
89
- name: 'Mouse',
90
- price: 29.99,
91
- category: 'electronics',
92
- inStock: true,
93
- },
94
- {
95
- name: 'Keyboard',
96
- price: 79.99,
97
- category: 'electronics',
98
- inStock: false,
99
- },
100
- { name: 'Desk', price: 299.99, category: 'furniture', inStock: true },
101
- { name: 'Chair', price: 199.99, category: 'furniture', inStock: true },
102
- ])
103
- .execute();
104
-
105
- // Record some monitoring data
106
- await studio.recordRequest({
107
- method: 'GET',
108
- path: '/api/users',
109
- status: 200,
110
- duration: 45,
111
- });
112
- await studio.recordRequest({
113
- method: 'POST',
114
- path: '/api/users',
115
- status: 201,
116
- duration: 80,
117
- });
118
- await studio.recordRequest({
119
- method: 'GET',
120
- path: '/api/products',
121
- status: 500,
122
- duration: 120,
123
- });
124
-
125
- await studio.info('Application started');
126
- await studio.warn('High memory usage detected');
127
- await studio.error('Database connection timeout');
128
-
129
- await studio.exception(new Error('Test exception'));
130
- });
131
-
132
- afterEach(async () => {
133
- studio.destroy();
134
- await db.deleteFrom('studioHonoProducts').execute();
135
- });
136
-
137
- afterAll(async () => {
138
- await db.schema.dropTable('studio_hono_products').ifExists().execute();
139
- await db.destroy();
140
- });
141
-
142
- describe('GET /api/schema', () => {
143
- it('should return database schema', async () => {
144
- const res = await app.request('/api/schema');
145
-
146
- expect(res.status).toBe(200);
147
- const data = await res.json();
148
- expect(data.tables).toBeDefined();
149
- expect(Array.isArray(data.tables)).toBe(true);
150
- expect(data.updatedAt).toBeDefined();
151
- });
152
-
153
- it('should force refresh when requested', async () => {
154
- const res1 = await app.request('/api/schema');
155
- const data1 = await res1.json();
156
-
157
- const res2 = await app.request('/api/schema?refresh=true');
158
- const data2 = await res2.json();
159
-
160
- // Both should have tables
161
- expect(data1.tables).toBeDefined();
162
- expect(data2.tables).toBeDefined();
163
- });
164
- });
165
-
166
- describe('GET /api/tables', () => {
167
- it('should return list of tables', async () => {
168
- const res = await app.request('/api/tables');
169
-
170
- expect(res.status).toBe(200);
171
- const data = await res.json();
172
- expect(data.tables).toBeDefined();
173
-
174
- const productTable = data.tables.find(
175
- (t: any) => t.name === 'studio_hono_products',
176
- );
177
- expect(productTable).toBeDefined();
178
- expect(productTable.columnCount).toBeGreaterThan(0);
179
- expect(productTable.primaryKey).toEqual(['id']);
180
- });
181
- });
182
-
183
- describe('GET /api/tables/:name', () => {
184
- it('should return table info for existing table', async () => {
185
- const res = await app.request('/api/tables/studio_hono_products');
186
-
187
- expect(res.status).toBe(200);
188
- const data = await res.json();
189
- expect(data.name).toBe('studio_hono_products');
190
- expect(data.columns).toBeDefined();
191
- expect(data.columns.length).toBeGreaterThan(0);
192
- });
193
-
194
- it('should return 404 for non-existent table', async () => {
195
- const res = await app.request('/api/tables/non_existent_table');
196
-
197
- expect(res.status).toBe(404);
198
- const data = await res.json();
199
- expect(data.error).toContain('not found');
200
- });
201
- });
202
-
203
- describe('GET /api/tables/:name/rows', () => {
204
- it('should return paginated rows', async () => {
205
- const res = await app.request('/api/tables/studio_hono_products/rows');
206
-
207
- expect(res.status).toBe(200);
208
- const data = await res.json();
209
- expect(data.rows).toBeDefined();
210
- expect(data.rows.length).toBe(5);
211
- expect(data.hasMore).toBe(false);
212
- });
213
-
214
- it('should respect pageSize parameter', async () => {
215
- const res = await app.request(
216
- '/api/tables/studio_hono_products/rows?pageSize=2',
217
- );
218
-
219
- expect(res.status).toBe(200);
220
- const data = await res.json();
221
- expect(data.rows.length).toBe(2);
222
- expect(data.hasMore).toBe(true);
223
- expect(data.nextCursor).toBeDefined();
224
- });
225
-
226
- it('should paginate using cursor', async () => {
227
- const res1 = await app.request(
228
- '/api/tables/studio_hono_products/rows?pageSize=2',
229
- );
230
- const data1 = await res1.json();
231
-
232
- const res2 = await app.request(
233
- `/api/tables/studio_hono_products/rows?pageSize=2&cursor=${data1.nextCursor}`,
234
- );
235
- const data2 = await res2.json();
236
-
237
- expect(data2.rows.length).toBe(2);
238
-
239
- // Rows should be different
240
- const ids1 = data1.rows.map((r: any) => r.id);
241
- const ids2 = data2.rows.map((r: any) => r.id);
242
- expect(ids1.every((id: number) => !ids2.includes(id))).toBe(true);
243
- });
244
-
245
- it('should apply equality filter', async () => {
246
- const res = await app.request(
247
- '/api/tables/studio_hono_products/rows?filter[category][eq]=electronics',
248
- );
249
-
250
- expect(res.status).toBe(200);
251
- const data = await res.json();
252
- expect(data.rows.length).toBe(3);
253
- data.rows.forEach((row: any) => {
254
- expect(row.category).toBe('electronics');
255
- });
256
- });
257
-
258
- it('should apply greater-than filter', async () => {
259
- const res = await app.request(
260
- '/api/tables/studio_hono_products/rows?filter[price][gt]=100',
261
- );
262
-
263
- expect(res.status).toBe(200);
264
- const data = await res.json();
265
- expect(data.rows.length).toBe(3);
266
- data.rows.forEach((row: any) => {
267
- expect(Number(row.price)).toBeGreaterThan(100);
268
- });
269
- });
270
-
271
- it('should apply boolean filter', async () => {
272
- const res = await app.request(
273
- '/api/tables/studio_hono_products/rows?filter[in_stock][eq]=false',
274
- );
275
-
276
- expect(res.status).toBe(200);
277
- const data = await res.json();
278
- expect(data.rows.length).toBe(1);
279
- expect(data.rows[0].name).toBe('Keyboard');
280
- });
281
-
282
- it('should apply IN filter', async () => {
283
- const res = await app.request(
284
- '/api/tables/studio_hono_products/rows?filter[name][in]=Laptop,Mouse',
285
- );
286
-
287
- expect(res.status).toBe(200);
288
- const data = await res.json();
289
- expect(data.rows.length).toBe(2);
290
- });
291
-
292
- it('should apply multiple filters', async () => {
293
- const res = await app.request(
294
- '/api/tables/studio_hono_products/rows?filter[category][eq]=electronics&filter[in_stock][eq]=true',
295
- );
296
-
297
- expect(res.status).toBe(200);
298
- const data = await res.json();
299
- expect(data.rows.length).toBe(2);
300
- data.rows.forEach((row: any) => {
301
- expect(row.category).toBe('electronics');
302
- expect(row.inStock).toBe(true);
303
- });
304
- });
305
-
306
- it('should apply sorting', async () => {
307
- const res = await app.request(
308
- '/api/tables/studio_hono_products/rows?sort=price:desc',
309
- );
310
-
311
- expect(res.status).toBe(200);
312
- const data = await res.json();
313
- const prices = data.rows.map((r: any) => Number(r.price));
314
-
315
- for (let i = 1; i < prices.length; i++) {
316
- expect(prices[i]).toBeLessThanOrEqual(prices[i - 1]);
317
- }
318
- });
319
-
320
- it('should apply multiple sort columns', async () => {
321
- const res = await app.request(
322
- '/api/tables/studio_hono_products/rows?sort=category:asc,price:desc',
323
- );
324
-
325
- expect(res.status).toBe(200);
326
- const data = await res.json();
327
- expect(data.rows.length).toBe(5);
328
-
329
- // Electronics should come first (alphabetically)
330
- const electronics = data.rows.filter(
331
- (r: any) => r.category === 'electronics',
332
- );
333
- expect(electronics.length).toBe(3);
334
-
335
- // Within electronics, prices should be descending
336
- const electronicPrices = electronics.map((r: any) => Number(r.price));
337
- for (let i = 1; i < electronicPrices.length; i++) {
338
- expect(electronicPrices[i]).toBeLessThanOrEqual(
339
- electronicPrices[i - 1],
340
- );
341
- }
342
- });
343
-
344
- it('should return 404 for non-existent table', async () => {
345
- const res = await app.request('/api/tables/non_existent_table/rows');
346
-
347
- expect(res.status).toBe(404);
348
- const data = await res.json();
349
- expect(data.error).toContain('not found');
350
- });
351
-
352
- it('should cap pageSize at 100', async () => {
353
- const res = await app.request(
354
- '/api/tables/studio_hono_products/rows?pageSize=500',
355
- );
356
-
357
- expect(res.status).toBe(200);
358
- // Since we only have 5 rows, we just verify the request succeeds
359
- const data = await res.json();
360
- expect(data.rows).toBeDefined();
361
- });
362
- });
363
-
364
- // ============================================
365
- // Monitoring API Tests
366
- // ============================================
367
-
368
- describe('GET /api/stats', () => {
369
- it('should return storage statistics', async () => {
370
- const res = await app.request('/api/stats');
371
-
372
- expect(res.status).toBe(200);
373
- const data = await res.json();
374
- expect(data.requests).toBe(3);
375
- expect(data.exceptions).toBe(1);
376
- expect(data.logs).toBe(3);
377
- });
378
- });
379
-
380
- describe('GET /api/requests', () => {
381
- it('should return request entries', async () => {
382
- const res = await app.request('/api/requests');
383
-
384
- expect(res.status).toBe(200);
385
- const data = await res.json();
386
- expect(Array.isArray(data)).toBe(true);
387
- expect(data.length).toBe(3);
388
- });
389
-
390
- it('should accept query parameters', async () => {
391
- const res = await app.request('/api/requests?limit=2');
392
-
393
- expect(res.status).toBe(200);
394
- const data = await res.json();
395
- expect(Array.isArray(data)).toBe(true);
396
- expect(data.length).toBe(2);
397
- });
398
- });
399
-
400
- describe('GET /api/requests/:id', () => {
401
- it('should return a single request by ID', async () => {
402
- const requests = await studio.getRequests();
403
- const requestId = requests[0].id;
404
-
405
- const res = await app.request(`/api/requests/${requestId}`);
406
-
407
- expect(res.status).toBe(200);
408
- const data = await res.json();
409
- expect(data.id).toBe(requestId);
410
- });
411
-
412
- it('should return 404 for non-existent request', async () => {
413
- const res = await app.request('/api/requests/non-existent');
414
-
415
- expect(res.status).toBe(404);
416
- const data = await res.json();
417
- expect(data.error).toBe('Request not found');
418
- });
419
- });
420
-
421
- describe('GET /api/exceptions', () => {
422
- it('should return exception entries', async () => {
423
- const res = await app.request('/api/exceptions');
424
-
425
- expect(res.status).toBe(200);
426
- const data = await res.json();
427
- expect(Array.isArray(data)).toBe(true);
428
- expect(data.length).toBe(1);
429
- expect(data[0].message).toBe('Test exception');
430
- });
431
- });
432
-
433
- describe('GET /api/exceptions/:id', () => {
434
- it('should return a single exception by ID', async () => {
435
- const exceptions = await studio.getExceptions();
436
- const exceptionId = exceptions[0].id;
437
-
438
- const res = await app.request(`/api/exceptions/${exceptionId}`);
439
-
440
- expect(res.status).toBe(200);
441
- const data = await res.json();
442
- expect(data.id).toBe(exceptionId);
443
- expect(data.message).toBe('Test exception');
444
- });
445
-
446
- it('should return 404 for non-existent exception', async () => {
447
- const res = await app.request('/api/exceptions/non-existent');
448
-
449
- expect(res.status).toBe(404);
450
- const data = await res.json();
451
- expect(data.error).toBe('Exception not found');
452
- });
453
- });
454
-
455
- describe('GET /api/logs', () => {
456
- it('should return log entries', async () => {
457
- const res = await app.request('/api/logs');
458
-
459
- expect(res.status).toBe(200);
460
- const data = await res.json();
461
- expect(Array.isArray(data)).toBe(true);
462
- expect(data.length).toBe(3);
463
- });
464
-
465
- it('should accept level filter', async () => {
466
- const res = await app.request('/api/logs?level=error');
467
-
468
- expect(res.status).toBe(200);
469
- const data = await res.json();
470
- expect(Array.isArray(data)).toBe(true);
471
- expect(data.length).toBe(1);
472
- expect(data[0].message).toBe('Database connection timeout');
473
- });
474
- });
475
-
476
- // ============================================
477
- // Metrics API Tests
478
- // ============================================
479
-
480
- describe('GET /api/metrics', () => {
481
- it('should return aggregated metrics', async () => {
482
- const res = await app.request('/api/metrics');
483
-
484
- expect(res.status).toBe(200);
485
- const data = await res.json();
486
- expect(data.totalRequests).toBe(3);
487
- expect(typeof data.avgDuration).toBe('number');
488
- });
489
-
490
- it('should accept time range parameters', async () => {
491
- const start = new Date(Date.now() - 3600000).toISOString();
492
- const end = new Date().toISOString();
493
- const res = await app.request(`/api/metrics?start=${start}&end=${end}`);
494
-
495
- expect(res.status).toBe(200);
496
- const data = await res.json();
497
- expect(data.totalRequests).toBeDefined();
498
- });
499
- });
500
-
501
- describe('GET /api/metrics/endpoints', () => {
502
- it('should return endpoint metrics', async () => {
503
- const res = await app.request('/api/metrics/endpoints');
504
-
505
- expect(res.status).toBe(200);
506
- const data = await res.json();
507
- expect(Array.isArray(data)).toBe(true);
508
- expect(data.length).toBeGreaterThan(0);
509
- });
510
-
511
- it('should accept limit parameter', async () => {
512
- const res = await app.request('/api/metrics/endpoints?limit=1');
513
-
514
- expect(res.status).toBe(200);
515
- const data = await res.json();
516
- expect(Array.isArray(data)).toBe(true);
517
- });
518
- });
519
-
520
- describe('GET /api/metrics/endpoint', () => {
521
- it('should return details for a specific endpoint', async () => {
522
- const res = await app.request(
523
- '/api/metrics/endpoint?method=GET&path=/api/users',
524
- );
525
-
526
- // May return 200 with data or 404 if endpoint not tracked
527
- expect([200, 404]).toContain(res.status);
528
- if (res.status === 200) {
529
- const data = await res.json();
530
- expect(data.method).toBe('GET');
531
- expect(data.path).toBe('/api/users');
532
- }
533
- });
534
-
535
- it('should return 400 if method is missing', async () => {
536
- const res = await app.request('/api/metrics/endpoint?path=/api/users');
537
-
538
- expect(res.status).toBe(400);
539
- const data = await res.json();
540
- expect(data.error).toBe('method and path are required');
541
- });
542
-
543
- it('should return 400 if path is missing', async () => {
544
- const res = await app.request('/api/metrics/endpoint?method=GET');
545
-
546
- expect(res.status).toBe(400);
547
- const data = await res.json();
548
- expect(data.error).toBe('method and path are required');
549
- });
550
-
551
- it('should return 404 for non-existent endpoint', async () => {
552
- const res = await app.request(
553
- '/api/metrics/endpoint?method=DELETE&path=/api/unknown',
554
- );
555
-
556
- expect(res.status).toBe(404);
557
- const data = await res.json();
558
- expect(data.error).toBe('Endpoint not found');
559
- });
560
- });
561
-
562
- describe('GET /api/metrics/status', () => {
563
- it('should return status distribution', async () => {
564
- const res = await app.request('/api/metrics/status');
565
-
566
- expect(res.status).toBe(200);
567
- const data = await res.json();
568
- expect(data).toBeDefined();
569
- });
570
- });
571
-
572
- describe('DELETE /api/metrics', () => {
573
- it('should reset metrics', async () => {
574
- const res = await app.request('/api/metrics', { method: 'DELETE' });
575
-
576
- expect(res.status).toBe(200);
577
- const data = await res.json();
578
- expect(data.success).toBe(true);
579
-
580
- // Verify metrics are reset
581
- const metricsRes = await app.request('/api/metrics');
582
- const metrics = await metricsRes.json();
583
- expect(metrics.totalRequests).toBe(0);
584
- });
585
- });
586
-
587
- // ============================================
588
- // UI & Static Assets
589
- // ============================================
590
-
591
- describe('GET /', () => {
592
- it('should return dashboard UI or API info', async () => {
593
- const res = await app.request('/');
594
-
595
- expect(res.status).toBe(200);
596
- const contentType = res.headers.get('content-type') || '';
597
-
598
- // If UI is embedded, returns HTML; otherwise returns JSON API info
599
- if (contentType.includes('text/html')) {
600
- const html = await res.text();
601
- expect(html).toContain('<!doctype html>');
602
- } else {
603
- const data = await res.json();
604
- expect(data.message).toBe('Studio API is running');
605
- expect(data.endpoints).toBeDefined();
606
- }
607
- });
608
- });
609
-
610
- describe('API 404 handling', () => {
611
- it('should return JSON 404 for unknown API routes', async () => {
612
- const res = await app.request('/api/unknown-route');
613
-
614
- expect(res.status).toBe(404);
615
- const data = await res.json();
616
- expect(data.error).toBe('Not found');
617
- });
618
- });
619
- });