Files
payload/test/graphql/int.spec.ts
Jarrod Flesch 029cac3cd3 fix(graphql): sanitize graphql field names for schema generation (#11556)
### What? Cannot generate GraphQL schema with hyphenated field names
Using field names that do not adhere to the GraphQL `_a-z & A-Z`
standard prevent you from generating a schema, even though it will work
just fine everywhere else.

Example: `my-field-name` will prevent schema generation.

### How? Field name sanitization on generation and querying
This PR adds sanitization to the schema generation that sanitizes field
names.
- It formats field names in a GraphQL safe format for schema generation.
**It does not change your config.**
- It adds resolvers for field names that do not adhere so they can be
mapped from the config name to the GraphQL safe name.

Example:
- `my-field` will turn into `my_field` in the schema generation
- `my_field` will resolve from `my-field` when data comes out

### Other notes
- Moves code from `packages/graphql/src/schema/buildObjectType.ts` to
`packages/graphql/src/schema/fieldToSchemaMap.ts`
- Resolvers are only added when necessary: `if (formatName(field.name)
!== field.name)`.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-03-07 14:43:09 +00:00

109 lines
2.6 KiB
TypeScript

import type { Payload } from 'payload'
import path from 'path'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import { idToString } from '../helpers/idToString.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
let payload: Payload
let restClient: NextRESTClient
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
describe('graphql', () => {
beforeAll(async () => {
;({ payload, restClient } = await initPayloadInt(dirname))
})
afterAll(async () => {
if (typeof payload.db.destroy === 'function') {
await payload.db.destroy()
}
})
describe('graphql', () => {
it('should not be able to query introspection', async () => {
const query = `query {
__schema {
queryType {
name
}
}
}`
const response = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
})
.then((res) => res.json())
expect(response.errors[0].message).toMatch(
'GraphQL introspection is not allowed, but the query contained __schema or __type',
)
})
it('should respect maxComplexity', async () => {
const post = await payload.create({
collection: 'posts',
data: {
title: 'example post',
},
})
await payload.update({
collection: 'posts',
id: post.id,
data: {
relationToSelf: post.id,
},
})
const query = `query {
Post(id: ${idToString(post.id, payload)}) {
title
relationToSelf {
id
}
}
}`
const response = await restClient
.GRAPHQL_POST({
body: JSON.stringify({ query }),
})
.then((res) => res.json())
expect(response.errors[0].message).toMatch(
'The query exceeds the maximum complexity of 800. Actual complexity is 804',
)
})
it('should sanitize hyphenated field names to snake case', async () => {
const post = await payload.create({
collection: 'posts',
data: {
title: 'example post',
'hyphenated-name': 'example-hyphenated-name',
},
})
const query = `query {
Post(id: ${idToString(post.id, payload)}) {
title
hyphenated_name
}
}`
const { data } = await restClient
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
.then((res) => res.json())
const res = data.Post
expect(res.hyphenated_name).toStrictEqual('example-hyphenated-name')
})
})
})