Add db event trigger and cron trigger to migration v2 (#14772)

## Context
Add DB events triggers and Cron triggers to migration v2 builder/runner
(and adding corresponding services/resolvers)
This commit is contained in:
Weiko
2025-09-30 11:14:18 +02:00
committed by GitHub
parent 123202df41
commit 68b21d2942
82 changed files with 3340 additions and 57 deletions
@@ -0,0 +1,5 @@
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
export const FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES = [
'settings',
] as const satisfies (keyof FlatDatabaseEventTrigger)[];
@@ -0,0 +1,6 @@
import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/database-event-trigger/constants/flat-database-event-trigger-editable-properties.constant';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
export const FLAT_DATABASE_EVENT_TRIGGER_PROPERTIES_TO_COMPARE = [
...FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES,
] as const satisfies (keyof FlatDatabaseEventTrigger)[];
@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { CallDatabaseEventTriggerJobsJob } from 'src/engine/metadata-modules/database-event-trigger/jobs/call-database-event-trigger-jobs.job';
import { DatabaseEventTriggerResolver } from 'src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver';
import { DatabaseEventTriggerV2Service } from 'src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service';
import { WorkspaceFlatDatabaseEventTriggerMapCacheService } from 'src/engine/metadata-modules/database-event-trigger/services/workspace-flat-database-event-trigger-map-cache.service';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, DatabaseEventTrigger]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
],
providers: [
CallDatabaseEventTriggerJobsJob,
DatabaseEventTriggerV2Service,
DatabaseEventTriggerResolver,
WorkspaceFlatDatabaseEventTriggerMapCacheService,
],
exports: [DatabaseEventTriggerV2Service],
})
export class DatabaseEventTriggerModule {}
@@ -0,0 +1,19 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { DatabaseEventTriggerSettings } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
@InputType()
export class CreateDatabaseEventTriggerInput {
@IsObject()
@IsNotEmpty()
@Field(() => GraphQLJSON)
settings: DatabaseEventTriggerSettings;
@IsUUID()
@IsNotEmpty()
@Field()
serverlessFunctionId: string;
}
@@ -0,0 +1,11 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsUUID } from 'class-validator';
@InputType()
export class DatabaseEventTriggerIdInput {
@IsUUID()
@IsNotEmpty()
@Field()
id: string;
}
@@ -0,0 +1,35 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { IsDateString, IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { DatabaseEventTriggerSettings } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
@ObjectType('DatabaseEventTrigger')
export class DatabaseEventTriggerDTO {
@IsUUID()
@IsNotEmpty()
@IDField(() => UUIDScalarType)
id: string;
@IsObject()
@Field(() => GraphQLJSON)
settings: DatabaseEventTriggerSettings;
@HideField()
workspaceId: string;
@IsUUID()
@Field()
serverlessFunctionId: string;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -0,0 +1,19 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { DatabaseEventTriggerSettings } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
@InputType()
export class UpdateDatabaseEventTriggerInput {
@IsUUID()
@IsNotEmpty()
@Field()
id: string;
@IsObject()
@IsNotEmpty()
@Field(() => GraphQLJSON)
settings: DatabaseEventTriggerSettings;
}
@@ -0,0 +1,57 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface';
import { DatabaseEventTriggerEntityRelationProperties } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
export type DatabaseEventTriggerSettings = {
eventName: string;
};
export const DATABASE_EVENT_TRIGGER_ENTITY_RELATION_PROPERTIES = [
'serverlessFunction',
] as const satisfies readonly DatabaseEventTriggerEntityRelationProperties[];
@Entity('databaseEventTrigger')
@Index('IDX_DATABASE_EVENT_TRIGGER_WORKSPACE_ID', ['workspaceId'])
export class DatabaseEventTrigger
extends SyncableEntity
implements Required<DatabaseEventTrigger>
{
@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: true, type: 'uuid' })
serverlessFunctionId: string | null;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -0,0 +1,12 @@
import { CustomException } from 'src/utils/custom-exception';
export class DatabaseEventTriggerException extends CustomException {
constructor(message: string, code: DatabaseEventTriggerExceptionCode) {
super(message, code);
}
}
export enum DatabaseEventTriggerExceptionCode {
DATABASE_EVENT_TRIGGER_NOT_FOUND = 'DATABASE_EVENT_TRIGGER_NOT_FOUND',
DATABASE_EVENT_TRIGGER_ALREADY_EXIST = 'DATABASE_EVENT_TRIGGER_ALREADY_EXIST',
}
@@ -0,0 +1,80 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import type { ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { DatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import {
ServerlessFunctionTriggerJob,
ServerlessFunctionTriggerJobData,
} from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
@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);
}
}
@@ -0,0 +1,107 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/create-database-event-trigger.input';
import { DatabaseEventTriggerIdInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger-id.input';
import { DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto';
import { UpdateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/update-database-event-trigger.input';
import { DatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { DatabaseEventTriggerV2Service } from 'src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service';
import { databaseEventTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
export class DatabaseEventTriggerResolver {
constructor(
private readonly databaseEventTriggerV2Service: DatabaseEventTriggerV2Service,
@InjectRepository(DatabaseEventTrigger)
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTrigger>,
) {}
@Query(() => DatabaseEventTriggerDTO)
async findOneDatabaseEventTrigger(
@Args('input') { id }: DatabaseEventTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.databaseEventTriggerRepository.findOneOrFail({
where: {
id,
workspaceId,
},
});
} catch (error) {
databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
@Query(() => [DatabaseEventTriggerDTO])
async findManyDatabaseEventTriggers(
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.databaseEventTriggerRepository.find({
where: { workspaceId },
});
} catch (error) {
databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => DatabaseEventTriggerDTO)
async deleteOneDatabaseEventTrigger(
@Args('input') input: DatabaseEventTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.databaseEventTriggerV2Service.destroyOne({
destroyDatabaseEventTriggerInput: input,
workspaceId,
});
} catch (error) {
databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => DatabaseEventTriggerDTO)
async updateOneDatabaseEventTrigger(
@Args('input')
input: UpdateDatabaseEventTriggerInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.databaseEventTriggerV2Service.updateOne(
input,
workspaceId,
);
} catch (error) {
databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => DatabaseEventTriggerDTO)
async createOneDatabaseEventTrigger(
@Args('input')
input: CreateDatabaseEventTriggerInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.databaseEventTriggerV2Service.createOne(
input,
workspaceId,
);
} catch (error) {
databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
}
@@ -0,0 +1,239 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service.';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { CreateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/create-database-event-trigger.input';
import { DatabaseEventTriggerIdInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger-id.input';
import { UpdateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/update-database-event-trigger.input';
import {
DatabaseEventTriggerException,
DatabaseEventTriggerExceptionCode,
} from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception';
import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/utils/from-create-database-event-trigger-input-to-flat-database-event-trigger.util';
import { fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/database-event-trigger/utils/from-update-database-event-trigger-input-to-flat-database-event-trigger-to-update-or-throw.util';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class DatabaseEventTriggerV2Service {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {}
async createOne(
databaseEventTriggerInput: CreateDatabaseEventTriggerInput,
workspaceId: string,
) {
const flatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatDatabaseEventTriggerMaps'],
},
);
const existingFlatDatabaseEventTriggerMaps =
flatEntityMaps.flatDatabaseEventTriggerMaps;
const flatDatabaseEventTriggerToCreate =
fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger({
createDatabaseEventTriggerInput: databaseEventTriggerInput,
workspaceId,
});
const toFlatDatabaseEventTriggerMaps = addFlatEntityToFlatEntityMapsOrThrow(
{
flatEntity: flatDatabaseEventTriggerToCreate,
flatEntityMaps: existingFlatDatabaseEventTriggerMaps,
},
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
workspaceId,
fromToAllFlatEntityMaps: {
flatDatabaseEventTriggerMaps: {
from: existingFlatDatabaseEventTriggerMaps,
to: toFlatDatabaseEventTriggerMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating database event trigger',
);
}
const {
flatDatabaseEventTriggerMaps:
recomputedExistingFlatDatabaseEventTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatDatabaseEventTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatDatabaseEventTriggerToCreate.id,
flatEntityMaps: recomputedExistingFlatDatabaseEventTriggerMaps,
});
}
async updateOne(
databaseEventTriggerInput: UpdateDatabaseEventTriggerInput,
workspaceId: string,
) {
const flatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatDatabaseEventTriggerMaps'],
},
);
const existingFlatDatabaseEventTriggerMaps =
flatEntityMaps.flatDatabaseEventTriggerMaps;
const optimisticallyUpdatedFlatDatabaseEventTrigger =
fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpdateOrThrow(
{
flatDatabaseEventTriggerMaps: existingFlatDatabaseEventTriggerMaps,
updateDatabaseEventTriggerInput: databaseEventTriggerInput,
},
);
const fromFlatDatabaseEventTriggerMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [optimisticallyUpdatedFlatDatabaseEventTrigger.id],
flatEntityMaps: existingFlatDatabaseEventTriggerMaps,
});
const toFlatDatabaseEventTriggerMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatDatabaseEventTrigger,
flatEntityMaps: fromFlatDatabaseEventTriggerMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
workspaceId,
fromToAllFlatEntityMaps: {
flatDatabaseEventTriggerMaps: {
from: existingFlatDatabaseEventTriggerMaps,
to: toFlatDatabaseEventTriggerMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating database event trigger',
);
}
const {
flatDatabaseEventTriggerMaps:
recomputedExistingFlatDatabaseEventTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatDatabaseEventTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatDatabaseEventTrigger.id,
flatEntityMaps: recomputedExistingFlatDatabaseEventTriggerMaps,
});
}
async destroyOne({
destroyDatabaseEventTriggerInput,
workspaceId,
}: {
destroyDatabaseEventTriggerInput: DatabaseEventTriggerIdInput;
workspaceId: string;
}): Promise<FlatDatabaseEventTrigger> {
const {
flatDatabaseEventTriggerMaps: existingFlatDatabaseEventTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatDatabaseEventTriggerMaps'],
},
);
const existingFlatDatabaseEventTrigger =
existingFlatDatabaseEventTriggerMaps.byId[
destroyDatabaseEventTriggerInput.id
];
if (!isDefined(existingFlatDatabaseEventTrigger)) {
throw new DatabaseEventTriggerException(
'Database event trigger to destroy not found',
DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND,
);
}
const fromFlatDatabaseEventTriggerMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [existingFlatDatabaseEventTrigger.id],
flatEntityMaps: existingFlatDatabaseEventTriggerMaps,
});
const toFlatDatabaseEventTriggerMaps =
deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: fromFlatDatabaseEventTriggerMaps,
entityToDeleteId: existingFlatDatabaseEventTrigger.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatDatabaseEventTriggerMaps: {
from: fromFlatDatabaseEventTriggerMaps,
to: toFlatDatabaseEventTriggerMaps,
},
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while destroying database event trigger',
);
}
return existingFlatDatabaseEventTrigger;
}
}
@@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import {
DATABASE_EVENT_TRIGGER_ENTITY_RELATION_PROPERTIES,
DatabaseEventTrigger,
} from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
@Injectable()
@WorkspaceFlatMapCache('flatDatabaseEventTriggerMaps')
export class WorkspaceFlatDatabaseEventTriggerMapCacheService extends WorkspaceFlatMapCacheService<
FlatEntityMaps<FlatDatabaseEventTrigger>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(DatabaseEventTrigger)
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTrigger>,
) {
super(cacheStorageService);
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatDatabaseEventTrigger>> {
const databaseEventTriggers =
await this.databaseEventTriggerRepository.find({
where: {
workspaceId,
},
});
const flatDatabaseEventTriggerMaps = databaseEventTriggers.reduce<
FlatEntityMaps<FlatDatabaseEventTrigger>
>((flatEntityMaps, databaseEventTrigger) => {
const flatDatabaseEventTrigger = {
...removePropertiesFromRecord(databaseEventTrigger, [
...DATABASE_EVENT_TRIGGER_ENTITY_RELATION_PROPERTIES,
]),
universalIdentifier: databaseEventTrigger.universalIdentifier ?? '',
} satisfies FlatDatabaseEventTrigger;
return {
byId: {
...flatEntityMaps.byId,
[flatDatabaseEventTrigger.id]: flatDatabaseEventTrigger,
},
idByUniversalIdentifier: {
...flatEntityMaps.idByUniversalIdentifier,
[flatDatabaseEventTrigger.universalIdentifier]:
flatDatabaseEventTrigger.id,
},
};
}, EMPTY_FLAT_ENTITY_MAPS);
return flatDatabaseEventTriggerMaps;
}
}
@@ -0,0 +1,4 @@
import { type FLAT_DATABASE_EVENT_TRIGGER_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/database-event-trigger/constants/flat-database-event-trigger-properties-to-compare.constant';
export type FlatDatabaseEventTriggerPropertiesToCompare =
(typeof FLAT_DATABASE_EVENT_TRIGGER_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,17 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type DatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type DatabaseEventTriggerEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
DatabaseEventTrigger,
ServerlessFunctionEntity | Workspace
>;
export type FlatDatabaseEventTrigger = Omit<
DatabaseEventTrigger,
DatabaseEventTriggerEntityRelationProperties
> & {
universalIdentifier: string;
};
@@ -0,0 +1,69 @@
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { parseJson } from 'twenty-shared/utils';
import { FLAT_DATABASE_EVENT_TRIGGER_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/database-event-trigger/constants/flat-database-event-trigger-properties-to-compare.constant';
import { type DatabaseEventTriggerSettings } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { type FlatDatabaseEventTriggerPropertiesToCompare } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger-properties-to-compare.type';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { type UpdateDatabaseEventTriggerAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-database-event-trigger-action-v2.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type GetWorkspaceMigrationUpdateDatabaseEventTriggerActionArgs = FromTo<
FlatDatabaseEventTrigger,
'FlatDatabaseEventTrigger'
>;
export const compareTwoFlatDatabaseEventTrigger = ({
fromFlatDatabaseEventTrigger,
toFlatDatabaseEventTrigger,
}: GetWorkspaceMigrationUpdateDatabaseEventTriggerActionArgs) => {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (property: string) =>
!FLAT_DATABASE_EVENT_TRIGGER_PROPERTIES_TO_COMPARE.includes(
property as FlatDatabaseEventTriggerPropertiesToCompare,
),
propertiesToStringify: ['settings'] as const,
};
const fromCompare = transformMetadataForComparison(
fromFlatDatabaseEventTrigger,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
toFlatDatabaseEventTrigger,
transformMetadataForComparisonParameters,
);
const flatDatabaseEventTriggerDifferences = diff(fromCompare, toCompare);
return flatDatabaseEventTriggerDifferences.flatMap<
UpdateDatabaseEventTriggerAction['updates'][number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatDatabaseEventTriggerPropertiesToCompare;
if (property === 'settings') {
return {
from: parseJson(oldValue) as DatabaseEventTriggerSettings,
to: parseJson(value) as DatabaseEventTriggerSettings,
property,
};
}
return {
from: oldValue,
to: value,
property,
};
}
case 'CREATE':
case 'REMOVE':
default: {
// Should never occurs, we should only provide null never undefined and so on
return [];
}
}
});
};
@@ -0,0 +1,25 @@
import {
ConflictError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
DatabaseEventTriggerException,
DatabaseEventTriggerExceptionCode,
} from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception';
export const databaseEventTriggerGraphQLApiExceptionHandler = (
error: Error,
): void => {
if (error instanceof DatabaseEventTriggerException) {
switch (error.code) {
case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND:
throw new NotFoundError('Database event trigger not found');
case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_ALREADY_EXIST:
throw new ConflictError('Database event trigger already exists');
default:
throw error;
}
}
throw error;
};
@@ -0,0 +1,24 @@
import { v4 as uuidV4 } from 'uuid';
import { type CreateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/create-database-event-trigger.input';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
export const fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger = ({
createDatabaseEventTriggerInput,
workspaceId,
}: {
createDatabaseEventTriggerInput: CreateDatabaseEventTriggerInput;
workspaceId: string;
}): FlatDatabaseEventTrigger => {
const now = new Date();
return {
id: uuidV4(),
universalIdentifier: uuidV4(),
settings: createDatabaseEventTriggerInput.settings,
serverlessFunctionId: createDatabaseEventTriggerInput.serverlessFunctionId,
workspaceId,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,32 @@
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type UpdateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/update-database-event-trigger.input';
import {
DatabaseEventTriggerException,
DatabaseEventTriggerExceptionCode,
} from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
export const fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpdateOrThrow =
({
flatDatabaseEventTriggerMaps,
updateDatabaseEventTriggerInput,
}: {
flatDatabaseEventTriggerMaps: FlatEntityMaps<FlatDatabaseEventTrigger>;
updateDatabaseEventTriggerInput: UpdateDatabaseEventTriggerInput;
}): FlatDatabaseEventTrigger => {
const existingFlatDatabaseEventTrigger =
flatDatabaseEventTriggerMaps.byId[updateDatabaseEventTriggerInput.id];
if (!existingFlatDatabaseEventTrigger) {
throw new DatabaseEventTriggerException(
'Database event trigger not found',
DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND,
);
}
return {
...existingFlatDatabaseEventTrigger,
settings: updateDatabaseEventTriggerInput.settings,
updatedAt: new Date(),
};
};