@checkstack/integration-backend 0.1.29 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/router.ts CHANGED
@@ -1,409 +1,57 @@
1
1
  import { implement, ORPCError } from "@orpc/server";
2
- import type { SafeDatabase } from "@checkstack/backend-api";
3
2
  import {
4
3
  autoAuthMiddleware,
5
4
  correlationMiddleware,
6
- type RpcContext,
7
5
  type Logger,
6
+ type RpcContext,
7
+ type SafeDatabase,
8
8
  } from "@checkstack/backend-api";
9
- import type { SignalService } from "@checkstack/signal-common";
10
- import { eq, desc, and, gte, count } from "drizzle-orm";
9
+ import { extractErrorMessage } from "@checkstack/common";
10
+ import { integrationContract } from "@checkstack/integration-common";
11
11
 
12
- import type { IntegrationEventRegistry } from "./event-registry";
13
12
  import type { IntegrationProviderRegistry } from "./provider-registry";
14
- import type { DeliveryCoordinator } from "./delivery-coordinator";
15
13
  import type { ConnectionStore } from "./connection-store";
16
14
  import * as schema from "./schema";
17
- import {
18
- integrationContract,
19
- INTEGRATION_SUBSCRIPTION_CHANGED,
20
- } from "@checkstack/integration-common";
21
- import { extractErrorMessage } from "@checkstack/common";
22
-
23
- /**
24
- * Recursively extracts flattened property paths from a JSON Schema.
25
- * Used to provide template hints for payload properties.
26
- */
27
- interface JsonSchemaProperty {
28
- path: string;
29
- type: string;
30
- description?: string;
31
- }
32
-
33
- function extractJsonSchemaProperties(
34
- schema: Record<string, unknown>,
35
- basePath: string = ""
36
- ): JsonSchemaProperty[] {
37
- const properties: JsonSchemaProperty[] = [];
38
-
39
- const schemaType = schema["type"] as string | string[] | undefined;
40
- const schemaProperties = schema["properties"] as
41
- | Record<string, Record<string, unknown>>
42
- | undefined;
43
- const schemaItems = schema["items"] as Record<string, unknown> | undefined;
44
- const schemaDescription = schema["description"] as string | undefined;
45
-
46
- // Handle object with properties
47
- if (schemaProperties) {
48
- for (const [key, propSchema] of Object.entries(schemaProperties)) {
49
- const propPath = basePath ? `${basePath}.${key}` : key;
50
- const propType = (propSchema["type"] as string) || "unknown";
51
- const propDescription = propSchema["description"] as string | undefined;
52
-
53
- // Add this property
54
- properties.push({
55
- path: propPath,
56
- type: Array.isArray(propType) ? propType.join(" | ") : propType,
57
- description: propDescription,
58
- });
59
-
60
- // Recurse into nested objects
61
- if (propType === "object" || propSchema["properties"]) {
62
- properties.push(...extractJsonSchemaProperties(propSchema, propPath));
63
- }
64
-
65
- // Recurse into arrays (add [n] notation)
66
- if (propType === "array" && propSchema["items"]) {
67
- const itemsSchema = propSchema["items"] as Record<string, unknown>;
68
- properties.push(
69
- ...extractJsonSchemaProperties(itemsSchema, `${propPath}[n]`)
70
- );
71
- }
72
- }
73
- }
74
-
75
- // Handle array at root level
76
- if (schemaType === "array" && schemaItems) {
77
- properties.push(
78
- ...extractJsonSchemaProperties(schemaItems, `${basePath}[n]`)
79
- );
80
- }
81
-
82
- // If this is a primitive with a path, add it
83
- if (
84
- basePath &&
85
- schemaType &&
86
- schemaType !== "object" &&
87
- schemaType !== "array" &&
88
- !schemaProperties
89
- ) {
90
- properties.push({
91
- path: basePath,
92
- type: Array.isArray(schemaType) ? schemaType.join(" | ") : schemaType,
93
- description: schemaDescription,
94
- });
95
- }
96
-
97
- return properties;
98
- }
99
15
 
100
16
  interface RouterDeps {
101
17
  db: SafeDatabase<typeof schema>;
102
- eventRegistry: IntegrationEventRegistry;
103
18
  providerRegistry: IntegrationProviderRegistry;
104
- deliveryCoordinator: DeliveryCoordinator;
105
19
  connectionStore: ConnectionStore;
106
- signalService: SignalService;
107
20
  logger: Logger;
108
21
  }
109
22
 
110
23
  /**
111
- * Creates the integration router using contract-based implementation.
112
- *
113
- * Auth and access rules are automatically enforced via autoAuthMiddleware
114
- * based on the contract's meta.userType and meta.access.
24
+ * Integration router connection management only. The legacy
25
+ * subscription / event-listing / delivery-log endpoints were removed
26
+ * when the platform moved to the Automation Platform model.
115
27
  */
116
28
  export function createIntegrationRouter(deps: RouterDeps) {
117
- const {
118
- db,
119
- eventRegistry,
120
- providerRegistry,
121
- deliveryCoordinator,
122
- connectionStore,
123
- signalService,
124
- logger,
125
- } = deps;
126
-
127
- // Create contract implementer with context type AND auto auth middleware
29
+ const { db, providerRegistry, connectionStore, logger } = deps;
30
+
128
31
  const os = implement(integrationContract)
129
32
  .$context<RpcContext>()
130
33
  .use(correlationMiddleware)
131
34
  .use(autoAuthMiddleware);
132
35
 
133
36
  return os.router({
134
- // =========================================================================
135
- // SUBSCRIPTION MANAGEMENT
136
- // =========================================================================
137
-
138
- listSubscriptions: os.listSubscriptions.handler(async ({ input }) => {
139
- const { limit, offset, providerId, eventType, enabled } = input;
140
-
141
- // Build where conditions
142
- const conditions = [];
143
- if (providerId) {
144
- conditions.push(eq(schema.webhookSubscriptions.providerId, providerId));
145
- }
146
- if (enabled !== undefined) {
147
- conditions.push(eq(schema.webhookSubscriptions.enabled, enabled));
148
- }
149
-
150
- const whereClause =
151
- conditions.length > 0 ? and(...conditions) : undefined;
152
-
153
- // Get total count
154
- const [{ value: total }] = await db
155
- .select({ value: count() })
156
- .from(schema.webhookSubscriptions)
157
- .where(whereClause);
158
-
159
- // Get paginated results
160
- let query = db
161
- .select()
162
- .from(schema.webhookSubscriptions)
163
- .orderBy(desc(schema.webhookSubscriptions.createdAt))
164
- .limit(limit)
165
- .offset(offset);
166
-
167
- if (whereClause) {
168
- query = query.where(whereClause) as typeof query;
169
- }
170
-
171
- const subscriptions = await query;
172
-
173
- // Filter by event type if specified
174
- const filtered = eventType
175
- ? subscriptions.filter((s) => s.eventId === eventType)
176
- : subscriptions;
177
-
178
- return {
179
- items: filtered.map((s) => ({
180
- ...s,
181
- description: s.description ?? undefined,
182
- systemFilter: s.systemFilter ?? undefined,
183
- createdAt: s.createdAt,
184
- updatedAt: s.updatedAt,
185
- })),
186
- total: Number(total),
187
- limit,
188
- offset,
189
- };
190
- }),
191
-
192
- getSubscription: os.getSubscription.handler(async ({ input }) => {
193
- const [subscription] = await db
194
- .select()
195
- .from(schema.webhookSubscriptions)
196
- .where(eq(schema.webhookSubscriptions.id, input.id));
197
-
198
- if (!subscription) {
199
- throw new ORPCError("NOT_FOUND", {
200
- message: "Subscription not found",
201
- });
202
- }
203
-
204
- return {
205
- ...subscription,
206
- description: subscription.description ?? undefined,
207
- systemFilter: subscription.systemFilter ?? undefined,
208
- createdAt: subscription.createdAt,
209
- updatedAt: subscription.updatedAt,
210
- };
211
- }),
212
-
213
- createSubscription: os.createSubscription.handler(async ({ input }) => {
214
- const {
215
- name,
216
- description,
217
- providerId,
218
- providerConfig,
219
- eventId,
220
- systemFilter,
221
- } = input;
222
-
223
- // Validate provider exists
224
- const provider = providerRegistry.getProvider(providerId);
225
- if (!provider) {
226
- throw new ORPCError("BAD_REQUEST", {
227
- message: `Provider not found: ${providerId}`,
228
- });
229
- }
230
-
231
- // Validate event exists
232
- if (!eventRegistry.hasEvent(eventId)) {
233
- throw new ORPCError("BAD_REQUEST", {
234
- message: `Event type not found: ${eventId}`,
235
- });
236
- }
237
-
238
- // Validate providerConfig against the provider's schema
239
- const configParseResult =
240
- provider.config.schema.safeParse(providerConfig);
241
- if (!configParseResult.success) {
242
- throw new ORPCError("BAD_REQUEST", {
243
- message: `Invalid provider configuration: ${configParseResult.error.message}`,
244
- });
245
- }
246
-
247
- const id = crypto.randomUUID();
248
- const now = new Date();
249
-
250
- await db.insert(schema.webhookSubscriptions).values({
251
- id,
252
- name,
253
- description,
254
- providerId,
255
- providerConfig,
256
- eventId,
257
- systemFilter,
258
- enabled: true,
259
- createdAt: now,
260
- updatedAt: now,
261
- });
262
-
263
- // Emit signal
264
- await signalService.broadcast(INTEGRATION_SUBSCRIPTION_CHANGED, {
265
- action: "created",
266
- subscriptionId: id,
267
- });
268
-
269
- logger.info(`Created webhook subscription: ${name} (${id})`);
270
-
271
- return {
272
- id,
273
- name,
274
- description,
275
- providerId,
276
- providerConfig,
277
- eventId,
278
- systemFilter,
279
- enabled: true,
280
- createdAt: now,
281
- updatedAt: now,
282
- };
283
- }),
284
-
285
- updateSubscription: os.updateSubscription.handler(async ({ input }) => {
286
- const { id, updates } = input;
287
-
288
- // Check subscription exists
289
- const [existing] = await db
290
- .select()
291
- .from(schema.webhookSubscriptions)
292
- .where(eq(schema.webhookSubscriptions.id, id));
293
-
294
- if (!existing) {
295
- throw new ORPCError("NOT_FOUND", {
296
- message: "Subscription not found",
297
- });
298
- }
299
-
300
- // Validate event if updated
301
- if (updates.eventId && !eventRegistry.hasEvent(updates.eventId)) {
302
- throw new ORPCError("BAD_REQUEST", {
303
- message: `Event type not found: ${updates.eventId}`,
304
- });
305
- }
306
-
307
- // Validate providerConfig if updated
308
- if (updates.providerConfig) {
309
- const provider = providerRegistry.getProvider(existing.providerId);
310
- if (provider) {
311
- const configParseResult = provider.config.schema.safeParse(
312
- updates.providerConfig
313
- );
314
- if (!configParseResult.success) {
315
- throw new ORPCError("BAD_REQUEST", {
316
- message: `Invalid provider configuration: ${configParseResult.error.message}`,
317
- });
318
- }
319
- }
320
- }
321
-
322
- const now = new Date();
323
-
324
- await db
325
- .update(schema.webhookSubscriptions)
326
- .set({
327
- ...updates,
328
- updatedAt: now,
329
- })
330
- .where(eq(schema.webhookSubscriptions.id, id));
331
-
332
- // Emit signal
333
- await signalService.broadcast(INTEGRATION_SUBSCRIPTION_CHANGED, {
334
- action: "updated",
335
- subscriptionId: id,
336
- });
337
-
338
- // Re-fetch updated subscription
339
- const [updated] = await db
340
- .select()
341
- .from(schema.webhookSubscriptions)
342
- .where(eq(schema.webhookSubscriptions.id, id));
343
-
344
- return {
345
- ...updated,
346
- description: updated.description ?? undefined,
347
- systemFilter: updated.systemFilter ?? undefined,
348
- createdAt: updated.createdAt,
349
- updatedAt: updated.updatedAt,
350
- };
351
- }),
352
-
353
- deleteSubscription: os.deleteSubscription.handler(async ({ input }) => {
354
- const { id } = input;
355
-
356
- await db
357
- .delete(schema.webhookSubscriptions)
358
- .where(eq(schema.webhookSubscriptions.id, id));
359
-
360
- // Emit signal
361
- await signalService.broadcast(INTEGRATION_SUBSCRIPTION_CHANGED, {
362
- action: "deleted",
363
- subscriptionId: id,
364
- });
365
-
366
- logger.info(`Deleted webhook subscription: ${id}`);
367
-
368
- return { success: true };
369
- }),
370
-
371
- toggleSubscription: os.toggleSubscription.handler(async ({ input }) => {
372
- const { id, enabled } = input;
373
-
374
- await db
375
- .update(schema.webhookSubscriptions)
376
- .set({
377
- enabled,
378
- updatedAt: new Date(),
379
- })
380
- .where(eq(schema.webhookSubscriptions.id, id));
381
-
382
- // Emit signal
383
- await signalService.broadcast(INTEGRATION_SUBSCRIPTION_CHANGED, {
384
- action: "updated",
385
- subscriptionId: id,
386
- });
387
-
388
- return { success: true };
389
- }),
390
-
391
- // =========================================================================
392
- // PROVIDER DISCOVERY
393
- // =========================================================================
37
+ // ─── Providers ───────────────────────────────────────────────────────
394
38
 
395
39
  listProviders: os.listProviders.handler(async () => {
396
40
  const providers = providerRegistry.getProviders();
397
-
398
41
  return providers.map((p) => ({
399
42
  qualifiedId: p.qualifiedId,
400
43
  displayName: p.displayName,
401
44
  description: p.description,
402
45
  icon: p.icon,
403
46
  ownerPluginId: p.ownerPluginId,
404
- supportedEvents: p.supportedEvents,
405
- configSchema:
406
- providerRegistry.getProviderConfigSchema(p.qualifiedId) ?? {},
47
+ // Legacy `supportedEvents` is no longer modelled on providers
48
+ // (the trigger registry owns event metadata now). Return empty
49
+ // so the wire schema stays stable.
50
+ supportedEvents: [],
51
+ // Legacy `configSchema` was the per-subscription config; that
52
+ // lives on action definitions now. Returning an empty object
53
+ // preserves the wire shape until the schema is bumped.
54
+ configSchema: {},
407
55
  hasConnectionSchema: !!p.connectionSchema,
408
56
  connectionSchema: p.connectionSchema
409
57
  ? providerRegistry.getProviderConnectionSchema(p.qualifiedId)
@@ -437,18 +85,14 @@ export function createIntegrationRouter(deps: RouterDeps) {
437
85
  message: extractErrorMessage(error),
438
86
  };
439
87
  }
440
- }
88
+ },
441
89
  ),
442
90
 
443
- // =========================================================================
444
- // CONNECTION MANAGEMENT
445
- // Generic CRUD for site-wide provider connections
446
- // =========================================================================
91
+ // ─── Connections ─────────────────────────────────────────────────────
447
92
 
448
93
  listConnections: os.listConnections.handler(async ({ input }) => {
449
94
  const { providerId } = input;
450
95
 
451
- // Verify provider exists and has connectionSchema
452
96
  const provider = providerRegistry.getProvider(providerId);
453
97
  if (!provider) {
454
98
  throw new ORPCError("NOT_FOUND", {
@@ -481,7 +125,6 @@ export function createIntegrationRouter(deps: RouterDeps) {
481
125
  createConnection: os.createConnection.handler(async ({ input }) => {
482
126
  const { providerId, name, config } = input;
483
127
 
484
- // Verify provider exists and has connectionSchema
485
128
  const provider = providerRegistry.getProvider(providerId);
486
129
  if (!provider) {
487
130
  throw new ORPCError("NOT_FOUND", {
@@ -495,7 +138,6 @@ export function createIntegrationRouter(deps: RouterDeps) {
495
138
  });
496
139
  }
497
140
 
498
- // Validate config against provider's connectionSchema
499
141
  const parseResult = provider.connectionSchema.schema.safeParse(config);
500
142
  if (!parseResult.success) {
501
143
  throw new ORPCError("BAD_REQUEST", {
@@ -503,7 +145,6 @@ export function createIntegrationRouter(deps: RouterDeps) {
503
145
  });
504
146
  }
505
147
 
506
- // parseResult.data is typed correctly after guard
507
148
  const validatedConfig = parseResult.data as unknown as Record<
508
149
  string,
509
150
  unknown
@@ -517,12 +158,11 @@ export function createIntegrationRouter(deps: RouterDeps) {
517
158
 
518
159
  logger.info(`Created connection "${name}" for provider ${providerId}`);
519
160
 
520
- // Return redacted version
521
161
  return {
522
162
  id: connection.id,
523
163
  providerId: connection.providerId,
524
164
  name: connection.name,
525
- configPreview: config, // Will be redacted in real usage
165
+ configPreview: config,
526
166
  createdAt: connection.createdAt,
527
167
  updatedAt: connection.updatedAt,
528
168
  };
@@ -547,8 +187,7 @@ export function createIntegrationRouter(deps: RouterDeps) {
547
187
  };
548
188
  } catch (error) {
549
189
  throw new ORPCError("NOT_FOUND", {
550
- message:
551
- extractErrorMessage(error, "Connection not found"),
190
+ message: extractErrorMessage(error, "Connection not found"),
552
191
  });
553
192
  }
554
193
  }),
@@ -570,7 +209,7 @@ export function createIntegrationRouter(deps: RouterDeps) {
570
209
  const { connectionId } = input;
571
210
 
572
211
  const connection = await connectionStore.getConnectionWithCredentials(
573
- connectionId
212
+ connectionId,
574
213
  );
575
214
  if (!connection) {
576
215
  return { success: false, message: "Connection not found" };
@@ -599,11 +238,27 @@ export function createIntegrationRouter(deps: RouterDeps) {
599
238
  }
600
239
  }),
601
240
 
241
+ // ─── One-time migration support ──────────────────────────────────────
242
+
243
+ listLegacySubscriptions: os.listLegacySubscriptions.handler(async () => {
244
+ const rows = await db.select().from(schema.webhookSubscriptions);
245
+ return rows.map((row) => ({
246
+ id: row.id,
247
+ name: row.name,
248
+ description: row.description ?? undefined,
249
+ providerId: row.providerId,
250
+ providerConfig: row.providerConfig,
251
+ eventId: row.eventId,
252
+ systemFilter: row.systemFilter ?? undefined,
253
+ enabled: row.enabled,
254
+ }));
255
+ }),
256
+
602
257
  getConnectionOptions: os.getConnectionOptions.handler(async ({ input }) => {
603
258
  const { providerId, connectionId, resolverName, context } = input;
604
259
 
605
260
  logger.debug(
606
- `getConnectionOptions called: providerId=${providerId}, connectionId=${connectionId}, resolverName=${resolverName}`
261
+ `getConnectionOptions called: providerId=${providerId}, connectionId=${connectionId}, resolverName=${resolverName}`,
607
262
  );
608
263
 
609
264
  const provider = providerRegistry.getProvider(providerId);
@@ -629,234 +284,16 @@ export function createIntegrationRouter(deps: RouterDeps) {
629
284
  connectionStore.getConnectionWithCredentials.bind(connectionStore),
630
285
  });
631
286
  logger.debug(
632
- `getConnectionOptions returned ${options.length} options for ${resolverName}`
287
+ `getConnectionOptions returned ${options.length} options for ${resolverName}`,
633
288
  );
634
289
  return options;
635
290
  } catch (error) {
636
291
  logger.error(`Failed to get connection options: ${error}`);
637
292
  throw new ORPCError("INTERNAL_SERVER_ERROR", {
638
- message:
639
- extractErrorMessage(error, "Failed to fetch options"),
293
+ message: extractErrorMessage(error, "Failed to fetch options"),
640
294
  });
641
295
  }
642
296
  }),
643
-
644
- // =========================================================================
645
- // EVENT DISCOVERY
646
- // =========================================================================
647
-
648
- listEventTypes: os.listEventTypes.handler(async () => {
649
- const events = eventRegistry.getEvents();
650
-
651
- return events.map((e) => ({
652
- eventId: e.eventId,
653
- displayName: e.displayName,
654
- description: e.description,
655
- category: e.category,
656
- ownerPluginId: e.ownerPluginId,
657
- payloadSchema: e.payloadJsonSchema,
658
- }));
659
- }),
660
-
661
- getEventsByCategory: os.getEventsByCategory.handler(async () => {
662
- const byCategory = eventRegistry.getEventsByCategory();
663
-
664
- return [...byCategory.entries()].map(([category, events]) => ({
665
- category,
666
- events: events.map((e) => ({
667
- eventId: e.eventId,
668
- displayName: e.displayName,
669
- description: e.description,
670
- category: e.category,
671
- ownerPluginId: e.ownerPluginId,
672
- payloadSchema: e.payloadJsonSchema,
673
- })),
674
- }));
675
- }),
676
-
677
- getEventPayloadSchema: os.getEventPayloadSchema.handler(
678
- async ({ input }) => {
679
- const { eventId } = input;
680
-
681
- const event = eventRegistry.getEvent(eventId);
682
- if (!event) {
683
- throw new ORPCError("NOT_FOUND", {
684
- message: `Event not found: ${eventId}`,
685
- });
686
- }
687
-
688
- // Extract flattened properties from JSON Schema
689
- const availableProperties = extractJsonSchemaProperties(
690
- event.payloadJsonSchema,
691
- "payload"
692
- );
693
-
694
- return {
695
- eventId: event.eventId,
696
- payloadSchema: event.payloadJsonSchema,
697
- availableProperties,
698
- };
699
- }
700
- ),
701
-
702
- // =========================================================================
703
- // DELIVERY LOGS
704
- // =========================================================================
705
-
706
- getDeliveryLogs: os.getDeliveryLogs.handler(async ({ input }) => {
707
- const { subscriptionId, eventType, status, limit, offset } = input;
708
-
709
- // Build where conditions
710
- const conditions = [];
711
- if (subscriptionId) {
712
- conditions.push(eq(schema.deliveryLogs.subscriptionId, subscriptionId));
713
- }
714
- if (eventType) {
715
- conditions.push(eq(schema.deliveryLogs.eventType, eventType));
716
- }
717
- if (status) {
718
- conditions.push(eq(schema.deliveryLogs.status, status));
719
- }
720
-
721
- const whereClause =
722
- conditions.length > 0 ? and(...conditions) : undefined;
723
-
724
- // Get total count
725
- const [{ value: total }] = await db
726
- .select({ value: count() })
727
- .from(schema.deliveryLogs)
728
- .where(whereClause);
729
-
730
- // Get paginated results with subscription name
731
- const logs = await db
732
- .select({
733
- log: schema.deliveryLogs,
734
- subscriptionName: schema.webhookSubscriptions.name,
735
- })
736
- .from(schema.deliveryLogs)
737
- .leftJoin(
738
- schema.webhookSubscriptions,
739
- eq(schema.deliveryLogs.subscriptionId, schema.webhookSubscriptions.id)
740
- )
741
- .where(whereClause)
742
- .orderBy(desc(schema.deliveryLogs.createdAt))
743
- .limit(limit)
744
- .offset(offset);
745
-
746
- return {
747
- items: logs.map(({ log, subscriptionName }) => ({
748
- ...log,
749
- subscriptionName: subscriptionName ?? undefined,
750
- createdAt: log.createdAt,
751
- lastAttemptAt: log.lastAttemptAt ?? undefined,
752
- nextRetryAt: log.nextRetryAt ?? undefined,
753
- externalId: log.externalId ?? undefined,
754
- errorMessage: log.errorMessage ?? undefined,
755
- })),
756
- total: Number(total),
757
- limit,
758
- offset,
759
- };
760
- }),
761
-
762
- getDeliveryLog: os.getDeliveryLog.handler(async ({ input }) => {
763
- const [result] = await db
764
- .select({
765
- log: schema.deliveryLogs,
766
- subscriptionName: schema.webhookSubscriptions.name,
767
- })
768
- .from(schema.deliveryLogs)
769
- .leftJoin(
770
- schema.webhookSubscriptions,
771
- eq(schema.deliveryLogs.subscriptionId, schema.webhookSubscriptions.id)
772
- )
773
- .where(eq(schema.deliveryLogs.id, input.id));
774
-
775
- if (!result) {
776
- throw new ORPCError("NOT_FOUND", {
777
- message: "Delivery log not found",
778
- });
779
- }
780
-
781
- return {
782
- ...result.log,
783
- subscriptionName: result.subscriptionName ?? undefined,
784
- createdAt: result.log.createdAt,
785
- lastAttemptAt: result.log.lastAttemptAt ?? undefined,
786
- nextRetryAt: result.log.nextRetryAt ?? undefined,
787
- externalId: result.log.externalId ?? undefined,
788
- errorMessage: result.log.errorMessage ?? undefined,
789
- };
790
- }),
791
-
792
- retryDelivery: os.retryDelivery.handler(async ({ input }) => {
793
- return deliveryCoordinator.retryDelivery(input.logId);
794
- }),
795
-
796
- getDeliveryStats: os.getDeliveryStats.handler(async ({ input }) => {
797
- const { hours } = input;
798
- const since = new Date(Date.now() - hours * 60 * 60 * 1000);
799
-
800
- // Get counts by status
801
- const statusCounts = await db
802
- .select({
803
- status: schema.deliveryLogs.status,
804
- count: count(),
805
- })
806
- .from(schema.deliveryLogs)
807
- .where(gte(schema.deliveryLogs.createdAt, since))
808
- .groupBy(schema.deliveryLogs.status);
809
-
810
- // Get counts by event type
811
- const eventCounts = await db
812
- .select({
813
- eventType: schema.deliveryLogs.eventType,
814
- count: count(),
815
- })
816
- .from(schema.deliveryLogs)
817
- .where(gte(schema.deliveryLogs.createdAt, since))
818
- .groupBy(schema.deliveryLogs.eventType);
819
-
820
- // Get counts by provider (via subscription)
821
- const providerCounts = await db
822
- .select({
823
- providerId: schema.webhookSubscriptions.providerId,
824
- count: count(),
825
- })
826
- .from(schema.deliveryLogs)
827
- .innerJoin(
828
- schema.webhookSubscriptions,
829
- eq(schema.deliveryLogs.subscriptionId, schema.webhookSubscriptions.id)
830
- )
831
- .where(gte(schema.deliveryLogs.createdAt, since))
832
- .groupBy(schema.webhookSubscriptions.providerId);
833
-
834
- // Build response
835
- const statusMap = new Map(
836
- statusCounts.map((s) => [s.status, Number(s.count)])
837
- );
838
- const total =
839
- (statusMap.get("success") ?? 0) +
840
- (statusMap.get("failed") ?? 0) +
841
- (statusMap.get("retrying") ?? 0) +
842
- (statusMap.get("pending") ?? 0);
843
-
844
- return {
845
- total,
846
- successful: statusMap.get("success") ?? 0,
847
- failed: statusMap.get("failed") ?? 0,
848
- retrying: statusMap.get("retrying") ?? 0,
849
- pending: statusMap.get("pending") ?? 0,
850
- byEvent: eventCounts.map((e) => ({
851
- eventType: e.eventType,
852
- count: Number(e.count),
853
- })),
854
- byProvider: providerCounts.map((p) => ({
855
- providerId: p.providerId,
856
- count: Number(p.count),
857
- })),
858
- };
859
- }),
860
297
  });
861
298
  }
862
299