Define server error messages to display in FE from the server (#12973)
Currently, when a server query or mutation from the front-end fails, the error message defined server-side is displayed in a snackbar in the front-end. These error messages usually contain technical details that don't belong to the user interface, such as "ObjectMetadataCollection not found" or "invalid ENUM value for ...". **BE** In addition to the original error message that is still needed (for the request response, debugging, sentry monitoring etc.), we add a `displayedErrorMessage` that will be used in the snackbars. It's only relevant to add it for the messages that will reach the FE (ie. not in jobs or in rest api for instance) and if it can help the user sort out / fix things (ie. we do add displayedErrorMessage for "Cannot create multiple draft versions for the same workflow" or "Cannot delete [field], please update the label identifier field first", but not "Object metadata does not exist"), even if in practice in the FE users should not be able to perform an action that will not work (ie should not be able to save creation of multiple draft versions of the same workflows). **FE** To ease the usage we replaced enqueueSnackBar with enqueueErrorSnackBar and enqueueSuccessSnackBar with an api that only requires to pass on the error. If no displayedErrorMessage is specified then the default error message is `An error occured.`
This commit is contained in:
@ -17,7 +17,11 @@ export class ApprovedAccessDomainExceptionFilter implements ExceptionFilter {
|
||||
case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_VALIDATION_TOKEN_INVALID:
|
||||
case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED:
|
||||
case ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_MUST_BE_A_COMPANY_DOMAIN:
|
||||
throw new ForbiddenError(exception.message);
|
||||
throw new ForbiddenError(exception.message, {
|
||||
extensions: {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
},
|
||||
});
|
||||
default: {
|
||||
const _exhaustiveCheck: never = exception.code;
|
||||
|
||||
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class ApprovedAccessDomainException extends CustomException {
|
||||
declare code: ApprovedAccessDomainExceptionCode;
|
||||
constructor(message: string, code: ApprovedAccessDomainExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: ApprovedAccessDomainExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { SendApprovedAccessDomainValidation } from 'twenty-emails';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
@ -18,8 +19,8 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { isWorkDomain } from 'src/utils/is-work-email';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { isWorkDomain } from 'src/utils/is-work-email';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@ -42,6 +43,9 @@ export class ApprovedAccessDomainService {
|
||||
throw new ApprovedAccessDomainException(
|
||||
'Approved access domain has already been validated',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VERIFIED,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain has already been validated`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -49,6 +53,9 @@ export class ApprovedAccessDomainService {
|
||||
throw new ApprovedAccessDomainException(
|
||||
'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`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -118,6 +125,9 @@ export class ApprovedAccessDomainService {
|
||||
throw new ApprovedAccessDomainException(
|
||||
'Approved access domain has already been validated',
|
||||
ApprovedAccessDomainExceptionCode.APPROVED_ACCESS_DOMAIN_ALREADY_VALIDATED,
|
||||
{
|
||||
userFriendlyMessage: t`Approved access domain has already been validated`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class AuthException extends CustomException {
|
||||
declare code: AuthExceptionCode;
|
||||
constructor(message: string, code: AuthExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: AuthExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Catch, ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
@ -16,26 +18,39 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: AuthException) {
|
||||
switch (exception.code) {
|
||||
case AuthExceptionCode.CLIENT_NOT_FOUND:
|
||||
throw new NotFoundError(exception.message);
|
||||
throw new NotFoundError(exception.message, {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
});
|
||||
case AuthExceptionCode.INVALID_INPUT:
|
||||
throw new UserInputError(exception.message);
|
||||
throw new UserInputError(exception.message, {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
});
|
||||
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
|
||||
case AuthExceptionCode.INSUFFICIENT_SCOPES:
|
||||
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
|
||||
case AuthExceptionCode.SSO_AUTH_FAILED:
|
||||
case AuthExceptionCode.USE_SSO_AUTH:
|
||||
case AuthExceptionCode.SIGNUP_DISABLED:
|
||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE:
|
||||
case AuthExceptionCode.INVALID_JWT_TOKEN_TYPE:
|
||||
throw new ForbiddenError(exception.message);
|
||||
throw new ForbiddenError(exception.message, {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
});
|
||||
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.`,
|
||||
});
|
||||
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.`,
|
||||
});
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
throw new AuthenticationError(exception.message, {
|
||||
userFriendlyMessage: t`You must be authenticated to perform this action.`,
|
||||
});
|
||||
case AuthExceptionCode.USER_NOT_FOUND:
|
||||
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
|
||||
throw new AuthenticationError(exception.message);
|
||||
|
||||
@ -169,6 +169,9 @@ export class AuthService {
|
||||
throw new AuthException(
|
||||
'Wrong password',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Wrong password`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared/constants';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
@ -67,6 +68,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,6 +115,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Password too weak',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Password too weak`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -130,6 +137,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Wrong password',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Wrong password`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -153,6 +163,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -194,6 +207,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Workspace is not ready to welcome new members',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`Workspace is not ready to welcome new members`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -207,6 +223,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'User is not part of the workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: t`User is not part of the workspace`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -340,6 +359,9 @@ export class SignInUpService {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Email is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Catch, ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
EmailVerificationException,
|
||||
EmailVerificationExceptionCode,
|
||||
@ -13,17 +15,32 @@ import {
|
||||
export class EmailVerificationExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: EmailVerificationException) {
|
||||
switch (exception.code) {
|
||||
case EmailVerificationExceptionCode.TOKEN_EXPIRED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Request has expired, please try again.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.INVALID_TOKEN:
|
||||
case EmailVerificationExceptionCode.INVALID_APP_TOKEN_TYPE:
|
||||
case EmailVerificationExceptionCode.TOKEN_EXPIRED:
|
||||
case EmailVerificationExceptionCode.RATE_LIMIT_EXCEEDED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: exception.code,
|
||||
});
|
||||
case EmailVerificationExceptionCode.EMAIL_MISSING:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
});
|
||||
case EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED:
|
||||
case EmailVerificationExceptionCode.INVALID_EMAIL:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Email already verified.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.EMAIL_VERIFICATION_NOT_REQUIRED:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
userFriendlyMessage: t`Email verification not required.`,
|
||||
});
|
||||
case EmailVerificationExceptionCode.INVALID_EMAIL:
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
OnExecuteDoneHookResultOnNextHook,
|
||||
Plugin,
|
||||
} from '@envelop/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
||||
|
||||
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
|
||||
@ -24,8 +25,7 @@ import {
|
||||
|
||||
const DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
|
||||
const SCHEMA_VERSION_HEADER = 'x-schema-version';
|
||||
const SCHEMA_MISMATCH_ERROR =
|
||||
'Your workspace has been updated with a new data model. Please refresh the page.';
|
||||
const SCHEMA_MISMATCH_ERROR = 'Schema version mismatch.';
|
||||
|
||||
type GraphQLErrorHandlerHookOptions = {
|
||||
metricsService: MetricsService;
|
||||
@ -191,11 +191,22 @@ export const useGraphQLErrorHandlerHook = <
|
||||
const transformedErrors = processedErrors.map((error) => {
|
||||
const graphqlError =
|
||||
error instanceof BaseGraphQLError
|
||||
? error
|
||||
? {
|
||||
...error,
|
||||
extensions: {
|
||||
...error.extensions,
|
||||
userFriendlyMessage:
|
||||
error.extensions.userFriendlyMessage ??
|
||||
t`An error occurred.`,
|
||||
},
|
||||
}
|
||||
: generateGraphQLErrorFromError(error);
|
||||
|
||||
if (error.eventId && eventIdKey) {
|
||||
graphqlError.extensions[eventIdKey] = error.eventId;
|
||||
graphqlError.extensions = {
|
||||
...graphqlError.extensions,
|
||||
[eventIdKey]: error.eventId,
|
||||
};
|
||||
}
|
||||
|
||||
return graphqlError;
|
||||
@ -224,7 +235,11 @@ export const useGraphQLErrorHandlerHook = <
|
||||
requestMetadataVersion &&
|
||||
requestMetadataVersion !== `${currentMetadataVersion}`
|
||||
) {
|
||||
throw new GraphQLError(SCHEMA_MISMATCH_ERROR);
|
||||
throw new GraphQLError(SCHEMA_MISMATCH_ERROR, {
|
||||
extensions: {
|
||||
userFriendlyMessage: t`Your workspace has been updated with a new data model. Please refresh the page.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,13 +1,25 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
ErrorCode,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export const generateGraphQLErrorFromError = (error: Error) => {
|
||||
export const generateGraphQLErrorFromError = (
|
||||
error: Error | CustomException,
|
||||
) => {
|
||||
const graphqlError = new BaseGraphQLError(
|
||||
error.message,
|
||||
ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
|
||||
if (error instanceof CustomException) {
|
||||
graphqlError.extensions.userFriendlyMessage =
|
||||
error.userFriendlyMessage ?? t`An error occurred.`;
|
||||
} else {
|
||||
graphqlError.extensions.userFriendlyMessage = t`An error occurred.`;
|
||||
}
|
||||
|
||||
return graphqlError;
|
||||
};
|
||||
|
||||
@ -159,8 +159,9 @@ export class UserInputError extends BaseGraphQLError {
|
||||
}
|
||||
|
||||
export class NotFoundError extends BaseGraphQLError {
|
||||
constructor(message: string) {
|
||||
super(message, ErrorCode.NOT_FOUND);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(message: string, extensions?: Record<string, any>) {
|
||||
super(message, ErrorCode.NOT_FOUND, extensions);
|
||||
|
||||
Object.defineProperty(this, 'name', { value: 'NotFoundError' });
|
||||
}
|
||||
@ -175,8 +176,9 @@ export class MethodNotAllowedError extends BaseGraphQLError {
|
||||
}
|
||||
|
||||
export class ConflictError extends BaseGraphQLError {
|
||||
constructor(message: string) {
|
||||
super(message, ErrorCode.CONFLICT);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(message: string, extensions?: Record<string, any>) {
|
||||
super(message, ErrorCode.CONFLICT, extensions);
|
||||
|
||||
Object.defineProperty(this, 'name', { value: 'ConflictError' });
|
||||
}
|
||||
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class RecordTransformerException extends CustomException {
|
||||
declare code: RecordTransformerExceptionCode;
|
||||
constructor(message: string, code: RecordTransformerExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: RecordTransformerExceptionCode,
|
||||
userFriendlyMessage?: string,
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,9 @@ export const recordTransformerGraphqlApiExceptionHandler = (
|
||||
case RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE:
|
||||
case RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE:
|
||||
case RecordTransformerExceptionCode.INVALID_URL:
|
||||
throw new UserInputError(error.message);
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
default: {
|
||||
assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
CountryCallingCode,
|
||||
@ -61,6 +62,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
throw new RecordTransformerException(
|
||||
`Invalid country code ${countryCode}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_COUNTRY_CODE,
|
||||
t`Invalid country code ${countryCode}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -74,6 +76,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
throw new RecordTransformerException(
|
||||
`Invalid calling code ${callingCode}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_CALLING_CODE,
|
||||
t`Invalid calling code ${callingCode}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -86,6 +89,7 @@ const validatePrimaryPhoneCountryCodeAndCallingCode = ({
|
||||
throw new RecordTransformerException(
|
||||
`Provided country code and calling code are conflicting`,
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE_AND_COUNTRY_CODE,
|
||||
t`Provided country code and calling code are conflicting`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -106,6 +110,7 @@ const parsePhoneNumberExceptionWrapper = ({
|
||||
throw new RecordTransformerException(
|
||||
`Provided phone number is invalid ${number}`,
|
||||
RecordTransformerExceptionCode.INVALID_PHONE_NUMBER,
|
||||
t`Provided phone number is invalid ${number}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -129,6 +134,7 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({
|
||||
throw new RecordTransformerException(
|
||||
'Provided and inferred country code are conflicting',
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_COUNTRY_CODE,
|
||||
t`Provided and inferred country code are conflicting`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -140,6 +146,7 @@ const validateAndInferMetadataFromPrimaryPhoneNumber = ({
|
||||
throw new RecordTransformerException(
|
||||
'Provided and inferred calling code are conflicting',
|
||||
RecordTransformerExceptionCode.CONFLICTING_PHONE_CALLING_CODE,
|
||||
t`Provided and inferred calling code are conflicting`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -19,9 +19,17 @@ export const handleWorkflowTriggerException = (
|
||||
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER:
|
||||
case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS:
|
||||
case WorkflowTriggerExceptionCode.FORBIDDEN:
|
||||
throw new UserInputError(exception.message);
|
||||
throw new UserInputError(exception.message, {
|
||||
extensions: {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
},
|
||||
});
|
||||
case WorkflowTriggerExceptionCode.NOT_FOUND:
|
||||
throw new NotFoundError(exception.message);
|
||||
throw new NotFoundError(exception.message, {
|
||||
extensions: {
|
||||
userFriendlyMessage: exception.userFriendlyMessage,
|
||||
},
|
||||
});
|
||||
case WorkflowTriggerExceptionCode.INTERNAL_ERROR:
|
||||
throw exception;
|
||||
default: {
|
||||
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class FieldMetadataException extends CustomException {
|
||||
declare code: FieldMetadataExceptionCode;
|
||||
constructor(message: string, code: FieldMetadataExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: FieldMetadataExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
@ -363,6 +364,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
throw new FieldMetadataException(
|
||||
'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`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -574,6 +578,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
throw new FieldMetadataException(
|
||||
`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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
@ -26,7 +27,10 @@ import {
|
||||
import { EnumFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
|
||||
import { isSnakeCaseString } from 'src/utils/is-snake-case-string';
|
||||
|
||||
type Validator<T> = { validator: (str: T) => boolean; message: string };
|
||||
type Validator<T> = {
|
||||
validator: (str: T) => boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type FieldMetadataUpdateCreateInput = CreateFieldInput | UpdateFieldInput;
|
||||
|
||||
@ -55,6 +59,9 @@ export class FieldMetadataEnumValidationService {
|
||||
throw new FieldMetadataException(
|
||||
message,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
{
|
||||
userFriendlyMessage: message,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -80,23 +87,23 @@ export class FieldMetadataEnumValidationService {
|
||||
const validators: Validator<string>[] = [
|
||||
{
|
||||
validator: (label) => !isDefined(label),
|
||||
message: 'Option label is required',
|
||||
message: t`Option label is required`,
|
||||
},
|
||||
{
|
||||
validator: exceedsDatabaseIdentifierMaximumLength,
|
||||
message: `Option label "${sanitizedLabel}" exceeds 63 characters`,
|
||||
message: t`Option label exceeds 63 characters`,
|
||||
},
|
||||
{
|
||||
validator: beneathDatabaseIdentifierMinimumLength,
|
||||
message: `Option label "${sanitizedLabel}" is beneath 1 character`,
|
||||
message: t`Option label "${sanitizedLabel}" is beneath 1 character`,
|
||||
},
|
||||
{
|
||||
validator: (label) => label.includes(','),
|
||||
message: 'Label must not contain a comma',
|
||||
message: t`Label must not contain a comma`,
|
||||
},
|
||||
{
|
||||
validator: (label) => !isNonEmptyString(label) || label === ' ',
|
||||
message: 'Label must not be empty',
|
||||
message: t`Label must not be empty`,
|
||||
},
|
||||
];
|
||||
|
||||
@ -109,15 +116,15 @@ export class FieldMetadataEnumValidationService {
|
||||
const validators: Validator<string>[] = [
|
||||
{
|
||||
validator: (value) => !isDefined(value),
|
||||
message: 'Option value is required',
|
||||
message: t`Option value is required`,
|
||||
},
|
||||
{
|
||||
validator: exceedsDatabaseIdentifierMaximumLength,
|
||||
message: `Option value "${sanitizedValue}" exceeds 63 characters`,
|
||||
message: t`Option value exceeds 63 characters`,
|
||||
},
|
||||
{
|
||||
validator: beneathDatabaseIdentifierMinimumLength,
|
||||
message: `Option value "${sanitizedValue}" is beneath 1 character`,
|
||||
message: t`Option value "${sanitizedValue}" is beneath 1 character`,
|
||||
},
|
||||
{
|
||||
validator: (value) => !isSnakeCaseString(value),
|
||||
|
||||
@ -18,13 +18,21 @@ export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof FieldMetadataException) {
|
||||
switch (error.code) {
|
||||
case FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
throw new NotFoundError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case FieldMetadataExceptionCode.INVALID_FIELD_INPUT:
|
||||
throw new UserInputError(error.message);
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED:
|
||||
throw new ForbiddenError(error.message);
|
||||
throw new ForbiddenError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS:
|
||||
throw new ConflictError(error.message);
|
||||
throw new ConflictError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
case FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED:
|
||||
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class ObjectMetadataException extends CustomException {
|
||||
declare code: ObjectMetadataExceptionCode;
|
||||
constructor(message: string, code: ObjectMetadataExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: ObjectMetadataExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`validateObjectMetadataInputOrThrow should fail when name exceeds maximum length 1`] = `"String "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters limit"`;
|
||||
exports[`validateObjectMetadataInputOrThrow should fail when name exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`;
|
||||
|
||||
exports[`validateObjectMetadataInputOrThrow should fail when namePlural has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`;
|
||||
|
||||
|
||||
@ -20,11 +20,15 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
||||
case ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT:
|
||||
throw new UserInputError(error.message);
|
||||
throw new UserInputError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED:
|
||||
throw new ForbiddenError(error.message);
|
||||
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
|
||||
throw new ConflictError(error.message);
|
||||
throw new ConflictError(error.message, {
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
});
|
||||
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
|
||||
throw error;
|
||||
default: {
|
||||
|
||||
@ -29,9 +29,14 @@ export const validateObjectMetadataInputNameOrThrow = (name: string): void => {
|
||||
validateMetadataNameOrThrow(name);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidMetadataException) {
|
||||
const errorMessage = error.message;
|
||||
|
||||
throw new ObjectMetadataException(
|
||||
error.message,
|
||||
errorMessage,
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
{
|
||||
userFriendlyMessage: errorMessage,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -62,6 +67,9 @@ const validateObjectMetadataInputLabelOrThrow = (name: string): void => {
|
||||
throw new ObjectMetadataException(
|
||||
error.message,
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
{
|
||||
userFriendlyMessage: error.userFriendlyMessage,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@ export enum PermissionsExceptionCode {
|
||||
CANNOT_UPDATE_SELF_ROLE = 'CANNOT_UPDATE_SELF_ROLE',
|
||||
NO_ROLE_FOUND_FOR_USER_WORKSPACE = 'NO_ROLE_FOUND_FOR_USER_WORKSPACE',
|
||||
INVALID_ARG = 'INVALID_ARG_PERMISSIONS',
|
||||
PERMISSIONS_V2_NOT_ENABLED = 'PERMISSIONS_V2_NOT_ENABLED',
|
||||
ROLE_LABEL_ALREADY_EXISTS = 'ROLE_LABEL_ALREADY_EXISTS',
|
||||
DEFAULT_ROLE_NOT_FOUND = 'DEFAULT_ROLE_NOT_FOUND',
|
||||
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND_PERMISSIONS',
|
||||
@ -53,7 +52,6 @@ export enum PermissionsExceptionMessage {
|
||||
UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission',
|
||||
CANNOT_UPDATE_SELF_ROLE = 'Cannot update self role',
|
||||
NO_ROLE_FOUND_FOR_USER_WORKSPACE = 'No role found for userWorkspace',
|
||||
PERMISSIONS_V2_NOT_ENABLED = 'Permissions V2 is not enabled',
|
||||
ROLE_LABEL_ALREADY_EXISTS = 'A role with this label already exists',
|
||||
DEFAULT_ROLE_NOT_FOUND = 'Default role not found',
|
||||
OBJECT_METADATA_NOT_FOUND = 'Object metadata not found',
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
@ -13,11 +15,16 @@ export const permissionGraphqlApiExceptionHandler = (
|
||||
) => {
|
||||
switch (error.code) {
|
||||
case PermissionsExceptionCode.PERMISSION_DENIED:
|
||||
throw new ForbiddenError(error.message, {
|
||||
userFriendlyMessage: 'User does not have permission.',
|
||||
});
|
||||
case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS:
|
||||
throw new ForbiddenError(error.message, {
|
||||
userFriendlyMessage: t`A role with this label already exists.`,
|
||||
});
|
||||
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
||||
case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE:
|
||||
case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER:
|
||||
case PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED:
|
||||
case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS:
|
||||
case PermissionsExceptionCode.ROLE_NOT_EDITABLE:
|
||||
case PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT:
|
||||
throw new ForbiddenError(error.message);
|
||||
|
||||
@ -10,7 +10,7 @@ exports[`validateMetadataNameOrThrow throws error when string has spaces 1`] = `
|
||||
|
||||
exports[`validateMetadataNameOrThrow throws error when string is a reserved word 1`] = `"The name "role" is not available"`;
|
||||
|
||||
exports[`validateMetadataNameOrThrow throws error when string is above 63 characters 1`] = `"String "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" exceeds 63 characters limit"`;
|
||||
exports[`validateMetadataNameOrThrow throws error when string is above 63 characters 1`] = `"Name is too long: it exceeds the 63 characters limit."`;
|
||||
|
||||
exports[`validateMetadataNameOrThrow throws error when string is empty 1`] = `"Input is too short: """`;
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { slugify } from 'transliteration';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@ -12,6 +13,9 @@ export const computeMetadataNameFromLabel = (label: string): string => {
|
||||
throw new InvalidMetadataException(
|
||||
'Label is required',
|
||||
InvalidMetadataExceptionCode.LABEL_REQUIRED,
|
||||
{
|
||||
userFriendlyMessage: t`Label is required`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -31,6 +35,9 @@ export const computeMetadataNameFromLabel = (label: string): string => {
|
||||
throw new InvalidMetadataException(
|
||||
`Invalid label: "${label}"`,
|
||||
InvalidMetadataExceptionCode.INVALID_LABEL,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid label: "${label}"`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class InvalidMetadataException extends CustomException {
|
||||
constructor(message: string, code: InvalidMetadataExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: InvalidMetadataExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
@ -43,6 +45,9 @@ export const validateFieldNameAvailabilityOrThrow = (
|
||||
throw new InvalidMetadataException(
|
||||
`Name "${name}" is not available`,
|
||||
InvalidMetadataExceptionCode.NOT_AVAILABLE,
|
||||
{
|
||||
userFriendlyMessage: t`This name is not available.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
|
||||
import {
|
||||
@ -8,7 +9,7 @@ import {
|
||||
export const validateMetadataNameIsCamelCaseOrThrow = (name: string) => {
|
||||
if (name !== camelCase(name)) {
|
||||
throw new InvalidMetadataException(
|
||||
`${name} should be in camelCase`,
|
||||
t`${name} should be in camelCase`,
|
||||
InvalidMetadataExceptionCode.NOT_CAMEL_CASE,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
InvalidMetadataException,
|
||||
InvalidMetadataExceptionCode,
|
||||
@ -71,6 +73,9 @@ export const validateMetadataNameIsNotReservedKeywordOrThrow = (
|
||||
throw new InvalidMetadataException(
|
||||
`The name "${name}" is not available`,
|
||||
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
|
||||
{
|
||||
userFriendlyMessage: t`This name is not available.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
InvalidMetadataException,
|
||||
InvalidMetadataExceptionCode,
|
||||
@ -7,7 +9,7 @@ import { exceedsDatabaseIdentifierMaximumLength } from 'src/engine/metadata-modu
|
||||
export const validateMetadataNameIsNotTooLongOrThrow = (name: string) => {
|
||||
if (exceedsDatabaseIdentifierMaximumLength(name)) {
|
||||
throw new InvalidMetadataException(
|
||||
`String "${name}" exceeds 63 characters limit`,
|
||||
t`Name is too long: it exceeds the 63 characters limit.`,
|
||||
InvalidMetadataExceptionCode.EXCEEDS_MAX_LENGTH,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
InvalidMetadataException,
|
||||
InvalidMetadataExceptionCode,
|
||||
@ -7,7 +9,7 @@ import { beneathDatabaseIdentifierMinimumLength } from 'src/engine/metadata-modu
|
||||
export const validateMetadataNameIsNotTooShortOrThrow = (name: string) => {
|
||||
if (beneathDatabaseIdentifierMinimumLength(name)) {
|
||||
throw new InvalidMetadataException(
|
||||
`Input is too short: "${name}"`,
|
||||
t`Input is too short: "${name}"`,
|
||||
InvalidMetadataExceptionCode.INPUT_TOO_SHORT,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
InvalidMetadataException,
|
||||
InvalidMetadataExceptionCode,
|
||||
@ -14,7 +16,7 @@ export const validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLett
|
||||
)
|
||||
) {
|
||||
throw new InvalidMetadataException(
|
||||
`String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`,
|
||||
t`String "${name}" is not valid: must start with lowercase letter and contain only alphanumeric letters`,
|
||||
InvalidMetadataExceptionCode.INVALID_STRING,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
@ -30,6 +32,9 @@ export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({
|
||||
throw new ObjectMetadataException(
|
||||
'Object already exists',
|
||||
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
|
||||
{
|
||||
userFriendlyMessage: t`Object already exists`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class WorkflowQueryValidationException extends CustomException {
|
||||
constructor(message: string, code: WorkflowQueryValidationExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: WorkflowQueryValidationExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class WorkflowVersionStepException extends CustomException {
|
||||
constructor(message: string, code: WorkflowVersionStepExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: WorkflowVersionStepExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
export enum WorkflowVersionStepExceptionCode {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
WorkflowQueryValidationException,
|
||||
WorkflowQueryValidationExceptionCode,
|
||||
@ -11,6 +13,9 @@ export const assertWorkflowStatusesNotSet = (
|
||||
throw new WorkflowQueryValidationException(
|
||||
'Statuses cannot be set manually.',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: t`Statuses cannot be set manually.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import {
|
||||
@ -14,6 +16,9 @@ export function assertWorkflowVersionHasSteps(
|
||||
throw new WorkflowTriggerException(
|
||||
'Workflow version does not contain at least one step',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||
{
|
||||
userFriendlyMessage: t`Workflow version does not contain at least one step`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
WorkflowQueryValidationException,
|
||||
WorkflowQueryValidationExceptionCode,
|
||||
@ -14,6 +16,9 @@ export const assertWorkflowVersionIsDraft = (
|
||||
throw new WorkflowQueryValidationException(
|
||||
'Workflow version is not in draft status',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: t`Workflow version is not in draft status`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import {
|
||||
WorkflowTriggerException,
|
||||
@ -17,6 +19,9 @@ export function assertWorkflowVersionTriggerIsDefined(
|
||||
throw new WorkflowTriggerException(
|
||||
'Workflow version does not contain trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||
{
|
||||
userFriendlyMessage: t`Workflow version does not contain trigger`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
|
||||
import {
|
||||
@ -38,6 +39,9 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'Cannot create workflow version with status other than draft',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: t`Cannot create workflow version with status other than draft`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -62,6 +66,9 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'Cannot create multiple draft versions for the same workflow',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: t`Cannot create multiple draft versions for the same workflow`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -89,6 +96,9 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'Cannot update workflow version status manually',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: t`Cannot update workflow version status manually`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -132,6 +142,9 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'The initial version of a workflow can not be deleted',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: t`The initial version of a workflow can not be deleted`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
@ -312,6 +313,9 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
throw new WorkflowVersionStepException(
|
||||
'Step is not a form',
|
||||
WorkflowVersionStepExceptionCode.INVALID,
|
||||
{
|
||||
userFriendlyMessage: t`Step is not a form`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class WorkflowStepExecutorException extends CustomException {
|
||||
constructor(message: string, code: WorkflowStepExecutorExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: WorkflowStepExecutorExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
export const getPreviousStepOutput = (
|
||||
steps: WorkflowAction[],
|
||||
currentStepId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: Record<string, any>,
|
||||
) => {
|
||||
const previousSteps = steps.filter((step) =>
|
||||
step?.nextStepIds?.includes(currentStepId),
|
||||
);
|
||||
|
||||
if (previousSteps.length === 0) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Filter action must have a previous step',
|
||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
||||
{
|
||||
userFriendlyMessage: t`Filter action must have a previous step`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (previousSteps.length > 1) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Filter action must have only one previous step',
|
||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
||||
{
|
||||
userFriendlyMessage: t`Filter action must have only one previous step`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const previousStep = previousSteps[0];
|
||||
const previousStepOutput = context[previousStep.id];
|
||||
|
||||
if (!previousStepOutput) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
'Previous step output not found',
|
||||
WorkflowStepExecutorExceptionCode.FAILED_TO_EXECUTE_STEP,
|
||||
);
|
||||
}
|
||||
|
||||
return previousStepOutput;
|
||||
};
|
||||
@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class WorkflowTriggerException extends CustomException {
|
||||
declare code: WorkflowTriggerExceptionCode;
|
||||
constructor(message: string, code: WorkflowTriggerExceptionCode) {
|
||||
super(message, code);
|
||||
constructor(
|
||||
message: string,
|
||||
code: WorkflowTriggerExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, userFriendlyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||
@ -11,6 +12,9 @@ export function assertFormStepIsValid(settings: WorkflowFormActionSettings) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No input provided in form step',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`No input provided in form step`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,6 +22,9 @@ export function assertFormStepIsValid(settings: WorkflowFormActionSettings) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Form action must have at least one field',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||
{
|
||||
userFriendlyMessage: t`Form action must have at least one field`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,6 +36,9 @@ export function assertFormStepIsValid(settings: WorkflowFormActionSettings) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Form action fields must have unique names',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||
{
|
||||
userFriendlyMessage: t`Form action fields must have unique names`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -41,6 +51,9 @@ export function assertFormStepIsValid(settings: WorkflowFormActionSettings) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Form action fields must have a defined label and type',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||
{
|
||||
userFriendlyMessage: t`Form action fields must have a defined label and type`,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import {
|
||||
WorkflowVersionStatus,
|
||||
WorkflowVersionWorkspaceEntity,
|
||||
@ -33,6 +35,9 @@ export function assertVersionCanBeActivated(
|
||||
throw new WorkflowTriggerException(
|
||||
'Cannot activate non-draft or non-last-published version',
|
||||
WorkflowTriggerExceptionCode.INVALID_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Cannot activate non-draft or non-last-published version`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -42,6 +47,9 @@ function assertVersionIsValid(workflowVersion: WorkflowVersionWorkspaceEntity) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Workflow version does not contain trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION,
|
||||
{
|
||||
userFriendlyMessage: t`Workflow version does not contain trigger`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -49,6 +57,9 @@ function assertVersionIsValid(workflowVersion: WorkflowVersionWorkspaceEntity) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No trigger type provided',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`No trigger type provided`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -56,6 +67,9 @@ function assertVersionIsValid(workflowVersion: WorkflowVersionWorkspaceEntity) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No steps provided in workflow version',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`No steps provided in workflow version`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -88,6 +102,9 @@ function assertTriggerSettingsAreValid(
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid trigger type for enabling workflow trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid trigger type for enabling workflow trigger`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -98,6 +115,9 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No setting type provided in cron trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`No setting type provided in cron trigger`,
|
||||
},
|
||||
);
|
||||
}
|
||||
switch (settings.type) {
|
||||
@ -106,6 +126,9 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No pattern provided in CUSTOM cron trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`No pattern provided in CUSTOM cron trigger`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -117,24 +140,36 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No schedule provided in cron trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`No schedule provided in cron trigger`,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (settings.schedule.day <= 0) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid day value. Should be integer greater than 1',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid day value. Should be integer greater than 1`,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (settings.schedule.hour < 0 || settings.schedule.hour > 23) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid hour value. Should be integer between 0 and 23',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid hour value. Should be integer between 0 and 23`,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (settings.schedule.minute < 0 || settings.schedule.minute > 59) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid minute value. Should be integer between 0 and 59',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid minute value. Should be integer between 0 and 59`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -146,12 +181,18 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No schedule provided in cron trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid hour value. Should be integer greater than 1`,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (settings.schedule.hour <= 0) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid hour value. Should be integer greater than 1',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid hour value. Should be integer greater than 1`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -159,6 +200,9 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid minute value. Should be integer between 0 and 59',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid minute value. Should be integer between 0 and 59`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -170,6 +214,9 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No schedule provided in cron trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid minute value. Should be integer greater than 1`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -177,6 +224,9 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid minute value. Should be integer greater than 1',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid minute value. Should be integer greater than 1`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -187,6 +237,9 @@ function assertCronTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Invalid setting type provided in cron trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Invalid setting type provided in cron trigger`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -197,6 +250,9 @@ function assertDatabaseEventTriggerSettingsAreValid(settings: any) {
|
||||
throw new WorkflowTriggerException(
|
||||
'No event name provided in database event trigger',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`No event name provided in database event trigger`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import cron from 'cron-validate';
|
||||
|
||||
import { WorkflowCronTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
import {
|
||||
WorkflowTriggerException,
|
||||
WorkflowTriggerExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
|
||||
import { WorkflowCronTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
const validatePattern = (pattern: string) => {
|
||||
const cronValidator = cron(pattern);
|
||||
@ -13,6 +14,9 @@ const validatePattern = (pattern: string) => {
|
||||
throw new WorkflowTriggerException(
|
||||
`Cron pattern '${pattern}' is invalid`,
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Cron pattern '${pattern}' is invalid`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -51,6 +55,9 @@ export const computeCronPatternFromSchedule = (
|
||||
throw new WorkflowTriggerException(
|
||||
'Unsupported cron schedule type',
|
||||
WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER,
|
||||
{
|
||||
userFriendlyMessage: t`Unsupported cron schedule type`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
@ -269,6 +270,9 @@ export class WorkflowTriggerWorkspaceService {
|
||||
throw new WorkflowTriggerException(
|
||||
'Cannot have more than one active workflow version',
|
||||
WorkflowTriggerExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: t`Cannot have more than one active workflow version`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -294,6 +298,9 @@ export class WorkflowTriggerWorkspaceService {
|
||||
throw new WorkflowTriggerException(
|
||||
'Cannot disable non-active workflow version',
|
||||
WorkflowTriggerExceptionCode.FORBIDDEN,
|
||||
{
|
||||
userFriendlyMessage: t`Cannot disable non-active workflow version`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
export class CustomException extends Error {
|
||||
code: string;
|
||||
userFriendlyMessage?: string;
|
||||
|
||||
constructor(message: string, code: string) {
|
||||
constructor(message: string, code: string, userFriendlyMessage?: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.userFriendlyMessage = userFriendlyMessage;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user