Move to many way & add populate option on assoc

This commit is contained in:
Alexandre Bodin 2021-03-05 15:43:48 +01:00
parent 0ac4e8ed3c
commit 996341f5d5
8 changed files with 253 additions and 143 deletions

View File

@ -1,13 +1,17 @@
{ {
"kind": "collectionType",
"collectionName": "restaurants", "collectionName": "restaurants",
"info": { "info": {
"name": "restaurant", "name": "restaurant",
"description": "" "description": ""
}, },
"options": { "options": {
"draftAndPublish": false, "draftAndPublish": true,
"increments": true, "increments": true,
"timestamps": ["created_at", "updated_at"], "timestamps": [
"created_at",
"updated_at"
],
"comment": "" "comment": ""
}, },
"pluginOptions": { "pluginOptions": {
@ -20,22 +24,53 @@
"maxLength": 50, "maxLength": 50,
"required": true, "required": true,
"minLength": 5, "minLength": 5,
"type": "string" "type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"slug": { "slug": {
"type": "uid", "type": "uid",
"targetField": "name" "targetField": "name",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"price_range": { "price_range": {
"enum": ["very_cheap", "cheap", "average", "expensive", "very_expensive"], "enum": [
"type": "enumeration" "very_cheap",
"cheap",
"average",
"expensive",
"very_expensive"
],
"type": "enumeration",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"closing_period": { "closing_period": {
"component": "default.closingperiod", "component": "default.closingperiod",
"type": "component" "type": "component",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"contact_email": { "contact_email": {
"type": "email" "type": "email",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"stars": { "stars": {
"required": true, "required": true,
@ -67,10 +102,20 @@
"required": false "required": false
}, },
"short_description": { "short_description": {
"type": "text" "type": "text",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"since": { "since": {
"type": "date" "type": "date",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"categories": { "categories": {
"collection": "category" "collection": "category"
@ -78,12 +123,22 @@
"description": { "description": {
"type": "richtext", "type": "richtext",
"required": true, "required": true,
"minLength": 10 "minLength": 10,
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"services": { "services": {
"component": "default.restaurantservice", "component": "default.restaurantservice",
"repeatable": true, "repeatable": true,
"type": "component" "type": "component",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"menu": { "menu": {
"model": "menu", "model": "menu",
@ -94,7 +149,12 @@
"type": "component", "type": "component",
"repeatable": true, "repeatable": true,
"min": 1, "min": 1,
"max": 10 "max": 10,
"pluginOptions": {
"i18n": {
"localized": true
}
}
}, },
"dz": { "dz": {
"type": "dynamiczone", "type": "dynamiczone",
@ -103,7 +163,21 @@
"default.restaurantservice", "default.restaurantservice",
"default.closingperiod", "default.closingperiod",
"default.dish" "default.dish"
] ],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"parent": {
"collection": "restaurant",
"via": "children",
"dominant": true
},
"children": {
"collection": "restaurant",
"via": "parent"
} }
} }
} }

View File

@ -1,14 +1,17 @@
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const pq = require('./utils/populate-queries'); const {
bindPopulateQueries,
extendWithPopulateQueries,
queryOptionsToQueryMap,
} = require('./utils/populate-queries');
const { getComponentAttributes, isComponent } = require('./utils/attributes'); const { getComponentAttributes, isComponent } = require('./utils/attributes');
const { isPolymorphic } = require('./utils/associations'); const { isPolymorphic } = require('./utils/associations');
/** /**
* Create utilities to populate a model on fetch * Create utilities to populate a model on fetch
*/ */
const populateFetch = (definition, options) => { const populateFetch = (definition, options) => {
// do not populate anything // do not populate anything
if (options.withRelated === false) return; 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 return definition.associations
.filter(ast => ast.autoPopulate !== false) .filter(ast => ast.autoPopulate !== false)
.map(assoc => { .map(assoc => {
if (isPolymorphic({ assoc })) { if (isPolymorphic({ assoc })) {
return formatPolymorphicPopulate({ return formatPolymorphicPopulate({ assoc }, options);
assoc,
prefix,
publicationState,
});
} }
return formatAssociationPopulate({ assoc }, { prefix, publicationState }); return formatAssociationPopulate({ assoc }, options);
}) })
.reduce((acc, val) => acc.concat(val), []); .reduce((acc, val) => acc.concat(val), []);
}; };
const populateBareAssociations = (definition, { prefix = '', publicationState } = {}) => { const populateBareAssociations = (definition, options = {}) => {
const { prefix = '', ...queryOptions } = options;
return (definition.associations || []) return (definition.associations || [])
.filter(ast => ast.autoPopulate !== false) .filter(ast => ast.autoPopulate !== false)
.map(assoc => { .map(assoc => {
if (isPolymorphic({ assoc })) { if (isPolymorphic({ assoc })) {
return formatPolymorphicPopulate({ return formatPolymorphicPopulate({ assoc }, options);
assoc,
prefix,
publicationState,
});
} }
const path = `${prefix}${assoc.alias}`; const path = `${prefix}${assoc.alias}`;
const assocModel = strapi.db.getModelByAssoc(assoc); const assocModel = strapi.db.getModelByAssoc(assoc);
const polyAssocs = assocModel.associations const populateOptions = bindPopulateQueries(
.filter(assoc => isPolymorphic({ assoc })) [path],
.map(assoc => queryOptionsToQueryMap(queryOptions, { model: assocModel })
formatPolymorphicPopulate({
assoc,
prefix: `${path}.`,
publicationState,
})
); );
return [ const polyAssocs = assocModel.associations
pq.bindPopulateQueries([path], { .filter(assoc => isPolymorphic({ assoc }))
publicationState: { query: publicationState, model: assocModel }, .map(assoc => {
}), return formatPolymorphicPopulate({ assoc }, { prefix: `${path}.`, ...queryOptions });
...polyAssocs, });
];
return [populateOptions, ...polyAssocs];
}) })
.reduce((acc, val) => acc.concat(val), []); .reduce((acc, val) => acc.concat(val), []);
}; };
const formatAssociationPopulate = ({ assoc, prefix = '' }, options = {}) => { const hasCustomPopulate = assoc => _.isArray(assoc.populate);
const { publicationState } = options;
const formatAssociationPopulate = ({ assoc }, options = {}) => {
const { prefix = '', ...queryOptions } = options;
const path = `${prefix}${assoc.alias}`; const path = `${prefix}${assoc.alias}`;
const assocModel = strapi.db.getModelByAssoc(assoc); const assocModel = strapi.db.getModelByAssoc(assoc);
const polyAssocs = assocModel.associations const polyAssocs = hasCustomPopulate(assoc)
.filter(assoc => isPolymorphic({ assoc })) ? []
.map(assoc => : assocModel.associations
formatPolymorphicPopulate({ .filter(polyAssoc => isPolymorphic({ assoc: polyAssoc }))
assoc, .map(polyAssoc => {
prefix: `${path}.`, return formatPolymorphicPopulate(
publicationState, { assoc: polyAssoc },
}) { prefix: `${path}.`, ...queryOptions }
);
});
const components = hasCustomPopulate(assoc)
? []
: populateComponents(assocModel, { prefix: `${path}.`, ...queryOptions });
const populateOpts = bindPopulateQueries(
[path],
queryOptionsToQueryMap(queryOptions, { model: assocModel })
); );
const components = populateComponents(assocModel, { prefix: `${path}.`, publicationState }); return [selectPopulateFields({ assoc, assocModel }, populateOpts), ...polyAssocs, ...components];
return [
pq.bindPopulateQueries([path], {
publicationState: { query: publicationState, model: assocModel },
}),
...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) return getComponentAttributes(definition)
.map(key => { .map(key => {
const attribute = definition.attributes[key]; const attribute = definition.attributes[key];
const autoPopulate = _.get(attribute, ['autoPopulate'], true); const autoPopulate = _.get(attribute, ['autoPopulate'], true);
if (autoPopulate === true) { if (autoPopulate === true) {
return populateComponent(key, attribute, { prefix, publicationState }); return populateComponent(key, attribute, options);
} }
}, []) })
.reduce((acc, val) => acc.concat(val), []); .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 path = `${prefix}${key}.component`;
const componentPrefix = `${path}.`; const componentPrefix = `${path}.`;
@ -129,12 +146,12 @@ const populateComponent = (key, attr, { prefix = '', publicationState } = {}) =>
const component = strapi.components[key]; const component = strapi.components[key];
const assocs = populateBareAssociations(component, { const assocs = populateBareAssociations(component, {
prefix: componentPrefix, prefix: componentPrefix,
publicationState, ...queryOptions,
}); });
const components = populateComponents(component, { const components = populateComponents(component, {
prefix: componentPrefix, prefix: componentPrefix,
publicationState, ...queryOptions,
}); });
return acc.concat([path, ...assocs, ...components]); return acc.concat([path, ...assocs, ...components]);
@ -142,20 +159,14 @@ const populateComponent = (key, attr, { prefix = '', publicationState } = {}) =>
} }
const component = strapi.components[attr.component]; const component = strapi.components[attr.component];
const assocs = populateBareAssociations(component, { const assocs = populateBareAssociations(component, { prefix: componentPrefix, ...queryOptions });
prefix: componentPrefix,
publicationState,
});
const components = populateComponents(component, { const components = populateComponents(component, { prefix: componentPrefix, ...queryOptions });
prefix: componentPrefix,
publicationState,
});
return [path, ...assocs, ...components]; return [path, ...assocs, ...components];
}; };
const formatPopulateOptions = (definition, { withRelated, publicationState } = {}) => { const formatPopulateOptions = (definition, { withRelated, ...queryOptions } = {}) => {
if (!Array.isArray(withRelated)) withRelated = [withRelated]; if (!Array.isArray(withRelated)) withRelated = [withRelated];
const obj = withRelated.reduce((acc, key) => { const obj = withRelated.reduce((acc, key) => {
@ -197,11 +208,7 @@ const formatPopulateOptions = (definition, { withRelated, publicationState } = {
tmpModel = strapi.db.getModelByAssoc(assoc); tmpModel = strapi.db.getModelByAssoc(assoc);
if (isPolymorphic({ assoc })) { if (isPolymorphic({ assoc })) {
const path = formatPolymorphicPopulate({ const path = formatPolymorphicPopulate({ assoc }, { prefix, ...queryOptions });
assoc,
prefix,
publicationState,
});
return _.extend(acc, path); return _.extend(acc, path);
} }
@ -210,9 +217,10 @@ const formatPopulateOptions = (definition, { withRelated, publicationState } = {
prefix = `${newKey}.`; prefix = `${newKey}.`;
_.extend(acc, { _.extend(acc, {
[newKey]: pq.extendWithPopulateQueries([obj[newKey], acc[newKey]], { [newKey]: extendWithPopulateQueries(
publicationState: { query: publicationState, model: tmpModel }, [obj[newKey], acc[newKey]],
}), queryOptionsToQueryMap(queryOptions, { model: tmpModel })
),
}); });
} }
@ -222,15 +230,18 @@ const formatPopulateOptions = (definition, { withRelated, publicationState } = {
return [finalObj]; 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 model = strapi.db.getModelByAssoc(assoc);
const populateOptions = {
publicationState: { query: publicationState, model }, const queryMap = queryOptionsToQueryMap(queryOptions, { model });
};
// MorphTo side. // MorphTo side.
if (assoc.related) { if (assoc.related) {
return pq.bindPopulateQueries([`${prefix}${assoc.alias}.related`], populateOptions); return bindPopulateQueries([`${prefix}${assoc.alias}.related`], queryMap);
} }
// oneToMorph or manyToMorph side. // oneToMorph or manyToMorph side.
@ -238,14 +249,7 @@ const formatPolymorphicPopulate = ({ assoc, prefix = '', publicationState }) =>
const path = `${prefix}${assoc.alias}.${model.collectionName}`; const path = `${prefix}${assoc.alias}.${model.collectionName}`;
return { return {
[path]: pq.extendWithPopulateQueries( [path]: extendWithPopulateQueries([defaultOrderBy], queryMap),
[
qb => {
qb.orderBy('created_at', 'desc');
},
],
populateOptions
),
}; };
}; };

View File

@ -23,12 +23,13 @@ const optionsMap = {
}, },
}; };
const availableOptions = Object.keys(optionsMap); const isValidOption = optionName => _.has(optionsMap, optionName);
const isValidOption = option => availableOptions.includes(option);
const validate = (option, params) => { const validate = (optionName, params) => {
const opt = _.get(optionsMap, option, {}); const opt = _.get(optionsMap, optionName, {});
return !_.isFunction(opt.validate) || opt.validate(params); return !_.isFunction(opt.validate) || opt.validate(params);
}; };
const resolveQuery = (option, params) => optionsMap[option].queries[params.query](params); const resolveQuery = (option, params) => optionsMap[option].queries[params.query](params);
/** /**
@ -37,9 +38,9 @@ const resolveQuery = (option, params) => optionsMap[option].queries[params.query
* @returns Array<Function> * @returns Array<Function>
*/ */
const toQueries = options => { const toQueries = options => {
return _.reduce( return Object.keys(options).reduce((acc, key) => {
options, const params = options[key];
(acc, params, key) => {
if (isValidOption(key) && validate(key, params)) { if (isValidOption(key) && validate(key, params)) {
const query = resolveQuery(key, params); const query = resolveQuery(key, params);
if (_.isFunction(query)) { if (_.isFunction(query)) {
@ -47,9 +48,7 @@ const toQueries = options => {
} }
} }
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 = { module.exports = {
toQueries, toQueries,
runPopulateQueries, runPopulateQueries,
bindPopulateQueries, bindPopulateQueries,
extendWithPopulateQueries, extendWithPopulateQueries,
queryOptionsToQueryMap,
}; };

View File

@ -319,21 +319,24 @@ const migrateSchema = () => {};
const createOnFetchPopulateFn = ({ morphAssociations, componentAttributes, definition }) => { const createOnFetchPopulateFn = ({ morphAssociations, componentAttributes, definition }) => {
return function() { return function() {
const populatedPaths = this.getPopulatedPaths(); const populatedPaths = this.getPopulatedPaths();
const { publicationState, _populateComponents = true } = this.getOptions(); const {
publicationState,
_populateComponents = true,
_populateMorphRelations = true,
} = this.getOptions();
const getMatchQuery = assoc => { const getMatchQuery = assoc => {
const assocModel = strapi.db.getModelByAssoc(assoc); const assocModel = strapi.db.getModelByAssoc(assoc);
if ( const hasDraftAndPublish = contentTypesUtils.hasDraftAndPublish(assocModel);
contentTypesUtils.hasDraftAndPublish(assocModel) && if (hasDraftAndPublish && DP_PUB_STATES.includes(publicationState)) {
DP_PUB_STATES.includes(publicationState)
) {
return populateQueries.publicationState[publicationState]; return populateQueries.publicationState[publicationState];
} }
return undefined; return undefined;
}; };
if (_populateMorphRelations) {
morphAssociations.forEach(association => { morphAssociations.forEach(association => {
const matchQuery = getMatchQuery(association); const matchQuery = getMatchQuery(association);
const { alias, nature } = association; const { alias, nature } = association;
@ -351,6 +354,7 @@ const createOnFetchPopulateFn = ({ morphAssociations, componentAttributes, defin
} }
} }
}); });
}
if (_populateComponents) { if (_populateComponents) {
componentAttributes.forEach(key => { componentAttributes.forEach(key => {

View File

@ -33,9 +33,17 @@ module.exports = ({ model, strapi }) => {
.filter(ast => ast.autoPopulate !== false) .filter(ast => ast.autoPopulate !== false)
.map(ast => { .map(ast => {
const assocModel = strapi.db.getModelByAssoc(ast); const assocModel = strapi.db.getModelByAssoc(ast);
const populateOptions = {
publicationState: options.publicationState,
_populateComponents: !_.isArray(ast.populate),
_populateMorphRelations: !_.isArray(ast.populate),
};
const populate = { const populate = {
path: ast.alias, path: ast.alias,
options: { publicationState: options.publicationState }, options: populateOptions,
select: _.isArray(ast.populate) ? ast.populate.join(' ') : null,
}; };
if ( if (

View File

@ -26,15 +26,15 @@ module.exports = async () => {
async beforeCreate(data) { async beforeCreate(data) {
await getService('localizations').assignDefaultLocale(data); await getService('localizations').assignDefaultLocale(data);
}, },
async afterCreate(entry) { // async afterCreate(entry) {
await getService('localizations').addLocalizations(entry, { model }); // await getService('localizations').addLocalizations(entry, { model });
}, // },
async afterUpdate(entry) { // async afterUpdate(entry) {
await getService('localizations').updateNonLocalizedFields(entry, { model }); // await getService('localizations').updateNonLocalizedFields(entry, { model });
}, // },
async afterDelete(entry) { // async afterDelete(entry) {
await getService('localizations').removeEntryFromRelatedLocalizations(entry, { model }); // await getService('localizations').removeEntryFromRelatedLocalizations(entry, { model });
}, // },
}); });
}); });
}; };

View File

@ -10,7 +10,8 @@ module.exports = () => {
writable: false, writable: false,
private: false, private: false,
configurable: false, configurable: false,
type: 'json', collection: model.modelName,
populate: ['id', 'locale', 'published_at'],
}); });
_.set(model.attributes, 'locale', { _.set(model.attributes, 'locale', {

View File

@ -394,6 +394,7 @@ module.exports = {
dominant: details.dominant !== true, dominant: details.dominant !== true,
plugin: association.plugin || undefined, plugin: association.plugin || undefined,
filter: details.filter, filter: details.filter,
populate: association.populate,
}; };
if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') { if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') {
@ -421,6 +422,7 @@ module.exports = {
dominant: details.dominant !== true, dominant: details.dominant !== true,
plugin: association.plugin || undefined, plugin: association.plugin || undefined,
filter: details.filter, filter: details.filter,
populate: association.populate,
}); });
return; return;
} }
@ -473,6 +475,7 @@ module.exports = {
nature: infos.nature, nature: infos.nature,
autoPopulate: _.get(association, 'autoPopulate', true), autoPopulate: _.get(association, 'autoPopulate', true),
filter: association.filter, filter: association.filter,
populate: association.populate,
}); });
} catch (e) { } catch (e) {
strapi.log.error( strapi.log.error(