@geekmidas/telescope 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 (103) hide show
  1. package/README.md +521 -0
  2. package/dist/Telescope-B3Wd82yk.cjs +602 -0
  3. package/dist/Telescope-B3Wd82yk.cjs.map +1 -0
  4. package/dist/Telescope-C5dyDYYB.d.cts +133 -0
  5. package/dist/Telescope-D-uoZB6b.mjs +596 -0
  6. package/dist/Telescope-D-uoZB6b.mjs.map +1 -0
  7. package/dist/Telescope-DyIWgh9-.d.mts +133 -0
  8. package/dist/Telescope.cjs +3 -0
  9. package/dist/Telescope.d.cts +3 -0
  10. package/dist/Telescope.d.mts +3 -0
  11. package/dist/Telescope.mjs +3 -0
  12. package/dist/chunk-CUT6urMc.cjs +30 -0
  13. package/dist/index.cjs +5 -0
  14. package/dist/index.d.cts +4 -0
  15. package/dist/index.d.mts +4 -0
  16. package/dist/index.mjs +4 -0
  17. package/dist/logger/console.cjs +161 -0
  18. package/dist/logger/console.cjs.map +1 -0
  19. package/dist/logger/console.d.cts +109 -0
  20. package/dist/logger/console.d.mts +109 -0
  21. package/dist/logger/console.mjs +159 -0
  22. package/dist/logger/console.mjs.map +1 -0
  23. package/dist/logger/pino.cjs +118 -0
  24. package/dist/logger/pino.cjs.map +1 -0
  25. package/dist/logger/pino.d.cts +89 -0
  26. package/dist/logger/pino.d.mts +89 -0
  27. package/dist/logger/pino.mjs +116 -0
  28. package/dist/logger/pino.mjs.map +1 -0
  29. package/dist/memory-9-B9WACq.cjs +110 -0
  30. package/dist/memory-9-B9WACq.cjs.map +1 -0
  31. package/dist/memory-Cm0eevCS.d.mts +38 -0
  32. package/dist/memory-DiP1a-pp.d.cts +38 -0
  33. package/dist/memory-SdN5vtG9.mjs +104 -0
  34. package/dist/memory-SdN5vtG9.mjs.map +1 -0
  35. package/dist/server/hono.cjs +180 -0
  36. package/dist/server/hono.cjs.map +1 -0
  37. package/dist/server/hono.d.cts +26 -0
  38. package/dist/server/hono.d.mts +26 -0
  39. package/dist/server/hono.mjs +176 -0
  40. package/dist/server/hono.mjs.map +1 -0
  41. package/dist/storage/kysely.cjs +336 -0
  42. package/dist/storage/kysely.cjs.map +1 -0
  43. package/dist/storage/kysely.d.cts +161 -0
  44. package/dist/storage/kysely.d.mts +161 -0
  45. package/dist/storage/kysely.mjs +334 -0
  46. package/dist/storage/kysely.mjs.map +1 -0
  47. package/dist/storage/memory.cjs +3 -0
  48. package/dist/storage/memory.d.cts +3 -0
  49. package/dist/storage/memory.d.mts +3 -0
  50. package/dist/storage/memory.mjs +3 -0
  51. package/dist/types-BGDhFv4R.d.cts +170 -0
  52. package/dist/types-CZbzz8kx.d.mts +170 -0
  53. package/dist/types.cjs +0 -0
  54. package/dist/types.d.cts +2 -0
  55. package/dist/types.d.mts +2 -0
  56. package/dist/types.mjs +0 -0
  57. package/dist/ui-assets-D6-8TAr_.mjs +30 -0
  58. package/dist/ui-assets-D6-8TAr_.mjs.map +1 -0
  59. package/dist/ui-assets-ulevVble.cjs +48 -0
  60. package/dist/ui-assets-ulevVble.cjs.map +1 -0
  61. package/dist/ui-assets.cjs +5 -0
  62. package/dist/ui-assets.d.cts +12 -0
  63. package/dist/ui-assets.d.mts +12 -0
  64. package/dist/ui-assets.mjs +3 -0
  65. package/package.json +83 -0
  66. package/scripts/embed-ui.ts +90 -0
  67. package/src/Telescope.ts +714 -0
  68. package/src/__tests__/Telescope.spec.ts +356 -0
  69. package/src/index.ts +23 -0
  70. package/src/logger/__tests__/console.spec.ts +266 -0
  71. package/src/logger/__tests__/pino.spec.ts +217 -0
  72. package/src/logger/console.ts +230 -0
  73. package/src/logger/pino.ts +191 -0
  74. package/src/server/__tests__/hono.spec.ts +340 -0
  75. package/src/server/hono.ts +247 -0
  76. package/src/storage/__tests__/kysely.spec.ts +715 -0
  77. package/src/storage/__tests__/memory.spec.ts +411 -0
  78. package/src/storage/kysely.ts +572 -0
  79. package/src/storage/memory.ts +168 -0
  80. package/src/types.ts +188 -0
  81. package/src/ui-assets.ts +40 -0
  82. package/ui/index.html +12 -0
  83. package/ui/node_modules/.bin/browserslist +21 -0
  84. package/ui/node_modules/.bin/jiti +21 -0
  85. package/ui/node_modules/.bin/terser +21 -0
  86. package/ui/node_modules/.bin/tsc +21 -0
  87. package/ui/node_modules/.bin/tsserver +21 -0
  88. package/ui/node_modules/.bin/tsx +21 -0
  89. package/ui/node_modules/.bin/vite +21 -0
  90. package/ui/package.json +24 -0
  91. package/ui/src/App.tsx +342 -0
  92. package/ui/src/api.ts +75 -0
  93. package/ui/src/components/ExceptionDetail.tsx +100 -0
  94. package/ui/src/components/LogDetail.tsx +91 -0
  95. package/ui/src/components/RequestDetail.tsx +143 -0
  96. package/ui/src/main.tsx +10 -0
  97. package/ui/src/styles.css +10 -0
  98. package/ui/src/types.ts +63 -0
  99. package/ui/src/vite-env.d.ts +1 -0
  100. package/ui/src/vite-plugin-gkm-config.ts +54 -0
  101. package/ui/tsconfig.json +20 -0
  102. package/ui/tsconfig.tsbuildinfo +14 -0
  103. package/ui/vite.config.ts +13 -0
@@ -0,0 +1,176 @@
1
+ import { getAsset, getIndexHtml } from "../ui-assets-D6-8TAr_.mjs";
2
+ import { Hono } from "hono";
3
+
4
+ //#region src/server/hono.ts
5
+ const CONTEXT_KEY = "telescope-request-id";
6
+ /**
7
+ * Create Hono middleware that captures requests and responses
8
+ */
9
+ function createMiddleware(telescope) {
10
+ return async (c, next) => {
11
+ if (!telescope.enabled) return next();
12
+ if (telescope.shouldIgnore(c.req.path)) return next();
13
+ const startTime = performance.now();
14
+ const headers = {};
15
+ c.req.raw.headers.forEach((value, key) => {
16
+ headers[key] = value;
17
+ });
18
+ const url = new URL(c.req.url);
19
+ const query = {};
20
+ url.searchParams.forEach((value, key) => {
21
+ query[key] = value;
22
+ });
23
+ let body;
24
+ if (telescope.recordBody && [
25
+ "POST",
26
+ "PUT",
27
+ "PATCH"
28
+ ].includes(c.req.method)) try {
29
+ const contentType = c.req.header("content-type") || "";
30
+ if (contentType.includes("application/json")) body = await c.req.json();
31
+ else if (contentType.includes("application/x-www-form-urlencoded")) {
32
+ const formData = await c.req.formData();
33
+ body = Object.fromEntries(formData.entries());
34
+ } else if (contentType.includes("text/")) body = await c.req.text();
35
+ } catch {}
36
+ const ip = c.req.header("x-forwarded-for") || c.req.header("x-real-ip");
37
+ try {
38
+ await next();
39
+ const duration = performance.now() - startTime;
40
+ const responseHeaders = {};
41
+ c.res.headers.forEach((value, key) => {
42
+ responseHeaders[key] = value;
43
+ });
44
+ let responseBody;
45
+ if (telescope.recordBody) try {
46
+ const contentType = c.res.headers.get("content-type") || "";
47
+ if (contentType.includes("application/json")) {
48
+ const cloned = c.res.clone();
49
+ responseBody = await cloned.json();
50
+ }
51
+ } catch {}
52
+ const requestId = await telescope.recordRequest({
53
+ method: c.req.method,
54
+ path: c.req.path,
55
+ url: c.req.url,
56
+ headers,
57
+ body,
58
+ query,
59
+ status: c.res.status,
60
+ responseHeaders,
61
+ responseBody,
62
+ duration,
63
+ ip
64
+ });
65
+ c.set(CONTEXT_KEY, requestId);
66
+ } catch (error) {
67
+ await telescope.exception(error);
68
+ throw error;
69
+ }
70
+ };
71
+ }
72
+ /**
73
+ * Parse query options from Hono context
74
+ */
75
+ function parseQueryOptions(c) {
76
+ const limit = parseInt(c.req.query("limit") || "50", 10);
77
+ const offset = parseInt(c.req.query("offset") || "0", 10);
78
+ const search = c.req.query("search");
79
+ const before = c.req.query("before");
80
+ const after = c.req.query("after");
81
+ const tags = c.req.query("tags")?.split(",").filter(Boolean);
82
+ return {
83
+ limit: Math.min(limit, 100),
84
+ offset,
85
+ search,
86
+ before: before ? new Date(before) : void 0,
87
+ after: after ? new Date(after) : void 0,
88
+ tags
89
+ };
90
+ }
91
+ /**
92
+ * Create Hono app with dashboard UI and API routes
93
+ */
94
+ function createUI(telescope) {
95
+ const app = new Hono();
96
+ app.get("/api/requests", async (c) => {
97
+ const options = parseQueryOptions(c);
98
+ const requests = await telescope.getRequests(options);
99
+ return c.json(requests);
100
+ });
101
+ app.get("/api/requests/:id", async (c) => {
102
+ const request = await telescope.getRequest(c.req.param("id"));
103
+ if (!request) return c.json({ error: "Request not found" }, 404);
104
+ return c.json(request);
105
+ });
106
+ app.get("/api/exceptions", async (c) => {
107
+ const options = parseQueryOptions(c);
108
+ const exceptions = await telescope.getExceptions(options);
109
+ return c.json(exceptions);
110
+ });
111
+ app.get("/api/exceptions/:id", async (c) => {
112
+ const exception = await telescope.getException(c.req.param("id"));
113
+ if (!exception) return c.json({ error: "Exception not found" }, 404);
114
+ return c.json(exception);
115
+ });
116
+ app.get("/api/logs", async (c) => {
117
+ const options = parseQueryOptions(c);
118
+ const logs = await telescope.getLogs(options);
119
+ return c.json(logs);
120
+ });
121
+ app.get("/api/stats", async (c) => {
122
+ const stats = await telescope.getStats();
123
+ return c.json(stats);
124
+ });
125
+ app.get("/assets/:filename", (c) => {
126
+ const filename = c.req.param("filename");
127
+ const assetPath = `assets/${filename}`;
128
+ const asset = getAsset(assetPath);
129
+ if (asset) return c.body(asset.content, 200, {
130
+ "Content-Type": asset.contentType,
131
+ "Cache-Control": "public, max-age=31536000, immutable"
132
+ });
133
+ return c.notFound();
134
+ });
135
+ app.get("/", (c) => {
136
+ const html = getIndexHtml();
137
+ if (html) return c.html(html);
138
+ return c.html(telescope.getDashboardHtml());
139
+ });
140
+ app.get("/*", (c) => {
141
+ const html = getIndexHtml();
142
+ if (html) return c.html(html);
143
+ return c.html(telescope.getDashboardHtml());
144
+ });
145
+ return app;
146
+ }
147
+ /**
148
+ * Set up WebSocket routes for real-time updates.
149
+ * Requires @hono/node-ws for Node.js or Bun's built-in WebSocket.
150
+ */
151
+ function setupWebSocket(app, telescope, upgradeWebSocket) {
152
+ app.get("/ws", upgradeWebSocket(() => ({
153
+ onOpen: (_event, ws) => {
154
+ telescope.addWsClient(ws);
155
+ },
156
+ onClose: (_event, ws) => {
157
+ telescope.removeWsClient(ws);
158
+ },
159
+ onMessage: (event, ws) => {
160
+ try {
161
+ const data = JSON.parse(event.data);
162
+ if (data.type === "ping") ws.send(JSON.stringify({ type: "pong" }));
163
+ } catch {}
164
+ }
165
+ })));
166
+ }
167
+ /**
168
+ * Get the request ID from Hono context (set by middleware)
169
+ */
170
+ function getRequestId(c) {
171
+ return c.get(CONTEXT_KEY);
172
+ }
173
+
174
+ //#endregion
175
+ export { createMiddleware, createUI, getRequestId, setupWebSocket };
176
+ //# sourceMappingURL=hono.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono.mjs","names":["telescope: Telescope","c: Context","next: Next","headers: Record<string, string>","query: Record<string, string>","body: unknown","responseHeaders: Record<string, string>","responseBody: unknown","app: Hono","upgradeWebSocket: (handler: any) => any","_event: Event","ws: WebSocket","event: MessageEvent"],"sources":["../../src/server/hono.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport type { Context, MiddlewareHandler, Next } from 'hono';\nimport type { Telescope } from '../Telescope';\nimport type { QueryOptions } from '../types';\nimport { getAsset, getIndexHtml } from '../ui-assets';\n\nconst CONTEXT_KEY = 'telescope-request-id';\n\n/**\n * Create Hono middleware that captures requests and responses\n */\nexport function createMiddleware(telescope: Telescope): MiddlewareHandler {\n return async (c: Context, next: Next) => {\n if (!telescope.enabled) {\n return next();\n }\n\n if (telescope.shouldIgnore(c.req.path)) {\n return next();\n }\n\n const startTime = performance.now();\n\n // Capture request data\n const headers: Record<string, string> = {};\n c.req.raw.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n const url = new URL(c.req.url);\n const query: Record<string, string> = {};\n url.searchParams.forEach((value, key) => {\n query[key] = value;\n });\n\n let body: unknown;\n if (\n telescope.recordBody &&\n ['POST', 'PUT', 'PATCH'].includes(c.req.method)\n ) {\n try {\n const contentType = c.req.header('content-type') || '';\n if (contentType.includes('application/json')) {\n body = await c.req.json();\n } else if (contentType.includes('application/x-www-form-urlencoded')) {\n const formData = await c.req.formData();\n body = Object.fromEntries(formData.entries());\n } else if (contentType.includes('text/')) {\n body = await c.req.text();\n }\n } catch {\n // Ignore body parsing errors\n }\n }\n\n const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip');\n\n try {\n await next();\n\n // Capture response data\n const duration = performance.now() - startTime;\n\n const responseHeaders: Record<string, string> = {};\n c.res.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n let responseBody: unknown;\n if (telescope.recordBody) {\n try {\n const contentType = c.res.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n const cloned = c.res.clone();\n responseBody = await cloned.json();\n }\n } catch {\n // Ignore body parsing errors\n }\n }\n\n const requestId = await telescope.recordRequest({\n method: c.req.method,\n path: c.req.path,\n url: c.req.url,\n headers,\n body,\n query,\n status: c.res.status,\n responseHeaders,\n responseBody,\n duration,\n ip,\n });\n\n c.set(CONTEXT_KEY, requestId);\n } catch (error) {\n await telescope.exception(error as Error);\n throw error;\n }\n };\n}\n\n/**\n * Parse query options from Hono context\n */\nfunction parseQueryOptions(c: Context): QueryOptions {\n const limit = parseInt(c.req.query('limit') || '50', 10);\n const offset = parseInt(c.req.query('offset') || '0', 10);\n const search = c.req.query('search');\n const before = c.req.query('before');\n const after = c.req.query('after');\n const tags = c.req.query('tags')?.split(',').filter(Boolean);\n\n return {\n limit: Math.min(limit, 100),\n offset,\n search,\n before: before ? new Date(before) : undefined,\n after: after ? new Date(after) : undefined,\n tags,\n };\n}\n\n/**\n * Create Hono app with dashboard UI and API routes\n */\nexport function createUI(telescope: Telescope): Hono {\n const app = new Hono();\n\n // API routes\n app.get('/api/requests', async (c) => {\n const options = parseQueryOptions(c);\n const requests = await telescope.getRequests(options);\n return c.json(requests);\n });\n\n app.get('/api/requests/:id', async (c) => {\n const request = await telescope.getRequest(c.req.param('id'));\n if (!request) {\n return c.json({ error: 'Request not found' }, 404);\n }\n return c.json(request);\n });\n\n app.get('/api/exceptions', async (c) => {\n const options = parseQueryOptions(c);\n const exceptions = await telescope.getExceptions(options);\n return c.json(exceptions);\n });\n\n app.get('/api/exceptions/:id', async (c) => {\n const exception = await telescope.getException(c.req.param('id'));\n if (!exception) {\n return c.json({ error: 'Exception not found' }, 404);\n }\n return c.json(exception);\n });\n\n app.get('/api/logs', async (c) => {\n const options = parseQueryOptions(c);\n const logs = await telescope.getLogs(options);\n return c.json(logs);\n });\n\n app.get('/api/stats', async (c) => {\n const stats = await telescope.getStats();\n return c.json(stats);\n });\n\n // Static assets\n app.get('/assets/:filename', (c) => {\n const filename = c.req.param('filename');\n const assetPath = `assets/${filename}`;\n const asset = getAsset(assetPath);\n if (asset) {\n return c.body(asset.content, 200, {\n 'Content-Type': asset.contentType,\n 'Cache-Control': 'public, max-age=31536000, immutable',\n });\n }\n return c.notFound();\n });\n\n // Dashboard UI - serve React app\n app.get('/', (c) => {\n const html = getIndexHtml();\n if (html) {\n return c.html(html);\n }\n // Fallback to inline HTML if UI assets not available\n return c.html(telescope.getDashboardHtml());\n });\n\n app.get('/*', (c) => {\n // SPA fallback - serve index.html for client-side routing\n const html = getIndexHtml();\n if (html) {\n return c.html(html);\n }\n return c.html(telescope.getDashboardHtml());\n });\n\n return app;\n}\n\n/**\n * Set up WebSocket routes for real-time updates.\n * Requires @hono/node-ws for Node.js or Bun's built-in WebSocket.\n */\nexport function setupWebSocket(\n app: Hono,\n telescope: Telescope,\n upgradeWebSocket: (handler: any) => any,\n): void {\n app.get(\n '/ws',\n upgradeWebSocket(() => ({\n onOpen: (_event: Event, ws: WebSocket) => {\n telescope.addWsClient(ws);\n },\n onClose: (_event: Event, ws: WebSocket) => {\n telescope.removeWsClient(ws);\n },\n onMessage: (event: MessageEvent, ws: WebSocket) => {\n try {\n const data = JSON.parse(event.data);\n if (data.type === 'ping') {\n ws.send(JSON.stringify({ type: 'pong' }));\n }\n } catch {\n // Ignore invalid messages\n }\n },\n })),\n );\n}\n\n/**\n * Get the request ID from Hono context (set by middleware)\n */\nexport function getRequestId(c: Context): string | undefined {\n return c.get(CONTEXT_KEY);\n}\n\n// Re-export types\nexport type { Telescope };\n"],"mappings":"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"}
@@ -0,0 +1,336 @@
1
+
2
+ //#region src/storage/kysely.ts
3
+ /**
4
+ * Kysely-based storage implementation for Telescope.
5
+ * Stores telescope data in PostgreSQL, MySQL, or SQLite using Kysely.
6
+ *
7
+ * @template DB - Your Kysely database schema (must include TelescopeTables)
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { Kysely, PostgresDialect } from 'kysely';
12
+ * import { KyselyStorage, type TelescopeTables } from '@geekmidas/telescope/storage/kysely';
13
+ *
14
+ * interface Database extends TelescopeTables {
15
+ * users: UserTable;
16
+ * }
17
+ *
18
+ * const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) });
19
+ * const storage = new KyselyStorage({ db });
20
+ *
21
+ * const telescope = new Telescope({ storage });
22
+ * ```
23
+ */
24
+ var KyselyStorage = class {
25
+ db;
26
+ requestsTable;
27
+ exceptionsTable;
28
+ logsTable;
29
+ constructor(config) {
30
+ this.db = config.db;
31
+ const prefix = config.tablePrefix ?? "telescope";
32
+ this.requestsTable = `${prefix}_requests`;
33
+ this.exceptionsTable = `${prefix}_exceptions`;
34
+ this.logsTable = `${prefix}_logs`;
35
+ }
36
+ async saveRequest(entry) {
37
+ const row = this.requestToRow(entry);
38
+ await this.db.insertInto(this.requestsTable).values(row).execute();
39
+ }
40
+ async saveRequests(entries) {
41
+ if (entries.length === 0) return;
42
+ const rows = entries.map((e) => this.requestToRow(e));
43
+ await this.db.insertInto(this.requestsTable).values(rows).execute();
44
+ }
45
+ async getRequests(options) {
46
+ let query = this.db.selectFrom(this.requestsTable).selectAll().orderBy("timestamp", "desc");
47
+ query = this.applyQueryOptions(query, options);
48
+ const rows = await query.execute();
49
+ return rows.map((row) => this.rowToRequest(row));
50
+ }
51
+ async getRequest(id) {
52
+ const row = await this.db.selectFrom(this.requestsTable).selectAll().where("id", "=", id).executeTakeFirst();
53
+ return row ? this.rowToRequest(row) : null;
54
+ }
55
+ async saveException(entry) {
56
+ const row = this.exceptionToRow(entry);
57
+ await this.db.insertInto(this.exceptionsTable).values(row).execute();
58
+ }
59
+ async saveExceptions(entries) {
60
+ if (entries.length === 0) return;
61
+ const rows = entries.map((e) => this.exceptionToRow(e));
62
+ await this.db.insertInto(this.exceptionsTable).values(rows).execute();
63
+ }
64
+ async getExceptions(options) {
65
+ let query = this.db.selectFrom(this.exceptionsTable).selectAll().orderBy("timestamp", "desc");
66
+ query = this.applyQueryOptions(query, options);
67
+ const rows = await query.execute();
68
+ return rows.map((row) => this.rowToException(row));
69
+ }
70
+ async getException(id) {
71
+ const row = await this.db.selectFrom(this.exceptionsTable).selectAll().where("id", "=", id).executeTakeFirst();
72
+ return row ? this.rowToException(row) : null;
73
+ }
74
+ async saveLog(entry) {
75
+ const row = this.logToRow(entry);
76
+ await this.db.insertInto(this.logsTable).values(row).execute();
77
+ }
78
+ async saveLogs(entries) {
79
+ if (entries.length === 0) return;
80
+ const rows = entries.map((e) => this.logToRow(e));
81
+ await this.db.insertInto(this.logsTable).values(rows).execute();
82
+ }
83
+ async getLogs(options) {
84
+ let query = this.db.selectFrom(this.logsTable).selectAll().orderBy("timestamp", "desc");
85
+ query = this.applyQueryOptions(query, options);
86
+ const rows = await query.execute();
87
+ return rows.map((row) => this.rowToLog(row));
88
+ }
89
+ async prune(olderThan) {
90
+ const results = await Promise.all([
91
+ this.db.deleteFrom(this.requestsTable).where("timestamp", "<", olderThan).executeTakeFirst(),
92
+ this.db.deleteFrom(this.exceptionsTable).where("timestamp", "<", olderThan).executeTakeFirst(),
93
+ this.db.deleteFrom(this.logsTable).where("timestamp", "<", olderThan).executeTakeFirst()
94
+ ]);
95
+ return results.reduce((sum, result) => sum + Number(result.numDeletedRows ?? 0), 0);
96
+ }
97
+ async getStats() {
98
+ const [requestsResult, exceptionsResult, logsResult] = await Promise.all([
99
+ this.db.selectFrom(this.requestsTable).select((eb) => [
100
+ eb.fn.count("id").as("count"),
101
+ eb.fn.min("timestamp").as("oldest"),
102
+ eb.fn.max("timestamp").as("newest")
103
+ ]).executeTakeFirst(),
104
+ this.db.selectFrom(this.exceptionsTable).select((eb) => [
105
+ eb.fn.count("id").as("count"),
106
+ eb.fn.min("timestamp").as("oldest"),
107
+ eb.fn.max("timestamp").as("newest")
108
+ ]).executeTakeFirst(),
109
+ this.db.selectFrom(this.logsTable).select((eb) => [
110
+ eb.fn.count("id").as("count"),
111
+ eb.fn.min("timestamp").as("oldest"),
112
+ eb.fn.max("timestamp").as("newest")
113
+ ]).executeTakeFirst()
114
+ ]);
115
+ const allDates = [
116
+ requestsResult?.oldest,
117
+ requestsResult?.newest,
118
+ exceptionsResult?.oldest,
119
+ exceptionsResult?.newest,
120
+ logsResult?.oldest,
121
+ logsResult?.newest
122
+ ].filter((d) => d != null).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
123
+ return {
124
+ requests: Number(requestsResult?.count ?? 0),
125
+ exceptions: Number(exceptionsResult?.count ?? 0),
126
+ logs: Number(logsResult?.count ?? 0),
127
+ oldestEntry: allDates[0] ? new Date(allDates[0]) : void 0,
128
+ newestEntry: allDates.length > 0 ? new Date(allDates[allDates.length - 1]) : void 0
129
+ };
130
+ }
131
+ applyQueryOptions(query, options) {
132
+ if (!options) return query.limit(50);
133
+ if (options.after) query = query.where("timestamp", ">=", options.after);
134
+ if (options.before) query = query.where("timestamp", "<=", options.before);
135
+ if (options.search) query = query.where((eb) => eb.or([
136
+ eb("message", "ilike", `%${options.search}%`),
137
+ eb("path", "ilike", `%${options.search}%`),
138
+ eb("url", "ilike", `%${options.search}%`)
139
+ ]));
140
+ const limit = options.limit ?? 50;
141
+ const offset = options.offset ?? 0;
142
+ return query.limit(limit).offset(offset);
143
+ }
144
+ requestToRow(entry) {
145
+ return {
146
+ id: entry.id,
147
+ method: entry.method,
148
+ path: entry.path,
149
+ url: entry.url,
150
+ headers: entry.headers,
151
+ body: entry.body ?? null,
152
+ query: entry.query ?? null,
153
+ status: entry.status,
154
+ response_headers: entry.responseHeaders,
155
+ response_body: entry.responseBody ?? null,
156
+ duration: entry.duration,
157
+ timestamp: entry.timestamp,
158
+ ip: entry.ip ?? null,
159
+ user_id: entry.userId ?? null,
160
+ tags: entry.tags ?? null
161
+ };
162
+ }
163
+ rowToRequest(row) {
164
+ return {
165
+ id: row.id,
166
+ method: row.method,
167
+ path: row.path,
168
+ url: row.url,
169
+ headers: this.parseJson(row.headers),
170
+ body: row.body ? this.parseJson(row.body) : void 0,
171
+ query: row.query ? this.parseJson(row.query) : void 0,
172
+ status: row.status,
173
+ responseHeaders: this.parseJson(row.response_headers),
174
+ responseBody: row.response_body ? this.parseJson(row.response_body) : void 0,
175
+ duration: row.duration,
176
+ timestamp: new Date(row.timestamp),
177
+ ip: row.ip ?? void 0,
178
+ userId: row.user_id ?? void 0,
179
+ tags: row.tags ? this.parseJson(row.tags) : void 0
180
+ };
181
+ }
182
+ exceptionToRow(entry) {
183
+ return {
184
+ id: entry.id,
185
+ name: entry.name,
186
+ message: entry.message,
187
+ stack: entry.stack,
188
+ source: entry.source ?? null,
189
+ request_id: entry.requestId ?? null,
190
+ timestamp: entry.timestamp,
191
+ handled: entry.handled,
192
+ tags: entry.tags ?? null
193
+ };
194
+ }
195
+ rowToException(row) {
196
+ return {
197
+ id: row.id,
198
+ name: row.name,
199
+ message: row.message,
200
+ stack: this.parseJson(row.stack),
201
+ source: row.source ? this.parseJson(row.source) : void 0,
202
+ requestId: row.request_id ?? void 0,
203
+ timestamp: new Date(row.timestamp),
204
+ handled: row.handled,
205
+ tags: row.tags ? this.parseJson(row.tags) : void 0
206
+ };
207
+ }
208
+ logToRow(entry) {
209
+ return {
210
+ id: entry.id,
211
+ level: entry.level,
212
+ message: entry.message,
213
+ context: entry.context ?? null,
214
+ request_id: entry.requestId ?? null,
215
+ timestamp: entry.timestamp
216
+ };
217
+ }
218
+ rowToLog(row) {
219
+ return {
220
+ id: row.id,
221
+ level: row.level,
222
+ message: row.message,
223
+ context: row.context ? this.parseJson(row.context) : void 0,
224
+ requestId: row.request_id ?? void 0,
225
+ timestamp: new Date(row.timestamp)
226
+ };
227
+ }
228
+ /**
229
+ * Parse a JSON value that may already be parsed (e.g., from jsonb columns).
230
+ */
231
+ parseJson(value) {
232
+ if (typeof value === "object" && value !== null) return value;
233
+ if (typeof value === "string") try {
234
+ return JSON.parse(value);
235
+ } catch {
236
+ return value;
237
+ }
238
+ return value;
239
+ }
240
+ };
241
+ /**
242
+ * SQL migration to create telescope tables.
243
+ * Use this to set up the required tables in your database.
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * import { getTelescopeMigration } from '@geekmidas/telescope/storage/kysely';
248
+ *
249
+ * // In your migration file
250
+ * export async function up(db: Kysely<any>): Promise<void> {
251
+ * const migration = getTelescopeMigration();
252
+ * await db.schema.executeRaw(migration.up).execute();
253
+ * }
254
+ *
255
+ * export async function down(db: Kysely<any>): Promise<void> {
256
+ * const migration = getTelescopeMigration();
257
+ * await db.schema.executeRaw(migration.down).execute();
258
+ * }
259
+ * ```
260
+ */
261
+ function getTelescopeMigration(tablePrefix = "telescope") {
262
+ return {
263
+ up: `
264
+ -- Telescope requests table
265
+ CREATE TABLE IF NOT EXISTS ${tablePrefix}_requests (
266
+ id VARCHAR(21) PRIMARY KEY,
267
+ method VARCHAR(10) NOT NULL,
268
+ path TEXT NOT NULL,
269
+ url TEXT NOT NULL,
270
+ headers JSONB NOT NULL,
271
+ body JSONB,
272
+ query JSONB,
273
+ status INTEGER NOT NULL,
274
+ response_headers JSONB NOT NULL,
275
+ response_body JSONB,
276
+ duration DOUBLE PRECISION NOT NULL,
277
+ timestamp TIMESTAMPTZ NOT NULL,
278
+ ip VARCHAR(45),
279
+ user_id VARCHAR(255),
280
+ tags JSONB
281
+ );
282
+
283
+ CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_timestamp
284
+ ON ${tablePrefix}_requests (timestamp DESC);
285
+ CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_path
286
+ ON ${tablePrefix}_requests (path);
287
+ CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_status
288
+ ON ${tablePrefix}_requests (status);
289
+
290
+ -- Telescope exceptions table
291
+ CREATE TABLE IF NOT EXISTS ${tablePrefix}_exceptions (
292
+ id VARCHAR(21) PRIMARY KEY,
293
+ name VARCHAR(255) NOT NULL,
294
+ message TEXT NOT NULL,
295
+ stack JSONB NOT NULL,
296
+ source JSONB,
297
+ request_id VARCHAR(21),
298
+ timestamp TIMESTAMPTZ NOT NULL,
299
+ handled BOOLEAN NOT NULL DEFAULT FALSE,
300
+ tags JSONB
301
+ );
302
+
303
+ CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_exceptions_timestamp
304
+ ON ${tablePrefix}_exceptions (timestamp DESC);
305
+ CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_exceptions_request_id
306
+ ON ${tablePrefix}_exceptions (request_id);
307
+
308
+ -- Telescope logs table
309
+ CREATE TABLE IF NOT EXISTS ${tablePrefix}_logs (
310
+ id VARCHAR(21) PRIMARY KEY,
311
+ level VARCHAR(10) NOT NULL,
312
+ message TEXT NOT NULL,
313
+ context JSONB,
314
+ request_id VARCHAR(21),
315
+ timestamp TIMESTAMPTZ NOT NULL
316
+ );
317
+
318
+ CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_timestamp
319
+ ON ${tablePrefix}_logs (timestamp DESC);
320
+ CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_level
321
+ ON ${tablePrefix}_logs (level);
322
+ CREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_request_id
323
+ ON ${tablePrefix}_logs (request_id);
324
+ `,
325
+ down: `
326
+ DROP TABLE IF EXISTS ${tablePrefix}_logs;
327
+ DROP TABLE IF EXISTS ${tablePrefix}_exceptions;
328
+ DROP TABLE IF EXISTS ${tablePrefix}_requests;
329
+ `
330
+ };
331
+ }
332
+
333
+ //#endregion
334
+ exports.KyselyStorage = KyselyStorage;
335
+ exports.getTelescopeMigration = getTelescopeMigration;
336
+ //# sourceMappingURL=kysely.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kysely.cjs","names":["config: KyselyStorageConfig<DB>","entry: RequestEntry","entries: RequestEntry[]","options?: QueryOptions","row: TelescopeRequestTable","id: string","entry: ExceptionEntry","entries: ExceptionEntry[]","row: TelescopeExceptionTable","entry: LogEntry","entries: LogEntry[]","row: TelescopeLogTable","olderThan: Date","eb: any","query: any","value: unknown"],"sources":["../../src/storage/kysely.ts"],"sourcesContent":["import type { Kysely } from 'kysely';\nimport type {\n ExceptionEntry,\n LogEntry,\n QueryOptions,\n RequestEntry,\n TelescopeStats,\n TelescopeStorage,\n} from '../types';\n\n/**\n * Database table interface for telescope requests.\n * Use this to define your telescope_requests table in your Kysely database schema.\n */\nexport interface TelescopeRequestTable {\n id: string;\n method: string;\n path: string;\n url: string;\n headers: unknown;\n body: unknown | null;\n query: unknown | null;\n status: number;\n response_headers: unknown;\n response_body: unknown | null;\n duration: number;\n timestamp: Date;\n ip: string | null;\n user_id: string | null;\n tags: unknown | null;\n}\n\n/**\n * Database table interface for telescope exceptions.\n */\nexport interface TelescopeExceptionTable {\n id: string;\n name: string;\n message: string;\n stack: unknown;\n source: unknown | null;\n request_id: string | null;\n timestamp: Date;\n handled: boolean;\n tags: unknown | null;\n}\n\n/**\n * Database table interface for telescope logs.\n */\nexport interface TelescopeLogTable {\n id: string;\n level: string;\n message: string;\n context: unknown | null;\n request_id: string | null;\n timestamp: Date;\n}\n\n/**\n * Combined database interface for all telescope tables.\n * Use this to extend your database schema.\n *\n * @example\n * ```typescript\n * import type { TelescopeTables } from '@geekmidas/telescope/storage/kysely';\n *\n * interface Database extends TelescopeTables {\n * users: UserTable;\n * // ... other tables\n * }\n * ```\n */\nexport interface TelescopeTables {\n telescope_requests: TelescopeRequestTable;\n telescope_exceptions: TelescopeExceptionTable;\n telescope_logs: TelescopeLogTable;\n}\n\n/**\n * Configuration for KyselyStorage.\n */\nexport interface KyselyStorageConfig<DB> {\n /** Kysely database instance */\n db: Kysely<DB>;\n /**\n * Table name prefix (default: 'telescope').\n * Tables will be named: {prefix}_requests, {prefix}_exceptions, {prefix}_logs\n */\n tablePrefix?: string;\n}\n\n/**\n * Kysely-based storage implementation for Telescope.\n * Stores telescope data in PostgreSQL, MySQL, or SQLite using Kysely.\n *\n * @template DB - Your Kysely database schema (must include TelescopeTables)\n *\n * @example\n * ```typescript\n * import { Kysely, PostgresDialect } from 'kysely';\n * import { KyselyStorage, type TelescopeTables } from '@geekmidas/telescope/storage/kysely';\n *\n * interface Database extends TelescopeTables {\n * users: UserTable;\n * }\n *\n * const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool }) });\n * const storage = new KyselyStorage({ db });\n *\n * const telescope = new Telescope({ storage });\n * ```\n */\nexport class KyselyStorage<DB> implements TelescopeStorage {\n private readonly db: Kysely<DB>;\n private readonly requestsTable: string;\n private readonly exceptionsTable: string;\n private readonly logsTable: string;\n\n constructor(config: KyselyStorageConfig<DB>) {\n this.db = config.db;\n const prefix = config.tablePrefix ?? 'telescope';\n this.requestsTable = `${prefix}_requests`;\n this.exceptionsTable = `${prefix}_exceptions`;\n this.logsTable = `${prefix}_logs`;\n }\n\n // ============================================\n // Requests\n // ============================================\n\n async saveRequest(entry: RequestEntry): Promise<void> {\n const row = this.requestToRow(entry);\n await (this.db as any)\n .insertInto(this.requestsTable)\n .values(row)\n .execute();\n }\n\n async saveRequests(entries: RequestEntry[]): Promise<void> {\n if (entries.length === 0) return;\n\n const rows = entries.map((e) => this.requestToRow(e));\n await (this.db as any)\n .insertInto(this.requestsTable)\n .values(rows)\n .execute();\n }\n\n async getRequests(options?: QueryOptions): Promise<RequestEntry[]> {\n let query = (this.db as any)\n .selectFrom(this.requestsTable)\n .selectAll()\n .orderBy('timestamp', 'desc');\n\n query = this.applyQueryOptions(query, options);\n\n const rows = await query.execute();\n return rows.map((row: TelescopeRequestTable) => this.rowToRequest(row));\n }\n\n async getRequest(id: string): Promise<RequestEntry | null> {\n const row = await (this.db as any)\n .selectFrom(this.requestsTable)\n .selectAll()\n .where('id', '=', id)\n .executeTakeFirst();\n\n return row ? this.rowToRequest(row) : null;\n }\n\n // ============================================\n // Exceptions\n // ============================================\n\n async saveException(entry: ExceptionEntry): Promise<void> {\n const row = this.exceptionToRow(entry);\n await (this.db as any)\n .insertInto(this.exceptionsTable)\n .values(row)\n .execute();\n }\n\n async saveExceptions(entries: ExceptionEntry[]): Promise<void> {\n if (entries.length === 0) return;\n\n const rows = entries.map((e) => this.exceptionToRow(e));\n await (this.db as any)\n .insertInto(this.exceptionsTable)\n .values(rows)\n .execute();\n }\n\n async getExceptions(options?: QueryOptions): Promise<ExceptionEntry[]> {\n let query = (this.db as any)\n .selectFrom(this.exceptionsTable)\n .selectAll()\n .orderBy('timestamp', 'desc');\n\n query = this.applyQueryOptions(query, options);\n\n const rows = await query.execute();\n return rows.map((row: TelescopeExceptionTable) => this.rowToException(row));\n }\n\n async getException(id: string): Promise<ExceptionEntry | null> {\n const row = await (this.db as any)\n .selectFrom(this.exceptionsTable)\n .selectAll()\n .where('id', '=', id)\n .executeTakeFirst();\n\n return row ? this.rowToException(row) : null;\n }\n\n // ============================================\n // Logs\n // ============================================\n\n async saveLog(entry: LogEntry): Promise<void> {\n const row = this.logToRow(entry);\n await (this.db as any).insertInto(this.logsTable).values(row).execute();\n }\n\n async saveLogs(entries: LogEntry[]): Promise<void> {\n if (entries.length === 0) return;\n\n const rows = entries.map((e) => this.logToRow(e));\n await (this.db as any).insertInto(this.logsTable).values(rows).execute();\n }\n\n async getLogs(options?: QueryOptions): Promise<LogEntry[]> {\n let query = (this.db as any)\n .selectFrom(this.logsTable)\n .selectAll()\n .orderBy('timestamp', 'desc');\n\n query = this.applyQueryOptions(query, options);\n\n const rows = await query.execute();\n return rows.map((row: TelescopeLogTable) => this.rowToLog(row));\n }\n\n // ============================================\n // Cleanup\n // ============================================\n\n async prune(olderThan: Date): Promise<number> {\n const results = await Promise.all([\n (this.db as any)\n .deleteFrom(this.requestsTable)\n .where('timestamp', '<', olderThan)\n .executeTakeFirst(),\n (this.db as any)\n .deleteFrom(this.exceptionsTable)\n .where('timestamp', '<', olderThan)\n .executeTakeFirst(),\n (this.db as any)\n .deleteFrom(this.logsTable)\n .where('timestamp', '<', olderThan)\n .executeTakeFirst(),\n ]);\n\n return results.reduce(\n (sum, result) => sum + Number(result.numDeletedRows ?? 0),\n 0,\n );\n }\n\n // ============================================\n // Stats\n // ============================================\n\n async getStats(): Promise<TelescopeStats> {\n const [requestsResult, exceptionsResult, logsResult] = await Promise.all([\n (this.db as any)\n .selectFrom(this.requestsTable)\n .select((eb: any) => [\n eb.fn.count('id').as('count'),\n eb.fn.min('timestamp').as('oldest'),\n eb.fn.max('timestamp').as('newest'),\n ])\n .executeTakeFirst(),\n (this.db as any)\n .selectFrom(this.exceptionsTable)\n .select((eb: any) => [\n eb.fn.count('id').as('count'),\n eb.fn.min('timestamp').as('oldest'),\n eb.fn.max('timestamp').as('newest'),\n ])\n .executeTakeFirst(),\n (this.db as any)\n .selectFrom(this.logsTable)\n .select((eb: any) => [\n eb.fn.count('id').as('count'),\n eb.fn.min('timestamp').as('oldest'),\n eb.fn.max('timestamp').as('newest'),\n ])\n .executeTakeFirst(),\n ]);\n\n const allDates = [\n requestsResult?.oldest,\n requestsResult?.newest,\n exceptionsResult?.oldest,\n exceptionsResult?.newest,\n logsResult?.oldest,\n logsResult?.newest,\n ]\n .filter((d): d is Date => d != null)\n .sort((a, b) => new Date(a).getTime() - new Date(b).getTime());\n\n return {\n requests: Number(requestsResult?.count ?? 0),\n exceptions: Number(exceptionsResult?.count ?? 0),\n logs: Number(logsResult?.count ?? 0),\n oldestEntry: allDates[0] ? new Date(allDates[0]) : undefined,\n newestEntry: allDates.length > 0\n ? new Date(allDates[allDates.length - 1])\n : undefined,\n };\n }\n\n // ============================================\n // Private Helpers\n // ============================================\n\n private applyQueryOptions(query: any, options?: QueryOptions): any {\n if (!options) {\n return query.limit(50);\n }\n\n if (options.after) {\n query = query.where('timestamp', '>=', options.after);\n }\n\n if (options.before) {\n query = query.where('timestamp', '<=', options.before);\n }\n\n if (options.search) {\n // Search in relevant text fields - using ILIKE for case-insensitive\n // This is a simple implementation; for production you'd want full-text search\n query = query.where((eb: any) =>\n eb.or([\n eb('message', 'ilike', `%${options.search}%`),\n eb('path', 'ilike', `%${options.search}%`),\n eb('url', 'ilike', `%${options.search}%`),\n ]),\n );\n }\n\n // Tags filter would require array contains operation\n // which is database-specific (PostgreSQL: @>, etc.)\n\n const limit = options.limit ?? 50;\n const offset = options.offset ?? 0;\n\n return query.limit(limit).offset(offset);\n }\n\n private requestToRow(entry: RequestEntry): TelescopeRequestTable {\n return {\n id: entry.id,\n method: entry.method,\n path: entry.path,\n url: entry.url,\n headers: entry.headers,\n body: entry.body ?? null,\n query: entry.query ?? null,\n status: entry.status,\n response_headers: entry.responseHeaders,\n response_body: entry.responseBody ?? null,\n duration: entry.duration,\n timestamp: entry.timestamp,\n ip: entry.ip ?? null,\n user_id: entry.userId ?? null,\n tags: entry.tags ?? null,\n };\n }\n\n private rowToRequest(row: TelescopeRequestTable): RequestEntry {\n return {\n id: row.id,\n method: row.method,\n path: row.path,\n url: row.url,\n headers: this.parseJson(row.headers) as Record<string, string>,\n body: row.body ? this.parseJson(row.body) : undefined,\n query: row.query\n ? (this.parseJson(row.query) as Record<string, string>)\n : undefined,\n status: row.status,\n responseHeaders: this.parseJson(row.response_headers) as Record<\n string,\n string\n >,\n responseBody: row.response_body\n ? this.parseJson(row.response_body)\n : undefined,\n duration: row.duration,\n timestamp: new Date(row.timestamp),\n ip: row.ip ?? undefined,\n userId: row.user_id ?? undefined,\n tags: row.tags ? (this.parseJson(row.tags) as string[]) : undefined,\n };\n }\n\n private exceptionToRow(entry: ExceptionEntry): TelescopeExceptionTable {\n return {\n id: entry.id,\n name: entry.name,\n message: entry.message,\n stack: entry.stack,\n source: entry.source ?? null,\n request_id: entry.requestId ?? null,\n timestamp: entry.timestamp,\n handled: entry.handled,\n tags: entry.tags ?? null,\n };\n }\n\n private rowToException(row: TelescopeExceptionTable): ExceptionEntry {\n return {\n id: row.id,\n name: row.name,\n message: row.message,\n stack: this.parseJson(row.stack) as ExceptionEntry['stack'],\n source: row.source\n ? (this.parseJson(row.source) as ExceptionEntry['source'])\n : undefined,\n requestId: row.request_id ?? undefined,\n timestamp: new Date(row.timestamp),\n handled: row.handled,\n tags: row.tags ? (this.parseJson(row.tags) as string[]) : undefined,\n };\n }\n\n private logToRow(entry: LogEntry): TelescopeLogTable {\n return {\n id: entry.id,\n level: entry.level,\n message: entry.message,\n context: entry.context ?? null,\n request_id: entry.requestId ?? null,\n timestamp: entry.timestamp,\n };\n }\n\n private rowToLog(row: TelescopeLogTable): LogEntry {\n return {\n id: row.id,\n level: row.level as LogEntry['level'],\n message: row.message,\n context: row.context\n ? (this.parseJson(row.context) as Record<string, unknown>)\n : undefined,\n requestId: row.request_id ?? undefined,\n timestamp: new Date(row.timestamp),\n };\n }\n\n /**\n * Parse a JSON value that may already be parsed (e.g., from jsonb columns).\n */\n private parseJson(value: unknown): unknown {\n if (typeof value === 'object' && value !== null) {\n return value;\n }\n if (typeof value === 'string') {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n return value;\n }\n}\n\n/**\n * SQL migration to create telescope tables.\n * Use this to set up the required tables in your database.\n *\n * @example\n * ```typescript\n * import { getTelescopeMigration } from '@geekmidas/telescope/storage/kysely';\n *\n * // In your migration file\n * export async function up(db: Kysely<any>): Promise<void> {\n * const migration = getTelescopeMigration();\n * await db.schema.executeRaw(migration.up).execute();\n * }\n *\n * export async function down(db: Kysely<any>): Promise<void> {\n * const migration = getTelescopeMigration();\n * await db.schema.executeRaw(migration.down).execute();\n * }\n * ```\n */\nexport function getTelescopeMigration(tablePrefix = 'telescope'): {\n up: string;\n down: string;\n} {\n return {\n up: `\n-- Telescope requests table\nCREATE TABLE IF NOT EXISTS ${tablePrefix}_requests (\n id VARCHAR(21) PRIMARY KEY,\n method VARCHAR(10) NOT NULL,\n path TEXT NOT NULL,\n url TEXT NOT NULL,\n headers JSONB NOT NULL,\n body JSONB,\n query JSONB,\n status INTEGER NOT NULL,\n response_headers JSONB NOT NULL,\n response_body JSONB,\n duration DOUBLE PRECISION NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL,\n ip VARCHAR(45),\n user_id VARCHAR(255),\n tags JSONB\n);\n\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_timestamp\n ON ${tablePrefix}_requests (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_path\n ON ${tablePrefix}_requests (path);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_requests_status\n ON ${tablePrefix}_requests (status);\n\n-- Telescope exceptions table\nCREATE TABLE IF NOT EXISTS ${tablePrefix}_exceptions (\n id VARCHAR(21) PRIMARY KEY,\n name VARCHAR(255) NOT NULL,\n message TEXT NOT NULL,\n stack JSONB NOT NULL,\n source JSONB,\n request_id VARCHAR(21),\n timestamp TIMESTAMPTZ NOT NULL,\n handled BOOLEAN NOT NULL DEFAULT FALSE,\n tags JSONB\n);\n\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_exceptions_timestamp\n ON ${tablePrefix}_exceptions (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_exceptions_request_id\n ON ${tablePrefix}_exceptions (request_id);\n\n-- Telescope logs table\nCREATE TABLE IF NOT EXISTS ${tablePrefix}_logs (\n id VARCHAR(21) PRIMARY KEY,\n level VARCHAR(10) NOT NULL,\n message TEXT NOT NULL,\n context JSONB,\n request_id VARCHAR(21),\n timestamp TIMESTAMPTZ NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_timestamp\n ON ${tablePrefix}_logs (timestamp DESC);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_level\n ON ${tablePrefix}_logs (level);\nCREATE INDEX IF NOT EXISTS idx_${tablePrefix}_logs_request_id\n ON ${tablePrefix}_logs (request_id);\n`,\n down: `\nDROP TABLE IF EXISTS ${tablePrefix}_logs;\nDROP TABLE IF EXISTS ${tablePrefix}_exceptions;\nDROP TABLE IF EXISTS ${tablePrefix}_requests;\n`,\n };\n}\n"],"mappings":"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"}