From 7747d1d3d16a1b2f3c07a084c35a72b5c3898d18 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 5 May 2023 13:11:59 +0200 Subject: [PATCH 01/17] initial commit --- .../core/utils/lib/convert-query-params.js | 55 +++++++++++++++++-- .../core/utils/lib/sanitize/sanitizers.js | 1 + 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/core/utils/lib/convert-query-params.js b/packages/core/utils/lib/convert-query-params.js index efc279535e..747e6fee5e 100644 --- a/packages/core/utils/lib/convert-query-params.js +++ b/packages/core/utils/lib/convert-query-params.js @@ -18,6 +18,7 @@ const { cloneDeep, get, mergeAll, + isString, } = require('lodash/fp'); const _ = require('lodash'); const parseType = require('./parse-type'); @@ -46,7 +47,7 @@ class InvalidSortError extends Error { } const validateOrder = (order) => { - if (!['asc', 'desc'].includes(order.toLocaleLowerCase())) { + if (!isString(order) || !['asc', 'desc'].includes(order.toLocaleLowerCase())) { throw new InvalidOrderError(); } }; @@ -80,6 +81,13 @@ const convertSortQueryParams = (sortQuery) => { }; const convertSingleSortQueryParam = (sortQuery) => { + if (!sortQuery) { + return {}; + } + if (!isString(sortQuery)) { + throw new Error('Invalid sort query'); + } + // split field and order param with default order to ascending const [field, order = 'asc'] = sortQuery.split(':'); @@ -89,6 +97,8 @@ const convertSingleSortQueryParam = (sortQuery) => { validateOrder(order); + // TODO: field should be a valid path on an object model + return _.set({}, field, order); }; @@ -368,6 +378,7 @@ const convertNestedPopulate = (subPopulate, schema) => { return query; }; + // TODO: ensure field is valid in content types (will probably have to check strapi.contentTypes since it can be a string.path) const convertFieldsQueryParams = (fields, depth = 0) => { if (depth === 0 && fields === '*') { return undefined; @@ -387,6 +398,33 @@ const convertFieldsQueryParams = (fields, depth = 0) => { throw new Error('Invalid fields parameter. Expected a string or an array of strings'); }; +// TODO: this is temporary as a POC, get operators from @strapi/database (will likely require refactoring) +const isOperator = (key) => { + return key.startsWith('$'); +}; + +const isValidSchemaAttribute = (key, schema) => { + if (key.trim().toLowerCase() === 'id') { + return true; + } + + if (!schema) { + return false; + } + + // case insensitive check if the attribute exists in the schema + const findAttribute = key.toLowerCase(); + if ( + Object.keys(schema.attributes) + .map((attributeName) => attributeName.toLowerCase()) + .includes(findAttribute) + ) { + return true; + } + + return false; +}; + 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 @@ -401,10 +439,6 @@ const convertFiltersQueryParams = (filters, schema) => { }; const convertAndSanitizeFilters = (filters, schema) => { - if (!isPlainObject(filters)) { - return filters; - } - if (Array.isArray(filters)) { return ( filters @@ -415,14 +449,23 @@ const convertAndSanitizeFilters = (filters, schema) => { ); } + // This must come after check for Array or else arrays are not filtered + if (!isPlainObject(filters)) { + return filters; + } + const removeOperator = (operator) => delete filters[operator]; // Here, `key` can either be an operator or an attribute name for (const [key, value] of Object.entries(filters)) { const attribute = get(key, schema?.attributes); + const validKey = isOperator(key) || isValidSchemaAttribute(key, schema); + if (!validKey) { + removeOperator(key); + } // Handle attributes - if (attribute) { + else if (attribute) { // Relations if (attribute.type === 'relation') { filters[key] = convertAndSanitizeFilters(value, strapi.getModel(attribute.target)); diff --git a/packages/core/utils/lib/sanitize/sanitizers.js b/packages/core/utils/lib/sanitize/sanitizers.js index 67ab23f380..b776062f6c 100644 --- a/packages/core/utils/lib/sanitize/sanitizers.js +++ b/packages/core/utils/lib/sanitize/sanitizers.js @@ -35,6 +35,7 @@ const defaultSanitizeOutput = async (schema, entity) => { ); }; +// TODO: remove fields that don't exist on the schema const defaultSanitizeFilters = curry((schema, filters) => { return pipeAsync( // Remove dynamic zones from filters From 980edbbcced8e9b6a70088fb128a298c4438800a Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 12 May 2023 12:13:29 +0200 Subject: [PATCH 02/17] move operators to utils --- .../core/database/lib/query/helpers/where.js | 77 +++++-------------- packages/core/database/package.json | 1 + .../core/utils/lib/convert-query-params.js | 10 +-- packages/core/utils/lib/index.js | 2 + packages/core/utils/lib/operators.js | 74 ++++++++++++++++++ yarn.lock | 1 + 6 files changed, 99 insertions(+), 66 deletions(-) create mode 100644 packages/core/utils/lib/operators.js diff --git a/packages/core/database/lib/query/helpers/where.js b/packages/core/database/lib/query/helpers/where.js index 6d2971ad45..1f7eec3b41 100644 --- a/packages/core/database/lib/query/helpers/where.js +++ b/packages/core/database/lib/query/helpers/where.js @@ -1,55 +1,14 @@ 'use strict'; -const _ = require('lodash/fp'); +const { isArray, castArray, keys, isPlainObject } = require('lodash/fp'); +const { isOperatorOfType } = require('@strapi/utils').operators; const types = require('../../types'); const { createField } = require('../../fields'); const { createJoin } = require('./join'); const { toColumnName } = require('./transform'); const { isKnexQuery } = require('../../utils/knex'); -const GROUP_OPERATORS = ['$and', '$or']; -const OPERATORS = [ - '$not', - '$in', - '$notIn', - '$eq', - '$eqi', - '$ne', - '$gt', - '$gte', - '$lt', - '$lte', - '$null', - '$notNull', - '$between', - '$startsWith', - '$endsWith', - '$startsWithi', - '$endsWithi', - '$contains', - '$notContains', - '$containsi', - '$notContainsi', -]; - -const CAST_OPERATORS = [ - '$not', - '$in', - '$notIn', - '$eq', - '$ne', - '$gt', - '$gte', - '$lt', - '$lte', - '$between', -]; - -const ARRAY_OPERATORS = ['$in', '$notIn', '$between']; - -const isOperator = (key) => OPERATORS.includes(key); - const castValue = (value, attribute) => { if (!attribute) { return value; @@ -65,12 +24,12 @@ const castValue = (value, attribute) => { }; const processAttributeWhere = (attribute, where, operator = '$eq') => { - if (_.isArray(where)) { + if (isArray(where)) { return where.map((sub) => processAttributeWhere(attribute, sub, operator)); } - if (!_.isPlainObject(where)) { - if (CAST_OPERATORS.includes(operator)) { + if (!isPlainObject(where)) { + if (isOperatorOfType('cast', operator)) { return castValue(where, attribute); } @@ -82,7 +41,7 @@ const processAttributeWhere = (attribute, where, operator = '$eq') => { for (const key of Object.keys(where)) { const value = where[key]; - if (!isOperator(key)) { + if (!isOperatorOfType('where', key)) { throw new Error(`Undefined attribute level operator ${key}`); } @@ -100,16 +59,16 @@ const processAttributeWhere = (attribute, where, operator = '$eq') => { * @returns {Object} */ const processWhere = (where, ctx) => { - if (!_.isArray(where) && !_.isPlainObject(where)) { + if (!isArray(where) && !isPlainObject(where)) { throw new Error('Where must be an array or an object'); } - if (_.isArray(where)) { + if (isArray(where)) { return where.map((sub) => processWhere(sub, ctx)); } const processNested = (where, ctx) => { - if (!_.isPlainObject(where)) { + if (!isPlainObject(where)) { return where; } @@ -126,7 +85,7 @@ const processWhere = (where, ctx) => { const value = where[key]; // if operator $and $or then loop over them - if (GROUP_OPERATORS.includes(key)) { + if (isOperatorOfType('group', key)) { filters[key] = value.map((sub) => processNested(sub, ctx)); continue; } @@ -136,7 +95,7 @@ const processWhere = (where, ctx) => { continue; } - if (isOperator(key)) { + if (isOperatorOfType('where', key)) { throw new Error( `Only $and, $or and $not can only be used as root level operators. Found ${key}.` ); @@ -165,7 +124,7 @@ const processWhere = (where, ctx) => { uid: attribute.target, }); - if (!_.isPlainObject(nestedWhere) || isOperator(_.keys(nestedWhere)[0])) { + if (!isPlainObject(nestedWhere) || isOperatorOfType('where', keys(nestedWhere)[0])) { nestedWhere = { [qb.aliasColumn('id', subAlias)]: nestedWhere }; } @@ -192,7 +151,7 @@ const processWhere = (where, ctx) => { // TODO: add type casting per operator at some point const applyOperator = (qb, column, operator, value) => { - if (Array.isArray(value) && !ARRAY_OPERATORS.includes(operator)) { + if (Array.isArray(value) && !isOperatorOfType('array', operator)) { return qb.where((subQB) => { value.forEach((subValue) => subQB.orWhere((innerQB) => { @@ -209,12 +168,12 @@ const applyOperator = (qb, column, operator, value) => { } case '$in': { - qb.whereIn(column, isKnexQuery(value) ? value : _.castArray(value)); + qb.whereIn(column, isKnexQuery(value) ? value : castArray(value)); break; } case '$notIn': { - qb.whereNotIn(column, isKnexQuery(value) ? value : _.castArray(value)); + qb.whereNotIn(column, isKnexQuery(value) ? value : castArray(value)); break; } @@ -328,7 +287,7 @@ const applyOperator = (qb, column, operator, value) => { }; const applyWhereToColumn = (qb, column, columnWhere) => { - if (!_.isPlainObject(columnWhere)) { + if (!isPlainObject(columnWhere)) { if (Array.isArray(columnWhere)) { return qb.whereIn(column, columnWhere); } @@ -344,11 +303,11 @@ const applyWhereToColumn = (qb, column, columnWhere) => { }; const applyWhere = (qb, where) => { - if (!_.isArray(where) && !_.isPlainObject(where)) { + if (!isArray(where) && !isPlainObject(where)) { throw new Error('Where must be an array or an object'); } - if (_.isArray(where)) { + if (isArray(where)) { return qb.where((subQB) => where.forEach((subWhere) => applyWhere(subQB, subWhere))); } diff --git a/packages/core/database/package.json b/packages/core/database/package.json index 21f34a708e..ad02470575 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -33,6 +33,7 @@ "lint": "run -T eslint ." }, "dependencies": { + "@strapi/utils": "4.10.2", "date-fns": "2.29.3", "debug": "4.3.4", "fs-extra": "10.0.0", diff --git a/packages/core/utils/lib/convert-query-params.js b/packages/core/utils/lib/convert-query-params.js index 747e6fee5e..5e70a71e34 100644 --- a/packages/core/utils/lib/convert-query-params.js +++ b/packages/core/utils/lib/convert-query-params.js @@ -29,6 +29,7 @@ const { isDynamicZoneAttribute, isMorphToRelationalAttribute, } = require('./content-types'); +const { isOperator } = require('./operators'); const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants; @@ -378,7 +379,7 @@ const convertNestedPopulate = (subPopulate, schema) => { return query; }; - // TODO: ensure field is valid in content types (will probably have to check strapi.contentTypes since it can be a string.path) +// TODO: ensure field is valid in content types (will probably have to check strapi.contentTypes since it can be a string.path) const convertFieldsQueryParams = (fields, depth = 0) => { if (depth === 0 && fields === '*') { return undefined; @@ -398,11 +399,6 @@ const convertFieldsQueryParams = (fields, depth = 0) => { throw new Error('Invalid fields parameter. Expected a string or an array of strings'); }; -// TODO: this is temporary as a POC, get operators from @strapi/database (will likely require refactoring) -const isOperator = (key) => { - return key.startsWith('$'); -}; - const isValidSchemaAttribute = (key, schema) => { if (key.trim().toLowerCase() === 'id') { return true; @@ -459,7 +455,7 @@ const convertAndSanitizeFilters = (filters, schema) => { // Here, `key` can either be an operator or an attribute name for (const [key, value] of Object.entries(filters)) { const attribute = get(key, schema?.attributes); - const validKey = isOperator(key) || isValidSchemaAttribute(key, schema); + const validKey = isOperator(key, true) || isValidSchemaAttribute(key, schema); if (!validKey) { removeOperator(key); diff --git a/packages/core/utils/lib/index.js b/packages/core/utils/lib/index.js index d9cdead14f..5a8d27e72a 100644 --- a/packages/core/utils/lib/index.js +++ b/packages/core/utils/lib/index.js @@ -43,6 +43,7 @@ const importDefault = require('./import-default'); const template = require('./template'); const file = require('./file'); const traverse = require('./traverse'); +const operators = require('./operators'); module.exports = { yup, @@ -93,4 +94,5 @@ module.exports = { importDefault, file, traverse, + operators, }; diff --git a/packages/core/utils/lib/operators.js b/packages/core/utils/lib/operators.js new file mode 100644 index 0000000000..79d1cc146c --- /dev/null +++ b/packages/core/utils/lib/operators.js @@ -0,0 +1,74 @@ +'use strict'; + +const GROUP_OPERATORS = ['$and', '$or']; + +const WHERE_OPERATORS = [ + '$not', + '$in', + '$notIn', + '$eq', + '$eqi', + '$ne', + '$gt', + '$gte', + '$lt', + '$lte', + '$null', + '$notNull', + '$between', + '$startsWith', + '$endsWith', + '$startsWithi', + '$endsWithi', + '$contains', + '$notContains', + '$containsi', + '$notContainsi', +]; + +const CAST_OPERATORS = [ + '$not', + '$in', + '$notIn', + '$eq', + '$ne', + '$gt', + '$gte', + '$lt', + '$lte', + '$between', +]; + +const ARRAY_OPERATORS = ['$in', '$notIn', '$between']; + +const OPERATORS = { + where: WHERE_OPERATORS, + cast: CAST_OPERATORS, + group: GROUP_OPERATORS, + array: ARRAY_OPERATORS, +}; + +// for performance, cache all operators in lowercase +const OPERATORS_LOWERCASE = Object.fromEntries( + Object.entries(OPERATORS).map(([key, values]) => [ + key, + values.map((value) => value.toLowerCase()), + ]) +); + +const isOperatorOfType = (type, key, ignoreCase = false) => { + if (ignoreCase) { + return OPERATORS_LOWERCASE[type]?.includes(key.toLowerCase()); + } + return OPERATORS[type]?.includes(key); +}; + +const isOperator = (key, ignoreCase = false) => { + return Object.keys(OPERATORS).some((type) => isOperatorOfType(type, key, ignoreCase)); +}; + +module.exports = { + isOperator, + isOperatorOfType, + OPERATORS, +}; diff --git a/yarn.lock b/yarn.lock index 6450db916f..1d34c3826d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7872,6 +7872,7 @@ __metadata: version: 0.0.0-use.local resolution: "@strapi/database@workspace:packages/core/database" dependencies: + "@strapi/utils": 4.10.2 date-fns: 2.29.3 debug: 4.3.4 fs-extra: 10.0.0 From b3998432d725ae28aa348a66114d6ca39092e3a6 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 12 May 2023 12:13:56 +0200 Subject: [PATCH 03/17] only allow attributes and operators on filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jean-Sébastien Herbaux --- packages/core/utils/lib/sanitize/sanitizers.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/core/utils/lib/sanitize/sanitizers.js b/packages/core/utils/lib/sanitize/sanitizers.js index b776062f6c..aff7f0a7f2 100644 --- a/packages/core/utils/lib/sanitize/sanitizers.js +++ b/packages/core/utils/lib/sanitize/sanitizers.js @@ -19,6 +19,7 @@ const { removeDynamicZones, removeMorphToRelations, } = require('./visitors'); +const { isOperator } = require('../operators'); const sanitizePasswords = (schema) => async (entity) => { return traverseEntity(removePassword, { schema }, entity); @@ -38,6 +39,17 @@ const defaultSanitizeOutput = async (schema, entity) => { // TODO: remove fields that don't exist on the schema const defaultSanitizeFilters = curry((schema, filters) => { return pipeAsync( + // Remove keys that are not attributes or valid operators + traverseQueryFilters( + ({ key, attribute }, { remove }) => { + const isAttribute = !!attribute; + + if (!isAttribute && !isOperator(key)) { + remove(key); + } + }, + { schema } + ), // Remove dynamic zones from filters traverseQueryFilters(removeDynamicZones, { schema }), // Remove morpTo relations from filters From d961309386121c599e9321e0486c74ce82c673f4 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 12 May 2023 12:14:23 +0200 Subject: [PATCH 04/17] remove comment --- packages/core/utils/lib/sanitize/sanitizers.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/utils/lib/sanitize/sanitizers.js b/packages/core/utils/lib/sanitize/sanitizers.js index aff7f0a7f2..9c41580672 100644 --- a/packages/core/utils/lib/sanitize/sanitizers.js +++ b/packages/core/utils/lib/sanitize/sanitizers.js @@ -36,7 +36,6 @@ const defaultSanitizeOutput = async (schema, entity) => { ); }; -// TODO: remove fields that don't exist on the schema const defaultSanitizeFilters = curry((schema, filters) => { return pipeAsync( // Remove keys that are not attributes or valid operators From 53de017ad0a414003156ca255a532a79ff2a1ecd Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 12 May 2023 12:50:22 +0200 Subject: [PATCH 05/17] export isOperator and isOperatorOfType directly --- packages/core/utils/lib/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/utils/lib/index.js b/packages/core/utils/lib/index.js index 5a8d27e72a..774de14a5f 100644 --- a/packages/core/utils/lib/index.js +++ b/packages/core/utils/lib/index.js @@ -43,7 +43,7 @@ const importDefault = require('./import-default'); const template = require('./template'); const file = require('./file'); const traverse = require('./traverse'); -const operators = require('./operators'); +const { isOperator, isOperatorOfType } = require('./operators'); module.exports = { yup, @@ -94,5 +94,6 @@ module.exports = { importDefault, file, traverse, - operators, + isOperator, + isOperatorOfType, }; From 8b08534ec81c1ae5c61dd1976fea1cc6d18aa7c2 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 12 May 2023 16:48:18 +0200 Subject: [PATCH 06/17] Update packages/core/utils/lib/operators.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jean-Sébastien Herbaux --- packages/core/utils/lib/operators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/utils/lib/operators.js b/packages/core/utils/lib/operators.js index 79d1cc146c..b60f773df7 100644 --- a/packages/core/utils/lib/operators.js +++ b/packages/core/utils/lib/operators.js @@ -58,7 +58,7 @@ const OPERATORS_LOWERCASE = Object.fromEntries( const isOperatorOfType = (type, key, ignoreCase = false) => { if (ignoreCase) { - return OPERATORS_LOWERCASE[type]?.includes(key.toLowerCase()); + return OPERATORS_LOWERCASE[type]?.includes(key.toLowerCase()) ?? false; } return OPERATORS[type]?.includes(key); }; From 00f43de9b7e3f265a4ac206f4d625e23b0c41dd4 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 12 May 2023 16:48:26 +0200 Subject: [PATCH 07/17] Update packages/core/utils/lib/operators.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jean-Sébastien Herbaux --- packages/core/utils/lib/operators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/utils/lib/operators.js b/packages/core/utils/lib/operators.js index b60f773df7..bdb57b866d 100644 --- a/packages/core/utils/lib/operators.js +++ b/packages/core/utils/lib/operators.js @@ -60,7 +60,7 @@ const isOperatorOfType = (type, key, ignoreCase = false) => { if (ignoreCase) { return OPERATORS_LOWERCASE[type]?.includes(key.toLowerCase()) ?? false; } - return OPERATORS[type]?.includes(key); + return OPERATORS[type]?.includes(key) ?? false; }; const isOperator = (key, ignoreCase = false) => { From cb0225e31688cdc4121c4f16f0e419d96fa28a87 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 12 May 2023 16:49:09 +0200 Subject: [PATCH 08/17] Update packages/core/utils/lib/convert-query-params.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jean-Sébastien Herbaux --- packages/core/utils/lib/convert-query-params.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/core/utils/lib/convert-query-params.js b/packages/core/utils/lib/convert-query-params.js index 5e70a71e34..b53e496761 100644 --- a/packages/core/utils/lib/convert-query-params.js +++ b/packages/core/utils/lib/convert-query-params.js @@ -410,15 +410,11 @@ const isValidSchemaAttribute = (key, schema) => { // case insensitive check if the attribute exists in the schema const findAttribute = key.toLowerCase(); - if ( + return ( Object.keys(schema.attributes) .map((attributeName) => attributeName.toLowerCase()) .includes(findAttribute) - ) { - return true; - } - - return false; + ); }; const convertFiltersQueryParams = (filters, schema) => { From c1cd5b5b902ebb6d74b51b5fceb0ac40ef8cc7df Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Thu, 18 May 2023 09:53:02 +0200 Subject: [PATCH 09/17] fix scope --- packages/core/database/lib/query/helpers/where.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/database/lib/query/helpers/where.js b/packages/core/database/lib/query/helpers/where.js index 1f7eec3b41..795f81217d 100644 --- a/packages/core/database/lib/query/helpers/where.js +++ b/packages/core/database/lib/query/helpers/where.js @@ -2,7 +2,7 @@ const { isArray, castArray, keys, isPlainObject } = require('lodash/fp'); -const { isOperatorOfType } = require('@strapi/utils').operators; +const { isOperatorOfType } = require('@strapi/utils'); const types = require('../../types'); const { createField } = require('../../fields'); const { createJoin } = require('./join'); From 6ffb7f99e198a775cd86629a9dce5fd83e3fd34f Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Thu, 18 May 2023 10:00:04 +0200 Subject: [PATCH 10/17] remove case insensitivity from id check --- packages/core/utils/lib/convert-query-params.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/utils/lib/convert-query-params.js b/packages/core/utils/lib/convert-query-params.js index b53e496761..9821890726 100644 --- a/packages/core/utils/lib/convert-query-params.js +++ b/packages/core/utils/lib/convert-query-params.js @@ -400,7 +400,7 @@ const convertFieldsQueryParams = (fields, depth = 0) => { }; const isValidSchemaAttribute = (key, schema) => { - if (key.trim().toLowerCase() === 'id') { + if (key === 'id') { return true; } From 6827350469f11ff0dbcd8415021499c965c25741 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Thu, 18 May 2023 17:01:20 +0200 Subject: [PATCH 11/17] allow id filter --- packages/core/utils/lib/sanitize/sanitizers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/utils/lib/sanitize/sanitizers.js b/packages/core/utils/lib/sanitize/sanitizers.js index 9c41580672..6a1ea49576 100644 --- a/packages/core/utils/lib/sanitize/sanitizers.js +++ b/packages/core/utils/lib/sanitize/sanitizers.js @@ -43,7 +43,7 @@ const defaultSanitizeFilters = curry((schema, filters) => { ({ key, attribute }, { remove }) => { const isAttribute = !!attribute; - if (!isAttribute && !isOperator(key)) { + if (!isAttribute && !isOperator(key) && key !== 'id') { remove(key); } }, From 5c5d27247b2cd77d49d2160e7654fa8fad2a0035 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Thu, 18 May 2023 17:16:02 +0200 Subject: [PATCH 12/17] remove case sensitivity --- packages/core/utils/lib/convert-query-params.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/core/utils/lib/convert-query-params.js b/packages/core/utils/lib/convert-query-params.js index 9821890726..164977a61a 100644 --- a/packages/core/utils/lib/convert-query-params.js +++ b/packages/core/utils/lib/convert-query-params.js @@ -408,13 +408,7 @@ const isValidSchemaAttribute = (key, schema) => { return false; } - // case insensitive check if the attribute exists in the schema - const findAttribute = key.toLowerCase(); - return ( - Object.keys(schema.attributes) - .map((attributeName) => attributeName.toLowerCase()) - .includes(findAttribute) - ); + return Object.keys(schema.attributes).includes(key); }; const convertFiltersQueryParams = (filters, schema) => { From 767f3dbd605a09dc0c1a4ef23e9084888f21ec4c Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Thu, 18 May 2023 17:17:03 +0200 Subject: [PATCH 13/17] test filtering --- .../api/sanitize/sanitize-query.test.api.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js b/api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js index 3851bae8f0..26ab4bb58d 100644 --- a/api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js +++ b/api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js @@ -135,6 +135,25 @@ describe('Core API - Sanitize', () => { checkAPIResultLength(res, 0); }); + it('Successfully filters invalid attributes', async () => { + const document = data.document[2]; + const filters = { + ID: document.id, // invalid casing on key 'id' + notAnAttribute: '', // doesn't exist on schema + t0: { createdBy: { id: { $lt: '1' } } }, // join table name + t1: { createdBy: { id: { $lt: '1' } } }, // join table name + $fakeOp: false, + }; + + const res = await rq.get('/api/documents', { qs: { filters } }); + + // Should not return a 500 error from notAnAttribute or $fakeOp + expect(res.status).toEqual(200); + + // Should receive all documents because createdBy was filtered out + checkAPIResultLength(res, documentsLength()); + }); + it('Successfully filters on valid ID', async () => { const document = data.document[2]; const res = await rq.get('/api/documents', { qs: { filters: { id: document.id } } }); From 6fcad7cf3e0102a9a32ea4c3e06b46f847eb4618 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Mon, 22 May 2023 10:26:32 +0200 Subject: [PATCH 14/17] update strapi/utils package --- packages/core/database/package.json | 2 +- yarn.lock | 23 +---------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/packages/core/database/package.json b/packages/core/database/package.json index 6075a2f9a5..4b2a49ca0f 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -33,7 +33,7 @@ "lint": "run -T eslint ." }, "dependencies": { - "@strapi/utils": "4.10.2", + "@strapi/utils": "4.10.5", "date-fns": "2.30.0", "debug": "4.3.4", "fs-extra": "10.0.0", diff --git a/yarn.lock b/yarn.lock index 1fdaa0d5c4..405b28d697 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7878,7 +7878,7 @@ __metadata: version: 0.0.0-use.local resolution: "@strapi/database@workspace:packages/core/database" dependencies: - "@strapi/utils": 4.10.2 + "@strapi/utils": 4.10.5 date-fns: 2.30.0 debug: 4.3.4 fs-extra: 10.0.0 @@ -8601,20 +8601,6 @@ __metadata: languageName: unknown linkType: soft -"@strapi/utils@npm:4.10.2": - version: 4.10.2 - resolution: "@strapi/utils@npm:4.10.2" - dependencies: - "@sindresorhus/slugify": 1.1.0 - date-fns: 2.29.3 - http-errors: 1.8.1 - lodash: 4.17.21 - p-map: 4.0.0 - yup: 0.32.9 - checksum: 61df367b3d4e6565f5933d844e5473b7486750f010a86dc7866ec9b3c1afa1aae9dc3a64a6e2bc2db9d230dbb700da3b9be2dc9c218c982b4245c4c688491797 - languageName: node - linkType: hard - "@stylelint/postcss-css-in-js@npm:^0.37.2": version: 0.37.3 resolution: "@stylelint/postcss-css-in-js@npm:0.37.3" @@ -14908,13 +14894,6 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:2.29.3": - version: 2.29.3 - resolution: "date-fns@npm:2.29.3" - checksum: e01cf5b62af04e05dfff921bb9c9933310ed0e1ae9a81eb8653452e64dc841acf7f6e01e1a5ae5644d0337e9a7f936175fd2cb6819dc122fdd9c5e86c56be484 - languageName: node - linkType: hard - "date-fns@npm:2.30.0": version: 2.30.0 resolution: "date-fns@npm:2.30.0" From eefdae1e5ecdab40b99e56ad628401bc36e4cfff Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Mon, 22 May 2023 10:27:39 +0200 Subject: [PATCH 15/17] require matching case for operators --- packages/core/utils/lib/convert-query-params.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/utils/lib/convert-query-params.js b/packages/core/utils/lib/convert-query-params.js index 164977a61a..013f0d24a7 100644 --- a/packages/core/utils/lib/convert-query-params.js +++ b/packages/core/utils/lib/convert-query-params.js @@ -445,7 +445,7 @@ const convertAndSanitizeFilters = (filters, schema) => { // Here, `key` can either be an operator or an attribute name for (const [key, value] of Object.entries(filters)) { const attribute = get(key, schema?.attributes); - const validKey = isOperator(key, true) || isValidSchemaAttribute(key, schema); + const validKey = isOperator(key) || isValidSchemaAttribute(key, schema); if (!validKey) { removeOperator(key); From 7f3da5d08d3e1d59056ffc0810c2b1ba4085f9de Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Mon, 5 Jun 2023 11:29:18 +0200 Subject: [PATCH 16/17] fix utils version --- packages/core/database/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/database/package.json b/packages/core/database/package.json index 9bbcb609fb..e728fba05f 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -33,7 +33,7 @@ "lint": "run -T eslint ." }, "dependencies": { - "@strapi/utils": "4.10.5", + "@strapi/utils": "4.10.7", "date-fns": "2.30.0", "debug": "4.3.4", "fs-extra": "10.0.0", diff --git a/yarn.lock b/yarn.lock index 982cfa73f1..34ea06e50b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7916,7 +7916,7 @@ __metadata: version: 0.0.0-use.local resolution: "@strapi/database@workspace:packages/core/database" dependencies: - "@strapi/utils": 4.10.5 + "@strapi/utils": 4.10.7 date-fns: 2.30.0 debug: 4.3.4 fs-extra: 10.0.0 From 54c4fa25b2706612f85aaf103f54c071c281f23b Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Mon, 5 Jun 2023 11:53:55 +0200 Subject: [PATCH 17/17] add more test cases --- api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js b/api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js index 26ab4bb58d..04b25cbcea 100644 --- a/api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js +++ b/api-tests/core/strapi/api/sanitize/sanitize-query.test.api.js @@ -142,7 +142,10 @@ describe('Core API - Sanitize', () => { notAnAttribute: '', // doesn't exist on schema t0: { createdBy: { id: { $lt: '1' } } }, // join table name t1: { createdBy: { id: { $lt: '1' } } }, // join table name + t0: { updatedBy: { id: { $lt: '1' } } }, // join table name + t1: { updatedBy: { id: { $lt: '1' } } }, // join table name $fakeOp: false, + id: { $STARTSWITH: '123' }, // wrong casing for operator }; const res = await rq.get('/api/documents', { qs: { filters } });