14064 extensibility add coretriggereventlistener table (#14138)

This Pr continues the extensibility journey
- adds a `core.databaseEventTrigger` table
- add a oneToMany relation between `core.serverlessFunction` and
`core.databaseEventTrigger`
- add a job `CallDatabaseEventTriggerJobsJob` triggered by
`EntityEventsToDbListener` triggering `serverlessFunction` based on the
`core.databaseEventTrigger.settings.eventName`
- add a new `trigger-queue` to carry this job
- renamed `DatabaseEventTriggerListener` into
`WorkflowDatabaseEventTriggerListener`
This commit is contained in:
martmull
2025-08-29 08:11:35 +02:00
committed by GitHub
parent b364acf5bb
commit 71d97c5398
13 changed files with 254 additions and 15 deletions
@@ -12,6 +12,7 @@ import {
import { InputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/input-schema.type';
import { CronTrigger } from 'src/engine/metadata-modules/trigger/entities/cron-trigger.entity';
import { DatabaseEventTrigger } from 'src/engine/metadata-modules/trigger/entities/database-event-trigger.entity';
const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
@@ -63,6 +64,15 @@ export class ServerlessFunctionEntity {
)
cronTriggers: CronTrigger[];
@OneToMany(
() => DatabaseEventTrigger,
(databaseEventTrigger) => databaseEventTrigger.serverlessFunction,
{
cascade: true,
},
)
databaseEventTriggers: DatabaseEventTrigger[];
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -48,7 +48,7 @@ export class CronTriggerCronJob {
where: {
workspaceId: activeWorkspace.id,
},
select: ['settings'],
select: ['id', 'settings', 'workspaceId'],
relations: ['serverlessFunction'],
});
@@ -67,7 +67,7 @@ export class CronTriggerCronJob {
ServerlessFunctionTriggerJob.name,
{
serverlessFunctionId: cronTrigger.serverlessFunction.id,
workspaceId: activeWorkspace.id,
workspaceId: cronTrigger.workspaceId,
payload: {},
},
{ retryLimit: 3 },
@@ -11,6 +11,8 @@ import {
Index,
} from 'typeorm';
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
export type CronTriggerSettings = {
@@ -19,7 +21,7 @@ export type CronTriggerSettings = {
@Entity({ name: 'cronTrigger', schema: 'core' })
@Index('IDX_CRON_TRIGGER_WORKSPACE_ID', ['workspaceId'])
export class CronTrigger {
export class CronTrigger extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -0,0 +1,50 @@
import {
Column,
CreateDateColumn,
Entity,
DeleteDateColumn,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
Relation,
Index,
} from 'typeorm';
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
export type DatabaseEventTriggerSettings = {
eventName: string;
};
@Entity({ name: 'databaseEventTrigger', schema: 'core' })
@Index('IDX_DATABASE_EVENT_TRIGGER_WORKSPACE_ID', ['workspaceId'])
export class DatabaseEventTrigger extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'jsonb' })
settings: DatabaseEventTriggerSettings;
@ManyToOne(
() => ServerlessFunctionEntity,
(serverlessFunction) => serverlessFunction.databaseEventTriggers,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'serverlessFunctionId' })
serverlessFunction: Relation<ServerlessFunctionEntity>;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date;
}
@@ -0,0 +1,80 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
import type { ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event';
import { DatabaseEventTrigger } from 'src/engine/metadata-modules/trigger/entities/database-event-trigger.entity';
import {
ServerlessFunctionTriggerJob,
ServerlessFunctionTriggerJobData,
} from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
@Processor(MessageQueue.triggerQueue)
export class CallDatabaseEventTriggerJobsJob {
constructor(
@InjectMessageQueue(MessageQueue.serverlessFunctionQueue)
private readonly messageQueueService: MessageQueueService,
@InjectRepository(DatabaseEventTrigger)
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTrigger>,
) {}
@Process(CallDatabaseEventTriggerJobsJob.name)
async handle(workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>) {
const databaseEventListeners =
await this.databaseEventTriggerRepository.find({
where: {
workspaceId: workspaceEventBatch.workspaceId,
},
select: ['id', 'settings', 'workspaceId'],
relations: ['serverlessFunction'],
});
for (const databaseEventListener of databaseEventListeners) {
if (
!this.shouldTriggerJob({
workspaceEventBatch,
eventName: databaseEventListener.settings.eventName,
})
) {
continue;
}
for (const eventData of workspaceEventBatch.events) {
await this.messageQueueService.add<ServerlessFunctionTriggerJobData>(
ServerlessFunctionTriggerJob.name,
{
serverlessFunctionId: databaseEventListener.serverlessFunction.id,
workspaceId: databaseEventListener.workspaceId,
payload: eventData,
},
{ retryLimit: 3 },
);
}
}
}
private shouldTriggerJob({
workspaceEventBatch,
eventName,
}: {
workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>;
eventName: string;
}) {
const [nameSingular, operation] = workspaceEventBatch.name.split('.');
const validEventNames = [
`${nameSingular}.${operation}`,
`*.${operation}`,
`${nameSingular}.*`,
'*.*',
];
return validEventNames.includes(eventName);
}
}
@@ -5,10 +5,18 @@ import { CronTriggerCronCommand } from 'src/engine/metadata-modules/trigger/cron
import { CronTriggerCronJob } from 'src/engine/metadata-modules/trigger/crons/jobs/cron-trigger.cron.job';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { CronTrigger } from 'src/engine/metadata-modules/trigger/entities/cron-trigger.entity';
import { CallDatabaseEventTriggerJobsJob } from 'src/engine/metadata-modules/trigger/jobs/call-database-event-trigger-jobs.job';
import { DatabaseEventTrigger } from 'src/engine/metadata-modules/trigger/entities/database-event-trigger.entity';
@Module({
imports: [TypeOrmModule.forFeature([Workspace, CronTrigger])],
providers: [CronTriggerCronJob, CronTriggerCronCommand],
imports: [
TypeOrmModule.forFeature([Workspace, CronTrigger, DatabaseEventTrigger]),
],
providers: [
CronTriggerCronJob,
CronTriggerCronCommand,
CallDatabaseEventTriggerJobsJob,
],
exports: [CronTriggerCronCommand],
})
export class TriggerModule {}