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:
@@ -107,20 +107,20 @@ export function buildMutationInputType({
|
||||
type = new GraphQLList(withNullableType({ type, field, forceNullable, parentIsLocalized }))
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type },
|
||||
[formatName(field.name)]: { type },
|
||||
}
|
||||
},
|
||||
blocks: (inputObjectTypeConfig: InputObjectTypeConfig, field: BlocksField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: GraphQLJSON },
|
||||
[formatName(field.name)]: { type: GraphQLJSON },
|
||||
}),
|
||||
checkbox: (inputObjectTypeConfig: InputObjectTypeConfig, field: CheckboxField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: GraphQLBoolean },
|
||||
[formatName(field.name)]: { type: GraphQLBoolean },
|
||||
}),
|
||||
code: (inputObjectTypeConfig: InputObjectTypeConfig, field: CodeField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -134,13 +134,13 @@ export function buildMutationInputType({
|
||||
}, inputObjectTypeConfig),
|
||||
date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
email: (inputObjectTypeConfig: InputObjectTypeConfig, field: EmailField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -165,12 +165,12 @@ export function buildMutationInputType({
|
||||
}
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type },
|
||||
[formatName(field.name)]: { type },
|
||||
}
|
||||
},
|
||||
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -178,7 +178,7 @@ export function buildMutationInputType({
|
||||
const type = field.name === 'id' ? GraphQLInt : GraphQLFloat
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({
|
||||
type: field.hasMany === true ? new GraphQLList(type) : type,
|
||||
field,
|
||||
@@ -190,7 +190,7 @@ export function buildMutationInputType({
|
||||
},
|
||||
point: (inputObjectTypeConfig: InputObjectTypeConfig, field: PointField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({
|
||||
type: new GraphQLList(GraphQLFloat),
|
||||
field,
|
||||
@@ -201,7 +201,7 @@ export function buildMutationInputType({
|
||||
}),
|
||||
radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -247,12 +247,12 @@ export function buildMutationInputType({
|
||||
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
[formatName(field.name)]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
}
|
||||
},
|
||||
richText: (inputObjectTypeConfig: InputObjectTypeConfig, field: RichTextField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -292,7 +292,7 @@ export function buildMutationInputType({
|
||||
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type },
|
||||
[formatName(field.name)]: { type },
|
||||
}
|
||||
},
|
||||
tabs: (inputObjectTypeConfig: InputObjectTypeConfig, field: TabsField) => {
|
||||
@@ -336,7 +336,7 @@ export function buildMutationInputType({
|
||||
},
|
||||
text: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({
|
||||
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
|
||||
field,
|
||||
@@ -347,7 +347,7 @@ export function buildMutationInputType({
|
||||
}),
|
||||
textarea: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextareaField) => ({
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
@@ -393,7 +393,7 @@ export function buildMutationInputType({
|
||||
|
||||
return {
|
||||
...inputObjectTypeConfig,
|
||||
[field.name]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
[formatName(field.name)]: { type: field.hasMany ? new GraphQLList(type) : type },
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,58 +1,12 @@
|
||||
import type { GraphQLFieldConfig, GraphQLType } from 'graphql'
|
||||
import type {
|
||||
ArrayField,
|
||||
BlocksField,
|
||||
CheckboxField,
|
||||
CodeField,
|
||||
CollapsibleField,
|
||||
DateField,
|
||||
EmailField,
|
||||
Field,
|
||||
GraphQLInfo,
|
||||
GroupField,
|
||||
JoinField,
|
||||
JSONField,
|
||||
NumberField,
|
||||
PointField,
|
||||
RadioField,
|
||||
RelationshipField,
|
||||
RichTextAdapter,
|
||||
RichTextField,
|
||||
RowField,
|
||||
SanitizedConfig,
|
||||
SelectField,
|
||||
TabsField,
|
||||
TextareaField,
|
||||
TextField,
|
||||
UploadField,
|
||||
} from 'payload'
|
||||
import type { GraphQLFieldConfig } from 'graphql'
|
||||
import type { Field, GraphQLInfo, SanitizedConfig } from 'payload'
|
||||
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLEnumType,
|
||||
GraphQLFloat,
|
||||
GraphQLInt,
|
||||
GraphQLList,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType,
|
||||
GraphQLString,
|
||||
GraphQLUnionType,
|
||||
} from 'graphql'
|
||||
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
|
||||
import { combineQueries, createDataloaderCacheKey, MissingEditorProp, toWords } from 'payload'
|
||||
import { tabHasName } from 'payload/shared'
|
||||
import { GraphQLObjectType } from 'graphql'
|
||||
|
||||
import type { Context } from '../resolvers/types.js'
|
||||
|
||||
import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
|
||||
import { combineParentName } from '../utilities/combineParentName.js'
|
||||
import { formatName } from '../utilities/formatName.js'
|
||||
import { formatOptions } from '../utilities/formatOptions.js'
|
||||
import { isFieldNullable } from './isFieldNullable.js'
|
||||
import { withNullableType } from './withNullableType.js'
|
||||
import { fieldToSchemaMap } from './fieldToSchemaMap.js'
|
||||
|
||||
export type ObjectTypeConfig = {
|
||||
[path: string]: GraphQLFieldConfig<any, any>
|
||||
[path: string]: GraphQLFieldConfig<any, any, any>
|
||||
}
|
||||
|
||||
type Args = {
|
||||
@@ -76,867 +30,6 @@ export function buildObjectType({
|
||||
parentIsLocalized,
|
||||
parentName,
|
||||
}: Args): GraphQLObjectType {
|
||||
const fieldToSchemaMap = {
|
||||
array: (objectTypeConfig: ObjectTypeConfig, field: ArrayField) => {
|
||||
const interfaceName =
|
||||
field?.interfaceName || combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
if (!graphqlResult.types.arrayTypes[interfaceName]) {
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: field.fields,
|
||||
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
|
||||
graphqlResult,
|
||||
parentIsLocalized: field.localized || parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.arrayTypes[interfaceName] = objectType
|
||||
}
|
||||
}
|
||||
|
||||
if (!graphqlResult.types.arrayTypes[interfaceName]) {
|
||||
return objectTypeConfig
|
||||
}
|
||||
|
||||
const arrayType = new GraphQLList(
|
||||
new GraphQLNonNull(graphqlResult.types.arrayTypes[interfaceName]),
|
||||
)
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: { type: withNullableType({ type: arrayType, field, parentIsLocalized }) },
|
||||
}
|
||||
},
|
||||
blocks: (objectTypeConfig: ObjectTypeConfig, field: BlocksField) => {
|
||||
const blockTypes: GraphQLObjectType<any, any>[] = (
|
||||
field.blockReferences ?? field.blocks
|
||||
).reduce((acc, _block) => {
|
||||
const blockSlug = typeof _block === 'string' ? _block : _block.slug
|
||||
if (!graphqlResult.types.blockTypes[blockSlug]) {
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block =
|
||||
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
|
||||
|
||||
const interfaceName =
|
||||
block?.interfaceName || block?.graphQL?.singularName || toWords(block.slug, true)
|
||||
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: [
|
||||
...block.fields,
|
||||
{
|
||||
name: 'blockType',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
forceNullable,
|
||||
graphqlResult,
|
||||
parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.blockTypes[block.slug] = objectType
|
||||
}
|
||||
}
|
||||
|
||||
if (graphqlResult.types.blockTypes[blockSlug]) {
|
||||
acc.push(graphqlResult.types.blockTypes[blockSlug])
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
if (blockTypes.length === 0) {
|
||||
return objectTypeConfig
|
||||
}
|
||||
|
||||
const fullName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
const type = new GraphQLList(
|
||||
new GraphQLNonNull(
|
||||
new GraphQLUnionType({
|
||||
name: fullName,
|
||||
resolveType: (data) => graphqlResult.types.blockTypes[data.blockType].name,
|
||||
types: blockTypes,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: { type: withNullableType({ type, field, parentIsLocalized }) },
|
||||
}
|
||||
},
|
||||
checkbox: (objectTypeConfig: ObjectTypeConfig, field: CheckboxField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLBoolean, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
code: (objectTypeConfig: ObjectTypeConfig, field: CodeField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
collapsible: (objectTypeConfig: ObjectTypeConfig, field: CollapsibleField) =>
|
||||
field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField) => {
|
||||
const addSubField = fieldToSchemaMap[subField.type]
|
||||
if (addSubField) {
|
||||
return addSubField(objectTypeConfigWithCollapsibleFields, subField)
|
||||
}
|
||||
return objectTypeConfigWithCollapsibleFields
|
||||
}, objectTypeConfig),
|
||||
date: (objectTypeConfig: ObjectTypeConfig, field: DateField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: DateTimeResolver, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
email: (objectTypeConfig: ObjectTypeConfig, field: EmailField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: EmailAddressResolver,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
group: (objectTypeConfig: ObjectTypeConfig, field: GroupField) => {
|
||||
const interfaceName =
|
||||
field?.interfaceName || combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: field.fields,
|
||||
forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
|
||||
graphqlResult,
|
||||
parentIsLocalized: field.localized || parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.groupTypes[interfaceName] = objectType
|
||||
}
|
||||
}
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
return objectTypeConfig
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: graphqlResult.types.groupTypes[interfaceName],
|
||||
resolve: (parent, args, context: Context) => {
|
||||
return {
|
||||
...parent[field.name],
|
||||
_id: parent._id ?? parent.id,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
join: (objectTypeConfig: ObjectTypeConfig, field: JoinField) => {
|
||||
const joinName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
const joinType = {
|
||||
type: new GraphQLObjectType({
|
||||
name: joinName,
|
||||
fields: {
|
||||
docs: {
|
||||
type: Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: new GraphQLList(graphqlResult.collections[field.collection].graphQL.type),
|
||||
},
|
||||
hasNextPage: { type: GraphQLBoolean },
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
limit: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
page: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
sort: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
where: {
|
||||
type: Array.isArray(field.collection)
|
||||
? GraphQLJSON
|
||||
: graphqlResult.collections[field.collection].graphQL.whereInputType,
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
complexity:
|
||||
typeof field?.graphQL?.complexity === 'number' ? field.graphQL.complexity : 10,
|
||||
},
|
||||
async resolve(parent, args, context: Context) {
|
||||
const { collection } = field
|
||||
const { limit, page, sort, where } = args
|
||||
const { req } = context
|
||||
|
||||
const fullWhere = combineQueries(where, {
|
||||
[field.on]: { equals: parent._id ?? parent.id },
|
||||
})
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
throw new Error('GraphQL with array of join.field.collection is not implemented')
|
||||
}
|
||||
|
||||
return await req.payload.find({
|
||||
collection,
|
||||
depth: 0,
|
||||
fallbackLocale: req.fallbackLocale,
|
||||
limit,
|
||||
locale: req.locale,
|
||||
overrideAccess: false,
|
||||
page,
|
||||
req,
|
||||
sort,
|
||||
where: fullWhere,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: joinType,
|
||||
}
|
||||
},
|
||||
json: (objectTypeConfig: ObjectTypeConfig, field: JSONField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
number: (objectTypeConfig: ObjectTypeConfig, field: NumberField) => {
|
||||
const type = field?.name === 'id' ? GraphQLInt : GraphQLFloat
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: field?.hasMany === true ? new GraphQLList(type) : type,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
point: (objectTypeConfig: ObjectTypeConfig, field: PointField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
radio: (objectTypeConfig: ObjectTypeConfig, field: RadioField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: new GraphQLEnumType({
|
||||
name: combineParentName(parentName, field.name),
|
||||
values: formatOptions(field),
|
||||
}),
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
relationship: (objectTypeConfig: ObjectTypeConfig, field: RelationshipField) => {
|
||||
const { relationTo } = field
|
||||
const isRelatedToManyCollections = Array.isArray(relationTo)
|
||||
const hasManyValues = field.hasMany
|
||||
const relationshipName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
let type
|
||||
let relationToType = null
|
||||
|
||||
const graphQLCollections = config.collections.filter(
|
||||
(collectionConfig) => collectionConfig.graphQL !== false,
|
||||
)
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
relationToType = new GraphQLEnumType({
|
||||
name: `${relationshipName}_RelationTo`,
|
||||
values: relationTo
|
||||
.filter((relation) =>
|
||||
graphQLCollections.some((collection) => collection.slug === relation),
|
||||
)
|
||||
.reduce(
|
||||
(relations, relation) => ({
|
||||
...relations,
|
||||
[formatName(relation)]: {
|
||||
value: relation,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
})
|
||||
|
||||
// Only pass collections that are GraphQL enabled
|
||||
const types = relationTo
|
||||
.filter((relation) =>
|
||||
graphQLCollections.some((collection) => collection.slug === relation),
|
||||
)
|
||||
.map((relation) => graphqlResult.collections[relation]?.graphQL.type)
|
||||
|
||||
type = new GraphQLObjectType({
|
||||
name: `${relationshipName}_Relationship`,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: relationToType,
|
||||
},
|
||||
value: {
|
||||
type: new GraphQLUnionType({
|
||||
name: relationshipName,
|
||||
resolveType(data) {
|
||||
return graphqlResult.collections[data.collection].graphQL.type.name
|
||||
},
|
||||
types,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
;({ type } = graphqlResult.collections[relationTo].graphQL)
|
||||
}
|
||||
|
||||
// If the relationshipType is undefined at this point,
|
||||
// it can be assumed that this blockType can have a relationship
|
||||
// to itself. Therefore, we set the relationshipType equal to the blockType
|
||||
// that is currently being created.
|
||||
|
||||
type = type || newlyCreatedBlockType
|
||||
|
||||
const relationshipArgs: {
|
||||
draft?: unknown
|
||||
fallbackLocale?: unknown
|
||||
limit?: unknown
|
||||
locale?: unknown
|
||||
page?: unknown
|
||||
where?: unknown
|
||||
} = {}
|
||||
|
||||
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo])
|
||||
.filter((relation) => graphQLCollections.some((collection) => collection.slug === relation))
|
||||
.some((relation) => graphqlResult.collections[relation].config.versions?.drafts)
|
||||
|
||||
if (relationsUseDrafts) {
|
||||
relationshipArgs.draft = {
|
||||
type: GraphQLBoolean,
|
||||
}
|
||||
}
|
||||
|
||||
if (config.localization) {
|
||||
relationshipArgs.locale = {
|
||||
type: graphqlResult.types.localeInputType,
|
||||
}
|
||||
|
||||
relationshipArgs.fallbackLocale = {
|
||||
type: graphqlResult.types.fallbackLocaleInputType,
|
||||
}
|
||||
}
|
||||
|
||||
const relationship = {
|
||||
type: withNullableType({
|
||||
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
args: relationshipArgs,
|
||||
extensions: {
|
||||
complexity:
|
||||
typeof field?.graphQL?.complexity === 'number' ? field.graphQL.complexity : 10,
|
||||
},
|
||||
async resolve(parent, args, context: Context) {
|
||||
const value = parent[field.name]
|
||||
const locale = args.locale || context.req.locale
|
||||
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
|
||||
let relatedCollectionSlug = field.relationTo
|
||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
||||
|
||||
if (hasManyValues) {
|
||||
const results = []
|
||||
const resultPromises = []
|
||||
|
||||
const createPopulationPromise = async (relatedDoc, i) => {
|
||||
let id = relatedDoc
|
||||
let collectionSlug = field.relationTo
|
||||
const isValidGraphQLCollection = isRelatedToManyCollections
|
||||
? graphQLCollections.some((collection) => collectionSlug.includes(collection.slug))
|
||||
: graphQLCollections.some((collection) => collectionSlug === collection.slug)
|
||||
|
||||
if (isValidGraphQLCollection) {
|
||||
if (isRelatedToManyCollections) {
|
||||
collectionSlug = relatedDoc.relationTo
|
||||
id = relatedDoc.value
|
||||
}
|
||||
|
||||
const result = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: collectionSlug as string,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (result) {
|
||||
if (isRelatedToManyCollections) {
|
||||
results[i] = {
|
||||
relationTo: collectionSlug,
|
||||
value: {
|
||||
...result,
|
||||
collection: collectionSlug,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
results[i] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
value.forEach((relatedDoc, i) => {
|
||||
resultPromises.push(createPopulationPromise(relatedDoc, i))
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(resultPromises)
|
||||
return results
|
||||
}
|
||||
|
||||
let id = value
|
||||
if (isRelatedToManyCollections && value) {
|
||||
id = value.value
|
||||
relatedCollectionSlug = value.relationTo
|
||||
}
|
||||
|
||||
if (id) {
|
||||
if (
|
||||
graphQLCollections.some((collection) => collection.slug === relatedCollectionSlug)
|
||||
) {
|
||||
const relatedDocument = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: relatedCollectionSlug as string,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (relatedDocument) {
|
||||
if (isRelatedToManyCollections) {
|
||||
return {
|
||||
relationTo: relatedCollectionSlug,
|
||||
value: {
|
||||
...relatedDocument,
|
||||
collection: relatedCollectionSlug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return relatedDocument
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: relationship,
|
||||
}
|
||||
},
|
||||
richText: (objectTypeConfig: ObjectTypeConfig, field: RichTextField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||
args: {
|
||||
depth: {
|
||||
type: GraphQLInt,
|
||||
},
|
||||
},
|
||||
async resolve(parent, args, context: Context) {
|
||||
let depth = config.defaultDepth
|
||||
if (typeof args.depth !== 'undefined') {
|
||||
depth = args.depth
|
||||
}
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
|
||||
const editor: RichTextAdapter = field?.editor
|
||||
|
||||
// RichText fields have their own depth argument in GraphQL.
|
||||
// This is why the populationPromise (which populates richtext fields like uploads and relationships)
|
||||
// is run here again, with the provided depth.
|
||||
// In the graphql find.ts resolver, the depth is then hard-coded to 0.
|
||||
// Effectively, this means that the populationPromise for GraphQL is only run here, and not in the find.ts resolver / normal population promise.
|
||||
if (editor?.graphQLPopulationPromises) {
|
||||
const fieldPromises = []
|
||||
const populationPromises = []
|
||||
const populateDepth =
|
||||
field?.maxDepth !== undefined && field?.maxDepth < depth ? field?.maxDepth : depth
|
||||
|
||||
editor?.graphQLPopulationPromises({
|
||||
context,
|
||||
depth: populateDepth,
|
||||
draft: args.draft,
|
||||
field,
|
||||
fieldPromises,
|
||||
findMany: false,
|
||||
flattenLocales: false,
|
||||
overrideAccess: false,
|
||||
parentIsLocalized,
|
||||
populationPromises,
|
||||
req: context.req,
|
||||
showHiddenFields: false,
|
||||
siblingDoc: parent,
|
||||
})
|
||||
await Promise.all(fieldPromises)
|
||||
await Promise.all(populationPromises)
|
||||
}
|
||||
|
||||
return parent[field.name]
|
||||
},
|
||||
},
|
||||
}),
|
||||
row: (objectTypeConfig: ObjectTypeConfig, field: RowField) =>
|
||||
field.fields.reduce((objectTypeConfigWithRowFields, subField) => {
|
||||
const addSubField = fieldToSchemaMap[subField.type]
|
||||
if (addSubField) {
|
||||
return addSubField(objectTypeConfigWithRowFields, subField)
|
||||
}
|
||||
return objectTypeConfigWithRowFields
|
||||
}, objectTypeConfig),
|
||||
select: (objectTypeConfig: ObjectTypeConfig, field: SelectField) => {
|
||||
const fullName = combineParentName(parentName, field.name)
|
||||
|
||||
let type: GraphQLType = new GraphQLEnumType({
|
||||
name: fullName,
|
||||
values: formatOptions(field),
|
||||
})
|
||||
|
||||
type = field.hasMany ? new GraphQLList(new GraphQLNonNull(type)) : type
|
||||
type = withNullableType({ type, field, forceNullable, parentIsLocalized })
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: { type },
|
||||
}
|
||||
},
|
||||
tabs: (objectTypeConfig: ObjectTypeConfig, field: TabsField) =>
|
||||
field.tabs.reduce((tabSchema, tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
const interfaceName =
|
||||
tab?.interfaceName || combineParentName(parentName, toWords(tab.name, true))
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
const objectType = buildObjectType({
|
||||
name: interfaceName,
|
||||
config,
|
||||
fields: tab.fields,
|
||||
forceNullable,
|
||||
graphqlResult,
|
||||
parentIsLocalized: tab.localized || parentIsLocalized,
|
||||
parentName: interfaceName,
|
||||
})
|
||||
|
||||
if (Object.keys(objectType.getFields()).length) {
|
||||
graphqlResult.types.groupTypes[interfaceName] = objectType
|
||||
}
|
||||
}
|
||||
|
||||
if (!graphqlResult.types.groupTypes[interfaceName]) {
|
||||
return tabSchema
|
||||
}
|
||||
|
||||
return {
|
||||
...tabSchema,
|
||||
[tab.name]: {
|
||||
type: graphqlResult.types.groupTypes[interfaceName],
|
||||
resolve(parent, args, context: Context) {
|
||||
return {
|
||||
...parent[tab.name],
|
||||
_id: parent._id ?? parent.id,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...tabSchema,
|
||||
...tab.fields.reduce((subFieldSchema, subField) => {
|
||||
const addSubField = fieldToSchemaMap[subField.type]
|
||||
if (addSubField) {
|
||||
return addSubField(subFieldSchema, subField)
|
||||
}
|
||||
return subFieldSchema
|
||||
}, tabSchema),
|
||||
}
|
||||
}, objectTypeConfig),
|
||||
text: (objectTypeConfig: ObjectTypeConfig, field: TextField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({
|
||||
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
textarea: (objectTypeConfig: ObjectTypeConfig, field: TextareaField) => ({
|
||||
...objectTypeConfig,
|
||||
[field.name]: {
|
||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||
},
|
||||
}),
|
||||
upload: (objectTypeConfig: ObjectTypeConfig, field: UploadField) => {
|
||||
const { relationTo } = field
|
||||
const isRelatedToManyCollections = Array.isArray(relationTo)
|
||||
const hasManyValues = field.hasMany
|
||||
const relationshipName = combineParentName(parentName, toWords(field.name, true))
|
||||
|
||||
let type
|
||||
let relationToType = null
|
||||
|
||||
if (Array.isArray(relationTo)) {
|
||||
relationToType = new GraphQLEnumType({
|
||||
name: `${relationshipName}_RelationTo`,
|
||||
values: relationTo.reduce(
|
||||
(relations, relation) => ({
|
||||
...relations,
|
||||
[formatName(relation)]: {
|
||||
value: relation,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
),
|
||||
})
|
||||
|
||||
const types = relationTo.map((relation) => graphqlResult.collections[relation].graphQL.type)
|
||||
|
||||
type = new GraphQLObjectType({
|
||||
name: `${relationshipName}_Relationship`,
|
||||
fields: {
|
||||
relationTo: {
|
||||
type: relationToType,
|
||||
},
|
||||
value: {
|
||||
type: new GraphQLUnionType({
|
||||
name: relationshipName,
|
||||
resolveType(data) {
|
||||
return graphqlResult.collections[data.collection].graphQL.type.name
|
||||
},
|
||||
types,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
;({ type } = graphqlResult.collections[relationTo].graphQL)
|
||||
}
|
||||
|
||||
// If the relationshipType is undefined at this point,
|
||||
// it can be assumed that this blockType can have a relationship
|
||||
// to itself. Therefore, we set the relationshipType equal to the blockType
|
||||
// that is currently being created.
|
||||
|
||||
type = type || newlyCreatedBlockType
|
||||
|
||||
const relationshipArgs: {
|
||||
draft?: unknown
|
||||
fallbackLocale?: unknown
|
||||
limit?: unknown
|
||||
locale?: unknown
|
||||
page?: unknown
|
||||
where?: unknown
|
||||
} = {}
|
||||
|
||||
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo]).some(
|
||||
(relation) => graphqlResult.collections[relation].config.versions?.drafts,
|
||||
)
|
||||
|
||||
if (relationsUseDrafts) {
|
||||
relationshipArgs.draft = {
|
||||
type: GraphQLBoolean,
|
||||
}
|
||||
}
|
||||
|
||||
if (config.localization) {
|
||||
relationshipArgs.locale = {
|
||||
type: graphqlResult.types.localeInputType,
|
||||
}
|
||||
|
||||
relationshipArgs.fallbackLocale = {
|
||||
type: graphqlResult.types.fallbackLocaleInputType,
|
||||
}
|
||||
}
|
||||
|
||||
const relationship = {
|
||||
type: withNullableType({
|
||||
type: hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
|
||||
field,
|
||||
forceNullable,
|
||||
parentIsLocalized,
|
||||
}),
|
||||
args: relationshipArgs,
|
||||
extensions: {
|
||||
complexity:
|
||||
typeof field?.graphQL?.complexity === 'number' ? field.graphQL.complexity : 10,
|
||||
},
|
||||
async resolve(parent, args, context: Context) {
|
||||
const value = parent[field.name]
|
||||
const locale = args.locale || context.req.locale
|
||||
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
|
||||
let relatedCollectionSlug = field.relationTo
|
||||
const draft = Boolean(args.draft ?? context.req.query?.draft)
|
||||
|
||||
if (hasManyValues) {
|
||||
const results = []
|
||||
const resultPromises = []
|
||||
|
||||
const createPopulationPromise = async (relatedDoc, i) => {
|
||||
let id = relatedDoc
|
||||
let collectionSlug = field.relationTo
|
||||
|
||||
if (isRelatedToManyCollections) {
|
||||
collectionSlug = relatedDoc.relationTo
|
||||
id = relatedDoc.value
|
||||
}
|
||||
|
||||
const result = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (result) {
|
||||
if (isRelatedToManyCollections) {
|
||||
results[i] = {
|
||||
relationTo: collectionSlug,
|
||||
value: {
|
||||
...result,
|
||||
collection: collectionSlug,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
results[i] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
value.forEach((relatedDoc, i) => {
|
||||
resultPromises.push(createPopulationPromise(relatedDoc, i))
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(resultPromises)
|
||||
return results
|
||||
}
|
||||
|
||||
let id = value
|
||||
if (isRelatedToManyCollections && value) {
|
||||
id = value.value
|
||||
relatedCollectionSlug = value.relationTo
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const relatedDocument = await context.req.payloadDataLoader.load(
|
||||
createDataloaderCacheKey({
|
||||
collectionSlug: relatedCollectionSlug,
|
||||
currentDepth: 0,
|
||||
depth: 0,
|
||||
docID: id,
|
||||
draft,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: false,
|
||||
transactionID: context.req.transactionID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (relatedDocument) {
|
||||
if (isRelatedToManyCollections) {
|
||||
return {
|
||||
relationTo: relatedCollectionSlug,
|
||||
value: {
|
||||
...relatedDocument,
|
||||
collection: relatedCollectionSlug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return relatedDocument
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
[field.name]: relationship,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const objectSchema = {
|
||||
name,
|
||||
fields: () =>
|
||||
@@ -949,7 +42,16 @@ export function buildObjectType({
|
||||
|
||||
return {
|
||||
...objectTypeConfig,
|
||||
...fieldSchema(objectTypeConfig, field),
|
||||
...fieldSchema({
|
||||
config,
|
||||
field,
|
||||
forceNullable,
|
||||
graphqlResult,
|
||||
newlyCreatedBlockType,
|
||||
objectTypeConfig,
|
||||
parentIsLocalized,
|
||||
parentName,
|
||||
}),
|
||||
}
|
||||
}, baseFields),
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ const buildFields = (label, fieldsToBuild) =>
|
||||
|
||||
return {
|
||||
...builtFields,
|
||||
[field.name]: {
|
||||
[formatName(field.name)]: {
|
||||
type: new GraphQLObjectType({
|
||||
name: `${label}_${fieldName}`,
|
||||
fields: objectTypeFields,
|
||||
|
||||
1086
packages/graphql/src/schema/fieldToSchemaMap.ts
Normal file
1086
packages/graphql/src/schema/fieldToSchemaMap.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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