mirror of
https://github.com/strapi/strapi.git
synced 2025-10-27 16:10:08 +00:00
Init very simple filters system for the query layer to implement publication state filtering
This commit is contained in:
parent
435ce3bc88
commit
9dd2824824
@ -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()
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user