From dc75b51d6f473ebfb53a3cb74961df6e25da641f Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Tue, 20 Feb 2018 19:59:05 +0100 Subject: [PATCH 1/8] [WIP] Polymorphic associations with Bookshelf --- packages/strapi-bookshelf/.editorconfig | 16 --- packages/strapi-bookshelf/lib/index.js | 103 ++++++++++++++++++ packages/strapi-bookshelf/package.json | 4 +- packages/strapi-knex/package.json | 2 +- packages/strapi-mongoose/lib/index.js | 64 ++++++----- .../models/User.settings.json | 8 ++ packages/strapi-utils/lib/models.js | 38 ++++++- packages/strapi/lib/core/store.js | 4 +- packages/strapi/lib/middlewares/mask/index.js | 3 + 9 files changed, 191 insertions(+), 51 deletions(-) delete mode 100755 packages/strapi-bookshelf/.editorconfig diff --git a/packages/strapi-bookshelf/.editorconfig b/packages/strapi-bookshelf/.editorconfig deleted file mode 100755 index 473e45184b..0000000000 --- a/packages/strapi-bookshelf/.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[{package.json,*.yml}] -indent_style = space -indent_size = 2 - -[*.md] -trim_trailing_whitespace = false diff --git a/packages/strapi-bookshelf/lib/index.js b/packages/strapi-bookshelf/lib/index.js index 883761975a..006475ef39 100755 --- a/packages/strapi-bookshelf/lib/index.js +++ b/packages/strapi-bookshelf/lib/index.js @@ -133,6 +133,30 @@ module.exports = function(strapi) { : key; }); + if (definition.globalId === 'Upload') { + loadedModel.serialize = function(options) { + const attrs = _.clone(this.attributes); + + if (options && options.shallow) { + return attrs; + } + + const relations = this.relations; + + for (let key in relations) { + const relation = relations[key]; + + attrs[key] = relation.toJSON ? relation.toJSON(options) : relation; + + if (key === 'related') { + attrs[key] = attrs[key].map(rel => rel['related']); + } + } + + return attrs; + } + } + // Initialize lifecycle callbacks. loadedModel.initialize = function() { const lifecycle = { @@ -150,6 +174,22 @@ module.exports = function(strapi) { saved: 'afterSave' }; + if (definition.globalId === 'Upload') { + this.on('fetching fetching:collection', (instance, attrs, options) => { + if (_.isArray(options.withRelated)) { + options.withRelated = options.withRelated.map(path => { + if (_.isString(path) && path === 'related') { + return 'related.related'; + } + + return path; + }) + } + + return Promise.resolve(); + }); + } + _.forEach(lifecycle, (fn, key) => { if (_.isFunction(target[model.toLowerCase()][fn])) { this.on(key, target[model.toLowerCase()][fn]); @@ -369,6 +409,69 @@ module.exports = function(strapi) { }; break; } + case 'morphOne': { + loadedModel[name] = function() { + return this.morphOne(GLOBALS[globalId], details.via); + } + break; + } + case 'morphMany': { + loadedModel[name] = function() { + return this.morphMany(GLOBALS[globalId], details.via); + } + break; + } + case 'belongsToMorph': + case 'belongsToManyMorph': { + const association = definition.associations + .find(association => association.alias === name); + + // console.log("coucou"); + // console.log(association.related.map(id => GLOBALS[id])); + + const morphValues = association.related.map(id => { + let models = Object.values(strapi.models).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 = { + tableName: `${loadedModel.tableName}_morph`, + related: function () { + return this.morphTo(name, ...association.related.map((id, index) => [GLOBALS[id], morphValues[index]])); + } + }; + + const MorphModel = ORM.Model.extend(options); + + loadedModel[name] = function () { + return this.hasMany( + MorphModel, + 'upload_id' + ); + }; + break; + } default: { break; } diff --git a/packages/strapi-bookshelf/package.json b/packages/strapi-bookshelf/package.json index 6ac489bacb..2db9f3e606 100755 --- a/packages/strapi-bookshelf/package.json +++ b/packages/strapi-bookshelf/package.json @@ -16,7 +16,7 @@ }, "main": "./lib", "dependencies": { - "bookshelf": "^0.10.3", + "bookshelf": "^0.12.1", "lodash": "^4.17.4", "pluralize": "^6.0.0", "strapi-knex": "3.0.0-alpha.9.3", @@ -55,4 +55,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/strapi-knex/package.json b/packages/strapi-knex/package.json index 839ed9abcb..1196150f53 100755 --- a/packages/strapi-knex/package.json +++ b/packages/strapi-knex/package.json @@ -46,4 +46,4 @@ "npm": ">= 5.0.0" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/strapi-mongoose/lib/index.js b/packages/strapi-mongoose/lib/index.js index 1e87bec89d..d446479138 100755 --- a/packages/strapi-mongoose/lib/index.js +++ b/packages/strapi-mongoose/lib/index.js @@ -296,20 +296,6 @@ module.exports = function (strapi) { justOne: true }; - // Set this info to be able to see if this field is a real database's field. - details.isVirtual = true; - } else if (FK.nature === 'oneToMorph') { - const key = details.plugin ? - strapi.plugins[details.plugin].models[details.model].attributes[details.via].key: - strapi.models[details.model].attributes[details.via].key; - - definition.loadedModel[name] = { - type: 'virtual', - ref, - via: `${FK.via}.${key}`, - justOne: true - }; - // Set this info to be able to see if this field is a real database's field. details.isVirtual = true; } else { @@ -326,26 +312,13 @@ module.exports = function (strapi) { const ref = details.plugin ? strapi.plugins[details.plugin].models[details.collection].globalId : strapi.models[details.collection].globalId; // One-side of the relationship has to be a virtual field to be bidirectional. - if ((FK && _.isUndefined(FK.via)) || details.dominant !== true && FK.nature !== 'manyToMorph') { + if ((FK && _.isUndefined(FK.via)) || details.dominant !== true) { definition.loadedModel[name] = { type: 'virtual', ref, via: FK.via }; - // Set this info to be able to see if this field is a real database's field. - details.isVirtual = true; - } else if (FK.nature === 'manyToMorph') { - const key = details.plugin ? - strapi.plugins[details.plugin].models[details.collection].attributes[details.via].key: - strapi.models[details.collection].attributes[details.via].key; - - definition.loadedModel[name] = { - type: 'virtual', - ref, - via: `${FK.via}.${key}` - }; - // Set this info to be able to see if this field is a real database's field. details.isVirtual = true; } else { @@ -356,6 +329,41 @@ module.exports = function (strapi) { } break; } + case 'morphOne': { + const FK = _.find(definition.associations, {alias: name}); + const ref = details.plugin ? strapi.plugins[details.plugin].models[details.model].globalId : strapi.models[details.model].globalId; + const key = details.plugin ? + strapi.plugins[details.plugin].models[details.model].attributes[details.via].key: + strapi.models[details.model].attributes[details.via].key; + + definition.loadedModel[name] = { + type: 'virtual', + ref, + via: `${FK.via}.${key}`, + justOne: true + }; + + // Set this info to be able to see if this field is a real database's field. + details.isVirtual = true; + break; + } + case 'morphMany': { + const FK = _.find(definition.associations, {alias: name}); + const ref = details.plugin ? strapi.plugins[details.plugin].models[details.collection].globalId : strapi.models[details.collection].globalId; + const key = details.plugin ? + strapi.plugins[details.plugin].models[details.collection].attributes[details.via].key: + strapi.models[details.collection].attributes[details.via].key; + + definition.loadedModel[name] = { + type: 'virtual', + ref, + via: `${FK.via}.${key}` + }; + + // Set this info to be able to see if this field is a real database's field. + details.isVirtual = true; + break; + } case 'belongsToMorph': { definition.loadedModel[name] = { kind: String, diff --git a/packages/strapi-plugin-users-permissions/models/User.settings.json b/packages/strapi-plugin-users-permissions/models/User.settings.json index 99479b1cba..289c93a6c1 100644 --- a/packages/strapi-plugin-users-permissions/models/User.settings.json +++ b/packages/strapi-plugin-users-permissions/models/User.settings.json @@ -38,6 +38,14 @@ "via": "users", "plugin": "users-permissions", "configurable": false + }, + "avatar": { + "model": "upload", + "via": "related" + }, + "photos": { + "collection": "upload", + "via": "related" } } } diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index b7872d5013..0bb00559b6 100755 --- a/packages/strapi-utils/lib/models.js +++ b/packages/strapi-utils/lib/models.js @@ -199,12 +199,12 @@ module.exports = { if (types.current === 'collection' && types.other === 'morphTo') { return { nature: 'manyToMorph', - verbose: 'belongsToMany' + verbose: 'morphMany' }; } else if (types.current === 'modelD' && types.other === 'morphTo') { return { nature: 'oneToMorph', - verbose: 'belongsTo' + verbose: 'morphOne' }; } else if (types.current === 'morphTo' && types.other === 'collection') { return { @@ -324,9 +324,43 @@ module.exports = { where: details.where, }); } else if (association.hasOwnProperty('key')) { + const pluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { + Object.keys(strapi.plugins[current].models).forEach((entity) => { + Object.keys(strapi.plugins[current].models[entity].attributes).forEach((attribute) => { + const attr = strapi.plugins[current].models[entity].attributes[attribute]; + if ( + (attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() && + strapi.plugins[current].models[entity].globalId !== definition.globalId + ) { + acc.push(strapi.plugins[current].models[entity].globalId); + } + }); + }); + + return acc; + }, []); + + const appModels = Object.keys(strapi.models).reduce((acc, entity) => { + Object.keys(strapi.models[entity].attributes).forEach((attribute) => { + const attr = strapi.models[entity].attributes[attribute]; + + if ( + (attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() && + strapi.models[entity].globalId !== definition.globalId + ) { + acc.push(strapi.models[entity].globalId); + } + }); + + return acc; + }, []); + + const models = _.uniq(appModels.concat(pluginsModels)); + definition.associations.push({ alias: key, type: 'collection', + related: models, nature: infos.nature, autoPopulate: _.get(association, 'autoPopulate', true), key: association.key, diff --git a/packages/strapi/lib/core/store.js b/packages/strapi/lib/core/store.js index 5051cdac63..266a7c2b95 100644 --- a/packages/strapi/lib/core/store.js +++ b/packages/strapi/lib/core/store.js @@ -154,8 +154,8 @@ module.exports = { CREATE TABLE ${quote}${Model.tableName || Model.collectionName}${quote} ( id ${Model.client === 'pg' ? 'SERIAL' : 'INT AUTO_INCREMENT'} NOT NULL PRIMARY KEY, - key text, - value text, + ${quote}key${quote} text, + ${quote}value${quote} text, environment text, type text, tag text diff --git a/packages/strapi/lib/middlewares/mask/index.js b/packages/strapi/lib/middlewares/mask/index.js index 5bf2e918e2..1e49818842 100644 --- a/packages/strapi/lib/middlewares/mask/index.js +++ b/packages/strapi/lib/middlewares/mask/index.js @@ -53,6 +53,9 @@ module.exports = strapi => { // Recursive to mask the private properties. const mask = (payload) => { + // Handle ORM toJSON() method to work on real JSON object. + payload = payload.toJSON ? payload.toJSON() : payload; + if (_.isArray(payload)) { return payload.map(mask); } else if (_.isPlainObject(payload)) { From dd354caf60d939784d923bfc63cc43260200b76a Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Wed, 21 Feb 2018 16:35:25 +0100 Subject: [PATCH 2/8] Handle most polymorphic use cases with Bookshelf --- packages/strapi-bookshelf/lib/index.js | 165 +++++++++++++----- packages/strapi-bookshelf/package.json | 2 +- packages/strapi-knex/package.json | 2 +- packages/strapi/lib/middlewares/mask/index.js | 2 +- 4 files changed, 122 insertions(+), 49 deletions(-) diff --git a/packages/strapi-bookshelf/lib/index.js b/packages/strapi-bookshelf/lib/index.js index 006475ef39..08768e2f4d 100755 --- a/packages/strapi-bookshelf/lib/index.js +++ b/packages/strapi-bookshelf/lib/index.js @@ -133,28 +133,44 @@ module.exports = function(strapi) { : key; }); - if (definition.globalId === 'Upload') { - loadedModel.serialize = function(options) { - const attrs = _.clone(this.attributes); - - if (options && options.shallow) { - return attrs; - } - - const relations = this.relations; - - for (let key in relations) { - const relation = relations[key]; - - attrs[key] = relation.toJSON ? relation.toJSON(options) : relation; - - if (key === 'related') { - attrs[key] = attrs[key].map(rel => rel['related']); - } - } + // Update serialize to reformat data for polymorphic associations. + loadedModel.serialize = function(options) { + const attrs = _.clone(this.attributes); + if (options && options.shallow) { return attrs; } + + const relations = this.relations; + + definition.associations + .filter(association => association.nature.toLowerCase().indexOf('morph') !== -1) + .map(association => { + // Retrieve relation Bookshelf object. + const relation = relations[association.alias]; + + if (relation) { + // Extract raw JSON data. + attrs[association.alias] = relation.toJSON ? relation.toJSON(options) : relation; + + // Retrieve opposite model. + const model = association.plugin ? + strapi.plugins[association.plugin].models[association.collection || association.model]: + strapi.models[association.collection || association.model]; + + // Reformat data by bypassing the many-to-many relationship. + if (association.nature === 'oneToMorph') { + attrs[association.alias] = attrs[association.alias][model.collectionName]; + } else if (association.nature === 'manyToMorph') { + attrs[association.alias] = attrs[association.alias].map(rel => rel[model.collectionName]); + } else { + // MorphToX cases. + attrs[association.alias] = attrs[association.alias].map(rel => rel[association.alias]); + } + } + }); + + return attrs; } // Initialize lifecycle callbacks. @@ -174,28 +190,52 @@ module.exports = function(strapi) { saved: 'afterSave' }; - if (definition.globalId === 'Upload') { - this.on('fetching fetching:collection', (instance, attrs, options) => { - if (_.isArray(options.withRelated)) { - options.withRelated = options.withRelated.map(path => { - if (_.isString(path) && path === 'related') { - return 'related.related'; - } - - return path; - }) - } - - return Promise.resolve(); - }); - } - _.forEach(lifecycle, (fn, key) => { if (_.isFunction(target[model.toLowerCase()][fn])) { this.on(key, target[model.toLowerCase()][fn]); } }); + // Update withRelated level to bypass many-to-many association for polymorphic relationshiips. + // Apply only during fetching. + this.on('fetching:collection', (instance, attrs, options) => { + if (_.isArray(options.withRelated)) { + options.withRelated = options.withRelated.map(path => { + const association = definition.associations + .filter(association => association.nature.toLowerCase().indexOf('morph') !== -1) + .filter(association => association.alias === path || association.via === path)[0]; + + if (association) { + // Override on polymorphic path only. + if (_.isString(path) && path === association.via) { + return `related.${association.via}`; + } else if (_.isString(path) && path === association.alias) { + // MorphTo side. + if (association.related) { + return `${association.alias}.related`; + } + + // oneToMorph or manyToMorph side. + // Retrieve collection name because we are using it to build our hidden model. + const model = association.plugin ? + strapi.plugins[association.plugin].models[association.collection || association.model]: + strapi.models[association.collection || association.model]; + + return `${association.alias}.${model.collectionName}`; + } + } + + return path; + }); + } + + return _.isFunction( + target[model.toLowerCase()]['beforeFetchCollection'] + ) + ? target[model.toLowerCase()]['beforeFetchCollection'] + : Promise.resolve(); + }); + this.on('saving', (instance, attrs, options) => { instance.attributes = mapper(instance.attributes); attrs = mapper(attrs); @@ -410,14 +450,34 @@ module.exports = function(strapi) { break; } case 'morphOne': { + const model = details.plugin ? + strapi.plugins[details.plugin].models[details.model]: + strapi.models[details.model]; + + const globalId = `${model.collectionName}_morph`; + loadedModel[name] = function() { - return this.morphOne(GLOBALS[globalId], details.via); + return this + .morphOne(GLOBALS[globalId], details.via, `${definition.collectionName}`) + .query(qb => { + qb.where('field', name); + }); } break; } case 'morphMany': { + const collection = details.plugin ? + strapi.plugins[details.plugin].models[details.collection]: + strapi.models[details.collection]; + + const globalId = `${collection.collectionName}_morph`; + loadedModel[name] = function() { - return this.morphMany(GLOBALS[globalId], details.via); + return this + .morphMany(GLOBALS[globalId], details.via, `${definition.collectionName}`) + .query(qb => { + qb.where('field', name); + }); } break; } @@ -426,9 +486,6 @@ module.exports = function(strapi) { const association = definition.associations .find(association => association.alias === name); - // console.log("coucou"); - // console.log(association.related.map(id => GLOBALS[id])); - const morphValues = association.related.map(id => { let models = Object.values(strapi.models).filter(model => model.globalId === id); @@ -453,21 +510,37 @@ module.exports = function(strapi) { return models[0].collectionName; }); - // Define new model. const options = { - tableName: `${loadedModel.tableName}_morph`, + tableName: `${definition.collectionName}_morph`, + [definition.collectionName]: function () { + return this + .belongsTo( + GLOBALS[definition.globalId], + `${definition.collectionName}_id` + ); + }, related: function () { - return this.morphTo(name, ...association.related.map((id, index) => [GLOBALS[id], morphValues[index]])); + return this + .morphTo(name, ...association.related.map((id, index) => [GLOBALS[id], morphValues[index]])); } }; - const MorphModel = ORM.Model.extend(options); + GLOBALS[options.tableName] = ORM.Model.extend(options); + // Hack Bookshelf to create a many-to-many polymorphic association. + // Upload has many Upload_morph that morph to different model. loadedModel[name] = function () { + if (verbose === 'belongsToMorph') { + return this.hasOne( + GLOBALS[options.tableName], + `${definition.collectionName}_id` + ); + } + return this.hasMany( - MorphModel, - 'upload_id' + GLOBALS[options.tableName], + `${definition.collectionName}_id` ); }; break; diff --git a/packages/strapi-bookshelf/package.json b/packages/strapi-bookshelf/package.json index 2db9f3e606..4bc5b61f29 100755 --- a/packages/strapi-bookshelf/package.json +++ b/packages/strapi-bookshelf/package.json @@ -55,4 +55,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-knex/package.json b/packages/strapi-knex/package.json index 1196150f53..839ed9abcb 100755 --- a/packages/strapi-knex/package.json +++ b/packages/strapi-knex/package.json @@ -46,4 +46,4 @@ "npm": ">= 5.0.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi/lib/middlewares/mask/index.js b/packages/strapi/lib/middlewares/mask/index.js index 1e49818842..d95ee8424f 100644 --- a/packages/strapi/lib/middlewares/mask/index.js +++ b/packages/strapi/lib/middlewares/mask/index.js @@ -54,7 +54,7 @@ module.exports = strapi => { // Recursive to mask the private properties. const mask = (payload) => { // Handle ORM toJSON() method to work on real JSON object. - payload = payload.toJSON ? payload.toJSON() : payload; + payload = payload && payload.toJSON ? payload.toJSON() : payload; if (_.isArray(payload)) { return payload.map(mask); From 26c10cddd08e15a57418422dc4c1d89b343ffc06 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Wed, 21 Feb 2018 17:33:30 +0100 Subject: [PATCH 3/8] Handle morphToOne and morphToMany verbose --- packages/strapi-bookshelf/lib/index.js | 26 ++++++++++------ packages/strapi-utils/lib/models.js | 42 +++++++++++++++++++++----- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/strapi-bookshelf/lib/index.js b/packages/strapi-bookshelf/lib/index.js index 08768e2f4d..8cae197bee 100755 --- a/packages/strapi-bookshelf/lib/index.js +++ b/packages/strapi-bookshelf/lib/index.js @@ -159,13 +159,21 @@ module.exports = function(strapi) { strapi.models[association.collection || association.model]; // Reformat data by bypassing the many-to-many relationship. - if (association.nature === 'oneToMorph') { - attrs[association.alias] = attrs[association.alias][model.collectionName]; - } else if (association.nature === 'manyToMorph') { - attrs[association.alias] = attrs[association.alias].map(rel => rel[model.collectionName]); - } else { - // MorphToX cases. - attrs[association.alias] = attrs[association.alias].map(rel => rel[association.alias]); + switch (association.nature) { + case 'oneToMorph': + attrs[association.alias] = attrs[association.alias][model.collectionName]; + break; + case 'manyToMorph': + attrs[association.alias] = attrs[association.alias].map(rel => rel[model.collectionName]); + break; + case 'morphToOne': + attrs[association.alias] = attrs[association.alias][association.alias]; + break; + case 'morphToMany': + attrs[association.alias] = attrs[association.alias].map(rel => rel[association.alias]); + break; + default: + } } }); @@ -460,7 +468,7 @@ module.exports = function(strapi) { return this .morphOne(GLOBALS[globalId], details.via, `${definition.collectionName}`) .query(qb => { - qb.where('field', name); + qb.where(_.get(model, `attributes.${details.via}.filter`, 'field'), name); }); } break; @@ -476,7 +484,7 @@ module.exports = function(strapi) { return this .morphMany(GLOBALS[globalId], details.via, `${definition.collectionName}`) .query(qb => { - qb.where('field', name); + qb.where(_.get(collection, `attributes.${details.via}.filter`, 'field'), name); }); } break; diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index 0bb00559b6..41c65decf8 100755 --- a/packages/strapi-utils/lib/models.js +++ b/packages/strapi-utils/lib/models.js @@ -163,8 +163,12 @@ module.exports = { } }); }); - } else if (association.hasOwnProperty('key')) { - types.current = 'morphTo'; + } else if (association.hasOwnProperty('morphTo')) { + if (association.morphTo === 'single') { + types.current = 'morphToD'; + } else { + types.current = 'morphTo'; + } const flattenedPluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { Object.keys(strapi.plugins[current].models).forEach((model) => { @@ -197,23 +201,43 @@ module.exports = { } if (types.current === 'collection' && types.other === 'morphTo') { + return { + nature: 'manyToManyMorph', + verbose: 'morphMany' + }; + } else if (types.current === 'collection' && types.other === 'morphToD') { return { nature: 'manyToMorph', verbose: 'morphMany' }; - } else if (types.current === 'modelD' && types.other === 'morphTo') { + } else if (types.current === 'modelD' && types.other === 'morphTo') { + return { + nature: 'oneToManyMorph', + verbose: 'morphOne' + }; + } else if (types.current === 'modelD' && types.other === 'morphToD') { return { nature: 'oneToMorph', verbose: 'morphOne' }; - } else if (types.current === 'morphTo' && types.other === 'collection') { + } else if (types.current === 'morphToD' && types.other === 'collection') { return { nature: 'morphToMany', verbose: 'belongsToMorph' }; - } else if (types.current === 'morphTo' && types.other === 'model') { + } else if (types.current === 'morphToD' && types.other === 'model') { return { nature: 'morphToOne', + verbose: 'belongsToMorph' + }; + } else if (types.current === 'morphTo' && types.other === 'model') { + return { + nature: 'manyMorphToOne', + verbose: 'belongsToManyMorph' + }; + } else if (types.current === 'morphTo' && types.other === 'collection') { + return { + nature: 'manyMorphToMany', verbose: 'belongsToManyMorph' }; } else if (types.current === 'modelD' && types.other === 'model') { @@ -266,6 +290,7 @@ module.exports = { return undefined; } catch (e) { strapi.log.error(`Something went wrong in the model \`${_.upperFirst(currentModelName)}\` with the attribute \`${key}\``); + strapi.log.error(e); strapi.stop(); } }, @@ -290,7 +315,7 @@ module.exports = { } // Exclude non-relational attribute - if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model') && !association.hasOwnProperty('key')) { + if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model') && !association.hasOwnProperty('morphTo')) { return undefined; } @@ -323,7 +348,7 @@ module.exports = { plugin: association.plugin || undefined, where: details.where, }); - } else if (association.hasOwnProperty('key')) { + } else if (association.hasOwnProperty('morphTo')) { const pluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { Object.keys(strapi.plugins[current].models).forEach((entity) => { Object.keys(strapi.plugins[current].models[entity].attributes).forEach((attribute) => { @@ -359,7 +384,7 @@ module.exports = { definition.associations.push({ alias: key, - type: 'collection', + type: association.morphTo === 'single' ? 'model' : 'collection', related: models, nature: infos.nature, autoPopulate: _.get(association, 'autoPopulate', true), @@ -368,6 +393,7 @@ module.exports = { } } catch (e) { strapi.log.error(`Something went wrong in the model \`${_.upperFirst(model)}\` with the attribute \`${key}\``); + strapi.log.error(e); strapi.stop(); } }, From 946f0f17b3814f070a9700d043d714b780501ab2 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Thu, 22 Feb 2018 15:34:33 +0100 Subject: [PATCH 4/8] Enhance polymorphic syntax --- packages/strapi-bookshelf/lib/index.js | 24 ++-- packages/strapi-mongoose/lib/index.js | 24 ++-- .../models/User.settings.json | 4 +- packages/strapi-utils/lib/models.js | 113 +++++++++--------- 4 files changed, 85 insertions(+), 80 deletions(-) diff --git a/packages/strapi-bookshelf/lib/index.js b/packages/strapi-bookshelf/lib/index.js index 8cae197bee..4731715b9f 100755 --- a/packages/strapi-bookshelf/lib/index.js +++ b/packages/strapi-bookshelf/lib/index.js @@ -160,17 +160,17 @@ module.exports = function(strapi) { // Reformat data by bypassing the many-to-many relationship. switch (association.nature) { - case 'oneToMorph': + case 'oneToManyMorph': attrs[association.alias] = attrs[association.alias][model.collectionName]; break; - case 'manyToMorph': + case 'manyToManyMorph': attrs[association.alias] = attrs[association.alias].map(rel => rel[model.collectionName]); break; - case 'morphToOne': - attrs[association.alias] = attrs[association.alias][association.alias]; + case 'oneMorphToOne': + attrs[association.alias] = attrs[association.alias].related; break; - case 'morphToMany': - attrs[association.alias] = attrs[association.alias].map(rel => rel[association.alias]); + case 'manyMorphToOne': + attrs[association.alias] = attrs[association.alias].map(obj => obj.related); break; default: @@ -313,9 +313,15 @@ module.exports = function(strapi) { name ); - const globalId = details.plugin ? - _.get(strapi.plugins,`${details.plugin}.models.${(details.model || details.collection || '').toLowerCase()}.globalId`): - _.get(strapi.models, `${(details.model || details.collection || '').toLowerCase()}.globalId`); + let globalId; + const globalName = details.model || details.collection || ''; + + // Exclude polymorphic association. + if (globalName !== '*') { + globalId = details.plugin ? + _.get(strapi.plugins,`${details.plugin}.models.${globalName.toLowerCase()}.globalId`): + _.get(strapi.models, `${globalName.toLowerCase()}.globalId`); + } switch (verbose) { case 'hasOne': { diff --git a/packages/strapi-mongoose/lib/index.js b/packages/strapi-mongoose/lib/index.js index d446479138..de3b248275 100755 --- a/packages/strapi-mongoose/lib/index.js +++ b/packages/strapi-mongoose/lib/index.js @@ -106,11 +106,11 @@ module.exports = function (strapi) { if (this._mongooseOptions.populate && this._mongooseOptions.populate[association.alias]) { if (association.nature === 'oneToMorph' || association.nature === 'manyToMorph') { this._mongooseOptions.populate[association.alias].match = { - [`${association.via}.${association.where}`]: association.alias, + [`${association.via}.${association.filter}`]: association.alias, [`${association.via}.kind`]: definition.globalId } } else { - this._mongooseOptions.populate[association.alias].path = `${association.alias}.${association.key}`; + this._mongooseOptions.populate[association.alias].path = `${association.alias}.${association.filter}`; } } @@ -163,7 +163,7 @@ module.exports = function (strapi) { transform: function (doc, returned, opts) { morphAssociations.forEach(association => { if (Array.isArray(returned[association.alias]) && returned[association.alias].length > 0) { - returned[association.alias] = returned[association.alias].map(o => o[association.key]); + returned[association.alias] = returned[association.alias].map(o => o[association.filter]); } }); } @@ -332,14 +332,11 @@ module.exports = function (strapi) { case 'morphOne': { const FK = _.find(definition.associations, {alias: name}); const ref = details.plugin ? strapi.plugins[details.plugin].models[details.model].globalId : strapi.models[details.model].globalId; - const key = details.plugin ? - strapi.plugins[details.plugin].models[details.model].attributes[details.via].key: - strapi.models[details.model].attributes[details.via].key; definition.loadedModel[name] = { type: 'virtual', ref, - via: `${FK.via}.${key}`, + via: `${FK.via}.ref`, justOne: true }; @@ -350,14 +347,11 @@ module.exports = function (strapi) { case 'morphMany': { const FK = _.find(definition.associations, {alias: name}); const ref = details.plugin ? strapi.plugins[details.plugin].models[details.collection].globalId : strapi.models[details.collection].globalId; - const key = details.plugin ? - strapi.plugins[details.plugin].models[details.collection].attributes[details.via].key: - strapi.models[details.collection].attributes[details.via].key; definition.loadedModel[name] = { type: 'virtual', ref, - via: `${FK.via}.${key}` + via: `${FK.via}.ref` }; // Set this info to be able to see if this field is a real database's field. @@ -367,8 +361,8 @@ module.exports = function (strapi) { case 'belongsToMorph': { definition.loadedModel[name] = { kind: String, - [details.where]: String, - [details.key]: { + [details.filter]: String, + ref: { type: instance.Schema.Types.ObjectId, refPath: `${name}.kind` } @@ -378,8 +372,8 @@ module.exports = function (strapi) { case 'belongsToManyMorph': { definition.loadedModel[name] = [{ kind: String, - [details.where]: String, - [details.key]: { + [details.filter]: String, + ref: { type: instance.Schema.Types.ObjectId, refPath: `${name}.kind` } diff --git a/packages/strapi-plugin-users-permissions/models/User.settings.json b/packages/strapi-plugin-users-permissions/models/User.settings.json index 289c93a6c1..aea98658d3 100644 --- a/packages/strapi-plugin-users-permissions/models/User.settings.json +++ b/packages/strapi-plugin-users-permissions/models/User.settings.json @@ -41,11 +41,11 @@ }, "avatar": { "model": "upload", - "via": "related" + "via": "uploaded" }, "photos": { "collection": "upload", - "via": "related" + "via": "uploaded" } } } diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index 41c65decf8..0fec435c4b 100755 --- a/packages/strapi-utils/lib/models.js +++ b/packages/strapi-utils/lib/models.js @@ -83,18 +83,53 @@ module.exports = { models = association.plugin ? strapi.plugins[association.plugin].models : strapi.models; } - if (association.hasOwnProperty('via') && association.hasOwnProperty('collection')) { + if ((association.hasOwnProperty('collection') && association.collection === '*') || (association.hasOwnProperty('model') && association.model === '*')) { + if (association.model) { + types.current = 'morphToD'; + } else { + types.current = 'morphTo'; + } + + const flattenedPluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { + Object.keys(strapi.plugins[current].models).forEach((model) => { + acc[`${current}_${model}`] = strapi.plugins[current].models[model]; + }); + + return acc; + }, {}); + + const allModels = _.merge({}, strapi.models, flattenedPluginsModels); + + // We have to find if they are a model linked to this key + _.forIn(allModels, model => { + _.forIn(model.attributes, attribute => { + if (attribute.hasOwnProperty('via') && attribute.via === key) { + if (attribute.hasOwnProperty('collection')) { + types.other = 'collection'; + + // Break loop + return false; + } else if (attribute.hasOwnProperty('model')) { + types.other = 'model'; + + // Break loop + return false; + } + } + }); + }); + } else if (association.hasOwnProperty('via') && association.hasOwnProperty('collection')) { const relatedAttribute = models[association.collection].attributes[association.via]; types.current = 'collection'; - if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.hasOwnProperty('via')) { + if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.collection !== '*' && relatedAttribute.hasOwnProperty('via')) { types.other = 'collection'; - } else if (relatedAttribute.hasOwnProperty('collection') && !relatedAttribute.hasOwnProperty('via')) { + } else if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.collection !== '*' && !relatedAttribute.hasOwnProperty('via')) { types.other = 'collectionD'; - } else if (relatedAttribute.hasOwnProperty('model')) { + } else if (relatedAttribute.hasOwnProperty('model') && relatedAttribute.model !== '*') { types.other = 'model'; - } else if (relatedAttribute.hasOwnProperty('key')) { + } else if (relatedAttribute.hasOwnProperty('collection') || relatedAttribute.hasOwnProperty('model')) { types.other = 'morphTo'; } } else if (association.hasOwnProperty('via') && association.hasOwnProperty('model')) { @@ -103,17 +138,17 @@ module.exports = { // We have to find if they are a model linked to this key _.forIn(_.omit(models, currentModelName || ''), model => { _.forIn(model.attributes, attribute => { - if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.hasOwnProperty('collection')) { + if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.hasOwnProperty('collection') && attribute.collection !== '*') { types.other = 'collection'; // Break loop return false; - } else if (attribute.hasOwnProperty('model')) { + } else if (attribute.hasOwnProperty('model') && attribute.model !== '*') { types.other = 'model'; // Break loop return false; - } else if (attribute.hasOwnProperty('key')) { + } else if (attribute.hasOwnProperty('collection') || attribute.hasOwnProperty('model')) { types.other = 'morphTo'; // Break loop @@ -157,41 +192,6 @@ module.exports = { } else if (attribute.hasOwnProperty('model')) { types.other = 'modelD'; - // Break loop - return false; - } - } - }); - }); - } else if (association.hasOwnProperty('morphTo')) { - if (association.morphTo === 'single') { - types.current = 'morphToD'; - } else { - types.current = 'morphTo'; - } - - const flattenedPluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { - Object.keys(strapi.plugins[current].models).forEach((model) => { - acc[`${current}_${model}`] = strapi.plugins[current].models[model]; - }); - - return acc; - }, {}); - - const allModels = _.merge({}, strapi.models, flattenedPluginsModels); - - // We have to find if they are a model linked to this key - _.forIn(allModels, model => { - _.forIn(model.attributes, attribute => { - if (attribute.hasOwnProperty('via') && attribute.via === key) { - if (attribute.hasOwnProperty('collection')) { - types.other = 'collection'; - - // Break loop - return false; - } else if (attribute.hasOwnProperty('model')) { - types.other = 'model'; - // Break loop return false; } @@ -207,7 +207,7 @@ module.exports = { }; } else if (types.current === 'collection' && types.other === 'morphToD') { return { - nature: 'manyToMorph', + nature: 'manyToOneMorph', verbose: 'morphMany' }; } else if (types.current === 'modelD' && types.other === 'morphTo') { @@ -217,17 +217,17 @@ module.exports = { }; } else if (types.current === 'modelD' && types.other === 'morphToD') { return { - nature: 'oneToMorph', + nature: 'oneToOneMorph', verbose: 'morphOne' }; } else if (types.current === 'morphToD' && types.other === 'collection') { return { - nature: 'morphToMany', + nature: 'oneMorphToMany', verbose: 'belongsToMorph' }; } else if (types.current === 'morphToD' && types.other === 'model') { return { - nature: 'morphToOne', + nature: 'oneMorphToOne', verbose: 'belongsToMorph' }; } else if (types.current === 'morphTo' && types.other === 'model') { @@ -315,16 +315,21 @@ module.exports = { } // Exclude non-relational attribute - if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model') && !association.hasOwnProperty('morphTo')) { + if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model')) { return undefined; } // Get relation nature + let details; + const globalName = association.model || association.collection || ''; const infos = this.getNature(association, key, undefined, model.toLowerCase()); - const details = _.get(strapi.models, `${association.model || association.collection}.attributes.${association.via}`, {}); + + if (globalName !== '*') { + details = _.get(strapi.models, `${globalName}.attributes.${association.via}`, {}); + } // Build associations object - if (association.hasOwnProperty('collection')) { + if (association.hasOwnProperty('collection') && association.collection !== '*') { definition.associations.push({ alias: key, type: 'collection', @@ -336,7 +341,7 @@ module.exports = { plugin: association.plugin || undefined, where: details.where, }); - } else if (association.hasOwnProperty('model')) { + } else if (association.hasOwnProperty('model') && association.model !== '*') { definition.associations.push({ alias: key, type: 'model', @@ -348,7 +353,7 @@ module.exports = { plugin: association.plugin || undefined, where: details.where, }); - } else if (association.hasOwnProperty('morphTo')) { + } else if (association.hasOwnProperty('collection') || association.hasOwnProperty('model')) { const pluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { Object.keys(strapi.plugins[current].models).forEach((entity) => { Object.keys(strapi.plugins[current].models[entity].attributes).forEach((attribute) => { @@ -384,11 +389,11 @@ module.exports = { definition.associations.push({ alias: key, - type: association.morphTo === 'single' ? 'model' : 'collection', + type: association.model ? 'model' : 'collection', related: models, nature: infos.nature, autoPopulate: _.get(association, 'autoPopulate', true), - key: association.key, + filter: association.filter, }); } } catch (e) { From 1e8272b80f5ec728d0c574012c0e13edfb089ff9 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Thu, 22 Feb 2018 16:08:11 +0100 Subject: [PATCH 5/8] Handle new syntax for polymorphic in strapi-mongoose --- packages/strapi-mongoose/lib/index.js | 18 ++++++++++++++---- packages/strapi-utils/lib/models.js | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/strapi-mongoose/lib/index.js b/packages/strapi-mongoose/lib/index.js index de3b248275..4b7ccf1bf1 100755 --- a/packages/strapi-mongoose/lib/index.js +++ b/packages/strapi-mongoose/lib/index.js @@ -104,16 +104,15 @@ module.exports = function (strapi) { .forEach(key => { collection.schema.pre(key, function (next) { if (this._mongooseOptions.populate && this._mongooseOptions.populate[association.alias]) { - if (association.nature === 'oneToMorph' || association.nature === 'manyToMorph') { + if (association.nature === 'oneToManyMorph' || association.nature === 'manyToManyMorph') { this._mongooseOptions.populate[association.alias].match = { [`${association.via}.${association.filter}`]: association.alias, [`${association.via}.kind`]: definition.globalId } } else { - this._mongooseOptions.populate[association.alias].path = `${association.alias}.${association.filter}`; + this._mongooseOptions.populate[association.alias].path = `${association.alias}.ref`; } } - next(); }); }); @@ -163,7 +162,18 @@ module.exports = function (strapi) { transform: function (doc, returned, opts) { morphAssociations.forEach(association => { if (Array.isArray(returned[association.alias]) && returned[association.alias].length > 0) { - returned[association.alias] = returned[association.alias].map(o => o[association.filter]); + // Reformat data by bypassing the many-to-many relationship. + switch (association.nature) { + case 'oneMorphToOne': + returned[association.alias] = returned[association.alias][0].ref; + break; + case 'manyMorphToOne': + returned[association.alias] = returned[association.alias].map(obj => obj.ref); + break; + default: + + } + } }); } diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index 0fec435c4b..a073fb2596 100755 --- a/packages/strapi-utils/lib/models.js +++ b/packages/strapi-utils/lib/models.js @@ -339,7 +339,7 @@ module.exports = { autoPopulate: _.get(association, 'autoPopulate', true), dominant: details.dominant !== true, plugin: association.plugin || undefined, - where: details.where, + filter: details.filter, }); } else if (association.hasOwnProperty('model') && association.model !== '*') { definition.associations.push({ @@ -351,7 +351,7 @@ module.exports = { autoPopulate: _.get(association, 'autoPopulate', true), dominant: details.dominant !== true, plugin: association.plugin || undefined, - where: details.where, + filter: details.filter, }); } else if (association.hasOwnProperty('collection') || association.hasOwnProperty('model')) { const pluginsModels = Object.keys(strapi.plugins).reduce((acc, current) => { From c1487c08d5ff84be504153b49bfc2b6895963586 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Thu, 22 Feb 2018 18:28:10 +0100 Subject: [PATCH 6/8] Write polymorphic documentation --- docs/3.x.x/en/guides/models.md | 418 +++++++++++++++++++++++++++------ 1 file changed, 341 insertions(+), 77 deletions(-) diff --git a/docs/3.x.x/en/guides/models.md b/docs/3.x.x/en/guides/models.md index 285710e845..8803631091 100644 --- a/docs/3.x.x/en/guides/models.md +++ b/docs/3.x.x/en/guides/models.md @@ -96,69 +96,100 @@ To improve the Developer eXperience when developing or using the administration Refer to the [relations concept](../concepts/concepts.md#relations) for more informations about relations type. -### Many-to-many +### One-way -Refer to the [many-to-many concept](../concepts/concepts.md#many-to-many). +Refer to the [one-way concept](../concepts/concepts.md#one-way) for informations. #### Example -A `product` can be related to many `categories`, so a `category` can have many `products`. +A `pet` can be owned by someone (a `user`). -**Path —** `./api/product/models/Product.settings.json`. +**Path —** `./api/pet/models/Pet.settings.json`. ```json { "attributes": { - "categories": { - "collection": "product", - "via": "products", - "dominant": true + "owner": { + "model": "user" } } } ``` -> Note: The `dominant` key allows you to define in which table/collection (only for NoSQL databases) should be stored the array that defines the relationship. Because there is no join table in NoSQL, this key is required for NoSQL databases (ex: MongoDB). +**Path —** `./api/pet/controllers/Pet.js`. +```js +// Mongoose example +module.exports = { + findPetsWithOwners: async (ctx) => { + // Retrieve the list of pets with their owners. + const pets = Pet + .find() + .populate('owner'); -**Path —** `./api/category/models/Category.settings.json`. + // Send the list of pets. + ctx.body = pets; + } +} +``` + +### One-to-one + +Refer to the [one-to-one concept](../concepts/concepts.md#one-to-one) for informations. + +#### Example + +A `user` can have one `address`. And this address is only related to this `user`. + +**Path —** `./api/user/models/User.settings.json`. ```json { "attributes": { - "products": { - "collection": "category", - "via": "categories" + "address": { + "model": "address", + "via": "user" } } } ``` -**Path —** `./api/product/controllers/Product.js`. -```js -// Mongoose example -module.exports = { - findProductsWithCategories: async (ctx) => { - // Retrieve the list of products. - const products = Product - .find() - .populate('categories'); - - // Send the list of products. - ctx.body = products; +**Path —** `./api/address/models/Address.settings.json`. +```json +{ + "attributes": { + "user": { + "model": "user" + } } } ``` -**Path —** `./api/category/controllers/Category.js`. +**Path —** `./api/user/controllers/User.js`. ```js // Mongoose example module.exports = { - findCategoriesWithProducts: async (ctx) => { - // Retrieve the list of categories. - const categories = Category + findUsersWithAddresses: async (ctx) => { + // Retrieve the list of users with their addresses. + const users = User .find() - .populate('products'); + .populate('address'); - // Send the list of categories. - ctx.body = categories; + // Send the list of users. + ctx.body = users; + } +} +``` + +**Path —** `./api/adress/controllers/Address.js`. +```js +// Mongoose example +module.exports = { + findArticlesWithUsers: async (ctx) => { + // Retrieve the list of addresses with their users. + const articles = Address + .find() + .populate('user'); + + // Send the list of addresses. + ctx.body = addresses; } } ``` @@ -226,102 +257,335 @@ module.exports = { } ``` -### One-to-one +### Many-to-many -Refer to the [one-to-one concept](../concepts/concepts.md#one-to-one) for informations. +Refer to the [many-to-many concept](../concepts/concepts.md#many-to-many). #### Example -A `user` can have one `address`. And this address is only related to this `user`. +A `product` can be related to many `categories`, so a `category` can have many `products`. -**Path —** `./api/user/models/User.settings.json`. +**Path —** `./api/product/models/Product.settings.json`. ```json { "attributes": { - "address": { - "model": "address", - "via": "user" + "categories": { + "collection": "product", + "via": "products", + "dominant": true } } } ``` -**Path —** `./api/address/models/Address.settings.json`. +> Note: The `dominant` key allows you to define in which table/collection (only for NoSQL databases) should be stored the array that defines the relationship. Because there is no join table in NoSQL, this key is required for NoSQL databases (ex: MongoDB). + +**Path —** `./api/category/models/Category.settings.json`. ```json { "attributes": { - "user": { - "model": "user" + "products": { + "collection": "category", + "via": "categories" } } } ``` -**Path —** `./api/user/controllers/User.js`. +**Path —** `./api/product/controllers/Product.js`. ```js // Mongoose example module.exports = { - findUsersWithAddresses: async (ctx) => { - // Retrieve the list of users with their addresses. - const users = User + findProductsWithCategories: async (ctx) => { + // Retrieve the list of products. + const products = Product .find() - .populate('address'); + .populate('categories'); + + // Send the list of products. + ctx.body = products; + } +} +``` + +**Path —** `./api/category/controllers/Category.js`. +```js +// Mongoose example +module.exports = { + findCategoriesWithProducts: async (ctx) => { + // Retrieve the list of categories. + const categories = Category + .find() + .populate('products'); + + // Send the list of categories. + ctx.body = categories; + } +} +``` + +### Polymorphic + +Refer to the [polymorphic concept](../concepts/concepts.md#polymorphic) for more informations. + +The polymorphic relationships are the solution when you don't know which kind of model will be associated to your entry. A common use case is an `Image` model that can be associated to many others kind of models (Article, Product, User, etc). + +#### Single vs Many + +Let's stay with our `Image` model which might belongs to **a single `Article` or `Product` entry**. + +> In other words, it means that a `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 which might belongs to **many `Article` or `Product` entries**. + +> In other words, it means that a `Article` entry can relate to the same image than a `Product` entry. + +**Path —** `./api/image/models/Image.settings.json`. +```json +{ + "attributes": { + "related": { + "collection": "*", + "filter": "field" + } + } +} +``` + +#### Filter + +The `filter` attribute is optional (but we highly recommend to use every time). If it's provided it adds a new match level to retrieve the related data. + +For example, the `Product` model might have two attributes which are associated to the `Image` model. To distinguish which image is attached to the `cover` field and which images are attached to the `pictures` field, we need to save and provide this to the database. + +**Path —** `./api/article/models/Product.settings.json`. +```json +{ + "attributes": { + "cover": { + "model": "image", + "via": "related", + }, + "pictures": { + "collection": "image", + "via": "related" + } + } +} +``` + +The value is the `filter` attribute is the name of the column where the information is stored. + +#### Example + +A `Image` model might belongs to many either `Article` models or a `Product` models. + +**Path —** `./api/image/models/Image.settings.json`. +```json +{ + "attributes": { + "related": { + "collection": "*", + "filter": "field" + } + } +} +``` + +**Path —** `./api/article/models/Article.settings.json`. +```json +{ + "attributes": { + "avatar": { + "model": "image", + "via": "related" + } + } +} +``` + +**Path —** `./api/article/models/Product.settings.json`. +```json +{ + "attributes": { + "pictures": { + "collection": "image", + "via": "related" + } + } +} +``` + +**Path —** `./api/image/controllers/Image.js`. +```js +// Mongoose example +module.exports = { + findFiles: async (ctx) => { + // Retrieve the list of images with the Article or Product entries related to them. + const images = Images + .find() + .populate('related'); + + /* + [{ + "_id": "5a81b0fa8c063a53298a934a", + "url": "http://....", + "name": "john_doe_avatar.png", + "related": [{ + "_id": "5a81b0fa8c063a5393qj934a", + "title": "John Doe is awesome", + "description": "..." + }, { + "_id": "5a81jei389ns5abd75f79c", + "name": "A simple chair", + "description": "..." + }] + }] + */ + + // Send the list of files. + ctx.body = images; + } +} +``` + +**Path —** `./api/article/controllers/Article.js`. +```js +// Mongoose example +module.exports = { + findArticlesWithAvatar: async (ctx) => { + // Retrieve the list of articles with the avatar (image). + const articles = Article + .find() + .populate('avatar'); + + /* + [{ + "_id": "5a81b0fa8c063a5393qj934a", + "title": "John Doe is awesome", + "description": "...", + "avatar": { + "_id": "5a81b0fa8c063a53298a934a", + "url": "http://....", + "name": "john_doe_avatar.png" + } + }] + */ // Send the list of users. - ctx.body = users; + ctx.body = articles; } } ``` -**Path —** `./api/adress/controllers/Address.js`. +**Path —** `./api/product/controllers/Product.js`. ```js // Mongoose example module.exports = { - findArticlesWithUsers: async (ctx) => { - // Retrieve the list of addresses with their users. - const articles = Address + findProductWithPictures: async (ctx) => { + // Retrieve the list of products with the pictures (images). + const products = Product .find() - .populate('user'); + .populate('pictures'); - // Send the list of addresses. - ctx.body = addresses; + /* + [{ + "_id": "5a81jei389ns5abd75f79c", + "name": "A simple chair", + "description": "...", + "pictures": [{ + "_id": "5a81b0fa8c063a53298a934a", + "url": "http://....", + "name": "chair_position_1.png" + }, { + "_id": "5a81d22bee1ad45abd75f79c", + "url": "http://....", + "name": "chair_position_2.png" + }, { + "_id": "5a81d232ee1ad45abd75f79e", + "url": "http://....", + "name": "chair_position_3.png" + }] + }] + */ + + // Send the list of users. + ctx.body = products; } } ``` -### One-way -Refer to the [one-way concept](../concepts/concepts.md#one-way) for informations. +#### Database implementation -#### Example +If you're using MongoDB as a 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. -A `pet` can be owned by someone (a `user`). - -**Path —** `./api/pet/models/Pet.settings.json`. +**Path —** `./api/image/models/Image.settings.json`. ```json { "attributes": { - "owner": { - "model": "user" + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "related": { + "collection": "*", + "filter": "field" } } } ``` -**Path —** `./api/pet/controllers/Pet.js`. -```js -// Mongoose example -module.exports = { - findPetsWithOwners: async (ctx) => { - // Retrieve the list of pets with their owners. - const pets = Pet - .find() - .populate('owner'); - - // Send the list of pets. - ctx.body = pets; - } -} +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 overrided 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 correspond 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 correspond 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 correspond 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 correspond to the attribute of a `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 | ## Lifecycle callbacks From e3b78019c8c22c1b6160a68f93c76ca7891b4131 Mon Sep 17 00:00:00 2001 From: Jim Laurie Date: Fri, 23 Feb 2018 15:14:21 +0100 Subject: [PATCH 7/8] Fix test symlink --- scripts/setup.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/setup.js b/scripts/setup.js index 7018bcef3a..27548856df 100755 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -99,17 +99,18 @@ shell.rm('-f', 'package-lock.json'); watcher('📦 Linking strapi-plugin-settings-manager...', 'npm link --no-optional', false); watcher('🏗 Building...', 'npm run build'); -shell.cd('../strapi-plugin-upload'); -watcher('', 'npm install ../strapi-helper-plugin --no-optional'); -shell.rm('-f', 'package-lock.json'); -watcher('📦 Linking strapi-plugin-upload...', 'npm link --no-optional', false); -watcher('🏗 Building...', 'npm run build'); shell.cd('../strapi-upload-local'); watcher('📦 Linking strapi-plugin-upload...', 'npm link --no-optional', false); shell.cd('../strapi-upload-aws-s3'); watcher('📦 Linking strapi-plugin-upload...', 'npm link --no-optional', false); +shell.cd('../strapi-plugin-upload'); +watcher('', 'npm install ../strapi-helper-plugin --no-optional'); +shell.rm('-f', 'package-lock.json'); +watcher('📦 Linking strapi-plugin-upload...', 'npm link --no-optional', false); +watcher('🏗 Building...', 'npm run build'); + shell.cd('../strapi-plugin-content-type-builder'); watcher('', 'npm install ../strapi-helper-plugin --no-optional'); watcher('', 'npm install ../strapi-generate --no-optional'); From d1cffa7cb0d2b8e5ada5b853e931148ee0f4eee1 Mon Sep 17 00:00:00 2001 From: Jim Laurie Date: Fri, 23 Feb 2018 15:35:46 +0100 Subject: [PATCH 8/8] Fix test symlink 2 --- packages/strapi-plugin-upload/package.json | 2 +- scripts/setup.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/strapi-plugin-upload/package.json b/packages/strapi-plugin-upload/package.json index e82e7c795d..befb99fd07 100644 --- a/packages/strapi-plugin-upload/package.json +++ b/packages/strapi-plugin-upload/package.json @@ -47,4 +47,4 @@ "npm": ">= 3.0.0" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/scripts/setup.js b/scripts/setup.js index 27548856df..6cca31e455 100755 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -101,12 +101,13 @@ watcher('🏗 Building...', 'npm run build'); shell.cd('../strapi-upload-local'); -watcher('📦 Linking strapi-plugin-upload...', 'npm link --no-optional', false); +watcher('📦 Linking strapi-upload-local...', 'npm link --no-optional', false); shell.cd('../strapi-upload-aws-s3'); -watcher('📦 Linking strapi-plugin-upload...', 'npm link --no-optional', false); +watcher('📦 Linking strapi-upload-aws-s3...', 'npm link --no-optional', false); shell.cd('../strapi-plugin-upload'); watcher('', 'npm install ../strapi-helper-plugin --no-optional'); +watcher('', 'npm install ../strapi-upload-local --no-optional'); shell.rm('-f', 'package-lock.json'); watcher('📦 Linking strapi-plugin-upload...', 'npm link --no-optional', false); watcher('🏗 Building...', 'npm run build');