Refactor populateFetch in its own module

This commit is contained in:
Alexandre Bodin 2019-10-28 12:25:06 +01:00
parent 2ee39bc9d8
commit 80fb99f38e
5 changed files with 229 additions and 209 deletions

View File

@ -7,7 +7,7 @@ const { getComponentAttributes } = require('./utils/attributes');
const createComponentModels = async ({ model, definition, ORM, GLOBALS }) => {
const { collectionName, primaryKey } = definition;
const componentAttributes = getComponentAttributes(definition.attributes);
const componentAttributes = getComponentAttributes(definition);
if (componentAttributes.length > 0) {
// create component model
@ -59,7 +59,7 @@ const createComponentModels = async ({ model, definition, ORM, GLOBALS }) => {
const createComponentJoinTables = async ({ definition, ORM }) => {
const { collectionName, primaryKey } = definition;
const componentAttributes = getComponentAttributes(definition.attributes);
const componentAttributes = getComponentAttributes(definition);
if (componentAttributes.length > 0) {
const joinTable = `${collectionName}_components`;

View File

@ -11,8 +11,25 @@ const {
createComponentModels,
} = require('./generate-component-relations');
const populateFetch = require('./populate');
const PIVOT_PREFIX = '_pivot_';
const LIFECYCLES = {
creating: 'beforeCreate',
created: 'afterCreate',
destroying: 'beforeDestroy',
destroyed: 'afterDestroy',
updating: 'beforeUpdate',
updated: 'afterUpdate',
fetching: 'beforeFetch',
'fetching:collection': 'beforeFetchAll',
fetched: 'afterFetch',
'fetched:collection': 'afterFetchAll',
saving: 'beforeSave',
saved: 'afterSave',
};
const getDatabaseName = connection => {
const dbName = _.get(connection.settings, 'database');
const dbSchema = _.get(connection.settings, 'schema', 'public');
@ -570,202 +587,12 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
return attrs;
};
const findModelByAssoc = ({ assoc }) => {
const target = assoc.collection || assoc.model;
return assoc.plugin === 'admin'
? strapi.admin.models[target]
: assoc.plugin
? strapi.plugins[assoc.plugin].models[target]
: strapi.models[target];
};
const isPolymorphic = ({ assoc }) => {
return assoc.nature.toLowerCase().indexOf('morph') !== -1;
};
const formatPolymorphicPopulate = ({ assoc, path, prefix = '' }) => {
if (_.isString(path) && path === assoc.via) {
return { [`related.${assoc.via}`]: () => {} };
} else if (_.isString(path) && path === assoc.alias) {
// MorphTo side.
if (assoc.related) {
return { [`${prefix}${assoc.alias}.related`]: () => {} };
}
// oneToMorph or manyToMorph side.
// Retrieve collection name because we are using it to build our hidden model.
const model = findModelByAssoc({ assoc });
return {
[`${prefix}${assoc.alias}.${model.collectionName}`]: function(
query
) {
query.orderBy('created_at', 'desc');
},
};
}
};
const createAssociationPopulate = () => {
return definition.associations
.filter(ast => ast.autoPopulate !== false)
.map(assoc => {
if (isPolymorphic({ assoc })) {
return formatPolymorphicPopulate({
assoc,
path: assoc.alias,
});
}
let path = assoc.alias;
let extraAssocs = [];
if (assoc) {
const assocModel = findModelByAssoc({ assoc });
extraAssocs = assocModel.associations
.filter(assoc => isPolymorphic({ assoc }))
.map(assoc =>
formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix: `${path}.`,
})
);
}
return [assoc.alias, ...extraAssocs];
})
.reduce((acc, val) => acc.concat(val), []);
};
const populateComponent = key => {
const attr = definition.attributes[key];
if (attr.type === 'dynamiczone') return [`${key}.component`];
let paths = [];
const component = strapi.components[attr.component];
const assocs = (component.associations || []).filter(
assoc => assoc.autoPopulate === true
);
// paths.push(`${key}.component`);
assocs.forEach(assoc => {
if (isPolymorphic({ assoc })) {
const rel = formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix: `${key}.component.`,
});
paths.push(rel);
} else {
paths.push(`${key}.component.${assoc.alias}`);
}
});
return [`${key}.component`, ...paths];
};
const createComponentsPopulate = () => {
const componentsToPopulate = componentAttributes.reduce((acc, key) => {
const attribute = definition.attributes[key];
const autoPopulate = _.get(attribute, ['autoPopulate'], true);
if (autoPopulate === true) {
return acc.concat(populateComponent(key));
}
return acc;
}, []);
return componentsToPopulate;
};
const isComponent = (def, key) =>
_.get(def, ['attributes', key, 'type']) === 'component';
const formatPopulateOptions = withRelated => {
if (!Array.isArray(withRelated)) withRelated = [withRelated];
const obj = withRelated.reduce((acc, key) => {
if (_.isString(key)) {
acc[key] = () => {};
return acc;
}
return _.extend(acc, key);
}, {});
// if components are no
const finalObj = Object.keys(obj).reduce((acc, key) => {
// check the key path and update it if necessary nothing more
const parts = key.split('.');
let newKey;
let prefix = '';
let tmpModel = definition;
for (let part of parts) {
if (isComponent(tmpModel, part)) {
tmpModel = strapi.components[tmpModel.attributes[part].component];
// add component path and there relations / images
const path = `${prefix}${part}.component`;
newKey = path;
prefix = `${path}.`;
continue;
}
const assoc = tmpModel.associations.find(
association => association.alias === part
);
if (!assoc) return acc;
tmpModel = findModelByAssoc({ assoc });
if (isPolymorphic({ assoc })) {
const path = formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix,
});
return _.extend(acc, path);
}
newKey = `${prefix}${part}`;
prefix = `${newKey}.`;
}
acc[newKey] = obj[key];
return acc;
}, {});
return [finalObj];
};
// Initialize lifecycle callbacks.
loadedModel.initialize = function() {
// Load bookshelf plugin arguments from model options
this.constructor.__super__.initialize.apply(this, arguments);
const lifecycle = {
creating: 'beforeCreate',
created: 'afterCreate',
destroying: 'beforeDestroy',
destroyed: 'afterDestroy',
updating: 'beforeUpdate',
updated: 'afterUpdate',
fetching: 'beforeFetch',
'fetching:collection': 'beforeFetchAll',
fetched: 'afterFetch',
'fetched:collection': 'afterFetchAll',
saving: 'beforeSave',
saved: 'afterSave',
};
_.forEach(lifecycle, (fn, key) => {
_.forEach(LIFECYCLES, (fn, key) => {
if (_.isFunction(target[model.toLowerCase()][fn])) {
this.on(key, target[model.toLowerCase()][fn]);
}
@ -774,19 +601,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
// Update withRelated level to bypass many-to-many association for polymorphic relationshiips.
// Apply only during fetching.
this.on('fetching fetching:collection', (instance, attrs, options) => {
// do not populate anything
if (options.withRelated === false) return;
if (options.isEager === true) return;
if (_.isNil(options.withRelated)) {
options.withRelated = []
.concat(createComponentsPopulate())
.concat(createAssociationPopulate());
} else if (_.isEmpty(options.withRelated)) {
options.withRelated = createComponentsPopulate();
} else {
options.withRelated = formatPopulateOptions(options.withRelated);
}
populateFetch(definition, options);
return _.isFunction(target[model.toLowerCase()]['beforeFetchAll'])
? target[model.toLowerCase()]['beforeFetchAll']

View File

@ -0,0 +1,186 @@
'use strict';
const _ = require('lodash');
const { getComponentAttributes } = require('./utils/attributes');
const { findModelByAssoc, 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;
if (options.isEager === true) return;
if (_.isNil(options.withRelated)) {
options.withRelated = []
.concat(createComponentsPopulate(definition))
.concat(createAssociationPopulate(definition));
} else if (_.isEmpty(options.withRelated)) {
options.withRelated = createComponentsPopulate(definition);
} else {
options.withRelated = formatPopulateOptions(
definition,
options.withRelated
);
}
};
const isComponent = (def, key) =>
_.get(def, ['attributes', key, 'type']) === 'component';
const createAssociationPopulate = definition => {
return definition.associations
.filter(ast => ast.autoPopulate !== false)
.map(assoc => {
if (isPolymorphic({ assoc })) {
return formatPolymorphicPopulate({
assoc,
path: assoc.alias,
});
}
let path = assoc.alias;
let extraAssocs = [];
if (assoc) {
const assocModel = findModelByAssoc({ assoc });
extraAssocs = assocModel.associations
.filter(assoc => isPolymorphic({ assoc }))
.map(assoc =>
formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix: `${path}.`,
})
);
}
return [assoc.alias, ...extraAssocs];
})
.reduce((acc, val) => acc.concat(val), []);
};
const formatPopulateOptions = (definition, withRelated) => {
if (!Array.isArray(withRelated)) withRelated = [withRelated];
const obj = withRelated.reduce((acc, key) => {
if (_.isString(key)) {
acc[key] = () => {};
return acc;
}
return _.extend(acc, key);
}, {});
// if components are no
const finalObj = Object.keys(obj).reduce((acc, key) => {
// check the key path and update it if necessary nothing more
const parts = key.split('.');
let newKey;
let prefix = '';
let tmpModel = definition;
for (let part of parts) {
if (isComponent(tmpModel, part)) {
tmpModel = strapi.components[tmpModel.attributes[part].component];
// add component path and there relations / images
const path = `${prefix}${part}.component`;
newKey = path;
prefix = `${path}.`;
continue;
}
const assoc = tmpModel.associations.find(
association => association.alias === part
);
if (!assoc) return acc;
tmpModel = findModelByAssoc({ assoc });
if (isPolymorphic({ assoc })) {
const path = formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix,
});
return _.extend(acc, path);
}
newKey = `${prefix}${part}`;
prefix = `${newKey}.`;
}
acc[newKey] = obj[key];
return acc;
}, {});
return [finalObj];
};
const populateComponent = (key, attr) => {
if (attr.type === 'dynamiczone') return [`${key}.component`];
let paths = [];
const component = strapi.components[attr.component];
const assocs = (component.associations || []).filter(
assoc => assoc.autoPopulate === true
);
// paths.push(`${key}.component`);
assocs.forEach(assoc => {
if (isPolymorphic({ assoc })) {
const rel = formatPolymorphicPopulate({
assoc,
path: assoc.alias,
prefix: `${key}.component.`,
});
paths.push(rel);
} else {
paths.push(`${key}.component.${assoc.alias}`);
}
});
return [`${key}.component`, ...paths];
};
const createComponentsPopulate = definition => {
return getComponentAttributes(definition).reduce((acc, key) => {
const attribute = definition.attributes[key];
const autoPopulate = _.get(attribute, ['autoPopulate'], true);
if (autoPopulate === true) {
return acc.concat(populateComponent(key, attribute));
}
return acc;
}, []);
};
const formatPolymorphicPopulate = ({ assoc, path, prefix = '' }) => {
if (_.isString(path) && path === assoc.via) {
return { [`related.${assoc.via}`]: () => {} };
} else if (_.isString(path) && path === assoc.alias) {
// MorphTo side.
if (assoc.related) {
return { [`${prefix}${assoc.alias}.related`]: () => {} };
}
// oneToMorph or manyToMorph side.
// Retrieve collection name because we are using it to build our hidden model.
const model = findModelByAssoc({ assoc });
return {
[`${prefix}${assoc.alias}.${model.collectionName}`]: function(query) {
query.orderBy('created_at', 'desc');
},
};
}
};
module.exports = populateFetch;

View File

@ -0,0 +1,19 @@
'use strict';
const findModelByAssoc = ({ assoc }) => {
const target = assoc.collection || assoc.model;
return assoc.plugin === 'admin'
? strapi.admin.models[target]
: assoc.plugin
? strapi.plugins[assoc.plugin].models[target]
: strapi.models[target];
};
const isPolymorphic = ({ assoc }) => {
return assoc.nature.toLowerCase().indexOf('morph') !== -1;
};
module.exports = {
findModelByAssoc,
isPolymorphic,
};

View File

@ -3,9 +3,9 @@
/**
* Returns the attribute keys of the component related attributes
*/
function getComponentAttributes(attributes) {
return Object.keys(attributes).filter(key =>
['component', 'dynamiczone'].includes(attributes[key].type)
function getComponentAttributes(definition) {
return Object.keys(definition.attributes).filter(key =>
['component', 'dynamiczone'].includes(definition.attributes[key].type)
);
}