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>
This commit is contained in:
@@ -19,6 +19,10 @@ export default buildConfigWithDefaults({
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'hyphenated-name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('graphql', () => {
|
||||
it('should not be able to query introspection', async () => {
|
||||
const query = `query {
|
||||
__schema {
|
||||
queryType {
|
||||
queryType {
|
||||
name
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ describe('graphql', () => {
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
relatedToSelf: post.id,
|
||||
relationToSelf: post.id,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -80,5 +80,29 @@ describe('graphql', () => {
|
||||
'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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,7 +64,6 @@ export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
posts: Post;
|
||||
users: User;
|
||||
@@ -119,6 +118,7 @@ export interface UserAuthOperations {
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
'hyphenated-name'?: string | null;
|
||||
relationToSelf?: (string | null) | Post;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -203,6 +203,7 @@ export interface PayloadMigration {
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
'hyphenated-name'?: T;
|
||||
relationToSelf?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
|
||||
@@ -23,6 +23,7 @@ type Query {
|
||||
type Post {
|
||||
id: String!
|
||||
title: String
|
||||
hyphenated_name: String
|
||||
relationToSelf: Post
|
||||
updatedAt: DateTime
|
||||
createdAt: DateTime
|
||||
@@ -49,6 +50,7 @@ type Posts {
|
||||
|
||||
input Post_where {
|
||||
title: Post_title_operator
|
||||
hyphenated_name: Post_hyphenated_name_operator
|
||||
relationToSelf: Post_relationToSelf_operator
|
||||
updatedAt: Post_updatedAt_operator
|
||||
createdAt: Post_createdAt_operator
|
||||
@@ -68,6 +70,17 @@ input Post_title_operator {
|
||||
exists: Boolean
|
||||
}
|
||||
|
||||
input Post_hyphenated_name_operator {
|
||||
equals: String
|
||||
not_equals: String
|
||||
like: String
|
||||
contains: String
|
||||
in: [String]
|
||||
not_in: [String]
|
||||
all: [String]
|
||||
exists: Boolean
|
||||
}
|
||||
|
||||
input Post_relationToSelf_operator {
|
||||
equals: JSON
|
||||
not_equals: JSON
|
||||
@@ -117,6 +130,7 @@ input Post_id_operator {
|
||||
|
||||
input Post_where_and {
|
||||
title: Post_title_operator
|
||||
hyphenated_name: Post_hyphenated_name_operator
|
||||
relationToSelf: Post_relationToSelf_operator
|
||||
updatedAt: Post_updatedAt_operator
|
||||
createdAt: Post_createdAt_operator
|
||||
@@ -127,6 +141,7 @@ input Post_where_and {
|
||||
|
||||
input Post_where_or {
|
||||
title: Post_title_operator
|
||||
hyphenated_name: Post_hyphenated_name_operator
|
||||
relationToSelf: Post_relationToSelf_operator
|
||||
updatedAt: Post_updatedAt_operator
|
||||
createdAt: Post_createdAt_operator
|
||||
@@ -149,6 +164,7 @@ type postsDocAccess {
|
||||
|
||||
type PostsDocAccessFields {
|
||||
title: PostsDocAccessFields_title
|
||||
hyphenated_name: PostsDocAccessFields_hyphenated_name
|
||||
relationToSelf: PostsDocAccessFields_relationToSelf
|
||||
updatedAt: PostsDocAccessFields_updatedAt
|
||||
createdAt: PostsDocAccessFields_createdAt
|
||||
@@ -177,6 +193,29 @@ type PostsDocAccessFields_title_Delete {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsDocAccessFields_hyphenated_name {
|
||||
create: PostsDocAccessFields_hyphenated_name_Create
|
||||
read: PostsDocAccessFields_hyphenated_name_Read
|
||||
update: PostsDocAccessFields_hyphenated_name_Update
|
||||
delete: PostsDocAccessFields_hyphenated_name_Delete
|
||||
}
|
||||
|
||||
type PostsDocAccessFields_hyphenated_name_Create {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsDocAccessFields_hyphenated_name_Read {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsDocAccessFields_hyphenated_name_Update {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsDocAccessFields_hyphenated_name_Delete {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsDocAccessFields_relationToSelf {
|
||||
create: PostsDocAccessFields_relationToSelf_Create
|
||||
read: PostsDocAccessFields_relationToSelf_Read
|
||||
@@ -1094,6 +1133,7 @@ type postsAccess {
|
||||
|
||||
type PostsFields {
|
||||
title: PostsFields_title
|
||||
hyphenated_name: PostsFields_hyphenated_name
|
||||
relationToSelf: PostsFields_relationToSelf
|
||||
updatedAt: PostsFields_updatedAt
|
||||
createdAt: PostsFields_createdAt
|
||||
@@ -1122,6 +1162,29 @@ type PostsFields_title_Delete {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsFields_hyphenated_name {
|
||||
create: PostsFields_hyphenated_name_Create
|
||||
read: PostsFields_hyphenated_name_Read
|
||||
update: PostsFields_hyphenated_name_Update
|
||||
delete: PostsFields_hyphenated_name_Delete
|
||||
}
|
||||
|
||||
type PostsFields_hyphenated_name_Create {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsFields_hyphenated_name_Read {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsFields_hyphenated_name_Update {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsFields_hyphenated_name_Delete {
|
||||
permission: Boolean!
|
||||
}
|
||||
|
||||
type PostsFields_relationToSelf {
|
||||
create: PostsFields_relationToSelf_Create
|
||||
read: PostsFields_relationToSelf_Read
|
||||
@@ -1649,6 +1712,7 @@ type Mutation {
|
||||
|
||||
input mutationPostInput {
|
||||
title: String
|
||||
hyphenated_name: String
|
||||
relationToSelf: String
|
||||
updatedAt: String
|
||||
createdAt: String
|
||||
@@ -1656,6 +1720,7 @@ input mutationPostInput {
|
||||
|
||||
input mutationPostUpdateInput {
|
||||
title: String
|
||||
hyphenated_name: String
|
||||
relationToSelf: String
|
||||
updatedAt: String
|
||||
createdAt: String
|
||||
|
||||
Reference in New Issue
Block a user