@shipfox/api-integration-core 2.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.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +264 -0
- package/LICENSE +21 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -0
- package/dist/core/agent-tool-selection.d.ts +15 -0
- package/dist/core/agent-tool-selection.d.ts.map +1 -0
- package/dist/core/agent-tool-selection.js +48 -0
- package/dist/core/agent-tool-selection.js.map +1 -0
- package/dist/core/entities/connection.d.ts +2 -0
- package/dist/core/entities/connection.d.ts.map +1 -0
- package/dist/core/entities/connection.js +3 -0
- package/dist/core/entities/connection.js.map +1 -0
- package/dist/core/entities/provider.d.ts +6 -0
- package/dist/core/entities/provider.d.ts.map +1 -0
- package/dist/core/entities/provider.js +3 -0
- package/dist/core/entities/provider.js.map +1 -0
- package/dist/core/errors.d.ts +29 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +40 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/providers/agent-tools.d.ts +2 -0
- package/dist/core/providers/agent-tools.d.ts.map +1 -0
- package/dist/core/providers/agent-tools.js +3 -0
- package/dist/core/providers/agent-tools.js.map +1 -0
- package/dist/core/providers/redact-checkout-spec.d.ts +9 -0
- package/dist/core/providers/redact-checkout-spec.d.ts.map +1 -0
- package/dist/core/providers/redact-checkout-spec.js +25 -0
- package/dist/core/providers/redact-checkout-spec.js.map +1 -0
- package/dist/core/providers/registry.d.ts +9 -0
- package/dist/core/providers/registry.d.ts.map +1 -0
- package/dist/core/providers/registry.js +52 -0
- package/dist/core/providers/registry.js.map +1 -0
- package/dist/core/providers/source-control.d.ts +2 -0
- package/dist/core/providers/source-control.d.ts.map +1 -0
- package/dist/core/providers/source-control.js +3 -0
- package/dist/core/providers/source-control.js.map +1 -0
- package/dist/core/source-control-service.d.ts +46 -0
- package/dist/core/source-control-service.d.ts.map +1 -0
- package/dist/core/source-control-service.js +84 -0
- package/dist/core/source-control-service.js.map +1 -0
- package/dist/db/connections.d.ts +67 -0
- package/dist/db/connections.d.ts.map +1 -0
- package/dist/db/connections.js +130 -0
- package/dist/db/connections.js.map +1 -0
- package/dist/db/db.d.ts +865 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +22 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/connections.d.ts +165 -0
- package/dist/db/schema/connections.d.ts.map +1 -0
- package/dist/db/schema/connections.js +37 -0
- package/dist/db/schema/connections.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/schema/webhook-deliveries.d.ts +76 -0
- package/dist/db/schema/webhook-deliveries.d.ts.map +1 -0
- package/dist/db/schema/webhook-deliveries.js +22 -0
- package/dist/db/schema/webhook-deliveries.js.map +1 -0
- package/dist/db/webhook-deliveries.d.ts +55 -0
- package/dist/db/webhook-deliveries.d.ts.map +1 -0
- package/dist/db/webhook-deliveries.js +132 -0
- package/dist/db/webhook-deliveries.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +8 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +17 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +36 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +26 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.d.ts +24 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.js +52 -0
- package/dist/presentation/routes/agent-tools-gateway/audit.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts +7 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.js +108 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.d.ts +12 -0
- package/dist/presentation/routes/agent-tools-gateway/index.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/index.js +46 -0
- package/dist/presentation/routes/agent-tools-gateway/index.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.d.ts +17 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.js +126 -0
- package/dist/presentation/routes/agent-tools-gateway/mcp-server.js.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts +37 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts.map +1 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js +145 -0
- package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +53 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +14 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +26 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/list-connections.d.ts +3 -0
- package/dist/presentation/routes/list-connections.d.ts.map +1 -0
- package/dist/presentation/routes/list-connections.js +58 -0
- package/dist/presentation/routes/list-connections.js.map +1 -0
- package/dist/presentation/routes/list-providers.d.ts +3 -0
- package/dist/presentation/routes/list-providers.d.ts.map +1 -0
- package/dist/presentation/routes/list-providers.js +27 -0
- package/dist/presentation/routes/list-providers.js.map +1 -0
- package/dist/presentation/routes/list-repositories.d.ts +3 -0
- package/dist/presentation/routes/list-repositories.d.ts.map +1 -0
- package/dist/presentation/routes/list-repositories.js +41 -0
- package/dist/presentation/routes/list-repositories.js.map +1 -0
- package/dist/presentation/routes/manage-connections.d.ts +4 -0
- package/dist/presentation/routes/manage-connections.d.ts.map +1 -0
- package/dist/presentation/routes/manage-connections.js +81 -0
- package/dist/presentation/routes/manage-connections.js.map +1 -0
- package/dist/providers/connection-slug.d.ts +2 -0
- package/dist/providers/connection-slug.d.ts.map +1 -0
- package/dist/providers/connection-slug.js +19 -0
- package/dist/providers/connection-slug.js.map +1 -0
- package/dist/providers/cron.d.ts +3 -0
- package/dist/providers/cron.d.ts.map +1 -0
- package/dist/providers/cron.js +13 -0
- package/dist/providers/cron.js.map +1 -0
- package/dist/providers/gitea.d.ts +3 -0
- package/dist/providers/gitea.d.ts.map +1 -0
- package/dist/providers/gitea.js +75 -0
- package/dist/providers/gitea.js.map +1 -0
- package/dist/providers/github.d.ts +3 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +109 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/linear.d.ts +3 -0
- package/dist/providers/linear.d.ts.map +1 -0
- package/dist/providers/linear.js +134 -0
- package/dist/providers/linear.js.map +1 -0
- package/dist/providers/modules.d.ts +3 -0
- package/dist/providers/modules.d.ts.map +1 -0
- package/dist/providers/modules.js +26 -0
- package/dist/providers/modules.js.map +1 -0
- package/dist/providers/sentry.d.ts +3 -0
- package/dist/providers/sentry.d.ts.map +1 -0
- package/dist/providers/sentry.js +83 -0
- package/dist/providers/sentry.js.map +1 -0
- package/dist/providers/types.d.ts +58 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/webhook.d.ts +3 -0
- package/dist/providers/webhook.d.ts.map +1 -0
- package/dist/providers/webhook.js +24 -0
- package/dist/providers/webhook.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +5 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +8 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/prune-webhook-deliveries.d.ts +4 -0
- package/dist/temporal/activities/prune-webhook-deliveries.d.ts.map +1 -0
- package/dist/temporal/activities/prune-webhook-deliveries.js +11 -0
- package/dist/temporal/activities/prune-webhook-deliveries.js.map +1 -0
- package/dist/temporal/constants.d.ts +3 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +4 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +2 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +3 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.d.ts +2 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.js +14 -0
- package/dist/temporal/workflows/prune-webhook-deliveries-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +41 -0
- package/drizzle/meta/0001_snapshot.json +341 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +85 -0
- package/src/config.test.ts +32 -0
- package/src/config.ts +28 -0
- package/src/core/agent-tool-selection.test.ts +137 -0
- package/src/core/agent-tool-selection.ts +80 -0
- package/src/core/entities/connection.ts +4 -0
- package/src/core/entities/provider.ts +21 -0
- package/src/core/errors.ts +55 -0
- package/src/core/providers/agent-tools.ts +10 -0
- package/src/core/providers/redact-checkout-spec.test.ts +78 -0
- package/src/core/providers/redact-checkout-spec.ts +20 -0
- package/src/core/providers/registry.test.ts +138 -0
- package/src/core/providers/registry.ts +90 -0
- package/src/core/providers/source-control.ts +17 -0
- package/src/core/source-control-service.test.ts +230 -0
- package/src/core/source-control-service.ts +161 -0
- package/src/db/connections.test.ts +296 -0
- package/src/db/connections.ts +262 -0
- package/src/db/db.ts +22 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/connections.ts +49 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/schema/webhook-deliveries.ts +19 -0
- package/src/db/webhook-deliveries.test.ts +267 -0
- package/src/db/webhook-deliveries.ts +207 -0
- package/src/index.ts +212 -0
- package/src/metrics/index.ts +1 -0
- package/src/metrics/instance.ts +35 -0
- package/src/presentation/dto/integrations.ts +39 -0
- package/src/presentation/routes/agent-tools-gateway/agent-tools-gateway.route.test.ts +371 -0
- package/src/presentation/routes/agent-tools-gateway/audit.test.ts +86 -0
- package/src/presentation/routes/agent-tools-gateway/audit.ts +90 -0
- package/src/presentation/routes/agent-tools-gateway/dispatch.ts +139 -0
- package/src/presentation/routes/agent-tools-gateway/index.ts +62 -0
- package/src/presentation/routes/agent-tools-gateway/mcp-server.test.ts +308 -0
- package/src/presentation/routes/agent-tools-gateway/mcp-server.ts +155 -0
- package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.test.ts +181 -0
- package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.ts +245 -0
- package/src/presentation/routes/errors.ts +63 -0
- package/src/presentation/routes/index.ts +53 -0
- package/src/presentation/routes/list-connections.test.ts +227 -0
- package/src/presentation/routes/list-connections.ts +56 -0
- package/src/presentation/routes/list-providers.test.ts +113 -0
- package/src/presentation/routes/list-providers.ts +28 -0
- package/src/presentation/routes/list-repositories.test.ts +150 -0
- package/src/presentation/routes/list-repositories.ts +47 -0
- package/src/presentation/routes/manage-connections.test.ts +125 -0
- package/src/presentation/routes/manage-connections.ts +77 -0
- package/src/providers/connection-slug.ts +19 -0
- package/src/providers/cron.ts +13 -0
- package/src/providers/gitea.ts +103 -0
- package/src/providers/github.ts +150 -0
- package/src/providers/linear.ts +179 -0
- package/src/providers/modules.test.ts +40 -0
- package/src/providers/modules.ts +32 -0
- package/src/providers/sentry.ts +113 -0
- package/src/providers/types.ts +52 -0
- package/src/providers/webhook.ts +30 -0
- package/src/temporal/activities/index.ts +7 -0
- package/src/temporal/activities/prune-webhook-deliveries.test.ts +39 -0
- package/src/temporal/activities/prune-webhook-deliveries.ts +9 -0
- package/src/temporal/constants.ts +3 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/prune-webhook-deliveries-cron.ts +15 -0
- package/test/agent-tools-gateway-helpers.ts +210 -0
- package/test/env.ts +23 -0
- package/test/factories/connection.ts +21 -0
- package/test/globalSetup.ts +22 -0
- package/test/index.ts +1 -0
- package/test/route-utils.ts +104 -0
- package/test/setup.ts +19 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONNECTION_SLUG_MAX_LENGTH,
|
|
3
|
+
ConnectionSlugConflictError,
|
|
4
|
+
} from '@shipfox/api-integration-core-dto';
|
|
5
|
+
import {and, eq} from 'drizzle-orm';
|
|
6
|
+
import type {
|
|
7
|
+
IntegrationConnection,
|
|
8
|
+
IntegrationConnectionLifecycleStatus,
|
|
9
|
+
} from '#core/entities/connection.js';
|
|
10
|
+
import type {IntegrationProviderKind} from '#core/entities/provider.js';
|
|
11
|
+
import {IntegrationConnectionAlreadyExistsError} from '#core/errors.js';
|
|
12
|
+
import {db} from './db.js';
|
|
13
|
+
import {integrationConnections, toIntegrationConnection} from './schema/connections.js';
|
|
14
|
+
|
|
15
|
+
type IntegrationDb = ReturnType<typeof db>;
|
|
16
|
+
type IntegrationTx = Parameters<Parameters<IntegrationDb['transaction']>[0]>[0];
|
|
17
|
+
|
|
18
|
+
export interface UpsertIntegrationConnectionParams {
|
|
19
|
+
workspaceId: string;
|
|
20
|
+
provider: IntegrationProviderKind;
|
|
21
|
+
externalAccountId: string;
|
|
22
|
+
slug: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
lifecycleStatus?: IntegrationConnectionLifecycleStatus | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function upsertIntegrationConnection(
|
|
28
|
+
params: UpsertIntegrationConnectionParams,
|
|
29
|
+
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
30
|
+
): Promise<IntegrationConnection> {
|
|
31
|
+
const executor = options.tx ?? db();
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const [row] = await executor
|
|
34
|
+
.insert(integrationConnections)
|
|
35
|
+
.values({
|
|
36
|
+
workspaceId: params.workspaceId,
|
|
37
|
+
provider: params.provider,
|
|
38
|
+
externalAccountId: params.externalAccountId,
|
|
39
|
+
slug: params.slug,
|
|
40
|
+
displayName: params.displayName,
|
|
41
|
+
lifecycleStatus: params.lifecycleStatus ?? 'active',
|
|
42
|
+
})
|
|
43
|
+
.onConflictDoUpdate({
|
|
44
|
+
target: [
|
|
45
|
+
integrationConnections.workspaceId,
|
|
46
|
+
integrationConnections.provider,
|
|
47
|
+
integrationConnections.externalAccountId,
|
|
48
|
+
],
|
|
49
|
+
set: {
|
|
50
|
+
displayName: params.displayName,
|
|
51
|
+
lifecycleStatus: params.lifecycleStatus ?? 'active',
|
|
52
|
+
updatedAt: now,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
.returning();
|
|
56
|
+
|
|
57
|
+
if (!row) throw new Error('Integration connection upsert returned no rows');
|
|
58
|
+
return toIntegrationConnection(row);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CreateIntegrationConnectionParams {
|
|
62
|
+
workspaceId: string;
|
|
63
|
+
provider: IntegrationProviderKind;
|
|
64
|
+
externalAccountId: string;
|
|
65
|
+
slug: string;
|
|
66
|
+
displayName: string;
|
|
67
|
+
lifecycleStatus?: IntegrationConnectionLifecycleStatus | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT =
|
|
71
|
+
'integrations_connections_workspace_external_unique';
|
|
72
|
+
const INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT =
|
|
73
|
+
'integrations_connections_workspace_slug_unique';
|
|
74
|
+
|
|
75
|
+
export function isIntegrationConnectionUniqueViolation(error: unknown): boolean {
|
|
76
|
+
let current: unknown = error;
|
|
77
|
+
for (let depth = 0; depth < 5 && current != null; depth += 1) {
|
|
78
|
+
if (typeof current !== 'object') return false;
|
|
79
|
+
const {code, constraint} = current as {code?: unknown; constraint?: unknown};
|
|
80
|
+
if (code === '23505' && constraint === INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
current = (current as {cause?: unknown}).cause;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isIntegrationConnectionSlugUniqueViolation(error: unknown): boolean {
|
|
89
|
+
let current: unknown = error;
|
|
90
|
+
for (let depth = 0; depth < 5 && current != null; depth += 1) {
|
|
91
|
+
if (typeof current !== 'object') return false;
|
|
92
|
+
const {code, constraint} = current as {code?: unknown; constraint?: unknown};
|
|
93
|
+
if (code === '23505' && constraint === INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
current = (current as {cause?: unknown}).cause;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function createIntegrationConnection(
|
|
102
|
+
params: CreateIntegrationConnectionParams,
|
|
103
|
+
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
104
|
+
): Promise<IntegrationConnection> {
|
|
105
|
+
const executor = options.tx ?? db();
|
|
106
|
+
let rows: (typeof integrationConnections.$inferSelect)[];
|
|
107
|
+
try {
|
|
108
|
+
rows = await executor
|
|
109
|
+
.insert(integrationConnections)
|
|
110
|
+
.values({
|
|
111
|
+
workspaceId: params.workspaceId,
|
|
112
|
+
provider: params.provider,
|
|
113
|
+
externalAccountId: params.externalAccountId,
|
|
114
|
+
slug: params.slug,
|
|
115
|
+
displayName: params.displayName,
|
|
116
|
+
lifecycleStatus: params.lifecycleStatus ?? 'active',
|
|
117
|
+
})
|
|
118
|
+
.returning();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (isIntegrationConnectionUniqueViolation(error)) {
|
|
121
|
+
throw new IntegrationConnectionAlreadyExistsError(
|
|
122
|
+
params.workspaceId,
|
|
123
|
+
params.provider,
|
|
124
|
+
params.externalAccountId,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (isIntegrationConnectionSlugUniqueViolation(error)) {
|
|
128
|
+
throw new ConnectionSlugConflictError(error);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const row = rows[0];
|
|
134
|
+
if (!row) throw new Error('Integration connection insert returned no rows');
|
|
135
|
+
return toIntegrationConnection(row);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ResolveUniqueConnectionSlugParams {
|
|
139
|
+
workspaceId: string;
|
|
140
|
+
provider: IntegrationProviderKind;
|
|
141
|
+
externalAccountId: string;
|
|
142
|
+
baseSlug: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function resolveUniqueConnectionSlug(
|
|
146
|
+
params: ResolveUniqueConnectionSlugParams,
|
|
147
|
+
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
148
|
+
): Promise<string> {
|
|
149
|
+
const executor = options.tx ?? db();
|
|
150
|
+
const [existing] = await executor
|
|
151
|
+
.select({slug: integrationConnections.slug})
|
|
152
|
+
.from(integrationConnections)
|
|
153
|
+
.where(
|
|
154
|
+
and(
|
|
155
|
+
eq(integrationConnections.workspaceId, params.workspaceId),
|
|
156
|
+
eq(integrationConnections.provider, params.provider),
|
|
157
|
+
eq(integrationConnections.externalAccountId, params.externalAccountId),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
.limit(1);
|
|
161
|
+
if (existing) return existing.slug;
|
|
162
|
+
|
|
163
|
+
const workspaceSlugs = await executor
|
|
164
|
+
.select({slug: integrationConnections.slug})
|
|
165
|
+
.from(integrationConnections)
|
|
166
|
+
.where(eq(integrationConnections.workspaceId, params.workspaceId));
|
|
167
|
+
const used = new Set(workspaceSlugs.map((row) => row.slug));
|
|
168
|
+
|
|
169
|
+
for (let suffixNumber = 1; ; suffixNumber += 1) {
|
|
170
|
+
const suffix = suffixNumber === 1 ? '' : `_${suffixNumber}`;
|
|
171
|
+
const baseBudget = CONNECTION_SLUG_MAX_LENGTH - suffix.length;
|
|
172
|
+
const candidate = `${params.baseSlug.slice(0, baseBudget).replaceAll(/[_-]+$/g, '')}${suffix}`;
|
|
173
|
+
if (!used.has(candidate)) return candidate;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type CreateIntegrationConnectionFn = typeof createIntegrationConnection;
|
|
178
|
+
|
|
179
|
+
export async function getIntegrationConnectionById(
|
|
180
|
+
id: string,
|
|
181
|
+
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
182
|
+
): Promise<IntegrationConnection | undefined> {
|
|
183
|
+
const executor = options.tx ?? db();
|
|
184
|
+
const rows = await executor
|
|
185
|
+
.select()
|
|
186
|
+
.from(integrationConnections)
|
|
187
|
+
.where(eq(integrationConnections.id, id))
|
|
188
|
+
.limit(1);
|
|
189
|
+
const row = rows[0];
|
|
190
|
+
if (!row) return undefined;
|
|
191
|
+
return toIntegrationConnection(row);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export type GetIntegrationConnectionByIdFn = typeof getIntegrationConnectionById;
|
|
195
|
+
|
|
196
|
+
export interface UpdateIntegrationConnectionLifecycleStatusParams {
|
|
197
|
+
id: string;
|
|
198
|
+
lifecycleStatus: IntegrationConnectionLifecycleStatus;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function updateIntegrationConnectionLifecycleStatus(
|
|
202
|
+
params: UpdateIntegrationConnectionLifecycleStatusParams,
|
|
203
|
+
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
204
|
+
): Promise<IntegrationConnection | undefined> {
|
|
205
|
+
const executor = options.tx ?? db();
|
|
206
|
+
const [row] = await executor
|
|
207
|
+
.update(integrationConnections)
|
|
208
|
+
.set({lifecycleStatus: params.lifecycleStatus, updatedAt: new Date()})
|
|
209
|
+
.where(eq(integrationConnections.id, params.id))
|
|
210
|
+
.returning();
|
|
211
|
+
if (!row) return undefined;
|
|
212
|
+
return toIntegrationConnection(row);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type UpdateIntegrationConnectionLifecycleStatusFn =
|
|
216
|
+
typeof updateIntegrationConnectionLifecycleStatus;
|
|
217
|
+
|
|
218
|
+
export async function deleteIntegrationConnection(
|
|
219
|
+
params: {id: string},
|
|
220
|
+
options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},
|
|
221
|
+
): Promise<boolean> {
|
|
222
|
+
const executor = options.tx ?? db();
|
|
223
|
+
const result = await executor
|
|
224
|
+
.delete(integrationConnections)
|
|
225
|
+
.where(eq(integrationConnections.id, params.id));
|
|
226
|
+
return (result.rowCount ?? 0) > 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export type DeleteIntegrationConnectionFn = typeof deleteIntegrationConnection;
|
|
230
|
+
|
|
231
|
+
export interface ListIntegrationConnectionsParams {
|
|
232
|
+
workspaceId: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function listIntegrationConnections(
|
|
236
|
+
params: ListIntegrationConnectionsParams,
|
|
237
|
+
): Promise<IntegrationConnection[]> {
|
|
238
|
+
const rows = await db()
|
|
239
|
+
.select()
|
|
240
|
+
.from(integrationConnections)
|
|
241
|
+
.where(eq(integrationConnections.workspaceId, params.workspaceId))
|
|
242
|
+
.orderBy(integrationConnections.createdAt, integrationConnections.id);
|
|
243
|
+
|
|
244
|
+
const connections = rows.map(toIntegrationConnection);
|
|
245
|
+
return connections;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface ListIntegrationConnectionsByProviderParams {
|
|
249
|
+
provider: IntegrationProviderKind;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function listIntegrationConnectionsByProvider(
|
|
253
|
+
params: ListIntegrationConnectionsByProviderParams,
|
|
254
|
+
): Promise<IntegrationConnection[]> {
|
|
255
|
+
const rows = await db()
|
|
256
|
+
.select()
|
|
257
|
+
.from(integrationConnections)
|
|
258
|
+
.where(eq(integrationConnections.provider, params.provider))
|
|
259
|
+
.orderBy(integrationConnections.createdAt, integrationConnections.id);
|
|
260
|
+
|
|
261
|
+
return rows.map(toIntegrationConnection);
|
|
262
|
+
}
|
package/src/db/db.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
|
|
2
|
+
import {pgClient} from '@shipfox/node-postgres';
|
|
3
|
+
import {integrationConnections} from './schema/connections.js';
|
|
4
|
+
import {integrationsOutbox} from './schema/outbox.js';
|
|
5
|
+
import {integrationsWebhookDeliveries} from './schema/webhook-deliveries.js';
|
|
6
|
+
|
|
7
|
+
export const schema = {
|
|
8
|
+
integrationConnections,
|
|
9
|
+
integrationsOutbox,
|
|
10
|
+
integrationsWebhookDeliveries,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let _db: NodePgDatabase<typeof schema> | undefined;
|
|
14
|
+
|
|
15
|
+
export function db() {
|
|
16
|
+
if (!_db) _db = drizzle(pgClient(), {schema});
|
|
17
|
+
return _db;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function closeDb(): void {
|
|
21
|
+
_db = undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
|
+
import {index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
|
|
3
|
+
import type {
|
|
4
|
+
IntegrationConnection,
|
|
5
|
+
IntegrationConnectionLifecycleStatus,
|
|
6
|
+
} from '#core/entities/connection.js';
|
|
7
|
+
import type {IntegrationProviderKind} from '#core/entities/provider.js';
|
|
8
|
+
import {pgTable} from './common.js';
|
|
9
|
+
|
|
10
|
+
export const integrationConnections = pgTable(
|
|
11
|
+
'connections',
|
|
12
|
+
{
|
|
13
|
+
id: uuidv7PrimaryKey(),
|
|
14
|
+
workspaceId: uuid('workspace_id').notNull(),
|
|
15
|
+
provider: text('provider').notNull(),
|
|
16
|
+
externalAccountId: text('external_account_id').notNull(),
|
|
17
|
+
slug: text('slug').notNull(),
|
|
18
|
+
displayName: text('display_name').notNull(),
|
|
19
|
+
lifecycleStatus: text('lifecycle_status').notNull().default('active'),
|
|
20
|
+
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
21
|
+
updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
|
|
22
|
+
},
|
|
23
|
+
(table) => [
|
|
24
|
+
uniqueIndex('integrations_connections_workspace_external_unique').on(
|
|
25
|
+
table.workspaceId,
|
|
26
|
+
table.provider,
|
|
27
|
+
table.externalAccountId,
|
|
28
|
+
),
|
|
29
|
+
uniqueIndex('integrations_connections_workspace_slug_unique').on(table.workspaceId, table.slug),
|
|
30
|
+
index('integrations_connections_workspace_id_idx').on(table.workspaceId),
|
|
31
|
+
],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export type IntegrationConnectionDb = typeof integrationConnections.$inferSelect;
|
|
35
|
+
export type IntegrationConnectionCreateDb = typeof integrationConnections.$inferInsert;
|
|
36
|
+
|
|
37
|
+
export function toIntegrationConnection(row: IntegrationConnectionDb): IntegrationConnection {
|
|
38
|
+
return {
|
|
39
|
+
id: row.id,
|
|
40
|
+
workspaceId: row.workspaceId,
|
|
41
|
+
provider: row.provider as IntegrationProviderKind,
|
|
42
|
+
externalAccountId: row.externalAccountId,
|
|
43
|
+
slug: row.slug,
|
|
44
|
+
displayName: row.displayName,
|
|
45
|
+
lifecycleStatus: row.lifecycleStatus as IntegrationConnectionLifecycleStatus,
|
|
46
|
+
createdAt: row.createdAt,
|
|
47
|
+
updatedAt: row.updatedAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {index, primaryKey, text, timestamp} from 'drizzle-orm/pg-core';
|
|
2
|
+
import {pgTable} from './common.js';
|
|
3
|
+
|
|
4
|
+
export const integrationsWebhookDeliveries = pgTable(
|
|
5
|
+
'webhook_deliveries',
|
|
6
|
+
{
|
|
7
|
+
provider: text('provider').notNull(),
|
|
8
|
+
dedupScope: text('dedup_scope').notNull(),
|
|
9
|
+
deliveryId: text('delivery_id').notNull(),
|
|
10
|
+
receivedAt: timestamp('received_at', {withTimezone: true}).notNull().defaultNow(),
|
|
11
|
+
},
|
|
12
|
+
(table) => [
|
|
13
|
+
primaryKey({
|
|
14
|
+
name: 'integrations_webhook_deliveries_dedup_pk',
|
|
15
|
+
columns: [table.provider, table.dedupScope, table.deliveryId],
|
|
16
|
+
}),
|
|
17
|
+
index('integrations_webhook_deliveries_received_at_idx').on(table.receivedAt),
|
|
18
|
+
],
|
|
19
|
+
);
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INTEGRATION_EVENT_RECEIVED,
|
|
3
|
+
INTEGRATION_SOURCE_COMMIT_PUSHED,
|
|
4
|
+
type IntegrationEventReceivedEvent,
|
|
5
|
+
type SourcePushPayload,
|
|
6
|
+
} from '@shipfox/api-integration-core-dto';
|
|
7
|
+
import {and, eq, sql} from 'drizzle-orm';
|
|
8
|
+
import {db} from './db.js';
|
|
9
|
+
import {integrationsOutbox} from './schema/outbox.js';
|
|
10
|
+
import {integrationsWebhookDeliveries} from './schema/webhook-deliveries.js';
|
|
11
|
+
import {
|
|
12
|
+
publishIntegrationEventReceived,
|
|
13
|
+
publishSourceCommitPushed,
|
|
14
|
+
publishSourcePush,
|
|
15
|
+
recordDeliveryOnly,
|
|
16
|
+
} from './webhook-deliveries.js';
|
|
17
|
+
|
|
18
|
+
function buildEvent(
|
|
19
|
+
overrides: Partial<IntegrationEventReceivedEvent> = {},
|
|
20
|
+
): IntegrationEventReceivedEvent {
|
|
21
|
+
return {
|
|
22
|
+
provider: 'github',
|
|
23
|
+
source: 'github_acme_production',
|
|
24
|
+
event: 'push',
|
|
25
|
+
workspaceId: crypto.randomUUID(),
|
|
26
|
+
connectionId: crypto.randomUUID(),
|
|
27
|
+
connectionName: 'Acme Production',
|
|
28
|
+
deliveryId: crypto.randomUUID(),
|
|
29
|
+
receivedAt: new Date().toISOString(),
|
|
30
|
+
payload: {
|
|
31
|
+
externalRepositoryId: 'github:42',
|
|
32
|
+
ref: 'main',
|
|
33
|
+
headCommitSha: 'abc123',
|
|
34
|
+
defaultBranch: 'main',
|
|
35
|
+
isDefaultBranch: true,
|
|
36
|
+
},
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildPush(overrides: Partial<SourcePushPayload> = {}): SourcePushPayload {
|
|
42
|
+
return {
|
|
43
|
+
externalRepositoryId: 'github:42',
|
|
44
|
+
ref: 'main',
|
|
45
|
+
headCommitSha: 'abc123',
|
|
46
|
+
defaultBranch: 'main',
|
|
47
|
+
isDefaultBranch: true,
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildSourcePushParams() {
|
|
53
|
+
const rawPayload = {
|
|
54
|
+
ref: 'refs/heads/main',
|
|
55
|
+
after: 'abc123',
|
|
56
|
+
repository: {id: 42, default_branch: 'main'},
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
provider: 'github',
|
|
60
|
+
source: 'github_acme_production',
|
|
61
|
+
workspaceId: crypto.randomUUID(),
|
|
62
|
+
connectionId: crypto.randomUUID(),
|
|
63
|
+
connectionName: 'Acme Production',
|
|
64
|
+
deliveryId: crypto.randomUUID(),
|
|
65
|
+
receivedAt: new Date().toISOString(),
|
|
66
|
+
rawPayload,
|
|
67
|
+
push: buildPush(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deliveriesFor(provider: string, deliveryId: string) {
|
|
72
|
+
return db()
|
|
73
|
+
.select()
|
|
74
|
+
.from(integrationsWebhookDeliveries)
|
|
75
|
+
.where(
|
|
76
|
+
and(
|
|
77
|
+
eq(integrationsWebhookDeliveries.provider, provider),
|
|
78
|
+
eq(integrationsWebhookDeliveries.deliveryId, deliveryId),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function outboxFor(deliveryId: string) {
|
|
84
|
+
return db()
|
|
85
|
+
.select()
|
|
86
|
+
.from(integrationsOutbox)
|
|
87
|
+
.where(sql`${integrationsOutbox.payload}->>'deliveryId' = ${deliveryId}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('integration webhook delivery persistence', () => {
|
|
91
|
+
it('writes a delivery row and an outbox event for a new delivery', async () => {
|
|
92
|
+
const event = buildEvent();
|
|
93
|
+
|
|
94
|
+
const result = await publishIntegrationEventReceived({tx: db(), event});
|
|
95
|
+
|
|
96
|
+
expect(result.published).toBe(true);
|
|
97
|
+
expect(await deliveriesFor(event.provider, event.deliveryId)).toHaveLength(1);
|
|
98
|
+
const outbox = await outboxFor(event.deliveryId);
|
|
99
|
+
expect(outbox).toHaveLength(1);
|
|
100
|
+
expect(outbox[0]?.eventType).toBe(INTEGRATION_EVENT_RECEIVED);
|
|
101
|
+
expect(outbox[0]?.payload).toMatchObject({
|
|
102
|
+
provider: 'github',
|
|
103
|
+
source: event.source,
|
|
104
|
+
event: 'push',
|
|
105
|
+
deliveryId: event.deliveryId,
|
|
106
|
+
payload: {externalRepositoryId: 'github:42', isDefaultBranch: true},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('does not publish a duplicate delivery twice', async () => {
|
|
111
|
+
const event = buildEvent();
|
|
112
|
+
|
|
113
|
+
const first = await publishIntegrationEventReceived({tx: db(), event});
|
|
114
|
+
const second = await publishIntegrationEventReceived({tx: db(), event});
|
|
115
|
+
|
|
116
|
+
expect(first.published).toBe(true);
|
|
117
|
+
expect(second.published).toBe(false);
|
|
118
|
+
expect(await deliveriesFor(event.provider, event.deliveryId)).toHaveLength(1);
|
|
119
|
+
expect(await outboxFor(event.deliveryId)).toHaveLength(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('deduplicates received events per connection', async () => {
|
|
123
|
+
const deliveryId = crypto.randomUUID();
|
|
124
|
+
const firstConnectionEvent = buildEvent({
|
|
125
|
+
provider: 'webhook',
|
|
126
|
+
source: 'stripe-prod',
|
|
127
|
+
event: 'received',
|
|
128
|
+
deliveryId,
|
|
129
|
+
});
|
|
130
|
+
const secondConnectionEvent = buildEvent({
|
|
131
|
+
provider: 'webhook',
|
|
132
|
+
source: 'stripe-dev',
|
|
133
|
+
event: 'received',
|
|
134
|
+
deliveryId,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const first = await publishIntegrationEventReceived({tx: db(), event: firstConnectionEvent});
|
|
138
|
+
const second = await publishIntegrationEventReceived({tx: db(), event: secondConnectionEvent});
|
|
139
|
+
const duplicate = await publishIntegrationEventReceived({
|
|
140
|
+
tx: db(),
|
|
141
|
+
event: firstConnectionEvent,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(first.published).toBe(true);
|
|
145
|
+
expect(second.published).toBe(true);
|
|
146
|
+
expect(duplicate.published).toBe(false);
|
|
147
|
+
expect(await deliveriesFor('webhook', deliveryId)).toHaveLength(2);
|
|
148
|
+
expect(await outboxFor(deliveryId)).toHaveLength(2);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('records a delivery without writing an outbox event', async () => {
|
|
152
|
+
const deliveryId = crypto.randomUUID();
|
|
153
|
+
|
|
154
|
+
await recordDeliveryOnly({tx: db(), provider: 'github', deliveryId});
|
|
155
|
+
|
|
156
|
+
expect(await deliveriesFor('github', deliveryId)).toHaveLength(1);
|
|
157
|
+
expect(await outboxFor(deliveryId)).toHaveLength(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('ignores a duplicate delivery record', async () => {
|
|
161
|
+
const deliveryId = crypto.randomUUID();
|
|
162
|
+
|
|
163
|
+
await recordDeliveryOnly({tx: db(), provider: 'github', deliveryId});
|
|
164
|
+
await recordDeliveryOnly({tx: db(), provider: 'github', deliveryId});
|
|
165
|
+
|
|
166
|
+
expect(await deliveriesFor('github', deliveryId)).toHaveLength(1);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('publishSourcePush', () => {
|
|
171
|
+
it('writes the delivery row plus both outbox events for a new delivery', async () => {
|
|
172
|
+
const params = buildSourcePushParams();
|
|
173
|
+
|
|
174
|
+
const result = await db().transaction((tx) => publishSourcePush({tx, ...params}));
|
|
175
|
+
|
|
176
|
+
expect(result.published).toBe(true);
|
|
177
|
+
expect(await deliveriesFor(params.provider, params.deliveryId)).toHaveLength(1);
|
|
178
|
+
const types = (await outboxFor(params.deliveryId)).map((row) => row.eventType).sort();
|
|
179
|
+
expect(types).toEqual([INTEGRATION_EVENT_RECEIVED, INTEGRATION_SOURCE_COMMIT_PUSHED].sort());
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('emits the generic envelope (for triggers) and the typed event (for projects)', async () => {
|
|
183
|
+
const params = buildSourcePushParams();
|
|
184
|
+
|
|
185
|
+
await db().transaction((tx) => publishSourcePush({tx, ...params}));
|
|
186
|
+
|
|
187
|
+
const outbox = await outboxFor(params.deliveryId);
|
|
188
|
+
const envelope = outbox.find((row) => row.eventType === INTEGRATION_EVENT_RECEIVED);
|
|
189
|
+
const typed = outbox.find((row) => row.eventType === INTEGRATION_SOURCE_COMMIT_PUSHED);
|
|
190
|
+
expect(envelope?.payload).toMatchObject({
|
|
191
|
+
provider: 'github',
|
|
192
|
+
source: params.source,
|
|
193
|
+
event: 'push',
|
|
194
|
+
deliveryId: params.deliveryId,
|
|
195
|
+
payload: params.rawPayload,
|
|
196
|
+
});
|
|
197
|
+
expect(typed?.payload).toMatchObject({
|
|
198
|
+
provider: 'github',
|
|
199
|
+
deliveryId: params.deliveryId,
|
|
200
|
+
push: {externalRepositoryId: 'github:42', ref: 'main', headCommitSha: 'abc123'},
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('writes nothing for a duplicate delivery', async () => {
|
|
205
|
+
const params = buildSourcePushParams();
|
|
206
|
+
|
|
207
|
+
const first = await db().transaction((tx) => publishSourcePush({tx, ...params}));
|
|
208
|
+
const second = await db().transaction((tx) => publishSourcePush({tx, ...params}));
|
|
209
|
+
|
|
210
|
+
expect(first.published).toBe(true);
|
|
211
|
+
expect(second.published).toBe(false);
|
|
212
|
+
expect(await deliveriesFor(params.provider, params.deliveryId)).toHaveLength(1);
|
|
213
|
+
expect(await outboxFor(params.deliveryId)).toHaveLength(2);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('does not publish when the delivery was already recorded (record-only first)', async () => {
|
|
217
|
+
const params = buildSourcePushParams();
|
|
218
|
+
await recordDeliveryOnly({
|
|
219
|
+
tx: db(),
|
|
220
|
+
provider: params.provider,
|
|
221
|
+
deliveryId: params.deliveryId,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await db().transaction((tx) => publishSourcePush({tx, ...params}));
|
|
225
|
+
|
|
226
|
+
expect(result.published).toBe(false);
|
|
227
|
+
expect(await deliveriesFor(params.provider, params.deliveryId)).toHaveLength(1);
|
|
228
|
+
expect(await outboxFor(params.deliveryId)).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('publishSourceCommitPushed', () => {
|
|
233
|
+
function buildParams() {
|
|
234
|
+
return {
|
|
235
|
+
provider: 'gitea',
|
|
236
|
+
workspaceId: crypto.randomUUID(),
|
|
237
|
+
connectionId: crypto.randomUUID(),
|
|
238
|
+
deliveryId: crypto.randomUUID(),
|
|
239
|
+
receivedAt: new Date().toISOString(),
|
|
240
|
+
push: buildPush({externalRepositoryId: 'gitea:gitea-owner/platform'}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
it('writes only the typed event, never the generic envelope', async () => {
|
|
245
|
+
const params = buildParams();
|
|
246
|
+
|
|
247
|
+
await publishSourceCommitPushed(params);
|
|
248
|
+
|
|
249
|
+
const outbox = await outboxFor(params.deliveryId);
|
|
250
|
+
expect(outbox).toHaveLength(1);
|
|
251
|
+
expect(outbox[0]?.eventType).toBe(INTEGRATION_SOURCE_COMMIT_PUSHED);
|
|
252
|
+
expect(outbox.some((row) => row.eventType === INTEGRATION_EVENT_RECEIVED)).toBe(false);
|
|
253
|
+
expect(outbox[0]?.payload).toMatchObject({
|
|
254
|
+
provider: 'gitea',
|
|
255
|
+
deliveryId: params.deliveryId,
|
|
256
|
+
push: {externalRepositoryId: 'gitea:gitea-owner/platform', isDefaultBranch: true},
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('does not write a webhook-delivery dedup row', async () => {
|
|
261
|
+
const params = buildParams();
|
|
262
|
+
|
|
263
|
+
await publishSourceCommitPushed(params);
|
|
264
|
+
|
|
265
|
+
expect(await deliveriesFor(params.provider, params.deliveryId)).toHaveLength(0);
|
|
266
|
+
});
|
|
267
|
+
});
|