@@ -1 +0,0 @@
|
||||
export const ON_DB_EVENT_TRIGGER = 'onDbEvent';
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum SubscriptionChannel {
|
||||
DATABASE_EVENT_CHANNEL = 'DATABASE_EVENT_CHANNEL',
|
||||
SERVERLESS_FUNCTION_LOGS_CHANNEL = 'SERVERLESS_FUNCTION_LOGS_CHANNEL',
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
|
||||
import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service';
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionService {
|
||||
constructor(private readonly redisClient: RedisClientService) {}
|
||||
|
||||
private getSubscriptionChannel({
|
||||
channel,
|
||||
workspaceId,
|
||||
}: {
|
||||
channel: SubscriptionChannel;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
return `${channel}:${workspaceId}`;
|
||||
}
|
||||
|
||||
async subscribe({
|
||||
channel,
|
||||
workspaceId,
|
||||
}: {
|
||||
channel: SubscriptionChannel;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const client = this.redisClient.getPubSubClient();
|
||||
|
||||
return client.asyncIterator(
|
||||
this.getSubscriptionChannel({ channel, workspaceId }),
|
||||
);
|
||||
}
|
||||
|
||||
async publish<T>({
|
||||
channel,
|
||||
payload,
|
||||
workspaceId,
|
||||
}: {
|
||||
channel: SubscriptionChannel;
|
||||
payload: T;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const client = this.redisClient.getPubSubClient();
|
||||
|
||||
await client.publish(
|
||||
this.getSubscriptionChannel({ channel, workspaceId }),
|
||||
payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,11 @@
|
||||
import { Inject, Module, type OnModuleDestroy } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { RedisPubSub } from 'graphql-redis-subscriptions';
|
||||
|
||||
import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service';
|
||||
import { SubscriptionsResolver } from 'src/engine/subscriptions/subscriptions.resolver';
|
||||
import { SubscriptionsService } from 'src/engine/subscriptions/subscriptions.service';
|
||||
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
|
||||
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'PUB_SUB',
|
||||
inject: [RedisClientService],
|
||||
|
||||
useFactory: (redisClientService: RedisClientService) =>
|
||||
new RedisPubSub({
|
||||
publisher: redisClientService.getClient().duplicate(),
|
||||
subscriber: redisClientService.getClient().duplicate(),
|
||||
}),
|
||||
},
|
||||
SubscriptionsResolver,
|
||||
SubscriptionsService,
|
||||
],
|
||||
exports: ['PUB_SUB', SubscriptionsService],
|
||||
imports: [RedisClientModule],
|
||||
providers: [SubscriptionService],
|
||||
exports: [SubscriptionService],
|
||||
})
|
||||
export class SubscriptionsModule implements OnModuleDestroy {
|
||||
constructor(@Inject('PUB_SUB') private readonly pubSub: RedisPubSub) {}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.pubSub) {
|
||||
await this.pubSub.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
export class SubscriptionsModule {}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Inject, UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Resolver, Subscription } from '@nestjs/graphql';
|
||||
|
||||
import { RedisPubSub } from 'graphql-redis-subscriptions';
|
||||
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 { ON_DB_EVENT_TRIGGER } from 'src/engine/subscriptions/constants/on-db-event-trigger';
|
||||
|
||||
@Resolver()
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, NoPermissionGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
|
||||
export class SubscriptionsResolver {
|
||||
constructor(@Inject('PUB_SUB') private readonly pubSub: RedisPubSub) {}
|
||||
|
||||
@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) {
|
||||
return this.pubSub.asyncIterator(ON_DB_EVENT_TRIGGER);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { RedisPubSub } from 'graphql-redis-subscriptions';
|
||||
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 { ON_DB_EVENT_TRIGGER } from 'src/engine/subscriptions/constants/on-db-event-trigger';
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionsService {
|
||||
constructor(@Inject('PUB_SUB') private readonly pubSub: RedisPubSub) {}
|
||||
|
||||
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.pubSub.publish(ON_DB_EVENT_TRIGGER, {
|
||||
onDbEvent: {
|
||||
action: operation,
|
||||
objectNameSingular: nameSingular,
|
||||
eventDate: new Date(),
|
||||
record,
|
||||
...(updatedFields && { updatedFields }),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user