diff --git a/examples/getstarted/api/post/models/Post.settings.json b/examples/getstarted/api/post/models/Post.settings.json index 2a4b9ae818..91e8a2a2f4 100644 --- a/examples/getstarted/api/post/models/Post.settings.json +++ b/examples/getstarted/api/post/models/Post.settings.json @@ -20,6 +20,15 @@ "via": "related", "plugin": "upload" }, + "images": { + "collection": "file", + "via": "related", + "plugin": "upload" + }, + "author": { + "model": "user", + "plugin": "users-permissions" + }, "metas": { "type": "component", "component": "default.metas", diff --git a/packages/strapi-connector-bookshelf/lib/mount-models.js b/packages/strapi-connector-bookshelf/lib/mount-models.js index 4e0135543a..fccb7b93b4 100644 --- a/packages/strapi-connector-bookshelf/lib/mount-models.js +++ b/packages/strapi-connector-bookshelf/lib/mount-models.js @@ -547,11 +547,10 @@ module.exports = ({ models, target, plugin = false }, ctx) => { : relation; // Retrieve opposite model. - const model = association.plugin - ? strapi.plugins[association.plugin].models[ - association.collection || association.model - ] - : strapi.models[association.collection || association.model]; + const model = strapi.getModel( + association.collection || association.model, + association.plugin + ); // Reformat data by bypassing the many-to-many relationship. switch (association.nature) { @@ -564,15 +563,36 @@ module.exports = ({ models, target, plugin = false }, ctx) => { rel => rel[model.collectionName] ); break; - case 'oneMorphToOne': - attrs[association.alias] = - attrs[association.alias].related || null; + case 'oneMorphToOne': { + const obj = attrs[association.alias]; + + if (obj === undefined || obj === null) { + break; + } + + const contentType = strapi.db.getModelByCollectionName( + obj[`${association.alias}_type`] + ); + + attrs[association.alias] = { + __contentType: contentType ? contentType.globalId : null, + ...obj.related, + }; + break; + } case 'manyMorphToOne': case 'manyMorphToMany': - attrs[association.alias] = attrs[association.alias].map( - obj => obj.related - ); + attrs[association.alias] = attrs[association.alias].map(obj => { + const contentType = strapi.db.getModelByCollectionName( + obj[`${association.alias}_type`] + ); + + return { + __contentType: contentType ? contentType.globalId : null, + ...obj.related, + }; + }); break; default: } diff --git a/packages/strapi-database/lib/database-manager.js b/packages/strapi-database/lib/database-manager.js index d0555e2151..944dabf661 100644 --- a/packages/strapi-database/lib/database-manager.js +++ b/packages/strapi-database/lib/database-manager.js @@ -107,6 +107,12 @@ class DatabaseManager { _.get(strapi, ['components', key]) ); } + + getModelByCollectionName(collectionName) { + return Array.from(this.models.values()).find(model => { + return model.collectionName === collectionName; + }); + } } function createDatabaseManager(strapi) { diff --git a/packages/strapi-plugin-content-manager/services/ContentManager.js b/packages/strapi-plugin-content-manager/services/ContentManager.js index 3a8e515ccd..2e55f816c5 100644 --- a/packages/strapi-plugin-content-manager/services/ContentManager.js +++ b/packages/strapi-plugin-content-manager/services/ContentManager.js @@ -6,8 +6,8 @@ const uploadFiles = require('../utils/upload-files'); * A set of functions called "actions" for `ContentManager` */ module.exports = { - fetch(params) { - return strapi.query(params.model).findOne({ id: params.id }); + fetch(params, populate) { + return strapi.query(params.model).findOne({ id: params.id }, populate); }, fetchAll(params, query) { diff --git a/packages/strapi-plugin-graphql/services/Loaders.js b/packages/strapi-plugin-graphql/services/Loaders.js index 4cf7254fd0..a0325cf45c 100644 --- a/packages/strapi-plugin-graphql/services/Loaders.js +++ b/packages/strapi-plugin-graphql/services/Loaders.js @@ -18,14 +18,16 @@ module.exports = { // Create loaders for each relational field (exclude core models). Object.keys(strapi.models) .filter(model => model !== 'core_store') - .forEach(model => { - this.createLoader(model); + .forEach(modelKey => { + const model = strapi.models[modelKey]; + this.createLoader(model.uid); }); // Reproduce the same pattern for each plugin. Object.keys(strapi.plugins).forEach(plugin => { - Object.keys(strapi.plugins[plugin].models).forEach(model => { - this.createLoader(model, plugin); + Object.keys(strapi.plugins[plugin].models).forEach(modelKey => { + const model = strapi.plugins[plugin].models[modelKey]; + this.createLoader(model.uid); }); }); }, @@ -34,29 +36,25 @@ module.exports = { this.loaders = {}; }, - createLoader: function(model, plugin) { - const name = plugin ? `${plugin}__${model}` : model; - - // Exclude polymorphic from loaders. - if (name === undefined) { - return; + createLoader: function(modelUID) { + if (this.loaders[modelUID]) { + return this.loaders[modelUID]; } - if (this.loaders[name]) { - return this.loaders[name]; - } - - this.loaders[name] = new DataLoader( + this.loaders[modelUID] = new DataLoader( keys => { // Extract queries from keys and merge similar queries. - const { queries, map } = this.extractQueries(model, _.cloneDeep(keys)); + const { queries, map } = this.extractQueries( + modelUID, + _.cloneDeep(keys) + ); // Run queries in parallel. return Promise.all( - queries.map(query => this.makeQuery(model, query)) + queries.map(query => this.makeQuery(modelUID, query)) ).then(results => { // Use to match initial queries order. - return this.mapData(model, keys, map, results); + return this.mapData(modelUID, keys, map, results); }); }, { @@ -67,7 +65,7 @@ module.exports = { ); }, - mapData: function(model, originalMap, map, results) { + mapData: function(modelUID, originalMap, map, results) { // Use map to re-dispatch data correctly based on initial keys. return originalMap.map((query, index) => { // Find the index of where we should extract the results. @@ -77,7 +75,7 @@ module.exports = { const data = results[indexResults]; // Retrieving referring model. - const ref = this.retrieveModel(model, query.options.source); + const ref = strapi.getModel(modelUID); if (query.single) { // Return object instead of array for one-to-many relationship. @@ -98,8 +96,8 @@ module.exports = { const ast = ref.associations.find(ast => ast.alias === ids.alias); const astModel = ast - ? this.retrieveModel(ast.model || ast.collection, ast.plugin) - : this.retrieveModel(model); + ? strapi.getModel(ast.model || ast.collection, ast.plugin) + : strapi.getModel(modelUID); if (!_.isArray(ids)) { return data @@ -144,12 +142,12 @@ module.exports = { }; }, - makeQuery: async function(model, query = {}) { + makeQuery: async function(modelUID, query = {}) { if (_.isEmpty(query.ids)) { return []; } - const ref = this.retrieveModel(model, query.options.source); + const ref = strapi.getModel(modelUID); const ast = ref.associations.find(ast => ast.alias === query.alias); const params = { @@ -169,15 +167,10 @@ module.exports = { // Run query and remove duplicated ID. return strapi.plugins['content-manager'].services[ 'contentmanager' - ].fetchAll({ model }, params); + ].fetchAll({ model: modelUID }, params); }, - retrieveModel: function(model, source) { - // Retrieve refering model. - return source ? strapi.plugins[source].models[model] : strapi.models[model]; - }, - - extractQueries: function(model, keys) { + extractQueries: function(modelUID, keys) { const queries = []; const map = []; @@ -188,16 +181,16 @@ module.exports = { const { query = {}, ...options } = current.options; // Retrieving referring model. - const ref = this.retrieveModel(model, options.source); + const { primaryKey } = strapi.getModel(modelUID); // Generate array of IDs to fetch. const ids = []; // Only one entry to fetch. if (single) { - ids.push(params[ref.primaryKey]); - } else if (_.isArray(query[ref.primaryKey])) { - ids.push(...query[ref.primaryKey]); + ids.push(params[primaryKey]); + } else if (_.isArray(query[primaryKey])) { + ids.push(...query[primaryKey]); } else { ids.push(query[association.via]); } @@ -205,7 +198,7 @@ module.exports = { queries.push({ ids, options, - alias: _.first(Object.keys(query)) || ref.primaryKey, + alias: _.first(Object.keys(query)) || primaryKey, }); map[queries.length - 1 > 0 ? queries.length - 1 : 0] = []; diff --git a/packages/strapi-plugin-graphql/services/Resolvers.js b/packages/strapi-plugin-graphql/services/Resolvers.js index ef8104a8bd..90ee9d878e 100644 --- a/packages/strapi-plugin-graphql/services/Resolvers.js +++ b/packages/strapi-plugin-graphql/services/Resolvers.js @@ -108,7 +108,7 @@ const mutateAssocAttributes = (associations = [], attributes) => { }); }; -const buildAssocResolvers = (model, name, { plugin }) => { +const buildAssocResolvers = model => { const contentManager = strapi.plugins['content-manager'].services['contentmanager']; @@ -117,100 +117,44 @@ const buildAssocResolvers = (model, name, { plugin }) => { return associations .filter(association => model.attributes[association.alias].private !== true) .reduce((resolver, association) => { + const target = association.model || association.collection; + const targetModel = strapi.getModel(target, association.plugin); + switch (association.nature) { - case 'oneToManyMorph': { - resolver[association.alias] = async obj => { - const entry = await contentManager.fetch( - { - id: obj[primaryKey], - model: name, - }, - plugin, - [association.alias] - ); - - // Set the _type only when the value is defined - if (entry[association.alias]) { - entry[association.alias]._type = _.upperFirst(association.model); - } - - return entry[association.alias]; - }; - break; - } + case 'oneToManyMorph': case 'manyMorphToOne': case 'manyMorphToMany': case 'manyToManyMorph': { resolver[association.alias] = async obj => { - // eslint-disable-line no-unused-vars - const [withRelated, withoutRelated] = await Promise.all([ - contentManager.fetch( - { - id: obj[primaryKey], - model: name, - }, - plugin, - [association.alias], - false - ), - contentManager.fetch( - { - id: obj[primaryKey], - model: name, - }, - plugin, - [] - ), - ]); + if (obj[association.alias]) { + return obj[association.alias]; + } - const entry = - withRelated && withRelated.toJSON - ? withRelated.toJSON() - : withRelated; - - entry[association.alias].map((entry, index) => { - const type = - _.get(withoutRelated, `${association.alias}.${index}.kind`) || - _.upperFirst( - _.camelCase( - _.get( - withoutRelated, - `${association.alias}.${index}.${association.alias}_type` - ) - ) - ) || - _.upperFirst(_.camelCase(association[association.type])); - - entry._type = type; - - return entry; - }); + const entry = await contentManager.fetch( + { + id: obj[primaryKey], + model: model.uid, + }, + [association.alias] + ); return entry[association.alias]; }; break; } - default: { resolver[association.alias] = async (obj, options) => { // Construct parameters object to retrieve the correct related entries. const params = { - model: association.model || association.collection, + model: targetModel.uid, }; - let queryOpts = { - source: association.plugin, - }; - - // Get refering model. - const ref = association.plugin - ? strapi.plugins[association.plugin].models[params.model] - : strapi.models[params.model]; + let queryOpts = {}; if (association.type === 'model') { - params[ref.primaryKey] = _.get( + params[targetModel.primaryKey] = _.get( obj, - [association.alias, ref.primaryKey], + [association.alias, targetModel.primaryKey], obj[association.alias] ); } else { @@ -229,10 +173,10 @@ const buildAssocResolvers = (model, name, { plugin }) => { ) { _.set( queryOpts, - ['query', ref.primaryKey], + ['query', targetModel.primaryKey], obj[association.alias] ? obj[association.alias] - .map(val => val[ref.primaryKey] || val) + .map(val => val[targetModel.primaryKey] || val) .sort() : [] ); @@ -240,25 +184,21 @@ const buildAssocResolvers = (model, name, { plugin }) => { _.set( queryOpts, ['query', association.via], - obj[ref.primaryKey] + obj[targetModel.primaryKey] ); } } - const loaderName = association.plugin - ? `${association.plugin}__${params.model}` - : params.model; - return association.model ? strapi.plugins.graphql.services.loaders.loaders[ - loaderName + targetModel.uid ].load({ params, options: queryOpts, single: true, }) : strapi.plugins.graphql.services.loaders.loaders[ - loaderName + targetModel.uid ].load({ options: queryOpts, association, @@ -272,16 +212,12 @@ const buildAssocResolvers = (model, name, { plugin }) => { }, {}); }; -const buildModel = ( - model, - name, - { schema, plugin, isComponent = false } = {} -) => { +const buildModel = (model, { schema, isComponent = false } = {}) => { const { globalId, primaryKey } = model; schema.resolvers[globalId] = { id: obj => obj[primaryKey], - ...buildAssocResolvers(model, name, { plugin }), + ...buildAssocResolvers(model), }; const initialState = { @@ -552,7 +488,7 @@ const buildShadowCRUD = (models, plugin) => { } // Build associations queries. - acc.resolvers[globalId] = buildAssocResolvers(model, name, { plugin }); + _.assign(acc.resolvers[globalId], buildAssocResolvers(model)); return acc; }, initialState); diff --git a/packages/strapi-plugin-graphql/services/Schema.js b/packages/strapi-plugin-graphql/services/Schema.js index 5f42291fe5..aa63281219 100644 --- a/packages/strapi-plugin-graphql/services/Schema.js +++ b/packages/strapi-plugin-graphql/services/Schema.js @@ -143,7 +143,7 @@ const schemaBuilder = { definition, query, mutation, - resolver, + resolvers, } = Resolvers.buildShadowCRUD(strapi.plugins[plugin].models, plugin); // We cannot put this in the merge because it's a string. @@ -151,7 +151,7 @@ const schemaBuilder = { return _.merge(acc, { query, - resolver, + resolvers, mutation, }); }, modelCruds); @@ -163,8 +163,7 @@ const schemaBuilder = { }; Object.keys(strapi.components).forEach(key => - Resolvers.buildModel(strapi.components[key], key, { - plugin: null, + Resolvers.buildModel(strapi.components[key], { isComponent: true, schema: componentsSchema, }) diff --git a/packages/strapi-plugin-graphql/services/Types.js b/packages/strapi-plugin-graphql/services/Types.js index d3da87d831..7b975c949e 100644 --- a/packages/strapi-plugin-graphql/services/Types.js +++ b/packages/strapi-plugin-graphql/services/Types.js @@ -222,8 +222,7 @@ module.exports = { polymorphicResolver: { Morph: { __resolveType(obj) { - // eslint-disable-line no-unused-vars - return obj.kind || obj._type; + return obj.kind || obj.__contentType || null; }, }, },