mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 09:56:44 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			471 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			471 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * Module dependencies
 | |
|  */
 | |
| 
 | |
| // Public node modules.
 | |
| const _ = require('lodash');
 | |
| const mongoose = require('mongoose');
 | |
| 
 | |
| // Utils
 | |
| const {
 | |
|   models: { getValuePrimaryKey },
 | |
| } = require('strapi-utils');
 | |
| 
 | |
| const transformToArrayID = (array, pk) => {
 | |
|   if (_.isArray(array)) {
 | |
|     return array
 | |
|       .map(value => value && (getValuePrimaryKey(value, pk) || value))
 | |
|       .filter(n => n)
 | |
|       .map(val => _.toString(val));
 | |
|   }
 | |
| 
 | |
|   return transformToArrayID([array]);
 | |
| };
 | |
| 
 | |
| const removeUndefinedKeys = (obj = {}) => _.pickBy(obj, _.negate(_.isUndefined));
 | |
| 
 | |
| const addRelationMorph = async (model, params) => {
 | |
|   const { id, alias, refId, ref, field, filter } = params;
 | |
| 
 | |
|   await model.updateMany(
 | |
|     {
 | |
|       [model.primaryKey]: id,
 | |
|     },
 | |
|     {
 | |
|       $push: {
 | |
|         [alias]: {
 | |
|           ref: new mongoose.Types.ObjectId(refId),
 | |
|           kind: ref,
 | |
|           [filter]: field,
 | |
|         },
 | |
|       },
 | |
|     }
 | |
|   );
 | |
| };
 | |
| 
 | |
| const removeRelationMorph = async (model, params) => {
 | |
|   const { alias } = params;
 | |
| 
 | |
|   let opts;
 | |
|   // if entry id is provided simply query it
 | |
|   if (params.id) {
 | |
|     opts = {
 | |
|       _id: params.id,
 | |
|     };
 | |
|   } else {
 | |
|     opts = {
 | |
|       [alias]: {
 | |
|         $elemMatch: {
 | |
|           ref: params.refId,
 | |
|           kind: params.ref,
 | |
|           [params.filter]: params.field,
 | |
|         },
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   await model.updateMany(opts, {
 | |
|     $pull: {
 | |
|       [alias]: {
 | |
|         ref: params.refId,
 | |
|         kind: params.ref,
 | |
|         [params.filter]: params.field,
 | |
|       },
 | |
|     },
 | |
|   });
 | |
| };
 | |
| 
 | |
| module.exports = {
 | |
|   async update(params) {
 | |
|     const relationUpdates = [];
 | |
|     const populate = this.associations.map(x => x.alias);
 | |
|     const primaryKeyValue = getValuePrimaryKey(params, this.primaryKey);
 | |
| 
 | |
|     const entry = await this.findOne({ [this.primaryKey]: primaryKeyValue })
 | |
|       .populate(populate)
 | |
|       .lean();
 | |
| 
 | |
|     // Only update fields which are on this document.
 | |
|     const values = Object.keys(removeUndefinedKeys(params.values)).reduce((acc, attribute) => {
 | |
|       const currentValue = entry[attribute];
 | |
|       const newValue = params.values[attribute];
 | |
| 
 | |
|       const association = this.associations.find(x => x.alias === attribute);
 | |
| 
 | |
|       const details = this._attributes[attribute];
 | |
| 
 | |
|       // set simple attributes
 | |
|       if (!association && _.get(details, 'isVirtual') !== true) {
 | |
|         return _.set(acc, attribute, newValue);
 | |
|       }
 | |
| 
 | |
|       const assocModel = strapi.db.getModel(details.model || details.collection, details.plugin);
 | |
| 
 | |
|       switch (association.nature) {
 | |
|         case 'oneWay': {
 | |
|           return _.set(acc, attribute, _.get(newValue, assocModel.primaryKey, newValue));
 | |
|         }
 | |
|         case 'oneToOne': {
 | |
|           // if value is the same don't do anything
 | |
|           if (currentValue === newValue) return acc;
 | |
| 
 | |
|           // if the value is null, set field to null on both sides
 | |
|           if (_.isNull(newValue)) {
 | |
|             const updatePromise = assocModel.updateOne(
 | |
|               {
 | |
|                 [assocModel.primaryKey]: getValuePrimaryKey(currentValue, assocModel.primaryKey),
 | |
|               },
 | |
|               { [details.via]: null }
 | |
|             );
 | |
| 
 | |
|             relationUpdates.push(updatePromise);
 | |
|             return _.set(acc, attribute, null);
 | |
|           }
 | |
| 
 | |
|           // set old relations to null
 | |
|           const updateLink = this.updateOne(
 | |
|             { [attribute]: new mongoose.Types.ObjectId(newValue) },
 | |
|             { [attribute]: null }
 | |
|           ).then(() => {
 | |
|             return assocModel.updateOne(
 | |
|               {
 | |
|                 [this.primaryKey]: new mongoose.Types.ObjectId(newValue),
 | |
|               },
 | |
|               { [details.via]: primaryKeyValue }
 | |
|             );
 | |
|           });
 | |
| 
 | |
|           // set new relation
 | |
|           relationUpdates.push(updateLink);
 | |
|           return _.set(acc, attribute, newValue);
 | |
|         }
 | |
|         case 'oneToMany': {
 | |
|           // set relation to null for all the ids not in the list
 | |
|           const attributeIds = currentValue;
 | |
|           const toRemove = _.differenceWith(attributeIds, newValue, (a, b) => {
 | |
|             return `${a[assocModel.primaryKey] || a}` === `${b[assocModel.primaryKey] || b}`;
 | |
|           });
 | |
| 
 | |
|           const updatePromise = assocModel
 | |
|             .updateMany(
 | |
|               {
 | |
|                 [assocModel.primaryKey]: {
 | |
|                   $in: toRemove.map(
 | |
|                     val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
 | |
|                   ),
 | |
|                 },
 | |
|               },
 | |
|               { [details.via]: null }
 | |
|             )
 | |
|             .then(() => {
 | |
|               return assocModel.updateMany(
 | |
|                 {
 | |
|                   [assocModel.primaryKey]: {
 | |
|                     $in: newValue.map(
 | |
|                       val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
 | |
|                     ),
 | |
|                   },
 | |
|                 },
 | |
|                 { [details.via]: primaryKeyValue }
 | |
|               );
 | |
|             });
 | |
| 
 | |
|           relationUpdates.push(updatePromise);
 | |
|           return acc;
 | |
|         }
 | |
|         case 'manyToOne': {
 | |
|           return _.set(acc, attribute, _.get(newValue, assocModel.primaryKey, newValue));
 | |
|         }
 | |
|         case 'manyWay':
 | |
|         case 'manyToMany': {
 | |
|           if (association.dominant) {
 | |
|             return _.set(
 | |
|               acc,
 | |
|               attribute,
 | |
|               newValue ? newValue.map(val => val[assocModel.primaryKey] || val) : newValue
 | |
|             );
 | |
|           }
 | |
| 
 | |
|           const updatePomise = assocModel
 | |
|             .updateMany(
 | |
|               {
 | |
|                 [assocModel.primaryKey]: {
 | |
|                   $in: currentValue.map(
 | |
|                     val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
 | |
|                   ),
 | |
|                 },
 | |
|               },
 | |
|               {
 | |
|                 $pull: {
 | |
|                   [association.via]: new mongoose.Types.ObjectId(primaryKeyValue),
 | |
|                 },
 | |
|               }
 | |
|             )
 | |
|             .then(() => {
 | |
|               return assocModel.updateMany(
 | |
|                 {
 | |
|                   [assocModel.primaryKey]: {
 | |
|                     $in: newValue
 | |
|                       ? newValue.map(
 | |
|                           val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
 | |
|                         )
 | |
|                       : newValue,
 | |
|                   },
 | |
|                 },
 | |
|                 {
 | |
|                   $addToSet: { [association.via]: [primaryKeyValue] },
 | |
|                 }
 | |
|               );
 | |
|             });
 | |
| 
 | |
|           relationUpdates.push(updatePomise);
 | |
|           return acc;
 | |
|         }
 | |
|         // media -> model
 | |
|         case 'manyMorphToMany':
 | |
|         case 'manyMorphToOne': {
 | |
|           newValue.forEach(obj => {
 | |
|             const refModel = strapi.db.getModel(obj.ref, obj.source);
 | |
| 
 | |
|             const createRelation = () => {
 | |
|               return addRelationMorph(this, {
 | |
|                 id: entry[this.primaryKey],
 | |
|                 alias: association.alias,
 | |
|                 ref: obj.kind || refModel.globalId,
 | |
|                 refId: new mongoose.Types.ObjectId(obj.refId),
 | |
|                 field: obj.field,
 | |
|                 filter: association.filter,
 | |
|               });
 | |
|             };
 | |
| 
 | |
|             // Clear relations to refModel
 | |
|             const reverseAssoc = refModel.associations.find(assoc => assoc.alias === obj.field);
 | |
|             if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') {
 | |
|               relationUpdates.push(
 | |
|                 removeRelationMorph(this, {
 | |
|                   alias: association.alias,
 | |
|                   ref: obj.kind || refModel.globalId,
 | |
|                   refId: new mongoose.Types.ObjectId(obj.refId),
 | |
|                   field: obj.field,
 | |
|                   filter: association.filter,
 | |
|                 })
 | |
|                   .then(createRelation)
 | |
|                   .then(() => {
 | |
|                     // set field inside refModel
 | |
|                     return refModel.updateMany(
 | |
|                       {
 | |
|                         [refModel.primaryKey]: new mongoose.Types.ObjectId(obj.refId),
 | |
|                       },
 | |
|                       {
 | |
|                         [obj.field]: new mongoose.Types.ObjectId(entry[this.primaryKey]),
 | |
|                       }
 | |
|                     );
 | |
|                   })
 | |
|               );
 | |
|             } else {
 | |
|               relationUpdates.push(
 | |
|                 createRelation().then(() => {
 | |
|                   // push to field inside refModel
 | |
|                   return refModel.updateMany(
 | |
|                     {
 | |
|                       [refModel.primaryKey]: new mongoose.Types.ObjectId(obj.refId),
 | |
|                     },
 | |
|                     {
 | |
|                       $push: { [obj.field]: new mongoose.Types.ObjectId(entry[this.primaryKey]) },
 | |
|                     }
 | |
|                   );
 | |
|                 })
 | |
|               );
 | |
|             }
 | |
|           });
 | |
|           break;
 | |
|         }
 | |
|         // model -> media
 | |
|         case 'oneToManyMorph':
 | |
|         case 'manyToManyMorph': {
 | |
|           // Compare array of ID to find deleted files.
 | |
|           const currentIds = transformToArrayID(currentValue, this.primaryKey);
 | |
|           const newIds = transformToArrayID(newValue, this.primaryKey);
 | |
| 
 | |
|           const toAdd = _.difference(newIds, currentIds);
 | |
|           const toRemove = _.difference(currentIds, newIds);
 | |
| 
 | |
|           const model = strapi.db.getModel(details.model || details.collection, details.plugin);
 | |
| 
 | |
|           if (!Array.isArray(newValue)) {
 | |
|             _.set(acc, attribute, newIds[0]);
 | |
|           } else {
 | |
|             _.set(acc, attribute, newIds);
 | |
|           }
 | |
| 
 | |
|           const addPromise = Promise.all(
 | |
|             toAdd.map(id => {
 | |
|               return addRelationMorph(model, {
 | |
|                 id,
 | |
|                 alias: association.via,
 | |
|                 ref: this.globalId,
 | |
|                 refId: entry._id,
 | |
|                 field: association.alias,
 | |
|                 filter: association.filter,
 | |
|               });
 | |
|             })
 | |
|           );
 | |
| 
 | |
|           relationUpdates.push(addPromise);
 | |
| 
 | |
|           toRemove.forEach(id => {
 | |
|             relationUpdates.push(
 | |
|               removeRelationMorph(model, {
 | |
|                 id,
 | |
|                 alias: association.via,
 | |
|                 ref: this.globalId,
 | |
|                 refId: entry._id,
 | |
|                 field: association.alias,
 | |
|                 filter: association.filter,
 | |
|               })
 | |
|             );
 | |
|           });
 | |
|           break;
 | |
|         }
 | |
|         case 'oneMorphToOne':
 | |
|         case 'oneMorphToMany':
 | |
|           break;
 | |
|         default:
 | |
|       }
 | |
| 
 | |
|       return acc;
 | |
|     }, {});
 | |
| 
 | |
|     // Update virtuals fields.
 | |
|     await Promise.all(relationUpdates).then(() =>
 | |
|       this.updateOne({ [this.primaryKey]: primaryKeyValue }, values, {
 | |
|         strict: false,
 | |
|       })
 | |
|     );
 | |
| 
 | |
|     const updatedEntity = await this.findOne({
 | |
|       [this.primaryKey]: primaryKeyValue,
 | |
|     }).populate(populate);
 | |
| 
 | |
|     return updatedEntity && updatedEntity.toObject ? updatedEntity.toObject() : updatedEntity;
 | |
|   },
 | |
| 
 | |
|   deleteRelations(entry) {
 | |
|     const primaryKeyValue = entry[this.primaryKey];
 | |
| 
 | |
|     return Promise.all(
 | |
|       this.associations.map(async association => {
 | |
|         const { nature, via, dominant } = association;
 | |
| 
 | |
|         // TODO: delete all the ref to the model
 | |
| 
 | |
|         switch (nature) {
 | |
|           case 'oneWay':
 | |
|           case 'manyWay': {
 | |
|             return;
 | |
|           }
 | |
|           case 'oneToMany':
 | |
|           case 'oneToOne': {
 | |
|             if (!via) {
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             const targetModel = strapi.db.getModel(
 | |
|               association.model || association.collection,
 | |
|               association.plugin
 | |
|             );
 | |
| 
 | |
|             return targetModel.updateMany({ [via]: primaryKeyValue }, { [via]: null });
 | |
|           }
 | |
|           case 'manyToMany':
 | |
|           case 'manyToOne': {
 | |
|             if (!via || dominant) {
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             const targetModel = strapi.db.getModel(
 | |
|               association.model || association.collection,
 | |
|               association.plugin
 | |
|             );
 | |
| 
 | |
|             return targetModel.updateMany(
 | |
|               { [via]: primaryKeyValue },
 | |
|               { $pull: { [via]: primaryKeyValue } }
 | |
|             );
 | |
|           }
 | |
|           case 'oneToManyMorph':
 | |
|           case 'manyToManyMorph': {
 | |
|             // delete relation inside of the ref model
 | |
| 
 | |
|             const targetModel = strapi.db.getModel(
 | |
|               association.model || association.collection,
 | |
|               association.plugin
 | |
|             );
 | |
| 
 | |
|             // ignore them ghost relations
 | |
|             if (!targetModel) return;
 | |
| 
 | |
|             const element = {
 | |
|               ref: primaryKeyValue,
 | |
|               kind: this.globalId,
 | |
|               [association.filter]: association.alias,
 | |
|             };
 | |
| 
 | |
|             return targetModel.updateMany(
 | |
|               { [via]: { $elemMatch: element } },
 | |
|               { $pull: { [via]: element } }
 | |
|             );
 | |
|           }
 | |
|           case 'manyMorphToMany':
 | |
|           case 'manyMorphToOne': {
 | |
|             // delete relation inside of the ref model
 | |
|             // console.log(entry[association.alias]);
 | |
| 
 | |
|             if (Array.isArray(entry[association.alias])) {
 | |
|               return Promise.all(
 | |
|                 entry[association.alias].map(val => {
 | |
|                   const targetModel = strapi.db.getModelByGlobalId(val.kind);
 | |
| 
 | |
|                   // ignore them ghost relations
 | |
|                   if (!targetModel) return;
 | |
| 
 | |
|                   const field = val[association.filter];
 | |
|                   const reverseAssoc = targetModel.associations.find(
 | |
|                     assoc => assoc.alias === field
 | |
|                   );
 | |
| 
 | |
|                   if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') {
 | |
|                     return targetModel.updateMany(
 | |
|                       {
 | |
|                         [targetModel.primaryKey]: val.ref && (val.ref._id || val.ref),
 | |
|                       },
 | |
|                       {
 | |
|                         [field]: null,
 | |
|                       }
 | |
|                     );
 | |
|                   }
 | |
| 
 | |
|                   return targetModel.updateMany(
 | |
|                     {
 | |
|                       [targetModel.primaryKey]: val.ref && (val.ref._id || val.ref),
 | |
|                     },
 | |
|                     {
 | |
|                       $pull: { [field]: primaryKeyValue },
 | |
|                     }
 | |
|                   );
 | |
|                 })
 | |
|               );
 | |
|             }
 | |
| 
 | |
|             return;
 | |
|           }
 | |
|           case 'oneMorphToOne':
 | |
|           case 'oneMorphToMany': {
 | |
|             return;
 | |
|           }
 | |
|         }
 | |
|       })
 | |
|     );
 | |
|   },
 | |
| };
 | 
