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 { fieldAffectsData, tabHasName } from 'payload/shared'; 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 { resolveSelect } from '../utilities/select.js'; import { buildObjectType } from './buildObjectType.js'; import { isFieldNullable } from './isFieldNullable.js'; import { withNullableType } from './withNullableType.js'; function formattedNameResolver({ field, ...rest }) { if ('name' in field) { if (formatName(field.name) !== field.name) { return { ...rest, extensions: { ...rest.extensions, field }, resolve: (parent)=>parent[field.name] }; } } return rest; } export const fieldToSchemaMap = { array: ({ config, field, forceNullable, graphqlResult, objectTypeConfig, parentIsLocalized, parentName })=>{ 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, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: arrayType, field, parentIsLocalized }), field }) }; }, blocks: ({ config, field, forceNullable, graphqlResult, objectTypeConfig, parentIsLocalized, parentName })=>{ const blockTypes = (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) { // Store block slug in extensions for use in select building objectType.extensions = { ...objectType.extensions, blockSlug: block.slug }; 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, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type, field, parentIsLocalized }), field }) }; }, checkbox: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: GraphQLBoolean, field, forceNullable, parentIsLocalized }), field }) }), code: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), field }) }), collapsible: ({ collectionSlug, config, field, forceNullable, graphqlResult, newlyCreatedBlockType, objectTypeConfig, parentIsLocalized, parentName })=>field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField)=>{ const addSubField = fieldToSchemaMap[subField.type]; if (addSubField) { return addSubField({ collectionSlug, config, field: subField, forceNullable, graphqlResult, newlyCreatedBlockType, objectTypeConfig: objectTypeConfigWithCollapsibleFields, parentIsLocalized, parentName }); } return objectTypeConfigWithCollapsibleFields; }, objectTypeConfig), date: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: DateTimeResolver, field, forceNullable, parentIsLocalized }), field }) }), email: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: EmailAddressResolver, field, forceNullable, parentIsLocalized }), field }) }), group: ({ collectionSlug, config, field, forceNullable, graphqlResult, newlyCreatedBlockType, objectTypeConfig, parentIsLocalized, parentName })=>{ if (fieldAffectsData(field)) { 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, [formatName(field.name)]: { type: graphqlResult.types.groupTypes[interfaceName], extensions: { field }, resolve: (parent, args, context)=>{ return { ...parent[field.name], _id: parent._id ?? parent.id }; } } }; } else { return field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField)=>{ const addSubField = fieldToSchemaMap[subField.type]; if (addSubField) { return addSubField({ collectionSlug, config, field: subField, forceNullable, graphqlResult, newlyCreatedBlockType, objectTypeConfig: objectTypeConfigWithCollapsibleFields, parentIsLocalized, parentName }); } return objectTypeConfigWithCollapsibleFields; }, objectTypeConfig); } }, join: ({ collectionSlug, field, graphqlResult, objectTypeConfig, parentName })=>{ const joinName = combineParentName(parentName, toWords(field.name, true)); const joinType = { type: new GraphQLObjectType({ name: joinName, fields: { docs: { type: new GraphQLNonNull(Array.isArray(field.collection) ? GraphQLJSON : new GraphQLList(new GraphQLNonNull(graphqlResult.collections[field.collection].graphQL.type))) }, hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) }, totalDocs: { type: GraphQLInt } } }), args: { count: { type: GraphQLBoolean }, 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, field }, async resolve (parent, args, context, info) { const { collection } = field; const { count = false, limit, page, sort, where } = args; const { req } = context; const draft = Boolean(args.draft ?? context.req.query?.draft); const select = resolveSelect(info, context.select); const targetField = field.targetField; const fullWhere = combineQueries(where, Array.isArray(targetField.relationTo) ? { [field.on]: { equals: { relationTo: collectionSlug, value: parent._id ?? parent.id } } } : { [field.on]: { equals: parent._id ?? parent.id } }); if (Array.isArray(collection)) { throw new Error('GraphQL with array of join.field.collection is not implemented'); } if (count && limit === 0) { return await req.payload.count({ collection, overrideAccess: false, req, where: fullWhere }); } const { docs, totalDocs } = await req.payload.find({ collection, depth: 0, draft, fallbackLocale: req.fallbackLocale, // Fetch one extra document to determine if there are more documents beyond the requested limit (used for hasNextPage calculation). limit: typeof limit === 'number' && limit > 0 ? limit + 1 : 0, locale: req.locale, overrideAccess: false, page, pagination: count ? true : false, req, select, sort, where: fullWhere }); let shouldSlice = false; if (typeof limit === 'number' && limit !== 0 && limit < docs.length) { shouldSlice = true; } return { docs: shouldSlice ? docs.slice(0, -1) : docs, hasNextPage: limit === 0 ? false : limit < docs.length, ...count ? { totalDocs } : {} }; } }; return { ...objectTypeConfig, [formatName(field.name)]: joinType }; }, json: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }), field }) }), number: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>{ const type = field?.name === 'id' ? GraphQLInt : GraphQLFloat; return { ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: field?.hasMany === true ? new GraphQLList(new GraphQLNonNull(type)) : type, field, forceNullable, parentIsLocalized }), field }) }; }, point: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)), field, forceNullable, parentIsLocalized }), field }) }), radio: ({ field, forceNullable, objectTypeConfig, parentIsLocalized, parentName })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: new GraphQLEnumType({ name: combineParentName(parentName, field.name), values: formatOptions(field) }), field, forceNullable, parentIsLocalized }), field }) }), relationship: ({ config, field, forceNullable, graphqlResult, newlyCreatedBlockType, objectTypeConfig, parentIsLocalized, parentName })=>{ 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 = {}; 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, field }, async resolve (parent, args, context, info) { 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); const select = resolveSelect(info, context.select); 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, currentDepth: 0, depth: 0, docID: id, draft, fallbackLocale, locale, overrideAccess: false, select, showHiddenFields: false, transactionID: context.req.transactionID })); if (result) { if (isRelatedToManyCollections) { results.push({ relationTo: collectionSlug, value: { ...result, collection: collectionSlug } }); } else { results.push(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, currentDepth: 0, depth: 0, docID: id, draft, fallbackLocale, locale, overrideAccess: false, select, 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, [formatName(field.name)]: relationship }; }, richText: ({ config, field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: { type: withNullableType({ type: GraphQLJSON, field, forceNullable, parentIsLocalized }), args: { depth: { type: GraphQLInt } }, extensions: { field }, async resolve (parent, args, 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 = 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: ({ field, objectTypeConfig, ...rest })=>field.fields.reduce((objectTypeConfigWithRowFields, subField)=>{ const addSubField = fieldToSchemaMap[subField.type]; if (addSubField) { return addSubField({ field: subField, objectTypeConfig: objectTypeConfigWithRowFields, ...rest }); } return objectTypeConfigWithRowFields; }, objectTypeConfig), select: ({ field, forceNullable, objectTypeConfig, parentIsLocalized, parentName })=>{ const fullName = combineParentName(parentName, field.name); let type = new GraphQLEnumType({ name: fullName, values: formatOptions(field) }); type = field.hasMany ? new GraphQLList(new GraphQLNonNull(type)) : type; type = withNullableType({ type, field, forceNullable, parentIsLocalized }); return { ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type, field }) }; }, tabs: ({ collectionSlug, config, field, forceNullable, graphqlResult, newlyCreatedBlockType, objectTypeConfig, parentIsLocalized, parentName })=>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, collectionSlug, 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) { 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({ collectionSlug, config, field: subField, forceNullable, graphqlResult, newlyCreatedBlockType, objectTypeConfig: subFieldSchema, parentIsLocalized, parentName }); } return subFieldSchema; }, tabSchema) }; }, objectTypeConfig), text: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: field.hasMany === true ? new GraphQLList(new GraphQLNonNull(GraphQLString)) : GraphQLString, field, forceNullable, parentIsLocalized }), field }) }), textarea: ({ field, forceNullable, objectTypeConfig, parentIsLocalized })=>({ ...objectTypeConfig, [formatName(field.name)]: formattedNameResolver({ type: withNullableType({ type: GraphQLString, field, forceNullable, parentIsLocalized }), field }) }), upload: ({ config, field, forceNullable, graphqlResult, newlyCreatedBlockType, objectTypeConfig, parentIsLocalized, parentName })=>{ 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.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 = {}; 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, field }, async resolve (parent, args, context, info) { 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); const select = resolveSelect(info, context.select); 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, currentDepth: 0, depth: 0, docID: id, draft, fallbackLocale, locale, overrideAccess: false, select, showHiddenFields: false, transactionID: context.req.transactionID })); if (result) { if (isRelatedToManyCollections) { results.push({ relationTo: collectionSlug, value: { ...result, collection: collectionSlug } }); } else { results.push(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, currentDepth: 0, depth: 0, docID: id, draft, fallbackLocale, locale, overrideAccess: false, select, 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, [formatName(field.name)]: relationship }; } }; //# sourceMappingURL=fieldToSchemaMap.js.map