@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/CHANGELOG.md +92 -0
- package/package.json +9 -9
- package/src/index.ts +24 -187
- package/src/provider-registry.test.ts +56 -286
- package/src/provider-registry.ts +5 -20
- package/src/provider-types.ts +23 -149
- package/src/router.ts +42 -605
- package/src/schema.ts +13 -59
- package/src/delivery-coordinator.ts +0 -391
- package/src/event-registry.test.ts +0 -396
- package/src/event-registry.ts +0 -99
- package/src/hook-subscriber.ts +0 -105
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
|
|
10
|
-
import {
|
|
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
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
-
|
|
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
|
|
405
|
-
|
|
406
|
-
|
|
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,
|
|
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
|
|