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:
+1
-1
@@ -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;
|
||||
|
||||
+8
@@ -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,
|
||||
|
||||
+35
-13
@@ -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(
|
||||
|
||||
+14
-6
@@ -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;
|
||||
|
||||
+34
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
+25
@@ -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,
|
||||
);
|
||||
};
|
||||
+14
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -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',
|
||||
|
||||
+9
@@ -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,
|
||||
|
||||
+3
@@ -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);
|
||||
}
|
||||
|
||||
+2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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:
|
||||
|
||||
+62
@@ -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);
|
||||
});
|
||||
});
|
||||
+1
@@ -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:
|
||||
|
||||
+43
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
+43
@@ -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',
|
||||
},
|
||||
);
|
||||
};
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user