From 60bf24ce8b4b68959cdab9c350bf5e78efc8f378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Fri, 6 Nov 2020 14:35:36 +0100 Subject: [PATCH] Fix query when populating morph relations (#8548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * change query when populating morph relations Signed-off-by: Pierre Noël * refacto Signed-off-by: Pierre Noël * refacto Signed-off-by: Pierre Noël * refacto Signed-off-by: Pierre Noël --- docs/v3.x/concepts/models.md | 76 ----------- .../lib/generate-component-relations.js | 4 +- .../lib/mount-models.js | 121 ++++++++++-------- packages/strapi-utils/lib/models.js | 8 +- 4 files changed, 76 insertions(+), 133 deletions(-) diff --git a/docs/v3.x/concepts/models.md b/docs/v3.x/concepts/models.md index 5c69640368..d5cd965550 100644 --- a/docs/v3.x/concepts/models.md +++ b/docs/v3.x/concepts/models.md @@ -475,19 +475,6 @@ Let's stay with our `Image` model which might belong to **a single `Article` or **NOTE**: In other words, it means that an `Image` entry can be associated to one entry. This entry can be a `Article` or `Product` entry. -**Path —** `./api/image/models/Image.settings.json`. - -```json -{ - "attributes": { - "related": { - "model": "*", - "filter": "field" - } - } -} -``` - Also our `Image` model might belong to **many `Article` or `Product` entries**. **NOTE**: @@ -574,69 +561,6 @@ An `Image` model might belong to many `Article` models or `Product` models. } ``` -#### Database implementation - -If you're using MongoDB for your database, you don't need to do anything. Everything is natively handled by Strapi. However, to implement a polymorphic relationship with SQL databases, you need to create two tables. - -**Path —** `./api/image/models/Image.settings.json`. - -```json -{ - "attributes": { - "name": { - "type": "string" - }, - "url": { - "type": "string" - }, - "related": { - "collection": "*", - "filter": "field" - } - } -} -``` - -The first table to create is the table which has the same name as your model. - -``` -CREATE TABLE `image` ( - `id` int(11) NOT NULL, - `name` text NOT NULL, - `text` text NOT NULL -) -``` - -**NOTE**: -If you've overridden the default table name given by Strapi by using the `collectionName` attribute. Use the value set in the `collectionName` to name the table. - -The second table will allow us to associate one or many others entries to the `Image` model. The name of the table is the same as the previous one with the suffix `_morph`. - -``` -CREATE TABLE `image_morph` ( - `id` int(11) NOT NULL, - `image_id` int(11) NOT NULL, - `related_id` int(11) NOT NULL, - `related_type` text NOT NULL, - `field` text NOT NULL -) -``` - -- `image_id` is using the name of the first table with the suffix `_id`. - - **Attempted value:** It corresponds to the id of an `Image` entry. -- `related_id` is using the attribute name where the relation happens with the suffix `_id`. - - **Attempted value:** It corresponds to the id of an `Article` or `Product` entry. -- `related_type` is using the attribute name where the relation happens with the suffix `_type`. - - **Attempted value:** It corresponds to the table name where the `Article` or `Product` entry is stored. -- `field` is using the filter property value defined in the model. If you change the filter value, you have to change the name of this column as well. - - **Attempted value:** It corresponds to the attribute of an `Article`, `Product` with which the `Image` entry is related. - -| id | image_id | related_id | related_type | field | -| --- | -------- | ---------- | ------------ | ------ | -| 1 | 1738 | 39 | product | cover | -| 2 | 4738 | 58 | article | avatar | -| 3 | 1738 | 71 | article | avatar | - ::: :::: diff --git a/packages/strapi-connector-bookshelf/lib/generate-component-relations.js b/packages/strapi-connector-bookshelf/lib/generate-component-relations.js index 8fd7da1e08..138587f65c 100644 --- a/packages/strapi-connector-bookshelf/lib/generate-component-relations.js +++ b/packages/strapi-connector-bookshelf/lib/generate-component-relations.js @@ -41,9 +41,7 @@ const createComponentModels = async ({ model, definition, ORM, GLOBALS }) => { component() { return this.morphTo( 'component', - ...relatedComponents.map(component => { - return GLOBALS[component.globalId]; - }) + ...relatedComponents.map(component => GLOBALS[component.globalId]) ); }, }); diff --git a/packages/strapi-connector-bookshelf/lib/mount-models.js b/packages/strapi-connector-bookshelf/lib/mount-models.js index 2d33feb2a0..b95f30aef7 100644 --- a/packages/strapi-connector-bookshelf/lib/mount-models.js +++ b/packages/strapi-connector-bookshelf/lib/mount-models.js @@ -36,6 +36,27 @@ const getDatabaseName = connection => { } }; +const isARelatedField = (morphAttrInfo, attr) => { + const samePlugin = + morphAttrInfo.plugin === attr.plugin || (_.isNil(morphAttrInfo.plugin) && _.isNil(attr.plugin)); + const sameModel = [attr.model, attr.collection].includes(morphAttrInfo.model); + const isMorph = attr.via === morphAttrInfo.name; + + return isMorph && sameModel && samePlugin; +}; + +const getRelatedFieldsOfMorphModel = morphAttrInfo => morphModel => { + const relatedFields = _.reduce( + morphModel.attributes, + (fields, attr, attrName) => { + return isARelatedField(morphAttrInfo, attr) ? fields.concat(attrName) : fields; + }, + [] + ); + + return { collectionName: morphModel.collectionName, relatedFields }; +}; + module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {}) => { const { GLOBALS, connection, ORM } = ctx; @@ -354,40 +375,16 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {}) } case 'belongsToMorph': case 'belongsToManyMorph': { - const association = definition.associations.find( - association => association.alias === name + const association = _.find(definition.associations, { alias: name }); + const morphAttrInfo = { + plugin: definition.plugin, + model: definition.modelName, + name, + }; + const morphModelsAndFields = association.related.map( + getRelatedFieldsOfMorphModel(morphAttrInfo) ); - const morphValues = association.related.map(id => { - let models = Object.values(strapi.models).filter(model => model.globalId === id); - - if (models.length === 0) { - models = Object.values(strapi.components).filter(model => model.globalId === id); - } - - if (models.length === 0) { - models = Object.keys(strapi.plugins).reduce((acc, current) => { - const models = Object.values(strapi.plugins[current].models).filter( - model => model.globalId === id - ); - - if (acc.length === 0 && models.length > 0) { - acc = models; - } - - return acc; - }, []); - } - - if (models.length === 0) { - strapi.log.error(`Impossible to register the '${model}' model.`); - strapi.log.error('The collection name cannot be found for the morphTo method.'); - strapi.stop(); - } - - return models[0].collectionName; - }); - // Define new model. const options = { requireFetch: false, @@ -401,7 +398,7 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {}) related: function() { return this.morphTo( name, - ...association.related.map((id, index) => [GLOBALS[id], morphValues[index]]) + ...association.related.map(morphModel => [morphModel, morphModel.collectionName]) ); }, }; @@ -413,12 +410,31 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {}) // Hack Bookshelf to create a many-to-many polymorphic association. // Upload has many Upload_morph that morph to different model. + const populateFn = qb => { + qb.where(qb => { + for (const modelAndFields of morphModelsAndFields) { + qb.orWhere(qb => { + qb.where({ related_type: modelAndFields.collectionName }).whereIn( + 'field', + modelAndFields.relatedFields + ); + }); + } + }); + }; + loadedModel[name] = function() { if (verbose === 'belongsToMorph') { - return this.hasOne(GLOBALS[options.tableName], `${definition.collectionName}_id`); + return this.hasOne( + GLOBALS[options.tableName], + `${definition.collectionName}_id` + ).query(populateFn); } - return this.hasMany(GLOBALS[options.tableName], `${definition.collectionName}_id`); + return this.hasMany( + GLOBALS[options.tableName], + `${definition.collectionName}_id` + ).query(populateFn); }; break; } @@ -632,28 +648,33 @@ module.exports = async ({ models, target }, ctx, { selfFinalize = false } = {}) target[model].privateAttributes = contentTypesUtils.getPrivateAttributes(target[model]); return async () => { - await buildDatabaseSchema({ - ORM, - definition, - loadedModel, - connection, - model: target[model], - }); + try { + await buildDatabaseSchema({ + ORM, + definition, + loadedModel, + connection, + model: target[model], + }); - await createComponentJoinTables({ definition, ORM }); + await createComponentJoinTables({ definition, ORM }); + } catch (err) { + if (['ER_TOO_LONG_IDENT'].includes(err.code)) { + strapi.stopWithError( + err, + `A table name is too long. If it is the name of a join table automatically generated by Strapi, you can customise it by adding \`collectionName: "customName"\` in the corresponding model's attribute. + When this happens on a manyToMany relation, make sure to set this parameter on the dominant side of the relation (e.g: where \`dominant: true\` is set)` + ); + } + + strapi.stopWithError(err); + } }; } catch (err) { if (err instanceof TypeError || err instanceof ReferenceError) { strapi.stopWithError(err, `Impossible to register the '${model}' model.`); } - if (['ER_TOO_LONG_IDENT'].includes(err.code)) { - strapi.stopWithError( - err, - `A table name is too long. If it is the name of a join table automatically generated by Strapi, you can customise it by adding \`collectionName: "customName"\` in the corresponding model's attribute. -When this happens on a manyToMany relation, make sure to set this parameter on the dominant side of the relation (e.g: where \`dominant: true\` is set)` - ); - } strapi.stopWithError(err); } }; diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index f847682e2f..154f411a4f 100644 --- a/packages/strapi-utils/lib/models.js +++ b/packages/strapi-utils/lib/models.js @@ -424,7 +424,7 @@ module.exports = { const attr = strapi.plugins[current].models[entity].attributes[attribute]; if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) { - acc.push(strapi.plugins[current].models[entity].globalId); + acc.push(strapi.plugins[current].models[entity]); } }); }); @@ -437,7 +437,7 @@ module.exports = { const attr = strapi.models[entity].attributes[attribute]; if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) { - acc.push(strapi.models[entity].globalId); + acc.push(strapi.models[entity]); } }); @@ -449,14 +449,14 @@ module.exports = { const attr = strapi.components[entity].attributes[attribute]; if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) { - acc.push(strapi.components[entity].globalId); + acc.push(strapi.components[entity]); } }); return acc; }, []); - const models = _.uniq(appModels.concat(pluginsModels).concat(componentModels)); + const models = _.uniqWith(appModels.concat(pluginsModels, componentModels), _.isEqual); definition.associations.push({ alias: key,