Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions packages/payload/src/errors/APIError.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { status as httpStatus } from 'http-status'

// This gets dynamically reassigned during compilation
export let APIErrorName = 'APIError'
/** @deprecated Use `instanceof APIError` instead of name comparison. */
export const APIErrorName = 'APIError'

class ExtendableError<TData extends object = { [key: string]: unknown }> extends Error {
data: TData
Expand All @@ -17,7 +17,6 @@ class ExtendableError<TData extends object = { [key: string]: unknown }> extends
// show data in cause
cause: data,
})
APIErrorName = this.constructor.name
this.name = this.constructor.name
this.message = message
this.status = status
Expand Down
6 changes: 2 additions & 4 deletions packages/payload/src/errors/ValidationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import type { PayloadRequest } from '../types/index.js'

import { APIError } from './APIError.js'

// This gets dynamically reassigned during compilation
export let ValidationErrorName = 'ValidationError'
/** @deprecated Use `instanceof ValidationError` instead of name comparison. */
export const ValidationErrorName = 'ValidationError'

export type ValidationFieldError = {
label?: LabelFunction | StaticLabel
Expand Down Expand Up @@ -75,7 +75,5 @@ export class ValidationError extends APIError<{
httpStatus.BAD_REQUEST,
results,
)

ValidationErrorName = this.constructor.name
}
}
82 changes: 82 additions & 0 deletions packages/payload/src/utilities/formatErrors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest'

import { APIError } from '../errors/APIError.js'
import { ValidationError } from '../errors/ValidationError.js'
import { formatErrors } from './formatErrors.js'

describe('formatErrors', () => {
it('should format a Payload ValidationError', () => {
const err = new ValidationError({
errors: [{ message: 'Field is required', path: 'title' }],
})

const result = formatErrors(err)

expect(result.errors).toHaveLength(1)
expect(result.errors[0]).toMatchObject({
name: 'ValidationError',
message: expect.stringContaining('title'),
data: { errors: [{ message: 'Field is required', path: 'title' }] },
})
})

it('should format a Payload APIError', () => {
const err = new APIError('Something went wrong', 400, { detail: 'bad input' }, true)

const result = formatErrors(err)

expect(result.errors).toHaveLength(1)
expect(result.errors[0]).toMatchObject({
name: 'APIError',
message: 'Something went wrong',
data: { detail: 'bad input' },
})
})

it('should format a Mongoose-style ValidationError', () => {
const mongooseError = {
name: 'ValidationError',
errors: {
email: { path: 'email', message: 'is invalid' },
},
}

const result = formatErrors(mongooseError as any)

expect(result.errors).toHaveLength(1)
expect(result.errors[0]).toMatchObject({ field: 'email', message: 'is invalid' })
})

it('should format an array message error', () => {
const err = { message: [{ message: 'item one' }, { message: 'item two' }] }

const result = formatErrors(err as any)

expect(result.errors).toHaveLength(2)
expect(result.errors[0]).toMatchObject({ message: 'item one' })
expect(result.errors[1]).toMatchObject({ message: 'item two' })
})

it('should format a named non-Payload error', () => {
const err = new Error('Unexpected failure')

const result = formatErrors(err as any)

expect(result.errors).toHaveLength(1)
expect(result.errors[0]!.message).toBe('Unexpected failure')
})

it('should format a Payload APIError with no data', () => {
const err = new APIError('Server error')

const result = formatErrors(err)

expect(result.errors).toHaveLength(1)
expect(result.errors[0]).toStrictEqual({ message: 'Server error' })
})

it('should return unknown error for null/undefined input', () => {
expect(formatErrors(null as any).errors[0]!.message).toBe('An unknown error occurred.')
expect(formatErrors(undefined as any).errors[0]!.message).toBe('An unknown error occurred.')
})
})
25 changes: 11 additions & 14 deletions packages/payload/src/utilities/formatErrors.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
import type { ErrorResult } from '../config/types.js'
import type { APIError } from '../errors/APIError.js'

import { APIErrorName } from '../errors/APIError.js'
import { ValidationErrorName } from '../errors/ValidationError.js'
import { APIError } from '../errors/APIError.js'
import { ValidationError } from '../errors/ValidationError.js'

export const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResult => {
if (incoming) {
// Cannot use `instanceof` to check error type: https://github.com/microsoft/TypeScript/issues/13965
// Instead, get the prototype of the incoming error and check its constructor name
const proto = Object.getPrototypeOf(incoming)

// Payload 'ValidationError' and 'APIError'
if (
(proto.constructor.name === ValidationErrorName || proto.constructor.name === APIErrorName) &&
incoming.data
) {
if ((incoming instanceof ValidationError || incoming instanceof APIError) && incoming.data) {
return {
errors: [
{
name: incoming.name as string,
name: incoming.name,
data: incoming.data as Record<string, unknown>,
message: incoming.message as string,
message: incoming.message,
},
],
}
}

// Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError
if (proto.constructor.name === ValidationErrorName && 'errors' in incoming && incoming.errors) {
if (
'name' in incoming &&
incoming.name === 'ValidationError' &&
'errors' in incoming &&
incoming.errors
) {
return {
errors: Object.keys(incoming.errors).reduce(
(acc, key) => {
Expand Down
9 changes: 3 additions & 6 deletions packages/plugin-nested-docs/src/hooks/resaveChildren.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CollectionAfterChangeHook, JsonObject, ValidationError } from 'payload'
import type { CollectionAfterChangeHook, JsonObject } from 'payload'

import { APIError, ValidationErrorName } from 'payload'
import { APIError, ValidationError } from 'payload'

import type { NestedDocsPluginConfig } from '../types.js'

Expand Down Expand Up @@ -91,10 +91,7 @@ export const resaveChildren =
)
req.payload.logger.error(err)

if (
(err as ValidationError)?.name === ValidationErrorName &&
(err as ValidationError)?.data?.errors?.length
) {
if (err instanceof ValidationError && err.data?.errors?.length) {
throw new APIError(
'Could not publish or save changes: One or more children are invalid.',
400,
Expand Down
Loading