Compare commits

...

5 Commits

Author SHA1 Message Date
Jacob Fletcher
96ddc79788 fix: field level validation errors were incorrectly throwing uniqueness errors 2025-01-16 09:23:45 -05:00
Jacob Fletcher
e45ce23cd7 resets community test suite 2025-01-14 16:57:37 -05:00
Jacob Fletcher
f17f36b62a removes log 2025-01-14 16:56:15 -05:00
Jacob Fletcher
802c4006b1 more validation skips 2025-01-14 16:53:51 -05:00
Jacob Fletcher
dc1b9bd8d6 perf: skips field validations until the form is submitted 2025-01-14 16:22:38 -05:00
18 changed files with 149 additions and 108 deletions

View File

@@ -10,7 +10,7 @@ export const handleError = ({
req, req,
}: { }: {
collection?: string collection?: string
error: unknown error: any
global?: string global?: string
req?: Partial<PayloadRequest> req?: Partial<PayloadRequest>
}) => { }) => {
@@ -18,8 +18,6 @@ export const handleError = ({
throw error throw error
} }
const message = req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique'
// Handle uniqueness error from MongoDB // Handle uniqueness error from MongoDB
if ('code' in error && error.code === 11000 && 'keyValue' in error && error.keyValue) { if ('code' in error && error.code === 11000 && 'keyValue' in error && error.keyValue) {
throw new ValidationError( throw new ValidationError(
@@ -27,7 +25,7 @@ export const handleError = ({
collection, collection,
errors: [ errors: [
{ {
message, message: req?.t ? req.t('error:valueMustBeUnique') : 'Value must be unique',
path: Object.keys(error.keyValue)[0], path: Object.keys(error.keyValue)[0],
}, },
], ],
@@ -37,5 +35,5 @@ export const handleError = ({
) )
} }
throw new APIError(message, httpStatus.BAD_REQUEST) throw new APIError(error.message, httpStatus.BAD_REQUEST)
} }

View File

@@ -88,6 +88,7 @@ export const Account: React.FC<AdminViewProps> = async ({
renderAllFields: true, renderAllFields: true,
req, req,
schemaPath: collectionConfig.slug, schemaPath: collectionConfig.slug,
skipValidation: true,
}) })
// Fetch document lock state // Fetch document lock state

View File

@@ -48,7 +48,7 @@ export const CreateFirstUserClient: React.FC<{
const collectionConfig = getEntityConfig({ collectionSlug: userSlug }) as ClientCollectionConfig const collectionConfig = getEntityConfig({ collectionSlug: userSlug }) as ClientCollectionConfig
const onChange: FormProps['onChange'][0] = React.useCallback( const onChange: FormProps['onChange'][0] = React.useCallback(
async ({ formState: prevFormState }) => { async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef) const controller = handleAbortRef(abortOnChangeRef)
const response = await getFormState({ const response = await getFormState({
@@ -59,6 +59,7 @@ export const CreateFirstUserClient: React.FC<{
operation: 'create', operation: 'create',
schemaPath: userSlug, schemaPath: userSlug,
signal: controller.signal, signal: controller.signal,
skipValidation: !submitted,
}) })
abortOnChangeRef.current = null abortOnChangeRef.current = null

View File

@@ -63,6 +63,7 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
renderAllFields: true, renderAllFields: true,
req, req,
schemaPath: collectionConfig.slug, schemaPath: collectionConfig.slug,
skipValidation: true,
}) })
return ( return (

View File

@@ -155,6 +155,7 @@ export const renderDocument = async ({
renderAllFields: true, renderAllFields: true,
req, req,
schemaPath: collectionSlug || globalSlug, schemaPath: collectionSlug || globalSlug,
skipValidation: true,
}), }),
]) ])

View File

@@ -225,6 +225,7 @@ const PreviewView: React.FC<Props> = ({
returnLockStatus: false, returnLockStatus: false,
schemaPath: entitySlug, schemaPath: entitySlug,
signal: controller.signal, signal: controller.signal,
skipValidation: true,
}) })
// Unlock the document after save // Unlock the document after save
@@ -267,7 +268,7 @@ const PreviewView: React.FC<Props> = ({
) )
const onChange: FormProps['onChange'][0] = useCallback( const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => { async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef) const controller = handleAbortRef(abortOnChangeRef)
const currentTime = Date.now() const currentTime = Date.now()
@@ -292,6 +293,7 @@ const PreviewView: React.FC<Props> = ({
returnLockStatus: isLockingEnabled ? true : false, returnLockStatus: isLockingEnabled ? true : false,
schemaPath, schemaPath,
signal: controller.signal, signal: controller.signal,
skipValidation: !submitted,
updateLastEdited, updateLastEdited,
}) })

View File

@@ -84,6 +84,7 @@ export type BuildFormStateArgs = {
req: PayloadRequest req: PayloadRequest
returnLockStatus?: boolean returnLockStatus?: boolean
schemaPath: string schemaPath: string
skipValidation?: boolean
updateLastEdited?: boolean updateLastEdited?: boolean
} & ( } & (
| { | {

View File

@@ -110,7 +110,7 @@ export function EditForm({ submitted }: EditFormProps) {
) )
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback( const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
async ({ formState: prevFormState }) => { async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef) const controller = handleAbortRef(abortOnChangeRef)
const docPreferences = await getDocPreferences() const docPreferences = await getDocPreferences()
@@ -123,6 +123,7 @@ export function EditForm({ submitted }: EditFormProps) {
operation: 'create', operation: 'create',
schemaPath, schemaPath,
signal: controller.signal, signal: controller.signal,
skipValidation: !submitted,
}) })
abortOnChangeRef.current = null abortOnChangeRef.current = null

View File

@@ -211,6 +211,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
operation: 'create', operation: 'create',
renderAllFields: true, renderAllFields: true,
schemaPath: collectionSlug, schemaPath: collectionSlug,
skipValidation: true,
}) })
initialStateRef.current = formStateWithoutFiles initialStateRef.current = formStateWithoutFiles
setHasInitializedState(true) setHasInitializedState(true)

View File

@@ -177,6 +177,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
operation: 'update', operation: 'update',
schemaPath: slug, schemaPath: slug,
signal: controller.signal, signal: controller.signal,
skipValidation: true,
}) })
setInitialState(result) setInitialState(result)
@@ -192,7 +193,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
}, [apiRoute, hasInitializedState, serverURL, slug, getFormState, user, collectionPermissions]) }, [apiRoute, hasInitializedState, serverURL, slug, getFormState, user, collectionPermissions])
const onChange: FormProps['onChange'][0] = useCallback( const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => { async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortFormStateRef) const controller = handleAbortRef(abortFormStateRef)
const { state } = await getFormState({ const { state } = await getFormState({
@@ -203,6 +204,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
operation: 'update', operation: 'update',
schemaPath: slug, schemaPath: slug,
signal: controller.signal, signal: controller.signal,
skipValidation: !submitted,
}) })
abortFormStateRef.current = null abortFormStateRef.current = null

View File

@@ -504,6 +504,7 @@ export const Form: React.FC<FormProps> = (props) => {
renderAllFields: true, renderAllFields: true,
schemaPath: collectionSlug ? collectionSlug : globalSlug, schemaPath: collectionSlug ? collectionSlug : globalSlug,
signal: controller.signal, signal: controller.signal,
skipValidation: true,
}) })
contextRef.current = { ...initContextState } as FormContextType contextRef.current = { ...initContextState } as FormContextType
@@ -664,6 +665,7 @@ export const Form: React.FC<FormProps> = (props) => {
// Edit view default onChange is in packages/ui/src/views/Edit/index.tsx. This onChange usually sends a form state request // Edit view default onChange is in packages/ui/src/views/Edit/index.tsx. This onChange usually sends a form state request
revalidatedFormState = await onChangeFn({ revalidatedFormState = await onChangeFn({
formState: deepCopyObjectSimpleWithoutReactComponents(contextRef.current.fields), formState: deepCopyObjectSimpleWithoutReactComponents(contextRef.current.fields),
submitted,
}) })
} }
@@ -698,7 +700,7 @@ export const Form: React.FC<FormProps> = (props) => {
`fields` updates before `modified`, because setModified is in a setTimeout. `fields` updates before `modified`, because setModified is in a setTimeout.
So on the first change, modified is false, so we don't trigger the effect even though we should. So on the first change, modified is false, so we don't trigger the effect even though we should.
**/ **/
[contextRef.current.fields, modified], [contextRef.current.fields, modified, submitted],
[dispatchFields, onChange], [dispatchFields, onChange],
{ {
delay: 250, delay: 250,

View File

@@ -39,7 +39,7 @@ export type FormProps = {
initialState?: FormState initialState?: FormState
isInitializing?: boolean isInitializing?: boolean
log?: boolean log?: boolean
onChange?: ((args: { formState: FormState }) => Promise<FormState>)[] onChange?: ((args: { formState: FormState; submitted?: boolean }) => Promise<FormState>)[]
onSubmit?: (fields: FormState, data: Data) => void onSubmit?: (fields: FormState, data: Data) => void
onSuccess?: (json: unknown) => Promise<FormState | void> | void onSuccess?: (json: unknown) => Promise<FormState | void> | void
redirect?: string redirect?: string

View File

@@ -47,34 +47,33 @@ type Args = {
renderAllFields: boolean renderAllFields: boolean
renderFieldFn?: RenderFieldMethod renderFieldFn?: RenderFieldMethod
req: PayloadRequest req: PayloadRequest
schemaPath: string schemaPath: string
skipValidation?: boolean
} }
export const fieldSchemasToFormState = async (args: Args): Promise<FormState> => { export const fieldSchemasToFormState = async ({
if (!args.clientFieldSchemaMap && args.renderFieldFn) { id,
clientFieldSchemaMap,
collectionSlug,
data = {},
fields,
fieldSchemaMap,
operation,
permissions,
preferences,
previousFormState,
renderAllFields,
renderFieldFn,
req,
schemaPath,
skipValidation,
}: Args): Promise<FormState> => {
if (!clientFieldSchemaMap && renderFieldFn) {
console.warn( console.warn(
'clientFieldSchemaMap is not passed to fieldSchemasToFormState - this will reduce performance', 'clientFieldSchemaMap is not passed to fieldSchemasToFormState - this will reduce performance',
) )
} }
const {
id,
clientFieldSchemaMap,
collectionSlug,
data = {},
fields,
fieldSchemaMap,
operation,
permissions,
preferences,
previousFormState,
renderAllFields,
renderFieldFn,
req,
schemaPath,
} = args
if (fields && fields.length) { if (fields && fields.length) {
const state: FormStateWithoutComponents = {} const state: FormStateWithoutComponents = {}
@@ -110,6 +109,7 @@ export const fieldSchemasToFormState = async (args: Args): Promise<FormState> =>
renderAllFields, renderAllFields,
renderFieldFn, renderFieldFn,
req, req,
skipValidation,
state, state,
}) })

View File

@@ -114,6 +114,7 @@ export const buildFormState = async (
}, },
returnLockStatus, returnLockStatus,
schemaPath = collectionSlug || globalSlug, schemaPath = collectionSlug || globalSlug,
skipValidation,
updateLastEdited, updateLastEdited,
} = args } = args
@@ -194,6 +195,7 @@ export const buildFormState = async (
renderFieldFn: renderField, renderFieldFn: renderField,
req, req,
schemaPath, schemaPath,
skipValidation,
}) })
// Maintain form state of auth / upload fields // Maintain form state of auth / upload fields

View File

@@ -286,6 +286,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
returnLockStatus: false, returnLockStatus: false,
schemaPath: schemaPathSegments.join('.'), schemaPath: schemaPathSegments.join('.'),
signal: controller.signal, signal: controller.signal,
skipValidation: true,
}) })
// Unlock the document after save // Unlock the document after save
@@ -329,7 +330,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
) )
const onChange: FormProps['onChange'][0] = useCallback( const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => { async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef) const controller = handleAbortRef(abortOnChangeRef)
const currentTime = Date.now() const currentTime = Date.now()
@@ -351,6 +352,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
formState: prevFormState, formState: prevFormState,
globalSlug, globalSlug,
operation, operation,
skipValidation: !submitted,
// Performance optimization: Setting it to false ensure that only fields that have explicit requireRender set in the form state will be rendered (e.g. new array rows). // Performance optimization: Setting it to false ensure that only fields that have explicit requireRender set in the form state will be rendered (e.g. new array rows).
// We only want to render ALL fields on initial render, not in onChange. // We only want to render ALL fields on initial render, not in onChange.
renderAllFields: false, renderAllFields: false,

View File

@@ -34,7 +34,7 @@ const collectionWithName = (collectionSlug: string): CollectionConfig => {
} }
} }
export const slug = 'posts' export const postsSlug = 'posts'
export const relationSlug = 'relation' export const relationSlug = 'relation'
export const pointSlug = 'point' export const pointSlug = 'point'
export const customIdSlug = 'custom-id' export const customIdSlug = 'custom-id'
@@ -51,7 +51,7 @@ export default buildConfigWithDefaults({
}, },
collections: [ collections: [
{ {
slug, slug: postsSlug,
access: openAccess, access: openAccess,
fields: [ fields: [
{ {
@@ -346,14 +346,14 @@ export default buildConfigWithDefaults({
// Relation - hasMany // Relation - hasMany
await payload.create({ await payload.create({
collection: slug, collection: postsSlug,
data: { data: {
relationHasManyField: rel1.id, relationHasManyField: rel1.id,
title: 'rel to hasMany', title: 'rel to hasMany',
}, },
}) })
await payload.create({ await payload.create({
collection: slug, collection: postsSlug,
data: { data: {
relationHasManyField: rel2.id, relationHasManyField: rel2.id,
title: 'rel to hasMany 2', title: 'rel to hasMany 2',
@@ -362,7 +362,7 @@ export default buildConfigWithDefaults({
// Relation - relationTo multi // Relation - relationTo multi
await payload.create({ await payload.create({
collection: slug, collection: postsSlug,
data: { data: {
relationMultiRelationTo: { relationMultiRelationTo: {
relationTo: relationSlug, relationTo: relationSlug,
@@ -374,7 +374,7 @@ export default buildConfigWithDefaults({
// Relation - relationTo multi hasMany // Relation - relationTo multi hasMany
await payload.create({ await payload.create({
collection: slug, collection: postsSlug,
data: { data: {
relationMultiRelationToHasMany: [ relationMultiRelationToHasMany: [
{ {

View File

@@ -18,7 +18,7 @@ import {
methods, methods,
pointSlug, pointSlug,
relationSlug, relationSlug,
slug, postsSlug,
} from './config.js' } from './config.js'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
@@ -55,7 +55,7 @@ describe('collections-rest', () => {
it('should find', async () => { it('should find', async () => {
const post1 = await createPost() const post1 = await createPost()
const post2 = await createPost() const post2 = await createPost()
const response = await restClient.GET(`/${slug}`) const response = await restClient.GET(`/${postsSlug}`)
const result = await response.json() const result = await response.json()
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
@@ -68,7 +68,7 @@ describe('collections-rest', () => {
it('should count', async () => { it('should count', async () => {
await createPost() await createPost()
await createPost() await createPost()
const response = await restClient.GET(`/${slug}/count`) const response = await restClient.GET(`/${postsSlug}/count`)
const result = await response.json() const result = await response.json()
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
@@ -78,7 +78,7 @@ describe('collections-rest', () => {
it('should find where id', async () => { it('should find where id', async () => {
const post1 = await createPost() const post1 = await createPost()
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { id: { equals: post1.id } }, where: { id: { equals: post1.id } },
}, },
@@ -95,7 +95,7 @@ describe('collections-rest', () => {
const post2 = await createPost() const post2 = await createPost()
const { docs, totalDocs } = await payload.find({ const { docs, totalDocs } = await payload.find({
collection: slug, collection: postsSlug,
overrideAccess: false, overrideAccess: false,
pagination: false, pagination: false,
}) })
@@ -111,7 +111,7 @@ describe('collections-rest', () => {
const { id, description } = await createPost({ description: 'desc' }) const { id, description } = await createPost({ description: 'desc' })
const updatedTitle = 'updated-title' const updatedTitle = 'updated-title'
const response = await restClient.PATCH(`/${slug}/${id}`, { const response = await restClient.PATCH(`/${postsSlug}/${id}`, {
body: JSON.stringify({ title: updatedTitle }), body: JSON.stringify({ title: updatedTitle }),
}) })
const { doc } = await response.json() const { doc } = await response.json()
@@ -128,7 +128,7 @@ describe('collections-rest', () => {
} }
const description = 'updated' const description = 'updated'
const response = await restClient.PATCH(`/${slug}`, { const response = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({ body: JSON.stringify({
description, description,
}), }),
@@ -151,7 +151,7 @@ describe('collections-rest', () => {
} }
const description = 'updated-description' const description = 'updated-description'
const response = await restClient.PATCH(`/${slug}`, { const response = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({ body: JSON.stringify({
description, description,
}), }),
@@ -167,7 +167,7 @@ describe('collections-rest', () => {
const { docs: resDocs } = await payload.find({ const { docs: resDocs } = await payload.find({
limit: 10, limit: 10,
collection: slug, collection: postsSlug,
where: { id: { in: ids } }, where: { id: { in: ids } },
}) })
expect(resDocs.at(-1).description).toEqual('to-update') expect(resDocs.at(-1).description).toEqual('to-update')
@@ -180,7 +180,7 @@ describe('collections-rest', () => {
const description = 'updated' const description = 'updated'
const response = await restClient.PATCH(`/${slug}`, { const response = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({ body: JSON.stringify({
description, description,
}), }),
@@ -193,7 +193,7 @@ describe('collections-rest', () => {
expect(errors).toHaveLength(1) expect(errors).toHaveLength(1)
const { docs } = await payload.find({ const { docs } = await payload.find({
collection: slug, collection: postsSlug,
}) })
expect(docs[0].description).not.toEqual(description) expect(docs[0].description).not.toEqual(description)
@@ -206,7 +206,7 @@ describe('collections-rest', () => {
} }
const description = 'updated' const description = 'updated'
const relationFieldResponse = await restClient.PATCH(`/${slug}`, { const relationFieldResponse = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({ body: JSON.stringify({
description, description,
}), }),
@@ -214,7 +214,7 @@ describe('collections-rest', () => {
}) })
expect(relationFieldResponse.status).toEqual(400) expect(relationFieldResponse.status).toEqual(400)
const relationMultiRelationToResponse = await restClient.PATCH(`/${slug}`, { const relationMultiRelationToResponse = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({ body: JSON.stringify({
description, description,
}), }),
@@ -223,7 +223,7 @@ describe('collections-rest', () => {
expect(relationMultiRelationToResponse.status).toEqual(400) expect(relationMultiRelationToResponse.status).toEqual(400)
const { docs } = await payload.find({ const { docs } = await payload.find({
collection: slug, collection: postsSlug,
}) })
expect(docs[0].description).not.toEqual(description) expect(docs[0].description).not.toEqual(description)
@@ -232,14 +232,14 @@ describe('collections-rest', () => {
it('should not bulk update with a read restricted field query', async () => { it('should not bulk update with a read restricted field query', async () => {
const { id } = await payload.create({ const { id } = await payload.create({
collection: slug, collection: postsSlug,
data: { data: {
restrictedField: 'restricted', restrictedField: 'restricted',
}, },
}) })
const description = 'description' const description = 'description'
const response = await restClient.PATCH(`/${slug}`, { const response = await restClient.PATCH(`/${postsSlug}`, {
body: JSON.stringify({ body: JSON.stringify({
description, description,
}), }),
@@ -249,7 +249,7 @@ describe('collections-rest', () => {
const doc = await payload.findByID({ const doc = await payload.findByID({
id, id,
collection: slug, collection: postsSlug,
}) })
expect(response.status).toEqual(400) expect(response.status).toEqual(400)
@@ -301,7 +301,7 @@ describe('collections-rest', () => {
await createPost({ description: `desc ${i}` }) await createPost({ description: `desc ${i}` })
} }
const response = await restClient.DELETE(`/${slug}`, { const response = await restClient.DELETE(`/${postsSlug}`, {
query: { where: { title: { equals: 'title' } } }, query: { where: { title: { equals: 'title' } } },
}) })
const { docs } = await response.json() const { docs } = await response.json()
@@ -481,7 +481,7 @@ describe('collections-rest', () => {
it('should delete', async () => { it('should delete', async () => {
const { id } = await createPost() const { id } = await createPost()
const response = await restClient.DELETE(`/${slug}/${id}`) const response = await restClient.DELETE(`/${postsSlug}/${id}`)
const { doc } = await response.json() const { doc } = await response.json()
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
@@ -491,7 +491,7 @@ describe('collections-rest', () => {
it('should include metadata', async () => { it('should include metadata', async () => {
await createPosts(11) await createPosts(11)
const result = await restClient.GET(`/${slug}`).then((res) => res.json()) const result = await restClient.GET(`/${postsSlug}`).then((res) => res.json())
expect(result.totalDocs).toBeGreaterThan(0) expect(result.totalDocs).toBeGreaterThan(0)
expect(result.limit).toBe(10) expect(result.limit).toBe(10)
@@ -534,7 +534,7 @@ describe('collections-rest', () => {
describe('regular relationship', () => { describe('regular relationship', () => {
it('query by property value', async () => { it('query by property value', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { relationField: { equals: relation.id } }, where: { relationField: { equals: relation.id } },
}, },
@@ -547,7 +547,7 @@ describe('collections-rest', () => {
}) })
it('should count query by property value', async () => { it('should count query by property value', async () => {
const response = await restClient.GET(`/${slug}/count`, { const response = await restClient.GET(`/${postsSlug}/count`, {
query: { query: {
where: { relationField: { equals: relation.id } }, where: { relationField: { equals: relation.id } },
}, },
@@ -559,7 +559,7 @@ describe('collections-rest', () => {
}) })
it('query by id', async () => { it('query by id', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { relationField: { equals: relation.id } }, where: { relationField: { equals: relation.id } },
}, },
@@ -573,13 +573,13 @@ describe('collections-rest', () => {
it('should query LIKE by ID', async () => { it('should query LIKE by ID', async () => {
const post = await payload.create({ const post = await payload.create({
collection: slug, collection: postsSlug,
data: { data: {
title: 'find me buddy', title: 'find me buddy',
}, },
}) })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
id: { id: {
@@ -600,7 +600,7 @@ describe('collections-rest', () => {
relationHasManyField: [relation.id, relation2.id], relationHasManyField: [relation.id, relation2.id],
}) })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { 'relationHasManyField.name': { equals: relation.name } }, where: { 'relationHasManyField.name': { equals: relation.name } },
}, },
@@ -612,7 +612,7 @@ describe('collections-rest', () => {
expect(result.totalDocs).toEqual(1) expect(result.totalDocs).toEqual(1)
// Query second relationship // Query second relationship
const response2 = await restClient.GET(`/${slug}`, { const response2 = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { 'relationHasManyField.name': { equals: relation2.name } }, where: { 'relationHasManyField.name': { equals: relation2.name } },
}, },
@@ -631,7 +631,7 @@ describe('collections-rest', () => {
}) })
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { 'relationMultiRelationTo.value': { equals: relation.id } }, where: { 'relationMultiRelationTo.value': { equals: relation.id } },
}, },
@@ -650,7 +650,7 @@ describe('collections-rest', () => {
}) })
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
and: [ and: [
@@ -678,7 +678,7 @@ describe('collections-rest', () => {
}) })
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { 'relationMultiRelationToHasMany.value': { equals: relation.id } }, where: { 'relationMultiRelationToHasMany.value': { equals: relation.id } },
}, },
@@ -690,7 +690,7 @@ describe('collections-rest', () => {
expect(result.totalDocs).toEqual(1) expect(result.totalDocs).toEqual(1)
// Query second relation // Query second relation
const response2 = await restClient.GET(`/${slug}`, { const response2 = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { 'relationMultiRelationToHasMany.value': { equals: relation.id } }, where: { 'relationMultiRelationToHasMany.value': { equals: relation.id } },
}, },
@@ -711,7 +711,7 @@ describe('collections-rest', () => {
const test = 'test' const test = 'test'
await createPost({ fakeLocalization: test }) await createPost({ fakeLocalization: test })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { fakeLocalization: { equals: test } }, where: { fakeLocalization: { equals: test } },
}, },
@@ -723,7 +723,7 @@ describe('collections-rest', () => {
}) })
it('should not error when attempting to sort on a field that does not exist', async () => { it('should not error when attempting to sort on a field that does not exist', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
sort: 'fake', sort: 'fake',
}, },
@@ -738,7 +738,7 @@ describe('collections-rest', () => {
const valueToQuery = 'valueToQuery' const valueToQuery = 'valueToQuery'
const post1 = await createPost({ title: valueToQuery }) const post1 = await createPost({ title: valueToQuery })
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { title: { equals: valueToQuery } }, where: { title: { equals: valueToQuery } },
}, },
@@ -754,7 +754,7 @@ describe('collections-rest', () => {
const post1 = await createPost({ title: 'not-equals' }) const post1 = await createPost({ title: 'not-equals' })
const post2 = await createPost() const post2 = await createPost()
const post3 = await createPost({ title: undefined }) const post3 = await createPost({ title: undefined })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { title: { not_equals: post1.title } }, where: { title: { not_equals: post1.title } },
}, },
@@ -769,7 +769,7 @@ describe('collections-rest', () => {
it('in', async () => { it('in', async () => {
const post1 = await createPost({ title: 'my-title' }) const post1 = await createPost({ title: 'my-title' })
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { title: { in: [post1.title] } }, where: { title: { in: [post1.title] } },
}, },
@@ -784,7 +784,7 @@ describe('collections-rest', () => {
it('not_in', async () => { it('not_in', async () => {
const post1 = await createPost({ title: 'not-me' }) const post1 = await createPost({ title: 'not-me' })
const post2 = await createPost() const post2 = await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { title: { not_in: [post1.title] } }, where: { title: { not_in: [post1.title] } },
}, },
@@ -805,7 +805,7 @@ describe('collections-rest', () => {
await createPost({ relationField: relationship.id, title: 'not-me' }) await createPost({ relationField: relationship.id, title: 'not-me' })
// await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' }) // await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' })
const post2 = await createPost({ title: 'me' }) const post2 = await createPost({ title: 'me' })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { relationField: { not_in: [relationship.id] } }, where: { relationField: { not_in: [relationship.id] } },
}, },
@@ -817,7 +817,7 @@ describe('collections-rest', () => {
expect(result.totalDocs).toEqual(1) expect(result.totalDocs).toEqual(1)
// do not want to error for empty arrays // do not want to error for empty arrays
const emptyNotInResponse = await restClient.GET(`/${slug}`, { const emptyNotInResponse = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { relationField: { not_in: [] } }, where: { relationField: { not_in: [] } },
}, },
@@ -835,7 +835,7 @@ describe('collections-rest', () => {
const post1 = await createPost({ relationField: relationship.id, title: 'me' }) const post1 = await createPost({ relationField: relationship.id, title: 'me' })
// await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' }) // await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' })
await createPost({ title: 'not-me' }) await createPost({ title: 'not-me' })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { relationField: { in: [relationship.id] } }, where: { relationField: { in: [relationship.id] } },
}, },
@@ -847,7 +847,7 @@ describe('collections-rest', () => {
expect(result.totalDocs).toEqual(1) expect(result.totalDocs).toEqual(1)
// do not want to error for empty arrays // do not want to error for empty arrays
const emptyNotInResponse = await restClient.GET(`/${slug}`, { const emptyNotInResponse = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { relationField: { in: [] } }, where: { relationField: { in: [] } },
}, },
@@ -859,7 +859,7 @@ describe('collections-rest', () => {
it('like', async () => { it('like', async () => {
const post1 = await createPost({ title: 'prefix-value' }) const post1 = await createPost({ title: 'prefix-value' })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { title: { like: 'prefix' } }, where: { title: { like: 'prefix' } },
}, },
@@ -881,7 +881,7 @@ describe('collections-rest', () => {
title: specialCharacters, title: specialCharacters,
}) })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
title: { title: {
@@ -902,7 +902,7 @@ describe('collections-rest', () => {
it('like - cyrillic characters', async () => { it('like - cyrillic characters', async () => {
const post1 = await createPost({ title: 'Тест' }) const post1 = await createPost({ title: 'Тест' })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
title: { title: {
@@ -921,7 +921,7 @@ describe('collections-rest', () => {
it('like - cyrillic characters in multiple words', async () => { it('like - cyrillic characters in multiple words', async () => {
const post1 = await createPost({ title: 'привет, это тест полезной нагрузки' }) const post1 = await createPost({ title: 'привет, это тест полезной нагрузки' })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
title: { title: {
@@ -939,7 +939,7 @@ describe('collections-rest', () => {
it('like - partial word match', async () => { it('like - partial word match', async () => {
const post = await createPost({ title: 'separate words should partially match' }) const post = await createPost({ title: 'separate words should partially match' })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
title: { title: {
@@ -958,7 +958,7 @@ describe('collections-rest', () => {
it('like - id should not crash', async () => { it('like - id should not crash', async () => {
const post = await createPost({ title: 'post' }) const post = await createPost({ title: 'post' })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
id: { id: {
@@ -974,7 +974,7 @@ describe('collections-rest', () => {
it('exists - true', async () => { it('exists - true', async () => {
const postWithDesc = await createPost({ description: 'exists' }) const postWithDesc = await createPost({ description: 'exists' })
await createPost({ description: undefined }) await createPost({ description: undefined })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
description: { description: {
@@ -993,7 +993,7 @@ describe('collections-rest', () => {
it('exists - false', async () => { it('exists - false', async () => {
const postWithoutDesc = await createPost({ description: undefined }) const postWithoutDesc = await createPost({ description: undefined })
await createPost({ description: 'exists' }) await createPost({ description: 'exists' })
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
description: { description: {
@@ -1018,7 +1018,7 @@ describe('collections-rest', () => {
}) })
it('greater_than', async () => { it('greater_than', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
number: { number: {
@@ -1035,7 +1035,7 @@ describe('collections-rest', () => {
}) })
it('greater_than_equal', async () => { it('greater_than_equal', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
number: { number: {
@@ -1054,7 +1054,7 @@ describe('collections-rest', () => {
}) })
it('less_than', async () => { it('less_than', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
number: { number: {
@@ -1071,7 +1071,7 @@ describe('collections-rest', () => {
}) })
it('less_than_equal', async () => { it('less_than_equal', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
number: { number: {
@@ -1312,7 +1312,7 @@ describe('collections-rest', () => {
const post2 = await createPost({ title: 'post2' }) const post2 = await createPost({ title: 'post2' })
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
or: [ or: [
@@ -1342,7 +1342,7 @@ describe('collections-rest', () => {
const post1 = await createPost({ title: 'post1' }) const post1 = await createPost({ title: 'post1' })
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
or: [ or: [
@@ -1374,7 +1374,7 @@ describe('collections-rest', () => {
await createPost({ description, title: 'post2' }) // Diff title, same desc await createPost({ description, title: 'post2' }) // Diff title, same desc
await createPost() await createPost()
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
and: [ and: [
@@ -1427,13 +1427,13 @@ describe('collections-rest', () => {
}, },
}, },
} }
let response = await restClient.GET(`/${slug}`, { query }) let response = await restClient.GET(`/${postsSlug}`, { query })
const page1 = await response.json() const page1 = await response.json()
response = await restClient.GET(`/${slug}`, { query: { ...query, page: 2 } }) response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 2 } })
const page2 = await response.json() const page2 = await response.json()
response = await restClient.GET(`/${slug}`, { query: { ...query, page: 3 } }) response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 3 } })
const page3 = await response.json() const page3 = await response.json()
expect(page1.hasNextPage).toStrictEqual(true) expect(page1.hasNextPage).toStrictEqual(true)
@@ -1476,13 +1476,13 @@ describe('collections-rest', () => {
}, },
}, },
} }
let response = await restClient.GET(`/${slug}`, { query }) let response = await restClient.GET(`/${postsSlug}`, { query })
const page1 = await response.json() const page1 = await response.json()
response = await restClient.GET(`/${slug}`, { query: { ...query, page: 2 } }) response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 2 } })
const page2 = await response.json() const page2 = await response.json()
response = await restClient.GET(`/${slug}`, { query: { ...query, page: 3 } }) response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 3 } })
const page3 = await response.json() const page3 = await response.json()
expect(page1.hasNextPage).toStrictEqual(true) expect(page1.hasNextPage).toStrictEqual(true)
@@ -1524,7 +1524,7 @@ describe('collections-rest', () => {
}) })
it('should query a limited set of docs', async () => { it('should query a limited set of docs', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
limit: 15, limit: 15,
where: { where: {
@@ -1541,7 +1541,7 @@ describe('collections-rest', () => {
}) })
it('should query all docs when limit=0', async () => { it('should query all docs when limit=0', async () => {
const response = await restClient.GET(`/${slug}`, { const response = await restClient.GET(`/${postsSlug}`, {
query: { query: {
limit: 0, limit: 0,
where: { where: {
@@ -1566,7 +1566,7 @@ describe('collections-rest', () => {
}) })
const result = await restClient const result = await restClient
.GET(`/${slug}`, { .GET(`/${postsSlug}`, {
query: { query: {
where: { where: {
'D1.D2.D3.D4': { 'D1.D2.D3.D4': {
@@ -1641,11 +1641,11 @@ describe('collections-rest', () => {
const post = await createPost({}) const post = await createPost({})
const response = await restClient.GET( const response = await restClient.GET(
`/${slug}/${typeof post.id === 'number' ? 1000 : randomUUID()}`, `/${postsSlug}/${typeof post.id === 'number' ? 1000 : randomUUID()}`,
) )
expect(response.status).toBe(404) expect(response.status).toBe(404)
expect(collection.slug).toBe(slug) expect(collection.slug).toBe(postsSlug)
expect(err).toBeInstanceOf(NotFound) expect(err).toBeInstanceOf(NotFound)
expect(errResult).toStrictEqual({ expect(errResult).toStrictEqual({
errors: [ errors: [
@@ -1660,6 +1660,32 @@ describe('collections-rest', () => {
payload.collections.posts.config.hooks.afterError = [] payload.collections.posts.config.hooks.afterError = []
}) })
it('should return field-level validation errors', async () => {
let errorMessage: string
try {
const result = await payload.create({
collection: postsSlug,
data: {
D1: {
D2: {
D3: {
// @ts-expect-error
D4: {},
},
},
},
},
})
} catch (e) {
errorMessage = e.message
}
await expect(errorMessage).toBe(
'posts validation failed: D1.D2.D3.D4: Cast to string failed for value "{}" (type Object) at path "D4"',
)
})
}) })
describe('Local', () => { describe('Local', () => {
@@ -1695,7 +1721,7 @@ describe('collections-rest', () => {
async function createPost(overrides?: Partial<Post>) { async function createPost(overrides?: Partial<Post>) {
const { doc } = await restClient const { doc } = await restClient
.POST(`/${slug}`, { .POST(`/${postsSlug}`, {
body: JSON.stringify({ title: 'title', ...overrides }), body: JSON.stringify({ title: 'title', ...overrides }),
}) })
.then((res) => res.json()) .then((res) => res.json())
@@ -1710,7 +1736,7 @@ async function createPosts(count: number) {
async function clearDocs(): Promise<void> { async function clearDocs(): Promise<void> {
await payload.delete({ await payload.delete({
collection: slug, collection: postsSlug,
where: { id: { exists: true } }, where: { id: { exists: true } },
}) })
} }

View File

@@ -28,7 +28,7 @@
} }
], ],
"paths": { "paths": {
"@payload-config": ["./test/field-perf/config.ts"], "@payload-config": ["./test/collections-rest/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"], "@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],