mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 09:56:44 +00:00 
			
		
		
		
	Merge pull request #4601 from strapi/dynamic-zone/mongoose-support
Dynamic zone/mongoose support
This commit is contained in:
		
						commit
						afa0f50a23
					
				| @ -7,10 +7,7 @@ | ||||
|   }, | ||||
|   "options": { | ||||
|     "increments": true, | ||||
|     "timestamps": [ | ||||
|       "created_at", | ||||
|       "updated_at" | ||||
|     ], | ||||
|     "timestamps": ["created_at", "updated_at"], | ||||
|     "comment": "" | ||||
|   }, | ||||
|   "attributes": { | ||||
| @ -38,21 +35,12 @@ | ||||
|       "collection": "category" | ||||
|     }, | ||||
|     "price_range": { | ||||
|       "enum": [ | ||||
|         "very_cheap", | ||||
|         "cheap", | ||||
|         "average", | ||||
|         "expensive", | ||||
|         "very_expensive" | ||||
|       ], | ||||
|       "enum": ["very_cheap", "cheap", "average", "expensive", "very_expensive"], | ||||
|       "type": "enumeration" | ||||
|     }, | ||||
|     "body": { | ||||
|       "type": "dynamiczone", | ||||
|       "components": [ | ||||
|         "default.closingperiod", | ||||
|         "default.restaurantservice" | ||||
|       ] | ||||
|       "components": ["default.closingperiod", "default.restaurantservice"] | ||||
|     }, | ||||
|     "description": { | ||||
|       "type": "richtext", | ||||
| @ -61,7 +49,6 @@ | ||||
|     "opening_times": { | ||||
|       "component": "default.openingtimes", | ||||
|       "type": "component", | ||||
|       "required": true, | ||||
|       "repeatable": true, | ||||
|       "min": 1, | ||||
|       "max": 10 | ||||
| @ -72,7 +59,6 @@ | ||||
|     }, | ||||
|     "services": { | ||||
|       "component": "default.restaurantservice", | ||||
|       "required": true, | ||||
|       "repeatable": true, | ||||
|       "type": "component" | ||||
|     }, | ||||
|  | ||||
| @ -17,6 +17,11 @@ | ||||
|       "type": "date", | ||||
|       "required": true | ||||
|     }, | ||||
|     "media": { | ||||
|       "model": "file", | ||||
|       "via": "related", | ||||
|       "plugin": "upload" | ||||
|     }, | ||||
|     "dish": { | ||||
|       "component": "default.dish", | ||||
|       "type": "component" | ||||
|  | ||||
| @ -10,6 +10,11 @@ | ||||
|       "type": "string", | ||||
|       "required": true | ||||
|     }, | ||||
|     "media": { | ||||
|       "model": "file", | ||||
|       "via": "related", | ||||
|       "plugin": "upload" | ||||
|     }, | ||||
|     "is_available": { | ||||
|       "type": "boolean", | ||||
|       "required": true, | ||||
|  | ||||
| @ -6,6 +6,7 @@ const mongoose = require('mongoose'); | ||||
| const utilsModels = require('strapi-utils').models; | ||||
| const utils = require('./utils'); | ||||
| const relations = require('./relations'); | ||||
| const { findComponentByGlobalId } = require('./utils/helpers'); | ||||
| 
 | ||||
| module.exports = ({ models, target, plugin = false }, ctx) => { | ||||
|   const { instance } = ctx; | ||||
| @ -27,13 +28,18 @@ module.exports = ({ models, target, plugin = false }, ctx) => { | ||||
|       global[definition.globalName] = {}; | ||||
|     } | ||||
| 
 | ||||
|     const componentAttributes = Object.keys(definition.attributes).filter( | ||||
|       key => definition.attributes[key].type === 'component' | ||||
|     const componentAttributes = Object.keys(definition.attributes).filter(key => | ||||
|       ['component', 'dynamiczone'].includes(definition.attributes[key].type) | ||||
|     ); | ||||
| 
 | ||||
|     const scalarAttributes = Object.keys(definition.attributes).filter(key => { | ||||
|       const { type } = definition.attributes[key]; | ||||
|       return type !== undefined && type !== null && type !== 'component'; | ||||
|       return ( | ||||
|         type !== undefined && | ||||
|         type !== null && | ||||
|         type !== 'component' && | ||||
|         type !== 'dynamiczone' | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     const relationalAttributes = Object.keys(definition.attributes).filter( | ||||
| @ -43,7 +49,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => { | ||||
|       } | ||||
|     ); | ||||
| 
 | ||||
|     // handle gorup attrs
 | ||||
|     // handle component and dynamic zone attrs
 | ||||
|     if (componentAttributes.length > 0) { | ||||
|       // create join morph collection thingy
 | ||||
|       componentAttributes.forEach(name => { | ||||
| @ -129,8 +135,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => { | ||||
| 
 | ||||
|       if (_.isFunction(target[model.toLowerCase()][fn])) { | ||||
|         schema.pre(key, function() { | ||||
|           return target[model.toLowerCase()] | ||||
|             [fn](this); | ||||
|           return target[model.toLowerCase()][fn](this); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
| @ -252,14 +257,28 @@ module.exports = ({ models, target, plugin = false }, ctx) => { | ||||
| 
 | ||||
|         componentAttributes.forEach(name => { | ||||
|           const attribute = definition.attributes[name]; | ||||
|           const { type } = attribute; | ||||
| 
 | ||||
|           if (Array.isArray(returned[name])) { | ||||
|             const components = returned[name].map(el => el.ref); | ||||
|             // Reformat data by bypassing the many-to-many relationship.
 | ||||
|             returned[name] = | ||||
|               attribute.repeatable === true | ||||
|                 ? components | ||||
|                 : _.first(components) || null; | ||||
|           if (type === 'component') { | ||||
|             if (Array.isArray(returned[name])) { | ||||
|               const components = returned[name].map(el => el.ref); | ||||
|               // Reformat data by bypassing the many-to-many relationship.
 | ||||
|               returned[name] = | ||||
|                 attribute.repeatable === true | ||||
|                   ? components | ||||
|                   : _.first(components) || null; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if (type === 'dynamiczone') { | ||||
|             const components = returned[name].map(el => { | ||||
|               return { | ||||
|                 __component: findComponentByGlobalId(el.kind).uid, | ||||
|                 ...el.ref, | ||||
|               }; | ||||
|             }); | ||||
| 
 | ||||
|             returned[name] = components; | ||||
|           } | ||||
|         }); | ||||
|       }, | ||||
| @ -304,121 +323,38 @@ const createOnFetchPopulateFn = ({ | ||||
|   componentAttributes, | ||||
|   definition, | ||||
| }) => { | ||||
|   return function(next) { | ||||
|   return function() { | ||||
|     const populatedPaths = this.getPopulatedPaths(); | ||||
| 
 | ||||
|     morphAssociations.forEach(association => { | ||||
|       if ( | ||||
|         this._mongooseOptions.populate && | ||||
|         this._mongooseOptions.populate[association.alias] | ||||
|       ) { | ||||
|         if ( | ||||
|           association.nature === 'oneToManyMorph' || | ||||
|           association.nature === 'manyToManyMorph' | ||||
|         ) { | ||||
|           this._mongooseOptions.populate[association.alias].match = { | ||||
|       const { alias, nature } = association; | ||||
| 
 | ||||
|       if (['oneToManyMorph', 'manyToManyMorph'].includes(nature)) { | ||||
|         this.populate({ | ||||
|           path: alias, | ||||
|           match: { | ||||
|             [`${association.via}.${association.filter}`]: association.alias, | ||||
|             [`${association.via}.kind`]: definition.globalId, | ||||
|           }; | ||||
| 
 | ||||
|           // Select last related to an entity.
 | ||||
|           this._mongooseOptions.populate[association.alias].options = { | ||||
|           }, | ||||
|           options: { | ||||
|             sort: '-createdAt', | ||||
|           }; | ||||
|         } else { | ||||
|           this._mongooseOptions.populate[ | ||||
|             association.alias | ||||
|           ].path = `${association.alias}.ref`; | ||||
|         } | ||||
|       } else { | ||||
|         if (!this._mongooseOptions.populate) { | ||||
|           this._mongooseOptions.populate = {}; | ||||
|         } | ||||
|         // Images are not displayed in populated data.
 | ||||
|         // We automatically populate morph relations.
 | ||||
|         if ( | ||||
|           association.nature === 'oneToManyMorph' || | ||||
|           association.nature === 'manyToManyMorph' | ||||
|         ) { | ||||
|           this._mongooseOptions.populate[association.alias] = { | ||||
|             path: association.alias, | ||||
|             match: { | ||||
|               [`${association.via}.${association.filter}`]: association.alias, | ||||
|               [`${association.via}.kind`]: definition.globalId, | ||||
|             }, | ||||
|             options: { | ||||
|               sort: '-createdAt', | ||||
|             }, | ||||
|             select: undefined, | ||||
|             model: undefined, | ||||
|             _docs: {}, | ||||
|           }; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     componentAttributes.forEach(name => { | ||||
|       const attr = definition.attributes[name]; | ||||
| 
 | ||||
|       const component = strapi.components[attr.component]; | ||||
| 
 | ||||
|       const assocs = (component.associations || []).filter( | ||||
|         assoc => assoc.autoPopulate === true | ||||
|       ); | ||||
| 
 | ||||
|       let subpopulates = []; | ||||
| 
 | ||||
|       assocs.forEach(assoc => { | ||||
|         if (isPolymorphic({ assoc })) { | ||||
|           if ( | ||||
|             assoc.nature === 'oneToManyMorph' || | ||||
|             assoc.nature === 'manyToManyMorph' | ||||
|           ) { | ||||
|             subpopulates.push({ | ||||
|               path: assoc.alias, | ||||
|               match: { | ||||
|                 [`${assoc.via}.${assoc.filter}`]: assoc.alias, | ||||
|                 [`${assoc.via}.kind`]: definition.globalId, | ||||
|               }, | ||||
|               options: { | ||||
|                 sort: '-createdAt', | ||||
|               }, | ||||
|               select: undefined, | ||||
|               model: undefined, | ||||
|               _docs: {}, | ||||
|             }); | ||||
|           } else { | ||||
|             subpopulates.push({ path: `${assoc.alias}.ref`, _docs: {} }); | ||||
|           } | ||||
|         } else { | ||||
|           subpopulates.push({ | ||||
|             path: assoc.alias, | ||||
|             _docs: {}, | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       if ( | ||||
|         this._mongooseOptions.populate && | ||||
|         this._mongooseOptions.populate[name] | ||||
|       ) { | ||||
|         this._mongooseOptions.populate[name].path = `${name}.ref`; | ||||
|         this._mongooseOptions.populate[name].populate = subpopulates; | ||||
|       } else { | ||||
|         _.set(this._mongooseOptions, ['populate', name], { | ||||
|           path: `${name}.ref`, | ||||
|           populate: subpopulates, | ||||
|           _docs: {}, | ||||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (populatedPaths.includes(alias)) { | ||||
|         _.set(this._mongooseOptions.populate, [alias, 'path'], `${alias}.ref`); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     next(); | ||||
|     componentAttributes.forEach(key => { | ||||
|       this.populate({ path: `${key}.ref` }); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const isPolymorphic = ({ assoc }) => { | ||||
|   return assoc.nature.toLowerCase().indexOf('morph') !== -1; | ||||
| }; | ||||
| 
 | ||||
| const buildRelation = ({ definition, model, instance, attribute, name }) => { | ||||
|   const { nature, verbose } = | ||||
|     utilsModels.getNature(attribute, name, undefined, model.toLowerCase()) || | ||||
|  | ||||
| @ -10,15 +10,18 @@ const { | ||||
|   models: modelUtils, | ||||
| } = require('strapi-utils'); | ||||
| 
 | ||||
| module.exports = ({ model, modelKey, strapi }) => { | ||||
|   const hasPK = obj => _.has(obj, model.primaryKey) || _.has(obj, 'id'); | ||||
|   const getPK = obj => | ||||
|     _.has(obj, model.primaryKey) ? obj[model.primaryKey] : obj.id; | ||||
| const { findComponentByGlobalId } = require('./utils/helpers'); | ||||
| 
 | ||||
| const hasPK = (obj, model) => _.has(obj, model.primaryKey) || _.has(obj, 'id'); | ||||
| const getPK = (obj, model) => | ||||
|   _.has(obj, model.primaryKey) ? obj[model.primaryKey] : obj.id; | ||||
| 
 | ||||
| module.exports = ({ model, modelKey, strapi }) => { | ||||
|   const assocKeys = model.associations.map(ast => ast.alias); | ||||
|   const componentKeys = Object.keys(model.attributes).filter(key => { | ||||
|     return model.attributes[key].type === 'component'; | ||||
|   }); | ||||
|   const componentKeys = Object.keys(model.attributes).filter(key => | ||||
|     ['component', 'dynamiczone'].includes(model.attributes[key].type) | ||||
|   ); | ||||
| 
 | ||||
|   const excludedKeys = assocKeys.concat(componentKeys); | ||||
| 
 | ||||
|   const defaultPopulate = model.associations | ||||
| @ -38,49 +41,96 @@ module.exports = ({ model, modelKey, strapi }) => { | ||||
| 
 | ||||
|     for (let key of componentKeys) { | ||||
|       const attr = model.attributes[key]; | ||||
|       const { component, required = false, repeatable = false } = attr; | ||||
|       const { type } = attr; | ||||
| 
 | ||||
|       const componentModel = strapi.components[component]; | ||||
|       if (type === 'component') { | ||||
|         const { component, required = false, repeatable = false } = attr; | ||||
| 
 | ||||
|       if (required === true && !_.has(values, key)) { | ||||
|         const err = new Error(`Component ${key} is required`); | ||||
|         err.status = 400; | ||||
|         throw err; | ||||
|         const componentModel = strapi.components[component]; | ||||
| 
 | ||||
|         if (required === true && !_.has(values, key)) { | ||||
|           const err = new Error(`Component ${key} is required`); | ||||
|           err.status = 400; | ||||
|           throw err; | ||||
|         } | ||||
| 
 | ||||
|         if (!_.has(values, key)) continue; | ||||
| 
 | ||||
|         const componentValue = values[key]; | ||||
| 
 | ||||
|         if (repeatable === true) { | ||||
|           validateRepeatableInput(componentValue, { key, ...attr }); | ||||
|           const components = await Promise.all( | ||||
|             componentValue.map(value => { | ||||
|               return strapi.query(component).create(value); | ||||
|             }) | ||||
|           ); | ||||
| 
 | ||||
|           const componentsArr = components.map(componentEntry => ({ | ||||
|             kind: componentModel.globalId, | ||||
|             ref: componentEntry, | ||||
|           })); | ||||
| 
 | ||||
|           entry[key] = componentsArr; | ||||
|           await entry.save(); | ||||
|         } else { | ||||
|           validateNonRepeatableInput(componentValue, { key, ...attr }); | ||||
|           if (componentValue === null) continue; | ||||
| 
 | ||||
|           const componentEntry = await strapi | ||||
|             .query(component) | ||||
|             .create(componentValue); | ||||
|           entry[key] = [ | ||||
|             { | ||||
|               kind: componentModel.globalId, | ||||
|               ref: componentEntry, | ||||
|             }, | ||||
|           ]; | ||||
|           await entry.save(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (!_.has(values, key)) continue; | ||||
|       if (type === 'dynamiczone') { | ||||
|         const { required = false } = attr; | ||||
| 
 | ||||
|       const componentValue = values[key]; | ||||
|         if (required === true && !_.has(values, key)) { | ||||
|           const err = new Error(`Dynamiczone ${key} is required`); | ||||
|           err.status = 400; | ||||
|           throw err; | ||||
|         } | ||||
| 
 | ||||
|       if (repeatable === true) { | ||||
|         validateRepeatableInput(componentValue, { key, ...attr }); | ||||
|         const components = await Promise.all( | ||||
|           componentValue.map(value => { | ||||
|             return strapi.query(component).create(value); | ||||
|         if (!_.has(values, key)) continue; | ||||
| 
 | ||||
|         const dynamiczoneValues = values[key]; | ||||
| 
 | ||||
|         validateDynamiczoneInput(dynamiczoneValues, { key, ...attr }); | ||||
| 
 | ||||
|         const dynamiczones = await Promise.all( | ||||
|           dynamiczoneValues.map(value => { | ||||
|             const component = value.__component; | ||||
|             return strapi | ||||
|               .query(component) | ||||
|               .create(value) | ||||
|               .then(entity => { | ||||
|                 return { | ||||
|                   __component: value.__component, | ||||
|                   entity, | ||||
|                 }; | ||||
|               }); | ||||
|           }) | ||||
|         ); | ||||
| 
 | ||||
|         const componentsArr = components.map(componentEntry => ({ | ||||
|           kind: componentModel.globalId, | ||||
|           ref: componentEntry, | ||||
|         })); | ||||
|         const componentsArr = dynamiczones.map(({ __component, entity }) => { | ||||
|           const componentModel = strapi.components[__component]; | ||||
| 
 | ||||
|           return { | ||||
|             kind: componentModel.globalId, | ||||
|             ref: entity, | ||||
|           }; | ||||
|         }); | ||||
| 
 | ||||
|         entry[key] = componentsArr; | ||||
|         await entry.save(); | ||||
|       } else { | ||||
|         validateNonRepeatableInput(componentValue, { key, ...attr }); | ||||
|         if (componentValue === null) continue; | ||||
| 
 | ||||
|         const componentEntry = await strapi | ||||
|           .query(component) | ||||
|           .create(componentValue); | ||||
|         entry[key] = [ | ||||
|           { | ||||
|             kind: componentModel.globalId, | ||||
|             ref: componentEntry, | ||||
|           }, | ||||
|         ]; | ||||
|         await entry.save(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @ -88,70 +138,185 @@ module.exports = ({ model, modelKey, strapi }) => { | ||||
|   async function updateComponents(entry, values) { | ||||
|     if (componentKeys.length === 0) return; | ||||
| 
 | ||||
|     const updateOrCreateComponent = async ({ componentUID, value }) => { | ||||
|       // check if value has an id then update else create
 | ||||
|       const query = strapi.query(componentUID); | ||||
|       if (hasPK(value, query.model)) { | ||||
|         return query.update( | ||||
|           { | ||||
|             [query.model.primaryKey]: getPK(value, query.model), | ||||
|           }, | ||||
|           value | ||||
|         ); | ||||
|       } | ||||
|       return query.create(value); | ||||
|     }; | ||||
| 
 | ||||
|     for (let key of componentKeys) { | ||||
|       // if key isn't present then don't change the current component data
 | ||||
|       if (!_.has(values, key)) continue; | ||||
| 
 | ||||
|       const attr = model.attributes[key]; | ||||
|       const { component, repeatable = false } = attr; | ||||
|       const { type } = attr; | ||||
| 
 | ||||
|       const componentModel = strapi.components[component]; | ||||
|       const componentValue = values[key]; | ||||
|       if (type === 'component') { | ||||
|         const { component: componentUID, repeatable = false } = attr; | ||||
| 
 | ||||
|       const updateOrCreateComponent = async value => { | ||||
|         // check if value has an id then update else create
 | ||||
|         if (hasPK(value)) { | ||||
|           return strapi.query(component).update( | ||||
|             { | ||||
|               [model.primaryKey]: getPK(value), | ||||
|             }, | ||||
|             value | ||||
|         const componentModel = strapi.components[componentUID]; | ||||
|         const componentValue = values[key]; | ||||
| 
 | ||||
|         if (repeatable === true) { | ||||
|           validateRepeatableInput(componentValue, { key, ...attr }); | ||||
| 
 | ||||
|           await deleteOldComponents(entry, componentValue, { | ||||
|             key, | ||||
|             componentModel, | ||||
|           }); | ||||
| 
 | ||||
|           const components = await Promise.all( | ||||
|             componentValue.map(value => | ||||
|               updateOrCreateComponent({ componentUID, value }) | ||||
|             ) | ||||
|           ); | ||||
|         } | ||||
|         return strapi.query(component).create(value); | ||||
|       }; | ||||
| 
 | ||||
|       if (repeatable === true) { | ||||
|         validateRepeatableInput(componentValue, { key, ...attr }); | ||||
| 
 | ||||
|         await deleteOldComponents(entry, componentValue, { | ||||
|           key, | ||||
|           componentModel, | ||||
|         }); | ||||
| 
 | ||||
|         const components = await Promise.all( | ||||
|           componentValue.map(updateOrCreateComponent) | ||||
|         ); | ||||
|         const componentsArr = components.map(component => ({ | ||||
|           kind: componentModel.globalId, | ||||
|           ref: component, | ||||
|         })); | ||||
| 
 | ||||
|         entry[key] = componentsArr; | ||||
|         await entry.save(); | ||||
|       } else { | ||||
|         validateNonRepeatableInput(componentValue, { key, ...attr }); | ||||
| 
 | ||||
|         await deleteOldComponents(entry, componentValue, { | ||||
|           key, | ||||
|           componentModel, | ||||
|         }); | ||||
| 
 | ||||
|         if (componentValue === null) continue; | ||||
| 
 | ||||
|         const component = await updateOrCreateComponent(componentValue); | ||||
|         entry[key] = [ | ||||
|           { | ||||
|           const componentsArr = components.map(component => ({ | ||||
|             kind: componentModel.globalId, | ||||
|             ref: component, | ||||
|           }, | ||||
|         ]; | ||||
|           })); | ||||
| 
 | ||||
|           entry[key] = componentsArr; | ||||
|           await entry.save(); | ||||
|         } else { | ||||
|           validateNonRepeatableInput(componentValue, { key, ...attr }); | ||||
| 
 | ||||
|           await deleteOldComponents(entry, componentValue, { | ||||
|             key, | ||||
|             componentModel, | ||||
|           }); | ||||
| 
 | ||||
|           if (componentValue === null) continue; | ||||
| 
 | ||||
|           const component = await updateOrCreateComponent({ | ||||
|             componentUID, | ||||
|             value: componentValue, | ||||
|           }); | ||||
| 
 | ||||
|           entry[key] = [ | ||||
|             { | ||||
|               kind: componentModel.globalId, | ||||
|               ref: component, | ||||
|             }, | ||||
|           ]; | ||||
|           await entry.save(); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (type === 'dynamiczone') { | ||||
|         const dynamiczoneValues = values[key]; | ||||
| 
 | ||||
|         validateDynamiczoneInput(dynamiczoneValues, { key, ...attr }); | ||||
| 
 | ||||
|         await deleteDynamicZoneOldComponents(entry, dynamiczoneValues, { | ||||
|           key, | ||||
|         }); | ||||
| 
 | ||||
|         const dynamiczones = await Promise.all( | ||||
|           dynamiczoneValues.map(value => { | ||||
|             const componentUID = value.__component; | ||||
|             return updateOrCreateComponent({ componentUID, value }).then( | ||||
|               entity => { | ||||
|                 return { | ||||
|                   componentUID, | ||||
|                   entity, | ||||
|                 }; | ||||
|               } | ||||
|             ); | ||||
|           }) | ||||
|         ); | ||||
| 
 | ||||
|         const componentsArr = dynamiczones.map(({ componentUID, entity }) => { | ||||
|           const componentModel = strapi.components[componentUID]; | ||||
| 
 | ||||
|           return { | ||||
|             kind: componentModel.globalId, | ||||
|             ref: entity, | ||||
|           }; | ||||
|         }); | ||||
| 
 | ||||
|         entry[key] = componentsArr; | ||||
|         await entry.save(); | ||||
|       } | ||||
|     } | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   async function deleteDynamicZoneOldComponents(entry, values, { key }) { | ||||
|     const idsToKeep = values.reduce((acc, value) => { | ||||
|       const component = value.__component; | ||||
|       const componentModel = strapi.components[component]; | ||||
|       if (hasPK(value, componentModel)) { | ||||
|         acc.push({ | ||||
|           id: getPK(value, componentModel).toString(), | ||||
|           componentUID: componentModel.uid, | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       return acc; | ||||
|     }, []); | ||||
| 
 | ||||
|     const allIds = [] | ||||
|       .concat(entry[key] || []) | ||||
|       .filter(el => el.ref) | ||||
|       .map(el => ({ | ||||
|         id: el.ref._id.toString(), | ||||
|         componentUID: findComponentByGlobalId(el.kind).uid, | ||||
|       })); | ||||
| 
 | ||||
|     // verify the provided ids are realted to this entity.
 | ||||
|     idsToKeep.forEach(({ id, componentUID }) => { | ||||
|       if ( | ||||
|         !allIds.find(el => el.id === id && el.componentUID === componentUID) | ||||
|       ) { | ||||
|         const err = new Error( | ||||
|           `Some of the provided components in ${key} are not related to the entity` | ||||
|         ); | ||||
|         err.status = 400; | ||||
|         throw err; | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     const idsToDelete = allIds.reduce((acc, { id, componentUID }) => { | ||||
|       if ( | ||||
|         !idsToKeep.find(el => el.id === id && el.componentUID === componentUID) | ||||
|       ) { | ||||
|         acc.push({ | ||||
|           id, | ||||
|           componentUID, | ||||
|         }); | ||||
|       } | ||||
|       return acc; | ||||
|     }, []); | ||||
| 
 | ||||
|     if (idsToDelete.length > 0) { | ||||
|       const deleteMap = idsToDelete.reduce((map, { id, componentUID }) => { | ||||
|         if (!_.has(map, componentUID)) { | ||||
|           map[componentUID] = [id]; | ||||
|           return map; | ||||
|         } | ||||
| 
 | ||||
|         map[componentUID].push(id); | ||||
|         return map; | ||||
|       }, {}); | ||||
| 
 | ||||
|       await Promise.all( | ||||
|         Object.keys(deleteMap).map(componentUID => { | ||||
|           return strapi | ||||
|             .query(componentUID) | ||||
|             .delete({ [`${model.primaryKey}_in`]: deleteMap[componentUID] }); | ||||
|         }) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async function deleteOldComponents( | ||||
|     entry, | ||||
|     componentValue, | ||||
| @ -161,8 +326,12 @@ module.exports = ({ model, modelKey, strapi }) => { | ||||
|       ? componentValue | ||||
|       : [componentValue]; | ||||
| 
 | ||||
|     const idsToKeep = componentArr.filter(hasPK).map(getPK); | ||||
|     const allIds = await (entry[key] || []) | ||||
|     const idsToKeep = componentArr | ||||
|       .filter(val => hasPK(val, componentModel)) | ||||
|       .map(val => getPK(val, componentModel)); | ||||
| 
 | ||||
|     const allIds = [] | ||||
|       .concat(entry[key] || []) | ||||
|       .filter(el => el.ref) | ||||
|       .map(el => el.ref._id); | ||||
| 
 | ||||
| @ -194,14 +363,45 @@ module.exports = ({ model, modelKey, strapi }) => { | ||||
| 
 | ||||
|     for (let key of componentKeys) { | ||||
|       const attr = model.attributes[key]; | ||||
|       const { component } = attr; | ||||
|       const componentModel = strapi.components[component]; | ||||
|       const { type } = attr; | ||||
| 
 | ||||
|       if (Array.isArray(entry[key]) && entry[key].length > 0) { | ||||
|         const idsToDelete = entry[key].map(el => el.ref); | ||||
|         await strapi | ||||
|           .query(componentModel.uid) | ||||
|           .delete({ [`${model.primaryKey}_in`]: idsToDelete }); | ||||
|       if (type === 'component') { | ||||
|         const { component } = attr; | ||||
|         const componentModel = strapi.components[component]; | ||||
| 
 | ||||
|         if (Array.isArray(entry[key]) && entry[key].length > 0) { | ||||
|           const idsToDelete = entry[key].map(el => el.ref); | ||||
|           await strapi | ||||
|             .query(componentModel.uid) | ||||
|             .delete({ [`${model.primaryKey}_in`]: idsToDelete }); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (type === 'dynamiczone') { | ||||
|         if (Array.isArray(entry[key]) && entry[key].length > 0) { | ||||
|           const idsToDelete = entry[key].map(el => ({ | ||||
|             componentUID: findComponentByGlobalId(el.kind).uid, | ||||
|             id: el.ref, | ||||
|           })); | ||||
| 
 | ||||
|           const deleteMap = idsToDelete.reduce((map, { id, componentUID }) => { | ||||
|             if (!_.has(map, componentUID)) { | ||||
|               map[componentUID] = [id]; | ||||
|               return map; | ||||
|             } | ||||
| 
 | ||||
|             map[componentUID].push(id); | ||||
|             return map; | ||||
|           }, {}); | ||||
| 
 | ||||
|           await Promise.all( | ||||
|             Object.keys(deleteMap).map(componentUID => { | ||||
|               return strapi.query(componentUID).delete({ | ||||
|                 [`${model.primaryKey}_in`]: idsToDelete[componentUID], | ||||
|               }); | ||||
|             }) | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @ -221,7 +421,7 @@ module.exports = ({ model, modelKey, strapi }) => { | ||||
|   } | ||||
| 
 | ||||
|   async function findOne(params, populate) { | ||||
|     const primaryKey = getPK(params); | ||||
|     const primaryKey = getPK(params, model); | ||||
| 
 | ||||
|     if (primaryKey) { | ||||
|       params = { | ||||
| @ -257,13 +457,13 @@ module.exports = ({ model, modelKey, strapi }) => { | ||||
| 
 | ||||
|     // Create relational data and return the entry.
 | ||||
|     return model.updateRelations({ | ||||
|       [model.primaryKey]: getPK(entry), | ||||
|       [model.primaryKey]: getPK(entry, model), | ||||
|       values: relations, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async function update(params, values) { | ||||
|     const primaryKey = getPK(params); | ||||
|     const primaryKey = getPK(params, model); | ||||
| 
 | ||||
|     if (primaryKey) { | ||||
|       params = { | ||||
| @ -293,7 +493,7 @@ module.exports = ({ model, modelKey, strapi }) => { | ||||
|   } | ||||
| 
 | ||||
|   async function deleteMany(params) { | ||||
|     const primaryKey = getPK(params); | ||||
|     const primaryKey = getPK(params, model); | ||||
| 
 | ||||
|     if (primaryKey) return deleteOne(params); | ||||
| 
 | ||||
| @ -303,7 +503,7 @@ module.exports = ({ model, modelKey, strapi }) => { | ||||
| 
 | ||||
|   async function deleteOne(params) { | ||||
|     const entry = await model | ||||
|       .findOneAndRemove({ [model.primaryKey]: getPK(params) }) | ||||
|       .findOneAndRemove({ [model.primaryKey]: getPK(params, model) }) | ||||
|       .populate(defaultPopulate); | ||||
| 
 | ||||
|     if (!entry) { | ||||
| @ -454,3 +654,56 @@ function validateNonRepeatableInput(value, { key, required }) { | ||||
|     throw err; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function validateDynamiczoneInput( | ||||
|   value, | ||||
|   { key, min, max, components, required } | ||||
| ) { | ||||
|   if (!Array.isArray(value)) { | ||||
|     const err = new Error(`Dynamiczone ${key} is invalid. Expected an array`); | ||||
|     err.status = 400; | ||||
|     throw err; | ||||
|   } | ||||
| 
 | ||||
|   value.forEach(val => { | ||||
|     if (typeof val !== 'object' || Array.isArray(val) || val === null) { | ||||
|       const err = new Error( | ||||
|         `Dynamiczone ${key} has invalid items. Expected each items to be objects` | ||||
|       ); | ||||
|       err.status = 400; | ||||
|       throw err; | ||||
|     } | ||||
| 
 | ||||
|     if (!_.has(val, '__component')) { | ||||
|       const err = new Error( | ||||
|         `Dynamiczone ${key} has invalid items. Expected each items to have a valid __component key` | ||||
|       ); | ||||
|       err.status = 400; | ||||
|       throw err; | ||||
|     } else if (!components.includes(val.__component)) { | ||||
|       const err = new Error( | ||||
|         `Dynamiczone ${key} has invalid items. Each item must have a __component key that is present in the attribute definition` | ||||
|       ); | ||||
|       err.status = 400; | ||||
|       throw err; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   if ( | ||||
|     (required === true || (required !== true && value.length > 0)) && | ||||
|     (min && value.length < min) | ||||
|   ) { | ||||
|     const err = new Error( | ||||
|       `Dynamiczone ${key} must contain at least ${min} items` | ||||
|     ); | ||||
|     err.status = 400; | ||||
|     throw err; | ||||
|   } | ||||
|   if (max && value.length > max) { | ||||
|     const err = new Error( | ||||
|       `Dynamiczone ${key} must contain at most ${max} items` | ||||
|     ); | ||||
|     err.status = 400; | ||||
|     throw err; | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										11
									
								
								packages/strapi-connector-mongoose/lib/utils/helpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								packages/strapi-connector-mongoose/lib/utils/helpers.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const findComponentByGlobalId = globalId => { | ||||
|   return Object.values(strapi.components).find( | ||||
|     compo => compo.globalId === globalId | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| module.exports = { | ||||
|   findComponentByGlobalId, | ||||
| }; | ||||
| @ -8,6 +8,10 @@ module.exports = async (ctx, next) => { | ||||
| 
 | ||||
|   const ct = strapi.contentTypes[model]; | ||||
| 
 | ||||
|   if (!ct) { | ||||
|     return ctx.send({ error: 'contentType.notFound' }, 404); | ||||
|   } | ||||
| 
 | ||||
|   const target = | ||||
|     ct.plugin === 'admin' ? strapi.admin : strapi.plugins[ct.plugin]; | ||||
| 
 | ||||
|  | ||||
| @ -576,6 +576,7 @@ describe.each([ | ||||
|       }; | ||||
| 
 | ||||
|       expect(updateRes.statusCode).toBe(200); | ||||
| 
 | ||||
|       expect(updateRes.body).toMatchObject(expectedResult); | ||||
| 
 | ||||
|       const getRes = await rq.get(`/${res.body.id}`); | ||||
|  | ||||
| @ -16,6 +16,7 @@ module.exports = async (entry, files, { model, source }) => { | ||||
|     let tmpModel = entity; | ||||
|     let modelName = model; | ||||
|     let sourceName; | ||||
| 
 | ||||
|     for (let i = 0; i < path.length; i++) { | ||||
|       if (!tmpModel) return {}; | ||||
|       const part = path[i]; | ||||
| @ -35,7 +36,11 @@ module.exports = async (entry, files, { model, source }) => { | ||||
|         tmpModel = strapi.components[attr.component]; | ||||
|       } else if (attr.type === 'dynamiczone') { | ||||
|         const entryIdx = path[i + 1]; // get component index
 | ||||
|         modelName = _.get(entry, [...currentPath, entryIdx]).__component; // get component type
 | ||||
|         const value = _.get(entry, [...currentPath, entryIdx]); | ||||
| 
 | ||||
|         if (!value) return {}; | ||||
| 
 | ||||
|         modelName = value.__component; // get component type
 | ||||
|         tmpModel = strapi.components[modelName]; | ||||
|       } else if (_.has(attr, 'model') || _.has(attr, 'collection')) { | ||||
|         sourceName = attr.plugin; | ||||
| @ -57,6 +62,7 @@ module.exports = async (entry, files, { model, source }) => { | ||||
| 
 | ||||
|     if (model) { | ||||
|       const id = _.get(entry, path.concat('id')); | ||||
| 
 | ||||
|       return uploadService.uploadToEntity( | ||||
|         { id, model }, | ||||
|         { [field]: files }, | ||||
|  | ||||
| @ -80,7 +80,7 @@ module.exports = { | ||||
|       delete file.buffer; | ||||
|       file.provider = provider.provider; | ||||
| 
 | ||||
|       const res = await strapi.plugins['upload'].services.upload.add(file); | ||||
|       const res = await this.add(file); | ||||
| 
 | ||||
|       // Remove temp file
 | ||||
|       if (file.tmpPath) { | ||||
|  | ||||
| @ -8,7 +8,9 @@ const findModelByAssoc = assoc => { | ||||
| }; | ||||
| 
 | ||||
| const isAttribute = (model, field) => | ||||
|   _.has(model.allAttributes, field) || model.primaryKey === field; | ||||
|   _.has(model.allAttributes, field) || | ||||
|   model.primaryKey === field || | ||||
|   field === 'id'; | ||||
| 
 | ||||
| /** | ||||
|  * Returns the model, attribute name and association from a path of relation | ||||
| @ -131,7 +133,11 @@ const buildQuery = ({ model, filters = {}, ...rest }) => { | ||||
|           ? value.map(val => castValue({ type, operator, value: val })) | ||||
|           : castValue({ type, operator, value: value }); | ||||
| 
 | ||||
|         return { field, operator, value: castedValue }; | ||||
|         return { | ||||
|           field: field === 'id' ? model.primaryKey : field, | ||||
|           operator, | ||||
|           value: castedValue, | ||||
|         }; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -16,6 +16,7 @@ module.exports = async (entry, files, { model, source }) => { | ||||
|     let tmpModel = entity; | ||||
|     let modelName = model; | ||||
|     let sourceName; | ||||
| 
 | ||||
|     for (let i = 0; i < path.length; i++) { | ||||
|       if (!tmpModel) return {}; | ||||
|       const part = path[i]; | ||||
| @ -35,7 +36,11 @@ module.exports = async (entry, files, { model, source }) => { | ||||
|         tmpModel = strapi.components[attr.component]; | ||||
|       } else if (attr.type === 'dynamiczone') { | ||||
|         const entryIdx = path[i + 1]; // get component index
 | ||||
|         modelName = _.get(entry, [...currentPath, entryIdx]).__component; // get component type
 | ||||
|         const value = _.get(entry, [...currentPath, entryIdx]); | ||||
| 
 | ||||
|         if (!value) return {}; | ||||
| 
 | ||||
|         modelName = value.__component; // get component type
 | ||||
|         tmpModel = strapi.components[modelName]; | ||||
|       } else if (_.has(attr, 'model') || _.has(attr, 'collection')) { | ||||
|         sourceName = attr.plugin; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Alexandre BODIN
						Alexandre BODIN