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:
-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;
|
||||
};
|
||||
Reference in New Issue
Block a user