2019-11-27 15:30:38 +01:00

429 lines
14 KiB
JavaScript

/**
* Module dependencies
*/
// Public node modules.
const _ = require('lodash');
const mongoose = require('mongoose');
// Utils
const {
models: { getValuePrimaryKey },
} = require('strapi-utils');
const getModel = function(model, plugin) {
return (
_.get(strapi.plugins, [plugin, 'models', model]) ||
_.get(strapi, ['models', model]) ||
undefined
);
};
const removeUndefinedKeys = obj => _.pickBy(obj, _.negate(_.isUndefined));
module.exports = {
update: async function(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 =
params.parseRelationships === false
? params.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 = 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;
}
case 'manyMorphToMany':
case 'manyMorphToOne': {
// Update the relational array.
newValue.forEach(obj => {
const refModel = strapi.getModel(obj.ref, obj.source);
const createRelation = () => {
return module.exports.addRelationMorph.call(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(
module.exports.removeRelationMorph
.call(this, {
alias: association.alias,
ref: obj.kind || refModel.globalId,
refId: new mongoose.Types.ObjectId(obj.refId),
field: obj.field,
filter: association.filter,
})
.then(createRelation)
);
} else {
relationUpdates.push(createRelation());
}
});
break;
}
case 'oneToManyMorph':
case 'manyToManyMorph': {
const transformToArrayID = array => {
if (_.isArray(array)) {
return array.map(value => {
if (_.isPlainObject(value)) {
return getValuePrimaryKey(value, this.primaryKey);
}
return value;
});
}
if (
association.type === 'model' ||
(association.type === 'collection' && _.isObject(array))
) {
return _.isEmpty(array)
? []
: transformToArrayID([array]);
}
return [];
};
// Compare array of ID to find deleted files.
const attributeValue = transformToArrayID(currentValue).map(
id => id.toString()
);
const storedValue = transformToArrayID(newValue).map(id =>
id.toString()
);
const toAdd = _.difference(storedValue, attributeValue);
const toRemove = _.difference(attributeValue, storedValue);
const model = getModel(
details.model || details.collection,
details.plugin
);
// Remove relations in the other side.
toAdd.forEach(id => {
relationUpdates.push(
module.exports.addRelationMorph.call(model, {
id,
alias: association.via,
ref: this.globalId,
refId: entry._id,
field: association.alias,
filter: association.filter,
})
);
});
// Remove relations in the other side.
toRemove.forEach(id => {
relationUpdates.push(
module.exports.removeRelationMorph.call(model, {
id,
alias: association.via,
ref: this.globalId,
refId: entry._id,
field: association.alias,
})
);
});
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;
},
async addRelationMorph(params) {
const { alias, id } = params;
let entry = await this.findOne({
[this.primaryKey]: id,
});
if (!entry) return Promise.resolve();
// if association already exists ignore
const relationExists = entry[alias].find(obj => {
if (
obj.kind === params.ref &&
obj.ref.toString() === params.refId.toString() &&
obj.field === params.field
) {
return true;
}
return false;
});
if (relationExists) return Promise.resolve();
entry[alias].push({
ref: new mongoose.Types.ObjectId(params.refId),
kind: params.ref,
[params.filter]: params.field,
});
await entry.save();
},
async removeRelationMorph(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,
},
},
};
}
const entries = await this.find(opts);
const updates = entries.map(entry => {
entry[alias] = entry[alias].filter(obj => {
if (
obj.kind === params.ref &&
obj.ref.toString() === params.refId.toString() &&
obj.field === params.field
) {
return false;
}
return true;
});
return entry.save();
});
await Promise.all(updates);
},
};