'use strict'; /** * Module dependencies */ // Public node modules. const _ = require('lodash'); const pluralize = require('pluralize'); /* * Set of utils for models */ module.exports = { /** * Initialize to prevent some mistakes */ initialize: cb => { cb(); }, /** * Retrieve the value based on the primary key */ getValuePrimaryKey: (value, defaultKey) => { return value[defaultKey] || value.id || value._id; }, /** * Find relation nature with verbose */ getNature: ({ attribute, attributeName, modelName }) => { const types = { current: '', other: '', }; const models = strapi.db.getModelsByPluginName(attribute.plugin); 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')) { if (!_.has(models, attribute.collection)) { throw new Error( `The collection \`${_.upperFirst( attribute.collection )}\`, used in the attribute \`${attributeName}\` in the model ${_.upperFirst( modelName )}, is missing from the${ attribute.plugin ? ' (plugin - ' + attribute.plugin + ')' : '' } models` ); } 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(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 { throw new Error( `The attribute \`${ attribute.via }\` is not correctly configured in the model ${_.upperFirst(attribute.collection)}${ attribute.plugin ? ' (plugin - ' + attribute.plugin + ')' : '' }` ); } } else if (_.has(attribute, 'via') && _.has(attribute, 'model')) { types.current = 'modelD'; // We have to find if they are a model linked to this attributeName if (!_.has(models, attribute.model)) { throw new Error( `The model \`${_.upperFirst( attribute.model )}\`, used in the attribute \`${attributeName}\` in the model ${_.upperFirst( modelName )}, is missing from the${ attribute.plugin ? ' (plugin - ' + attribute.plugin + ')' : '' } models` ); } const reverseAttribute = models[attribute.model].attributes[attribute.via]; if (!reverseAttribute) { throw new Error( `The attribute \`${attribute.via}\` is missing in the model ${_.upperFirst( attribute.model )}${attribute.plugin ? ' (plugin - ' + attribute.plugin + ')' : ''}` ); } if ( _.has(reverseAttribute, 'via') && reverseAttribute.via === attributeName && _.has(reverseAttribute, 'collection') && reverseAttribute.collection !== '*' ) { 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 { throw new Error( `The attribute \`${ attribute.via }\` is not correctly configured in the model ${_.upperFirst(attribute.model)}${ attribute.plugin ? ' (plugin - ' + attribute.plugin + ')' : '' }` ); } } else if (_.has(attribute, 'model')) { types.current = 'model'; // 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; } } }); }); } else { throw new Error( `The attribute \`${attributeName}\` is not correctly configured in the model ${_.upperFirst( modelName )}${attribute.plugin ? ' (plugin - ' + attribute.plugin + ')' : ''}` ); } 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; }, /** * Return table name for a collection many-to-many */ getCollectionName: (associationA, associationB) => { if (associationA.dominant && _.has(associationA, 'collectionName')) { return associationA.collectionName; } if (associationB.dominant && _.has(associationB, 'collectionName')) { return associationB.collectionName; } return [associationA, associationB] .sort((a, b) => { if (a.collection === b.collection) { if (a.dominant) return 1; else return -1; } return a.collection < b.collection ? -1 : 1; }) .map(table => _.snakeCase(`${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`) ) .join('__'); }, /** * Define associations key to models */ defineAssociations: function(model, definition, association, key) { try { // Initialize associations object if (definition.associations === undefined) { definition.associations = []; } // Exclude non-relational attribute if (!_.has(association, 'collection') && !_.has(association, 'model')) { return; } // Get relation nature let details; const targetName = association.model || association.collection || ''; const targetModel = targetName !== '*' ? strapi.db.getModel(targetName, association.plugin) : null; const infos = this.getNature({ attribute: association, attributeName: key, modelName: model.toLowerCase(), }); if (targetName !== '*') { const model = strapi.db.getModel(targetName, association.plugin); details = _.get(model, ['attributes', association.via], {}); } // Build associations object if (_.has(association, 'collection') && association.collection !== '*') { const ast = { alias: key, type: 'collection', targetUid: targetModel.uid, collection: association.collection, via: association.via || undefined, nature: infos.nature, autoPopulate: _.get(association, 'autoPopulate', true), dominant: details.dominant !== true, plugin: association.plugin || undefined, filter: details.filter, }; if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') { ast.tableCollectionName = this.getCollectionName(details, association); } if (infos.nature === 'manyWay' && definition.orm === 'bookshelf') { ast.tableCollectionName = _.get(association, 'collectionName') || `${definition.collectionName}__${_.snakeCase(key)}`; } definition.associations.push(ast); return; } if (_.has(association, 'model') && association.model !== '*') { definition.associations.push({ alias: key, type: 'model', targetUid: targetModel.uid, model: association.model, via: association.via || undefined, nature: infos.nature, autoPopulate: _.get(association, 'autoPopulate', true), dominant: details.dominant !== true, plugin: association.plugin || undefined, filter: details.filter, }); 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]; if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) { acc.push(strapi.plugins[current].models[entity]); } }); }); 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()) { acc.push(strapi.models[entity]); } }); 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]; if ((attr.collection || attr.model || '').toLowerCase() === model.toLowerCase()) { acc.push(strapi.components[entity]); } }); return acc; }, []); const models = _.uniqWith(appModels.concat(pluginsModels, componentModels), _.isEqual); definition.associations.push({ alias: key, targetUid: '*', type: association.model ? 'model' : 'collection', related: models, nature: infos.nature, autoPopulate: _.get(association, 'autoPopulate', true), filter: association.filter, }); } catch (e) { strapi.log.error( `Something went wrong in the model \`${_.upperFirst(model)}\` with the attribute \`${key}\`` ); strapi.log.error(e); strapi.stop(); } }, };