Improve sentry grouping (#12007)

This PR attemps at improving sentry grouping and filtering by 
- Using the exceptionCode as the fingerprint when the error is a
customException. For this to work in this PR we are now throwing
customExceptions instead of internalServerError deprived of their code.
They will still be converted to Internal server errors when sent back as
response
- Filtering 4xx issues where it was missing (for emailVerification
because errors were not handled, for invalid captcha and billing errors
because they are httpErrors and not graphqlErrors)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Marie
2025-05-14 11:00:06 +02:00
committed by GitHub
parent 6a1da5fe53
commit 9c2b88870f
57 changed files with 376 additions and 152 deletions

View File

@ -194,7 +194,7 @@ export const useAuth = () => {
// TODO: Get intellisense for graphql error extensions code (codegen?) // TODO: Get intellisense for graphql error extensions code (codegen?)
if ( if (
error instanceof ApolloError && error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.code === 'EMAIL_NOT_VERIFIED' error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
) { ) {
setSearchParams({ email }); setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification); setSignInUpStep(SignInUpStep.EmailVerification);

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class GraphqlQueryRunnerException extends CustomException { export class GraphqlQueryRunnerException extends CustomException {
declare code: GraphqlQueryRunnerExceptionCode;
constructor(message: string, code: GraphqlQueryRunnerExceptionCode) { constructor(message: string, code: GraphqlQueryRunnerExceptionCode) {
super(message, code); super(message, code);
} }
@ -18,8 +19,6 @@ export enum GraphqlQueryRunnerExceptionCode {
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND', RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST', INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',
INVALID_ARGS_LAST = 'INVALID_ARGS_LAST', INVALID_ARGS_LAST = 'INVALID_ARGS_LAST',
METADATA_CACHE_VERSION_NOT_FOUND = 'METADATA_CACHE_VERSION_NOT_FOUND',
METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED = 'METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED',
RELATION_SETTINGS_NOT_FOUND = 'RELATION_SETTINGS_NOT_FOUND', RELATION_SETTINGS_NOT_FOUND = 'RELATION_SETTINGS_NOT_FOUND',
RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND', RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND',
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED', NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',

View File

@ -3,7 +3,6 @@ import {
GraphqlQueryRunnerExceptionCode, GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { import {
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -26,7 +25,15 @@ export const graphqlQueryRunnerExceptionHandler = (
throw new UserInputError(error.message); throw new UserInputError(error.message);
case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND: case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND:
throw new NotFoundError(error.message); throw new NotFoundError(error.message);
default: case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND:
throw new InternalServerError(error.message); case GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND:
case GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_COLLECTION_NOT_FOUND:
case GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD:
throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
} }
}; };

View File

@ -4,7 +4,6 @@ import {
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { import {
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
TimeoutError, TimeoutError,
UserInputError, UserInputError,
@ -26,7 +25,11 @@ export const workspaceExceptionHandler = (
case WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT: case WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT:
throw new TimeoutError(error.message); throw new TimeoutError(error.message);
case WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR: case WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR:
default: throw error;
throw new InternalServerError(error.message); default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
} }
}; };

View File

@ -1,8 +1,10 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceQueryRunnerException extends CustomException { export class WorkspaceQueryRunnerException extends CustomException {
code: WorkspaceQueryRunnerExceptionCode;
constructor(message: string, code: WorkspaceQueryRunnerExceptionCode) { constructor(message: string, code: WorkspaceQueryRunnerExceptionCode) {
super(message, code); super(message, code);
this.code = code;
} }
} }

View File

@ -19,6 +19,10 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import {
WorkspaceMetadataVersionException,
WorkspaceMetadataVersionExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable() @Injectable()
@ -89,9 +93,9 @@ export class WorkspaceSchemaFactory {
} }
if (!currentCacheVersion) { if (!currentCacheVersion) {
throw new GraphqlQueryRunnerException( throw new WorkspaceMetadataVersionException(
'Metadata cache version not found', 'Metadata cache version not found',
GraphqlQueryRunnerExceptionCode.METADATA_CACHE_VERSION_NOT_FOUND, WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
); );
} }

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class AuthException extends CustomException { export class AuthException extends CustomException {
declare code: AuthExceptionCode;
constructor(message: string, code: AuthExceptionCode) { constructor(message: string, code: AuthExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -6,9 +6,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception'; } from 'src/engine/core-modules/auth/auth.exception';
import { import {
AuthenticationError, AuthenticationError,
EmailNotVerifiedError,
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -21,18 +19,33 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
throw new NotFoundError(exception.message); throw new NotFoundError(exception.message);
case AuthExceptionCode.INVALID_INPUT: case AuthExceptionCode.INVALID_INPUT:
throw new UserInputError(exception.message); throw new UserInputError(exception.message);
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
throw new EmailNotVerifiedError(exception.message);
case AuthExceptionCode.FORBIDDEN_EXCEPTION: case AuthExceptionCode.FORBIDDEN_EXCEPTION:
throw new ForbiddenError(exception.message); throw new ForbiddenError(exception.message);
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
throw new ForbiddenError(exception.message, {
subCode: AuthExceptionCode.EMAIL_NOT_VERIFIED,
});
case AuthExceptionCode.UNAUTHENTICATED: case AuthExceptionCode.UNAUTHENTICATED:
case AuthExceptionCode.USER_NOT_FOUND: case AuthExceptionCode.USER_NOT_FOUND:
case AuthExceptionCode.WORKSPACE_NOT_FOUND: case AuthExceptionCode.WORKSPACE_NOT_FOUND:
throw new AuthenticationError(exception.message); throw new AuthenticationError(exception.message);
case AuthExceptionCode.INVALID_DATA: case AuthExceptionCode.INVALID_DATA:
case AuthExceptionCode.INTERNAL_SERVER_ERROR: case AuthExceptionCode.INTERNAL_SERVER_ERROR:
default: case AuthExceptionCode.USER_WORKSPACE_NOT_FOUND:
throw new InternalServerError(exception.message); case AuthExceptionCode.INSUFFICIENT_SCOPES:
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
case AuthExceptionCode.SSO_AUTH_FAILED:
case AuthExceptionCode.USE_SSO_AUTH:
case AuthExceptionCode.SIGNUP_DISABLED:
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE:
throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
} }
} }
} }

View File

@ -0,0 +1,29 @@
import {
EmailVerificationException,
EmailVerificationExceptionCode,
} from 'src/engine/core-modules/email-verification/email-verification.exception';
import {
ForbiddenError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
export const emailVerificationGraphqlApiExceptionHandler = (
error: EmailVerificationException,
) => {
switch (error.code) {
case EmailVerificationExceptionCode.INVALID_TOKEN:
case EmailVerificationExceptionCode.INVALID_APP_TOKEN_TYPE:
case EmailVerificationExceptionCode.TOKEN_EXPIRED:
case EmailVerificationExceptionCode.RATE_LIMIT_EXCEEDED:
throw new ForbiddenError(error.message);
case EmailVerificationExceptionCode.EMAIL_MISSING:
case EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED:
case EmailVerificationExceptionCode.EMAIL_VERIFICATION_NOT_REQUIRED:
throw new UserInputError(error.message);
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
};

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class EmailVerificationException extends CustomException { export class EmailVerificationException extends CustomException {
declare code: EmailVerificationExceptionCode;
constructor(message: string, code: EmailVerificationExceptionCode) { constructor(message: string, code: EmailVerificationExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -5,6 +5,8 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input'; import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input';
import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output'; import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output';
import { emailVerificationGraphqlApiExceptionHandler } from 'src/engine/core-modules/email-verification/email-verification-exception-handler.util';
import { EmailVerificationException } from 'src/engine/core-modules/email-verification/email-verification.exception';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator'; import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
@ -23,15 +25,22 @@ export class EmailVerificationResolver {
@OriginHeader() origin: string, @OriginHeader() origin: string,
@Context() context: I18nContext, @Context() context: I18nContext,
): Promise<ResendEmailVerificationTokenOutput> { ): Promise<ResendEmailVerificationTokenOutput> {
const workspace = try {
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( const workspace =
origin, await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
); origin,
);
return await this.emailVerificationService.resendEmailVerificationToken( return await this.emailVerificationService.resendEmailVerificationToken(
resendEmailVerificationTokenInput.email, resendEmailVerificationTokenInput.email,
workspace, workspace,
context.req.headers['x-locale'] ?? SOURCE_LOCALE, context.req.headers['x-locale'] ?? SOURCE_LOCALE,
); );
} catch (error) {
if (error instanceof EmailVerificationException) {
return emailVerificationGraphqlApiExceptionHandler(error);
}
throw error;
}
} }
} }

View File

@ -3,6 +3,7 @@ import * as Sentry from '@sentry/node';
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface'; import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
import { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces'; import { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces';
import { CustomException } from 'src/utils/custom-exception';
export class ExceptionHandlerSentryDriver export class ExceptionHandlerSentryDriver
implements ExceptionHandlerDriverInterface implements ExceptionHandlerDriverInterface
@ -15,8 +16,7 @@ export class ExceptionHandlerSentryDriver
Sentry.withScope((scope) => { Sentry.withScope((scope) => {
if (options?.operation) { if (options?.operation) {
scope.setTag('operation', options.operation.name); scope.setExtra('operation', options.operation.name);
scope.setTag('operationName', options.operation.name);
} }
if (options?.document) { if (options?.document) {
@ -49,6 +49,11 @@ export class ExceptionHandlerSentryDriver
}); });
} }
if (exception instanceof CustomException) {
scope.setTag('customExceptionCode', exception.code);
scope.setFingerprint([exception.code]);
}
const eventId = Sentry.captureException(exception, { const eventId = Sentry.captureException(exception, {
contexts: { contexts: {
GraphQL: { GraphQL: {

View File

@ -24,7 +24,6 @@ export enum ErrorCode {
PERSISTED_QUERY_NOT_SUPPORTED = 'PERSISTED_QUERY_NOT_SUPPORTED', PERSISTED_QUERY_NOT_SUPPORTED = 'PERSISTED_QUERY_NOT_SUPPORTED',
BAD_USER_INPUT = 'BAD_USER_INPUT', BAD_USER_INPUT = 'BAD_USER_INPUT',
NOT_FOUND = 'NOT_FOUND', NOT_FOUND = 'NOT_FOUND',
EMAIL_NOT_VERIFIED = 'EMAIL_NOT_VERIFIED',
METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED', METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED',
CONFLICT = 'CONFLICT', CONFLICT = 'CONFLICT',
TIMEOUT = 'TIMEOUT', TIMEOUT = 'TIMEOUT',
@ -107,16 +106,16 @@ export class ValidationError extends BaseGraphQLError {
} }
export class AuthenticationError extends BaseGraphQLError { export class AuthenticationError extends BaseGraphQLError {
constructor(message: string) { constructor(message: string, extensions?: Record<string, any>) {
super(message, ErrorCode.UNAUTHENTICATED); super(message, ErrorCode.UNAUTHENTICATED, extensions);
Object.defineProperty(this, 'name', { value: 'AuthenticationError' }); Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
} }
} }
export class ForbiddenError extends BaseGraphQLError { export class ForbiddenError extends BaseGraphQLError {
constructor(message: string) { constructor(message: string, extensions?: Record<string, any>) {
super(message, ErrorCode.FORBIDDEN); super(message, ErrorCode.FORBIDDEN, extensions);
Object.defineProperty(this, 'name', { value: 'ForbiddenError' }); Object.defineProperty(this, 'name', { value: 'ForbiddenError' });
} }
@ -161,14 +160,6 @@ export class NotFoundError extends BaseGraphQLError {
} }
} }
export class EmailNotVerifiedError extends BaseGraphQLError {
constructor(message: string) {
super(message, ErrorCode.EMAIL_NOT_VERIFIED);
Object.defineProperty(this, 'name', { value: 'EmailNotVerifiedError' });
}
}
export class MethodNotAllowedError extends BaseGraphQLError { export class MethodNotAllowedError extends BaseGraphQLError {
constructor(message: string) { constructor(message: string) {
super(message, ErrorCode.METHOD_NOT_ALLOWED); super(message, ErrorCode.METHOD_NOT_ALLOWED);

View File

@ -1,3 +1,5 @@
import { HttpException } from '@nestjs/common';
import { import {
BaseGraphQLError, BaseGraphQLError,
ErrorCode, ErrorCode,
@ -12,7 +14,6 @@ export const graphQLErrorCodesToFilterOut = [
ErrorCode.TIMEOUT, ErrorCode.TIMEOUT,
ErrorCode.CONFLICT, ErrorCode.CONFLICT,
ErrorCode.BAD_USER_INPUT, ErrorCode.BAD_USER_INPUT,
ErrorCode.EMAIL_NOT_VERIFIED,
]; ];
export const shouldCaptureException = (exception: Error): boolean => { export const shouldCaptureException = (exception: Error): boolean => {
@ -23,5 +24,13 @@ export const shouldCaptureException = (exception: Error): boolean => {
return false; return false;
} }
if (
exception instanceof HttpException &&
exception.getStatus() >= 400 &&
exception.getStatus() < 500
) {
return false;
}
return true; return true;
}; };

View File

@ -1,13 +1,12 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class SearchException extends CustomException { export class SearchException extends CustomException {
declare code: SearchExceptionCode;
constructor(message: string, code: SearchExceptionCode) { constructor(message: string, code: SearchExceptionCode) {
super(message, code); super(message, code);
} }
} }
export enum SearchExceptionCode { export enum SearchExceptionCode {
METADATA_CACHE_VERSION_NOT_FOUND = 'METADATA_CACHE_VERSION_NOT_FOUND',
LABEL_IDENTIFIER_FIELD_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_NOT_FOUND', LABEL_IDENTIFIER_FIELD_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_NOT_FOUND',
OBJECT_METADATA_MAP_NOT_FOUND = 'OBJECT_METADATA_MAP_NOT_FOUND',
} }

View File

@ -1,7 +1,9 @@
import { Catch, ExceptionFilter } from '@nestjs/common'; import { Catch, ExceptionFilter } from '@nestjs/common';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import {
import { SearchException } from 'src/engine/core-modules/search/exceptions/search.exception'; SearchException,
SearchExceptionCode,
} from 'src/engine/core-modules/search/exceptions/search.exception';
@Catch(SearchException) @Catch(SearchException)
export class SearchApiExceptionFilter implements ExceptionFilter { export class SearchApiExceptionFilter implements ExceptionFilter {
@ -9,8 +11,13 @@ export class SearchApiExceptionFilter implements ExceptionFilter {
catch(exception: SearchException) { catch(exception: SearchException) {
switch (exception.code) { switch (exception.code) {
default: case SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND:
throw new InternalServerError(exception.message); throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
} }
} }
} }

View File

@ -7,16 +7,20 @@ import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-build
import { SearchArgs } from 'src/engine/core-modules/search/dtos/search-args'; import { SearchArgs } from 'src/engine/core-modules/search/dtos/search-args';
import { SearchRecordDTO } from 'src/engine/core-modules/search/dtos/search-record-dto'; import { SearchRecordDTO } from 'src/engine/core-modules/search/dtos/search-record-dto';
import {
SearchException,
SearchExceptionCode,
} from 'src/engine/core-modules/search/exceptions/search.exception';
import { SearchApiExceptionFilter } from 'src/engine/core-modules/search/filters/search-api-exception.filter'; import { SearchApiExceptionFilter } from 'src/engine/core-modules/search/filters/search-api-exception.filter';
import { SearchService } from 'src/engine/core-modules/search/services/search.service'; import { SearchService } from 'src/engine/core-modules/search/services/search.service';
import { RecordsWithObjectMetadataItem } from 'src/engine/core-modules/search/types/records-with-object-metadata-item'; import { RecordsWithObjectMetadataItem } from 'src/engine/core-modules/search/types/records-with-object-metadata-item';
import { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms'; import { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import {
WorkspaceMetadataCacheException,
WorkspaceMetadataCacheExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception';
import {
WorkspaceMetadataVersionException,
WorkspaceMetadataVersionExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@ -47,9 +51,9 @@ export class SearchResolver {
await this.workspaceCacheStorageService.getMetadataVersion(workspace.id); await this.workspaceCacheStorageService.getMetadataVersion(workspace.id);
if (currentCacheVersion === undefined) { if (currentCacheVersion === undefined) {
throw new SearchException( throw new WorkspaceMetadataVersionException(
'Metadata cache version not found', `Metadata version not found for workspace ${workspace.id}`,
SearchExceptionCode.METADATA_CACHE_VERSION_NOT_FOUND, WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
); );
} }
@ -60,9 +64,9 @@ export class SearchResolver {
); );
if (!objectMetadataMaps) { if (!objectMetadataMaps) {
throw new SearchException( throw new WorkspaceMetadataCacheException(
`Object metadata map not found for workspace ${workspace.id} and metadata version ${currentCacheVersion}`, `Object metadata map not found for workspace ${workspace.id} and metadata version ${currentCacheVersion}`,
SearchExceptionCode.OBJECT_METADATA_MAP_NOT_FOUND, WorkspaceMetadataCacheExceptionCode.OBJECT_METADATA_MAP_NOT_FOUND,
); );
} }

View File

@ -2,7 +2,6 @@ import { Catch, ExceptionFilter } from '@nestjs/common';
import { import {
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -25,8 +24,13 @@ export class ConfigVariableGraphqlApiExceptionFilter
case ConfigVariableExceptionCode.VALIDATION_FAILED: case ConfigVariableExceptionCode.VALIDATION_FAILED:
throw new UserInputError(exception.message); throw new UserInputError(exception.message);
case ConfigVariableExceptionCode.INTERNAL_ERROR: case ConfigVariableExceptionCode.INTERNAL_ERROR:
default: case ConfigVariableExceptionCode.UNSUPPORTED_CONFIG_TYPE:
throw new InternalServerError(exception.message); throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
} }
} }
} }

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class ConfigVariableException extends CustomException { export class ConfigVariableException extends CustomException {
declare code: ConfigVariableExceptionCode;
constructor(message: string, code: ConfigVariableExceptionCode) { constructor(message: string, code: ConfigVariableExceptionCode) {
super(message, code); super(message, code);
} }
@ -12,6 +13,5 @@ export enum ConfigVariableExceptionCode {
VARIABLE_NOT_FOUND = 'VARIABLE_NOT_FOUND', VARIABLE_NOT_FOUND = 'VARIABLE_NOT_FOUND',
VALIDATION_FAILED = 'VALIDATION_FAILED', VALIDATION_FAILED = 'VALIDATION_FAILED',
UNSUPPORTED_CONFIG_TYPE = 'UNSUPPORTED_CONFIG_TYPE', UNSUPPORTED_CONFIG_TYPE = 'UNSUPPORTED_CONFIG_TYPE',
METADATA_NOT_FOUND = 'METADATA_NOT_FOUND',
INTERNAL_ERROR = 'INTERNAL_ERROR', INTERNAL_ERROR = 'INTERNAL_ERROR',
} }

View File

@ -1,7 +1,6 @@
import { Catch, ExceptionFilter } from '@nestjs/common'; import { Catch, ExceptionFilter } from '@nestjs/common';
import { import {
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -25,8 +24,13 @@ export class WorkflowTriggerGraphqlApiExceptionFilter
throw new UserInputError(exception.message); throw new UserInputError(exception.message);
case WorkflowTriggerExceptionCode.NOT_FOUND: case WorkflowTriggerExceptionCode.NOT_FOUND:
throw new NotFoundError(exception.message); throw new NotFoundError(exception.message);
default: case WorkflowTriggerExceptionCode.INTERNAL_ERROR:
throw new InternalServerError(exception.message); throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
} }
} }
} }

View File

@ -1,6 +1,5 @@
import { import {
ConflictError, ConflictError,
InternalServerError,
NotFoundError, NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util'; import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util';
@ -8,6 +7,7 @@ import {
WorkspaceException, WorkspaceException,
WorkspaceExceptionCode, WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception'; } from 'src/engine/core-modules/workspace/workspace.exception';
import { CustomException } from 'src/utils/custom-exception';
describe('workspaceGraphqlApiExceptionHandler', () => { describe('workspaceGraphqlApiExceptionHandler', () => {
it('should throw NotFoundError when WorkspaceExceptionCode is SUBDOMAIN_NOT_FOUND', () => { it('should throw NotFoundError when WorkspaceExceptionCode is SUBDOMAIN_NOT_FOUND', () => {
@ -48,7 +48,7 @@ describe('workspaceGraphqlApiExceptionHandler', () => {
const error = new WorkspaceException('Unknown error', 'UNKNOWN_CODE'); const error = new WorkspaceException('Unknown error', 'UNKNOWN_CODE');
expect(() => workspaceGraphqlApiExceptionHandler(error)).toThrow( expect(() => workspaceGraphqlApiExceptionHandler(error)).toThrow(
InternalServerError, CustomException,
); );
}); });

View File

@ -1,7 +1,6 @@
import { import {
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { import {
@ -15,12 +14,17 @@ export const workspaceGraphqlApiExceptionHandler = (error: Error) => {
case WorkspaceExceptionCode.SUBDOMAIN_NOT_FOUND: case WorkspaceExceptionCode.SUBDOMAIN_NOT_FOUND:
case WorkspaceExceptionCode.WORKSPACE_NOT_FOUND: case WorkspaceExceptionCode.WORKSPACE_NOT_FOUND:
throw new NotFoundError(error.message); throw new NotFoundError(error.message);
case WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN:
case WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN: case WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN:
throw new ConflictError(error.message); throw new ConflictError(error.message);
case WorkspaceExceptionCode.ENVIRONMENT_VAR_NOT_ENABLED: case WorkspaceExceptionCode.ENVIRONMENT_VAR_NOT_ENABLED:
case WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED:
throw new ForbiddenError(error.message); throw new ForbiddenError(error.message);
default: default: {
throw new InternalServerError(error.message); const _exhaustiveCheck: never = error.code;
throw error;
}
} }
} }

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceException extends CustomException { export class WorkspaceException extends CustomException {
declare code: WorkspaceExceptionCode;
constructor(message: string, code: WorkspaceExceptionCode) { constructor(message: string, code: WorkspaceExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class FieldMetadataException extends CustomException { export class FieldMetadataException extends CustomException {
declare code: FieldMetadataExceptionCode;
constructor(message: string, code: FieldMetadataExceptionCode) { constructor(message: string, code: FieldMetadataExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -46,7 +46,7 @@ import {
RelationMetadataEntity, RelationMetadataEntity,
RelationMetadataType, RelationMetadataType,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
@ -614,7 +614,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
try { try {
validateMetadataNameOrThrow(fieldMetadataInput.name); validateMetadataNameOrThrow(fieldMetadataInput.name);
} catch (error) { } catch (error) {
if (error instanceof InvalidMetadataNameException) { if (error instanceof InvalidMetadataException) {
throw new FieldMetadataException( throw new FieldMetadataException(
error.message, error.message,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT, FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
@ -630,7 +630,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
objectMetadata, objectMetadata,
); );
} catch (error) { } catch (error) {
if (error instanceof InvalidMetadataNameException) { if (error instanceof InvalidMetadataException) {
throw new FieldMetadataException( throw new FieldMetadataException(
`Name "${fieldMetadataInput.name}" is not available, check that it is not duplicating another field's name.`, `Name "${fieldMetadataInput.name}" is not available, check that it is not duplicating another field's name.`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT, FieldMetadataExceptionCode.INVALID_FIELD_INPUT,

View File

@ -11,6 +11,14 @@ import {
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { removeFieldMapsFromObjectMetadata } from 'src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util'; import { removeFieldMapsFromObjectMetadata } from 'src/engine/metadata-modules/utils/remove-field-maps-from-object-metadata.util';
import {
WorkspaceMetadataCacheException,
WorkspaceMetadataCacheExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception';
import {
WorkspaceMetadataVersionException,
WorkspaceMetadataVersionExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable() @Injectable()
@ -43,9 +51,9 @@ export class FieldMetadataRelationService {
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
if (!isDefined(metadataVersion)) { if (!isDefined(metadataVersion)) {
throw new FieldMetadataException( throw new WorkspaceMetadataVersionException(
`Metadata version not found for workspace ${workspaceId}`, `Metadata version not found for workspace ${workspaceId}`,
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
); );
} }
@ -56,9 +64,9 @@ export class FieldMetadataRelationService {
); );
if (!objectMetadataMaps) { if (!objectMetadataMaps) {
throw new FieldMetadataException( throw new WorkspaceMetadataCacheException(
`Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`, `Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`,
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, WorkspaceMetadataCacheExceptionCode.OBJECT_METADATA_MAP_NOT_FOUND,
); );
} }

View File

@ -1,7 +1,6 @@
import { import {
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -31,8 +30,12 @@ export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED: case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED:
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED: case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED:
case FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND: case FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND:
default: throw error;
throw new InternalServerError(error.message); default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
} }
} }

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class ObjectMetadataException extends CustomException { export class ObjectMetadataException extends CustomException {
declare code: ObjectMetadataExceptionCode;
constructor(message: string, code: ObjectMetadataExceptionCode) { constructor(message: string, code: ObjectMetadataExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -1,7 +1,6 @@
import { import {
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -26,8 +25,13 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
throw new ForbiddenError(error.message); throw new ForbiddenError(error.message);
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS: case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
throw new ConflictError(error.message); throw new ConflictError(error.message);
default: case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
throw new InternalServerError(error.message); throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
} }
} }

View File

@ -6,7 +6,7 @@ import {
ObjectMetadataException, ObjectMetadataException,
ObjectMetadataExceptionCode, ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { validateMetadataNameIsNotTooLongOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils'; import { validateMetadataNameIsNotTooLongOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-long.utils';
import { validateMetadataNameIsNotTooShortOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils'; import { validateMetadataNameIsNotTooShortOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-too-short.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
@ -28,7 +28,7 @@ export const validateObjectMetadataInputNameOrThrow = (name: string): void => {
try { try {
validateMetadataNameOrThrow(name); validateMetadataNameOrThrow(name);
} catch (error) { } catch (error) {
if (error instanceof InvalidMetadataNameException) { if (error instanceof InvalidMetadataException) {
throw new ObjectMetadataException( throw new ObjectMetadataException(
error.message, error.message,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
@ -58,7 +58,7 @@ const validateObjectMetadataInputLabelOrThrow = (name: string): void => {
try { try {
validators.forEach((validator) => validator(name.trim())); validators.forEach((validator) => validator(name.trim()));
} catch (error) { } catch (error) {
if (error instanceof InvalidMetadataNameException) { if (error instanceof InvalidMetadataException) {
throw new ObjectMetadataException( throw new ObjectMetadataException(
error.message, error.message,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class PermissionsException extends CustomException { export class PermissionsException extends CustomException {
declare code: PermissionsExceptionCode;
constructor(message: string, code: PermissionsExceptionCode) { constructor(message: string, code: PermissionsExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -1,6 +1,5 @@
import { import {
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -29,7 +28,20 @@ export const permissionGraphqlApiExceptionHandler = (
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND: case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
throw new NotFoundError(error.message); throw new NotFoundError(error.message);
case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND: case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND:
default: case PermissionsExceptionCode.WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH:
throw new InternalServerError(error.message); case PermissionsExceptionCode.TOO_MANY_ADMIN_CANDIDATES:
case PermissionsExceptionCode.USER_WORKSPACE_ALREADY_HAS_ROLE:
case PermissionsExceptionCode.ADMIN_ROLE_NOT_FOUND:
case PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED:
case PermissionsExceptionCode.WORKSPACE_MEMBER_NOT_FOUND:
case PermissionsExceptionCode.UNKNOWN_OPERATION_NAME:
case PermissionsExceptionCode.UNKNOWN_REQUIRED_PERMISSION:
case PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE:
throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
} }
}; };

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class RelationMetadataException extends CustomException { export class RelationMetadataException extends CustomException {
declare code: RelationMetadataExceptionCode;
constructor(message: string, code: RelationMetadataExceptionCode) { constructor(message: string, code: RelationMetadataExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -22,9 +22,17 @@ import {
RelationMetadataException, RelationMetadataException,
RelationMetadataExceptionCode, RelationMetadataExceptionCode,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception'; } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import {
WorkspaceMetadataCacheException,
WorkspaceMetadataCacheExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception';
import {
WorkspaceMetadataVersionException,
WorkspaceMetadataVersionExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import { import {
@ -73,7 +81,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
validateMetadataNameOrThrow(relationMetadataInput.fromName); validateMetadataNameOrThrow(relationMetadataInput.fromName);
validateMetadataNameOrThrow(relationMetadataInput.toName); validateMetadataNameOrThrow(relationMetadataInput.toName);
} catch (error) { } catch (error) {
if (error instanceof InvalidMetadataNameException) if (error instanceof InvalidMetadataException)
throw new RelationMetadataException( throw new RelationMetadataException(
error.message, error.message,
RelationMetadataExceptionCode.INVALID_RELATION_INPUT, RelationMetadataExceptionCode.INVALID_RELATION_INPUT,
@ -497,8 +505,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
if (!isDefined(metadataVersion)) { if (!isDefined(metadataVersion)) {
throw new NotFoundException( throw new WorkspaceMetadataVersionException(
`Metadata version not found for workspace ${workspaceId}`, `Metadata version not found for workspace ${workspaceId}`,
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
); );
} }
@ -509,8 +518,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
); );
if (!objectMetadataMaps) { if (!objectMetadataMaps) {
throw new NotFoundException( throw new WorkspaceMetadataCacheException(
`Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`, `Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`,
WorkspaceMetadataCacheExceptionCode.OBJECT_METADATA_MAP_NOT_FOUND,
); );
} }

View File

@ -1,7 +1,5 @@
import { import {
ConflictError, ConflictError,
InternalServerError,
NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { import {
@ -17,15 +15,18 @@ export const relationMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof RelationMetadataException) { if (error instanceof RelationMetadataException) {
switch (error.code) { switch (error.code) {
case RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND:
throw new NotFoundError(error.message);
case RelationMetadataExceptionCode.INVALID_RELATION_INPUT: case RelationMetadataExceptionCode.INVALID_RELATION_INPUT:
throw new UserInputError(error.message); throw new UserInputError(error.message);
case RelationMetadataExceptionCode.RELATION_ALREADY_EXISTS: case RelationMetadataExceptionCode.RELATION_ALREADY_EXISTS:
throw new ConflictError(error.message); throw new ConflictError(error.message);
case RelationMetadataExceptionCode.FOREIGN_KEY_NOT_FOUND: case RelationMetadataExceptionCode.FOREIGN_KEY_NOT_FOUND:
default: case RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND:
throw new InternalServerError(error.message); throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
} }
} }

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class RemoteServerException extends CustomException { export class RemoteServerException extends CustomException {
declare code: RemoteServerExceptionCode;
constructor(message: string, code: RemoteServerExceptionCode) { constructor(message: string, code: RemoteServerExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class RemoteTableException extends CustomException { export class RemoteTableException extends CustomException {
declare code: RemoteTableExceptionCode;
constructor(message: string, code: RemoteTableExceptionCode) { constructor(message: string, code: RemoteTableExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -1,6 +1,5 @@
import { import {
ConflictError, ConflictError,
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -21,8 +20,11 @@ export const remoteTableGraphqlApiExceptionHandler = (error: Error) => {
throw new UserInputError(error.message); throw new UserInputError(error.message);
case RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS: case RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS:
throw new ConflictError(error.message); throw new ConflictError(error.message);
default: default: {
throw new InternalServerError(error.message); const _exhaustiveCheck: never = error.code;
throw error;
}
} }
} }

View File

@ -1,7 +1,6 @@
import { import {
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
UserInputError, UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@ -21,8 +20,13 @@ export const remoteServerGraphqlApiExceptionHandler = (error: any) => {
throw new ForbiddenError(error.message); throw new ForbiddenError(error.message);
case RemoteServerExceptionCode.REMOTE_SERVER_ALREADY_EXISTS: case RemoteServerExceptionCode.REMOTE_SERVER_ALREADY_EXISTS:
throw new ConflictError(error.message); throw new ConflictError(error.message);
default: case RemoteServerExceptionCode.REMOTE_SERVER_CONNECTION_ERROR:
throw new InternalServerError(error.message); throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
} }
} }

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class ServerlessFunctionException extends CustomException { export class ServerlessFunctionException extends CustomException {
declare code: ServerlessFunctionExceptionCode;
constructor(message: string, code: ServerlessFunctionExceptionCode) { constructor(message: string, code: ServerlessFunctionExceptionCode) {
super(message, code); super(message, code);
} }

View File

@ -1,7 +1,6 @@
import { import {
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
InternalServerError,
NotFoundError, NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { import {
@ -20,9 +19,15 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY: case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING: case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING:
case ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID: case ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED:
throw new ForbiddenError(error.message); throw new ForbiddenError(error.message);
default: case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CODE_UNCHANGED:
throw new InternalServerError(error.message); throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
} }
} }
throw error; throw error;

View File

@ -2,11 +2,17 @@ import camelCase from 'lodash.camelcase';
import { slugify } from 'transliteration'; import { slugify } from 'transliteration';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
export const computeMetadataNameFromLabel = (label: string): string => { export const computeMetadataNameFromLabel = (label: string): string => {
if (!isDefined(label)) { if (!isDefined(label)) {
throw new InvalidMetadataException('Label is required'); throw new InvalidMetadataException(
'Label is required',
InvalidMetadataExceptionCode.LABEL_REQUIRED,
);
} }
const prefixedLabel = /^\d/.test(label) ? `n${label}` : label; const prefixedLabel = /^\d/.test(label) ? `n${label}` : label;
@ -22,7 +28,10 @@ export const computeMetadataNameFromLabel = (label: string): string => {
}); });
if (formattedString === '') { if (formattedString === '') {
throw new InvalidMetadataException(`Invalid label: "${label}"`); throw new InvalidMetadataException(
`Invalid label: "${label}"`,
InvalidMetadataExceptionCode.INVALID_LABEL,
);
} }
return camelCase(formattedString); return camelCase(formattedString);

View File

@ -1,5 +0,0 @@
export class InvalidMetadataNameException extends Error {
constructor(message: string) {
super(message);
}
}

View File

@ -1,5 +1,19 @@
export class InvalidMetadataException extends Error { import { CustomException } from 'src/utils/custom-exception';
constructor(message: string) {
super(message); export class InvalidMetadataException extends CustomException {
constructor(message: string, code: InvalidMetadataExceptionCode) {
super(message, code);
} }
} }
export enum InvalidMetadataExceptionCode {
LABEL_REQUIRED = 'Label required',
INPUT_TOO_SHORT = 'Input too short',
EXCEEDS_MAX_LENGTH = 'Exceeds max length',
RESERVED_KEYWORD = 'Reserved keyword',
NOT_CAMEL_CASE = 'Not camel case',
INVALID_LABEL = 'Invalid label',
NAME_NOT_SYNCED_WITH_LABEL = 'Name not synced with label',
INVALID_STRING = 'Invalid string',
NOT_AVAILABLE = 'Name not available',
}

View File

@ -2,7 +2,10 @@ import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-meta
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const getReservedCompositeFieldNames = ( const getReservedCompositeFieldNames = (
objectMetadata: ObjectMetadataEntity, objectMetadata: ObjectMetadataEntity,
@ -33,10 +36,16 @@ export const validateFieldNameAvailabilityOrThrow = (
getReservedCompositeFieldNames(objectMetadata); getReservedCompositeFieldNames(objectMetadata);
if (objectMetadata.fields.some((field) => field.name === name)) { if (objectMetadata.fields.some((field) => field.name === name)) {
throw new InvalidMetadataNameException(`Name "${name}" is not available`); throw new InvalidMetadataException(
`Name "${name}" is not available`,
InvalidMetadataExceptionCode.NOT_AVAILABLE,
);
} }
if (reservedCompositeFieldsNames.includes(name)) { if (reservedCompositeFieldsNames.includes(name)) {
throw new InvalidMetadataNameException(`Name "${name}" is not available`); throw new InvalidMetadataException(
`Name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
);
} }
}; };

View File

@ -1,11 +1,15 @@
import camelCase from 'lodash.camelcase'; import camelCase from 'lodash.camelcase';
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
export const validateMetadataNameIsCamelCaseOrThrow = (name: string) => { export const validateMetadataNameIsCamelCaseOrThrow = (name: string) => {
if (name !== camelCase(name)) { if (name !== camelCase(name)) {
throw new InvalidMetadataNameException( throw new InvalidMetadataException(
`Name should be in camelCase: ${name}`, `Name should be in camelCase: ${name}`,
InvalidMetadataExceptionCode.NOT_CAMEL_CASE,
); );
} }
}; };

View File

@ -1,4 +1,7 @@
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const coreObjectNames = [ const coreObjectNames = [
'approvedAccessDomain', 'approvedAccessDomain',
@ -64,8 +67,9 @@ export const validateMetadataNameIsNotReservedKeywordOrThrow = (
name: string, name: string,
) => { ) => {
if (reservedKeywords.includes(name)) { if (reservedKeywords.includes(name)) {
throw new InvalidMetadataNameException( throw new InvalidMetadataException(
`The name "${name}" is not available`, `The name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
); );
} }
}; };

View File

@ -1,10 +1,14 @@
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils';
export const validateMetadataNameIsNotTooLongOrThrow = (name: string) => { export const validateMetadataNameIsNotTooLongOrThrow = (name: string) => {
if (exceedsDatabaseIdentifierMaximumLength(name)) { if (exceedsDatabaseIdentifierMaximumLength(name)) {
throw new InvalidMetadataNameException( throw new InvalidMetadataException(
`String "${name}" exceeds 63 characters limit`, `String "${name}" exceeds 63 characters limit`,
InvalidMetadataExceptionCode.EXCEEDS_MAX_LENGTH,
); );
} }
}; };

View File

@ -1,8 +1,14 @@
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
import { beneathDatabaseIdentifierMinimumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils'; import { beneathDatabaseIdentifierMinimumLength } from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils';
export const validateMetadataNameIsNotTooShortOrThrow = (name: string) => { export const validateMetadataNameIsNotTooShortOrThrow = (name: string) => {
if (beneathDatabaseIdentifierMinimumLength(name)) { if (beneathDatabaseIdentifierMinimumLength(name)) {
throw new InvalidMetadataNameException(`Input is too short: "${name}"`); throw new InvalidMetadataException(
`Input is too short: "${name}"`,
InvalidMetadataExceptionCode.INPUT_TOO_SHORT,
);
} }
}; };

View File

@ -1,4 +1,7 @@
import { InvalidMetadataNameException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata-name.exception'; import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX = const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX =
/^[a-z][a-zA-Z0-9]*$/; /^[a-z][a-zA-Z0-9]*$/;
@ -10,8 +13,9 @@ export const validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLett
STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX, STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX,
) )
) { ) {
throw new InvalidMetadataNameException( throw new InvalidMetadataException(
`String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`, `String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`,
InvalidMetadataExceptionCode.INVALID_STRING,
); );
} }
}; };

View File

@ -2,7 +2,10 @@ import camelCase from 'lodash.camelcase';
import { slugify } from 'transliteration'; import { slugify } from 'transliteration';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
export const validateNameAndLabelAreSyncOrThrow = ( export const validateNameAndLabelAreSyncOrThrow = (
label: string, label: string,
@ -13,13 +16,17 @@ export const validateNameAndLabelAreSyncOrThrow = (
if (name !== computedName) { if (name !== computedName) {
throw new InvalidMetadataException( throw new InvalidMetadataException(
`Name is not synced with label. Expected name: "${computedName}", got ${name}`, `Name is not synced with label. Expected name: "${computedName}", got ${name}`,
InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL,
); );
} }
}; };
export const computeMetadataNameFromLabel = (label: string): string => { export const computeMetadataNameFromLabel = (label: string): string => {
if (!isDefined(label)) { if (!isDefined(label)) {
throw new InvalidMetadataException('Label is required'); throw new InvalidMetadataException(
'Label is required',
InvalidMetadataExceptionCode.LABEL_REQUIRED,
);
} }
const prefixedLabel = /^\d/.test(label) ? `n${label}` : label; const prefixedLabel = /^\d/.test(label) ? `n${label}` : label;
@ -35,7 +42,10 @@ export const computeMetadataNameFromLabel = (label: string): string => {
}); });
if (formattedString === '') { if (formattedString === '') {
throw new InvalidMetadataException(`Invalid label: "${label}"`); throw new InvalidMetadataException(
`Invalid label: "${label}"`,
InvalidMetadataExceptionCode.INVALID_LABEL,
);
} }
return camelCase(formattedString); return camelCase(formattedString);

View File

@ -7,5 +7,5 @@ export class WorkspaceMetadataCacheException extends CustomException {
} }
export enum WorkspaceMetadataCacheExceptionCode { export enum WorkspaceMetadataCacheExceptionCode {
METADATA_VERSION_NOT_FOUND = 'METADATA_VERSION_NOT_FOUND', OBJECT_METADATA_MAP_NOT_FOUND = 'Object Metadata map not found',
} }

View File

@ -1,17 +1,17 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
import { import {
WorkspaceMetadataCacheException, WorkspaceMetadataVersionException,
WorkspaceMetadataCacheExceptionCode, WorkspaceMetadataVersionExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception'; } from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable() @Injectable()
@ -46,9 +46,9 @@ export class WorkspaceMetadataCacheService {
await this.getMetadataVersionFromDatabase(workspaceId); await this.getMetadataVersionFromDatabase(workspaceId);
if (!isDefined(currentDatabaseVersion)) { if (!isDefined(currentDatabaseVersion)) {
throw new WorkspaceMetadataCacheException( throw new WorkspaceMetadataVersionException(
'Metadata version not found in the database', 'Metadata version not found in the database',
WorkspaceMetadataCacheExceptionCode.METADATA_VERSION_NOT_FOUND, WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
); );
} }

View File

@ -11,6 +11,10 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import {
WorkspaceMetadataVersionException,
WorkspaceMetadataVersionExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
import { import {
ROLES_PERMISSIONS, ROLES_PERMISSIONS,
@ -312,9 +316,9 @@ export class WorkspaceDatasourceFactory {
if (!isDefined(latestWorkspaceMetadataVersion)) { if (!isDefined(latestWorkspaceMetadataVersion)) {
if (shouldFailIfMetadataNotFound) { if (shouldFailIfMetadataNotFound) {
throw new TwentyORMException( throw new WorkspaceMetadataVersionException(
`Metadata version not found for workspace ${workspaceId}`, `Metadata version not found for workspace ${workspaceId}`,
TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND, WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
); );
} else { } else {
await this.workspaceMetadataCacheService.recomputeMetadataCache({ await this.workspaceMetadataCacheService.recomputeMetadataCache({
@ -330,7 +334,7 @@ export class WorkspaceDatasourceFactory {
if (!isDefined(latestWorkspaceMetadataVersion)) { if (!isDefined(latestWorkspaceMetadataVersion)) {
throw new TwentyORMException( throw new TwentyORMException(
`Metadata version not found after recompute for workspace ${workspaceId}`, `Metadata version not found after recompute`,
TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND, TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND,
); );
} }

View File

@ -148,16 +148,21 @@ export class CalendarEventImportErrorHandlerService {
workspaceId, workspaceId,
); );
this.exceptionHandlerService.captureExceptions([exception], { const calendarEventImportException = new CalendarEventImportException(
workspace: {
id: workspaceId,
},
});
throw new CalendarEventImportException(
`Unknown error importing calendar events for calendar channel ${calendarChannel.id} in workspace ${workspaceId}: ${exception.message}`, `Unknown error importing calendar events for calendar channel ${calendarChannel.id} in workspace ${workspaceId}: ${exception.message}`,
CalendarEventImportExceptionCode.UNKNOWN, CalendarEventImportExceptionCode.UNKNOWN,
); );
this.exceptionHandlerService.captureExceptions(
[calendarEventImportException],
{
workspace: {
id: workspaceId,
},
},
);
throw calendarEventImportException;
} }
private async handleNotFoundException( private async handleNotFoundException(

View File

@ -162,7 +162,10 @@ export class MessageImportExceptionHandlerService {
); );
this.exceptionHandlerService.captureExceptions([ this.exceptionHandlerService.captureExceptions([
`Unknown error importing messages for message channel ${messageChannel.id} in workspace ${workspaceId}: ${exception.message}`, new MessageImportException(
`Unknown error importing messages for message channel ${messageChannel.id} in workspace ${workspaceId}: ${exception.message}`,
MessageImportExceptionCode.UNKNOWN,
),
]); ]);
throw new MessageImportException( throw new MessageImportException(

View File

@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
export class WorkflowTriggerException extends CustomException { export class WorkflowTriggerException extends CustomException {
declare code: WorkflowTriggerExceptionCode;
constructor(message: string, code: WorkflowTriggerExceptionCode) { constructor(message: string, code: WorkflowTriggerExceptionCode) {
super(message, code); super(message, code);
} }