@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.
- package/dist/DataBrowser-DQ3-ZxdV.mjs +427 -0
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +1 -0
- package/dist/DataBrowser-SOcqmZb2.d.mts +267 -0
- package/dist/DataBrowser-c-Gs6PZB.cjs +432 -0
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +1 -0
- package/dist/DataBrowser-hGwiTffZ.d.cts +267 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/data/index.cjs +4 -0
- package/dist/data/index.d.cts +2 -0
- package/dist/data/index.d.mts +2 -0
- package/dist/data/index.mjs +4 -0
- package/dist/index.cjs +239 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +132 -0
- package/dist/index.d.mts +132 -0
- package/dist/index.mjs +230 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/hono.cjs +192 -0
- package/dist/server/hono.cjs.map +1 -0
- package/dist/server/hono.d.cts +19 -0
- package/dist/server/hono.d.mts +19 -0
- package/dist/server/hono.mjs +191 -0
- package/dist/server/hono.mjs.map +1 -0
- package/dist/types-BZv87Ikv.mjs +31 -0
- package/dist/types-BZv87Ikv.mjs.map +1 -0
- package/dist/types-CMttUZYk.cjs +43 -0
- package/dist/types-CMttUZYk.cjs.map +1 -0
- package/package.json +54 -0
- package/src/Studio.ts +318 -0
- package/src/data/DataBrowser.ts +166 -0
- package/src/data/__tests__/DataBrowser.integration.spec.ts +418 -0
- package/src/data/__tests__/filtering.integration.spec.ts +741 -0
- package/src/data/__tests__/introspection.integration.spec.ts +352 -0
- package/src/data/filtering.ts +191 -0
- package/src/data/index.ts +1 -0
- package/src/data/introspection.ts +220 -0
- package/src/data/pagination.ts +33 -0
- package/src/index.ts +31 -0
- package/src/server/__tests__/hono.integration.spec.ts +361 -0
- package/src/server/hono.ts +225 -0
- package/src/types.ts +278 -0
- package/src/ui-assets.ts +40 -0
- package/tsdown.config.ts +13 -0
- package/ui/index.html +12 -0
- package/ui/node_modules/.bin/browserslist +21 -0
- package/ui/node_modules/.bin/jiti +21 -0
- package/ui/node_modules/.bin/terser +21 -0
- package/ui/node_modules/.bin/tsc +21 -0
- package/ui/node_modules/.bin/tsserver +21 -0
- package/ui/node_modules/.bin/tsx +21 -0
- package/ui/node_modules/.bin/vite +21 -0
- package/ui/package.json +24 -0
- package/ui/src/App.tsx +141 -0
- package/ui/src/api.ts +71 -0
- package/ui/src/components/RowDetail.tsx +113 -0
- package/ui/src/components/TableList.tsx +51 -0
- package/ui/src/components/TableView.tsx +219 -0
- package/ui/src/main.tsx +10 -0
- package/ui/src/styles.css +36 -0
- package/ui/src/types.ts +50 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/tsconfig.json +21 -0
- package/ui/tsconfig.tsbuildinfo +1 -0
- package/ui/vite.config.ts +12 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { Direction, FilterOperator } from "./types-BZv87Ikv.mjs";
|
|
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
|
+
FilterOperator.Eq,
|
|
11
|
+
FilterOperator.Neq,
|
|
12
|
+
FilterOperator.Like,
|
|
13
|
+
FilterOperator.Ilike,
|
|
14
|
+
FilterOperator.In,
|
|
15
|
+
FilterOperator.Nin,
|
|
16
|
+
FilterOperator.IsNull,
|
|
17
|
+
FilterOperator.IsNotNull
|
|
18
|
+
],
|
|
19
|
+
number: [
|
|
20
|
+
FilterOperator.Eq,
|
|
21
|
+
FilterOperator.Neq,
|
|
22
|
+
FilterOperator.Gt,
|
|
23
|
+
FilterOperator.Gte,
|
|
24
|
+
FilterOperator.Lt,
|
|
25
|
+
FilterOperator.Lte,
|
|
26
|
+
FilterOperator.In,
|
|
27
|
+
FilterOperator.Nin,
|
|
28
|
+
FilterOperator.IsNull,
|
|
29
|
+
FilterOperator.IsNotNull
|
|
30
|
+
],
|
|
31
|
+
boolean: [
|
|
32
|
+
FilterOperator.Eq,
|
|
33
|
+
FilterOperator.Neq,
|
|
34
|
+
FilterOperator.IsNull,
|
|
35
|
+
FilterOperator.IsNotNull
|
|
36
|
+
],
|
|
37
|
+
date: [
|
|
38
|
+
FilterOperator.Eq,
|
|
39
|
+
FilterOperator.Neq,
|
|
40
|
+
FilterOperator.Gt,
|
|
41
|
+
FilterOperator.Gte,
|
|
42
|
+
FilterOperator.Lt,
|
|
43
|
+
FilterOperator.Lte,
|
|
44
|
+
FilterOperator.IsNull,
|
|
45
|
+
FilterOperator.IsNotNull
|
|
46
|
+
],
|
|
47
|
+
datetime: [
|
|
48
|
+
FilterOperator.Eq,
|
|
49
|
+
FilterOperator.Neq,
|
|
50
|
+
FilterOperator.Gt,
|
|
51
|
+
FilterOperator.Gte,
|
|
52
|
+
FilterOperator.Lt,
|
|
53
|
+
FilterOperator.Lte,
|
|
54
|
+
FilterOperator.IsNull,
|
|
55
|
+
FilterOperator.IsNotNull
|
|
56
|
+
],
|
|
57
|
+
uuid: [
|
|
58
|
+
FilterOperator.Eq,
|
|
59
|
+
FilterOperator.Neq,
|
|
60
|
+
FilterOperator.In,
|
|
61
|
+
FilterOperator.Nin,
|
|
62
|
+
FilterOperator.IsNull,
|
|
63
|
+
FilterOperator.IsNotNull
|
|
64
|
+
],
|
|
65
|
+
json: [FilterOperator.IsNull, FilterOperator.IsNotNull],
|
|
66
|
+
binary: [FilterOperator.IsNull, FilterOperator.IsNotNull],
|
|
67
|
+
unknown: [
|
|
68
|
+
FilterOperator.Eq,
|
|
69
|
+
FilterOperator.Neq,
|
|
70
|
+
FilterOperator.IsNull,
|
|
71
|
+
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 FilterOperator.Eq: return query.where(column, "=", value);
|
|
99
|
+
case FilterOperator.Neq: return query.where(column, "!=", value);
|
|
100
|
+
case FilterOperator.Gt: return query.where(column, ">", value);
|
|
101
|
+
case FilterOperator.Gte: return query.where(column, ">=", value);
|
|
102
|
+
case FilterOperator.Lt: return query.where(column, "<", value);
|
|
103
|
+
case FilterOperator.Lte: return query.where(column, "<=", value);
|
|
104
|
+
case FilterOperator.Like: return query.where(column, "like", value);
|
|
105
|
+
case FilterOperator.Ilike: return query.where(column, "ilike", value);
|
|
106
|
+
case FilterOperator.In: return query.where(column, "in", value);
|
|
107
|
+
case FilterOperator.Nin: return query.where(column, "not in", value);
|
|
108
|
+
case FilterOperator.IsNull: return query.where(column, "is", null);
|
|
109
|
+
case 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 === 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 === 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
|
+
export { DataBrowser };
|
|
427
|
+
//# sourceMappingURL=DataBrowser-DQ3-ZxdV.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataBrowser-DQ3-ZxdV.mjs","names":["filter: FilterCondition","columnInfo: ColumnInfo","typeCompatibility: Record<string, FilterOperator[]>","query: SelectQueryBuilder<DB, TB, O>","filters: FilterCondition[]","tableInfo: TableInfo","sorts: SortConfig[]","db: Kysely<DB>","excludeTables: string[]","tables: TableInfo[]","tableName: string","columns: ColumnInfo[]","estimatedRowCount: number | undefined","udtName: string","typeMap: Record<string, ColumnType>","value: unknown","cursor: string","options: Required<DataBrowserOptions<DB>>","tableName: string","options: QueryOptions","nextCursor: string | null","prevCursor: string | null"],"sources":["../src/data/filtering.ts","../src/data/introspection.ts","../src/data/pagination.ts","../src/data/DataBrowser.ts"],"sourcesContent":["import type { SelectQueryBuilder } from 'kysely';\nimport {\n type ColumnInfo,\n Direction,\n type FilterCondition,\n FilterOperator,\n type SortConfig,\n type TableInfo,\n} from '../types';\n\n/**\n * Validates that a filter is applicable to the given column.\n */\nexport function validateFilter(\n filter: FilterCondition,\n columnInfo: ColumnInfo,\n): { valid: boolean; error?: string } {\n // Validate operator compatibility with column type\n const typeCompatibility: Record<string, FilterOperator[]> = {\n string: [\n FilterOperator.Eq,\n FilterOperator.Neq,\n FilterOperator.Like,\n FilterOperator.Ilike,\n FilterOperator.In,\n FilterOperator.Nin,\n FilterOperator.IsNull,\n FilterOperator.IsNotNull,\n ],\n number: [\n FilterOperator.Eq,\n FilterOperator.Neq,\n FilterOperator.Gt,\n FilterOperator.Gte,\n FilterOperator.Lt,\n FilterOperator.Lte,\n FilterOperator.In,\n FilterOperator.Nin,\n FilterOperator.IsNull,\n FilterOperator.IsNotNull,\n ],\n boolean: [\n FilterOperator.Eq,\n FilterOperator.Neq,\n FilterOperator.IsNull,\n FilterOperator.IsNotNull,\n ],\n date: [\n FilterOperator.Eq,\n FilterOperator.Neq,\n FilterOperator.Gt,\n FilterOperator.Gte,\n FilterOperator.Lt,\n FilterOperator.Lte,\n FilterOperator.IsNull,\n FilterOperator.IsNotNull,\n ],\n datetime: [\n FilterOperator.Eq,\n FilterOperator.Neq,\n FilterOperator.Gt,\n FilterOperator.Gte,\n FilterOperator.Lt,\n FilterOperator.Lte,\n FilterOperator.IsNull,\n FilterOperator.IsNotNull,\n ],\n uuid: [\n FilterOperator.Eq,\n FilterOperator.Neq,\n FilterOperator.In,\n FilterOperator.Nin,\n FilterOperator.IsNull,\n FilterOperator.IsNotNull,\n ],\n json: [FilterOperator.IsNull, FilterOperator.IsNotNull],\n binary: [FilterOperator.IsNull, FilterOperator.IsNotNull],\n unknown: [\n FilterOperator.Eq,\n FilterOperator.Neq,\n FilterOperator.IsNull,\n FilterOperator.IsNotNull,\n ],\n };\n\n const allowedOps =\n typeCompatibility[columnInfo.type] ?? typeCompatibility.unknown;\n\n if (!allowedOps.includes(filter.operator)) {\n return {\n valid: false,\n error: `Operator '${filter.operator}' not supported for column type '${columnInfo.type}'`,\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Applies filters to a Kysely query builder.\n */\nexport function applyFilters<DB, TB extends keyof DB, O>(\n query: SelectQueryBuilder<DB, TB, O>,\n filters: FilterCondition[],\n tableInfo: TableInfo,\n): SelectQueryBuilder<DB, TB, O> {\n let result = query;\n\n for (const filter of filters) {\n const column = tableInfo.columns.find((c) => c.name === filter.column);\n\n if (!column) {\n throw new Error(\n `Column '${filter.column}' not found in table '${tableInfo.name}'`,\n );\n }\n\n const validation = validateFilter(filter, column);\n if (!validation.valid) {\n throw new Error(validation.error);\n }\n\n result = applyFilterCondition(result, filter);\n }\n\n return result;\n}\n\nfunction applyFilterCondition<DB, TB extends keyof DB, O>(\n query: SelectQueryBuilder<DB, TB, O>,\n filter: FilterCondition,\n): SelectQueryBuilder<DB, TB, O> {\n const { column, operator, value } = filter;\n\n switch (operator) {\n case FilterOperator.Eq:\n return query.where(column as any, '=', value);\n case FilterOperator.Neq:\n return query.where(column as any, '!=', value);\n case FilterOperator.Gt:\n return query.where(column as any, '>', value);\n case FilterOperator.Gte:\n return query.where(column as any, '>=', value);\n case FilterOperator.Lt:\n return query.where(column as any, '<', value);\n case FilterOperator.Lte:\n return query.where(column as any, '<=', value);\n case FilterOperator.Like:\n return query.where(column as any, 'like', value);\n case FilterOperator.Ilike:\n return query.where(column as any, 'ilike', value);\n case FilterOperator.In:\n return query.where(column as any, 'in', value as any[]);\n case FilterOperator.Nin:\n return query.where(column as any, 'not in', value as any[]);\n case FilterOperator.IsNull:\n return query.where(column as any, 'is', null);\n case FilterOperator.IsNotNull:\n return query.where(column as any, 'is not', null);\n default:\n throw new Error(`Unknown filter operator: ${operator}`);\n }\n}\n\n/**\n * Applies sorting to a Kysely query builder.\n */\nexport function applySorting<DB, TB extends keyof DB, O>(\n query: SelectQueryBuilder<DB, TB, O>,\n sorts: SortConfig[],\n tableInfo: TableInfo,\n): SelectQueryBuilder<DB, TB, O> {\n let result = query;\n\n for (const sort of sorts) {\n const column = tableInfo.columns.find((c) => c.name === sort.column);\n\n if (!column) {\n throw new Error(\n `Column '${sort.column}' not found in table '${tableInfo.name}'`,\n );\n }\n\n result = result.orderBy(\n sort.column as any,\n sort.direction === Direction.Asc ? 'asc' : 'desc',\n );\n }\n\n return result;\n}\n","import type { Kysely } from 'kysely';\nimport type { ColumnInfo, ColumnType, SchemaInfo, TableInfo } from '../types';\n\n/**\n * Introspects the database schema to discover tables and columns.\n * Uses PostgreSQL information_schema for metadata.\n */\nexport async function introspectSchema<DB>(\n db: Kysely<DB>,\n excludeTables: string[],\n): Promise<SchemaInfo> {\n // Query tables from information_schema\n const excludePlaceholders =\n excludeTables.length > 0\n ? excludeTables.map((_, i) => `$${i + 1}`).join(', ')\n : \"''\";\n\n const tablesQuery = `\n SELECT\n table_name,\n table_schema\n FROM information_schema.tables\n WHERE table_schema = 'public'\n AND table_type = 'BASE TABLE'\n ${excludeTables.length > 0 ? `AND table_name NOT IN (${excludePlaceholders})` : ''}\n ORDER BY table_name\n `;\n\n const tablesResult = await db.executeQuery({\n sql: tablesQuery,\n parameters: excludeTables,\n } as any);\n\n const tables: TableInfo[] = [];\n\n for (const row of tablesResult.rows as any[]) {\n // Support both snake_case (raw) and camelCase (with CamelCasePlugin)\n const tableName = row.table_name ?? row.tableName;\n const tableSchema = row.table_schema ?? row.tableSchema;\n const tableInfo = await introspectTable(db, tableName, tableSchema);\n tables.push(tableInfo);\n }\n\n return {\n tables,\n updatedAt: new Date(),\n };\n}\n\n/**\n * Introspects a single table to get column information.\n */\nexport async function introspectTable<DB>(\n db: Kysely<DB>,\n tableName: string,\n schema = 'public',\n): Promise<TableInfo> {\n // Query columns\n const columnsQuery = `\n SELECT\n c.column_name,\n c.data_type,\n c.udt_name,\n c.is_nullable,\n c.column_default,\n CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key\n FROM information_schema.columns c\n LEFT JOIN (\n SELECT ku.column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage ku\n ON tc.constraint_name = ku.constraint_name\n AND tc.table_schema = ku.table_schema\n WHERE tc.table_name = $1\n AND tc.table_schema = $2\n AND tc.constraint_type = 'PRIMARY KEY'\n ) pk ON c.column_name = pk.column_name\n WHERE c.table_name = $1\n AND c.table_schema = $2\n ORDER BY c.ordinal_position\n `;\n\n const columnsResult = await db.executeQuery({\n sql: columnsQuery,\n parameters: [tableName, schema],\n } as any);\n\n // Query foreign keys\n const fkQuery = `\n SELECT\n kcu.column_name,\n ccu.table_name AS foreign_table,\n ccu.column_name AS foreign_column\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name\n AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage ccu\n ON tc.constraint_name = ccu.constraint_name\n AND tc.table_schema = ccu.table_schema\n WHERE tc.table_name = $1\n AND tc.table_schema = $2\n AND tc.constraint_type = 'FOREIGN KEY'\n `;\n\n const fkResult = await db.executeQuery({\n sql: fkQuery,\n parameters: [tableName, schema],\n } as any);\n\n const foreignKeys = new Map<string, { table: string; column: string }>();\n for (const row of fkResult.rows as any[]) {\n // Support both snake_case (raw) and camelCase (with CamelCasePlugin)\n const colName = row.column_name ?? row.columnName;\n foreignKeys.set(colName, {\n table: row.foreign_table ?? row.foreignTable,\n column: row.foreign_column ?? row.foreignColumn,\n });\n }\n\n const columns: ColumnInfo[] = (columnsResult.rows as any[]).map((row) => {\n // Support both snake_case (raw) and camelCase (with CamelCasePlugin)\n const colName = row.column_name ?? row.columnName;\n const udtName = row.udt_name ?? row.udtName;\n const isNullable = row.is_nullable ?? row.isNullable;\n const isPrimaryKey = row.is_primary_key ?? row.isPrimaryKey;\n const columnDefault = row.column_default ?? row.columnDefault;\n\n const fk = foreignKeys.get(colName);\n return {\n name: colName,\n type: mapPostgresType(udtName),\n rawType: udtName,\n nullable: isNullable === 'YES',\n isPrimaryKey: isPrimaryKey,\n isForeignKey: !!fk,\n foreignKeyTable: fk?.table,\n foreignKeyColumn: fk?.column,\n defaultValue: columnDefault ?? undefined,\n };\n });\n\n const primaryKey = columns.filter((c) => c.isPrimaryKey).map((c) => c.name);\n\n // Get estimated row count\n const countQuery = `\n SELECT reltuples::bigint AS estimate\n FROM pg_class\n WHERE relname = $1\n `;\n\n let estimatedRowCount: number | undefined;\n try {\n const countResult = await db.executeQuery({\n sql: countQuery,\n parameters: [tableName],\n } as any);\n if (countResult.rows.length > 0) {\n const estimate = (countResult.rows[0] as any).estimate;\n estimatedRowCount = estimate > 0 ? Number(estimate) : undefined;\n }\n } catch {\n // Ignore errors, row count is optional\n }\n\n return {\n name: tableName,\n schema,\n columns,\n primaryKey,\n estimatedRowCount,\n };\n}\n\n/**\n * Maps PostgreSQL types to generic column types.\n */\nfunction mapPostgresType(udtName: string): ColumnType {\n const typeMap: Record<string, ColumnType> = {\n // Strings\n varchar: 'string',\n char: 'string',\n text: 'string',\n name: 'string',\n bpchar: 'string',\n\n // Numbers\n int2: 'number',\n int4: 'number',\n int8: 'number',\n float4: 'number',\n float8: 'number',\n numeric: 'number',\n money: 'number',\n serial: 'number',\n bigserial: 'number',\n\n // Boolean\n bool: 'boolean',\n\n // Dates\n date: 'date',\n timestamp: 'datetime',\n timestamptz: 'datetime',\n time: 'datetime',\n timetz: 'datetime',\n\n // JSON\n json: 'json',\n jsonb: 'json',\n\n // Binary\n bytea: 'binary',\n\n // UUID\n uuid: 'uuid',\n };\n\n return typeMap[udtName] ?? 'unknown';\n}\n","/**\n * Cursor encoding/decoding utilities for pagination.\n */\n\n/**\n * Encode a cursor value for safe URL transmission.\n * Supports various types: string, number, Date, etc.\n */\nexport function encodeCursor(value: unknown): string {\n const payload = {\n v: value instanceof Date ? value.toISOString() : value,\n t: value instanceof Date ? 'date' : typeof value,\n };\n return Buffer.from(JSON.stringify(payload)).toString('base64url');\n}\n\n/**\n * Decode a cursor string back to its original value.\n */\nexport function decodeCursor(cursor: string): unknown {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf-8');\n const payload = JSON.parse(json);\n\n if (payload.t === 'date') {\n return new Date(payload.v);\n }\n\n return payload.v;\n } catch {\n throw new Error('Invalid cursor format');\n }\n}\n","import type { Kysely, SelectQueryBuilder } from 'kysely';\nimport {\n type CursorConfig,\n type DataBrowserOptions,\n Direction,\n type QueryOptions,\n type QueryResult,\n type SchemaInfo,\n type TableInfo,\n} from '../types';\nimport { applyFilters, applySorting } from './filtering';\nimport { introspectSchema } from './introspection';\nimport { decodeCursor, encodeCursor } from './pagination';\n\n/**\n * Database browser for introspecting and querying PostgreSQL databases.\n *\n * @example\n * ```typescript\n * const browser = new DataBrowser({\n * db: kyselyInstance,\n * cursor: { field: 'id', direction: Direction.Desc },\n * });\n *\n * const schema = await browser.getSchema();\n * const result = await browser.query({ table: 'users', pageSize: 20 });\n * ```\n */\nexport class DataBrowser<DB = unknown> {\n private db: Kysely<DB>;\n private options: Required<DataBrowserOptions<DB>>;\n private schemaCache: SchemaInfo | null = null;\n private schemaCacheExpiry = 0;\n private readonly CACHE_TTL_MS = 60 * 1000; // 1 minute\n\n constructor(options: Required<DataBrowserOptions<DB>>) {\n this.db = options.db;\n this.options = options;\n }\n\n // ============================================\n // Schema Introspection\n // ============================================\n\n /**\n * Get the database schema information.\n * Results are cached for 1 minute.\n *\n * @param forceRefresh - Force a refresh of the cache\n */\n async getSchema(forceRefresh = false): Promise<SchemaInfo> {\n const now = Date.now();\n\n if (!forceRefresh && this.schemaCache && now < this.schemaCacheExpiry) {\n return this.schemaCache;\n }\n\n const schema = await introspectSchema(this.db, this.options.excludeTables);\n\n this.schemaCache = schema;\n this.schemaCacheExpiry = now + this.CACHE_TTL_MS;\n\n return schema;\n }\n\n /**\n * Get information about a specific table.\n */\n async getTableInfo(tableName: string): Promise<TableInfo | null> {\n const schema = await this.getSchema();\n return schema.tables.find((t) => t.name === tableName) ?? null;\n }\n\n // ============================================\n // Data Querying\n // ============================================\n\n /**\n * Query table data with pagination, filtering, and sorting.\n */\n async query(options: QueryOptions): Promise<QueryResult> {\n const tableInfo = await this.getTableInfo(options.table);\n\n if (!tableInfo) {\n throw new Error(`Table '${options.table}' not found`);\n }\n\n const cursorConfig = this.getCursorConfig(options.table);\n const pageSize = Math.min(\n options.pageSize ?? this.options.defaultPageSize,\n 100,\n );\n\n // Build base query selecting all columns\n let query = this.db\n .selectFrom(options.table as any)\n .selectAll() as SelectQueryBuilder<any, any, any>;\n\n // Apply filters if provided\n if (options.filters && options.filters.length > 0) {\n query = applyFilters(query, options.filters, tableInfo);\n }\n\n // Apply sorting if provided, otherwise use cursor field\n if (options.sort && options.sort.length > 0) {\n query = applySorting(query, options.sort, tableInfo);\n } else {\n query = query.orderBy(cursorConfig.field as any, cursorConfig.direction);\n }\n\n // Handle cursor-based pagination\n if (options.cursor) {\n const cursorValue = decodeCursor(options.cursor);\n const operator = cursorConfig.direction === Direction.Asc ? '>' : '<';\n query = query.where(cursorConfig.field as any, operator, cursorValue);\n }\n\n // Fetch one extra row to determine if there are more results\n const rows = await query.limit(pageSize + 1).execute();\n\n const hasMore = rows.length > pageSize;\n const resultRows = hasMore ? rows.slice(0, pageSize) : rows;\n\n // Generate cursors\n let nextCursor: string | null = null;\n let prevCursor: string | null = null;\n\n if (hasMore && resultRows.length > 0) {\n const lastRow = resultRows[resultRows.length - 1];\n nextCursor = encodeCursor(lastRow[cursorConfig.field]);\n }\n\n // For prev cursor, we need to know if there are previous results\n // This would require a separate query, so we'll only set it if there's an input cursor\n if (options.cursor && resultRows.length > 0) {\n const firstRow = resultRows[0];\n prevCursor = encodeCursor(firstRow[cursorConfig.field]);\n }\n\n return {\n rows: resultRows,\n hasMore,\n nextCursor,\n prevCursor,\n };\n }\n\n // ============================================\n // Configuration Access\n // ============================================\n\n /**\n * Get the cursor configuration for a table.\n * Returns the table-specific config if defined, otherwise the default.\n */\n getCursorConfig(tableName: string): CursorConfig {\n return this.options.tableCursors[tableName] ?? this.options.cursor;\n }\n\n /**\n * Get the underlying Kysely database instance.\n */\n get database(): Kysely<DB> {\n return this.db;\n }\n}\n"],"mappings":"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"}
|