feat(server): introduce user friendly server errors (#7111)

This commit is contained in:
liuyi
2024-06-17 11:30:58 +08:00
committed by GitHub
parent 5307a55f8a
commit 54fc1197ad
65 changed files with 3170 additions and 924 deletions

View File

@@ -0,0 +1,481 @@
import { STATUS_CODES } from 'node:http';
import { HttpStatus, Logger } from '@nestjs/common';
import { capitalize } from 'lodash-es';
export type UserFriendlyErrorBaseType =
| 'bad_request'
| 'too_many_requests'
| 'resource_not_found'
| 'resource_already_exists'
| 'invalid_input'
| 'action_forbidden'
| 'no_permission'
| 'quota_exceeded'
| 'authentication_required'
| 'internal_server_error';
type ErrorArgType = 'string' | 'number' | 'boolean';
type ErrorArgs = Record<string, ErrorArgType | Record<string, ErrorArgType>>;
export type UserFriendlyErrorOptions = {
type: UserFriendlyErrorBaseType;
args?: ErrorArgs;
message: string | ((args: any) => string);
};
const BaseTypeToHttpStatusMap: Record<UserFriendlyErrorBaseType, HttpStatus> = {
too_many_requests: HttpStatus.TOO_MANY_REQUESTS,
bad_request: HttpStatus.BAD_REQUEST,
resource_not_found: HttpStatus.NOT_FOUND,
resource_already_exists: HttpStatus.BAD_REQUEST,
invalid_input: HttpStatus.BAD_REQUEST,
action_forbidden: HttpStatus.FORBIDDEN,
no_permission: HttpStatus.FORBIDDEN,
quota_exceeded: HttpStatus.PAYMENT_REQUIRED,
authentication_required: HttpStatus.UNAUTHORIZED,
internal_server_error: HttpStatus.INTERNAL_SERVER_ERROR,
};
export class UserFriendlyError extends Error {
/**
* Standard HTTP status code
*/
status: number;
/**
* Business error category, for example 'resource_already_exists' or 'quota_exceeded'
*/
type: string;
/**
* Additional data that could be used for error handling or formatting
*/
data: any;
constructor(
type: UserFriendlyErrorBaseType,
name: keyof typeof USER_FRIENDLY_ERRORS,
message?: string | ((args?: any) => string),
args?: any
) {
const defaultMsg = USER_FRIENDLY_ERRORS[name].message;
// disallow message override for `internal_server_error`
// to avoid leak internal information to user
let msg =
name === 'internal_server_error' ? defaultMsg : message ?? defaultMsg;
if (typeof msg === 'function') {
msg = msg(args);
}
super(msg);
this.status = BaseTypeToHttpStatusMap[type];
this.type = type;
this.name = name;
this.data = args;
}
json() {
return {
status: this.status,
code: STATUS_CODES[this.status] ?? 'BAD REQUEST',
type: this.type.toUpperCase(),
name: this.name.toUpperCase(),
message: this.message,
data: this.data,
};
}
log(context: string) {
// ignore all user behavior error log
if (this.type !== 'internal_server_error') {
return;
}
new Logger(context).error(
'Internal server error',
this.cause ? (this.cause as any).stack ?? this.cause : this.stack
);
}
}
/**
*
* @ObjectType()
* export class XXXDataType {
* @Field()
*
* }
*/
function generateErrorArgs(name: string, args: ErrorArgs) {
const typeName = `${name}DataType`;
const lines = [`@ObjectType()`, `class ${typeName} {`];
Object.entries(args).forEach(([arg, fieldArgs]) => {
if (typeof fieldArgs === 'object') {
const subResult = generateErrorArgs(
name + 'Field' + capitalize(arg),
fieldArgs
);
lines.unshift(subResult.def);
lines.push(
` @Field(() => ${subResult.name}) ${arg}!: ${subResult.name};`
);
} else {
lines.push(` @Field() ${arg}!: ${fieldArgs}`);
}
});
lines.push('}');
return { name: typeName, def: lines.join('\n') };
}
export function generateUserFriendlyErrors() {
const output = [
'// AUTO GENERATED FILE',
`import { createUnionType, Field, ObjectType, registerEnumType } from '@nestjs/graphql';`,
'',
`import { UserFriendlyError } from './def';`,
];
const errorNames: string[] = [];
const argTypes: string[] = [];
for (const code in USER_FRIENDLY_ERRORS) {
errorNames.push(code.toUpperCase());
// @ts-expect-error allow
const options: UserFriendlyErrorOptions = USER_FRIENDLY_ERRORS[code];
const className = code
.split('_')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
const args = options.args
? generateErrorArgs(className, options.args)
: null;
const classDef = `
export class ${className} extends UserFriendlyError {
constructor(${args ? `args: ${args.name}, ` : ''}message?: string${args ? ` | ((args: ${args.name}) => string)` : ''}) {
super('${options.type}', '${code}', message${args ? ', args' : ''});
}
}`;
if (args) {
output.push(args.def);
argTypes.push(args.name);
}
output.push(classDef);
}
output.push(`export enum ErrorNames {
${errorNames.join(',\n ')}
}
registerEnumType(ErrorNames, {
name: 'ErrorNames'
})
export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion',
types: () =>
[${argTypes.join(', ')}] as const,
});
`);
return output.join('\n');
}
// DEFINE ALL USER FRIENDLY ERRORS HERE
export const USER_FRIENDLY_ERRORS = {
// Internal uncaught errors
internal_server_error: {
type: 'internal_server_error',
message: 'An internal error occurred.',
},
too_many_request: {
type: 'too_many_requests',
message: 'Too many requests.',
},
// User Errors
user_not_found: {
type: 'resource_not_found',
message: 'User not found.',
},
user_avatar_not_found: {
type: 'resource_not_found',
message: 'User avatar not found.',
},
email_already_used: {
type: 'resource_already_exists',
message: 'This email has already been registered.',
},
same_email_provided: {
type: 'invalid_input',
message:
'You are trying to update your account email to the same as the old one.',
},
wrong_sign_in_credentials: {
type: 'invalid_input',
message: 'Wrong user email or password.',
},
unknown_oauth_provider: {
type: 'invalid_input',
args: { name: 'string' },
message: ({ name }) => `Unknown authentication provider ${name}.`,
},
oauth_state_expired: {
type: 'bad_request',
message: 'OAuth state expired, please try again.',
},
invalid_oauth_callback_state: {
type: 'bad_request',
message: 'Invalid callback state parameter.',
},
missing_oauth_query_parameter: {
type: 'bad_request',
args: { name: 'string' },
message: ({ name }) => `Missing query parameter \`${name}\`.`,
},
oauth_account_already_connected: {
type: 'bad_request',
message:
'The third-party account has already been connected to another user.',
},
invalid_email: {
type: 'invalid_input',
message: 'An invalid email provided.',
},
invalid_password_length: {
type: 'invalid_input',
args: { min: 'number', max: 'number' },
message: ({ min, max }) =>
`Password must be between ${min} and ${max} characters`,
},
wrong_sign_in_method: {
type: 'invalid_input',
message:
'You are trying to sign in by a different method than you signed up with.',
},
early_access_required: {
type: 'action_forbidden',
message: `You don't have early access permission. Visit https://community.affine.pro/c/insider-general/ for more information.`,
},
sign_up_forbidden: {
type: 'action_forbidden',
message: `You are not allowed to sign up.`,
},
email_token_not_found: {
type: 'invalid_input',
message: 'The email token provided is not found.',
},
invalid_email_token: {
type: 'invalid_input',
message: 'An invalid email token provided.',
},
// Authentication & Permission Errors
authentication_required: {
type: 'authentication_required',
message: 'You must sign in first to access this resource.',
},
action_forbidden: {
type: 'action_forbidden',
message: 'You are not allowed to perform this action.',
},
access_denied: {
type: 'no_permission',
message: 'You do not have permission to access this resource.',
},
email_verification_required: {
type: 'action_forbidden',
message: 'You must verify your email before accessing this resource.',
},
// Workspace & Doc & Sync errors
workspace_not_found: {
type: 'resource_not_found',
args: { workspaceId: 'string' },
message: ({ workspaceId }) => `Workspace ${workspaceId} not found.`,
},
not_in_workspace: {
type: 'action_forbidden',
args: { workspaceId: 'string' },
message: ({ workspaceId }) =>
`You should join in workspace ${workspaceId} before broadcasting messages.`,
},
workspace_access_denied: {
type: 'no_permission',
args: { workspaceId: 'string' },
message: ({ workspaceId }) =>
`You do not have permission to access workspace ${workspaceId}.`,
},
workspace_owner_not_found: {
type: 'internal_server_error',
args: { workspaceId: 'string' },
message: ({ workspaceId }) =>
`Owner of workspace ${workspaceId} not found.`,
},
cant_change_workspace_owner: {
type: 'action_forbidden',
message: 'You are not allowed to change the owner of a workspace.',
},
doc_not_found: {
type: 'resource_not_found',
args: { workspaceId: 'string', docId: 'string' },
message: ({ workspaceId, docId }) =>
`Doc ${docId} under workspace ${workspaceId} not found.`,
},
doc_access_denied: {
type: 'no_permission',
args: { workspaceId: 'string', docId: 'string' },
message: ({ workspaceId, docId }) =>
`You do not have permission to access doc ${docId} under workspace ${workspaceId}.`,
},
version_rejected: {
type: 'action_forbidden',
args: { version: 'string', serverVersion: 'string' },
message: ({ version, serverVersion }) =>
`Your client with version ${version} is rejected by remote sync server. Please upgrade to ${serverVersion}.`,
},
invalid_history_timestamp: {
type: 'invalid_input',
args: { timestamp: 'string' },
message: 'Invalid doc history timestamp provided.',
},
doc_history_not_found: {
type: 'resource_not_found',
args: { workspaceId: 'string', docId: 'string', timestamp: 'number' },
message: ({ workspaceId, docId, timestamp }) =>
`History of ${docId} at ${timestamp} under workspace ${workspaceId}.`,
},
blob_not_found: {
type: 'resource_not_found',
args: { workspaceId: 'string', blobId: 'string' },
message: ({ workspaceId, blobId }) =>
`Blob ${blobId} not found in workspace ${workspaceId}.`,
},
expect_to_publish_page: {
type: 'invalid_input',
message: 'Expected to publish a page, not a workspace.',
},
expect_to_revoke_public_page: {
type: 'invalid_input',
message: 'Expected to revoke a public page, not a workspace.',
},
page_is_not_public: {
type: 'bad_request',
message: 'Page is not public.',
},
// Subscription Errors
failed_to_checkout: {
type: 'internal_server_error',
message: 'Failed to create checkout session.',
},
subscription_already_exists: {
type: 'resource_already_exists',
args: { plan: 'string' },
message: ({ plan }) => `You have already subscribed to the ${plan} plan.`,
},
subscription_not_exists: {
type: 'resource_not_found',
args: { plan: 'string' },
message: ({ plan }) => `You didn't subscribe to the ${plan} plan.`,
},
subscription_has_been_canceled: {
type: 'action_forbidden',
message: 'Your subscription has already been canceled.',
},
subscription_expired: {
type: 'action_forbidden',
message: 'Your subscription has expired.',
},
same_subscription_recurring: {
type: 'bad_request',
args: { recurring: 'string' },
message: ({ recurring }) =>
`Your subscription has already been in ${recurring} recurring state.`,
},
customer_portal_create_failed: {
type: 'internal_server_error',
message: 'Failed to create customer portal session.',
},
subscription_plan_not_found: {
type: 'resource_not_found',
args: { plan: 'string', recurring: 'string' },
message: 'You are trying to access a unknown subscription plan.',
},
// Copilot errors
copilot_session_not_found: {
type: 'resource_not_found',
message: `Copilot session not found.`,
},
copilot_session_deleted: {
type: 'action_forbidden',
message: `Copilot session has been deleted.`,
},
no_copilot_provider_available: {
type: 'internal_server_error',
message: `No copilot provider available.`,
},
copilot_failed_to_generate_text: {
type: 'internal_server_error',
message: `Failed to generate text.`,
},
copilot_failed_to_create_message: {
type: 'internal_server_error',
message: `Failed to create chat message.`,
},
unsplash_is_not_configured: {
type: 'internal_server_error',
message: `Unsplash is not configured.`,
},
copilot_action_taken: {
type: 'action_forbidden',
message: `Action has been taken, no more messages allowed.`,
},
copilot_message_not_found: {
type: 'resource_not_found',
message: `Copilot message not found.`,
},
copilot_prompt_not_found: {
type: 'resource_not_found',
args: { name: 'string' },
message: ({ name }) => `Copilot prompt ${name} not found.`,
},
// Quota & Limit errors
blob_quota_exceeded: {
type: 'quota_exceeded',
message: 'You have exceeded your blob storage quota.',
},
member_quota_exceeded: {
type: 'quota_exceeded',
message: 'You have exceeded your workspace member quota.',
},
copilot_quota_exceeded: {
type: 'quota_exceeded',
message:
'You have reached the limit of actions in this workspace, please upgrade your plan.',
},
// Config errors
runtime_config_not_found: {
type: 'resource_not_found',
args: { key: 'string' },
message: ({ key }) => `Runtime config ${key} not found.`,
},
invalid_runtime_config_type: {
type: 'invalid_input',
args: { key: 'string', want: 'string', get: 'string' },
message: ({ key, want, get }) =>
`Invalid runtime config type for '${key}', want '${want}', but get ${get}.`,
},
mailer_service_is_not_configured: {
type: 'internal_server_error',
message: 'Mailer service is not configured.',
},
} satisfies Record<string, UserFriendlyErrorOptions>;

View File

@@ -0,0 +1,616 @@
// AUTO GENERATED FILE
import {
createUnionType,
Field,
ObjectType,
registerEnumType,
} from '@nestjs/graphql';
import { UserFriendlyError } from './def';
export class InternalServerError extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'internal_server_error', message);
}
}
export class TooManyRequest extends UserFriendlyError {
constructor(message?: string) {
super('too_many_requests', 'too_many_request', message);
}
}
export class UserNotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'user_not_found', message);
}
}
export class UserAvatarNotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'user_avatar_not_found', message);
}
}
export class EmailAlreadyUsed extends UserFriendlyError {
constructor(message?: string) {
super('resource_already_exists', 'email_already_used', message);
}
}
export class SameEmailProvided extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'same_email_provided', message);
}
}
export class WrongSignInCredentials extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'wrong_sign_in_credentials', message);
}
}
@ObjectType()
class UnknownOauthProviderDataType {
@Field() name!: string;
}
export class UnknownOauthProvider extends UserFriendlyError {
constructor(
args: UnknownOauthProviderDataType,
message?: string | ((args: UnknownOauthProviderDataType) => string)
) {
super('invalid_input', 'unknown_oauth_provider', message, args);
}
}
export class OauthStateExpired extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'oauth_state_expired', message);
}
}
export class InvalidOauthCallbackState extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'invalid_oauth_callback_state', message);
}
}
@ObjectType()
class MissingOauthQueryParameterDataType {
@Field() name!: string;
}
export class MissingOauthQueryParameter extends UserFriendlyError {
constructor(
args: MissingOauthQueryParameterDataType,
message?: string | ((args: MissingOauthQueryParameterDataType) => string)
) {
super('bad_request', 'missing_oauth_query_parameter', message, args);
}
}
export class OauthAccountAlreadyConnected extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'oauth_account_already_connected', message);
}
}
export class InvalidEmail extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'invalid_email', message);
}
}
@ObjectType()
class InvalidPasswordLengthDataType {
@Field() min!: number;
@Field() max!: number;
}
export class InvalidPasswordLength extends UserFriendlyError {
constructor(
args: InvalidPasswordLengthDataType,
message?: string | ((args: InvalidPasswordLengthDataType) => string)
) {
super('invalid_input', 'invalid_password_length', message, args);
}
}
export class WrongSignInMethod extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'wrong_sign_in_method', message);
}
}
export class EarlyAccessRequired extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'early_access_required', message);
}
}
export class SignUpForbidden extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'sign_up_forbidden', message);
}
}
export class EmailTokenNotFound extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'email_token_not_found', message);
}
}
export class InvalidEmailToken extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'invalid_email_token', message);
}
}
export class AuthenticationRequired extends UserFriendlyError {
constructor(message?: string) {
super('authentication_required', 'authentication_required', message);
}
}
export class ActionForbidden extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'action_forbidden', message);
}
}
export class AccessDenied extends UserFriendlyError {
constructor(message?: string) {
super('no_permission', 'access_denied', message);
}
}
export class EmailVerificationRequired extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'email_verification_required', message);
}
}
@ObjectType()
class WorkspaceNotFoundDataType {
@Field() workspaceId!: string;
}
export class WorkspaceNotFound extends UserFriendlyError {
constructor(
args: WorkspaceNotFoundDataType,
message?: string | ((args: WorkspaceNotFoundDataType) => string)
) {
super('resource_not_found', 'workspace_not_found', message, args);
}
}
@ObjectType()
class NotInWorkspaceDataType {
@Field() workspaceId!: string;
}
export class NotInWorkspace extends UserFriendlyError {
constructor(
args: NotInWorkspaceDataType,
message?: string | ((args: NotInWorkspaceDataType) => string)
) {
super('action_forbidden', 'not_in_workspace', message, args);
}
}
@ObjectType()
class WorkspaceAccessDeniedDataType {
@Field() workspaceId!: string;
}
export class WorkspaceAccessDenied extends UserFriendlyError {
constructor(
args: WorkspaceAccessDeniedDataType,
message?: string | ((args: WorkspaceAccessDeniedDataType) => string)
) {
super('no_permission', 'workspace_access_denied', message, args);
}
}
@ObjectType()
class WorkspaceOwnerNotFoundDataType {
@Field() workspaceId!: string;
}
export class WorkspaceOwnerNotFound extends UserFriendlyError {
constructor(
args: WorkspaceOwnerNotFoundDataType,
message?: string | ((args: WorkspaceOwnerNotFoundDataType) => string)
) {
super('internal_server_error', 'workspace_owner_not_found', message, args);
}
}
export class CantChangeWorkspaceOwner extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'cant_change_workspace_owner', message);
}
}
@ObjectType()
class DocNotFoundDataType {
@Field() workspaceId!: string;
@Field() docId!: string;
}
export class DocNotFound extends UserFriendlyError {
constructor(
args: DocNotFoundDataType,
message?: string | ((args: DocNotFoundDataType) => string)
) {
super('resource_not_found', 'doc_not_found', message, args);
}
}
@ObjectType()
class DocAccessDeniedDataType {
@Field() workspaceId!: string;
@Field() docId!: string;
}
export class DocAccessDenied extends UserFriendlyError {
constructor(
args: DocAccessDeniedDataType,
message?: string | ((args: DocAccessDeniedDataType) => string)
) {
super('no_permission', 'doc_access_denied', message, args);
}
}
@ObjectType()
class VersionRejectedDataType {
@Field() version!: string;
@Field() serverVersion!: string;
}
export class VersionRejected extends UserFriendlyError {
constructor(
args: VersionRejectedDataType,
message?: string | ((args: VersionRejectedDataType) => string)
) {
super('action_forbidden', 'version_rejected', message, args);
}
}
@ObjectType()
class InvalidHistoryTimestampDataType {
@Field() timestamp!: string;
}
export class InvalidHistoryTimestamp extends UserFriendlyError {
constructor(
args: InvalidHistoryTimestampDataType,
message?: string | ((args: InvalidHistoryTimestampDataType) => string)
) {
super('invalid_input', 'invalid_history_timestamp', message, args);
}
}
@ObjectType()
class DocHistoryNotFoundDataType {
@Field() workspaceId!: string;
@Field() docId!: string;
@Field() timestamp!: number;
}
export class DocHistoryNotFound extends UserFriendlyError {
constructor(
args: DocHistoryNotFoundDataType,
message?: string | ((args: DocHistoryNotFoundDataType) => string)
) {
super('resource_not_found', 'doc_history_not_found', message, args);
}
}
@ObjectType()
class BlobNotFoundDataType {
@Field() workspaceId!: string;
@Field() blobId!: string;
}
export class BlobNotFound extends UserFriendlyError {
constructor(
args: BlobNotFoundDataType,
message?: string | ((args: BlobNotFoundDataType) => string)
) {
super('resource_not_found', 'blob_not_found', message, args);
}
}
export class ExpectToPublishPage extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'expect_to_publish_page', message);
}
}
export class ExpectToRevokePublicPage extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'expect_to_revoke_public_page', message);
}
}
export class PageIsNotPublic extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'page_is_not_public', message);
}
}
export class FailedToCheckout extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'failed_to_checkout', message);
}
}
@ObjectType()
class SubscriptionAlreadyExistsDataType {
@Field() plan!: string;
}
export class SubscriptionAlreadyExists extends UserFriendlyError {
constructor(
args: SubscriptionAlreadyExistsDataType,
message?: string | ((args: SubscriptionAlreadyExistsDataType) => string)
) {
super(
'resource_already_exists',
'subscription_already_exists',
message,
args
);
}
}
@ObjectType()
class SubscriptionNotExistsDataType {
@Field() plan!: string;
}
export class SubscriptionNotExists extends UserFriendlyError {
constructor(
args: SubscriptionNotExistsDataType,
message?: string | ((args: SubscriptionNotExistsDataType) => string)
) {
super('resource_not_found', 'subscription_not_exists', message, args);
}
}
export class SubscriptionHasBeenCanceled extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'subscription_has_been_canceled', message);
}
}
export class SubscriptionExpired extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'subscription_expired', message);
}
}
@ObjectType()
class SameSubscriptionRecurringDataType {
@Field() recurring!: string;
}
export class SameSubscriptionRecurring extends UserFriendlyError {
constructor(
args: SameSubscriptionRecurringDataType,
message?: string | ((args: SameSubscriptionRecurringDataType) => string)
) {
super('bad_request', 'same_subscription_recurring', message, args);
}
}
export class CustomerPortalCreateFailed extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'customer_portal_create_failed', message);
}
}
@ObjectType()
class SubscriptionPlanNotFoundDataType {
@Field() plan!: string;
@Field() recurring!: string;
}
export class SubscriptionPlanNotFound extends UserFriendlyError {
constructor(
args: SubscriptionPlanNotFoundDataType,
message?: string | ((args: SubscriptionPlanNotFoundDataType) => string)
) {
super('resource_not_found', 'subscription_plan_not_found', message, args);
}
}
export class CopilotSessionNotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'copilot_session_not_found', message);
}
}
export class CopilotSessionDeleted extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'copilot_session_deleted', message);
}
}
export class NoCopilotProviderAvailable extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'no_copilot_provider_available', message);
}
}
export class CopilotFailedToGenerateText extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'copilot_failed_to_generate_text', message);
}
}
export class CopilotFailedToCreateMessage extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'copilot_failed_to_create_message', message);
}
}
export class UnsplashIsNotConfigured extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'unsplash_is_not_configured', message);
}
}
export class CopilotActionTaken extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'copilot_action_taken', message);
}
}
export class CopilotMessageNotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'copilot_message_not_found', message);
}
}
@ObjectType()
class CopilotPromptNotFoundDataType {
@Field() name!: string;
}
export class CopilotPromptNotFound extends UserFriendlyError {
constructor(
args: CopilotPromptNotFoundDataType,
message?: string | ((args: CopilotPromptNotFoundDataType) => string)
) {
super('resource_not_found', 'copilot_prompt_not_found', message, args);
}
}
export class BlobQuotaExceeded extends UserFriendlyError {
constructor(message?: string) {
super('quota_exceeded', 'blob_quota_exceeded', message);
}
}
export class MemberQuotaExceeded extends UserFriendlyError {
constructor(message?: string) {
super('quota_exceeded', 'member_quota_exceeded', message);
}
}
export class CopilotQuotaExceeded extends UserFriendlyError {
constructor(message?: string) {
super('quota_exceeded', 'copilot_quota_exceeded', message);
}
}
@ObjectType()
class RuntimeConfigNotFoundDataType {
@Field() key!: string;
}
export class RuntimeConfigNotFound extends UserFriendlyError {
constructor(
args: RuntimeConfigNotFoundDataType,
message?: string | ((args: RuntimeConfigNotFoundDataType) => string)
) {
super('resource_not_found', 'runtime_config_not_found', message, args);
}
}
@ObjectType()
class InvalidRuntimeConfigTypeDataType {
@Field() key!: string;
@Field() want!: string;
@Field() get!: string;
}
export class InvalidRuntimeConfigType extends UserFriendlyError {
constructor(
args: InvalidRuntimeConfigTypeDataType,
message?: string | ((args: InvalidRuntimeConfigTypeDataType) => string)
) {
super('invalid_input', 'invalid_runtime_config_type', message, args);
}
}
export class MailerServiceIsNotConfigured extends UserFriendlyError {
constructor(message?: string) {
super('internal_server_error', 'mailer_service_is_not_configured', message);
}
}
export enum ErrorNames {
INTERNAL_SERVER_ERROR,
TOO_MANY_REQUEST,
USER_NOT_FOUND,
USER_AVATAR_NOT_FOUND,
EMAIL_ALREADY_USED,
SAME_EMAIL_PROVIDED,
WRONG_SIGN_IN_CREDENTIALS,
UNKNOWN_OAUTH_PROVIDER,
OAUTH_STATE_EXPIRED,
INVALID_OAUTH_CALLBACK_STATE,
MISSING_OAUTH_QUERY_PARAMETER,
OAUTH_ACCOUNT_ALREADY_CONNECTED,
INVALID_EMAIL,
INVALID_PASSWORD_LENGTH,
WRONG_SIGN_IN_METHOD,
EARLY_ACCESS_REQUIRED,
SIGN_UP_FORBIDDEN,
EMAIL_TOKEN_NOT_FOUND,
INVALID_EMAIL_TOKEN,
AUTHENTICATION_REQUIRED,
ACTION_FORBIDDEN,
ACCESS_DENIED,
EMAIL_VERIFICATION_REQUIRED,
WORKSPACE_NOT_FOUND,
NOT_IN_WORKSPACE,
WORKSPACE_ACCESS_DENIED,
WORKSPACE_OWNER_NOT_FOUND,
CANT_CHANGE_WORKSPACE_OWNER,
DOC_NOT_FOUND,
DOC_ACCESS_DENIED,
VERSION_REJECTED,
INVALID_HISTORY_TIMESTAMP,
DOC_HISTORY_NOT_FOUND,
BLOB_NOT_FOUND,
EXPECT_TO_PUBLISH_PAGE,
EXPECT_TO_REVOKE_PUBLIC_PAGE,
PAGE_IS_NOT_PUBLIC,
FAILED_TO_CHECKOUT,
SUBSCRIPTION_ALREADY_EXISTS,
SUBSCRIPTION_NOT_EXISTS,
SUBSCRIPTION_HAS_BEEN_CANCELED,
SUBSCRIPTION_EXPIRED,
SAME_SUBSCRIPTION_RECURRING,
CUSTOMER_PORTAL_CREATE_FAILED,
SUBSCRIPTION_PLAN_NOT_FOUND,
COPILOT_SESSION_NOT_FOUND,
COPILOT_SESSION_DELETED,
NO_COPILOT_PROVIDER_AVAILABLE,
COPILOT_FAILED_TO_GENERATE_TEXT,
COPILOT_FAILED_TO_CREATE_MESSAGE,
UNSPLASH_IS_NOT_CONFIGURED,
COPILOT_ACTION_TAKEN,
COPILOT_MESSAGE_NOT_FOUND,
COPILOT_PROMPT_NOT_FOUND,
BLOB_QUOTA_EXCEEDED,
MEMBER_QUOTA_EXCEEDED,
COPILOT_QUOTA_EXCEEDED,
RUNTIME_CONFIG_NOT_FOUND,
INVALID_RUNTIME_CONFIG_TYPE,
MAILER_SERVICE_IS_NOT_CONFIGURED,
}
registerEnumType(ErrorNames, {
name: 'ErrorNames',
});
export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion',
types: () =>
[
UnknownOauthProviderDataType,
MissingOauthQueryParameterDataType,
InvalidPasswordLengthDataType,
WorkspaceNotFoundDataType,
NotInWorkspaceDataType,
WorkspaceAccessDeniedDataType,
WorkspaceOwnerNotFoundDataType,
DocNotFoundDataType,
DocAccessDeniedDataType,
VersionRejectedDataType,
InvalidHistoryTimestampDataType,
DocHistoryNotFoundDataType,
BlobNotFoundDataType,
SubscriptionAlreadyExistsDataType,
SubscriptionNotExistsDataType,
SameSubscriptionRecurringDataType,
SubscriptionPlanNotFoundDataType,
CopilotPromptNotFoundDataType,
RuntimeConfigNotFoundDataType,
InvalidRuntimeConfigTypeDataType,
] as const,
});

View File

@@ -1,2 +1,44 @@
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { Args, Query, Resolver } from '@nestjs/graphql';
import { Config } from '../config';
import { generateUserFriendlyErrors } from './def';
import { ActionForbidden, ErrorDataUnionType, ErrorNames } from './errors.gen';
@Resolver(() => ErrorDataUnionType)
class ErrorResolver {
// only exists for type registering
@Query(() => ErrorDataUnionType)
error(@Args({ name: 'name', type: () => ErrorNames }) _name: ErrorNames) {
throw new ActionForbidden();
}
}
@Module({
providers: [ErrorResolver],
})
export class ErrorModule implements OnModuleInit {
logger = new Logger('ErrorModule');
constructor(private readonly config: Config) {}
onModuleInit() {
if (!this.config.node.dev) {
return;
}
this.logger.log('Generating UserFriendlyError classes');
const def = generateUserFriendlyErrors();
writeFileSync(
join(fileURLToPath(import.meta.url), '../errors.gen.ts'),
def
);
}
}
export { UserFriendlyError } from './def';
export * from './errors.gen';
export * from './payment-required';
export * from './too-many-requests';