trickle-backend 0.1.0

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 (65) hide show
  1. package/dist/db/connection.d.ts +3 -0
  2. package/dist/db/connection.js +16 -0
  3. package/dist/db/migrations.d.ts +2 -0
  4. package/dist/db/migrations.js +51 -0
  5. package/dist/db/queries.d.ts +70 -0
  6. package/dist/db/queries.js +186 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +10 -0
  9. package/dist/routes/audit.d.ts +2 -0
  10. package/dist/routes/audit.js +251 -0
  11. package/dist/routes/codegen.d.ts +2 -0
  12. package/dist/routes/codegen.js +224 -0
  13. package/dist/routes/coverage.d.ts +2 -0
  14. package/dist/routes/coverage.js +98 -0
  15. package/dist/routes/dashboard.d.ts +2 -0
  16. package/dist/routes/dashboard.js +433 -0
  17. package/dist/routes/diff.d.ts +2 -0
  18. package/dist/routes/diff.js +181 -0
  19. package/dist/routes/errors.d.ts +2 -0
  20. package/dist/routes/errors.js +86 -0
  21. package/dist/routes/functions.d.ts +2 -0
  22. package/dist/routes/functions.js +69 -0
  23. package/dist/routes/ingest.d.ts +2 -0
  24. package/dist/routes/ingest.js +111 -0
  25. package/dist/routes/mock.d.ts +2 -0
  26. package/dist/routes/mock.js +57 -0
  27. package/dist/routes/search.d.ts +2 -0
  28. package/dist/routes/search.js +136 -0
  29. package/dist/routes/tail.d.ts +2 -0
  30. package/dist/routes/tail.js +11 -0
  31. package/dist/routes/types.d.ts +2 -0
  32. package/dist/routes/types.js +97 -0
  33. package/dist/server.d.ts +2 -0
  34. package/dist/server.js +40 -0
  35. package/dist/services/sse-broker.d.ts +10 -0
  36. package/dist/services/sse-broker.js +39 -0
  37. package/dist/services/type-differ.d.ts +2 -0
  38. package/dist/services/type-differ.js +126 -0
  39. package/dist/services/type-generator.d.ts +319 -0
  40. package/dist/services/type-generator.js +3207 -0
  41. package/dist/types.d.ts +56 -0
  42. package/dist/types.js +2 -0
  43. package/package.json +22 -0
  44. package/src/db/connection.ts +16 -0
  45. package/src/db/migrations.ts +50 -0
  46. package/src/db/queries.ts +260 -0
  47. package/src/index.ts +11 -0
  48. package/src/routes/audit.ts +283 -0
  49. package/src/routes/codegen.ts +237 -0
  50. package/src/routes/coverage.ts +120 -0
  51. package/src/routes/dashboard.ts +435 -0
  52. package/src/routes/diff.ts +215 -0
  53. package/src/routes/errors.ts +91 -0
  54. package/src/routes/functions.ts +75 -0
  55. package/src/routes/ingest.ts +139 -0
  56. package/src/routes/mock.ts +66 -0
  57. package/src/routes/search.ts +169 -0
  58. package/src/routes/tail.ts +12 -0
  59. package/src/routes/types.ts +106 -0
  60. package/src/server.ts +40 -0
  61. package/src/services/sse-broker.ts +51 -0
  62. package/src/services/type-differ.ts +141 -0
  63. package/src/services/type-generator.ts +3853 -0
  64. package/src/types.ts +37 -0
  65. package/tsconfig.json +8 -0
@@ -0,0 +1,433 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const express_1 = require("express");
4
+ const router = (0, express_1.Router)();
5
+ router.get("/", (_req, res) => {
6
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
7
+ res.send(dashboardHtml());
8
+ });
9
+ function dashboardHtml() {
10
+ return `<!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <title>trickle dashboard</title>
16
+ <style>
17
+ :root {
18
+ --bg: #0d1117; --surface: #161b22; --border: #30363d;
19
+ --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;
20
+ --green: #3fb950; --red: #f85149; --yellow: #d29922; --purple: #bc8cff;
21
+ }
22
+ * { margin: 0; padding: 0; box-sizing: border-box; }
23
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
24
+ background: var(--bg); color: var(--text); line-height: 1.5; }
25
+ .container { max-width: 1100px; margin: 0 auto; padding: 24px; }
26
+
27
+ header { display: flex; align-items: center; justify-content: space-between;
28
+ padding: 16px 0; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
29
+ header h1 { font-size: 20px; font-weight: 600; }
30
+ header h1 span { color: var(--accent); }
31
+ .status { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); }
32
+ .dot { width: 8px; height: 8px; border-radius: 50%; }
33
+ .dot.live { background: var(--green); animation: pulse 2s infinite; }
34
+ .dot.off { background: var(--red); }
35
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
36
+
37
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
38
+ gap: 12px; margin-bottom: 24px; }
39
+ .stat { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
40
+ padding: 16px; }
41
+ .stat-value { font-size: 28px; font-weight: 700; color: var(--accent); }
42
+ .stat-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
43
+
44
+ .search { margin-bottom: 16px; }
45
+ .search input { width: 100%; padding: 10px 14px; background: var(--surface);
46
+ border: 1px solid var(--border); border-radius: 6px; color: var(--text);
47
+ font-size: 14px; outline: none; }
48
+ .search input:focus { border-color: var(--accent); }
49
+ .search input::placeholder { color: var(--muted); }
50
+
51
+ .route-list { display: flex; flex-direction: column; gap: 8px; }
52
+ .route { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
53
+ overflow: hidden; transition: border-color 0.15s; }
54
+ .route:hover { border-color: var(--accent); }
55
+ .route.new { animation: highlight 1.5s; }
56
+ @keyframes highlight { 0% { border-color: var(--green); box-shadow: 0 0 12px rgba(63,185,80,0.15); }
57
+ 100% { border-color: var(--border); box-shadow: none; } }
58
+
59
+ .route-header { display: flex; align-items: center; gap: 10px; padding: 12px 16px;
60
+ cursor: pointer; user-select: none; }
61
+ .method { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px;
62
+ text-transform: uppercase; letter-spacing: 0.5px; }
63
+ .method.get { background: rgba(63,185,80,0.15); color: var(--green); }
64
+ .method.post { background: rgba(88,166,255,0.15); color: var(--accent); }
65
+ .method.put { background: rgba(210,153,34,0.15); color: var(--yellow); }
66
+ .method.delete { background: rgba(248,81,73,0.15); color: var(--red); }
67
+ .method.patch { background: rgba(188,140,255,0.15); color: var(--purple); }
68
+ .route-path { font-size: 14px; font-weight: 500; font-family: 'SF Mono', Menlo, monospace; }
69
+ .route-meta { margin-left: auto; font-size: 12px; color: var(--muted); }
70
+ .chevron { color: var(--muted); transition: transform 0.15s; font-size: 12px; }
71
+ .route.open .chevron { transform: rotate(90deg); }
72
+
73
+ .route-detail { display: none; padding: 0 16px 16px; border-top: 1px solid var(--border); }
74
+ .route.open .route-detail { display: block; }
75
+
76
+ .type-section { margin-top: 12px; }
77
+ .type-label { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase;
78
+ letter-spacing: 0.5px; margin-bottom: 6px; }
79
+ .type-block { background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
80
+ padding: 12px; font-family: 'SF Mono', Menlo, monospace; font-size: 13px;
81
+ line-height: 1.6; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
82
+ .type-block .key { color: var(--accent); }
83
+ .type-block .type { color: var(--green); }
84
+ .type-block .punct { color: var(--muted); }
85
+
86
+ .sample-section { margin-top: 12px; }
87
+ .sample-block { background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
88
+ padding: 12px; font-family: 'SF Mono', Menlo, monospace; font-size: 12px;
89
+ line-height: 1.5; overflow-x: auto; white-space: pre-wrap; color: var(--muted); max-height: 200px; overflow-y: auto; }
90
+
91
+ .empty { text-align: center; padding: 48px; color: var(--muted); }
92
+ .empty p { margin-top: 8px; font-size: 14px; }
93
+
94
+ .live-banner { display: none; padding: 8px 16px; background: rgba(63,185,80,0.1);
95
+ border: 1px solid rgba(63,185,80,0.3); border-radius: 6px; margin-bottom: 16px;
96
+ font-size: 13px; color: var(--green); text-align: center; }
97
+ .live-banner.show { display: block; animation: fadeIn 0.3s; }
98
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
99
+
100
+ .tabs { display: flex; gap: 4px; margin-bottom: 16px; }
101
+ .tab { padding: 6px 14px; border-radius: 6px; font-size: 13px; cursor: pointer;
102
+ color: var(--muted); background: transparent; border: 1px solid transparent; }
103
+ .tab:hover { color: var(--text); }
104
+ .tab.active { color: var(--text); background: var(--surface); border-color: var(--border); }
105
+ </style>
106
+ </head>
107
+ <body>
108
+ <div class="container">
109
+ <header>
110
+ <h1><span>trickle</span> dashboard</h1>
111
+ <div class="status">
112
+ <div class="dot" id="statusDot"></div>
113
+ <span id="statusText">connecting...</span>
114
+ </div>
115
+ </header>
116
+
117
+ <div class="stats" id="stats"></div>
118
+
119
+ <div class="live-banner" id="liveBanner">New types observed — view updated</div>
120
+
121
+ <div class="search">
122
+ <input type="text" id="searchInput" placeholder="Search routes... (e.g. /api/users, GET, POST)">
123
+ </div>
124
+
125
+ <div class="tabs" id="tabs">
126
+ <div class="tab active" data-filter="all">All</div>
127
+ <div class="tab" data-filter="GET">GET</div>
128
+ <div class="tab" data-filter="POST">POST</div>
129
+ <div class="tab" data-filter="PUT">PUT</div>
130
+ <div class="tab" data-filter="DELETE">DELETE</div>
131
+ </div>
132
+
133
+ <div class="route-list" id="routeList"></div>
134
+ </div>
135
+
136
+ <script>
137
+ const API = window.location.origin;
138
+ let allRoutes = [];
139
+ let currentFilter = 'all';
140
+ let searchQuery = '';
141
+
142
+ async function fetchRoutes() {
143
+ try {
144
+ const res = await fetch(API + '/api/functions?limit=500');
145
+ const data = await res.json();
146
+ return data.functions || [];
147
+ } catch { return []; }
148
+ }
149
+
150
+ async function fetchSnapshot(funcId, env) {
151
+ try {
152
+ const url = API + '/api/types/' + funcId + (env ? '?env=' + env : '');
153
+ const res = await fetch(url);
154
+ const data = await res.json();
155
+ return data.snapshots?.[0] || null;
156
+ } catch { return null; }
157
+ }
158
+
159
+ async function fetchMockConfig() {
160
+ try {
161
+ const res = await fetch(API + '/api/mock-config');
162
+ const data = await res.json();
163
+ return data.routes || [];
164
+ } catch { return []; }
165
+ }
166
+
167
+ function parseRoute(name) {
168
+ const m = name.match(/^(GET|POST|PUT|DELETE|PATCH)\\s+(.+)$/i);
169
+ if (!m) return null;
170
+ return { method: m[1].toUpperCase(), path: m[2] };
171
+ }
172
+
173
+ function timeAgo(iso) {
174
+ const d = new Date(iso);
175
+ const s = Math.floor((Date.now() - d.getTime()) / 1000);
176
+ if (s < 60) return s + 's ago';
177
+ const m = Math.floor(s / 60);
178
+ if (m < 60) return m + 'm ago';
179
+ const h = Math.floor(m / 60);
180
+ if (h < 24) return h + 'h ago';
181
+ return Math.floor(h / 24) + 'd ago';
182
+ }
183
+
184
+ function renderTypeNode(node, indent) {
185
+ indent = indent || 0;
186
+ const pad = ' '.repeat(indent);
187
+ if (!node || !node.kind) return '<span class="type">unknown</span>';
188
+
189
+ switch (node.kind) {
190
+ case 'primitive':
191
+ return '<span class="type">' + node.name + '</span>';
192
+ case 'unknown':
193
+ return '<span class="type">unknown</span>';
194
+ case 'array':
195
+ return renderTypeNode(node.element, indent) + '<span class="punct">[]</span>';
196
+ case 'object': {
197
+ const keys = Object.keys(node.properties || {});
198
+ if (keys.length === 0) return '<span class="punct">{}</span>';
199
+ let s = '<span class="punct">{</span>\\n';
200
+ for (const k of keys) {
201
+ s += pad + ' <span class="key">' + k + '</span><span class="punct">: </span>';
202
+ s += renderTypeNode(node.properties[k], indent + 1);
203
+ s += '<span class="punct">;</span>\\n';
204
+ }
205
+ s += pad + '<span class="punct">}</span>';
206
+ return s;
207
+ }
208
+ case 'union': {
209
+ return (node.members || []).map(m => renderTypeNode(m, indent)).join(' <span class="punct">|</span> ');
210
+ }
211
+ case 'tuple': {
212
+ const els = (node.elements || []).map(e => renderTypeNode(e, indent));
213
+ return '<span class="punct">[</span>' + els.join(', ') + '<span class="punct">]</span>';
214
+ }
215
+ case 'map':
216
+ return '<span class="type">Map</span><span class="punct">&lt;</span>' +
217
+ renderTypeNode(node.key, indent) + ', ' + renderTypeNode(node.value, indent) +
218
+ '<span class="punct">&gt;</span>';
219
+ case 'set':
220
+ return '<span class="type">Set</span><span class="punct">&lt;</span>' +
221
+ renderTypeNode(node.element, indent) + '<span class="punct">&gt;</span>';
222
+ case 'promise':
223
+ return '<span class="type">Promise</span><span class="punct">&lt;</span>' +
224
+ renderTypeNode(node.resolved, indent) + '<span class="punct">&gt;</span>';
225
+ case 'function':
226
+ return '<span class="type">Function</span>';
227
+ default:
228
+ return '<span class="type">unknown</span>';
229
+ }
230
+ }
231
+
232
+ function renderRoute(r) {
233
+ const parsed = parseRoute(r.function_name);
234
+ if (!parsed) return '';
235
+ const method = parsed.method.toLowerCase();
236
+ const ago = timeAgo(r.last_seen_at);
237
+
238
+ return '<div class="route" data-method="' + parsed.method + '" data-id="' + r.id + '">' +
239
+ '<div class="route-header" onclick="toggleRoute(this.parentElement)">' +
240
+ '<span class="chevron">&#9654;</span>' +
241
+ '<span class="method ' + method + '">' + parsed.method + '</span>' +
242
+ '<span class="route-path">' + escHtml(parsed.path) + '</span>' +
243
+ '<span class="route-meta">' + ago + '</span>' +
244
+ '</div>' +
245
+ '<div class="route-detail" id="detail-' + r.id + '"><div class="loading" style="color:var(--muted);font-size:13px;padding:8px 0">Loading types...</div></div>' +
246
+ '</div>';
247
+ }
248
+
249
+ function escHtml(s) {
250
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
251
+ }
252
+
253
+ async function toggleRoute(el) {
254
+ el.classList.toggle('open');
255
+ if (el.classList.contains('open')) {
256
+ const id = el.dataset.id;
257
+ const detail = document.getElementById('detail-' + id);
258
+ if (detail.querySelector('.loading')) {
259
+ const snap = await fetchSnapshot(id);
260
+ const mockRoutes = await fetchMockConfig();
261
+ const fn = allRoutes.find(r => r.id == id);
262
+ const mockRoute = fn ? mockRoutes.find(m => m.functionName === fn.function_name) : null;
263
+
264
+ let html = '';
265
+
266
+ if (snap) {
267
+ let argsType, returnType;
268
+ try { argsType = typeof snap.args_type === 'string' ? JSON.parse(snap.args_type) : snap.args_type; } catch { argsType = null; }
269
+ try { returnType = typeof snap.return_type === 'string' ? JSON.parse(snap.return_type) : snap.return_type; } catch { returnType = null; }
270
+
271
+ if (returnType) {
272
+ html += '<div class="type-section"><div class="type-label">Response Type</div>';
273
+ html += '<div class="type-block">' + renderTypeNode(returnType, 0) + '</div></div>';
274
+ }
275
+
276
+ if (argsType) {
277
+ // For routes, show body/params/query separately
278
+ if (argsType.kind === 'object' && argsType.properties) {
279
+ if (argsType.properties.body && argsType.properties.body.kind === 'object' &&
280
+ Object.keys(argsType.properties.body.properties || {}).length > 0) {
281
+ html += '<div class="type-section"><div class="type-label">Request Body</div>';
282
+ html += '<div class="type-block">' + renderTypeNode(argsType.properties.body, 0) + '</div></div>';
283
+ }
284
+ if (argsType.properties.params && argsType.properties.params.kind === 'object' &&
285
+ Object.keys(argsType.properties.params.properties || {}).length > 0) {
286
+ html += '<div class="type-section"><div class="type-label">Path Params</div>';
287
+ html += '<div class="type-block">' + renderTypeNode(argsType.properties.params, 0) + '</div></div>';
288
+ }
289
+ if (argsType.properties.query && argsType.properties.query.kind === 'object' &&
290
+ Object.keys(argsType.properties.query.properties || {}).length > 0) {
291
+ html += '<div class="type-section"><div class="type-label">Query Params</div>';
292
+ html += '<div class="type-block">' + renderTypeNode(argsType.properties.query, 0) + '</div></div>';
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ if (mockRoute && mockRoute.sampleOutput) {
299
+ html += '<div class="sample-section"><div class="type-label">Sample Response</div>';
300
+ html += '<div class="sample-block">' + escHtml(JSON.stringify(mockRoute.sampleOutput, null, 2)) + '</div></div>';
301
+ }
302
+
303
+ if (!html) html = '<div style="color:var(--muted);font-size:13px;padding:8px 0">No type data available yet.</div>';
304
+ detail.innerHTML = html;
305
+ }
306
+ }
307
+ }
308
+
309
+ function renderStats(routes) {
310
+ const methods = {};
311
+ let routeCount = 0;
312
+ for (const r of routes) {
313
+ const p = parseRoute(r.function_name);
314
+ if (p) {
315
+ routeCount++;
316
+ methods[p.method] = (methods[p.method] || 0) + 1;
317
+ }
318
+ }
319
+
320
+ let html = '<div class="stat"><div class="stat-value">' + routes.length + '</div><div class="stat-label">Functions</div></div>';
321
+ html += '<div class="stat"><div class="stat-value">' + routeCount + '</div><div class="stat-label">API Routes</div></div>';
322
+ for (const [m, c] of Object.entries(methods)) {
323
+ html += '<div class="stat"><div class="stat-value">' + c + '</div><div class="stat-label">' + m + ' routes</div></div>';
324
+ }
325
+ document.getElementById('stats').innerHTML = html;
326
+ }
327
+
328
+ function applyFilters() {
329
+ const cards = document.querySelectorAll('.route');
330
+ cards.forEach(card => {
331
+ const method = card.dataset.method;
332
+ const text = card.textContent.toLowerCase();
333
+ const matchFilter = currentFilter === 'all' || method === currentFilter;
334
+ const matchSearch = !searchQuery || text.includes(searchQuery.toLowerCase());
335
+ card.style.display = matchFilter && matchSearch ? '' : 'none';
336
+ });
337
+ }
338
+
339
+ async function loadData() {
340
+ allRoutes = await fetchRoutes();
341
+ renderStats(allRoutes);
342
+
343
+ if (allRoutes.length === 0) {
344
+ document.getElementById('routeList').innerHTML =
345
+ '<div class="empty"><h3>No observations yet</h3><p>Instrument your app and make some requests to see types here.</p></div>';
346
+ return;
347
+ }
348
+
349
+ // Sort: routes first, then by last_seen_at descending
350
+ const sorted = [...allRoutes].sort((a, b) => {
351
+ const aRoute = parseRoute(a.function_name);
352
+ const bRoute = parseRoute(b.function_name);
353
+ if (aRoute && !bRoute) return -1;
354
+ if (!aRoute && bRoute) return 1;
355
+ return new Date(b.last_seen_at).getTime() - new Date(a.last_seen_at).getTime();
356
+ });
357
+
358
+ document.getElementById('routeList').innerHTML = sorted.map(renderRoute).join('');
359
+ applyFilters();
360
+ }
361
+
362
+ // Search
363
+ document.getElementById('searchInput').addEventListener('input', e => {
364
+ searchQuery = e.target.value;
365
+ applyFilters();
366
+ });
367
+
368
+ // Tabs
369
+ document.querySelectorAll('.tab').forEach(tab => {
370
+ tab.addEventListener('click', () => {
371
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
372
+ tab.classList.add('active');
373
+ currentFilter = tab.dataset.filter;
374
+ applyFilters();
375
+ });
376
+ });
377
+
378
+ // SSE for live updates
379
+ function connectSSE() {
380
+ const dot = document.getElementById('statusDot');
381
+ const text = document.getElementById('statusText');
382
+ const banner = document.getElementById('liveBanner');
383
+
384
+ try {
385
+ const es = new EventSource(API + '/api/tail');
386
+ es.onopen = () => {
387
+ dot.className = 'dot live';
388
+ text.textContent = 'live';
389
+ };
390
+ es.addEventListener('type_snapshot', () => {
391
+ banner.classList.add('show');
392
+ setTimeout(() => banner.classList.remove('show'), 3000);
393
+ // Reload data
394
+ loadData();
395
+ });
396
+ es.addEventListener('function_seen', () => {
397
+ loadData();
398
+ });
399
+ es.onerror = () => {
400
+ dot.className = 'dot off';
401
+ text.textContent = 'disconnected';
402
+ // Retry
403
+ setTimeout(connectSSE, 5000);
404
+ };
405
+ } catch {
406
+ dot.className = 'dot off';
407
+ text.textContent = 'disconnected';
408
+ }
409
+ }
410
+
411
+ // Health check
412
+ async function checkHealth() {
413
+ try {
414
+ const res = await fetch(API + '/api/health');
415
+ if (res.ok) {
416
+ document.getElementById('statusDot').className = 'dot live';
417
+ document.getElementById('statusText').textContent = 'connected';
418
+ }
419
+ } catch {
420
+ document.getElementById('statusDot').className = 'dot off';
421
+ document.getElementById('statusText').textContent = 'disconnected';
422
+ }
423
+ }
424
+
425
+ // Init
426
+ checkHealth();
427
+ loadData();
428
+ connectSSE();
429
+ </script>
430
+ </body>
431
+ </html>`;
432
+ }
433
+ exports.default = router;
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const express_1 = require("express");
4
+ const connection_1 = require("../db/connection");
5
+ const type_differ_1 = require("../services/type-differ");
6
+ const router = (0, express_1.Router)();
7
+ // GET / — cross-function type drift report
8
+ // Query params:
9
+ // since: ISO datetime string — only show changes after this time
10
+ // env1, env2: compare latest snapshots across environments
11
+ // env: filter functions by environment
12
+ router.get("/", (req, res) => {
13
+ try {
14
+ const { since, env1, env2, env } = req.query;
15
+ if (env1 && env2) {
16
+ // Cross-environment diff: for each function, compare latest snapshot in env1 vs env2
17
+ const functionsStmt = connection_1.db.prepare(`
18
+ SELECT DISTINCT f.id, f.function_name, f.module, f.language
19
+ FROM functions f
20
+ JOIN type_snapshots s ON s.function_id = f.id
21
+ WHERE s.env IN (?, ?)
22
+ ORDER BY f.function_name
23
+ `);
24
+ const functions = functionsStmt.all(env1, env2);
25
+ const snapshotStmt = connection_1.db.prepare(`
26
+ SELECT * FROM type_snapshots
27
+ WHERE function_id = ? AND env = ?
28
+ ORDER BY observed_at DESC
29
+ LIMIT 1
30
+ `);
31
+ const entries = [];
32
+ for (const fn of functions) {
33
+ const fromSnap = snapshotStmt.get(fn.id, env1);
34
+ const toSnap = snapshotStmt.get(fn.id, env2);
35
+ if (!fromSnap || !toSnap)
36
+ continue;
37
+ if (fromSnap.type_hash === toSnap.type_hash)
38
+ continue;
39
+ const fromArgs = JSON.parse(fromSnap.args_type);
40
+ const toArgs = JSON.parse(toSnap.args_type);
41
+ const fromReturn = JSON.parse(fromSnap.return_type);
42
+ const toReturn = JSON.parse(toSnap.return_type);
43
+ const diffs = [
44
+ ...(0, type_differ_1.diffTypes)(fromArgs, toArgs, "args"),
45
+ ...(0, type_differ_1.diffTypes)(fromReturn, toReturn, "return"),
46
+ ];
47
+ if (diffs.length > 0) {
48
+ entries.push({
49
+ functionName: fn.function_name,
50
+ module: fn.module,
51
+ language: fn.language,
52
+ from: {
53
+ id: fromSnap.id,
54
+ env: fromSnap.env,
55
+ observed_at: fromSnap.observed_at,
56
+ type_hash: fromSnap.type_hash,
57
+ },
58
+ to: {
59
+ id: toSnap.id,
60
+ env: toSnap.env,
61
+ observed_at: toSnap.observed_at,
62
+ type_hash: toSnap.type_hash,
63
+ },
64
+ diffs,
65
+ });
66
+ }
67
+ }
68
+ res.json({
69
+ mode: "cross-env",
70
+ env1,
71
+ env2,
72
+ entries,
73
+ total: entries.length,
74
+ });
75
+ return;
76
+ }
77
+ // Time-based diff: for each function, compare the two most recent snapshots
78
+ // If `since` is provided, only consider functions with snapshots after that time
79
+ let functionsQuery;
80
+ const bindings = [];
81
+ if (since) {
82
+ if (env) {
83
+ functionsQuery = `
84
+ SELECT DISTINCT f.id, f.function_name, f.module, f.language
85
+ FROM functions f
86
+ JOIN type_snapshots s ON s.function_id = f.id
87
+ WHERE s.observed_at >= ? AND s.env = ?
88
+ ORDER BY f.function_name
89
+ `;
90
+ bindings.push(since, env);
91
+ }
92
+ else {
93
+ functionsQuery = `
94
+ SELECT DISTINCT f.id, f.function_name, f.module, f.language
95
+ FROM functions f
96
+ JOIN type_snapshots s ON s.function_id = f.id
97
+ WHERE s.observed_at >= ?
98
+ ORDER BY f.function_name
99
+ `;
100
+ bindings.push(since);
101
+ }
102
+ }
103
+ else {
104
+ if (env) {
105
+ functionsQuery = `
106
+ SELECT DISTINCT f.id, f.function_name, f.module, f.language
107
+ FROM functions f
108
+ JOIN type_snapshots s ON s.function_id = f.id
109
+ WHERE s.env = ?
110
+ ORDER BY f.function_name
111
+ `;
112
+ bindings.push(env);
113
+ }
114
+ else {
115
+ functionsQuery = `
116
+ SELECT DISTINCT f.id, f.function_name, f.module, f.language
117
+ FROM functions f
118
+ JOIN type_snapshots s ON s.function_id = f.id
119
+ ORDER BY f.function_name
120
+ `;
121
+ }
122
+ }
123
+ const functions = connection_1.db.prepare(functionsQuery).all(...bindings);
124
+ // For each function, get the two most recent snapshots and diff them
125
+ const latestTwoStmt = connection_1.db.prepare(`
126
+ SELECT * FROM type_snapshots
127
+ WHERE function_id = ?
128
+ ORDER BY observed_at DESC
129
+ LIMIT 2
130
+ `);
131
+ const entries = [];
132
+ for (const fn of functions) {
133
+ const snapshots = latestTwoStmt.all(fn.id);
134
+ if (snapshots.length < 2)
135
+ continue;
136
+ const [newer, older] = snapshots;
137
+ if (newer.type_hash === older.type_hash)
138
+ continue;
139
+ const fromArgs = JSON.parse(older.args_type);
140
+ const toArgs = JSON.parse(newer.args_type);
141
+ const fromReturn = JSON.parse(older.return_type);
142
+ const toReturn = JSON.parse(newer.return_type);
143
+ const diffs = [
144
+ ...(0, type_differ_1.diffTypes)(fromArgs, toArgs, "args"),
145
+ ...(0, type_differ_1.diffTypes)(fromReturn, toReturn, "return"),
146
+ ];
147
+ if (diffs.length > 0) {
148
+ entries.push({
149
+ functionName: fn.function_name,
150
+ module: fn.module,
151
+ language: fn.language,
152
+ from: {
153
+ id: older.id,
154
+ env: older.env,
155
+ observed_at: older.observed_at,
156
+ type_hash: older.type_hash,
157
+ },
158
+ to: {
159
+ id: newer.id,
160
+ env: newer.env,
161
+ observed_at: newer.observed_at,
162
+ type_hash: newer.type_hash,
163
+ },
164
+ diffs,
165
+ });
166
+ }
167
+ }
168
+ res.json({
169
+ mode: "temporal",
170
+ since: since || null,
171
+ env: env || null,
172
+ entries,
173
+ total: entries.length,
174
+ });
175
+ }
176
+ catch (err) {
177
+ console.error("Diff report error:", err);
178
+ res.status(500).json({ error: "Internal server error" });
179
+ }
180
+ });
181
+ exports.default = router;
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;