@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,267 @@
1
+ import { TelescopeStorage } from "@geekmidas/telescope";
2
+ import { Kysely } from "kysely";
3
+
4
+ //#region src/types.d.ts
5
+
6
+ /**
7
+ * Sort direction for cursor-based pagination and sorting.
8
+ */
9
+ declare enum Direction {
10
+ Asc = "asc",
11
+ Desc = "desc",
12
+ }
13
+ /**
14
+ * Filter operators for querying data.
15
+ */
16
+ declare enum FilterOperator {
17
+ Eq = "eq",
18
+ Neq = "neq",
19
+ Gt = "gt",
20
+ Gte = "gte",
21
+ Lt = "lt",
22
+ Lte = "lte",
23
+ Like = "like",
24
+ Ilike = "ilike",
25
+ In = "in",
26
+ Nin = "nin",
27
+ IsNull = "is_null",
28
+ IsNotNull = "is_not_null",
29
+ }
30
+ /**
31
+ * Configuration for cursor-based pagination.
32
+ */
33
+ interface CursorConfig {
34
+ /** The field to use for cursor-based pagination (e.g., 'id', 'created_at') */
35
+ field: string;
36
+ /** Sort direction for the cursor field */
37
+ direction: Direction;
38
+ }
39
+ /**
40
+ * Per-table cursor configuration overrides.
41
+ */
42
+ interface TableCursorConfig {
43
+ [tableName: string]: CursorConfig;
44
+ }
45
+ /**
46
+ * Configuration for the monitoring feature (Telescope).
47
+ */
48
+ interface MonitoringOptions {
49
+ /** Storage backend for monitoring data */
50
+ storage: TelescopeStorage;
51
+ /** Patterns to ignore when recording requests (supports wildcards) */
52
+ ignorePatterns?: string[];
53
+ /** Whether to record request/response bodies (default: true) */
54
+ recordBody?: boolean;
55
+ /** Maximum body size to record in bytes (default: 64KB) */
56
+ maxBodySize?: number;
57
+ /** Hours after which to prune old entries */
58
+ pruneAfterHours?: number;
59
+ }
60
+ /**
61
+ * Configuration for the data browser feature.
62
+ */
63
+ interface DataBrowserOptions<DB = unknown> {
64
+ /** Kysely database instance */
65
+ db: Kysely<DB>;
66
+ /** Default cursor configuration for all tables */
67
+ cursor: CursorConfig;
68
+ /** Per-table cursor overrides */
69
+ tableCursors?: TableCursorConfig;
70
+ /** Tables to exclude from browsing */
71
+ excludeTables?: string[];
72
+ /** Maximum rows per page (default: 50, max: 100) */
73
+ defaultPageSize?: number;
74
+ /** Whether to allow viewing of binary/blob columns (default: false) */
75
+ showBinaryColumns?: boolean;
76
+ }
77
+ /**
78
+ * Configuration for the Studio dashboard.
79
+ */
80
+ interface StudioOptions<DB = unknown> {
81
+ /** Monitoring configuration */
82
+ monitoring: MonitoringOptions;
83
+ /** Data browser configuration */
84
+ data: DataBrowserOptions<DB>;
85
+ /** Dashboard path (default: '/__studio') */
86
+ path?: string;
87
+ /** Whether Studio is enabled (default: true) */
88
+ enabled?: boolean;
89
+ }
90
+ /**
91
+ * Normalized Studio options with all defaults applied.
92
+ */
93
+ interface NormalizedStudioOptions<DB = unknown> {
94
+ monitoring: Required<MonitoringOptions>;
95
+ data: Required<DataBrowserOptions<DB>>;
96
+ path: string;
97
+ enabled: boolean;
98
+ }
99
+ /**
100
+ * Generic column type classification.
101
+ */
102
+ type ColumnType = 'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'json' | 'binary' | 'uuid' | 'unknown';
103
+ /**
104
+ * Information about a database column.
105
+ */
106
+ interface ColumnInfo {
107
+ /** Column name */
108
+ name: string;
109
+ /** Generic column type */
110
+ type: ColumnType;
111
+ /** Raw database type (e.g., 'varchar', 'int4') */
112
+ rawType: string;
113
+ /** Whether the column allows NULL values */
114
+ nullable: boolean;
115
+ /** Whether this column is part of the primary key */
116
+ isPrimaryKey: boolean;
117
+ /** Whether this column is a foreign key */
118
+ isForeignKey: boolean;
119
+ /** Referenced table if foreign key */
120
+ foreignKeyTable?: string;
121
+ /** Referenced column if foreign key */
122
+ foreignKeyColumn?: string;
123
+ /** Default value expression */
124
+ defaultValue?: string;
125
+ }
126
+ /**
127
+ * Information about a database table.
128
+ */
129
+ interface TableInfo {
130
+ /** Table name */
131
+ name: string;
132
+ /** Schema name (e.g., 'public') */
133
+ schema: string;
134
+ /** List of columns */
135
+ columns: ColumnInfo[];
136
+ /** Primary key column names */
137
+ primaryKey: string[];
138
+ /** Estimated row count (if available) */
139
+ estimatedRowCount?: number;
140
+ }
141
+ /**
142
+ * Complete schema information.
143
+ */
144
+ interface SchemaInfo {
145
+ /** List of tables */
146
+ tables: TableInfo[];
147
+ /** When the schema was last introspected */
148
+ updatedAt: Date;
149
+ }
150
+ /**
151
+ * A single filter condition.
152
+ */
153
+ interface FilterCondition {
154
+ /** Column to filter on */
155
+ column: string;
156
+ /** Filter operator */
157
+ operator: FilterOperator;
158
+ /** Value to compare against (optional for IsNull/IsNotNull operators) */
159
+ value?: unknown;
160
+ }
161
+ /**
162
+ * Sort configuration for a column.
163
+ */
164
+ interface SortConfig {
165
+ /** Column to sort by */
166
+ column: string;
167
+ /** Sort direction */
168
+ direction: Direction;
169
+ }
170
+ /**
171
+ * Options for querying table data.
172
+ */
173
+ interface QueryOptions {
174
+ /** Table to query */
175
+ table: string;
176
+ /** Filter conditions */
177
+ filters?: FilterCondition[];
178
+ /** Sort configuration */
179
+ sort?: SortConfig[];
180
+ /** Cursor for pagination */
181
+ cursor?: string | null;
182
+ /** Number of rows per page */
183
+ pageSize?: number;
184
+ /** Pagination direction */
185
+ direction?: 'next' | 'prev';
186
+ }
187
+ /**
188
+ * Result of a paginated query.
189
+ */
190
+ interface QueryResult<T = Record<string, unknown>> {
191
+ /** Retrieved rows */
192
+ rows: T[];
193
+ /** Whether there are more rows */
194
+ hasMore: boolean;
195
+ /** Cursor for next page */
196
+ nextCursor: string | null;
197
+ /** Cursor for previous page */
198
+ prevCursor: string | null;
199
+ /** Estimated total row count */
200
+ totalEstimate?: number;
201
+ }
202
+ /**
203
+ * Types of events broadcast via WebSocket.
204
+ */
205
+ type StudioEventType = 'request' | 'exception' | 'log' | 'stats' | 'connected' | 'schema_updated';
206
+ /**
207
+ * A WebSocket event payload.
208
+ */
209
+ interface StudioEvent<T = unknown> {
210
+ /** Event type */
211
+ type: StudioEventType;
212
+ /** Event payload */
213
+ payload: T;
214
+ /** Event timestamp */
215
+ timestamp: number;
216
+ }
217
+ //#endregion
218
+ //#region src/data/DataBrowser.d.ts
219
+ /**
220
+ * Database browser for introspecting and querying PostgreSQL databases.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * const browser = new DataBrowser({
225
+ * db: kyselyInstance,
226
+ * cursor: { field: 'id', direction: Direction.Desc },
227
+ * });
228
+ *
229
+ * const schema = await browser.getSchema();
230
+ * const result = await browser.query({ table: 'users', pageSize: 20 });
231
+ * ```
232
+ */
233
+ declare class DataBrowser<DB = unknown> {
234
+ private db;
235
+ private options;
236
+ private schemaCache;
237
+ private schemaCacheExpiry;
238
+ private readonly CACHE_TTL_MS;
239
+ constructor(options: Required<DataBrowserOptions<DB>>);
240
+ /**
241
+ * Get the database schema information.
242
+ * Results are cached for 1 minute.
243
+ *
244
+ * @param forceRefresh - Force a refresh of the cache
245
+ */
246
+ getSchema(forceRefresh?: boolean): Promise<SchemaInfo>;
247
+ /**
248
+ * Get information about a specific table.
249
+ */
250
+ getTableInfo(tableName: string): Promise<TableInfo | null>;
251
+ /**
252
+ * Query table data with pagination, filtering, and sorting.
253
+ */
254
+ query(options: QueryOptions): Promise<QueryResult>;
255
+ /**
256
+ * Get the cursor configuration for a table.
257
+ * Returns the table-specific config if defined, otherwise the default.
258
+ */
259
+ getCursorConfig(tableName: string): CursorConfig;
260
+ /**
261
+ * Get the underlying Kysely database instance.
262
+ */
263
+ get database(): Kysely<DB>;
264
+ }
265
+ //#endregion
266
+ export { ColumnInfo, ColumnType, CursorConfig, DataBrowser, DataBrowserOptions, Direction, FilterCondition, FilterOperator, MonitoringOptions, NormalizedStudioOptions, QueryOptions, QueryResult, SchemaInfo, SortConfig, StudioEvent, StudioEventType, StudioOptions, TableCursorConfig, TableInfo };
267
+ //# sourceMappingURL=DataBrowser-SOcqmZb2.d.mts.map
@@ -0,0 +1,432 @@
1
+ const require_types = require('./types-CMttUZYk.cjs');
2
+
3
+ //#region src/data/filtering.ts
4
+ /**
5
+ * Validates that a filter is applicable to the given column.
6
+ */
7
+ function validateFilter(filter, columnInfo) {
8
+ const typeCompatibility = {
9
+ string: [
10
+ require_types.FilterOperator.Eq,
11
+ require_types.FilterOperator.Neq,
12
+ require_types.FilterOperator.Like,
13
+ require_types.FilterOperator.Ilike,
14
+ require_types.FilterOperator.In,
15
+ require_types.FilterOperator.Nin,
16
+ require_types.FilterOperator.IsNull,
17
+ require_types.FilterOperator.IsNotNull
18
+ ],
19
+ number: [
20
+ require_types.FilterOperator.Eq,
21
+ require_types.FilterOperator.Neq,
22
+ require_types.FilterOperator.Gt,
23
+ require_types.FilterOperator.Gte,
24
+ require_types.FilterOperator.Lt,
25
+ require_types.FilterOperator.Lte,
26
+ require_types.FilterOperator.In,
27
+ require_types.FilterOperator.Nin,
28
+ require_types.FilterOperator.IsNull,
29
+ require_types.FilterOperator.IsNotNull
30
+ ],
31
+ boolean: [
32
+ require_types.FilterOperator.Eq,
33
+ require_types.FilterOperator.Neq,
34
+ require_types.FilterOperator.IsNull,
35
+ require_types.FilterOperator.IsNotNull
36
+ ],
37
+ date: [
38
+ require_types.FilterOperator.Eq,
39
+ require_types.FilterOperator.Neq,
40
+ require_types.FilterOperator.Gt,
41
+ require_types.FilterOperator.Gte,
42
+ require_types.FilterOperator.Lt,
43
+ require_types.FilterOperator.Lte,
44
+ require_types.FilterOperator.IsNull,
45
+ require_types.FilterOperator.IsNotNull
46
+ ],
47
+ datetime: [
48
+ require_types.FilterOperator.Eq,
49
+ require_types.FilterOperator.Neq,
50
+ require_types.FilterOperator.Gt,
51
+ require_types.FilterOperator.Gte,
52
+ require_types.FilterOperator.Lt,
53
+ require_types.FilterOperator.Lte,
54
+ require_types.FilterOperator.IsNull,
55
+ require_types.FilterOperator.IsNotNull
56
+ ],
57
+ uuid: [
58
+ require_types.FilterOperator.Eq,
59
+ require_types.FilterOperator.Neq,
60
+ require_types.FilterOperator.In,
61
+ require_types.FilterOperator.Nin,
62
+ require_types.FilterOperator.IsNull,
63
+ require_types.FilterOperator.IsNotNull
64
+ ],
65
+ json: [require_types.FilterOperator.IsNull, require_types.FilterOperator.IsNotNull],
66
+ binary: [require_types.FilterOperator.IsNull, require_types.FilterOperator.IsNotNull],
67
+ unknown: [
68
+ require_types.FilterOperator.Eq,
69
+ require_types.FilterOperator.Neq,
70
+ require_types.FilterOperator.IsNull,
71
+ require_types.FilterOperator.IsNotNull
72
+ ]
73
+ };
74
+ const allowedOps = typeCompatibility[columnInfo.type] ?? typeCompatibility.unknown;
75
+ if (!allowedOps.includes(filter.operator)) return {
76
+ valid: false,
77
+ error: `Operator '${filter.operator}' not supported for column type '${columnInfo.type}'`
78
+ };
79
+ return { valid: true };
80
+ }
81
+ /**
82
+ * Applies filters to a Kysely query builder.
83
+ */
84
+ function applyFilters(query, filters, tableInfo) {
85
+ let result = query;
86
+ for (const filter of filters) {
87
+ const column = tableInfo.columns.find((c) => c.name === filter.column);
88
+ if (!column) throw new Error(`Column '${filter.column}' not found in table '${tableInfo.name}'`);
89
+ const validation = validateFilter(filter, column);
90
+ if (!validation.valid) throw new Error(validation.error);
91
+ result = applyFilterCondition(result, filter);
92
+ }
93
+ return result;
94
+ }
95
+ function applyFilterCondition(query, filter) {
96
+ const { column, operator, value } = filter;
97
+ switch (operator) {
98
+ case require_types.FilterOperator.Eq: return query.where(column, "=", value);
99
+ case require_types.FilterOperator.Neq: return query.where(column, "!=", value);
100
+ case require_types.FilterOperator.Gt: return query.where(column, ">", value);
101
+ case require_types.FilterOperator.Gte: return query.where(column, ">=", value);
102
+ case require_types.FilterOperator.Lt: return query.where(column, "<", value);
103
+ case require_types.FilterOperator.Lte: return query.where(column, "<=", value);
104
+ case require_types.FilterOperator.Like: return query.where(column, "like", value);
105
+ case require_types.FilterOperator.Ilike: return query.where(column, "ilike", value);
106
+ case require_types.FilterOperator.In: return query.where(column, "in", value);
107
+ case require_types.FilterOperator.Nin: return query.where(column, "not in", value);
108
+ case require_types.FilterOperator.IsNull: return query.where(column, "is", null);
109
+ case require_types.FilterOperator.IsNotNull: return query.where(column, "is not", null);
110
+ default: throw new Error(`Unknown filter operator: ${operator}`);
111
+ }
112
+ }
113
+ /**
114
+ * Applies sorting to a Kysely query builder.
115
+ */
116
+ function applySorting(query, sorts, tableInfo) {
117
+ let result = query;
118
+ for (const sort of sorts) {
119
+ const column = tableInfo.columns.find((c) => c.name === sort.column);
120
+ if (!column) throw new Error(`Column '${sort.column}' not found in table '${tableInfo.name}'`);
121
+ result = result.orderBy(sort.column, sort.direction === require_types.Direction.Asc ? "asc" : "desc");
122
+ }
123
+ return result;
124
+ }
125
+
126
+ //#endregion
127
+ //#region src/data/introspection.ts
128
+ /**
129
+ * Introspects the database schema to discover tables and columns.
130
+ * Uses PostgreSQL information_schema for metadata.
131
+ */
132
+ async function introspectSchema(db, excludeTables) {
133
+ const excludePlaceholders = excludeTables.length > 0 ? excludeTables.map((_, i) => `$${i + 1}`).join(", ") : "''";
134
+ const tablesQuery = `
135
+ SELECT
136
+ table_name,
137
+ table_schema
138
+ FROM information_schema.tables
139
+ WHERE table_schema = 'public'
140
+ AND table_type = 'BASE TABLE'
141
+ ${excludeTables.length > 0 ? `AND table_name NOT IN (${excludePlaceholders})` : ""}
142
+ ORDER BY table_name
143
+ `;
144
+ const tablesResult = await db.executeQuery({
145
+ sql: tablesQuery,
146
+ parameters: excludeTables
147
+ });
148
+ const tables = [];
149
+ for (const row of tablesResult.rows) {
150
+ const tableName = row.table_name ?? row.tableName;
151
+ const tableSchema = row.table_schema ?? row.tableSchema;
152
+ const tableInfo = await introspectTable(db, tableName, tableSchema);
153
+ tables.push(tableInfo);
154
+ }
155
+ return {
156
+ tables,
157
+ updatedAt: /* @__PURE__ */ new Date()
158
+ };
159
+ }
160
+ /**
161
+ * Introspects a single table to get column information.
162
+ */
163
+ async function introspectTable(db, tableName, schema = "public") {
164
+ const columnsQuery = `
165
+ SELECT
166
+ c.column_name,
167
+ c.data_type,
168
+ c.udt_name,
169
+ c.is_nullable,
170
+ c.column_default,
171
+ CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key
172
+ FROM information_schema.columns c
173
+ LEFT JOIN (
174
+ SELECT ku.column_name
175
+ FROM information_schema.table_constraints tc
176
+ JOIN information_schema.key_column_usage ku
177
+ ON tc.constraint_name = ku.constraint_name
178
+ AND tc.table_schema = ku.table_schema
179
+ WHERE tc.table_name = $1
180
+ AND tc.table_schema = $2
181
+ AND tc.constraint_type = 'PRIMARY KEY'
182
+ ) pk ON c.column_name = pk.column_name
183
+ WHERE c.table_name = $1
184
+ AND c.table_schema = $2
185
+ ORDER BY c.ordinal_position
186
+ `;
187
+ const columnsResult = await db.executeQuery({
188
+ sql: columnsQuery,
189
+ parameters: [tableName, schema]
190
+ });
191
+ const fkQuery = `
192
+ SELECT
193
+ kcu.column_name,
194
+ ccu.table_name AS foreign_table,
195
+ ccu.column_name AS foreign_column
196
+ FROM information_schema.table_constraints tc
197
+ JOIN information_schema.key_column_usage kcu
198
+ ON tc.constraint_name = kcu.constraint_name
199
+ AND tc.table_schema = kcu.table_schema
200
+ JOIN information_schema.constraint_column_usage ccu
201
+ ON tc.constraint_name = ccu.constraint_name
202
+ AND tc.table_schema = ccu.table_schema
203
+ WHERE tc.table_name = $1
204
+ AND tc.table_schema = $2
205
+ AND tc.constraint_type = 'FOREIGN KEY'
206
+ `;
207
+ const fkResult = await db.executeQuery({
208
+ sql: fkQuery,
209
+ parameters: [tableName, schema]
210
+ });
211
+ const foreignKeys = /* @__PURE__ */ new Map();
212
+ for (const row of fkResult.rows) {
213
+ const colName = row.column_name ?? row.columnName;
214
+ foreignKeys.set(colName, {
215
+ table: row.foreign_table ?? row.foreignTable,
216
+ column: row.foreign_column ?? row.foreignColumn
217
+ });
218
+ }
219
+ const columns = columnsResult.rows.map((row) => {
220
+ const colName = row.column_name ?? row.columnName;
221
+ const udtName = row.udt_name ?? row.udtName;
222
+ const isNullable = row.is_nullable ?? row.isNullable;
223
+ const isPrimaryKey = row.is_primary_key ?? row.isPrimaryKey;
224
+ const columnDefault = row.column_default ?? row.columnDefault;
225
+ const fk = foreignKeys.get(colName);
226
+ return {
227
+ name: colName,
228
+ type: mapPostgresType(udtName),
229
+ rawType: udtName,
230
+ nullable: isNullable === "YES",
231
+ isPrimaryKey,
232
+ isForeignKey: !!fk,
233
+ foreignKeyTable: fk?.table,
234
+ foreignKeyColumn: fk?.column,
235
+ defaultValue: columnDefault ?? void 0
236
+ };
237
+ });
238
+ const primaryKey = columns.filter((c) => c.isPrimaryKey).map((c) => c.name);
239
+ const countQuery = `
240
+ SELECT reltuples::bigint AS estimate
241
+ FROM pg_class
242
+ WHERE relname = $1
243
+ `;
244
+ let estimatedRowCount;
245
+ try {
246
+ const countResult = await db.executeQuery({
247
+ sql: countQuery,
248
+ parameters: [tableName]
249
+ });
250
+ if (countResult.rows.length > 0) {
251
+ const estimate = countResult.rows[0].estimate;
252
+ estimatedRowCount = estimate > 0 ? Number(estimate) : void 0;
253
+ }
254
+ } catch {}
255
+ return {
256
+ name: tableName,
257
+ schema,
258
+ columns,
259
+ primaryKey,
260
+ estimatedRowCount
261
+ };
262
+ }
263
+ /**
264
+ * Maps PostgreSQL types to generic column types.
265
+ */
266
+ function mapPostgresType(udtName) {
267
+ const typeMap = {
268
+ varchar: "string",
269
+ char: "string",
270
+ text: "string",
271
+ name: "string",
272
+ bpchar: "string",
273
+ int2: "number",
274
+ int4: "number",
275
+ int8: "number",
276
+ float4: "number",
277
+ float8: "number",
278
+ numeric: "number",
279
+ money: "number",
280
+ serial: "number",
281
+ bigserial: "number",
282
+ bool: "boolean",
283
+ date: "date",
284
+ timestamp: "datetime",
285
+ timestamptz: "datetime",
286
+ time: "datetime",
287
+ timetz: "datetime",
288
+ json: "json",
289
+ jsonb: "json",
290
+ bytea: "binary",
291
+ uuid: "uuid"
292
+ };
293
+ return typeMap[udtName] ?? "unknown";
294
+ }
295
+
296
+ //#endregion
297
+ //#region src/data/pagination.ts
298
+ /**
299
+ * Cursor encoding/decoding utilities for pagination.
300
+ */
301
+ /**
302
+ * Encode a cursor value for safe URL transmission.
303
+ * Supports various types: string, number, Date, etc.
304
+ */
305
+ function encodeCursor(value) {
306
+ const payload = {
307
+ v: value instanceof Date ? value.toISOString() : value,
308
+ t: value instanceof Date ? "date" : typeof value
309
+ };
310
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
311
+ }
312
+ /**
313
+ * Decode a cursor string back to its original value.
314
+ */
315
+ function decodeCursor(cursor) {
316
+ try {
317
+ const json = Buffer.from(cursor, "base64url").toString("utf-8");
318
+ const payload = JSON.parse(json);
319
+ if (payload.t === "date") return new Date(payload.v);
320
+ return payload.v;
321
+ } catch {
322
+ throw new Error("Invalid cursor format");
323
+ }
324
+ }
325
+
326
+ //#endregion
327
+ //#region src/data/DataBrowser.ts
328
+ /**
329
+ * Database browser for introspecting and querying PostgreSQL databases.
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * const browser = new DataBrowser({
334
+ * db: kyselyInstance,
335
+ * cursor: { field: 'id', direction: Direction.Desc },
336
+ * });
337
+ *
338
+ * const schema = await browser.getSchema();
339
+ * const result = await browser.query({ table: 'users', pageSize: 20 });
340
+ * ```
341
+ */
342
+ var DataBrowser = class {
343
+ db;
344
+ options;
345
+ schemaCache = null;
346
+ schemaCacheExpiry = 0;
347
+ CACHE_TTL_MS = 60 * 1e3;
348
+ constructor(options) {
349
+ this.db = options.db;
350
+ this.options = options;
351
+ }
352
+ /**
353
+ * Get the database schema information.
354
+ * Results are cached for 1 minute.
355
+ *
356
+ * @param forceRefresh - Force a refresh of the cache
357
+ */
358
+ async getSchema(forceRefresh = false) {
359
+ const now = Date.now();
360
+ if (!forceRefresh && this.schemaCache && now < this.schemaCacheExpiry) return this.schemaCache;
361
+ const schema = await introspectSchema(this.db, this.options.excludeTables);
362
+ this.schemaCache = schema;
363
+ this.schemaCacheExpiry = now + this.CACHE_TTL_MS;
364
+ return schema;
365
+ }
366
+ /**
367
+ * Get information about a specific table.
368
+ */
369
+ async getTableInfo(tableName) {
370
+ const schema = await this.getSchema();
371
+ return schema.tables.find((t) => t.name === tableName) ?? null;
372
+ }
373
+ /**
374
+ * Query table data with pagination, filtering, and sorting.
375
+ */
376
+ async query(options) {
377
+ const tableInfo = await this.getTableInfo(options.table);
378
+ if (!tableInfo) throw new Error(`Table '${options.table}' not found`);
379
+ const cursorConfig = this.getCursorConfig(options.table);
380
+ const pageSize = Math.min(options.pageSize ?? this.options.defaultPageSize, 100);
381
+ let query = this.db.selectFrom(options.table).selectAll();
382
+ if (options.filters && options.filters.length > 0) query = applyFilters(query, options.filters, tableInfo);
383
+ if (options.sort && options.sort.length > 0) query = applySorting(query, options.sort, tableInfo);
384
+ else query = query.orderBy(cursorConfig.field, cursorConfig.direction);
385
+ if (options.cursor) {
386
+ const cursorValue = decodeCursor(options.cursor);
387
+ const operator = cursorConfig.direction === require_types.Direction.Asc ? ">" : "<";
388
+ query = query.where(cursorConfig.field, operator, cursorValue);
389
+ }
390
+ const rows = await query.limit(pageSize + 1).execute();
391
+ const hasMore = rows.length > pageSize;
392
+ const resultRows = hasMore ? rows.slice(0, pageSize) : rows;
393
+ let nextCursor = null;
394
+ let prevCursor = null;
395
+ if (hasMore && resultRows.length > 0) {
396
+ const lastRow = resultRows[resultRows.length - 1];
397
+ nextCursor = encodeCursor(lastRow[cursorConfig.field]);
398
+ }
399
+ if (options.cursor && resultRows.length > 0) {
400
+ const firstRow = resultRows[0];
401
+ prevCursor = encodeCursor(firstRow[cursorConfig.field]);
402
+ }
403
+ return {
404
+ rows: resultRows,
405
+ hasMore,
406
+ nextCursor,
407
+ prevCursor
408
+ };
409
+ }
410
+ /**
411
+ * Get the cursor configuration for a table.
412
+ * Returns the table-specific config if defined, otherwise the default.
413
+ */
414
+ getCursorConfig(tableName) {
415
+ return this.options.tableCursors[tableName] ?? this.options.cursor;
416
+ }
417
+ /**
418
+ * Get the underlying Kysely database instance.
419
+ */
420
+ get database() {
421
+ return this.db;
422
+ }
423
+ };
424
+
425
+ //#endregion
426
+ Object.defineProperty(exports, 'DataBrowser', {
427
+ enumerable: true,
428
+ get: function () {
429
+ return DataBrowser;
430
+ }
431
+ });
432
+ //# sourceMappingURL=DataBrowser-c-Gs6PZB.cjs.map