feat(connections): add an onDisconnect lifecycle hook to connection providers (#23538)
Platform half of the follow-up to https://github.com/twentyhq/twenty/pull/22984#discussion_r3673946334. The Slack app claims a `team_id` on connect and had no way to release it, because connection providers only had an on-connect hook. Nothing here is Slack-specific, so it targets `main`. The app side is #23540, on top of `feat/slack-bot`, and waits on this plus an SDK release. ## What changes `defineConnectionProvider` accepts `onDisconnectLogicFunction` alongside `onConnectLogicFunction`. It is stored on `connectionProvider.onDisconnectLogicFunctionUniversalIdentifier` (fast instance command `2.26.0_...1785350000000`) and enqueued right after the `ConnectedAccount` row is deleted, in the disconnecting workspace, with the same payload as on-connect: ```ts type OnDisconnectPayload = { connectionProviderId: string; connectionProviderName: string; connectedAccountId: string; }; ``` The `ConnectedAccount` is gone by the time the hook runs, so `getConnection` no longer resolves. Anything the cleanup needs has to be in the key-value store, written at connect time and keyed by `connectedAccountId`. The docs section spells that out, along with the fact that uninstalling an app drops its connections through a cascade that never reaches this hook, where `uninstallLogicFunction` is the right tool instead. Both dispatches moved into a new `ConnectionProviderLifecycleHookService`, so `ConnectionProviderOAuthFlowService` no longer owns hook plumbing and `ConnectedAccountMetadataService.delete` can reuse it. On-connect behaviour is unchanged: best effort, never blocks the caller, failures go to Sentry. ## Tests - `connection-provider-lifecycle-hook.service.spec.ts`: the on-connect cases moved over, plus on-disconnect dispatch, no-hook, and missing-provider cases - `connection-provider-oauth-flow.service.spec.ts`: now asserts delegation to the lifecycle hook service - SDK validation, manifest duplicate-identifier, and manifest to flat converter specs extended Server unit tests and typecheck for shared, sdk and server pass locally.
This commit is contained in:
+2
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ConnectionProviderModule } from 'src/engine/core-modules/application/connection-provider/connection-provider.module';
|
||||
import { AppOAuthRefreshModule } from 'src/engine/core-modules/application/connection-provider/refresh/app-oauth-refresh.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
@@ -21,6 +22,7 @@ import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/
|
||||
MessageChannelEntity,
|
||||
]),
|
||||
AppOAuthRefreshModule,
|
||||
ConnectionProviderModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
WorkspaceEventEmitterModule,
|
||||
|
||||
+12
@@ -3,6 +3,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { In, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ConnectionProviderLifecycleHookService } from 'src/engine/core-modules/application/connection-provider/connection-provider-lifecycle-hook.service';
|
||||
import { AppOAuthRevokeService } from 'src/engine/core-modules/application/connection-provider/refresh/services/app-oauth-revoke.service';
|
||||
import { CALENDAR_CHANNEL_DELETED_EVENT } from 'src/engine/metadata-modules/calendar-channel/constants/calendar-channel-deleted.constant';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
@@ -31,6 +34,7 @@ export class ConnectedAccountMetadataService {
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
|
||||
private readonly appOAuthRevokeService: AppOAuthRevokeService,
|
||||
private readonly connectionProviderLifecycleHookService: ConnectionProviderLifecycleHookService,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
) {}
|
||||
|
||||
@@ -246,6 +250,14 @@ export class ConnectedAccountMetadataService {
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
if (isDefined(connectedAccount.connectionProviderId)) {
|
||||
await this.connectionProviderLifecycleHookService.dispatchOnDisconnect({
|
||||
connectionProviderId: connectedAccount.connectionProviderId,
|
||||
workspaceId,
|
||||
connectedAccountId: id,
|
||||
});
|
||||
}
|
||||
|
||||
this.workspaceEventEmitter.emitCustomBatchEvent<MessageChannelDeletedEvent>(
|
||||
MESSAGE_CHANNEL_DELETED_EVENT,
|
||||
messageChannels.map((messageChannel) => ({
|
||||
|
||||
+1
@@ -61,6 +61,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"type",
|
||||
"oauthConfig",
|
||||
"onConnectLogicFunctionUniversalIdentifier",
|
||||
"onDisconnectLogicFunctionUniversalIdentifier",
|
||||
],
|
||||
"propertiesToStringify": [
|
||||
"oauthConfig",
|
||||
|
||||
+5
@@ -1813,6 +1813,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
onDisconnectLogicFunctionUniversalIdentifier: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
|
||||
Reference in New Issue
Block a user