Files
twenty_crm/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts
Paul Rastoin 9d61337396 Fix event emitter and viewGroup (#13340)
Emit an even with an [undefined] entity
2025-07-22 11:52:11 +00:00

186 lines
5.7 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ObjectLiteral } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
import { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
import { ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { CustomEventName } from 'src/engine/workspace-event-emitter/types/custom-event-name.type';
type ActionEventMap<T> = {
[DatabaseEventAction.CREATED]: ObjectRecordCreateEvent<T>;
[DatabaseEventAction.UPDATED]: ObjectRecordUpdateEvent<T>;
[DatabaseEventAction.DELETED]: ObjectRecordDeleteEvent<T>;
[DatabaseEventAction.DESTROYED]: ObjectRecordDestroyEvent<T>;
[DatabaseEventAction.RESTORED]: ObjectRecordRestoreEvent<T>;
};
@Injectable()
export class WorkspaceEventEmitter {
constructor(private readonly eventEmitter: EventEmitter2) {}
async emitMutationEvent<T extends ObjectLiteral>({
action,
objectMetadataItem,
workspaceId,
authContext,
entities,
beforeEntities,
}: {
action: DatabaseEventAction;
objectMetadataItem: ObjectMetadataItemWithFieldMaps;
workspaceId: string;
authContext?: AuthContext;
entities: T | T[];
beforeEntities?: T | T[];
}) {
const objectMetadataNameSingular = objectMetadataItem.nameSingular;
const fields = Object.values(objectMetadataItem.fieldsById ?? {});
const entityArray = isDefined(entities)
? Array.isArray(entities)
? entities
: [entities]
: [];
let events: (
| ObjectRecordCreateEvent<T>
| ObjectRecordUpdateEvent<T>
| ObjectRecordDeleteEvent<T>
)[] = [];
switch (action) {
case DatabaseEventAction.CREATED:
events = entityArray.map((after) => {
const event = new ObjectRecordCreateEvent<T>();
event.userId = authContext?.user?.id;
event.recordId = after.id;
event.objectMetadata = { ...objectMetadataItem, fields };
event.properties = { after };
return event;
});
break;
case DatabaseEventAction.UPDATED:
events = entityArray.map((after, idx) => {
if (!beforeEntities) {
throw new Error('beforeEntities is required for UPDATED action');
}
const before = Array.isArray(beforeEntities)
? beforeEntities?.[idx]
: beforeEntities;
const diff = objectRecordChangedValues(
before,
after,
objectMetadataItem,
) as Partial<ObjectRecordDiff<T>>;
const updatedFields = Object.keys(diff);
const event = new ObjectRecordUpdateEvent<T>();
event.userId = authContext?.user?.id;
event.recordId = after.id;
event.objectMetadata = { ...objectMetadataItem, fields };
event.properties = {
before,
after,
updatedFields,
diff,
};
return event;
});
break;
case DatabaseEventAction.DELETED:
events = entityArray.map((before) => {
const event = new ObjectRecordDeleteEvent<T>();
event.userId = authContext?.user?.id;
event.recordId = before.id;
event.objectMetadata = { ...objectMetadataItem, fields };
event.properties = { before };
return event;
});
break;
case DatabaseEventAction.DESTROYED:
events = entityArray.map((before) => {
const event = new ObjectRecordDestroyEvent<T>();
event.userId = authContext?.user?.id;
event.recordId = before.id;
event.objectMetadata = { ...objectMetadataItem, fields };
event.properties = { before };
return event;
});
break;
default:
return;
}
if (!events.length) {
return;
}
const eventName = `${objectMetadataNameSingular}.${action}`;
this.eventEmitter.emit(eventName, {
name: eventName,
workspaceId,
events,
});
}
public emitDatabaseBatchEvent<T, A extends keyof ActionEventMap<T>>({
objectMetadataNameSingular,
action,
events,
workspaceId,
}: {
objectMetadataNameSingular: string;
action: A;
events: ActionEventMap<T>[A][];
workspaceId: string | undefined;
}) {
if (!events.length) {
return;
}
const eventName = `${objectMetadataNameSingular}.${action}`;
this.eventEmitter.emit(eventName, {
name: eventName,
workspaceId,
events,
});
}
public emitCustomBatchEvent<T extends object>(
eventName: CustomEventName,
events: T[],
workspaceId: string | undefined,
) {
if (!events.length) {
return;
}
this.eventEmitter.emit(eventName, {
name: eventName,
workspaceId,
events,
});
}
}