diff --git a/packages/strapi-hook-bookshelf/lib/mount-models.js b/packages/strapi-hook-bookshelf/lib/mount-models.js index 98e6ea9d6a..b2e4c0fd76 100644 --- a/packages/strapi-hook-bookshelf/lib/mount-models.js +++ b/packages/strapi-hook-bookshelf/lib/mount-models.js @@ -119,7 +119,8 @@ module.exports = ({ models, target, plugin = false }, ctx) => { // Add every relationships to the loaded model for Bookshelf. // Basic attributes don't need this-- only relations. - _.forEach(definition.attributes, (details, name) => { + Object.keys(definition.attributes).forEach(name => { + const details = definition.attributes[name]; if (details.type !== undefined) { return; } @@ -433,7 +434,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => { attrs[key] = definition.attributes[key].repeatable === true ? groups - : _.first(groups); + : _.first(groups) || null; } }); diff --git a/packages/strapi-hook-mongoose/lib/index.js b/packages/strapi-hook-mongoose/lib/index.js index efbb179a97..dd65ab9f3d 100644 --- a/packages/strapi-hook-mongoose/lib/index.js +++ b/packages/strapi-hook-mongoose/lib/index.js @@ -43,6 +43,7 @@ module.exports = function(strapi) { .map(async connectionName => { const connection = connections[connectionName]; const instance = new Mongoose(); + _.defaults(connection.settings, strapi.config.hook.settings.mongoose); const { @@ -65,9 +66,6 @@ module.exports = function(strapi) { // Connect to mongo database const connectOptions = {}; - const options = { - useFindAndModify: false, - }; if (!_.isEmpty(username)) { connectOptions.user = username; @@ -86,8 +84,6 @@ module.exports = function(strapi) { connectOptions.dbName = database; connectOptions.useCreateIndex = true; - options.debug = debug === true || debug === 'true'; - try { /* FIXME: for now, mongoose doesn't support srv auth except the way including user/pass in URI. * https://github.com/Automattic/mongoose/issues/6881 */ @@ -117,7 +113,8 @@ module.exports = function(strapi) { require(initFunctionPath)(instance, connection); } - Object.keys(options, key => instance.set(key, options[key])); + instance.set('debug', debug === true || debug === 'true'); + instance.set('useFindAndModify', false); const ctx = { instance, diff --git a/packages/strapi-hook-mongoose/lib/mount-models.js b/packages/strapi-hook-mongoose/lib/mount-models.js index 5551a48f42..e87270feae 100644 --- a/packages/strapi-hook-mongoose/lib/mount-models.js +++ b/packages/strapi-hook-mongoose/lib/mount-models.js @@ -2,525 +2,534 @@ const _ = require('lodash'); const mongoose = require('mongoose'); -const mongooseUtils = require('mongoose/lib/utils'); const utilsModels = require('strapi-utils').models; -const utils = require('./utils/'); +const utils = require('./utils'); const relations = require('./relations'); module.exports = ({ models, target, plugin = false }, ctx) => { const { instance } = ctx; - const loadedAttributes = _.after(_.size(models), () => { - _.forEach(models, (definition, model) => { - try { - let collection = - strapi.config.hook.settings.mongoose.collections[ - mongooseUtils.toCollectionName(definition.globalName) - ]; - - // Set the default values to model settings. - _.defaults(definition, { - primaryKey: '_id', - }); - - // Initialize lifecycle callbacks. - const preLifecycle = { - validate: 'beforeCreate', - findOneAndUpdate: 'beforeUpdate', - findOneAndRemove: 'beforeDestroy', - remove: 'beforeDestroy', - update: 'beforeUpdate', - updateOne: 'beforeUpdate', - find: 'beforeFetchAll', - findOne: 'beforeFetch', - 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 === 'oneToManyMorph' || - association.nature === 'manyToManyMorph' - ) { - this._mongooseOptions.populate[ - association.alias - ].match = { - [`${association.via}.${association.filter}`]: association.alias, - [`${association.via}.kind`]: definition.globalId, - }; - - // Select last related to an entity. - this._mongooseOptions.populate[ - association.alias - ].options = { - sort: '-createdAt', - }; - } else { - this._mongooseOptions.populate[ - association.alias - ].path = `${association.alias}.ref`; - } - } else { - if (!this._mongooseOptions.populate) { - this._mongooseOptions.populate = {}; - } - - // Images are not displayed in populated data. - // We automatically populate morph relations. - if ( - association.nature === 'oneToManyMorph' || - association.nature === 'manyToManyMorph' - ) { - this._mongooseOptions.populate[association.alias] = { - path: association.alias, - match: { - [`${association.via}.${association.filter}`]: association.alias, - [`${association.via}.kind`]: definition.globalId, - }, - options: { - sort: '-createdAt', - }, - select: undefined, - model: undefined, - _docs: {}, - }; - } - } - next(); - }); - }); - }); - } - - _.forEach(preLifecycle, (fn, key) => { - if (_.isFunction(target[model.toLowerCase()][fn])) { - collection.schema.pre(key, function(next) { - target[model.toLowerCase()] - [fn](this) - .then(next) - .catch(err => strapi.log.error(err)); - }); - } - }); - - const postLifecycle = { - validate: 'afterCreate', - findOneAndRemove: 'afterDestroy', - remove: 'afterDestroy', - update: 'afterUpdate', - updateOne: 'afterUpdate', - find: 'afterFetchAll', - findOne: 'afterFetch', - save: 'afterSave', - }; - - // Mongoose doesn't allow post 'remove' event on model. - // See https://github.com/Automattic/mongoose/issues/3054 - _.forEach(postLifecycle, (fn, key) => { - if (_.isFunction(target[model.toLowerCase()][fn])) { - collection.schema.post(key, function(doc, next) { - target[model.toLowerCase()] - [fn](this, doc) - .then(next) - .catch(err => { - strapi.log.error(err); - next(err); - }); - }); - } - }); - - // Add virtual key to provide populate and reverse populate - _.forEach( - _.pickBy(definition.loadedModel, model => { - return model.type === 'virtual'; - }), - (value, key) => { - collection.schema.virtual(key.replace('_v', ''), { - ref: value.ref, - localField: '_id', - foreignField: value.via, - justOne: value.justOne || false, - }); - } - ); - - // Use provided timestamps if the elemnets in the array are string else use default. - if (_.isArray(_.get(definition, 'options.timestamps'))) { - const timestamps = { - createdAt: _.isString(_.get(definition, 'options.timestamps[0]')) - ? _.get(definition, 'options.timestamps[0]') - : 'createdAt', - updatedAt: _.isString(_.get(definition, 'options.timestamps[1]')) - ? _.get(definition, 'options.timestamps[1]') - : 'updatedAt', - }; - collection.schema.set('timestamps', timestamps); - } else { - collection.schema.set( - 'timestamps', - _.get(definition, 'options.timestamps') === true - ); - _.set( - definition, - 'options.timestamps', - _.get(definition, 'options.timestamps') === true - ? ['createdAt', 'updatedAt'] - : false - ); - } - collection.schema.set( - 'minimize', - _.get(definition, 'options.minimize', false) === true - ); - - // Save all attributes (with timestamps) - target[model].allAttributes = _.clone(definition.attributes); - - collection.schema.options.toObject = collection.schema.options.toJSON = { - virtuals: true, - transform: function(doc, returned) { - // Remover $numberDecimal nested property. - Object.keys(returned) - .filter(key => returned[key] instanceof mongoose.Types.Decimal128) - .forEach(key => { - // Parse to float number. - returned[key] = parseFloat(returned[key].toString()); - }); - - morphAssociations.forEach(association => { - if ( - Array.isArray(returned[association.alias]) && - returned[association.alias].length > 0 - ) { - // Reformat data by bypassing the many-to-many relationship. - switch (association.nature) { - case 'oneMorphToOne': - returned[association.alias] = - returned[association.alias][0].ref; - break; - case 'manyMorphToMany': - case 'manyMorphToOne': - returned[association.alias] = returned[ - association.alias - ].map(obj => obj.ref); - break; - default: - } - } - }); - }, - }; - - // Instantiate model. - const Model = instance.model( - definition.globalId, - collection.schema, - definition.collectionName - ); - - if (!plugin) { - global[definition.globalName] = Model; - } - - // Expose ORM functions through the `target` object. - target[model] = _.assign(Model, target[model]); - - // Push attributes to be aware of model schema. - target[model]._attributes = definition.attributes; - target[model].updateRelations = relations.update; - } catch (err) { - strapi.log.error('Impossible to register the `' + model + '` model.'); - strapi.log.error(err); - strapi.stop(); - } - }); - }); - // Parse every authenticated model. - _.forEach(models, (definition, model) => { - definition.globalName = _.upperFirst(_.camelCase(definition.globalId)); - - // Make sure the model has a connection. - // If not, use the default connection. - if (_.isEmpty(definition.connection)) { - definition.connection = - strapi.config.currentEnvironment.database.defaultConnection; - } - - // Make sure this connection exists. - if (!_.has(strapi.config.connections, definition.connection)) { - strapi.log.error( - 'The connection `' + - definition.connection + - '` specified in the `' + - model + - '` model does not exist.' - ); - strapi.stop(); - } - - // Add some informations about ORM & client connection + Object.keys(models).map(model => { + const definition = models[model]; definition.orm = 'mongoose'; - definition.client = _.get( - strapi.config.connections[definition.connection], - 'client' - ); definition.associations = []; + definition.globalName = _.upperFirst(_.camelCase(definition.globalId)); + definition.loadedModel = {}; + // Set the default values to model settings. + _.defaults(definition, { + primaryKey: '_id', + }); - // Register the final model for Mongoose. - definition.loadedModel = _.cloneDeep(definition.attributes); - - // Initialize the global variable with the - // capitalized model name. if (!plugin) { global[definition.globalName] = {}; } - if (_.isEmpty(definition.attributes)) { - // Generate empty schema - _.set( - strapi.config.hook.settings.mongoose, - 'collections.' + - mongooseUtils.toCollectionName(definition.globalName) + - '.schema', - new instance.Schema({}) - ); + const groupAttributes = Object.keys(definition.attributes).filter( + key => definition.attributes[key].type === 'group' + ); - return loadedAttributes(); - } - - // Call this callback function after we are done parsing - // all attributes for relationships-- see below. - const done = _.after(_.size(definition.attributes), () => { - // Generate schema without virtual populate - const schema = new instance.Schema( - _.omitBy(definition.loadedModel, model => { - return model.type === 'virtual'; - }) - ); - - _.set( - strapi.config.hook.settings.mongoose, - 'collections.' + - mongooseUtils.toCollectionName(definition.globalName) + - '.schema', - schema - ); - - loadedAttributes(); + const scalarAttributes = Object.keys(definition.attributes).filter(key => { + const { type } = definition.attributes[key]; + return type !== undefined && type !== null && type !== 'group'; }); - // Add every relationships to the loaded model for Bookshelf. - // Basic attributes don't need this-- only relations. - _.forEach(definition.attributes, (details, name) => { - const verbose = - _.get( - utilsModels.getNature(details, name, undefined, model.toLowerCase()), - 'verbose' - ) || ''; - - // Build associations key - utilsModels.defineAssociations( - model.toLowerCase(), - definition, - details, - name - ); - - if (_.isEmpty(verbose)) { - definition.loadedModel[name].type = utils(instance).convertType( - details.type - ); + const relationalAttributes = Object.keys(definition.attributes).filter( + key => { + const { type } = definition.attributes[key]; + return type === undefined; } + ); - switch (verbose) { - case 'hasOne': { - const ref = details.plugin - ? strapi.plugins[details.plugin].models[details.model].globalId - : strapi.models[details.model].globalId; - - definition.loadedModel[name] = { - type: instance.Schema.Types.ObjectId, - ref, - }; - break; - } - case 'hasMany': { - const FK = _.find(definition.associations, { - alias: name, - }); - const ref = details.plugin - ? strapi.plugins[details.plugin].models[details.collection].globalId - : strapi.models[details.collection].globalId; - - if (FK) { - definition.loadedModel[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. - details.isVirtual = true; - } else { - definition.loadedModel[name] = [ - { - type: instance.Schema.Types.ObjectId, - ref, - }, - ]; - } - break; - } - case 'belongsTo': { - const FK = _.find(definition.associations, { - alias: name, - }); - 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' && - FK.nature !== 'oneToMorph' - ) { - definition.loadedModel[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. - details.isVirtual = true; - } else { - definition.loadedModel[name] = { - type: instance.Schema.Types.ObjectId, - ref, - }; - } - - break; - } - case 'belongsToMany': { - const FK = _.find(definition.associations, { - alias: name, - }); - 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. - if ((FK && _.isUndefined(FK.via)) || details.dominant !== true) { - definition.loadedModel[name] = { - type: 'virtual', - ref, - via: FK.via, - }; - - // Set this info to be able to see if this field is a real database's field. - details.isVirtual = true; - } else { - definition.loadedModel[name] = [ - { - type: instance.Schema.Types.ObjectId, - ref, - }, - ]; - } - break; - } - case 'morphOne': { - const FK = _.find(definition.associations, { - alias: name, - }); - const ref = details.plugin - ? strapi.plugins[details.plugin].models[details.model].globalId - : strapi.models[details.model].globalId; - - definition.loadedModel[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. - details.isVirtual = true; - break; - } - case 'morphMany': { - const FK = _.find(definition.associations, { - alias: name, - }); - const ref = details.plugin - ? strapi.plugins[details.plugin].models[details.collection].globalId - : strapi.models[details.collection].globalId; - - definition.loadedModel[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. - details.isVirtual = true; - break; - } - case 'belongsToMorph': { - definition.loadedModel[name] = { + // handle gorup attrs + if (groupAttributes.length > 0) { + // create join morph collection thingy + groupAttributes.forEach(name => { + definition.loadedModel[name] = [ + { kind: String, - [details.filter]: String, ref: { - type: instance.Schema.Types.ObjectId, + type: mongoose.Schema.Types.ObjectId, refPath: `${name}.kind`, }, - }; - break; - } - case 'belongsToManyMorph': { - definition.loadedModel[name] = [ - { - kind: String, - [details.filter]: String, - ref: { - type: instance.Schema.Types.ObjectId, - refPath: `${name}.kind`, - }, - }, - ]; - break; - } - default: - break; - } + }, + ]; + }); + } - done(); + // handle scalar attrs + scalarAttributes.forEach(name => { + const attr = definition.attributes[name]; + + definition.loadedModel[name] = { + ...attr, + type: utils(instance).convertType(attr.type), + }; }); + + // handle relational attrs + relationalAttributes.forEach(name => { + buildRelation({ + definition, + model, + instance, + name, + attribute: definition.attributes[name], + }); + }); + + const schema = new instance.Schema( + _.omitBy(definition.loadedModel, ({ type }) => type === 'virtual') + ); + + // Initialize lifecycle callbacks. + const preLifecycle = { + validate: 'beforeCreate', + find: 'beforeFetchAll', + findOne: 'beforeFetch', + findOneAndUpdate: 'beforeUpdate', + findOneAndRemove: 'beforeDestroy', + remove: 'beforeDestroy', + update: 'beforeUpdate', + updateOne: 'beforeUpdate', + save: 'beforeSave', + }; + + const findLifecycles = [ + 'find', + 'findOne', + 'findOneAndUpdate', + 'findOneAndRemove', + ]; + + /* + 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 + ); + + const populateFn = createOnFetchPopulateFn({ + groupAttributes, + morphAssociations, + definition, + }); + + findLifecycles.forEach(key => { + schema.pre(key, populateFn); + }); + + Object.keys(preLifecycle).forEach(key => { + const fn = preLifecycle[key]; + + if (_.isFunction(target[model.toLowerCase()][fn])) { + schema.pre(key, function(next) { + target[model.toLowerCase()] + [fn](this) + .then(next) + .catch(err => strapi.log.error(err)); + }); + } + }); + + const postLifecycle = { + validate: 'afterCreate', + findOneAndRemove: 'afterDestroy', + remove: 'afterDestroy', + update: 'afterUpdate', + updateOne: 'afterUpdate', + find: 'afterFetchAll', + findOne: 'afterFetch', + save: 'afterSave', + }; + + // Mongoose doesn't allow post 'remove' event on model. + // See https://github.com/Automattic/mongoose/issues/3054 + Object.keys(postLifecycle).forEach(key => { + const fn = postLifecycle[key]; + + if (_.isFunction(target[model.toLowerCase()][fn])) { + schema.post(key, function(doc, next) { + target[model.toLowerCase()] + [fn](this, doc) + .then(next) + .catch(err => { + strapi.log.error(err); + next(err); + }); + }); + } + }); + + // Add virtual key to provide populate and reverse populate + _.forEach( + _.pickBy(definition.loadedModel, model => { + return model.type === 'virtual'; + }), + (value, key) => { + schema.virtual(key.replace('_v', ''), { + ref: value.ref, + localField: '_id', + foreignField: value.via, + justOne: value.justOne || false, + }); + } + ); + + // Use provided timestamps if the elemnets in the array are string else use default. + if (_.isArray(_.get(definition, 'options.timestamps'))) { + const timestamps = { + createdAt: _.isString(_.get(definition, 'options.timestamps[0]')) + ? _.get(definition, 'options.timestamps[0]') + : 'createdAt', + updatedAt: _.isString(_.get(definition, 'options.timestamps[1]')) + ? _.get(definition, 'options.timestamps[1]') + : 'updatedAt', + }; + schema.set('timestamps', timestamps); + } else { + schema.set( + 'timestamps', + _.get(definition, 'options.timestamps') === true + ); + _.set( + definition, + 'options.timestamps', + _.get(definition, 'options.timestamps') === true + ? ['createdAt', 'updatedAt'] + : false + ); + } + schema.set( + 'minimize', + _.get(definition, 'options.minimize', false) === true + ); + + // Save all attributes (with timestamps) + target[model].allAttributes = _.clone(definition.attributes); + + schema.options.toObject = schema.options.toJSON = { + virtuals: true, + transform: function(doc, returned) { + // Remover $numberDecimal nested property. + Object.keys(returned) + .filter(key => returned[key] instanceof mongoose.Types.Decimal128) + .forEach(key => { + // Parse to float number. + returned[key] = parseFloat(returned[key].toString()); + }); + + morphAssociations.forEach(association => { + if ( + Array.isArray(returned[association.alias]) && + returned[association.alias].length > 0 + ) { + // Reformat data by bypassing the many-to-many relationship. + switch (association.nature) { + case 'oneMorphToOne': + returned[association.alias] = + returned[association.alias][0].ref; + break; + case 'manyMorphToMany': + case 'manyMorphToOne': + returned[association.alias] = returned[association.alias].map( + obj => obj.ref + ); + break; + default: + } + } + }); + + groupAttributes.forEach(name => { + const attribute = definition.attributes[name]; + + if (Array.isArray(returned[name])) { + const groups = returned[name].map(el => el.ref); + // Reformat data by bypassing the many-to-many relationship. + returned[name] = + attribute.repeatable === true ? groups : _.first(groups) || null; + } + }); + }, + }; + + // Instantiate model. + const Model = instance.model( + definition.globalId, + schema, + definition.collectionName + ); + + if (!plugin) { + global[definition.globalName] = Model; + } + + // Expose ORM functions through the `target` object. + target[model] = _.assign(Model, target[model]); + + // Push attributes to be aware of model schema. + target[model]._attributes = definition.attributes; + target[model].updateRelations = relations.update; }); }; + +const createOnFetchPopulateFn = ({ + morphAssociations, + groupAttributes, + definition, +}) => { + return function(next) { + morphAssociations.forEach(association => { + if ( + this._mongooseOptions.populate && + this._mongooseOptions.populate[association.alias] + ) { + if ( + association.nature === 'oneToManyMorph' || + association.nature === 'manyToManyMorph' + ) { + this._mongooseOptions.populate[association.alias].match = { + [`${association.via}.${association.filter}`]: association.alias, + [`${association.via}.kind`]: definition.globalId, + }; + + // Select last related to an entity. + this._mongooseOptions.populate[association.alias].options = { + sort: '-createdAt', + }; + } else { + this._mongooseOptions.populate[ + association.alias + ].path = `${association.alias}.ref`; + } + } else { + if (!this._mongooseOptions.populate) { + this._mongooseOptions.populate = {}; + } + // Images are not displayed in populated data. + // We automatically populate morph relations. + if ( + association.nature === 'oneToManyMorph' || + association.nature === 'manyToManyMorph' + ) { + this._mongooseOptions.populate[association.alias] = { + path: association.alias, + match: { + [`${association.via}.${association.filter}`]: association.alias, + [`${association.via}.kind`]: definition.globalId, + }, + options: { + sort: '-createdAt', + }, + select: undefined, + model: undefined, + _docs: {}, + }; + } + } + }); + + groupAttributes.forEach(name => { + if ( + this._mongooseOptions.populate && + this._mongooseOptions.populate[name] + ) { + this._mongooseOptions.populate[name].path = `${name}.ref`; + } else { + this._mongooseOptions.populate[name] = { + path: `${name}.ref`, + _docs: {}, + }; + } + }); + + next(); + }; +}; + +const buildRelation = ({ definition, model, instance, attribute, name }) => { + const verbose = + _.get( + utilsModels.getNature(attribute, name, undefined, model.toLowerCase()), + 'verbose' + ) || ''; + + // Build associations key + utilsModels.defineAssociations( + model.toLowerCase(), + definition, + attribute, + name + ); + + switch (verbose) { + case 'hasOne': { + const ref = attribute.plugin + ? strapi.plugins[attribute.plugin].models[attribute.model].globalId + : strapi.models[attribute.model].globalId; + + 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; + + if (FK) { + definition.loadedModel[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, + }, + ]; + } + break; + } + case 'belongsTo': { + const FK = _.find(definition.associations, { + alias: name, + }); + const ref = attribute.plugin + ? strapi.plugins[attribute.plugin].models[attribute.model].globalId + : strapi.models[attribute.model].globalId; + + if ( + FK && + FK.nature !== 'oneToOne' && + FK.nature !== 'manyToOne' && + FK.nature !== 'oneWay' && + FK.nature !== 'oneToMorph' + ) { + definition.loadedModel[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, + }; + } + + break; + } + case 'belongsToMany': { + const FK = _.find(definition.associations, { + alias: name, + }); + const ref = attribute.plugin + ? strapi.plugins[attribute.plugin].models[attribute.collection].globalId + : strapi.models[attribute.collection].globalId; + + // 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] = { + 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, + }, + ]; + } + 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; + + definition.loadedModel[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; + 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; + + definition.loadedModel[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; + break; + } + case 'belongsToMorph': { + definition.loadedModel[name] = { + kind: String, + [attribute.filter]: String, + ref: { + type: instance.Schema.Types.ObjectId, + refPath: `${name}.kind`, + }, + }; + break; + } + case 'belongsToManyMorph': { + definition.loadedModel[name] = [ + { + kind: String, + [attribute.filter]: String, + ref: { + type: instance.Schema.Types.ObjectId, + refPath: `${name}.kind`, + }, + }, + ]; + break; + } + default: + break; + } +}; diff --git a/packages/strapi-hook-mongoose/lib/utils/index.js b/packages/strapi-hook-mongoose/lib/utils/index.js index d38bcc9dc5..a2d0dbdda1 100644 --- a/packages/strapi-hook-mongoose/lib/utils/index.js +++ b/packages/strapi-hook-mongoose/lib/utils/index.js @@ -8,8 +8,14 @@ const Mongoose = require('mongoose'); */ module.exports = (mongoose = Mongoose) => { - mongoose.Schema.Types.Decimal = require('mongoose-float').loadType(mongoose, 2); - mongoose.Schema.Types.Float = require('mongoose-float').loadType(mongoose, 20); + mongoose.Schema.Types.Decimal = require('mongoose-float').loadType( + mongoose, + 2 + ); + mongoose.Schema.Types.Float = require('mongoose-float').loadType( + mongoose, + 20 + ); /** * Convert MongoDB ID to the stringify version as GraphQL throws an error if not. @@ -20,62 +26,67 @@ module.exports = (mongoose = Mongoose) => { return this.toString(); }; - const utils = { - convertType: mongooseType => { - switch (mongooseType.toLowerCase()) { - case 'array': - return Array; - case 'boolean': - return 'Boolean'; - case 'binary': - return 'Buffer'; - case 'date': - case 'datetime': - case 'time': - case 'timestamp': - return Date; - case 'decimal': - return 'Decimal'; - case 'float': - return 'Float'; - case 'json': - return 'Mixed'; - case 'biginteger': - case 'integer': - return 'Number'; - case 'uuid': - return 'ObjectId'; - case 'email': - case 'enumeration': - case 'password': - case 'string': - case 'text': - return 'String'; - default: - } - }, - valueToId: value => { - if (utils.isMongoId(value)) { - return mongoose.Types.ObjectId(value); - } - - return value; - }, - isMongoId: value => { - if (value instanceof mongoose.Types.ObjectId) { - return true; - } - - if (!_.isString(value)) { - return false; - } - - // Here we don't use mongoose.Types.ObjectId.isValid method because it's a weird check, - // it returns for instance true for any integer value - const hexadecimal = /^[0-9A-F]+$/i; - return hexadecimal.test(value) && value.length === 24; - }, + const convertType = mongooseType => { + switch (mongooseType.toLowerCase()) { + case 'array': + return Array; + case 'boolean': + return 'Boolean'; + case 'binary': + return 'Buffer'; + case 'date': + case 'datetime': + case 'time': + case 'timestamp': + return Date; + case 'decimal': + return 'Decimal'; + case 'float': + return 'Float'; + case 'json': + return 'Mixed'; + case 'biginteger': + case 'integer': + return 'Number'; + case 'uuid': + return 'ObjectId'; + case 'email': + case 'enumeration': + case 'password': + case 'string': + case 'text': + return 'String'; + default: + return undefined; + } }; - return utils; + const isMongoId = value => { + if (value instanceof mongoose.Types.ObjectId) { + return true; + } + + if (!_.isString(value)) { + return false; + } + + // Here we don't use mongoose.Types.ObjectId.isValid method because it's a weird check, + // it returns for instance true for any integer value + const hexadecimal = /^[0-9A-F]+$/i; + return hexadecimal.test(value) && value.length === 24; + }; + + const valueToId = value => { + if (isMongoId(value)) { + return mongoose.Types.ObjectId(value); + } + + return value; + }; + + return { + convertType, + valueToId, + isMongoId, + }; };