mirror of
https://github.com/strapi/strapi.git
synced 2025-08-22 15:48:59 +00:00
Merge branch 'master' of github.com:strapi/strapi into file-upload-front
This commit is contained in:
commit
e2bc04b66f
@ -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
|
||||
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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`
|
||||
}
|
||||
|
@ -38,6 +38,14 @@
|
||||
"via": "users",
|
||||
"plugin": "users-permissions",
|
||||
"configurable": false
|
||||
},
|
||||
"avatar": {
|
||||
"model": "upload",
|
||||
"via": "uploaded"
|
||||
},
|
||||
"photos": {
|
||||
"collection": "upload",
|
||||
"via": "uploaded"
|
||||
}
|
||||
}
|
||||
}
|
@ -83,27 +83,28 @@ module.exports = {
|
||||
models = association.plugin ? strapi.plugins[association.plugin].models : strapi.models;
|
||||
}
|
||||
|
||||
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')) {
|
||||
types.other = 'collection';
|
||||
} else if (relatedAttribute.hasOwnProperty('collection') && !relatedAttribute.hasOwnProperty('via')) {
|
||||
types.other = 'collectionD';
|
||||
} else if (relatedAttribute.hasOwnProperty('model')) {
|
||||
types.other = 'model';
|
||||
} else if (relatedAttribute.hasOwnProperty('key')) {
|
||||
types.other = 'morphTo';
|
||||
if ((association.hasOwnProperty('collection') && association.collection === '*') || (association.hasOwnProperty('model') && association.model === '*')) {
|
||||
if (association.model) {
|
||||
types.current = 'morphToD';
|
||||
} else {
|
||||
types.current = 'morphTo';
|
||||
}
|
||||
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('model')) {
|
||||
types.current = 'modelD';
|
||||
|
||||
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(_.omit(models, currentModelName || ''), model => {
|
||||
_.forIn(allModels, model => {
|
||||
_.forIn(model.attributes, attribute => {
|
||||
if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.hasOwnProperty('collection')) {
|
||||
if (attribute.hasOwnProperty('via') && attribute.via === key) {
|
||||
if (attribute.hasOwnProperty('collection')) {
|
||||
types.other = 'collection';
|
||||
|
||||
// Break loop
|
||||
@ -113,7 +114,41 @@ module.exports = {
|
||||
|
||||
// Break loop
|
||||
return false;
|
||||
} else if (attribute.hasOwnProperty('key')) {
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('collection')) {
|
||||
const relatedAttribute = models[association.collection].attributes[association.via];
|
||||
|
||||
types.current = 'collection';
|
||||
|
||||
if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.collection !== '*' && relatedAttribute.hasOwnProperty('via')) {
|
||||
types.other = 'collection';
|
||||
} else if (relatedAttribute.hasOwnProperty('collection') && relatedAttribute.collection !== '*' && !relatedAttribute.hasOwnProperty('via')) {
|
||||
types.other = 'collectionD';
|
||||
} else if (relatedAttribute.hasOwnProperty('model') && relatedAttribute.model !== '*') {
|
||||
types.other = 'model';
|
||||
} else if (relatedAttribute.hasOwnProperty('collection') || relatedAttribute.hasOwnProperty('model')) {
|
||||
types.other = 'morphTo';
|
||||
}
|
||||
} else if (association.hasOwnProperty('via') && association.hasOwnProperty('model')) {
|
||||
types.current = 'modelD';
|
||||
|
||||
// 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') && attribute.collection !== '*') {
|
||||
types.other = 'collection';
|
||||
|
||||
// Break loop
|
||||
return false;
|
||||
} else if (attribute.hasOwnProperty('model') && attribute.model !== '*') {
|
||||
types.other = 'model';
|
||||
|
||||
// Break loop
|
||||
return false;
|
||||
} 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 === 'collection' && types.other === 'morphToD') {
|
||||
return {
|
||||
nature: 'manyToOneMorph',
|
||||
verbose: 'morphMany'
|
||||
};
|
||||
} else if (types.current === 'modelD' && types.other === 'morphTo') {
|
||||
return {
|
||||
nature: 'oneToMorph',
|
||||
verbose: 'belongsTo'
|
||||
nature: 'oneToManyMorph',
|
||||
verbose: 'morphOne'
|
||||
};
|
||||
} else if (types.current === 'morphTo' && types.other === 'collection') {
|
||||
} else if (types.current === 'modelD' && types.other === 'morphToD') {
|
||||
return {
|
||||
nature: 'morphToMany',
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
@ -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)) {
|
||||
|
@ -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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user