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.
- package/dist/db/connection.d.ts +3 -0
- package/dist/db/connection.js +16 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.js +51 -0
- package/dist/db/queries.d.ts +70 -0
- package/dist/db/queries.js +186 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -0
- package/dist/routes/audit.d.ts +2 -0
- package/dist/routes/audit.js +251 -0
- package/dist/routes/codegen.d.ts +2 -0
- package/dist/routes/codegen.js +224 -0
- package/dist/routes/coverage.d.ts +2 -0
- package/dist/routes/coverage.js +98 -0
- package/dist/routes/dashboard.d.ts +2 -0
- package/dist/routes/dashboard.js +433 -0
- package/dist/routes/diff.d.ts +2 -0
- package/dist/routes/diff.js +181 -0
- package/dist/routes/errors.d.ts +2 -0
- package/dist/routes/errors.js +86 -0
- package/dist/routes/functions.d.ts +2 -0
- package/dist/routes/functions.js +69 -0
- package/dist/routes/ingest.d.ts +2 -0
- package/dist/routes/ingest.js +111 -0
- package/dist/routes/mock.d.ts +2 -0
- package/dist/routes/mock.js +57 -0
- package/dist/routes/search.d.ts +2 -0
- package/dist/routes/search.js +136 -0
- package/dist/routes/tail.d.ts +2 -0
- package/dist/routes/tail.js +11 -0
- package/dist/routes/types.d.ts +2 -0
- package/dist/routes/types.js +97 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +40 -0
- package/dist/services/sse-broker.d.ts +10 -0
- package/dist/services/sse-broker.js +39 -0
- package/dist/services/type-differ.d.ts +2 -0
- package/dist/services/type-differ.js +126 -0
- package/dist/services/type-generator.d.ts +319 -0
- package/dist/services/type-generator.js +3207 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +2 -0
- package/package.json +22 -0
- package/src/db/connection.ts +16 -0
- package/src/db/migrations.ts +50 -0
- package/src/db/queries.ts +260 -0
- package/src/index.ts +11 -0
- package/src/routes/audit.ts +283 -0
- package/src/routes/codegen.ts +237 -0
- package/src/routes/coverage.ts +120 -0
- package/src/routes/dashboard.ts +435 -0
- package/src/routes/diff.ts +215 -0
- package/src/routes/errors.ts +91 -0
- package/src/routes/functions.ts +75 -0
- package/src/routes/ingest.ts +139 -0
- package/src/routes/mock.ts +66 -0
- package/src/routes/search.ts +169 -0
- package/src/routes/tail.ts +12 -0
- package/src/routes/types.ts +106 -0
- package/src/server.ts +40 -0
- package/src/services/sse-broker.ts +51 -0
- package/src/services/type-differ.ts +141 -0
- package/src/services/type-generator.ts +3853 -0
- package/src/types.ts +37 -0
- 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 };
|