@geekmidas/studio 0.0.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 (64) hide show
  1. package/dist/DataBrowser-DQ3-ZxdV.mjs +427 -0
  2. package/dist/DataBrowser-DQ3-ZxdV.mjs.map +1 -0
  3. package/dist/DataBrowser-SOcqmZb2.d.mts +267 -0
  4. package/dist/DataBrowser-c-Gs6PZB.cjs +432 -0
  5. package/dist/DataBrowser-c-Gs6PZB.cjs.map +1 -0
  6. package/dist/DataBrowser-hGwiTffZ.d.cts +267 -0
  7. package/dist/chunk-CUT6urMc.cjs +30 -0
  8. package/dist/data/index.cjs +4 -0
  9. package/dist/data/index.d.cts +2 -0
  10. package/dist/data/index.d.mts +2 -0
  11. package/dist/data/index.mjs +4 -0
  12. package/dist/index.cjs +239 -0
  13. package/dist/index.cjs.map +1 -0
  14. package/dist/index.d.cts +132 -0
  15. package/dist/index.d.mts +132 -0
  16. package/dist/index.mjs +230 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/server/hono.cjs +192 -0
  19. package/dist/server/hono.cjs.map +1 -0
  20. package/dist/server/hono.d.cts +19 -0
  21. package/dist/server/hono.d.mts +19 -0
  22. package/dist/server/hono.mjs +191 -0
  23. package/dist/server/hono.mjs.map +1 -0
  24. package/dist/types-BZv87Ikv.mjs +31 -0
  25. package/dist/types-BZv87Ikv.mjs.map +1 -0
  26. package/dist/types-CMttUZYk.cjs +43 -0
  27. package/dist/types-CMttUZYk.cjs.map +1 -0
  28. package/package.json +54 -0
  29. package/src/Studio.ts +318 -0
  30. package/src/data/DataBrowser.ts +166 -0
  31. package/src/data/__tests__/DataBrowser.integration.spec.ts +418 -0
  32. package/src/data/__tests__/filtering.integration.spec.ts +741 -0
  33. package/src/data/__tests__/introspection.integration.spec.ts +352 -0
  34. package/src/data/filtering.ts +191 -0
  35. package/src/data/index.ts +1 -0
  36. package/src/data/introspection.ts +220 -0
  37. package/src/data/pagination.ts +33 -0
  38. package/src/index.ts +31 -0
  39. package/src/server/__tests__/hono.integration.spec.ts +361 -0
  40. package/src/server/hono.ts +225 -0
  41. package/src/types.ts +278 -0
  42. package/src/ui-assets.ts +40 -0
  43. package/tsdown.config.ts +13 -0
  44. package/ui/index.html +12 -0
  45. package/ui/node_modules/.bin/browserslist +21 -0
  46. package/ui/node_modules/.bin/jiti +21 -0
  47. package/ui/node_modules/.bin/terser +21 -0
  48. package/ui/node_modules/.bin/tsc +21 -0
  49. package/ui/node_modules/.bin/tsserver +21 -0
  50. package/ui/node_modules/.bin/tsx +21 -0
  51. package/ui/node_modules/.bin/vite +21 -0
  52. package/ui/package.json +24 -0
  53. package/ui/src/App.tsx +141 -0
  54. package/ui/src/api.ts +71 -0
  55. package/ui/src/components/RowDetail.tsx +113 -0
  56. package/ui/src/components/TableList.tsx +51 -0
  57. package/ui/src/components/TableView.tsx +219 -0
  58. package/ui/src/main.tsx +10 -0
  59. package/ui/src/styles.css +36 -0
  60. package/ui/src/types.ts +50 -0
  61. package/ui/src/vite-env.d.ts +1 -0
  62. package/ui/tsconfig.json +21 -0
  63. package/ui/tsconfig.tsbuildinfo +1 -0
  64. package/ui/vite.config.ts +12 -0
@@ -0,0 +1,31 @@
1
+ //#region src/types.ts
2
+ /**
3
+ * Sort direction for cursor-based pagination and sorting.
4
+ */
5
+ let Direction = /* @__PURE__ */ function(Direction$1) {
6
+ Direction$1["Asc"] = "asc";
7
+ Direction$1["Desc"] = "desc";
8
+ return Direction$1;
9
+ }({});
10
+ /**
11
+ * Filter operators for querying data.
12
+ */
13
+ let FilterOperator = /* @__PURE__ */ function(FilterOperator$1) {
14
+ FilterOperator$1["Eq"] = "eq";
15
+ FilterOperator$1["Neq"] = "neq";
16
+ FilterOperator$1["Gt"] = "gt";
17
+ FilterOperator$1["Gte"] = "gte";
18
+ FilterOperator$1["Lt"] = "lt";
19
+ FilterOperator$1["Lte"] = "lte";
20
+ FilterOperator$1["Like"] = "like";
21
+ FilterOperator$1["Ilike"] = "ilike";
22
+ FilterOperator$1["In"] = "in";
23
+ FilterOperator$1["Nin"] = "nin";
24
+ FilterOperator$1["IsNull"] = "is_null";
25
+ FilterOperator$1["IsNotNull"] = "is_not_null";
26
+ return FilterOperator$1;
27
+ }({});
28
+
29
+ //#endregion
30
+ export { Direction, FilterOperator };
31
+ //# sourceMappingURL=types-BZv87Ikv.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-BZv87Ikv.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type { TelescopeStorage } from '@geekmidas/telescope';\nimport type { Kysely } from 'kysely';\n\n// ============================================\n// Enums\n// ============================================\n\n/**\n * Sort direction for cursor-based pagination and sorting.\n */\nexport enum Direction {\n Asc = 'asc',\n Desc = 'desc',\n}\n\n/**\n * Filter operators for querying data.\n */\nexport enum FilterOperator {\n Eq = 'eq',\n Neq = 'neq',\n Gt = 'gt',\n Gte = 'gte',\n Lt = 'lt',\n Lte = 'lte',\n Like = 'like',\n Ilike = 'ilike',\n In = 'in',\n Nin = 'nin',\n IsNull = 'is_null',\n IsNotNull = 'is_not_null',\n}\n\n// ============================================\n// Cursor Configuration\n// ============================================\n\n/**\n * Configuration for cursor-based pagination.\n */\nexport interface CursorConfig {\n /** The field to use for cursor-based pagination (e.g., 'id', 'created_at') */\n field: string;\n /** Sort direction for the cursor field */\n direction: Direction;\n}\n\n/**\n * Per-table cursor configuration overrides.\n */\nexport interface TableCursorConfig {\n [tableName: string]: CursorConfig;\n}\n\n// ============================================\n// Monitoring Configuration\n// ============================================\n\n/**\n * Configuration for the monitoring feature (Telescope).\n */\nexport interface MonitoringOptions {\n /** Storage backend for monitoring data */\n storage: TelescopeStorage;\n /** Patterns to ignore when recording requests (supports wildcards) */\n ignorePatterns?: string[];\n /** Whether to record request/response bodies (default: true) */\n recordBody?: boolean;\n /** Maximum body size to record in bytes (default: 64KB) */\n maxBodySize?: number;\n /** Hours after which to prune old entries */\n pruneAfterHours?: number;\n}\n\n// ============================================\n// Data Browser Configuration\n// ============================================\n\n/**\n * Configuration for the data browser feature.\n */\nexport interface DataBrowserOptions<DB = unknown> {\n /** Kysely database instance */\n db: Kysely<DB>;\n /** Default cursor configuration for all tables */\n cursor: CursorConfig;\n /** Per-table cursor overrides */\n tableCursors?: TableCursorConfig;\n /** Tables to exclude from browsing */\n excludeTables?: string[];\n /** Maximum rows per page (default: 50, max: 100) */\n defaultPageSize?: number;\n /** Whether to allow viewing of binary/blob columns (default: false) */\n showBinaryColumns?: boolean;\n}\n\n// ============================================\n// Studio Configuration\n// ============================================\n\n/**\n * Configuration for the Studio dashboard.\n */\nexport interface StudioOptions<DB = unknown> {\n /** Monitoring configuration */\n monitoring: MonitoringOptions;\n /** Data browser configuration */\n data: DataBrowserOptions<DB>;\n /** Dashboard path (default: '/__studio') */\n path?: string;\n /** Whether Studio is enabled (default: true) */\n enabled?: boolean;\n}\n\n/**\n * Normalized Studio options with all defaults applied.\n */\nexport interface NormalizedStudioOptions<DB = unknown> {\n monitoring: Required<MonitoringOptions>;\n data: Required<DataBrowserOptions<DB>>;\n path: string;\n enabled: boolean;\n}\n\n// ============================================\n// Table Introspection Types\n// ============================================\n\n/**\n * Generic column type classification.\n */\nexport type ColumnType =\n | 'string'\n | 'number'\n | 'boolean'\n | 'date'\n | 'datetime'\n | 'json'\n | 'binary'\n | 'uuid'\n | 'unknown';\n\n/**\n * Information about a database column.\n */\nexport interface ColumnInfo {\n /** Column name */\n name: string;\n /** Generic column type */\n type: ColumnType;\n /** Raw database type (e.g., 'varchar', 'int4') */\n rawType: string;\n /** Whether the column allows NULL values */\n nullable: boolean;\n /** Whether this column is part of the primary key */\n isPrimaryKey: boolean;\n /** Whether this column is a foreign key */\n isForeignKey: boolean;\n /** Referenced table if foreign key */\n foreignKeyTable?: string;\n /** Referenced column if foreign key */\n foreignKeyColumn?: string;\n /** Default value expression */\n defaultValue?: string;\n}\n\n/**\n * Information about a database table.\n */\nexport interface TableInfo {\n /** Table name */\n name: string;\n /** Schema name (e.g., 'public') */\n schema: string;\n /** List of columns */\n columns: ColumnInfo[];\n /** Primary key column names */\n primaryKey: string[];\n /** Estimated row count (if available) */\n estimatedRowCount?: number;\n}\n\n/**\n * Complete schema information.\n */\nexport interface SchemaInfo {\n /** List of tables */\n tables: TableInfo[];\n /** When the schema was last introspected */\n updatedAt: Date;\n}\n\n// ============================================\n// Query Types\n// ============================================\n\n/**\n * A single filter condition.\n */\nexport interface FilterCondition {\n /** Column to filter on */\n column: string;\n /** Filter operator */\n operator: FilterOperator;\n /** Value to compare against (optional for IsNull/IsNotNull operators) */\n value?: unknown;\n}\n\n/**\n * Sort configuration for a column.\n */\nexport interface SortConfig {\n /** Column to sort by */\n column: string;\n /** Sort direction */\n direction: Direction;\n}\n\n/**\n * Options for querying table data.\n */\nexport interface QueryOptions {\n /** Table to query */\n table: string;\n /** Filter conditions */\n filters?: FilterCondition[];\n /** Sort configuration */\n sort?: SortConfig[];\n /** Cursor for pagination */\n cursor?: string | null;\n /** Number of rows per page */\n pageSize?: number;\n /** Pagination direction */\n direction?: 'next' | 'prev';\n}\n\n/**\n * Result of a paginated query.\n */\nexport interface QueryResult<T = Record<string, unknown>> {\n /** Retrieved rows */\n rows: T[];\n /** Whether there are more rows */\n hasMore: boolean;\n /** Cursor for next page */\n nextCursor: string | null;\n /** Cursor for previous page */\n prevCursor: string | null;\n /** Estimated total row count */\n totalEstimate?: number;\n}\n\n// ============================================\n// WebSocket Events\n// ============================================\n\n/**\n * Types of events broadcast via WebSocket.\n */\nexport type StudioEventType =\n | 'request'\n | 'exception'\n | 'log'\n | 'stats'\n | 'connected'\n | 'schema_updated';\n\n/**\n * A WebSocket event payload.\n */\nexport interface StudioEvent<T = unknown> {\n /** Event type */\n type: StudioEventType;\n /** Event payload */\n payload: T;\n /** Event timestamp */\n timestamp: number;\n}\n"],"mappings":";;;;AAUA,IAAY,kDAAL;AACL;AACA;;AACD;;;;AAKD,IAAY,4DAAL;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACD"}
@@ -0,0 +1,43 @@
1
+
2
+ //#region src/types.ts
3
+ /**
4
+ * Sort direction for cursor-based pagination and sorting.
5
+ */
6
+ let Direction = /* @__PURE__ */ function(Direction$1) {
7
+ Direction$1["Asc"] = "asc";
8
+ Direction$1["Desc"] = "desc";
9
+ return Direction$1;
10
+ }({});
11
+ /**
12
+ * Filter operators for querying data.
13
+ */
14
+ let FilterOperator = /* @__PURE__ */ function(FilterOperator$1) {
15
+ FilterOperator$1["Eq"] = "eq";
16
+ FilterOperator$1["Neq"] = "neq";
17
+ FilterOperator$1["Gt"] = "gt";
18
+ FilterOperator$1["Gte"] = "gte";
19
+ FilterOperator$1["Lt"] = "lt";
20
+ FilterOperator$1["Lte"] = "lte";
21
+ FilterOperator$1["Like"] = "like";
22
+ FilterOperator$1["Ilike"] = "ilike";
23
+ FilterOperator$1["In"] = "in";
24
+ FilterOperator$1["Nin"] = "nin";
25
+ FilterOperator$1["IsNull"] = "is_null";
26
+ FilterOperator$1["IsNotNull"] = "is_not_null";
27
+ return FilterOperator$1;
28
+ }({});
29
+
30
+ //#endregion
31
+ Object.defineProperty(exports, 'Direction', {
32
+ enumerable: true,
33
+ get: function () {
34
+ return Direction;
35
+ }
36
+ });
37
+ Object.defineProperty(exports, 'FilterOperator', {
38
+ enumerable: true,
39
+ get: function () {
40
+ return FilterOperator;
41
+ }
42
+ });
43
+ //# sourceMappingURL=types-CMttUZYk.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-CMttUZYk.cjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type { TelescopeStorage } from '@geekmidas/telescope';\nimport type { Kysely } from 'kysely';\n\n// ============================================\n// Enums\n// ============================================\n\n/**\n * Sort direction for cursor-based pagination and sorting.\n */\nexport enum Direction {\n Asc = 'asc',\n Desc = 'desc',\n}\n\n/**\n * Filter operators for querying data.\n */\nexport enum FilterOperator {\n Eq = 'eq',\n Neq = 'neq',\n Gt = 'gt',\n Gte = 'gte',\n Lt = 'lt',\n Lte = 'lte',\n Like = 'like',\n Ilike = 'ilike',\n In = 'in',\n Nin = 'nin',\n IsNull = 'is_null',\n IsNotNull = 'is_not_null',\n}\n\n// ============================================\n// Cursor Configuration\n// ============================================\n\n/**\n * Configuration for cursor-based pagination.\n */\nexport interface CursorConfig {\n /** The field to use for cursor-based pagination (e.g., 'id', 'created_at') */\n field: string;\n /** Sort direction for the cursor field */\n direction: Direction;\n}\n\n/**\n * Per-table cursor configuration overrides.\n */\nexport interface TableCursorConfig {\n [tableName: string]: CursorConfig;\n}\n\n// ============================================\n// Monitoring Configuration\n// ============================================\n\n/**\n * Configuration for the monitoring feature (Telescope).\n */\nexport interface MonitoringOptions {\n /** Storage backend for monitoring data */\n storage: TelescopeStorage;\n /** Patterns to ignore when recording requests (supports wildcards) */\n ignorePatterns?: string[];\n /** Whether to record request/response bodies (default: true) */\n recordBody?: boolean;\n /** Maximum body size to record in bytes (default: 64KB) */\n maxBodySize?: number;\n /** Hours after which to prune old entries */\n pruneAfterHours?: number;\n}\n\n// ============================================\n// Data Browser Configuration\n// ============================================\n\n/**\n * Configuration for the data browser feature.\n */\nexport interface DataBrowserOptions<DB = unknown> {\n /** Kysely database instance */\n db: Kysely<DB>;\n /** Default cursor configuration for all tables */\n cursor: CursorConfig;\n /** Per-table cursor overrides */\n tableCursors?: TableCursorConfig;\n /** Tables to exclude from browsing */\n excludeTables?: string[];\n /** Maximum rows per page (default: 50, max: 100) */\n defaultPageSize?: number;\n /** Whether to allow viewing of binary/blob columns (default: false) */\n showBinaryColumns?: boolean;\n}\n\n// ============================================\n// Studio Configuration\n// ============================================\n\n/**\n * Configuration for the Studio dashboard.\n */\nexport interface StudioOptions<DB = unknown> {\n /** Monitoring configuration */\n monitoring: MonitoringOptions;\n /** Data browser configuration */\n data: DataBrowserOptions<DB>;\n /** Dashboard path (default: '/__studio') */\n path?: string;\n /** Whether Studio is enabled (default: true) */\n enabled?: boolean;\n}\n\n/**\n * Normalized Studio options with all defaults applied.\n */\nexport interface NormalizedStudioOptions<DB = unknown> {\n monitoring: Required<MonitoringOptions>;\n data: Required<DataBrowserOptions<DB>>;\n path: string;\n enabled: boolean;\n}\n\n// ============================================\n// Table Introspection Types\n// ============================================\n\n/**\n * Generic column type classification.\n */\nexport type ColumnType =\n | 'string'\n | 'number'\n | 'boolean'\n | 'date'\n | 'datetime'\n | 'json'\n | 'binary'\n | 'uuid'\n | 'unknown';\n\n/**\n * Information about a database column.\n */\nexport interface ColumnInfo {\n /** Column name */\n name: string;\n /** Generic column type */\n type: ColumnType;\n /** Raw database type (e.g., 'varchar', 'int4') */\n rawType: string;\n /** Whether the column allows NULL values */\n nullable: boolean;\n /** Whether this column is part of the primary key */\n isPrimaryKey: boolean;\n /** Whether this column is a foreign key */\n isForeignKey: boolean;\n /** Referenced table if foreign key */\n foreignKeyTable?: string;\n /** Referenced column if foreign key */\n foreignKeyColumn?: string;\n /** Default value expression */\n defaultValue?: string;\n}\n\n/**\n * Information about a database table.\n */\nexport interface TableInfo {\n /** Table name */\n name: string;\n /** Schema name (e.g., 'public') */\n schema: string;\n /** List of columns */\n columns: ColumnInfo[];\n /** Primary key column names */\n primaryKey: string[];\n /** Estimated row count (if available) */\n estimatedRowCount?: number;\n}\n\n/**\n * Complete schema information.\n */\nexport interface SchemaInfo {\n /** List of tables */\n tables: TableInfo[];\n /** When the schema was last introspected */\n updatedAt: Date;\n}\n\n// ============================================\n// Query Types\n// ============================================\n\n/**\n * A single filter condition.\n */\nexport interface FilterCondition {\n /** Column to filter on */\n column: string;\n /** Filter operator */\n operator: FilterOperator;\n /** Value to compare against (optional for IsNull/IsNotNull operators) */\n value?: unknown;\n}\n\n/**\n * Sort configuration for a column.\n */\nexport interface SortConfig {\n /** Column to sort by */\n column: string;\n /** Sort direction */\n direction: Direction;\n}\n\n/**\n * Options for querying table data.\n */\nexport interface QueryOptions {\n /** Table to query */\n table: string;\n /** Filter conditions */\n filters?: FilterCondition[];\n /** Sort configuration */\n sort?: SortConfig[];\n /** Cursor for pagination */\n cursor?: string | null;\n /** Number of rows per page */\n pageSize?: number;\n /** Pagination direction */\n direction?: 'next' | 'prev';\n}\n\n/**\n * Result of a paginated query.\n */\nexport interface QueryResult<T = Record<string, unknown>> {\n /** Retrieved rows */\n rows: T[];\n /** Whether there are more rows */\n hasMore: boolean;\n /** Cursor for next page */\n nextCursor: string | null;\n /** Cursor for previous page */\n prevCursor: string | null;\n /** Estimated total row count */\n totalEstimate?: number;\n}\n\n// ============================================\n// WebSocket Events\n// ============================================\n\n/**\n * Types of events broadcast via WebSocket.\n */\nexport type StudioEventType =\n | 'request'\n | 'exception'\n | 'log'\n | 'stats'\n | 'connected'\n | 'schema_updated';\n\n/**\n * A WebSocket event payload.\n */\nexport interface StudioEvent<T = unknown> {\n /** Event type */\n type: StudioEventType;\n /** Event payload */\n payload: T;\n /** Event timestamp */\n timestamp: number;\n}\n"],"mappings":";;;;;AAUA,IAAY,kDAAL;AACL;AACA;;AACD;;;;AAKD,IAAY,4DAAL;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACD"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@geekmidas/studio",
3
+ "version": "0.0.1",
4
+ "description": "Unified development tools dashboard with database browser and request monitoring",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.mjs",
10
+ "require": "./dist/index.cjs"
11
+ },
12
+ "./data": {
13
+ "types": "./dist/data/index.d.ts",
14
+ "import": "./dist/data/index.mjs",
15
+ "require": "./dist/data/index.cjs"
16
+ },
17
+ "./server/hono": {
18
+ "types": "./dist/server/hono.d.ts",
19
+ "import": "./dist/server/hono.mjs",
20
+ "require": "./dist/server/hono.cjs"
21
+ }
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/geekmidas/toolbox"
26
+ },
27
+ "publishConfig": {
28
+ "registry": "https://registry.npmjs.org/",
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "nanoid": "~5.1.5",
33
+ "@geekmidas/telescope": "0.0.1",
34
+ "@geekmidas/db": "0.1.0"
35
+ },
36
+ "peerDependencies": {
37
+ "hono": "^4.0.0",
38
+ "kysely": "^0.28.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "hono": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "devDependencies": {
46
+ "@types/pg": "~8.16.0",
47
+ "hono": "^4.7.0",
48
+ "kysely": "~0.28.2",
49
+ "pg": "~8.16.3"
50
+ },
51
+ "scripts": {
52
+ "build:ui": "pnpm --filter @geekmidas/studio-ui build && tsx ../ui/scripts/embed-ui.ts dist/ui src/ui-assets.ts"
53
+ }
54
+ }
package/src/Studio.ts ADDED
@@ -0,0 +1,318 @@
1
+ import { Telescope } from '@geekmidas/telescope';
2
+ import { DataBrowser } from './data/DataBrowser';
3
+ import type {
4
+ NormalizedStudioOptions,
5
+ StudioEvent,
6
+ StudioOptions,
7
+ } from './types';
8
+
9
+ /**
10
+ * Unified development tools dashboard combining monitoring and database browsing.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { Studio, Direction } from '@geekmidas/studio';
15
+ * import { InMemoryStorage } from '@geekmidas/telescope/storage/memory';
16
+ *
17
+ * const studio = new Studio({
18
+ * monitoring: {
19
+ * storage: new InMemoryStorage(),
20
+ * },
21
+ * data: {
22
+ * db: kyselyInstance,
23
+ * cursor: { field: 'id', direction: Direction.Desc },
24
+ * },
25
+ * });
26
+ * ```
27
+ */
28
+ export class Studio<DB = unknown> {
29
+ private telescope: Telescope;
30
+ private dataBrowser: DataBrowser<DB>;
31
+ private options: NormalizedStudioOptions<DB>;
32
+ private wsClients = new Set<WebSocket>();
33
+
34
+ constructor(options: StudioOptions<DB>) {
35
+ this.options = this.normalizeOptions(options);
36
+
37
+ // Initialize Telescope internally
38
+ this.telescope = new Telescope({
39
+ storage: this.options.monitoring.storage,
40
+ enabled: this.options.enabled,
41
+ path: `${this.options.path}/monitoring`,
42
+ recordBody: this.options.monitoring.recordBody,
43
+ maxBodySize: this.options.monitoring.maxBodySize,
44
+ ignorePatterns: [
45
+ ...this.options.monitoring.ignorePatterns,
46
+ `${this.options.path}/*`, // Ignore Studio's own routes
47
+ ],
48
+ pruneAfterHours: this.options.monitoring.pruneAfterHours,
49
+ });
50
+
51
+ // Initialize DataBrowser
52
+ this.dataBrowser = new DataBrowser(this.options.data);
53
+ }
54
+
55
+ // ============================================
56
+ // Public API - Configuration
57
+ // ============================================
58
+
59
+ /**
60
+ * Get the Studio dashboard path.
61
+ */
62
+ get path(): string {
63
+ return this.options.path;
64
+ }
65
+
66
+ /**
67
+ * Check if Studio is enabled.
68
+ */
69
+ get enabled(): boolean {
70
+ return this.options.enabled;
71
+ }
72
+
73
+ /**
74
+ * Get the data browser instance.
75
+ */
76
+ get data(): DataBrowser<DB> {
77
+ return this.dataBrowser;
78
+ }
79
+
80
+ /**
81
+ * Check if body recording is enabled for monitoring.
82
+ */
83
+ get recordBody(): boolean {
84
+ return this.options.monitoring.recordBody;
85
+ }
86
+
87
+ /**
88
+ * Get max body size for monitoring.
89
+ */
90
+ get maxBodySize(): number {
91
+ return this.options.monitoring.maxBodySize;
92
+ }
93
+
94
+ // ============================================
95
+ // Public API - Monitoring (delegated to Telescope)
96
+ // ============================================
97
+
98
+ /**
99
+ * Record a request entry.
100
+ */
101
+ async recordRequest(
102
+ entry: Parameters<Telescope['recordRequest']>[0],
103
+ ): Promise<string> {
104
+ return this.telescope.recordRequest(entry);
105
+ }
106
+
107
+ /**
108
+ * Record log entries in batch.
109
+ */
110
+ async log(entries: Parameters<Telescope['log']>[0]): Promise<void> {
111
+ return this.telescope.log(entries);
112
+ }
113
+
114
+ /**
115
+ * Log a debug message.
116
+ */
117
+ async debug(
118
+ message: string,
119
+ context?: Record<string, unknown>,
120
+ requestId?: string,
121
+ ): Promise<void> {
122
+ return this.telescope.debug(message, context, requestId);
123
+ }
124
+
125
+ /**
126
+ * Log an info message.
127
+ */
128
+ async info(
129
+ message: string,
130
+ context?: Record<string, unknown>,
131
+ requestId?: string,
132
+ ): Promise<void> {
133
+ return this.telescope.info(message, context, requestId);
134
+ }
135
+
136
+ /**
137
+ * Log a warning message.
138
+ */
139
+ async warn(
140
+ message: string,
141
+ context?: Record<string, unknown>,
142
+ requestId?: string,
143
+ ): Promise<void> {
144
+ return this.telescope.warn(message, context, requestId);
145
+ }
146
+
147
+ /**
148
+ * Log an error message.
149
+ */
150
+ async error(
151
+ message: string,
152
+ context?: Record<string, unknown>,
153
+ requestId?: string,
154
+ ): Promise<void> {
155
+ return this.telescope.error(message, context, requestId);
156
+ }
157
+
158
+ /**
159
+ * Record an exception.
160
+ */
161
+ async exception(error: Error, requestId?: string): Promise<void> {
162
+ return this.telescope.exception(error, requestId);
163
+ }
164
+
165
+ /**
166
+ * Get requests from storage.
167
+ */
168
+ async getRequests(
169
+ options?: Parameters<Telescope['getRequests']>[0],
170
+ ): ReturnType<Telescope['getRequests']> {
171
+ return this.telescope.getRequests(options);
172
+ }
173
+
174
+ /**
175
+ * Get a single request by ID.
176
+ */
177
+ async getRequest(id: string): ReturnType<Telescope['getRequest']> {
178
+ return this.telescope.getRequest(id);
179
+ }
180
+
181
+ /**
182
+ * Get exceptions from storage.
183
+ */
184
+ async getExceptions(
185
+ options?: Parameters<Telescope['getExceptions']>[0],
186
+ ): ReturnType<Telescope['getExceptions']> {
187
+ return this.telescope.getExceptions(options);
188
+ }
189
+
190
+ /**
191
+ * Get a single exception by ID.
192
+ */
193
+ async getException(id: string): ReturnType<Telescope['getException']> {
194
+ return this.telescope.getException(id);
195
+ }
196
+
197
+ /**
198
+ * Get logs from storage.
199
+ */
200
+ async getLogs(
201
+ options?: Parameters<Telescope['getLogs']>[0],
202
+ ): ReturnType<Telescope['getLogs']> {
203
+ return this.telescope.getLogs(options);
204
+ }
205
+
206
+ /**
207
+ * Get storage statistics.
208
+ */
209
+ async getStats(): ReturnType<Telescope['getStats']> {
210
+ return this.telescope.getStats();
211
+ }
212
+
213
+ // ============================================
214
+ // Public API - WebSocket
215
+ // ============================================
216
+
217
+ /**
218
+ * Add a WebSocket client for real-time updates.
219
+ */
220
+ addWsClient(ws: WebSocket): void {
221
+ this.wsClients.add(ws);
222
+ // Also add to Telescope for monitoring events
223
+ this.telescope.addWsClient(ws);
224
+ }
225
+
226
+ /**
227
+ * Remove a WebSocket client.
228
+ */
229
+ removeWsClient(ws: WebSocket): void {
230
+ this.wsClients.delete(ws);
231
+ this.telescope.removeWsClient(ws);
232
+ }
233
+
234
+ /**
235
+ * Broadcast an event to all connected WebSocket clients.
236
+ */
237
+ broadcast(event: StudioEvent): void {
238
+ const data = JSON.stringify(event);
239
+ for (const client of this.wsClients) {
240
+ try {
241
+ client.send(data);
242
+ } catch {
243
+ this.wsClients.delete(client);
244
+ }
245
+ }
246
+ }
247
+
248
+ // ============================================
249
+ // Public API - Lifecycle
250
+ // ============================================
251
+
252
+ /**
253
+ * Check if a path should be ignored for request recording.
254
+ */
255
+ shouldIgnore(path: string): boolean {
256
+ // Ignore Studio's own routes
257
+ if (path.startsWith(this.options.path)) {
258
+ return true;
259
+ }
260
+ return this.telescope.shouldIgnore(path);
261
+ }
262
+
263
+ /**
264
+ * Manually prune old monitoring entries.
265
+ */
266
+ async prune(olderThan: Date): Promise<number> {
267
+ return this.telescope.prune(olderThan);
268
+ }
269
+
270
+ /**
271
+ * Clean up resources.
272
+ */
273
+ destroy(): void {
274
+ this.telescope.destroy();
275
+ this.wsClients.clear();
276
+ }
277
+
278
+ // ============================================
279
+ // Private Methods
280
+ // ============================================
281
+
282
+ private normalizeOptions(
283
+ options: StudioOptions<DB>,
284
+ ): NormalizedStudioOptions<DB> {
285
+ const path = options.path ?? '/__studio';
286
+
287
+ return {
288
+ monitoring: {
289
+ storage: options.monitoring.storage,
290
+ ignorePatterns: options.monitoring.ignorePatterns ?? [],
291
+ recordBody: options.monitoring.recordBody ?? true,
292
+ maxBodySize: options.monitoring.maxBodySize ?? 64 * 1024,
293
+ pruneAfterHours: options.monitoring.pruneAfterHours,
294
+ },
295
+ data: {
296
+ db: options.data.db,
297
+ cursor: options.data.cursor,
298
+ tableCursors: options.data.tableCursors ?? {},
299
+ excludeTables: options.data.excludeTables ?? [
300
+ // Kysely
301
+ 'kysely_migration',
302
+ 'kysely_migration_lock',
303
+ // Prisma
304
+ '_prisma_migrations',
305
+ // Rails/Knex
306
+ 'schema_migrations',
307
+ // Generic
308
+ '_migrations',
309
+ 'migrations',
310
+ ],
311
+ defaultPageSize: Math.min(options.data.defaultPageSize ?? 50, 100),
312
+ showBinaryColumns: options.data.showBinaryColumns ?? false,
313
+ },
314
+ path,
315
+ enabled: options.enabled ?? true,
316
+ };
317
+ }
318
+ }
@@ -0,0 +1,166 @@
1
+ import type { Kysely, SelectQueryBuilder } from 'kysely';
2
+ import {
3
+ type CursorConfig,
4
+ type DataBrowserOptions,
5
+ Direction,
6
+ type QueryOptions,
7
+ type QueryResult,
8
+ type SchemaInfo,
9
+ type TableInfo,
10
+ } from '../types';
11
+ import { applyFilters, applySorting } from './filtering';
12
+ import { introspectSchema } from './introspection';
13
+ import { decodeCursor, encodeCursor } from './pagination';
14
+
15
+ /**
16
+ * Database browser for introspecting and querying PostgreSQL databases.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const browser = new DataBrowser({
21
+ * db: kyselyInstance,
22
+ * cursor: { field: 'id', direction: Direction.Desc },
23
+ * });
24
+ *
25
+ * const schema = await browser.getSchema();
26
+ * const result = await browser.query({ table: 'users', pageSize: 20 });
27
+ * ```
28
+ */
29
+ export class DataBrowser<DB = unknown> {
30
+ private db: Kysely<DB>;
31
+ private options: Required<DataBrowserOptions<DB>>;
32
+ private schemaCache: SchemaInfo | null = null;
33
+ private schemaCacheExpiry = 0;
34
+ private readonly CACHE_TTL_MS = 60 * 1000; // 1 minute
35
+
36
+ constructor(options: Required<DataBrowserOptions<DB>>) {
37
+ this.db = options.db;
38
+ this.options = options;
39
+ }
40
+
41
+ // ============================================
42
+ // Schema Introspection
43
+ // ============================================
44
+
45
+ /**
46
+ * Get the database schema information.
47
+ * Results are cached for 1 minute.
48
+ *
49
+ * @param forceRefresh - Force a refresh of the cache
50
+ */
51
+ async getSchema(forceRefresh = false): Promise<SchemaInfo> {
52
+ const now = Date.now();
53
+
54
+ if (!forceRefresh && this.schemaCache && now < this.schemaCacheExpiry) {
55
+ return this.schemaCache;
56
+ }
57
+
58
+ const schema = await introspectSchema(this.db, this.options.excludeTables);
59
+
60
+ this.schemaCache = schema;
61
+ this.schemaCacheExpiry = now + this.CACHE_TTL_MS;
62
+
63
+ return schema;
64
+ }
65
+
66
+ /**
67
+ * Get information about a specific table.
68
+ */
69
+ async getTableInfo(tableName: string): Promise<TableInfo | null> {
70
+ const schema = await this.getSchema();
71
+ return schema.tables.find((t) => t.name === tableName) ?? null;
72
+ }
73
+
74
+ // ============================================
75
+ // Data Querying
76
+ // ============================================
77
+
78
+ /**
79
+ * Query table data with pagination, filtering, and sorting.
80
+ */
81
+ async query(options: QueryOptions): Promise<QueryResult> {
82
+ const tableInfo = await this.getTableInfo(options.table);
83
+
84
+ if (!tableInfo) {
85
+ throw new Error(`Table '${options.table}' not found`);
86
+ }
87
+
88
+ const cursorConfig = this.getCursorConfig(options.table);
89
+ const pageSize = Math.min(
90
+ options.pageSize ?? this.options.defaultPageSize,
91
+ 100,
92
+ );
93
+
94
+ // Build base query selecting all columns
95
+ let query = this.db
96
+ .selectFrom(options.table as any)
97
+ .selectAll() as SelectQueryBuilder<any, any, any>;
98
+
99
+ // Apply filters if provided
100
+ if (options.filters && options.filters.length > 0) {
101
+ query = applyFilters(query, options.filters, tableInfo);
102
+ }
103
+
104
+ // Apply sorting if provided, otherwise use cursor field
105
+ if (options.sort && options.sort.length > 0) {
106
+ query = applySorting(query, options.sort, tableInfo);
107
+ } else {
108
+ query = query.orderBy(cursorConfig.field as any, cursorConfig.direction);
109
+ }
110
+
111
+ // Handle cursor-based pagination
112
+ if (options.cursor) {
113
+ const cursorValue = decodeCursor(options.cursor);
114
+ const operator = cursorConfig.direction === Direction.Asc ? '>' : '<';
115
+ query = query.where(cursorConfig.field as any, operator, cursorValue);
116
+ }
117
+
118
+ // Fetch one extra row to determine if there are more results
119
+ const rows = await query.limit(pageSize + 1).execute();
120
+
121
+ const hasMore = rows.length > pageSize;
122
+ const resultRows = hasMore ? rows.slice(0, pageSize) : rows;
123
+
124
+ // Generate cursors
125
+ let nextCursor: string | null = null;
126
+ let prevCursor: string | null = null;
127
+
128
+ if (hasMore && resultRows.length > 0) {
129
+ const lastRow = resultRows[resultRows.length - 1];
130
+ nextCursor = encodeCursor(lastRow[cursorConfig.field]);
131
+ }
132
+
133
+ // For prev cursor, we need to know if there are previous results
134
+ // This would require a separate query, so we'll only set it if there's an input cursor
135
+ if (options.cursor && resultRows.length > 0) {
136
+ const firstRow = resultRows[0];
137
+ prevCursor = encodeCursor(firstRow[cursorConfig.field]);
138
+ }
139
+
140
+ return {
141
+ rows: resultRows,
142
+ hasMore,
143
+ nextCursor,
144
+ prevCursor,
145
+ };
146
+ }
147
+
148
+ // ============================================
149
+ // Configuration Access
150
+ // ============================================
151
+
152
+ /**
153
+ * Get the cursor configuration for a table.
154
+ * Returns the table-specific config if defined, otherwise the default.
155
+ */
156
+ getCursorConfig(tableName: string): CursorConfig {
157
+ return this.options.tableCursors[tableName] ?? this.options.cursor;
158
+ }
159
+
160
+ /**
161
+ * Get the underlying Kysely database instance.
162
+ */
163
+ get database(): Kysely<DB> {
164
+ return this.db;
165
+ }
166
+ }