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",
"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"
}
}
}

View File

@ -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),
};
};

View File

@ -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<Function>
*/
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,
};

View File

@ -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 => {

View File

@ -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 (

View File

@ -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 });
// },
});
});
};

View File

@ -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', {

View File

@ -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(