Make polymorphic associations possible

This commit is contained in:
Aurelsicoko 2018-02-12 18:54:34 +01:00
parent 824567de0d
commit 46ff89e404
3 changed files with 152 additions and 60 deletions

View File

@ -80,6 +80,7 @@ module.exports = function (strapi) {
// Initialize lifecycle callbacks. // Initialize lifecycle callbacks.
const preLifecycle = { const preLifecycle = {
validate: 'beforeCreate', validate: 'beforeCreate',
findOneAndUpdate: 'beforeUpdate',
findOneAndRemove: 'beforeDestroy', findOneAndRemove: 'beforeDestroy',
remove: 'beforeDestroy', remove: 'beforeDestroy',
update: 'beforeUpdate', update: 'beforeUpdate',
@ -88,6 +89,36 @@ module.exports = function (strapi) {
save: 'beforeSave' save: 'beforeSave'
}; };
/*
Override populate path for polymorphic association.
It allows us to make Upload.find().populate('related')
instead of Upload.find().populate('related.item')
*/
const morphAssociations = definition.associations.filter(association => association.nature.toLowerCase().indexOf('morph') !== -1);
if (morphAssociations.length > 0) {
morphAssociations.forEach(association => {
Object.keys(preLifecycle)
.filter(key => key.indexOf('find') !== -1)
.forEach(key => {
collection.schema.pre(key, function (next) {
if (this._mongooseOptions.populate && this._mongooseOptions.populate[association.alias]) {
if (association.nature === 'oneToMorph' || association.nature === 'manyToMorph') {
this._mongooseOptions.populate[association.alias].match = {
[`${association.via}.${association.where}`]: association.alias
}
} else {
this._mongooseOptions.populate[association.alias].path = `${association.alias}.${association.key}`;
}
}
next();
});
});
});
}
_.forEach(preLifecycle, (fn, key) => { _.forEach(preLifecycle, (fn, key) => {
if (_.isFunction(target[model.toLowerCase()][fn])) { if (_.isFunction(target[model.toLowerCase()][fn])) {
collection.schema.pre(key, function (next) { collection.schema.pre(key, function (next) {
@ -126,29 +157,21 @@ module.exports = function (strapi) {
}); });
}); });
collection.schema.set('toObject', { collection.schema.options.toObject = collection.schema.options.toJSON = {
virtuals: true virtuals: true,
}); transform: function (doc, returned, opts) {
morphAssociations.forEach(association => {
collection.schema.set('toJSON', { if (Array.isArray(returned[association.alias]) && returned[association.alias].length > 0) {
virtuals: true returned[association.alias] = returned[association.alias].map(o => o[association.key]);
}); }
});
}
};
if (!plugin) { if (!plugin) {
// Enhance schema with polymorphic relationships. global[definition.globalName] = instance.model(definition.globalId, collection.schema, definition.collectionName);
const morphs = _.pickBy(definition.loadedModel, model => {
return model.type === 'discriminator';
});
if (_.size(morphs) > 0) {
const morph = morphs[Object.keys(morphs)[0]];
global[definition.globalName] = global[morph.ref].discriminator(morph.discriminator, collection.schema);
} else {
global[definition.globalName] = instance.model(definition.globalName, collection.schema, definition.collectionName);
}
} else { } else {
instance.model(definition.globalName, collection.schema, definition.collectionName); instance.model(definition.globalId, collection.schema, definition.collectionName);
} }
// Expose ORM functions through the `target` object. // Expose ORM functions through the `target` object.
@ -206,21 +229,10 @@ module.exports = function (strapi) {
// Call this callback function after we are done parsing // Call this callback function after we are done parsing
// all attributes for relationships-- see below. // all attributes for relationships-- see below.
const done = _.after(_.size(definition.attributes), () => { const done = _.after(_.size(definition.attributes), () => {
// Extract discriminator (morphTo side) // Generate schema without virtual populate
const discriminators = _.pickBy(definition.loadedModel, model => { const schema = new instance.Schema(_.omitBy(definition.loadedModel, model => {
return model.hasOwnProperty('discriminatorKey'); return model.type === 'virtual';
}); }));
const options = {};
if (_.size(discriminators) > 0) {
options.discriminatorKey = 'type';
}
// Generate schema without virtual populate & discriminators.
let schema = new instance.Schema(_.omitBy(definition.loadedModel, model => {
return model.type === 'virtual' || model.type === 'discriminator' || model.hasOwnProperty('discriminatorKey');
}), options);
_.set(strapi.config.hook.settings.mongoose, 'collections.' + mongooseUtils.toCollectionName(definition.globalName) + '.schema', schema); _.set(strapi.config.hook.settings.mongoose, 'collections.' + mongooseUtils.toCollectionName(definition.globalName) + '.schema', schema);
@ -275,7 +287,7 @@ module.exports = function (strapi) {
const FK = _.find(definition.associations, {alias: name}); const FK = _.find(definition.associations, {alias: name});
const ref = details.plugin ? strapi.plugins[details.plugin].models[details.model].globalId : strapi.models[details.model].globalId; const ref = details.plugin ? strapi.plugins[details.plugin].models[details.model].globalId : strapi.models[details.model].globalId;
if (FK && FK.nature !== 'oneToOne' && FK.nature !== 'manyToOne' && FK.nature !== 'oneWay') { if (FK && FK.nature !== 'oneToOne' && FK.nature !== 'manyToOne' && FK.nature !== 'oneWay' && FK.nature !== 'oneToMorph') {
definition.loadedModel[name] = { definition.loadedModel[name] = {
type: 'virtual', type: 'virtual',
ref, ref,
@ -283,6 +295,20 @@ module.exports = function (strapi) {
justOne: true justOne: true
}; };
// Set this info to be able to see if this field is a real database's field.
details.isVirtual = true;
} else if (FK.nature === 'oneToMorph') {
const key = details.plugin ?
strapi.plugins[details.plugin].models[details.model].attributes[details.via].key:
strapi.models[details.model].attributes[details.via].key;
definition.loadedModel[name] = {
type: 'virtual',
ref,
via: `${FK.via}.${key}`,
justOne: true
};
// Set this info to be able to see if this field is a real database's field. // Set this info to be able to see if this field is a real database's field.
details.isVirtual = true; details.isVirtual = true;
} else { } else {
@ -299,13 +325,26 @@ module.exports = function (strapi) {
const ref = details.plugin ? strapi.plugins[details.plugin].models[details.collection].globalId : strapi.models[details.collection].globalId; const ref = details.plugin ? strapi.plugins[details.plugin].models[details.collection].globalId : strapi.models[details.collection].globalId;
// One-side of the relationship has to be a virtual field to be bidirectional. // One-side of the relationship has to be a virtual field to be bidirectional.
if ((FK && _.isUndefined(FK.via)) || details.dominant !== true) { if ((FK && _.isUndefined(FK.via)) || details.dominant !== true && FK.nature !== 'manyToMorph') {
definition.loadedModel[name] = { definition.loadedModel[name] = {
type: 'virtual', type: 'virtual',
ref, ref,
via: FK.via via: FK.via
}; };
// Set this info to be able to see if this field is a real database's field.
details.isVirtual = true;
} else if (FK.nature === 'manyToMorph') {
const key = details.plugin ?
strapi.plugins[details.plugin].models[details.collection].attributes[details.via].key:
strapi.models[details.collection].attributes[details.via].key;
definition.loadedModel[name] = {
type: 'virtual',
ref,
via: `${FK.via}.${key}`
};
// Set this info to be able to see if this field is a real database's field. // Set this info to be able to see if this field is a real database's field.
details.isVirtual = true; details.isVirtual = true;
} else { } else {
@ -316,21 +355,27 @@ module.exports = function (strapi) {
} }
break; break;
} }
case 'morphOne': { case 'belongsToMorph': {
const discriminator = plugin ? `${plugin}_${model.toLowerCase()}` : model.toLowerCase();
const ref = details.plugin ? strapi.plugins[details.plugin].models[details.model].globalId : strapi.models[details.model].globalId;
definition.loadedModel[name] = { definition.loadedModel[name] = {
type: 'discriminator', kind: String,
ref, [details.where]: String,
discriminator [details.key]: {
} type: instance.Schema.Types.ObjectId,
refPath: `${name}.kind`
}
};
break; break;
} }
case 'morphTo': { case 'belongsToManyMorph': {
definition.loadedModel[name] = { definition.loadedModel[name] = [{
discriminatorKey: 'kind' kind: String,
}; [details.where]: String,
[details.key]: {
type: instance.Schema.Types.ObjectId,
refPath: `${name}.kind`
}
}];
break;
} }
default: default:
break; break;

View File

@ -16,7 +16,7 @@
"main": "./lib", "main": "./lib",
"dependencies": { "dependencies": {
"lodash": "^4.17.4", "lodash": "^4.17.4",
"mongoose": "^5.0.0-rc1", "mongoose": "^5.0.4",
"mongoose-float": "^1.0.2", "mongoose-float": "^1.0.2",
"pluralize": "^6.0.0", "pluralize": "^6.0.0",
"strapi-utils": "3.0.0-alpha.9.3" "strapi-utils": "3.0.0-alpha.9.3"

View File

@ -114,7 +114,10 @@ module.exports = {
// Break loop // Break loop
return false; return false;
} else if (attribute.hasOwnProperty('key')) { } else if (attribute.hasOwnProperty('key')) {
types.other = 'morphTo'; // MorphTo types.other = 'morphTo';
// Break loop
return false;
} }
}); });
}); });
@ -162,22 +165,56 @@ module.exports = {
}); });
} else if (association.hasOwnProperty('key')) { } else if (association.hasOwnProperty('key')) {
types.current = 'morphTo'; 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 (attribute.hasOwnProperty('via') && attribute.via === key) {
if (attribute.hasOwnProperty('collection')) {
types.other = 'collection';
// Break loop
return false;
} else if (attribute.hasOwnProperty('model')) {
types.other = 'model';
// Break loop
return false;
}
}
});
});
} }
if (types.current === 'morphTo') { if (types.current === 'collection' && types.other === 'morphTo') {
return { return {
nature: 'morphTo', nature: 'manyToMorph',
verbose: 'morphTo' verbose: 'belongsToMany'
}; };
} else if (types.current === 'modelD' && types.other === 'morphTo') { } else if (types.current === 'modelD' && types.other === 'morphTo') {
return { return {
nature: 'oneToOne', nature: 'oneToMorph',
verbose: 'morphOne' verbose: 'belongsTo'
}; };
} else if (types.current === 'collection' && types.other === 'morphTo') { } else if (types.current === 'morphTo' && types.other === 'collection') {
return { return {
nature: 'oneToMany', nature: 'morphToMany',
verbose: 'morphMany' verbose: 'belongsToMorph'
};
} else if (types.current === 'morphTo' && types.other === 'model') {
return {
nature: 'morphToOne',
verbose: 'belongsToManyMorph'
}; };
} else if (types.current === 'modelD' && types.other === 'model') { } else if (types.current === 'modelD' && types.other === 'model') {
return { return {
@ -253,7 +290,7 @@ module.exports = {
} }
// Exclude non-relational attribute // Exclude non-relational attribute
if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model')) { if (!association.hasOwnProperty('collection') && !association.hasOwnProperty('model') && !association.hasOwnProperty('key')) {
return undefined; return undefined;
} }
@ -272,6 +309,7 @@ module.exports = {
autoPopulate: _.get(association, 'autoPopulate', true), autoPopulate: _.get(association, 'autoPopulate', true),
dominant: details.dominant !== true, dominant: details.dominant !== true,
plugin: association.plugin || undefined, plugin: association.plugin || undefined,
where: details.where,
}); });
} else if (association.hasOwnProperty('model')) { } else if (association.hasOwnProperty('model')) {
definition.associations.push({ definition.associations.push({
@ -283,6 +321,15 @@ module.exports = {
autoPopulate: _.get(association, 'autoPopulate', true), autoPopulate: _.get(association, 'autoPopulate', true),
dominant: details.dominant !== true, dominant: details.dominant !== true,
plugin: association.plugin || undefined, plugin: association.plugin || undefined,
where: details.where,
});
} else if (association.hasOwnProperty('key')) {
definition.associations.push({
alias: key,
type: 'collection',
nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true),
key: association.key,
}); });
} }
} catch (e) { } catch (e) {