Update user friendly errors for translations (#15000)

Force msg typing instead of string for user friendly errors
This commit is contained in:
Félix Malfait
2025-10-09 18:17:32 +02:00
committed by GitHub
parent 660cd38f35
commit e577c2d746
124 changed files with 796 additions and 563 deletions
@@ -18,6 +18,7 @@ import {
ForbiddenError,
ValidationError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
@@ -52,6 +53,7 @@ export class FieldMetadataResolver {
private readonly beforeUpdateOneField: BeforeUpdateOneField<UpdateFieldInput>,
private readonly featureFlagService: FeatureFlagService,
private readonly fieldMetadataServiceV2: FieldMetadataServiceV2,
private readonly i18nService: I18nService,
) {}
@UseGuards(SettingsPermissionsGuard(PermissionFlagType.DATA_MODEL))
@@ -59,6 +61,7 @@ export class FieldMetadataResolver {
async createOneField(
@Args('input') input: CreateOneFieldMetadataInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
@Context() context: I18nContext,
) {
try {
return await this.fieldMetadataService.createOne({
@@ -66,7 +69,10 @@ export class FieldMetadataResolver {
workspaceId,
});
} catch (error) {
return fieldMetadataGraphqlApiExceptionHandler(error);
return fieldMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
}
@@ -101,7 +107,10 @@ export class FieldMetadataResolver {
workspaceId,
});
} catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error);
fieldMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
}
@@ -110,6 +119,7 @@ export class FieldMetadataResolver {
async deleteOneField(
@Args('input') input: DeleteOneFieldInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
@Context() context: I18nContext,
) {
if (!isDefined(workspaceId)) {
throw new ForbiddenError('Could not retrieve workspace ID');
@@ -129,7 +139,10 @@ export class FieldMetadataResolver {
});
}
} catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error);
fieldMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
const fieldMetadata =
@@ -154,7 +167,10 @@ export class FieldMetadataResolver {
try {
return await this.fieldMetadataService.deleteOneField(input, workspaceId);
} catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error);
fieldMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
}
@@ -167,7 +183,7 @@ export class FieldMetadataResolver {
id: fieldMetadataId,
objectMetadataId,
}: Pick<FieldMetadataDTO, 'id' | 'objectMetadataId'>,
@Context() context: { loaders: IDataloaders },
@Context() context: { loaders: IDataloaders } & I18nContext,
): Promise<RelationDTO | null> {
try {
return await context.loaders.relationLoader.load({
@@ -176,7 +192,10 @@ export class FieldMetadataResolver {
workspaceId: workspace.id,
});
} catch (error) {
return fieldMetadataGraphqlApiExceptionHandler(error);
return fieldMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
}
@@ -188,7 +207,7 @@ export class FieldMetadataResolver {
id: fieldMetadataId,
objectMetadataId,
}: Pick<FieldMetadataDTO, 'id' | 'objectMetadataId'>,
@Context() context: { loaders: IDataloaders },
@Context() context: { loaders: IDataloaders } & I18nContext,
): Promise<RelationDTO[] | null> {
try {
return await context.loaders.morphRelationLoader.load({
@@ -197,7 +216,10 @@ export class FieldMetadataResolver {
workspaceId: workspace.id,
});
} catch (error) {
return fieldMetadataGraphqlApiExceptionHandler(error);
return fieldMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
}
}
@@ -1,20 +1,34 @@
import {
type CallHandler,
type ExecutionContext,
Injectable,
type NestInterceptor,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { type Observable, catchError } from 'rxjs';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
@Injectable()
export class FieldMetadataGraphqlApiExceptionInterceptor
implements NestInterceptor
{
constructor(private readonly i18nService: I18nService) {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
intercept(_: ExecutionContext, next: CallHandler): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const gqlContext = GqlExecutionContext.create(context);
const ctx = gqlContext.getContext();
const locale = ctx.req?.locale ?? SOURCE_LOCALE;
const i18n = this.i18nService.getI18nInstance(locale);
return next
.handle()
.pipe(catchError((err) => fieldMetadataGraphqlApiExceptionHandler(err)));
.pipe(
catchError((err) => fieldMetadataGraphqlApiExceptionHandler(err, i18n)),
);
}
}
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import {
type EnumFieldMetadataType,
@@ -31,7 +32,7 @@ import { isSnakeCaseString } from 'src/utils/is-snake-case-string';
type Validator<T> = {
validator: (str: T) => boolean;
message: string;
message: MessageDescriptor;
};
type FieldMetadataUpdateCreateInput = CreateFieldInput | UpdateFieldInput;
@@ -59,7 +60,7 @@ export class FieldMetadataEnumValidationService {
if (shouldThrow) {
throw new FieldMetadataException(
message,
message.message ?? 'Invalid field input',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: message,
@@ -72,11 +73,11 @@ export class FieldMetadataEnumValidationService {
const validators: Validator<string>[] = [
{
validator: (id) => !isDefined(id),
message: 'Option id is required',
message: msg`Option id is required`,
},
{
validator: (id) => !z.string().uuid().safeParse(id).success,
message: 'Option id is invalid',
message: msg`Option id is invalid`,
},
];
@@ -89,23 +90,23 @@ export class FieldMetadataEnumValidationService {
const validators: Validator<string>[] = [
{
validator: (label) => !isDefined(label),
message: t`Option label is required`,
message: msg`Option label is required`,
},
{
validator: exceedsDatabaseIdentifierMaximumLength,
message: t`Option label exceeds 63 characters`,
message: msg`Option label exceeds 63 characters`,
},
{
validator: beneathDatabaseIdentifierMinimumLength,
message: t`Option label "${sanitizedLabel}" is beneath 1 character`,
message: msg`Option label "${sanitizedLabel}" is beneath 1 character`,
},
{
validator: (label) => label.includes(','),
message: t`Label must not contain a comma`,
message: msg`Label must not contain a comma`,
},
{
validator: (label) => !isNonEmptyString(label) || label === ' ',
message: t`Label must not be empty`,
message: msg`Label must not be empty`,
},
];
@@ -118,19 +119,19 @@ export class FieldMetadataEnumValidationService {
const validators: Validator<string>[] = [
{
validator: (value) => !isDefined(value),
message: t`Option value is required`,
message: msg`Option value is required`,
},
{
validator: exceedsDatabaseIdentifierMaximumLength,
message: t`Option value exceeds 63 characters`,
message: msg`Option value exceeds 63 characters`,
},
{
validator: beneathDatabaseIdentifierMinimumLength,
message: t`Option value "${sanitizedValue}" is beneath 1 character`,
message: msg`Option value "${sanitizedValue}" is beneath 1 character`,
},
{
validator: (value) => !isSnakeCaseString(value),
message: `Value must be in UPPER_CASE and follow snake_case "${sanitizedValue}"`,
message: msg`Value must be in UPPER_CASE and follow snake_case "${sanitizedValue}"`,
},
];
@@ -153,7 +154,7 @@ export class FieldMetadataEnumValidationService {
const duplicatedValidators = fieldsToCheckForDuplicates.map<
Validator<FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]>
>((field) => ({
message: `Duplicated option ${field}`,
message: msg`Duplicated option ${field}`,
validator: () =>
new Set(options.map((option) => option[field])).size !== options.length,
}));
@@ -198,7 +199,7 @@ export class FieldMetadataEnumValidationService {
const validators: Validator<string>[] = [
{
validator: (value: string) => !QUOTED_STRING_REGEX.test(value),
message: 'Default value should be as quoted string',
message: msg`Default value should be as quoted string`,
},
{
validator: (value: string) =>
@@ -206,7 +207,7 @@ export class FieldMetadataEnumValidationService {
(option) =>
option.value === value.replace(QUOTED_STRING_REGEX, '$1'),
),
message: `Default value "${defaultValue}" must be one of the option values`,
message: msg`Default value "${defaultValue}" must be one of the option values`,
},
];
@@ -229,11 +230,11 @@ export class FieldMetadataEnumValidationService {
const validators: Validator<string[]>[] = [
{
validator: (values) => values.length === 0,
message: 'If defined default value must contain at least one value',
message: msg`If defined default value must contain at least one value`,
},
{
validator: (values) => new Set(values).size !== values.length,
message: 'Default values must be unique',
message: msg`Default values must be unique`,
},
];
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { IsEnum, IsString, IsUUID } from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -201,7 +201,7 @@ export class FieldMetadataRelationService {
`Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: t`Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`,
userFriendlyMessage: msg`Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`,
},
);
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type ClassConstructor, plainToInstance } from 'class-transformer';
import {
IsArray,
@@ -196,7 +196,7 @@ export class FieldMetadataValidationService {
`Name "${fieldMetadataInput.name}" is not available, check that it is not duplicating another field's name.`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: t`Name is not available, it may be duplicating another field's name.`,
userFriendlyMessage: msg`Name is not available, it may be duplicating another field's name.`,
},
);
}
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -190,7 +190,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
'Unique field cannot have a default value',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: t`Unique field cannot have a default value`,
userFriendlyMessage: msg`Unique field cannot have a default value`,
},
);
}
@@ -469,7 +469,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
'Cannot delete, please update the label identifier field first',
FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED,
{
userFriendlyMessage: t`Cannot delete, please update the label identifier field first`,
userFriendlyMessage: msg`Cannot delete, please update the label identifier field first`,
},
);
}
@@ -830,7 +830,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
'Unique field cannot have a default value',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: t`Unique field cannot have a default value`,
userFriendlyMessage: msg`Unique field cannot have a default value`,
},
);
@@ -1,3 +1,4 @@
import { type I18n } from '@lingui/core';
import { assertUnreachable } from 'twenty-shared/utils';
import {
@@ -14,9 +15,12 @@ import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exce
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
export const fieldMetadataGraphqlApiExceptionHandler = (
error: Error,
i18n: I18n,
) => {
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
workspaceMigrationBuilderExceptionV2Formatter(error);
workspaceMigrationBuilderExceptionV2Formatter(error, i18n);
}
if (error instanceof InvalidMetadataException) {
@@ -5,7 +5,10 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"error": {
"code": "FIELD_METADATA_RELATION_MALFORMED",
"message": "Morph relation creation payloads must have the same relation type",
"userFriendlyMessage": "Morph relation creation payloads must have the same relation type",
"userFriendlyMessage": {
"id": Any<String>,
"message": "Morph relation creation payloads must have the same relation type",
},
},
"status": "fail",
}
@@ -16,7 +19,10 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"error": {
"code": "FIELD_METADATA_RELATION_MALFORMED",
"message": "Morph relation input transpilation failed",
"userFriendlyMessage": "Invalid morph relation input",
"userFriendlyMessage": {
"id": Any<String>,
"message": "Invalid morph relation input",
},
"value": [
{
"targetObjectMetadataId": Any<String>,
@@ -33,7 +39,10 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"error": {
"code": "FIELD_METADATA_RELATION_MALFORMED",
"message": "Morph relation creation payloads must have only relation to the same object metadata",
"userFriendlyMessage": "Morph relation creation payloads must only contain relation to the same object metadata",
"userFriendlyMessage": {
"id": Any<String>,
"message": "Morph relation creation payloads must only contain relation to the same object metadata",
},
},
"status": "fail",
}
@@ -44,7 +53,10 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"error": {
"code": "FIELD_METADATA_RELATION_MALFORMED",
"message": "Morph relation creation payloads are empty",
"userFriendlyMessage": "At least one relation is require",
"userFriendlyMessage": {
"id": Any<String>,
"message": "At least one relation is require",
},
},
"status": "fail",
}
@@ -55,7 +67,10 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"error": {
"code": "INVALID_FIELD_INPUT",
"message": "Relation creation payload is required",
"userFriendlyMessage": "Relation creation payload is required",
"userFriendlyMessage": {
"id": Any<String>,
"message": "Relation creation payload is required",
},
"value": undefined,
},
"status": "fail",
@@ -67,7 +82,10 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"error": {
"code": "FIELD_METADATA_RELATION_MALFORMED",
"message": "Morph relation input transpilation failed",
"userFriendlyMessage": "Invalid morph relation input",
"userFriendlyMessage": {
"id": Any<String>,
"message": "Invalid morph relation input",
},
"value": [
{
"targetFieldIcon": "IconPet",
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';
@@ -66,7 +66,7 @@ export class FlatFieldMetadataTypeValidatorService {
{
code: FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION,
message: 'Morph relation feature flag is disabled',
userFriendlyMessage: t`Morph relation fields are disabled for your workspace`,
userFriendlyMessage: msg`Morph relation fields are disabled for your workspace`,
},
];
}
@@ -192,7 +192,7 @@ export class FlatFieldMetadataTypeValidatorService {
code: FieldMetadataExceptionCode.UNCOVERED_FIELD_METADATA_TYPE_VALIDATION,
message: `Unsupported field metadata type ${fieldType}`,
value: fieldType,
userFriendlyMessage: t`Unsupported field metadata type ${fieldType}`,
userFriendlyMessage: msg`Unsupported field metadata type ${fieldType}`,
},
];
}
@@ -1,8 +1,10 @@
import { type MessageDescriptor } from '@lingui/core';
import { type FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
export type FlatFieldMetadataValidationError = {
code: FieldMetadataExceptionCode;
message: string;
userFriendlyMessage?: string;
userFriendlyMessage?: MessageDescriptor;
value?: unknown;
};
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import {
assertUnreachable,
@@ -59,7 +59,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
error: {
code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
message: 'Provided object metadata id does not exist',
userFriendlyMessage: t`Created field metadata, parent object metadata not found`,
userFriendlyMessage: msg`Created field metadata, parent object metadata not found`,
},
};
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type FieldMetadataType } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
@@ -45,7 +45,7 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
error: {
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: `Relation creation payload is required`,
userFriendlyMessage: t`Relation creation payload is required`,
userFriendlyMessage: msg`Relation creation payload is required`,
value: rawMorphCreationPayload,
},
};
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -38,7 +38,7 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
error: {
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: `Relation creation payload is required`,
userFriendlyMessage: t`Relation creation payload is required`,
userFriendlyMessage: msg`Relation creation payload is required`,
value: rawCreationPayload,
},
};
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import {
extractAndSanitizeObjectStringFields,
isDefined,
@@ -150,7 +150,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
error: {
code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
message: 'Field metadata to update not found',
userFriendlyMessage: t`Field metadata to update not found`,
userFriendlyMessage: msg`Field metadata to update not found`,
},
};
}
@@ -172,7 +172,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
error: {
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: `Cannot update standard field metadata properties: ${invalidProperties}`,
userFriendlyMessage: t`Cannot update standard field properties: ${invalidProperties}`,
userFriendlyMessage: msg`Cannot update standard field properties: ${invalidProperties}`,
},
};
}
@@ -53,7 +53,7 @@ const validateMetadataOptionLabel = (
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t`Option label is required`,
userFriendlyMessage: t`Option label is required`,
userFriendlyMessage: msg`Option label is required`,
},
];
}
@@ -63,7 +63,7 @@ const validateMetadataOptionLabel = (
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t`Option label must be a string of at least one character`,
userFriendlyMessage: t`Option label format not supported`,
userFriendlyMessage: msg`Option label format not supported`,
value: sanitizedLabel,
},
];
@@ -102,7 +102,7 @@ const validateMetadataOptionValue = (
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t`Option value is required`,
userFriendlyMessage: t`Option value is required`,
userFriendlyMessage: msg`Option value is required`,
},
];
}
@@ -112,7 +112,7 @@ const validateMetadataOptionValue = (
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t`Option value must be a string of at least one character`,
userFriendlyMessage: t`Option value format not supported`,
userFriendlyMessage: msg`Option value format not supported`,
},
];
}
@@ -175,7 +175,7 @@ const validateFieldMetadataInputOptions = <T extends EnumFieldMetadataType>(
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: 'Options are required for enum fields',
userFriendlyMessage: t`Options are required for enum fields`,
userFriendlyMessage: msg`Options are required for enum fields`,
value: options,
},
];
@@ -206,7 +206,7 @@ const validateSelectDefaultValue = ({
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: `Default value for select must be a string got ${defaultValue}`,
userFriendlyMessage: t`Default value must be a string`,
userFriendlyMessage: msg`Default value must be a string`,
value: defaultValue,
},
];
@@ -243,7 +243,7 @@ const validateMultiSelectDefaultValue = ({
return [
{
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
userFriendlyMessage: t`Multi-select field default value must be an array`,
userFriendlyMessage: msg`Multi-select field default value must be an array`,
message: `Default value for multi-select must be an array got ${multiSelectDefaultValue}`,
value: multiSelectDefaultValue,
},
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -101,7 +101,7 @@ export const validateFlatFieldMetadataNameAvailability = ({
code: FieldMetadataExceptionCode.NOT_AVAILABLE,
value: flatFieldMetadataName,
message: `Name "${flatFieldMetadataName}" is not available as it is already used by another field`,
userFriendlyMessage: t`Name "${flatFieldMetadataName}" is not available as it is already used by another field`,
userFriendlyMessage: msg`Name "${flatFieldMetadataName}" is not available as it is already used by another field`,
});
}
@@ -110,7 +110,7 @@ export const validateFlatFieldMetadataNameAvailability = ({
code: FieldMetadataExceptionCode.RESERVED_KEYWORD,
message: `Name "${flatFieldMetadataName}" is reserved composite field name`,
value: flatFieldMetadataName,
userFriendlyMessage: t`Name "${flatFieldMetadataName}" is not available`,
userFriendlyMessage: msg`Name "${flatFieldMetadataName}" is not available`,
});
}
@@ -1,5 +1,3 @@
import { t } from '@lingui/core/macro';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
import { METADATA_NAME_VALIDATORS } from 'src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants';
@@ -13,8 +11,8 @@ export const validateFlatFieldMetadataName = (
if (isInvalid) {
return {
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: t(message),
userFriendlyMessage: t(message),
message: message.message ?? '',
userFriendlyMessage: message,
value: name,
};
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
@@ -26,7 +26,7 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({
: {
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: `Invalid uuid ${id}`,
userFriendlyMessage: t`Invalid uuid ${id}`,
userFriendlyMessage: msg`Invalid uuid ${id}`,
value: id,
},
);
@@ -44,7 +44,7 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({
errors.push({
code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
message: 'Relation target object metadata not found',
userFriendlyMessage: t`Object targeted by the relation not found`,
userFriendlyMessage: msg`Object targeted by the relation not found`,
});
}
@@ -61,7 +61,7 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({
message: isDefined(remainingFlatEntityMapsToValidate)
? 'Relation field target metadata not found in both existing and about to be created field metadatas'
: 'Relation field target metadata not found',
userFriendlyMessage: t`Relation field target metadata not found`,
userFriendlyMessage: msg`Relation field target metadata not found`,
});
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'class-validator';
import { type RelationCreationPayload } from 'twenty-shared/types';
@@ -35,7 +35,7 @@ export const validateMorphRelationCreationPayload = async ({
error: {
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message: 'Morph relation creation payloads are empty',
userFriendlyMessage: t`At least one relation is require`,
userFriendlyMessage: msg`At least one relation is require`,
},
};
}
@@ -55,7 +55,7 @@ export const validateMorphRelationCreationPayload = async ({
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message:
'Morph relation creation payloads must have the same relation type',
userFriendlyMessage: t`Morph relation creation payloads must have the same relation type`,
userFriendlyMessage: msg`Morph relation creation payloads must have the same relation type`,
},
};
}
@@ -74,7 +74,7 @@ export const validateMorphRelationCreationPayload = async ({
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message:
'Morph relation creation payloads must not target source object metadata',
userFriendlyMessage: t`Morph relation creation payloads must only contain relation to other object metadata`,
userFriendlyMessage: msg`Morph relation creation payloads must only contain relation to other object metadata`,
},
};
}
@@ -88,7 +88,7 @@ export const validateMorphRelationCreationPayload = async ({
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message:
'Morph relation creation payloads must have only relation to the same object metadata',
userFriendlyMessage: t`Morph relation creation payloads must only contain relation to the same object metadata`,
userFriendlyMessage: msg`Morph relation creation payloads must only contain relation to the same object metadata`,
},
};
}
@@ -130,7 +130,7 @@ export const validateMorphRelationCreationPayload = async ({
error: {
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message: 'Morph relation input transpilation failed',
userFriendlyMessage: t`Invalid morph relation input`,
userFriendlyMessage: msg`Invalid morph relation input`,
value: relationCreationPayloadReport.failed
.map((failedTranspilation) => failedTranspilation.error.value)
.filter(isDefined),
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type RelationCreationPayload } from 'twenty-shared/types';
import {
isDefined,
@@ -42,7 +42,7 @@ export const validateRelationCreationPayload = async ({
error: {
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message: `Relation creation payload is invalid`,
userFriendlyMessage: t`Invalid relation creation payload`,
userFriendlyMessage: msg`Invalid relation creation payload`,
value: relationCreationPayload,
},
};
@@ -62,7 +62,7 @@ export const validateRelationCreationPayload = async ({
error: {
code: FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
message: `Object metadata relation target not found for relation creation payload`,
userFriendlyMessage: t`Object targeted by field to create not found`,
userFriendlyMessage: msg`Object targeted by field to create not found`,
value: relationCreationPayload,
},
};
@@ -1,8 +1,10 @@
import { type MessageDescriptor } from '@lingui/core';
import { type ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
export type FlatObjectMetadataValidationError = {
code: ObjectMetadataExceptionCode;
message: string;
userFriendlyMessage?: string;
userFriendlyMessage?: MessageDescriptor;
value?: unknown;
};
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import {
isDefined,
isLabelIdentifierFieldMetadataTypes,
@@ -38,14 +38,14 @@ export const validateFlatObjectMetadataIdentifiers = ({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message:
'labelIdentifierFieldMetadataId validation failed: related field metadata not found',
userFriendlyMessage: t`Field declared as label identifier not found`,
userFriendlyMessage: msg`Field declared as label identifier not found`,
});
} else if (!isLabelIdentifierFieldMetadataTypes(flatFieldMetadata.type)) {
errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message:
'labelIdentifierFieldMetadataId validation failed: field type not compatible',
userFriendlyMessage: t`Field cannot be used as label identifier`,
userFriendlyMessage: msg`Field cannot be used as label identifier`,
});
}
}
@@ -61,7 +61,7 @@ export const validateFlatObjectMetadataIdentifiers = ({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message:
'imageIdentifierFieldMetadataId validation failed: related field metadata not found',
userFriendlyMessage: t`Field declared as image identifier not found`,
userFriendlyMessage: msg`Field declared as image identifier not found`,
});
}
}
@@ -1,4 +1,4 @@
import { msg, t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -44,7 +44,7 @@ export const validateFlatObjectMetadataLabel = ({
errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: `The singular and plural labels cannot be the same for an object`,
userFriendlyMessage: t`The singular and plural labels cannot be the same for an object`,
userFriendlyMessage: msg`The singular and plural labels cannot be the same for an object`,
value: labelSingular,
});
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { t, msg } from '@lingui/core/macro';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
@@ -39,7 +39,7 @@ export const validateFlatObjectMetadataNameAndLabels = ({
errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`Names are not synced with labels`,
userFriendlyMessage: t`Names are not synced with labels`,
userFriendlyMessage: msg`Names are not synced with labels`,
});
}
@@ -54,7 +54,7 @@ export const validateFlatObjectMetadataNameAndLabels = ({
errors.push({
code: ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
message: 'Object already exists',
userFriendlyMessage: t`Object already exists`,
userFriendlyMessage: msg`Object already exists`,
});
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
@@ -30,7 +30,7 @@ export const validateFlatObjectMetadataNames = ({
errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: `The singular and plural names cannot be the same for an object`,
userFriendlyMessage: t`The singular and plural names cannot be the same for an object`,
userFriendlyMessage: msg`The singular and plural names cannot be the same for an object`,
value: namePlural,
});
}
@@ -1,3 +1,5 @@
import { type MessageDescriptor } from '@lingui/core';
import { CustomException } from 'src/utils/custom-exception';
export class IndexMetadataException extends CustomException {
@@ -5,7 +7,7 @@ export class IndexMetadataException extends CustomException {
constructor(
message: string,
code: IndexMetadataExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, { userFriendlyMessage });
}
@@ -3,6 +3,8 @@ import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@@ -20,11 +22,13 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module
PermissionsGraphqlApiExceptionFilter,
)
export class IndexMetadataResolver {
constructor(private readonly i18nService: I18nService) {}
@ResolveField(() => [IndexFieldMetadataDTO], { nullable: false })
async indexFieldMetadataList(
@AuthWorkspace() workspace: Workspace,
@Parent() indexMetadata: IndexMetadataDTO,
@Context() context: { loaders: IDataloaders },
@Context() context: { loaders: IDataloaders } & I18nContext,
): Promise<IndexFieldMetadataDTO[]> {
try {
const indexFieldMetadataItems =
@@ -36,7 +40,10 @@ export class IndexMetadataResolver {
return indexFieldMetadataItems;
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
objectMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
return [];
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@@ -30,7 +30,7 @@ export const validateCanCreateUniqueIndex = (
`Unique index cannot be created for field ${field.name} of type ${fieldType}`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: t`${fieldType} fields cannot be unique.`,
userFriendlyMessage: msg`${fieldType} fields cannot be unique.`,
},
);
}
@@ -1,20 +1,36 @@
import {
type CallHandler,
type ExecutionContext,
Injectable,
type NestInterceptor,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { type Observable, catchError } from 'rxjs';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
@Injectable()
export class ObjectMetadataGraphqlApiExceptionInterceptor
implements NestInterceptor
{
constructor(private readonly i18nService: I18nService) {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
intercept(_: ExecutionContext, next: CallHandler): Observable<any> {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const gqlContext = GqlExecutionContext.create(context);
const ctx = gqlContext.getContext();
const locale = ctx.req?.locale ?? SOURCE_LOCALE;
const i18n = this.i18nService.getI18nInstance(locale);
return next
.handle()
.pipe(catchError((err) => objectMetadataGraphqlApiExceptionHandler(err)));
.pipe(
catchError((err) =>
objectMetadataGraphqlApiExceptionHandler(err, i18n),
),
);
}
}
@@ -117,6 +117,7 @@ export class ObjectMetadataResolver {
async deleteOneObject(
@Args('input') input: DeleteOneObjectInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
@Context() context: I18nContext,
) {
try {
return await this.objectMetadataService.deleteOneObject(
@@ -124,7 +125,10 @@ export class ObjectMetadataResolver {
workspaceId,
);
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
objectMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
}
@@ -148,7 +152,10 @@ export class ObjectMetadataResolver {
workspaceId,
});
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
objectMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
}
@@ -166,7 +173,10 @@ export class ObjectMetadataResolver {
workspaceId,
);
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
objectMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
}
}
@@ -187,7 +197,10 @@ export class ObjectMetadataResolver {
return fieldMetadataItems;
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
objectMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
return [];
}
@@ -197,7 +210,7 @@ export class ObjectMetadataResolver {
async indexMetadataList(
@AuthWorkspace() workspace: Workspace,
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: { loaders: IDataloaders },
@Context() context: { loaders: IDataloaders } & I18nContext,
): Promise<IndexMetadataDTO[]> {
try {
const indexMetadataItems = await context.loaders.indexMetadataLoader.load(
@@ -209,7 +222,10 @@ export class ObjectMetadataResolver {
return indexMetadataItems;
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
objectMetadataGraphqlApiExceptionHandler(
error,
this.i18nService.getI18nInstance(context.req.locale),
);
return [];
}
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { type QueryRunner, Repository } from 'typeorm';
@@ -607,7 +608,7 @@ export class ObjectMetadataFieldRelationService {
`Name "${name}" is not available.`,
ObjectMetadataExceptionCode.NAME_CONFLICT,
{
userFriendlyMessage: `Name "${name}" is not available.`,
userFriendlyMessage: msg`Name "${name}" is not available.`,
},
);
}
@@ -1,3 +1,4 @@
import { type I18n } from '@lingui/core';
import { assertUnreachable } from 'twenty-shared/utils';
import {
@@ -15,9 +16,12 @@ import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exce
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
export const objectMetadataGraphqlApiExceptionHandler = (
error: Error,
i18n: I18n,
) => {
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
workspaceMigrationBuilderExceptionV2Formatter(error);
workspaceMigrationBuilderExceptionV2Formatter(error, i18n);
}
if (error instanceof InvalidMetadataException) {
@@ -1,3 +1,4 @@
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
@@ -35,7 +36,7 @@ export const validateObjectMetadataInputNameOrThrow = (name: string): void => {
errorMessage,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
{
userFriendlyMessage: errorMessage,
userFriendlyMessage: msg`Invalid object metadata input`,
},
);
}
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
@@ -207,8 +208,7 @@ export class FieldPermissionService {
PermissionsExceptionMessage.ONLY_FIELD_RESTRICTION_ALLOWED,
PermissionsExceptionCode.ONLY_FIELD_RESTRICTION_ALLOWED,
{
userFriendlyMessage:
'Field permissions can only be used to restrict access, not to grant additional permissions.',
userFriendlyMessage: msg`Field permissions can only be used to restrict access, not to grant additional permissions.`,
},
);
}
@@ -221,8 +221,7 @@ export class FieldPermissionService {
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
{
userFriendlyMessage:
'The object you are trying to set permissions for could not be found. It may have been deleted.',
userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`,
},
);
}
@@ -232,8 +231,7 @@ export class FieldPermissionService {
PermissionsExceptionMessage.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT,
PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT,
{
userFriendlyMessage:
'You cannot set field permissions on system objects as they are managed by the platform.',
userFriendlyMessage: msg`You cannot set field permissions on system objects as they are managed by the platform.`,
},
);
}
@@ -248,8 +246,7 @@ export class FieldPermissionService {
PermissionsExceptionMessage.FIELD_METADATA_NOT_FOUND,
PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND,
{
userFriendlyMessage:
'The field you are trying to set permissions for could not be found. It may have been deleted.',
userFriendlyMessage: msg`The field you are trying to set permissions for could not be found. It may have been deleted.`,
},
);
}
@@ -262,8 +259,7 @@ export class FieldPermissionService {
PermissionsExceptionMessage.OBJECT_PERMISSION_NOT_FOUND,
PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND,
{
userFriendlyMessage:
'No permissions are set for this role on the selected object. Please set object permissions first.',
userFriendlyMessage: msg`No permissions are set for this role on the selected object. Please set object permissions first.`,
},
);
}
@@ -289,8 +285,7 @@ export class FieldPermissionService {
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage:
'The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.',
userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
@@ -304,8 +299,7 @@ export class FieldPermissionService {
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
{
userFriendlyMessage:
'This role cannot be modified because it is a system role. Only custom roles can be edited.',
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
},
);
}
@@ -436,10 +430,12 @@ export class FieldPermissionService {
firstFieldPermission.canUpdateFieldValue;
if (hasConflictingPermissions) {
const fieldName = fieldMetadata.name;
throw new UserInputError(
'Conflicting field permissions found for relation target field',
{
userFriendlyMessage: `Contradicting field permissions have been detected on a relation field (${fieldMetadata.name}).`,
userFriendlyMessage: msg`Contradicting field permissions have been detected on a relation field (${fieldName}).`,
},
);
}
@@ -1,5 +1,6 @@
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
@@ -66,8 +67,7 @@ export class ObjectPermissionService {
'Object metadata id not found',
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
{
userFriendlyMessage:
'The object you are trying to set permissions for could not be found. It may have been deleted.',
userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`,
},
);
}
@@ -77,8 +77,7 @@ export class ObjectPermissionService {
PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
{
userFriendlyMessage:
'You cannot set permissions on system objects as they are managed by the platform.',
userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`,
},
);
}
@@ -183,8 +182,7 @@ export class ObjectPermissionService {
PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT,
PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT,
{
userFriendlyMessage:
'You cannot grant edit permissions without also granting read permissions. Please enable read access first.',
userFriendlyMessage: msg`You cannot grant edit permissions without also granting read permissions. Please enable read access first.`,
},
);
}
@@ -216,8 +214,7 @@ export class ObjectPermissionService {
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage:
'The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.',
userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
@@ -234,8 +231,7 @@ export class ObjectPermissionService {
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
{
userFriendlyMessage:
'One or more objects you are trying to set permissions for could not be found. They may have been deleted.',
userFriendlyMessage: msg`One or more objects you are trying to set permissions for could not be found. They may have been deleted.`,
},
);
}
@@ -262,8 +258,7 @@ export class ObjectPermissionService {
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage:
'The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.',
userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
@@ -277,8 +272,7 @@ export class ObjectPermissionService {
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
{
userFriendlyMessage:
'This role cannot be modified because it is a system role. Only custom roles can be edited.',
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
},
);
}
@@ -1,5 +1,6 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { DataSource, In, Repository } from 'typeorm';
@@ -44,8 +45,7 @@ export class PermissionFlagService {
`${PermissionsExceptionMessage.INVALID_SETTING}: ${invalidFlags.join(', ')}`,
PermissionsExceptionCode.INVALID_SETTING,
{
userFriendlyMessage:
'Some of the permissions you selected are not valid. Please try again with valid permission settings.',
userFriendlyMessage: msg`Some of the permissions you selected are not valid. Please try again with valid permission settings.`,
},
);
}
@@ -119,8 +119,7 @@ export class PermissionFlagService {
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage:
'The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.',
userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
@@ -157,8 +156,7 @@ export class PermissionFlagService {
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
{
userFriendlyMessage:
'This role cannot be modified because it is a system role. Only custom roles can be edited.',
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
},
);
}
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@@ -50,8 +51,7 @@ export class PermissionsService {
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
{
userFriendlyMessage:
'Your role in this workspace could not be found. Please contact your workspace administrator.',
userFriendlyMessage: msg`Your role in this workspace could not be found. Please contact your workspace administrator.`,
},
);
}
@@ -135,8 +135,7 @@ export class PermissionsService {
PermissionsExceptionMessage.API_KEY_ROLE_NOT_FOUND,
PermissionsExceptionCode.API_KEY_ROLE_NOT_FOUND,
{
userFriendlyMessage:
'The API key does not have a valid role assigned. Please check your API key configuration.',
userFriendlyMessage: msg`The API key does not have a valid role assigned. Please check your API key configuration.`,
},
);
}
@@ -157,8 +156,7 @@ export class PermissionsService {
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
{
userFriendlyMessage:
'Your role in this workspace could not be found. Please contact your workspace administrator.',
userFriendlyMessage: msg`Your role in this workspace could not be found. Please contact your workspace administrator.`,
},
);
}
@@ -170,8 +168,7 @@ export class PermissionsService {
PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT,
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
{
userFriendlyMessage:
'Authentication is required to access this feature. Please sign in and try again.',
userFriendlyMessage: msg`Authentication is required to access this feature. Please sign in and try again.`,
},
);
}
@@ -1,3 +1,4 @@
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import {
@@ -16,12 +17,12 @@ export const permissionGraphqlApiExceptionHandler = (
switch (error.code) {
case PermissionsExceptionCode.PERMISSION_DENIED:
throw new ForbiddenError(error.message, {
userFriendlyMessage: 'User does not have permission.',
userFriendlyMessage: msg`User does not have permission.`,
subCode: error.code,
});
case PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT:
throw new ForbiddenError(error.message, {
userFriendlyMessage: 'No valid authentication context found.',
userFriendlyMessage: msg`No valid authentication context found.`,
subCode: error.code,
});
case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS:
@@ -8,6 +8,8 @@ import {
Resolver,
} from '@nestjs/graphql';
import { msg } from '@lingui/core/macro';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
@@ -92,8 +94,7 @@ export class RoleResolver {
PermissionsExceptionMessage.CANNOT_UPDATE_SELF_ROLE,
PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE,
{
userFriendlyMessage:
'You cannot change your own role. Please ask another administrator to update your role.',
userFriendlyMessage: msg`You cannot change your own role. Please ask another administrator to update your role.`,
},
);
}
@@ -1,6 +1,6 @@
import { InjectRepository } from '@nestjs/typeorm';
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@@ -122,8 +122,7 @@ export class RoleService {
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage:
'The role you are looking for could not be found. It may have been deleted or you may not have access to it.',
userFriendlyMessage: msg`The role you are looking for could not be found. It may have been deleted or you may not have access to it.`,
},
);
}
@@ -169,8 +168,7 @@ export class RoleService {
PermissionsExceptionMessage.DEFAULT_ROLE_NOT_FOUND,
PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND,
{
userFriendlyMessage:
'The default role for this workspace could not be found. Please contact support for assistance.',
userFriendlyMessage: msg`The default role for this workspace could not be found. Please contact support for assistance.`,
},
);
}
@@ -277,8 +275,7 @@ export class RoleService {
error.message,
PermissionsExceptionCode.INVALID_ARG,
{
userFriendlyMessage:
'Some of the information provided is invalid. Please check your input and try again.',
userFriendlyMessage: msg`Some of the information provided is invalid. Please check your input and try again.`,
},
);
}
@@ -299,7 +296,7 @@ export class RoleService {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS,
PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS,
{ userFriendlyMessage: t`A role with this label already exists.` },
{ userFriendlyMessage: msg`A role with this label already exists.` },
);
}
}
@@ -344,8 +341,7 @@ export class RoleService {
PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
{
userFriendlyMessage:
'You cannot grant edit permissions without also granting read permissions. Please enable read access first.',
userFriendlyMessage: msg`You cannot grant edit permissions without also granting read permissions. Please enable read access first.`,
},
);
}
@@ -403,8 +399,7 @@ export class RoleService {
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
{
userFriendlyMessage:
'This role cannot be modified because it is a system role. Only custom roles can be edited.',
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
},
);
}
@@ -422,8 +417,7 @@ export class RoleService {
PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED,
PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED,
{
userFriendlyMessage:
'The default role cannot be deleted as it is required for the workspace to function properly.',
userFriendlyMessage: msg`The default role cannot be deleted as it is required for the workspace to function properly.`,
},
);
}
@@ -1,5 +1,6 @@
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { In, Not, Repository } from 'typeorm';
@@ -196,8 +197,7 @@ export class UserRoleService {
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
{
userFriendlyMessage:
'Your role in this workspace could not be found. Please contact your workspace administrator.',
userFriendlyMessage: msg`Your role in this workspace could not be found. Please contact your workspace administrator.`,
},
);
}
@@ -235,8 +235,7 @@ export class UserRoleService {
'User workspace not found',
PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND,
{
userFriendlyMessage:
'Your workspace membership could not be found. You may no longer have access to this workspace.',
userFriendlyMessage: msg`Your workspace membership could not be found. You may no longer have access to this workspace.`,
},
);
}
@@ -252,8 +251,7 @@ export class UserRoleService {
'Role not found',
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage:
'The role you are trying to assign could not be found. It may have been deleted.',
userFriendlyMessage: msg`The role you are trying to assign could not be found. It may have been deleted.`,
},
);
}
@@ -263,8 +261,7 @@ export class UserRoleService {
`Role "${role.label}" cannot be assigned to users`,
PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS,
{
userFriendlyMessage:
'This role cannot be assigned to users. Please select a different role.',
userFriendlyMessage: msg`This role cannot be assigned to users. Please select a different role.`,
},
);
}
@@ -312,8 +309,7 @@ export class UserRoleService {
PermissionsExceptionMessage.CANNOT_UNASSIGN_LAST_ADMIN,
PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN,
{
userFriendlyMessage:
'You cannot remove the admin role from the last administrator. Please assign another administrator first.',
userFriendlyMessage: msg`You cannot remove the admin role from the last administrator. Please assign another administrator first.`,
},
);
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import camelCase from 'lodash.camelcase';
import { slugify } from 'transliteration';
import { isDefined } from 'twenty-shared/utils';
@@ -14,7 +14,7 @@ export const computeMetadataNameFromLabel = (label: string): string => {
'Label is required',
InvalidMetadataExceptionCode.LABEL_REQUIRED,
{
userFriendlyMessage: t`Label is required`,
userFriendlyMessage: msg`Label is required`,
},
);
}
@@ -36,7 +36,7 @@ export const computeMetadataNameFromLabel = (label: string): string => {
`Invalid label: "${label}"`,
InvalidMetadataExceptionCode.INVALID_LABEL,
{
userFriendlyMessage: t`Invalid label: "${label}"`,
userFriendlyMessage: msg`Invalid label: "${label}"`,
},
);
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
@@ -54,7 +54,7 @@ export const validateFieldNameAvailabilityOrThrow = ({
`Name "${name}" is not available as it is already used by another field`,
InvalidMetadataExceptionCode.NOT_AVAILABLE,
{
userFriendlyMessage: t`This name is not available as it is already used by another field.`,
userFriendlyMessage: msg`This name is not available as it is already used by another field.`,
},
);
}
@@ -64,7 +64,7 @@ export const validateFieldNameAvailabilityOrThrow = ({
`Name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
{
userFriendlyMessage: t`This name is not available.`,
userFriendlyMessage: msg`This name is not available.`,
},
);
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import {
InvalidMetadataException,
@@ -74,7 +74,7 @@ export const validateMetadataNameIsNotReservedKeywordOrThrow = (
`The name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
{
userFriendlyMessage: t`This name is not available.`,
userFriendlyMessage: msg`This name is not available.`,
},
);
}
@@ -1,4 +1,4 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
@@ -43,7 +43,7 @@ export const validatesNoOtherObjectWithSameNameExistsOrThrows = (
'Object already exists',
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
{
userFriendlyMessage: t`Object already exists`,
userFriendlyMessage: msg`Object already exists`,
},
);
}
@@ -1,4 +1,5 @@
import { t } from '@lingui/core/macro';
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -8,7 +9,7 @@ export class ViewFieldException extends CustomException {
constructor(
message: string,
code: ViewFieldExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, { userFriendlyMessage });
}
@@ -52,15 +53,15 @@ export const generateViewFieldExceptionMessage = (
export const generateViewFieldUserFriendlyExceptionMessage = (
key: ViewFieldExceptionMessageKey,
) => {
): MessageDescriptor | undefined => {
switch (key) {
case ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view field.`;
return msg`WorkspaceId is required to create a view field.`;
case ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view field.`;
return msg`ViewId is required to create a view field.`;
case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view field.`;
return msg`FieldMetadataId is required to create a view field.`;
case ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS:
return t`View field already exists.`;
return msg`View field already exists.`;
}
};
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
@@ -319,7 +320,7 @@ export class ViewFieldService {
if (newOrUpdatedViewField.isVisible === false) {
throw new UserInputError('Label metadata identifier must stay visible.', {
userFriendlyMessage: 'Record text must stay visible.',
userFriendlyMessage: msg`Record text must stay visible.`,
});
}
}
@@ -370,8 +371,7 @@ export class ViewFieldService {
throw new UserInputError(
'Label metadata identifier must keep the minimal position in the view.',
{
userFriendlyMessage:
'Record text must be in first position of the view.',
userFriendlyMessage: msg`Record text must be in first position of the view.`,
},
);
}
@@ -389,8 +389,7 @@ export class ViewFieldService {
throw new UserInputError(
'Label metadata identifier must keep the minimal position in the view.',
{
userFriendlyMessage:
'Record text must be in first position of the view.',
userFriendlyMessage: msg`Record text must be in first position of the view.`,
},
);
}
@@ -1,4 +1,5 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type MessageDescriptor } from '@lingui/core';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -8,7 +9,7 @@ export class ViewFilterGroupException extends CustomException {
constructor(
message: string,
code: ViewFilterGroupExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, { userFriendlyMessage });
}
@@ -49,13 +50,13 @@ export const generateViewFilterGroupExceptionMessage = (
export const generateViewFilterGroupUserFriendlyExceptionMessage = (
key: ViewFilterGroupExceptionMessageKey,
) => {
): MessageDescriptor | undefined => {
switch (key) {
case ViewFilterGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view filter group.`;
return msg`WorkspaceId is required to create a view filter group.`;
case ViewFilterGroupExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view filter group.`;
return msg`ViewId is required to create a view filter group.`;
case ViewFilterGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view filter group.`;
return msg`FieldMetadataId is required to create a view filter group.`;
}
};
@@ -1,4 +1,5 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type MessageDescriptor } from '@lingui/core';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -8,7 +9,7 @@ export class ViewFilterException extends CustomException {
constructor(
message: string,
code: ViewFilterExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, { userFriendlyMessage });
}
@@ -49,13 +50,13 @@ export const generateViewFilterExceptionMessage = (
export const generateViewFilterUserFriendlyExceptionMessage = (
key: ViewFilterExceptionMessageKey,
) => {
): MessageDescriptor | undefined => {
switch (key) {
case ViewFilterExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view filter.`;
return msg`WorkspaceId is required to create a view filter.`;
case ViewFilterExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view filter.`;
return msg`ViewId is required to create a view filter.`;
case ViewFilterExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view filter.`;
return msg`FieldMetadataId is required to create a view filter.`;
}
};
@@ -1,4 +1,5 @@
import { t } from '@lingui/core/macro';
import { msg } from '@lingui/core/macro';
import { type MessageDescriptor } from '@lingui/core';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -8,7 +9,7 @@ export class ViewGroupException extends CustomException {
constructor(
message: string,
code: ViewGroupExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, { userFriendlyMessage });
}
@@ -49,13 +50,13 @@ export const generateViewGroupExceptionMessage = (
export const generateViewGroupUserFriendlyExceptionMessage = (
key: ViewGroupExceptionMessageKey,
) => {
): MessageDescriptor | undefined => {
switch (key) {
case ViewGroupExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view group.`;
return msg`WorkspaceId is required to create a view group.`;
case ViewGroupExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view group.`;
return msg`ViewId is required to create a view group.`;
case ViewGroupExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view group.`;
return msg`FieldMetadataId is required to create a view group.`;
}
};
@@ -1,4 +1,5 @@
import { t } from '@lingui/core/macro';
import { msg, t } from '@lingui/core/macro';
import { type MessageDescriptor } from '@lingui/core';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -8,7 +9,7 @@ export class ViewSortException extends CustomException {
constructor(
message: string,
code: ViewSortExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, { userFriendlyMessage });
}
@@ -62,13 +63,13 @@ export const generateViewSortExceptionMessage = (
export const generateViewSortUserFriendlyExceptionMessage = (
key: ViewSortExceptionMessageKey,
) => {
): MessageDescriptor | undefined => {
switch (key) {
case ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view sort.`;
return msg`WorkspaceId is required to create a view sort.`;
case ViewSortExceptionMessageKey.VIEW_ID_REQUIRED:
return t`ViewId is required to create a view sort.`;
return msg`ViewId is required to create a view sort.`;
case ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
return t`FieldMetadataId is required to create a view sort.`;
return msg`FieldMetadataId is required to create a view sort.`;
}
};
@@ -1,4 +1,5 @@
import { t } from '@lingui/core/macro';
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -8,7 +9,7 @@ export class ViewException extends CustomException {
constructor(
message: string,
code: ViewExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, { userFriendlyMessage });
}
@@ -46,11 +47,11 @@ export const generateViewExceptionMessage = (
export const generateViewUserFriendlyExceptionMessage = (
key: ViewExceptionMessageKey,
) => {
): MessageDescriptor | undefined => {
switch (key) {
case ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED:
return t`WorkspaceId is required to create a view.`;
return msg`WorkspaceId is required to create a view.`;
case ViewExceptionMessageKey.OBJECT_METADATA_ID_REQUIRED:
return t`ObjectMetadataId is required to create a view.`;
return msg`ObjectMetadataId is required to create a view.`;
}
};
@@ -1,3 +1,4 @@
import { type I18n } from '@lingui/core';
import { assertUnreachable } from 'twenty-shared/utils';
import {
@@ -31,9 +32,9 @@ import {
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
export const viewGraphqlApiExceptionHandler = (error: Error) => {
export const viewGraphqlApiExceptionHandler = (error: Error, i18n: I18n) => {
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
return workspaceMigrationBuilderExceptionV2Formatter(error);
return workspaceMigrationBuilderExceptionV2Formatter(error, i18n);
}
if (error instanceof ViewException) {
@@ -1,5 +1,14 @@
import { Catch, type ExceptionFilter } from '@nestjs/common';
import {
Catch,
type ExecutionContext,
type ExceptionFilter,
Injectable,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { ViewFieldException } from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
import { ViewFilterGroupException } from 'src/engine/metadata-modules/view-filter-group/exceptions/view-filter-group.exception';
import { ViewFilterException } from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception';
@@ -18,7 +27,10 @@ import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manag
ViewSortException,
WorkspaceMigrationBuilderExceptionV2,
)
@Injectable()
export class ViewGraphqlApiExceptionFilter implements ExceptionFilter {
constructor(private readonly i18nService: I18nService) {}
catch(
exception:
| ViewException
@@ -28,7 +40,13 @@ export class ViewGraphqlApiExceptionFilter implements ExceptionFilter {
| ViewGroupException
| ViewSortException
| WorkspaceMigrationBuilderExceptionV2,
host: ExecutionContext,
) {
return viewGraphqlApiExceptionHandler(exception);
const gqlContext = GqlExecutionContext.create(host);
const ctx = gqlContext.getContext();
const userLocale = ctx.req?.locale ?? SOURCE_LOCALE;
const i18n = this.i18nService.getI18nInstance(userLocale);
return viewGraphqlApiExceptionHandler(exception, i18n);
}
}