feat: add create or update workflow trigger (#14708)
### Description This PR focuses on https://github.com/twentyhq/core-team-issues/issues/1476 - introduces the handling of upserted events in the workflow trigger system ## Visual Appearance <img width="1792" height="1039" alt="Screenshot 2025-09-21 at 11 40 28 PM" src="https://github.com/user-attachments/assets/d926b566-f7d3-412b-b1ab-ed2064425f3d" /> <img width="501" height="1025" alt="Screenshot 2025-09-21 at 11 40 39 PM" src="https://github.com/user-attachments/assets/13c3031a-0762-4c80-8df1-fa74ca70a540" /> ### Changes - Added `UPSERTED` action to the `DatabaseEventAction` enum. - Updated the `WorkflowEditTriggerDatabaseEventForm` to support upserted events. --------- Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
This commit is contained in:
+1
@@ -4,4 +4,5 @@ export enum DatabaseEventAction {
|
||||
DELETED = 'deleted',
|
||||
DESTROYED = 'destroyed',
|
||||
RESTORED = 'restored',
|
||||
UPSERTED = 'upserted',
|
||||
}
|
||||
|
||||
+7
@@ -2,6 +2,7 @@ import { AuditService } from 'src/engine/core-modules/audit/services/audit.servi
|
||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-created';
|
||||
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-delete';
|
||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-updated';
|
||||
import { OBJECT_RECORD_UPSERTED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-upserted';
|
||||
import { type ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
@@ -50,6 +51,12 @@ export class CreateAuditLogFromInternalEvent {
|
||||
recordId: eventData.recordId,
|
||||
objectMetadataId: eventData.objectMetadata.id,
|
||||
});
|
||||
} else if (workspaceEventBatch.name.endsWith('.upserted')) {
|
||||
auditService.createObjectEvent(OBJECT_RECORD_UPSERTED_EVENT, {
|
||||
...eventProperties,
|
||||
recordId: eventData.recordId,
|
||||
objectMetadataId: eventData.objectMetadata.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
type OBJECT_RECORD_UPDATED_EVENT,
|
||||
type ObjectRecordUpdatedTrackEvent,
|
||||
} from 'src/engine/core-modules/audit/utils/events/object-event/object-record-updated';
|
||||
import {
|
||||
type OBJECT_RECORD_UPSERTED_EVENT,
|
||||
type ObjectRecordUpsertedTrackEvent,
|
||||
} from 'src/engine/core-modules/audit/utils/events/object-event/object-record-upserted';
|
||||
import {
|
||||
type CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||
type CustomDomainActivatedTrackEvent,
|
||||
@@ -50,6 +54,7 @@ export type TrackEventName =
|
||||
| typeof OBJECT_RECORD_CREATED_EVENT
|
||||
| typeof OBJECT_RECORD_UPDATED_EVENT
|
||||
| typeof OBJECT_RECORD_DELETED_EVENT
|
||||
| typeof OBJECT_RECORD_UPSERTED_EVENT
|
||||
| typeof USER_SIGNUP_EVENT;
|
||||
|
||||
// Map event names to their corresponding event types
|
||||
@@ -64,6 +69,7 @@ export interface TrackEvents {
|
||||
[OBJECT_RECORD_DELETED_EVENT]: ObjectRecordDeletedTrackEvent;
|
||||
[OBJECT_RECORD_CREATED_EVENT]: ObjectRecordCreatedTrackEvent;
|
||||
[OBJECT_RECORD_UPDATED_EVENT]: ObjectRecordUpdatedTrackEvent;
|
||||
[OBJECT_RECORD_UPSERTED_EVENT]: ObjectRecordUpsertedTrackEvent;
|
||||
}
|
||||
|
||||
export type TrackEventProperties<T extends TrackEventName> =
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||
|
||||
export const OBJECT_RECORD_UPSERTED_EVENT = 'Object Record Upserted' as const;
|
||||
export const objectRecordUpsertedSchema = z.object({
|
||||
event: z.literal(OBJECT_RECORD_UPSERTED_EVENT),
|
||||
properties: z.looseObject({}),
|
||||
});
|
||||
|
||||
export type ObjectRecordUpsertedTrackEvent = z.infer<
|
||||
typeof objectRecordUpsertedSchema
|
||||
>;
|
||||
|
||||
registerEvent(OBJECT_RECORD_UPSERTED_EVENT, objectRecordUpsertedSchema);
|
||||
+3
-1
@@ -3,10 +3,12 @@ import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emit
|
||||
import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { type ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event';
|
||||
|
||||
export type ObjectRecordEvent<T = object> =
|
||||
| ObjectRecordUpdateEvent<T>
|
||||
| ObjectRecordDeleteEvent<T>
|
||||
| ObjectRecordCreateEvent<T>
|
||||
| ObjectRecordDestroyEvent<T>
|
||||
| ObjectRecordRestoreEvent<T>;
|
||||
| ObjectRecordRestoreEvent<T>
|
||||
| ObjectRecordUpsertEvent<T>;
|
||||
|
||||
+3
-1
@@ -2,9 +2,11 @@ import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emit
|
||||
import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { type ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event';
|
||||
|
||||
export type ObjectRecordNonDestructiveEvent =
|
||||
| ObjectRecordCreateEvent
|
||||
| ObjectRecordUpdateEvent
|
||||
| ObjectRecordDeleteEvent
|
||||
| ObjectRecordRestoreEvent;
|
||||
| ObjectRecordRestoreEvent
|
||||
| ObjectRecordUpsertEvent;
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event';
|
||||
|
||||
export class ObjectRecordUpsertEvent<
|
||||
T = object,
|
||||
> extends ObjectRecordBaseEvent<T> {
|
||||
properties: {
|
||||
before?: T;
|
||||
after: T;
|
||||
diff?: Partial<ObjectRecordDiff<T>>;
|
||||
updatedFields?: string[];
|
||||
};
|
||||
}
|
||||
+8
@@ -168,6 +168,14 @@ export class WorkspaceInsertQueryBuilder<
|
||||
authContext: this.authContext,
|
||||
});
|
||||
|
||||
await this.internalContext.eventEmitterService.emitMutationEvent({
|
||||
action: DatabaseEventAction.UPSERTED,
|
||||
objectMetadataItem: objectMetadata,
|
||||
workspaceId: this.internalContext.workspaceId,
|
||||
entities: formattedResultForEvent,
|
||||
authContext: this.authContext,
|
||||
});
|
||||
|
||||
// TypeORM returns all entity columns for insertions
|
||||
const resultWithoutInsertionExtraColumns = !isDefined(result.raw)
|
||||
? []
|
||||
|
||||
+18
@@ -161,6 +161,15 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
authContext: this.authContext,
|
||||
});
|
||||
|
||||
await this.internalContext.eventEmitterService.emitMutationEvent({
|
||||
action: DatabaseEventAction.UPSERTED,
|
||||
objectMetadataItem: objectMetadata,
|
||||
workspaceId: this.internalContext.workspaceId,
|
||||
entities: formattedAfter,
|
||||
beforeEntities: formattedBefore,
|
||||
authContext: this.authContext,
|
||||
});
|
||||
|
||||
const formattedResult = formatResult<T[]>(
|
||||
result.raw,
|
||||
objectMetadata,
|
||||
@@ -293,6 +302,15 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
authContext: this.authContext,
|
||||
});
|
||||
|
||||
await this.internalContext.eventEmitterService.emitMutationEvent({
|
||||
action: DatabaseEventAction.UPSERTED,
|
||||
objectMetadataItem: objectMetadata,
|
||||
workspaceId: this.internalContext.workspaceId,
|
||||
entities: formattedAfter,
|
||||
beforeEntities: formattedBefore,
|
||||
authContext: this.authContext,
|
||||
});
|
||||
|
||||
const formattedResults = formatResult<T[]>(
|
||||
results.flatMap((result) => result.raw),
|
||||
objectMetadata,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { type 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 { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event';
|
||||
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type CustomEventName } from 'src/engine/workspace-event-emitter/types/custom-event-name.type';
|
||||
@@ -24,6 +25,7 @@ type ActionEventMap<T> = {
|
||||
[DatabaseEventAction.DELETED]: ObjectRecordDeleteEvent<T>;
|
||||
[DatabaseEventAction.DESTROYED]: ObjectRecordDestroyEvent<T>;
|
||||
[DatabaseEventAction.RESTORED]: ObjectRecordRestoreEvent<T>;
|
||||
[DatabaseEventAction.UPSERTED]: ObjectRecordUpsertEvent<T>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -62,6 +64,7 @@ export class WorkspaceEventEmitter {
|
||||
| ObjectRecordCreateEvent<T>
|
||||
| ObjectRecordUpdateEvent<T>
|
||||
| ObjectRecordDeleteEvent<T>
|
||||
| ObjectRecordUpsertEvent<T>
|
||||
)[] = [];
|
||||
|
||||
switch (action) {
|
||||
@@ -140,6 +143,41 @@ export class WorkspaceEventEmitter {
|
||||
return event;
|
||||
});
|
||||
break;
|
||||
case DatabaseEventAction.UPSERTED:
|
||||
events = entityArray.map((after, index) => {
|
||||
const event = new ObjectRecordUpsertEvent<T>();
|
||||
|
||||
event.userId = authContext?.user?.id;
|
||||
event.recordId = after.id;
|
||||
event.objectMetadata = { ...objectMetadataItem, fields };
|
||||
|
||||
const before = beforeEntities
|
||||
? Array.isArray(beforeEntities)
|
||||
? beforeEntities[index]
|
||||
: beforeEntities
|
||||
: undefined;
|
||||
|
||||
let updatedFields;
|
||||
let diff;
|
||||
|
||||
diff = objectRecordChangedValues(
|
||||
before ?? {},
|
||||
after,
|
||||
objectMetadataItem,
|
||||
) as Partial<ObjectRecordDiff<T>>;
|
||||
|
||||
updatedFields = Object.keys(diff);
|
||||
|
||||
event.properties = {
|
||||
after,
|
||||
...(before && { before }),
|
||||
...(diff && { diff }),
|
||||
...(updatedFields && { updatedFields }),
|
||||
};
|
||||
|
||||
return event;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user