@shipfox/api-integration-core 12.1.1 → 12.3.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 +11 -8
- package/.turbo/turbo-type.log +0 -1
- package/CHANGELOG.md +44 -0
- package/dist/core/secret-cleanup.d.ts +20 -0
- package/dist/core/secret-cleanup.d.ts.map +1 -0
- package/dist/core/secret-cleanup.js +178 -0
- package/dist/core/secret-cleanup.js.map +1 -0
- package/dist/db/connections.d.ts.map +1 -1
- package/dist/db/connections.js +66 -13
- package/dist/db/connections.js.map +1 -1
- package/dist/db/db.d.ts +562 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +2 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/schema/secret-cleanups.d.ts +284 -0
- package/dist/db/schema/secret-cleanups.d.ts.map +1 -0
- package/dist/db/schema/secret-cleanups.js +39 -0
- package/dist/db/schema/secret-cleanups.js.map +1 -0
- package/dist/db/secret-cleanups.d.ts +52 -0
- package/dist/db/secret-cleanups.d.ts.map +1 -0
- package/dist/db/secret-cleanups.js +105 -0
- package/dist/db/secret-cleanups.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/manage-connections.d.ts.map +1 -1
- package/dist/presentation/routes/manage-connections.js +57 -18
- package/dist/presentation/routes/manage-connections.js.map +1 -1
- package/dist/providers/jira.d.ts.map +1 -1
- package/dist/providers/jira.js +67 -14
- package/dist/providers/jira.js.map +1 -1
- package/dist/temporal/activities/index.d.ts +5 -1
- package/dist/temporal/activities/index.d.ts.map +1 -1
- package/dist/temporal/activities/index.js +8 -2
- package/dist/temporal/activities/index.js.map +1 -1
- package/dist/temporal/constants.d.ts +1 -0
- package/dist/temporal/constants.d.ts.map +1 -1
- package/dist/temporal/constants.js +1 -0
- package/dist/temporal/constants.js.map +1 -1
- package/dist/temporal/workflows/cleanup-integration-secrets-cron.d.ts +2 -0
- package/dist/temporal/workflows/cleanup-integration-secrets-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/cleanup-integration-secrets-cron.js +21 -0
- package/dist/temporal/workflows/cleanup-integration-secrets-cron.js.map +1 -0
- package/dist/temporal/workflows/index.bundle.js +64 -3
- package/dist/temporal/workflows/index.d.ts +1 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -1
- package/dist/temporal/workflows/index.js +1 -0
- package/dist/temporal/workflows/index.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_durable_secret_cleanup.sql +22 -0
- package/drizzle/meta/0001_snapshot.json +180 -2
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +18 -17
- package/src/core/secret-cleanup.test.ts +129 -0
- package/src/core/secret-cleanup.ts +229 -0
- package/src/db/connections.test.ts +90 -0
- package/src/db/connections.ts +92 -13
- package/src/db/db.ts +2 -0
- package/src/db/schema/secret-cleanups.ts +42 -0
- package/src/db/secret-cleanups.test.ts +113 -0
- package/src/db/secret-cleanups.ts +209 -0
- package/src/index.ts +6 -1
- package/src/presentation/routes/manage-connections.test.ts +232 -0
- package/src/presentation/routes/manage-connections.ts +45 -11
- package/src/providers/jira.test.ts +133 -0
- package/src/providers/jira.ts +78 -13
- package/src/temporal/activities/index.ts +11 -1
- package/src/temporal/constants.ts +2 -0
- package/src/temporal/workflows/cleanup-integration-secrets-cron.ts +20 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/test/env.ts +3 -0
- package/test/globalSetup.ts +1 -0
- package/test/route-utils.ts +3 -0
- package/test/setup.ts +12 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
$ shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js
|
|
2
|
-
Successfully compiled:
|
|
3
|
-
2026-08-
|
|
2
|
+
Successfully compiled: 54 files with swc (479.94ms)
|
|
3
|
+
2026-08-07T08:47:05.106Z [INFO] asset workflow-bundle-e0d869d865fd59433325.js 3.17 MiB [emitted] [immutable] (name: main)
|
|
4
4
|
orphan modules 33.5 KiB [orphan] 21 modules
|
|
5
5
|
runtime modules 1.13 KiB 5 modules
|
|
6
6
|
modules by path ../../../../node_modules/.pnpm/ 970 KiB 196 modules
|
|
7
|
-
modules by path ./dist/temporal/
|
|
7
|
+
modules by path ./dist/temporal/ 2.41 KiB
|
|
8
|
+
modules by path ./dist/temporal/workflows/*.js 1.29 KiB
|
|
9
|
+
./dist/temporal/workflows/index.js 169 bytes [built] [code generated]
|
|
10
|
+
./dist/temporal/workflows/cleanup-integration-secrets-cron.js 750 bytes [built] [code generated]
|
|
11
|
+
./dist/temporal/workflows/prune-webhook-deliveries-cron.js 405 bytes [built] [code generated]
|
|
8
12
|
./dist/temporal/workflows/index-autogenerated-entrypoint.cjs 941 bytes [built] [code generated]
|
|
9
|
-
./dist/temporal/
|
|
10
|
-
./dist/temporal/workflows/prune-webhook-deliveries-cron.js 405 bytes [built] [code generated]
|
|
13
|
+
./dist/temporal/constants.js 198 bytes [built] [code generated]
|
|
11
14
|
optional modules 30 bytes [optional]
|
|
12
15
|
__temporal_custom_payload_converter (ignored) 15 bytes [optional] [built] [code generated]
|
|
13
16
|
__temporal_custom_failure_converter (ignored) 15 bytes [optional] [built] [code generated]
|
|
14
17
|
../../../shared/node/temporal/dist/workflow-error-interceptor.js 1.21 KiB [built] [code generated]
|
|
15
|
-
webpack 5.107.2 compiled successfully in
|
|
16
|
-
2026-08-
|
|
17
|
-
/home/runner/work/shipfox/shipfox/libs/api/integration/core/dist/temporal/workflows/index.js:
|
|
18
|
+
webpack 5.107.2 compiled successfully in 5802 ms
|
|
19
|
+
2026-08-07T08:47:05.141Z [INFO] Workflow bundle created { size: '3.17MB' }
|
|
20
|
+
/home/runner/work/shipfox/shipfox/libs/api/integration/core/dist/temporal/workflows/index.js: 3328998 bytes
|
package/.turbo/turbo-type.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @shipfox/api-integration-core
|
|
2
2
|
|
|
3
|
+
## 12.3.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e972eb3: Makes integration secret cleanup retryable after connection deletion.
|
|
8
|
+
- e0110fe: Adds Jira webhook renewal and disconnect deregistration support.
|
|
9
|
+
- Updated dependencies [4b0731e]
|
|
10
|
+
- Updated dependencies [e0110fe]
|
|
11
|
+
- @shipfox/api-workflows-dto@12.3.0
|
|
12
|
+
- @shipfox/api-integration-jira@12.3.0
|
|
13
|
+
- @shipfox/api-integration-spi@1.1.0
|
|
14
|
+
- @shipfox/api-integration-gitea@12.3.0
|
|
15
|
+
- @shipfox/api-integration-github@12.3.0
|
|
16
|
+
- @shipfox/api-integration-linear@12.3.0
|
|
17
|
+
- @shipfox/api-integration-sentry@12.3.0
|
|
18
|
+
- @shipfox/api-integration-slack@12.3.0
|
|
19
|
+
- @shipfox/api-integration-webhook@12.3.0
|
|
20
|
+
|
|
21
|
+
## 12.2.0
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 9695359: Cleans up Jira installation records and tokens when a workspace deletes an integration so the site can be reinstalled.
|
|
26
|
+
- 7901a60: Retry workspace workflow definition syncs when an integration connection becomes available.
|
|
27
|
+
- Updated dependencies [9695359]
|
|
28
|
+
- Updated dependencies [7901a60]
|
|
29
|
+
- Updated dependencies [ce0984d]
|
|
30
|
+
- @shipfox/api-integration-jira@12.2.0
|
|
31
|
+
- @shipfox/api-integration-core-dto@12.2.0
|
|
32
|
+
- @shipfox/api-workflows-dto@12.2.0
|
|
33
|
+
- @shipfox/api-integration-spi@1.0.1
|
|
34
|
+
- @shipfox/node-opentelemetry@0.6.4
|
|
35
|
+
- @shipfox/api-agent-dto@12.2.0
|
|
36
|
+
- @shipfox/api-integration-gitea@12.2.0
|
|
37
|
+
- @shipfox/api-integration-github@12.2.0
|
|
38
|
+
- @shipfox/api-integration-linear@12.2.0
|
|
39
|
+
- @shipfox/api-integration-sentry@12.2.0
|
|
40
|
+
- @shipfox/api-integration-slack@12.2.0
|
|
41
|
+
- @shipfox/api-integration-webhook@12.2.0
|
|
42
|
+
- @shipfox/node-fastify@0.4.2
|
|
43
|
+
- @shipfox/node-module@1.0.6
|
|
44
|
+
- @shipfox/node-temporal@0.4.5
|
|
45
|
+
- @shipfox/api-auth-context@12.2.0
|
|
46
|
+
|
|
3
47
|
## 12.1.1
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IntegrationConnection } from '#core/entities/connection.js';
|
|
2
|
+
import type { IntegrationProviderRegistry } from '#core/providers/registry.js';
|
|
3
|
+
export interface ProcessIntegrationSecretCleanupsOptions {
|
|
4
|
+
registry: IntegrationProviderRegistry;
|
|
5
|
+
connectionId?: string | undefined;
|
|
6
|
+
connection?: IntegrationConnection | undefined;
|
|
7
|
+
limit?: number | undefined;
|
|
8
|
+
now?: Date | undefined;
|
|
9
|
+
heartbeat?: (() => void) | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface ProcessIntegrationSecretCleanupsResult {
|
|
12
|
+
claimed: number;
|
|
13
|
+
completed: number;
|
|
14
|
+
failed: number;
|
|
15
|
+
unavailable: number;
|
|
16
|
+
/** Rows whose lease was taken over by another sweep before this one acknowledged them. */
|
|
17
|
+
unacknowledged: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function processIntegrationSecretCleanups(options: ProcessIntegrationSecretCleanupsOptions): Promise<ProcessIntegrationSecretCleanupsResult>;
|
|
20
|
+
//# sourceMappingURL=secret-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-cleanup.d.ts","sourceRoot":"","sources":["../../src/core/secret-cleanup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,6BAA6B,CAAC;AAkB7E,MAAM,WAAW,uCAAuC;IACtD,QAAQ,EAAE,2BAA2B,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,UAAU,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAC/C,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,CAAC;IACvB,SAAS,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,sCAAsC;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,cAAc,EAAE,MAAM,CAAC;CACxB;AAID,wBAAsB,gCAAgC,CACpD,OAAO,EAAE,uCAAuC,GAC/C,OAAO,CAAC,sCAAsC,CAAC,CAyDjD"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { reportError } from '@shipfox/node-error-monitoring';
|
|
2
|
+
import { logger } from '@shipfox/node-opentelemetry';
|
|
3
|
+
import { claimIntegrationSecretCleanups, completeIntegrationSecretCleanup, retryIntegrationSecretCleanup } from '#db/secret-cleanups.js';
|
|
4
|
+
const RETRY_BASE_DELAY_MS = 60 * 1_000;
|
|
5
|
+
const RETRY_MAX_DELAY_MS = 60 * 60 * 1_000;
|
|
6
|
+
const DEFAULT_BATCH_SIZE = 100;
|
|
7
|
+
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
8
|
+
const STUCK_CLEANUP_ALERT_CACHE_LIMIT = 10_000;
|
|
9
|
+
// Attempts, not hours: the backoff caps at RETRY_MAX_DELAY_MS, so a stuck row is retried
|
|
10
|
+
// hourly and this threshold lands about a week out. Revisit it alongside that cap.
|
|
11
|
+
const STUCK_CLEANUP_ATTEMPT_COUNT = 7 * 24;
|
|
12
|
+
const reportedStuckCleanupIds = new Set();
|
|
13
|
+
export async function processIntegrationSecretCleanups(options) {
|
|
14
|
+
const cleanups = await claimIntegrationSecretCleanups({
|
|
15
|
+
connectionId: options.connectionId,
|
|
16
|
+
limit: options.limit ?? DEFAULT_BATCH_SIZE,
|
|
17
|
+
now: options.now ?? new Date()
|
|
18
|
+
});
|
|
19
|
+
const result = {
|
|
20
|
+
claimed: cleanups.length,
|
|
21
|
+
completed: 0,
|
|
22
|
+
failed: 0,
|
|
23
|
+
unavailable: 0,
|
|
24
|
+
unacknowledged: 0
|
|
25
|
+
};
|
|
26
|
+
for (const cleanup of cleanups){
|
|
27
|
+
// Keep heartbeating through the provider call itself. Beating only between rows would
|
|
28
|
+
// let one slow deletion trip the activity's heartbeat timeout and kill the whole sweep.
|
|
29
|
+
options.heartbeat?.();
|
|
30
|
+
const heartbeatInterval = setInterval(()=>options.heartbeat?.(), HEARTBEAT_INTERVAL_MS);
|
|
31
|
+
// A single row must never strand the rest of the claimed batch under its lease.
|
|
32
|
+
try {
|
|
33
|
+
const provider = options.registry.list().find((candidate)=>candidate.provider === cleanup.provider);
|
|
34
|
+
if (!provider) {
|
|
35
|
+
result.unavailable += 1;
|
|
36
|
+
if (!await scheduleRetry(cleanup, 'provider-unavailable')) {
|
|
37
|
+
result.unacknowledged += 1;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// A loaded provider without the hook has no secrets to delete, so the intent is
|
|
42
|
+
// already satisfied and the row can retire.
|
|
43
|
+
await provider.deleteConnectionSecrets?.(getCleanupConnection(cleanup, options.connection));
|
|
44
|
+
const acknowledged = await completeIntegrationSecretCleanup({
|
|
45
|
+
id: cleanup.id,
|
|
46
|
+
leaseToken: requireLeaseToken(cleanup)
|
|
47
|
+
});
|
|
48
|
+
if (acknowledged) {
|
|
49
|
+
result.completed += 1;
|
|
50
|
+
} else {
|
|
51
|
+
result.unacknowledged += 1;
|
|
52
|
+
logLeaseLost(cleanup, 'complete');
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
result.failed += 1;
|
|
56
|
+
if (!await scheduleRetry(cleanup, 'cleanup-failed', error)) {
|
|
57
|
+
result.unacknowledged += 1;
|
|
58
|
+
}
|
|
59
|
+
} finally{
|
|
60
|
+
clearInterval(heartbeatInterval);
|
|
61
|
+
options.heartbeat?.();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function getCleanupConnection(cleanup, connection) {
|
|
67
|
+
if (connection?.id === cleanup.connectionId) return connection;
|
|
68
|
+
return {
|
|
69
|
+
id: cleanup.connectionId,
|
|
70
|
+
workspaceId: cleanup.workspaceId,
|
|
71
|
+
provider: cleanup.provider,
|
|
72
|
+
externalAccountId: cleanup.externalAccountId,
|
|
73
|
+
slug: cleanup.slug,
|
|
74
|
+
displayName: cleanup.displayName,
|
|
75
|
+
lifecycleStatus: cleanup.lifecycleStatus,
|
|
76
|
+
createdAt: cleanup.connectionCreatedAt,
|
|
77
|
+
updatedAt: cleanup.connectionUpdatedAt
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function scheduleRetry(cleanup, reason, error) {
|
|
81
|
+
const delayMs = retryDelayMs(cleanup.attemptCount);
|
|
82
|
+
logCleanupRetry(cleanup, reason, delayMs, error);
|
|
83
|
+
if (error !== undefined) {
|
|
84
|
+
reportError(error, {
|
|
85
|
+
boundary: 'integration.secret-cleanup',
|
|
86
|
+
operation: 'delete-connection-secrets',
|
|
87
|
+
tags: {
|
|
88
|
+
provider: cleanup.provider
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Keep escalating past the threshold. A stuck row means secrets may still exist, and
|
|
93
|
+
// one missed alert would hide that forever. The bounded per-cleanup cache suppresses
|
|
94
|
+
// duplicate reports in this worker while allowing a fresh process to re-alert.
|
|
95
|
+
if (cleanup.attemptCount >= STUCK_CLEANUP_ATTEMPT_COUNT) {
|
|
96
|
+
const stuckErrorMessage = 'Integration connection secret cleanup exceeded its retry threshold';
|
|
97
|
+
logger().error({
|
|
98
|
+
provider: cleanup.provider,
|
|
99
|
+
connectionId: cleanup.connectionId,
|
|
100
|
+
attempt: cleanup.attemptCount
|
|
101
|
+
}, stuckErrorMessage);
|
|
102
|
+
if (!reportedStuckCleanupIds.has(cleanup.id)) {
|
|
103
|
+
const eventId = reportError(new Error(stuckErrorMessage), {
|
|
104
|
+
boundary: 'integration.secret-cleanup',
|
|
105
|
+
operation: 'stuck-cleanup',
|
|
106
|
+
tags: {
|
|
107
|
+
provider: cleanup.provider
|
|
108
|
+
},
|
|
109
|
+
extra: {
|
|
110
|
+
cleanupId: cleanup.id
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (eventId) {
|
|
114
|
+
reportedStuckCleanupIds.add(cleanup.id);
|
|
115
|
+
if (reportedStuckCleanupIds.size > STUCK_CLEANUP_ALERT_CACHE_LIMIT) {
|
|
116
|
+
const oldestCleanupId = reportedStuckCleanupIds.values().next().value;
|
|
117
|
+
if (oldestCleanupId !== undefined) reportedStuckCleanupIds.delete(oldestCleanupId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const rescheduled = await retryIntegrationSecretCleanup({
|
|
124
|
+
id: cleanup.id,
|
|
125
|
+
leaseToken: requireLeaseToken(cleanup),
|
|
126
|
+
delayMs
|
|
127
|
+
});
|
|
128
|
+
if (!rescheduled) {
|
|
129
|
+
logLeaseLost(cleanup, 'retry');
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
} catch (retryError) {
|
|
134
|
+
// The lease expires on its own, so a later sweep still picks this row up.
|
|
135
|
+
logger().error({
|
|
136
|
+
provider: cleanup.provider,
|
|
137
|
+
connectionId: cleanup.connectionId,
|
|
138
|
+
err: retryError
|
|
139
|
+
}, 'Failed to reschedule integration connection secret cleanup');
|
|
140
|
+
reportError(retryError, {
|
|
141
|
+
boundary: 'integration.secret-cleanup',
|
|
142
|
+
operation: 'reschedule-cleanup',
|
|
143
|
+
tags: {
|
|
144
|
+
provider: cleanup.provider
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function retryDelayMs(attemptCount) {
|
|
151
|
+
return Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attemptCount - 1), RETRY_MAX_DELAY_MS);
|
|
152
|
+
}
|
|
153
|
+
function requireLeaseToken(cleanup) {
|
|
154
|
+
if (!cleanup.leaseToken) throw new Error('Claimed integration secret cleanup has no lease token');
|
|
155
|
+
return cleanup.leaseToken;
|
|
156
|
+
}
|
|
157
|
+
function logCleanupRetry(cleanup, reason, delayMs, error) {
|
|
158
|
+
logger().warn({
|
|
159
|
+
provider: cleanup.provider,
|
|
160
|
+
connectionId: cleanup.connectionId,
|
|
161
|
+
attempt: cleanup.attemptCount,
|
|
162
|
+
reason,
|
|
163
|
+
delayMs,
|
|
164
|
+
...error === undefined ? {} : {
|
|
165
|
+
err: error
|
|
166
|
+
}
|
|
167
|
+
}, 'Integration connection secret cleanup will be retried');
|
|
168
|
+
}
|
|
169
|
+
function logLeaseLost(cleanup, operation) {
|
|
170
|
+
logger().warn({
|
|
171
|
+
provider: cleanup.provider,
|
|
172
|
+
connectionId: cleanup.connectionId,
|
|
173
|
+
attempt: cleanup.attemptCount,
|
|
174
|
+
operation
|
|
175
|
+
}, 'Integration connection secret cleanup lease was lost before acknowledgement');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
//# sourceMappingURL=secret-cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/secret-cleanup.ts"],"sourcesContent":["import {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {IntegrationConnection} from '#core/entities/connection.js';\nimport type {IntegrationProviderRegistry} from '#core/providers/registry.js';\nimport {\n claimIntegrationSecretCleanups,\n completeIntegrationSecretCleanup,\n type IntegrationSecretCleanup,\n retryIntegrationSecretCleanup,\n} from '#db/secret-cleanups.js';\n\nconst RETRY_BASE_DELAY_MS = 60 * 1_000;\nconst RETRY_MAX_DELAY_MS = 60 * 60 * 1_000;\nconst DEFAULT_BATCH_SIZE = 100;\nconst HEARTBEAT_INTERVAL_MS = 15_000;\nconst STUCK_CLEANUP_ALERT_CACHE_LIMIT = 10_000;\n// Attempts, not hours: the backoff caps at RETRY_MAX_DELAY_MS, so a stuck row is retried\n// hourly and this threshold lands about a week out. Revisit it alongside that cap.\nconst STUCK_CLEANUP_ATTEMPT_COUNT = 7 * 24;\nconst reportedStuckCleanupIds = new Set<string>();\n\nexport interface ProcessIntegrationSecretCleanupsOptions {\n registry: IntegrationProviderRegistry;\n connectionId?: string | undefined;\n connection?: IntegrationConnection | undefined;\n limit?: number | undefined;\n now?: Date | undefined;\n heartbeat?: (() => void) | undefined;\n}\n\nexport interface ProcessIntegrationSecretCleanupsResult {\n claimed: number;\n completed: number;\n failed: number;\n unavailable: number;\n /** Rows whose lease was taken over by another sweep before this one acknowledged them. */\n unacknowledged: number;\n}\n\ntype CleanupRetryReason = 'provider-unavailable' | 'cleanup-failed';\n\nexport async function processIntegrationSecretCleanups(\n options: ProcessIntegrationSecretCleanupsOptions,\n): Promise<ProcessIntegrationSecretCleanupsResult> {\n const cleanups = await claimIntegrationSecretCleanups({\n connectionId: options.connectionId,\n limit: options.limit ?? DEFAULT_BATCH_SIZE,\n now: options.now ?? new Date(),\n });\n const result: ProcessIntegrationSecretCleanupsResult = {\n claimed: cleanups.length,\n completed: 0,\n failed: 0,\n unavailable: 0,\n unacknowledged: 0,\n };\n\n for (const cleanup of cleanups) {\n // Keep heartbeating through the provider call itself. Beating only between rows would\n // let one slow deletion trip the activity's heartbeat timeout and kill the whole sweep.\n options.heartbeat?.();\n const heartbeatInterval = setInterval(() => options.heartbeat?.(), HEARTBEAT_INTERVAL_MS);\n // A single row must never strand the rest of the claimed batch under its lease.\n try {\n const provider = options.registry\n .list()\n .find((candidate) => candidate.provider === cleanup.provider);\n if (!provider) {\n result.unavailable += 1;\n if (!(await scheduleRetry(cleanup, 'provider-unavailable'))) {\n result.unacknowledged += 1;\n }\n continue;\n }\n\n // A loaded provider without the hook has no secrets to delete, so the intent is\n // already satisfied and the row can retire.\n await provider.deleteConnectionSecrets?.(getCleanupConnection(cleanup, options.connection));\n const acknowledged = await completeIntegrationSecretCleanup({\n id: cleanup.id,\n leaseToken: requireLeaseToken(cleanup),\n });\n if (acknowledged) {\n result.completed += 1;\n } else {\n result.unacknowledged += 1;\n logLeaseLost(cleanup, 'complete');\n }\n } catch (error) {\n result.failed += 1;\n if (!(await scheduleRetry(cleanup, 'cleanup-failed', error))) {\n result.unacknowledged += 1;\n }\n } finally {\n clearInterval(heartbeatInterval);\n options.heartbeat?.();\n }\n }\n\n return result;\n}\n\nfunction getCleanupConnection(\n cleanup: IntegrationSecretCleanup,\n connection: IntegrationConnection | undefined,\n): IntegrationConnection {\n if (connection?.id === cleanup.connectionId) return connection;\n return {\n id: cleanup.connectionId,\n workspaceId: cleanup.workspaceId,\n provider: cleanup.provider,\n externalAccountId: cleanup.externalAccountId,\n slug: cleanup.slug,\n displayName: cleanup.displayName,\n lifecycleStatus: cleanup.lifecycleStatus,\n createdAt: cleanup.connectionCreatedAt,\n updatedAt: cleanup.connectionUpdatedAt,\n };\n}\n\nasync function scheduleRetry(\n cleanup: IntegrationSecretCleanup,\n reason: CleanupRetryReason,\n error?: unknown,\n): Promise<boolean> {\n const delayMs = retryDelayMs(cleanup.attemptCount);\n logCleanupRetry(cleanup, reason, delayMs, error);\n if (error !== undefined) {\n reportError(error, {\n boundary: 'integration.secret-cleanup',\n operation: 'delete-connection-secrets',\n tags: {provider: cleanup.provider},\n });\n }\n // Keep escalating past the threshold. A stuck row means secrets may still exist, and\n // one missed alert would hide that forever. The bounded per-cleanup cache suppresses\n // duplicate reports in this worker while allowing a fresh process to re-alert.\n if (cleanup.attemptCount >= STUCK_CLEANUP_ATTEMPT_COUNT) {\n const stuckErrorMessage = 'Integration connection secret cleanup exceeded its retry threshold';\n logger().error(\n {\n provider: cleanup.provider,\n connectionId: cleanup.connectionId,\n attempt: cleanup.attemptCount,\n },\n stuckErrorMessage,\n );\n if (!reportedStuckCleanupIds.has(cleanup.id)) {\n const eventId = reportError(new Error(stuckErrorMessage), {\n boundary: 'integration.secret-cleanup',\n operation: 'stuck-cleanup',\n tags: {provider: cleanup.provider},\n extra: {cleanupId: cleanup.id},\n });\n if (eventId) {\n reportedStuckCleanupIds.add(cleanup.id);\n if (reportedStuckCleanupIds.size > STUCK_CLEANUP_ALERT_CACHE_LIMIT) {\n const oldestCleanupId = reportedStuckCleanupIds.values().next().value;\n if (oldestCleanupId !== undefined) reportedStuckCleanupIds.delete(oldestCleanupId);\n }\n }\n }\n }\n\n try {\n const rescheduled = await retryIntegrationSecretCleanup({\n id: cleanup.id,\n leaseToken: requireLeaseToken(cleanup),\n delayMs,\n });\n if (!rescheduled) {\n logLeaseLost(cleanup, 'retry');\n return false;\n }\n return true;\n } catch (retryError) {\n // The lease expires on its own, so a later sweep still picks this row up.\n logger().error(\n {provider: cleanup.provider, connectionId: cleanup.connectionId, err: retryError},\n 'Failed to reschedule integration connection secret cleanup',\n );\n reportError(retryError, {\n boundary: 'integration.secret-cleanup',\n operation: 'reschedule-cleanup',\n tags: {provider: cleanup.provider},\n });\n return false;\n }\n}\n\nfunction retryDelayMs(attemptCount: number): number {\n return Math.min(RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attemptCount - 1), RETRY_MAX_DELAY_MS);\n}\n\nfunction requireLeaseToken(cleanup: IntegrationSecretCleanup): string {\n if (!cleanup.leaseToken) throw new Error('Claimed integration secret cleanup has no lease token');\n return cleanup.leaseToken;\n}\n\nfunction logCleanupRetry(\n cleanup: IntegrationSecretCleanup,\n reason: CleanupRetryReason,\n delayMs: number,\n error: unknown,\n): void {\n logger().warn(\n {\n provider: cleanup.provider,\n connectionId: cleanup.connectionId,\n attempt: cleanup.attemptCount,\n reason,\n delayMs,\n ...(error === undefined ? {} : {err: error}),\n },\n 'Integration connection secret cleanup will be retried',\n );\n}\n\nfunction logLeaseLost(cleanup: IntegrationSecretCleanup, operation: 'complete' | 'retry'): void {\n logger().warn(\n {\n provider: cleanup.provider,\n connectionId: cleanup.connectionId,\n attempt: cleanup.attemptCount,\n operation,\n },\n 'Integration connection secret cleanup lease was lost before acknowledgement',\n );\n}\n"],"names":["reportError","logger","claimIntegrationSecretCleanups","completeIntegrationSecretCleanup","retryIntegrationSecretCleanup","RETRY_BASE_DELAY_MS","RETRY_MAX_DELAY_MS","DEFAULT_BATCH_SIZE","HEARTBEAT_INTERVAL_MS","STUCK_CLEANUP_ALERT_CACHE_LIMIT","STUCK_CLEANUP_ATTEMPT_COUNT","reportedStuckCleanupIds","Set","processIntegrationSecretCleanups","options","cleanups","connectionId","limit","now","Date","result","claimed","length","completed","failed","unavailable","unacknowledged","cleanup","heartbeat","heartbeatInterval","setInterval","provider","registry","list","find","candidate","scheduleRetry","deleteConnectionSecrets","getCleanupConnection","connection","acknowledged","id","leaseToken","requireLeaseToken","logLeaseLost","error","clearInterval","workspaceId","externalAccountId","slug","displayName","lifecycleStatus","createdAt","connectionCreatedAt","updatedAt","connectionUpdatedAt","reason","delayMs","retryDelayMs","attemptCount","logCleanupRetry","undefined","boundary","operation","tags","stuckErrorMessage","attempt","has","eventId","Error","extra","cleanupId","add","size","oldestCleanupId","values","next","value","delete","rescheduled","retryError","err","Math","min","max","warn"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/db/connections.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/db/connections.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,qBAAqB,EACrB,oCAAoC,EACrC,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,4BAA4B,CAAC;AAExE,OAAO,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAI3B,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC;AAC3C,KAAK,aAAa,GAAG,UAAU,CAAC,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEhF,MAAM,WAAW,iCAAiC;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,uBAAuB,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,oCAAoC,GAAG,SAAS,CAAC;CACpE;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,iCAAiC,EACzC,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,aAAa,GAAG,aAAa,GAAG,SAAS,CAAA;CAAM,GAC7D,OAAO,CAAC,qBAAqB,CAAC,CA+DhC;AAED,MAAM,WAAW,iCAAiC;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,uBAAuB,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,oCAAoC,GAAG,SAAS,CAAC;CACpE;AAOD,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAW9E;AAED,wBAAgB,0CAA0C,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAWlF;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,iCAAiC,EACzC,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,aAAa,GAAG,aAAa,GAAG,SAAS,CAAA;CAAM,GAC7D,OAAO,CAAC,qBAAqB,CAAC,CAwChC;AAED,MAAM,WAAW,iCAAiC;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,uBAAuB,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,iCAAiC,EACzC,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,aAAa,GAAG,aAAa,GAAG,SAAS,CAAA;CAAM,GAC7D,OAAO,CAAC,MAAM,CAAC,CA2BjB;AAED,MAAM,MAAM,6BAA6B,GAAG,OAAO,2BAA2B,CAAC;AAE/E,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,MAAM,EACV,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,aAAa,GAAG,aAAa,GAAG,SAAS,CAAA;CAAM,GAC7D,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAU5C;AAED,MAAM,MAAM,8BAA8B,GAAG,OAAO,4BAA4B,CAAC;AAEjF,MAAM,WAAW,oCAAoC;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,oCAAoC,GAC3C,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAc5C;AAED,MAAM,MAAM,gCAAgC,GAAG,OAAO,8BAA8B,CAAC;AAErF,MAAM,WAAW,gDAAgD;IAC/D,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,oCAAoC,CAAC;CACvD;AAED,wBAAsB,0CAA0C,CAC9D,MAAM,EAAE,gDAAgD,EACxD,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,aAAa,GAAG,aAAa,GAAG,SAAS,CAAA;CAAM,GAC7D,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAyB5C;AAED,MAAM,MAAM,4CAA4C,GACtD,OAAO,0CAA0C,CAAC;AAiBpD,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE;IAAC,EAAE,EAAE,MAAM,CAAA;CAAC,EACpB,OAAO,GAAE;IAAC,EAAE,CAAC,EAAE,aAAa,GAAG,aAAa,GAAG,SAAS,CAAA;CAAM,GAC7D,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,MAAM,MAAM,6BAA6B,GAAG,OAAO,2BAA2B,CAAC;AAE/E,MAAM,WAAW,gCAAgC;IAC/C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,gCAAgC,GACvC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CASlC;AAED,MAAM,WAAW,0CAA0C;IACzD,QAAQ,EAAE,uBAAuB,CAAC;CACnC;AAED,wBAAsB,oCAAoC,CACxD,MAAM,EAAE,0CAA0C,GACjD,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAQlC"}
|
package/dist/db/connections.js
CHANGED
|
@@ -1,33 +1,53 @@
|
|
|
1
|
-
import { CONNECTION_SLUG_MAX_LENGTH } from '@shipfox/api-integration-core-dto';
|
|
1
|
+
import { CONNECTION_SLUG_MAX_LENGTH, INTEGRATION_CONNECTION_AVAILABLE } from '@shipfox/api-integration-core-dto';
|
|
2
2
|
import { ConnectionSlugConflictError } from '@shipfox/api-integration-spi';
|
|
3
|
+
import { writeOutboxEvent } from '@shipfox/node-outbox';
|
|
3
4
|
import { and, eq } from 'drizzle-orm';
|
|
4
5
|
import { IntegrationConnectionAlreadyExistsError } from '#core/errors.js';
|
|
5
6
|
import { db } from './db.js';
|
|
6
7
|
import { integrationConnections, toIntegrationConnection } from './schema/connections.js';
|
|
8
|
+
import { integrationsOutbox } from './schema/outbox.js';
|
|
7
9
|
export async function upsertIntegrationConnection(params, options = {}) {
|
|
8
|
-
|
|
10
|
+
if (options.tx === undefined) {
|
|
11
|
+
return await db().transaction((tx)=>upsertIntegrationConnection(params, {
|
|
12
|
+
tx
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
const executor = options.tx;
|
|
9
16
|
const now = new Date();
|
|
10
|
-
|
|
17
|
+
let [row] = await executor.insert(integrationConnections).values({
|
|
11
18
|
workspaceId: params.workspaceId,
|
|
12
19
|
provider: params.provider,
|
|
13
20
|
externalAccountId: params.externalAccountId,
|
|
14
21
|
slug: params.slug,
|
|
15
22
|
displayName: params.displayName,
|
|
16
23
|
lifecycleStatus: params.lifecycleStatus ?? 'active'
|
|
17
|
-
}).
|
|
24
|
+
}).onConflictDoNothing({
|
|
18
25
|
target: [
|
|
19
26
|
integrationConnections.workspaceId,
|
|
20
27
|
integrationConnections.provider,
|
|
21
28
|
integrationConnections.externalAccountId
|
|
22
|
-
]
|
|
23
|
-
|
|
29
|
+
]
|
|
30
|
+
}).returning();
|
|
31
|
+
let becameAvailable = row?.lifecycleStatus === 'active';
|
|
32
|
+
if (!row) {
|
|
33
|
+
const [existing] = await executor.select({
|
|
34
|
+
id: integrationConnections.id,
|
|
35
|
+
lifecycleStatus: integrationConnections.lifecycleStatus
|
|
36
|
+
}).from(integrationConnections).where(and(eq(integrationConnections.workspaceId, params.workspaceId), eq(integrationConnections.provider, params.provider), eq(integrationConnections.externalAccountId, params.externalAccountId))).limit(1).for('update');
|
|
37
|
+
if (!existing) throw new Error('Integration connection upsert conflict row was not found');
|
|
38
|
+
[row] = await executor.update(integrationConnections).set({
|
|
24
39
|
displayName: params.displayName,
|
|
25
40
|
lifecycleStatus: params.lifecycleStatus ?? 'active',
|
|
26
41
|
updatedAt: now
|
|
27
|
-
}
|
|
28
|
-
|
|
42
|
+
}).where(eq(integrationConnections.id, existing.id)).returning();
|
|
43
|
+
becameAvailable = existing.lifecycleStatus !== 'active' && row?.lifecycleStatus === 'active';
|
|
44
|
+
}
|
|
29
45
|
if (!row) throw new Error('Integration connection upsert returned no rows');
|
|
30
|
-
|
|
46
|
+
const connection = toIntegrationConnection(row);
|
|
47
|
+
if (becameAvailable) {
|
|
48
|
+
await writeConnectionAvailableEvent(executor, connection);
|
|
49
|
+
}
|
|
50
|
+
return connection;
|
|
31
51
|
}
|
|
32
52
|
const INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT = 'integrations_connections_workspace_external_unique';
|
|
33
53
|
const INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT = 'integrations_connections_workspace_slug_unique';
|
|
@@ -56,7 +76,12 @@ export function isIntegrationConnectionSlugUniqueViolation(error) {
|
|
|
56
76
|
return false;
|
|
57
77
|
}
|
|
58
78
|
export async function createIntegrationConnection(params, options = {}) {
|
|
59
|
-
|
|
79
|
+
if (options.tx === undefined) {
|
|
80
|
+
return await db().transaction((tx)=>createIntegrationConnection(params, {
|
|
81
|
+
tx
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
const executor = options.tx;
|
|
60
85
|
let rows;
|
|
61
86
|
try {
|
|
62
87
|
rows = await executor.insert(integrationConnections).values({
|
|
@@ -78,7 +103,11 @@ export async function createIntegrationConnection(params, options = {}) {
|
|
|
78
103
|
}
|
|
79
104
|
const row = rows[0];
|
|
80
105
|
if (!row) throw new Error('Integration connection insert returned no rows');
|
|
81
|
-
|
|
106
|
+
const connection = toIntegrationConnection(row);
|
|
107
|
+
if (connection.lifecycleStatus === 'active') {
|
|
108
|
+
await writeConnectionAvailableEvent(executor, connection);
|
|
109
|
+
}
|
|
110
|
+
return connection;
|
|
82
111
|
}
|
|
83
112
|
export async function resolveUniqueConnectionSlug(params, options = {}) {
|
|
84
113
|
const executor = options.tx ?? db();
|
|
@@ -111,13 +140,37 @@ export async function getIntegrationConnectionBySlug(params) {
|
|
|
111
140
|
return toIntegrationConnection(row);
|
|
112
141
|
}
|
|
113
142
|
export async function updateIntegrationConnectionLifecycleStatus(params, options = {}) {
|
|
114
|
-
|
|
143
|
+
if (options.tx === undefined) {
|
|
144
|
+
return await db().transaction((tx)=>updateIntegrationConnectionLifecycleStatus(params, {
|
|
145
|
+
tx
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
const executor = options.tx;
|
|
149
|
+
const [existing] = await executor.select({
|
|
150
|
+
lifecycleStatus: integrationConnections.lifecycleStatus
|
|
151
|
+
}).from(integrationConnections).where(eq(integrationConnections.id, params.id)).limit(1).for('update');
|
|
152
|
+
if (!existing) return undefined;
|
|
115
153
|
const [row] = await executor.update(integrationConnections).set({
|
|
116
154
|
lifecycleStatus: params.lifecycleStatus,
|
|
117
155
|
updatedAt: new Date()
|
|
118
156
|
}).where(eq(integrationConnections.id, params.id)).returning();
|
|
119
157
|
if (!row) return undefined;
|
|
120
|
-
|
|
158
|
+
const connection = toIntegrationConnection(row);
|
|
159
|
+
if (existing.lifecycleStatus !== 'active' && connection.lifecycleStatus === 'active') {
|
|
160
|
+
await writeConnectionAvailableEvent(executor, connection);
|
|
161
|
+
}
|
|
162
|
+
return connection;
|
|
163
|
+
}
|
|
164
|
+
async function writeConnectionAvailableEvent(executor, connection) {
|
|
165
|
+
await writeOutboxEvent(executor, integrationsOutbox, {
|
|
166
|
+
type: INTEGRATION_CONNECTION_AVAILABLE,
|
|
167
|
+
payload: {
|
|
168
|
+
provider: connection.provider,
|
|
169
|
+
workspaceId: connection.workspaceId,
|
|
170
|
+
connectionId: connection.id,
|
|
171
|
+
slug: connection.slug
|
|
172
|
+
}
|
|
173
|
+
});
|
|
121
174
|
}
|
|
122
175
|
export async function deleteIntegrationConnection(params, options = {}) {
|
|
123
176
|
const executor = options.tx ?? db();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/connections.ts"],"sourcesContent":["import {CONNECTION_SLUG_MAX_LENGTH} from '@shipfox/api-integration-core-dto';\nimport {ConnectionSlugConflictError} from '@shipfox/api-integration-spi';\nimport {and, eq} from 'drizzle-orm';\nimport type {\n IntegrationConnection,\n IntegrationConnectionLifecycleStatus,\n} from '#core/entities/connection.js';\nimport type {IntegrationProviderKind} from '#core/entities/provider.js';\nimport {IntegrationConnectionAlreadyExistsError} from '#core/errors.js';\nimport {db} from './db.js';\nimport {integrationConnections, toIntegrationConnection} from './schema/connections.js';\n\ntype IntegrationDb = ReturnType<typeof db>;\ntype IntegrationTx = Parameters<Parameters<IntegrationDb['transaction']>[0]>[0];\n\nexport interface UpsertIntegrationConnectionParams {\n workspaceId: string;\n provider: IntegrationProviderKind;\n externalAccountId: string;\n slug: string;\n displayName: string;\n lifecycleStatus?: IntegrationConnectionLifecycleStatus | undefined;\n}\n\nexport async function upsertIntegrationConnection(\n params: UpsertIntegrationConnectionParams,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<IntegrationConnection> {\n const executor = options.tx ?? db();\n const now = new Date();\n const [row] = await executor\n .insert(integrationConnections)\n .values({\n workspaceId: params.workspaceId,\n provider: params.provider,\n externalAccountId: params.externalAccountId,\n slug: params.slug,\n displayName: params.displayName,\n lifecycleStatus: params.lifecycleStatus ?? 'active',\n })\n .onConflictDoUpdate({\n target: [\n integrationConnections.workspaceId,\n integrationConnections.provider,\n integrationConnections.externalAccountId,\n ],\n set: {\n displayName: params.displayName,\n lifecycleStatus: params.lifecycleStatus ?? 'active',\n updatedAt: now,\n },\n })\n .returning();\n\n if (!row) throw new Error('Integration connection upsert returned no rows');\n return toIntegrationConnection(row);\n}\n\nexport interface CreateIntegrationConnectionParams {\n workspaceId: string;\n provider: IntegrationProviderKind;\n externalAccountId: string;\n slug: string;\n displayName: string;\n lifecycleStatus?: IntegrationConnectionLifecycleStatus | undefined;\n}\n\nconst INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT =\n 'integrations_connections_workspace_external_unique';\nconst INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT =\n 'integrations_connections_workspace_slug_unique';\n\nexport function isIntegrationConnectionUniqueViolation(error: unknown): boolean {\n let current: unknown = error;\n for (let depth = 0; depth < 5 && current != null; depth += 1) {\n if (typeof current !== 'object') return false;\n const {code, constraint} = current as {code?: unknown; constraint?: unknown};\n if (code === '23505' && constraint === INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT) {\n return true;\n }\n current = (current as {cause?: unknown}).cause;\n }\n return false;\n}\n\nexport function isIntegrationConnectionSlugUniqueViolation(error: unknown): boolean {\n let current: unknown = error;\n for (let depth = 0; depth < 5 && current != null; depth += 1) {\n if (typeof current !== 'object') return false;\n const {code, constraint} = current as {code?: unknown; constraint?: unknown};\n if (code === '23505' && constraint === INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT) {\n return true;\n }\n current = (current as {cause?: unknown}).cause;\n }\n return false;\n}\n\nexport async function createIntegrationConnection(\n params: CreateIntegrationConnectionParams,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<IntegrationConnection> {\n const executor = options.tx ?? db();\n let rows: (typeof integrationConnections.$inferSelect)[];\n try {\n rows = await executor\n .insert(integrationConnections)\n .values({\n workspaceId: params.workspaceId,\n provider: params.provider,\n externalAccountId: params.externalAccountId,\n slug: params.slug,\n displayName: params.displayName,\n lifecycleStatus: params.lifecycleStatus ?? 'active',\n })\n .returning();\n } catch (error) {\n if (isIntegrationConnectionUniqueViolation(error)) {\n throw new IntegrationConnectionAlreadyExistsError(\n params.workspaceId,\n params.provider,\n params.externalAccountId,\n );\n }\n if (isIntegrationConnectionSlugUniqueViolation(error)) {\n throw new ConnectionSlugConflictError(error);\n }\n throw error;\n }\n\n const row = rows[0];\n if (!row) throw new Error('Integration connection insert returned no rows');\n return toIntegrationConnection(row);\n}\n\nexport interface ResolveUniqueConnectionSlugParams {\n workspaceId: string;\n provider: IntegrationProviderKind;\n externalAccountId: string;\n baseSlug: string;\n}\n\nexport async function resolveUniqueConnectionSlug(\n params: ResolveUniqueConnectionSlugParams,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<string> {\n const executor = options.tx ?? db();\n const [existing] = await executor\n .select({slug: integrationConnections.slug})\n .from(integrationConnections)\n .where(\n and(\n eq(integrationConnections.workspaceId, params.workspaceId),\n eq(integrationConnections.provider, params.provider),\n eq(integrationConnections.externalAccountId, params.externalAccountId),\n ),\n )\n .limit(1);\n if (existing) return existing.slug;\n\n const workspaceSlugs = await executor\n .select({slug: integrationConnections.slug})\n .from(integrationConnections)\n .where(eq(integrationConnections.workspaceId, params.workspaceId));\n const used = new Set(workspaceSlugs.map((row) => row.slug));\n\n for (let suffixNumber = 1; ; suffixNumber += 1) {\n const suffix = suffixNumber === 1 ? '' : `_${suffixNumber}`;\n const baseBudget = CONNECTION_SLUG_MAX_LENGTH - suffix.length;\n const candidate = `${params.baseSlug.slice(0, baseBudget).replaceAll(/[_-]+$/g, '')}${suffix}`;\n if (!used.has(candidate)) return candidate;\n }\n}\n\nexport type CreateIntegrationConnectionFn = typeof createIntegrationConnection;\n\nexport async function getIntegrationConnectionById(\n id: string,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<IntegrationConnection | undefined> {\n const executor = options.tx ?? db();\n const rows = await executor\n .select()\n .from(integrationConnections)\n .where(eq(integrationConnections.id, id))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toIntegrationConnection(row);\n}\n\nexport type GetIntegrationConnectionByIdFn = typeof getIntegrationConnectionById;\n\nexport interface GetIntegrationConnectionBySlugParams {\n workspaceId: string;\n slug: string;\n}\n\nexport async function getIntegrationConnectionBySlug(\n params: GetIntegrationConnectionBySlugParams,\n): Promise<IntegrationConnection | undefined> {\n const rows = await db()\n .select()\n .from(integrationConnections)\n .where(\n and(\n eq(integrationConnections.workspaceId, params.workspaceId),\n eq(integrationConnections.slug, params.slug),\n ),\n )\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toIntegrationConnection(row);\n}\n\nexport type GetIntegrationConnectionBySlugFn = typeof getIntegrationConnectionBySlug;\n\nexport interface UpdateIntegrationConnectionLifecycleStatusParams {\n id: string;\n lifecycleStatus: IntegrationConnectionLifecycleStatus;\n}\n\nexport async function updateIntegrationConnectionLifecycleStatus(\n params: UpdateIntegrationConnectionLifecycleStatusParams,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<IntegrationConnection | undefined> {\n const executor = options.tx ?? db();\n const [row] = await executor\n .update(integrationConnections)\n .set({lifecycleStatus: params.lifecycleStatus, updatedAt: new Date()})\n .where(eq(integrationConnections.id, params.id))\n .returning();\n if (!row) return undefined;\n return toIntegrationConnection(row);\n}\n\nexport type UpdateIntegrationConnectionLifecycleStatusFn =\n typeof updateIntegrationConnectionLifecycleStatus;\n\nexport async function deleteIntegrationConnection(\n params: {id: string},\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<boolean> {\n const executor = options.tx ?? db();\n const result = await executor\n .delete(integrationConnections)\n .where(eq(integrationConnections.id, params.id));\n return (result.rowCount ?? 0) > 0;\n}\n\nexport type DeleteIntegrationConnectionFn = typeof deleteIntegrationConnection;\n\nexport interface ListIntegrationConnectionsParams {\n workspaceId: string;\n}\n\nexport async function listIntegrationConnections(\n params: ListIntegrationConnectionsParams,\n): Promise<IntegrationConnection[]> {\n const rows = await db()\n .select()\n .from(integrationConnections)\n .where(eq(integrationConnections.workspaceId, params.workspaceId))\n .orderBy(integrationConnections.createdAt, integrationConnections.id);\n\n const connections = rows.map(toIntegrationConnection);\n return connections;\n}\n\nexport interface ListIntegrationConnectionsByProviderParams {\n provider: IntegrationProviderKind;\n}\n\nexport async function listIntegrationConnectionsByProvider(\n params: ListIntegrationConnectionsByProviderParams,\n): Promise<IntegrationConnection[]> {\n const rows = await db()\n .select()\n .from(integrationConnections)\n .where(eq(integrationConnections.provider, params.provider))\n .orderBy(integrationConnections.createdAt, integrationConnections.id);\n\n return rows.map(toIntegrationConnection);\n}\n"],"names":["CONNECTION_SLUG_MAX_LENGTH","ConnectionSlugConflictError","and","eq","IntegrationConnectionAlreadyExistsError","db","integrationConnections","toIntegrationConnection","upsertIntegrationConnection","params","options","executor","tx","now","Date","row","insert","values","workspaceId","provider","externalAccountId","slug","displayName","lifecycleStatus","onConflictDoUpdate","target","set","updatedAt","returning","Error","INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT","INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT","isIntegrationConnectionUniqueViolation","error","current","depth","code","constraint","cause","isIntegrationConnectionSlugUniqueViolation","createIntegrationConnection","rows","resolveUniqueConnectionSlug","existing","select","from","where","limit","workspaceSlugs","used","Set","map","suffixNumber","suffix","baseBudget","length","candidate","baseSlug","slice","replaceAll","has","getIntegrationConnectionById","id","undefined","getIntegrationConnectionBySlug","updateIntegrationConnectionLifecycleStatus","update","deleteIntegrationConnection","result","delete","rowCount","listIntegrationConnections","orderBy","createdAt","connections","listIntegrationConnectionsByProvider"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/connections.ts"],"sourcesContent":["import {\n CONNECTION_SLUG_MAX_LENGTH,\n INTEGRATION_CONNECTION_AVAILABLE,\n type IntegrationsEventMap,\n} from '@shipfox/api-integration-core-dto';\nimport {ConnectionSlugConflictError} from '@shipfox/api-integration-spi';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, eq} from 'drizzle-orm';\nimport type {\n IntegrationConnection,\n IntegrationConnectionLifecycleStatus,\n} from '#core/entities/connection.js';\nimport type {IntegrationProviderKind} from '#core/entities/provider.js';\nimport {IntegrationConnectionAlreadyExistsError} from '#core/errors.js';\nimport {db} from './db.js';\nimport {integrationConnections, toIntegrationConnection} from './schema/connections.js';\nimport {integrationsOutbox} from './schema/outbox.js';\n\ntype IntegrationDb = ReturnType<typeof db>;\ntype IntegrationTx = Parameters<Parameters<IntegrationDb['transaction']>[0]>[0];\n\nexport interface UpsertIntegrationConnectionParams {\n workspaceId: string;\n provider: IntegrationProviderKind;\n externalAccountId: string;\n slug: string;\n displayName: string;\n lifecycleStatus?: IntegrationConnectionLifecycleStatus | undefined;\n}\n\nexport async function upsertIntegrationConnection(\n params: UpsertIntegrationConnectionParams,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<IntegrationConnection> {\n if (options.tx === undefined) {\n return await db().transaction((tx) => upsertIntegrationConnection(params, {tx}));\n }\n\n const executor = options.tx;\n const now = new Date();\n let [row] = await executor\n .insert(integrationConnections)\n .values({\n workspaceId: params.workspaceId,\n provider: params.provider,\n externalAccountId: params.externalAccountId,\n slug: params.slug,\n displayName: params.displayName,\n lifecycleStatus: params.lifecycleStatus ?? 'active',\n })\n .onConflictDoNothing({\n target: [\n integrationConnections.workspaceId,\n integrationConnections.provider,\n integrationConnections.externalAccountId,\n ],\n })\n .returning();\n\n let becameAvailable = row?.lifecycleStatus === 'active';\n if (!row) {\n const [existing] = await executor\n .select({\n id: integrationConnections.id,\n lifecycleStatus: integrationConnections.lifecycleStatus,\n })\n .from(integrationConnections)\n .where(\n and(\n eq(integrationConnections.workspaceId, params.workspaceId),\n eq(integrationConnections.provider, params.provider),\n eq(integrationConnections.externalAccountId, params.externalAccountId),\n ),\n )\n .limit(1)\n .for('update');\n if (!existing) throw new Error('Integration connection upsert conflict row was not found');\n\n [row] = await executor\n .update(integrationConnections)\n .set({\n displayName: params.displayName,\n lifecycleStatus: params.lifecycleStatus ?? 'active',\n updatedAt: now,\n })\n .where(eq(integrationConnections.id, existing.id))\n .returning();\n becameAvailable = existing.lifecycleStatus !== 'active' && row?.lifecycleStatus === 'active';\n }\n\n if (!row) throw new Error('Integration connection upsert returned no rows');\n const connection = toIntegrationConnection(row);\n if (becameAvailable) {\n await writeConnectionAvailableEvent(executor, connection);\n }\n return connection;\n}\n\nexport interface CreateIntegrationConnectionParams {\n workspaceId: string;\n provider: IntegrationProviderKind;\n externalAccountId: string;\n slug: string;\n displayName: string;\n lifecycleStatus?: IntegrationConnectionLifecycleStatus | undefined;\n}\n\nconst INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT =\n 'integrations_connections_workspace_external_unique';\nconst INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT =\n 'integrations_connections_workspace_slug_unique';\n\nexport function isIntegrationConnectionUniqueViolation(error: unknown): boolean {\n let current: unknown = error;\n for (let depth = 0; depth < 5 && current != null; depth += 1) {\n if (typeof current !== 'object') return false;\n const {code, constraint} = current as {code?: unknown; constraint?: unknown};\n if (code === '23505' && constraint === INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT) {\n return true;\n }\n current = (current as {cause?: unknown}).cause;\n }\n return false;\n}\n\nexport function isIntegrationConnectionSlugUniqueViolation(error: unknown): boolean {\n let current: unknown = error;\n for (let depth = 0; depth < 5 && current != null; depth += 1) {\n if (typeof current !== 'object') return false;\n const {code, constraint} = current as {code?: unknown; constraint?: unknown};\n if (code === '23505' && constraint === INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT) {\n return true;\n }\n current = (current as {cause?: unknown}).cause;\n }\n return false;\n}\n\nexport async function createIntegrationConnection(\n params: CreateIntegrationConnectionParams,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<IntegrationConnection> {\n if (options.tx === undefined) {\n return await db().transaction((tx) => createIntegrationConnection(params, {tx}));\n }\n\n const executor = options.tx;\n let rows: (typeof integrationConnections.$inferSelect)[];\n try {\n rows = await executor\n .insert(integrationConnections)\n .values({\n workspaceId: params.workspaceId,\n provider: params.provider,\n externalAccountId: params.externalAccountId,\n slug: params.slug,\n displayName: params.displayName,\n lifecycleStatus: params.lifecycleStatus ?? 'active',\n })\n .returning();\n } catch (error) {\n if (isIntegrationConnectionUniqueViolation(error)) {\n throw new IntegrationConnectionAlreadyExistsError(\n params.workspaceId,\n params.provider,\n params.externalAccountId,\n );\n }\n if (isIntegrationConnectionSlugUniqueViolation(error)) {\n throw new ConnectionSlugConflictError(error);\n }\n throw error;\n }\n\n const row = rows[0];\n if (!row) throw new Error('Integration connection insert returned no rows');\n const connection = toIntegrationConnection(row);\n if (connection.lifecycleStatus === 'active') {\n await writeConnectionAvailableEvent(executor, connection);\n }\n return connection;\n}\n\nexport interface ResolveUniqueConnectionSlugParams {\n workspaceId: string;\n provider: IntegrationProviderKind;\n externalAccountId: string;\n baseSlug: string;\n}\n\nexport async function resolveUniqueConnectionSlug(\n params: ResolveUniqueConnectionSlugParams,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<string> {\n const executor = options.tx ?? db();\n const [existing] = await executor\n .select({slug: integrationConnections.slug})\n .from(integrationConnections)\n .where(\n and(\n eq(integrationConnections.workspaceId, params.workspaceId),\n eq(integrationConnections.provider, params.provider),\n eq(integrationConnections.externalAccountId, params.externalAccountId),\n ),\n )\n .limit(1);\n if (existing) return existing.slug;\n\n const workspaceSlugs = await executor\n .select({slug: integrationConnections.slug})\n .from(integrationConnections)\n .where(eq(integrationConnections.workspaceId, params.workspaceId));\n const used = new Set(workspaceSlugs.map((row) => row.slug));\n\n for (let suffixNumber = 1; ; suffixNumber += 1) {\n const suffix = suffixNumber === 1 ? '' : `_${suffixNumber}`;\n const baseBudget = CONNECTION_SLUG_MAX_LENGTH - suffix.length;\n const candidate = `${params.baseSlug.slice(0, baseBudget).replaceAll(/[_-]+$/g, '')}${suffix}`;\n if (!used.has(candidate)) return candidate;\n }\n}\n\nexport type CreateIntegrationConnectionFn = typeof createIntegrationConnection;\n\nexport async function getIntegrationConnectionById(\n id: string,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<IntegrationConnection | undefined> {\n const executor = options.tx ?? db();\n const rows = await executor\n .select()\n .from(integrationConnections)\n .where(eq(integrationConnections.id, id))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toIntegrationConnection(row);\n}\n\nexport type GetIntegrationConnectionByIdFn = typeof getIntegrationConnectionById;\n\nexport interface GetIntegrationConnectionBySlugParams {\n workspaceId: string;\n slug: string;\n}\n\nexport async function getIntegrationConnectionBySlug(\n params: GetIntegrationConnectionBySlugParams,\n): Promise<IntegrationConnection | undefined> {\n const rows = await db()\n .select()\n .from(integrationConnections)\n .where(\n and(\n eq(integrationConnections.workspaceId, params.workspaceId),\n eq(integrationConnections.slug, params.slug),\n ),\n )\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toIntegrationConnection(row);\n}\n\nexport type GetIntegrationConnectionBySlugFn = typeof getIntegrationConnectionBySlug;\n\nexport interface UpdateIntegrationConnectionLifecycleStatusParams {\n id: string;\n lifecycleStatus: IntegrationConnectionLifecycleStatus;\n}\n\nexport async function updateIntegrationConnectionLifecycleStatus(\n params: UpdateIntegrationConnectionLifecycleStatusParams,\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<IntegrationConnection | undefined> {\n if (options.tx === undefined) {\n return await db().transaction((tx) => updateIntegrationConnectionLifecycleStatus(params, {tx}));\n }\n\n const executor = options.tx;\n const [existing] = await executor\n .select({lifecycleStatus: integrationConnections.lifecycleStatus})\n .from(integrationConnections)\n .where(eq(integrationConnections.id, params.id))\n .limit(1)\n .for('update');\n if (!existing) return undefined;\n\n const [row] = await executor\n .update(integrationConnections)\n .set({lifecycleStatus: params.lifecycleStatus, updatedAt: new Date()})\n .where(eq(integrationConnections.id, params.id))\n .returning();\n if (!row) return undefined;\n const connection = toIntegrationConnection(row);\n if (existing.lifecycleStatus !== 'active' && connection.lifecycleStatus === 'active') {\n await writeConnectionAvailableEvent(executor, connection);\n }\n return connection;\n}\n\nexport type UpdateIntegrationConnectionLifecycleStatusFn =\n typeof updateIntegrationConnectionLifecycleStatus;\n\nasync function writeConnectionAvailableEvent(\n executor: IntegrationDb | IntegrationTx,\n connection: IntegrationConnection,\n): Promise<void> {\n await writeOutboxEvent<IntegrationsEventMap>(executor, integrationsOutbox, {\n type: INTEGRATION_CONNECTION_AVAILABLE,\n payload: {\n provider: connection.provider,\n workspaceId: connection.workspaceId,\n connectionId: connection.id,\n slug: connection.slug,\n },\n });\n}\n\nexport async function deleteIntegrationConnection(\n params: {id: string},\n options: {tx?: IntegrationDb | IntegrationTx | undefined} = {},\n): Promise<boolean> {\n const executor = options.tx ?? db();\n const result = await executor\n .delete(integrationConnections)\n .where(eq(integrationConnections.id, params.id));\n return (result.rowCount ?? 0) > 0;\n}\n\nexport type DeleteIntegrationConnectionFn = typeof deleteIntegrationConnection;\n\nexport interface ListIntegrationConnectionsParams {\n workspaceId: string;\n}\n\nexport async function listIntegrationConnections(\n params: ListIntegrationConnectionsParams,\n): Promise<IntegrationConnection[]> {\n const rows = await db()\n .select()\n .from(integrationConnections)\n .where(eq(integrationConnections.workspaceId, params.workspaceId))\n .orderBy(integrationConnections.createdAt, integrationConnections.id);\n\n const connections = rows.map(toIntegrationConnection);\n return connections;\n}\n\nexport interface ListIntegrationConnectionsByProviderParams {\n provider: IntegrationProviderKind;\n}\n\nexport async function listIntegrationConnectionsByProvider(\n params: ListIntegrationConnectionsByProviderParams,\n): Promise<IntegrationConnection[]> {\n const rows = await db()\n .select()\n .from(integrationConnections)\n .where(eq(integrationConnections.provider, params.provider))\n .orderBy(integrationConnections.createdAt, integrationConnections.id);\n\n return rows.map(toIntegrationConnection);\n}\n"],"names":["CONNECTION_SLUG_MAX_LENGTH","INTEGRATION_CONNECTION_AVAILABLE","ConnectionSlugConflictError","writeOutboxEvent","and","eq","IntegrationConnectionAlreadyExistsError","db","integrationConnections","toIntegrationConnection","integrationsOutbox","upsertIntegrationConnection","params","options","tx","undefined","transaction","executor","now","Date","row","insert","values","workspaceId","provider","externalAccountId","slug","displayName","lifecycleStatus","onConflictDoNothing","target","returning","becameAvailable","existing","select","id","from","where","limit","for","Error","update","set","updatedAt","connection","writeConnectionAvailableEvent","INTEGRATION_CONNECTION_EXTERNAL_UNIQUE_CONSTRAINT","INTEGRATION_CONNECTION_SLUG_UNIQUE_CONSTRAINT","isIntegrationConnectionUniqueViolation","error","current","depth","code","constraint","cause","isIntegrationConnectionSlugUniqueViolation","createIntegrationConnection","rows","resolveUniqueConnectionSlug","workspaceSlugs","used","Set","map","suffixNumber","suffix","baseBudget","length","candidate","baseSlug","slice","replaceAll","has","getIntegrationConnectionById","getIntegrationConnectionBySlug","updateIntegrationConnectionLifecycleStatus","type","payload","connectionId","deleteIntegrationConnection","result","delete","rowCount","listIntegrationConnections","orderBy","createdAt","connections","listIntegrationConnectionsByProvider"],"mappings":"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"}
|