diff --git a/examples/getstarted/api/restaurant/models/Restaurant.settings.json b/examples/getstarted/api/restaurant/models/Restaurant.settings.json index eb9bb751a1..2ab966e87f 100755 --- a/examples/getstarted/api/restaurant/models/Restaurant.settings.json +++ b/examples/getstarted/api/restaurant/models/Restaurant.settings.json @@ -1,13 +1,17 @@ { + "kind": "collectionType", "collectionName": "restaurants", "info": { "name": "restaurant", "description": "" }, "options": { - "draftAndPublish": false, + "draftAndPublish": true, "increments": true, - "timestamps": ["created_at", "updated_at"], + "timestamps": [ + "created_at", + "updated_at" + ], "comment": "" }, "pluginOptions": { @@ -20,22 +24,53 @@ "maxLength": 50, "required": true, "minLength": 5, - "type": "string" + "type": "string", + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "slug": { "type": "uid", - "targetField": "name" + "targetField": "name", + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "price_range": { - "enum": ["very_cheap", "cheap", "average", "expensive", "very_expensive"], - "type": "enumeration" + "enum": [ + "very_cheap", + "cheap", + "average", + "expensive", + "very_expensive" + ], + "type": "enumeration", + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "closing_period": { "component": "default.closingperiod", - "type": "component" + "type": "component", + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "contact_email": { - "type": "email" + "type": "email", + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "stars": { "required": true, @@ -67,10 +102,20 @@ "required": false }, "short_description": { - "type": "text" + "type": "text", + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "since": { - "type": "date" + "type": "date", + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "categories": { "collection": "category" @@ -78,12 +123,22 @@ "description": { "type": "richtext", "required": true, - "minLength": 10 + "minLength": 10, + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "services": { "component": "default.restaurantservice", "repeatable": true, - "type": "component" + "type": "component", + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "menu": { "model": "menu", @@ -94,7 +149,12 @@ "type": "component", "repeatable": true, "min": 1, - "max": 10 + "max": 10, + "pluginOptions": { + "i18n": { + "localized": true + } + } }, "dz": { "type": "dynamiczone", @@ -103,7 +163,21 @@ "default.restaurantservice", "default.closingperiod", "default.dish" - ] + ], + "pluginOptions": { + "i18n": { + "localized": true + } + } + }, + "parent": { + "collection": "restaurant", + "via": "children", + "dominant": true + }, + "children": { + "collection": "restaurant", + "via": "parent" } } } diff --git a/packages/strapi-connector-bookshelf/lib/populate.js b/packages/strapi-connector-bookshelf/lib/populate.js index 92e2da1e55..79dcf83f66 100644 --- a/packages/strapi-connector-bookshelf/lib/populate.js +++ b/packages/strapi-connector-bookshelf/lib/populate.js @@ -1,14 +1,17 @@ 'use strict'; const _ = require('lodash'); -const pq = require('./utils/populate-queries'); +const { + bindPopulateQueries, + extendWithPopulateQueries, + queryOptionsToQueryMap, +} = require('./utils/populate-queries'); const { getComponentAttributes, isComponent } = require('./utils/attributes'); const { isPolymorphic } = require('./utils/associations'); /** * Create utilities to populate a model on fetch */ - const populateFetch = (definition, options) => { // do not populate anything if (options.withRelated === false) return; @@ -27,98 +30,112 @@ const populateFetch = (definition, options) => { } }; -const populateAssociations = (definition, { prefix = '', publicationState } = {}) => { +const populateAssociations = (definition, options = {}) => { return definition.associations .filter(ast => ast.autoPopulate !== false) .map(assoc => { if (isPolymorphic({ assoc })) { - return formatPolymorphicPopulate({ - assoc, - prefix, - publicationState, - }); + return formatPolymorphicPopulate({ assoc }, options); } - return formatAssociationPopulate({ assoc }, { prefix, publicationState }); + return formatAssociationPopulate({ assoc }, options); }) .reduce((acc, val) => acc.concat(val), []); }; -const populateBareAssociations = (definition, { prefix = '', publicationState } = {}) => { +const populateBareAssociations = (definition, options = {}) => { + const { prefix = '', ...queryOptions } = options; + return (definition.associations || []) .filter(ast => ast.autoPopulate !== false) .map(assoc => { if (isPolymorphic({ assoc })) { - return formatPolymorphicPopulate({ - assoc, - prefix, - publicationState, - }); + return formatPolymorphicPopulate({ assoc }, options); } const path = `${prefix}${assoc.alias}`; const assocModel = strapi.db.getModelByAssoc(assoc); + const populateOptions = bindPopulateQueries( + [path], + queryOptionsToQueryMap(queryOptions, { model: assocModel }) + ); + const polyAssocs = assocModel.associations .filter(assoc => isPolymorphic({ assoc })) - .map(assoc => - formatPolymorphicPopulate({ - assoc, - prefix: `${path}.`, - publicationState, - }) - ); + .map(assoc => { + return formatPolymorphicPopulate({ assoc }, { prefix: `${path}.`, ...queryOptions }); + }); - return [ - pq.bindPopulateQueries([path], { - publicationState: { query: publicationState, model: assocModel }, - }), - ...polyAssocs, - ]; + return [populateOptions, ...polyAssocs]; }) .reduce((acc, val) => acc.concat(val), []); }; -const formatAssociationPopulate = ({ assoc, prefix = '' }, options = {}) => { - const { publicationState } = options; +const hasCustomPopulate = assoc => _.isArray(assoc.populate); + +const formatAssociationPopulate = ({ assoc }, options = {}) => { + const { prefix = '', ...queryOptions } = options; + const path = `${prefix}${assoc.alias}`; const assocModel = strapi.db.getModelByAssoc(assoc); - const polyAssocs = assocModel.associations - .filter(assoc => isPolymorphic({ assoc })) - .map(assoc => - formatPolymorphicPopulate({ - assoc, - prefix: `${path}.`, - publicationState, - }) - ); + const polyAssocs = hasCustomPopulate(assoc) + ? [] + : assocModel.associations + .filter(polyAssoc => isPolymorphic({ assoc: polyAssoc })) + .map(polyAssoc => { + return formatPolymorphicPopulate( + { assoc: polyAssoc }, + { prefix: `${path}.`, ...queryOptions } + ); + }); - const components = populateComponents(assocModel, { prefix: `${path}.`, publicationState }); + const components = hasCustomPopulate(assoc) + ? [] + : populateComponents(assocModel, { prefix: `${path}.`, ...queryOptions }); - return [ - pq.bindPopulateQueries([path], { - publicationState: { query: publicationState, model: assocModel }, - }), - ...polyAssocs, - ...components, - ]; + const populateOpts = bindPopulateQueries( + [path], + queryOptionsToQueryMap(queryOptions, { model: assocModel }) + ); + + return [selectPopulateFields({ assoc, assocModel }, populateOpts), ...polyAssocs, ...components]; }; -const populateComponents = (definition, { prefix = '', publicationState } = {}) => { +const selectPopulateFields = ({ assoc, assocModel }, populateOpts) => { + if (!hasCustomPopulate(assoc)) return populateOpts; + + const columns = assoc.populate + .filter(name => _.has(assocModel.attributes, name)) + .concat('id') + .map(name => `${assocModel.collectionName}.${name}`); + + return Object.keys(populateOpts).reduce((opts, key) => { + opts[key] = qb => { + qb.column(columns); + populateOpts[key](qb); + }; + return opts; + }, {}); +}; + +const populateComponents = (definition, options = {}) => { return getComponentAttributes(definition) .map(key => { const attribute = definition.attributes[key]; const autoPopulate = _.get(attribute, ['autoPopulate'], true); if (autoPopulate === true) { - return populateComponent(key, attribute, { prefix, publicationState }); + return populateComponent(key, attribute, options); } - }, []) + }) .reduce((acc, val) => acc.concat(val), []); }; -const populateComponent = (key, attr, { prefix = '', publicationState } = {}) => { +const populateComponent = (key, attr, options = {}) => { + const { prefix = '', ...queryOptions } = options; + const path = `${prefix}${key}.component`; const componentPrefix = `${path}.`; @@ -129,12 +146,12 @@ const populateComponent = (key, attr, { prefix = '', publicationState } = {}) => const component = strapi.components[key]; const assocs = populateBareAssociations(component, { prefix: componentPrefix, - publicationState, + ...queryOptions, }); const components = populateComponents(component, { prefix: componentPrefix, - publicationState, + ...queryOptions, }); return acc.concat([path, ...assocs, ...components]); @@ -142,20 +159,14 @@ const populateComponent = (key, attr, { prefix = '', publicationState } = {}) => } const component = strapi.components[attr.component]; - const assocs = populateBareAssociations(component, { - prefix: componentPrefix, - publicationState, - }); + const assocs = populateBareAssociations(component, { prefix: componentPrefix, ...queryOptions }); - const components = populateComponents(component, { - prefix: componentPrefix, - publicationState, - }); + const components = populateComponents(component, { prefix: componentPrefix, ...queryOptions }); return [path, ...assocs, ...components]; }; -const formatPopulateOptions = (definition, { withRelated, publicationState } = {}) => { +const formatPopulateOptions = (definition, { withRelated, ...queryOptions } = {}) => { if (!Array.isArray(withRelated)) withRelated = [withRelated]; const obj = withRelated.reduce((acc, key) => { @@ -197,11 +208,7 @@ const formatPopulateOptions = (definition, { withRelated, publicationState } = { tmpModel = strapi.db.getModelByAssoc(assoc); if (isPolymorphic({ assoc })) { - const path = formatPolymorphicPopulate({ - assoc, - prefix, - publicationState, - }); + const path = formatPolymorphicPopulate({ assoc }, { prefix, ...queryOptions }); return _.extend(acc, path); } @@ -210,9 +217,10 @@ const formatPopulateOptions = (definition, { withRelated, publicationState } = { prefix = `${newKey}.`; _.extend(acc, { - [newKey]: pq.extendWithPopulateQueries([obj[newKey], acc[newKey]], { - publicationState: { query: publicationState, model: tmpModel }, - }), + [newKey]: extendWithPopulateQueries( + [obj[newKey], acc[newKey]], + queryOptionsToQueryMap(queryOptions, { model: tmpModel }) + ), }); } @@ -222,15 +230,18 @@ const formatPopulateOptions = (definition, { withRelated, publicationState } = { return [finalObj]; }; -const formatPolymorphicPopulate = ({ assoc, prefix = '', publicationState }) => { +const defaultOrderBy = qb => qb.orderBy('created_at', 'desc'); + +const formatPolymorphicPopulate = ({ assoc }, options = {}) => { + const { prefix = '', ...queryOptions } = options; + const model = strapi.db.getModelByAssoc(assoc); - const populateOptions = { - publicationState: { query: publicationState, model }, - }; + + const queryMap = queryOptionsToQueryMap(queryOptions, { model }); // MorphTo side. if (assoc.related) { - return pq.bindPopulateQueries([`${prefix}${assoc.alias}.related`], populateOptions); + return bindPopulateQueries([`${prefix}${assoc.alias}.related`], queryMap); } // oneToMorph or manyToMorph side. @@ -238,14 +249,7 @@ const formatPolymorphicPopulate = ({ assoc, prefix = '', publicationState }) => const path = `${prefix}${assoc.alias}.${model.collectionName}`; return { - [path]: pq.extendWithPopulateQueries( - [ - qb => { - qb.orderBy('created_at', 'desc'); - }, - ], - populateOptions - ), + [path]: extendWithPopulateQueries([defaultOrderBy], queryMap), }; }; diff --git a/packages/strapi-connector-bookshelf/lib/utils/populate-queries.js b/packages/strapi-connector-bookshelf/lib/utils/populate-queries.js index 8fe151484f..5a5fd3e5b7 100644 --- a/packages/strapi-connector-bookshelf/lib/utils/populate-queries.js +++ b/packages/strapi-connector-bookshelf/lib/utils/populate-queries.js @@ -23,12 +23,13 @@ const optionsMap = { }, }; -const availableOptions = Object.keys(optionsMap); -const isValidOption = option => availableOptions.includes(option); -const validate = (option, params) => { - const opt = _.get(optionsMap, option, {}); +const isValidOption = optionName => _.has(optionsMap, optionName); + +const validate = (optionName, params) => { + const opt = _.get(optionsMap, optionName, {}); return !_.isFunction(opt.validate) || opt.validate(params); }; + const resolveQuery = (option, params) => optionsMap[option].queries[params.query](params); /** @@ -37,19 +38,17 @@ const resolveQuery = (option, params) => optionsMap[option].queries[params.query * @returns Array */ const toQueries = options => { - return _.reduce( - options, - (acc, params, key) => { - if (isValidOption(key) && validate(key, params)) { - const query = resolveQuery(key, params); - if (_.isFunction(query)) { - return [...acc, query]; - } + return Object.keys(options).reduce((acc, key) => { + const params = options[key]; + + if (isValidOption(key) && validate(key, params)) { + const query = resolveQuery(key, params); + if (_.isFunction(query)) { + return [...acc, query]; } - return acc; - }, - [] - ); + } + return acc; + }, []); }; /** @@ -91,9 +90,26 @@ const extendWithPopulateQueries = (fns, options) => { }; }; +/** + * Transforms queryOptions (e.g { publicationState: 'live' }) + * into query map + * { + * publicationState: { query: 'live', ...context } + * } + * @param {{ [key: string]: string }} queryOptions + * @param {object} context + */ +const queryOptionsToQueryMap = (queryOptions, context) => { + return Object.keys(queryOptions).reduce((acc, key) => { + acc[key] = { query: queryOptions[key], ...context }; + return acc; + }, {}); +}; + module.exports = { toQueries, runPopulateQueries, bindPopulateQueries, extendWithPopulateQueries, + queryOptionsToQueryMap, }; diff --git a/packages/strapi-connector-mongoose/lib/mount-models.js b/packages/strapi-connector-mongoose/lib/mount-models.js index a86a2d20c8..0148573f9f 100644 --- a/packages/strapi-connector-mongoose/lib/mount-models.js +++ b/packages/strapi-connector-mongoose/lib/mount-models.js @@ -319,38 +319,42 @@ const migrateSchema = () => {}; const createOnFetchPopulateFn = ({ morphAssociations, componentAttributes, definition }) => { return function() { const populatedPaths = this.getPopulatedPaths(); - const { publicationState, _populateComponents = true } = this.getOptions(); + const { + publicationState, + _populateComponents = true, + _populateMorphRelations = true, + } = this.getOptions(); const getMatchQuery = assoc => { const assocModel = strapi.db.getModelByAssoc(assoc); - if ( - contentTypesUtils.hasDraftAndPublish(assocModel) && - DP_PUB_STATES.includes(publicationState) - ) { + const hasDraftAndPublish = contentTypesUtils.hasDraftAndPublish(assocModel); + if (hasDraftAndPublish && DP_PUB_STATES.includes(publicationState)) { return populateQueries.publicationState[publicationState]; } return undefined; }; - morphAssociations.forEach(association => { - const matchQuery = getMatchQuery(association); - const { alias, nature } = association; + if (_populateMorphRelations) { + morphAssociations.forEach(association => { + const matchQuery = getMatchQuery(association); + const { alias, nature } = association; - if (['oneToManyMorph', 'manyToManyMorph'].includes(nature)) { - this.populate({ path: alias, match: matchQuery, options: { publicationState } }); - } else if (populatedPaths.includes(alias)) { - _.set(this._mongooseOptions.populate, [alias, 'path'], `${alias}.ref`); - _.set(this._mongooseOptions.populate, [alias, 'options'], { - publicationState, - }); + if (['oneToManyMorph', 'manyToManyMorph'].includes(nature)) { + this.populate({ path: alias, match: matchQuery, options: { publicationState } }); + } else if (populatedPaths.includes(alias)) { + _.set(this._mongooseOptions.populate, [alias, 'path'], `${alias}.ref`); + _.set(this._mongooseOptions.populate, [alias, 'options'], { + publicationState, + }); - if (matchQuery !== undefined) { - _.set(this._mongooseOptions.populate, [alias, 'match'], matchQuery); + if (matchQuery !== undefined) { + _.set(this._mongooseOptions.populate, [alias, 'match'], matchQuery); + } } - } - }); + }); + } if (_populateComponents) { componentAttributes.forEach(key => { diff --git a/packages/strapi-connector-mongoose/lib/queries.js b/packages/strapi-connector-mongoose/lib/queries.js index 3006dc6997..8d6b7947d2 100644 --- a/packages/strapi-connector-mongoose/lib/queries.js +++ b/packages/strapi-connector-mongoose/lib/queries.js @@ -33,9 +33,17 @@ module.exports = ({ model, strapi }) => { .filter(ast => ast.autoPopulate !== false) .map(ast => { const assocModel = strapi.db.getModelByAssoc(ast); + + const populateOptions = { + publicationState: options.publicationState, + _populateComponents: !_.isArray(ast.populate), + _populateMorphRelations: !_.isArray(ast.populate), + }; + const populate = { path: ast.alias, - options: { publicationState: options.publicationState }, + options: populateOptions, + select: _.isArray(ast.populate) ? ast.populate.join(' ') : null, }; if ( diff --git a/packages/strapi-plugin-i18n/config/functions/bootstrap.js b/packages/strapi-plugin-i18n/config/functions/bootstrap.js index cb99c990c2..2de3e694fd 100644 --- a/packages/strapi-plugin-i18n/config/functions/bootstrap.js +++ b/packages/strapi-plugin-i18n/config/functions/bootstrap.js @@ -26,15 +26,15 @@ module.exports = async () => { async beforeCreate(data) { await getService('localizations').assignDefaultLocale(data); }, - async afterCreate(entry) { - await getService('localizations').addLocalizations(entry, { model }); - }, - async afterUpdate(entry) { - await getService('localizations').updateNonLocalizedFields(entry, { model }); - }, - async afterDelete(entry) { - await getService('localizations').removeEntryFromRelatedLocalizations(entry, { model }); - }, + // async afterCreate(entry) { + // await getService('localizations').addLocalizations(entry, { model }); + // }, + // async afterUpdate(entry) { + // await getService('localizations').updateNonLocalizedFields(entry, { model }); + // }, + // async afterDelete(entry) { + // await getService('localizations').removeEntryFromRelatedLocalizations(entry, { model }); + // }, }); }); }; diff --git a/packages/strapi-plugin-i18n/config/functions/register.js b/packages/strapi-plugin-i18n/config/functions/register.js index 40d766be27..a766bd969e 100644 --- a/packages/strapi-plugin-i18n/config/functions/register.js +++ b/packages/strapi-plugin-i18n/config/functions/register.js @@ -10,7 +10,8 @@ module.exports = () => { writable: false, private: false, configurable: false, - type: 'json', + collection: model.modelName, + populate: ['id', 'locale', 'published_at'], }); _.set(model.attributes, 'locale', { diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index 7c606718ea..ed4394694f 100644 --- a/packages/strapi-utils/lib/models.js +++ b/packages/strapi-utils/lib/models.js @@ -394,6 +394,7 @@ module.exports = { dominant: details.dominant !== true, plugin: association.plugin || undefined, filter: details.filter, + populate: association.populate, }; if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') { @@ -421,6 +422,7 @@ module.exports = { dominant: details.dominant !== true, plugin: association.plugin || undefined, filter: details.filter, + populate: association.populate, }); return; } @@ -473,6 +475,7 @@ module.exports = { nature: infos.nature, autoPopulate: _.get(association, 'autoPopulate', true), filter: association.filter, + populate: association.populate, }); } catch (e) { strapi.log.error(