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,237 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { db } from "../db/connection";
|
|
3
|
+
import { listFunctions, getFunctionByName, getLatestSnapshot } from "../db/queries";
|
|
4
|
+
import { generateAllTypes, generatePythonTypes, generateApiClient, generateOpenApiSpec, generateHandlerTypes, generateZodSchemas, generateReactQueryHooks, generateTypeGuards, generateMiddleware, generateMswHandlers, generateJsonSchemas, generateSwrHooks, generatePydanticModels, generateClassValidatorDtos, generateGraphqlSchema, generateTrpcRouter, generateAxiosClient, generateInlineAnnotations } from "../services/type-generator";
|
|
5
|
+
import { TypeNode } from "../types";
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
|
|
9
|
+
interface FunctionTypeData {
|
|
10
|
+
name: string;
|
|
11
|
+
argsType: TypeNode;
|
|
12
|
+
returnType: TypeNode;
|
|
13
|
+
module?: string;
|
|
14
|
+
env?: string;
|
|
15
|
+
observedAt?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function tryParseJson(value: string): unknown {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(value);
|
|
21
|
+
} catch {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Collect type data for all matching functions.
|
|
28
|
+
*/
|
|
29
|
+
function collectFunctionTypes(opts: {
|
|
30
|
+
functionName?: string;
|
|
31
|
+
env?: string;
|
|
32
|
+
}): FunctionTypeData[] {
|
|
33
|
+
const results: FunctionTypeData[] = [];
|
|
34
|
+
|
|
35
|
+
let functionRows: Record<string, unknown>[];
|
|
36
|
+
|
|
37
|
+
if (opts.functionName) {
|
|
38
|
+
functionRows = getFunctionByName(db, opts.functionName);
|
|
39
|
+
} else {
|
|
40
|
+
const listed = listFunctions(db, {
|
|
41
|
+
env: opts.env,
|
|
42
|
+
limit: 500,
|
|
43
|
+
});
|
|
44
|
+
functionRows = listed.rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const fn of functionRows) {
|
|
48
|
+
const functionId = fn.id as number;
|
|
49
|
+
const functionName = fn.function_name as string;
|
|
50
|
+
const moduleName = fn.module as string;
|
|
51
|
+
const environment = (fn.environment as string) || undefined;
|
|
52
|
+
|
|
53
|
+
const snapshot = getLatestSnapshot(db, functionId, opts.env);
|
|
54
|
+
if (!snapshot) continue;
|
|
55
|
+
|
|
56
|
+
const argsType = tryParseJson(snapshot.args_type as string) as TypeNode;
|
|
57
|
+
const returnType = tryParseJson(snapshot.return_type as string) as TypeNode;
|
|
58
|
+
|
|
59
|
+
if (!argsType || !returnType) continue;
|
|
60
|
+
|
|
61
|
+
results.push({
|
|
62
|
+
name: functionName,
|
|
63
|
+
argsType,
|
|
64
|
+
returnType,
|
|
65
|
+
module: moduleName,
|
|
66
|
+
env: (snapshot.env as string) || environment,
|
|
67
|
+
observedAt: snapshot.observed_at as string,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// GET / — generate types for all (or filtered) functions
|
|
75
|
+
router.get("/", (req: Request, res: Response) => {
|
|
76
|
+
try {
|
|
77
|
+
const { functionName, env, language } = req.query;
|
|
78
|
+
|
|
79
|
+
const functions = collectFunctionTypes({
|
|
80
|
+
functionName: functionName as string | undefined,
|
|
81
|
+
env: env as string | undefined,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (functions.length === 0) {
|
|
85
|
+
res.json({ types: "// No functions found matching the given filters.\n" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const format = (req.query.format as string)?.toLowerCase();
|
|
90
|
+
const isPython = (language as string)?.toLowerCase() === "python";
|
|
91
|
+
|
|
92
|
+
if (format === "snapshot") {
|
|
93
|
+
// Return raw type data as a portable JSON snapshot for `trickle check`
|
|
94
|
+
const snapshot = {
|
|
95
|
+
version: 1,
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
functions: functions.map((f) => ({
|
|
98
|
+
name: f.name,
|
|
99
|
+
module: f.module,
|
|
100
|
+
env: f.env,
|
|
101
|
+
argsType: f.argsType,
|
|
102
|
+
returnType: f.returnType,
|
|
103
|
+
})),
|
|
104
|
+
};
|
|
105
|
+
res.json(snapshot);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (format === "openapi") {
|
|
110
|
+
const title = (req.query.title as string) || undefined;
|
|
111
|
+
const version = (req.query.version as string) || undefined;
|
|
112
|
+
const serverUrl = (req.query.serverUrl as string) || undefined;
|
|
113
|
+
const spec = generateOpenApiSpec(functions, { title, version, serverUrl });
|
|
114
|
+
res.json(spec);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let types: string;
|
|
119
|
+
if (format === "handlers") {
|
|
120
|
+
types = generateHandlerTypes(functions);
|
|
121
|
+
res.json({ types });
|
|
122
|
+
return;
|
|
123
|
+
} else if (format === "zod") {
|
|
124
|
+
types = generateZodSchemas(functions);
|
|
125
|
+
res.json({ types });
|
|
126
|
+
return;
|
|
127
|
+
} else if (format === "react-query") {
|
|
128
|
+
types = generateReactQueryHooks(functions);
|
|
129
|
+
res.json({ types });
|
|
130
|
+
return;
|
|
131
|
+
} else if (format === "guards") {
|
|
132
|
+
types = generateTypeGuards(functions);
|
|
133
|
+
res.json({ types });
|
|
134
|
+
return;
|
|
135
|
+
} else if (format === "middleware") {
|
|
136
|
+
types = generateMiddleware(functions);
|
|
137
|
+
res.json({ types });
|
|
138
|
+
return;
|
|
139
|
+
} else if (format === "msw") {
|
|
140
|
+
types = generateMswHandlers(functions);
|
|
141
|
+
res.json({ types });
|
|
142
|
+
return;
|
|
143
|
+
} else if (format === "json-schema") {
|
|
144
|
+
types = generateJsonSchemas(functions);
|
|
145
|
+
res.json({ types });
|
|
146
|
+
return;
|
|
147
|
+
} else if (format === "swr") {
|
|
148
|
+
types = generateSwrHooks(functions);
|
|
149
|
+
res.json({ types });
|
|
150
|
+
return;
|
|
151
|
+
} else if (format === "pydantic") {
|
|
152
|
+
types = generatePydanticModels(functions);
|
|
153
|
+
res.json({ types });
|
|
154
|
+
return;
|
|
155
|
+
} else if (format === "class-validator") {
|
|
156
|
+
types = generateClassValidatorDtos(functions);
|
|
157
|
+
res.json({ types });
|
|
158
|
+
return;
|
|
159
|
+
} else if (format === "graphql") {
|
|
160
|
+
types = generateGraphqlSchema(functions);
|
|
161
|
+
res.json({ types });
|
|
162
|
+
return;
|
|
163
|
+
} else if (format === "trpc") {
|
|
164
|
+
types = generateTrpcRouter(functions);
|
|
165
|
+
res.json({ types });
|
|
166
|
+
return;
|
|
167
|
+
} else if (format === "axios") {
|
|
168
|
+
types = generateAxiosClient(functions);
|
|
169
|
+
res.json({ types });
|
|
170
|
+
return;
|
|
171
|
+
} else if (format === "annotate") {
|
|
172
|
+
const lang = (language as string)?.toLowerCase() === "python" ? "python" : "typescript";
|
|
173
|
+
const annotations = generateInlineAnnotations(functions, lang as "typescript" | "python");
|
|
174
|
+
res.json({ annotations });
|
|
175
|
+
return;
|
|
176
|
+
} else if (format === "stubs") {
|
|
177
|
+
// Group functions by module and return per-module type stubs
|
|
178
|
+
const byModule: Record<string, typeof functions> = {};
|
|
179
|
+
for (const fn of functions) {
|
|
180
|
+
const mod = fn.module || "_default";
|
|
181
|
+
if (!byModule[mod]) byModule[mod] = [];
|
|
182
|
+
byModule[mod].push(fn);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const stubs: Record<string, { ts: string; python: string }> = {};
|
|
186
|
+
for (const [mod, fns] of Object.entries(byModule)) {
|
|
187
|
+
stubs[mod] = {
|
|
188
|
+
ts: generateAllTypes(fns),
|
|
189
|
+
python: generatePythonTypes(fns),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
res.json({ stubs });
|
|
193
|
+
return;
|
|
194
|
+
} else if (format === "client") {
|
|
195
|
+
types = generateApiClient(functions);
|
|
196
|
+
} else if (isPython) {
|
|
197
|
+
types = generatePythonTypes(functions);
|
|
198
|
+
} else {
|
|
199
|
+
types = generateAllTypes(functions);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
res.json({ types });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error("Codegen error:", err);
|
|
205
|
+
res.status(500).json({ error: "Internal server error" });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// GET /:functionName — generate types for a specific function
|
|
210
|
+
router.get("/:functionName", (req: Request, res: Response) => {
|
|
211
|
+
try {
|
|
212
|
+
const { functionName } = req.params;
|
|
213
|
+
const { env, language } = req.query;
|
|
214
|
+
|
|
215
|
+
const functions = collectFunctionTypes({
|
|
216
|
+
functionName,
|
|
217
|
+
env: env as string | undefined,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (functions.length === 0) {
|
|
221
|
+
res.status(404).json({ error: `No function found matching "${functionName}"` });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const isPython = (language as string)?.toLowerCase() === "python";
|
|
226
|
+
const types = isPython
|
|
227
|
+
? generatePythonTypes(functions)
|
|
228
|
+
: generateAllTypes(functions);
|
|
229
|
+
|
|
230
|
+
res.json({ types });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error("Codegen error:", err);
|
|
233
|
+
res.status(500).json({ error: "Internal server error" });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
export default router;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { db } from "../db/connection";
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/coverage — Type observation coverage report.
|
|
8
|
+
*
|
|
9
|
+
* Returns per-function stats: snapshot count, variant count,
|
|
10
|
+
* freshness, error count, and an overall health score.
|
|
11
|
+
*/
|
|
12
|
+
router.get("/", (req: Request, res: Response) => {
|
|
13
|
+
try {
|
|
14
|
+
const { env, stale_hours } = req.query;
|
|
15
|
+
const staleThresholdHours = stale_hours ? parseInt(stale_hours as string, 10) : 24;
|
|
16
|
+
|
|
17
|
+
// Get all functions
|
|
18
|
+
let functionsQuery = `SELECT * FROM functions`;
|
|
19
|
+
const params: unknown[] = [];
|
|
20
|
+
if (env) {
|
|
21
|
+
functionsQuery += ` WHERE environment = ?`;
|
|
22
|
+
params.push(env);
|
|
23
|
+
}
|
|
24
|
+
functionsQuery += ` ORDER BY last_seen_at DESC`;
|
|
25
|
+
|
|
26
|
+
const functions = db.prepare(functionsQuery).all(...params) as Array<{
|
|
27
|
+
id: number;
|
|
28
|
+
function_name: string;
|
|
29
|
+
module: string;
|
|
30
|
+
language: string;
|
|
31
|
+
environment: string;
|
|
32
|
+
first_seen_at: string;
|
|
33
|
+
last_seen_at: string;
|
|
34
|
+
}>;
|
|
35
|
+
|
|
36
|
+
// Per-function stats
|
|
37
|
+
const snapshotCountStmt = db.prepare(
|
|
38
|
+
`SELECT COUNT(*) as count FROM type_snapshots WHERE function_id = ?`,
|
|
39
|
+
);
|
|
40
|
+
const variantCountStmt = db.prepare(
|
|
41
|
+
`SELECT COUNT(DISTINCT type_hash) as count FROM type_snapshots WHERE function_id = ?`,
|
|
42
|
+
);
|
|
43
|
+
const errorCountStmt = db.prepare(
|
|
44
|
+
`SELECT COUNT(*) as count FROM errors WHERE function_id = ?`,
|
|
45
|
+
);
|
|
46
|
+
const latestSnapshotStmt = db.prepare(
|
|
47
|
+
`SELECT observed_at FROM type_snapshots WHERE function_id = ? ORDER BY observed_at DESC LIMIT 1`,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const now = new Date();
|
|
51
|
+
const staleThreshold = new Date(now.getTime() - staleThresholdHours * 60 * 60 * 1000);
|
|
52
|
+
|
|
53
|
+
const entries = functions.map((fn) => {
|
|
54
|
+
const snapshots = (snapshotCountStmt.get(fn.id) as { count: number }).count;
|
|
55
|
+
const variants = (variantCountStmt.get(fn.id) as { count: number }).count;
|
|
56
|
+
const errors = (errorCountStmt.get(fn.id) as { count: number }).count;
|
|
57
|
+
const latestRow = latestSnapshotStmt.get(fn.id) as { observed_at: string } | undefined;
|
|
58
|
+
|
|
59
|
+
const lastObserved = latestRow ? latestRow.observed_at : fn.last_seen_at;
|
|
60
|
+
const lastObservedDate = new Date(lastObserved);
|
|
61
|
+
const isStale = lastObservedDate < staleThreshold;
|
|
62
|
+
const hasTypes = snapshots > 0;
|
|
63
|
+
const hasMultipleVariants = variants > 1;
|
|
64
|
+
const hasErrors = errors > 0;
|
|
65
|
+
|
|
66
|
+
// Per-function health: 0-100
|
|
67
|
+
let health = 0;
|
|
68
|
+
if (hasTypes) health += 60; // Has type observations
|
|
69
|
+
if (!isStale) health += 20; // Recently observed
|
|
70
|
+
if (!hasErrors) health += 10; // No errors
|
|
71
|
+
if (!hasMultipleVariants) health += 10; // Consistent types (single variant)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
functionName: fn.function_name,
|
|
75
|
+
module: fn.module,
|
|
76
|
+
language: fn.language,
|
|
77
|
+
environment: fn.environment,
|
|
78
|
+
firstSeen: fn.first_seen_at,
|
|
79
|
+
lastObserved,
|
|
80
|
+
snapshots,
|
|
81
|
+
variants,
|
|
82
|
+
errors,
|
|
83
|
+
isStale,
|
|
84
|
+
hasTypes,
|
|
85
|
+
health,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Aggregate stats
|
|
90
|
+
const total = entries.length;
|
|
91
|
+
const withTypes = entries.filter((e) => e.hasTypes).length;
|
|
92
|
+
const staleCount = entries.filter((e) => e.isStale).length;
|
|
93
|
+
const freshCount = entries.filter((e) => !e.isStale).length;
|
|
94
|
+
const withErrors = entries.filter((e) => e.errors > 0).length;
|
|
95
|
+
const withMultipleVariants = entries.filter((e) => e.variants > 1).length;
|
|
96
|
+
const overallHealth = total > 0
|
|
97
|
+
? Math.round(entries.reduce((sum, e) => sum + e.health, 0) / total)
|
|
98
|
+
: 0;
|
|
99
|
+
|
|
100
|
+
res.json({
|
|
101
|
+
summary: {
|
|
102
|
+
total,
|
|
103
|
+
withTypes,
|
|
104
|
+
withoutTypes: total - withTypes,
|
|
105
|
+
fresh: freshCount,
|
|
106
|
+
stale: staleCount,
|
|
107
|
+
withErrors,
|
|
108
|
+
withMultipleVariants,
|
|
109
|
+
health: overallHealth,
|
|
110
|
+
staleThresholdHours,
|
|
111
|
+
},
|
|
112
|
+
entries,
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error("Coverage error:", err);
|
|
116
|
+
res.status(500).json({ error: "Internal server error" });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export default router;
|