Update user friendly errors for translations (#15000)
Force msg typing instead of string for user friendly errors
This commit is contained in:
@@ -96,11 +96,14 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
const authLink = setContext(async (_, { headers }) => {
|
||||
const tokenPair = getTokenPair();
|
||||
|
||||
const locale = this.currentWorkspaceMember?.locale ?? i18n.locale;
|
||||
|
||||
if (isUndefinedOrNull(tokenPair)) {
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
...options.headers,
|
||||
'x-locale': locale,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -112,9 +115,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
...headers,
|
||||
...options.headers,
|
||||
authorization: token ? `Bearer ${token}` : '',
|
||||
...(this.currentWorkspaceMember?.locale
|
||||
? { 'x-locale': this.currentWorkspaceMember.locale }
|
||||
: { 'x-locale': i18n.locale }),
|
||||
'x-locale': locale,
|
||||
...(this.currentWorkspace?.metadataVersion && {
|
||||
'X-Schema-Version': `${this.currentWorkspace.metadataVersion}`,
|
||||
}),
|
||||
|
||||
@@ -24,6 +24,7 @@ import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing';
|
||||
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type User } from 'src/engine/core-modules/user/user.entity';
|
||||
@@ -47,6 +48,7 @@ export class GraphQLConfigService
|
||||
private readonly moduleRef: ModuleRef,
|
||||
private readonly metricsService: MetricsService,
|
||||
private readonly dataloaderService: DataloaderService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
createGqlOptions(): YogaDriverConfig {
|
||||
@@ -63,6 +65,7 @@ export class GraphQLConfigService
|
||||
useGraphQLErrorHandlerHook({
|
||||
metricsService: this.metricsService,
|
||||
exceptionHandlerService: this.exceptionHandlerService,
|
||||
i18nService: this.i18nService,
|
||||
twentyConfigService: this.twentyConfigService,
|
||||
}),
|
||||
];
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -47,7 +47,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
|
||||
GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED,
|
||||
{
|
||||
userFriendlyMessage: t`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
|
||||
userFriendlyMessage: msg`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -359,7 +359,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
|
||||
GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT,
|
||||
{
|
||||
userFriendlyMessage: t`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
|
||||
userFriendlyMessage: msg`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graph
|
||||
import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -21,13 +23,19 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
|
||||
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
||||
driver: YogaDriver,
|
||||
useFactory: metadataModuleFactory,
|
||||
imports: [GraphQLConfigModule, DataloaderModule, MetricsModule],
|
||||
imports: [
|
||||
GraphQLConfigModule,
|
||||
DataloaderModule,
|
||||
MetricsModule,
|
||||
I18nModule,
|
||||
],
|
||||
inject: [
|
||||
TwentyConfigService,
|
||||
ExceptionHandlerService,
|
||||
DataloaderService,
|
||||
CacheStorageNamespace.EngineWorkspace,
|
||||
MetricsService,
|
||||
I18nService,
|
||||
],
|
||||
}),
|
||||
MetadataEngineModule,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphq
|
||||
import { type CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { type ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
|
||||
import { type I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type DataloaderService } from 'src/engine/dataloaders/dataloader.service';
|
||||
@@ -20,6 +21,7 @@ export const metadataModuleFactory = async (
|
||||
dataloaderService: DataloaderService,
|
||||
cacheStorageService: CacheStorageService,
|
||||
metricsService: MetricsService,
|
||||
i18nService: I18nService,
|
||||
): Promise<YogaDriverConfig> => {
|
||||
const config: YogaDriverConfig = {
|
||||
autoSchemaFile: true,
|
||||
@@ -39,6 +41,7 @@ export const metadataModuleFactory = async (
|
||||
useGraphQLErrorHandlerHook({
|
||||
metricsService: metricsService,
|
||||
exceptionHandlerService,
|
||||
i18nService,
|
||||
twentyConfigService,
|
||||
}),
|
||||
useCachedMetadata({
|
||||
|
||||
+10
-3
@@ -1,3 +1,4 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryFailedError } from 'typeorm';
|
||||
|
||||
@@ -42,7 +43,7 @@ export const handleDuplicateKeyError = (
|
||||
`A duplicate entry was detected`,
|
||||
TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED,
|
||||
{
|
||||
userFriendlyMessage: `This record already exists. Please check your data and try again.`,
|
||||
userFriendlyMessage: msg`This record already exists. Please check your data and try again.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -50,20 +51,26 @@ export const handleDuplicateKeyError = (
|
||||
const columnNames = affectedColumns.join(', ');
|
||||
|
||||
if (affectedColumns?.length === 1) {
|
||||
const fieldName = columnNames.toLowerCase();
|
||||
|
||||
throw new UserInputError(
|
||||
`Duplicate ${columnNames} ${duplicatedValues ? `with value ${duplicatedValues}` : ''}. Please set a unique one.`,
|
||||
{
|
||||
userFriendlyMessage: `This ${columnNames.toLowerCase()} ${duplicatedValues ? `with value ${duplicatedValues}` : ''} is already taken. Please choose a different value.`,
|
||||
userFriendlyMessage: duplicatedValues
|
||||
? msg`This ${fieldName} with value ${duplicatedValues} is already taken. Please choose a different value.`
|
||||
: msg`This ${fieldName} is already taken. Please choose a different value.`,
|
||||
isExpected: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const fieldNames = columnNames.toLowerCase();
|
||||
|
||||
throw new TwentyORMException(
|
||||
`A duplicate entry was detected. The combination of ${columnNames} must be unique.`,
|
||||
TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED,
|
||||
{
|
||||
userFriendlyMessage: `This combination of ${columnNames.toLowerCase()} already exists. Please use different values.`,
|
||||
userFriendlyMessage: msg`This combination of ${fieldNames} already exists. Please use different values.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import axios from 'axios';
|
||||
import semver from 'semver';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -65,7 +66,7 @@ export class AdminPanelService {
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
targetUser,
|
||||
new AuthException('User not found', AuthExceptionCode.INVALID_INPUT, {
|
||||
userFriendlyMessage: 'User not found. Please check the email or ID.',
|
||||
userFriendlyMessage: msg`User not found. Please check the email or ID.`,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DataSource, IsNull, Repository } from 'typeorm';
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
@@ -113,8 +114,7 @@ export class ApiKeyService {
|
||||
'This API Key is revoked',
|
||||
ApiKeyExceptionCode.API_KEY_REVOKED,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'This API Key has been revoked and can no longer be used.',
|
||||
userFriendlyMessage: msg`This API Key has been revoked and can no longer be used.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -124,8 +124,7 @@ export class ApiKeyService {
|
||||
'This API Key has expired',
|
||||
ApiKeyExceptionCode.API_KEY_EXPIRED,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'This API Key has expired. Please create a new one.',
|
||||
userFriendlyMessage: msg`This API Key has expired. Please create a new one.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { SendApprovedAccessDomainValidation } from 'twenty-emails';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
@@ -46,7 +46,7 @@ export class ApprovedAccessDomainService {
|
||||
'Approved access domain has already been validated',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain has already been validated`,
|
||||
userFriendlyMessage: msg`Approved access domain has already been validated`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export class ApprovedAccessDomainService {
|
||||
'Approved access domain does not match email domain',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_DOES_NOT_MATCH_DOMAIN_EMAIL,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain does not match email domain`,
|
||||
userFriendlyMessage: msg`Approved access domain does not match email domain`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export class ApprovedAccessDomainService {
|
||||
'Approved access domain has already been validated',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain has already been validated`,
|
||||
userFriendlyMessage: msg`Approved access domain has already been validated`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -180,7 +180,7 @@ export class ApprovedAccessDomainService {
|
||||
'Approved access domain already registered.',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_REGISTERED,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain already registered.`,
|
||||
userFriendlyMessage: msg`Approved access domain already registered.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto, { randomUUID } from 'node:crypto';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@@ -163,7 +163,7 @@ export class AuthService {
|
||||
'Incorrect login method',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`User was not created with email/password`,
|
||||
userFriendlyMessage: msg`User was not created with email/password`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -175,7 +175,7 @@ export class AuthService {
|
||||
'Wrong password',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Wrong password`,
|
||||
userFriendlyMessage: msg`Wrong password`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -746,7 +746,7 @@ export class AuthService {
|
||||
'User does not have access to this workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`User does not have access to this workspace`,
|
||||
userFriendlyMessage: msg`User does not have access to this workspace`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+11
-11
@@ -2,7 +2,7 @@ import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -30,6 +30,7 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
@@ -37,7 +38,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@@ -66,7 +66,7 @@ export class SignInUpService {
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
userFriendlyMessage: msg`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export class SignInUpService {
|
||||
'Password too weak',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Password too weak`,
|
||||
userFriendlyMessage: msg`Password too weak`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export class SignInUpService {
|
||||
'Wrong password',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Wrong password`,
|
||||
userFriendlyMessage: msg`Wrong password`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export class SignInUpService {
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
userFriendlyMessage: msg`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -216,7 +216,7 @@ export class SignInUpService {
|
||||
'Workspace is not ready to welcome new members',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Workspace is not ready to welcome new members`,
|
||||
userFriendlyMessage: msg`Workspace is not ready to welcome new members`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -232,7 +232,7 @@ export class SignInUpService {
|
||||
'User is not part of the workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`User is not part of the workspace`,
|
||||
userFriendlyMessage: msg`User is not part of the workspace`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -379,7 +379,7 @@ export class SignInUpService {
|
||||
'Workspace creation is restricted to admins',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Workspace creation is restricted to admins`,
|
||||
userFriendlyMessage: msg`Workspace creation is restricted to admins`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -398,7 +398,7 @@ export class SignInUpService {
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
userFriendlyMessage: msg`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -470,7 +470,7 @@ export class SignInUpService {
|
||||
new AuthException(
|
||||
'User already exist',
|
||||
AuthExceptionCode.USER_ALREADY_EXIST,
|
||||
{ userFriendlyMessage: t`User already exists` },
|
||||
{ userFriendlyMessage: msg`User already exists` },
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Strategy } from 'passport-jwt';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -158,7 +158,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
'UserWorkspace not found',
|
||||
AuthExceptionCode.USER_WORKSPACE_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: t`User does not have access to this workspace`,
|
||||
userFriendlyMessage: msg`User does not have access to this workspace`,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
@@ -31,14 +31,14 @@ export const authGraphqlApiExceptionHandler = (exception: AuthException) => {
|
||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
userFriendlyMessage: t`Authentication is not enabled with this provider.`,
|
||||
userFriendlyMessage: msg`Authentication is not enabled with this provider.`,
|
||||
subCode: exception.code,
|
||||
});
|
||||
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
|
||||
case AuthExceptionCode.INVALID_DATA:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: AuthExceptionCode.EMAIL_NOT_VERIFIED,
|
||||
userFriendlyMessage: t`Email is not verified.`,
|
||||
userFriendlyMessage: msg`Email is not verified.`,
|
||||
});
|
||||
case AuthExceptionCode.TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED:
|
||||
case AuthExceptionCode.TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED:
|
||||
@@ -47,7 +47,7 @@ export const authGraphqlApiExceptionHandler = (exception: AuthException) => {
|
||||
});
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
throw new AuthenticationError(exception.message, {
|
||||
userFriendlyMessage: t`You must be authenticated to perform this action.`,
|
||||
userFriendlyMessage: msg`You must be authenticated to perform this action.`,
|
||||
subCode: exception.code,
|
||||
});
|
||||
case AuthExceptionCode.USER_NOT_FOUND:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { type BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { type MeterBillingPriceTiers } from 'src/engine/core-modules/billing/types/meter-billing-price-tier.type';
|
||||
@@ -137,8 +138,7 @@ const assertIsSubscription = (
|
||||
'Subscription must have exactly two subscription items. Check that stripe and database are in sync',
|
||||
BillingExceptionCode.BILLING_SUBSCRIPTION_INVALID,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Your billing subscription is corrupted. Please contact support.',
|
||||
userFriendlyMessage: msg`Your billing subscription is corrupted. Please contact support.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
CaptchaException,
|
||||
@@ -41,7 +41,9 @@ export class CaptchaGuard implements CanActivate {
|
||||
throw new CaptchaException(
|
||||
'Invalid Captcha, please try another device',
|
||||
CaptchaExceptionCode.INVALID_CAPTCHA,
|
||||
{ userFriendlyMessage: t`Invalid Captcha, please try another device` },
|
||||
{
|
||||
userFriendlyMessage: msg`Invalid Captcha, please try another device`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -1,6 +1,7 @@
|
||||
/* @license Enterprise */
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import Cloudflare from 'cloudflare';
|
||||
import {
|
||||
type CustomHostnameCreateParams,
|
||||
@@ -8,14 +9,14 @@ import {
|
||||
} from 'cloudflare/resources/custom-hostnames/custom-hostnames';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
|
||||
import {
|
||||
DnsManagerException,
|
||||
DnsManagerExceptionCode,
|
||||
} from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { dnsManagerValidator } from 'src/engine/core-modules/dns-manager/validator/dns-manager.validate';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { type DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
|
||||
|
||||
type DnsManagerOptions = {
|
||||
isPublicDomain?: boolean;
|
||||
@@ -43,7 +44,7 @@ export class DnsManagerService {
|
||||
throw new DnsManagerException(
|
||||
'Hostname already registered',
|
||||
DnsManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED,
|
||||
{ userFriendlyMessage: 'Domain is already registered' },
|
||||
{ userFriendlyMessage: msg`Domain is already registered` },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,8 +67,7 @@ export class DnsManagerService {
|
||||
'Missing public domain URL',
|
||||
DnsManagerExceptionCode.MISSING_PUBLIC_DOMAIN_URL,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Public domain URL is not defined. Please set the PUBLIC_DOMAIN_URL environment variable',
|
||||
userFriendlyMessage: msg`Public domain URL is not defined. Please set the PUBLIC_DOMAIN_URL environment variable`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -219,11 +219,14 @@ export class DnsManagerService {
|
||||
}
|
||||
|
||||
// should never happen. error 5xx
|
||||
const hostnameCount = customHostnames.result.length;
|
||||
const domainName = hostname;
|
||||
|
||||
throw new DnsManagerException(
|
||||
'More than one custom hostname found in cloudflare',
|
||||
DnsManagerExceptionCode.MULTIPLE_HOSTNAMES_FOUND,
|
||||
{
|
||||
userFriendlyMessage: `${customHostnames.result.length} hostnames found for domain '${hostname}'. Expect 1`,
|
||||
userFriendlyMessage: msg`${hostnameCount} hostnames found for domain '${domainName}'. Expect 1`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import type Cloudflare from 'cloudflare';
|
||||
|
||||
@@ -15,7 +15,7 @@ const isCloudflareInstanceDefined = (
|
||||
'Cloudflare instance is not defined',
|
||||
DnsManagerExceptionCode.CLOUDFLARE_CLIENT_NOT_INITIALIZED,
|
||||
{
|
||||
userFriendlyMessage: t`Environment variable CLOUDFLARE_API_KEY must be defined to use this feature.`,
|
||||
userFriendlyMessage: msg`Environment variable CLOUDFLARE_API_KEY must be defined to use this feature.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { Catch, type ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
EmailVerificationException,
|
||||
@@ -19,7 +19,7 @@ export class EmailVerificationExceptionFilter implements ExceptionFilter {
|
||||
case EmailVerificationExceptionCode.TOKEN_EXPIRED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Request has expired, please try again.`,
|
||||
userFriendlyMessage: msg`Request has expired, please try again.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.INVALID_TOKEN:
|
||||
case EmailVerificationExceptionCode.INVALID_APP_TOKEN_TYPE:
|
||||
@@ -30,12 +30,12 @@ export class EmailVerificationExceptionFilter implements ExceptionFilter {
|
||||
case EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Email already verified.`,
|
||||
userFriendlyMessage: msg`Email already verified.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.EMAIL_VERIFICATION_NOT_REQUIRED:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Email verification not required.`,
|
||||
userFriendlyMessage: msg`Email verification not required.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.INVALID_EMAIL:
|
||||
throw new UserInputError(exception);
|
||||
|
||||
+15
-6
@@ -4,7 +4,7 @@ import {
|
||||
type OnExecuteDoneHookResultOnNextHook,
|
||||
type Plugin,
|
||||
} from '@envelop/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
GraphQLError,
|
||||
Kind,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
print,
|
||||
} from 'graphql';
|
||||
import semver from 'semver';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
convertGraphQLErrorToBaseGraphQLError,
|
||||
ErrorCode,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { type I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -46,6 +48,8 @@ type GraphQLErrorHandlerHookOptions = {
|
||||
*/
|
||||
exceptionHandlerService: ExceptionHandlerService;
|
||||
|
||||
i18nService: I18nService;
|
||||
|
||||
twentyConfigService: TwentyConfigService;
|
||||
/**
|
||||
* The key of the event id in the error's extension. `null` to disable.
|
||||
@@ -201,6 +205,10 @@ export const useGraphQLErrorHandlerHook = <
|
||||
}
|
||||
|
||||
// Step 3: Transform errors for GraphQL response (clean GraphQL errors)
|
||||
const userLocale = args.contextValue.req.locale ?? SOURCE_LOCALE;
|
||||
const i18n = options.i18nService.getI18nInstance(userLocale);
|
||||
const defaultErrorMessage = msg`An error occurred.`;
|
||||
|
||||
const transformedErrors = processedErrors.map((error) => {
|
||||
const graphqlError =
|
||||
error instanceof BaseGraphQLError
|
||||
@@ -208,12 +216,13 @@ export const useGraphQLErrorHandlerHook = <
|
||||
...error,
|
||||
extensions: {
|
||||
...error.extensions,
|
||||
userFriendlyMessage:
|
||||
userFriendlyMessage: i18n._(
|
||||
error.extensions.userFriendlyMessage ??
|
||||
t`An error occurred.`,
|
||||
defaultErrorMessage,
|
||||
),
|
||||
},
|
||||
}
|
||||
: generateGraphQLErrorFromError(error);
|
||||
: generateGraphQLErrorFromError(error, i18n);
|
||||
|
||||
if (error.eventId && eventIdKey) {
|
||||
graphqlError.extensions = {
|
||||
@@ -260,7 +269,7 @@ export const useGraphQLErrorHandlerHook = <
|
||||
});
|
||||
throw new GraphQLError(SCHEMA_MISMATCH_ERROR, {
|
||||
extensions: {
|
||||
userFriendlyMessage: t`Your workspace has been updated with a new data model. Please refresh the page.`,
|
||||
userFriendlyMessage: msg`Your workspace has been updated with a new data model. Please refresh the page.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -288,7 +297,7 @@ export const useGraphQLErrorHandlerHook = <
|
||||
throw new GraphQLError(APP_VERSION_MISMATCH_ERROR, {
|
||||
extensions: {
|
||||
code: APP_VERSION_MISMATCH_CODE,
|
||||
userFriendlyMessage: t`Your app version is out of date. Please refresh the page to continue.`,
|
||||
userFriendlyMessage: msg`Your app version is out of date. Please refresh the page to continue.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,4 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type I18n } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
@@ -8,17 +9,21 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export const generateGraphQLErrorFromError = (
|
||||
error: Error | CustomException,
|
||||
i18n: I18n,
|
||||
) => {
|
||||
const graphqlError = new BaseGraphQLError(
|
||||
error.message,
|
||||
ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
|
||||
const defaultErrorMessage = msg`An error occurred.`;
|
||||
|
||||
if (error instanceof CustomException) {
|
||||
graphqlError.extensions.userFriendlyMessage =
|
||||
error.userFriendlyMessage ?? t`An error occurred.`;
|
||||
graphqlError.extensions.userFriendlyMessage = i18n._(
|
||||
error.userFriendlyMessage ?? defaultErrorMessage,
|
||||
);
|
||||
} else {
|
||||
graphqlError.extensions.userFriendlyMessage = t`An error occurred.`;
|
||||
graphqlError.extensions.userFriendlyMessage = i18n._(defaultErrorMessage);
|
||||
}
|
||||
|
||||
return graphqlError;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import {
|
||||
type ASTNode,
|
||||
GraphQLError,
|
||||
@@ -33,7 +34,7 @@ export enum ErrorCode {
|
||||
}
|
||||
|
||||
type RestrictedGraphQLErrorExtensions = {
|
||||
userFriendlyMessage?: string;
|
||||
userFriendlyMessage?: MessageDescriptor;
|
||||
subCode?: string;
|
||||
};
|
||||
|
||||
|
||||
+4
-6
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
@@ -20,8 +21,7 @@ export class ImapSmtpCaldavValidatorService {
|
||||
): ConnectionParameters {
|
||||
if (!params) {
|
||||
throw new UserInputError('Protocol connection parameters are required', {
|
||||
userFriendlyMessage:
|
||||
'Please provide connection details to configure your email account.',
|
||||
userFriendlyMessage: msg`Please provide connection details to configure your email account.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,15 +36,13 @@ export class ImapSmtpCaldavValidatorService {
|
||||
throw new UserInputError(
|
||||
`Protocol connection validation failed: ${errorMessages}`,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Please check your connection settings. Make sure the server host, port, and password are correct.',
|
||||
userFriendlyMessage: msg`Please check your connection settings. Make sure the server host, port, and password are correct.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new UserInputError('Protocol connection validation failed', {
|
||||
userFriendlyMessage:
|
||||
'There was an issue with your connection settings. Please try again.',
|
||||
userFriendlyMessage: msg`There was an issue with your connection settings. Please try again.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+8
-14
@@ -1,5 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import { createTransport } from 'nodemailer';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
@@ -59,8 +60,7 @@ export class ImapSmtpCaldavService {
|
||||
throw new UserInputError(
|
||||
'IMAP authentication failed. Please check your credentials.',
|
||||
{
|
||||
userFriendlyMessage:
|
||||
"We couldn't log in to your email account. Please check your email address and password, then try again.",
|
||||
userFriendlyMessage: msg`We couldn't log in to your email account. Please check your email address and password, then try again.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -69,15 +69,13 @@ export class ImapSmtpCaldavService {
|
||||
throw new UserInputError(
|
||||
`IMAP connection refused. Please verify server and port.`,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
"We couldn't connect to your email server. Please check your server settings and try again.",
|
||||
userFriendlyMessage: msg`We couldn't connect to your email server. Please check your server settings and try again.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new UserInputError(`IMAP connection failed: ${error.message}`, {
|
||||
userFriendlyMessage:
|
||||
'We encountered an issue connecting to your email account. Please check your settings and try again.',
|
||||
userFriendlyMessage: msg`We encountered an issue connecting to your email account. Please check your settings and try again.`,
|
||||
});
|
||||
} finally {
|
||||
if (client.authenticated) {
|
||||
@@ -110,8 +108,7 @@ export class ImapSmtpCaldavService {
|
||||
error.stack,
|
||||
);
|
||||
throw new UserInputError(`SMTP connection failed: ${error.message}`, {
|
||||
userFriendlyMessage:
|
||||
"We couldn't connect to your outgoing email server. Please check your SMTP settings and try again.",
|
||||
userFriendlyMessage: msg`We couldn't connect to your outgoing email server. Please check your SMTP settings and try again.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,14 +134,12 @@ export class ImapSmtpCaldavService {
|
||||
);
|
||||
if (error.code === 'FailedToOpenSocket') {
|
||||
throw new UserInputError(`CALDAV connection failed: ${error.message}`, {
|
||||
userFriendlyMessage:
|
||||
"We couldn't connect to your CalDAV server. Please check your server settings and try again.",
|
||||
userFriendlyMessage: msg`We couldn't connect to your CalDAV server. Please check your server settings and try again.`,
|
||||
});
|
||||
}
|
||||
|
||||
throw new UserInputError(`CALDAV connection failed: ${error.message}`, {
|
||||
userFriendlyMessage:
|
||||
'Invalid credentials. Please check your username and password.',
|
||||
userFriendlyMessage: msg`Invalid credentials. Please check your username and password.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,8 +166,7 @@ export class ImapSmtpCaldavService {
|
||||
throw new UserInputError(
|
||||
'Invalid account type. Must be one of: IMAP, SMTP, CALDAV',
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Please select a valid connection type (IMAP, SMTP, or CalDAV) and try again.',
|
||||
userFriendlyMessage: msg`Please select a valid connection type (IMAP, SMTP, or CalDAV) and try again.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Repository } from 'typeorm';
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
@@ -62,7 +62,7 @@ export class PublicDomainService {
|
||||
'Domain already used for workspace custom domain',
|
||||
PublicDomainExceptionCode.DOMAIN_ALREADY_REGISTERED_AS_CUSTOM_DOMAIN,
|
||||
{
|
||||
userFriendlyMessage: t`Domain already used for workspace custom domain`,
|
||||
userFriendlyMessage: msg`Domain already used for workspace custom domain`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export class PublicDomainService {
|
||||
'Public domain already registered',
|
||||
PublicDomainExceptionCode.PUBLIC_DOMAIN_ALREADY_REGISTERED,
|
||||
{
|
||||
userFriendlyMessage: t`Public domain already registered`,
|
||||
userFriendlyMessage: msg`Public domain already registered`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
type CountryCallingCode,
|
||||
@@ -44,7 +44,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
throw new RecordTransformerException(
|
||||
`Invalid country code ${countryCode}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_COUNTRY_CODE,
|
||||
{ userFriendlyMessage: t`Invalid country code ${countryCode}` },
|
||||
{ userFriendlyMessage: msg`Invalid country code ${countryCode}` },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
throw new RecordTransformerException(
|
||||
`Invalid calling code ${callingCode}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE,
|
||||
{ userFriendlyMessage: t`Invalid calling code ${callingCode}` },
|
||||
{ userFriendlyMessage: msg`Invalid calling code ${callingCode}` },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
`Provided country code and calling code are conflicting`,
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE,
|
||||
{
|
||||
userFriendlyMessage: t`Provided country code and calling code are conflicting`,
|
||||
userFriendlyMessage: msg`Provided country code and calling code are conflicting`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -94,7 +94,7 @@ const parsePhoneNumberExceptionWrapper = ({
|
||||
throw new RecordTransformerException(
|
||||
`Provided phone number is invalid ${number}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_NUMBER,
|
||||
{ userFriendlyMessage: t`Provided phone number is invalid ${number}` },
|
||||
{ userFriendlyMessage: msg`Provided phone number is invalid ${number}` },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -119,7 +119,7 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({
|
||||
'Provided and inferred country code are conflicting',
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_COUNTRY_CODE,
|
||||
{
|
||||
userFriendlyMessage: t`Provided and inferred country code are conflicting`,
|
||||
userFriendlyMessage: msg`Provided and inferred country code are conflicting`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({
|
||||
'Provided and inferred calling code are conflicting',
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE,
|
||||
{
|
||||
userFriendlyMessage: t`Provided and inferred calling code are conflicting`,
|
||||
userFriendlyMessage: msg`Provided and inferred calling code are conflicting`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+5
-2
@@ -37,8 +37,11 @@ describe('TwoFactorAuthenticationExceptionFilter', () => {
|
||||
expect(error.extensions.subCode).toBe(
|
||||
TwoFactorAuthenticationExceptionCode.INVALID_OTP,
|
||||
);
|
||||
expect(error.extensions.userFriendlyMessage).toBe(
|
||||
'Invalid verification code. Please try again.',
|
||||
expect(error.extensions.userFriendlyMessage).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
message: 'Invalid verification code. Please try again.',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Catch, type ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
@@ -19,7 +19,7 @@ export class TwoFactorAuthenticationExceptionFilter implements ExceptionFilter {
|
||||
case TwoFactorAuthenticationExceptionCode.INVALID_OTP:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Invalid verification code. Please try again.`,
|
||||
userFriendlyMessage: msg`Invalid verification code. Please try again.`,
|
||||
});
|
||||
case TwoFactorAuthenticationExceptionCode.INVALID_CONFIGURATION:
|
||||
case TwoFactorAuthenticationExceptionCode.TWO_FACTOR_AUTHENTICATION_METHOD_NOT_FOUND:
|
||||
|
||||
@@ -2,6 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
@@ -124,8 +125,7 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
PermissionsExceptionMessage.CANNOT_DELETE_LAST_ADMIN_USER,
|
||||
PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'Cannot delete account: you are the only admin. Assign another admin or delete the workspace(s) first.',
|
||||
userFriendlyMessage: msg`Cannot delete account: you are the only admin. Assign another admin or delete the workspace(s) first.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 { ArrayContains, IsNull, Repository } from 'typeorm';
|
||||
import { type QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
@@ -78,7 +79,7 @@ export class WebhookService {
|
||||
throw new WebhookException(
|
||||
'Invalid target URL provided',
|
||||
WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
{ userFriendlyMessage: 'Please provide a valid HTTP or HTTPS URL.' },
|
||||
{ userFriendlyMessage: msg`Please provide a valid HTTP or HTTPS URL.` },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +112,9 @@ export class WebhookService {
|
||||
throw new WebhookException(
|
||||
'Invalid target URL provided',
|
||||
WebhookExceptionCode.INVALID_TARGET_URL,
|
||||
{ userFriendlyMessage: 'Please provide a valid HTTP or HTTPS URL.' },
|
||||
{
|
||||
userFriendlyMessage: msg`Please provide a valid HTTP or HTTPS URL.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+4
-6
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
@@ -136,7 +136,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
'Domain is already registered as public domain',
|
||||
WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN,
|
||||
{
|
||||
userFriendlyMessage: t`Domain is already registered as public domain`,
|
||||
userFriendlyMessage: msg`Domain is already registered as public domain`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -452,8 +452,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'You do not have permission to manage security settings. Please contact your workspace administrator.',
|
||||
userFriendlyMessage: msg`You do not have permission to manage security settings. Please contact your workspace administrator.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -502,8 +501,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'You do not have permission to manage workspace settings. Please contact your workspace administrator.',
|
||||
userFriendlyMessage: msg`You do not have permission to manage workspace settings. Please contact your workspace administrator.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+9
-3
@@ -109,8 +109,11 @@ describe('ImpersonateGuard', () => {
|
||||
|
||||
await expect(guard.canActivate(mockExecutionContext)).rejects.toMatchObject(
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'You do not have permission to impersonate users. Please contact your workspace administrator for access.',
|
||||
userFriendlyMessage: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
message:
|
||||
'You do not have permission to impersonate users. Please contact your workspace administrator for access.',
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -137,7 +140,10 @@ describe('ImpersonateGuard', () => {
|
||||
|
||||
await expect(guard.canActivate(mockExecutionContext)).rejects.toMatchObject(
|
||||
{
|
||||
userFriendlyMessage: "Can't impersonate user via api key",
|
||||
userFriendlyMessage: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
message: "Can't impersonate user via api key",
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'class-validator';
|
||||
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
@@ -30,7 +31,7 @@ export class ImpersonatePermissionGuard implements CanActivate {
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage: "Can't impersonate user via api key",
|
||||
userFriendlyMessage: msg`Can't impersonate user via api key`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -50,8 +51,7 @@ export class ImpersonatePermissionGuard implements CanActivate {
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'You do not have permission to impersonate users. Please contact your workspace administrator for access.',
|
||||
userFriendlyMessage: msg`You do not have permission to impersonate users. Please contact your workspace administrator for access.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
|
||||
import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
@@ -56,8 +57,7 @@ export const SettingsPermissionsGuard = (
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'You do not have permission to access this feature. Please contact your workspace administrator for access.',
|
||||
userFriendlyMessage: msg`You do not have permission to access this feature. Please contact your workspace administrator for access.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+30
-8
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-19
@@ -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`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+2
-2
@@ -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`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+4
-4
@@ -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`,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+6
-2
@@ -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) {
|
||||
|
||||
+24
-6
@@ -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",
|
||||
|
||||
+3
-3
@@ -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}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+3
-1
@@ -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;
|
||||
};
|
||||
|
||||
+2
-2
@@ -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`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+2
-2
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+3
-3
@@ -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}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+7
-7
@@ -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,
|
||||
},
|
||||
|
||||
+3
-3
@@ -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`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
+4
-4
@@ -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`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -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),
|
||||
|
||||
+3
-3
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+3
-1
@@ -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;
|
||||
};
|
||||
|
||||
+4
-4
@@ -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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
+3
-3
@@ -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`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
+3
-1
@@ -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 });
|
||||
}
|
||||
|
||||
+9
-2
@@ -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 [];
|
||||
}
|
||||
|
||||
+2
-2
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+18
-2
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-6
@@ -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 [];
|
||||
}
|
||||
|
||||
+2
-1
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+6
-2
@@ -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) {
|
||||
|
||||
+2
-1
@@ -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`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+11
-15
@@ -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}).`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+8
-14
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+4
-6
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+5
-8
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+3
-2
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -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}"`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+8
-7
@@ -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.`;
|
||||
}
|
||||
};
|
||||
|
||||
+4
-5
@@ -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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+7
-6
@@ -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.`;
|
||||
}
|
||||
};
|
||||
|
||||
+7
-6
@@ -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.`;
|
||||
}
|
||||
};
|
||||
|
||||
+7
-6
@@ -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.`;
|
||||
}
|
||||
};
|
||||
|
||||
+7
-6
@@ -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.`;
|
||||
}
|
||||
};
|
||||
|
||||
+6
-5
@@ -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.`;
|
||||
}
|
||||
};
|
||||
|
||||
+3
-2
@@ -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) {
|
||||
|
||||
+20
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
@@ -25,7 +25,7 @@ export const computeTwentyORMException = (
|
||||
'Query read timeout',
|
||||
TwentyORMExceptionCode.QUERY_READ_TIMEOUT,
|
||||
{
|
||||
userFriendlyMessage: t`We are experiencing a temporary issue with our database. Please try again later.`,
|
||||
userFriendlyMessage: msg`We are experiencing a temporary issue with our database. Please try again later.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+9
-2
@@ -14,8 +14,15 @@ describe('formatConnectRecordNotFoundErrorMessage', () => {
|
||||
expect(result).toEqual({
|
||||
errorMessage:
|
||||
'Expected 1 record to connect to connectFieldName, but found 0 for field1 = value1 and field2 = value2',
|
||||
userFriendlyMessage:
|
||||
"Can't connect to connectFieldName. No unique record found with condition: field1 = value1 and field2 = value2",
|
||||
userFriendlyMessage: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
message:
|
||||
"Can't connect to {connectFieldName}. No unique record found with condition: {formattedConnectCondition}",
|
||||
values: {
|
||||
connectFieldName: 'connectFieldName',
|
||||
formattedConnectCondition: 'field1 = value1 and field2 = value2',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { type UniqueConstraintCondition } from 'src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type';
|
||||
|
||||
@@ -13,6 +13,6 @@ export const formatConnectRecordNotFoundErrorMessage = (
|
||||
|
||||
return {
|
||||
errorMessage: `Expected 1 record to connect to ${connectFieldName}, but found ${recordToConnectTotal} for ${formattedConnectCondition}`,
|
||||
userFriendlyMessage: t`Can't connect to ${connectFieldName}. No unique record found with condition: ${formattedConnectCondition}`,
|
||||
userFriendlyMessage: msg`Can't connect to ${connectFieldName}. No unique record found with condition: ${formattedConnectCondition}`,
|
||||
};
|
||||
};
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import deepEqual from 'deep-equal';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils';
|
||||
@@ -142,7 +142,7 @@ const computeRecordToConnectCondition = (
|
||||
`Connect is not allowed for ${connectFieldName} on ${objectMetadata.nameSingular}`,
|
||||
TwentyORMExceptionCode.CONNECT_NOT_ALLOWED,
|
||||
{
|
||||
userFriendlyMessage: t`Connect is not allowed for ${connectFieldName} on ${objectMetadataNameSingular}`,
|
||||
userFriendlyMessage: msg`Connect is not allowed for ${connectFieldName} on ${objectMetadataNameSingular}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ const computeRecordToConnectCondition = (
|
||||
`Target object metadata not found for ${connectFieldName}`,
|
||||
TwentyORMExceptionCode.MALFORMED_METADATA,
|
||||
{
|
||||
userFriendlyMessage: t`Target object metadata not found for ${connectFieldName}`,
|
||||
userFriendlyMessage: msg`Target object metadata not found for ${connectFieldName}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -202,7 +202,7 @@ const checkUniqueConstraintFullyPopulated = (
|
||||
`Missing required fields: at least one unique constraint have to be fully populated for '${connectFieldName}'.`,
|
||||
TwentyORMExceptionCode.CONNECT_UNIQUE_CONSTRAINT_ERROR,
|
||||
{
|
||||
userFriendlyMessage: t`Missing required fields: at least one unique constraint have to be fully populated for '${connectFieldName}'.`,
|
||||
userFriendlyMessage: msg`Missing required fields: at least one unique constraint have to be fully populated for '${connectFieldName}'.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -235,7 +235,7 @@ const checkNoRelationFieldConflictOrThrow = (
|
||||
`${fieldName} and ${fieldName}Id cannot be both provided.`,
|
||||
TwentyORMExceptionCode.CONNECT_NOT_ALLOWED,
|
||||
{
|
||||
userFriendlyMessage: t`${fieldName} and ${fieldName}Id cannot be both provided.`,
|
||||
userFriendlyMessage: msg`${fieldName} and ${fieldName}Id cannot be both provided.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -284,7 +284,7 @@ const checkUniqueConstraintsAreSameOrThrow = (
|
||||
`Expected the same constraint fields to be used consistently across all operations for ${relationConnectQueryConfig.connectFieldName}.`,
|
||||
TwentyORMExceptionCode.CONNECT_UNIQUE_CONSTRAINT_ERROR,
|
||||
{
|
||||
userFriendlyMessage: t`Expected the same constraint fields to be used consistently across all operations for ${connectFieldName}.`,
|
||||
userFriendlyMessage: msg`Expected the same constraint fields to be used consistently across all operations for ${connectFieldName}.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type DataSource, type EntityManager } from 'typeorm';
|
||||
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
@@ -75,8 +76,7 @@ export class WorkspaceDataSourceService {
|
||||
'Method not allowed as permissions are not handled at datasource level.',
|
||||
PermissionsExceptionCode.METHOD_NOT_ALLOWED,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
'This operation is not allowed. Please try a different approach or contact support if you need assistance.',
|
||||
userFriendlyMessage: msg`This operation is not allowed. Please try a different approach or contact support if you need assistance.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DataSource, type QueryRunner, Table, type TableColumn } from 'typeorm';
|
||||
|
||||
@@ -257,7 +257,7 @@ export class WorkspaceMigrationRunnerService {
|
||||
`Unique index creation failed because of unique constraint violation`,
|
||||
IndexMetadataExceptionCode.INDEX_CREATION_FAILED,
|
||||
{
|
||||
userFriendlyMessage: t`Cannot enable uniqueness due to existing duplicate values. Please review and fix your data first (including soft deleted records).`,
|
||||
userFriendlyMessage: msg`Cannot enable uniqueness due to existing duplicate values. Please review and fix your data first (including soft deleted records).`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,21 +1,20 @@
|
||||
import { type AllFlatEntitiesByMetadataEngineName } from 'src/engine/core-modules/common/types/all-flat-entities-by-metadata-engine-name.type';
|
||||
import { type FieldMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-field-metadata/types/field-metadata-minimal-information.type';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
|
||||
import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type';
|
||||
import { type OrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
|
||||
import { type TranslatedFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/translate-validation-errors.util';
|
||||
import { type WorkspaceMigrationFieldActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
|
||||
import { type WorkspaceMigrationObjectActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
|
||||
|
||||
export type ValidationErrorFieldResponse =
|
||||
Partial<FieldMetadataMinimalInformation> & {
|
||||
operation: WorkspaceMigrationFieldActionTypeV2;
|
||||
errors: FlatFieldMetadataValidationError[];
|
||||
errors: TranslatedFlatEntityValidationError[];
|
||||
};
|
||||
export type ValidationErrorObjectResponse =
|
||||
Partial<ObjectMetadataMinimalInformation> & {
|
||||
operation: WorkspaceMigrationObjectActionTypeV2;
|
||||
errors: FlatObjectMetadataValidationError[];
|
||||
errors: TranslatedFlatEntityValidationError[];
|
||||
fields: ValidationErrorFieldResponse[];
|
||||
};
|
||||
|
||||
|
||||
+12
-3
@@ -1,8 +1,12 @@
|
||||
import { type I18n } from '@lingui/core';
|
||||
|
||||
import { type WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
|
||||
import { type ValidationErrorResponse } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/types/validate-error-response.type';
|
||||
import { translateOrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/utils/translate-validation-errors.util';
|
||||
|
||||
export const fromWorkspaceMigrationBuilderExceptionToValidationResponseError = (
|
||||
workspaceMigrationBuilderException: WorkspaceMigrationBuilderExceptionV2,
|
||||
i18n: I18n,
|
||||
): ValidationErrorResponse => {
|
||||
const emptyResponseError: ValidationErrorResponse = {
|
||||
summary: {
|
||||
@@ -30,10 +34,15 @@ export const fromWorkspaceMigrationBuilderExceptionToValidationResponseError = (
|
||||
},
|
||||
};
|
||||
|
||||
const report =
|
||||
workspaceMigrationBuilderException.failedWorkspaceMigrationBuildResult
|
||||
.report;
|
||||
|
||||
// Translate all MessageDescriptors in the report structure
|
||||
const translatedReport = translateOrchestratorFailureReport(report, i18n);
|
||||
|
||||
return {
|
||||
...emptyResponseError,
|
||||
errors:
|
||||
workspaceMigrationBuilderException.failedWorkspaceMigrationBuildResult
|
||||
.report,
|
||||
errors: translatedReport,
|
||||
};
|
||||
};
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { type I18n, type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
import { type OrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
|
||||
|
||||
export type TranslatedFlatEntityValidationError<TCode extends string = string> =
|
||||
{
|
||||
code: TCode;
|
||||
message: string;
|
||||
userFriendlyMessage?: string;
|
||||
value?: unknown;
|
||||
};
|
||||
|
||||
const translateMessageDescriptorsInObject = <T>(obj: T, i18n: I18n): T => {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof obj === 'object' &&
|
||||
'id' in obj &&
|
||||
'message' in obj &&
|
||||
typeof (obj as MessageDescriptor).id === 'string'
|
||||
) {
|
||||
return i18n._(obj as MessageDescriptor) as T;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) =>
|
||||
translateMessageDescriptorsInObject(item, i18n),
|
||||
) as T;
|
||||
}
|
||||
|
||||
const result = {} as T;
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
(result as Record<string, unknown>)[key] =
|
||||
translateMessageDescriptorsInObject(value, i18n);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const translateOrchestratorFailureReport = (
|
||||
report: OrchestratorFailureReport,
|
||||
i18n: I18n,
|
||||
): OrchestratorFailureReport => {
|
||||
return translateMessageDescriptorsInObject(report, i18n);
|
||||
};
|
||||
+8
-3
@@ -1,4 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type I18n } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
@@ -9,15 +10,19 @@ import { fromWorkspaceMigrationBuilderExceptionToValidationResponseError } from
|
||||
|
||||
export const workspaceMigrationBuilderExceptionV2Formatter = (
|
||||
error: WorkspaceMigrationBuilderExceptionV2,
|
||||
i18n: I18n,
|
||||
) => {
|
||||
const { errors, summary } =
|
||||
fromWorkspaceMigrationBuilderExceptionToValidationResponseError(error);
|
||||
fromWorkspaceMigrationBuilderExceptionToValidationResponseError(
|
||||
error,
|
||||
i18n,
|
||||
);
|
||||
|
||||
throw new BaseGraphQLError(error.message, ErrorCode.BAD_USER_INPUT, {
|
||||
code: 'METADATA_VALIDATION_ERROR',
|
||||
errors,
|
||||
summary,
|
||||
message: `Validation failed for 0 object(s) and 0 field(s)`,
|
||||
userFriendlyMessage: t`Validation failed for 0 object(s) and 0 field(s)`,
|
||||
userFriendlyMessage: msg`Validation failed for 0 object(s) and 0 field(s)`,
|
||||
});
|
||||
};
|
||||
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type';
|
||||
import { type WorkspaceMigrationActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
|
||||
|
||||
export type FlatEntityValidationError<TCode extends string = string> = {
|
||||
code: TCode; // should be a better extends
|
||||
message: string;
|
||||
userFriendlyMessage?: string;
|
||||
userFriendlyMessage?: MessageDescriptor;
|
||||
value?: unknown;
|
||||
};
|
||||
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { t, msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
|
||||
@@ -33,7 +33,7 @@ export class FlatCronTriggerValidatorService {
|
||||
errors.push({
|
||||
code: CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND,
|
||||
message: t`Cron trigger not found`,
|
||||
userFriendlyMessage: t`Cron trigger not found`,
|
||||
userFriendlyMessage: msg`Cron trigger not found`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export class FlatCronTriggerValidatorService {
|
||||
errors.push({
|
||||
code: CronTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
message: t`Serverless function not found`,
|
||||
userFriendlyMessage: t`Serverless function not found`,
|
||||
userFriendlyMessage: msg`Serverless function not found`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export class FlatCronTriggerValidatorService {
|
||||
errors.push({
|
||||
code: CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND,
|
||||
message: t`Cron trigger not found`,
|
||||
userFriendlyMessage: t`Cron trigger not found`,
|
||||
userFriendlyMessage: msg`Cron trigger not found`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export class FlatCronTriggerValidatorService {
|
||||
errors.push({
|
||||
code: CronTriggerExceptionCode.CRON_TRIGGER_ALREADY_EXIST,
|
||||
message: t`Cron trigger with same id already exists`,
|
||||
userFriendlyMessage: t`Cron trigger already exists`,
|
||||
userFriendlyMessage: msg`Cron trigger already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export class FlatCronTriggerValidatorService {
|
||||
errors.push({
|
||||
code: CronTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
message: t`Serverless function not found`,
|
||||
userFriendlyMessage: t`Serverless function not found`,
|
||||
userFriendlyMessage: msg`Serverless function not found`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { t, msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type';
|
||||
@@ -35,7 +35,7 @@ export class FlatDatabaseEventTriggerValidatorService {
|
||||
errors.push({
|
||||
code: DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND,
|
||||
message: t`Database event trigger not found`,
|
||||
userFriendlyMessage: t`Database event trigger not found`,
|
||||
userFriendlyMessage: msg`Database event trigger not found`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class FlatDatabaseEventTriggerValidatorService {
|
||||
errors.push({
|
||||
code: DatabaseEventTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
message: t`Serverless function not found`,
|
||||
userFriendlyMessage: t`Serverless function not found`,
|
||||
userFriendlyMessage: msg`Serverless function not found`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class FlatDatabaseEventTriggerValidatorService {
|
||||
errors.push({
|
||||
code: DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND,
|
||||
message: t`Database event trigger not found`,
|
||||
userFriendlyMessage: t`Database event trigger not found`,
|
||||
userFriendlyMessage: msg`Database event trigger not found`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export class FlatDatabaseEventTriggerValidatorService {
|
||||
errors.push({
|
||||
code: DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_ALREADY_EXIST,
|
||||
message: t`Database event trigger with same id already exists`,
|
||||
userFriendlyMessage: t`Database event trigger already exists`,
|
||||
userFriendlyMessage: msg`Database event trigger already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export class FlatDatabaseEventTriggerValidatorService {
|
||||
errors.push({
|
||||
code: DatabaseEventTriggerExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
message: t`Serverless function not found`,
|
||||
userFriendlyMessage: t`Serverless function not found`,
|
||||
userFriendlyMessage: msg`Serverless function not found`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user