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 }))
|
type = new GraphQLList(withNullableType({ type, field, forceNullable, parentIsLocalized }))
|
||||||
return {
|
return {
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: { type },
|
[formatName(field.name)]: { type },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
blocks: (inputObjectTypeConfig: InputObjectTypeConfig, field: BlocksField) => ({
|
blocks: (inputObjectTypeConfig: InputObjectTypeConfig, field: BlocksField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: { type: GraphQLJSON },
|
[formatName(field.name)]: { type: GraphQLJSON },
|
||||||
}),
|
}),
|
||||||
checkbox: (inputObjectTypeConfig: InputObjectTypeConfig, field: CheckboxField) => ({
|
checkbox: (inputObjectTypeConfig: InputObjectTypeConfig, field: CheckboxField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: { type: GraphQLBoolean },
|
[formatName(field.name)]: { type: GraphQLBoolean },
|
||||||
}),
|
}),
|
||||||
code: (inputObjectTypeConfig: InputObjectTypeConfig, field: CodeField) => ({
|
code: (inputObjectTypeConfig: InputObjectTypeConfig, field: CodeField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -134,13 +134,13 @@ export function buildMutationInputType({
|
|||||||
}, inputObjectTypeConfig),
|
}, inputObjectTypeConfig),
|
||||||
date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({
|
date: (inputObjectTypeConfig: InputObjectTypeConfig, field: DateField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
email: (inputObjectTypeConfig: InputObjectTypeConfig, field: EmailField) => ({
|
email: (inputObjectTypeConfig: InputObjectTypeConfig, field: EmailField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -165,12 +165,12 @@ export function buildMutationInputType({
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: { type },
|
[formatName(field.name)]: { type },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
|
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -178,7 +178,7 @@ export function buildMutationInputType({
|
|||||||
const type = field.name === 'id' ? GraphQLInt : GraphQLFloat
|
const type = field.name === 'id' ? GraphQLInt : GraphQLFloat
|
||||||
return {
|
return {
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({
|
type: withNullableType({
|
||||||
type: field.hasMany === true ? new GraphQLList(type) : type,
|
type: field.hasMany === true ? new GraphQLList(type) : type,
|
||||||
field,
|
field,
|
||||||
@@ -190,7 +190,7 @@ export function buildMutationInputType({
|
|||||||
},
|
},
|
||||||
point: (inputObjectTypeConfig: InputObjectTypeConfig, field: PointField) => ({
|
point: (inputObjectTypeConfig: InputObjectTypeConfig, field: PointField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({
|
type: withNullableType({
|
||||||
type: new GraphQLList(GraphQLFloat),
|
type: new GraphQLList(GraphQLFloat),
|
||||||
field,
|
field,
|
||||||
@@ -201,7 +201,7 @@ export function buildMutationInputType({
|
|||||||
}),
|
}),
|
||||||
radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({
|
radio: (inputObjectTypeConfig: InputObjectTypeConfig, field: RadioField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -247,12 +247,12 @@ export function buildMutationInputType({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...inputObjectTypeConfig,
|
...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) => ({
|
richText: (inputObjectTypeConfig: InputObjectTypeConfig, field: RichTextField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -292,7 +292,7 @@ export function buildMutationInputType({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: { type },
|
[formatName(field.name)]: { type },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tabs: (inputObjectTypeConfig: InputObjectTypeConfig, field: TabsField) => {
|
tabs: (inputObjectTypeConfig: InputObjectTypeConfig, field: TabsField) => {
|
||||||
@@ -336,7 +336,7 @@ export function buildMutationInputType({
|
|||||||
},
|
},
|
||||||
text: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextField) => ({
|
text: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({
|
type: withNullableType({
|
||||||
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
|
type: field.hasMany === true ? new GraphQLList(GraphQLString) : GraphQLString,
|
||||||
field,
|
field,
|
||||||
@@ -347,7 +347,7 @@ export function buildMutationInputType({
|
|||||||
}),
|
}),
|
||||||
textarea: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextareaField) => ({
|
textarea: (inputObjectTypeConfig: InputObjectTypeConfig, field: TextareaField) => ({
|
||||||
...inputObjectTypeConfig,
|
...inputObjectTypeConfig,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -393,7 +393,7 @@ export function buildMutationInputType({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...inputObjectTypeConfig,
|
...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 { GraphQLFieldConfig } from 'graphql'
|
||||||
import type {
|
import type { Field, GraphQLInfo, SanitizedConfig } from 'payload'
|
||||||
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 {
|
import { GraphQLObjectType } from 'graphql'
|
||||||
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 type { Context } from '../resolvers/types.js'
|
import { fieldToSchemaMap } from './fieldToSchemaMap.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'
|
|
||||||
|
|
||||||
export type ObjectTypeConfig = {
|
export type ObjectTypeConfig = {
|
||||||
[path: string]: GraphQLFieldConfig<any, any>
|
[path: string]: GraphQLFieldConfig<any, any, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
@@ -76,867 +30,6 @@ export function buildObjectType({
|
|||||||
parentIsLocalized,
|
parentIsLocalized,
|
||||||
parentName,
|
parentName,
|
||||||
}: Args): GraphQLObjectType {
|
}: 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 = {
|
const objectSchema = {
|
||||||
name,
|
name,
|
||||||
fields: () =>
|
fields: () =>
|
||||||
@@ -949,7 +42,16 @@ export function buildObjectType({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...objectTypeConfig,
|
...objectTypeConfig,
|
||||||
...fieldSchema(objectTypeConfig, field),
|
...fieldSchema({
|
||||||
|
config,
|
||||||
|
field,
|
||||||
|
forceNullable,
|
||||||
|
graphqlResult,
|
||||||
|
newlyCreatedBlockType,
|
||||||
|
objectTypeConfig,
|
||||||
|
parentIsLocalized,
|
||||||
|
parentName,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}, baseFields),
|
}, baseFields),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const buildFields = (label, fieldsToBuild) =>
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...builtFields,
|
...builtFields,
|
||||||
[field.name]: {
|
[formatName(field.name)]: {
|
||||||
type: new GraphQLObjectType({
|
type: new GraphQLObjectType({
|
||||||
name: `${label}_${fieldName}`,
|
name: `${label}_${fieldName}`,
|
||||||
fields: objectTypeFields,
|
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',
|
label: 'Title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'hyphenated-name',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
relationTo: 'posts',
|
relationTo: 'posts',
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe('graphql', () => {
|
|||||||
it('should not be able to query introspection', async () => {
|
it('should not be able to query introspection', async () => {
|
||||||
const query = `query {
|
const query = `query {
|
||||||
__schema {
|
__schema {
|
||||||
queryType {
|
queryType {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ describe('graphql', () => {
|
|||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
id: post.id,
|
id: post.id,
|
||||||
data: {
|
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',
|
'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: {
|
auth: {
|
||||||
users: UserAuthOperations;
|
users: UserAuthOperations;
|
||||||
};
|
};
|
||||||
blocks: {};
|
|
||||||
collections: {
|
collections: {
|
||||||
posts: Post;
|
posts: Post;
|
||||||
users: User;
|
users: User;
|
||||||
@@ -119,6 +118,7 @@ export interface UserAuthOperations {
|
|||||||
export interface Post {
|
export interface Post {
|
||||||
id: string;
|
id: string;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
|
'hyphenated-name'?: string | null;
|
||||||
relationToSelf?: (string | null) | Post;
|
relationToSelf?: (string | null) | Post;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -203,6 +203,7 @@ export interface PayloadMigration {
|
|||||||
*/
|
*/
|
||||||
export interface PostsSelect<T extends boolean = true> {
|
export interface PostsSelect<T extends boolean = true> {
|
||||||
title?: T;
|
title?: T;
|
||||||
|
'hyphenated-name'?: T;
|
||||||
relationToSelf?: T;
|
relationToSelf?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type Query {
|
|||||||
type Post {
|
type Post {
|
||||||
id: String!
|
id: String!
|
||||||
title: String
|
title: String
|
||||||
|
hyphenated_name: String
|
||||||
relationToSelf: Post
|
relationToSelf: Post
|
||||||
updatedAt: DateTime
|
updatedAt: DateTime
|
||||||
createdAt: DateTime
|
createdAt: DateTime
|
||||||
@@ -49,6 +50,7 @@ type Posts {
|
|||||||
|
|
||||||
input Post_where {
|
input Post_where {
|
||||||
title: Post_title_operator
|
title: Post_title_operator
|
||||||
|
hyphenated_name: Post_hyphenated_name_operator
|
||||||
relationToSelf: Post_relationToSelf_operator
|
relationToSelf: Post_relationToSelf_operator
|
||||||
updatedAt: Post_updatedAt_operator
|
updatedAt: Post_updatedAt_operator
|
||||||
createdAt: Post_createdAt_operator
|
createdAt: Post_createdAt_operator
|
||||||
@@ -68,6 +70,17 @@ input Post_title_operator {
|
|||||||
exists: Boolean
|
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 {
|
input Post_relationToSelf_operator {
|
||||||
equals: JSON
|
equals: JSON
|
||||||
not_equals: JSON
|
not_equals: JSON
|
||||||
@@ -117,6 +130,7 @@ input Post_id_operator {
|
|||||||
|
|
||||||
input Post_where_and {
|
input Post_where_and {
|
||||||
title: Post_title_operator
|
title: Post_title_operator
|
||||||
|
hyphenated_name: Post_hyphenated_name_operator
|
||||||
relationToSelf: Post_relationToSelf_operator
|
relationToSelf: Post_relationToSelf_operator
|
||||||
updatedAt: Post_updatedAt_operator
|
updatedAt: Post_updatedAt_operator
|
||||||
createdAt: Post_createdAt_operator
|
createdAt: Post_createdAt_operator
|
||||||
@@ -127,6 +141,7 @@ input Post_where_and {
|
|||||||
|
|
||||||
input Post_where_or {
|
input Post_where_or {
|
||||||
title: Post_title_operator
|
title: Post_title_operator
|
||||||
|
hyphenated_name: Post_hyphenated_name_operator
|
||||||
relationToSelf: Post_relationToSelf_operator
|
relationToSelf: Post_relationToSelf_operator
|
||||||
updatedAt: Post_updatedAt_operator
|
updatedAt: Post_updatedAt_operator
|
||||||
createdAt: Post_createdAt_operator
|
createdAt: Post_createdAt_operator
|
||||||
@@ -149,6 +164,7 @@ type postsDocAccess {
|
|||||||
|
|
||||||
type PostsDocAccessFields {
|
type PostsDocAccessFields {
|
||||||
title: PostsDocAccessFields_title
|
title: PostsDocAccessFields_title
|
||||||
|
hyphenated_name: PostsDocAccessFields_hyphenated_name
|
||||||
relationToSelf: PostsDocAccessFields_relationToSelf
|
relationToSelf: PostsDocAccessFields_relationToSelf
|
||||||
updatedAt: PostsDocAccessFields_updatedAt
|
updatedAt: PostsDocAccessFields_updatedAt
|
||||||
createdAt: PostsDocAccessFields_createdAt
|
createdAt: PostsDocAccessFields_createdAt
|
||||||
@@ -177,6 +193,29 @@ type PostsDocAccessFields_title_Delete {
|
|||||||
permission: Boolean!
|
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 {
|
type PostsDocAccessFields_relationToSelf {
|
||||||
create: PostsDocAccessFields_relationToSelf_Create
|
create: PostsDocAccessFields_relationToSelf_Create
|
||||||
read: PostsDocAccessFields_relationToSelf_Read
|
read: PostsDocAccessFields_relationToSelf_Read
|
||||||
@@ -1094,6 +1133,7 @@ type postsAccess {
|
|||||||
|
|
||||||
type PostsFields {
|
type PostsFields {
|
||||||
title: PostsFields_title
|
title: PostsFields_title
|
||||||
|
hyphenated_name: PostsFields_hyphenated_name
|
||||||
relationToSelf: PostsFields_relationToSelf
|
relationToSelf: PostsFields_relationToSelf
|
||||||
updatedAt: PostsFields_updatedAt
|
updatedAt: PostsFields_updatedAt
|
||||||
createdAt: PostsFields_createdAt
|
createdAt: PostsFields_createdAt
|
||||||
@@ -1122,6 +1162,29 @@ type PostsFields_title_Delete {
|
|||||||
permission: Boolean!
|
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 {
|
type PostsFields_relationToSelf {
|
||||||
create: PostsFields_relationToSelf_Create
|
create: PostsFields_relationToSelf_Create
|
||||||
read: PostsFields_relationToSelf_Read
|
read: PostsFields_relationToSelf_Read
|
||||||
@@ -1649,6 +1712,7 @@ type Mutation {
|
|||||||
|
|
||||||
input mutationPostInput {
|
input mutationPostInput {
|
||||||
title: String
|
title: String
|
||||||
|
hyphenated_name: String
|
||||||
relationToSelf: String
|
relationToSelf: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
createdAt: String
|
createdAt: String
|
||||||
@@ -1656,6 +1720,7 @@ input mutationPostInput {
|
|||||||
|
|
||||||
input mutationPostUpdateInput {
|
input mutationPostUpdateInput {
|
||||||
title: String
|
title: String
|
||||||
|
hyphenated_name: String
|
||||||
relationToSelf: String
|
relationToSelf: String
|
||||||
updatedAt: String
|
updatedAt: String
|
||||||
createdAt: String
|
createdAt: String
|
||||||
|
|||||||
Reference in New Issue
Block a user