diff --git a/docs/3.x.x/en/guides/models.md b/docs/3.x.x/en/guides/models.md index 99a1c55e4a..1c455b6c33 100644 --- a/docs/3.x.x/en/guides/models.md +++ b/docs/3.x.x/en/guides/models.md @@ -97,69 +97,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; } } ``` @@ -227,102 +258,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 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..4731715b9f 100755 --- a/packages/strapi-bookshelf/lib/index.js +++ b/packages/strapi-bookshelf/lib/index.js @@ -133,6 +133,54 @@ module.exports = function(strapi) { : key; }); + // 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. + switch (association.nature) { + case 'oneToManyMorph': + attrs[association.alias] = attrs[association.alias][model.collectionName]; + break; + case 'manyToManyMorph': + attrs[association.alias] = attrs[association.alias].map(rel => rel[model.collectionName]); + break; + case 'oneMorphToOne': + attrs[association.alias] = attrs[association.alias].related; + break; + case 'manyMorphToOne': + attrs[association.alias] = attrs[association.alias].map(obj => obj.related); + break; + default: + + } + } + }); + + return attrs; + } + // Initialize lifecycle callbacks. loadedModel.initialize = function() { const lifecycle = { @@ -156,6 +204,46 @@ module.exports = function(strapi) { } }); + // 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); @@ -225,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': { @@ -369,6 +463,102 @@ 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, `${definition.collectionName}`) + .query(qb => { + qb.where(_.get(model, `attributes.${details.via}.filter`, '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, `${definition.collectionName}`) + .query(qb => { + qb.where(_.get(collection, `attributes.${details.via}.filter`, 'field'), name); + }); + } + break; + } + case 'belongsToMorph': + case 'belongsToManyMorph': { + const association = definition.associations + .find(association => association.alias === name); + + 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: `${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]])); + } + }; + + 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( + GLOBALS[options.tableName], + `${definition.collectionName}_id` + ); + }; + break; + } default: { break; } diff --git a/packages/strapi-bookshelf/package.json b/packages/strapi-bookshelf/package.json index 34ca0d39e1..6de0a15a7a 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.10.2", diff --git a/packages/strapi-mongoose/lib/index.js b/packages/strapi-mongoose/lib/index.js index 1e87bec89d..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.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}.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.key]); + // 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: + + } + } }); } @@ -296,20 +306,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 +322,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,11 +339,40 @@ 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; + + definition.loadedModel[name] = { + type: 'virtual', + ref, + via: `${FK.via}.ref`, + 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; + + definition.loadedModel[name] = { + type: 'virtual', + ref, + via: `${FK.via}.ref` + }; + + // 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, - [details.where]: String, - [details.key]: { + [details.filter]: String, + ref: { type: instance.Schema.Types.ObjectId, refPath: `${name}.kind` } @@ -370,8 +382,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-upload/package.json b/packages/strapi-plugin-upload/package.json index 33a4efae3f..a3fc2bdee6 100644 --- a/packages/strapi-plugin-upload/package.json +++ b/packages/strapi-plugin-upload/package.json @@ -48,4 +48,4 @@ "npm": ">= 3.0.0" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/strapi-plugin-users-permissions/models/User.settings.json b/packages/strapi-plugin-users-permissions/models/User.settings.json index da72db76e3..e18430d84d 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": "uploaded" + }, + "photos": { + "collection": "upload", + "via": "uploaded" } } } \ No newline at end of file diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index b7872d5013..a073fb2596 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,37 +192,6 @@ module.exports = { } else if (attribute.hasOwnProperty('model')) { types.other = 'modelD'; - // Break loop - return false; - } - } - }); - }); - } else if (association.hasOwnProperty('key')) { - 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; } @@ -198,22 +202,42 @@ module.exports = { if (types.current === 'collection' && types.other === 'morphTo') { return { - nature: 'manyToMorph', - verbose: 'belongsToMany' + nature: 'manyToManyMorph', + verbose: 'morphMany' }; - } else if (types.current === 'modelD' && types.other === 'morphTo') { + } else if (types.current === 'collection' && types.other === 'morphToD') { return { - nature: 'oneToMorph', - verbose: 'belongsTo' + nature: 'manyToOneMorph', + verbose: 'morphMany' }; - } else if (types.current === 'morphTo' && types.other === 'collection') { + } else if (types.current === 'modelD' && types.other === 'morphTo') { return { - nature: 'morphToMany', + nature: 'oneToManyMorph', + verbose: 'morphOne' + }; + } else if (types.current === 'modelD' && types.other === 'morphToD') { + return { + nature: 'oneToOneMorph', + verbose: 'morphOne' + }; + } else if (types.current === 'morphToD' && types.other === 'collection') { + return { + nature: 'oneMorphToMany', + verbose: 'belongsToMorph' + }; + } else if (types.current === 'morphToD' && types.other === 'model') { + return { + nature: 'oneMorphToOne', verbose: 'belongsToMorph' }; } else if (types.current === 'morphTo' && types.other === 'model') { return { - nature: 'morphToOne', + 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,16 +315,21 @@ module.exports = { } // Exclude non-relational attribute - if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model') && !association.hasOwnProperty('key')) { + 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', @@ -309,9 +339,9 @@ 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')) { + } else if (association.hasOwnProperty('model') && association.model !== '*') { definition.associations.push({ alias: key, type: 'model', @@ -321,19 +351,54 @@ 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('key')) { + } 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) => { + 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', + type: association.model ? 'model' : 'collection', + related: models, nature: infos.nature, autoPopulate: _.get(association, 'autoPopulate', true), - key: association.key, + filter: association.filter, }); } } catch (e) { strapi.log.error(`Something went wrong in the model \`${_.upperFirst(model)}\` with the attribute \`${key}\``); + strapi.log.error(e); strapi.stop(); } }, diff --git a/packages/strapi/lib/middlewares/mask/index.js b/packages/strapi/lib/middlewares/mask/index.js index 5bf2e918e2..d95ee8424f 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 && payload.toJSON ? payload.toJSON() : payload; + if (_.isArray(payload)) { return payload.map(mask); } else if (_.isPlainObject(payload)) { diff --git a/scripts/setup.js b/scripts/setup.js index 7018bcef3a..6cca31e455 100755 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -99,17 +99,19 @@ 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-upload-local'); +watcher('📦 Linking strapi-upload-local...', 'npm link --no-optional', false); +shell.cd('../strapi-upload-aws-s3'); +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'); -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-content-type-builder'); watcher('', 'npm install ../strapi-helper-plugin --no-optional'); watcher('', 'npm install ../strapi-generate --no-optional');