@zintrust/trace 1.6.6 → 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/package.json +2 -3
  2. package/src/TraceConnection.ts +0 -182
  3. package/src/cli-register.ts +0 -63
  4. package/src/config.ts +0 -383
  5. package/src/context.ts +0 -101
  6. package/src/dashboard/handlers.ts +0 -353
  7. package/src/dashboard/routes.ts +0 -114
  8. package/src/dashboard/ui.ts +0 -1262
  9. package/src/dashboard/zintrust-debuger.svg +0 -30
  10. package/src/index.ts +0 -102
  11. package/src/ingest/TraceIngestGateway.ts +0 -414
  12. package/src/plugin.ts +0 -9
  13. package/src/register.ts +0 -702
  14. package/src/storage/ProxyTraceStorage.ts +0 -190
  15. package/src/storage/TraceContentBudget.ts +0 -493
  16. package/src/storage/TraceContentRedaction.ts +0 -44
  17. package/src/storage/TraceEntryFiltering.ts +0 -50
  18. package/src/storage/TraceServiceTag.ts +0 -56
  19. package/src/storage/TraceStorage.ts +0 -543
  20. package/src/storage/TraceWriteDiagnostics.ts +0 -289
  21. package/src/storage/index.ts +0 -4
  22. package/src/types.ts +0 -430
  23. package/src/ui.ts +0 -9
  24. package/src/utils/authTag.ts +0 -20
  25. package/src/utils/entryFilter.ts +0 -131
  26. package/src/utils/familyHash.ts +0 -8
  27. package/src/utils/redact.ts +0 -112
  28. package/src/utils/requestFilter.ts +0 -79
  29. package/src/utils/stackFrame.ts +0 -44
  30. package/src/watchers/AuthWatcher.ts +0 -53
  31. package/src/watchers/BatchWatcher.ts +0 -55
  32. package/src/watchers/CacheWatcher.ts +0 -72
  33. package/src/watchers/CommandWatcher.ts +0 -58
  34. package/src/watchers/DumpWatcher.ts +0 -45
  35. package/src/watchers/EventWatcher.ts +0 -46
  36. package/src/watchers/ExceptionWatcher.ts +0 -130
  37. package/src/watchers/GateWatcher.ts +0 -53
  38. package/src/watchers/HttpClientWatcher.ts +0 -219
  39. package/src/watchers/HttpWatcher.ts +0 -249
  40. package/src/watchers/JobWatcher.ts +0 -124
  41. package/src/watchers/LogWatcher.ts +0 -120
  42. package/src/watchers/MailWatcher.ts +0 -65
  43. package/src/watchers/MiddlewareWatcher.ts +0 -54
  44. package/src/watchers/ModelWatcher.ts +0 -60
  45. package/src/watchers/NotificationWatcher.ts +0 -60
  46. package/src/watchers/QueryWatcher.ts +0 -105
  47. package/src/watchers/RedisWatcher.ts +0 -42
  48. package/src/watchers/ScheduleWatcher.ts +0 -57
  49. package/src/watchers/ViewWatcher.ts +0 -40
@@ -1,44 +0,0 @@
1
- import type { ITraceEntry, ITraceStorage, RedactionConfig } from '../types';
2
- import { redactUnknown } from '../utils/redact';
3
-
4
- const collectRedactionFields = (redaction: RedactionConfig): string[] => {
5
- return [
6
- ...new Set([...redaction.keys, ...redaction.headers, ...redaction.body, ...redaction.query]),
7
- ];
8
- };
9
-
10
- const redactTraceEntry = (entry: ITraceEntry, redaction: RedactionConfig): ITraceEntry => {
11
- return {
12
- ...entry,
13
- content: redactUnknown(entry.content, collectRedactionFields(redaction)),
14
- };
15
- };
16
-
17
- const redactTracePatch = (
18
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>,
19
- redaction: RedactionConfig
20
- ): Partial<Pick<ITraceEntry, 'content' | 'isLatest'>> => {
21
- if (patch.content === undefined) return patch;
22
-
23
- return {
24
- ...patch,
25
- content: redactUnknown(patch.content, collectRedactionFields(redaction)),
26
- };
27
- };
28
-
29
- export const TraceContentRedaction = Object.freeze({
30
- wrapStorage(storage: ITraceStorage, redaction: RedactionConfig): ITraceStorage {
31
- return Object.freeze({
32
- ...storage,
33
- writeEntry: async (entry: ITraceEntry): Promise<void> => {
34
- await storage.writeEntry(redactTraceEntry(entry, redaction));
35
- },
36
- updateEntry: async (
37
- uuid: string,
38
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
39
- ): Promise<void> => {
40
- await storage.updateEntry(uuid, redactTracePatch(patch, redaction));
41
- },
42
- });
43
- },
44
- });
@@ -1,50 +0,0 @@
1
- import { TraceContext } from '../context';
2
- import type { ITraceConfig, ITraceEntry, ITraceStorage } from '../types';
3
- import { EntryType } from '../types';
4
- import { TraceEntryFilter } from '../utils/entryFilter';
5
- import { RequestFilter } from '../utils/requestFilter';
6
-
7
- const isObjectValue = (value: unknown): value is Record<string, unknown> => {
8
- return typeof value === 'object' && value !== null && !Array.isArray(value);
9
- };
10
-
11
- const getEntryUri = (entry: ITraceEntry): string | undefined => {
12
- if (entry.type !== EntryType.REQUEST) return undefined;
13
-
14
- const content = isObjectValue(entry.content) ? entry.content : undefined;
15
- const uri = content?.['uri'];
16
- return typeof uri === 'string' && uri.trim() !== '' ? uri : undefined;
17
- };
18
-
19
- const shouldDropForIgnoredRequest = (entry: ITraceEntry, config: ITraceConfig): boolean => {
20
- if (entry.type !== EntryType.REQUEST) return false;
21
-
22
- const currentPath = TraceContext.getRequestPath();
23
- if (
24
- typeof currentPath === 'string' &&
25
- currentPath !== '' &&
26
- RequestFilter.matchesIgnoredPath(currentPath, config)
27
- ) {
28
- return true;
29
- }
30
-
31
- const uri = getEntryUri(entry);
32
- if (typeof uri === 'string' && RequestFilter.matchesIgnoredPath(uri, config)) {
33
- return true;
34
- }
35
-
36
- return false;
37
- };
38
-
39
- export const TraceEntryFiltering = Object.freeze({
40
- wrapStorage(storage: ITraceStorage, config: ITraceConfig): ITraceStorage {
41
- return Object.freeze({
42
- ...storage,
43
- async writeEntry(entry: ITraceEntry): Promise<void> {
44
- if (shouldDropForIgnoredRequest(entry, config)) return;
45
- if (!TraceEntryFilter.shouldCapture(entry, config)) return;
46
- await storage.writeEntry(entry);
47
- },
48
- });
49
- },
50
- });
@@ -1,56 +0,0 @@
1
- import { ErrorFactory } from '@zintrust/core';
2
- import type { ITraceConfig, ITraceEntry, ITraceStorage } from '../types';
3
-
4
- const appendServiceTag = (entry: ITraceEntry, serviceTag?: string): ITraceEntry => {
5
- const normalizedTag = serviceTag?.trim() ?? '';
6
- if (normalizedTag === '' || entry.tags.includes(normalizedTag)) {
7
- return entry;
8
- }
9
-
10
- return {
11
- ...entry,
12
- tags: [...entry.tags, normalizedTag],
13
- };
14
- };
15
-
16
- const unsupportedRead = async <T>(): Promise<T> => {
17
- throw ErrorFactory.createConfigError(
18
- 'Trace proxy mode only supports runtime persistence on the sender. Query the trace server database or dashboard directly.'
19
- );
20
- };
21
-
22
- const bindOrUnsupported = <T extends (...args: never[]) => Promise<unknown>>(
23
- method: T | undefined
24
- ): T => {
25
- if (method === undefined) {
26
- return unsupportedRead as unknown as T;
27
- }
28
-
29
- return method;
30
- };
31
-
32
- export const TraceServiceTag = Object.freeze({
33
- wrapStorage(storage: ITraceStorage, config: ITraceConfig): ITraceStorage {
34
- const writeEntry = async (entry: ITraceEntry): Promise<void> => {
35
- await storage.writeEntry(appendServiceTag(entry, config.serviceTag));
36
- };
37
-
38
- return Object.freeze({
39
- writeEntry,
40
- updateEntry: storage.updateEntry.bind(storage),
41
- markFamilyStale: storage.markFamilyStale.bind(storage),
42
- queryEntries: bindOrUnsupported(storage.queryEntries?.bind(storage)),
43
- getEntry: bindOrUnsupported(storage.getEntry?.bind(storage)),
44
- getBatch: bindOrUnsupported(storage.getBatch?.bind(storage)),
45
- queryBatchEntries: bindOrUnsupported(storage.queryBatchEntries?.bind(storage)),
46
- prune: bindOrUnsupported(storage.prune?.bind(storage)),
47
- clear: bindOrUnsupported(storage.clear?.bind(storage)),
48
- getMonitoring: bindOrUnsupported(storage.getMonitoring?.bind(storage)),
49
- addMonitoring: bindOrUnsupported(storage.addMonitoring?.bind(storage)),
50
- removeMonitoring: bindOrUnsupported(storage.removeMonitoring?.bind(storage)),
51
- stats: bindOrUnsupported(storage.stats?.bind(storage)),
52
- });
53
- },
54
- });
55
-
56
- export default TraceServiceTag;
@@ -1,543 +0,0 @@
1
- /**
2
- * TraceStorage — sealed namespace wrapping the D1/SQLite driver.
3
- * Resolves the correct IDatabase from the app config, then delegates all
4
- * read/write operations to the trace storage facade.
5
- */
6
- import type { IDatabase } from '@zintrust/core';
7
- import type {
8
- EntryTypeValue,
9
- ITraceEntry,
10
- ITraceStorage,
11
- QueryBatchEntriesOptions,
12
- QueryBatchEntriesResult,
13
- QueryEntriesOptions,
14
- } from '../types';
15
- import { familyHash } from '../utils/familyHash';
16
-
17
- const TABLE_ENTRIES = 'zin_trace_entries';
18
- const TABLE_TAGS = 'zin_trace_entries_tags';
19
- const TABLE_MONITORING = 'zin_trace_monitoring';
20
-
21
- const generateUuid = (): string => crypto.randomUUID();
22
-
23
- type EntryRow = {
24
- id: number;
25
- uuid: string;
26
- batch_id: string;
27
- family_hash: string | null;
28
- type: string;
29
- content: string;
30
- is_latest: number | boolean;
31
- created_at: number;
32
- };
33
-
34
- type TagRow = { entry_uuid: string; tag: string };
35
-
36
- const decodeJsonStringLiteral = (value: string): string | undefined => {
37
- try {
38
- return JSON.parse(`"${value}"`) as string;
39
- } catch {
40
- return undefined;
41
- }
42
- };
43
-
44
- const matchJsonStringField = (content: string, key: string): string | undefined => {
45
- const match = new RegExp(String.raw`"${key}"\s*:\s*"((?:\\.|[^"\\])*)"`, 's').exec(content);
46
- return match ? decodeJsonStringLiteral(match[1]) : undefined;
47
- };
48
-
49
- const matchJsonNumberField = (content: string, key: string): number | undefined => {
50
- const match = new RegExp(String.raw`"${key}"\s*:\s*(-?\d+(?:\.\d+)?)`, 's').exec(content);
51
- if (!match) return undefined;
52
- const parsed = Number(match[1]);
53
- return Number.isFinite(parsed) ? parsed : undefined;
54
- };
55
-
56
- const matchJsonNullOrNumberField = (content: string, key: string): number | null | undefined => {
57
- const nullMatch = new RegExp(String.raw`"${key}"\s*:\s*null`, 's').exec(content);
58
- if (nullMatch) return null;
59
- return matchJsonNumberField(content, key);
60
- };
61
-
62
- const matchJsonStringArrayField = (content: string, key: string): string[] | undefined => {
63
- const match = new RegExp(String.raw`"${key}"\s*:\s*(\[.*?\])`, 's').exec(content);
64
- if (!match) return undefined;
65
-
66
- try {
67
- const parsed = JSON.parse(match[1]) as unknown;
68
- return Array.isArray(parsed)
69
- ? parsed.filter((value): value is string => typeof value === 'string')
70
- : undefined;
71
- } catch {
72
- return undefined;
73
- }
74
- };
75
-
76
- const compactRequestContent = (content: string): Record<string, unknown> | undefined => {
77
- const compact: Record<string, unknown> = {};
78
- const method = matchJsonStringField(content, 'method');
79
- const uri = matchJsonStringField(content, 'uri');
80
- const responseStatus = matchJsonNumberField(content, 'responseStatus');
81
- const duration = matchJsonNumberField(content, 'duration');
82
- const memory = matchJsonNullOrNumberField(content, 'memory');
83
- const middleware = matchJsonStringArrayField(content, 'middleware');
84
- const hostname = matchJsonStringField(content, 'hostname');
85
- const userId = matchJsonStringField(content, 'userId');
86
-
87
- if (method !== undefined) compact['method'] = method;
88
- if (uri !== undefined) compact['uri'] = uri;
89
- if (responseStatus !== undefined) compact['responseStatus'] = responseStatus;
90
- if (duration !== undefined) compact['duration'] = duration;
91
- if (memory !== undefined) compact['memory'] = memory;
92
- if (middleware !== undefined) compact['middleware'] = middleware;
93
- if (hostname !== undefined) compact['hostname'] = hostname;
94
- if (userId !== undefined) compact['userId'] = userId;
95
-
96
- return Object.keys(compact).length > 0 ? compact : undefined;
97
- };
98
-
99
- const compactClientRequestContent = (content: string): Record<string, unknown> | undefined => {
100
- const compact: Record<string, unknown> = {};
101
- const source = matchJsonStringField(content, 'source');
102
- const method = matchJsonStringField(content, 'method');
103
- const url = matchJsonStringField(content, 'url');
104
- const responseStatus = matchJsonNumberField(content, 'responseStatus');
105
- const error = matchJsonStringField(content, 'error');
106
- const duration = matchJsonNumberField(content, 'duration');
107
- const hostname = matchJsonStringField(content, 'hostname');
108
-
109
- if (source !== undefined) compact['source'] = source;
110
- if (method !== undefined) compact['method'] = method;
111
- if (url !== undefined) compact['url'] = url;
112
- if (responseStatus !== undefined) compact['responseStatus'] = responseStatus;
113
- if (error !== undefined) compact['error'] = error;
114
- if (duration !== undefined) compact['duration'] = duration;
115
- if (hostname !== undefined) compact['hostname'] = hostname;
116
-
117
- return Object.keys(compact).length > 0 ? compact : undefined;
118
- };
119
-
120
- const summarizeEntryContent = (row: EntryRow): unknown => {
121
- if (row.type === 'request') {
122
- return compactRequestContent(row.content) ?? (JSON.parse(row.content) as unknown);
123
- }
124
-
125
- if (row.type === 'client_request') {
126
- return compactClientRequestContent(row.content) ?? (JSON.parse(row.content) as unknown);
127
- }
128
-
129
- return JSON.parse(row.content) as unknown;
130
- };
131
-
132
- type DatabaseWithDriver = IDatabase & {
133
- getType?: () => string;
134
- };
135
-
136
- const buildIgnoreInsert = (
137
- db: IDatabase,
138
- table: string,
139
- columns: string[],
140
- conflictColumns: string[]
141
- ): string => {
142
- const columnList = columns.join(', ');
143
- const placeholders = columns.map(() => '?').join(', ');
144
- const driver = (db as DatabaseWithDriver).getType?.() ?? 'sqlite';
145
-
146
- if (driver === 'sqlite' || driver === 'd1' || driver === 'd1-remote') {
147
- return `INSERT OR IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
148
- }
149
-
150
- if (driver === 'mysql') {
151
- return `INSERT IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
152
- }
153
-
154
- if (driver === 'postgresql') {
155
- return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns.join(', ')}) DO NOTHING`;
156
- }
157
-
158
- if (driver === 'sqlserver') {
159
- const sourceColumns = columns.map((_, index) => `v${index + 1}`);
160
- const selectClause = sourceColumns.map((name) => `? AS ${name}`).join(', ');
161
- const conflictClause = conflictColumns
162
- .map((column) => `target.${column} = source.${column}`)
163
- .join(' AND ');
164
- const insertValues = columns.map((column) => `source.${column}`).join(', ');
165
- const sourceProjection = columns
166
- .map((column, index) => `${sourceColumns[index]} AS ${column}`)
167
- .join(', ');
168
-
169
- return [
170
- `MERGE INTO ${table} WITH (HOLDLOCK) AS target`,
171
- `USING (SELECT ${sourceProjection} FROM (SELECT ${selectClause}) seed) AS source`,
172
- `ON ${conflictClause}`,
173
- `WHEN NOT MATCHED THEN INSERT (${columnList}) VALUES (${insertValues});`,
174
- ].join(' ');
175
- }
176
-
177
- return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`;
178
- };
179
-
180
- const rowToEntry = (row: EntryRow, tags: string[], summary = false): ITraceEntry => ({
181
- uuid: row.uuid,
182
- batchId: row.batch_id,
183
- familyHash: row.family_hash ?? undefined,
184
- type: row.type as EntryTypeValue,
185
- content: summary ? summarizeEntryContent(row) : (JSON.parse(row.content) as unknown),
186
- tags,
187
- isLatest: Boolean(row.is_latest),
188
- createdAt: row.created_at,
189
- });
190
-
191
- const insertTags = async (db: IDatabase, uuid: string, tags: string[]): Promise<void> => {
192
- if (tags.length === 0) return;
193
-
194
- const sql = buildIgnoreInsert(db, TABLE_TAGS, ['entry_uuid', 'tag'], ['entry_uuid', 'tag']);
195
-
196
- await Promise.all(
197
- tags.map(async (tag) => {
198
- await db.execute(sql, [uuid, tag]);
199
- })
200
- );
201
- };
202
-
203
- const buildEntryFilters = (
204
- opts: QueryEntriesOptions
205
- ): { joinClause: string; whereClause: string; params: unknown[]; countParams: unknown[] } => {
206
- const conditions: string[] = [];
207
- const params: unknown[] = [];
208
-
209
- if (opts.type) {
210
- conditions.push('e.type = ?');
211
- params.push(opts.type);
212
- }
213
- if (opts.batchId) {
214
- conditions.push('e.batch_id = ?');
215
- params.push(opts.batchId);
216
- }
217
- if (opts.from) {
218
- conditions.push('e.created_at >= ?');
219
- params.push(opts.from);
220
- }
221
- if (opts.to) {
222
- conditions.push('e.created_at <= ?');
223
- params.push(opts.to);
224
- }
225
-
226
- let joinClause = '';
227
- if (opts.tag) {
228
- joinClause = `INNER JOIN ${TABLE_TAGS} t ON t.entry_uuid = e.uuid AND t.tag = ?`;
229
- params.unshift(opts.tag);
230
- }
231
-
232
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
233
- const countParams = opts.tag ? [opts.tag, ...params.slice(1)] : [...params];
234
-
235
- return { joinClause, whereClause, params, countParams };
236
- };
237
-
238
- const buildBatchCounts = async (
239
- db: IDatabase,
240
- batchId: string
241
- ): Promise<Partial<Record<EntryTypeValue, number>>> => {
242
- const rows = (await db.query(
243
- `SELECT type, COUNT(*) as cnt FROM ${TABLE_ENTRIES} WHERE batch_id = ? GROUP BY type`,
244
- [batchId]
245
- )) as Array<{ type: string; cnt: number }>;
246
-
247
- const counts: Partial<Record<EntryTypeValue, number>> = {};
248
- for (const row of rows) {
249
- counts[row.type as EntryTypeValue] = row.cnt;
250
- }
251
-
252
- return counts;
253
- };
254
-
255
- const buildBatchEntryFilters = (
256
- batchId: string,
257
- opts: QueryBatchEntriesOptions
258
- ): { whereClause: string; params: unknown[] } => {
259
- const conditions = ['batch_id = ?'];
260
- const params: unknown[] = [batchId];
261
-
262
- if (opts.type) {
263
- conditions.push('type = ?');
264
- params.push(opts.type);
265
- }
266
-
267
- const excludeTypes = opts.excludeTypes ?? [];
268
- if (excludeTypes.length > 0) {
269
- const placeholders = excludeTypes.map(() => '?').join(', ');
270
- conditions.push(`type NOT IN (${placeholders})`);
271
- params.push(...excludeTypes);
272
- }
273
-
274
- return { whereClause: `WHERE ${conditions.join(' AND ')}`, params };
275
- };
276
-
277
- const loadTagsByUuid = async (db: IDatabase, uuids: string[]): Promise<Map<string, string[]>> => {
278
- const tagsByUuid = new Map<string, string[]>();
279
- if (uuids.length === 0) return tagsByUuid;
280
-
281
- const tagRows = (await db.query(
282
- `SELECT entry_uuid, tag FROM ${TABLE_TAGS} WHERE entry_uuid IN (${uuids.map(() => '?').join(',')})`,
283
- uuids
284
- )) as TagRow[];
285
-
286
- for (const tagRow of tagRows) {
287
- const tags = tagsByUuid.get(tagRow.entry_uuid) ?? [];
288
- tags.push(tagRow.tag);
289
- tagsByUuid.set(tagRow.entry_uuid, tags);
290
- }
291
-
292
- return tagsByUuid;
293
- };
294
-
295
- // The storage facade intentionally groups related DB operations in one factory.
296
- // eslint-disable-next-line max-lines-per-function
297
- const createStorage = (db: IDatabase): ITraceStorage => {
298
- const writeEntry = async (entry: ITraceEntry): Promise<void> => {
299
- const uuid = entry.uuid || generateUuid();
300
- await db.execute(
301
- `INSERT INTO ${TABLE_ENTRIES} (uuid, batch_id, family_hash, type, content, is_latest, created_at)
302
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
303
- [
304
- uuid,
305
- entry.batchId,
306
- entry.familyHash ?? null,
307
- entry.type,
308
- JSON.stringify(entry.content),
309
- entry.isLatest ? 1 : 0,
310
- entry.createdAt,
311
- ]
312
- );
313
-
314
- await insertTags(db, uuid, entry.tags);
315
- };
316
-
317
- const updateEntry = async (
318
- uuid: string,
319
- patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
320
- ): Promise<void> => {
321
- const sets: string[] = [];
322
- const params: unknown[] = [];
323
-
324
- if (patch.content !== undefined) {
325
- sets.push('content = ?');
326
- params.push(JSON.stringify(patch.content));
327
- }
328
- if (patch.isLatest !== undefined) {
329
- sets.push('is_latest = ?');
330
- params.push(patch.isLatest ? 1 : 0);
331
- }
332
-
333
- if (sets.length === 0) return;
334
- params.push(uuid);
335
-
336
- await db.execute(`UPDATE ${TABLE_ENTRIES} SET ${sets.join(', ')} WHERE uuid = ?`, params);
337
- };
338
-
339
- const markFamilyStale = async (hash: string, exceptUuid: string): Promise<void> => {
340
- await db.execute(
341
- `UPDATE ${TABLE_ENTRIES} SET is_latest = 0
342
- WHERE family_hash = ? AND uuid != ? AND is_latest = 1`,
343
- [hash, exceptUuid]
344
- );
345
- };
346
-
347
- const queryEntries = async (
348
- opts: QueryEntriesOptions
349
- ): Promise<{ data: ITraceEntry[]; total: number }> => {
350
- const page = opts.page ?? 1;
351
- const perPage = opts.perPage ?? 50;
352
- const offset = (page - 1) * perPage;
353
- const { joinClause, whereClause, params, countParams } = buildEntryFilters(opts);
354
-
355
- const countResult = (await db.queryOne(
356
- `SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}`,
357
- countParams
358
- )) as { cnt: number } | undefined;
359
- const total = countResult?.cnt ?? 0;
360
-
361
- const rows = (await db.query(
362
- `SELECT e.id, e.uuid, e.batch_id, e.family_hash, e.type, e.content, e.is_latest, e.created_at
363
- FROM ${TABLE_ENTRIES} e ${joinClause} ${whereClause}
364
- ORDER BY e.created_at DESC, e.id DESC
365
- LIMIT ? OFFSET ?`,
366
- [...params, perPage, offset]
367
- )) as EntryRow[];
368
-
369
- const tagsByUuid = await loadTagsByUuid(
370
- db,
371
- rows.map((row) => row.uuid)
372
- );
373
-
374
- return {
375
- data: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [], opts.summary)),
376
- total,
377
- };
378
- };
379
-
380
- const getEntry = async (uuid: string): Promise<ITraceEntry | null> => {
381
- const row = (await db.queryOne(
382
- `SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
383
- FROM ${TABLE_ENTRIES}
384
- WHERE uuid = ?`,
385
- [uuid]
386
- )) as EntryRow | undefined;
387
- if (!row) return null;
388
-
389
- const tags = (await db.query(`SELECT tag FROM ${TABLE_TAGS} WHERE entry_uuid = ?`, [
390
- uuid,
391
- ])) as Array<{
392
- tag: string;
393
- }>;
394
- return rowToEntry(
395
- row,
396
- tags.map((tag) => tag.tag)
397
- );
398
- };
399
-
400
- const getBatch = async (batchId: string): Promise<ITraceEntry[]> => {
401
- const rows = (await db.query(
402
- `SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
403
- FROM ${TABLE_ENTRIES}
404
- WHERE batch_id = ?
405
- ORDER BY created_at ASC, id ASC`,
406
- [batchId]
407
- )) as EntryRow[];
408
- if (rows.length === 0) return [];
409
-
410
- const tagsByUuid = await loadTagsByUuid(
411
- db,
412
- rows.map((row) => row.uuid)
413
- );
414
-
415
- return rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? []));
416
- };
417
-
418
- const queryBatchEntries = async (
419
- batchId: string,
420
- opts: QueryBatchEntriesOptions = {}
421
- ): Promise<QueryBatchEntriesResult> => {
422
- const page = opts.page ?? 1;
423
- const perPage = opts.perPage ?? 10;
424
- const offset = (page - 1) * perPage;
425
- const counts = await buildBatchCounts(db, batchId);
426
-
427
- if (opts.countsOnly) {
428
- const total = Object.values(counts).reduce((sum, value) => sum + Number(value ?? 0), 0);
429
- return { entries: [], total, counts, page, perPage };
430
- }
431
-
432
- const { whereClause, params } = buildBatchEntryFilters(batchId, opts);
433
- const countResult = (await db.queryOne(
434
- `SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES} ${whereClause}`,
435
- params
436
- )) as { cnt: number } | undefined;
437
- const total = countResult?.cnt ?? 0;
438
- if (total === 0) {
439
- return { entries: [], total: 0, counts, page, perPage };
440
- }
441
-
442
- const rows = (await db.query(
443
- `SELECT id, uuid, batch_id, family_hash, type, content, is_latest, created_at
444
- FROM ${TABLE_ENTRIES}
445
- ${whereClause}
446
- ORDER BY created_at ASC, id ASC
447
- LIMIT ? OFFSET ?`,
448
- [...params, perPage, offset]
449
- )) as EntryRow[];
450
-
451
- const tagsByUuid = await loadTagsByUuid(
452
- db,
453
- rows.map((row) => row.uuid)
454
- );
455
-
456
- return {
457
- entries: rows.map((row) => rowToEntry(row, tagsByUuid.get(row.uuid) ?? [], opts.summary)),
458
- total,
459
- counts,
460
- page,
461
- perPage,
462
- };
463
- };
464
-
465
- const prune = async (olderThanMs: number, keepExceptions = false): Promise<number> => {
466
- const countResult = (await db.queryOne(
467
- `SELECT COUNT(*) as cnt FROM ${TABLE_ENTRIES}
468
- WHERE created_at < ?
469
- ${keepExceptions ? "AND type != 'exception'" : ''}`,
470
- [olderThanMs]
471
- )) as { cnt: number } | undefined;
472
- const deleted = countResult?.cnt ?? 0;
473
- if (deleted === 0) return 0;
474
-
475
- await db.execute(
476
- `DELETE FROM ${TABLE_ENTRIES}
477
- WHERE created_at < ?
478
- ${keepExceptions ? "AND type != 'exception'" : ''}`,
479
- [olderThanMs]
480
- );
481
-
482
- return deleted;
483
- };
484
-
485
- const clear = async (): Promise<void> => {
486
- await db.execute(`DELETE FROM ${TABLE_ENTRIES}`, []);
487
- };
488
-
489
- const getMonitoring = async (): Promise<string[]> => {
490
- const rows = (await db.query(`SELECT tag FROM ${TABLE_MONITORING}`, [])) as Array<{
491
- tag: string;
492
- }>;
493
- return rows.map((row) => row.tag);
494
- };
495
-
496
- const addMonitoring = async (tag: string): Promise<void> => {
497
- await db.execute(buildIgnoreInsert(db, TABLE_MONITORING, ['tag'], ['tag']), [tag]);
498
- };
499
-
500
- const removeMonitoring = async (tag: string): Promise<void> => {
501
- await db.execute(`DELETE FROM ${TABLE_MONITORING} WHERE tag = ?`, [tag]);
502
- };
503
-
504
- const stats = async (): Promise<Record<EntryTypeValue, number>> => {
505
- const rows = (await db.query(
506
- `SELECT type, COUNT(*) as cnt FROM ${TABLE_ENTRIES} GROUP BY type`,
507
- []
508
- )) as Array<{ type: string; cnt: number }>;
509
- const output: Record<string, number> = {};
510
- for (const row of rows) {
511
- output[row.type] = row.cnt;
512
- }
513
- return output as Record<EntryTypeValue, number>;
514
- };
515
-
516
- return {
517
- writeEntry,
518
- updateEntry,
519
- markFamilyStale,
520
- queryEntries,
521
- getEntry,
522
- getBatch,
523
- queryBatchEntries,
524
- prune,
525
- clear,
526
- getMonitoring,
527
- addMonitoring,
528
- removeMonitoring,
529
- stats,
530
- };
531
- };
532
-
533
- const resolveStorage = (db: IDatabase): ITraceStorage => {
534
- return createStorage(db);
535
- };
536
-
537
- const reset = (): void => {
538
- return;
539
- };
540
-
541
- export const TraceStorage = Object.freeze({ resolveStorage, reset, familyHash });
542
-
543
- export { type ITraceStorage } from '../types';