Files
supabase/apps/docs/app/api/graphql/tests/errors.collection.test.ts
Charis 4e916fc16a feat(graphql): add paginated errors collection query (#36149)
* feat(graphql): add paginated errors collection query

- Add new GraphQL query field 'errors' with cursor-based pagination

- Add UUID id column to content.error table for cursor pagination

- Implement error collection resolver with forward/backward pagination

- Add comprehensive test suite for pagination functionality

- Update database types and schema to support new error collection

- Add utility functions for handling collection queries and errors

- Add seed data for testing pagination scenarios

This change allows clients to efficiently paginate through error codes using cursor-based pagination, supporting both forward and backward traversal. The implementation follows the Relay connection specification and includes proper error handling and type safety.

* docs(graphql): add comprehensive GraphQL architecture documentation

Add detailed documentation for the docs GraphQL endpoint architecture, including:
- Modular query pattern and folder structure
- Step-by-step guide for creating new top-level queries
- Best practices for error handling, field optimization, and testing
- Code examples for schemas, models, resolvers, and tests

* feat(graphql): add service filtering to errors collection query

Enable filtering error codes by Supabase service in the GraphQL errors collection:
- Add optional service argument to errors query resolver
- Update error model to support service-based filtering in database queries
- Maintain pagination compatibility with service filtering
- Add comprehensive tests for service filtering with and without pagination

* feat(graphql): add service filtering and fix cursor encoding for errors collection

- Add service parameter to errors GraphQL query for filtering by Supabase service
- Implement base64 encoding/decoding for pagination cursors in error resolver
- Fix test cursor encoding to match resolver implementation
- Update GraphQL schema snapshot to reflect new service filter field

* docs(graphql): fix codegen instruction
2025-06-09 10:24:17 -04:00

357 lines
10 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { supabase } from '~/lib/supabase'
import { POST } from '../route'
describe('/api/graphql errors collection', () => {
it('returns a list of errors with pagination info', async () => {
// Get the expected order of errors from the database
const { data: dbErrors } = await supabase()
.schema('content')
.from('error')
.select('id, code, ...service(service:name), httpStatusCode:http_status_code, message')
.is('deleted_at', null)
.order('id', { ascending: true })
const errorsQuery = `
query {
errors(first: 2) {
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
code
service
httpStatusCode
message
}
}
nodes {
code
service
httpStatusCode
message
}
}
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: errorsQuery }),
})
const result = await POST(request)
const {
data: { errors },
errors: queryErrors,
} = await result.json()
expect(queryErrors).toBeUndefined()
expect(errors.totalCount).toBe(3)
expect(errors.edges).toHaveLength(2)
expect(errors.nodes).toHaveLength(2)
expect(errors.pageInfo.hasNextPage).toBe(true)
expect(errors.pageInfo.hasPreviousPage).toBe(false)
expect(errors.pageInfo.startCursor).toBeDefined()
expect(errors.pageInfo.endCursor).toBeDefined()
// Compare against the first error from the database
expect(dbErrors).not.toBe(null)
const firstDbError = dbErrors![0]
const firstError = errors.nodes[0]
expect(firstError.code).toBe(firstDbError.code)
expect(firstError.service).toBe(firstDbError.service)
expect(firstError.httpStatusCode).toBe(firstDbError.httpStatusCode)
expect(firstError.message).toBe(firstDbError.message)
const firstEdge = errors.edges[0]
expect(firstEdge.cursor).toBeDefined()
expect(firstEdge.node).toEqual(firstError)
})
it('supports cursor-based pagination', async () => {
const firstPageQuery = `
query {
errors(first: 1) {
edges {
cursor
node {
code
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
const firstRequest = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: firstPageQuery }),
})
const firstResult = await POST(firstRequest)
const firstJson = await firstResult.json()
expect(firstJson.errors).toBeUndefined()
expect(firstJson.data.errors.edges).toHaveLength(1)
expect(firstJson.data.errors.pageInfo.hasNextPage).toBe(true)
expect(firstJson.data.errors.pageInfo.hasPreviousPage).toBe(false)
const firstCursor = firstJson.data.errors.edges[0].cursor
const secondPageQuery = `
query {
errors(first: 1, after: "${firstCursor}") {
edges {
cursor
node {
code
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
const secondRequest = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: secondPageQuery }),
})
const secondResult = await POST(secondRequest)
const secondJson = await secondResult.json()
expect(secondJson.errors).toBeUndefined()
expect(secondJson.data.errors.edges).toHaveLength(1)
expect(secondJson.data.errors.pageInfo.hasPreviousPage).toBe(true)
expect(firstJson.data.errors.edges[0].node.code).not.toBe(
secondJson.data.errors.edges[0].node.code
)
})
it('returns empty list when paginating past available results', async () => {
// Base64 encode the UUID that's guaranteed to be after any real data
const afterCursor = Buffer.from('ffffffff-ffff-ffff-ffff-ffffffffffff', 'utf8').toString(
'base64'
)
const query = `
query {
errors(first: 1, after: "${afterCursor}") {
edges {
cursor
node {
code
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
const request = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query }),
})
const result = await POST(request)
const json = await result.json()
expect(json.errors).toBeUndefined()
expect(json.data.errors.edges).toHaveLength(0)
expect(json.data.errors.pageInfo.hasNextPage).toBe(false)
expect(json.data.errors.pageInfo.hasPreviousPage).toBe(true)
})
it('supports backward pagination with last', async () => {
const lastPageQuery = `
query {
errors(last: 1) {
edges {
cursor
node {
code
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
const lastRequest = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: lastPageQuery }),
})
const lastResult = await POST(lastRequest)
const lastJson = await lastResult.json()
expect(lastJson.errors).toBeUndefined()
expect(lastJson.data.errors.edges).toHaveLength(1)
expect(lastJson.data.errors.pageInfo.hasNextPage).toBe(false)
expect(lastJson.data.errors.pageInfo.hasPreviousPage).toBe(true)
const lastCursor = lastJson.data.errors.edges[0].cursor
const beforeLastQuery = `
query {
errors(last: 1, before: "${lastCursor}") {
edges {
cursor
node {
code
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
const beforeLastRequest = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: beforeLastQuery }),
})
const beforeLastResult = await POST(beforeLastRequest)
const beforeLastJson = await beforeLastResult.json()
expect(beforeLastJson.errors).toBeUndefined()
expect(beforeLastJson.data.errors.edges).toHaveLength(1)
expect(beforeLastJson.data.errors.pageInfo.hasNextPage).toBe(true)
expect(beforeLastJson.data.errors.edges[0].node.code).not.toBe(
lastJson.data.errors.edges[0].node.code
)
})
it('filters by service when service argument is provided', async () => {
// First, get all errors to check we have errors from different services
const allErrorsQuery = `
query {
errors {
nodes {
service
}
}
}
`
const allRequest = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: allErrorsQuery }),
})
const allResult = await POST(allRequest)
const allJson = await allResult.json()
expect(allJson.errors).toBeUndefined()
// Verify we have errors from multiple services
const services = new Set(allJson.data.errors.nodes.map((e: any) => e.service))
expect(services.size).toBeGreaterThan(1)
// Test filtering by AUTH service
const authErrorsQuery = `
query {
errors(service: AUTH) {
totalCount
nodes {
code
service
}
}
}
`
const authRequest = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: authErrorsQuery }),
})
const authResult = await POST(authRequest)
const authJson = await authResult.json()
expect(authJson.errors).toBeUndefined()
// Verify all returned errors are from AUTH service
expect(authJson.data.errors.nodes.length).toBeGreaterThan(0)
expect(authJson.data.errors.nodes.every((e: any) => e.service === 'AUTH')).toBe(true)
})
it('supports service filtering with pagination', async () => {
const firstPageQuery = `
query {
errors(service: AUTH, first: 1) {
edges {
cursor
node {
code
service
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`
const firstRequest = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: firstPageQuery }),
})
const firstResult = await POST(firstRequest)
const firstJson = await firstResult.json()
expect(firstJson.errors).toBeUndefined()
// Verify the returned error is from AUTH service
expect(firstJson.data.errors.edges[0].node.service).toBe('AUTH')
// If there are more AUTH errors, test pagination
if (firstJson.data.errors.pageInfo.hasNextPage) {
const cursor = firstJson.data.errors.pageInfo.endCursor
const secondPageQuery = `
query {
errors(service: AUTH, first: 1, after: "${cursor}") {
edges {
node {
code
service
}
}
}
}
`
const secondRequest = new Request('http://localhost/api/graphql', {
method: 'POST',
body: JSON.stringify({ query: secondPageQuery }),
})
const secondResult = await POST(secondRequest)
const secondJson = await secondResult.json()
expect(secondJson.errors).toBeUndefined()
// Verify the second page also returns AUTH errors
expect(secondJson.data.errors.edges[0].node.service).toBe('AUTH')
// And it's a different error
expect(secondJson.data.errors.edges[0].node.code).not.toBe(
firstJson.data.errors.edges[0].node.code
)
}
})
})