Compare commits

...

2 Commits

Author SHA1 Message Date
Etienne 1f12178ad2 fix calendar events and messages post hooks (#12034)
Context
workspaceMemberId is not always present in AuthContext. Solution
implemented here is to fetch workspaceMemberId via userId.

QRQC coming ... 

Tested
- FindManyCalendarEvents / FindOneCalendarEvent
- FindManyMessages / FindOneMessage

closes https://github.com/twentyhq/twenty/issues/12027
2025-05-14 17:19:16 +02:00
Marie ffe9fff33c 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>
2025-05-14 17:19:00 +02:00
65 changed files with 639 additions and 201 deletions
@@ -194,7 +194,7 @@ export const useAuth = () => {
// TODO: Get intellisense for graphql error extensions code (codegen?)
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.code === 'EMAIL_NOT_VERIFIED'
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class GraphqlQueryRunnerException extends CustomException {
declare code: GraphqlQueryRunnerExceptionCode;
constructor(message: string, code: GraphqlQueryRunnerExceptionCode) {
super(message, code);
}
@@ -18,8 +19,6 @@ export enum GraphqlQueryRunnerExceptionCode {
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',
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_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND',
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
@@ -3,7 +3,6 @@ import {
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import {
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -26,7 +25,15 @@ export const graphqlQueryRunnerExceptionHandler = (
throw new UserInputError(error.message);
case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND:
throw new NotFoundError(error.message);
default:
throw new InternalServerError(error.message);
case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND:
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;
}
}
};
@@ -4,7 +4,6 @@ import {
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import {
ForbiddenError,
InternalServerError,
NotFoundError,
TimeoutError,
UserInputError,
@@ -26,7 +25,11 @@ export const workspaceExceptionHandler = (
case WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT:
throw new TimeoutError(error.message);
case WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR:
default:
throw new InternalServerError(error.message);
throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
};
@@ -1,8 +1,10 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceQueryRunnerException extends CustomException {
code: WorkspaceQueryRunnerExceptionCode;
constructor(message: string, code: WorkspaceQueryRunnerExceptionCode) {
super(message, code);
this.code = code;
}
}
@@ -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 { 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 {
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';
@Injectable()
@@ -89,9 +93,9 @@ export class WorkspaceSchemaFactory {
}
if (!currentCacheVersion) {
throw new GraphqlQueryRunnerException(
throw new WorkspaceMetadataVersionException(
'Metadata cache version not found',
GraphqlQueryRunnerExceptionCode.METADATA_CACHE_VERSION_NOT_FOUND,
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
);
}
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class AuthException extends CustomException {
declare code: AuthExceptionCode;
constructor(message: string, code: AuthExceptionCode) {
super(message, code);
}
@@ -6,9 +6,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import {
AuthenticationError,
EmailNotVerifiedError,
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -21,18 +19,33 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
throw new NotFoundError(exception.message);
case AuthExceptionCode.INVALID_INPUT:
throw new UserInputError(exception.message);
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
throw new EmailNotVerifiedError(exception.message);
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
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.USER_NOT_FOUND:
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
throw new AuthenticationError(exception.message);
case AuthExceptionCode.INVALID_DATA:
case AuthExceptionCode.INTERNAL_SERVER_ERROR:
default:
throw new InternalServerError(exception.message);
case AuthExceptionCode.USER_WORKSPACE_NOT_FOUND:
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;
}
}
}
}
@@ -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;
}
}
};
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class EmailVerificationException extends CustomException {
declare code: EmailVerificationExceptionCode;
constructor(message: string, code: EmailVerificationExceptionCode) {
super(message, code);
}
@@ -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 { 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 { 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 { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
@@ -23,15 +25,22 @@ export class EmailVerificationResolver {
@OriginHeader() origin: string,
@Context() context: I18nContext,
): Promise<ResendEmailVerificationTokenOutput> {
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
try {
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
return await this.emailVerificationService.resendEmailVerificationToken(
resendEmailVerificationTokenInput.email,
workspace,
context.req.headers['x-locale'] ?? SOURCE_LOCALE,
);
return await this.emailVerificationService.resendEmailVerificationToken(
resendEmailVerificationTokenInput.email,
workspace,
context.req.headers['x-locale'] ?? SOURCE_LOCALE,
);
} catch (error) {
if (error instanceof EmailVerificationException) {
return emailVerificationGraphqlApiExceptionHandler(error);
}
throw error;
}
}
}
@@ -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 { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces';
import { CustomException } from 'src/utils/custom-exception';
export class ExceptionHandlerSentryDriver
implements ExceptionHandlerDriverInterface
@@ -15,8 +16,7 @@ export class ExceptionHandlerSentryDriver
Sentry.withScope((scope) => {
if (options?.operation) {
scope.setTag('operation', options.operation.name);
scope.setTag('operationName', options.operation.name);
scope.setExtra('operation', options.operation.name);
}
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, {
contexts: {
GraphQL: {
@@ -24,7 +24,6 @@ export enum ErrorCode {
PERSISTED_QUERY_NOT_SUPPORTED = 'PERSISTED_QUERY_NOT_SUPPORTED',
BAD_USER_INPUT = 'BAD_USER_INPUT',
NOT_FOUND = 'NOT_FOUND',
EMAIL_NOT_VERIFIED = 'EMAIL_NOT_VERIFIED',
METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED',
CONFLICT = 'CONFLICT',
TIMEOUT = 'TIMEOUT',
@@ -107,16 +106,16 @@ export class ValidationError extends BaseGraphQLError {
}
export class AuthenticationError extends BaseGraphQLError {
constructor(message: string) {
super(message, ErrorCode.UNAUTHENTICATED);
constructor(message: string, extensions?: Record<string, any>) {
super(message, ErrorCode.UNAUTHENTICATED, extensions);
Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
}
}
export class ForbiddenError extends BaseGraphQLError {
constructor(message: string) {
super(message, ErrorCode.FORBIDDEN);
constructor(message: string, extensions?: Record<string, any>) {
super(message, ErrorCode.FORBIDDEN, extensions);
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 {
constructor(message: string) {
super(message, ErrorCode.METHOD_NOT_ALLOWED);
@@ -1,3 +1,5 @@
import { HttpException } from '@nestjs/common';
import {
BaseGraphQLError,
ErrorCode,
@@ -12,7 +14,6 @@ export const graphQLErrorCodesToFilterOut = [
ErrorCode.TIMEOUT,
ErrorCode.CONFLICT,
ErrorCode.BAD_USER_INPUT,
ErrorCode.EMAIL_NOT_VERIFIED,
];
export const shouldCaptureException = (exception: Error): boolean => {
@@ -23,5 +24,13 @@ export const shouldCaptureException = (exception: Error): boolean => {
return false;
}
if (
exception instanceof HttpException &&
exception.getStatus() >= 400 &&
exception.getStatus() < 500
) {
return false;
}
return true;
};
@@ -1,13 +1,12 @@
import { CustomException } from 'src/utils/custom-exception';
export class SearchException extends CustomException {
declare code: SearchExceptionCode;
constructor(message: string, code: SearchExceptionCode) {
super(message, code);
}
}
export enum SearchExceptionCode {
METADATA_CACHE_VERSION_NOT_FOUND = 'METADATA_CACHE_VERSION_NOT_FOUND',
LABEL_IDENTIFIER_FIELD_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_NOT_FOUND',
OBJECT_METADATA_MAP_NOT_FOUND = 'OBJECT_METADATA_MAP_NOT_FOUND',
}
@@ -1,7 +1,9 @@
import { Catch, ExceptionFilter } from '@nestjs/common';
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { SearchException } from 'src/engine/core-modules/search/exceptions/search.exception';
import {
SearchException,
SearchExceptionCode,
} from 'src/engine/core-modules/search/exceptions/search.exception';
@Catch(SearchException)
export class SearchApiExceptionFilter implements ExceptionFilter {
@@ -9,8 +11,13 @@ export class SearchApiExceptionFilter implements ExceptionFilter {
catch(exception: SearchException) {
switch (exception.code) {
default:
throw new InternalServerError(exception.message);
case SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND:
throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
}
}
}
@@ -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 { 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 { 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 { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
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 { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@@ -47,9 +51,9 @@ export class SearchResolver {
await this.workspaceCacheStorageService.getMetadataVersion(workspace.id);
if (currentCacheVersion === undefined) {
throw new SearchException(
'Metadata cache version not found',
SearchExceptionCode.METADATA_CACHE_VERSION_NOT_FOUND,
throw new WorkspaceMetadataVersionException(
`Metadata version not found for workspace ${workspace.id}`,
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
);
}
@@ -60,9 +64,9 @@ export class SearchResolver {
);
if (!objectMetadataMaps) {
throw new SearchException(
throw new WorkspaceMetadataCacheException(
`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,
);
}
@@ -2,7 +2,6 @@ import { Catch, ExceptionFilter } from '@nestjs/common';
import {
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -25,8 +24,13 @@ export class ConfigVariableGraphqlApiExceptionFilter
case ConfigVariableExceptionCode.VALIDATION_FAILED:
throw new UserInputError(exception.message);
case ConfigVariableExceptionCode.INTERNAL_ERROR:
default:
throw new InternalServerError(exception.message);
case ConfigVariableExceptionCode.UNSUPPORTED_CONFIG_TYPE:
throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
}
}
}
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class ConfigVariableException extends CustomException {
declare code: ConfigVariableExceptionCode;
constructor(message: string, code: ConfigVariableExceptionCode) {
super(message, code);
}
@@ -12,6 +13,5 @@ export enum ConfigVariableExceptionCode {
VARIABLE_NOT_FOUND = 'VARIABLE_NOT_FOUND',
VALIDATION_FAILED = 'VALIDATION_FAILED',
UNSUPPORTED_CONFIG_TYPE = 'UNSUPPORTED_CONFIG_TYPE',
METADATA_NOT_FOUND = 'METADATA_NOT_FOUND',
INTERNAL_ERROR = 'INTERNAL_ERROR',
}
@@ -1,7 +1,6 @@
import { Catch, ExceptionFilter } from '@nestjs/common';
import {
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -25,8 +24,13 @@ export class WorkflowTriggerGraphqlApiExceptionFilter
throw new UserInputError(exception.message);
case WorkflowTriggerExceptionCode.NOT_FOUND:
throw new NotFoundError(exception.message);
default:
throw new InternalServerError(exception.message);
case WorkflowTriggerExceptionCode.INTERNAL_ERROR:
throw exception;
default: {
const _exhaustiveCheck: never = exception.code;
throw exception;
}
}
}
}
@@ -1,6 +1,5 @@
import {
ConflictError,
InternalServerError,
NotFoundError,
} 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';
@@ -8,6 +7,7 @@ import {
WorkspaceException,
WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception';
import { CustomException } from 'src/utils/custom-exception';
describe('workspaceGraphqlApiExceptionHandler', () => {
it('should throw NotFoundError when WorkspaceExceptionCode is SUBDOMAIN_NOT_FOUND', () => {
@@ -48,7 +48,7 @@ describe('workspaceGraphqlApiExceptionHandler', () => {
const error = new WorkspaceException('Unknown error', 'UNKNOWN_CODE');
expect(() => workspaceGraphqlApiExceptionHandler(error)).toThrow(
InternalServerError,
CustomException,
);
});
@@ -1,7 +1,6 @@
import {
ConflictError,
ForbiddenError,
InternalServerError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
@@ -15,12 +14,17 @@ export const workspaceGraphqlApiExceptionHandler = (error: Error) => {
case WorkspaceExceptionCode.SUBDOMAIN_NOT_FOUND:
case WorkspaceExceptionCode.WORKSPACE_NOT_FOUND:
throw new NotFoundError(error.message);
case WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN:
case WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN:
throw new ConflictError(error.message);
case WorkspaceExceptionCode.ENVIRONMENT_VAR_NOT_ENABLED:
case WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED:
throw new ForbiddenError(error.message);
default:
throw new InternalServerError(error.message);
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceException extends CustomException {
declare code: WorkspaceExceptionCode;
constructor(message: string, code: WorkspaceExceptionCode) {
super(message, code);
}
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class FieldMetadataException extends CustomException {
declare code: FieldMetadataExceptionCode;
constructor(message: string, code: FieldMetadataExceptionCode) {
super(message, code);
}
@@ -46,7 +46,7 @@ import {
RelationMetadataEntity,
RelationMetadataType,
} 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 { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
@@ -614,7 +614,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
try {
validateMetadataNameOrThrow(fieldMetadataInput.name);
} catch (error) {
if (error instanceof InvalidMetadataNameException) {
if (error instanceof InvalidMetadataException) {
throw new FieldMetadataException(
error.message,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
@@ -630,7 +630,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
objectMetadata,
);
} catch (error) {
if (error instanceof InvalidMetadataNameException) {
if (error instanceof InvalidMetadataException) {
throw new FieldMetadataException(
`Name "${fieldMetadataInput.name}" is not available, check that it is not duplicating another field's name.`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
@@ -11,6 +11,14 @@ import {
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
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 {
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';
@Injectable()
@@ -43,9 +51,9 @@ export class FieldMetadataRelationService {
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
if (!isDefined(metadataVersion)) {
throw new FieldMetadataException(
throw new WorkspaceMetadataVersionException(
`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) {
throw new FieldMetadataException(
throw new WorkspaceMetadataCacheException(
`Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`,
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
WorkspaceMetadataCacheExceptionCode.OBJECT_METADATA_MAP_NOT_FOUND,
);
}
@@ -1,7 +1,6 @@
import {
ConflictError,
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} 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_MALFORMED:
case FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND:
default:
throw new InternalServerError(error.message);
throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class ObjectMetadataException extends CustomException {
declare code: ObjectMetadataExceptionCode;
constructor(message: string, code: ObjectMetadataExceptionCode) {
super(message, code);
}
@@ -1,7 +1,6 @@
import {
ConflictError,
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -26,8 +25,13 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
throw new ForbiddenError(error.message);
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
throw new ConflictError(error.message);
default:
throw new InternalServerError(error.message);
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
@@ -6,7 +6,7 @@ import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} 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 { 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';
@@ -28,7 +28,7 @@ export const validateObjectMetadataInputNameOrThrow = (name: string): void => {
try {
validateMetadataNameOrThrow(name);
} catch (error) {
if (error instanceof InvalidMetadataNameException) {
if (error instanceof InvalidMetadataException) {
throw new ObjectMetadataException(
error.message,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
@@ -58,7 +58,7 @@ const validateObjectMetadataInputLabelOrThrow = (name: string): void => {
try {
validators.forEach((validator) => validator(name.trim()));
} catch (error) {
if (error instanceof InvalidMetadataNameException) {
if (error instanceof InvalidMetadataException) {
throw new ObjectMetadataException(
error.message,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class PermissionsException extends CustomException {
declare code: PermissionsExceptionCode;
constructor(message: string, code: PermissionsExceptionCode) {
super(message, code);
}
@@ -1,6 +1,5 @@
import {
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -29,7 +28,20 @@ export const permissionGraphqlApiExceptionHandler = (
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
throw new NotFoundError(error.message);
case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND:
default:
throw new InternalServerError(error.message);
case PermissionsExceptionCode.WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH:
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;
}
}
};
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class RelationMetadataException extends CustomException {
declare code: RelationMetadataExceptionCode;
constructor(message: string, code: RelationMetadataExceptionCode) {
super(message, code);
}
@@ -22,9 +22,17 @@ import {
RelationMetadataException,
RelationMetadataExceptionCode,
} 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 { 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 { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
@@ -73,7 +81,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
validateMetadataNameOrThrow(relationMetadataInput.fromName);
validateMetadataNameOrThrow(relationMetadataInput.toName);
} catch (error) {
if (error instanceof InvalidMetadataNameException)
if (error instanceof InvalidMetadataException)
throw new RelationMetadataException(
error.message,
RelationMetadataExceptionCode.INVALID_RELATION_INPUT,
@@ -497,8 +505,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
if (!isDefined(metadataVersion)) {
throw new NotFoundException(
throw new WorkspaceMetadataVersionException(
`Metadata version not found for workspace ${workspaceId}`,
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
);
}
@@ -509,8 +518,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
);
if (!objectMetadataMaps) {
throw new NotFoundException(
throw new WorkspaceMetadataCacheException(
`Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`,
WorkspaceMetadataCacheExceptionCode.OBJECT_METADATA_MAP_NOT_FOUND,
);
}
@@ -1,7 +1,5 @@
import {
ConflictError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
@@ -17,15 +15,18 @@ export const relationMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof RelationMetadataException) {
switch (error.code) {
case RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND:
throw new NotFoundError(error.message);
case RelationMetadataExceptionCode.INVALID_RELATION_INPUT:
throw new UserInputError(error.message);
case RelationMetadataExceptionCode.RELATION_ALREADY_EXISTS:
throw new ConflictError(error.message);
case RelationMetadataExceptionCode.FOREIGN_KEY_NOT_FOUND:
default:
throw new InternalServerError(error.message);
case RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND:
throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class RemoteServerException extends CustomException {
declare code: RemoteServerExceptionCode;
constructor(message: string, code: RemoteServerExceptionCode) {
super(message, code);
}
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class RemoteTableException extends CustomException {
declare code: RemoteTableExceptionCode;
constructor(message: string, code: RemoteTableExceptionCode) {
super(message, code);
}
@@ -1,6 +1,5 @@
import {
ConflictError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -21,8 +20,11 @@ export const remoteTableGraphqlApiExceptionHandler = (error: Error) => {
throw new UserInputError(error.message);
case RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS:
throw new ConflictError(error.message);
default:
throw new InternalServerError(error.message);
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
@@ -1,7 +1,6 @@
import {
ConflictError,
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -21,8 +20,13 @@ export const remoteServerGraphqlApiExceptionHandler = (error: any) => {
throw new ForbiddenError(error.message);
case RemoteServerExceptionCode.REMOTE_SERVER_ALREADY_EXISTS:
throw new ConflictError(error.message);
default:
throw new InternalServerError(error.message);
case RemoteServerExceptionCode.REMOTE_SERVER_CONNECTION_ERROR:
throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class ServerlessFunctionException extends CustomException {
declare code: ServerlessFunctionExceptionCode;
constructor(message: string, code: ServerlessFunctionExceptionCode) {
super(message, code);
}
@@ -1,7 +1,6 @@
import {
ConflictError,
ForbiddenError,
InternalServerError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
@@ -20,9 +19,15 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING:
case ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED:
throw new ForbiddenError(error.message);
default:
throw new InternalServerError(error.message);
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CODE_UNCHANGED:
throw error;
default: {
const _exhaustiveCheck: never = error.code;
throw error;
}
}
}
throw error;
@@ -2,11 +2,17 @@ import camelCase from 'lodash.camelcase';
import { slugify } from 'transliteration';
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 => {
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;
@@ -22,7 +28,10 @@ export const computeMetadataNameFromLabel = (label: string): string => {
});
if (formattedString === '') {
throw new InvalidMetadataException(`Invalid label: "${label}"`);
throw new InvalidMetadataException(
`Invalid label: "${label}"`,
InvalidMetadataExceptionCode.INVALID_LABEL,
);
}
return camelCase(formattedString);
@@ -1,5 +0,0 @@
export class InvalidMetadataNameException extends Error {
constructor(message: string) {
super(message);
}
}
@@ -1,5 +1,19 @@
export class InvalidMetadataException extends Error {
constructor(message: string) {
super(message);
import { CustomException } from 'src/utils/custom-exception';
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',
}
@@ -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 { 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 { 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 = (
objectMetadata: ObjectMetadataEntity,
@@ -33,10 +36,16 @@ export const validateFieldNameAvailabilityOrThrow = (
getReservedCompositeFieldNames(objectMetadata);
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)) {
throw new InvalidMetadataNameException(`Name "${name}" is not available`);
throw new InvalidMetadataException(
`Name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
);
}
};
@@ -1,11 +1,15 @@
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) => {
if (name !== camelCase(name)) {
throw new InvalidMetadataNameException(
throw new InvalidMetadataException(
`Name should be in camelCase: ${name}`,
InvalidMetadataExceptionCode.NOT_CAMEL_CASE,
);
}
};
@@ -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 = [
'approvedAccessDomain',
@@ -64,8 +67,9 @@ export const validateMetadataNameIsNotReservedKeywordOrThrow = (
name: string,
) => {
if (reservedKeywords.includes(name)) {
throw new InvalidMetadataNameException(
throw new InvalidMetadataException(
`The name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
);
}
};
@@ -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';
export const validateMetadataNameIsNotTooLongOrThrow = (name: string) => {
if (exceedsDatabaseIdentifierMaximumLength(name)) {
throw new InvalidMetadataNameException(
throw new InvalidMetadataException(
`String "${name}" exceeds 63 characters limit`,
InvalidMetadataExceptionCode.EXCEEDS_MAX_LENGTH,
);
}
};
@@ -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';
export const validateMetadataNameIsNotTooShortOrThrow = (name: string) => {
if (beneathDatabaseIdentifierMinimumLength(name)) {
throw new InvalidMetadataNameException(`Input is too short: "${name}"`);
throw new InvalidMetadataException(
`Input is too short: "${name}"`,
InvalidMetadataExceptionCode.INPUT_TOO_SHORT,
);
}
};
@@ -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 =
/^[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,
)
) {
throw new InvalidMetadataNameException(
throw new InvalidMetadataException(
`String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`,
InvalidMetadataExceptionCode.INVALID_STRING,
);
}
};
@@ -2,7 +2,10 @@ import camelCase from 'lodash.camelcase';
import { slugify } from 'transliteration';
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 = (
label: string,
@@ -13,13 +16,17 @@ export const validateNameAndLabelAreSyncOrThrow = (
if (name !== computedName) {
throw new InvalidMetadataException(
`Name is not synced with label. Expected name: "${computedName}", got ${name}`,
InvalidMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL,
);
}
};
export const computeMetadataNameFromLabel = (label: string): string => {
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;
@@ -35,7 +42,10 @@ export const computeMetadataNameFromLabel = (label: string): string => {
});
if (formattedString === '') {
throw new InvalidMetadataException(`Invalid label: "${label}"`);
throw new InvalidMetadataException(
`Invalid label: "${label}"`,
InvalidMetadataExceptionCode.INVALID_LABEL,
);
}
return camelCase(formattedString);
@@ -7,5 +7,5 @@ export class WorkspaceMetadataCacheException extends CustomException {
}
export enum WorkspaceMetadataCacheExceptionCode {
METADATA_VERSION_NOT_FOUND = 'METADATA_VERSION_NOT_FOUND',
OBJECT_METADATA_MAP_NOT_FOUND = 'Object Metadata map not found',
}
@@ -1,17 +1,17 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.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 { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
import {
WorkspaceMetadataCacheException,
WorkspaceMetadataCacheExceptionCode,
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception';
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';
@Injectable()
@@ -46,9 +46,9 @@ export class WorkspaceMetadataCacheService {
await this.getMetadataVersionFromDatabase(workspaceId);
if (!isDefined(currentDatabaseVersion)) {
throw new WorkspaceMetadataCacheException(
throw new WorkspaceMetadataVersionException(
'Metadata version not found in the database',
WorkspaceMetadataCacheExceptionCode.METADATA_VERSION_NOT_FOUND,
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
);
}
@@ -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 { 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 {
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 {
ROLES_PERMISSIONS,
@@ -312,9 +316,9 @@ export class WorkspaceDatasourceFactory {
if (!isDefined(latestWorkspaceMetadataVersion)) {
if (shouldFailIfMetadataNotFound) {
throw new TwentyORMException(
throw new WorkspaceMetadataVersionException(
`Metadata version not found for workspace ${workspaceId}`,
TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND,
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
);
} else {
await this.workspaceMetadataCacheService.recomputeMetadataCache({
@@ -330,7 +334,7 @@ export class WorkspaceDatasourceFactory {
if (!isDefined(latestWorkspaceMetadataVersion)) {
throw new TwentyORMException(
`Metadata version not found after recompute for workspace ${workspaceId}`,
`Metadata version not found after recompute`,
TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND,
);
}
@@ -148,16 +148,21 @@ export class CalendarEventImportErrorHandlerService {
workspaceId,
);
this.exceptionHandlerService.captureExceptions([exception], {
workspace: {
id: workspaceId,
},
});
throw new CalendarEventImportException(
const calendarEventImportException = new CalendarEventImportException(
`Unknown error importing calendar events for calendar channel ${calendarChannel.id} in workspace ${workspaceId}: ${exception.message}`,
CalendarEventImportExceptionCode.UNKNOWN,
);
this.exceptionHandlerService.captureExceptions(
[calendarEventImportException],
{
workspace: {
id: workspaceId,
},
},
);
throw calendarEventImportException;
}
private async handleNotFoundException(
@@ -1,9 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
@@ -23,13 +25,15 @@ export class CalendarEventFindManyPostQueryHook
_objectName: string,
payload: CalendarEventWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
const { user, apiKey } = authContext;
if (!isDefined(user) && !isDefined(apiKey)) {
throw new ForbiddenError('User is required');
}
await this.applyCalendarEventsVisibilityRestrictionsService.applyCalendarEventsVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
user?.id,
);
}
}
@@ -1,9 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ApplyCalendarEventsVisibilityRestrictionsService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
@@ -23,13 +25,15 @@ export class CalendarEventFindOnePostQueryHook
_objectName: string,
payload: CalendarEventWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
const { user, apiKey } = authContext;
if (!isDefined(user) && !isDefined(apiKey)) {
throw new ForbiddenError('User is required');
}
await this.applyCalendarEventsVisibilityRestrictionsService.applyCalendarEventsVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
user?.id,
);
}
}
@@ -48,6 +48,10 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
find: jest.fn(),
};
const mockWorkspaceMemberRepository = {
findOneByOrFail: jest.fn(),
};
const mockTwentyORMManager = {
getRepository: jest.fn().mockImplementation((name) => {
if (name === 'calendarChannelEventAssociation') {
@@ -56,6 +60,9 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
if (name === 'connectedAccount') {
return mockConnectedAccountRepository;
}
if (name === 'workspaceMember') {
return mockWorkspaceMemberRepository;
}
}),
};
@@ -83,6 +90,10 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
createMockCalendarEvent('1', 'Test Event', 'Test Description'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
@@ -94,8 +105,8 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual(calendarEvents);
@@ -124,11 +135,15 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual([
@@ -155,11 +170,15 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-account-owner-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual(calendarEvents);
@@ -186,11 +205,15 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-not-account-owner-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual([]);
@@ -203,6 +226,10 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
createMockCalendarEvent('3', 'Event 3', 'Description 3'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
@@ -232,8 +259,63 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => {
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
const result = await service.applyCalendarEventsVisibilityRestrictions(
'workspace-member-id',
calendarEvents,
'user-id',
);
expect(result).toEqual([
calendarEvents[0],
calendarEvents[1],
{
...calendarEvents[2],
title: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
description: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
]);
});
it('should return all calendar events with the right visibility when userId is undefined (api key request)', async () => {
const calendarEvents = [
createMockCalendarEvent('1', 'Event 1', 'Description 1'),
createMockCalendarEvent('2', 'Event 2', 'Description 2'),
createMockCalendarEvent('3', 'Event 3', 'Description 3'),
];
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockCalendarEventAssociationRepository.find.mockResolvedValue([
{
calendarEventId: '1',
calendarChannel: {
id: '1',
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
},
},
{
calendarEventId: '2',
calendarChannel: {
id: '2',
visibility: CalendarChannelVisibility.METADATA,
},
},
{
calendarEventId: '3',
calendarChannel: {
id: '3',
visibility: CalendarChannelVisibility.METADATA,
},
},
]);
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for calendar event 3
.mockResolvedValueOnce([{ id: '1' }]); // request for calendar event 2
const result = await service.applyCalendarEventsVisibilityRestrictions(
calendarEvents,
undefined,
);
expect(result).toEqual([
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
@@ -9,14 +10,15 @@ import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/cale
import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class ApplyCalendarEventsVisibilityRestrictionsService {
constructor(private readonly twentyORMManager: TwentyORMManager) {}
public async applyCalendarEventsVisibilityRestrictions(
workspaceMemberId: string,
calendarEvents: CalendarEventWorkspaceEntity[],
userId?: string, // undefined when request is made with api key
) {
const calendarChannelEventAssociationRepository =
await this.twentyORMManager.getRepository<CalendarChannelEventAssociationWorkspaceEntity>(
@@ -36,6 +38,11 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
'connectedAccount',
);
const workspaceMemberRepository =
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
'workspaceMember',
);
for (let i = calendarEvents.length - 1; i >= 0; i--) {
const calendarChannelCalendarEventAssociations =
calendarChannelCalendarEventsAssociations.filter(
@@ -59,18 +66,26 @@ export class ApplyCalendarEventsVisibilityRestrictionsService {
continue;
}
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
calendarChannels: {
id: In(calendarChannels.map((channel) => channel.id)),
if (isDefined(userId)) {
const workspaceMember = await workspaceMemberRepository.findOneByOrFail(
{
userId: userId,
},
accountOwnerId: workspaceMemberId,
},
});
);
if (connectedAccounts.length > 0) {
continue;
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
calendarChannels: {
id: In(calendarChannels.map((channel) => channel.id)),
},
accountOwnerId: workspaceMember.id,
},
});
if (connectedAccounts.length > 0) {
continue;
}
}
if (
@@ -38,6 +38,10 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
find: jest.fn(),
};
const mockWorkspaceMemberRepository = {
findOneByOrFail: jest.fn(),
};
const mockTwentyORMManager = {
getRepository: jest.fn().mockImplementation((name) => {
if (name === 'messageChannelMessageAssociation') {
@@ -46,6 +50,9 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
if (name === 'connectedAccount') {
return mockConnectedAccountRepository;
}
if (name === 'workspaceMember') {
return mockWorkspaceMemberRepository;
}
}),
};
@@ -83,8 +90,8 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual(messages);
@@ -114,9 +121,13 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockConnectedAccountRepository.find.mockResolvedValue([]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual([
@@ -144,9 +155,13 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockConnectedAccountRepository.find.mockResolvedValue([]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual([
@@ -173,11 +188,15 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-account-owner-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual(messages);
@@ -203,11 +222,15 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-not-account-owner-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual([]);
@@ -244,13 +267,75 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
const result = await service.applyMessagesVisibilityRestrictions(
'workspace-member-id',
messages,
'user-id',
);
expect(result).toEqual([
messages[0],
{
...messages[1],
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
{
...messages[2],
subject: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
},
]);
});
it('should return all messages with the right visibility when userId is undefined (api key request)', async () => {
const messages = [
createMockMessage('1', 'Subject 1', 'Message 1'),
createMockMessage('2', 'Subject 2', 'Message 2'),
createMockMessage('3', 'Subject 3', 'Message 3'),
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: '1',
messageChannel: {
id: '1',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
},
{
messageId: '2',
messageChannel: {
id: '2',
visibility: MessageChannelVisibility.SUBJECT,
},
},
{
messageId: '3',
messageChannel: {
id: '3',
visibility: MessageChannelVisibility.METADATA,
},
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
const result = await service.applyMessagesVisibilityRestrictions(
messages,
undefined,
);
expect(result).toEqual([
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import groupBy from 'lodash.groupby';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -10,14 +11,15 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class ApplyMessagesVisibilityRestrictionsService {
constructor(private readonly twentyORMManager: TwentyORMManager) {}
public async applyMessagesVisibilityRestrictions(
workspaceMemberId: string,
messages: MessageWorkspaceEntity[],
userId?: string, // undefined when request is made with api key
) {
const messageChannelMessageAssociationRepository =
await this.twentyORMManager.getRepository<MessageChannelMessageAssociationWorkspaceEntity>(
@@ -37,6 +39,11 @@ export class ApplyMessagesVisibilityRestrictionsService {
'connectedAccount',
);
const workspaceMemberRepository =
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
'workspaceMember',
);
for (let i = messages.length - 1; i >= 0; i--) {
const messageChannelMessageAssociations =
messageChannelMessagesAssociations.filter(
@@ -66,19 +73,28 @@ export class ApplyMessagesVisibilityRestrictionsService {
continue;
}
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
if (isDefined(userId)) {
const workspaceMember = await workspaceMemberRepository.findOneByOrFail(
{
userId,
},
accountOwnerId: workspaceMemberId,
},
});
);
if (connectedAccounts.length > 0) {
continue;
const connectedAccounts = await connectedAccountRepository.find({
select: ['id'],
where: {
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
},
accountOwnerId: workspaceMember.id,
},
});
if (connectedAccounts.length > 0) {
continue;
}
}
if (messageChannelsGroupByVisibility[MessageChannelVisibility.SUBJECT]) {
messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
continue;
@@ -1,9 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
@@ -23,13 +25,15 @@ export class MessageFindManyPostQueryHook
_objectName: string,
payload: MessageWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
const { user, apiKey } = authContext;
if (!isDefined(user) && !isDefined(apiKey)) {
throw new ForbiddenError('User is required');
}
await this.applyMessagesVisibilityRestrictionsService.applyMessagesVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
user?.id,
);
}
}
@@ -1,9 +1,11 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
@@ -23,13 +25,15 @@ export class MessageFindOnePostQueryHook
_objectName: string,
payload: MessageWorkspaceEntity[],
): Promise<void> {
if (!authContext.workspaceMemberId) {
throw new UserInputError('Workspace member id is required');
const { user, apiKey } = authContext;
if (!isDefined(user) && !isDefined(apiKey)) {
throw new ForbiddenError('User is required');
}
await this.applyMessagesVisibilityRestrictionsService.applyMessagesVisibilityRestrictions(
authContext.workspaceMemberId,
payload,
user?.id,
);
}
}
@@ -162,7 +162,10 @@ export class MessageImportExceptionHandlerService {
);
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(
@@ -1,6 +1,7 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkflowTriggerException extends CustomException {
declare code: WorkflowTriggerExceptionCode;
constructor(message: string, code: WorkflowTriggerExceptionCode) {
super(message, code);
}