@apisurf/wireui 0.1.2
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/LICENSE +15 -0
- package/README.md +157 -0
- package/bin/wireui.mjs +233 -0
- package/dist/nitro.json +17 -0
- package/dist/public/assets/collections-CLHcfRfi.js +1 -0
- package/dist/public/assets/globals-CZiNFGaK.css +1 -0
- package/dist/public/assets/index-D5VFd_3H.js +14 -0
- package/dist/public/assets/kv-table-oB5gcq0i.js +2 -0
- package/dist/public/assets/query-BKAhHNIi.js +19 -0
- package/dist/public/assets/requests._requestId-Bu7E2eoI.js +1 -0
- package/dist/public/assets/routes-DDY2CfzA.js +1 -0
- package/dist/public/assets/runs._runId-CzwtVjYq.js +1 -0
- package/dist/public/assets/scoped-DB8raIWx.js +1 -0
- package/dist/public/assets/split-Br1RX2I4.js +1 -0
- package/dist/public/favicon.svg +1 -0
- package/dist/server/_chunks/ssr-renderer.mjs +26 -0
- package/dist/server/_libs/@tanstack/react-router+[...].mjs +15246 -0
- package/dist/server/_libs/h3+rou3+srvx.mjs +2086 -0
- package/dist/server/_libs/hookable.mjs +41 -0
- package/dist/server/_libs/tanstack__history.mjs +369 -0
- package/dist/server/_libs/ufo.mjs +64 -0
- package/dist/server/_runtime.mjs +35 -0
- package/dist/server/_ssr/api-Dw1Zb_PK.mjs +931 -0
- package/dist/server/_ssr/collections-CqidTQpw.mjs +495 -0
- package/dist/server/_ssr/empty-plugin-adapters-CCuiUfKt.mjs +5 -0
- package/dist/server/_ssr/kv-table-DQUkoItO.mjs +458 -0
- package/dist/server/_ssr/query-D4so9xiY.mjs +416 -0
- package/dist/server/_ssr/requests._requestId-BFukRwqL.mjs +942 -0
- package/dist/server/_ssr/router-CPcgc_kh.mjs +3176 -0
- package/dist/server/_ssr/routes-DaV9vGHn.mjs +375 -0
- package/dist/server/_ssr/runs._runId-Df_LmPHa.mjs +836 -0
- package/dist/server/_ssr/scoped-CnQ56K8h.mjs +42 -0
- package/dist/server/_ssr/split-DWrMIs7K.mjs +115 -0
- package/dist/server/_ssr/ssr.mjs +6827 -0
- package/dist/server/_ssr/start-zk_ZfrWl.mjs +4 -0
- package/dist/server/_tanstack-start-manifest_v-CARKjHfs.mjs +50 -0
- package/dist/server/index.mjs +368 -0
- package/package.json +71 -0
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
import { TSS_SERVER_FUNCTION, createServerFn } from "./ssr.mjs";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/api-Dw1Zb_PK.js
|
|
7
|
+
var createServerRpc = (serverFnMeta, splitImportFn) => {
|
|
8
|
+
const url = "/_serverFn/" + serverFnMeta.id;
|
|
9
|
+
return Object.assign(splitImportFn, {
|
|
10
|
+
url,
|
|
11
|
+
serverFnMeta,
|
|
12
|
+
[TSS_SERVER_FUNCTION]: true
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
/** Schema version this build writes. Equals the number of migrations. */
|
|
16
|
+
var SCHEMA_VERSION = 4;
|
|
17
|
+
/** Default database filename, created in the working directory. */
|
|
18
|
+
var DEFAULT_DB_FILENAME = "wire.sqlite";
|
|
19
|
+
function resolveDbPath(options = {}) {
|
|
20
|
+
const cwd = options.cwd ?? process.cwd();
|
|
21
|
+
const raw = options.path ?? DEFAULT_DB_FILENAME;
|
|
22
|
+
return isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Types for view columns SQLite cannot type on its own.
|
|
26
|
+
*
|
|
27
|
+
* `pragma_table_info` on a view resolves each column back to its origin
|
|
28
|
+
* column's declared type — `v_requests.status` reports INTEGER — but returns an
|
|
29
|
+
* empty string for columns that are expressions, because there is no origin
|
|
30
|
+
* column to inherit from. That is a dozen columns across the views, all of
|
|
31
|
+
* them either a correlated subquery counter or a CASE/concatenation.
|
|
32
|
+
* better-sqlite3's `statement.columns()` reports null for exactly the same set
|
|
33
|
+
* and for the same reason, so there is nothing to fall back to but this map.
|
|
34
|
+
*
|
|
35
|
+
* Keep it in step with the views in ./schema.ts. {@link untypedColumns} below
|
|
36
|
+
* turns a drift into a test failure rather than a silently untyped column.
|
|
37
|
+
*/
|
|
38
|
+
var COMPUTED_TYPES = {
|
|
39
|
+
"v_runs.request_count": "INTEGER",
|
|
40
|
+
"v_runs.error_count": "INTEGER",
|
|
41
|
+
"v_runs.connection_count": "INTEGER",
|
|
42
|
+
"v_runs.message_count": "INTEGER",
|
|
43
|
+
"v_runs.check_count": "INTEGER",
|
|
44
|
+
"v_runs.failed_check_count": "INTEGER",
|
|
45
|
+
"v_runs.total_duration_ms": "REAL",
|
|
46
|
+
"v_runs.total_bytes": "INTEGER",
|
|
47
|
+
"v_requests.message_count": "INTEGER",
|
|
48
|
+
"v_messages.endpoint": "TEXT",
|
|
49
|
+
"v_messages.direction": "TEXT",
|
|
50
|
+
"v_messages.text": "TEXT",
|
|
51
|
+
"v_headers.kind": "TEXT",
|
|
52
|
+
"v_bodies.text": "TEXT"
|
|
53
|
+
};
|
|
54
|
+
/** One line per view, for `wire schema` with no argument. */
|
|
55
|
+
var RELATION_DESCRIPTIONS = {
|
|
56
|
+
v_runs: "One row per execute, counts computed on read.",
|
|
57
|
+
v_requests: "One HTTP request, or one WebSocket/SSE connection.",
|
|
58
|
+
v_headers: "Request and response headers, in wire order.",
|
|
59
|
+
v_bodies: "Deduped payloads, content-addressed by SHA-256.",
|
|
60
|
+
v_messages: "WebSocket frames and SSE events, with payloads.",
|
|
61
|
+
v_checks: "Assertions a run made, from check() and assert().",
|
|
62
|
+
tags: "Per-request key/value tags, from execute --tag.",
|
|
63
|
+
body_fts: "FTS5 index over text payloads. Join on body_id."
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Every relation worth querying.
|
|
67
|
+
*
|
|
68
|
+
* Views only by default, because that is the documented surface and the raw
|
|
69
|
+
* tables store interned ids rather than the strings anyone wants to read.
|
|
70
|
+
* `all` adds the base tables — minus FTS5's shadow tables, which are storage
|
|
71
|
+
* for `body_fts` and not something to query directly.
|
|
72
|
+
*/
|
|
73
|
+
function listRelations(db, options = {}) {
|
|
74
|
+
const types = options.all ? ["view", "table"] : ["view"];
|
|
75
|
+
return db.prepare(`SELECT name, type FROM sqlite_master
|
|
76
|
+
WHERE type IN (${types.map(() => "?").join(", ")})
|
|
77
|
+
AND name NOT LIKE 'sqlite_%'
|
|
78
|
+
AND name NOT LIKE 'body_fts_%'
|
|
79
|
+
ORDER BY type DESC, name`).all(...types).map((r) => ({
|
|
80
|
+
name: r.name,
|
|
81
|
+
type: r.type,
|
|
82
|
+
description: RELATION_DESCRIPTIONS[r.name] ?? null
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Columns of a view or table, with types.
|
|
87
|
+
*
|
|
88
|
+
* Uses the `pragma_table_info` table-valued function rather than the `PRAGMA`
|
|
89
|
+
* statement so the relation name is a bound parameter and never string
|
|
90
|
+
* interpolation.
|
|
91
|
+
*/
|
|
92
|
+
function describeRelation(db, name) {
|
|
93
|
+
return db.prepare(`SELECT name, type FROM pragma_table_info(?) ORDER BY cid`).all(name).map((r) => {
|
|
94
|
+
const declared = r.type.trim();
|
|
95
|
+
if (declared) return {
|
|
96
|
+
name: r.name,
|
|
97
|
+
type: declared,
|
|
98
|
+
computed: false
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
name: r.name,
|
|
102
|
+
type: COMPUTED_TYPES[`${name}.${r.name}`] ?? "",
|
|
103
|
+
computed: true
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Runs, newest first. */
|
|
108
|
+
function listRuns(db, limit = 200) {
|
|
109
|
+
return db.prepare(`SELECT * FROM v_runs ORDER BY started_at DESC LIMIT ?`).all(limit);
|
|
110
|
+
}
|
|
111
|
+
function getRun$1(db, id) {
|
|
112
|
+
return db.prepare(`SELECT * FROM v_runs WHERE id = ?`).get(id) ?? null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Every script that has been run, one row each, most recently run first.
|
|
116
|
+
*
|
|
117
|
+
* Grouping is on `entry` — the absolute path of the executed file — because
|
|
118
|
+
* that is the only stable identity a script has. `--label` is per run and the
|
|
119
|
+
* default label is a basename, which collides the moment two folders hold a
|
|
120
|
+
* `sync.ts`.
|
|
121
|
+
*
|
|
122
|
+
* `label`, `id` and `exit_status` are bare columns under a GROUP BY, which
|
|
123
|
+
* SQLite answers from the row that produced the query's single MAX() — so they
|
|
124
|
+
* describe the latest run, not an arbitrary one. That is a documented SQLite
|
|
125
|
+
* guarantee (one min/max aggregate, bare columns come from that row), not an
|
|
126
|
+
* accident of the query plan.
|
|
127
|
+
*/
|
|
128
|
+
function listScripts(db) {
|
|
129
|
+
return db.prepare(`SELECT entry,
|
|
130
|
+
cwd,
|
|
131
|
+
label,
|
|
132
|
+
id AS last_run_id,
|
|
133
|
+
MAX(started_at) AS last_started_at,
|
|
134
|
+
exit_status AS last_exit_status,
|
|
135
|
+
COUNT(*) AS run_count,
|
|
136
|
+
SUM(exit_status = 'error') AS failed_run_count,
|
|
137
|
+
SUM(request_count) AS request_count,
|
|
138
|
+
SUM(connection_count) AS connection_count,
|
|
139
|
+
SUM(message_count) AS message_count,
|
|
140
|
+
SUM(error_count) AS error_count,
|
|
141
|
+
SUM(check_count) AS check_count,
|
|
142
|
+
SUM(failed_check_count) AS failed_check_count,
|
|
143
|
+
SUM(total_bytes) AS total_bytes
|
|
144
|
+
FROM v_runs
|
|
145
|
+
GROUP BY entry
|
|
146
|
+
ORDER BY last_started_at DESC`).all();
|
|
147
|
+
}
|
|
148
|
+
/** Runs of one script, newest first — the sessions behind a script row. */
|
|
149
|
+
function listRunsForEntry(db, entry, limit = 200) {
|
|
150
|
+
return db.prepare(`SELECT * FROM v_runs WHERE entry = ? ORDER BY started_at DESC LIMIT ?`).all(entry, limit);
|
|
151
|
+
}
|
|
152
|
+
/** Requests for a run, in capture order. */
|
|
153
|
+
function listRequests(db, runId) {
|
|
154
|
+
return db.prepare(`SELECT id, seq, ts, kind, message_count, method, host, path, status, ok, error_name,
|
|
155
|
+
duration_ms, request_bytes, response_bytes
|
|
156
|
+
FROM v_requests WHERE run_id = ? ORDER BY seq ASC`).all(runId);
|
|
157
|
+
}
|
|
158
|
+
function getRequestDetail(db, id) {
|
|
159
|
+
const request = db.prepare(`SELECT * FROM v_requests WHERE id = ?`).get(id);
|
|
160
|
+
if (!request) return null;
|
|
161
|
+
const headers = db.prepare(`SELECT kind, name, value FROM v_headers WHERE request_id = ? ORDER BY kind, ord`).all(id);
|
|
162
|
+
const tags = db.prepare(`SELECT key, value FROM tags WHERE request_id = ? ORDER BY key`).all(id);
|
|
163
|
+
const split = (kind) => headers.filter((h) => h.kind === kind).map((h) => [h.name, h.value]);
|
|
164
|
+
return {
|
|
165
|
+
request,
|
|
166
|
+
requestHeaders: split("request"),
|
|
167
|
+
responseHeaders: split("response"),
|
|
168
|
+
requestBody: loadBody(db, request.request_body_id, request.request_content_type),
|
|
169
|
+
responseBody: loadBody(db, request.response_body_id, request.response_content_type),
|
|
170
|
+
tags,
|
|
171
|
+
messages: request.kind === "http" ? [] : listMessages(db, id)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Every tag on a run's requests, once each.
|
|
176
|
+
*
|
|
177
|
+
* `execute --tag` attaches the same pairs to every capture in the run, so read
|
|
178
|
+
* back per request they are the same two or three facts repeated a hundred
|
|
179
|
+
* times — they describe the run, not the call. The DISTINCT is what makes this
|
|
180
|
+
* honest for the one case that is not uniform: a library caller can configure
|
|
181
|
+
* two recorders differently inside one run, and then the union is the answer.
|
|
182
|
+
*/
|
|
183
|
+
function listRunTags(db, runId) {
|
|
184
|
+
return db.prepare(`SELECT DISTINCT t.key, t.value
|
|
185
|
+
FROM tags t JOIN requests r ON r.id = t.request_id
|
|
186
|
+
WHERE r.run_id = ?
|
|
187
|
+
ORDER BY t.key, t.value`).all(runId);
|
|
188
|
+
}
|
|
189
|
+
/** Every assertion a run made, in the order the script made them. */
|
|
190
|
+
function listChecks(db, runId) {
|
|
191
|
+
return db.prepare(`SELECT id, run_id, seq, ts, name, mode, status, message
|
|
192
|
+
FROM v_checks WHERE run_id = ? ORDER BY seq ASC`).all(runId);
|
|
193
|
+
}
|
|
194
|
+
/** Every frame or event on one connection, in the order it happened. */
|
|
195
|
+
function listMessages(db, requestId) {
|
|
196
|
+
return db.prepare(`SELECT id, request_id, seq, ts, direction, name, code, event_id,
|
|
197
|
+
bytes, body_id, truncated, text
|
|
198
|
+
FROM v_messages WHERE request_id = ? ORDER BY seq ASC`).all(requestId);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Image types worth inlining into the page.
|
|
202
|
+
*
|
|
203
|
+
* An allow-list rather than a `image/*` prefix test: the content type recorded
|
|
204
|
+
* off the wire ends up verbatim in a `data:` URL, and only these are types a
|
|
205
|
+
* browser will draw.
|
|
206
|
+
*/
|
|
207
|
+
var IMAGE_MIME = /* @__PURE__ */ new Set([
|
|
208
|
+
"image/apng",
|
|
209
|
+
"image/avif",
|
|
210
|
+
"image/bmp",
|
|
211
|
+
"image/gif",
|
|
212
|
+
"image/jpeg",
|
|
213
|
+
"image/png",
|
|
214
|
+
"image/svg+xml",
|
|
215
|
+
"image/webp",
|
|
216
|
+
"image/x-icon",
|
|
217
|
+
"image/vnd.microsoft.icon"
|
|
218
|
+
]);
|
|
219
|
+
/**
|
|
220
|
+
* Largest image inlined as a `data:` URL.
|
|
221
|
+
*
|
|
222
|
+
* Base64 costs a third on top, and the encoded string travels inside the page's
|
|
223
|
+
* loader payload — past a couple of megabytes the picture costs more than it is
|
|
224
|
+
* worth. The capture cap (1 MiB by default) usually bites first.
|
|
225
|
+
*/
|
|
226
|
+
var MAX_INLINE_IMAGE_BYTES = 2e6;
|
|
227
|
+
/** `image/png; charset=x` → `image/png`, or null when it is not a drawable image. */
|
|
228
|
+
function imageMime(contentType) {
|
|
229
|
+
if (!contentType) return null;
|
|
230
|
+
const mime = contentType.split(";")[0].trim().toLowerCase();
|
|
231
|
+
return IMAGE_MIME.has(mime) ? mime : null;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Fetch a payload and shape it for display: decode text, parse JSON, inline an
|
|
235
|
+
* image, and say plainly why there is nothing to show when there isn't.
|
|
236
|
+
*
|
|
237
|
+
* `kind` is the whole answer — the UI switches on it and never re-sniffs the
|
|
238
|
+
* content type.
|
|
239
|
+
*/
|
|
240
|
+
function loadBody(db, bodyId, contentType) {
|
|
241
|
+
const base = {
|
|
242
|
+
present: false,
|
|
243
|
+
text: null,
|
|
244
|
+
json: null,
|
|
245
|
+
dataUrl: null,
|
|
246
|
+
contentType,
|
|
247
|
+
size: null,
|
|
248
|
+
truncated: false
|
|
249
|
+
};
|
|
250
|
+
const empty = (note) => ({
|
|
251
|
+
...base,
|
|
252
|
+
kind: "empty",
|
|
253
|
+
note
|
|
254
|
+
});
|
|
255
|
+
if (bodyId == null) return empty("No body captured (absent or redacted).");
|
|
256
|
+
const row = db.prepare(`SELECT size, stored_size, truncated, is_text, content FROM bodies WHERE id = ?`).get(bodyId);
|
|
257
|
+
if (!row) return empty("Body reference is set but the payload row is missing.");
|
|
258
|
+
const truncated = row.truncated === 1;
|
|
259
|
+
const stored = {
|
|
260
|
+
...base,
|
|
261
|
+
present: true,
|
|
262
|
+
size: row.size,
|
|
263
|
+
truncated
|
|
264
|
+
};
|
|
265
|
+
const binary = (note) => ({
|
|
266
|
+
...stored,
|
|
267
|
+
kind: "binary",
|
|
268
|
+
note
|
|
269
|
+
});
|
|
270
|
+
const mime = imageMime(contentType);
|
|
271
|
+
if (mime) {
|
|
272
|
+
if (truncated) return binary(`Image truncated at ${row.stored_size} of ${row.size} bytes — too incomplete to draw.`);
|
|
273
|
+
if (row.stored_size > MAX_INLINE_IMAGE_BYTES) return binary(`Image is ${row.stored_size} bytes — too large to inline.`);
|
|
274
|
+
return {
|
|
275
|
+
...stored,
|
|
276
|
+
kind: "image",
|
|
277
|
+
text: row.is_text === 1 ? row.content.toString("utf8") : null,
|
|
278
|
+
dataUrl: `data:${mime};base64,${row.content.toString("base64")}`,
|
|
279
|
+
note: null
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (row.is_text !== 1) return binary(`Binary body — ${row.stored_size} bytes stored, not shown.`);
|
|
283
|
+
const text = row.content.toString("utf8");
|
|
284
|
+
if (contentType && /json/i.test(contentType)) try {
|
|
285
|
+
const json = JSON.parse(text);
|
|
286
|
+
if (json !== null && typeof json === "object") return {
|
|
287
|
+
...stored,
|
|
288
|
+
kind: "json",
|
|
289
|
+
json,
|
|
290
|
+
note: null
|
|
291
|
+
};
|
|
292
|
+
} catch {
|
|
293
|
+
return {
|
|
294
|
+
...stored,
|
|
295
|
+
kind: "text",
|
|
296
|
+
text,
|
|
297
|
+
note: "Content-Type is JSON but the body did not parse; shown as-is."
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
...stored,
|
|
302
|
+
kind: "text",
|
|
303
|
+
text,
|
|
304
|
+
note: null
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Per-run aggregates.
|
|
309
|
+
*
|
|
310
|
+
* SQLite has no `quantile()`, so percentiles use the nearest-rank definition:
|
|
311
|
+
* order the durations, take the row at ceil(q * n). One ordered CTE serves all
|
|
312
|
+
* three, and with `idx_requests_run_seq` narrowing to the run first the sort is
|
|
313
|
+
* over that run's rows only.
|
|
314
|
+
*
|
|
315
|
+
* Latency figures cover HTTP requests only. A connection's `duration_ms` is how
|
|
316
|
+
* long it stayed open, so letting one WebSocket sit in the same distribution
|
|
317
|
+
* would make "p95" mean nothing. `latencySamples` says how many rows they are
|
|
318
|
+
* computed over; the status mix and totals still count everything.
|
|
319
|
+
*/
|
|
320
|
+
function getRunAnalytics(db, runId) {
|
|
321
|
+
const quantiles = db.prepare(`WITH d AS (
|
|
322
|
+
SELECT duration_ms, ROW_NUMBER() OVER (ORDER BY duration_ms) AS rn
|
|
323
|
+
FROM requests WHERE run_id = @run AND kind = 'http'
|
|
324
|
+
), c AS (SELECT COUNT(*) AS n FROM d)
|
|
325
|
+
SELECT
|
|
326
|
+
(SELECT COUNT(*) FROM requests WHERE run_id = @run) AS total,
|
|
327
|
+
(SELECT n FROM c) AS latency_samples,
|
|
328
|
+
(SELECT duration_ms FROM d
|
|
329
|
+
WHERE rn = MAX(1, CAST((SELECT n FROM c) * 0.50 AS INTEGER))) AS p50,
|
|
330
|
+
(SELECT duration_ms FROM d
|
|
331
|
+
WHERE rn = MAX(1, CAST((SELECT n FROM c) * 0.95 AS INTEGER))) AS p95,
|
|
332
|
+
(SELECT duration_ms FROM d
|
|
333
|
+
WHERE rn = MAX(1, CAST((SELECT n FROM c) * 0.99 AS INTEGER))) AS p99`).get({ run: runId });
|
|
334
|
+
const statusMix = db.prepare(`SELECT CASE
|
|
335
|
+
WHEN status IS NULL THEN 'error'
|
|
336
|
+
ELSE CAST(status / 100 AS TEXT) || 'xx'
|
|
337
|
+
END AS status_class,
|
|
338
|
+
COUNT(*) AS count
|
|
339
|
+
FROM requests WHERE run_id = ?
|
|
340
|
+
GROUP BY status_class ORDER BY status_class`).all(runId);
|
|
341
|
+
const slowest = db.prepare(`SELECT id, method, host, path, duration_ms FROM v_requests
|
|
342
|
+
WHERE run_id = ? AND kind = 'http' ORDER BY duration_ms DESC LIMIT 5`).all(runId);
|
|
343
|
+
const byHost = db.prepare(`WITH d AS (
|
|
344
|
+
SELECT host_id, duration_ms, status, error_name,
|
|
345
|
+
ROW_NUMBER() OVER (PARTITION BY host_id ORDER BY duration_ms) AS rn,
|
|
346
|
+
COUNT(*) OVER (PARTITION BY host_id) AS n
|
|
347
|
+
FROM requests WHERE run_id = @run AND kind = 'http'
|
|
348
|
+
)
|
|
349
|
+
SELECT h.host AS host,
|
|
350
|
+
MAX(d.n) AS count,
|
|
351
|
+
MAX(CASE WHEN d.rn = MAX(1, CAST(d.n * 0.95 AS INTEGER))
|
|
352
|
+
THEN d.duration_ms END) AS p95,
|
|
353
|
+
SUM(CASE WHEN d.error_name IS NOT NULL OR d.status >= 400
|
|
354
|
+
THEN 1 ELSE 0 END) AS errors
|
|
355
|
+
FROM d JOIN hosts h ON h.id = d.host_id
|
|
356
|
+
GROUP BY d.host_id ORDER BY count DESC`).all({ run: runId });
|
|
357
|
+
return {
|
|
358
|
+
total: quantiles.total,
|
|
359
|
+
latencySamples: quantiles.latency_samples,
|
|
360
|
+
statusMix,
|
|
361
|
+
p50: quantiles.latency_samples > 0 ? quantiles.p50 : null,
|
|
362
|
+
p95: quantiles.latency_samples > 0 ? quantiles.p95 : null,
|
|
363
|
+
p99: quantiles.latency_samples > 0 ? quantiles.p99 : null,
|
|
364
|
+
slowest,
|
|
365
|
+
byHost
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
var cached = null;
|
|
369
|
+
/** Absolute path of the database this server is showing. */
|
|
370
|
+
function dbPath() {
|
|
371
|
+
const raw = process.env.WIRE_DB;
|
|
372
|
+
return resolveDbPath({
|
|
373
|
+
...raw ? { path: raw } : {},
|
|
374
|
+
cwd: process.cwd()
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The open database, or null when the file does not exist yet.
|
|
379
|
+
*
|
|
380
|
+
* Null is a normal state, not an error: someone can start the UI before their
|
|
381
|
+
* first run. Pages render an empty state for it rather than a 500. A missing
|
|
382
|
+
* file is re-checked on every call, so the first `wire execute` after start-up
|
|
383
|
+
* is picked up without a restart.
|
|
384
|
+
*
|
|
385
|
+
* The connection is opened `readonly` and additionally `query_only`, which is
|
|
386
|
+
* what makes the SQL console safe to offer: a write fails in SQLite itself, not
|
|
387
|
+
* in a check this module could get wrong. Migrations are never applied — a
|
|
388
|
+
* viewer must not change the file it is showing.
|
|
389
|
+
*/
|
|
390
|
+
function getDb() {
|
|
391
|
+
if (cached) return cached;
|
|
392
|
+
const file = dbPath();
|
|
393
|
+
if (!existsSync(file)) return null;
|
|
394
|
+
const db = new Database(file, {
|
|
395
|
+
readonly: true,
|
|
396
|
+
fileMustExist: true
|
|
397
|
+
});
|
|
398
|
+
db.pragma("query_only = ON");
|
|
399
|
+
db.pragma("temp_store = MEMORY");
|
|
400
|
+
cached = db;
|
|
401
|
+
return cached;
|
|
402
|
+
}
|
|
403
|
+
/** The schema version of the file being read, or 0 when there is nothing to read. */
|
|
404
|
+
function fileSchemaVersion(db) {
|
|
405
|
+
if (!db) return 0;
|
|
406
|
+
return db.pragma("user_version", { simple: true });
|
|
407
|
+
}
|
|
408
|
+
function schemaState(db) {
|
|
409
|
+
const version = fileSchemaVersion(db);
|
|
410
|
+
if (!db || version === 0) return "missing";
|
|
411
|
+
if (version < SCHEMA_VERSION) return "stale";
|
|
412
|
+
return version > SCHEMA_VERSION ? "newer" : "current";
|
|
413
|
+
}
|
|
414
|
+
var EXPECTED_SCHEMA_VERSION = SCHEMA_VERSION;
|
|
415
|
+
/** The database's size on disk, WAL included — what a recording costs. */
|
|
416
|
+
function fileBytes() {
|
|
417
|
+
const file = dbPath();
|
|
418
|
+
let total = 0;
|
|
419
|
+
for (const path of [file, `${file}-wal`]) try {
|
|
420
|
+
total += statSync(path).size;
|
|
421
|
+
} catch {}
|
|
422
|
+
return total;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* How far above the root a script may sit and still be shown relative to it.
|
|
426
|
+
*
|
|
427
|
+
* One or two `../` reads fine and stays searchable. Six means the database and
|
|
428
|
+
* the script only share a home directory, and `~/work/api/scripts/sync.ts` is
|
|
429
|
+
* the more honest rendering of that.
|
|
430
|
+
*/
|
|
431
|
+
var MAX_ASCENT = 2;
|
|
432
|
+
/** The folder paths are shown relative to: the one holding the database. */
|
|
433
|
+
function pathRoot(dbPath) {
|
|
434
|
+
return dirname(dbPath);
|
|
435
|
+
}
|
|
436
|
+
/** Describe one absolute entry path against a root. */
|
|
437
|
+
function describeEntry(root, entry) {
|
|
438
|
+
return split(shorten(root, entry));
|
|
439
|
+
}
|
|
440
|
+
function shorten(root, entry) {
|
|
441
|
+
if (!isAbsolute(entry)) return entry;
|
|
442
|
+
const rel = toPosix(relative(root, entry));
|
|
443
|
+
if (rel === "" || isAbsolute(rel)) return home(entry);
|
|
444
|
+
return rel.split("/").filter((segment) => segment === "..").length > MAX_ASCENT ? home(entry) : rel;
|
|
445
|
+
}
|
|
446
|
+
/** `/Users/you/work/sync.ts` → `~/work/sync.ts`. */
|
|
447
|
+
function home(absolute) {
|
|
448
|
+
const dir = toPosix(homedir());
|
|
449
|
+
const path = toPosix(absolute);
|
|
450
|
+
return path === dir || path.startsWith(`${dir}/`) ? `~${path.slice(dir.length)}` : path;
|
|
451
|
+
}
|
|
452
|
+
function split(path) {
|
|
453
|
+
const cut = path.lastIndexOf("/");
|
|
454
|
+
return {
|
|
455
|
+
rel: path,
|
|
456
|
+
dir: cut < 0 ? "" : path.slice(0, cut + 1),
|
|
457
|
+
file: cut < 0 ? path : path.slice(cut + 1)
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function toPosix(path) {
|
|
461
|
+
return sep === "/" ? path : path.split(sep).join("/");
|
|
462
|
+
}
|
|
463
|
+
function workspaceCounts(db) {
|
|
464
|
+
return db.prepare(`SELECT
|
|
465
|
+
(SELECT COUNT(DISTINCT entry) FROM runs) AS scripts,
|
|
466
|
+
(SELECT COUNT(*) FROM runs) AS runs,
|
|
467
|
+
(SELECT COUNT(*) FROM requests WHERE kind = 'http') AS requests,
|
|
468
|
+
(SELECT COUNT(*) FROM requests WHERE kind <> 'http') AS connections,
|
|
469
|
+
(SELECT COUNT(*) FROM messages) AS messages,
|
|
470
|
+
(SELECT COUNT(*) FROM checks) AS checks,
|
|
471
|
+
(SELECT COUNT(*) FROM checks WHERE status = 'fail') AS failedChecks,
|
|
472
|
+
(SELECT COUNT(*) FROM requests
|
|
473
|
+
WHERE error_name IS NOT NULL OR status >= 400) AS failedRequests,
|
|
474
|
+
(SELECT COUNT(*) FROM bodies) AS bodies,
|
|
475
|
+
(SELECT COALESCE(SUM(stored_size), 0) FROM bodies) AS storedBodyBytes,
|
|
476
|
+
(SELECT COALESCE(SUM(request_bytes + response_bytes), 0)
|
|
477
|
+
FROM requests) AS trafficBytes`).get();
|
|
478
|
+
}
|
|
479
|
+
function envClause(env, column = "env") {
|
|
480
|
+
if (env === void 0) return {
|
|
481
|
+
sql: "",
|
|
482
|
+
params: []
|
|
483
|
+
};
|
|
484
|
+
if (env === null) return {
|
|
485
|
+
sql: ` AND ${column} IS NULL`,
|
|
486
|
+
params: []
|
|
487
|
+
};
|
|
488
|
+
return {
|
|
489
|
+
sql: ` AND ${column} = ?`,
|
|
490
|
+
params: [env]
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/** Every environment the recording has runs for, most recently used first. */
|
|
494
|
+
function listEnvs(db) {
|
|
495
|
+
return db.prepare(`SELECT env,
|
|
496
|
+
COUNT(*) AS run_count,
|
|
497
|
+
COUNT(DISTINCT entry) AS script_count,
|
|
498
|
+
MAX(started_at) AS last_started_at,
|
|
499
|
+
SUM(exit_status = 'error') AS failed_run_count
|
|
500
|
+
FROM runs GROUP BY env ORDER BY last_started_at DESC`).all();
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* An FTS5 MATCH expression for free text.
|
|
504
|
+
*
|
|
505
|
+
* Every term is quoted, so what someone types is always searched for as words
|
|
506
|
+
* and never parsed as FTS syntax — `a-b` or a stray `"` would otherwise be a
|
|
507
|
+
* query error. Terms are ANDed, which is what a search box means.
|
|
508
|
+
*/
|
|
509
|
+
function ftsExpression(text) {
|
|
510
|
+
const terms = text.split(/\s+/).map((t) => t.trim()).filter(Boolean);
|
|
511
|
+
if (terms.length === 0) return null;
|
|
512
|
+
return terms.map((t) => `"${t.replace(/"/g, "\"\"")}"`).join(" ");
|
|
513
|
+
}
|
|
514
|
+
/** Recent requests across every run, newest first, with the run they belong to. */
|
|
515
|
+
function listHistory(db, filter) {
|
|
516
|
+
const where = [];
|
|
517
|
+
const params = [];
|
|
518
|
+
const q = filter.q?.trim() ?? "";
|
|
519
|
+
if (q) {
|
|
520
|
+
const text = [`q.url LIKE ?`, `q.method = ?`];
|
|
521
|
+
params.push(`%${q}%`, q.toUpperCase());
|
|
522
|
+
if (/^\d{3}$/.test(q)) {
|
|
523
|
+
text.push(`q.status = ?`);
|
|
524
|
+
params.push(Number(q));
|
|
525
|
+
}
|
|
526
|
+
const match = filter.bodies ? ftsExpression(q) : null;
|
|
527
|
+
if (match) {
|
|
528
|
+
const hits = `(SELECT CAST(body_id AS INTEGER) FROM body_fts WHERE body_fts MATCH ?)`;
|
|
529
|
+
text.push(`q.request_body_id IN ${hits}`, `q.response_body_id IN ${hits}`, `q.id IN (SELECT request_id FROM messages WHERE body_id IN ${hits})`);
|
|
530
|
+
params.push(match, match, match);
|
|
531
|
+
}
|
|
532
|
+
where.push(`(${text.join(" OR ")})`);
|
|
533
|
+
}
|
|
534
|
+
if (filter.kind === "http") where.push(`q.kind = 'http'`);
|
|
535
|
+
if (filter.kind === "stream") where.push(`q.kind <> 'http'`);
|
|
536
|
+
if (filter.failed) where.push(`(q.error_name IS NOT NULL OR q.status >= 400)`);
|
|
537
|
+
const env = envClause(filter.env, "r.env");
|
|
538
|
+
const limit = Math.min(Math.max(filter.limit ?? 300, 1), 2e3);
|
|
539
|
+
const sql = `SELECT q.id, q.run_id, q.seq, q.ts, q.kind, q.method, q.host, q.path, q.query,
|
|
540
|
+
q.status, q.error_name, q.duration_ms, q.response_bytes, q.message_count,
|
|
541
|
+
r.label AS run_label, r.env, r.entry
|
|
542
|
+
FROM v_requests q JOIN runs r ON r.id = q.run_id
|
|
543
|
+
WHERE 1 = 1 ${where.length ? `AND ${where.join(" AND ")}` : ""}${env.sql}
|
|
544
|
+
ORDER BY q.ts DESC, q.id DESC
|
|
545
|
+
LIMIT ${limit}`;
|
|
546
|
+
return db.prepare(sql).all(...params, ...env.params);
|
|
547
|
+
}
|
|
548
|
+
function recentFailedChecks(db, env, limit = 12) {
|
|
549
|
+
const e = envClause(env, "r.env");
|
|
550
|
+
return db.prepare(`SELECT c.id, c.run_id, c.ts, c.name, c.mode, c.message,
|
|
551
|
+
r.label AS run_label, r.entry, r.env
|
|
552
|
+
FROM checks c JOIN runs r ON r.id = c.run_id
|
|
553
|
+
WHERE c.status = 'fail'${e.sql}
|
|
554
|
+
ORDER BY c.ts DESC LIMIT ?`).all(...e.params, limit);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* The endpoints that cost the most, across every run.
|
|
558
|
+
*
|
|
559
|
+
* Grouped on method + host + path — the query string is left out on purpose,
|
|
560
|
+
* because `/products?skip=0` and `/products?skip=30` are one endpoint to anyone
|
|
561
|
+
* asking what is slow. HTTP only: a connection's duration is its lifetime.
|
|
562
|
+
*/
|
|
563
|
+
function slowestEndpoints(db, env, limit = 8) {
|
|
564
|
+
const e = envClause(env, "r.env");
|
|
565
|
+
return db.prepare(`SELECT m.method, h.host, q.path,
|
|
566
|
+
COUNT(*) AS count,
|
|
567
|
+
AVG(q.duration_ms) AS avg_ms,
|
|
568
|
+
MAX(q.duration_ms) AS max_ms,
|
|
569
|
+
SUM(q.error_name IS NOT NULL OR q.status >= 400) AS errors,
|
|
570
|
+
-- Bare column under the one MAX(): SQLite takes it from the
|
|
571
|
+
-- slowest row, which is the call worth opening.
|
|
572
|
+
q.id AS slowest_request_id
|
|
573
|
+
FROM requests q
|
|
574
|
+
JOIN runs r ON r.id = q.run_id
|
|
575
|
+
JOIN methods m ON m.id = q.method_id
|
|
576
|
+
JOIN hosts h ON h.id = q.host_id
|
|
577
|
+
WHERE q.kind = 'http'${e.sql}
|
|
578
|
+
GROUP BY q.method_id, q.host_id, q.path
|
|
579
|
+
ORDER BY avg_ms DESC LIMIT ?`).all(...e.params, limit);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* The requests a script makes, once each — what Postman calls a collection.
|
|
583
|
+
*
|
|
584
|
+
* A script has no saved request definitions; its collection is whatever it
|
|
585
|
+
* actually called. Ordered by where each endpoint first appears in a run, which
|
|
586
|
+
* reads as the script's own order. The `last_*` columns come from joining the
|
|
587
|
+
* newest exchange back in: SQLite only takes bare columns from the MAX() row
|
|
588
|
+
* when a query has a single min/max aggregate, and this one needs two.
|
|
589
|
+
*/
|
|
590
|
+
function listEndpoints(db, entry, env) {
|
|
591
|
+
const e = envClause(env, "r.env");
|
|
592
|
+
return db.prepare(`WITH g AS (
|
|
593
|
+
SELECT q.kind, q.method_id, q.host_id, q.path,
|
|
594
|
+
COUNT(*) AS count,
|
|
595
|
+
SUM(q.error_name IS NOT NULL OR q.status >= 400) AS errors,
|
|
596
|
+
AVG(q.duration_ms) AS avg_ms,
|
|
597
|
+
MAX(q.id) AS last_id,
|
|
598
|
+
MIN(q.seq) AS first_seq
|
|
599
|
+
FROM requests q JOIN runs r ON r.id = q.run_id
|
|
600
|
+
WHERE r.entry = ?${e.sql}
|
|
601
|
+
GROUP BY q.kind, q.method_id, q.host_id, q.path
|
|
602
|
+
)
|
|
603
|
+
SELECT g.kind, m.method, h.host, g.path, g.count, g.errors, g.avg_ms,
|
|
604
|
+
g.last_id AS last_request_id,
|
|
605
|
+
l.status AS last_status,
|
|
606
|
+
l.error_name AS last_error_name,
|
|
607
|
+
l.ts AS last_ts
|
|
608
|
+
FROM g
|
|
609
|
+
JOIN requests l ON l.id = g.last_id
|
|
610
|
+
JOIN methods m ON m.id = g.method_id
|
|
611
|
+
JOIN hosts h ON h.id = g.host_id
|
|
612
|
+
ORDER BY g.first_seq, g.count DESC`).all(entry, ...e.params);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Every check a script makes, with its record across runs: has it always
|
|
616
|
+
* passed? The newest outcome is joined back in, for the reason given on
|
|
617
|
+
* {@link listEndpoints}.
|
|
618
|
+
*/
|
|
619
|
+
function listCheckHistory(db, entry, env) {
|
|
620
|
+
const e = envClause(env, "r.env");
|
|
621
|
+
return db.prepare(`WITH g AS (
|
|
622
|
+
SELECT c.name,
|
|
623
|
+
COUNT(*) AS total,
|
|
624
|
+
SUM(c.status = 'fail') AS failures,
|
|
625
|
+
MAX(c.id) AS last_id,
|
|
626
|
+
MIN(c.seq) AS first_seq
|
|
627
|
+
FROM checks c JOIN runs r ON r.id = c.run_id
|
|
628
|
+
WHERE r.entry = ?${e.sql}
|
|
629
|
+
GROUP BY c.name
|
|
630
|
+
)
|
|
631
|
+
SELECT g.name, g.total, g.failures,
|
|
632
|
+
g.last_id AS last_check_id,
|
|
633
|
+
l.run_id AS last_run_id,
|
|
634
|
+
l.status AS last_status,
|
|
635
|
+
l.message AS last_message
|
|
636
|
+
FROM g JOIN checks l ON l.id = g.last_id
|
|
637
|
+
ORDER BY g.first_seq`).all(entry, ...e.params);
|
|
638
|
+
}
|
|
639
|
+
function requestNeighbours(db, runId, seq) {
|
|
640
|
+
return db.prepare(`SELECT
|
|
641
|
+
(SELECT id FROM requests WHERE run_id = @run AND seq < @seq ORDER BY seq DESC LIMIT 1) AS prev,
|
|
642
|
+
(SELECT id FROM requests WHERE run_id = @run AND seq > @seq ORDER BY seq ASC LIMIT 1) AS next`).get({
|
|
643
|
+
run: runId,
|
|
644
|
+
seq
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
/** Older and newer runs of the same script. */
|
|
648
|
+
function runNeighbours(db, entry, startedAt, id) {
|
|
649
|
+
return db.prepare(`SELECT
|
|
650
|
+
(SELECT id FROM runs WHERE entry = @entry AND (started_at, id) < (@at, @id)
|
|
651
|
+
ORDER BY started_at DESC, id DESC LIMIT 1) AS prev,
|
|
652
|
+
(SELECT id FROM runs WHERE entry = @entry AND (started_at, id) > (@at, @id)
|
|
653
|
+
ORDER BY started_at ASC, id ASC LIMIT 1) AS next`).get({
|
|
654
|
+
entry,
|
|
655
|
+
at: startedAt,
|
|
656
|
+
id
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Run one read statement and return at most {@link MAX_QUERY_ROWS} rows.
|
|
661
|
+
*
|
|
662
|
+
* Safety is not this function's job — the connection is read-only and
|
|
663
|
+
* `query_only`, so SQLite itself refuses a write. What this adds is a clear
|
|
664
|
+
* message for a statement that returns nothing to show, and a row cap enforced
|
|
665
|
+
* by stepping the cursor rather than by rewriting the SQL with a LIMIT.
|
|
666
|
+
*/
|
|
667
|
+
function runQuery(db, sql) {
|
|
668
|
+
const started = performance.now();
|
|
669
|
+
try {
|
|
670
|
+
const statement = db.prepare(sql);
|
|
671
|
+
if (!statement.reader) return {
|
|
672
|
+
ok: false,
|
|
673
|
+
error: "Only statements that return rows run here — SELECT, WITH, PRAGMA, EXPLAIN. The database is open read-only."
|
|
674
|
+
};
|
|
675
|
+
const columns = statement.columns().map((c) => c.name);
|
|
676
|
+
const rows = [];
|
|
677
|
+
let truncated = false;
|
|
678
|
+
for (const row of statement.raw(true).iterate()) {
|
|
679
|
+
if (rows.length === 1e3) {
|
|
680
|
+
truncated = true;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
rows.push(row.map(cell));
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
ok: true,
|
|
687
|
+
columns,
|
|
688
|
+
rows,
|
|
689
|
+
truncated,
|
|
690
|
+
ms: performance.now() - started
|
|
691
|
+
};
|
|
692
|
+
} catch (err) {
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
error: err instanceof Error ? err.message : String(err)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/** A value that survives the trip to the browser and says what it is. */
|
|
700
|
+
function cell(value) {
|
|
701
|
+
if (value === null || value === void 0) return null;
|
|
702
|
+
if (typeof value === "number" || typeof value === "string") return value;
|
|
703
|
+
if (typeof value === "bigint") return value.toString();
|
|
704
|
+
if (Buffer.isBuffer(value)) return `<blob · ${value.length} bytes>`;
|
|
705
|
+
return String(value);
|
|
706
|
+
}
|
|
707
|
+
function record(input) {
|
|
708
|
+
return input && typeof input === "object" ? input : {};
|
|
709
|
+
}
|
|
710
|
+
function id(value, name) {
|
|
711
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
712
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) throw new Error(`${name} must be a positive integer`);
|
|
713
|
+
return n;
|
|
714
|
+
}
|
|
715
|
+
function optionalString(value) {
|
|
716
|
+
return typeof value === "string" ? value : void 0;
|
|
717
|
+
}
|
|
718
|
+
/** `undefined` → every environment, `null` → runs recorded without one. */
|
|
719
|
+
function env(value) {
|
|
720
|
+
if (value === null) return null;
|
|
721
|
+
return typeof value === "string" ? value : void 0;
|
|
722
|
+
}
|
|
723
|
+
/** The open database when its schema is one these queries can read, else null. */
|
|
724
|
+
function readable() {
|
|
725
|
+
const db = getDb();
|
|
726
|
+
const state = schemaState(db);
|
|
727
|
+
return state === "current" || state === "newer" ? db : null;
|
|
728
|
+
}
|
|
729
|
+
function describe(row) {
|
|
730
|
+
return {
|
|
731
|
+
...row,
|
|
732
|
+
...describeEntry(pathRoot(dbPath()), row.entry)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/** Runs the sidebar holds. Older ones stay reachable from their script's page. */
|
|
736
|
+
var RUN_WINDOW = 500;
|
|
737
|
+
var getWorkspace_createServerFn_handler = createServerRpc({
|
|
738
|
+
id: "65bc418eb60581304ebde68d2b8b775b58aa758789bcdd2e616a2bdd5657ef20",
|
|
739
|
+
name: "getWorkspace",
|
|
740
|
+
filename: "src/server/api.ts"
|
|
741
|
+
}, (opts) => getWorkspace.__executeServer(opts));
|
|
742
|
+
var getWorkspace = createServerFn({ method: "GET" }).handler(getWorkspace_createServerFn_handler, async () => {
|
|
743
|
+
const path = dbPath();
|
|
744
|
+
const handle = getDb();
|
|
745
|
+
const schema = {
|
|
746
|
+
state: schemaState(handle),
|
|
747
|
+
file: fileSchemaVersion(handle),
|
|
748
|
+
expected: EXPECTED_SCHEMA_VERSION
|
|
749
|
+
};
|
|
750
|
+
const base = {
|
|
751
|
+
path,
|
|
752
|
+
root: pathRoot(path),
|
|
753
|
+
schema,
|
|
754
|
+
now: Date.now()
|
|
755
|
+
};
|
|
756
|
+
const db = readable();
|
|
757
|
+
if (!db) return {
|
|
758
|
+
...base,
|
|
759
|
+
bytes: handle ? fileBytes() : 0,
|
|
760
|
+
counts: null,
|
|
761
|
+
envs: [],
|
|
762
|
+
scripts: [],
|
|
763
|
+
runs: []
|
|
764
|
+
};
|
|
765
|
+
return {
|
|
766
|
+
...base,
|
|
767
|
+
bytes: fileBytes(),
|
|
768
|
+
counts: workspaceCounts(db),
|
|
769
|
+
envs: listEnvs(db),
|
|
770
|
+
scripts: listScripts(db).map(describe),
|
|
771
|
+
runs: listRuns(db, RUN_WINDOW).map(describe)
|
|
772
|
+
};
|
|
773
|
+
});
|
|
774
|
+
var getOverview_createServerFn_handler = createServerRpc({
|
|
775
|
+
id: "edb0cb28f3af0347a499a4c076ecc5f8247f0ace0fb524957f40f6a706d7224d",
|
|
776
|
+
name: "getOverview",
|
|
777
|
+
filename: "src/server/api.ts"
|
|
778
|
+
}, (opts) => getOverview.__executeServer(opts));
|
|
779
|
+
var getOverview = createServerFn({ method: "GET" }).validator((input) => ({ env: env(record(input).env) })).handler(getOverview_createServerFn_handler, async ({ data }) => {
|
|
780
|
+
const db = readable();
|
|
781
|
+
if (!db) return {
|
|
782
|
+
runs: [],
|
|
783
|
+
failedRequests: [],
|
|
784
|
+
failedChecks: [],
|
|
785
|
+
slowest: []
|
|
786
|
+
};
|
|
787
|
+
return {
|
|
788
|
+
runs: listRuns(db, 200).filter((r) => data.env === void 0 || r.env === data.env).slice(0, 12).map(describe),
|
|
789
|
+
failedRequests: listHistory(db, {
|
|
790
|
+
env: data.env,
|
|
791
|
+
failed: true,
|
|
792
|
+
limit: 12
|
|
793
|
+
}).map(describe),
|
|
794
|
+
failedChecks: recentFailedChecks(db, data.env).map(describe),
|
|
795
|
+
slowest: slowestEndpoints(db, data.env)
|
|
796
|
+
};
|
|
797
|
+
});
|
|
798
|
+
var getHistory_createServerFn_handler = createServerRpc({
|
|
799
|
+
id: "c1e4e973ed505d1515bcb9192482c65b1680975a5daa6973befb704515a618e3",
|
|
800
|
+
name: "getHistory",
|
|
801
|
+
filename: "src/server/api.ts"
|
|
802
|
+
}, (opts) => getHistory.__executeServer(opts));
|
|
803
|
+
var getHistory = createServerFn({ method: "GET" }).validator((input) => {
|
|
804
|
+
const r = record(input);
|
|
805
|
+
const kind = r.kind === "http" || r.kind === "stream" ? r.kind : void 0;
|
|
806
|
+
return {
|
|
807
|
+
q: optionalString(r.q),
|
|
808
|
+
env: env(r.env),
|
|
809
|
+
kind,
|
|
810
|
+
failed: r.failed === true,
|
|
811
|
+
bodies: r.bodies === true,
|
|
812
|
+
limit: typeof r.limit === "number" ? r.limit : void 0
|
|
813
|
+
};
|
|
814
|
+
}).handler(getHistory_createServerFn_handler, async ({ data }) => {
|
|
815
|
+
const db = readable();
|
|
816
|
+
if (!db) return [];
|
|
817
|
+
try {
|
|
818
|
+
return listHistory(db, data).map(describe);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
if (data.bodies) return listHistory(db, {
|
|
821
|
+
...data,
|
|
822
|
+
bodies: false
|
|
823
|
+
}).map(describe);
|
|
824
|
+
throw err;
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
var getScript_createServerFn_handler = createServerRpc({
|
|
828
|
+
id: "5eff62884e56f81742c81b44dc7dcd1afc2e67323a30591a8b94b8895eda0dc9",
|
|
829
|
+
name: "getScript",
|
|
830
|
+
filename: "src/server/api.ts"
|
|
831
|
+
}, (opts) => getScript.__executeServer(opts));
|
|
832
|
+
var getScript = createServerFn({ method: "GET" }).validator((input) => {
|
|
833
|
+
const r = record(input);
|
|
834
|
+
const entry = optionalString(r.entry);
|
|
835
|
+
if (!entry) throw new Error("entry is required");
|
|
836
|
+
return {
|
|
837
|
+
entry,
|
|
838
|
+
env: env(r.env)
|
|
839
|
+
};
|
|
840
|
+
}).handler(getScript_createServerFn_handler, async ({ data }) => {
|
|
841
|
+
const db = readable();
|
|
842
|
+
if (!db) return null;
|
|
843
|
+
const script = listScripts(db).find((s) => s.entry === data.entry);
|
|
844
|
+
if (!script) return null;
|
|
845
|
+
const runs = listRunsForEntry(db, data.entry, 1e3).filter((r) => data.env === void 0 || r.env === data.env);
|
|
846
|
+
return {
|
|
847
|
+
script: describe(script),
|
|
848
|
+
runs,
|
|
849
|
+
endpoints: listEndpoints(db, data.entry, data.env),
|
|
850
|
+
checks: listCheckHistory(db, data.entry, data.env)
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
var getRunRequests_createServerFn_handler = createServerRpc({
|
|
854
|
+
id: "3828e982aebdd19568d199b4820f9fb3ca9fab2c60ce502ee35e3f6075d422c9",
|
|
855
|
+
name: "getRunRequests",
|
|
856
|
+
filename: "src/server/api.ts"
|
|
857
|
+
}, (opts) => getRunRequests.__executeServer(opts));
|
|
858
|
+
var getRunRequests = createServerFn({ method: "GET" }).validator((input) => ({ runId: id(record(input).runId, "runId") })).handler(getRunRequests_createServerFn_handler, async ({ data }) => {
|
|
859
|
+
const db = readable();
|
|
860
|
+
return db ? listRequests(db, data.runId) : [];
|
|
861
|
+
});
|
|
862
|
+
var getRun_createServerFn_handler = createServerRpc({
|
|
863
|
+
id: "56c165307cd91159eb72bfa882932f9552f05a5bbd6b5725f0fd5f4ad6b8295d",
|
|
864
|
+
name: "getRun",
|
|
865
|
+
filename: "src/server/api.ts"
|
|
866
|
+
}, (opts) => getRun.__executeServer(opts));
|
|
867
|
+
var getRun = createServerFn({ method: "GET" }).validator((input) => ({ runId: id(record(input).runId, "runId") })).handler(getRun_createServerFn_handler, async ({ data }) => {
|
|
868
|
+
const db = readable();
|
|
869
|
+
if (!db) return null;
|
|
870
|
+
const run = getRun$1(db, data.runId);
|
|
871
|
+
if (!run) return null;
|
|
872
|
+
return {
|
|
873
|
+
run: describe(run),
|
|
874
|
+
tags: listRunTags(db, run.id),
|
|
875
|
+
requests: listRequests(db, run.id),
|
|
876
|
+
checks: listChecks(db, run.id),
|
|
877
|
+
analytics: getRunAnalytics(db, run.id),
|
|
878
|
+
neighbours: runNeighbours(db, run.entry, run.started_at, run.id)
|
|
879
|
+
};
|
|
880
|
+
});
|
|
881
|
+
var getRequest_createServerFn_handler = createServerRpc({
|
|
882
|
+
id: "3772b5ea3e53d85a63e4df11f128b1c907e508d82cca6e71e6ee48dba1664133",
|
|
883
|
+
name: "getRequest",
|
|
884
|
+
filename: "src/server/api.ts"
|
|
885
|
+
}, (opts) => getRequest.__executeServer(opts));
|
|
886
|
+
var getRequest = createServerFn({ method: "GET" }).validator((input) => ({ requestId: id(record(input).requestId, "requestId") })).handler(getRequest_createServerFn_handler, async ({ data }) => {
|
|
887
|
+
const db = readable();
|
|
888
|
+
if (!db) return null;
|
|
889
|
+
const detail = getRequestDetail(db, data.requestId);
|
|
890
|
+
if (!detail) return null;
|
|
891
|
+
const run = getRun$1(db, detail.request.run_id);
|
|
892
|
+
if (!run) return null;
|
|
893
|
+
return {
|
|
894
|
+
detail,
|
|
895
|
+
run: describe(run),
|
|
896
|
+
neighbours: requestNeighbours(db, run.id, detail.request.seq),
|
|
897
|
+
runRequestCount: run.request_count
|
|
898
|
+
};
|
|
899
|
+
});
|
|
900
|
+
var getSchema_createServerFn_handler = createServerRpc({
|
|
901
|
+
id: "06ff3639e7164884f56665ea621dc233dbfc78fa1557b1cb20105ff2fa90521e",
|
|
902
|
+
name: "getSchema",
|
|
903
|
+
filename: "src/server/api.ts"
|
|
904
|
+
}, (opts) => getSchema.__executeServer(opts));
|
|
905
|
+
var getSchema = createServerFn({ method: "GET" }).handler(getSchema_createServerFn_handler, async () => {
|
|
906
|
+
const db = getDb();
|
|
907
|
+
if (!db) return [];
|
|
908
|
+
return listRelations(db, { all: true }).map((relation) => ({
|
|
909
|
+
...relation,
|
|
910
|
+
columns: describeRelation(db, relation.name)
|
|
911
|
+
}));
|
|
912
|
+
});
|
|
913
|
+
var runSql_createServerFn_handler = createServerRpc({
|
|
914
|
+
id: "0f45629470ab3fbe618e9693d8685920d6ce48b2384512e5d9eb88f6b46ec2d9",
|
|
915
|
+
name: "runSql",
|
|
916
|
+
filename: "src/server/api.ts"
|
|
917
|
+
}, (opts) => runSql.__executeServer(opts));
|
|
918
|
+
var runSql = createServerFn({ method: "POST" }).validator((input) => {
|
|
919
|
+
const sql = optionalString(record(input).sql)?.trim();
|
|
920
|
+
if (!sql) throw new Error("sql is required");
|
|
921
|
+
return { sql };
|
|
922
|
+
}).handler(runSql_createServerFn_handler, async ({ data }) => {
|
|
923
|
+
const db = getDb();
|
|
924
|
+
if (!db) return {
|
|
925
|
+
ok: false,
|
|
926
|
+
error: `No database at ${dbPath()} yet.`
|
|
927
|
+
};
|
|
928
|
+
return runQuery(db, data.sql);
|
|
929
|
+
});
|
|
930
|
+
//#endregion
|
|
931
|
+
export { getHistory_createServerFn_handler, getOverview_createServerFn_handler, getRequest_createServerFn_handler, getRunRequests_createServerFn_handler, getRun_createServerFn_handler, getSchema_createServerFn_handler, getScript_createServerFn_handler, getWorkspace_createServerFn_handler, runSql_createServerFn_handler };
|