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,91 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { db } from "../db/connection";
3
+ import { listErrors, getError } from "../db/queries";
4
+
5
+ const router = Router();
6
+
7
+ // GET / — list errors with filters
8
+ router.get("/", (req: Request, res: Response) => {
9
+ try {
10
+ const { functionName, env, since, limit, offset } = req.query;
11
+
12
+ const result = listErrors(db, {
13
+ functionName: functionName as string | undefined,
14
+ env: env as string | undefined,
15
+ since: since as string | undefined,
16
+ limit: limit ? parseInt(limit as string, 10) : undefined,
17
+ offset: offset ? parseInt(offset as string, 10) : undefined,
18
+ });
19
+
20
+ const parsed = result.rows.map((row: Record<string, unknown>) => ({
21
+ ...row,
22
+ args_type: tryParseJson(row.args_type as string),
23
+ return_type: tryParseJson(row.return_type as string),
24
+ args_snapshot: tryParseJson(row.args_snapshot as string),
25
+ }));
26
+
27
+ res.json({ errors: parsed, total: result.total });
28
+ } catch (err) {
29
+ console.error("List errors error:", err);
30
+ res.status(500).json({ error: "Internal server error" });
31
+ }
32
+ });
33
+
34
+ // GET /:id — get single error with full context
35
+ router.get("/:id", (req: Request, res: Response) => {
36
+ try {
37
+ const id = parseInt(req.params.id, 10);
38
+ if (isNaN(id)) {
39
+ res.status(400).json({ error: "Invalid error id" });
40
+ return;
41
+ }
42
+
43
+ const errorRow = getError(db, id);
44
+ if (!errorRow) {
45
+ res.status(404).json({ error: "Error not found" });
46
+ return;
47
+ }
48
+
49
+ // Find the associated type snapshot if type_hash is available
50
+ let snapshot: Record<string, unknown> | undefined;
51
+ if (errorRow.type_hash && errorRow.function_id) {
52
+ const stmt = db.prepare(`
53
+ SELECT * FROM type_snapshots
54
+ WHERE function_id = ? AND type_hash = ?
55
+ ORDER BY observed_at DESC
56
+ LIMIT 1
57
+ `);
58
+ snapshot = stmt.get(errorRow.function_id, errorRow.type_hash) as Record<string, unknown> | undefined;
59
+ }
60
+
61
+ res.json({
62
+ error: {
63
+ ...errorRow,
64
+ args_type: tryParseJson(errorRow.args_type as string),
65
+ return_type: tryParseJson(errorRow.return_type as string),
66
+ args_snapshot: tryParseJson(errorRow.args_snapshot as string),
67
+ },
68
+ snapshot: snapshot
69
+ ? {
70
+ ...snapshot,
71
+ args_type: tryParseJson(snapshot.args_type as string),
72
+ return_type: tryParseJson(snapshot.return_type as string),
73
+ }
74
+ : null,
75
+ });
76
+ } catch (err) {
77
+ console.error("Get error error:", err);
78
+ res.status(500).json({ error: "Internal server error" });
79
+ }
80
+ });
81
+
82
+ function tryParseJson(value: unknown): unknown {
83
+ if (typeof value !== "string") return value;
84
+ try {
85
+ return JSON.parse(value);
86
+ } catch {
87
+ return value;
88
+ }
89
+ }
90
+
91
+ export default router;
@@ -0,0 +1,75 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { db } from "../db/connection";
3
+ import { listFunctions, getFunction, getLatestSnapshot } from "../db/queries";
4
+
5
+ const router = Router();
6
+
7
+ // GET / — list functions
8
+ router.get("/", (req: Request, res: Response) => {
9
+ try {
10
+ const { q, env, language, limit, offset } = req.query;
11
+
12
+ const result = listFunctions(db, {
13
+ search: q as string | undefined,
14
+ env: env as string | undefined,
15
+ language: language as string | undefined,
16
+ limit: limit ? parseInt(limit as string, 10) : undefined,
17
+ offset: offset ? parseInt(offset as string, 10) : undefined,
18
+ });
19
+
20
+ res.json({ functions: result.rows, total: result.total });
21
+ } catch (err) {
22
+ console.error("List functions error:", err);
23
+ res.status(500).json({ error: "Internal server error" });
24
+ }
25
+ });
26
+
27
+ // GET /:id — get single function with latest snapshots
28
+ router.get("/:id", (req: Request, res: Response) => {
29
+ try {
30
+ const id = parseInt(req.params.id, 10);
31
+ if (isNaN(id)) {
32
+ res.status(400).json({ error: "Invalid function id" });
33
+ return;
34
+ }
35
+
36
+ const func = getFunction(db, id);
37
+ if (!func) {
38
+ res.status(404).json({ error: "Function not found" });
39
+ return;
40
+ }
41
+
42
+ // Get latest snapshots per known env
43
+ const envStmt = db.prepare(`
44
+ SELECT DISTINCT env FROM type_snapshots WHERE function_id = ?
45
+ `);
46
+ const envs = (envStmt.all(id) as { env: string }[]).map((r) => r.env);
47
+
48
+ const latestSnapshots: Record<string, unknown> = {};
49
+ for (const env of envs) {
50
+ const snapshot = getLatestSnapshot(db, id, env);
51
+ if (snapshot) {
52
+ latestSnapshots[env] = {
53
+ ...snapshot,
54
+ args_type: tryParseJson(snapshot.args_type as string),
55
+ return_type: tryParseJson(snapshot.return_type as string),
56
+ };
57
+ }
58
+ }
59
+
60
+ res.json({ function: func, latestSnapshots });
61
+ } catch (err) {
62
+ console.error("Get function error:", err);
63
+ res.status(500).json({ error: "Internal server error" });
64
+ }
65
+ });
66
+
67
+ function tryParseJson(value: string): unknown {
68
+ try {
69
+ return JSON.parse(value);
70
+ } catch {
71
+ return value;
72
+ }
73
+ }
74
+
75
+ export default router;
@@ -0,0 +1,139 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { db } from "../db/connection";
3
+ import { upsertFunction, findSnapshotByHash, insertSnapshot, insertError } from "../db/queries";
4
+ import { sseBroker } from "../services/sse-broker";
5
+ import { IngestPayload } from "../types";
6
+
7
+ const router = Router();
8
+
9
+ function processPayload(payload: IngestPayload) {
10
+ const {
11
+ functionName,
12
+ module,
13
+ language,
14
+ environment,
15
+ typeHash,
16
+ argsType,
17
+ returnType,
18
+ sampleInput,
19
+ sampleOutput,
20
+ error,
21
+ } = payload;
22
+
23
+ const env = environment || "unknown";
24
+ const func = upsertFunction(db, { functionName, module, environment: env, language });
25
+ const functionId = func.id as number;
26
+
27
+ let isNewType = false;
28
+ const existingSnapshot = findSnapshotByHash(db, functionId, typeHash, env);
29
+
30
+ if (!existingSnapshot) {
31
+ insertSnapshot(db, {
32
+ functionId,
33
+ typeHash,
34
+ argsType: JSON.stringify(argsType),
35
+ returnType: JSON.stringify(returnType),
36
+ variablesType: null as any,
37
+ sampleInput: sampleInput !== undefined ? JSON.stringify(sampleInput) : null as any,
38
+ sampleOutput: sampleOutput !== undefined ? JSON.stringify(sampleOutput) : null as any,
39
+ env,
40
+ });
41
+ isNewType = true;
42
+
43
+ sseBroker.broadcast("type:new", {
44
+ functionName,
45
+ module,
46
+ typeHash,
47
+ environment: env,
48
+ });
49
+ }
50
+
51
+ let errorRecord: Record<string, unknown> | undefined;
52
+ if (error) {
53
+ errorRecord = insertError(db, {
54
+ functionId,
55
+ errorType: error.type,
56
+ errorMessage: error.message,
57
+ stackTrace: error.stackTrace || null as any,
58
+ argsType: JSON.stringify(argsType),
59
+ returnType: JSON.stringify(returnType),
60
+ variablesType: null as any,
61
+ argsSnapshot: error.argsSnapshot !== undefined ? JSON.stringify(error.argsSnapshot) : null as any,
62
+ typeHash,
63
+ env,
64
+ });
65
+
66
+ sseBroker.broadcast("error:new", {
67
+ functionName,
68
+ module,
69
+ errorType: error.type,
70
+ errorMessage: error.message,
71
+ environment: env,
72
+ });
73
+ }
74
+
75
+ return { functionId, isNewType, error: errorRecord };
76
+ }
77
+
78
+ // POST / — single payload ingest
79
+ router.post("/", (req: Request, res: Response) => {
80
+ try {
81
+ const payload = req.body as IngestPayload;
82
+
83
+ if (!payload.functionName || !payload.module || !payload.typeHash) {
84
+ res.status(400).json({ error: "Missing required fields: functionName, module, typeHash" });
85
+ return;
86
+ }
87
+
88
+ const result = processPayload(payload);
89
+
90
+ res.status(200).json({
91
+ ok: true,
92
+ functionId: result.functionId,
93
+ isNewType: result.isNewType,
94
+ errorId: result.error?.id,
95
+ });
96
+ } catch (err) {
97
+ console.error("Ingest error:", err);
98
+ res.status(500).json({ error: "Internal server error" });
99
+ }
100
+ });
101
+
102
+ // POST /batch — batch ingest
103
+ router.post("/batch", (req: Request, res: Response) => {
104
+ try {
105
+ const { payloads } = req.body as { payloads: IngestPayload[] };
106
+
107
+ if (!Array.isArray(payloads) || payloads.length === 0) {
108
+ res.status(400).json({ error: "Expected non-empty payloads array" });
109
+ return;
110
+ }
111
+
112
+ const results: unknown[] = [];
113
+
114
+ const transaction = db.transaction(() => {
115
+ for (const payload of payloads) {
116
+ if (!payload.functionName || !payload.module || !payload.typeHash) {
117
+ results.push({ error: "Missing required fields", functionName: payload.functionName });
118
+ continue;
119
+ }
120
+ const result = processPayload(payload);
121
+ results.push({
122
+ ok: true,
123
+ functionId: result.functionId,
124
+ isNewType: result.isNewType,
125
+ errorId: result.error?.id,
126
+ });
127
+ }
128
+ });
129
+
130
+ transaction();
131
+
132
+ res.status(200).json({ ok: true, results });
133
+ } catch (err) {
134
+ console.error("Batch ingest error:", err);
135
+ res.status(500).json({ error: "Internal server error" });
136
+ }
137
+ });
138
+
139
+ export default router;
@@ -0,0 +1,66 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { db } from "../db/connection";
3
+ import { listFunctions, getLatestSnapshot } from "../db/queries";
4
+
5
+ const router = Router();
6
+
7
+ function tryParseJson(value: unknown): unknown {
8
+ if (typeof value !== "string") return value;
9
+ try {
10
+ return JSON.parse(value);
11
+ } catch {
12
+ return value;
13
+ }
14
+ }
15
+
16
+ /** Route-style function name → { method, path } */
17
+ function parseRouteName(name: string): { method: string; path: string } | null {
18
+ const match = name.match(/^(GET|POST|PUT|DELETE|PATCH)\s+(.+)$/i);
19
+ if (!match) return null;
20
+ return { method: match[1].toUpperCase(), path: match[2] };
21
+ }
22
+
23
+ /**
24
+ * GET /api/mock-config
25
+ *
26
+ * Returns all observed routes with their sample data, ready for mock server use.
27
+ */
28
+ router.get("/", (_req: Request, res: Response) => {
29
+ try {
30
+ const { rows } = listFunctions(db, { limit: 500 });
31
+ const routes: Array<{
32
+ method: string;
33
+ path: string;
34
+ functionName: string;
35
+ module: string;
36
+ sampleInput: unknown;
37
+ sampleOutput: unknown;
38
+ observedAt: string;
39
+ }> = [];
40
+
41
+ for (const fn of rows) {
42
+ const parsed = parseRouteName(fn.function_name as string);
43
+ if (!parsed) continue; // Skip non-route functions
44
+
45
+ const snapshot = getLatestSnapshot(db, fn.id as number);
46
+ if (!snapshot) continue;
47
+
48
+ routes.push({
49
+ method: parsed.method,
50
+ path: parsed.path,
51
+ functionName: fn.function_name as string,
52
+ module: fn.module as string,
53
+ sampleInput: tryParseJson(snapshot.sample_input),
54
+ sampleOutput: tryParseJson(snapshot.sample_output),
55
+ observedAt: snapshot.observed_at as string,
56
+ });
57
+ }
58
+
59
+ res.json({ routes });
60
+ } catch (err) {
61
+ console.error("Mock config error:", err);
62
+ res.status(500).json({ error: "Internal server error" });
63
+ }
64
+ });
65
+
66
+ export default router;
@@ -0,0 +1,169 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { db } from "../db/connection";
3
+ import { listFunctions, getLatestSnapshot } from "../db/queries";
4
+ import { TypeNode } from "../types";
5
+
6
+ const router = Router();
7
+
8
+ interface FieldMatch {
9
+ path: string;
10
+ kind: string;
11
+ typeName?: string;
12
+ }
13
+
14
+ interface SearchResult {
15
+ functionName: string;
16
+ module: string;
17
+ environment: string;
18
+ lastSeen: string;
19
+ matches: FieldMatch[];
20
+ }
21
+
22
+ function tryParseJson(value: string): unknown {
23
+ try {
24
+ return JSON.parse(value);
25
+ } catch {
26
+ return value;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Recursively search a TypeNode tree for fields matching the query.
32
+ * Returns an array of matching field paths.
33
+ */
34
+ function searchTypeNode(
35
+ node: TypeNode,
36
+ query: string,
37
+ currentPath: string,
38
+ results: FieldMatch[],
39
+ ): void {
40
+ const lowerQuery = query.toLowerCase();
41
+
42
+ switch (node.kind) {
43
+ case "object": {
44
+ for (const [key, val] of Object.entries(node.properties)) {
45
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
46
+
47
+ // Check if the field name matches
48
+ if (key.toLowerCase().includes(lowerQuery)) {
49
+ results.push({
50
+ path: fieldPath,
51
+ kind: val.kind,
52
+ typeName: val.kind === "primitive" ? (val as { name: string }).name : val.kind,
53
+ });
54
+ }
55
+
56
+ // Recurse into the value
57
+ searchTypeNode(val, query, fieldPath, results);
58
+ }
59
+ break;
60
+ }
61
+ case "array":
62
+ searchTypeNode(node.element, query, `${currentPath}[]`, results);
63
+ break;
64
+ case "union":
65
+ for (const member of node.members) {
66
+ searchTypeNode(member, query, currentPath, results);
67
+ }
68
+ break;
69
+ case "tuple":
70
+ for (let i = 0; i < node.elements.length; i++) {
71
+ searchTypeNode(node.elements[i], query, `${currentPath}[${i}]`, results);
72
+ }
73
+ break;
74
+ case "promise":
75
+ searchTypeNode(node.resolved, query, currentPath, results);
76
+ break;
77
+ case "map":
78
+ searchTypeNode(node.key, query, `${currentPath}.key`, results);
79
+ searchTypeNode(node.value, query, `${currentPath}.value`, results);
80
+ break;
81
+ case "set":
82
+ searchTypeNode(node.element, query, `${currentPath}[]`, results);
83
+ break;
84
+ case "primitive": {
85
+ // Match on primitive type name (e.g., searching "number" finds all number fields)
86
+ if (node.name.toLowerCase().includes(lowerQuery) && currentPath) {
87
+ // Only add if not already matched by field name
88
+ const alreadyMatched = results.some((r) => r.path === currentPath);
89
+ if (!alreadyMatched) {
90
+ results.push({
91
+ path: currentPath,
92
+ kind: "primitive",
93
+ typeName: node.name,
94
+ });
95
+ }
96
+ }
97
+ break;
98
+ }
99
+ }
100
+ }
101
+
102
+ // GET / — search across all observed types
103
+ router.get("/", (req: Request, res: Response) => {
104
+ try {
105
+ const query = req.query.q as string;
106
+ if (!query || query.trim().length === 0) {
107
+ res.status(400).json({ error: "Missing query parameter 'q'" });
108
+ return;
109
+ }
110
+
111
+ const env = req.query.env as string | undefined;
112
+ const lowerQuery = query.toLowerCase();
113
+
114
+ // Get all functions
115
+ const { rows: functionRows } = listFunctions(db, { env, limit: 500 });
116
+ const results: SearchResult[] = [];
117
+
118
+ for (const fn of functionRows) {
119
+ const functionId = fn.id as number;
120
+ const functionName = fn.function_name as string;
121
+ const moduleName = fn.module as string;
122
+ const environment = (fn.environment as string) || "development";
123
+ const lastSeen = fn.last_seen_at as string;
124
+
125
+ // Check function name match
126
+ const nameMatches = functionName.toLowerCase().includes(lowerQuery);
127
+
128
+ // Get latest snapshot and search types
129
+ const snapshot = getLatestSnapshot(db, functionId, env);
130
+ const fieldMatches: FieldMatch[] = [];
131
+
132
+ if (snapshot) {
133
+ const argsType = tryParseJson(snapshot.args_type as string) as TypeNode;
134
+ const returnType = tryParseJson(snapshot.return_type as string) as TypeNode;
135
+
136
+ if (argsType) {
137
+ searchTypeNode(argsType, query, "args", fieldMatches);
138
+ }
139
+ if (returnType) {
140
+ searchTypeNode(returnType, query, "response", fieldMatches);
141
+ }
142
+ }
143
+
144
+ // Include if function name matches or any fields match
145
+ if (nameMatches || fieldMatches.length > 0) {
146
+ results.push({
147
+ functionName,
148
+ module: moduleName,
149
+ environment,
150
+ lastSeen,
151
+ matches: nameMatches && fieldMatches.length === 0
152
+ ? [{ path: "(function name)", kind: "name", typeName: undefined }]
153
+ : fieldMatches,
154
+ });
155
+ }
156
+ }
157
+
158
+ res.json({
159
+ query,
160
+ total: results.length,
161
+ results,
162
+ });
163
+ } catch (err) {
164
+ console.error("Search error:", err);
165
+ res.status(500).json({ error: "Internal server error" });
166
+ }
167
+ });
168
+
169
+ export default router;
@@ -0,0 +1,12 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { sseBroker } from "../services/sse-broker";
3
+
4
+ const router = Router();
5
+
6
+ // GET / — SSE endpoint
7
+ router.get("/", (req: Request, res: Response) => {
8
+ const filter = req.query.filter as string | undefined;
9
+ sseBroker.addClient(res, filter);
10
+ });
11
+
12
+ export default router;
@@ -0,0 +1,106 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { db } from "../db/connection";
3
+ import { listSnapshots } from "../db/queries";
4
+ import { diffTypes } from "../services/type-differ";
5
+ import { TypeNode } from "../types";
6
+
7
+ const router = Router();
8
+
9
+ // GET /:functionId — get type snapshots for a function
10
+ router.get("/:functionId", (req: Request, res: Response) => {
11
+ try {
12
+ const functionId = parseInt(req.params.functionId, 10);
13
+ if (isNaN(functionId)) {
14
+ res.status(400).json({ error: "Invalid functionId" });
15
+ return;
16
+ }
17
+
18
+ const { env, limit } = req.query;
19
+ const snapshots = listSnapshots(db, {
20
+ functionId,
21
+ env: env as string | undefined,
22
+ limit: limit ? parseInt(limit as string, 10) : undefined,
23
+ });
24
+
25
+ const parsed = snapshots.map((s) => ({
26
+ ...s,
27
+ args_type: tryParseJson(s.args_type as string),
28
+ return_type: tryParseJson(s.return_type as string),
29
+ sample_input: tryParseJson(s.sample_input as string),
30
+ sample_output: tryParseJson(s.sample_output as string),
31
+ }));
32
+
33
+ res.json({ snapshots: parsed });
34
+ } catch (err) {
35
+ console.error("List types error:", err);
36
+ res.status(500).json({ error: "Internal server error" });
37
+ }
38
+ });
39
+
40
+ // GET /:functionId/diff — diff between two snapshots or envs
41
+ router.get("/:functionId/diff", (req: Request, res: Response) => {
42
+ try {
43
+ const functionId = parseInt(req.params.functionId, 10);
44
+ if (isNaN(functionId)) {
45
+ res.status(400).json({ error: "Invalid functionId" });
46
+ return;
47
+ }
48
+
49
+ const { from, to, fromEnv, toEnv } = req.query;
50
+
51
+ let fromSnapshot: Record<string, unknown> | undefined;
52
+ let toSnapshot: Record<string, unknown> | undefined;
53
+
54
+ if (from && to) {
55
+ // Diff by snapshot IDs
56
+ const stmt = db.prepare(`SELECT * FROM type_snapshots WHERE id = ? AND function_id = ?`);
57
+ fromSnapshot = stmt.get(parseInt(from as string, 10), functionId) as Record<string, unknown> | undefined;
58
+ toSnapshot = stmt.get(parseInt(to as string, 10), functionId) as Record<string, unknown> | undefined;
59
+ } else if (fromEnv && toEnv) {
60
+ // Diff between envs (latest snapshot in each)
61
+ const stmt = db.prepare(`
62
+ SELECT * FROM type_snapshots
63
+ WHERE function_id = ? AND env = ?
64
+ ORDER BY observed_at DESC
65
+ LIMIT 1
66
+ `);
67
+ fromSnapshot = stmt.get(functionId, fromEnv as string) as Record<string, unknown> | undefined;
68
+ toSnapshot = stmt.get(functionId, toEnv as string) as Record<string, unknown> | undefined;
69
+ } else {
70
+ res.status(400).json({ error: "Provide 'from' and 'to' snapshot IDs, or 'fromEnv' and 'toEnv'" });
71
+ return;
72
+ }
73
+
74
+ if (!fromSnapshot || !toSnapshot) {
75
+ res.status(404).json({ error: "One or both snapshots not found" });
76
+ return;
77
+ }
78
+
79
+ const fromArgs = JSON.parse(fromSnapshot.args_type as string) as TypeNode;
80
+ const toArgs = JSON.parse(toSnapshot.args_type as string) as TypeNode;
81
+ const fromReturn = JSON.parse(fromSnapshot.return_type as string) as TypeNode;
82
+ const toReturn = JSON.parse(toSnapshot.return_type as string) as TypeNode;
83
+
84
+ const argsDiff = diffTypes(fromArgs, toArgs, "args");
85
+ const returnDiff = diffTypes(fromReturn, toReturn, "return");
86
+
87
+ res.json({
88
+ from: { id: fromSnapshot.id, env: fromSnapshot.env, observed_at: fromSnapshot.observed_at },
89
+ to: { id: toSnapshot.id, env: toSnapshot.env, observed_at: toSnapshot.observed_at },
90
+ diffs: [...argsDiff, ...returnDiff],
91
+ });
92
+ } catch (err) {
93
+ console.error("Type diff error:", err);
94
+ res.status(500).json({ error: "Internal server error" });
95
+ }
96
+ });
97
+
98
+ function tryParseJson(value: string): unknown {
99
+ try {
100
+ return JSON.parse(value);
101
+ } catch {
102
+ return value;
103
+ }
104
+ }
105
+
106
+ export default router;
package/src/server.ts ADDED
@@ -0,0 +1,40 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+
4
+ import ingestRouter from "./routes/ingest";
5
+ import functionsRouter from "./routes/functions";
6
+ import typesRouter from "./routes/types";
7
+ import errorsRouter from "./routes/errors";
8
+ import tailRouter from "./routes/tail";
9
+ import codegenRouter from "./routes/codegen";
10
+ import mockRouter from "./routes/mock";
11
+ import diffRouter from "./routes/diff";
12
+ import dashboardRouter from "./routes/dashboard";
13
+ import coverageRouter from "./routes/coverage";
14
+ import auditRouter from "./routes/audit";
15
+ import searchRouter from "./routes/search";
16
+
17
+ const app = express();
18
+
19
+ app.use(cors());
20
+ app.use(express.json({ limit: "5mb" }));
21
+
22
+ app.use("/api/ingest", ingestRouter);
23
+ app.use("/api/functions", functionsRouter);
24
+ app.use("/api/types", typesRouter);
25
+ app.use("/api/errors", errorsRouter);
26
+ app.use("/api/tail", tailRouter);
27
+ app.use("/api/codegen", codegenRouter);
28
+ app.use("/api/mock-config", mockRouter);
29
+ app.use("/api/diff", diffRouter);
30
+ app.use("/dashboard", dashboardRouter);
31
+ app.use("/api/coverage", coverageRouter);
32
+ app.use("/api/audit", auditRouter);
33
+ app.use("/api/search", searchRouter);
34
+
35
+ // Health check
36
+ app.get("/api/health", (_req, res) => {
37
+ res.json({ ok: true, timestamp: new Date().toISOString() });
38
+ });
39
+
40
+ export { app };