2021-08-30 13:58:04 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/**
|
2021-09-01 21:13:23 +02:00
|
|
|
* Converts the standard Strapi REST query params to a more usable format for querying
|
2021-11-29 07:01:25 -08:00
|
|
|
* You can read more here: https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#filters
|
2021-08-30 13:58:04 +02:00
|
|
|
*/
|
2021-12-15 11:15:58 +01:00
|
|
|
const { has, isEmpty } = require('lodash/fp');
|
2021-08-30 13:58:04 +02:00
|
|
|
const _ = require('lodash');
|
2021-09-24 17:39:09 +02:00
|
|
|
const parseType = require('./parse-type');
|
2021-10-13 14:06:16 +02:00
|
|
|
const contentTypesUtils = require('./content-types');
|
|
|
|
|
|
|
|
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
2021-08-30 13:58:04 +02:00
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
class InvalidOrderError extends Error {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.message = 'Invalid order. order can only be one of asc|desc|ASC|DESC';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
class InvalidSortError extends Error {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.message =
|
2021-09-01 21:13:23 +02:00
|
|
|
'Invalid sort parameter. Expected a string, an array of strings, a sort object or an array of sort objects';
|
2021-08-30 19:34:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const validateOrder = order => {
|
|
|
|
if (!['asc', 'desc'].includes(order.toLocaleLowerCase())) {
|
|
|
|
throw new InvalidOrderError();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-09-24 17:39:09 +02:00
|
|
|
const convertCountQueryParams = countQuery => {
|
|
|
|
return parseType({ type: 'boolean', value: countQuery });
|
|
|
|
};
|
|
|
|
|
2021-08-30 13:58:04 +02:00
|
|
|
/**
|
|
|
|
* Sort query parser
|
|
|
|
* @param {string} sortQuery - ex: id:asc,price:desc
|
|
|
|
*/
|
|
|
|
const convertSortQueryParams = sortQuery => {
|
2021-08-30 19:34:19 +02:00
|
|
|
if (typeof sortQuery === 'string') {
|
|
|
|
return sortQuery.split(',').map(value => convertSingleSortQueryParam(value));
|
|
|
|
}
|
|
|
|
|
2021-08-30 13:58:04 +02:00
|
|
|
if (Array.isArray(sortQuery)) {
|
|
|
|
return sortQuery.flatMap(sortValue => convertSortQueryParams(sortValue));
|
|
|
|
}
|
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
if (_.isPlainObject(sortQuery)) {
|
|
|
|
return convertNestedSortQueryParam(sortQuery);
|
2021-08-30 13:58:04 +02:00
|
|
|
}
|
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
throw new InvalidSortError();
|
|
|
|
};
|
2021-08-30 13:58:04 +02:00
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
const convertSingleSortQueryParam = sortQuery => {
|
|
|
|
// split field and order param with default order to ascending
|
|
|
|
const [field, order = 'asc'] = sortQuery.split(':');
|
2021-08-30 13:58:04 +02:00
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
if (field.length === 0) {
|
|
|
|
throw new Error('Field cannot be empty');
|
|
|
|
}
|
2021-08-30 13:58:04 +02:00
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
validateOrder(order);
|
2021-08-30 13:58:04 +02:00
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
return _.set({}, field, order);
|
|
|
|
};
|
2021-08-30 13:58:04 +02:00
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
const convertNestedSortQueryParam = sortQuery => {
|
|
|
|
const transformedSort = {};
|
|
|
|
for (const field in sortQuery) {
|
|
|
|
const order = sortQuery[field];
|
|
|
|
|
|
|
|
// this is a deep sort
|
|
|
|
if (_.isPlainObject(order)) {
|
|
|
|
transformedSort[field] = convertNestedSortQueryParam(order);
|
|
|
|
} else {
|
|
|
|
validateOrder(order);
|
|
|
|
transformedSort[field] = order;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return transformedSort;
|
2021-08-30 13:58:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start query parser
|
2021-09-01 21:13:23 +02:00
|
|
|
* @param {string} startQuery
|
2021-08-30 13:58:04 +02:00
|
|
|
*/
|
|
|
|
const convertStartQueryParams = startQuery => {
|
|
|
|
const startAsANumber = _.toNumber(startQuery);
|
|
|
|
|
|
|
|
if (!_.isInteger(startAsANumber) || startAsANumber < 0) {
|
|
|
|
throw new Error(`convertStartQueryParams expected a positive integer got ${startAsANumber}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return startAsANumber;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Limit query parser
|
2021-09-01 21:13:23 +02:00
|
|
|
* @param {string} limitQuery
|
2021-08-30 13:58:04 +02:00
|
|
|
*/
|
|
|
|
const convertLimitQueryParams = limitQuery => {
|
|
|
|
const limitAsANumber = _.toNumber(limitQuery);
|
|
|
|
|
|
|
|
if (!_.isInteger(limitAsANumber) || (limitAsANumber !== -1 && limitAsANumber < 0)) {
|
|
|
|
throw new Error(`convertLimitQueryParams expected a positive integer got ${limitAsANumber}`);
|
|
|
|
}
|
|
|
|
|
2021-11-08 17:32:40 +01:00
|
|
|
if (limitAsANumber === -1) return null;
|
|
|
|
|
2021-08-30 13:58:04 +02:00
|
|
|
return limitAsANumber;
|
|
|
|
};
|
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
class InvalidPopulateError extends Error {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.message =
|
|
|
|
'Invalid populate parameter. Expected a string, an array of strings, a populate object';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-30 13:58:04 +02:00
|
|
|
// NOTE: we could support foo.* or foo.bar.* etc later on
|
|
|
|
const convertPopulateQueryParams = (populate, depth = 0) => {
|
|
|
|
if (depth === 0 && populate === '*') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof populate === 'string') {
|
|
|
|
return populate.split(',').map(value => _.trim(value));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Array.isArray(populate)) {
|
|
|
|
// map convert
|
2021-10-13 14:06:16 +02:00
|
|
|
return _.uniq(
|
|
|
|
populate.flatMap(value => {
|
|
|
|
if (typeof value !== 'string') {
|
|
|
|
throw new InvalidPopulateError();
|
|
|
|
}
|
2021-08-30 19:34:19 +02:00
|
|
|
|
2021-10-13 14:06:16 +02:00
|
|
|
return value.split(',').map(value => _.trim(value));
|
|
|
|
})
|
|
|
|
);
|
2021-08-30 13:58:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (_.isPlainObject(populate)) {
|
|
|
|
const transformedPopulate = {};
|
|
|
|
for (const key in populate) {
|
|
|
|
transformedPopulate[key] = convertNestedPopulate(populate[key]);
|
|
|
|
}
|
2021-10-13 14:06:16 +02:00
|
|
|
|
2021-08-30 13:58:04 +02:00
|
|
|
return transformedPopulate;
|
|
|
|
}
|
|
|
|
|
2021-08-30 19:34:19 +02:00
|
|
|
throw new InvalidPopulateError();
|
2021-08-30 13:58:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const convertNestedPopulate = subPopulate => {
|
|
|
|
if (subPopulate === '*') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-08-30 14:23:19 +02:00
|
|
|
if (_.isBoolean(subPopulate)) {
|
|
|
|
return subPopulate;
|
|
|
|
}
|
|
|
|
|
2021-08-30 13:58:04 +02:00
|
|
|
if (!_.isPlainObject(subPopulate)) {
|
|
|
|
throw new Error(`Invalid nested populate. Expected '*' or an object`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: We will need to consider a way to add limitation / pagination
|
2021-09-24 17:39:09 +02:00
|
|
|
const { sort, filters, fields, populate, count } = subPopulate;
|
2021-08-30 13:58:04 +02:00
|
|
|
|
|
|
|
const query = {};
|
|
|
|
|
|
|
|
if (sort) {
|
|
|
|
query.orderBy = convertSortQueryParams(sort);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filters) {
|
|
|
|
query.where = convertFiltersQueryParams(filters);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fields) {
|
2021-08-30 18:31:09 +02:00
|
|
|
query.select = convertFieldsQueryParams(fields);
|
2021-08-30 13:58:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (populate) {
|
|
|
|
query.populate = convertPopulateQueryParams(populate);
|
|
|
|
}
|
|
|
|
|
2021-09-24 17:39:09 +02:00
|
|
|
if (count) {
|
|
|
|
query.count = convertCountQueryParams(count);
|
|
|
|
}
|
|
|
|
|
2021-08-30 13:58:04 +02:00
|
|
|
return query;
|
|
|
|
};
|
|
|
|
|
2021-08-30 18:31:09 +02:00
|
|
|
const convertFieldsQueryParams = (fields, depth = 0) => {
|
|
|
|
if (depth === 0 && fields === '*') {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof fields === 'string') {
|
|
|
|
const fieldsValues = fields.split(',').map(value => _.trim(value));
|
|
|
|
return _.uniq(['id', ...fieldsValues]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Array.isArray(fields)) {
|
|
|
|
// map convert
|
2021-09-01 21:13:23 +02:00
|
|
|
const fieldsValues = fields.flatMap(value => convertFieldsQueryParams(value, depth + 1));
|
2021-08-30 18:31:09 +02:00
|
|
|
return _.uniq(['id', ...fieldsValues]);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('Invalid fields parameter. Expected a string or an array of strings');
|
|
|
|
};
|
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
const convertFiltersQueryParams = (filters, schema) => {
|
|
|
|
// Filters need to be either an array or an object
|
|
|
|
// Here we're only checking for 'object' type since typeof [] => object and typeof {} => object
|
|
|
|
if (typeof filters !== 'object') {
|
|
|
|
throw new Error('The filters parameter must be an object or an array');
|
|
|
|
}
|
|
|
|
|
|
|
|
const sanitizeFilters = (filters, schema) => {
|
2021-12-15 11:15:58 +01:00
|
|
|
if (Array.isArray(filters)) {
|
2022-01-06 16:19:27 +01:00
|
|
|
return (
|
|
|
|
filters
|
|
|
|
// Sanitize each filter
|
|
|
|
.map(filter => sanitizeFilters(filter, schema))
|
|
|
|
// Filter out empty filters
|
|
|
|
.filter(filter => !isEmpty(filter))
|
|
|
|
);
|
2021-12-15 11:15:58 +01:00
|
|
|
}
|
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
// Here, `key` can either be an operator or an attribute name
|
|
|
|
for (const [key, value] of Object.entries(filters)) {
|
|
|
|
const removeOperator = () => delete filters[key];
|
|
|
|
const attribute = schema.attributes[key];
|
2021-12-15 11:15:58 +01:00
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
// Handle attributes
|
2021-12-15 11:15:58 +01:00
|
|
|
if (attribute) {
|
2022-01-06 16:19:27 +01:00
|
|
|
console.log(key, attribute.type);
|
|
|
|
// Always remove password attributes from filters object
|
2021-12-15 11:15:58 +01:00
|
|
|
if (attribute.type === 'password') {
|
2022-01-06 16:19:27 +01:00
|
|
|
removeOperator();
|
2021-12-15 11:15:58 +01:00
|
|
|
}
|
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
// Relations
|
2021-12-15 11:15:58 +01:00
|
|
|
if (attribute.type === 'relation') {
|
2022-01-06 16:19:27 +01:00
|
|
|
filters[key] = sanitizeFilters(value, strapi.getModel(attribute.target));
|
2021-12-15 11:15:58 +01:00
|
|
|
}
|
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
// Components
|
|
|
|
else if (attribute.type === 'component') {
|
|
|
|
filters[key] = sanitizeFilters(value, strapi.getModel(attribute.component));
|
2021-12-15 11:15:58 +01:00
|
|
|
}
|
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
// Media
|
|
|
|
else if (attribute.type === 'media') {
|
|
|
|
filters[key] = sanitizeFilters(value, strapi.getModel('plugin::upload.file'));
|
2021-12-15 11:15:58 +01:00
|
|
|
}
|
2022-01-06 16:19:27 +01:00
|
|
|
}
|
2021-12-15 11:15:58 +01:00
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
// Handle operators
|
|
|
|
else {
|
|
|
|
if (typeof value !== 'object') {
|
|
|
|
throw new Error(`Invalid value supplied for "${key}"`);
|
2021-12-15 11:15:58 +01:00
|
|
|
}
|
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
filters[key] = sanitizeFilters(value, schema);
|
|
|
|
}
|
2021-12-15 11:15:58 +01:00
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
if (isEmpty(filters[key])) {
|
|
|
|
removeOperator();
|
|
|
|
}
|
2021-12-15 11:15:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return filters;
|
|
|
|
};
|
|
|
|
|
2022-01-06 16:19:27 +01:00
|
|
|
return sanitizeFilters(filters, schema);
|
2021-12-15 11:15:58 +01:00
|
|
|
};
|
2021-08-30 13:58:04 +02:00
|
|
|
|
2021-10-13 14:06:16 +02:00
|
|
|
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 } };
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-08-30 13:58:04 +02:00
|
|
|
module.exports = {
|
|
|
|
convertSortQueryParams,
|
|
|
|
convertStartQueryParams,
|
|
|
|
convertLimitQueryParams,
|
|
|
|
convertPopulateQueryParams,
|
|
|
|
convertFiltersQueryParams,
|
2021-08-30 18:31:09 +02:00
|
|
|
convertFieldsQueryParams,
|
2021-10-13 14:06:16 +02:00
|
|
|
convertPublicationStateParams,
|
2021-08-30 13:58:04 +02:00
|
|
|
};
|