datapeek 0.1.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.
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+ import {
4
+ connect,
5
+ disconnect,
6
+ executeQuery,
7
+ getConnection,
8
+ testConnection
9
+ } from "./chunk-4IGBRCNN.js";
10
+
11
+ // src/cli/index.ts
12
+ import { Command } from "commander";
13
+
14
+ // src/server/index.ts
15
+ import express from "express";
16
+ import cors from "cors";
17
+ import path from "path";
18
+ import { fileURLToPath } from "url";
19
+
20
+ // src/server/routes/connection.ts
21
+ import { Router } from "express";
22
+ var connectionRoutes = Router();
23
+ connectionRoutes.post("/test", async (req, res) => {
24
+ const timeout = setTimeout(() => {
25
+ if (!res.headersSent) {
26
+ res.status(408).json({
27
+ success: false,
28
+ message: "Connection test timeout"
29
+ });
30
+ }
31
+ }, 1e4);
32
+ try {
33
+ const config = req.body;
34
+ await testConnection(config);
35
+ clearTimeout(timeout);
36
+ if (!res.headersSent) {
37
+ res.json({ success: true, message: "Connection successful" });
38
+ }
39
+ } catch (error) {
40
+ clearTimeout(timeout);
41
+ if (!res.headersSent) {
42
+ res.status(400).json({
43
+ success: false,
44
+ message: error.message || "Connection failed"
45
+ });
46
+ }
47
+ }
48
+ });
49
+ connectionRoutes.get("/provided", (req, res) => {
50
+ const connString = getProvidedConnectionString();
51
+ res.json({ connectionString: connString || null });
52
+ });
53
+ connectionRoutes.post("/", async (req, res) => {
54
+ const timeout = setTimeout(() => {
55
+ if (!res.headersSent) {
56
+ res.status(408).json({
57
+ success: false,
58
+ message: "Connection timeout"
59
+ });
60
+ }
61
+ }, 15e3);
62
+ try {
63
+ const config = req.body;
64
+ await connect(config);
65
+ clearTimeout(timeout);
66
+ if (!res.headersSent) {
67
+ res.json({ success: true, message: "Connected" });
68
+ }
69
+ } catch (error) {
70
+ clearTimeout(timeout);
71
+ if (!res.headersSent) {
72
+ res.status(400).json({
73
+ success: false,
74
+ message: error.message || "Connection failed"
75
+ });
76
+ }
77
+ }
78
+ });
79
+ connectionRoutes.delete("/", async (req, res) => {
80
+ try {
81
+ await disconnect();
82
+ res.json({ success: true, message: "Disconnected" });
83
+ } catch (error) {
84
+ res.status(500).json({
85
+ success: false,
86
+ message: error.message || "Disconnect failed"
87
+ });
88
+ }
89
+ });
90
+ connectionRoutes.get("/status", async (req, res) => {
91
+ try {
92
+ const { getConnection: getConnection2, executeQuery: executeQuery2 } = await import("./mssql-4KECNFPA.js");
93
+ const pool = getConnection2();
94
+ if (pool && pool.connected) {
95
+ try {
96
+ const result = await executeQuery2("SELECT DB_NAME() as databaseName");
97
+ const databaseName = result[0]?.databaseName || null;
98
+ res.json({ connected: true, databaseName });
99
+ } catch {
100
+ res.json({ connected: true, databaseName: null });
101
+ }
102
+ } else {
103
+ res.json({ connected: false });
104
+ }
105
+ } catch {
106
+ res.json({ connected: false });
107
+ }
108
+ });
109
+
110
+ // src/server/routes/tables.ts
111
+ import { Router as Router2 } from "express";
112
+ import sql from "mssql";
113
+ var tableRoutes = Router2();
114
+ tableRoutes.get("/", async (req, res) => {
115
+ try {
116
+ const pool = getConnection();
117
+ if (!pool || !pool.connected) {
118
+ return res.status(400).json({ error: "Not connected to database" });
119
+ }
120
+ const query = `
121
+ SELECT
122
+ TABLE_SCHEMA as schemaName,
123
+ TABLE_NAME as tableName
124
+ FROM INFORMATION_SCHEMA.TABLES
125
+ WHERE TABLE_TYPE = 'BASE TABLE'
126
+ ORDER BY TABLE_SCHEMA, TABLE_NAME
127
+ `;
128
+ const result = await executeQuery(query);
129
+ res.json(result);
130
+ } catch (error) {
131
+ res.status(500).json({ error: error.message || "Failed to fetch tables" });
132
+ }
133
+ });
134
+ tableRoutes.get("/:schema/:table", async (req, res) => {
135
+ try {
136
+ const { schema, table } = req.params;
137
+ const pool = getConnection();
138
+ if (!pool || !pool.connected) {
139
+ return res.status(400).json({ error: "Not connected to database" });
140
+ }
141
+ const query = `
142
+ SELECT
143
+ c.COLUMN_NAME as columnName,
144
+ c.DATA_TYPE as dataType,
145
+ c.CHARACTER_MAXIMUM_LENGTH as maxLength,
146
+ c.IS_NULLABLE as isNullable,
147
+ c.COLUMN_DEFAULT as defaultValue,
148
+ CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END as isPrimaryKey
149
+ FROM INFORMATION_SCHEMA.COLUMNS c
150
+ LEFT JOIN (
151
+ SELECT ku.TABLE_SCHEMA, ku.TABLE_NAME, ku.COLUMN_NAME
152
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
153
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
154
+ ON tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
155
+ AND tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
156
+ ) pk ON c.TABLE_SCHEMA = pk.TABLE_SCHEMA
157
+ AND c.TABLE_NAME = pk.TABLE_NAME
158
+ AND c.COLUMN_NAME = pk.COLUMN_NAME
159
+ WHERE c.TABLE_SCHEMA = @schema
160
+ AND c.TABLE_NAME = @table
161
+ ORDER BY c.ORDINAL_POSITION
162
+ `;
163
+ const result = await executeQuery(query, [
164
+ { name: "schema", value: schema, type: sql.NVarChar },
165
+ { name: "table", value: table, type: sql.NVarChar }
166
+ ]);
167
+ res.json(result);
168
+ } catch (error) {
169
+ res.status(500).json({ error: error.message || "Failed to fetch table structure" });
170
+ }
171
+ });
172
+ tableRoutes.get("/:schema/:table/data", async (req, res) => {
173
+ try {
174
+ const { schema, table } = req.params;
175
+ const page = parseInt(req.query.page) || 1;
176
+ const pageSize = Math.min(parseInt(req.query.pageSize) || 100, 1e3);
177
+ const sortColumn = req.query.sortColumn;
178
+ const sortDirection = req.query.sortDirection || "asc";
179
+ const offset = (page - 1) * pageSize;
180
+ const pool = getConnection();
181
+ if (!pool || !pool.connected) {
182
+ return res.status(400).json({ error: "Not connected to database" });
183
+ }
184
+ const countQuery = `SELECT COUNT(*) as total FROM [${schema}].[${table}]`;
185
+ const countResult = await executeQuery(countQuery);
186
+ const total = countResult[0]?.total || 0;
187
+ let orderByColumn = sortColumn || "";
188
+ let orderByDirection = sortDirection?.toUpperCase() === "DESC" ? "DESC" : "ASC";
189
+ if (orderByColumn) {
190
+ try {
191
+ const validateQuery = `
192
+ SELECT COLUMN_NAME
193
+ FROM INFORMATION_SCHEMA.COLUMNS
194
+ WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table AND COLUMN_NAME = @column
195
+ `;
196
+ const validateResult = await executeQuery(validateQuery, [
197
+ { name: "schema", value: schema, type: sql.NVarChar },
198
+ { name: "table", value: table, type: sql.NVarChar },
199
+ { name: "column", value: orderByColumn, type: sql.NVarChar }
200
+ ]);
201
+ if (validateResult.length === 0) {
202
+ orderByColumn = "";
203
+ }
204
+ } catch (e) {
205
+ orderByColumn = "";
206
+ }
207
+ }
208
+ if (!orderByColumn) {
209
+ try {
210
+ const structureQuery = `
211
+ SELECT TOP 1 COLUMN_NAME
212
+ FROM INFORMATION_SCHEMA.COLUMNS
213
+ WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table
214
+ ORDER BY ORDINAL_POSITION
215
+ `;
216
+ const structureResult = await executeQuery(structureQuery, [
217
+ { name: "schema", value: schema, type: sql.NVarChar },
218
+ { name: "table", value: table, type: sql.NVarChar }
219
+ ]);
220
+ if (structureResult.length > 0) {
221
+ orderByColumn = structureResult[0].COLUMN_NAME;
222
+ }
223
+ } catch (e) {
224
+ }
225
+ }
226
+ let data;
227
+ let generatedQuery = "";
228
+ if (orderByColumn) {
229
+ generatedQuery = `SELECT * FROM [${schema}].[${table}]
230
+ ORDER BY [${orderByColumn}] ${orderByDirection}
231
+ OFFSET ${offset} ROWS
232
+ FETCH NEXT ${pageSize} ROWS ONLY`;
233
+ const dataQuery = `
234
+ SELECT * FROM [${schema}].[${table}]
235
+ ORDER BY [${orderByColumn}] ${orderByDirection}
236
+ OFFSET @offset ROWS
237
+ FETCH NEXT @pageSize ROWS ONLY
238
+ `;
239
+ data = await executeQuery(dataQuery, [
240
+ { name: "offset", value: offset, type: sql.Int },
241
+ { name: "pageSize", value: pageSize, type: sql.Int }
242
+ ]);
243
+ } else {
244
+ generatedQuery = `SELECT * FROM (
245
+ SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as rn
246
+ FROM [${schema}].[${table}]
247
+ ) t
248
+ WHERE rn > ${offset} AND rn <= ${offset + pageSize}
249
+ ORDER BY rn`;
250
+ const dataQuery = `
251
+ SELECT * FROM (
252
+ SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as rn
253
+ FROM [${schema}].[${table}]
254
+ ) t
255
+ WHERE rn > @offset AND rn <= @offset + @pageSize
256
+ ORDER BY rn
257
+ `;
258
+ data = await executeQuery(dataQuery, [
259
+ { name: "offset", value: offset, type: sql.Int },
260
+ { name: "pageSize", value: pageSize, type: sql.Int }
261
+ ]);
262
+ data = data.map((row) => {
263
+ const { rn, ...rest } = row;
264
+ return rest;
265
+ });
266
+ }
267
+ res.json({
268
+ data,
269
+ query: generatedQuery,
270
+ pagination: {
271
+ page,
272
+ pageSize,
273
+ total,
274
+ totalPages: Math.ceil(total / pageSize)
275
+ }
276
+ });
277
+ } catch (error) {
278
+ console.error("Error fetching table data:", error);
279
+ const errorMessage = error.message || "Failed to fetch table data";
280
+ const errorDetails = error.originalError?.message || error.originalError?.info?.message || "";
281
+ res.status(500).json({
282
+ error: errorMessage,
283
+ details: errorDetails
284
+ });
285
+ }
286
+ });
287
+
288
+ // src/server/routes/query.ts
289
+ import { Router as Router3 } from "express";
290
+ var queryRoutes = Router3();
291
+ queryRoutes.post("/", async (req, res) => {
292
+ try {
293
+ const { query: sqlQuery } = req.body;
294
+ if (!sqlQuery || typeof sqlQuery !== "string") {
295
+ return res.status(400).json({ error: "Query is required" });
296
+ }
297
+ const trimmedQuery = sqlQuery.trim().toUpperCase();
298
+ if (!trimmedQuery.startsWith("SELECT")) {
299
+ return res.status(400).json({
300
+ error: "Only SELECT queries are allowed (read-only mode)"
301
+ });
302
+ }
303
+ const dangerousKeywords = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "TRUNCATE", "EXEC", "EXECUTE"];
304
+ const hasDangerousKeyword = dangerousKeywords.some(
305
+ (keyword) => trimmedQuery.includes(keyword)
306
+ );
307
+ if (hasDangerousKeyword) {
308
+ return res.status(400).json({
309
+ error: "Query contains prohibited keywords. Only SELECT queries are allowed."
310
+ });
311
+ }
312
+ const result = await executeQuery(sqlQuery);
313
+ res.json({ data: result });
314
+ } catch (error) {
315
+ res.status(500).json({
316
+ error: error.message || "Query execution failed",
317
+ details: error.originalError?.message
318
+ });
319
+ }
320
+ });
321
+
322
+ // src/server/index.ts
323
+ var __filename = fileURLToPath(import.meta.url);
324
+ var __dirname = path.dirname(__filename);
325
+ var providedConnectionString;
326
+ async function startServer(port, connectionString) {
327
+ providedConnectionString = connectionString;
328
+ const app = express();
329
+ app.use(cors());
330
+ app.use(express.json());
331
+ app.use("/api/connect", connectionRoutes);
332
+ app.use("/api/tables", tableRoutes);
333
+ app.use("/api/query", queryRoutes);
334
+ if (process.env.NODE_ENV === "production") {
335
+ const clientDist = path.join(__dirname, "../client");
336
+ app.use(express.static(clientDist));
337
+ app.get("*", (req, res) => {
338
+ if (req.path.startsWith("/api")) {
339
+ return res.status(404).json({ error: "Not found" });
340
+ }
341
+ res.sendFile(path.join(clientDist, "index.html"));
342
+ });
343
+ }
344
+ app.get("/api/health", (req, res) => {
345
+ res.json({ status: "ok", hasConnectionString: !!providedConnectionString });
346
+ });
347
+ return new Promise((resolve, reject) => {
348
+ const server = app.listen(port, () => {
349
+ resolve(app);
350
+ });
351
+ server.on("error", (error) => {
352
+ if (error.code === "EADDRINUSE") {
353
+ reject(new Error(`Port ${port} is already in use`));
354
+ } else {
355
+ reject(error);
356
+ }
357
+ });
358
+ });
359
+ }
360
+ function getProvidedConnectionString() {
361
+ return providedConnectionString;
362
+ }
363
+
364
+ // src/cli/index.ts
365
+ import open from "open";
366
+ var program = new Command();
367
+ program.name("datapeek").description("A local SQL database browser").version("0.1.0").argument("[connectionString]", "SQL Server connection string").option("-p, --port <port>", "Port to run the server on", "4983").option("--no-open", "Do not open browser automatically").action(async (connectionString, options) => {
368
+ const port = parseInt(options?.port || "4983", 10);
369
+ try {
370
+ const server = await startServer(port, connectionString);
371
+ const url = `http://localhost:${port}`;
372
+ console.log(`
373
+ \u{1F680} Datapeek server running at ${url}`);
374
+ if (connectionString) {
375
+ console.log(`\u{1F4CA} Connection string provided: ${connectionString.substring(0, 20)}...`);
376
+ }
377
+ console.log(`
378
+ Press Ctrl+C to stop
379
+ `);
380
+ if (options?.open !== false) {
381
+ await open(url);
382
+ }
383
+ } catch (error) {
384
+ console.error("Failed to start server:", error);
385
+ process.exit(1);
386
+ }
387
+ });
388
+ program.parse();
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ connect,
4
+ disconnect,
5
+ executeQuery,
6
+ getConnection,
7
+ parseConnectionString,
8
+ testConnection
9
+ } from "./chunk-IPR5JXB5.js";
10
+ export {
11
+ connect,
12
+ disconnect,
13
+ executeQuery,
14
+ getConnection,
15
+ parseConnectionString,
16
+ testConnection
17
+ };
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ connect,
4
+ disconnect,
5
+ executeQuery,
6
+ getConnection,
7
+ parseConnectionString,
8
+ testConnection
9
+ } from "./chunk-4IGBRCNN.js";
10
+ export {
11
+ connect,
12
+ disconnect,
13
+ executeQuery,
14
+ getConnection,
15
+ parseConnectionString,
16
+ testConnection
17
+ };