@geekmidas/studio 9.0.2 → 10.0.0-alpha.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.
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,427 +0,0 @@
1
- import type { Context } from 'hono';
2
- import { Hono } from 'hono';
3
- import type { DataBrowser } from '../data/DataBrowser';
4
- import type { Studio } from '../Studio';
5
- import {
6
- Direction,
7
- type FilterCondition,
8
- FilterOperator,
9
- type SortConfig,
10
- } from '../types';
11
- import { getAsset, getIndexHtml } from '../ui-assets';
12
-
13
- /**
14
- * Interface for the Studio instance used by the Hono adapter.
15
- */
16
- export interface StudioLike {
17
- data: DataBrowser<unknown>;
18
- // Monitoring methods
19
- getRequests: Studio<unknown>['getRequests'];
20
- getRequest: Studio<unknown>['getRequest'];
21
- getExceptions: Studio<unknown>['getExceptions'];
22
- getException: Studio<unknown>['getException'];
23
- getLogs: Studio<unknown>['getLogs'];
24
- getStats: Studio<unknown>['getStats'];
25
- // Metrics methods
26
- getMetrics: Studio<unknown>['getMetrics'];
27
- getEndpointMetrics: Studio<unknown>['getEndpointMetrics'];
28
- getEndpointDetails: Studio<unknown>['getEndpointDetails'];
29
- getStatusDistribution: Studio<unknown>['getStatusDistribution'];
30
- resetMetrics: Studio<unknown>['resetMetrics'];
31
- }
32
-
33
- /**
34
- * Parse filter conditions from query parameters.
35
- * Format: filter[column][operator]=value
36
- * Example: filter[name][eq]=John&filter[age][gt]=18
37
- */
38
- function parseFilters(c: Context): FilterCondition[] {
39
- const filters: FilterCondition[] = [];
40
- const url = new URL(c.req.url);
41
-
42
- url.searchParams.forEach((value, key) => {
43
- const match = key.match(/^filter\[(\w+)\]\[(\w+)\]$/);
44
- if (match) {
45
- const column = match[1];
46
- const operator = match[2];
47
- if (!column || !operator) return;
48
-
49
- const op = operator as FilterOperator;
50
-
51
- // Validate operator
52
- if (!Object.values(FilterOperator).includes(op)) {
53
- return;
54
- }
55
-
56
- // Handle special cases
57
- if (op === FilterOperator.In || op === FilterOperator.Nin) {
58
- filters.push({ column, operator: op, value: value.split(',') });
59
- } else if (
60
- op === FilterOperator.IsNull ||
61
- op === FilterOperator.IsNotNull
62
- ) {
63
- filters.push({ column, operator: op });
64
- } else {
65
- // Try to parse as number or boolean
66
- let parsedValue: unknown = value;
67
- if (value === 'true') parsedValue = true;
68
- else if (value === 'false') parsedValue = false;
69
- else if (!Number.isNaN(Number(value)) && value !== '')
70
- parsedValue = Number(value);
71
-
72
- filters.push({ column, operator: op, value: parsedValue });
73
- }
74
- }
75
- });
76
-
77
- return filters;
78
- }
79
-
80
- /**
81
- * Parse sort configuration from query parameters.
82
- * Format: sort=column:direction,column:direction
83
- * Example: sort=name:asc,created_at:desc
84
- */
85
- function parseSort(c: Context): SortConfig[] {
86
- const sortParam = c.req.query('sort');
87
- if (!sortParam) return [];
88
-
89
- return sortParam
90
- .split(',')
91
- .map((part) => {
92
- const parts = part.split(':');
93
- const column = parts[0];
94
- const dir = parts[1];
95
- if (!column) return null;
96
- return {
97
- column,
98
- direction: dir === 'desc' ? Direction.Desc : Direction.Asc,
99
- };
100
- })
101
- .filter((s): s is SortConfig => s !== null);
102
- }
103
-
104
- /**
105
- * Parse query options for monitoring endpoints.
106
- */
107
- function parseQueryOptions(c: Context) {
108
- const limit = parseInt(c.req.query('limit') || '50', 10);
109
- const offset = parseInt(c.req.query('offset') || '0', 10);
110
- const search = c.req.query('search');
111
- const before = c.req.query('before');
112
- const after = c.req.query('after');
113
- const tags = c.req.query('tags')?.split(',').filter(Boolean);
114
- const method = c.req.query('method');
115
- const status = c.req.query('status');
116
- const level = c.req.query('level') as
117
- | 'debug'
118
- | 'info'
119
- | 'warn'
120
- | 'error'
121
- | undefined;
122
-
123
- return {
124
- limit: Math.min(limit, 100),
125
- offset,
126
- search,
127
- before: before ? new Date(before) : undefined,
128
- after: after ? new Date(after) : undefined,
129
- tags,
130
- method: method || undefined,
131
- status: status || undefined,
132
- level: level || undefined,
133
- };
134
- }
135
-
136
- /**
137
- * Parse metrics query options from query parameters.
138
- */
139
- function parseMetricsQueryOptions(c: Context) {
140
- const start = c.req.query('start');
141
- const end = c.req.query('end');
142
- const bucketSize = c.req.query('bucketSize');
143
- const limit = c.req.query('limit');
144
-
145
- return {
146
- range:
147
- start && end ? { start: new Date(start), end: new Date(end) } : undefined,
148
- bucketSize: bucketSize ? parseInt(bucketSize, 10) : undefined,
149
- limit: limit ? parseInt(limit, 10) : undefined,
150
- };
151
- }
152
-
153
- /**
154
- * Create Hono app with Studio API routes and dashboard UI.
155
- */
156
- export function createStudioApp(studio: StudioLike): Hono {
157
- const app = new Hono();
158
-
159
- // ============================================
160
- // Database API
161
- // ============================================
162
-
163
- /**
164
- * GET /api/schema
165
- * Get the complete database schema
166
- */
167
- app.get('/api/schema', async (c) => {
168
- const forceRefresh = c.req.query('refresh') === 'true';
169
- const schema = await studio.data.getSchema(forceRefresh);
170
- return c.json(schema);
171
- });
172
-
173
- /**
174
- * GET /api/tables
175
- * List all tables with basic info
176
- */
177
- app.get('/api/tables', async (c) => {
178
- const schema = await studio.data.getSchema();
179
- const tables = schema.tables.map((t) => ({
180
- name: t.name,
181
- schema: t.schema,
182
- columnCount: t.columns.length,
183
- primaryKey: t.primaryKey,
184
- estimatedRowCount: t.estimatedRowCount,
185
- }));
186
- return c.json({ tables });
187
- });
188
-
189
- /**
190
- * GET /api/tables/:name
191
- * Get detailed information about a specific table
192
- */
193
- app.get('/api/tables/:name', async (c) => {
194
- const tableName = c.req.param('name');
195
- const tableInfo = await studio.data.getTableInfo(tableName);
196
-
197
- if (!tableInfo) {
198
- return c.json({ error: `Table '${tableName}' not found` }, 404);
199
- }
200
-
201
- return c.json(tableInfo);
202
- });
203
-
204
- /**
205
- * GET /api/tables/:name/rows
206
- * Query table data with pagination, filtering, and sorting
207
- */
208
- app.get('/api/tables/:name/rows', async (c) => {
209
- const tableName = c.req.param('name');
210
- const pageSize = Math.min(
211
- parseInt(c.req.query('pageSize') || '50', 10),
212
- 100,
213
- );
214
- const cursor = c.req.query('cursor') || undefined;
215
- const filters = parseFilters(c);
216
- const sort = parseSort(c);
217
-
218
- try {
219
- const result = await studio.data.query({
220
- table: tableName,
221
- pageSize,
222
- cursor,
223
- filters: filters.length > 0 ? filters : undefined,
224
- sort: sort.length > 0 ? sort : undefined,
225
- });
226
-
227
- return c.json(result);
228
- } catch (error) {
229
- if (error instanceof Error && error.message.includes('not found')) {
230
- return c.json({ error: error.message }, 404);
231
- }
232
- throw error;
233
- }
234
- });
235
-
236
- // ============================================
237
- // Monitoring API
238
- // ============================================
239
-
240
- /**
241
- * GET /api/stats
242
- * Get storage statistics
243
- */
244
- app.get('/api/stats', async (c) => {
245
- const stats = await studio.getStats();
246
- return c.json(stats);
247
- });
248
-
249
- /**
250
- * GET /api/requests
251
- * Get request entries
252
- */
253
- app.get('/api/requests', async (c) => {
254
- const options = parseQueryOptions(c);
255
- const requests = await studio.getRequests(options);
256
- return c.json(requests);
257
- });
258
-
259
- /**
260
- * GET /api/requests/:id
261
- * Get a single request by ID
262
- */
263
- app.get('/api/requests/:id', async (c) => {
264
- const request = await studio.getRequest(c.req.param('id'));
265
- if (!request) {
266
- return c.json({ error: 'Request not found' }, 404);
267
- }
268
- return c.json(request);
269
- });
270
-
271
- /**
272
- * GET /api/exceptions
273
- * Get exception entries
274
- */
275
- app.get('/api/exceptions', async (c) => {
276
- const options = parseQueryOptions(c);
277
- const exceptions = await studio.getExceptions(options);
278
- return c.json(exceptions);
279
- });
280
-
281
- /**
282
- * GET /api/exceptions/:id
283
- * Get a single exception by ID
284
- */
285
- app.get('/api/exceptions/:id', async (c) => {
286
- const exception = await studio.getException(c.req.param('id'));
287
- if (!exception) {
288
- return c.json({ error: 'Exception not found' }, 404);
289
- }
290
- return c.json(exception);
291
- });
292
-
293
- /**
294
- * GET /api/logs
295
- * Get log entries
296
- */
297
- app.get('/api/logs', async (c) => {
298
- const options = parseQueryOptions(c);
299
- const logs = await studio.getLogs(options);
300
- return c.json(logs);
301
- });
302
-
303
- // ============================================
304
- // Metrics API
305
- // ============================================
306
-
307
- /**
308
- * GET /api/metrics
309
- * Get aggregated request metrics
310
- */
311
- app.get('/api/metrics', (c) => {
312
- const options = parseMetricsQueryOptions(c);
313
- const metrics = studio.getMetrics(options);
314
- return c.json(metrics);
315
- });
316
-
317
- /**
318
- * GET /api/metrics/endpoints
319
- * Get metrics grouped by endpoint
320
- */
321
- app.get('/api/metrics/endpoints', (c) => {
322
- const options = parseMetricsQueryOptions(c);
323
- const endpoints = studio.getEndpointMetrics(options);
324
- return c.json(endpoints);
325
- });
326
-
327
- /**
328
- * GET /api/metrics/endpoint
329
- * Get detailed metrics for a specific endpoint
330
- */
331
- app.get('/api/metrics/endpoint', (c) => {
332
- const method = c.req.query('method');
333
- const path = c.req.query('path');
334
-
335
- if (!method || !path) {
336
- return c.json({ error: 'method and path are required' }, 400);
337
- }
338
-
339
- const options = parseMetricsQueryOptions(c);
340
- const details = studio.getEndpointDetails(method, path, options);
341
-
342
- if (!details) {
343
- return c.json({ error: 'Endpoint not found' }, 404);
344
- }
345
-
346
- return c.json(details);
347
- });
348
-
349
- /**
350
- * GET /api/metrics/status
351
- * Get HTTP status code distribution
352
- */
353
- app.get('/api/metrics/status', (c) => {
354
- const options = parseMetricsQueryOptions(c);
355
- const distribution = studio.getStatusDistribution(options);
356
- return c.json(distribution);
357
- });
358
-
359
- /**
360
- * DELETE /api/metrics
361
- * Reset all metrics
362
- */
363
- app.delete('/api/metrics', (c) => {
364
- studio.resetMetrics();
365
- return c.json({ success: true });
366
- });
367
-
368
- // ============================================
369
- // Static Assets & Dashboard UI
370
- // ============================================
371
-
372
- // Static assets
373
- app.get('/assets/:filename', (c) => {
374
- const filename = c.req.param('filename');
375
- const assetPath = `assets/${filename}`;
376
- const asset = getAsset(assetPath);
377
- if (asset) {
378
- return c.body(asset.content, 200, {
379
- 'Content-Type': asset.contentType,
380
- 'Cache-Control': 'public, max-age=31536000, immutable',
381
- });
382
- }
383
- return c.notFound();
384
- });
385
-
386
- // Dashboard UI - serve React app
387
- app.get('/', (c) => {
388
- const html = getIndexHtml();
389
- if (!html) {
390
- return c.json({
391
- message: 'Studio API is running',
392
- note: 'UI not available. Run "pnpm build:ui" first.',
393
- endpoints: {
394
- schema: '/api/schema',
395
- tables: '/api/tables',
396
- tableInfo: '/api/tables/:name',
397
- tableRows: '/api/tables/:name/rows',
398
- stats: '/api/stats',
399
- requests: '/api/requests',
400
- exceptions: '/api/exceptions',
401
- logs: '/api/logs',
402
- metrics: '/api/metrics',
403
- },
404
- });
405
- }
406
- return c.html(html);
407
- });
408
-
409
- // SPA fallback - serve index.html for client-side routing
410
- app.get('/*', (c) => {
411
- // Return 404 JSON for API routes
412
- if (c.req.path.startsWith('/api/')) {
413
- return c.json({ error: 'Not found' }, 404);
414
- }
415
-
416
- const html = getIndexHtml();
417
- if (!html) {
418
- return c.notFound();
419
- }
420
- return c.html(html);
421
- });
422
-
423
- return app;
424
- }
425
-
426
- // Re-export types
427
- export type { DataBrowser };
package/src/types.ts DELETED
@@ -1,278 +0,0 @@
1
- import type { TelescopeStorage } from '@geekmidas/telescope';
2
- import type { Kysely } from 'kysely';
3
-
4
- // ============================================
5
- // Enums
6
- // ============================================
7
-
8
- /**
9
- * Sort direction for cursor-based pagination and sorting.
10
- */
11
- export enum Direction {
12
- Asc = 'asc',
13
- Desc = 'desc',
14
- }
15
-
16
- /**
17
- * Filter operators for querying data.
18
- */
19
- export enum FilterOperator {
20
- Eq = 'eq',
21
- Neq = 'neq',
22
- Gt = 'gt',
23
- Gte = 'gte',
24
- Lt = 'lt',
25
- Lte = 'lte',
26
- Like = 'like',
27
- Ilike = 'ilike',
28
- In = 'in',
29
- Nin = 'nin',
30
- IsNull = 'is_null',
31
- IsNotNull = 'is_not_null',
32
- }
33
-
34
- // ============================================
35
- // Cursor Configuration
36
- // ============================================
37
-
38
- /**
39
- * Configuration for cursor-based pagination.
40
- */
41
- export interface CursorConfig {
42
- /** The field to use for cursor-based pagination (e.g., 'id', 'created_at') */
43
- field: string;
44
- /** Sort direction for the cursor field */
45
- direction: Direction;
46
- }
47
-
48
- /**
49
- * Per-table cursor configuration overrides.
50
- */
51
- export interface TableCursorConfig {
52
- [tableName: string]: CursorConfig;
53
- }
54
-
55
- // ============================================
56
- // Monitoring Configuration
57
- // ============================================
58
-
59
- /**
60
- * Configuration for the monitoring feature (Telescope).
61
- */
62
- export interface MonitoringOptions {
63
- /** Storage backend for monitoring data */
64
- storage: TelescopeStorage;
65
- /** Patterns to ignore when recording requests (supports wildcards) */
66
- ignorePatterns?: string[];
67
- /** Whether to record request/response bodies (default: true) */
68
- recordBody?: boolean;
69
- /** Maximum body size to record in bytes (default: 64KB) */
70
- maxBodySize?: number;
71
- /** Hours after which to prune old entries */
72
- pruneAfterHours?: number;
73
- }
74
-
75
- // ============================================
76
- // Data Browser Configuration
77
- // ============================================
78
-
79
- /**
80
- * Configuration for the data browser feature.
81
- */
82
- export interface DataBrowserOptions<DB = unknown> {
83
- /** Kysely database instance */
84
- db: Kysely<DB>;
85
- /** Default cursor configuration for all tables */
86
- cursor: CursorConfig;
87
- /** Per-table cursor overrides */
88
- tableCursors?: TableCursorConfig;
89
- /** Tables to exclude from browsing */
90
- excludeTables?: string[];
91
- /** Maximum rows per page (default: 50, max: 100) */
92
- defaultPageSize?: number;
93
- /** Whether to allow viewing of binary/blob columns (default: false) */
94
- showBinaryColumns?: boolean;
95
- }
96
-
97
- // ============================================
98
- // Studio Configuration
99
- // ============================================
100
-
101
- /**
102
- * Configuration for the Studio dashboard.
103
- */
104
- export interface StudioOptions<DB = unknown> {
105
- /** Monitoring configuration */
106
- monitoring: MonitoringOptions;
107
- /** Data browser configuration */
108
- data: DataBrowserOptions<DB>;
109
- /** Dashboard path (default: '/__studio') */
110
- path?: string;
111
- /** Whether Studio is enabled (default: true) */
112
- enabled?: boolean;
113
- }
114
-
115
- /**
116
- * Normalized Studio options with all defaults applied.
117
- */
118
- export interface NormalizedStudioOptions<DB = unknown> {
119
- monitoring: Required<MonitoringOptions>;
120
- data: Required<DataBrowserOptions<DB>>;
121
- path: string;
122
- enabled: boolean;
123
- }
124
-
125
- // ============================================
126
- // Table Introspection Types
127
- // ============================================
128
-
129
- /**
130
- * Generic column type classification.
131
- */
132
- export type ColumnType =
133
- | 'string'
134
- | 'number'
135
- | 'boolean'
136
- | 'date'
137
- | 'datetime'
138
- | 'json'
139
- | 'binary'
140
- | 'uuid'
141
- | 'unknown';
142
-
143
- /**
144
- * Information about a database column.
145
- */
146
- export interface ColumnInfo {
147
- /** Column name */
148
- name: string;
149
- /** Generic column type */
150
- type: ColumnType;
151
- /** Raw database type (e.g., 'varchar', 'int4') */
152
- rawType: string;
153
- /** Whether the column allows NULL values */
154
- nullable: boolean;
155
- /** Whether this column is part of the primary key */
156
- isPrimaryKey: boolean;
157
- /** Whether this column is a foreign key */
158
- isForeignKey: boolean;
159
- /** Referenced table if foreign key */
160
- foreignKeyTable?: string;
161
- /** Referenced column if foreign key */
162
- foreignKeyColumn?: string;
163
- /** Default value expression */
164
- defaultValue?: string;
165
- }
166
-
167
- /**
168
- * Information about a database table.
169
- */
170
- export interface TableInfo {
171
- /** Table name */
172
- name: string;
173
- /** Schema name (e.g., 'public') */
174
- schema: string;
175
- /** List of columns */
176
- columns: ColumnInfo[];
177
- /** Primary key column names */
178
- primaryKey: string[];
179
- /** Estimated row count (if available) */
180
- estimatedRowCount?: number;
181
- }
182
-
183
- /**
184
- * Complete schema information.
185
- */
186
- export interface SchemaInfo {
187
- /** List of tables */
188
- tables: TableInfo[];
189
- /** When the schema was last introspected */
190
- updatedAt: Date;
191
- }
192
-
193
- // ============================================
194
- // Query Types
195
- // ============================================
196
-
197
- /**
198
- * A single filter condition.
199
- */
200
- export interface FilterCondition {
201
- /** Column to filter on */
202
- column: string;
203
- /** Filter operator */
204
- operator: FilterOperator;
205
- /** Value to compare against (optional for IsNull/IsNotNull operators) */
206
- value?: unknown;
207
- }
208
-
209
- /**
210
- * Sort configuration for a column.
211
- */
212
- export interface SortConfig {
213
- /** Column to sort by */
214
- column: string;
215
- /** Sort direction */
216
- direction: Direction;
217
- }
218
-
219
- /**
220
- * Options for querying table data.
221
- */
222
- export interface QueryOptions {
223
- /** Table to query */
224
- table: string;
225
- /** Filter conditions */
226
- filters?: FilterCondition[];
227
- /** Sort configuration */
228
- sort?: SortConfig[];
229
- /** Cursor for pagination */
230
- cursor?: string | null;
231
- /** Number of rows per page */
232
- pageSize?: number;
233
- /** Pagination direction */
234
- direction?: 'next' | 'prev';
235
- }
236
-
237
- /**
238
- * Result of a paginated query.
239
- */
240
- export interface QueryResult<T = Record<string, unknown>> {
241
- /** Retrieved rows */
242
- rows: T[];
243
- /** Whether there are more rows */
244
- hasMore: boolean;
245
- /** Cursor for next page */
246
- nextCursor: string | null;
247
- /** Cursor for previous page */
248
- prevCursor: string | null;
249
- /** Estimated total row count */
250
- totalEstimate?: number;
251
- }
252
-
253
- // ============================================
254
- // WebSocket Events
255
- // ============================================
256
-
257
- /**
258
- * Types of events broadcast via WebSocket.
259
- */
260
- export type StudioEventType =
261
- | 'request'
262
- | 'exception'
263
- | 'log'
264
- | 'stats'
265
- | 'connected'
266
- | 'schema_updated';
267
-
268
- /**
269
- * A WebSocket event payload.
270
- */
271
- export interface StudioEvent<T = unknown> {
272
- /** Event type */
273
- type: StudioEventType;
274
- /** Event payload */
275
- payload: T;
276
- /** Event timestamp */
277
- timestamp: number;
278
- }