@syncular/server-hono 0.0.6-93 → 0.0.6-96
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/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +212 -40
- package/dist/console/routes.js.map +1 -1
- package/dist/console/types.d.ts +38 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +7 -1
- package/dist/create-server.js.map +1 -1
- package/dist/routes.d.ts +21 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +92 -56
- package/dist/routes.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/console-routes.test.ts +155 -5
- package/src/__tests__/create-server.test.ts +147 -4
- package/src/console/routes.ts +280 -48
- package/src/console/types.ts +39 -0
- package/src/create-server.ts +8 -1
- package/src/routes.ts +134 -63
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/console/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAQ9E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAqE5B,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EAEnB,0BAA0B,EAC3B,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,mBAAmB,CA0DtB;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/console/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAQ9E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAqE5B,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EAEnB,0BAA0B,EAC3B,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,mBAAmB,CA0DtB;AAsPD,wBAAgB,mBAAmB,CACjC,EAAE,SAAS,UAAU,EACrB,IAAI,SAAS,cAAc,EAC3B,CAAC,SAAS,SAAS,GAAG,SAAS,EAC/B,OAAO,EAAE,0BAA0B,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CA44GxD;AA6BD;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,CAAC,EAAE,MAAM,GACb,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAgBnD"}
|
package/dist/console/routes.js
CHANGED
|
@@ -262,6 +262,20 @@ const apiKeysQuerySchema = ConsolePaginationQuerySchema.extend({
|
|
|
262
262
|
const handlersResponseSchema = z.object({
|
|
263
263
|
items: z.array(ConsoleHandlerSchema),
|
|
264
264
|
});
|
|
265
|
+
const DEFAULT_REQUEST_EVENTS_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
266
|
+
const DEFAULT_REQUEST_EVENTS_MAX_ROWS = 10_000;
|
|
267
|
+
const DEFAULT_OPERATION_EVENTS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
268
|
+
const DEFAULT_OPERATION_EVENTS_MAX_ROWS = 5_000;
|
|
269
|
+
const DEFAULT_AUTO_EVENTS_PRUNE_INTERVAL_MS = 5 * 60 * 1000;
|
|
270
|
+
function readNonNegativeInteger(value, fallback) {
|
|
271
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
272
|
+
return fallback;
|
|
273
|
+
}
|
|
274
|
+
if (value < 0) {
|
|
275
|
+
return fallback;
|
|
276
|
+
}
|
|
277
|
+
return Math.floor(value);
|
|
278
|
+
}
|
|
265
279
|
export function createConsoleRoutes(options) {
|
|
266
280
|
const routes = new Hono();
|
|
267
281
|
routes.onError((error, context) => {
|
|
@@ -275,18 +289,27 @@ export function createConsoleRoutes(options) {
|
|
|
275
289
|
const db = options.db;
|
|
276
290
|
const metricsAggregationMode = options.metrics?.aggregationMode ?? 'auto';
|
|
277
291
|
const rawFallbackMaxEvents = Math.max(1, options.metrics?.rawFallbackMaxEvents ?? 5000);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
options.
|
|
292
|
+
const requestEventsMaxAgeMs = readNonNegativeInteger(options.maintenance?.requestEventsMaxAgeMs, DEFAULT_REQUEST_EVENTS_MAX_AGE_MS);
|
|
293
|
+
const requestEventsMaxRows = readNonNegativeInteger(options.maintenance?.requestEventsMaxRows, DEFAULT_REQUEST_EVENTS_MAX_ROWS);
|
|
294
|
+
const operationEventsMaxAgeMs = readNonNegativeInteger(options.maintenance?.operationEventsMaxAgeMs, DEFAULT_OPERATION_EVENTS_MAX_AGE_MS);
|
|
295
|
+
const operationEventsMaxRows = readNonNegativeInteger(options.maintenance?.operationEventsMaxRows, DEFAULT_OPERATION_EVENTS_MAX_ROWS);
|
|
296
|
+
const autoEventsPruneIntervalMs = readNonNegativeInteger(options.maintenance?.autoPruneIntervalMs, DEFAULT_AUTO_EVENTS_PRUNE_INTERVAL_MS);
|
|
297
|
+
let lastEventsPruneRunAt = 0;
|
|
298
|
+
// Ensure console schema exists before handlers query console tables.
|
|
299
|
+
const consoleSchemaReadyPromise = (options.consoleSchemaReady ??
|
|
300
|
+
options.dialect.ensureConsoleSchema?.(options.db) ??
|
|
301
|
+
Promise.resolve()).catch((err) => {
|
|
281
302
|
console.error('[console] Failed to ensure console schema:', err);
|
|
303
|
+
throw err;
|
|
282
304
|
});
|
|
283
305
|
// CORS configuration
|
|
284
306
|
const corsOrigins = options.corsOrigins ?? [
|
|
285
307
|
'http://localhost:5173',
|
|
286
308
|
'https://console.sync.dev',
|
|
287
309
|
];
|
|
310
|
+
const allowWildcardCors = corsOrigins === '*';
|
|
288
311
|
routes.use('*', cors({
|
|
289
|
-
origin:
|
|
312
|
+
origin: allowWildcardCors ? '*' : corsOrigins,
|
|
290
313
|
allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
|
291
314
|
allowHeaders: [
|
|
292
315
|
'Content-Type',
|
|
@@ -298,8 +321,30 @@ export function createConsoleRoutes(options) {
|
|
|
298
321
|
'Tracestate',
|
|
299
322
|
],
|
|
300
323
|
exposeHeaders: ['X-Total-Count'],
|
|
301
|
-
credentials:
|
|
324
|
+
credentials: !allowWildcardCors,
|
|
302
325
|
}));
|
|
326
|
+
const ensureConsoleSchemaReady = async (c) => {
|
|
327
|
+
try {
|
|
328
|
+
await consoleSchemaReadyPromise;
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return c.json({ error: 'CONSOLE_SCHEMA_UNAVAILABLE' }, 503);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
routes.use('*', async (c, next) => {
|
|
336
|
+
const readyError = await ensureConsoleSchemaReady(c);
|
|
337
|
+
if (readyError) {
|
|
338
|
+
return readyError;
|
|
339
|
+
}
|
|
340
|
+
await next();
|
|
341
|
+
});
|
|
342
|
+
routes.use('*', async (c, next) => {
|
|
343
|
+
if (c.req.method !== 'OPTIONS') {
|
|
344
|
+
triggerAutomaticEventsPrune();
|
|
345
|
+
}
|
|
346
|
+
await next();
|
|
347
|
+
});
|
|
303
348
|
// Auth middleware
|
|
304
349
|
const requireAuth = async (c) => {
|
|
305
350
|
const auth = await options.authenticate(c);
|
|
@@ -385,6 +430,161 @@ export function createConsoleRoutes(options) {
|
|
|
385
430
|
resultPayload: parseJsonValue(row.result_payload),
|
|
386
431
|
createdAt: row.created_at ?? '',
|
|
387
432
|
});
|
|
433
|
+
const deleteUnreferencedPayloadSnapshots = async () => {
|
|
434
|
+
const result = await db
|
|
435
|
+
.deleteFrom('sync_request_payloads')
|
|
436
|
+
.where('payload_ref', 'not in', db
|
|
437
|
+
.selectFrom('sync_request_events')
|
|
438
|
+
.select('payload_ref')
|
|
439
|
+
.where('payload_ref', 'is not', null))
|
|
440
|
+
.executeTakeFirst();
|
|
441
|
+
return Number(result?.numDeletedRows ?? 0);
|
|
442
|
+
};
|
|
443
|
+
const pruneRequestEventsByAge = async () => {
|
|
444
|
+
if (requestEventsMaxAgeMs <= 0) {
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
const cutoffDate = new Date(Date.now() - requestEventsMaxAgeMs);
|
|
448
|
+
const result = await db
|
|
449
|
+
.deleteFrom('sync_request_events')
|
|
450
|
+
.where('created_at', '<', cutoffDate.toISOString())
|
|
451
|
+
.executeTakeFirst();
|
|
452
|
+
return Number(result?.numDeletedRows ?? 0);
|
|
453
|
+
};
|
|
454
|
+
const pruneRequestEventsByCount = async () => {
|
|
455
|
+
if (requestEventsMaxRows <= 0) {
|
|
456
|
+
return 0;
|
|
457
|
+
}
|
|
458
|
+
const countRow = await db
|
|
459
|
+
.selectFrom('sync_request_events')
|
|
460
|
+
.select(({ fn }) => fn.countAll().as('total'))
|
|
461
|
+
.executeTakeFirst();
|
|
462
|
+
const total = coerceNumber(countRow?.total) ?? 0;
|
|
463
|
+
if (total <= requestEventsMaxRows) {
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
const cutoffRow = await db
|
|
467
|
+
.selectFrom('sync_request_events')
|
|
468
|
+
.select(['event_id'])
|
|
469
|
+
.orderBy('event_id', 'desc')
|
|
470
|
+
.offset(requestEventsMaxRows)
|
|
471
|
+
.limit(1)
|
|
472
|
+
.executeTakeFirst();
|
|
473
|
+
const cutoffEventId = coerceNumber(cutoffRow?.event_id);
|
|
474
|
+
if (cutoffEventId === null) {
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
const result = await db
|
|
478
|
+
.deleteFrom('sync_request_events')
|
|
479
|
+
.where('event_id', '<=', cutoffEventId)
|
|
480
|
+
.executeTakeFirst();
|
|
481
|
+
return Number(result?.numDeletedRows ?? 0);
|
|
482
|
+
};
|
|
483
|
+
const pruneOperationEventsByAge = async () => {
|
|
484
|
+
if (operationEventsMaxAgeMs <= 0) {
|
|
485
|
+
return 0;
|
|
486
|
+
}
|
|
487
|
+
const cutoffDate = new Date(Date.now() - operationEventsMaxAgeMs);
|
|
488
|
+
const result = await db
|
|
489
|
+
.deleteFrom('sync_operation_events')
|
|
490
|
+
.where('created_at', '<', cutoffDate.toISOString())
|
|
491
|
+
.executeTakeFirst();
|
|
492
|
+
return Number(result?.numDeletedRows ?? 0);
|
|
493
|
+
};
|
|
494
|
+
const pruneOperationEventsByCount = async () => {
|
|
495
|
+
if (operationEventsMaxRows <= 0) {
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
const countRow = await db
|
|
499
|
+
.selectFrom('sync_operation_events')
|
|
500
|
+
.select(({ fn }) => fn.countAll().as('total'))
|
|
501
|
+
.executeTakeFirst();
|
|
502
|
+
const total = coerceNumber(countRow?.total) ?? 0;
|
|
503
|
+
if (total <= operationEventsMaxRows) {
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
506
|
+
const cutoffRow = await db
|
|
507
|
+
.selectFrom('sync_operation_events')
|
|
508
|
+
.select(['operation_id'])
|
|
509
|
+
.orderBy('operation_id', 'desc')
|
|
510
|
+
.offset(operationEventsMaxRows)
|
|
511
|
+
.limit(1)
|
|
512
|
+
.executeTakeFirst();
|
|
513
|
+
const cutoffOperationId = coerceNumber(cutoffRow?.operation_id);
|
|
514
|
+
if (cutoffOperationId === null) {
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
const result = await db
|
|
518
|
+
.deleteFrom('sync_operation_events')
|
|
519
|
+
.where('operation_id', '<=', cutoffOperationId)
|
|
520
|
+
.executeTakeFirst();
|
|
521
|
+
return Number(result?.numDeletedRows ?? 0);
|
|
522
|
+
};
|
|
523
|
+
const pruneConsoleEvents = async () => {
|
|
524
|
+
const requestEventsDeletedByAge = await pruneRequestEventsByAge();
|
|
525
|
+
const requestEventsDeletedByCount = await pruneRequestEventsByCount();
|
|
526
|
+
const requestEventsDeleted = requestEventsDeletedByAge + requestEventsDeletedByCount;
|
|
527
|
+
const operationEventsDeletedByAge = await pruneOperationEventsByAge();
|
|
528
|
+
const operationEventsDeletedByCount = await pruneOperationEventsByCount();
|
|
529
|
+
const operationEventsDeleted = operationEventsDeletedByAge + operationEventsDeletedByCount;
|
|
530
|
+
const payloadSnapshotsDeleted = await deleteUnreferencedPayloadSnapshots();
|
|
531
|
+
const totalDeleted = requestEventsDeleted + operationEventsDeleted;
|
|
532
|
+
return {
|
|
533
|
+
requestEventsDeleted,
|
|
534
|
+
operationEventsDeleted,
|
|
535
|
+
payloadSnapshotsDeleted,
|
|
536
|
+
totalDeleted,
|
|
537
|
+
};
|
|
538
|
+
};
|
|
539
|
+
let eventsPrunePromise = null;
|
|
540
|
+
const runEventsPrune = async () => {
|
|
541
|
+
if (eventsPrunePromise) {
|
|
542
|
+
return eventsPrunePromise;
|
|
543
|
+
}
|
|
544
|
+
let pending;
|
|
545
|
+
pending = pruneConsoleEvents()
|
|
546
|
+
.then((result) => {
|
|
547
|
+
lastEventsPruneRunAt = Date.now();
|
|
548
|
+
return result;
|
|
549
|
+
})
|
|
550
|
+
.finally(() => {
|
|
551
|
+
if (eventsPrunePromise === pending) {
|
|
552
|
+
eventsPrunePromise = null;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
eventsPrunePromise = pending;
|
|
556
|
+
return pending;
|
|
557
|
+
};
|
|
558
|
+
const triggerAutomaticEventsPrune = () => {
|
|
559
|
+
if (autoEventsPruneIntervalMs <= 0) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (eventsPrunePromise) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (Date.now() - lastEventsPruneRunAt < autoEventsPruneIntervalMs) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
void runEventsPrune()
|
|
569
|
+
.then((result) => {
|
|
570
|
+
if (result.totalDeleted <= 0 && result.payloadSnapshotsDeleted <= 0) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
logSyncEvent({
|
|
574
|
+
event: 'console.prune_events_auto',
|
|
575
|
+
deletedCount: result.totalDeleted,
|
|
576
|
+
requestEventsDeleted: result.requestEventsDeleted,
|
|
577
|
+
operationEventsDeleted: result.operationEventsDeleted,
|
|
578
|
+
payloadDeletedCount: result.payloadSnapshotsDeleted,
|
|
579
|
+
});
|
|
580
|
+
})
|
|
581
|
+
.catch((error) => {
|
|
582
|
+
logSyncEvent({
|
|
583
|
+
event: 'console.prune_events_auto_failed',
|
|
584
|
+
error: error instanceof Error ? error.message : String(error),
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
};
|
|
388
588
|
const recordOperationEvent = async (event) => {
|
|
389
589
|
await db
|
|
390
590
|
.insertInto('sync_operation_events')
|
|
@@ -2030,10 +2230,12 @@ export function createConsoleRoutes(options) {
|
|
|
2030
2230
|
return c.json({ error: 'UNAUTHENTICATED' }, 401);
|
|
2031
2231
|
const res = await db.deleteFrom('sync_request_events').executeTakeFirst();
|
|
2032
2232
|
const deletedCount = Number(res?.numDeletedRows ?? 0);
|
|
2233
|
+
const payloadDeletedCount = await deleteUnreferencedPayloadSnapshots();
|
|
2033
2234
|
logSyncEvent({
|
|
2034
2235
|
event: 'console.clear_events',
|
|
2035
2236
|
consoleUserId: auth.consoleUserId,
|
|
2036
2237
|
deletedCount,
|
|
2238
|
+
payloadDeletedCount,
|
|
2037
2239
|
});
|
|
2038
2240
|
const result = { deletedCount };
|
|
2039
2241
|
return c.json(result, 200);
|
|
@@ -2064,45 +2266,15 @@ export function createConsoleRoutes(options) {
|
|
|
2064
2266
|
const auth = await requireAuth(c);
|
|
2065
2267
|
if (!auth)
|
|
2066
2268
|
return c.json({ error: 'UNAUTHENTICATED' }, 401);
|
|
2067
|
-
|
|
2068
|
-
const
|
|
2069
|
-
// Delete by date first
|
|
2070
|
-
const resByDate = await db
|
|
2071
|
-
.deleteFrom('sync_request_events')
|
|
2072
|
-
.where('created_at', '<', cutoffDate.toISOString())
|
|
2073
|
-
.executeTakeFirst();
|
|
2074
|
-
let deletedCount = Number(resByDate?.numDeletedRows ?? 0);
|
|
2075
|
-
// Then delete oldest if we still have more than 10000 events
|
|
2076
|
-
const countRow = await db
|
|
2077
|
-
.selectFrom('sync_request_events')
|
|
2078
|
-
.select(({ fn }) => fn.countAll().as('total'))
|
|
2079
|
-
.executeTakeFirst();
|
|
2080
|
-
const total = coerceNumber(countRow?.total) ?? 0;
|
|
2081
|
-
const maxEvents = 10000;
|
|
2082
|
-
if (total > maxEvents) {
|
|
2083
|
-
// Find event_id cutoff to keep only newest maxEvents
|
|
2084
|
-
const cutoffRow = await db
|
|
2085
|
-
.selectFrom('sync_request_events')
|
|
2086
|
-
.select(['event_id'])
|
|
2087
|
-
.orderBy('event_id', 'desc')
|
|
2088
|
-
.offset(maxEvents)
|
|
2089
|
-
.limit(1)
|
|
2090
|
-
.executeTakeFirst();
|
|
2091
|
-
if (cutoffRow) {
|
|
2092
|
-
const cutoffEventId = coerceNumber(cutoffRow.event_id);
|
|
2093
|
-
if (cutoffEventId !== null) {
|
|
2094
|
-
const resByCount = await db
|
|
2095
|
-
.deleteFrom('sync_request_events')
|
|
2096
|
-
.where('event_id', '<=', cutoffEventId)
|
|
2097
|
-
.executeTakeFirst();
|
|
2098
|
-
deletedCount += Number(resByCount?.numDeletedRows ?? 0);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2269
|
+
const pruneResult = await runEventsPrune();
|
|
2270
|
+
const deletedCount = pruneResult.totalDeleted;
|
|
2102
2271
|
logSyncEvent({
|
|
2103
2272
|
event: 'console.prune_events',
|
|
2104
2273
|
consoleUserId: auth.consoleUserId,
|
|
2105
2274
|
deletedCount,
|
|
2275
|
+
requestEventsDeleted: pruneResult.requestEventsDeleted,
|
|
2276
|
+
operationEventsDeleted: pruneResult.operationEventsDeleted,
|
|
2277
|
+
payloadDeletedCount: pruneResult.payloadSnapshotsDeleted,
|
|
2106
2278
|
});
|
|
2107
2279
|
const result = { deletedCount };
|
|
2108
2280
|
return c.json(result, 200);
|