Add deleteRelations logic in connectors

Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
Alexandre Bodin 2020-03-19 16:46:27 +01:00
parent 0c395eef7e
commit c8de166051
10 changed files with 833 additions and 780 deletions

View File

@ -142,7 +142,11 @@ module.exports = ({ models, target }, ctx) => {
}
const { nature, verbose } =
utilsModels.getNature(details, name, undefined, model.toLowerCase()) || {};
utilsModels.getNature({
attribute: details,
attributeName: name,
modelName: model.toLowerCase(),
}) || {};
// Build associations key
utilsModels.defineAssociations(model.toLowerCase(), definition, details, name);
@ -652,6 +656,7 @@ module.exports = ({ models, target }, ctx) => {
// Push attributes to be aware of model schema.
target[model]._attributes = definition.attributes;
target[model].updateRelations = relations.update;
target[model].deleteRelations = relations.deleteRelations;
await buildDatabaseSchema({
ORM,

View File

@ -132,28 +132,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
throw err;
}
const values = {};
model.associations.map(association => {
switch (association.nature) {
case 'oneWay':
case 'oneToOne':
case 'manyToOne':
case 'oneToManyMorph':
values[association.alias] = null;
break;
case 'manyWay':
case 'oneToMany':
case 'manyToMany':
case 'manyToManyMorph':
case 'manyMorphToMany':
case 'manyMorphToOne':
values[association.alias] = [];
break;
default:
}
});
await model.updateRelations({ [model.primaryKey]: id, values }, { transacting });
await model.deleteRelations(id, { transacting });
const runDelete = async trx => {
await deleteComponents(entry, { transacting: trx });

View File

@ -244,6 +244,7 @@ module.exports = {
relationUpdates.push(updatePromise);
return acc;
}
// media -> model
case 'manyMorphToMany':
case 'manyMorphToOne': {
// Update the relational array.
@ -325,6 +326,7 @@ module.exports = {
});
break;
}
// model -> media
case 'oneToManyMorph':
case 'manyToManyMorph': {
const currentValue = transformToArrayID(params.values[current]);
@ -391,4 +393,30 @@ module.exports = {
return result && result.toJSON ? result.toJSON() : result;
},
deleteRelations(id, { transacting }) {
const values = {};
this.associations.map(association => {
switch (association.nature) {
case 'oneWay':
case 'oneToOne':
case 'manyToOne':
case 'oneToManyMorph':
values[association.alias] = null;
break;
case 'manyWay':
case 'oneToMany':
case 'manyToMany':
case 'manyToManyMorph':
case 'manyMorphToMany':
case 'manyMorphToOne':
values[association.alias] = [];
break;
default:
}
});
return this.updateRelations({ [this.primaryKey]: id, values }, { transacting });
},
};

View File

@ -219,6 +219,7 @@ module.exports = ({ models, target }, ctx) => {
virtuals: true,
transform: function(doc, returned) {
// Remover $numberDecimal nested property.
Object.keys(returned)
.filter(key => returned[key] instanceof mongoose.Types.Decimal128)
.forEach(key => {
@ -239,11 +240,13 @@ module.exports = ({ models, target }, ctx) => {
break;
case 'manyMorphToMany':
case 'manyMorphToOne':
case 'manyMorphToOne': {
returned[association.alias] = returned[association.alias].map(obj =>
refToStrapiRef(obj)
);
break;
}
default:
}
}
@ -297,6 +300,7 @@ module.exports = ({ models, target }, ctx) => {
// Push attributes to be aware of model schema.
target[model]._attributes = definition.attributes;
target[model].updateRelations = relations.update;
target[model].deleteRelations = relations.deleteRelations;
});
};
@ -343,48 +347,52 @@ const createOnFetchPopulateFn = ({ morphAssociations, componentAttributes, defin
const buildRelation = ({ definition, model, instance, attribute, name }) => {
const { nature, verbose } =
utilsModels.getNature(attribute, name, undefined, model.toLowerCase()) || {};
utilsModels.getNature({
attribute,
attributeName: name,
modelName: model.toLowerCase(),
}) || {};
// Build associations key
utilsModels.defineAssociations(model.toLowerCase(), definition, attribute, name);
const getRef = (name, plugin) => {
return plugin ? strapi.plugins[plugin].models[name].globalId : strapi.models[name].globalId;
};
const setField = (name, val) => {
definition.loadedModel[name] = val;
};
const { ObjectId } = instance.Schema.Types;
switch (verbose) {
case 'hasOne': {
const ref = attribute.plugin
? strapi.plugins[attribute.plugin].models[attribute.model].globalId
: strapi.models[attribute.model].globalId;
const ref = getRef(attribute.model, attribute.plugin);
setField(name, { type: ObjectId, ref });
definition.loadedModel[name] = {
type: instance.Schema.Types.ObjectId,
ref,
};
break;
}
case 'hasMany': {
const FK = _.find(definition.associations, {
alias: name,
});
const ref = attribute.plugin
? strapi.plugins[attribute.plugin].models[attribute.collection].globalId
: strapi.models[attribute.collection].globalId;
const ref = getRef(attribute.collection, attribute.plugin);
if (FK) {
definition.loadedModel[name] = {
setField(name, {
type: 'virtual',
ref,
via: FK.via,
justOne: false,
};
});
// Set this info to be able to see if this field is a real database's field.
attribute.isVirtual = true;
} else {
definition.loadedModel[name] = [
{
type: instance.Schema.Types.ObjectId,
ref,
},
];
setField(name, [{ type: ObjectId, ref }]);
}
break;
}
@ -392,9 +400,8 @@ const buildRelation = ({ definition, model, instance, attribute, name }) => {
const FK = _.find(definition.associations, {
alias: name,
});
const ref = attribute.plugin
? strapi.plugins[attribute.plugin].models[attribute.model].globalId
: strapi.models[attribute.model].globalId;
const ref = getRef(attribute.model, attribute.plugin);
if (
FK &&
@ -403,38 +410,26 @@ const buildRelation = ({ definition, model, instance, attribute, name }) => {
FK.nature !== 'oneWay' &&
FK.nature !== 'oneToMorph'
) {
definition.loadedModel[name] = {
setField(name, {
type: 'virtual',
ref,
via: FK.via,
justOne: true,
};
});
// Set this info to be able to see if this field is a real database's field.
attribute.isVirtual = true;
} else {
definition.loadedModel[name] = {
type: instance.Schema.Types.ObjectId,
ref,
};
setField(name, { type: ObjectId, ref });
}
break;
}
case 'belongsToMany': {
const targetModel = attribute.plugin
? strapi.plugins[attribute.plugin].models[attribute.collection]
: strapi.models[attribute.collection];
const ref = targetModel.globalId;
const ref = getRef(attribute.collection, attribute.plugin);
if (nature === 'manyWay') {
definition.loadedModel[name] = [
{
type: instance.Schema.Types.ObjectId,
ref,
},
];
setField(name, [{ type: ObjectId, ref }]);
} else {
const FK = _.find(definition.associations, {
alias: name,
@ -442,84 +437,81 @@ const buildRelation = ({ definition, model, instance, attribute, name }) => {
// One-side of the relationship has to be a virtual field to be bidirectional.
if ((FK && _.isUndefined(FK.via)) || attribute.dominant !== true) {
definition.loadedModel[name] = {
setField(name, {
type: 'virtual',
ref,
via: FK.via,
};
});
// Set this info to be able to see if this field is a real database's field.
attribute.isVirtual = true;
} else {
definition.loadedModel[name] = [
{
type: instance.Schema.Types.ObjectId,
ref,
},
];
setField(name, [{ type: ObjectId, ref }]);
}
}
break;
}
case 'morphOne': {
const FK = _.find(definition.associations, {
alias: name,
});
const ref = attribute.plugin
? strapi.plugins[attribute.plugin].models[attribute.model].globalId
: strapi.models[attribute.model].globalId;
// const FK = _.find(definition.associations, {
// alias: name,
// });
definition.loadedModel[name] = {
type: 'virtual',
ref,
via: `${FK.via}.ref`,
justOne: true,
};
// const ref = getRef(attribute.model, attribute.plugin);
// setField(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.
// attribute.isVirtual = true;
const ref = getRef(attribute.model, attribute.plugin);
setField(name, { type: ObjectId, ref });
// Set this info to be able to see if this field is a real database's field.
attribute.isVirtual = true;
break;
}
case 'morphMany': {
const FK = _.find(definition.associations, {
alias: name,
});
const ref = attribute.plugin
? strapi.plugins[attribute.plugin].models[attribute.collection].globalId
: strapi.models[attribute.collection].globalId;
// const FK = _.find(definition.associations, {
// alias: name,
// });
// const ref = getRef(attribute.collection, attribute.plugin);
definition.loadedModel[name] = {
type: 'virtual',
ref,
via: `${FK.via}.ref`,
};
// setField(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.
// attribute.isVirtual = true;
const ref = getRef(attribute.collection, attribute.plugin);
setField(name, [{ type: ObjectId, ref }]);
// Set this info to be able to see if this field is a real database's field.
attribute.isVirtual = true;
break;
}
case 'belongsToMorph': {
definition.loadedModel[name] = {
setField(name, {
kind: String,
[attribute.filter]: String,
ref: {
type: instance.Schema.Types.ObjectId,
refPath: `${name}.kind`,
},
};
ref: { type: ObjectId, refPath: `${name}.kind` },
});
break;
}
case 'belongsToManyMorph': {
definition.loadedModel[name] = [
setField(name, [
{
kind: String,
[attribute.filter]: String,
ref: {
type: instance.Schema.Types.ObjectId,
refPath: `${name}.kind`,
},
ref: { type: ObjectId, refPath: `${name}.kind` },
},
];
]);
break;
}
default:

View File

@ -474,29 +474,7 @@ module.exports = ({ model, modelKey, strapi }) => {
await deleteComponents(entry);
await Promise.all(
model.associations.map(async association => {
if (!association.via || !entry._id || association.dominant) {
return true;
}
const search =
_.endsWith(association.nature, 'One') || association.nature === 'oneToMany'
? { [association.via]: entry._id }
: { [association.via]: { $in: [entry._id] } };
const update =
_.endsWith(association.nature, 'One') || association.nature === 'oneToMany'
? { [association.via]: null }
: { $pull: { [association.via]: entry._id } };
// Retrieve model.
const model = association.plugin
? strapi.plugins[association.plugin].models[association.model || association.collection]
: strapi.models[association.model || association.collection];
return model.updateMany(search, update);
})
);
await model.deleteRelations(entry);
return entry.toObject ? entry.toObject() : null;
}

View File

@ -22,7 +22,7 @@ const getModel = function(model, plugin) {
const transformToArrayID = (array, pk) => {
if (_.isArray(array)) {
return array
.map(value => getValuePrimaryKey(value, pk) || value)
.map(value => value && (getValuePrimaryKey(value, pk) || value))
.filter(n => n)
.map(val => _.toString(val));
}
@ -30,10 +30,61 @@ const transformToArrayID = (array, pk) => {
return transformToArrayID([array]);
};
const removeUndefinedKeys = obj => _.pickBy(obj, _.negate(_.isUndefined));
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 = {
update: async function(params) {
async update(params) {
const relationUpdates = [];
const populate = this.associations.map(x => x.alias);
const primaryKeyValue = getValuePrimaryKey(params, this.primaryKey);
@ -43,239 +94,251 @@ module.exports = {
.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 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 association = this.associations.find(x => x.alias === attribute);
const details = this._attributes[attribute];
const details = this._attributes[attribute];
// set simple attributes
if (!association && _.get(details, 'isVirtual') !== true) {
return _.set(acc, attribute, newValue);
}
// set simple attributes
if (!association && _.get(details, 'isVirtual') !== true) {
return _.set(acc, attribute, newValue);
}
const assocModel = getModel(details.model || details.collection, details.plugin);
const assocModel = getModel(details.model || details.collection, details.plugin);
switch (association.nature) {
case 'oneWay': {
return _.set(acc, attribute, _.get(newValue, assocModel.primaryKey, newValue));
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),
},
}
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(
)
.then(() => {
return assocModel.updateMany(
{
[assocModel.primaryKey]: {
$in: newValue
? newValue.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
);
)
: newValue,
},
},
{
$addToSet: { [association.via]: [primaryKeyValue] },
}
);
});
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),
},
}
)
relationUpdates.push(updatePomise);
return acc;
}
// media -> model
case 'manyMorphToMany':
case 'manyMorphToOne': {
newValue.forEach(obj => {
const refModel = strapi.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(() => {
return assocModel.updateMany(
// set field inside refModel
return refModel.updateMany(
{
[assocModel.primaryKey]: {
$in: newValue
? newValue.map(
val =>
new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val)
)
: newValue,
},
[refModel.primaryKey]: new mongoose.Types.ObjectId(obj.refId),
},
{
$addToSet: { [association.via]: [primaryKeyValue] },
[obj.field]: new mongoose.Types.ObjectId(entry[this.primaryKey]),
}
);
});
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': {
// Compare array of ID to find deleted files.
const currentIds = transformToArrayID(currentValue, this.primaryKey);
const newIds = transformToArrayID(newValue, this.primaryKey);
console.log({ currentIds, newIds });
const toAdd = _.difference(newIds, currentIds);
const toRemove = _.difference(currentIds, newIds);
const model = getModel(details.model || details.collection, details.plugin);
console.log(toAdd);
const addPromise = Promise.all(
toAdd.map(id => {
return module.exports.addRelationMorph.call(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(
module.exports.removeRelationMorph.call(model, {
id,
alias: association.via,
ref: this.globalId,
refId: entry._id,
field: association.alias,
})
);
} 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;
}
case 'oneMorphToOne':
case 'oneMorphToMany':
break;
default:
})
);
}
});
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);
return acc;
}, {});
const toAdd = _.difference(newIds, currentIds);
const toRemove = _.difference(currentIds, newIds);
const model = getModel(details.model || details.collection, details.plugin);
_.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(() =>
@ -291,78 +354,121 @@ module.exports = {
return updatedEntity && updatedEntity.toObject ? updatedEntity.toObject() : updatedEntity;
},
async addRelationMorph(params) {
const { alias, id } = params;
deleteRelations(entry) {
const primaryKeyValue = entry[this.primaryKey];
let entry = await this.findOne({
[this.primaryKey]: id,
});
return Promise.all(
this.associations.map(async association => {
const { nature, via, dominant } = association;
if (!entry) return Promise.resolve();
switch (nature) {
case 'oneWay':
case 'manyWay': {
return;
}
case 'oneToMany':
case 'oneToOne': {
if (!via || dominant) {
return;
}
// 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;
}
const targetModel = strapi.db.getModel(
association.model || association.collection,
association.plugin
);
return false;
});
return targetModel.updateMany({ [via]: primaryKeyValue }, { [via]: null });
}
case 'manyToMany':
case 'manyToOne': {
if (!via || dominant) {
return;
}
if (relationExists) return Promise.resolve();
const targetModel = strapi.db.getModel(
association.model || association.collection,
association.plugin
);
entry[alias].push({
ref: new mongoose.Types.ObjectId(params.refId),
kind: params.ref,
[params.filter]: params.field,
});
return targetModel.updateMany(
{ [via]: primaryKeyValue },
{ $pull: { [via]: primaryKeyValue } }
);
}
case 'oneToManyMorph':
case 'manyToManyMorph': {
// delete relation inside of the ref model
await entry.save();
},
const targetModel = strapi.db.getModel(
association.model || association.collection,
association.plugin
);
async removeRelationMorph(params) {
const { alias } = params;
// ignore them ghost relations
if (!targetModel) return;
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 element = {
ref: primaryKeyValue,
kind: this.globalId,
[association.filter]: association.alias,
};
const entries = await this.find(opts);
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]);
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;
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': {
// TODO: to implement
return;
}
}
return true;
});
return entry.save();
});
await Promise.all(updates);
})
);
},
};

View File

@ -117,6 +117,12 @@ class DatabaseManager {
return model.collectionName === collectionName;
});
}
getModelByGlobalId(globalId) {
return Array.from(this.models.values()).find(model => {
return model.globalId === globalId;
});
}
}
function createDatabaseManager(strapi) {

View File

@ -0,0 +1,68 @@
const { getNature } = require('../models');
describe('getNature', () => {
describe('oneWay', () => {
test('oneWay', () => {
global.strapi = {
models: {
baseModel: {
attributes: {
test: {
model: 'modelName',
},
},
},
modelName: {},
},
plugins: {},
};
const nature = getNature({
attribute: global.strapi.models.baseModel.attributes.test,
attributeName: 'test',
modelName: 'baseModel',
});
expect(nature).toEqual({
nature: 'oneWay',
verbose: 'belongsTo',
});
});
});
describe('oneToOne', () => {
test('oneToOne', () => {
global.strapi = {
models: {
baseModel: {
attributes: {
test: {
model: 'modelName',
via: 'reverseAttribute',
},
},
},
modelName: {
attributes: {
reverseAttribute: {
model: 'baseModel',
},
},
},
},
plugins: {},
};
const nature = getNature({
attribute: global.strapi.models.baseModel.attributes.test,
attributeName: 'test',
modelName: 'baseModel',
});
expect(nature).toEqual({
nature: 'oneToOne',
verbose: 'belongsTo',
});
});
});
});

View File

@ -13,7 +13,6 @@ const isNumeric = value => {
return !_.isObject(value) && !isNaN(parseFloat(value)) && isFinite(value);
};
/* eslint-disable prefer-template */
/*
* Set of utils for models
*/
@ -38,300 +37,242 @@ module.exports = {
* Find relation nature with verbose
*/
getNature: (association, key, models, currentModelName) => {
try {
const types = {
current: '',
other: '',
};
getNature: ({ attribute, attributeName, modelName }) => {
const types = {
current: '',
other: '',
};
if (_.isUndefined(models)) {
models = association.plugin
? strapi.plugins[association.plugin].models
: strapi.models;
const models = attribute.plugin ? strapi.plugins[attribute.plugin].models : strapi.models;
const pluginModels = Object.values(strapi.plugins).reduce((acc, plugin) => {
return acc.concat(Object.values(plugin.models));
}, []);
const allModels = Object.values(strapi.models).concat(pluginModels);
if (
(_.has(attribute, 'collection') && attribute.collection === '*') ||
(_.has(attribute, 'model') && attribute.model === '*')
) {
if (attribute.model) {
types.current = 'morphToD';
} else {
types.current = 'morphTo';
}
// We have to find if they are a model linked to this key
_.forEach(allModels, model => {
_.forIn(model.attributes, attribute => {
if (_.has(attribute, 'via') && attribute.via === attributeName) {
if (_.has(attribute, 'collection') && attribute.collection === modelName) {
types.other = 'collection';
// Break loop
return false;
} else if (_.has(attribute, 'model') && attribute.model === modelName) {
types.other = 'modelD';
// Break loop
return false;
}
}
});
});
} else if (_.has(attribute, 'via') && _.has(attribute, 'collection')) {
const relatedAttribute = models[attribute.collection].attributes[attribute.via];
if (!relatedAttribute) {
throw new Error(
`The attribute \`${attribute.via}\` is missing in the model ${_.upperFirst(
attribute.collection
)} ${attribute.plugin ? '(plugin - ' + attribute.plugin + ')' : ''}`
);
}
types.current = 'collection';
if (
(_.has(association, 'collection') && association.collection === '*') ||
(_.has(association, 'model') && association.model === '*')
_.has(relatedAttribute, 'collection') &&
relatedAttribute.collection !== '*' &&
_.has(relatedAttribute, 'via')
) {
if (association.model) {
types.current = 'morphToD';
} else {
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 (
_.has(attribute, 'via') &&
attribute.via === key &&
attribute.model === currentModelName
) {
if (_.has(attribute, 'collection')) {
types.other = 'collection';
// Break loop
return false;
} else if (_.has(attribute, 'model')) {
types.other = 'model';
// Break loop
return false;
}
}
});
});
types.other = 'collection';
} else if (
_.has(association, 'via') &&
_.has(association, 'collection')
_.has(relatedAttribute, 'collection') &&
relatedAttribute.collection !== '*' &&
!_.has(relatedAttribute, 'via')
) {
const relatedAttribute =
models[association.collection].attributes[association.via];
if (!relatedAttribute) {
throw new Error(
`The attribute \`${
association.via
}\` is missing in the model ${_.upperFirst(
association.collection
)} ${
association.plugin ? '(plugin - ' + association.plugin + ')' : ''
}`
);
}
types.current = 'collection';
if (
_.has(relatedAttribute, 'collection') &&
relatedAttribute.collection !== '*' &&
_.has(relatedAttribute, 'via')
) {
types.other = 'collection';
} else if (
_.has(relatedAttribute, 'collection') &&
relatedAttribute.collection !== '*' &&
!_.has(relatedAttribute, 'via')
) {
types.other = 'collectionD';
} else if (
_.has(relatedAttribute, 'model') &&
relatedAttribute.model !== '*'
) {
types.other = 'model';
} else if (
_.has(relatedAttribute, 'collection') ||
_.has(relatedAttribute, 'model')
) {
types.other = 'morphTo';
}
} else if (_.has(association, 'via') && _.has(association, 'model')) {
types.current = 'modelD';
// We have to find if they are a model linked to this key
const model = models[association.model];
const attribute = model.attributes[association.via];
if (
_.has(attribute, 'via') &&
attribute.via === key &&
_.has(attribute, 'collection') &&
attribute.collection !== '*'
) {
types.other = 'collection';
} else if (_.has(attribute, 'model') && attribute.model !== '*') {
types.other = 'model';
} else if (
_.has(attribute, 'collection') ||
_.has(attribute, 'model')
) {
types.other = 'morphTo';
}
} else if (_.has(association, 'model')) {
types.current = 'model';
// We have to find if they are a model linked to this key
_.forIn(models, model => {
_.forIn(model.attributes, attribute => {
if (_.has(attribute, 'via') && attribute.via === key) {
if (
_.has(attribute, 'collection') &&
attribute.collection === currentModelName
) {
types.other = 'collection';
// Break loop
return false;
} else if (
_.has(attribute, 'model') &&
attribute.model === currentModelName
) {
types.other = 'modelD';
// Break loop
return false;
}
}
});
});
} else if (_.has(association, 'collection')) {
types.current = 'collectionD';
// We have to find if they are a model linked to this key
_.forIn(models, model => {
_.forIn(model.attributes, attribute => {
if (_.has(attribute, 'via') && attribute.via === key) {
if (
_.has(attribute, 'collection') &&
attribute.collection === currentModelName
) {
types.other = 'collection';
// Break loop
return false;
} else if (
_.has(attribute, 'model') &&
attribute.model === currentModelName
) {
types.other = 'modelD';
// Break loop
return false;
}
}
});
});
types.other = 'collectionD';
} else if (_.has(relatedAttribute, 'model') && relatedAttribute.model !== '*') {
types.other = 'model';
} else if (_.has(relatedAttribute, 'collection') || _.has(relatedAttribute, 'model')) {
types.other = 'morphTo';
}
} else if (_.has(attribute, 'via') && _.has(attribute, 'model')) {
types.current = 'modelD';
if (types.current === 'collection' && types.other === 'morphTo') {
return {
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: 'oneToManyMorph',
verbose: 'morphOne',
};
} else if (types.current === 'modelD' && types.other === 'morphToD') {
return {
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' || _.has(association, 'model'))
// We have to find if they are a model linked to this attributeName
const model = models[attribute.model];
const reverseAttribute = model.attributes[attribute.via];
if (
_.has(reverseAttribute, 'via') &&
reverseAttribute.via === attributeName &&
_.has(reverseAttribute, 'collection') &&
reverseAttribute.collection !== '*'
) {
return {
nature: 'manyMorphToOne',
verbose: 'belongsToManyMorph',
};
} else if (
types.current === 'morphTo' &&
(types.other === 'collection' || _.has(association, 'collection'))
) {
return {
nature: 'manyMorphToMany',
verbose: 'belongsToManyMorph',
};
} else if (types.current === 'modelD' && types.other === 'model') {
return {
nature: 'oneToOne',
verbose: 'belongsTo',
};
} else if (types.current === 'model' && types.other === 'modelD') {
return {
nature: 'oneToOne',
verbose: 'hasOne',
};
} else if (
(types.current === 'model' || types.current === 'modelD') &&
types.other === 'collection'
) {
return {
nature: 'manyToOne',
verbose: 'belongsTo',
};
} else if (types.current === 'modelD' && types.other === 'collection') {
return {
nature: 'oneToMany',
verbose: 'hasMany',
};
} else if (types.current === 'collection' && types.other === 'model') {
return {
nature: 'oneToMany',
verbose: 'hasMany',
};
} else if (
types.current === 'collection' &&
types.other === 'collection'
) {
return {
nature: 'manyToMany',
verbose: 'belongsToMany',
};
} else if (
(types.current === 'collectionD' && types.other === 'collection') ||
(types.current === 'collection' && types.other === 'collectionD')
) {
return {
nature: 'manyToMany',
verbose: 'belongsToMany',
};
} else if (types.current === 'collectionD' && types.other === '') {
return {
nature: 'manyWay',
verbose: 'belongsToMany',
};
} else if (types.current === 'model' && types.other === '') {
return {
nature: 'oneWay',
verbose: 'belongsTo',
};
types.other = 'collection';
} else if (_.has(reverseAttribute, 'model') && reverseAttribute.model !== '*') {
types.other = 'model';
} else if (_.has(reverseAttribute, 'collection') || _.has(reverseAttribute, 'model')) {
types.other = 'morphTo';
}
} else if (_.has(attribute, 'model')) {
types.current = 'model';
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();
// We have to find if they are a model linked to this attributeName
_.forIn(models, model => {
_.forIn(model.attributes, attribute => {
if (_.has(attribute, 'via') && attribute.via === attributeName) {
if (_.has(attribute, 'collection') && attribute.collection === modelName) {
types.other = 'collection';
// Break loop
return false;
} else if (_.has(attribute, 'model') && attribute.model === modelName) {
types.other = 'modelD';
// Break loop
return false;
}
}
});
});
} else if (_.has(attribute, 'collection')) {
types.current = 'collectionD';
// We have to find if they are a model linked to this attributeName
_.forIn(models, model => {
_.forIn(model.attributes, attribute => {
if (_.has(attribute, 'via') && attribute.via === attributeName) {
if (_.has(attribute, 'collection') && attribute.collection === modelName) {
types.other = 'collection';
// Break loop
return false;
} else if (_.has(attribute, 'model') && attribute.model === modelName) {
types.other = 'modelD';
// Break loop
return false;
}
}
});
});
}
if (types.current === 'collection' && types.other === 'morphTo') {
return {
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: 'oneToManyMorph',
verbose: 'morphOne',
};
} else if (types.current === 'modelD' && types.other === 'morphToD') {
return {
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' || _.has(attribute, 'model'))
) {
return {
nature: 'manyMorphToOne',
verbose: 'belongsToManyMorph',
};
} else if (
types.current === 'morphTo' &&
(types.other === 'collection' || _.has(attribute, 'collection'))
) {
return {
nature: 'manyMorphToMany',
verbose: 'belongsToManyMorph',
};
} else if (types.current === 'modelD' && types.other === 'model') {
return {
nature: 'oneToOne',
verbose: 'belongsTo',
};
} else if (types.current === 'model' && types.other === 'modelD') {
return {
nature: 'oneToOne',
verbose: 'hasOne',
};
} else if (
(types.current === 'model' || types.current === 'modelD') &&
types.other === 'collection'
) {
return {
nature: 'manyToOne',
verbose: 'belongsTo',
};
} else if (types.current === 'modelD' && types.other === 'collection') {
return {
nature: 'oneToMany',
verbose: 'hasMany',
};
} else if (types.current === 'collection' && types.other === 'model') {
return {
nature: 'oneToMany',
verbose: 'hasMany',
};
} else if (types.current === 'collection' && types.other === 'collection') {
return {
nature: 'manyToMany',
verbose: 'belongsToMany',
};
} else if (
(types.current === 'collectionD' && types.other === 'collection') ||
(types.current === 'collection' && types.other === 'collectionD')
) {
return {
nature: 'manyToMany',
verbose: 'belongsToMany',
};
} else if (types.current === 'collectionD' && types.other === '') {
return {
nature: 'manyWay',
verbose: 'belongsToMany',
};
} else if (types.current === 'model' && types.other === '') {
return {
nature: 'oneWay',
verbose: 'belongsTo',
};
}
return undefined;
},
/**
@ -347,9 +288,7 @@ module.exports = {
return a.collection < b.collection ? -1 : 1;
})
.map(table =>
_.snakeCase(
`${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`
)
_.snakeCase(`${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`)
)
.join('__');
},
@ -373,32 +312,21 @@ module.exports = {
// Get relation nature
let details;
const targetName = association.model || association.collection || '';
const infos = this.getNature(
association,
key,
undefined,
model.toLowerCase()
);
const infos = this.getNature({
attribute: association,
attributeName: key,
modelName: model.toLowerCase(),
});
if (targetName !== '*') {
if (association.plugin) {
details = _.get(
strapi.plugins,
[
association.plugin,
'models',
targetName,
'attributes',
association.via,
],
[association.plugin, 'models', targetName, 'attributes', association.via],
{}
);
} else {
details = _.get(
strapi.models,
[targetName, 'attributes', association.via],
{}
);
details = _.get(strapi.models, [targetName, 'attributes', association.via], {});
}
}
@ -418,14 +346,11 @@ module.exports = {
if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') {
ast.tableCollectionName =
_.get(association, 'collectionName') ||
this.getCollectionName(details, association);
_.get(association, 'collectionName') || this.getCollectionName(details, association);
}
if (infos.nature === 'manyWay' && definition.orm === 'bookshelf') {
ast.tableCollectionName = `${
definition.collectionName
}__${_.snakeCase(key)}`;
ast.tableCollectionName = `${definition.collectionName}__${_.snakeCase(key)}`;
}
definition.associations.push(ast);
@ -447,37 +372,25 @@ module.exports = {
return;
}
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];
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()
) {
acc.push(strapi.plugins[current].models[entity].globalId);
}
});
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
acc.push(strapi.plugins[current].models[entity].globalId);
}
});
});
return acc;
},
[]
);
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()
) {
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
acc.push(strapi.models[entity].globalId);
}
});
@ -485,29 +398,19 @@ module.exports = {
return acc;
}, []);
const componentModels = Object.keys(strapi.components).reduce(
(acc, entity) => {
Object.keys(strapi.components[entity].attributes).forEach(
attribute => {
const attr = strapi.components[entity].attributes[attribute];
const componentModels = Object.keys(strapi.components).reduce((acc, entity) => {
Object.keys(strapi.components[entity].attributes).forEach(attribute => {
const attr = strapi.components[entity].attributes[attribute];
if (
(attr.collection || attr.model || '').toLowerCase() ===
model.toLowerCase()
) {
acc.push(strapi.components[entity].globalId);
}
}
);
if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) {
acc.push(strapi.components[entity].globalId);
}
});
return acc;
},
[]
);
return acc;
}, []);
const models = _.uniq(
appModels.concat(pluginsModels).concat(componentModels)
);
const models = _.uniq(appModels.concat(pluginsModels).concat(componentModels));
definition.associations.push({
alias: key,
@ -519,9 +422,7 @@ module.exports = {
});
} catch (e) {
strapi.log.error(
`Something went wrong in the model \`${_.upperFirst(
model
)}\` with the attribute \`${key}\``
`Something went wrong in the model \`${_.upperFirst(model)}\` with the attribute \`${key}\``
);
strapi.log.error(e);
strapi.stop();
@ -559,9 +460,7 @@ module.exports = {
const connector = models[model].orm;
if (!connector) {
throw new Error(
`Impossible to determine the ORM used for the model ${model}.`
);
throw new Error(`Impossible to determine the ORM used for the model ${model}.`);
}
const convertor = strapi.db.connectors.get(connector).getQueryParams;
@ -614,17 +513,7 @@ module.exports = {
if (
_.includes(
[
'ne',
'lt',
'gt',
'lte',
'gte',
'contains',
'containss',
'in',
'nin',
],
['ne', 'lt', 'gt', 'lte', 'gte', 'contains', 'containss', 'in', 'nin'],
_.last(suffix)
)
) {

View File

@ -289,7 +289,9 @@ class Strapi extends EventEmitter {
stop(exitCode = 1) {
// Destroy server and available connections.
this.server.destroy();
if (_.has(this, 'server.destroy')) {
this.server.destroy();
}
if (this.config.autoReload) {
process.send('stop');