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:
-5
@@ -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)[];
|
||||
+4
-25
@@ -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 {}
|
||||
|
||||
+18
-17
@@ -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: {},
|
||||
},
|
||||
],
|
||||
|
||||
-25
@@ -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;
|
||||
}
|
||||
-11
@@ -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;
|
||||
}
|
||||
-28
@@ -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;
|
||||
}
|
||||
-32
@@ -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;
|
||||
}
|
||||
-50
@@ -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;
|
||||
}
|
||||
-39
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
-145
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-193
@@ -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;
|
||||
}
|
||||
}
|
||||
-59
@@ -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;
|
||||
}
|
||||
}
|
||||
-6
@@ -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'
|
||||
>;
|
||||
-4
@@ -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>;
|
||||
-23
@@ -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;
|
||||
};
|
||||
-9
@@ -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;
|
||||
};
|
||||
-28
@@ -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,
|
||||
};
|
||||
};
|
||||
-11
@@ -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),
|
||||
});
|
||||
-31
@@ -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(),
|
||||
};
|
||||
};
|
||||
-5
@@ -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)[];
|
||||
+4
-23
@@ -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 {}
|
||||
|
||||
-25
@@ -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;
|
||||
}
|
||||
-11
@@ -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;
|
||||
}
|
||||
-28
@@ -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;
|
||||
}
|
||||
-32
@@ -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;
|
||||
}
|
||||
-53
@@ -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;
|
||||
}
|
||||
-43
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
+16
-13
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
-150
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-202
@@ -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;
|
||||
}
|
||||
}
|
||||
-60
@@ -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;
|
||||
}
|
||||
}
|
||||
-5
@@ -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>;
|
||||
+68
-63
@@ -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);
|
||||
|
||||
-25
@@ -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;
|
||||
};
|
||||
-29
@@ -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,
|
||||
};
|
||||
};
|
||||
-11
@@ -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),
|
||||
});
|
||||
-32
@@ -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(),
|
||||
};
|
||||
};
|
||||
+8
-7
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
+5
-8
@@ -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
-6
@@ -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,
|
||||
|
||||
+7
-23
@@ -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: {},
|
||||
},
|
||||
|
||||
+15
-33
@@ -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: {
|
||||
|
||||
+5
-23
@@ -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`] = `[]`;
|
||||
|
||||
|
||||
+5
-5
@@ -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: [],
|
||||
|
||||
-8
@@ -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)[];
|
||||
-51
@@ -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;
|
||||
}
|
||||
-11
@@ -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;
|
||||
}
|
||||
-32
@@ -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;
|
||||
}
|
||||
-59
@@ -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;
|
||||
}
|
||||
-149
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-66
@@ -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;
|
||||
}
|
||||
+4
-20
@@ -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 {}
|
||||
|
||||
+38
-28
@@ -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',
|
||||
});
|
||||
|
||||
-192
@@ -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;
|
||||
}
|
||||
}
|
||||
-60
@@ -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;
|
||||
}
|
||||
}
|
||||
-4
@@ -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>;
|
||||
-33
@@ -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,
|
||||
};
|
||||
};
|
||||
-14
@@ -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),
|
||||
});
|
||||
-36
@@ -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,
|
||||
};
|
||||
};
|
||||
-23
@@ -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;
|
||||
};
|
||||
+17
-9
@@ -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()
|
||||
|
||||
+23
-28
@@ -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;
|
||||
|
||||
+1
-9
@@ -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,
|
||||
|
||||
+4
-106
@@ -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);
|
||||
|
||||
+7
-72
@@ -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,
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+6
-33
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+1
-13
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user