Handle polymorphic types & relations, clean relational attributes code

This commit is contained in:
Convly 2021-08-05 15:57:31 +02:00
parent 0e8025df6b
commit aa2d2bf29a
5 changed files with 138 additions and 59 deletions

View File

@ -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);
},
});
},
});

View File

@ -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,
];
/**

View File

@ -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,32 +244,51 @@ 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)) {
// 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 });
} else if (!target) {
builder.field(attributeName, { type: 'GenericMorph', resolve: associationResolver });
}
} else {
};
/**
* 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 });
}
};
/**

View File

@ -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);
},
}),

View File

@ -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,
};