diff --git a/packages/plugins/graphql/services/schema/builders/generic-morph.js b/packages/plugins/graphql/services/schema/builders/generic-morph.js new file mode 100644 index 0000000000..c65f38490e --- /dev/null +++ b/packages/plugins/graphql/services/schema/builders/generic-morph.js @@ -0,0 +1,26 @@ +'use strict'; + +const { unionType } = require('nexus'); +const { prop } = require('lodash/fp'); + +const { + constants: { GENERIC_MORPH_TYPENAME }, +} = require('../../types'); + +module.exports = ({ registry }) => ({ + buildGenericMorphDefinition() { + return unionType({ + name: GENERIC_MORPH_TYPENAME, + + resolveType: prop('__typename'), + + definition(t) { + const members = registry + .where(({ config }) => ['types', 'components'].includes(config.kind)) + .map(prop('name')); + + t.members(...members); + }, + }); + }, +}); diff --git a/packages/plugins/graphql/services/schema/builders/index.js b/packages/plugins/graphql/services/schema/builders/index.js index 856b5db648..4aaf40ba06 100644 --- a/packages/plugins/graphql/services/schema/builders/index.js +++ b/packages/plugins/graphql/services/schema/builders/index.js @@ -18,6 +18,8 @@ const mutations = require('./mutations'); const filters = require('./filters'); const inputs = require('./input'); +const genericMorph = require('./generic-morph'); + const buildersFactories = [ enums, dynamicZone, @@ -30,6 +32,7 @@ const buildersFactories = [ mutations, filters, inputs, + genericMorph, ]; /** diff --git a/packages/plugins/graphql/services/schema/builders/type.js b/packages/plugins/graphql/services/schema/builders/type.js index 962885d7d0..2fbfd47982 100644 --- a/packages/plugins/graphql/services/schema/builders/type.js +++ b/packages/plugins/graphql/services/schema/builders/type.js @@ -1,11 +1,11 @@ 'use strict'; -const { isArray } = require('lodash/fp'); +const { isArray, isString, isUndefined } = require('lodash/fp'); const { objectType } = require('nexus'); const { contentTypes } = require('@strapi/utils'); -const { mappers, utils: typeUtils } = require('../../types'); +const { mappers, utils: typeUtils, constants } = require('../../types'); const { buildAssociationResolver } = require('../resolvers'); /** @@ -62,7 +62,9 @@ module.exports = context => ({ * - Component * - Dynamic Zone * - Enum - * - Relation + * - Media + * - Polymorphic Relations + * - Regular Relations * * Here, we iterate over each non-private attribute * and add it to the type definition based on its type @@ -107,9 +109,19 @@ module.exports = context => ({ addEnumAttribute(options); } - // Relations + // Media + else if (typeUtils.isMedia(attribute)) { + addMediaAttribute(options); + } + + // Polymorphic Relations + else if (typeUtils.isMorphRelation(attribute)) { + addPolymorphicRelationalAttribute(options); + } + + // Regular Relations else if (typeUtils.isRelation(attribute) || typeUtils.isMedia(attribute)) { - addRelationalAttribute(options); + addRegularRelationalAttribute(options); } }); }, @@ -178,10 +190,10 @@ const addEnumAttribute = ({ builder, attributeName, contentType }) => { }; /** - * Add a relational attribute to the type definition + * Add a media attribute to the type definition * @param {TypeBuildersOptions} options */ -const addRelationalAttribute = options => { +const addMediaAttribute = options => { let { builder } = options; const { attributeName, @@ -190,10 +202,37 @@ const addRelationalAttribute = options => { context: { strapi }, } = options; - // todo[V4]: Clean the logic below + if (attribute.multiple) { + builder = builder.list; + } - const isMorphLike = typeUtils.isMorphRelation(attribute); - const isToManyRelation = typeUtils.isRelation(attribute) && attribute.relation.endsWith('Many'); + const fileContentType = strapi.getModel('plugins::upload.file'); + const type = typeUtils.getTypeName(fileContentType); + + const associationResolver = buildAssociationResolver({ + contentTypeUID: contentType.uid, + attributeName, + strapi, + }); + + builder.field(attributeName, { type, resolve: associationResolver }); +}; + +/** + * Add a polymorphic relational attribute to the type definition + * @param {TypeBuildersOptions} options + */ +const addPolymorphicRelationalAttribute = options => { + let { builder } = options; + const { + attributeName, + attribute, + contentType, + context: { strapi }, + } = options; + + const { target } = attribute; + const isToManyRelation = attribute.relation.endsWith('Many'); if (isToManyRelation) { builder = builder.list; @@ -205,34 +244,53 @@ const addRelationalAttribute = options => { strapi, }); - if (typeUtils.isMedia(attribute)) { - const fileContentType = strapi.getModel('plugins::upload.file'); - const type = typeUtils.getTypeName(fileContentType); + // If there is no specific target specified, then use the GenericMorph type + if (isUndefined(target)) { + builder.field(attributeName, { + type: constants.GENERIC_MORPH_TYPENAME, + resolve: associationResolver, + }); + } - builder.field(attributeName, { type, resolve: associationResolver }); - } else if (isMorphLike) { - const { target } = attribute; - - if (typeof target === 'string') { - const targetContentType = strapi.getModel(target); - const type = typeUtils.getTypeName(targetContentType); - - builder.field(attributeName, { type, resolve: associationResolver }); - } else if (Array.isArray(target)) { - const type = typeUtils.getMorphRelationTypeName(contentType, attributeName); - - builder.field(attributeName, { type, resolve: associationResolver }); - } else if (!target) { - builder.field(attributeName, { type: 'GenericMorph', resolve: associationResolver }); - } - } else { - const targetContentType = strapi.getModel(attribute.target); - const type = typeUtils.getTypeName(targetContentType); + // If the target is an array of string, resolve the associated morph type and use it + else if (isArray(target) && target.every(isString)) { + const type = typeUtils.getMorphRelationTypeName(contentType, attributeName); builder.field(attributeName, { type, resolve: associationResolver }); } }; +/** + * Add a regular relational attribute to the type definition + * @param {TypeBuildersOptions} options + */ +const addRegularRelationalAttribute = options => { + let { builder } = options; + const { + attributeName, + attribute, + contentType, + context: { strapi }, + } = options; + + const isToManyRelation = attribute.relation.endsWith('Many'); + + if (isToManyRelation) { + builder = builder.list; + } + + const associationResolver = buildAssociationResolver({ + contentTypeUID: contentType.uid, + attributeName, + strapi, + }); + + const targetContentType = strapi.getModel(attribute.target); + const type = typeUtils.getTypeName(targetContentType); + + builder.field(attributeName, { type, resolve: associationResolver }); +}; + /** * Bind a content type on an attribute privacy checker * diff --git a/packages/plugins/graphql/services/schema/generators/content-api.js b/packages/plugins/graphql/services/schema/generators/content-api.js index 7a477bcc4f..d011a45864 100644 --- a/packages/plugins/graphql/services/schema/generators/content-api.js +++ b/packages/plugins/graphql/services/schema/generators/content-api.js @@ -4,7 +4,7 @@ const { prop } = require('lodash/fp'); const { makeSchema, unionType } = require('nexus'); const createBuilders = require('../builders'); -const { utils, scalars, buildInternals } = require('../../types'); +const { utils, constants, scalars, buildInternals } = require('../../types'); const { create: createTypeRegistry } = require('../../type-registry'); @@ -42,56 +42,45 @@ module.exports = strapi => { const registerMorphTypes = contentTypes => { // Create & register a union type that includes every type or component registered - registry.register( - 'GenericMorph', - - unionType({ - name: 'GenericMorph', - - resolveType(obj) { - return obj.__typename; - }, - - definition(t) { - const members = registry - .where(({ config: { kind } }) => ['types', 'components'].includes(kind)) - .map(prop('name')); - - t.members(...members); - }, - }), - { kind: 'morphs' } - ); + const genericMorphType = builders.buildGenericMorphDefinition(); + registry.register(constants.GENERIC_MORPH_TYPENAME, genericMorphType, { kind: 'morphs' }); + // For every content type contentTypes.forEach(contentType => { const { attributes = {} } = contentType; + // Isolate its polymorphic attributes const morphAttributes = Object.entries(attributes).filter(([, attribute]) => utils.isMorphRelation(attribute) ); + // For each one of those polymorphic attribute for (const [attributeName, attribute] of morphAttributes) { const name = utils.getMorphRelationTypeName(contentType, attributeName); const { target } = attribute; + // Ignore those whose target is not an array if (!Array.isArray(target)) { continue; } + // Transform target UIDs into types names + const members = target + // Get content types definitions + .map(uid => strapi.getModel(uid)) + // Resolve types names + .map(contentType => utils.getTypeName(contentType)); + + // Register the new polymorphic union type registry.register( name, unionType({ name, - resolveType(obj) { - return obj.__typename; - }, + resolveType: prop('__typename'), definition(t) { - // const members = backLinks.map(prop('definition')); - const members = target || ['GenericMorph']; - t.members(...members); }, }), diff --git a/packages/plugins/graphql/services/types/constants.js b/packages/plugins/graphql/services/types/constants.js index 2950e6db85..c1baf3526a 100644 --- a/packages/plugins/graphql/services/types/constants.js +++ b/packages/plugins/graphql/services/types/constants.js @@ -32,10 +32,13 @@ const STRAPI_SCALARS = [ 'timestamp', ]; +const GENERIC_MORPH_TYPENAME = 'GenericMorph'; + module.exports = { PAGINATION_TYPE_NAME, RESPONSE_COLLECTION_META_TYPE_NAME, PUBLICATION_STATE_TYPE_NAME, GRAPHQL_SCALARS, STRAPI_SCALARS, + GENERIC_MORPH_TYPENAME, };