diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index b1d077d207..90277d6087 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -33,6 +33,7 @@ FRONTEND_URL=http://localhost:3001 # AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect # AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token # SERVERLESS_TYPE=local +# SERVERLESS_LOGS_ENABLED=true # STORAGE_TYPE=local # STORAGE_LOCAL_PATH=.local-storage # SUPPORT_DRIVER=front @@ -79,4 +80,3 @@ FRONTEND_URL=http://localhost:3001 # IS_CONFIG_VARIABLES_IN_DB_ENABLED=false # ANALYTICS_ENABLED= # CLICKHOUSE_URL=http://default:clickhousePassword@localhost:8123/twenty - diff --git a/packages/twenty-server/src/engine/core-modules/application/types/application.types.ts b/packages/twenty-server/src/engine/core-modules/application/types/application.types.ts index 447ab06b04..3354a2ecac 100644 --- a/packages/twenty-server/src/engine/core-modules/application/types/application.types.ts +++ b/packages/twenty-server/src/engine/core-modules/application/types/application.types.ts @@ -11,21 +11,21 @@ export type PackageJson = { npm: string; yarn: string; }; - env: Record< - string, - { - key: string; - value?: string; - description?: string; - isSecret: boolean; - } - >; + env?: EnvManifest; icon?: string; version: string; dependencies?: object; devDependencies?: object; }; +export type EnvManifest = Record; + +export type EnvVariableManifest = { + value?: string; + description?: string; + isSecret: boolean; +}; + export type AppManifest = PackageJson & { agents: AgentManifest[]; objects: ObjectManifest[]; diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts index 9111e1c5f2..fad36cbf32 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts @@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils'; import { In, Not, Repository } from 'typeorm'; import { ApplicationVariable } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { EnvManifest } from 'src/engine/core-modules/application/types/application.types'; export class ApplicationVariableService { constructor( @@ -28,14 +29,7 @@ export class ApplicationVariableService { env, applicationId, }: { - env: Record< - string, - { - value?: string; - description?: string; - isSecret: boolean; - } - >; + env?: EnvManifest; applicationId: string; }) { if (!isDefined(env)) { diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index d67240d023..f288817ee6 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -444,6 +444,15 @@ export class ConfigVariables { @CastToUpperSnakeCase() SERVERLESS_TYPE: ServerlessDriverType = ServerlessDriverType.LOCAL; + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.ServerlessConfig, + description: + 'Configure whether console logs from serverless functions are displayed in the terminal', + type: ConfigVariableType.BOOLEAN, + }) + @IsOptional() + SERVERLESS_LOGS_ENABLED: false; + @ConfigVariablesMetadata({ group: ConfigVariablesGroup.ServerlessConfig, description: 'Throttle limit for serverless function execution', diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/exceptions/route-trigger-rest-api-exception-filter.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/exceptions/route-trigger-rest-api-exception-filter.ts new file mode 100644 index 0000000000..bde7237249 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/exceptions/route-trigger-rest-api-exception-filter.ts @@ -0,0 +1,59 @@ +import { + type ArgumentsHost, + Catch, + type ExceptionFilter, +} from '@nestjs/common'; + +import type { Response } from 'express'; + +import { + RouteTriggerException, + RouteTriggerExceptionCode, +} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; +import type { CustomException } from 'src/utils/custom-exception'; +import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service'; + +@Catch(RouteTriggerException) +export class RouteTriggerRestApiExceptionFilter implements ExceptionFilter { + constructor( + private readonly httpExceptionHandlerService: HttpExceptionHandlerService, + ) {} + + catch(exception: RouteTriggerException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + switch (exception.code) { + case RouteTriggerExceptionCode.WORKSPACE_NOT_FOUND: + case RouteTriggerExceptionCode.ROUTE_NOT_FOUND: + case RouteTriggerExceptionCode.TRIGGER_NOT_FOUND: + case RouteTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 404, + ); + case RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 403, + ); + case RouteTriggerExceptionCode.SERVERLESS_FUNCTION_EXECUTION_ERROR: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 500, + ); + case RouteTriggerExceptionCode.ROUTE_ALREADY_EXIST: + case RouteTriggerExceptionCode.ROUTE_PATH_ALREADY_EXIST: + default: { + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 400, + ); + } + } + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception.ts index b4af040d57..560f60bfb9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception.ts @@ -8,9 +8,12 @@ export class RouteTriggerException extends CustomException { } export enum RouteTriggerExceptionCode { + WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', ROUTE_NOT_FOUND = 'ROUTE_NOT_FOUND', - ROUTE_ALREADY_EXIST = 'ROUTE_ALREADY_EXIST', - ROUTE_INVALID = 'ROUTE_INVALID', - ROUTE_PATH_ALREADY_EXIST = 'ROUTE_PATH_ALREADY_EXIST', + TRIGGER_NOT_FOUND = 'TRIGGER_NOT_FOUND', SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND', + ROUTE_ALREADY_EXIST = 'ROUTE_ALREADY_EXIST', + ROUTE_PATH_ALREADY_EXIST = 'ROUTE_PATH_ALREADY_EXIST', + FORBIDDEN_EXCEPTION = 'FORBIDDEN_EXCEPTION', + SERVERLESS_FUNCTION_EXECUTION_ERROR = 'SERVERLESS_FUNCTION_EXECUTION_ERROR', } diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.controller.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.controller.ts index cc61a87464..e344a70ff1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.controller.ts @@ -6,6 +6,7 @@ import { Post, Put, Req, + UseFilters, UseGuards, } from '@nestjs/common'; @@ -14,60 +15,51 @@ import { Request } from 'express'; 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'; -import { formatServerlessControllerResponse } from 'src/engine/metadata-modules/route-trigger/utils/format-serverless-controller-response'; +import { RouteTriggerRestApiExceptionFilter } from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger-rest-api-exception-filter'; @Controller('s') @UseGuards(PublicEndpointGuard) +@UseFilters(RouteTriggerRestApiExceptionFilter) export class RouteTriggerController { constructor(private readonly routeTriggerService: RouteTriggerService) {} @Get('*') async get(@Req() request: Request) { - const result = await this.routeTriggerService.handle({ + return await this.routeTriggerService.handle({ request, httpMethod: HTTPMethod.GET, }); - - return formatServerlessControllerResponse(result); } @Post('*') async post(@Req() request: Request) { - const result = await this.routeTriggerService.handle({ + return await this.routeTriggerService.handle({ request, httpMethod: HTTPMethod.POST, }); - - return formatServerlessControllerResponse(result); } @Put('*') async put(@Req() request: Request) { - const result = await this.routeTriggerService.handle({ + return await this.routeTriggerService.handle({ request, httpMethod: HTTPMethod.PUT, }); - - return formatServerlessControllerResponse(result); } @Patch('*') async patch(@Req() request: Request) { - const result = await this.routeTriggerService.handle({ + return await this.routeTriggerService.handle({ request, httpMethod: HTTPMethod.PATCH, }); - - return formatServerlessControllerResponse(result); } @Delete('*') async delete(@Req() request: Request) { - const result = await this.routeTriggerService.handle({ + return await this.routeTriggerService.handle({ request, httpMethod: HTTPMethod.DELETE, }); - - return formatServerlessControllerResponse(result); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.service.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.service.ts index 1defb99ad8..658c768219 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.service.ts @@ -1,8 +1,4 @@ -import { - ForbiddenException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -16,11 +12,11 @@ import { } 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 { - AuthException, - AuthExceptionCode, -} from 'src/engine/core-modules/auth/auth.exception'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { + RouteTriggerException, + RouteTriggerExceptionCode, +} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; @Injectable() export class RouteTriggerService { @@ -51,9 +47,9 @@ export class RouteTriggerService { assertIsDefinedOrThrow( workspace, - new AuthException( + new RouteTriggerException( 'Workspace not found', - AuthExceptionCode.WORKSPACE_NOT_FOUND, + RouteTriggerExceptionCode.WORKSPACE_NOT_FOUND, ), ); @@ -81,7 +77,10 @@ export class RouteTriggerService { } } - throw new NotFoundException('No Route trigger found'); + throw new RouteTriggerException( + 'No Route trigger found', + RouteTriggerExceptionCode.TRIGGER_NOT_FOUND, + ); } private async validateWorkspaceFromRequest({ @@ -95,11 +94,17 @@ export class RouteTriggerService { await this.accessTokenService.validateTokenByRequest(request); if (!isDefined(workspace)) { - throw new NotFoundException('Workspace not found'); + throw new RouteTriggerException( + 'Workspace not found', + RouteTriggerExceptionCode.WORKSPACE_NOT_FOUND, + ); } if (workspace.id !== workspaceId) { - throw new ForbiddenException('Invalid Workspace'); + throw new RouteTriggerException( + 'You are not authorized', + RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION, + ); } } @@ -133,11 +138,25 @@ export class RouteTriggerService { ...routeTriggerWithPathParams.pathParams, }; - return await this.serverlessFunctionService.executeOneServerlessFunction( - routeTriggerWithPathParams.routeTrigger.serverlessFunction.id, - routeTriggerWithPathParams.routeTrigger.workspaceId, - executionParams, - 'draft', - ); + const result = + await this.serverlessFunctionService.executeOneServerlessFunction( + routeTriggerWithPathParams.routeTrigger.serverlessFunction.id, + routeTriggerWithPathParams.routeTrigger.workspaceId, + executionParams, + 'draft', + ); + + if (!isDefined(result)) { + return result; + } + + if (result.error) { + throw new RouteTriggerException( + result.error.errorMessage, + RouteTriggerExceptionCode.SERVERLESS_FUNCTION_EXECUTION_ERROR, + ); + } + + return result.data; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/format-serverless-controller-response.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/format-serverless-controller-response.ts deleted file mode 100644 index 8f0540f229..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/format-serverless-controller-response.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; - -import { isDefined } from 'twenty-shared/utils'; - -import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; - -export const formatServerlessControllerResponse = ( - result?: ServerlessExecuteResult | null, -) => { - if (!isDefined(result)) { - return result; - } - - if (result.error) { - throw new HttpException( - { errorType: result.error.errorType, message: result.error.errorMessage }, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - - return result.data; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job.ts index e5b947ce46..5a22d0ec3c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job.ts @@ -1,4 +1,4 @@ -import { Logger, Scope } from '@nestjs/common'; +import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; @@ -16,22 +16,17 @@ export type ServerlessFunctionTriggerJobData = { scope: Scope.REQUEST, }) export class ServerlessFunctionTriggerJob { - private readonly logger = new Logger(ServerlessFunctionTriggerJob.name); - constructor( private readonly serverlessFunctionService: ServerlessFunctionService, ) {} @Process(ServerlessFunctionTriggerJob.name) async handle(data: ServerlessFunctionTriggerJobData) { - const result = - await this.serverlessFunctionService.executeOneServerlessFunction( - data.serverlessFunctionId, - data.workspaceId, - data.payload || {}, - 'draft', - ); - - this.logger.log(result.logs); + await this.serverlessFunctionService.executeOneServerlessFunction( + data.serverlessFunctionId, + data.workspaceId, + data.payload || {}, + 'draft', + ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index e1d22cbc86..fae6eefe24 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -108,6 +108,11 @@ export class ServerlessFunctionService { version, ); + if (this.twentyConfigService.get('SERVERLESS_LOGS_ENABLED')) { + /* eslint-disable no-console */ + console.log(resultServerlessFunction.logs); + } + this.auditService .createContext({ workspaceId,