import { GraphQLClient } from 'graphql-request' import type { Post } from './payload-types' import payload from '../../packages/payload/src' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' import { initPayloadTest } from '../helpers/configHelpers' import configPromise, { pointSlug, slug } from './config' const title = 'title' let client: GraphQLClient describe('collections-graphql', () => { beforeAll(async () => { const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }) const config = await configPromise const url = `${serverURL}${config.routes.api}${config.routes.graphQL}` client = new GraphQLClient(url) }) afterAll(async () => { if (typeof payload.db.destroy === 'function') { await payload.db.destroy(payload) } }) describe('CRUD', () => { let existingDoc: Post beforeEach(async () => { existingDoc = await createPost() }) it('should create', async () => { const query = `mutation { createPost(data: {title: "${title}"}) { id title } }` const response = await client.request(query) const doc: Post = response.createPost expect(doc).toMatchObject({ title }) expect(doc.id.length).toBeGreaterThan(0) }) it('should create using graphql variables', async () => { const query = `mutation Create($title: String!) { createPost(data: {title: $title}) { id title } }` const response = await client.request(query, { title }) const doc: Post = response.createPost expect(doc).toMatchObject({ title }) expect(doc.id.length).toBeGreaterThan(0) }) it('should read', async () => { const query = `query { Post(id: "${existingDoc.id}") { id title } }` const response = await client.request(query) const doc: Post = response.Post expect(doc).toMatchObject({ id: existingDoc.id, title }) }) it('should find', async () => { const query = `query { Posts { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: existingDoc.id })) }) it('should retain payload api', async () => { const query = ` query { PayloadApiTestTwos { docs { payloadAPI relation { payloadAPI } } } } ` const response = await client.request(query) const res = response.PayloadApiTestTwos expect(res.docs[0].relation.payloadAPI).toStrictEqual('GraphQL') }) it('should update existing', async () => { const updatedTitle = 'updated title' const query = `mutation { updatePost(id: "${existingDoc.id}", data: { title: "${updatedTitle}"}) { id title } }` const response = await client.request(query) const doc: Post = response.updatePost expect(doc).toMatchObject({ id: existingDoc.id, title: updatedTitle }) }) it('should delete', async () => { const query = `mutation { deletePost(id: "${existingDoc.id}") { id title } }` const response = await client.request(query) const doc: Post = response.deletePost expect(doc).toMatchObject({ id: existingDoc.id }) }) }) describe('Querying', () => { describe('Operators', () => { let post1: Post let post2: Post beforeEach(async () => { post1 = await createPost({ title: 'post1' }) post2 = await createPost({ title: 'post2' }) }) it('equals', async () => { const query = `query { Posts(where:{title: {equals:"${post1.title}"}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: post1.id, title: post1.title })) }) it('not_equals', async () => { const query = `query { Posts(where:{title: {not_equals:"${post1.title}"}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs[0]).toMatchObject({ id: post2.id, title: post2.title }) }) it('like', async () => { const postWithWords = await createPost({ title: 'the quick brown fox' }) const query = `query { Posts(where:{title: {like:"${postWithWords.title?.split(' ')[1]}"}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs[0]).toMatchObject({ id: postWithWords.id, title: postWithWords.title }) }) it('contains', async () => { const query = `query { Posts(where:{title: {contains:"${post1.title?.slice(0, 4)}"}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: post1.id, title: post1.title })) expect(docs).toContainEqual(expect.objectContaining({ id: post2.id, title: post2.title })) }) it('exists - true', async () => { const withDescription = await createPost({ description: 'description' }) const query = `query { Posts(where:{description: {exists:true}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual( expect.objectContaining({ id: withDescription.id, title: withDescription.title }), ) }) it('exists - false', async () => { const withDescription = await createPost({ description: 'description' }) const query = `query { Posts(where:{description: {exists:false}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).not.toContainEqual(expect.objectContaining({ id: withDescription.id })) expect(docs).toContainEqual(expect.objectContaining({ id: post1.id })) }) describe('numbers', () => { let numPost1: Post let numPost2: Post beforeEach(async () => { numPost1 = await createPost({ number: 1 }) numPost2 = await createPost({ number: 2 }) }) it('greater_than', async () => { const query = `query { Posts(where:{number: {greater_than:1}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: numPost2.id })) }) it('greater_than_equal', async () => { const query = `query { Posts(where:{number: {greater_than_equal:1}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: numPost1.id })) expect(docs).toContainEqual(expect.objectContaining({ id: numPost2.id })) }) it('less_than', async () => { const query = `query { Posts(where:{number: {less_than:2}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: numPost1.id })) }) it('less_than_equal', async () => { const query = `query { Posts(where:{number: {less_than_equal:2}}) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: numPost1.id })) expect(docs).toContainEqual(expect.objectContaining({ id: numPost2.id })) }) }) it('or', async () => { const query = `query { Posts( where: {OR: [{ title: { equals: "${post1.title}" } }, { title: { equals: "${post2.title}" } }] }) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: post1.id })) expect(docs).toContainEqual(expect.objectContaining({ id: post2.id })) }) it('or - 1 result', async () => { const query = `query { Posts( where: {OR: [{ title: { equals: "${post1.title}" } }, { title: { equals: "nope" } }] }) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: post1.id })) expect(docs).not.toContainEqual(expect.objectContaining({ id: post2.id })) }) it('and', async () => { const specialPost = await createPost({ description: 'special-123123' }) const query = `query { Posts( where: { AND: [ { title: { equals: "${specialPost.title}" } } { description: { equals: "${specialPost.description}" } } ] }) { docs { id title } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual(expect.objectContaining({ id: specialPost.id })) }) describe('near', () => { const point = [10, 20] const [lat, lng] = point it('should return a document near a point', async () => { const nearQuery = ` query { Points( where: { point: { near: [${lat + 0.01}, ${lng + 0.01}, 10000] } } ) { docs { id point } } }` const response = await client.request(nearQuery) const { docs } = response.Points expect(docs).toHaveLength(1) }) it('should not return a point far away', async () => { const nearQuery = ` query { Points( where: { point: { near: [${lng + 1}, ${lat - 1}, 5000] } } ) { docs { id point } } }` const response = await client.request(nearQuery) const { docs } = response.Points expect(docs).toHaveLength(0) }) it('should sort find results by nearest distance', async () => { // creating twice as many records as we are querying to get a random sample await mapAsync([...Array(10)], async () => { // setTimeout used to randomize the creation timestamp setTimeout(async () => { await payload.create({ collection: pointSlug, data: { // only randomize longitude to make distance comparison easy point: [Math.random(), 0], }, }) }, Math.random()) }) const nearQuery = ` query { Points( where: { point: { near: [0, 0, 100000, 0] } }, limit: 5 ) { docs { id point } } }` const response = await client.request(nearQuery) const { docs } = response.Points 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 () => { const query = ` query { Points( where: { point: { within: { type: "Polygon", coordinates: ${JSON.stringify([polygon])} } } }) { docs { id point } } }` const response = await client.request(query) const { docs } = response.Points expect(docs).toHaveLength(1) expect(docs[0].point).toEqual([10, 20]) }) it('should not return a document with the point outside the polygon', async () => { const reducedPolygon = polygon.map((vertex) => vertex.map((coord) => coord * 0.1)) const query = ` query { Points( where: { point: { within: { type: "Polygon", coordinates: ${JSON.stringify([reducedPolygon])} } } }) { docs { id point } } }` const response = await client.request(query) const { docs } = response.Points expect(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 () => { const query = ` query { Points( where: { point: { intersects: { type: "Polygon", coordinates: ${JSON.stringify([polygon])} } } }) { docs { id point } } }` const response = await client.request(query) const { docs } = response.Points expect(docs).toHaveLength(1) expect(docs[0].point).toEqual([10, 20]) }) it('should not return a document with the point not intersecting a smaller polygon', async () => { const reducedPolygon = polygon.map((vertex) => vertex.map((coord) => coord * 0.1)) const query = ` query { Points( where: { point: { within: { type: "Polygon", coordinates: ${JSON.stringify([reducedPolygon])} } } }) { docs { id point } } }` const response = await client.request(query) const { docs } = response.Points expect(docs).toHaveLength(0) }) }) it('can query deeply nested fields within rows, tabs, collapsibles', async () => { const withNestedField = await createPost({ D1: { D2: { D3: { D4: 'nested message' } } } }) const query = `{ Posts(where: { D1__D2__D3__D4: { equals: "nested message" } }) { docs { id D1 { D2 { D3 { D4 } } } } } }` const response = await client.request(query) const { docs } = response.Posts expect(docs).toContainEqual( expect.objectContaining({ id: withNestedField.id, D1: { D2: { D3: { D4: 'nested message' } } }, }), ) }) }) describe('relationships', () => { it('should query on relationships with custom IDs', async () => { const query = `query { Posts(where: { title: { equals: "has custom ID relation" }}) { docs { id title relationToCustomID { id } } totalDocs } }` const response = await client.request(query) const { docs, totalDocs } = response.Posts expect(totalDocs).toStrictEqual(1) expect(docs[0].relationToCustomID.id).toStrictEqual(1) }) }) }) describe('Error Handler', () => { it('should return have an array of errors when making a bad request', async () => { let error // language=graphQL const query = `query { Posts(where: { title: { exists: true }}) { docs { badFieldName } } }` await client.request(query).catch((err) => { error = err }) expect(Array.isArray(error.response.errors)).toBe(true) expect(typeof error.response.errors[0].message).toBe('string') }) it('should return have an array of errors when failing to pass validation', async () => { let error // language=graphQL const query = `mutation { createPost(data: {min: 1}) { id min createdAt updatedAt } }` await client.request(query).catch((err) => { error = err }) expect(Array.isArray(error.response.errors)).toBe(true) expect(error.response.errors[0].message).toEqual('The following field is invalid: min') expect(typeof error.response.errors[0].locations).toBeDefined() }) it('should return have an array of errors when failing multiple mutations', async () => { let error // language=graphQL const query = `mutation createTest { test1:createUser(data: { email: "test@test.com", password: "test" }) { email } test2:createUser(data: { email: "test2@test.com", password: "" }) { email } test3:createUser(data: { email: "test@test.com", password: "test" }) { email } test4:createUser(data: { email: "", password: "test" }) { email } }` await client.request(query).catch((err) => { error = err }) expect(Array.isArray(error.response.errors)).toBe(true) expect(Array.isArray(error.response.errors[0].locations)).toEqual(true) expect(error.response.errors[0].message).toEqual('The following field is invalid: password') expect(error.response.errors[0].path[0]).toEqual('test2') expect(error.response.errors[0].extensions.name).toEqual('ValidationError') expect(error.response.errors[0].extensions.data[0].message).toEqual('No password was given') expect(error.response.errors[0].extensions.data[0].field).toEqual('password') expect(Array.isArray(error.response.errors[1].locations)).toEqual(true) expect(error.response.errors[1].message).toEqual('The following field is invalid: email') expect(error.response.errors[1].path[0]).toEqual('test3') expect(error.response.errors[1].extensions.name).toEqual('ValidationError') expect(error.response.errors[1].extensions.data[0].message).toEqual( 'A user with the given email is already registered', ) expect(error.response.errors[1].extensions.data[0].field).toEqual('email') expect(Array.isArray(error.response.errors[2].locations)).toEqual(true) expect(error.response.errors[2].message).toEqual('The following field is invalid: email') expect(error.response.errors[2].path[0]).toEqual('test4') expect(error.response.errors[2].extensions.name).toEqual('ValidationError') expect(error.response.errors[2].extensions.data[0].message).toEqual( 'Please enter a valid email address.', ) expect(error.response.errors[2].extensions.data[0].field).toEqual('email') }) it('should return the minimum allowed information about internal errors', async () => { let error // language=graphQL const query = `query { QueryWithInternalError { text } }` await client.request(query).catch((err) => { error = err }) expect(Array.isArray(error.response.errors)).toBe(true) expect(Array.isArray(error.response.errors[0].locations)).toEqual(true) expect(error.response.errors[0].message).toEqual('Something went wrong.') expect(error.response.errors[0].path[0]).toEqual('QueryWithInternalError') expect(error.response.errors[0].extensions.statusCode).toEqual(500) expect(error.response.errors[0].extensions.name).toEqual('Error') }) }) }) async function createPost(overrides?: Partial) { const doc = await payload.create({ collection: slug, data: { title: 'title', ...overrides }, }) return doc }