Migrate cron, databaseEventTrigger, httpRoute triggers to serverless functions (#17488)

## Summary

Migrates trigger entities (`CronTriggerEntity`,
`DatabaseEventTriggerEntity`, `RouteTriggerEntity`) into
`ServerlessFunctionEntity` by storing trigger settings as JSONB columns
directly on the serverless function. This simplifies the architecture
since these relationships were effectively one-to-one.

## Changes

### Schema Changes
- Added three new nullable JSONB columns to `ServerlessFunctionEntity`:
  - `cronTriggerSettings` - stores cron pattern
- `databaseEventTriggerSettings` - stores event name and updated fields
filter
- `httpRouteTriggerSettings` - stores path, HTTP method, auth
requirements, and forwarded headers

### Core Logic Updates
- `CronTriggerCronJob` - now queries `ServerlessFunctionEntity` directly
instead of `CronTriggerEntity`
- `CallDatabaseEventTriggerJobsJob` - now queries
`ServerlessFunctionEntity` directly
- `RouteTriggerService` - now queries `ServerlessFunctionEntity`
directly
- `ApplicationSyncService` - extracts trigger settings from manifest and
writes to serverless function
This commit is contained in:
Charles Bochet
2026-01-27 20:54:09 +01:00
committed by GitHub
parent e1f92bd951
commit a4499d21bb
110 changed files with 439 additions and 5193 deletions
@@ -1,5 +0,0 @@
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
export const FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES = [
'settings',
] as const satisfies (keyof FlatCronTrigger)[];
@@ -1,37 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { CronTriggerCronCommand } from 'src/engine/metadata-modules/cron-trigger/crons/commands/cron-trigger.cron.command';
import { CronTriggerCronJob } from 'src/engine/metadata-modules/cron-trigger/crons/jobs/cron-trigger.cron.job';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { CronTriggerResolver } from 'src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver';
import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service';
import { WorkspaceFlatCronTriggerMapCacheService } from 'src/engine/metadata-modules/cron-trigger/services/workspace-flat-cron-trigger-map-cache.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@Module({
imports: [
TypeOrmModule.forFeature([WorkspaceEntity, CronTriggerEntity]),
ApplicationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
PermissionsModule,
WorkspaceMigrationModule,
],
providers: [
CronTriggerCronJob,
CronTriggerCronCommand,
CronTriggerResolver,
CronTriggerV2Service,
WorkspaceFlatCronTriggerMapCacheService,
],
exports: [
CronTriggerCronCommand,
CronTriggerV2Service,
WorkspaceFlatCronTriggerMapCacheService,
TypeOrmModule.forFeature([WorkspaceEntity, ServerlessFunctionEntity]),
],
providers: [CronTriggerCronJob, CronTriggerCronCommand],
exports: [CronTriggerCronCommand],
})
export class CronTriggerModule {}
@@ -2,7 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { IsNull, Not, Repository } from 'typeorm';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
@@ -11,11 +11,11 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import {
ServerlessFunctionTriggerJob,
ServerlessFunctionTriggerJobData,
} from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { shouldRunNow } from 'src/utils/should-run-now.utils';
export const CRON_TRIGGER_CRON_PATTERN = '* * * * *';
@@ -27,8 +27,8 @@ export class CronTriggerCronJob {
private readonly messageQueueService: MessageQueueService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(CronTriggerEntity)
private readonly cronTriggerRepository: Repository<CronTriggerEntity>,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
) {}
@Process(CronTriggerCronJob.name)
@@ -44,22 +44,23 @@ export class CronTriggerCronJob {
const now = new Date();
for (const activeWorkspace of activeWorkspaces) {
const cronTriggers = await this.cronTriggerRepository.find({
where: {
workspaceId: activeWorkspace.id,
},
select: ['id', 'settings', 'workspaceId'],
relations: ['serverlessFunction'],
});
const serverlessFunctionsWithCronTrigger =
await this.serverlessFunctionRepository.find({
where: {
workspaceId: activeWorkspace.id,
cronTriggerSettings: Not(IsNull()),
},
select: ['id', 'cronTriggerSettings', 'workspaceId'],
});
for (const cronTrigger of cronTriggers) {
const settings = cronTrigger.settings;
for (const serverlessFunction of serverlessFunctionsWithCronTrigger) {
const cronSettings = serverlessFunction.cronTriggerSettings;
if (!isDefined(settings.pattern)) {
if (!isDefined(cronSettings?.pattern)) {
continue;
}
if (!shouldRunNow(settings.pattern, now)) {
if (!shouldRunNow(cronSettings.pattern, now)) {
continue;
}
@@ -67,8 +68,8 @@ export class CronTriggerCronJob {
ServerlessFunctionTriggerJob.name,
[
{
serverlessFunctionId: cronTrigger.serverlessFunction.id,
workspaceId: cronTrigger.workspaceId,
serverlessFunctionId: serverlessFunction.id,
workspaceId: serverlessFunction.workspaceId,
payload: {},
},
],
@@ -1,25 +0,0 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { CronTriggerSettings } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
@InputType()
export class CreateCronTriggerInput {
@IsObject()
@IsNotEmpty()
@Field(() => GraphQLJSON)
settings: CronTriggerSettings;
@IsUUID()
@IsNotEmpty()
@Field()
serverlessFunctionId: string;
@HideField()
universalIdentifier?: string;
@HideField()
applicationId?: string;
}
@@ -1,11 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsUUID } from 'class-validator';
@InputType()
export class CronTriggerIdInput {
@IsUUID()
@IsNotEmpty()
@Field(() => String)
id: string;
}
@@ -1,28 +0,0 @@
import { Field, 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 { CronTriggerSettings } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
@ObjectType('CronTrigger')
export class CronTriggerDTO {
@IsUUID()
@IsNotEmpty()
@IDField(() => UUIDScalarType)
id: string;
@IsObject()
@Field(() => GraphQLJSON)
settings: CronTriggerSettings;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -1,32 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsObject, IsUUID, ValidateNested } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { CronTriggerSettings } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
@InputType()
class UpdateCronTriggerInputUpdates {
@IsObject()
@IsNotEmpty()
@Field(() => GraphQLJSON)
settings: CronTriggerSettings;
}
@InputType()
export class UpdateCronTriggerInput {
@IsUUID()
@IsNotEmpty()
@Field(() => String, {
description: 'The id of the cron trigger to update',
})
id: string;
@Type(() => UpdateCronTriggerInputUpdates)
@ValidateNested()
@Field(() => UpdateCronTriggerInputUpdates, {
description: 'The cron trigger updates',
})
update: UpdateCronTriggerInputUpdates;
}
@@ -1,50 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
export type CronTriggerSettings = {
pattern: string;
};
@Entity({ name: 'cronTrigger', schema: 'core' })
@Index('IDX_CRON_TRIGGER_WORKSPACE_ID', ['workspaceId'])
@Index('IDX_CRON_TRIGGER_SERVERLESS_FUNCTION_ID', ['serverlessFunctionId'])
export class CronTriggerEntity
extends SyncableEntity
implements Required<CronTriggerEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'jsonb' })
settings: JsonbProperty<CronTriggerSettings>;
@ManyToOne(
() => ServerlessFunctionEntity,
(serverlessFunction) => serverlessFunction.cronTriggers,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'serverlessFunctionId' })
serverlessFunction: Relation<ServerlessFunctionEntity>;
@Column({ nullable: false, type: 'uuid' })
serverlessFunctionId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -1,39 +0,0 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum CronTriggerExceptionCode {
CRON_TRIGGER_NOT_FOUND = 'CRON_TRIGGER_NOT_FOUND',
CRON_TRIGGER_ALREADY_EXIST = 'CRON_TRIGGER_ALREADY_EXIST',
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
}
const getCronTriggerExceptionUserFriendlyMessage = (
code: CronTriggerExceptionCode,
) => {
switch (code) {
case CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND:
return msg`Cron trigger not found.`;
case CronTriggerExceptionCode.CRON_TRIGGER_ALREADY_EXIST:
return msg`Cron trigger already exists.`;
case CronTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
return msg`Serverless function not found.`;
default:
assertUnreachable(code);
}
};
export class CronTriggerException extends CustomException<CronTriggerExceptionCode> {
constructor(
message: string,
code: CronTriggerExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ?? getCronTriggerExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,145 +0,0 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/create-cron-trigger.input';
import { CronTriggerIdInput } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger-id.input';
import { CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto';
import { UpdateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/update-cron-trigger.input';
import {
CronTriggerException,
CronTriggerExceptionCode,
} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception';
import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service';
import { cronTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util';
import { fromFlatCronTriggerToCronTriggerDto } from 'src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.WORKFLOWS),
)
@Resolver()
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
export class CronTriggerResolver {
constructor(
private readonly cronTriggerV2Service: CronTriggerV2Service,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@Query(() => CronTriggerDTO)
async findOneCronTrigger(
@Args('input') { id }: CronTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<CronTriggerDTO> {
try {
const { flatCronTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCronTriggerMaps'],
},
);
const flatCronTrigger = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatCronTriggerMaps,
});
if (!isDefined(flatCronTrigger)) {
throw new CronTriggerException(
`Cron trigger with id ${id} not found`,
CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND,
);
}
return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger);
} catch (error) {
return cronTriggerGraphQLApiExceptionHandler(error);
}
}
@Query(() => [CronTriggerDTO])
async findManyCronTriggers(
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<CronTriggerDTO[]> {
try {
const { flatCronTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCronTriggerMaps'],
},
);
return Object.values(flatCronTriggerMaps.byId)
.filter(isDefined)
.map(fromFlatCronTriggerToCronTriggerDto);
} catch (error) {
return cronTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => CronTriggerDTO)
async deleteOneCronTrigger(
@Args('input') input: CronTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<CronTriggerDTO> {
try {
const flatCronTrigger = await this.cronTriggerV2Service.destroyOne({
destroyCronTriggerInput: input,
workspaceId,
});
return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger);
} catch (error) {
return cronTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => CronTriggerDTO)
async updateOneCronTrigger(
@Args('input')
input: UpdateCronTriggerInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<CronTriggerDTO> {
try {
const flatCronTrigger = await this.cronTriggerV2Service.updateOne(
input,
workspaceId,
);
return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger);
} catch (error) {
return cronTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => CronTriggerDTO)
async createOneCronTrigger(
@Args('input')
input: CreateCronTriggerInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<CronTriggerDTO> {
try {
const flatCronTrigger = await this.cronTriggerV2Service.createOne(
input,
workspaceId,
);
return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger);
} catch (error) {
return cronTriggerGraphQLApiExceptionHandler(error);
}
}
}
@@ -1,193 +0,0 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { CreateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/create-cron-trigger.input';
import { CronTriggerIdInput } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger-id.input';
import { UpdateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/update-cron-trigger.input';
import {
CronTriggerException,
CronTriggerExceptionCode,
} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { fromCreateCronTriggerInputToFlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/utils/from-create-cron-trigger-input-to-flat-cron-trigger.util';
import { fromUpdateCronTriggerInputToFlatCronTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/cron-trigger/utils/from-update-cron-trigger-input-to-flat-cron-trigger-to-update-or-throw.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class CronTriggerV2Service {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
) {}
async createOne(
cronTriggerInput: CreateCronTriggerInput,
workspaceId: string,
/**
* @deprecated do not use call validateBuildAndRunWorkspaceMigration contextually
* when interacting with another application than workspace custom one
* */
applicationId?: string,
): Promise<FlatCronTrigger> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const flatCronTriggerToCreate = fromCreateCronTriggerInputToFlatCronTrigger(
{
createCronTriggerInput: cronTriggerInput,
workspaceId,
workspaceCustomApplicationId:
applicationId ?? workspaceCustomFlatApplication.id,
},
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
cronTrigger: {
flatEntityToCreate: [flatCronTriggerToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating cron trigger',
);
}
const { flatCronTriggerMaps: recomputedExistingFlatCronTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCronTriggerMaps', 'flatServerlessFunctionMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatCronTriggerToCreate.id,
flatEntityMaps: recomputedExistingFlatCronTriggerMaps,
});
}
async updateOne(
cronTriggerInput: UpdateCronTriggerInput,
workspaceId: string,
): Promise<FlatCronTrigger> {
const { flatCronTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCronTriggerMaps'],
},
);
const optimisticallyUpdatedFlatCronTrigger =
fromUpdateCronTriggerInputToFlatCronTriggerToUpdateOrThrow({
flatCronTriggerMaps,
updateCronTriggerInput: cronTriggerInput,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
cronTrigger: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [optimisticallyUpdatedFlatCronTrigger],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating cron trigger',
);
}
const { flatCronTriggerMaps: recomputedExistingFlatCronTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCronTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatCronTrigger.id,
flatEntityMaps: recomputedExistingFlatCronTriggerMaps,
});
}
async destroyOne({
destroyCronTriggerInput,
workspaceId,
}: {
destroyCronTriggerInput: CronTriggerIdInput;
workspaceId: string;
}): Promise<FlatCronTrigger> {
const { flatCronTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatCronTriggerMaps'],
},
);
const existingFlatCronTrigger =
flatCronTriggerMaps.byId[destroyCronTriggerInput.id];
if (!isDefined(existingFlatCronTrigger)) {
throw new CronTriggerException(
'Cron trigger to destroy not found',
CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND,
);
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
cronTrigger: {
flatEntityToCreate: [],
flatEntityToDelete: [existingFlatCronTrigger],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying cron trigger',
);
}
return existingFlatCronTrigger;
}
}
@@ -1,59 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatCronTriggerMaps')
export class WorkspaceFlatCronTriggerMapCacheService extends WorkspaceCacheProvider<
FlatEntityMaps<FlatCronTrigger>
> {
constructor(
@InjectRepository(CronTriggerEntity)
private readonly cronTriggerRepository: Repository<CronTriggerEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatCronTrigger>> {
const cronTriggers = await this.cronTriggerRepository.find({
where: {
workspaceId,
},
});
const flatCronTriggerMaps = createEmptyFlatEntityMaps();
for (const cronTriggerEntity of cronTriggers) {
const flatCronTrigger = {
...removePropertiesFromRecord(
cronTriggerEntity,
getMetadataEntityRelationProperties('cronTrigger'),
),
universalIdentifier: cronTriggerEntity.universalIdentifier,
createdAt: cronTriggerEntity.createdAt.toISOString(),
updatedAt: cronTriggerEntity.updatedAt.toISOString(),
} satisfies FlatCronTrigger;
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatCronTrigger,
flatEntityMapsToMutate: flatCronTriggerMaps,
});
}
return flatCronTriggerMaps;
}
}
@@ -1,6 +0,0 @@
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
export type FlatCronTriggerPropertiesToCompare = keyof Pick<
FlatCronTrigger,
'settings'
>;
@@ -1,4 +0,0 @@
import { type CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
export type FlatCronTrigger = FlatEntityFrom<CronTriggerEntity>;
@@ -1,23 +0,0 @@
import {
ConflictError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
CronTriggerException,
CronTriggerExceptionCode,
} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception';
export const cronTriggerGraphQLApiExceptionHandler = (error: Error): never => {
if (error instanceof CronTriggerException) {
switch (error.code) {
case CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND:
throw new NotFoundError('Cron trigger not found');
case CronTriggerExceptionCode.CRON_TRIGGER_ALREADY_EXIST:
throw new ConflictError('Cron trigger already exists');
default:
throw error;
}
}
throw error;
};
@@ -1,9 +0,0 @@
import { CronTriggerException } from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception';
export const cronTriggerGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof CronTriggerException) {
throw error;
}
throw error;
};
@@ -1,28 +0,0 @@
import { v4 } from 'uuid';
import { type CreateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/create-cron-trigger.input';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
export const fromCreateCronTriggerInputToFlatCronTrigger = ({
createCronTriggerInput,
workspaceId,
workspaceCustomApplicationId,
}: {
createCronTriggerInput: CreateCronTriggerInput;
workspaceId: string;
workspaceCustomApplicationId: string;
}): FlatCronTrigger => {
const now = new Date().toISOString();
const id = v4();
return {
id,
universalIdentifier: createCronTriggerInput.universalIdentifier ?? v4(),
settings: createCronTriggerInput.settings,
serverlessFunctionId: createCronTriggerInput.serverlessFunctionId,
applicationId: workspaceCustomApplicationId,
workspaceId,
createdAt: now,
updatedAt: now,
};
};
@@ -1,11 +0,0 @@
import { type CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
export const fromFlatCronTriggerToCronTriggerDto = (
flatCronTrigger: FlatCronTrigger,
): CronTriggerDTO => ({
id: flatCronTrigger.id,
settings: flatCronTrigger.settings,
createdAt: new Date(flatCronTrigger.createdAt),
updatedAt: new Date(flatCronTrigger.updatedAt),
});
@@ -1,31 +0,0 @@
import { type UpdateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/update-cron-trigger.input';
import {
CronTriggerException,
CronTriggerExceptionCode,
} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
export const fromUpdateCronTriggerInputToFlatCronTriggerToUpdateOrThrow = ({
flatCronTriggerMaps,
updateCronTriggerInput,
}: {
flatCronTriggerMaps: FlatEntityMaps<FlatCronTrigger>;
updateCronTriggerInput: UpdateCronTriggerInput;
}): FlatCronTrigger => {
const existingFlatCronTrigger =
flatCronTriggerMaps.byId[updateCronTriggerInput.id];
if (!existingFlatCronTrigger) {
throw new CronTriggerException(
'Cron trigger not found',
CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND,
);
}
return {
...existingFlatCronTrigger,
settings: updateCronTriggerInput.update.settings,
updatedAt: new Date().toISOString(),
};
};
@@ -1,5 +0,0 @@
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)[];
@@ -1,31 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DatabaseEventTriggerEntity } 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 { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@Module({
imports: [
TypeOrmModule.forFeature([WorkspaceEntity, DatabaseEventTriggerEntity]),
ApplicationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
PermissionsModule,
WorkspaceMigrationModule,
],
providers: [
CallDatabaseEventTriggerJobsJob,
DatabaseEventTriggerV2Service,
DatabaseEventTriggerResolver,
WorkspaceFlatDatabaseEventTriggerMapCacheService,
],
exports: [DatabaseEventTriggerV2Service],
imports: [TypeOrmModule.forFeature([ServerlessFunctionEntity])],
providers: [CallDatabaseEventTriggerJobsJob],
exports: [],
})
export class DatabaseEventTriggerModule {}
@@ -1,25 +0,0 @@
import { Field, HideField, 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;
@HideField()
universalIdentifier?: string;
@HideField()
applicationId?: string;
}
@@ -1,11 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsUUID } from 'class-validator';
@InputType()
export class DatabaseEventTriggerIdInput {
@IsUUID()
@IsNotEmpty()
@Field()
id: string;
}
@@ -1,28 +0,0 @@
import { Field, 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;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -1,32 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsObject, IsUUID, ValidateNested } 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()
class UpdateDatabaseEventTriggerInputUpdates {
@IsObject()
@IsNotEmpty()
@Field(() => GraphQLJSON)
settings: DatabaseEventTriggerSettings;
}
@InputType()
export class UpdateDatabaseEventTriggerInput {
@IsUUID()
@IsNotEmpty()
@Field({
description: 'The id of the database event trigger to update',
})
id: string;
@Type(() => UpdateDatabaseEventTriggerInputUpdates)
@ValidateNested()
@Field(() => UpdateDatabaseEventTriggerInputUpdates, {
description: 'The database event trigger updates',
})
update: UpdateDatabaseEventTriggerInputUpdates;
}
@@ -1,53 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
export type DatabaseEventTriggerSettings = {
eventName: string;
updatedFields?: string[];
};
@Entity('databaseEventTrigger')
@Index('IDX_DATABASE_EVENT_TRIGGER_WORKSPACE_ID', ['workspaceId'])
@Index('IDX_DATABASE_EVENT_TRIGGER_SERVERLESS_FUNCTION_ID', [
'serverlessFunctionId',
])
export class DatabaseEventTriggerEntity
extends SyncableEntity
implements Required<DatabaseEventTriggerEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'jsonb' })
settings: JsonbProperty<DatabaseEventTriggerSettings>;
@ManyToOne(
() => ServerlessFunctionEntity,
(serverlessFunction) => serverlessFunction.databaseEventTriggers,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'serverlessFunctionId' })
serverlessFunction: Relation<ServerlessFunctionEntity>;
@Column({ nullable: false, type: 'uuid' })
serverlessFunctionId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -1,43 +0,0 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum DatabaseEventTriggerExceptionCode {
DATABASE_EVENT_TRIGGER_NOT_FOUND = 'DATABASE_EVENT_TRIGGER_NOT_FOUND',
DATABASE_EVENT_TRIGGER_ALREADY_EXIST = 'DATABASE_EVENT_TRIGGER_ALREADY_EXIST',
DATABASE_EVENT_TRIGGER_INVALID = 'DATABASE_EVENT_TRIGGER_INVALID',
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
}
const getDatabaseEventTriggerExceptionUserFriendlyMessage = (
code: DatabaseEventTriggerExceptionCode,
) => {
switch (code) {
case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND:
return msg`Database event trigger not found.`;
case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_ALREADY_EXIST:
return msg`Database event trigger already exists.`;
case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_INVALID:
return msg`Invalid database event trigger.`;
case DatabaseEventTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
return msg`Serverless function not found.`;
default:
assertUnreachable(code);
}
};
export class DatabaseEventTriggerException extends CustomException<DatabaseEventTriggerExceptionCode> {
constructor(
message: string,
code: DatabaseEventTriggerExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getDatabaseEventTriggerExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,7 +1,8 @@
import { InjectRepository } from '@nestjs/typeorm';
import chunk from 'lodash.chunk';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Not, Repository } from 'typeorm';
import type { ObjectRecordEvent } from 'twenty-shared/database-events';
@@ -10,12 +11,12 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces
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 { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { transformEventBatchToEventPayloads } from 'src/engine/metadata-modules/database-event-trigger/utils/transform-event-batch-to-event-payloads';
import {
ServerlessFunctionTriggerJob,
ServerlessFunctionTriggerJobData,
} from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
const DATABASE_EVENT_JOBS_CHUNK_SIZE = 20;
@@ -25,31 +26,33 @@ export class CallDatabaseEventTriggerJobsJob {
constructor(
@InjectMessageQueue(MessageQueue.serverlessFunctionQueue)
private readonly messageQueueService: MessageQueueService,
@InjectRepository(DatabaseEventTriggerEntity)
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTriggerEntity>,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
) {}
@Process(CallDatabaseEventTriggerJobsJob.name)
async handle(workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>) {
const databaseEventListeners =
await this.databaseEventTriggerRepository.find({
const serverlessFunctionsWithDatabaseEventTrigger =
await this.serverlessFunctionRepository.find({
where: {
workspaceId: workspaceEventBatch.workspaceId,
databaseEventTriggerSettings: Not(IsNull()),
},
select: ['id', 'settings', 'workspaceId'],
relations: ['serverlessFunction'],
select: ['id', 'databaseEventTriggerSettings', 'workspaceId'],
});
const databaseEventListenersToTrigger = databaseEventListeners.filter(
(databaseEventListener) =>
const serverlessFunctionsToTrigger =
serverlessFunctionsWithDatabaseEventTrigger.filter((serverlessFunction) =>
this.shouldTriggerJob({
workspaceEventBatch,
eventName: databaseEventListener.settings.eventName,
eventName: isDefined(serverlessFunction.databaseEventTriggerSettings)
? serverlessFunction.databaseEventTriggerSettings.eventName
: '',
}),
);
);
const serverlessFunctionPayloads = transformEventBatchToEventPayloads({
databaseEventListeners: databaseEventListenersToTrigger,
serverlessFunctions: serverlessFunctionsToTrigger,
workspaceEventBatch,
});
@@ -1,150 +0,0 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
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 {
DatabaseEventTriggerException,
DatabaseEventTriggerExceptionCode,
} from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception';
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';
import { fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto } from 'src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.WORKFLOWS),
)
@Resolver()
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
export class DatabaseEventTriggerResolver {
constructor(
private readonly databaseEventTriggerV2Service: DatabaseEventTriggerV2Service,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@Query(() => DatabaseEventTriggerDTO)
async findOneDatabaseEventTrigger(
@Args('input') { id }: DatabaseEventTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<DatabaseEventTriggerDTO> {
try {
const { flatDatabaseEventTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatDatabaseEventTriggerMaps'],
},
);
const flatDatabaseEventTrigger = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatDatabaseEventTriggerMaps,
});
if (!isDefined(flatDatabaseEventTrigger)) {
throw new DatabaseEventTriggerException(
`Database event trigger with id ${id} not found`,
DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND,
);
}
return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto(
flatDatabaseEventTrigger,
);
} catch (error) {
return databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
@Query(() => [DatabaseEventTriggerDTO])
async findManyDatabaseEventTriggers(
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<DatabaseEventTriggerDTO[]> {
try {
const { flatDatabaseEventTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatDatabaseEventTriggerMaps'],
},
);
return Object.values(flatDatabaseEventTriggerMaps.byId)
.filter(isDefined)
.map(fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto);
} catch (error) {
return databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => DatabaseEventTriggerDTO)
async deleteOneDatabaseEventTrigger(
@Args('input') input: DatabaseEventTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<DatabaseEventTriggerDTO> {
try {
const flatDatabaseEventTrigger =
await this.databaseEventTriggerV2Service.destroyOne({
destroyDatabaseEventTriggerInput: input,
workspaceId,
});
return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto(
flatDatabaseEventTrigger,
);
} catch (error) {
return databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => DatabaseEventTriggerDTO)
async updateOneDatabaseEventTrigger(
@Args('input')
input: UpdateDatabaseEventTriggerInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<DatabaseEventTriggerDTO> {
try {
const flatDatabaseEventTrigger =
await this.databaseEventTriggerV2Service.updateOne(input, workspaceId);
return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto(
flatDatabaseEventTrigger,
);
} catch (error) {
return databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => DatabaseEventTriggerDTO)
async createOneDatabaseEventTrigger(
@Args('input')
input: CreateDatabaseEventTriggerInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<DatabaseEventTriggerDTO> {
try {
const flatDatabaseEventTrigger =
await this.databaseEventTriggerV2Service.createOne(input, workspaceId);
return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto(
flatDatabaseEventTrigger,
);
} catch (error) {
return databaseEventTriggerGraphQLApiExceptionHandler(error);
}
}
}
@@ -1,202 +0,0 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
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 { type 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 { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class DatabaseEventTriggerV2Service {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
) {}
async createOne(
databaseEventTriggerInput: CreateDatabaseEventTriggerInput,
workspaceId: string,
/**
* @deprecated do not use call validateBuildAndRunWorkspaceMigration contextually
* when interacting with another application than workspace custom one
* */
applicationId?: string,
): Promise<FlatDatabaseEventTrigger> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const flatDatabaseEventTriggerToCreate =
fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger({
createDatabaseEventTriggerInput: databaseEventTriggerInput,
workspaceId,
workspaceCustomApplicationId:
applicationId ?? workspaceCustomFlatApplication.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
databaseEventTrigger: {
flatEntityToCreate: [flatDatabaseEventTriggerToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating database event trigger',
);
}
const {
flatDatabaseEventTriggerMaps:
recomputedExistingFlatDatabaseEventTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatDatabaseEventTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatDatabaseEventTriggerToCreate.id,
flatEntityMaps: recomputedExistingFlatDatabaseEventTriggerMaps,
});
}
async updateOne(
databaseEventTriggerInput: UpdateDatabaseEventTriggerInput,
workspaceId: string,
): Promise<FlatDatabaseEventTrigger> {
const { flatDatabaseEventTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatDatabaseEventTriggerMaps'],
},
);
const optimisticallyUpdatedFlatDatabaseEventTrigger =
fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpdateOrThrow(
{
flatDatabaseEventTriggerMaps,
updateDatabaseEventTriggerInput: databaseEventTriggerInput,
},
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
databaseEventTrigger: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [
optimisticallyUpdatedFlatDatabaseEventTrigger,
],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating database event trigger',
);
}
const {
flatDatabaseEventTriggerMaps:
recomputedExistingFlatDatabaseEventTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatDatabaseEventTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatDatabaseEventTrigger.id,
flatEntityMaps: recomputedExistingFlatDatabaseEventTriggerMaps,
});
}
async destroyOne({
destroyDatabaseEventTriggerInput,
workspaceId,
}: {
destroyDatabaseEventTriggerInput: DatabaseEventTriggerIdInput;
workspaceId: string;
}): Promise<FlatDatabaseEventTrigger> {
const { flatDatabaseEventTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatDatabaseEventTriggerMaps'],
},
);
const existingFlatDatabaseEventTrigger =
flatDatabaseEventTriggerMaps.byId[destroyDatabaseEventTriggerInput.id];
if (!isDefined(existingFlatDatabaseEventTrigger)) {
throw new DatabaseEventTriggerException(
'Database event trigger to destroy not found',
DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND,
);
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
databaseEventTrigger: {
flatEntityToCreate: [],
flatEntityToDelete: [existingFlatDatabaseEventTrigger],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying database event trigger',
);
}
return existingFlatDatabaseEventTrigger;
}
}
@@ -1,60 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { DatabaseEventTriggerEntity } 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 { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatDatabaseEventTriggerMaps')
export class WorkspaceFlatDatabaseEventTriggerMapCacheService extends WorkspaceCacheProvider<
FlatEntityMaps<FlatDatabaseEventTrigger>
> {
constructor(
@InjectRepository(DatabaseEventTriggerEntity)
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTriggerEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatDatabaseEventTrigger>> {
const databaseEventTriggers =
await this.databaseEventTriggerRepository.find({
where: {
workspaceId,
},
});
const flatDatabaseEventTriggerMaps = createEmptyFlatEntityMaps();
for (const databaseEventTriggerEntity of databaseEventTriggers) {
const flatDatabaseEventTrigger = {
...removePropertiesFromRecord(
databaseEventTriggerEntity,
getMetadataEntityRelationProperties('databaseEventTrigger'),
),
universalIdentifier: databaseEventTriggerEntity.universalIdentifier,
createdAt: databaseEventTriggerEntity.createdAt.toISOString(),
updatedAt: databaseEventTriggerEntity.updatedAt.toISOString(),
} satisfies FlatDatabaseEventTrigger;
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatDatabaseEventTrigger,
flatEntityMapsToMutate: flatDatabaseEventTriggerMaps,
});
}
return flatDatabaseEventTriggerMaps;
}
}
@@ -1,5 +0,0 @@
import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
export type FlatDatabaseEventTrigger =
FlatEntityFrom<DatabaseEventTriggerEntity>;
@@ -1,24 +1,21 @@
import type { ObjectRecordEvent } from 'twenty-shared/database-events';
import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { transformEventBatchToEventPayloads } from 'src/engine/metadata-modules/database-event-trigger/utils/transform-event-batch-to-event-payloads';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import type { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
const createMockDatabaseEventListener = (
overrides: Partial<DatabaseEventTriggerEntity> = {},
): DatabaseEventTriggerEntity =>
const createMockServerlessFunction = (
overrides: Partial<ServerlessFunctionEntity> = {},
): ServerlessFunctionEntity =>
({
id: 'listener-1',
id: 'function-1',
workspaceId: 'workspace-1',
settings: {
databaseEventTriggerSettings: {
eventName: 'company.updated',
},
serverlessFunction: {
id: 'function-1',
},
...overrides,
}) as DatabaseEventTriggerEntity;
}) as ServerlessFunctionEntity;
const createMockEvent = (
overrides: Partial<ObjectRecordEvent> = {},
@@ -46,13 +43,13 @@ const createMockWorkspaceEventBatch = (
describe('transformEventBatchToEventPayloads', () => {
describe('basic transformation', () => {
it('should transform a single event batch with a single listener', () => {
it('should transform a single event batch with a single serverless function', () => {
const workspaceEventBatch = createMockWorkspaceEventBatch();
const databaseEventListeners = [createMockDatabaseEventListener()];
const serverlessFunctions = [createMockServerlessFunction()];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(1);
@@ -75,11 +72,11 @@ describe('transformEventBatchToEventPayloads', () => {
createMockEvent({ recordId: 'record-3' }),
],
});
const databaseEventListeners = [createMockDatabaseEventListener()];
const serverlessFunctions = [createMockServerlessFunction()];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(3);
@@ -88,22 +85,20 @@ describe('transformEventBatchToEventPayloads', () => {
).toEqual(['record-1', 'record-2', 'record-3']);
});
it('should create payloads for each listener', () => {
it('should create payloads for each serverless function', () => {
const workspaceEventBatch = createMockWorkspaceEventBatch();
const databaseEventListeners = [
createMockDatabaseEventListener({
id: 'listener-1',
serverlessFunction: { id: 'function-1' },
} as Partial<DatabaseEventTriggerEntity>),
createMockDatabaseEventListener({
id: 'listener-2',
serverlessFunction: { id: 'function-2' },
} as Partial<DatabaseEventTriggerEntity>),
const serverlessFunctions = [
createMockServerlessFunction({
id: 'function-1',
}),
createMockServerlessFunction({
id: 'function-2',
}),
];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(2);
@@ -129,15 +124,15 @@ describe('transformEventBatchToEventPayloads', () => {
}),
],
});
const databaseEventListeners = [
createMockDatabaseEventListener({
settings: { eventName: 'company.updated' },
const serverlessFunctions = [
createMockServerlessFunction({
databaseEventTriggerSettings: { eventName: 'company.updated' },
}),
];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(2);
@@ -157,15 +152,18 @@ describe('transformEventBatchToEventPayloads', () => {
}),
],
});
const databaseEventListeners = [
createMockDatabaseEventListener({
settings: { eventName: 'company.updated', updatedFields: [] },
const serverlessFunctions = [
createMockServerlessFunction({
databaseEventTriggerSettings: {
eventName: 'company.updated',
updatedFields: [],
},
}),
];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(2);
@@ -189,15 +187,18 @@ describe('transformEventBatchToEventPayloads', () => {
}),
],
});
const databaseEventListeners = [
createMockDatabaseEventListener({
settings: { eventName: 'company.updated', updatedFields: ['name'] },
const serverlessFunctions = [
createMockServerlessFunction({
databaseEventTriggerSettings: {
eventName: 'company.updated',
updatedFields: ['name'],
},
}),
];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(2);
@@ -224,9 +225,9 @@ describe('transformEventBatchToEventPayloads', () => {
}),
],
});
const databaseEventListeners = [
createMockDatabaseEventListener({
settings: {
const serverlessFunctions = [
createMockServerlessFunction({
databaseEventTriggerSettings: {
eventName: 'company.updated',
updatedFields: ['name', 'address'],
},
@@ -235,7 +236,7 @@ describe('transformEventBatchToEventPayloads', () => {
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(2);
@@ -258,21 +259,24 @@ describe('transformEventBatchToEventPayloads', () => {
}),
],
});
const databaseEventListeners = [
createMockDatabaseEventListener({
settings: { eventName: 'company.updated', updatedFields: ['phone'] },
const serverlessFunctions = [
createMockServerlessFunction({
databaseEventTriggerSettings: {
eventName: 'company.updated',
updatedFields: ['phone'],
},
}),
];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(0);
});
it('should handle different updatedFields filters per listener', () => {
it('should handle different updatedFields filters per serverless function', () => {
const workspaceEventBatch = createMockWorkspaceEventBatch({
name: 'company.updated',
events: [
@@ -286,25 +290,26 @@ describe('transformEventBatchToEventPayloads', () => {
}),
],
});
const databaseEventListeners = [
createMockDatabaseEventListener({
id: 'listener-1',
settings: { eventName: 'company.updated', updatedFields: ['name'] },
serverlessFunction: { id: 'function-1' },
} as Partial<DatabaseEventTriggerEntity>),
createMockDatabaseEventListener({
id: 'listener-2',
settings: {
const serverlessFunctions = [
createMockServerlessFunction({
id: 'function-1',
databaseEventTriggerSettings: {
eventName: 'company.updated',
updatedFields: ['name'],
},
}),
createMockServerlessFunction({
id: 'function-2',
databaseEventTriggerSettings: {
eventName: 'company.updated',
updatedFields: ['address'],
},
serverlessFunction: { id: 'function-2' },
} as Partial<DatabaseEventTriggerEntity>),
}),
];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(2);
@@ -329,12 +334,12 @@ describe('transformEventBatchToEventPayloads', () => {
});
describe('edge cases', () => {
it('should return empty array when no listeners provided', () => {
it('should return empty array when no serverless functions provided', () => {
const workspaceEventBatch = createMockWorkspaceEventBatch();
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners: [],
serverlessFunctions: [],
});
expect(result).toHaveLength(0);
@@ -344,11 +349,11 @@ describe('transformEventBatchToEventPayloads', () => {
const workspaceEventBatch = createMockWorkspaceEventBatch({
events: [],
});
const databaseEventListeners = [createMockDatabaseEventListener()];
const serverlessFunctions = [createMockServerlessFunction()];
const result = transformEventBatchToEventPayloads({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
});
expect(result).toHaveLength(0);
@@ -1,25 +0,0 @@
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,
): never => {
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;
};
@@ -1,29 +0,0 @@
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,
workspaceCustomApplicationId,
}: {
createDatabaseEventTriggerInput: CreateDatabaseEventTriggerInput;
workspaceId: string;
workspaceCustomApplicationId: string;
}): FlatDatabaseEventTrigger => {
const now = new Date().toISOString();
const id = uuidV4();
return {
id,
universalIdentifier:
createDatabaseEventTriggerInput.universalIdentifier ?? uuidV4(),
settings: createDatabaseEventTriggerInput.settings,
serverlessFunctionId: createDatabaseEventTriggerInput.serverlessFunctionId,
workspaceId,
createdAt: now,
updatedAt: now,
applicationId: workspaceCustomApplicationId,
};
};
@@ -1,11 +0,0 @@
import { type DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
export const fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto = (
flatDatabaseEventTrigger: FlatDatabaseEventTrigger,
): DatabaseEventTriggerDTO => ({
id: flatDatabaseEventTrigger.id,
settings: flatDatabaseEventTrigger.settings,
createdAt: new Date(flatDatabaseEventTrigger.createdAt),
updatedAt: new Date(flatDatabaseEventTrigger.updatedAt),
});
@@ -1,32 +0,0 @@
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';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.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.update.settings,
updatedAt: new Date().toISOString(),
};
};
@@ -5,23 +5,24 @@ import type {
ObjectRecordEvent,
} from 'twenty-shared/database-events';
import { type DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { type ServerlessFunctionTriggerJobData } from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import type { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
export const transformEventBatchToEventPayloads = ({
workspaceEventBatch,
databaseEventListeners,
serverlessFunctions,
}: {
workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>;
databaseEventListeners: DatabaseEventTriggerEntity[];
serverlessFunctions: ServerlessFunctionEntity[];
}): ServerlessFunctionTriggerJobData[] => {
const result: ServerlessFunctionTriggerJobData[] = [];
const { events, ...batchEventInfo } = workspaceEventBatch;
const [, operation] = workspaceEventBatch.name.split('.');
for (const databaseEventListener of databaseEventListeners) {
const triggerUpdatedFields = databaseEventListener.settings.updatedFields;
for (const serverlessFunction of serverlessFunctions) {
const triggerUpdatedFields =
serverlessFunction.databaseEventTriggerSettings?.updatedFields;
const filteredEvents = filterEventsByUpdatedFields({
events,
@@ -33,8 +34,8 @@ export const transformEventBatchToEventPayloads = ({
const payload: DatabaseEventPayload = { ...batchEventInfo, ...event };
result.push({
serverlessFunctionId: databaseEventListener.serverlessFunction.id,
workspaceId: databaseEventListener.workspaceId,
serverlessFunctionId: serverlessFunction.id,
workspaceId: serverlessFunction.workspaceId,
payload,
});
}
@@ -1,7 +1,5 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/cron-trigger/constants/flat-cron-trigger-editable-properties.constant';
import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/database-event-trigger/constants/flat-database-event-trigger-editable-properties.constant';
import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant';
import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
@@ -22,7 +20,6 @@ import { FLAT_VIEW_FILTER_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-
import { FLAT_VIEW_FILTER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-editable-properties.constant';
import { FLAT_VIEW_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-group/constants/flat-view-group-editable-properties.constant';
import { FLAT_VIEW_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view/constants/flat-view-editable-properties.constant';
import { FLAT_ROUTE_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/route-trigger/constants/flat-route-trigger-editable-properties.constant';
import { FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant';
type OneFlatEntityConfiguration<T extends AllMetadataName> = {
@@ -88,15 +85,15 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
propertiesToStringify: ['toolInputSchema', 'publishedVersions'],
},
cronTrigger: {
propertiesToCompare: [...FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES],
propertiesToStringify: ['settings'],
propertiesToCompare: [],
propertiesToStringify: [],
},
databaseEventTrigger: {
propertiesToCompare: [...FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES],
propertiesToStringify: ['settings'],
propertiesToCompare: [],
propertiesToStringify: [],
},
routeTrigger: {
propertiesToCompare: [...FLAT_ROUTE_TRIGGER_EDITABLE_PROPERTIES],
propertiesToCompare: [],
propertiesToStringify: [],
},
viewFilter: {
@@ -3,8 +3,6 @@ import { type EntityTarget, type ObjectLiteral } from 'typeorm';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
@@ -16,7 +14,6 @@ import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@@ -39,9 +36,9 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
view: ViewEntity,
index: IndexMetadataEntity,
pageLayoutTab: PageLayoutTabEntity,
routeTrigger: RouteTriggerEntity,
cronTrigger: CronTriggerEntity,
databaseEventTrigger: DatabaseEventTriggerEntity,
routeTrigger: ServerlessFunctionEntity,
cronTrigger: ServerlessFunctionEntity,
databaseEventTrigger: ServerlessFunctionEntity,
frontComponent: FrontComponentEntity,
fieldMetadata: FieldMetadataEntity,
pageLayout: PageLayoutEntity,
@@ -267,45 +267,29 @@ export const ALL_METADATA_RELATIONS = {
application: null,
serverlessFunctionLayer: null,
},
oneToMany: {
cronTriggers: { metadataName: 'cronTrigger' },
databaseEventTriggers: { metadataName: 'databaseEventTrigger' },
routeTriggers: { metadataName: 'routeTrigger' },
},
oneToMany: {},
},
cronTrigger: {
manyToOne: {
serverlessFunction: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'cronTriggerIds',
foreignKey: 'serverlessFunctionId',
},
workspace: null,
application: null,
serverlessFunctionLayer: null,
workspace: null,
},
oneToMany: {},
},
databaseEventTrigger: {
manyToOne: {
serverlessFunction: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'databaseEventTriggerIds',
foreignKey: 'serverlessFunctionId',
},
workspace: null,
application: null,
serverlessFunctionLayer: null,
workspace: null,
},
oneToMany: {},
},
routeTrigger: {
manyToOne: {
serverlessFunction: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'routeTriggerIds',
foreignKey: 'serverlessFunctionId',
},
workspace: null,
application: null,
serverlessFunctionLayer: null,
workspace: null,
},
oneToMany: {},
},
@@ -1,5 +1,3 @@
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
@@ -21,7 +19,6 @@ import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filte
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type';
import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
@@ -35,16 +32,6 @@ import {
type DeleteCommandMenuItemAction,
type UpdateCommandMenuItemAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
import {
type CreateCronTriggerAction,
type DeleteCronTriggerAction,
type UpdateCronTriggerAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/cron-trigger/types/workspace-migration-cron-trigger-action.type';
import {
type CreateDatabaseEventTriggerAction,
type DeleteDatabaseEventTriggerAction,
type UpdateDatabaseEventTriggerAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/database-event-trigger/types/workspace-migration-database-event-trigger-action.type';
import {
type CreateFieldAction,
type DeleteFieldAction,
@@ -95,11 +82,6 @@ import {
type DeleteRoleAction,
type UpdateRoleAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role/types/workspace-migration-role-action.type';
import {
type CreateRouteTriggerAction,
type DeleteRouteTriggerAction,
type UpdateRouteTriggerAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/route-trigger/types/workspace-migration-route-trigger-action.type';
import {
type CreateRowLevelPermissionPredicateGroupAction,
type DeleteRowLevelPermissionPredicateGroupAction,
@@ -244,30 +226,30 @@ export type AllFlatEntityTypesByMetadataName = {
};
cronTrigger: {
actions: {
create: CreateCronTriggerAction;
update: UpdateCronTriggerAction;
delete: DeleteCronTriggerAction;
create: never;
update: never;
delete: never;
};
flatEntity: FlatCronTrigger;
entity: MetadataEntity<'cronTrigger'>;
flatEntity: FlatServerlessFunction;
entity: MetadataEntity<'serverlessFunction'>;
};
databaseEventTrigger: {
actions: {
create: CreateDatabaseEventTriggerAction;
update: UpdateDatabaseEventTriggerAction;
delete: DeleteDatabaseEventTriggerAction;
create: never;
update: never;
delete: never;
};
flatEntity: FlatDatabaseEventTrigger;
entity: MetadataEntity<'databaseEventTrigger'>;
flatEntity: FlatServerlessFunction;
entity: MetadataEntity<'serverlessFunction'>;
};
routeTrigger: {
actions: {
create: CreateRouteTriggerAction;
update: UpdateRouteTriggerAction;
delete: DeleteRouteTriggerAction;
create: never;
update: never;
delete: never;
};
flatEntity: FlatRouteTrigger;
entity: MetadataEntity<'routeTrigger'>;
flatEntity: FlatServerlessFunction;
entity: MetadataEntity<'serverlessFunction'>;
};
viewFilter: {
actions: {
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`getMetadataRelatedMetadataNames should return related metadata names for agent 1`] = `[]`;
@@ -8,17 +8,9 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for cronTrigger 1`] = `
[
"serverlessFunction",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for cronTrigger 1`] = `[]`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for databaseEventTrigger 1`] = `
[
"serverlessFunction",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for databaseEventTrigger 1`] = `[]`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for fieldMetadata 1`] = `
[
@@ -89,11 +81,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for routeTrigger 1`] = `
[
"serverlessFunction",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for routeTrigger 1`] = `[]`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for rowLevelPermissionPredicate 1`] = `
[
@@ -113,13 +101,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for serverlessFunction 1`] = `
[
"cronTrigger",
"databaseEventTrigger",
"routeTrigger",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for serverlessFunction 1`] = `[]`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for skill 1`] = `[]`;
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
[
@@ -8,24 +8,24 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"pageLayoutWidget",
"viewField",
"commandMenuItem",
"cronTrigger",
"databaseEventTrigger",
"index",
"roleTarget",
"routeTrigger",
"rowLevelPermissionPredicateGroup",
"viewGroup",
"agent",
"cronTrigger",
"databaseEventTrigger",
"frontComponent",
"pageLayout",
"pageLayoutTab",
"routeTrigger",
"serverlessFunction",
"skill",
"view",
"viewFilterGroup",
"webhook",
"fieldMetadata",
"role",
"serverlessFunction",
"objectMetadata",
]
`;
@@ -5,9 +5,7 @@ import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.
import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module';
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { FrontComponentModule } from 'src/engine/metadata-modules/front-component/front-component.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
@@ -42,8 +40,6 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
RoleModule,
PermissionsModule,
RouteTriggerModule,
CronTriggerModule,
DatabaseEventTriggerModule,
WebhookModule,
],
providers: [],
@@ -1,8 +0,0 @@
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
export const FLAT_ROUTE_TRIGGER_EDITABLE_PROPERTIES = [
'path',
'isAuthRequired',
'httpMethod',
'forwardedRequestHeaders',
] as const satisfies (keyof FlatRouteTrigger)[];
@@ -1,51 +0,0 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import {
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { HTTPMethod } from 'twenty-shared/types';
@InputType()
export class CreateRouteTriggerInput {
@IsString()
@IsNotEmpty()
@Field()
path: string;
@IsBoolean()
@IsNotEmpty()
@Field({ defaultValue: true })
isAuthRequired: boolean;
@IsEnum(HTTPMethod)
@IsNotEmpty()
@Field(() => HTTPMethod, { defaultValue: HTTPMethod.GET })
httpMethod: HTTPMethod;
@IsUUID()
@IsNotEmpty()
@Field()
serverlessFunctionId: string;
@IsArray()
@IsString({ each: true })
@IsOptional()
@Field(() => [String], {
defaultValue: [],
description:
'List of HTTP header names to forward to the serverless function event',
})
forwardedRequestHeaders: string[];
@HideField()
universalIdentifier?: string;
@HideField()
applicationId?: string;
}
@@ -1,11 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsUUID } from 'class-validator';
@InputType()
export class RouteTriggerIdInput {
@IsUUID()
@IsNotEmpty()
@Field()
id: string;
}
@@ -1,32 +0,0 @@
import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql';
import { HTTPMethod } from 'twenty-shared/types';
registerEnumType(HTTPMethod, { name: 'HTTPMethod' });
@ObjectType('RouteTrigger')
export class RouteTriggerDTO {
@Field(() => ID)
id: string;
@Field()
path: string;
@Field()
isAuthRequired: boolean;
@Field(() => HTTPMethod)
httpMethod: HTTPMethod;
@Field(() => [String], {
description:
'List of HTTP header names to forward to the serverless function event',
})
forwardedRequestHeaders: string[];
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
}
@@ -1,59 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import { HTTPMethod } from 'twenty-shared/types';
@InputType()
class UpdateRouteTriggerInputUpdates {
@IsString()
@IsNotEmpty()
@Field()
path: string;
@IsBoolean()
@IsNotEmpty()
@Field()
isAuthRequired: boolean;
@IsEnum(HTTPMethod)
@IsNotEmpty()
@Field(() => HTTPMethod)
httpMethod: HTTPMethod;
@IsArray()
@IsString({ each: true })
@IsOptional()
@Field(() => [String], {
defaultValue: [],
description:
'List of HTTP header names to forward to the serverless function event',
})
forwardedRequestHeaders: string[];
}
@InputType()
export class UpdateRouteTriggerInput {
@IsUUID()
@IsNotEmpty()
@Field({
description: 'The id of the route to update',
})
id: string;
@Type(() => UpdateRouteTriggerInputUpdates)
@ValidateNested()
@Field(() => UpdateRouteTriggerInputUpdates, {
description: 'The route updates',
})
update: UpdateRouteTriggerInputUpdates;
}
@@ -1,149 +0,0 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { CreateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/create-route-trigger.input';
import { RouteTriggerIdInput } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger-id.input';
import { RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto';
import { UpdateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/update-route-trigger.input';
import {
RouteTriggerException,
RouteTriggerExceptionCode,
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
import { RouteTriggerV2Service } from 'src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service';
import { fromFlatRouteTriggerToRouteTriggerDto } from 'src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util';
import { routeTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils';
@UseGuards(
WorkspaceAuthGuard,
SettingsPermissionGuard(PermissionFlagType.WORKFLOWS),
)
@Resolver()
@UsePipes(ResolverValidationPipe)
@UseFilters(
PreventNestToAutoLogGraphqlErrorsFilter,
PermissionsGraphqlApiExceptionFilter,
)
export class RouteTriggerResolver {
constructor(
private readonly routeV2Service: RouteTriggerV2Service,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@Query(() => RouteTriggerDTO)
async findOneRouteTrigger(
@Args('input') { id }: RouteTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<RouteTriggerDTO> {
try {
const { flatRouteTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
const flatRouteTrigger = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatRouteTriggerMaps,
});
if (!isDefined(flatRouteTrigger)) {
throw new RouteTriggerException(
`Route trigger with id ${id} not found`,
RouteTriggerExceptionCode.ROUTE_NOT_FOUND,
);
}
return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger);
} catch (error) {
return routeTriggerGraphQLApiExceptionHandler(error);
}
}
@Query(() => [RouteTriggerDTO])
async findManyRouteTriggers(
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<RouteTriggerDTO[]> {
try {
const { flatRouteTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
return Object.values(flatRouteTriggerMaps.byId)
.filter(isDefined)
.map(fromFlatRouteTriggerToRouteTriggerDto);
} catch (error) {
return routeTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => RouteTriggerDTO)
async deleteOneRouteTrigger(
@Args('input') input: RouteTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<RouteTriggerDTO> {
try {
const flatRouteTrigger = await this.routeV2Service.destroyOne({
destroyRouteTriggerInput: input,
workspaceId,
});
return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger);
} catch (error) {
return routeTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => RouteTriggerDTO)
async updateOneRouteTrigger(
@Args('input')
input: UpdateRouteTriggerInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<RouteTriggerDTO> {
try {
const flatRouteTrigger = await this.routeV2Service.updateOne(
input,
workspaceId,
);
return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger);
} catch (error) {
return routeTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => RouteTriggerDTO)
async createOneRouteTrigger(
@Args('input')
input: CreateRouteTriggerInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<RouteTriggerDTO> {
try {
const flatRouteTrigger = await this.routeV2Service.createOne(
input,
workspaceId,
);
return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger);
} catch (error) {
return routeTriggerGraphQLApiExceptionHandler(error);
}
}
}
@@ -1,66 +0,0 @@
import { HTTPMethod } from 'twenty-shared/types';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity({ name: 'routeTrigger', schema: 'core' })
@Unique('IDX_ROUTE_TRIGGER_PATH_HTTP_METHOD_WORKSPACE_ID_UNIQUE', [
'path',
'httpMethod',
'workspaceId',
])
@Index('IDX_ROUTE_TRIGGER_SERVERLESS_FUNCTION_ID', ['serverlessFunctionId'])
export class RouteTriggerEntity
extends SyncableEntity
implements Required<RouteTriggerEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
path: string;
@Column({ nullable: false, default: true })
isAuthRequired: boolean;
@Column({
type: 'enum',
enum: HTTPMethod,
default: HTTPMethod.GET,
nullable: false,
})
httpMethod: HTTPMethod;
@Column({ nullable: false, type: 'jsonb', default: [] })
forwardedRequestHeaders: JsonbProperty<string[]>;
@ManyToOne(
() => ServerlessFunctionEntity,
(serverlessFunction) => serverlessFunction.routeTriggers,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'serverlessFunctionId' })
serverlessFunction: Relation<ServerlessFunctionEntity>;
@Column({ nullable: false, type: 'uuid' })
serverlessFunctionId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -1,38 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RouteTriggerResolver } from 'src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver';
import { RouteTriggerController } from 'src/engine/metadata-modules/route-trigger/route-trigger.controller';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { RouteTriggerService } from 'src/engine/metadata-modules/route-trigger/route-trigger.service';
import { RouteTriggerV2Service } from 'src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service';
import { WorkspaceFlatRouteTriggerMapCacheService } from 'src/engine/metadata-modules/route-trigger/services/workspace-flat-route-trigger-map-cache.service';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
TypeOrmModule.forFeature([RouteTriggerEntity]),
ApplicationModule,
TypeOrmModule.forFeature([ServerlessFunctionEntity]),
TokenModule,
WorkspaceDomainsModule,
ServerlessFunctionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
PermissionsModule,
WorkspaceMigrationModule,
],
controllers: [RouteTriggerController],
providers: [
RouteTriggerService,
RouteTriggerV2Service,
WorkspaceFlatRouteTriggerMapCacheService,
RouteTriggerResolver,
],
exports: [RouteTriggerV2Service, WorkspaceFlatRouteTriggerMapCacheService],
providers: [RouteTriggerService],
exports: [],
})
export class RouteTriggerModule {}
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Request } from 'express';
import { match } from 'path-to-regexp';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { IsNull, Not, Repository } from 'typeorm';
import { HTTPMethod } from 'twenty-shared/types';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
@@ -13,8 +13,8 @@ import {
RouteTriggerException,
RouteTriggerExceptionCode,
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { buildServerlessFunctionEvent } from 'src/engine/metadata-modules/route-trigger/utils/build-serverless-function-event.util';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
@Injectable()
@@ -23,18 +23,18 @@ export class RouteTriggerService {
private readonly accessTokenService: AccessTokenService,
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
@InjectRepository(RouteTriggerEntity)
private readonly routeTriggerRepository: Repository<RouteTriggerEntity>,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
) {}
private async getOneRouteTriggerWithPathParamsOrFail({
private async getServerlessFunctionWithPathParamsOrFail({
request,
httpMethod,
}: {
request: Request;
httpMethod: HTTPMethod;
}): Promise<{
routeTrigger: RouteTriggerEntity;
serverlessFunction: ServerlessFunctionEntity;
pathParams: Partial<Record<string, string | string[]>>;
}> {
const host = `${request.protocol}://${request.get('host')}`;
@@ -52,26 +52,35 @@ export class RouteTriggerService {
),
);
const routeTriggers = await this.routeTriggerRepository.find({
where: {
httpMethod,
workspaceId: workspace.id,
},
relations: ['serverlessFunction'],
});
const serverlessFunctionsWithHttpRouteTrigger =
await this.serverlessFunctionRepository.find({
where: {
workspaceId: workspace.id,
httpRouteTriggerSettings: Not(IsNull()),
},
});
const requestPath = request.path.replace(/^\/s\//, '/');
for (const routeTrigger of routeTriggers) {
const routeTriggerMatcher = match(routeTrigger.path, {
for (const serverlessFunction of serverlessFunctionsWithHttpRouteTrigger) {
const httpRouteSettings = serverlessFunction.httpRouteTriggerSettings;
if (
!isDefined(httpRouteSettings) ||
httpRouteSettings.httpMethod !== httpMethod
) {
continue;
}
const routeMatcher = match(httpRouteSettings.path, {
decode: decodeURIComponent,
});
const routeTriggerMatched = routeTriggerMatcher(requestPath);
const routeMatched = routeMatcher(requestPath);
if (routeTriggerMatched) {
if (routeMatched) {
return {
routeTrigger,
pathParams: routeTriggerMatched.params,
serverlessFunction,
pathParams: routeMatched.params,
};
}
}
@@ -116,30 +125,31 @@ export class RouteTriggerService {
request: Request;
httpMethod: HTTPMethod;
}) {
const routeTriggerWithPathParams =
await this.getOneRouteTriggerWithPathParamsOrFail({
const { serverlessFunction, pathParams } =
await this.getServerlessFunctionWithPathParamsOrFail({
request,
httpMethod,
});
if (routeTriggerWithPathParams.routeTrigger.isAuthRequired) {
const httpRouteSettings = serverlessFunction.httpRouteTriggerSettings;
if (httpRouteSettings?.isAuthRequired) {
await this.validateWorkspaceFromRequest({
request,
workspaceId: routeTriggerWithPathParams.routeTrigger.workspaceId,
workspaceId: serverlessFunction.workspaceId,
});
}
const event = buildServerlessFunctionEvent({
request,
pathParameters: routeTriggerWithPathParams.pathParams,
forwardedRequestHeaders:
routeTriggerWithPathParams.routeTrigger.forwardedRequestHeaders ?? [],
pathParameters: pathParams,
forwardedRequestHeaders: httpRouteSettings?.forwardedRequestHeaders ?? [],
});
const result =
await this.serverlessFunctionService.executeOneServerlessFunction({
id: routeTriggerWithPathParams.routeTrigger.serverlessFunction.id,
workspaceId: routeTriggerWithPathParams.routeTrigger.workspaceId,
id: serverlessFunction.id,
workspaceId: serverlessFunction.workspaceId,
payload: event,
version: 'draft',
});
@@ -1,192 +0,0 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { CreateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/create-route-trigger.input';
import { RouteTriggerIdInput } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger-id.input';
import { UpdateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/update-route-trigger.input';
import {
RouteTriggerException,
RouteTriggerExceptionCode,
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { fromCreateRouteTriggerInputToFlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/utils/from-create-route-trigger-input-to-flat-route-trigger.util';
import { fromUpdateRouteTriggerInputToFlatRouteTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/route-trigger/utils/from-update-route-trigger-input-to-flat-route-trigger-to-update-or-throw.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class RouteTriggerV2Service {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
) {}
async createOne(
routeTriggerInput: CreateRouteTriggerInput,
workspaceId: string,
/**
* @deprecated do not use call validateBuildAndRunWorkspaceMigration contextually
* when interacting with another application than workspace custom one
* */
applicationId?: string,
): Promise<FlatRouteTrigger> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const flatRouteTriggerToCreate =
fromCreateRouteTriggerInputToFlatRouteTrigger({
createRouteTriggerInput: routeTriggerInput,
workspaceId,
workspaceCustomApplicationId:
applicationId ?? workspaceCustomFlatApplication.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
routeTrigger: {
flatEntityToCreate: [flatRouteTriggerToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating route',
);
}
const { flatRouteTriggerMaps: recomputedExistingFlatRouteMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatRouteTriggerToCreate.id,
flatEntityMaps: recomputedExistingFlatRouteMaps,
});
}
async updateOne(
routeTriggerInput: UpdateRouteTriggerInput,
workspaceId: string,
): Promise<FlatRouteTrigger> {
const { flatRouteTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
const optimisticallyUpdatedFlatRouteTrigger =
fromUpdateRouteTriggerInputToFlatRouteTriggerToUpdateOrThrow({
flatRouteTriggerMaps,
updateRouteTriggerInput: routeTriggerInput,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
routeTrigger: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [optimisticallyUpdatedFlatRouteTrigger],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating route',
);
}
const { flatRouteTriggerMaps: recomputedExistingFlatRouteMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatRouteTrigger.id,
flatEntityMaps: recomputedExistingFlatRouteMaps,
});
}
async destroyOne({
destroyRouteTriggerInput,
workspaceId,
}: {
destroyRouteTriggerInput: RouteTriggerIdInput;
workspaceId: string;
}): Promise<FlatRouteTrigger> {
const { flatRouteTriggerMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRouteTriggerMaps'],
},
);
const existingFlatRoute =
flatRouteTriggerMaps.byId[destroyRouteTriggerInput.id];
if (!isDefined(existingFlatRoute)) {
throw new RouteTriggerException(
'Route to destroy not found',
RouteTriggerExceptionCode.ROUTE_NOT_FOUND,
);
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
routeTrigger: {
flatEntityToCreate: [],
flatEntityToDelete: [existingFlatRoute],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying route',
);
}
return existingFlatRoute;
}
}
@@ -1,60 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatRouteTriggerMaps')
export class WorkspaceFlatRouteTriggerMapCacheService extends WorkspaceCacheProvider<
FlatEntityMaps<FlatRouteTrigger>
> {
constructor(
@InjectRepository(RouteTriggerEntity)
private readonly routeTriggerRepository: Repository<RouteTriggerEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatRouteTrigger>> {
const routeTriggers = await this.routeTriggerRepository.find({
where: {
workspaceId,
},
});
const flatRouteTriggerMaps = createEmptyFlatEntityMaps();
for (const routeTriggerEntity of routeTriggers) {
const routeTriggerEntityWithoutRelations = removePropertiesFromRecord(
routeTriggerEntity,
getMetadataEntityRelationProperties('routeTrigger'),
);
const flatRouteTrigger = {
...routeTriggerEntityWithoutRelations,
createdAt: routeTriggerEntity.createdAt.toISOString(),
updatedAt: routeTriggerEntity.updatedAt.toISOString(),
universalIdentifier: routeTriggerEntity.universalIdentifier,
} satisfies FlatRouteTrigger;
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRouteTrigger,
flatEntityMapsToMutate: flatRouteTriggerMaps,
});
}
return flatRouteTriggerMaps;
}
}
@@ -1,4 +0,0 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
export type FlatRouteTrigger = FlatEntityFrom<RouteTriggerEntity>;
@@ -1,33 +0,0 @@
import { v4 as uuidV4 } from 'uuid';
import { type CreateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/create-route-trigger.input';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
export const fromCreateRouteTriggerInputToFlatRouteTrigger = ({
createRouteTriggerInput,
workspaceId,
workspaceCustomApplicationId,
}: {
createRouteTriggerInput: CreateRouteTriggerInput;
workspaceId: string;
workspaceCustomApplicationId: string;
}): FlatRouteTrigger => {
const now = new Date();
const id = uuidV4();
return {
id,
universalIdentifier:
createRouteTriggerInput.universalIdentifier ?? uuidV4(),
path: createRouteTriggerInput.path,
isAuthRequired: createRouteTriggerInput.isAuthRequired,
httpMethod: createRouteTriggerInput.httpMethod,
forwardedRequestHeaders:
createRouteTriggerInput.forwardedRequestHeaders ?? [],
serverlessFunctionId: createRouteTriggerInput.serverlessFunctionId,
workspaceId,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
applicationId: workspaceCustomApplicationId,
};
};
@@ -1,14 +0,0 @@
import { type RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
export const fromFlatRouteTriggerToRouteTriggerDto = (
flatRouteTrigger: FlatRouteTrigger,
): RouteTriggerDTO => ({
id: flatRouteTrigger.id,
path: flatRouteTrigger.path,
isAuthRequired: flatRouteTrigger.isAuthRequired,
httpMethod: flatRouteTrigger.httpMethod,
forwardedRequestHeaders: flatRouteTrigger.forwardedRequestHeaders,
createdAt: new Date(flatRouteTrigger.createdAt),
updatedAt: new Date(flatRouteTrigger.updatedAt),
});
@@ -1,36 +0,0 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type UpdateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/update-route-trigger.input';
import {
RouteTriggerException,
RouteTriggerExceptionCode,
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
export const fromUpdateRouteTriggerInputToFlatRouteTriggerToUpdateOrThrow = ({
flatRouteTriggerMaps,
updateRouteTriggerInput,
}: {
flatRouteTriggerMaps: FlatEntityMaps<FlatRouteTrigger>;
updateRouteTriggerInput: UpdateRouteTriggerInput;
}): FlatRouteTrigger => {
const existingFlatRouteTrigger =
flatRouteTriggerMaps.byId[updateRouteTriggerInput.id];
if (!existingFlatRouteTrigger) {
throw new RouteTriggerException(
'Route not found',
RouteTriggerExceptionCode.ROUTE_NOT_FOUND,
);
}
return {
...existingFlatRouteTrigger,
updatedAt: new Date().toISOString(),
path: updateRouteTriggerInput.update.path,
isAuthRequired: updateRouteTriggerInput.update.isAuthRequired,
httpMethod: updateRouteTriggerInput.update.httpMethod,
forwardedRequestHeaders:
updateRouteTriggerInput.update.forwardedRequestHeaders ??
existingFlatRouteTrigger.forwardedRequestHeaders,
};
};
@@ -1,23 +0,0 @@
import {
ConflictError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
RouteTriggerException,
RouteTriggerExceptionCode,
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
export const routeTriggerGraphQLApiExceptionHandler = (error: Error): never => {
if (error instanceof RouteTriggerException) {
switch (error.code) {
case RouteTriggerExceptionCode.ROUTE_NOT_FOUND:
throw new NotFoundError('Route not found');
case RouteTriggerExceptionCode.ROUTE_ALREADY_EXIST:
throw new ConflictError('Route already exists');
default:
throw error;
}
}
throw error;
};
@@ -19,9 +19,11 @@ import {
import graphqlTypeJson from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto';
import { DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto';
import { RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto';
import {
CronTriggerSettings,
DatabaseEventTriggerSettings,
HttpRouteTriggerSettings,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@ObjectType('ServerlessFunction')
@Authorize({
@@ -86,14 +88,20 @@ export class ServerlessFunctionDTO {
@Field()
isTool: boolean;
@Field(() => [CronTriggerDTO], { nullable: true })
cronTriggers?: CronTriggerDTO[];
@IsObject()
@IsOptional()
@Field(() => graphqlTypeJson, { nullable: true })
cronTriggerSettings?: CronTriggerSettings;
@Field(() => [DatabaseEventTriggerDTO], { nullable: true })
databaseEventTriggers?: DatabaseEventTriggerDTO[];
@IsObject()
@IsOptional()
@Field(() => graphqlTypeJson, { nullable: true })
databaseEventTriggerSettings?: DatabaseEventTriggerSettings;
@Field(() => [RouteTriggerDTO], { nullable: true })
routeTriggers?: RouteTriggerDTO[];
@IsObject()
@IsOptional()
@Field(() => graphqlTypeJson, { nullable: true })
httpRouteTriggerSettings?: HttpRouteTriggerSettings;
@IsUUID()
@IsOptional()
@@ -1,3 +1,4 @@
import { HTTPMethod } from 'twenty-shared/types';
import {
Check,
Column,
@@ -7,19 +8,31 @@ import {
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
export type CronTriggerSettings = {
pattern: string;
};
export type DatabaseEventTriggerSettings = {
eventName: string;
updatedFields?: string[];
};
export type HttpRouteTriggerSettings = {
path: string;
httpMethod: HTTPMethod;
isAuthRequired: boolean;
forwardedRequestHeaders?: string[];
};
const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
export enum ServerlessFunctionRuntime {
@@ -89,32 +102,14 @@ export class ServerlessFunctionEntity
@JoinColumn({ name: 'serverlessFunctionLayerId' })
serverlessFunctionLayer: Relation<ServerlessFunctionLayerEntity>;
@OneToMany(
() => CronTriggerEntity,
(cronTrigger) => cronTrigger.serverlessFunction,
{
cascade: true,
},
)
cronTriggers: CronTriggerEntity[];
@Column({ nullable: true, type: 'jsonb' })
cronTriggerSettings: JsonbProperty<CronTriggerSettings> | null;
@OneToMany(
() => DatabaseEventTriggerEntity,
(databaseEventTrigger) => databaseEventTrigger.serverlessFunction,
{
cascade: true,
},
)
databaseEventTriggers: DatabaseEventTriggerEntity[];
@Column({ nullable: true, type: 'jsonb' })
databaseEventTriggerSettings: JsonbProperty<DatabaseEventTriggerSettings> | null;
@OneToMany(
() => RouteTriggerEntity,
(routeTrigger) => routeTrigger.serverlessFunction,
{
cascade: true,
},
)
routeTriggers: RouteTriggerEntity[];
@Column({ nullable: true, type: 'jsonb' })
httpRouteTriggerSettings: JsonbProperty<HttpRouteTriggerSettings> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -12,11 +12,8 @@ import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module';
import { ServerlessFunctionTriggerJob } from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@@ -33,12 +30,7 @@ import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/
imports: [
FileUploadModule,
NestjsQueryTypeOrmModule.forFeature([ServerlessFunctionEntity]),
TypeOrmModule.forFeature([
FeatureFlagEntity,
DatabaseEventTriggerEntity,
CronTriggerEntity,
RouteTriggerEntity,
]),
TypeOrmModule.forFeature([FeatureFlagEntity]),
FileModule,
ThrottlerModule,
ApplicationModule,
@@ -52,21 +52,11 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ServerlessFunctionDTO> {
try {
const {
flatServerlessFunctionMaps,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
} =
const { flatServerlessFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatServerlessFunctionMaps',
'flatCronTriggerMaps',
'flatDatabaseEventTriggerMaps',
'flatRouteTriggerMaps',
],
flatMapsKeys: ['flatServerlessFunctionMaps'],
},
);
@@ -77,9 +67,6 @@ export class ServerlessFunctionResolver {
return fromFlatServerlessFunctionToServerlessFunctionDto({
flatServerlessFunction,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
});
} catch (error) {
return serverlessFunctionGraphQLApiExceptionHandler(error);
@@ -91,21 +78,11 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ServerlessFunctionDTO[]> {
try {
const {
flatServerlessFunctionMaps,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
} =
const { flatServerlessFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatServerlessFunctionMaps',
'flatCronTriggerMaps',
'flatDatabaseEventTriggerMaps',
'flatRouteTriggerMaps',
],
flatMapsKeys: ['flatServerlessFunctionMaps'],
},
);
@@ -120,9 +97,6 @@ export class ServerlessFunctionResolver {
.map((flatServerlessFunction) =>
fromFlatServerlessFunctionToServerlessFunctionDto({
flatServerlessFunction,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
}),
);
} catch (error) {
@@ -168,27 +142,8 @@ export class ServerlessFunctionResolver {
workspaceId,
});
const {
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatCronTriggerMaps',
'flatDatabaseEventTriggerMaps',
'flatRouteTriggerMaps',
],
},
);
return fromFlatServerlessFunctionToServerlessFunctionDto({
flatServerlessFunction,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
});
} catch (error) {
return serverlessFunctionGraphQLApiExceptionHandler(error);
@@ -209,27 +164,8 @@ export class ServerlessFunctionResolver {
workspaceId,
);
const {
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatCronTriggerMaps',
'flatDatabaseEventTriggerMaps',
'flatRouteTriggerMaps',
],
},
);
return fromFlatServerlessFunctionToServerlessFunctionDto({
flatServerlessFunction,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
});
} catch (error) {
return serverlessFunctionGraphQLApiExceptionHandler(error);
@@ -250,27 +186,8 @@ export class ServerlessFunctionResolver {
workspaceId,
);
const {
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatCronTriggerMaps',
'flatDatabaseEventTriggerMaps',
'flatRouteTriggerMaps',
],
},
);
return fromFlatServerlessFunctionToServerlessFunctionDto({
flatServerlessFunction,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
});
} catch (error) {
return serverlessFunctionGraphQLApiExceptionHandler(error);
@@ -312,27 +229,8 @@ export class ServerlessFunctionResolver {
workspaceId,
);
const {
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatCronTriggerMaps',
'flatDatabaseEventTriggerMaps',
'flatRouteTriggerMaps',
],
},
);
return fromFlatServerlessFunctionToServerlessFunctionDto({
flatServerlessFunction,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
});
} catch (error) {
return serverlessFunctionGraphQLApiExceptionHandler(error);
@@ -5,16 +5,12 @@ import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.util';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -25,12 +21,6 @@ export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceCac
constructor(
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
@InjectRepository(DatabaseEventTriggerEntity)
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTriggerEntity>,
@InjectRepository(CronTriggerEntity)
private readonly cronTriggerRepository: Repository<CronTriggerEntity>,
@InjectRepository(RouteTriggerEntity)
private readonly routeTriggerRepository: Repository<RouteTriggerEntity>,
) {
super();
}
@@ -38,73 +28,18 @@ export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceCac
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatServerlessFunction>> {
const [
serverlessFunctions,
routeTriggers,
cronTriggers,
databaseEventTriggers,
] = await Promise.all([
this.serverlessFunctionRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.cronTriggerRepository.find({
where: { workspaceId },
select: ['id', 'serverlessFunctionId'],
withDeleted: true,
}),
this.routeTriggerRepository.find({
where: { workspaceId },
select: ['id', 'serverlessFunctionId'],
withDeleted: true,
}),
this.databaseEventTriggerRepository.find({
where: { workspaceId },
select: ['id', 'serverlessFunctionId'],
withDeleted: true,
}),
]);
const [
routeTriggersByServerlessFunctionId,
cronTriggersByServerlessFunctionId,
databaseEventTriggersByServerlessFunctionId,
] = (
[
{
entities: routeTriggers,
foreignKey: 'serverlessFunctionId',
},
{
entities: cronTriggers,
foreignKey: 'serverlessFunctionId',
},
{
entities: databaseEventTriggers,
foreignKey: 'serverlessFunctionId',
},
] as const
).map(regroupEntitiesByRelatedEntityId);
const serverlessFunctions = await this.serverlessFunctionRepository.find({
where: { workspaceId },
withDeleted: true,
});
const flatServerlessFunctionMaps = createEmptyFlatEntityMaps();
for (const serverlessFunctionEntity of serverlessFunctions) {
const flatServerlessFunction =
fromServerlessFunctionEntityToFlatServerlessFunction({
...serverlessFunctionEntity,
routeTriggers:
routeTriggersByServerlessFunctionId.get(
serverlessFunctionEntity.id,
) || [],
cronTriggers:
cronTriggersByServerlessFunctionId.get(
serverlessFunctionEntity.id,
) || [],
databaseEventTriggers:
databaseEventTriggersByServerlessFunctionId.get(
serverlessFunctionEntity.id,
) || [],
} as ServerlessFunctionEntity);
fromServerlessFunctionEntityToFlatServerlessFunction(
serverlessFunctionEntity,
);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatServerlessFunction,
@@ -29,10 +29,10 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
const currentDate = new Date();
return {
cronTriggerIds: [],
databaseEventTriggerIds: [],
routeTriggerIds: [],
id,
cronTriggerSettings: null,
databaseEventTriggerSettings: null,
httpRouteTriggerSettings: null,
name: rawCreateServerlessFunctionInput.name,
description: rawCreateServerlessFunctionInput.description ?? null,
sourceHandlerPath:
@@ -1,41 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { fromFlatCronTriggerToCronTriggerDto } from 'src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util';
import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto } from 'src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { fromFlatRouteTriggerToRouteTriggerDto } from 'src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util';
import { type ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
export const fromFlatServerlessFunctionToServerlessFunctionDto = ({
flatServerlessFunction,
flatCronTriggerMaps,
flatDatabaseEventTriggerMaps,
flatRouteTriggerMaps,
}: {
flatServerlessFunction: FlatServerlessFunction;
flatCronTriggerMaps: FlatEntityMaps<FlatCronTrigger>;
flatDatabaseEventTriggerMaps: FlatEntityMaps<FlatDatabaseEventTrigger>;
flatRouteTriggerMaps: FlatEntityMaps<FlatRouteTrigger>;
}): ServerlessFunctionDTO => {
const cronTriggers = flatServerlessFunction.cronTriggerIds
.map((id) => flatCronTriggerMaps.byId[id])
.filter(isDefined)
.map(fromFlatCronTriggerToCronTriggerDto);
const databaseEventTriggers = flatServerlessFunction.databaseEventTriggerIds
.map((id) => flatDatabaseEventTriggerMaps.byId[id])
.filter(isDefined)
.map(fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto);
const routeTriggers = flatServerlessFunction.routeTriggerIds
.map((id) => flatRouteTriggerMaps.byId[id])
.filter(isDefined)
.map(fromFlatRouteTriggerToRouteTriggerDto);
return {
id: flatServerlessFunction.id,
name: flatServerlessFunction.name,
@@ -53,8 +23,11 @@ export const fromFlatServerlessFunctionToServerlessFunctionDto = ({
workspaceId: flatServerlessFunction.workspaceId,
createdAt: new Date(flatServerlessFunction.createdAt),
updatedAt: new Date(flatServerlessFunction.updatedAt),
cronTriggers,
databaseEventTriggers,
routeTriggers,
cronTriggerSettings:
flatServerlessFunction.cronTriggerSettings ?? undefined,
databaseEventTriggerSettings:
flatServerlessFunction.databaseEventTriggerSettings ?? undefined,
httpRouteTriggerSettings:
flatServerlessFunction.httpRouteTriggerSettings ?? undefined,
};
};
@@ -8,13 +8,7 @@ export const fromServerlessFunctionEntityToFlatServerlessFunction = (
): FlatServerlessFunction => {
const serverlessFunctionWithoutRelations = removePropertiesFromRecord(
serverlessFunctionEntity,
[
'databaseEventTriggers',
'routeTriggers',
'cronTriggers',
'serverlessFunctionLayer',
'application',
],
['serverlessFunctionLayer', 'application'],
);
return {
@@ -22,12 +16,6 @@ export const fromServerlessFunctionEntityToFlatServerlessFunction = (
createdAt: serverlessFunctionEntity.createdAt.toISOString(),
updatedAt: serverlessFunctionEntity.updatedAt.toISOString(),
deletedAt: serverlessFunctionEntity.deletedAt?.toISOString() ?? null,
cronTriggerIds:
serverlessFunctionEntity.cronTriggers.map((el) => el.id) ?? [],
routeTriggerIds:
serverlessFunctionEntity.routeTriggers.map((el) => el.id) ?? [],
databaseEventTriggerIds:
serverlessFunctionEntity.databaseEventTriggers.map((el) => el.id) ?? [],
universalIdentifier: serverlessFunctionEntity.universalIdentifier,
};
};