Fix subscription cross tenant issue (#16670)

As title
This commit is contained in:
martmull
2025-12-18 15:22:17 +01:00
committed by GitHub
parent c36bb8f3a1
commit 52cf3775b3
13 changed files with 178 additions and 105 deletions
@@ -1,11 +1,18 @@
import { Global, Module } from '@nestjs/common';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { WorkspaceEventEmitterService } from 'src/engine/workspace-event-emitter/workspace-event-emitter.service';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceEventEmitterResolver } from 'src/engine/workspace-event-emitter/workspace-event-emitter.resolver';
@Global()
@Module({
imports: [],
providers: [WorkspaceEventEmitter],
exports: [WorkspaceEventEmitter],
imports: [SubscriptionsModule],
providers: [
WorkspaceEventEmitter,
WorkspaceEventEmitterService,
WorkspaceEventEmitterResolver,
],
exports: [WorkspaceEventEmitter, WorkspaceEventEmitterService],
})
export class WorkspaceEventEmitterModule {}
@@ -0,0 +1,57 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Resolver, Subscription } from '@nestjs/graphql';
import { isDefined } from 'twenty-shared/utils';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { OnDbEventDTO } from 'src/engine/subscriptions/dtos/on-db-event.dto';
import { OnDbEventInput } from 'src/engine/subscriptions/dtos/on-db-event.input';
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
@Resolver()
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, NoPermissionGuard)
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
export class WorkspaceEventEmitterResolver {
constructor(private readonly subscriptionService: SubscriptionService) {}
@Subscription(() => OnDbEventDTO, {
filter: (
payload: { onDbEvent: OnDbEventDTO },
variables: { input: OnDbEventInput },
) => {
const isActionMatching =
!isDefined(variables.input.action) ||
payload.onDbEvent.action === variables.input.action;
const isObjectNameSingularMatching =
!isDefined(variables.input.objectNameSingular) ||
payload.onDbEvent.objectNameSingular ===
variables.input.objectNameSingular;
const isRecordIdMatching =
!isDefined(variables.input.recordId) ||
payload.onDbEvent.record.id === variables.input.recordId;
return (
isActionMatching && isObjectNameSingularMatching && isRecordIdMatching
);
},
})
onDbEvent(
@Args('input') _: OnDbEventInput,
@AuthWorkspace() workspace: WorkspaceEntity,
) {
return this.subscriptionService.subscribe({
channel: SubscriptionChannel.DATABASE_EVENT_CHANNEL,
workspaceId: workspace.id,
});
}
}
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { type ObjectRecordEvent } from 'twenty-shared/database-events';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { transformEventToWebhookEvent } from 'src/engine/core-modules/webhook/utils/transform-event-to-webhook-event';
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
@Injectable()
export class WorkspaceEventEmitterService {
constructor(private readonly subscriptionService: SubscriptionService) {}
async publish(
workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>,
): Promise<void> {
const [nameSingular, operation] = workspaceEventBatch.name.split('.');
for (const eventData of workspaceEventBatch.events) {
const { record, updatedFields } = transformEventToWebhookEvent({
eventName: workspaceEventBatch.name,
event: eventData,
});
await this.subscriptionService.publish({
channel: SubscriptionChannel.DATABASE_EVENT_CHANNEL,
workspaceId: workspaceEventBatch.workspaceId,
payload: {
onDbEvent: {
action: operation,
objectNameSingular: nameSingular,
eventDate: new Date(),
record,
...(updatedFields && { updatedFields }),
},
},
});
}
}
}