diff --git a/examples/getstarted/api/restaurant/models/Restaurant.settings.json b/examples/getstarted/api/restaurant/models/Restaurant.settings.json index 33c0086e40..25149eb6ff 100755 --- a/examples/getstarted/api/restaurant/models/Restaurant.settings.json +++ b/examples/getstarted/api/restaurant/models/Restaurant.settings.json @@ -44,7 +44,6 @@ "max": 35.12 }, "address": { - "required": true, "model": "address" }, "cover": { diff --git a/packages/strapi-connector-bookshelf/lib/buildDatabaseSchema.js b/packages/strapi-connector-bookshelf/lib/buildDatabaseSchema.js index 2e6e2d56a8..8b53479eea 100644 --- a/packages/strapi-connector-bookshelf/lib/buildDatabaseSchema.js +++ b/packages/strapi-connector-bookshelf/lib/buildDatabaseSchema.js @@ -416,6 +416,9 @@ module.exports = async ({ ORM, loadedModel, definition, connection, model }) => [definition.attributes[morphRelation.alias].filter]: { type: 'text', }, + order: { + type: 'integer', + }, }; if (connection.options && connection.options.autoMigration !== false) { @@ -423,7 +426,7 @@ module.exports = async ({ ORM, loadedModel, definition, connection, model }) => } } - // Equilize many to many releations + // Equilize many to many relations const manyRelations = definition.associations.filter(({ nature }) => ['manyToMany', 'manyWay'].includes(nature) ); diff --git a/packages/strapi-connector-bookshelf/lib/mount-models.js b/packages/strapi-connector-bookshelf/lib/mount-models.js index 06aa5c2e51..48edacd750 100644 --- a/packages/strapi-connector-bookshelf/lib/mount-models.js +++ b/packages/strapi-connector-bookshelf/lib/mount-models.js @@ -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); @@ -302,6 +306,7 @@ module.exports = ({ models, target }, ctx) => { : strapi.models[details.model]; const globalId = `${model.collectionName}_morph`; + const filter = _.get(model, ['attributes', details.via, 'filter'], 'field'); loadedModel[name] = function() { return this.morphOne( @@ -309,7 +314,7 @@ module.exports = ({ models, target }, ctx) => { details.via, `${definition.collectionName}` ).query(qb => { - qb.where(_.get(model, ['attributes', details.via, 'filter'], 'field'), name); + qb.where(filter, name); }); }; break; @@ -320,6 +325,7 @@ module.exports = ({ models, target }, ctx) => { : strapi.models[details.collection]; const globalId = `${collection.collectionName}_morph`; + const filter = _.get(model, ['attributes', details.via, 'filter'], 'field'); loadedModel[name] = function() { return this.morphMany( @@ -327,7 +333,7 @@ module.exports = ({ models, target }, ctx) => { details.via, `${definition.collectionName}` ).query(qb => { - qb.where(_.get(model, ['attributes', details.via, 'filter'], 'field'), name); + qb.where(filter, name).orderBy('order'); }); }; break; @@ -650,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, diff --git a/packages/strapi-connector-bookshelf/lib/queries.js b/packages/strapi-connector-bookshelf/lib/queries.js index 945928888e..d0f0c1a7d1 100644 --- a/packages/strapi-connector-bookshelf/lib/queries.js +++ b/packages/strapi-connector-bookshelf/lib/queries.js @@ -132,26 +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': - 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 }); diff --git a/packages/strapi-connector-bookshelf/lib/relations.js b/packages/strapi-connector-bookshelf/lib/relations.js index ae4494db17..372856c092 100644 --- a/packages/strapi-connector-bookshelf/lib/relations.js +++ b/packages/strapi-connector-bookshelf/lib/relations.js @@ -12,27 +12,15 @@ const { models: { getValuePrimaryKey }, } = require('strapi-utils'); -const transformToArrayID = (array, association) => { +const transformToArrayID = array => { if (_.isArray(array)) { - array = array.map(value => { - if (_.isPlainObject(value)) { - return value._id || value.id || false; - } - - return value; - }); - - return array.filter(n => n); + return array + .map(value => _.get(value, 'id') || value) + .filter(n => n) + .map(val => _.toString(val)); } - if ( - association.type === 'model' || - (association.type === 'collection' && _.isObject(array)) - ) { - return _.isEmpty(_.toString(array)) ? [] : transformToArrayID([array]); - } - - return []; + return transformToArrayID([array]); }; const getModel = (model, plugin) => { @@ -45,6 +33,39 @@ const getModel = (model, plugin) => { const removeUndefinedKeys = obj => _.pickBy(obj, _.negate(_.isUndefined)); +const addRelationMorph = async (model, { params, transacting } = {}) => { + return await model.morph.forge().save( + { + [`${model.collectionName}_id`]: params.id, + [`${params.alias}_id`]: params.refId, + [`${params.alias}_type`]: params.ref, + field: params.field, + order: params.order, + }, + { transacting } + ); +}; + +const removeRelationMorph = async (model, { params, transacting } = {}) => { + return await model.morph + .forge() + .where( + _.omitBy( + { + [`${model.collectionName}_id`]: params.id, + [`${params.alias}_id`]: params.refId, + [`${params.alias}_type`]: params.ref, + field: params.field, + }, + _.isUndefined + ) + ) + .destroy({ + require: false, + transacting, + }); +}; + module.exports = { async findOne(params, populate, { transacting } = {}) { const record = await this.forge({ @@ -59,17 +80,12 @@ module.exports = { // Retrieve data manually. if (_.isEmpty(populate)) { const arrayOfPromises = this.associations - .filter(association => - ['manyMorphToOne', 'manyMorphToMany'].includes(association.nature) - ) + .filter(association => ['manyMorphToOne', 'manyMorphToMany'].includes(association.nature)) .map(() => { return this.morph .forge() .where({ - [`${this.collectionName}_id`]: getValuePrimaryKey( - params, - this.primaryKey - ), + [`${this.collectionName}_id`]: getValuePrimaryKey(params, this.primaryKey), }) .fetchAll({ transacting, @@ -94,300 +110,268 @@ module.exports = { }); // Only update fields which are on this document. - const values = - params.parseRelationships === false - ? params.values - : Object.keys(removeUndefinedKeys(params.values)).reduce( - (acc, current) => { - const property = params.values[current]; - const association = this.associations.filter( - x => x.alias === current - )[0]; - const details = this._attributes[current]; + const values = Object.keys(removeUndefinedKeys(params.values)).reduce((acc, current) => { + const property = params.values[current]; + const association = this.associations.filter(x => x.alias === current)[0]; + const details = this._attributes[current]; - if (!association && _.get(details, 'isVirtual') !== true) { - return _.set(acc, current, property); - } + if (!association && _.get(details, 'isVirtual') !== true) { + return _.set(acc, current, property); + } - 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, current, _.get(property, assocModel.primaryKey, property)); + } + case 'oneToOne': { + if (response[current] === property) return acc; + + if (_.isNull(property)) { + const updatePromise = assocModel + .where({ + [assocModel.primaryKey]: getValuePrimaryKey( + response[current], + assocModel.primaryKey + ), + }) + .save( + { [details.via]: null }, + { + method: 'update', + patch: true, + require: false, + transacting, + } ); - switch (association.nature) { - case 'oneWay': { - return _.set( - acc, - current, - _.get(property, assocModel.primaryKey, property) - ); - } - case 'oneToOne': { - if (response[current] === property) return acc; - if (_.isNull(property)) { - const updatePromise = assocModel - .where({ - [assocModel.primaryKey]: getValuePrimaryKey( - response[current], - assocModel.primaryKey - ), - }) - .save( - { [details.via]: null }, - { - method: 'update', - patch: true, - require: false, - transacting, - } - ); + relationUpdates.push(updatePromise); + return _.set(acc, current, null); + } - relationUpdates.push(updatePromise); - return _.set(acc, current, null); - } - - // set old relations to null - const updateLink = this.where({ [current]: property }) - .save( - { [current]: null }, - { - method: 'update', - patch: true, - require: false, - transacting, - } - ) - .then(() => { - return assocModel - .where({ [this.primaryKey]: property }) - .save( - { [details.via]: primaryKeyValue }, - { - method: 'update', - patch: true, - require: false, - transacting, - } - ); - }); - - // set new relation - relationUpdates.push(updateLink); - return _.set(acc, current, property); - } - case 'oneToMany': { - // receive array of ids or array of objects with ids - - // set relation to null for all the ids not in the list - const currentIds = response[current]; - const toRemove = _.differenceWith( - currentIds, - property, - (a, b) => { - return ( - `${a[assocModel.primaryKey] || a}` === - `${b[assocModel.primaryKey] || b}` - ); - } - ); - - const updatePromise = assocModel - .where( - assocModel.primaryKey, - 'in', - toRemove.map(val => val[assocModel.primaryKey] || val) - ) - .save( - { [details.via]: null }, - { - method: 'update', - patch: true, - require: false, - transacting, - } - ) - .then(() => { - return assocModel - .where( - assocModel.primaryKey, - 'in', - property.map(val => val[assocModel.primaryKey] || val) - ) - .save( - { [details.via]: primaryKeyValue }, - { - method: 'update', - patch: true, - require: false, - transacting, - } - ); - }); - - relationUpdates.push(updatePromise); - return acc; - } - case 'manyToOne': { - return _.set( - acc, - current, - _.get(property, assocModel.primaryKey, property) - ); - } - case 'manyWay': - case 'manyToMany': { - const currentValue = transformToArrayID( - response[current], - association - ).map(id => id.toString()); - const storedValue = transformToArrayID( - params.values[current], - association - ).map(id => id.toString()); - - const toAdd = _.difference(storedValue, currentValue); - const toRemove = _.difference(currentValue, storedValue); - - const collection = this.forge({ - [this.primaryKey]: primaryKeyValue, - })[association.alias](); - - const updatePromise = collection - .detach(toRemove, { transacting }) - .then(() => collection.attach(toAdd, { transacting })); - - relationUpdates.push(updatePromise); - return acc; - } - case 'manyMorphToMany': - case 'manyMorphToOne': - // Update the relational array. - params.values[current].forEach(obj => { - const model = strapi.getModel( - obj.ref, - obj.source && obj.source !== 'content-manager' - ? obj.source - : null - ); - - const reverseAssoc = model.associations.find( - assoc => assoc.alias === obj.field - ); - - // Remove existing relationship because only one file - // can be related to this field. - if ( - reverseAssoc && - reverseAssoc.nature === 'oneToManyMorph' - ) { - relationUpdates.push( - module.exports.removeRelationMorph - .call( - this, - { - alias: association.alias, - ref: model.collectionName, - refId: obj.refId, - field: obj.field, - }, - { transacting } - ) - .then(() => - module.exports.addRelationMorph.call( - this, - { - id: response[this.primaryKey], - alias: association.alias, - ref: model.collectionName, - refId: obj.refId, - field: obj.field, - }, - { transacting } - ) - ) - ); - } else { - relationUpdates.push( - module.exports.addRelationMorph.call( - this, - { - id: response[this.primaryKey], - alias: association.alias, - ref: model.collectionName, - refId: obj.refId, - field: obj.field, - }, - { transacting } - ) - ); - } - }); - break; - case 'oneToManyMorph': - case 'manyToManyMorph': { - // Compare array of ID to find deleted files. - const currentValue = transformToArrayID( - response[current], - association - ).map(id => id.toString()); - const storedValue = transformToArrayID( - params.values[current], - association - ).map(id => id.toString()); - - const toAdd = _.difference(storedValue, currentValue); - const toRemove = _.difference(currentValue, storedValue); - - const model = getModel( - details.collection || details.model, - details.plugin - ); - - toAdd.forEach(id => { - relationUpdates.push( - module.exports.addRelationMorph.call( - model, - { - id, - alias: association.via, - ref: this.collectionName, - refId: response.id, - field: association.alias, - }, - { transacting } - ) - ); - }); - - // Update the relational array. - toRemove.forEach(id => { - relationUpdates.push( - module.exports.removeRelationMorph.call( - model, - { - id, - alias: association.via, - ref: this.collectionName, - refId: response.id, - field: association.alias, - }, - { transacting } - ) - ); - }); - break; - } - case 'oneMorphToOne': - case 'oneMorphToMany': { - break; - } - default: + // set old relations to null + const updateLink = this.where({ [current]: property }) + .save( + { [current]: null }, + { + method: 'update', + patch: true, + require: false, + transacting, } + ) + .then(() => { + return assocModel.where({ [this.primaryKey]: property }).save( + { [details.via]: primaryKeyValue }, + { + method: 'update', + patch: true, + require: false, + transacting, + } + ); + }); - return acc; + // set new relation + relationUpdates.push(updateLink); + return _.set(acc, current, property); + } + case 'oneToMany': { + // receive array of ids or array of objects with ids + + // set relation to null for all the ids not in the list + const currentIds = response[current]; + const toRemove = _.differenceWith(currentIds, property, (a, b) => { + return `${a[assocModel.primaryKey] || a}` === `${b[assocModel.primaryKey] || b}`; + }); + + const updatePromise = assocModel + .where( + assocModel.primaryKey, + 'in', + toRemove.map(val => val[assocModel.primaryKey] || val) + ) + .save( + { [details.via]: null }, + { + method: 'update', + patch: true, + require: false, + transacting, + } + ) + .then(() => { + return assocModel + .where( + assocModel.primaryKey, + 'in', + property.map(val => val[assocModel.primaryKey] || val) + ) + .save( + { [details.via]: primaryKeyValue }, + { + method: 'update', + patch: true, + require: false, + transacting, + } + ); + }); + + relationUpdates.push(updatePromise); + return acc; + } + case 'manyToOne': { + return _.set(acc, current, _.get(property, assocModel.primaryKey, property)); + } + case 'manyWay': + case 'manyToMany': { + const storedValue = transformToArrayID(response[current]); + const currentValue = transformToArrayID(params.values[current]); + + const toAdd = _.difference(currentValue, storedValue); + const toRemove = _.difference(storedValue, currentValue); + + const collection = this.forge({ + [this.primaryKey]: primaryKeyValue, + })[association.alias](); + + const updatePromise = collection + .detach(toRemove, { transacting }) + .then(() => collection.attach(toAdd, { transacting })); + + relationUpdates.push(updatePromise); + return acc; + } + // media -> model + case 'manyMorphToMany': + case 'manyMorphToOne': { + // Update the relational array. + const refs = params.values[current]; + + if (Array.isArray(refs) && refs.length === 0) { + // clear related + relationUpdates.push( + removeRelationMorph(this, { params: { id: primaryKeyValue }, transacting }) + ); + break; + } + + refs.forEach(obj => { + const targetModel = strapi.getModel( + obj.ref, + obj.source !== 'content-manager' ? obj.source : null + ); + + const reverseAssoc = targetModel.associations.find(assoc => assoc.alias === obj.field); + + // Remove existing relationship because only one file + // can be related to this field. + if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') { + relationUpdates.push( + removeRelationMorph(this, { + params: { + alias: association.alias, + ref: targetModel.collectionName, + refId: obj.refId, + field: obj.field, + }, + transacting, + }).then(() => + addRelationMorph(this, { + params: { + id: response[this.primaryKey], + alias: association.alias, + ref: targetModel.collectionName, + refId: obj.refId, + field: obj.field, + order: 1, + }, + transacting, + }) + ) + ); + + return; + } + + const addRelation = async () => { + const maxOrder = await this.morph + .query(qb => { + qb.max('order as order').where({ + [`${association.alias}_id`]: obj.refId, + [`${association.alias}_type`]: targetModel.collectionName, + field: obj.field, + }); + }) + .fetch({ transacting }); + + const { order = 0 } = maxOrder.toJSON(); + + await addRelationMorph(this, { + params: { + id: response[this.primaryKey], + alias: association.alias, + ref: targetModel.collectionName, + refId: obj.refId, + field: obj.field, + order: order + 1, + }, + transacting, + }); + }; + + relationUpdates.push(addRelation()); + }); + break; + } + // model -> media + case 'oneToManyMorph': + case 'manyToManyMorph': { + const currentValue = transformToArrayID(params.values[current]); + + const model = getModel(details.collection || details.model, details.plugin); + + const promise = removeRelationMorph(model, { + params: { + alias: association.via, + ref: this.collectionName, + refId: response.id, + field: association.alias, }, - {} - ); + transacting, + }).then(() => { + return Promise.all( + currentValue.map((id, idx) => { + return addRelationMorph(model, { + params: { + id, + alias: association.via, + ref: this.collectionName, + refId: response.id, + field: association.alias, + order: idx + 1, + }, + transacting, + }); + }) + ); + }); + + relationUpdates.push(promise); + + break; + } + case 'oneMorphToOne': + case 'oneMorphToMany': { + break; + } + default: + } + + return acc; + }, {}); await Promise.all(relationUpdates); @@ -410,52 +394,29 @@ module.exports = { return result && result.toJSON ? result.toJSON() : result; }, - async addRelationMorph(params, { transacting } = {}) { - const record = await this.morph - .forge() - .where({ - [`${this.collectionName}_id`]: params.id, - [`${params.alias}_id`]: params.refId, - [`${params.alias}_type`]: params.ref, - field: params.field, - }) - .fetch({ - transacting, - }); + deleteRelations(id, { transacting }) { + const values = {}; - const entry = record ? record.toJSON() : record; + 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: + } + }); - if (entry) { - return Promise.resolve(); - } - - return await this.morph - .forge({ - [`${this.collectionName}_id`]: params.id, - [`${params.alias}_id`]: params.refId, - [`${params.alias}_type`]: params.ref, - field: params.field, - }) - .save(null, { transacting }); - }, - - async removeRelationMorph(params, { transacting } = {}) { - return await this.morph - .forge() - .where( - _.omitBy( - { - [`${this.collectionName}_id`]: params.id, - [`${params.alias}_id`]: params.refId, - [`${params.alias}_type`]: params.ref, - field: params.field, - }, - _.isUndefined - ) - ) - .destroy({ - require: false, - transacting, - }); + return this.updateRelations({ [this.primaryKey]: id, values }, { transacting }); }, }; diff --git a/packages/strapi-connector-mongoose/lib/mount-models.js b/packages/strapi-connector-mongoose/lib/mount-models.js index 924f950299..2d2771d0d4 100644 --- a/packages/strapi-connector-mongoose/lib/mount-models.js +++ b/packages/strapi-connector-mongoose/lib/mount-models.js @@ -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; }); }; @@ -308,20 +312,8 @@ const createOnFetchPopulateFn = ({ morphAssociations, componentAttributes, defin const { alias, nature } = association; if (['oneToManyMorph', 'manyToManyMorph'].includes(nature)) { - this.populate({ - path: alias, - match: { - [`${association.via}.${association.filter}`]: association.alias, - [`${association.via}.kind`]: definition.globalId, - }, - options: { - sort: '-createdAt', - }, - }); - return; - } - - if (populatedPaths.includes(alias)) { + this.populate(alias); + } else if (populatedPaths.includes(alias)) { _.set(this._mongooseOptions.populate, [alias, 'path'], `${alias}.ref`); } }); @@ -343,48 +335,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 +388,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 +398,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 +425,47 @@ 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; - - 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; + const ref = getRef(attribute.model, attribute.plugin); + setField(name, { type: ObjectId, ref }); 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; + const ref = getRef(attribute.collection, attribute.plugin); + setField(name, [{ type: ObjectId, ref }]); 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: diff --git a/packages/strapi-connector-mongoose/lib/queries.js b/packages/strapi-connector-mongoose/lib/queries.js index c69a11ec61..3977056bcf 100644 --- a/packages/strapi-connector-mongoose/lib/queries.js +++ b/packages/strapi-connector-mongoose/lib/queries.js @@ -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; } diff --git a/packages/strapi-connector-mongoose/lib/relations.js b/packages/strapi-connector-mongoose/lib/relations.js index 96c583fa79..8039db0496 100644 --- a/packages/strapi-connector-mongoose/lib/relations.js +++ b/packages/strapi-connector-mongoose/lib/relations.js @@ -19,10 +19,72 @@ const getModel = function(model, plugin) { ); }; -const removeUndefinedKeys = obj => _.pickBy(obj, _.negate(_.isUndefined)); +const transformToArrayID = (array, pk) => { + if (_.isArray(array)) { + return array + .map(value => value && (getValuePrimaryKey(value, pk) || value)) + .filter(n => n) + .map(val => _.toString(val)); + } + + return transformToArrayID([array]); +}; + +const removeUndefinedKeys = (obj = {}) => _.pickBy(obj, _.negate(_.isUndefined)); + +const addRelationMorph = async (model, params) => { + const { id, alias, refId, ref, field, filter } = params; + + await model.updateMany( + { + [model.primaryKey]: id, + }, + { + $push: { + [alias]: { + ref: new mongoose.Types.ObjectId(refId), + kind: ref, + [filter]: field, + }, + }, + } + ); +}; + +const removeRelationMorph = async (model, params) => { + const { alias } = params; + + let opts; + // if entry id is provided simply query it + if (params.id) { + opts = { + _id: params.id, + }; + } else { + opts = { + [alias]: { + $elemMatch: { + ref: params.refId, + kind: params.ref, + [params.filter]: params.field, + }, + }, + }; + } + + await model.updateMany(opts, { + $pull: { + [alias]: { + ref: params.refId, + kind: params.ref, + [params.filter]: params.field, + }, + }, + }); +}; module.exports = { - update: async function(params) { + async update(params) { const relationUpdates = []; const populate = this.associations.map(x => x.alias); const primaryKeyValue = getValuePrimaryKey(params, this.primaryKey); @@ -32,309 +94,252 @@ 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]; + + // 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 } ); + }); - const details = this._attributes[attribute]; + 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 + ); + } - // set simple attributes - if (!association && _.get(details, 'isVirtual') !== true) { - return _.set(acc, attribute, 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), + }, } - - const assocModel = getModel( - details.model || details.collection, - details.plugin + ) + .then(() => { + return assocModel.updateMany( + { + [assocModel.primaryKey]: { + $in: newValue + ? newValue.map( + val => new mongoose.Types.ObjectId(val[assocModel.primaryKey] || val) + ) + : newValue, + }, + }, + { + $addToSet: { [association.via]: [primaryKeyValue] }, + } ); + }); - 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; + relationUpdates.push(updatePomise); + return acc; + } + // media -> model + case 'manyMorphToMany': + case 'manyMorphToOne': { + newValue.forEach(obj => { + const refModel = strapi.getModel(obj.ref, obj.source); - // if the value is null, set field to null on both sides - if (_.isNull(newValue)) { - const updatePromise = assocModel.updateOne( + const createRelation = () => { + return addRelationMorph(this, { + id: entry[this.primaryKey], + alias: association.alias, + ref: obj.kind || refModel.globalId, + refId: new mongoose.Types.ObjectId(obj.refId), + field: obj.field, + filter: association.filter, + }); + }; + + // Clear relations to refModel + const reverseAssoc = refModel.associations.find(assoc => assoc.alias === obj.field); + if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') { + relationUpdates.push( + removeRelationMorph(this, { + alias: association.alias, + ref: obj.kind || refModel.globalId, + refId: new mongoose.Types.ObjectId(obj.refId), + field: obj.field, + filter: association.filter, + }) + .then(createRelation) + .then(() => { + // set field inside refModel + return refModel.updateMany( { - [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 - ) - ), - }, + [refModel.primaryKey]: new mongoose.Types.ObjectId(obj.refId), }, { - $pull: { - [association.via]: new mongoose.Types.ObjectId( - primaryKeyValue - ), - }, + [obj.field]: new mongoose.Types.ObjectId(entry[this.primaryKey]), } - ) - .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()); + }) + ); + } 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 '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() ); + }) + ); + } + }); + break; + } + // model -> media + case 'oneToManyMorph': + case 'manyToManyMorph': { + // Compare array of ID to find deleted files. + const currentIds = transformToArrayID(currentValue, this.primaryKey); + const newIds = transformToArrayID(newValue, this.primaryKey); - const toAdd = _.difference(storedValue, attributeValue); - const toRemove = _.difference(attributeValue, storedValue); + const toAdd = _.difference(newIds, currentIds); + const toRemove = _.difference(currentIds, newIds); - const model = getModel( - details.model || details.collection, - details.plugin - ); + 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, - }) - ); - }); + _.set(acc, attribute, newIds); - // 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; - }, - {} + const addPromise = Promise.all( + toAdd.map(id => { + return addRelationMorph(model, { + id, + alias: association.via, + ref: this.globalId, + refId: entry._id, + field: association.alias, + filter: association.filter, + }); + }) ); + relationUpdates.push(addPromise); + + toRemove.forEach(id => { + relationUpdates.push( + removeRelationMorph(model, { + id, + alias: association.via, + ref: this.globalId, + refId: entry._id, + field: association.alias, + filter: association.filter, + }) + ); + }); + break; + } + case 'oneMorphToOne': + case 'oneMorphToMany': + break; + default: + } + + return acc; + }, {}); + // Update virtuals fields. await Promise.all(relationUpdates).then(() => this.updateOne({ [this.primaryKey]: primaryKeyValue }, values, { @@ -346,83 +351,124 @@ module.exports = { [this.primaryKey]: primaryKeyValue, }).populate(populate); - return updatedEntity && updatedEntity.toObject - ? updatedEntity.toObject() - : updatedEntity; + 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(); + // TODO: delete all the ref to the model - // 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; - } + switch (nature) { + case 'oneWay': + case 'manyWay': { + return; + } + case 'oneToMany': + case 'oneToOne': { + if (!via) { + return; + } - return false; - }); + const targetModel = strapi.db.getModel( + association.model || association.collection, + association.plugin + ); - if (relationExists) return Promise.resolve(); + return targetModel.updateMany({ [via]: primaryKeyValue }, { [via]: null }); + } + case 'manyToMany': + case 'manyToOne': { + if (!via || dominant) { + return; + } - entry[alias].push({ - ref: new mongoose.Types.ObjectId(params.refId), - kind: params.ref, - [params.filter]: params.field, - }); + const targetModel = strapi.db.getModel( + association.model || association.collection, + association.plugin + ); - await entry.save(); - }, + return targetModel.updateMany( + { [via]: primaryKeyValue }, + { $pull: { [via]: primaryKeyValue } } + ); + } + case 'oneToManyMorph': + case 'manyToManyMorph': { + // delete relation inside of the ref model - async removeRelationMorph(params) { - const { alias } = params; + const targetModel = strapi.db.getModel( + association.model || association.collection, + association.plugin + ); - 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, - }, - }, - }; - } + // ignore them ghost relations + if (!targetModel) return; - const entries = await this.find(opts); + const element = { + ref: primaryKeyValue, + kind: this.globalId, + [association.filter]: 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; + return targetModel.updateMany( + { [via]: { $elemMatch: element } }, + { $pull: { [via]: element } } + ); + } + case 'manyMorphToMany': + case 'manyMorphToOne': { + // delete relation inside of the ref model + // console.log(entry[association.alias]); + + if (Array.isArray(entry[association.alias])) { + return Promise.all( + entry[association.alias].map(val => { + const targetModel = strapi.db.getModelByGlobalId(val.kind); + + // ignore them ghost relations + if (!targetModel) return; + + const field = val[association.filter]; + const reverseAssoc = targetModel.associations.find( + assoc => assoc.alias === field + ); + + if (reverseAssoc && reverseAssoc.nature === 'oneToManyMorph') { + return targetModel.updateMany( + { + [targetModel.primaryKey]: val.ref && (val.ref._id || val.ref), + }, + { + [field]: null, + } + ); + } + + return targetModel.updateMany( + { + [targetModel.primaryKey]: val.ref && (val.ref._id || val.ref), + }, + { + $pull: { [field]: primaryKeyValue }, + } + ); + }) + ); + } + + return; + } + case 'oneMorphToOne': + case 'oneMorphToMany': { + return; + } } - - return true; - }); - - return entry.save(); - }); - - await Promise.all(updates); + }) + ); }, }; diff --git a/packages/strapi-database/lib/database-manager.js b/packages/strapi-database/lib/database-manager.js index d3096f83de..d25721e875 100644 --- a/packages/strapi-database/lib/database-manager.js +++ b/packages/strapi-database/lib/database-manager.js @@ -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) { diff --git a/packages/strapi-plugin-upload/services/Upload.js b/packages/strapi-plugin-upload/services/Upload.js index 72651666c2..3e3f8dfd82 100644 --- a/packages/strapi-plugin-upload/services/Upload.js +++ b/packages/strapi-plugin-upload/services/Upload.js @@ -269,7 +269,7 @@ module.exports = { const { id, model, field } = params; const arr = Array.isArray(files) ? files : [files]; - return Promise.all( + const enhancedFiles = await Promise.all( arr.map(file => { return this.enhanceFile( file, @@ -282,7 +282,9 @@ module.exports = { } ); }) - ).then(files => this.uploadFileAndPersist(files)); + ); + + await Promise.all(enhancedFiles.map(file => this.uploadFileAndPersist(file))); }, getSettings() { diff --git a/packages/strapi-utils/lib/__tests__/models.test.js b/packages/strapi-utils/lib/__tests__/models.test.js new file mode 100644 index 0000000000..d2375e49ac --- /dev/null +++ b/packages/strapi-utils/lib/__tests__/models.test.js @@ -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', + }); + }); + }); +}); diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index 2c9bd3ea38..3e28cde324 100644 --- a/packages/strapi-utils/lib/models.js +++ b/packages/strapi-utils/lib/models.js @@ -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) ) ) { diff --git a/packages/strapi/lib/Strapi.js b/packages/strapi/lib/Strapi.js index aa228295fd..90b60809e6 100644 --- a/packages/strapi/lib/Strapi.js +++ b/packages/strapi/lib/Strapi.js @@ -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');