diff --git a/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx b/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx index 8acb8eeb31..00b6ef5318 100644 --- a/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx +++ b/packages/twenty-docs/developers/extend/apps/getting-started/project-structure.mdx @@ -76,4 +76,4 @@ The scaffolder pins `twenty-sdk` and `twenty-client-sdk` to its own version — Keeping either package under `dependencies` pulls it into the installed app's runtime bundle, where it is dead weight. `twenty dev:build` emits a warning when either is still listed under `dependencies`. -Add your app's own runtime dependencies (libraries your logic functions actually import at runtime) under `dependencies` as usual. +Add your app's own runtime dependencies (libraries your logic functions actually import at runtime) under `dependencies` as usual. Keep the tree lean: `dependencies` are installed into a runtime layer capped at 250MB unpacked, and a sync or install fails with a dependencies size error beyond that — UI libraries and dev tooling belong in `devDependencies`. diff --git a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx index 1d295b38c2..46d8e2b72a 100644 --- a/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx +++ b/packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx @@ -172,6 +172,10 @@ For security reasons, response headers are restricted to an allow-list. Any head The status code must be a valid HTTP status code (between 100 and 599). Response header names are matched case-insensitively. +#### Platform error responses + +Beyond your handler's own responses, the platform answers route calls directly in some situations: `404` when the route or function does not exist, `403` when the application is stopped, `429` when the execution rate limit is hit, and `422` when the application's production `dependencies` are too large to install — see [dependencies size limits](/developers/extend/apps/getting-started/project-structure#dependencies). + #### Server route trigger `httpRouteTriggerSettings` exposes a function under `/s/` and resolves the workspace from the request host — which works when each workspace has its own domain. Third-party providers, however, deliver every tenant's events to **one** URL. For that case, use `serverRouteTriggerSettings`. diff --git a/packages/twenty-docs/developers/extend/apps/operations/sync-and-recovery.mdx b/packages/twenty-docs/developers/extend/apps/operations/sync-and-recovery.mdx index b6054081f0..f0a807569b 100644 --- a/packages/twenty-docs/developers/extend/apps/operations/sync-and-recovery.mdx +++ b/packages/twenty-docs/developers/extend/apps/operations/sync-and-recovery.mdx @@ -121,5 +121,6 @@ When something goes wrong, the metadata diff and named errors let you place the - **Manifest build error** — the CLI fails before syncing (`MANIFEST_BUILD_FAILED`, `TYPECHECK_FAILED`); fix your app source. - **Sync / migration error** — the build succeeds but applying the diff fails, naming the entity and `universalIdentifier`; fix the conflicting metadata. +- **Dependencies size error** — the sync or install fails because the app's production `dependencies` are too large to install as a runtime layer (`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED`); move packages your logic functions do not import at runtime (UI libraries, dev tooling) to `devDependencies`. - **App code runtime error** — the sync succeeds but your logic functions or components misbehave at runtime; check [function logs](/developers/extend/apps/operations/cli). - **Local instance state** — none of the above and the workspace still looks wrong; work down the recovery ladder. diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/minimal-app/__integration__/app-dev/sync-error-surfacing.integration.spec.ts b/packages/twenty-sdk/src/cli/__tests__/apps/minimal-app/__integration__/app-dev/sync-error-surfacing.integration.spec.ts new file mode 100644 index 0000000000..0107acec7e --- /dev/null +++ b/packages/twenty-sdk/src/cli/__tests__/apps/minimal-app/__integration__/app-dev/sync-error-surfacing.integration.spec.ts @@ -0,0 +1,70 @@ +import { MINIMAL_APP_PATH } from '@/cli/__tests__/apps/fixture-paths'; +import { mockApiService } from '@/cli/__tests__/integration/utils/setup-app-dev-mocks'; +import { AppDevCommand } from '@/cli/commands/dev/dev'; + +const DEPENDENCIES_SIZE_ERROR_CODE = + 'LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED'; +const DEPENDENCIES_SIZE_ERROR_MESSAGE = + 'Production dependencies are too large to install. Move packages that are not imported by your logic functions (UI libraries, dev tooling) out of "dependencies".'; + +describe('minimal-app dev sync error surfacing', () => { + it('should render the dependencies size error through the validation error report when the sync fails', async () => { + mockApiService.syncApplication.mockResolvedValue({ + success: false, + error: { + summary: { totalErrors: 1, logicFunction: 1 }, + errors: { + logicFunction: [ + { + type: 'update', + metadataName: 'logicFunction', + errors: [ + { + code: DEPENDENCIES_SIZE_ERROR_CODE, + message: DEPENDENCIES_SIZE_ERROR_MESSAGE, + value: + "Dependency layer 'deps-abc' exceeds the Lambda layer size limit: Unzipped size must be smaller than 262144000 bytes", + }, + ], + flatEntityMinimalInformation: {}, + }, + ], + }, + }, + message: 'Validation failed for 1 logicFunction', + }); + + const command = new AppDevCommand(); + + await command.execute({ appPath: MINIMAL_APP_PATH, headless: true }); + + try { + const deadline = Date.now() + 30_000; + let syncStepStatus: string | undefined; + + while (Date.now() < deadline) { + syncStepStatus = command.getOrchestrator()?.getState()?.steps + .syncApplication.status; + + if (syncStepStatus === 'error') { + break; + } + + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + expect(syncStepStatus).toBe('error'); + + const eventMessages = ( + command.getOrchestrator()?.getState()?.events ?? [] + ) + .map((event) => event.message) + .join('\n'); + + expect(eventMessages).toContain(DEPENDENCIES_SIZE_ERROR_CODE); + expect(eventMessages).toContain(DEPENDENCIES_SIZE_ERROR_MESSAGE); + } finally { + await command.close(); + } + }, 60_000); +}); diff --git a/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts b/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts index f71b541908..31aa92ceb2 100644 --- a/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts +++ b/packages/twenty-sdk/src/cli/__tests__/integration/utils/setup-app-dev-mocks.ts @@ -1,6 +1,6 @@ import { vi } from 'vitest'; -const mockApiService = { +export const mockApiService = { validateAuth: vi.fn().mockResolvedValue({ authValid: true, serverUp: true }), getWorkspaceFrontendUrl: vi.fn().mockResolvedValue('http://localhost:3000'), generateApplicationToken: vi.fn().mockResolvedValue({ diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/constants/lambda-driver.constant.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/constants/lambda-driver.constant.ts index 3a95995612..bfc51baa26 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/constants/lambda-driver.constant.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/constants/lambda-driver.constant.ts @@ -9,7 +9,7 @@ export const LAMBDA_CLIENT_MAX_ATTEMPTS = 8; export const LAMBDA_CLIENT_RETRY_MODE = 'adaptive' as const; export const YARN_INSTALL_LAMBDA_TIMEOUT_SECONDS = 300; -export const YARN_INSTALL_LAMBDA_MEMORY_MB = 1024; +export const YARN_INSTALL_LAMBDA_MEMORY_MB = 4096; export const BUILDER_LAMBDA_TIMEOUT_SECONDS = 60; export const BUILDER_LAMBDA_MEMORY_MB = 512; export const EXECUTOR_LAMBDA_MEMORY_MB = 512; diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-executor-manager.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-executor-manager.service.ts index be7a5f9d8c..529ea8d598 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-executor-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-executor-manager.service.ts @@ -311,6 +311,14 @@ export class LambdaExecutorManagerService { applicationUniversalIdentifier, }); } catch (error) { + if ( + error instanceof LogicFunctionException && + error.code === + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED + ) { + throw error; + } + this.logger.error( `Failed to get dependency layer for function ${flatLogicFunction.id}: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined, diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-layer-manager.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-layer-manager.service.ts index d9b44b307d..0d6685c232 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-layer-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-layer-manager.service.ts @@ -3,6 +3,7 @@ import * as fs from 'fs/promises'; import { DeleteLayerVersionCommand, type GetFunctionCommandOutput, + InvalidParameterValueException, ListLayerVersionsCommand, PublishLayerVersionCommand, ResourceNotFoundException, @@ -22,6 +23,10 @@ import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logi import { type LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service'; import { type SdkClientArchiveService } from 'src/engine/core-modules/sdk-client/sdk-client-archive.service'; import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; +import { + LogicFunctionException, + LogicFunctionExceptionCode, +} from 'src/engine/metadata-modules/logic-function/logic-function.exception'; type LayerAppContext = { flatApplication: FlatApplication; @@ -176,19 +181,36 @@ export class LambdaLayerManagerService { }); const lambdaClient = await this.awsClient.getLambdaClient(); - const publishResult = await lambdaClient.send( - new PublishLayerVersionCommand({ - LayerName: layerName, - Content: { - S3Bucket: this.options.layerBucket, - S3Key: s3Key, - }, - CompatibleRuntimes: [ - LogicFunctionRuntime.NODE18, - LogicFunctionRuntime.NODE22, - ], - }), - ); + + let publishResult; + + try { + publishResult = await lambdaClient.send( + new PublishLayerVersionCommand({ + LayerName: layerName, + Content: { + S3Bucket: this.options.layerBucket, + S3Key: s3Key, + }, + CompatibleRuntimes: [ + LogicFunctionRuntime.NODE18, + LogicFunctionRuntime.NODE22, + ], + }), + ); + } catch (error) { + if ( + error instanceof InvalidParameterValueException && + error.message.toLowerCase().includes('size') + ) { + throw new LogicFunctionException( + `Dependency layer '${layerName}' exceeds the Lambda layer size limit: ${error.message}`, + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED, + ); + } + + throw error; + } if (!publishResult.LayerVersionArn) { throw new Error( diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-tool-functions.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-tool-functions.service.ts index 37ae0d9729..c7286e6da4 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-tool-functions.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-tool-functions.service.ts @@ -33,6 +33,7 @@ import { type YarnInstallLambdaPayload, type YarnInstallLambdaResult, } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/types/lambda-driver.type'; +import { buildYarnInstallFailureException } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/build-yarn-install-failure-exception.util'; import { computeHashedLambdaResourceName } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/compute-hashed-lambda-resource-name.util'; import { type LambdaAwsClientService } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/services/lambda-aws-client.service'; import { copyBuilder } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/copy-builder'; @@ -150,10 +151,7 @@ export class LambdaToolFunctionsService { ? JSON.parse(result.Payload.transformToString()) : {}; - throw new LogicFunctionException( - `Yarn install Lambda failed: ${JSON.stringify(parsedResult)}`, - LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED, - ); + throw buildYarnInstallFailureException(parsedResult); } const parsedResult: YarnInstallLambdaResult = result.Payload @@ -346,7 +344,12 @@ export class LambdaToolFunctionsService { this.yarnInstallFunctionName = computeHashedLambdaResourceName({ resourceNamePrefix: YARN_INSTALL_FUNCTION_NAME_PREFIX, namespace: this.options.resourceNamespace, - contents: [handlerContent], + contents: [ + handlerContent, + String(YARN_INSTALL_LAMBDA_MEMORY_MB), + String(YARN_INSTALL_LAMBDA_TIMEOUT_SECONDS), + String(LAMBDA_EPHEMERAL_STORAGE_MB), + ], }); return this.yarnInstallFunctionName; @@ -362,7 +365,12 @@ export class LambdaToolFunctionsService { this.builderFunctionName = computeHashedLambdaResourceName({ resourceNamePrefix: BUILDER_FUNCTION_NAME_PREFIX, namespace: this.options.resourceNamespace, - contents: [handlerContent], + contents: [ + handlerContent, + String(BUILDER_LAMBDA_MEMORY_MB), + String(BUILDER_LAMBDA_TIMEOUT_SECONDS), + String(LAMBDA_EPHEMERAL_STORAGE_MB), + ], }); return this.builderFunctionName; diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/__tests__/build-yarn-install-failure-exception.util.spec.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/__tests__/build-yarn-install-failure-exception.util.spec.ts new file mode 100644 index 0000000000..a338935062 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/__tests__/build-yarn-install-failure-exception.util.spec.ts @@ -0,0 +1,34 @@ +import { buildYarnInstallFailureException } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/build-yarn-install-failure-exception.util'; +import { LogicFunctionExceptionCode } from 'src/engine/metadata-modules/logic-function/logic-function.exception'; + +describe('buildYarnInstallFailureException', () => { + it('should map an out-of-memory kill to LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED', () => { + const exception = buildYarnInstallFailureException({ + errorType: 'Runtime.OutOfMemory', + errorMessage: 'Error: Runtime exited with error: signal: killed', + }); + + expect(exception.code).toBe( + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED, + ); + }); + + it('should map any other failure to LOGIC_FUNCTION_CREATE_FAILED', () => { + const exception = buildYarnInstallFailureException({ + errorType: 'Error', + errorMessage: 'yarn install failed: ENETDOWN', + }); + + expect(exception.code).toBe( + LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED, + ); + }); + + it('should map an empty payload to LOGIC_FUNCTION_CREATE_FAILED', () => { + const exception = buildYarnInstallFailureException({}); + + expect(exception.code).toBe( + LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED, + ); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/build-yarn-install-failure-exception.util.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/build-yarn-install-failure-exception.util.ts new file mode 100644 index 0000000000..922b293d64 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda/utils/build-yarn-install-failure-exception.util.ts @@ -0,0 +1,25 @@ +import { + LogicFunctionException, + LogicFunctionExceptionCode, +} from 'src/engine/metadata-modules/logic-function/logic-function.exception'; + +type YarnInstallLambdaErrorPayload = { + errorType?: string; + errorMessage?: string; +}; + +export const buildYarnInstallFailureException = ( + payload: YarnInstallLambdaErrorPayload, +): LogicFunctionException => { + if (payload.errorType === 'Runtime.OutOfMemory') { + return new LogicFunctionException( + `Yarn install Lambda ran out of memory: the dependency tree is too large to install`, + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED, + ); + } + + return new LogicFunctionException( + `Yarn install Lambda failed: ${JSON.stringify(payload)}`, + LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED, + ); +}; diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job.ts index f3890cb2a9..b52ce45a53 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job.ts @@ -1,4 +1,4 @@ -import { Scope } from '@nestjs/common'; +import { Logger, 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'; @@ -22,6 +22,8 @@ export type LogicFunctionTriggerJobData = { scope: Scope.REQUEST, }) export class LogicFunctionTriggerJob { + private readonly logger = new Logger(LogicFunctionTriggerJob.name); + constructor( private readonly logicFunctionExecutorService: LogicFunctionExecutorService, ) {} @@ -52,6 +54,17 @@ export class LogicFunctionTriggerJob { continue; } + if ( + error instanceof LogicFunctionException && + error.code === + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED + ) { + this.logger.warn( + `Skipping function ${logicFunctionPayload.logicFunctionId} (workspace ${logicFunctionPayload.workspaceId}): ${error.message}`, + ); + continue; + } + throw error; } } diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/__tests__/route-trigger-rest-api-exception-filter.spec.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/__tests__/route-trigger-rest-api-exception-filter.spec.ts index 5735ff2293..ef6c3cad3d 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/__tests__/route-trigger-rest-api-exception-filter.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/__tests__/route-trigger-rest-api-exception-filter.spec.ts @@ -58,6 +58,24 @@ describe('RouteTriggerRestApiExceptionFilter', () => { expect(handleError).toHaveBeenCalledWith(exception, response, 500); }); + it('maps oversized dependencies to 422 without Sentry capture', () => { + const exception = new RouteTriggerException( + 'dependencies too large', + RouteTriggerExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED, + ); + + filter.catch(exception, host); + + expect(handleError).toHaveBeenCalledWith( + exception, + response, + 422, + undefined, + undefined, + { shouldBeCapturedBySentry: false }, + ); + }); + it('maps a disabled function to 403', () => { const exception = new RouteTriggerException( 'disabled', diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter.ts index e1446d42d3..cc0e5bfb60 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter.ts @@ -52,6 +52,15 @@ export class RouteTriggerRestApiExceptionFilter implements ExceptionFilter { response, 410, ); + case RouteTriggerExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 422, + undefined, + undefined, + { shouldBeCapturedBySentry: false }, + ); case RouteTriggerExceptionCode.ROUTE_TRIGGER_USER_UNCAUGHT_ERROR: return this.httpExceptionHandlerService.handleError( exception as CustomException, diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts index e469aea4f1..af1b24c20c 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts @@ -17,6 +17,7 @@ export enum RouteTriggerExceptionCode { ROUTE_TRIGGER_PLATFORM_ERROR = 'ROUTE_TRIGGER_PLATFORM_ERROR', RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', LEGACY_ROUTE_DEPRECATED = 'LEGACY_ROUTE_DEPRECATED', + LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED = 'LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED', } const getRouteTriggerExceptionUserFriendlyMessage = ( @@ -47,6 +48,8 @@ const getRouteTriggerExceptionUserFriendlyMessage = ( return msg`Too many requests. Please try again later.`; case RouteTriggerExceptionCode.LEGACY_ROUTE_DEPRECATED: return msg`This endpoint is no longer available on /s/. Use the dedicated public domain URL instead.`; + case RouteTriggerExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED: + return msg`The application's production dependencies are too large to install. Move packages that are not imported by its logic functions (UI libraries, dev tooling) out of "dependencies".`; default: assertUnreachable(code); } diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts index 1748369b1d..e374568927 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts @@ -239,6 +239,8 @@ export class RouteTriggerService { return RouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND; case LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED: return RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION; + case LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED: + return RouteTriggerExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.exception.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.exception.ts index 8c8ab0e61d..bae9240d6e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.exception.ts @@ -16,6 +16,7 @@ export enum LogicFunctionExceptionCode { LOGIC_FUNCTION_EXECUTION_TIMEOUT = 'LOGIC_FUNCTION_EXECUTION_TIMEOUT', LOGIC_FUNCTION_PLATFORM_EXECUTION_ERROR = 'LOGIC_FUNCTION_PLATFORM_EXECUTION_ERROR', LOGIC_FUNCTION_LAYER_BUILD_FAILED = 'LOGIC_FUNCTION_LAYER_BUILD_FAILED', + LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED = 'LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED', LOGIC_FUNCTION_DISABLED = 'LOGIC_FUNCTION_DISABLED', LOGIC_FUNCTION_INVALID_SEED_PROJECT = 'LOGIC_FUNCTION_INVALID_SEED_PROJECT', INVALID_LOGIC_FUNCTION_INPUT = 'INVALID_LOGIC_FUNCTION_INPUT', @@ -48,6 +49,8 @@ const getLogicFunctionExceptionUserFriendlyMessage = ( return msg`Function execution failed.`; case LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED: return msg`Failed to build function dependencies.`; + case LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED: + return msg`Your application's production dependencies are too large to install. Move packages that are not imported by your logic functions (UI libraries, dev tooling) out of "dependencies".`; case LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED: return msg`Logic function execution is disabled.`; case LogicFunctionExceptionCode.LOGIC_FUNCTION_INVALID_SEED_PROJECT: diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/__tests__/logic-function-graphql-api-exception-handler.utils.spec.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/__tests__/logic-function-graphql-api-exception-handler.utils.spec.ts new file mode 100644 index 0000000000..66fac8e410 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/__tests__/logic-function-graphql-api-exception-handler.utils.spec.ts @@ -0,0 +1,62 @@ +import { + ForbiddenError, + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + LogicFunctionException, + LogicFunctionExceptionCode, +} from 'src/engine/metadata-modules/logic-function/logic-function.exception'; +import { logicFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils'; + +describe('logicFunctionGraphQLApiExceptionHandler', () => { + it('should map LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED to UserInputError', () => { + expect(() => + logicFunctionGraphQLApiExceptionHandler( + new LogicFunctionException( + 'dependencies too large', + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED, + ), + ), + ).toThrow(UserInputError); + }); + + it('should map LOGIC_FUNCTION_NOT_FOUND to NotFoundError', () => { + expect(() => + logicFunctionGraphQLApiExceptionHandler( + new LogicFunctionException( + 'not found', + LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND, + ), + ), + ).toThrow(NotFoundError); + }); + + it('should map LOGIC_FUNCTION_DISABLED to ForbiddenError', () => { + expect(() => + logicFunctionGraphQLApiExceptionHandler( + new LogicFunctionException( + 'disabled', + LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED, + ), + ), + ).toThrow(ForbiddenError); + }); + + it('should rethrow LOGIC_FUNCTION_LAYER_BUILD_FAILED unchanged', () => { + const exception = new LogicFunctionException( + 'layer build failed', + LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED, + ); + + expect(() => logicFunctionGraphQLApiExceptionHandler(exception)).toThrow( + exception, + ); + }); + + it('should rethrow unknown errors unchanged', () => { + const error = new Error('unrelated'); + + expect(() => logicFunctionGraphQLApiExceptionHandler(error)).toThrow(error); + }); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils.ts index 9318d69161..47d4ca7cbd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils.ts @@ -33,6 +33,7 @@ export const logicFunctionGraphQLApiExceptionHandler = (error: any) => { case LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED: throw error; case LogicFunctionExceptionCode.LOGIC_FUNCTION_COMPILATION_FAILED: + case LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED: case LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT: throw new UserInputError(error); case LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED: diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/logic-function-dependencies-size-graphql-api-exception-handler.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/logic-function-dependencies-size-graphql-api-exception-handler.util.spec.ts new file mode 100644 index 0000000000..261fac874a --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/utils/__tests__/logic-function-dependencies-size-graphql-api-exception-handler.util.spec.ts @@ -0,0 +1,43 @@ +import { BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + LogicFunctionException, + LogicFunctionExceptionCode, +} from 'src/engine/metadata-modules/logic-function/logic-function.exception'; +import { logicFunctionDependenciesSizeGraphqlApiExceptionHandler } from 'src/engine/workspace-manager/workspace-migration/interceptors/utils/logic-function-dependencies-size-graphql-api-exception-handler.util'; + +describe('logicFunctionDependenciesSizeGraphqlApiExceptionHandler', () => { + it('should throw a metadata validation error carrying one logicFunction entry', () => { + const exception = new LogicFunctionException( + "Dependency layer 'deps-abc' exceeds the Lambda layer size limit", + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED, + ); + + let thrown: BaseGraphQLError | undefined; + + try { + logicFunctionDependenciesSizeGraphqlApiExceptionHandler(exception); + } catch (error) { + thrown = error as BaseGraphQLError; + } + + expect(thrown).toBeInstanceOf(BaseGraphQLError); + + const extensions = thrown?.extensions as { + summary: { totalErrors: number; logicFunction?: number }; + errors: { + logicFunction?: { + errors: { code: string; value?: unknown }[]; + }[]; + }; + }; + + expect(extensions.summary).toEqual({ totalErrors: 1, logicFunction: 1 }); + expect(extensions.errors.logicFunction).toHaveLength(1); + expect(extensions.errors.logicFunction?.[0].errors[0].code).toBe( + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED, + ); + expect(extensions.errors.logicFunction?.[0].errors[0].value).toBe( + exception.message, + ); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/utils/logic-function-dependencies-size-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/utils/logic-function-dependencies-size-graphql-api-exception-handler.util.ts new file mode 100644 index 0000000000..30b9e1774c --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/utils/logic-function-dependencies-size-graphql-api-exception-handler.util.ts @@ -0,0 +1,43 @@ +import { + BaseGraphQLError, + ErrorCode, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { type LogicFunctionException } from 'src/engine/metadata-modules/logic-function/logic-function.exception'; +import { type MetadataValidationErrorResponseDescriptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/types/metadata-validation-error-response-descriptor.type'; + +export const logicFunctionDependenciesSizeGraphqlApiExceptionHandler = ( + exception: LogicFunctionException, +) => { + const payload: MetadataValidationErrorResponseDescriptor = { + summary: { totalErrors: 1, logicFunction: 1 }, + errors: { + logicFunction: [ + { + type: 'update', + metadataName: 'logicFunction', + errors: [ + { + code: exception.code, + message: + 'Production dependencies are too large to install. Move packages that are not imported by your logic functions (UI libraries, dev tooling) out of "dependencies".', + userFriendlyMessage: exception.userFriendlyMessage, + value: exception.message, + }, + ], + flatEntityMinimalInformation: {}, + }, + ], + }, + }; + + throw new BaseGraphQLError( + exception.message, + ErrorCode.METADATA_VALIDATION_FAILED, + { + code: 'METADATA_VALIDATION_ERROR', + ...payload, + userFriendlyMessage: exception.userFriendlyMessage, + message: 'Validation failed for 1 logicFunction', + }, + ); +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor.ts index d14332bcf9..9f3cedbdcf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor.ts @@ -12,7 +12,12 @@ import { FlatEntityMapsException, FlatEntityMapsExceptionCode, } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { + LogicFunctionException, + LogicFunctionExceptionCode, +} from 'src/engine/metadata-modules/logic-function/logic-function.exception'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; +import { logicFunctionDependenciesSizeGraphqlApiExceptionHandler } from 'src/engine/workspace-manager/workspace-migration/interceptors/utils/logic-function-dependencies-size-graphql-api-exception-handler.util'; import { workspaceMigrationBuilderGraphqlApiExceptionHandler } from 'src/engine/workspace-manager/workspace-migration/interceptors/utils/workspace-migration-builder-graphql-api-exception-handler.util'; import { workspaceMigrationRunnerExceptionFormatter } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-runner-exception-formatter'; import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; @@ -37,6 +42,14 @@ export class WorkspaceMigrationGraphqlApiExceptionInterceptor implements NestInt workspaceMigrationBuilderGraphqlApiExceptionHandler(error); } + if ( + error instanceof LogicFunctionException && + error.code === + LogicFunctionExceptionCode.LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED + ) { + logicFunctionDependenciesSizeGraphqlApiExceptionHandler(error); + } + if (error instanceof WorkspaceMigrationRunnerException) { workspaceMigrationRunnerExceptionFormatter(error); }