Init very simple filters system for the query layer to implement publication state filtering

This commit is contained in:
Alexandre Bodin 2021-10-13 14:06:16 +02:00
parent 435ce3bc88
commit 9dd2824824
7 changed files with 148 additions and 119 deletions

View File

@ -142,7 +142,7 @@ const createEntityManager = db => {
await db.lifecycles.run('beforeCount', uid, { params });
const res = await this.createQueryBuilder(uid)
.init(_.pick(['_q', 'where'], params))
.init(_.pick(['_q', 'where', 'filters'], params))
.count()
.first()
.execute();
@ -172,7 +172,7 @@ const createEntityManager = db => {
await this.attachRelations(uid, id, data);
// TODO: in case there is not select or populate specified return the inserted data ?
// TODO: in case there is no select or populate specified return the inserted data ?
// TODO: do not trigger the findOne lifecycles ?
const result = await this.findOne(uid, {
where: { id },
@ -296,7 +296,7 @@ const createEntityManager = db => {
throw new Error('Delete requires a where parameter');
}
// TODO: avoid trigger the findOne lifecycles in the case ?
// TODO: do not trigger the findOne lifecycles ?
const entity = await this.findOne(uid, {
select: select && ['id'].concat(select),
where,
@ -469,7 +469,6 @@ const createEntityManager = db => {
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
// TODO: validate logic of delete
if (isOneToAny(attribute) && isBidirectional(attribute)) {
await this.createQueryBuilder(joinTable.name)
.delete()

View File

@ -122,7 +122,11 @@ const applyPopulate = async (results, populate, ctx) => {
const attribute = meta.attributes[key];
const targetMeta = db.metadata.get(attribute.target);
const populateValue = pickPopulateParams(populate[key]);
const populateValue = {
...pickPopulateParams(populate[key]),
filters: qb.state.filters,
};
const isCount = populateValue.count === true;
const fromTargetRow = rowOrRows => fromRow(targetMeta, rowOrRows);

View File

@ -29,6 +29,7 @@ const createQueryBuilder = (uid, db) => {
return {
alias: getAlias(),
getAlias,
state,
select(args) {
state.type = 'select';
@ -115,7 +116,7 @@ const createQueryBuilder = (uid, db) => {
},
init(params = {}) {
const { _q, where, select, limit, offset, orderBy, groupBy, populate } = params;
const { _q, filters, where, select, limit, offset, orderBy, groupBy, populate } = params;
if (!_.isNil(where)) {
this.where(where);
@ -151,9 +152,17 @@ const createQueryBuilder = (uid, db) => {
this.populate(populate);
}
if (!_.isNil(filters)) {
this.filters(filters);
}
return this;
},
filters(filters) {
state.filters = filters;
},
first() {
state.first = true;
return this;
@ -206,6 +215,19 @@ const createQueryBuilder = (uid, db) => {
processState() {
state.orderBy = helpers.processOrderBy(state.orderBy, { qb: this, uid, db });
if (!_.isNil(state.filters)) {
if (_.isFunction(state.filters)) {
const filters = state.filters({ qb: this, uid, meta, db });
if (!_.isNil(filters)) {
state.where.push(filters);
}
} else {
state.where.push(state.filters);
}
}
state.where = helpers.processWhere(state.where, { qb: this, uid, db });
state.populate = helpers.processPopulate(state.populate, { qb: this, uid, db });
state.data = helpers.toRow(meta, state.data);

View File

@ -1,7 +1,6 @@
'use strict';
const delegate = require('delegates');
const { pipe } = require('lodash/fp');
const {
sanitizeEntity,
@ -16,12 +15,7 @@ const {
updateComponents,
deleteComponents,
} = require('./components');
const {
transformCommonParams,
transformPaginationParams,
transformParamsToQuery,
pickSelectionParams,
} = require('./params');
const { transformParamsToQuery, pickSelectionParams } = require('./params');
// TODO: those should be strapi events used by the webhooks not the other way arround
const { ENTRY_CREATE, ENTRY_UPDATE, ENTRY_DELETE } = webhookUtils.webhookEvents;
@ -231,13 +225,34 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const attribute = attributes[field];
const loadParams =
attribute.type === 'relation'
? transformParamsToQuery(attribute.target, params)
: pipe(
transformCommonParams,
transformPaginationParams
)(params);
const loadParams = {};
switch (attribute.type) {
case 'relation': {
Object.assign(loadParams, transformParamsToQuery(attribute.target, params));
break;
}
case 'component': {
Object.assign(loadParams, transformParamsToQuery(attribute.component, params));
break;
}
case 'dynamiczone': {
Object.assign(loadParams, transformParamsToQuery(null, params));
break;
}
case 'media': {
Object.assign(loadParams, transformParamsToQuery('plugin::upload.file', params));
break;
}
}
// const loadParams =
// attribute.type === 'relation'
// ? transformParamsToQuery(attribute.target, params)
// : pipe(
// transformCommonParams,
// transformPaginationParams
// )(params);
return db.query(uid).load(entity, field, loadParams);
},

View File

@ -1,6 +1,7 @@
'use strict';
const { pick, pipe, isNil } = require('lodash/fp');
const assert = require('assert').strict;
const { pick, isNil, toNumber, isInteger } = require('lodash/fp');
const {
convertSortQueryParams,
@ -9,41 +10,39 @@ const {
convertPopulateQueryParams,
convertFiltersQueryParams,
convertFieldsQueryParams,
convertPublicationStateParams,
} = require('@strapi/utils/lib/convert-query-params');
const { contentTypes: contentTypesUtils } = require('@strapi/utils');
const pickSelectionParams = pick(['fields', 'populate']);
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
const transformParamsToQuery = (uid, params) => {
// NOTE: can be a CT, a Compo or nothing in the case of polymorphism (DZ & morph relations)
const type = strapi.getModel(uid);
const transformCommonParams = (params = {}) => {
const { _q, sort, filters, fields, populate, ...query } = params;
const query = {};
if (_q) {
const { _q, sort, filters, fields, populate, page, pageSize, start, limit } = params;
if (!isNil(_q)) {
query._q = _q;
}
if (sort) {
if (!isNil(sort)) {
query.orderBy = convertSortQueryParams(sort);
}
if (filters) {
if (!isNil(filters)) {
query.where = convertFiltersQueryParams(filters);
}
if (fields) {
if (!isNil(fields)) {
query.select = convertFieldsQueryParams(fields);
}
if (populate) {
if (!isNil(populate)) {
query.populate = convertPopulateQueryParams(populate);
}
return query;
};
const transformPaginationParams = (params = {}) => {
const { page, pageSize, start, limit, ...query } = params;
const isPagePagination = !isNil(page) || !isNil(pageSize);
const isOffsetPagination = !isNil(start) || !isNil(limit);
@ -53,72 +52,38 @@ const transformPaginationParams = (params = {}) => {
);
}
if (page) {
query.page = Number(page);
if (!isNil(page)) {
const pageVal = toNumber(page);
const isValid = isInteger(pageVal) && pageVal > 0;
assert(isValid, `Invalid 'page' parameter. Expected an integer > 0, received: ${page}`);
query.page = pageVal;
}
if (pageSize) {
query.pageSize = Number(pageSize);
if (!isNil(pageSize)) {
const pageSizeVal = toNumber(pageSize);
const isValid = isInteger(pageSizeVal) && pageSizeVal > 0;
assert(isValid, `Invalid 'pageSize' parameter. Expected an integer > 0, received: ${page}`);
query.pageSize = pageSizeVal;
}
if (start) {
if (!isNil(start)) {
query.offset = convertStartQueryParams(start);
}
if (limit) {
if (!isNil(limit)) {
query.limit = convertLimitQueryParams(limit);
}
return query;
};
const transformPublicationStateParams = uid => (params = {}) => {
const contentType = strapi.getModel(uid);
if (!contentType) {
return params;
}
const { publicationState, ...query } = params;
if (publicationState && contentTypesUtils.hasDraftAndPublish(contentType)) {
const { publicationState = 'live' } = params;
const liveClause = {
[PUBLISHED_AT_ATTRIBUTE]: {
$notNull: true,
},
};
if (publicationState === 'live') {
query.where = {
$and: [liveClause].concat(query.where || []),
};
// TODO: propagate nested publicationState filter somehow
}
}
convertPublicationStateParams(type, params, query);
return query;
};
const pickSelectionParams = pick(['fields', 'populate']);
const transformParamsToQuery = (uid, params) => {
return pipe(
// _q, filters, etc...
transformCommonParams,
// page, pageSize, start, limit
transformPaginationParams,
// publicationState
transformPublicationStateParams(uid)
)(params);
};
module.exports = {
transformCommonParams,
transformPublicationStateParams,
transformPaginationParams,
transformParamsToQuery,
pickSelectionParams,
};

View File

@ -171,6 +171,7 @@ describe('Publication State', () => {
const res = await rq({ method: 'GET', url: `${baseUrl}${query}` });
expect(res.body.data).toHaveLength(lengthFor(modelName, { mode }));
expect(res.body.meta.pagination.total).toBe(lengthFor(modelName, { mode }));
});
});
@ -190,7 +191,7 @@ describe('Publication State', () => {
},
});
products = res.body.data.map(res => ({ id: res.id, ...res.attributes }));
products = res.body.data;
});
test('Payload integrity', () => {
@ -199,35 +200,29 @@ describe('Publication State', () => {
test('Root level', () => {
products.forEach(product => {
expect(product.publishedAt).toBeISODate();
expect(product.attributes.publishedAt).toBeISODate();
});
});
// const getApiRef = id => data.product.find(product => product.id === id);
test('First level (categories) to be published only', () => {
products.forEach(({ attributes }) => {
const categories = attributes.categories.data;
test.todo('First level (categories)');
categories.forEach(category => {
expect(category.attributes.publishedAt).toBeISODate();
});
});
});
// products.forEach(({ id, categories }) => {
// const length = getApiRef(id).categories.filter(c => c.publishedAt !== null).length;
// expect(categories).toHaveLength(length);
test('Second level through component (countries) to be published only', () => {
products.forEach(({ attributes }) => {
const countries = attributes.comp.countries.data;
// categories.forEach(category => {
// expect(category.publishedAt).toBeISODate();
// });
// });
// });
test.todo('Second level through component (countries)');
// products.forEach(({ id, comp: { countries } }) => {
// const length = getApiRef(id).comp.countries.filter(c => c.publishedAt !== null).length;
// expect(countries).toHaveLength(length);
// countries.forEach(country => {
// expect(country.publishedAt).toBeISODate();
// });
// });
// });
countries.forEach(country => {
expect(country.attributes.publishedAt).toBeISODate();
});
});
});
});
});
});

View File

@ -4,9 +4,12 @@
* Converts the standard Strapi REST query params to a more usable format for querying
* You can read more here: https://strapi.io/documentation/developer-docs/latest/developer-resources/content-api/content-api.html#filters
*/
const { has } = require('lodash/fp');
const _ = require('lodash');
const parseType = require('./parse-type');
const contentTypesUtils = require('./content-types');
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
class InvalidOrderError extends Error {
constructor() {
@ -130,13 +133,15 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
if (Array.isArray(populate)) {
// map convert
return populate.flatMap(value => {
return _.uniq(
populate.flatMap(value => {
if (typeof value !== 'string') {
throw new InvalidPopulateError();
}
return value.split(',').map(value => _.trim(value));
});
})
);
}
if (_.isPlainObject(populate)) {
@ -144,6 +149,7 @@ const convertPopulateQueryParams = (populate, depth = 0) => {
for (const key in populate) {
transformedPopulate[key] = convertNestedPopulate(populate[key]);
}
return transformedPopulate;
}
@ -210,9 +216,31 @@ const convertFieldsQueryParams = (fields, depth = 0) => {
throw new Error('Invalid fields parameter. Expected a string or an array of strings');
};
// NOTE: We could validate the parameters are on existing / non private attributes
const convertFiltersQueryParams = filters => filters;
const convertPublicationStateParams = (type, params = {}, query = {}) => {
if (!type) {
return;
}
const { publicationState } = params;
if (!_.isNil(publicationState)) {
if (!contentTypesUtils.constants.DP_PUB_STATES.includes(publicationState)) {
throw new Error(
`Invalid publicationState. Expected one of 'preview','live' received: ${publicationState}.`
);
}
// NOTE: this is the query layer filters not the entity service filters
query.filters = ({ meta }) => {
if (publicationState === 'live' && has(PUBLISHED_AT_ATTRIBUTE, meta.attributes)) {
return { [PUBLISHED_AT_ATTRIBUTE]: { $notNull: true } };
}
};
}
};
module.exports = {
convertSortQueryParams,
convertStartQueryParams,
@ -220,4 +248,5 @@ module.exports = {
convertPopulateQueryParams,
convertFiltersQueryParams,
convertFieldsQueryParams,
convertPublicationStateParams,
};