@syncular/console 0.0.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.
@@ -0,0 +1,926 @@
1
+ /**
2
+ * React Query hooks for Console API
3
+ */
4
+
5
+ import { unwrap } from '@syncular/transport-http';
6
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
7
+ import type {
8
+ ConsoleApiKey,
9
+ ConsoleApiKeyBulkRevokeResponse,
10
+ ConsoleClient,
11
+ ConsoleCommitDetail,
12
+ ConsoleCommitListItem,
13
+ ConsoleHandler,
14
+ ConsoleNotifyDataChangeResponse,
15
+ ConsoleOperationEvent,
16
+ ConsoleOperationType,
17
+ ConsoleRequestEvent,
18
+ ConsoleRequestPayload,
19
+ ConsoleTimelineItem,
20
+ LatencyStatsResponse,
21
+ PaginatedResponse,
22
+ SyncStats,
23
+ TimeseriesInterval,
24
+ TimeseriesRange,
25
+ TimeseriesStatsResponse,
26
+ } from '../lib/types';
27
+ import { useApiClient, useConnection } from './ConnectionContext';
28
+ import { useInstanceContext } from './useInstanceContext';
29
+
30
+ const queryKeys = {
31
+ stats: (params?: { partitionId?: string; instanceId?: string }) =>
32
+ ['console', 'stats', params] as const,
33
+ timeseries: (params?: {
34
+ interval?: TimeseriesInterval;
35
+ range?: TimeseriesRange;
36
+ partitionId?: string;
37
+ instanceId?: string;
38
+ }) => ['console', 'stats', 'timeseries', params] as const,
39
+ latency: (params?: {
40
+ range?: TimeseriesRange;
41
+ partitionId?: string;
42
+ instanceId?: string;
43
+ }) => ['console', 'stats', 'latency', params] as const,
44
+ commits: (params?: {
45
+ limit?: number;
46
+ offset?: number;
47
+ partitionId?: string;
48
+ instanceId?: string;
49
+ }) => ['console', 'commits', params] as const,
50
+ commitDetail: (
51
+ seq?: string | number,
52
+ partitionId?: string,
53
+ instanceId?: string
54
+ ) => ['console', 'commit-detail', seq, partitionId, instanceId] as const,
55
+ timeline: (params?: {
56
+ limit?: number;
57
+ offset?: number;
58
+ partitionId?: string;
59
+ instanceId?: string;
60
+ view?: 'all' | 'commits' | 'events';
61
+ eventType?: 'push' | 'pull';
62
+ actorId?: string;
63
+ clientId?: string;
64
+ requestId?: string;
65
+ traceId?: string;
66
+ table?: string;
67
+ outcome?: string;
68
+ search?: string;
69
+ from?: string;
70
+ to?: string;
71
+ }) => ['console', 'timeline', params] as const,
72
+ clients: (params?: {
73
+ limit?: number;
74
+ offset?: number;
75
+ partitionId?: string;
76
+ instanceId?: string;
77
+ }) => ['console', 'clients', params] as const,
78
+ eventDetail: (
79
+ id?: string | number,
80
+ partitionId?: string,
81
+ instanceId?: string
82
+ ) => ['console', 'event-detail', id, partitionId, instanceId] as const,
83
+ eventPayload: (
84
+ id?: string | number,
85
+ partitionId?: string,
86
+ instanceId?: string
87
+ ) => ['console', 'event-payload', id, partitionId, instanceId] as const,
88
+ handlers: (instanceId?: string) =>
89
+ ['console', 'handlers', instanceId] as const,
90
+ prunePreview: (instanceId?: string) =>
91
+ ['console', 'prune', 'preview', instanceId] as const,
92
+ operations: (params?: {
93
+ limit?: number;
94
+ offset?: number;
95
+ operationType?: ConsoleOperationType;
96
+ partitionId?: string;
97
+ instanceId?: string;
98
+ }) => ['console', 'operations', params] as const,
99
+ apiKeys: (params?: {
100
+ limit?: number;
101
+ offset?: number;
102
+ type?: 'relay' | 'proxy' | 'admin';
103
+ status?: 'active' | 'revoked' | 'expiring';
104
+ expiresWithinDays?: number;
105
+ instanceId?: string;
106
+ }) => ['console', 'api-keys', params] as const,
107
+ };
108
+
109
+ function resolveRefetchInterval(
110
+ refreshIntervalMs: number | undefined,
111
+ defaultValueMs: number
112
+ ): number | false {
113
+ if (refreshIntervalMs === 0) return false;
114
+ return refreshIntervalMs ?? defaultValueMs;
115
+ }
116
+
117
+ interface InstanceQueryFilter {
118
+ instanceId?: string;
119
+ }
120
+
121
+ function withInstanceQuery<T extends Record<string, unknown>>(
122
+ query: T,
123
+ instanceId: string | undefined
124
+ ): T & InstanceQueryFilter {
125
+ if (!instanceId) return query;
126
+ return { ...query, instanceId };
127
+ }
128
+
129
+ function serializePathSegment(value: string | number): string {
130
+ return encodeURIComponent(String(value));
131
+ }
132
+
133
+ function buildConsoleUrl(
134
+ serverUrl: string,
135
+ path: string,
136
+ queryString?: URLSearchParams
137
+ ): string {
138
+ const baseUrl = serverUrl.endsWith('/') ? serverUrl.slice(0, -1) : serverUrl;
139
+ const suffix = queryString?.toString();
140
+ return `${baseUrl}${path}${suffix ? `?${suffix}` : ''}`;
141
+ }
142
+
143
+ export function useStats(
144
+ options: {
145
+ refetchIntervalMs?: number;
146
+ partitionId?: string;
147
+ instanceId?: string;
148
+ } = {}
149
+ ) {
150
+ const client = useApiClient();
151
+ const { instanceId: selectedInstanceId } = useInstanceContext();
152
+ const instanceId = options.instanceId ?? selectedInstanceId;
153
+ const query = withInstanceQuery(
154
+ options.partitionId ? { partitionId: options.partitionId } : {},
155
+ instanceId
156
+ );
157
+
158
+ return useQuery<SyncStats>({
159
+ queryKey: queryKeys.stats({
160
+ partitionId: options.partitionId,
161
+ instanceId,
162
+ }),
163
+ queryFn: () => {
164
+ if (!client) throw new Error('Not connected');
165
+ return unwrap(client.GET('/console/stats', { params: { query } }));
166
+ },
167
+ enabled: !!client,
168
+ refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 5000),
169
+ });
170
+ }
171
+
172
+ export function useTimeseriesStats(
173
+ params: {
174
+ interval?: TimeseriesInterval;
175
+ range?: TimeseriesRange;
176
+ partitionId?: string;
177
+ instanceId?: string;
178
+ } = {},
179
+ options: { refetchIntervalMs?: number; enabled?: boolean } = {}
180
+ ) {
181
+ const client = useApiClient();
182
+ const { config: connectionConfig } = useConnection();
183
+ const { instanceId: selectedInstanceId } = useInstanceContext();
184
+ const instanceId = params.instanceId ?? selectedInstanceId;
185
+
186
+ return useQuery<TimeseriesStatsResponse>({
187
+ queryKey: queryKeys.timeseries({ ...params, instanceId }),
188
+ queryFn: async () => {
189
+ if (!client || !connectionConfig) throw new Error('Not connected');
190
+ // Use fetch directly since this endpoint may not be in OpenAPI yet
191
+ const queryString = new URLSearchParams();
192
+ if (params.interval) queryString.set('interval', params.interval);
193
+ if (params.range) queryString.set('range', params.range);
194
+ if (params.partitionId)
195
+ queryString.set('partitionId', params.partitionId);
196
+ if (instanceId) queryString.set('instanceId', instanceId);
197
+ const response = await fetch(
198
+ `${connectionConfig.serverUrl}/console/stats/timeseries?${queryString}`,
199
+ { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
200
+ );
201
+ if (!response.ok) throw new Error('Failed to fetch timeseries stats');
202
+ return response.json();
203
+ },
204
+ enabled: (options.enabled ?? true) && !!client && !!connectionConfig,
205
+ refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
206
+ });
207
+ }
208
+
209
+ export function useLatencyStats(
210
+ params: {
211
+ range?: TimeseriesRange;
212
+ partitionId?: string;
213
+ instanceId?: string;
214
+ } = {},
215
+ options: { refetchIntervalMs?: number; enabled?: boolean } = {}
216
+ ) {
217
+ const client = useApiClient();
218
+ const { config: connectionConfig } = useConnection();
219
+ const { instanceId: selectedInstanceId } = useInstanceContext();
220
+ const instanceId = params.instanceId ?? selectedInstanceId;
221
+
222
+ return useQuery<LatencyStatsResponse>({
223
+ queryKey: queryKeys.latency({ ...params, instanceId }),
224
+ queryFn: async () => {
225
+ if (!client || !connectionConfig) throw new Error('Not connected');
226
+ // Use fetch directly since this endpoint may not be in OpenAPI yet
227
+ const queryString = new URLSearchParams();
228
+ if (params.range) queryString.set('range', params.range);
229
+ if (params.partitionId)
230
+ queryString.set('partitionId', params.partitionId);
231
+ if (instanceId) queryString.set('instanceId', instanceId);
232
+ const response = await fetch(
233
+ `${connectionConfig.serverUrl}/console/stats/latency?${queryString}`,
234
+ { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
235
+ );
236
+ if (!response.ok) throw new Error('Failed to fetch latency stats');
237
+ return response.json();
238
+ },
239
+ enabled: (options.enabled ?? true) && !!client && !!connectionConfig,
240
+ refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 30000),
241
+ });
242
+ }
243
+
244
+ export function useCommits(
245
+ params: {
246
+ limit?: number;
247
+ offset?: number;
248
+ partitionId?: string;
249
+ instanceId?: string;
250
+ } = {},
251
+ options: { refetchIntervalMs?: number; enabled?: boolean } = {}
252
+ ) {
253
+ const client = useApiClient();
254
+ const { instanceId: selectedInstanceId } = useInstanceContext();
255
+ const instanceId = params.instanceId ?? selectedInstanceId;
256
+ const query = withInstanceQuery(
257
+ {
258
+ limit: params.limit,
259
+ offset: params.offset,
260
+ partitionId: params.partitionId,
261
+ },
262
+ instanceId
263
+ );
264
+
265
+ return useQuery<PaginatedResponse<ConsoleCommitListItem>>({
266
+ queryKey: queryKeys.commits({ ...params, instanceId }),
267
+ queryFn: () => {
268
+ if (!client) throw new Error('Not connected');
269
+ return unwrap(client.GET('/console/commits', { params: { query } }));
270
+ },
271
+ enabled: (options.enabled ?? true) && !!client,
272
+ refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
273
+ });
274
+ }
275
+
276
+ export function useCommitDetail(
277
+ seq: string | number | undefined,
278
+ options: { enabled?: boolean; partitionId?: string; instanceId?: string } = {}
279
+ ) {
280
+ const client = useApiClient();
281
+ const { config: connectionConfig } = useConnection();
282
+ const { instanceId: selectedInstanceId } = useInstanceContext();
283
+ const instanceId = options.instanceId ?? selectedInstanceId;
284
+
285
+ return useQuery<ConsoleCommitDetail>({
286
+ queryKey: queryKeys.commitDetail(seq, options.partitionId, instanceId),
287
+ queryFn: async () => {
288
+ if (!client || !connectionConfig) throw new Error('Not connected');
289
+ if (seq === undefined) throw new Error('Commit sequence is required');
290
+ const queryString = new URLSearchParams();
291
+ if (options.partitionId)
292
+ queryString.set('partitionId', options.partitionId);
293
+ if (instanceId) queryString.set('instanceId', instanceId);
294
+ const suffix = queryString.toString();
295
+ const response = await fetch(
296
+ `${connectionConfig.serverUrl}/console/commits/${serializePathSegment(seq)}${suffix ? `?${suffix}` : ''}`,
297
+ { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
298
+ );
299
+ if (!response.ok) throw new Error('Failed to fetch commit detail');
300
+ return response.json();
301
+ },
302
+ enabled: (options.enabled ?? true) && seq !== undefined && !!client,
303
+ });
304
+ }
305
+
306
+ export function useTimeline(
307
+ params: {
308
+ limit?: number;
309
+ offset?: number;
310
+ partitionId?: string;
311
+ instanceId?: string;
312
+ view?: 'all' | 'commits' | 'events';
313
+ eventType?: 'push' | 'pull';
314
+ actorId?: string;
315
+ clientId?: string;
316
+ requestId?: string;
317
+ traceId?: string;
318
+ table?: string;
319
+ outcome?: string;
320
+ search?: string;
321
+ from?: string;
322
+ to?: string;
323
+ } = {},
324
+ options: { refetchIntervalMs?: number; enabled?: boolean } = {}
325
+ ) {
326
+ const client = useApiClient();
327
+ const { instanceId: selectedInstanceId } = useInstanceContext();
328
+ const instanceId = params.instanceId ?? selectedInstanceId;
329
+ const query = withInstanceQuery(
330
+ {
331
+ limit: params.limit,
332
+ offset: params.offset,
333
+ partitionId: params.partitionId,
334
+ view: params.view,
335
+ eventType: params.eventType,
336
+ actorId: params.actorId,
337
+ clientId: params.clientId,
338
+ requestId: params.requestId,
339
+ traceId: params.traceId,
340
+ table: params.table,
341
+ outcome: params.outcome,
342
+ search: params.search,
343
+ from: params.from,
344
+ to: params.to,
345
+ },
346
+ instanceId
347
+ );
348
+
349
+ return useQuery<PaginatedResponse<ConsoleTimelineItem>>({
350
+ queryKey: queryKeys.timeline({ ...params, instanceId }),
351
+ queryFn: () => {
352
+ if (!client) throw new Error('Not connected');
353
+ return unwrap(client.GET('/console/timeline', { params: { query } }));
354
+ },
355
+ enabled: (options.enabled ?? true) && !!client,
356
+ refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
357
+ });
358
+ }
359
+
360
+ export function useClients(
361
+ params: {
362
+ limit?: number;
363
+ offset?: number;
364
+ partitionId?: string;
365
+ instanceId?: string;
366
+ } = {},
367
+ options: { refetchIntervalMs?: number; enabled?: boolean } = {}
368
+ ) {
369
+ const client = useApiClient();
370
+ const { instanceId: selectedInstanceId } = useInstanceContext();
371
+ const instanceId = params.instanceId ?? selectedInstanceId;
372
+ const query = withInstanceQuery(
373
+ {
374
+ limit: params.limit,
375
+ offset: params.offset,
376
+ partitionId: params.partitionId,
377
+ },
378
+ instanceId
379
+ );
380
+
381
+ return useQuery<PaginatedResponse<ConsoleClient>>({
382
+ queryKey: queryKeys.clients({ ...params, instanceId }),
383
+ queryFn: () => {
384
+ if (!client) throw new Error('Not connected');
385
+ return unwrap(client.GET('/console/clients', { params: { query } }));
386
+ },
387
+ enabled: (options.enabled ?? true) && !!client,
388
+ refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
389
+ });
390
+ }
391
+
392
+ export function useRequestEventDetail(
393
+ id: string | number | undefined,
394
+ options: { enabled?: boolean; partitionId?: string; instanceId?: string } = {}
395
+ ) {
396
+ const client = useApiClient();
397
+ const { config: connectionConfig } = useConnection();
398
+ const { instanceId: selectedInstanceId } = useInstanceContext();
399
+ const instanceId = options.instanceId ?? selectedInstanceId;
400
+
401
+ return useQuery<ConsoleRequestEvent>({
402
+ queryKey: queryKeys.eventDetail(id, options.partitionId, instanceId),
403
+ queryFn: async () => {
404
+ if (!client || !connectionConfig) throw new Error('Not connected');
405
+ if (id === undefined) throw new Error('Event id is required');
406
+ const queryString = new URLSearchParams();
407
+ if (options.partitionId)
408
+ queryString.set('partitionId', options.partitionId);
409
+ if (instanceId) queryString.set('instanceId', instanceId);
410
+ const suffix = queryString.toString();
411
+ const response = await fetch(
412
+ `${connectionConfig.serverUrl}/console/events/${serializePathSegment(id)}${suffix ? `?${suffix}` : ''}`,
413
+ { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
414
+ );
415
+ if (!response.ok) throw new Error('Failed to fetch event detail');
416
+ return response.json();
417
+ },
418
+ enabled: (options.enabled ?? true) && id !== undefined && !!client,
419
+ });
420
+ }
421
+
422
+ export function useRequestEventPayload(
423
+ id: string | number | undefined,
424
+ options: { enabled?: boolean; partitionId?: string; instanceId?: string } = {}
425
+ ) {
426
+ const client = useApiClient();
427
+ const { config: connectionConfig } = useConnection();
428
+ const { instanceId: selectedInstanceId } = useInstanceContext();
429
+ const instanceId = options.instanceId ?? selectedInstanceId;
430
+
431
+ return useQuery<ConsoleRequestPayload>({
432
+ queryKey: queryKeys.eventPayload(id, options.partitionId, instanceId),
433
+ queryFn: async () => {
434
+ if (!client || !connectionConfig) throw new Error('Not connected');
435
+ if (id === undefined) throw new Error('Event id is required');
436
+ const queryString = new URLSearchParams();
437
+ if (options.partitionId)
438
+ queryString.set('partitionId', options.partitionId);
439
+ if (instanceId) queryString.set('instanceId', instanceId);
440
+ const suffix = queryString.toString();
441
+ const response = await fetch(
442
+ `${connectionConfig.serverUrl}/console/events/${serializePathSegment(id)}/payload${suffix ? `?${suffix}` : ''}`,
443
+ { headers: { Authorization: `Bearer ${connectionConfig.token}` } }
444
+ );
445
+ if (!response.ok) throw new Error('Failed to fetch event payload');
446
+ return response.json();
447
+ },
448
+ enabled: (options.enabled ?? true) && id !== undefined && !!client,
449
+ });
450
+ }
451
+
452
+ export function useHandlers(options: { instanceId?: string } = {}) {
453
+ const client = useApiClient();
454
+ const { config: connectionConfig } = useConnection();
455
+ const { instanceId: selectedInstanceId } = useInstanceContext();
456
+ const instanceId = options.instanceId ?? selectedInstanceId;
457
+
458
+ return useQuery<{ items: ConsoleHandler[] }>({
459
+ queryKey: queryKeys.handlers(instanceId),
460
+ queryFn: async () => {
461
+ if (!client || !connectionConfig) throw new Error('Not connected');
462
+ const queryString = new URLSearchParams();
463
+ if (instanceId) queryString.set('instanceId', instanceId);
464
+ const response = await fetch(
465
+ buildConsoleUrl(
466
+ connectionConfig.serverUrl,
467
+ '/console/handlers',
468
+ queryString
469
+ ),
470
+ {
471
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
472
+ }
473
+ );
474
+ if (!response.ok) throw new Error('Failed to fetch handlers');
475
+ return response.json();
476
+ },
477
+ enabled: !!client && !!connectionConfig,
478
+ });
479
+ }
480
+
481
+ export function usePrunePreview(
482
+ options: { enabled?: boolean; instanceId?: string } = {}
483
+ ) {
484
+ const client = useApiClient();
485
+ const { config: connectionConfig } = useConnection();
486
+ const { instanceId: selectedInstanceId } = useInstanceContext();
487
+ const instanceId = options.instanceId ?? selectedInstanceId;
488
+
489
+ return useQuery<{ watermarkCommitSeq: number; commitsToDelete: number }>({
490
+ queryKey: queryKeys.prunePreview(instanceId),
491
+ queryFn: async () => {
492
+ if (!client || !connectionConfig) throw new Error('Not connected');
493
+ const queryString = new URLSearchParams();
494
+ if (instanceId) queryString.set('instanceId', instanceId);
495
+ const response = await fetch(
496
+ buildConsoleUrl(
497
+ connectionConfig.serverUrl,
498
+ '/console/prune/preview',
499
+ queryString
500
+ ),
501
+ {
502
+ method: 'POST',
503
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
504
+ }
505
+ );
506
+ if (!response.ok) throw new Error('Failed to fetch prune preview');
507
+ return response.json();
508
+ },
509
+ enabled: !!client && !!connectionConfig && (options.enabled ?? true),
510
+ });
511
+ }
512
+
513
+ export function useOperationEvents(
514
+ params: {
515
+ limit?: number;
516
+ offset?: number;
517
+ operationType?: ConsoleOperationType;
518
+ partitionId?: string;
519
+ instanceId?: string;
520
+ } = {},
521
+ options: { enabled?: boolean; refetchIntervalMs?: number } = {}
522
+ ) {
523
+ const client = useApiClient();
524
+ const { instanceId: selectedInstanceId } = useInstanceContext();
525
+ const instanceId = params.instanceId ?? selectedInstanceId;
526
+ const query = withInstanceQuery(
527
+ {
528
+ limit: params.limit,
529
+ offset: params.offset,
530
+ operationType: params.operationType,
531
+ partitionId: params.partitionId,
532
+ },
533
+ instanceId
534
+ );
535
+
536
+ return useQuery<PaginatedResponse<ConsoleOperationEvent>>({
537
+ queryKey: queryKeys.operations({ ...params, instanceId }),
538
+ queryFn: () => {
539
+ if (!client) throw new Error('Not connected');
540
+ return unwrap(client.GET('/console/operations', { params: { query } }));
541
+ },
542
+ enabled: (options.enabled ?? true) && !!client,
543
+ refetchInterval: resolveRefetchInterval(options.refetchIntervalMs, 10000),
544
+ });
545
+ }
546
+
547
+ export function useEvictClientMutation() {
548
+ const client = useApiClient();
549
+ const { config: connectionConfig } = useConnection();
550
+ const { instanceId: selectedInstanceId } = useInstanceContext();
551
+ const queryClient = useQueryClient();
552
+
553
+ return useMutation<
554
+ { evicted: boolean },
555
+ Error,
556
+ { clientId: string; partitionId?: string; instanceId?: string }
557
+ >({
558
+ mutationFn: async ({ clientId, partitionId, instanceId }) => {
559
+ if (!client || !connectionConfig) throw new Error('Not connected');
560
+ const effectiveInstanceId = instanceId ?? selectedInstanceId;
561
+ const queryString = new URLSearchParams();
562
+ if (partitionId) queryString.set('partitionId', partitionId);
563
+ if (effectiveInstanceId)
564
+ queryString.set('instanceId', effectiveInstanceId);
565
+ const response = await fetch(
566
+ buildConsoleUrl(
567
+ connectionConfig.serverUrl,
568
+ `/console/clients/${serializePathSegment(clientId)}`,
569
+ queryString
570
+ ),
571
+ {
572
+ method: 'DELETE',
573
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
574
+ }
575
+ );
576
+ if (!response.ok) throw new Error('Failed to evict client');
577
+ return response.json();
578
+ },
579
+ onSuccess: () => {
580
+ queryClient.invalidateQueries({ queryKey: ['console', 'clients'] });
581
+ queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
582
+ queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
583
+ },
584
+ });
585
+ }
586
+
587
+ export function usePruneMutation() {
588
+ const client = useApiClient();
589
+ const { config: connectionConfig } = useConnection();
590
+ const { instanceId } = useInstanceContext();
591
+ const queryClient = useQueryClient();
592
+
593
+ return useMutation<{ deletedCommits: number }, Error, void>({
594
+ mutationFn: async () => {
595
+ if (!client || !connectionConfig) throw new Error('Not connected');
596
+ const queryString = new URLSearchParams();
597
+ if (instanceId) queryString.set('instanceId', instanceId);
598
+ const response = await fetch(
599
+ buildConsoleUrl(
600
+ connectionConfig.serverUrl,
601
+ '/console/prune',
602
+ queryString
603
+ ),
604
+ {
605
+ method: 'POST',
606
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
607
+ }
608
+ );
609
+ if (!response.ok) throw new Error('Failed to prune');
610
+ return response.json();
611
+ },
612
+ onSuccess: () => {
613
+ queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
614
+ queryClient.invalidateQueries({ queryKey: ['console', 'commits'] });
615
+ queryClient.invalidateQueries({ queryKey: ['console', 'timeline'] });
616
+ queryClient.invalidateQueries({
617
+ queryKey: ['console', 'prune', 'preview'],
618
+ });
619
+ queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
620
+ },
621
+ });
622
+ }
623
+
624
+ export function useCompactMutation() {
625
+ const client = useApiClient();
626
+ const { config: connectionConfig } = useConnection();
627
+ const { instanceId } = useInstanceContext();
628
+ const queryClient = useQueryClient();
629
+
630
+ return useMutation<{ deletedChanges: number }, Error, void>({
631
+ mutationFn: async () => {
632
+ if (!client || !connectionConfig) throw new Error('Not connected');
633
+ const queryString = new URLSearchParams();
634
+ if (instanceId) queryString.set('instanceId', instanceId);
635
+ const response = await fetch(
636
+ buildConsoleUrl(
637
+ connectionConfig.serverUrl,
638
+ '/console/compact',
639
+ queryString
640
+ ),
641
+ {
642
+ method: 'POST',
643
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
644
+ }
645
+ );
646
+ if (!response.ok) throw new Error('Failed to compact');
647
+ return response.json();
648
+ },
649
+ onSuccess: () => {
650
+ queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
651
+ queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
652
+ },
653
+ });
654
+ }
655
+
656
+ export function useNotifyDataChangeMutation() {
657
+ const client = useApiClient();
658
+ const { config: connectionConfig } = useConnection();
659
+ const { instanceId: selectedInstanceId } = useInstanceContext();
660
+ const queryClient = useQueryClient();
661
+
662
+ return useMutation<
663
+ ConsoleNotifyDataChangeResponse,
664
+ Error,
665
+ { tables: string[]; partitionId?: string; instanceId?: string }
666
+ >({
667
+ mutationFn: async (request) => {
668
+ if (!client || !connectionConfig) throw new Error('Not connected');
669
+ const effectiveInstanceId = request.instanceId ?? selectedInstanceId;
670
+ const queryString = new URLSearchParams();
671
+ if (effectiveInstanceId)
672
+ queryString.set('instanceId', effectiveInstanceId);
673
+ const response = await fetch(
674
+ buildConsoleUrl(
675
+ connectionConfig.serverUrl,
676
+ '/console/notify-data-change',
677
+ queryString
678
+ ),
679
+ {
680
+ method: 'POST',
681
+ headers: {
682
+ Authorization: `Bearer ${connectionConfig.token}`,
683
+ 'Content-Type': 'application/json',
684
+ },
685
+ body: JSON.stringify({
686
+ tables: request.tables,
687
+ partitionId: request.partitionId,
688
+ }),
689
+ }
690
+ );
691
+ if (!response.ok) throw new Error('Failed to notify data change');
692
+ return response.json();
693
+ },
694
+ onSuccess: () => {
695
+ queryClient.invalidateQueries({ queryKey: ['console', 'stats'] });
696
+ queryClient.invalidateQueries({ queryKey: ['console', 'commits'] });
697
+ queryClient.invalidateQueries({ queryKey: ['console', 'timeline'] });
698
+ queryClient.invalidateQueries({ queryKey: ['console', 'operations'] });
699
+ },
700
+ });
701
+ }
702
+
703
+ export function useApiKeys(
704
+ params: {
705
+ limit?: number;
706
+ offset?: number;
707
+ type?: 'relay' | 'proxy' | 'admin';
708
+ status?: 'active' | 'revoked' | 'expiring';
709
+ expiresWithinDays?: number;
710
+ instanceId?: string;
711
+ } = {}
712
+ ) {
713
+ const client = useApiClient();
714
+ const { config: connectionConfig } = useConnection();
715
+ const { instanceId: selectedInstanceId } = useInstanceContext();
716
+ const instanceId = params.instanceId ?? selectedInstanceId;
717
+
718
+ return useQuery<PaginatedResponse<ConsoleApiKey>>({
719
+ queryKey: queryKeys.apiKeys({ ...params, instanceId }),
720
+ queryFn: async () => {
721
+ if (!client || !connectionConfig) throw new Error('Not connected');
722
+ const queryString = new URLSearchParams();
723
+ if (params.limit !== undefined)
724
+ queryString.set('limit', String(params.limit));
725
+ if (params.offset !== undefined)
726
+ queryString.set('offset', String(params.offset));
727
+ if (params.type) queryString.set('type', params.type);
728
+ if (params.status) queryString.set('status', params.status);
729
+ if (params.expiresWithinDays !== undefined) {
730
+ queryString.set('expiresWithinDays', String(params.expiresWithinDays));
731
+ }
732
+ if (instanceId) queryString.set('instanceId', instanceId);
733
+
734
+ const response = await fetch(
735
+ buildConsoleUrl(
736
+ connectionConfig.serverUrl,
737
+ '/console/api-keys',
738
+ queryString
739
+ ),
740
+ {
741
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
742
+ }
743
+ );
744
+ if (!response.ok) throw new Error('Failed to fetch API keys');
745
+ return response.json();
746
+ },
747
+ enabled: !!client && !!connectionConfig,
748
+ });
749
+ }
750
+
751
+ export function useCreateApiKeyMutation() {
752
+ const client = useApiClient();
753
+ const { config: connectionConfig } = useConnection();
754
+ const { instanceId } = useInstanceContext();
755
+ const queryClient = useQueryClient();
756
+
757
+ return useMutation<
758
+ { key: ConsoleApiKey; secretKey: string },
759
+ Error,
760
+ {
761
+ name: string;
762
+ keyType: 'relay' | 'proxy' | 'admin';
763
+ scopeKeys?: string[];
764
+ actorId?: string;
765
+ expiresInDays?: number;
766
+ }
767
+ >({
768
+ mutationFn: async (request) => {
769
+ if (!client || !connectionConfig) throw new Error('Not connected');
770
+ const queryString = new URLSearchParams();
771
+ if (instanceId) queryString.set('instanceId', instanceId);
772
+ const response = await fetch(
773
+ buildConsoleUrl(
774
+ connectionConfig.serverUrl,
775
+ '/console/api-keys',
776
+ queryString
777
+ ),
778
+ {
779
+ method: 'POST',
780
+ headers: {
781
+ Authorization: `Bearer ${connectionConfig.token}`,
782
+ 'Content-Type': 'application/json',
783
+ },
784
+ body: JSON.stringify(request),
785
+ }
786
+ );
787
+ if (!response.ok) throw new Error('Failed to create API key');
788
+ return response.json();
789
+ },
790
+ onSuccess: () => {
791
+ queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
792
+ },
793
+ });
794
+ }
795
+
796
+ export function useRevokeApiKeyMutation() {
797
+ const client = useApiClient();
798
+ const { config: connectionConfig } = useConnection();
799
+ const { instanceId } = useInstanceContext();
800
+ const queryClient = useQueryClient();
801
+
802
+ return useMutation<{ revoked: boolean }, Error, string>({
803
+ mutationFn: async (keyId) => {
804
+ if (!client || !connectionConfig) throw new Error('Not connected');
805
+ const queryString = new URLSearchParams();
806
+ if (instanceId) queryString.set('instanceId', instanceId);
807
+ const response = await fetch(
808
+ buildConsoleUrl(
809
+ connectionConfig.serverUrl,
810
+ `/console/api-keys/${serializePathSegment(keyId)}`,
811
+ queryString
812
+ ),
813
+ {
814
+ method: 'DELETE',
815
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
816
+ }
817
+ );
818
+ if (!response.ok) throw new Error('Failed to revoke API key');
819
+ return response.json();
820
+ },
821
+ onSuccess: () => {
822
+ queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
823
+ },
824
+ });
825
+ }
826
+
827
+ export function useBulkRevokeApiKeysMutation() {
828
+ const client = useApiClient();
829
+ const { config: connectionConfig } = useConnection();
830
+ const { instanceId } = useInstanceContext();
831
+ const queryClient = useQueryClient();
832
+
833
+ return useMutation<
834
+ ConsoleApiKeyBulkRevokeResponse,
835
+ Error,
836
+ { keyIds: string[] }
837
+ >({
838
+ mutationFn: async (request) => {
839
+ if (!client || !connectionConfig) throw new Error('Not connected');
840
+ const queryString = new URLSearchParams();
841
+ if (instanceId) queryString.set('instanceId', instanceId);
842
+ const response = await fetch(
843
+ buildConsoleUrl(
844
+ connectionConfig.serverUrl,
845
+ '/console/api-keys/bulk-revoke',
846
+ queryString
847
+ ),
848
+ {
849
+ method: 'POST',
850
+ headers: {
851
+ Authorization: `Bearer ${connectionConfig.token}`,
852
+ 'Content-Type': 'application/json',
853
+ },
854
+ body: JSON.stringify(request),
855
+ }
856
+ );
857
+ if (!response.ok) throw new Error('Failed to bulk revoke API keys');
858
+ return response.json();
859
+ },
860
+ onSuccess: () => {
861
+ queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
862
+ },
863
+ });
864
+ }
865
+
866
+ export function useRotateApiKeyMutation() {
867
+ const client = useApiClient();
868
+ const { config: connectionConfig } = useConnection();
869
+ const { instanceId } = useInstanceContext();
870
+ const queryClient = useQueryClient();
871
+
872
+ return useMutation<{ key: ConsoleApiKey; secretKey: string }, Error, string>({
873
+ mutationFn: async (keyId) => {
874
+ if (!client || !connectionConfig) throw new Error('Not connected');
875
+ const queryString = new URLSearchParams();
876
+ if (instanceId) queryString.set('instanceId', instanceId);
877
+ const response = await fetch(
878
+ buildConsoleUrl(
879
+ connectionConfig.serverUrl,
880
+ `/console/api-keys/${serializePathSegment(keyId)}/rotate`,
881
+ queryString
882
+ ),
883
+ {
884
+ method: 'POST',
885
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
886
+ }
887
+ );
888
+ if (!response.ok) throw new Error('Failed to rotate API key');
889
+ return response.json();
890
+ },
891
+ onSuccess: () => {
892
+ queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
893
+ },
894
+ });
895
+ }
896
+
897
+ export function useStageRotateApiKeyMutation() {
898
+ const client = useApiClient();
899
+ const { config: connectionConfig } = useConnection();
900
+ const { instanceId } = useInstanceContext();
901
+ const queryClient = useQueryClient();
902
+
903
+ return useMutation<{ key: ConsoleApiKey; secretKey: string }, Error, string>({
904
+ mutationFn: async (keyId) => {
905
+ if (!client || !connectionConfig) throw new Error('Not connected');
906
+ const queryString = new URLSearchParams();
907
+ if (instanceId) queryString.set('instanceId', instanceId);
908
+ const response = await fetch(
909
+ buildConsoleUrl(
910
+ connectionConfig.serverUrl,
911
+ `/console/api-keys/${serializePathSegment(keyId)}/rotate/stage`,
912
+ queryString
913
+ ),
914
+ {
915
+ method: 'POST',
916
+ headers: { Authorization: `Bearer ${connectionConfig.token}` },
917
+ }
918
+ );
919
+ if (!response.ok) throw new Error('Failed to stage-rotate API key');
920
+ return response.json();
921
+ },
922
+ onSuccess: () => {
923
+ queryClient.invalidateQueries({ queryKey: ['console', 'api-keys'] });
924
+ },
925
+ });
926
+ }