mirror of
https://github.com/strapi/strapi.git
synced 2025-08-06 07:50:02 +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;
|
|
}
|
|
}
|
|
})
|
|
);
|
|
},
|
|
};
|