fix(twenty-server): stop the yarn-install Lambda from running out of memory (#23805)

## Context

Sentry issue
[7438578272](https://twenty-v7.sentry.io/issues/7438578272/) (Logic
Function Layer Build Failed, 7.9K events over 3 months): the
yarn-install tool Lambda dies with `Runtime.OutOfMemory` / `signal:
killed` while building an application's dependency layer. Every layer
build for the affected application fails permanently, each
database-event trigger re-attempts it, and one workspace produced ~2.5K
events in the last week alone.

## Root cause

The offending application declares `twenty-ui@1.0.0-alpha.0` (181MB
unpacked, dragging in 141MB of `@tabler/icons*`) and dev tooling as
production `dependencies` of server-side logic functions. Installing
that tree needs just under 3GB during Yarn 4's fetch/link phase, so the
1024MB sandbox is OOM-killed. And even a successful install could never
ship: AWS caps a function plus all its layers at 250MB unzipped.

The user never sees any of this: the OOM is retried forever, and nothing
tells them their dependencies are the problem.

## Fix

1. **Raise the yarn-install Lambda to 4096MB** so legitimate dependency
trees install. Tool function names now include the
memory/timeout/ephemeral-storage constants in their content hash, so a
config change rotates the function name and the ensure path creates a
fresh function with the new configuration — without this, the constant
change would never reach already-deployed functions (their config is
only applied at creation, and the ensure path early-returns when the
function exists).
2. **Propagate Lambda's own errors to the user.** The install OOM
(`Runtime.OutOfMemory` on the invoke) and the layer size rejection
(`InvalidParameterValueException` at `PublishLayerVersion`) map to a new
`LOGIC_FUNCTION_DEPENDENCIES_SIZE_EXCEEDED` code telling the user to
move packages their logic functions don't import out of `dependencies`.
Surfacing per API boundary:
- **Sync / install (CLI)**: the workspace migration interceptor formats
it into the same metadata validation error shape the SDK already
renders, as one `logicFunction` entry carrying the remedy and the
underlying AWS detail — no SDK rendering changes needed.
- **`executeOneLogicFunction`**: mapped to `UserInputError` in the
GraphQL handler.
- **Route triggers**: HTTP 422 with the user-facing message, no Sentry
capture.
- **Background triggers**: skip instead of retrying, since no retry can
succeed until the user changes their application.

## Test

The error originates in AWS behavior, which CI (local driver, no AWS)
cannot reproduce — so the chain is verified link by link:

- **Real AWS, manual (not in CI)**: reproduced with the offending
application's actual package.json against real Lambdas in the dev
account — OOM-killed at 1024MB and 2048MB (exact prod error signature),
install succeeds at 4096MB (~4min), and the resulting 292MB layer is
rejected by `PublishLayerVersion` with the exact
`InvalidParameterValueException` this PR matches. Same matrix reproduced
in local cgroups beforehand.
- **Server unit specs**: AWS error payload → exception mapping
(`build-yarn-install-failure-exception`), exception → validation payload
formatting (interceptor handler), `executeOneLogicFunction` GraphQL
mapping, route filter 422 mapping, tool-function/layer name hashing.
- **SDK integration spec (mocked server)**: runs the real `app dev`
orchestrator on the minimal app with `syncApplication` mocked to return
the validation-shaped failure, and asserts the CLI report renders the
error code and remedy. It covers CLI rendering only — no test installs
actual oversized dependencies, by design.
- Docs updated (dependency size limits, sync failure taxonomy, route
platform error responses).
This commit is contained in:
Charles Bochet
2026-08-06 15:45:37 +02:00
committed by GitHub
parent c7cfd143c7
commit a7324252fd
22 changed files with 405 additions and 23 deletions
@@ -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`.
@@ -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.
</Note>
#### 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`.
@@ -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.
@@ -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);
});
@@ -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({
@@ -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;
@@ -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,
@@ -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(
@@ -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;
@@ -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,
);
});
});
@@ -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,
);
};
@@ -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;
}
}
@@ -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',
@@ -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,
@@ -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);
}
@@ -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;
}
}
@@ -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:
@@ -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);
});
});
@@ -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:
@@ -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,
);
});
});
@@ -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',
},
);
};
@@ -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);
}