Add custom routes to migration v2 (#14846)

## Context
Add routes to migration V2
- Resolvers
- Service v2
- Builder
- Validator
- Action runner

Next PR: Add to twenty-cli to sync routes with serverless
This commit is contained in:
Weiko
2025-10-03 00:20:21 +02:00
committed by GitHub
parent dcfae3821c
commit 0b60aa4249
80 changed files with 2242 additions and 151 deletions
@@ -1,19 +1,32 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
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()
export class UpdateCronTriggerInput {
@IsUUID()
@IsNotEmpty()
@Field(() => String)
id: string;
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;
}
@@ -40,8 +40,8 @@ export class CronTrigger extends SyncableEntity {
@JoinColumn({ name: 'serverlessFunctionId' })
serverlessFunction: Relation<ServerlessFunctionEntity>;
@Column({ nullable: true, type: 'uuid' })
serverlessFunctionId: string | null;
@Column({ nullable: false, type: 'uuid' })
serverlessFunctionId: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@@ -9,4 +9,5 @@ export class CronTriggerException extends CustomException {
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',
}
@@ -36,7 +36,7 @@ export class CronTriggerV2Service {
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatCronTriggerMaps'],
flatEntities: ['flatCronTriggerMaps', 'flatServerlessFunctionMaps'],
},
);
@@ -64,6 +64,10 @@ export class CronTriggerV2Service {
to: toFlatCronTriggerMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps:
flatEntityMaps.flatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -82,7 +86,7 @@ export class CronTriggerV2Service {
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatCronTriggerMaps'],
flatEntities: ['flatCronTriggerMaps', 'flatServerlessFunctionMaps'],
},
);
@@ -100,7 +104,7 @@ export class CronTriggerV2Service {
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatCronTriggerMaps'],
flatEntities: ['flatCronTriggerMaps', 'flatServerlessFunctionMaps'],
},
);
@@ -131,6 +135,10 @@ export class CronTriggerV2Service {
to: toFlatCronTriggerMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps:
flatEntityMaps.flatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -166,11 +174,14 @@ export class CronTriggerV2Service {
destroyCronTriggerInput: CronTriggerIdInput;
workspaceId: string;
}): Promise<FlatCronTrigger> {
const { flatCronTriggerMaps: existingFlatCronTriggerMaps } =
const {
flatCronTriggerMaps: existingFlatCronTriggerMaps,
flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatCronTriggerMaps'],
flatEntities: ['flatCronTriggerMaps', 'flatServerlessFunctionMaps'],
},
);
@@ -202,6 +213,9 @@ export class CronTriggerV2Service {
to: toFlatCronTriggerMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
@@ -25,7 +25,7 @@ export const fromUpdateCronTriggerInputToFlatCronTriggerToUpdateOrThrow = ({
return {
...existingFlatCronTrigger,
settings: updateCronTriggerInput.settings,
settings: updateCronTriggerInput.update.settings,
updatedAt: new Date(),
};
};
@@ -1,19 +1,32 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
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()
export class UpdateDatabaseEventTriggerInput {
@IsUUID()
@IsNotEmpty()
@Field()
id: string;
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;
}
@@ -43,8 +43,8 @@ export class DatabaseEventTrigger
@JoinColumn({ name: 'serverlessFunctionId' })
serverlessFunction: Relation<ServerlessFunctionEntity>;
@Column({ nullable: true, type: 'uuid' })
serverlessFunctionId: string | null;
@Column({ nullable: false, type: 'uuid' })
serverlessFunctionId: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@@ -9,4 +9,6 @@ export class DatabaseEventTriggerException extends CustomException {
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',
}
@@ -36,7 +36,10 @@ export class DatabaseEventTriggerV2Service {
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatDatabaseEventTriggerMaps'],
flatEntities: [
'flatDatabaseEventTriggerMaps',
'flatServerlessFunctionMaps',
],
},
);
@@ -66,6 +69,10 @@ export class DatabaseEventTriggerV2Service {
to: toFlatDatabaseEventTriggerMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps:
flatEntityMaps.flatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -105,7 +112,10 @@ export class DatabaseEventTriggerV2Service {
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatDatabaseEventTriggerMaps'],
flatEntities: [
'flatDatabaseEventTriggerMaps',
'flatServerlessFunctionMaps',
],
},
);
@@ -140,6 +150,10 @@ export class DatabaseEventTriggerV2Service {
to: toFlatDatabaseEventTriggerMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps:
flatEntityMaps.flatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
@@ -180,11 +194,15 @@ export class DatabaseEventTriggerV2Service {
}): Promise<FlatDatabaseEventTrigger> {
const {
flatDatabaseEventTriggerMaps: existingFlatDatabaseEventTriggerMaps,
flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatDatabaseEventTriggerMaps'],
flatEntities: [
'flatDatabaseEventTriggerMaps',
'flatServerlessFunctionMaps',
],
},
);
@@ -219,6 +237,9 @@ export class DatabaseEventTriggerV2Service {
to: toFlatDatabaseEventTriggerMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
@@ -26,7 +26,7 @@ export const fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpda
return {
...existingFlatDatabaseEventTrigger,
settings: updateDatabaseEventTriggerInput.settings,
settings: updateDatabaseEventTriggerInput.update.settings,
updatedAt: new Date(),
};
};
@@ -38,6 +38,7 @@ export const throwOnFieldInputTranspilationsError: ThrowOnFieldInputTranspilatio
serverlessFunction: [],
databaseEventTrigger: [],
cronTrigger: [],
routeTrigger: [],
},
status: 'fail',
},
@@ -13,7 +13,7 @@ import { SearchFieldMetadataModule } from 'src/engine/metadata-modules/search-fi
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { RouteModule } from 'src/engine/metadata-modules/route/route.module';
import { RouteTriggerModule } from 'src/engine/metadata-modules/route-trigger/route-trigger.module';
import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module';
@Module({
@@ -30,7 +30,7 @@ import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serve
RemoteServerModule,
RoleModule,
PermissionsModule,
RouteModule,
RouteTriggerModule,
CronTriggerModule,
DatabaseEventTriggerModule,
],
@@ -0,0 +1,7 @@
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',
] as const satisfies (keyof FlatRouteTrigger)[];
@@ -0,0 +1,6 @@
import { FLAT_ROUTE_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/route-trigger/constants/flat-route-trigger-editable-properties.constant';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
export const FLAT_ROUTE_TRIGGER_PROPERTIES_TO_COMPARE = [
...FLAT_ROUTE_TRIGGER_EDITABLE_PROPERTIES,
] as const satisfies (keyof FlatRouteTrigger)[];
@@ -0,0 +1,37 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsEnum,
IsNotEmpty,
IsString,
IsUUID,
} from 'class-validator';
import { HTTPMethod } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
@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;
@HideField()
universalIdentifier?: string;
}
@@ -0,0 +1,11 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsUUID } from 'class-validator';
@InputType()
export class RouteTriggerIdInput {
@IsUUID()
@IsNotEmpty()
@Field()
id: string;
}
@@ -0,0 +1,32 @@
import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql';
import { HTTPMethod } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
registerEnumType(HTTPMethod, { name: 'HTTPMethod' });
@ObjectType('RouteTrigger')
export class RouteTriggerDTO {
@Field(() => ID)
id: string;
@Field()
path: string;
@Field()
isAuthRequired: boolean;
@Field(() => HTTPMethod)
httpMethod: HTTPMethod;
@Field()
serverlessFunctionId: string;
@Field()
workspaceId: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
}
@@ -0,0 +1,48 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsEnum,
IsNotEmpty,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import { HTTPMethod } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
@InputType()
class UpdateRouteTriggerInputUpdates {
@IsString()
@IsNotEmpty()
@Field()
path: string;
@IsBoolean()
@IsNotEmpty()
@Field()
isAuthRequired: boolean;
@IsEnum(HTTPMethod)
@IsNotEmpty()
@Field(() => HTTPMethod)
httpMethod: HTTPMethod;
}
@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;
}
@@ -0,0 +1,16 @@
import { CustomException } from 'src/utils/custom-exception';
export class RouteTriggerException extends CustomException {
code: RouteTriggerExceptionCode;
constructor(message: string, code: RouteTriggerExceptionCode) {
super(message, code);
}
}
export enum RouteTriggerExceptionCode {
ROUTE_NOT_FOUND = 'ROUTE_NOT_FOUND',
ROUTE_ALREADY_EXIST = 'ROUTE_ALREADY_EXIST',
ROUTE_INVALID = 'ROUTE_INVALID',
ROUTE_PATH_ALREADY_EXIST = 'ROUTE_PATH_ALREADY_EXIST',
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
}
@@ -0,0 +1,99 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { 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 { RouteTrigger } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { RouteTriggerV2Service } from 'src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service';
import { routeTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
@UsePipes(ResolverValidationPipe)
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
export class RouteTriggerResolver {
constructor(
private readonly routeV2Service: RouteTriggerV2Service,
@InjectRepository(RouteTrigger)
private readonly routeTriggerRepository: Repository<RouteTrigger>,
) {}
@Query(() => RouteTriggerDTO)
async findOneRouteTrigger(
@Args('input') { id }: RouteTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.routeTriggerRepository.findOneOrFail({
where: {
id,
workspaceId,
},
});
} catch (error) {
routeTriggerGraphQLApiExceptionHandler(error);
}
}
@Query(() => [RouteTriggerDTO])
async findManyRouteTriggers(@AuthWorkspace() { id: workspaceId }: Workspace) {
try {
return await this.routeTriggerRepository.find({
where: { workspaceId },
});
} catch (error) {
routeTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => RouteTriggerDTO)
async deleteOneRouteTrigger(
@Args('input') input: RouteTriggerIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.routeV2Service.destroyOne({
destroyRouteInput: input,
workspaceId,
});
} catch (error) {
routeTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => RouteTriggerDTO)
async updateOneRouteTrigger(
@Args('input')
input: UpdateRouteTriggerInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.routeV2Service.updateOne(input, workspaceId);
} catch (error) {
routeTriggerGraphQLApiExceptionHandler(error);
}
}
@Mutation(() => RouteTriggerDTO)
async createOneRouteTrigger(
@Args('input')
input: CreateRouteTriggerInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
return await this.routeV2Service.createOne(input, workspaceId);
} catch (error) {
routeTriggerGraphQLApiExceptionHandler(error);
}
}
}
@@ -12,20 +12,20 @@ import {
import { Request } from 'express';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { RouteService } from 'src/engine/metadata-modules/route/route.service';
import { HTTPMethod } from 'src/engine/metadata-modules/route/route.entity';
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { HTTPMethod } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { RouteTriggerService } from 'src/engine/metadata-modules/route-trigger/route-trigger.service';
@Controller('s')
@UseGuards(PublicEndpointGuard)
@UseFilters(RestApiExceptionFilter)
export class RouteController {
constructor(private readonly routeService: RouteService) {}
export class RouteTriggerController {
constructor(private readonly routeTriggerService: RouteTriggerService) {}
@Get('*')
async get(@Req() request: Request) {
return await this.routeService.handle({
return await this.routeTriggerService.handle({
request,
httpMethod: HTTPMethod.GET,
});
@@ -33,7 +33,7 @@ export class RouteController {
@Post('*')
async post(@Req() request: Request) {
return await this.routeService.handle({
return await this.routeTriggerService.handle({
request,
httpMethod: HTTPMethod.POST,
});
@@ -41,7 +41,7 @@ export class RouteController {
@Put('*')
async put(@Req() request: Request) {
return await this.routeService.handle({
return await this.routeTriggerService.handle({
request,
httpMethod: HTTPMethod.PUT,
});
@@ -49,7 +49,7 @@ export class RouteController {
@Patch('*')
async patch(@Req() request: Request) {
return await this.routeService.handle({
return await this.routeTriggerService.handle({
request,
httpMethod: HTTPMethod.PATCH,
});
@@ -57,7 +57,7 @@ export class RouteController {
@Delete('*')
async delete(@Req() request: Request) {
return await this.routeService.handle({
return await this.routeTriggerService.handle({
request,
httpMethod: HTTPMethod.DELETE,
});
@@ -6,12 +6,13 @@ import {
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface';
import { RouteTriggerEntityRelationProperties } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
export enum HTTPMethod {
@@ -22,13 +23,20 @@ export enum HTTPMethod {
DELETE = 'DELETE',
}
@Entity({ name: 'route', schema: 'core' })
@Unique('IDX_ROUTE_PATH_HTTP_METHOD_WORKSPACE_ID_UNIQUE', [
export const ROUTE_TRIGGER_ENTITY_RELATION_PROPERTIES = [
'serverlessFunction',
] as const satisfies readonly RouteTriggerEntityRelationProperties[];
@Entity({ name: 'routeTrigger', schema: 'core' })
@Unique('IDX_ROUTE_TRIGGER_PATH_HTTP_METHOD_WORKSPACE_ID_UNIQUE', [
'path',
'httpMethod',
'workspaceId',
])
export class Route extends SyncableEntity {
export class RouteTrigger
extends SyncableEntity
implements Required<RouteTrigger>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -48,12 +56,15 @@ export class Route extends SyncableEntity {
@ManyToOne(
() => ServerlessFunctionEntity,
(serverlessFunction) => serverlessFunction.routes,
(serverlessFunction) => serverlessFunction.routeTriggers,
{ onDelete: 'CASCADE' },
)
@JoinColumn({ name: 'serverlessFunctionId' })
serverlessFunction: Relation<ServerlessFunctionEntity>;
@Column({ nullable: false, type: 'uuid' })
serverlessFunctionId: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@@ -0,0 +1,34 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.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 { RouteTrigger } 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 { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@Module({
imports: [
TypeOrmModule.forFeature([RouteTrigger]),
AuthModule,
DomainManagerModule,
ServerlessFunctionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
],
controllers: [RouteTriggerController],
providers: [
RouteTriggerService,
RouteTriggerV2Service,
WorkspaceFlatRouteTriggerMapCacheService,
RouteTriggerResolver,
],
exports: [RouteTriggerV2Service, WorkspaceFlatRouteTriggerMapCacheService],
})
export class RouteTriggerModule {}
@@ -12,8 +12,8 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import {
HTTPMethod,
Route,
} from 'src/engine/metadata-modules/route/route.entity';
RouteTrigger,
} from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import {
@@ -23,23 +23,23 @@ import {
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Injectable()
export class RouteService {
export class RouteTriggerService {
constructor(
private readonly accessTokenService: AccessTokenService,
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly domainManagerService: DomainManagerService,
@InjectRepository(Route)
private readonly routeRepository: Repository<Route>,
@InjectRepository(RouteTrigger)
private readonly routeTriggerRepository: Repository<RouteTrigger>,
) {}
private async getOneRouteWithPathParamsOrFail({
private async getOneRouteTriggerWithPathParamsOrFail({
request,
httpMethod,
}: {
request: Request;
httpMethod: HTTPMethod;
}): Promise<{
route: Route;
routeTrigger: RouteTrigger;
pathParams: Partial<Record<string, string | string[]>>;
}> {
const host = `${request.protocol}://${request.get('host')}`;
@@ -57,7 +57,7 @@ export class RouteService {
),
);
const routes = await this.routeRepository.find({
const routeTriggers = await this.routeTriggerRepository.find({
where: {
httpMethod,
workspaceId: workspace.id,
@@ -67,19 +67,21 @@ export class RouteService {
const requestPath = request.path.replace(/^\/s\//, '/');
for (const route of routes) {
const routeMatcher = match(route.path, { decode: decodeURIComponent });
const routeMatched = routeMatcher(requestPath);
for (const routeTrigger of routeTriggers) {
const routeTriggerMatcher = match(routeTrigger.path, {
decode: decodeURIComponent,
});
const routeTriggerMatched = routeTriggerMatcher(requestPath);
if (routeMatched) {
if (routeTriggerMatched) {
return {
route,
pathParams: routeMatched.params,
routeTrigger,
pathParams: routeTriggerMatched.params,
};
}
}
throw new NotFoundException('No Route found');
throw new NotFoundException('No Route trigger found');
}
private async validateWorkspaceFromRequest({
@@ -108,15 +110,16 @@ export class RouteService {
request: Request;
httpMethod: HTTPMethod;
}) {
const routeWithPathParams = await this.getOneRouteWithPathParamsOrFail({
request,
httpMethod,
});
const routeTriggerWithPathParams =
await this.getOneRouteTriggerWithPathParamsOrFail({
request,
httpMethod,
});
if (routeWithPathParams.route.isAuthRequired) {
if (routeTriggerWithPathParams.routeTrigger.isAuthRequired) {
await this.validateWorkspaceFromRequest({
request,
workspaceId: routeWithPathParams.route.workspaceId,
workspaceId: routeTriggerWithPathParams.routeTrigger.workspaceId,
});
}
@@ -127,12 +130,12 @@ export class RouteService {
const executionParams = {
...queryParams,
...bodyParams,
...routeWithPathParams.pathParams,
...routeTriggerWithPathParams.pathParams,
};
return await this.serverlessFunctionService.executeOneServerlessFunction(
routeWithPathParams.route.serverlessFunction.id,
routeWithPathParams.route.workspaceId,
routeTriggerWithPathParams.routeTrigger.serverlessFunction.id,
routeTriggerWithPathParams.routeTrigger.workspaceId,
executionParams,
'draft',
);
@@ -0,0 +1,228 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/get-sub-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { 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 { 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 { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class RouteTriggerV2Service {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {}
async createOne(routeInput: CreateRouteTriggerInput, workspaceId: string) {
const flatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatRouteTriggerMaps', 'flatServerlessFunctionMaps'],
},
);
const existingFlatRouteMaps = flatEntityMaps.flatRouteTriggerMaps;
const flatRouteTriggerToCreate =
fromCreateRouteTriggerInputToFlatRouteTrigger({
createRouteTriggerInput: routeInput,
workspaceId,
});
const toFlatRouteMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: flatRouteTriggerToCreate,
flatEntityMaps: existingFlatRouteMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
workspaceId,
fromToAllFlatEntityMaps: {
flatRouteTriggerMaps: {
from: existingFlatRouteMaps,
to: toFlatRouteMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps:
flatEntityMaps.flatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while creating route',
);
}
const { flatRouteTriggerMaps: recomputedExistingFlatRouteMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatRouteTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatRouteTriggerToCreate.id,
flatEntityMaps: recomputedExistingFlatRouteMaps,
});
}
async updateOne(routeInput: UpdateRouteTriggerInput, workspaceId: string) {
const flatEntityMaps =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatRouteTriggerMaps', 'flatServerlessFunctionMaps'],
},
);
const existingFlatRouteMaps = flatEntityMaps.flatRouteTriggerMaps;
const optimisticallyUpdatedFlatRouteTrigger =
fromUpdateRouteTriggerInputToFlatRouteTriggerToUpdateOrThrow({
flatRouteTriggerMaps: existingFlatRouteMaps,
updateRouteTriggerInput: routeInput,
});
const fromFlatRouteMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [optimisticallyUpdatedFlatRouteTrigger.id],
flatEntityMaps: existingFlatRouteMaps,
});
const toFlatRouteMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: optimisticallyUpdatedFlatRouteTrigger,
flatEntityMaps: fromFlatRouteMaps,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
workspaceId,
fromToAllFlatEntityMaps: {
flatRouteTriggerMaps: {
from: existingFlatRouteMaps,
to: toFlatRouteMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps:
flatEntityMaps.flatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: false,
},
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while updating route',
);
}
const { flatRouteTriggerMaps: recomputedExistingFlatRouteMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatRouteTriggerMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatRouteTrigger.id,
flatEntityMaps: recomputedExistingFlatRouteMaps,
});
}
async destroyOne({
destroyRouteInput,
workspaceId,
}: {
destroyRouteInput: RouteTriggerIdInput;
workspaceId: string;
}): Promise<FlatRouteTrigger> {
const {
flatRouteTriggerMaps: existingFlatRouteMaps,
flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatEntities: ['flatRouteTriggerMaps', 'flatServerlessFunctionMaps'],
},
);
const existingFlatRoute = existingFlatRouteMaps.byId[destroyRouteInput.id];
if (!isDefined(existingFlatRoute)) {
throw new RouteTriggerException(
'Route to destroy not found',
RouteTriggerExceptionCode.ROUTE_NOT_FOUND,
);
}
const fromFlatRouteMaps = getSubFlatEntityMapsOrThrow({
flatEntityIds: [existingFlatRoute.id],
flatEntityMaps: existingFlatRouteMaps,
});
const toFlatRouteMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
flatEntityMaps: fromFlatRouteMaps,
entityToDeleteId: existingFlatRoute.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
fromToAllFlatEntityMaps: {
flatRouteTriggerMaps: {
from: fromFlatRouteMaps,
to: toFlatRouteMaps,
},
},
dependencyAllFlatEntityMaps: {
flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps,
},
buildOptions: {
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
},
workspaceId,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while destroying route',
);
}
return existingFlatRoute;
}
}
@@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/core-modules/common/constant/empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import {
ROUTE_TRIGGER_ENTITY_RELATION_PROPERTIES,
RouteTrigger,
} 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 { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator';
import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service';
@Injectable()
@WorkspaceFlatMapCache('flatRouteTriggerMaps')
export class WorkspaceFlatRouteTriggerMapCacheService extends WorkspaceFlatMapCacheService<
FlatEntityMaps<FlatRouteTrigger>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(RouteTrigger)
private readonly routeTriggerRepository: Repository<RouteTrigger>,
) {
super(cacheStorageService);
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatRouteTrigger>> {
const routeTriggers = await this.routeTriggerRepository.find({
where: {
workspaceId,
},
});
const flatRouteTriggerMaps = routeTriggers.reduce<
FlatEntityMaps<FlatRouteTrigger>
>((flatEntityMaps, routeTrigger) => {
const flatRouteTrigger = {
...removePropertiesFromRecord(routeTrigger, [
...ROUTE_TRIGGER_ENTITY_RELATION_PROPERTIES,
]),
universalIdentifier: routeTrigger.universalIdentifier ?? '',
} satisfies FlatRouteTrigger;
return {
byId: {
...flatEntityMaps.byId,
[flatRouteTrigger.id]: flatRouteTrigger,
},
idByUniversalIdentifier: {
...flatEntityMaps.idByUniversalIdentifier,
[flatRouteTrigger.universalIdentifier]: flatRouteTrigger.id,
},
};
}, EMPTY_FLAT_ENTITY_MAPS);
return flatRouteTriggerMaps;
}
}
@@ -0,0 +1,4 @@
import { type FLAT_ROUTE_TRIGGER_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/route-trigger/constants/flat-route-trigger-properties-to-compare.constant';
export type FlatRouteTriggerPropertiesToCompare =
(typeof FLAT_ROUTE_TRIGGER_PROPERTIES_TO_COMPARE)[number];
@@ -0,0 +1,17 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type RouteTrigger } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
export type RouteTriggerEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
RouteTrigger,
ServerlessFunctionEntity | Workspace
>;
export type FlatRouteTrigger = Omit<
RouteTrigger,
RouteTriggerEntityRelationProperties
> & {
universalIdentifier: string;
};
@@ -0,0 +1,59 @@
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { FLAT_ROUTE_TRIGGER_PROPERTIES_TO_COMPARE } from 'src/engine/metadata-modules/route-trigger/constants/flat-route-trigger-properties-to-compare.constant';
import { type FlatRouteTriggerPropertiesToCompare } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger-properties-to-compare.type';
import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { type UpdateRouteTriggerAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-route-trigger-action-v2.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type GetWorkspaceMigrationUpdateRouteTriggerActionArgs = FromTo<
FlatRouteTrigger,
'FlatRouteTrigger'
>;
export const compareTwoFlatRouteTrigger = ({
fromFlatRouteTrigger,
toFlatRouteTrigger,
}: GetWorkspaceMigrationUpdateRouteTriggerActionArgs) => {
const transformMetadataForComparisonParameters = {
shouldIgnoreProperty: (property: string) =>
!FLAT_ROUTE_TRIGGER_PROPERTIES_TO_COMPARE.includes(
property as FlatRouteTriggerPropertiesToCompare,
),
propertiesToStringify: [] as const,
};
const fromCompare = transformMetadataForComparison(
fromFlatRouteTrigger,
transformMetadataForComparisonParameters,
);
const toCompare = transformMetadataForComparison(
toFlatRouteTrigger,
transformMetadataForComparisonParameters,
);
const flatRouteTriggerDifferences = diff(fromCompare, toCompare);
return flatRouteTriggerDifferences.flatMap<
UpdateRouteTriggerAction['updates'][number]
>((difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0] as FlatRouteTriggerPropertiesToCompare;
return {
from: oldValue,
to: value,
property,
};
}
case 'CREATE':
case 'REMOVE':
default: {
// Should never occurs, we should only provide null never undefined and so on
return [];
}
}
});
};
@@ -0,0 +1,27 @@
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,
}: {
createRouteTriggerInput: CreateRouteTriggerInput;
workspaceId: string;
}): FlatRouteTrigger => {
const now = new Date();
return {
id: uuidV4(),
universalIdentifier:
createRouteTriggerInput.universalIdentifier ?? uuidV4(),
path: createRouteTriggerInput.path,
isAuthRequired: createRouteTriggerInput.isAuthRequired,
httpMethod: createRouteTriggerInput.httpMethod,
serverlessFunctionId: createRouteTriggerInput.serverlessFunctionId,
workspaceId,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,33 @@
import { type FlatEntityMaps } from 'src/engine/core-modules/common/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,
path: updateRouteTriggerInput.update.path,
isAuthRequired: updateRouteTriggerInput.update.isAuthRequired,
httpMethod: updateRouteTriggerInput.update.httpMethod,
updatedAt: new Date(),
};
};
@@ -0,0 +1,23 @@
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): void => {
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;
};
@@ -1,21 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RouteService } from 'src/engine/metadata-modules/route/route.service';
import { Route } from 'src/engine/metadata-modules/route/route.entity';
import { RouteController } from 'src/engine/metadata-modules/route/route.controller';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
@Module({
imports: [
TypeOrmModule.forFeature([Route]),
AuthModule,
DomainManagerModule,
ServerlessFunctionModule,
],
controllers: [RouteController],
providers: [RouteService],
})
export class RouteModule {}
@@ -1,5 +1,6 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsNotEmpty,
IsNumber,
@@ -9,6 +10,7 @@ import {
IsUUID,
Max,
Min,
ValidateNested,
} from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
@@ -16,14 +18,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
import { ServerlessFunctionCode } from 'src/engine/metadata-modules/serverless-function/types/serverless-function-code.type';
@InputType()
export class UpdateServerlessFunctionInput {
@Field(() => UUIDScalarType, {
description: 'Id of the serverless function to execute',
})
@IsNotEmpty()
@IsUUID()
id: string;
class UpdateServerlessFunctionInputUpdates {
@IsString()
@Field()
name: string;
@@ -44,3 +39,20 @@ export class UpdateServerlessFunctionInput {
@IsObject()
code: ServerlessFunctionCode;
}
@InputType()
export class UpdateServerlessFunctionInput {
@Field(() => UUIDScalarType, {
description: 'Id of the serverless function to update',
})
@IsNotEmpty()
@IsUUID()
id: string;
@Type(() => UpdateServerlessFunctionInputUpdates)
@ValidateNested()
@Field(() => UpdateServerlessFunctionInputUpdates, {
description: 'The serverless function updates',
})
update: UpdateServerlessFunctionInputUpdates;
}
@@ -17,7 +17,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/inte
import { CronTrigger } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { DatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { Route } from 'src/engine/metadata-modules/route/route.entity';
import { RouteTrigger } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { ServerlessFunctionEntityRelationProperties } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { InputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/input-schema.type';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@@ -33,7 +33,7 @@ export enum ServerlessFunctionRuntime {
export const SERVERLESS_FUNCTION_ENTITY_RELATION_PROPERTIES = [
'cronTriggers',
'databaseEventTriggers',
'routes',
'routeTriggers',
] as const satisfies readonly ServerlessFunctionEntityRelationProperties[];
@Entity('serverlessFunction')
@@ -119,10 +119,14 @@ export class ServerlessFunctionEntity
)
databaseEventTriggers: DatabaseEventTrigger[];
@OneToMany(() => Route, (route) => route.serverlessFunction, {
cascade: true,
})
routes: Route[];
@OneToMany(
() => RouteTrigger,
(routeTrigger) => routeTrigger.serverlessFunction,
{
cascade: true,
},
)
routeTriggers: RouteTrigger[];
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -263,9 +263,9 @@ export class ServerlessFunctionService {
await this.serverlessFunctionRepository.update(
existingServerlessFunction.id,
{
name: serverlessFunctionInput.name,
description: serverlessFunctionInput.description,
timeoutSeconds: serverlessFunctionInput.timeoutSeconds,
name: serverlessFunctionInput.update.name,
description: serverlessFunctionInput.update.description,
timeoutSeconds: serverlessFunctionInput.update.timeoutSeconds,
},
);
@@ -275,7 +275,7 @@ export class ServerlessFunctionService {
});
await this.fileStorageService.writeFolder(
serverlessFunctionInput.code,
serverlessFunctionInput.update.code,
fileFolder,
);
@@ -1,7 +1,7 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type CronTrigger } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { type DatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { type Route } from 'src/engine/metadata-modules/route/route.entity';
import { type RouteTrigger } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@@ -13,7 +13,7 @@ export type ServerlessFunctionEntityRelationProperties =
ServerlessFunctionEntity,
| CronTrigger
| DatabaseEventTrigger
| Route
| RouteTrigger
| Workspace
| ApplicationEntity
| ServerlessFunctionLayerEntity
@@ -42,14 +42,14 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr
const updatedEditableFieldProperties = {
...extractAndSanitizeObjectStringFields(
{
...rawUpdateServerlessFunctionInput,
...rawUpdateServerlessFunctionInput.update,
checksum: serverlessFunctionCreateHash(
JSON.stringify(rawUpdateServerlessFunctionInput.code),
JSON.stringify(rawUpdateServerlessFunctionInput.update.code),
),
},
FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES,
),
code: rawUpdateServerlessFunctionInput.code,
code: rawUpdateServerlessFunctionInput.update.code,
};
return mergeUpdateInExistingRecord({