import type { Payload, SanitizedCollectionConfig } from 'payload' import { randomBytes, randomUUID } from 'crypto' import path from 'path' import { APIError, NotFound } from 'payload' import { fileURLToPath } from 'url' import type { NextRESTClient } from '../helpers/NextRESTClient.js' import type { Relation } from './config.js' import type { Post } from './payload-types.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' import { customIdNumberSlug, customIdSlug, endpointsSlug, errorOnHookSlug, methods, pointSlug, postsSlug, relationSlug, } from './config.js' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) let restClient: NextRESTClient let payload: Payload describe('collections-rest', () => { beforeAll(async () => { ;({ payload, restClient } = await initPayloadInt(dirname)) }) afterAll(async () => { if (typeof payload.db.destroy === 'function') { await payload.db.destroy() } }) beforeEach(async () => { await clearDocs() }) describe('CRUD', () => { it('should create', async () => { const data = { title: 'title', } const doc = await createPost(data) expect(doc).toMatchObject(data) }) it('should find', async () => { const post1 = await createPost() const post2 = await createPost() const response = await restClient.GET(`/${postsSlug}`) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(2) const expectedDocs = [post1, post2] expect(result.docs).toHaveLength(expectedDocs.length) expect(result.docs).toEqual(expect.arrayContaining(expectedDocs)) }) it('should count', async () => { await createPost() await createPost() const response = await restClient.GET(`/${postsSlug}/count`) const result = await response.json() expect(response.status).toEqual(200) expect(result).toEqual({ totalDocs: 2 }) }) it('should find where id', async () => { const post1 = await createPost() await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { id: { equals: post1.id } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(1) expect(result.docs[0].id).toEqual(post1.id) }) it('should find with pagination false', async () => { const post1 = await createPost() const post2 = await createPost() const { docs, totalDocs } = await payload.find({ collection: postsSlug, overrideAccess: false, pagination: false, }) const expectedDocs = [post1, post2] expect(docs).toHaveLength(expectedDocs.length) expect(docs).toEqual(expect.arrayContaining(expectedDocs)) expect(totalDocs).toEqual(2) }) it('should update existing', async () => { const { id, description } = await createPost({ description: 'desc' }) const updatedTitle = 'updated-title' const response = await restClient.PATCH(`/${postsSlug}/${id}`, { body: JSON.stringify({ title: updatedTitle }), }) const { doc } = await response.json() expect(response.status).toEqual(200) expect(doc.title).toEqual(updatedTitle) expect(doc.description).toEqual(description) // Check was not modified }) describe('Bulk operations', () => { it('should bulk update', async () => { for (let i = 0; i < 11; i++) { await createPost({ description: `desc ${i}` }) } const description = 'updated' const response = await restClient.PATCH(`/${postsSlug}`, { body: JSON.stringify({ description, }), query: { where: { title: { equals: 'title' } } }, }) const { docs, errors } = await response.json() expect(errors).toHaveLength(0) expect(response.status).toEqual(200) expect(docs[0].title).toEqual('title') // Check was not modified expect(docs[0].description).toEqual(description) expect(docs.pop().description).toEqual(description) }) it('should bulk update with limit', async () => { const ids = [] for (let i = 0; i < 3; i++) { const post = await createPost({ description: `to-update` }) ids.push(post.id) } const description = 'updated-description' const response = await restClient.PATCH(`/${postsSlug}`, { body: JSON.stringify({ description, }), query: { limit: 2, where: { id: { in: ids } } }, }) const { docs, errors } = await response.json() expect(errors).toHaveLength(0) expect(response.status).toEqual(200) expect(docs).toHaveLength(2) expect(docs[0].description).toEqual(description) expect(docs.pop().description).toEqual(description) const { docs: resDocs } = await payload.find({ limit: 10, collection: postsSlug, where: { id: { in: ids } }, }) expect(resDocs.at(-1).description).toEqual('to-update') }) it('should not bulk update with a bad query', async () => { for (let i = 0; i < 2; i++) { await createPost({ description: `desc ${i}` }) } const description = 'updated' const response = await restClient.PATCH(`/${postsSlug}`, { body: JSON.stringify({ description, }), query: { where: { missing: { equals: 'title' } } }, }) const { docs: noDocs, errors } = await response.json() expect(response.status).toEqual(400) expect(noDocs).toBeUndefined() expect(errors).toHaveLength(1) const { docs } = await payload.find({ collection: postsSlug, }) expect(docs[0].description).not.toEqual(description) expect(docs.pop().description).not.toEqual(description) }) it('should not bulk update with a bad relationship query', async () => { for (let i = 0; i < 2; i++) { await createPost({ description: `desc ${i}` }) } const description = 'updated' const relationFieldResponse = await restClient.PATCH(`/${postsSlug}`, { body: JSON.stringify({ description, }), query: { where: { 'relationField.missing': { equals: 'title' } } }, }) expect(relationFieldResponse.status).toEqual(400) const relationMultiRelationToResponse = await restClient.PATCH(`/${postsSlug}`, { body: JSON.stringify({ description, }), query: { where: { 'relationMultiRelationTo.missing': { equals: 'title' } } }, }) expect(relationMultiRelationToResponse.status).toEqual(400) const { docs } = await payload.find({ collection: postsSlug, }) expect(docs[0].description).not.toEqual(description) expect(docs.pop().description).not.toEqual(description) }) it('should not bulk update with a read restricted field query', async () => { const { id } = await payload.create({ collection: postsSlug, data: { restrictedField: 'restricted', }, }) const description = 'description' const response = await restClient.PATCH(`/${postsSlug}`, { body: JSON.stringify({ description, }), query: { where: { restrictedField: { equals: 'restricted' } } }, }) const result = await response.json() const doc = await payload.findByID({ id, collection: postsSlug, }) expect(response.status).toEqual(400) expect(result.errors).toHaveLength(1) expect(result.errors[0].message).toEqual( 'The following path cannot be queried: restrictedField', ) expect(doc.description).toBeFalsy() }) it('should return formatted errors for bulk updates', async () => { const text = 'bulk-update-test-errors' const errorDoc = await payload.create({ collection: errorOnHookSlug, data: { errorBeforeChange: true, text, }, }) const successDoc = await payload.create({ collection: errorOnHookSlug, data: { errorBeforeChange: false, text, }, }) const update = 'update' const response = await restClient.PATCH(`/${errorOnHookSlug}`, { body: JSON.stringify({ text: update, }), query: { where: { text: { equals: text } } }, }) const result = await response.json() expect(response.status).toEqual(400) expect(result.docs).toHaveLength(1) expect(result.docs[0].id).toEqual(successDoc.id) expect(result.errors).toHaveLength(1) expect(result.errors[0].message).toBeDefined() expect(result.errors[0].id).toEqual(errorDoc.id) expect(result.docs[0].text).toEqual(update) }) it('should bulk delete', async () => { const count = 11 for (let i = 0; i < count; i++) { await createPost({ description: `desc ${i}` }) } const response = await restClient.DELETE(`/${postsSlug}`, { query: { where: { title: { equals: 'title' } } }, }) const { docs } = await response.json() expect(response.status).toEqual(200) expect(docs[0].title).toEqual('title') // Check was not modified expect(docs).toHaveLength(count) }) it('should return formatted errors for bulk deletes', async () => { await payload.create({ collection: errorOnHookSlug, data: { errorAfterDelete: true, text: 'test', }, }) await payload.create({ collection: errorOnHookSlug, data: { errorAfterDelete: false, text: 'test', }, }) const response = await restClient.DELETE(`/${errorOnHookSlug}`, { query: { where: { text: { equals: 'test' } } }, }) const result = await response.json() expect(response.status).toEqual(400) expect(result.docs).toHaveLength(1) expect(result.errors).toHaveLength(1) expect(result.errors[0].message).toBeDefined() expect(result.errors[0].id).toBeDefined() }) }) describe('Custom ID', () => { describe('string', () => { it('should create', async () => { const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}` const customIdName = 'custom-id-name' const { doc } = await restClient .POST(`/${customIdSlug}`, { body: JSON.stringify({ id: customId, name: customIdName }), }) .then((res) => res.json()) expect(doc.id).toEqual(customId) expect(doc.name).toEqual(customIdName) }) it('should find', async () => { const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}` const { doc } = await restClient .POST(`/${customIdSlug}`, { body: JSON.stringify({ id: customId, name: 'custom-id-name' }), }) .then((res) => res.json()) const { id } = await restClient .GET(`/${customIdSlug}/${customId}`) .then((res) => res.json()) expect(id).toEqual(doc.id) }) it('should query - equals', async () => { const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}` const { doc } = await restClient .POST(`/${customIdSlug}`, { body: JSON.stringify({ id: customId, name: 'custom-id-name' }), }) .then((res) => res.json()) const { docs } = await restClient .GET(`/${customIdSlug}`, { query: { where: { id: { equals: customId } }, }, }) .then((res) => res.json()) expect(docs.map(({ id }) => id)).toContain(doc.id) }) it('should query - like', async () => { const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}` const { doc } = await restClient .POST(`/${customIdSlug}`, { body: JSON.stringify({ id: customId, name: 'custom-id-name' }), }) .then((res) => res.json()) const { docs } = await restClient .GET(`/${customIdSlug}`, { query: { where: { id: { like: 'custom' } }, }, }) .then((res) => res.json()) expect(docs.map(({ id }) => id)).toContain(doc.id) }) it('should update', async () => { const customId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}` const { doc } = await restClient .POST(`/${customIdSlug}`, { body: JSON.stringify({ id: customId, name: 'custom-id-name' }), }) .then((res) => res.json()) const { doc: updatedDoc } = await restClient .PATCH(`/${customIdSlug}/${doc.id}`, { body: JSON.stringify({ name: 'updated' }), }) .then((res) => res.json()) expect(updatedDoc.name).toEqual('updated') }) }) describe('number', () => { it('should create', async () => { const customId = Math.floor(Math.random() * 1_000_000) + 1 const { doc } = await restClient .POST(`/${customIdNumberSlug}`, { body: JSON.stringify({ id: customId, name: 'custom-id-number-name' }), }) .then((res) => res.json()) expect(doc.id).toEqual(customId) }) it('should find', async () => { const customId = Math.floor(Math.random() * 1_000_000) + 1 const { doc } = await restClient .POST(`/${customIdNumberSlug}`, { body: JSON.stringify({ id: customId, name: 'custom-id-number-name' }), }) .then((res) => res.json()) const { id } = await restClient .GET(`/${customIdNumberSlug}/${customId}`) .then((res) => res.json()) expect(id).toEqual(doc.id) }) it('should update', async () => { const customId = Math.floor(Math.random() * 1_000_000) + 1 const { doc } = await restClient .POST(`/${customIdNumberSlug}`, { body: JSON.stringify({ id: customId, name: 'custom-id-number-name' }), }) .then((res) => res.json()) const { doc: updatedDoc } = await restClient .PATCH(`/${customIdNumberSlug}/${doc.id}`, { body: JSON.stringify({ name: 'updated' }), }) .then((res) => res.json()) expect(updatedDoc.name).toEqual('updated') }) it('should allow querying by in', async () => { const id = 98234698237 await restClient.POST(`/${customIdNumberSlug}`, { body: JSON.stringify({ id, name: 'query using in operator' }), }) const { docs } = await restClient .GET(`/${customIdNumberSlug}`, { query: { where: { id: { in: `${id}, ${2349856723948764}` } }, }, }) .then((res) => res.json()) expect(docs).toHaveLength(1) }) }) }) it('should delete', async () => { const { id } = await createPost() const response = await restClient.DELETE(`/${postsSlug}/${id}`) const { doc } = await response.json() expect(response.status).toEqual(200) expect(doc.id).toEqual(id) }) it('should include metadata', async () => { await createPosts(11) const result = await restClient.GET(`/${postsSlug}`).then((res) => res.json()) expect(result.totalDocs).toBeGreaterThan(0) expect(result.limit).toBe(10) expect(result.page).toBe(1) expect(result.pagingCounter).toBe(1) expect(result.hasPrevPage).toBe(false) expect(result.hasNextPage).toBe(true) expect(result.prevPage).toBeNull() expect(result.nextPage).toBe(2) }) }) describe('Querying', () => { it.todo('should allow querying by a field within a group') describe('Relationships', () => { let post: Post let relation: Relation let relation2: Relation const nameToQuery = 'name' const nameToQuery2 = 'name2' beforeEach(async () => { ;({ doc: relation } = await restClient .POST(`/${relationSlug}`, { body: JSON.stringify({ name: nameToQuery }), }) .then((res) => res.json())) ;({ doc: relation2 } = await restClient .POST(`/${relationSlug}`, { body: JSON.stringify({ name: nameToQuery2 }), }) .then((res) => res.json())) post = await createPost({ relationField: relation.id, }) await createPost() // Extra post to allow asserting totalDoc count }) describe('regular relationship', () => { it('query by property value', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { where: { relationField: { equals: relation.id } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post]) expect(result.totalDocs).toEqual(1) }) it('should count query by property value', async () => { const response = await restClient.GET(`/${postsSlug}/count`, { query: { where: { relationField: { equals: relation.id } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result).toEqual({ totalDocs: 1 }) }) it('query by id', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { where: { relationField: { equals: relation.id } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post]) expect(result.totalDocs).toEqual(1) }) it('should query LIKE by ID', async () => { const post = await payload.create({ collection: postsSlug, data: { title: 'find me buddy', }, }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { id: { like: post.id, }, }, }, }) const result = await response.json() expect(response.status).toStrictEqual(200) expect(result.totalDocs).toStrictEqual(1) }) }) it('should query nested relationship - hasMany', async () => { const post1 = await createPost({ relationHasManyField: [relation.id, relation2.id], }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { 'relationHasManyField.name': { equals: relation.name } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) // Query second relationship const response2 = await restClient.GET(`/${postsSlug}`, { query: { where: { 'relationHasManyField.name': { equals: relation2.name } }, }, }) const result2 = await response2.json() expect(response2.status).toEqual(200) expect(result2.docs).toEqual([post1]) expect(result2.totalDocs).toEqual(1) }) describe('relationTo multi', () => { it('nested by id', async () => { const post1 = await createPost({ relationMultiRelationTo: { relationTo: relationSlug, value: relation.id }, }) await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { 'relationMultiRelationTo.value': { equals: relation.id } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) }) }) it('should query relationships by not_equals', async () => { const ogPost = await createPost({ relationMultiRelationTo: { relationTo: relationSlug, value: relation.id }, }) await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { and: [ { 'relationMultiRelationTo.value': { not_equals: relation.id }, }, ], }, }, }) const result = await response.json() expect(response.status).toEqual(200) const foundExcludedDoc = result.docs.some((doc) => ogPost.id === doc.id) expect(foundExcludedDoc).toBe(false) }) describe('relationTo multi hasMany', () => { it('nested by id', async () => { const post1 = await createPost({ relationMultiRelationToHasMany: [ { relationTo: relationSlug, value: relation.id }, { relationTo: relationSlug, value: relation2.id }, ], }) await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { 'relationMultiRelationToHasMany.value': { equals: relation.id } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) // Query second relation const response2 = await restClient.GET(`/${postsSlug}`, { query: { where: { 'relationMultiRelationToHasMany.value': { equals: relation.id } }, }, }) const result2 = await response2.json() expect(response2.status).toEqual(200) expect(result2.docs).toEqual([post1]) expect(result2.totalDocs).toEqual(1) }) it.todo('nested by property value') }) }) describe('Edge cases', () => { it('should query a localized field without localization configured', async () => { const test = 'test' await createPost({ fakeLocalization: test }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { fakeLocalization: { equals: test } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(1) }) it('should not error when attempting to sort on a field that does not exist', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { sort: 'fake', }, }) expect(response.status).toStrictEqual(200) }) }) describe('Operators', () => { it('equals', async () => { const valueToQuery = 'valueToQuery' const post1 = await createPost({ title: valueToQuery }) await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { equals: valueToQuery } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(1) expect(result.docs).toEqual([post1]) }) it('not_equals', async () => { const post1 = await createPost({ title: 'not-equals' }) const post2 = await createPost() const post3 = await createPost({ title: undefined }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { not_equals: post1.title } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(2) expect(result.docs).toEqual([post3, post2]) }) it('in', async () => { const post1 = await createPost({ title: 'my-title' }) await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { in: [post1.title] } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) }) it('not_in', async () => { const post1 = await createPost({ title: 'not-me' }) const post2 = await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { not_in: [post1.title] } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post2]) expect(result.totalDocs).toEqual(1) }) it('not_in (relationships)', async () => { const relationship = await payload.create({ collection: relationSlug, data: {}, }) await createPost({ relationField: relationship.id, title: 'not-me' }) // await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' }) const post2 = await createPost({ title: 'me' }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { relationField: { not_in: [relationship.id] } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post2]) expect(result.totalDocs).toEqual(1) // do not want to error for empty arrays const emptyNotInResponse = await restClient.GET(`/${postsSlug}`, { query: { where: { relationField: { not_in: [] } }, }, }) expect(emptyNotInResponse.status).toEqual(200) }) it('in (relationships)', async () => { const relationship = await payload.create({ collection: relationSlug, data: {}, }) const post1 = await createPost({ relationField: relationship.id, title: 'me' }) // await createPost({ relationMultiRelationTo: relationship.id, title: 'not-me' }) await createPost({ title: 'not-me' }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { relationField: { in: [relationship.id] } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) // do not want to error for empty arrays const emptyNotInResponse = await restClient.GET(`/${postsSlug}`, { query: { where: { relationField: { in: [] } }, }, }) expect(emptyNotInResponse.status).toEqual(200) }) it('like', async () => { const post1 = await createPost({ title: 'prefix-value' }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { like: 'prefix' } }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) }) describe('like - special characters', () => { const specialCharacters = '~!@#$%^&*()_+-+[]{}|;:"<>,.?/})' it.each(specialCharacters.split(''))( 'like - special characters - %s', async (character) => { const post1 = await createPost({ title: specialCharacters, }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { like: character, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) }, ) }) it('like - cyrillic characters', async () => { const post1 = await createPost({ title: 'Тест' }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { like: 'Тест', }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) }) it('like - cyrillic characters in multiple words', async () => { const post1 = await createPost({ title: 'привет, это тест полезной нагрузки' }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { like: 'привет нагрузки', }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post1]) expect(result.totalDocs).toEqual(1) }) it('like - partial word match', async () => { const post = await createPost({ title: 'separate words should partially match' }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { title: { like: 'words partial', }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toEqual([post]) expect(result.totalDocs).toEqual(1) }) it('like - id should not crash', async () => { const post = await createPost({ title: 'post' }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { id: { like: 'words partial', }, }, }, }) expect(response.status).toEqual(200) }) it('exists - true', async () => { const postWithDesc = await createPost({ description: 'exists' }) await createPost({ description: undefined }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { description: { exists: true, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(1) expect(result.docs).toEqual([postWithDesc]) }) it('exists - false', async () => { const postWithoutDesc = await createPost({ description: undefined }) await createPost({ description: 'exists' }) const response = await restClient.GET(`/${postsSlug}`, { query: { where: { description: { exists: false, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(1) expect(result.docs).toEqual([postWithoutDesc]) }) describe('numbers', () => { let post1: Post let post2: Post beforeEach(async () => { post1 = await createPost({ number: 1 }) post2 = await createPost({ number: 2 }) }) it('greater_than', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { where: { number: { greater_than: 1, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(1) expect(result.docs).toEqual([post2]) }) it('greater_than_equal', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { where: { number: { greater_than_equal: 1, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(2) const expectedDocs = [post1, post2] expect(result.docs).toHaveLength(expectedDocs.length) expect(result.docs).toEqual(expect.arrayContaining(expectedDocs)) }) it('less_than', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { where: { number: { less_than: 2, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(1) expect(result.docs).toEqual([post1]) }) it('less_than_equal', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { where: { number: { less_than_equal: 2, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(2) const expectedDocs = [post1, post2] expect(result.docs).toHaveLength(expectedDocs.length) expect(result.docs).toEqual(expect.arrayContaining(expectedDocs)) }) }) describe('near', () => { const point = [10, 20] const [lat, lng] = point it('should return a document near a point', async () => { if (payload.db.name === 'sqlite') { return } const near = `${lat + 0.01}, ${lng + 0.01}, 10000` const response = await restClient.GET(`/${pointSlug}`, { query: { where: { point: { near, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(1) const responseCount = await restClient.GET(`/${pointSlug}/count`, { query: { where: { point: { near, }, }, }, }) const resultCount = await responseCount.json() expect(responseCount.status).toEqual(200) expect(resultCount.totalDocs).toBe(1) }) it('should omit maxDistance and return a document from minDistance', async () => { if (payload.db.name === 'sqlite') { return } const near = `${lat + 0.01}, ${lng + 0.01}, null, 1500` const response = await restClient.GET(`/${pointSlug}`, { query: { where: { point: { near, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(1) }) it('should omit maxDistance and not return a document because exceeds minDistance', async () => { if (payload.db.name === 'sqlite') { return } const near = `${lat + 0.01}, ${lng + 0.01}, null, 1700` const response = await restClient.GET(`/${pointSlug}`, { query: { where: { point: { near, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(0) }) it('should not return a point far away', async () => { if (payload.db.name === 'sqlite') { return } const near = `${lng + 1}, ${lat + 1}, 5000` const response = await restClient.GET(`/${pointSlug}`, { query: { where: { point: { near, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(0) }) it('should sort find results by nearest distance', async () => { if (payload.db.name === 'sqlite') { return } // creating twice as many records as we are querying to get a random sample const promises = [] for (let i = 0; i < 11; i++) { // setTimeout used to randomize the creation timestamp setTimeout(() => { promises.push( payload.create({ collection: pointSlug, data: { // only randomize longitude to make distance comparison easy point: [Math.random(), 0], }, }), ) }, Math.random()) } await Promise.all(promises) const { docs } = await restClient .GET(`/${pointSlug}`, { query: { limit: 5, where: { point: { // querying large enough range to include all docs near: '0, 0, 100000, 0', }, }, }, }) .then((res) => res.json()) let previous = 0 docs.forEach(({ point: coordinates }) => { // the next document point should always be greater than the one before expect(previous).toBeLessThanOrEqual(coordinates[0]) ;[previous] = coordinates }) }) }) describe('within', () => { type Point = [number, number] const polygon: Point[] = [ [9.0, 19.0], // bottom-left [9.0, 21.0], // top-left [11.0, 21.0], // top-right [11.0, 19.0], // bottom-right [9.0, 19.0], // back to starting point to close the polygon ] it('should return a document with the point inside the polygon', async () => { if (payload.db.name === 'sqlite') { return } // There should be 1 total points document populated by default with the point [10, 20] const response = await restClient.GET(`/${pointSlug}`, { query: { where: { point: { within: { type: 'Polygon', coordinates: [polygon], }, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(1) }) it('should not return a document with the point outside a smaller polygon', async () => { if (payload.db.name === 'sqlite') { return } const response = await restClient.GET(`/${pointSlug}`, { query: { where: { point: { within: { type: 'Polygon', coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size }, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(0) }) }) describe('intersects', () => { type Point = [number, number] const polygon: Point[] = [ [9.0, 19.0], // bottom-left [9.0, 21.0], // top-left [11.0, 21.0], // top-right [11.0, 19.0], // bottom-right [9.0, 19.0], // back to starting point to close the polygon ] it('should return a document with the point intersecting the polygon', async () => { if (payload.db.name === 'sqlite') { return } // There should be 1 total points document populated by default with the point [10, 20] const response = await restClient.GET(`/${pointSlug}`, { query: { where: { point: { intersects: { type: 'Polygon', coordinates: [polygon], }, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(1) }) it('should not return a document with the point not intersecting a smaller polygon', async () => { if (payload.db.name === 'sqlite') { return } const response = await restClient.GET(`/${pointSlug}`, { query: { where: { point: { intersects: { type: 'Polygon', coordinates: [polygon.map((vertex) => vertex.map((coord) => coord * 0.1))], // Reduce polygon to 10% of its size }, }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(0) }) }) it('or', async () => { const post1 = await createPost({ title: 'post1' }) const post2 = await createPost({ title: 'post2' }) await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { or: [ { title: { equals: post1.title, }, }, { title: { equals: post2.title, }, }, ], }, }, }) const result = await response.json() expect(response.status).toEqual(200) const expectedDocs = [post1, post2] expect(result.totalDocs).toEqual(expectedDocs.length) expect(result.docs).toEqual(expect.arrayContaining(expectedDocs)) }) it('or - 1 result', async () => { const post1 = await createPost({ title: 'post1' }) await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { or: [ { title: { equals: post1.title, }, }, { title: { equals: 'non-existent', }, }, ], }, }, }) const result = await response.json() expect(response.status).toEqual(200) const expectedDocs = [post1] expect(result.totalDocs).toEqual(expectedDocs.length) expect(result.docs).toEqual(expect.arrayContaining(expectedDocs)) }) it('and', async () => { const description = 'description' const post1 = await createPost({ description, title: 'post1' }) await createPost({ description, title: 'post2' }) // Diff title, same desc await createPost() const response = await restClient.GET(`/${postsSlug}`, { query: { where: { and: [ { title: { equals: post1.title, }, }, { description: { equals: description, }, }, ], }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.totalDocs).toEqual(1) expect(result.docs).toEqual([post1]) }) describe('pagination', () => { let relatedDoc beforeEach(async () => { relatedDoc = await payload.create({ collection: relationSlug, data: { name: 'test', }, }) for (let i = 0; i < 10; i++) { await createPost({ number: i, relationField: relatedDoc.id, title: 'paginate-test', }) } }) it('should paginate with where query', async () => { const query = { limit: 4, where: { title: { equals: 'paginate-test', }, }, } let response = await restClient.GET(`/${postsSlug}`, { query }) const page1 = await response.json() response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 2 } }) const page2 = await response.json() response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 3 } }) const page3 = await response.json() expect(page1.hasNextPage).toStrictEqual(true) expect(page1.hasPrevPage).toStrictEqual(false) expect(page1.limit).toStrictEqual(4) expect(page1.nextPage).toStrictEqual(2) expect(page1.page).toStrictEqual(1) expect(page1.pagingCounter).toStrictEqual(1) expect(page1.prevPage).toStrictEqual(null) expect(page1.totalDocs).toStrictEqual(10) expect(page1.totalPages).toStrictEqual(3) expect(page2.hasNextPage).toStrictEqual(true) expect(page2.hasPrevPage).toStrictEqual(true) expect(page2.limit).toStrictEqual(4) expect(page2.nextPage).toStrictEqual(3) expect(page2.page).toStrictEqual(2) expect(page2.pagingCounter).toStrictEqual(5) expect(page2.prevPage).toStrictEqual(1) expect(page2.totalDocs).toStrictEqual(10) expect(page2.totalPages).toStrictEqual(3) expect(page3.hasNextPage).toStrictEqual(false) expect(page3.hasPrevPage).toStrictEqual(true) expect(page3.limit).toStrictEqual(4) expect(page3.nextPage).toStrictEqual(null) expect(page3.page).toStrictEqual(3) expect(page3.pagingCounter).toStrictEqual(9) expect(page3.prevPage).toStrictEqual(2) expect(page3.totalDocs).toStrictEqual(10) expect(page3.totalPages).toStrictEqual(3) }) it('should paginate with where query on relationships', async () => { const query = { limit: 4, where: { relationField: { equals: relatedDoc.id, }, }, } let response = await restClient.GET(`/${postsSlug}`, { query }) const page1 = await response.json() response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 2 } }) const page2 = await response.json() response = await restClient.GET(`/${postsSlug}`, { query: { ...query, page: 3 } }) const page3 = await response.json() expect(page1.hasNextPage).toStrictEqual(true) expect(page1.hasPrevPage).toStrictEqual(false) expect(page1.limit).toStrictEqual(4) expect(page1.nextPage).toStrictEqual(2) expect(page1.page).toStrictEqual(1) expect(page1.pagingCounter).toStrictEqual(1) expect(page1.prevPage).toStrictEqual(null) expect(page1.totalDocs).toStrictEqual(10) expect(page1.totalPages).toStrictEqual(3) expect(page2.hasNextPage).toStrictEqual(true) expect(page2.hasPrevPage).toStrictEqual(true) expect(page2.limit).toStrictEqual(4) expect(page2.nextPage).toStrictEqual(3) expect(page2.page).toStrictEqual(2) expect(page2.pagingCounter).toStrictEqual(5) expect(page2.prevPage).toStrictEqual(1) expect(page2.totalDocs).toStrictEqual(10) expect(page2.totalPages).toStrictEqual(3) expect(page3.hasNextPage).toStrictEqual(false) expect(page3.hasPrevPage).toStrictEqual(true) expect(page3.limit).toStrictEqual(4) expect(page3.nextPage).toStrictEqual(null) expect(page3.page).toStrictEqual(3) expect(page3.pagingCounter).toStrictEqual(9) expect(page3.prevPage).toStrictEqual(2) expect(page3.totalDocs).toStrictEqual(10) expect(page3.totalPages).toStrictEqual(3) }) describe('limit', () => { beforeEach(async () => { for (let i = 0; i < 50; i++) { await createPost({ number: i, title: 'limit-test' }) } }) it('should query a limited set of docs', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { limit: 15, where: { title: { equals: 'limit-test', }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(15) }) it('should query all docs when limit=0', async () => { const response = await restClient.GET(`/${postsSlug}`, { query: { limit: 0, where: { title: { equals: 'limit-test', }, }, }, }) const result = await response.json() expect(response.status).toEqual(200) expect(result.docs).toHaveLength(50) expect(result.totalPages).toEqual(1) }) }) }) it('can query deeply nested fields within rows, tabs, collapsibles', async () => { const withDeeplyNestedField = await createPost({ D1: { D2: { D3: { D4: 'nested message' } } }, }) const result = await restClient .GET(`/${postsSlug}`, { query: { where: { 'D1.D2.D3.D4': { equals: 'nested message', }, }, }, }) .then((res) => res.json()) expect(result.totalDocs).toEqual(1) expect(result.docs).toEqual([withDeeplyNestedField]) }) }) }) describe('Error Handler', () => { it('should return the minimum allowed information about internal errors', async () => { const response = await restClient.GET('/internal-error-here') const result = await response.json() expect(response.status).toBe(500) expect(Array.isArray(result.errors)).toEqual(true) expect(result.errors[0].message).toStrictEqual('Something went wrong.') }) it('should execute afterError hook on root level and modify result/status', async () => { let err: unknown let errResult: any payload.config.hooks.afterError = [ ({ error, result }) => { err = error errResult = result return { status: 400, response: { modified: true } } }, ] const response = await restClient.GET(`/api-error-here`) expect(response.status).toBe(400) expect(err).toBeInstanceOf(APIError) expect(errResult).toStrictEqual({ errors: [ { message: 'Something went wrong.', }, ], }) const result = await response.json() expect(result.modified).toBe(true) payload.config.hooks.afterError = [] }) it('should execute afterError hook on collection level and modify result', async () => { let err: unknown let errResult: any let collection: SanitizedCollectionConfig payload.collections.posts.config.hooks.afterError = [ ({ error, result, collection: incomingCollection }) => { err = error errResult = result collection = incomingCollection return { response: { modified: true } } }, ] const post = await createPost({}) const response = await restClient.GET( `/${postsSlug}/${typeof post.id === 'number' ? 1000 : randomUUID()}`, ) expect(response.status).toBe(404) expect(collection.slug).toBe(postsSlug) expect(err).toBeInstanceOf(NotFound) expect(errResult).toStrictEqual({ errors: [ { message: 'Not Found', }, ], }) const result = await response.json() expect(result.modified).toBe(true) payload.collections.posts.config.hooks.afterError = [] }) }) describe('Local', () => { it('findByID should throw NotFound if the doc was not found, if disableErrors: true then return null', async () => { const post = await createPost() const id = typeof post.id === 'string' ? randomUUID() : 999 await expect(payload.findByID({ collection: 'posts', id })).rejects.toBeInstanceOf(NotFound) await expect( payload.findByID({ collection: 'posts', id, disableErrors: true }), ).resolves.toBeNull() }) }) describe('Custom endpoints', () => { it('should execute custom root endpoints', async () => { for (const method of methods) { const response = await restClient[method.toUpperCase()](`/${method}-test`, {}) await expect(response.text()).resolves.toBe(`${method} response`) } }) it('should execute custom collection endpoints', async () => { for (const method of methods) { const response = await restClient[method.toUpperCase()]( `/${endpointsSlug}/${method}-test`, {}, ) await expect(response.text()).resolves.toBe(`${method} response`) } }) }) it('should not mount auth endpoints for collection without auth', async () => { const authEndpoints = [ { method: 'post', path: '/forgot-password', }, { method: 'post', path: '/login', }, { method: 'post', path: '/logout', }, { method: 'post', path: '/refresh-token', }, { method: 'post', path: '/first-register', }, { method: 'post', path: '/reset-password', }, { method: 'post', path: '/unlock', }, ] for (const endpoint of authEndpoints) { const result = await restClient[endpoint.method.toUpperCase()]( `/${endpointsSlug}${endpoint.path}`, ) expect(result.status).toBe(404) const json = await result.json() expect(json.message.startsWith('Route not found')).toBeTruthy() } }) it('should not mount upload endpoints for collection without auth', async () => { const uploadEndpoints = [ { method: 'get', path: '/paste-url/some-id', }, { method: 'get', path: '/file/some-filename.png', }, ] for (const endpoint of uploadEndpoints) { const result = await restClient[endpoint.method.toUpperCase()]( `/${endpointsSlug}${endpoint.path}`, ) expect(result.status).toBe(404) expect((await result.json()).message.startsWith('Route not found')).toBeTruthy() } }) }) async function createPost(overrides?: Partial) { const { doc } = await restClient .POST(`/${postsSlug}`, { body: JSON.stringify({ title: 'title', ...overrides }), }) .then((res) => res.json()) return doc } async function createPosts(count: number) { for (let i = 0; i < count; i++) { await createPost() } } async function clearDocs(): Promise { await payload.delete({ collection: postsSlug, where: { id: { exists: true } }, }) }