diff --git a/api-tests/core/strapi/api/validate-query/resources/fixtures/document.js b/api-tests/core/strapi/api/validate-query/resources/fixtures/document.js index 8afe9e0a60..26d73253b8 100644 --- a/api-tests/core/strapi/api/validate-query/resources/fixtures/document.js +++ b/api-tests/core/strapi/api/validate-query/resources/fixtures/document.js @@ -11,6 +11,7 @@ module.exports = (fixtures) => { password: 'Password1234', misc: 2, relations: [relation[0].id, relation[1].id], + private_relations: [relation[0].id, relation[1].id], componentA: { name: 'Component A Name A', name_private: 'Private Component A Name A', @@ -44,6 +45,7 @@ module.exports = (fixtures) => { password: 'Password5678', misc: 3, relations: [relation[1].id], + private_relations: [relation[1].id], componentA: { name: 'Component A Name B', name_private: 'Private Component A Name B', diff --git a/api-tests/core/strapi/api/validate-query/resources/schemas/document.js b/api-tests/core/strapi/api/validate-query/resources/schemas/document.js index b121c0b889..8018878e44 100644 --- a/api-tests/core/strapi/api/validate-query/resources/schemas/document.js +++ b/api-tests/core/strapi/api/validate-query/resources/schemas/document.js @@ -31,6 +31,13 @@ module.exports = { target: 'api::relation.relation', targetAttribute: 'documents', }, + private_relations: { + type: 'relation', + private: true, + relation: 'manyToMany', + target: 'api::relation.relation', + targetAttribute: 'documents', + }, componentA: { type: 'component', component: 'default.component-a', diff --git a/api-tests/core/strapi/api/validate-query/validate-query.test.api.js b/api-tests/core/strapi/api/validate-query/validate-query.test.api.js index f7ba656e60..cf6665df0f 100644 --- a/api-tests/core/strapi/api/validate-query/validate-query.test.api.js +++ b/api-tests/core/strapi/api/validate-query/validate-query.test.api.js @@ -933,6 +933,13 @@ describe('Core API - Validate', () => { }); }); + it('Does not populate private relation', async () => { + const populate = { private_relations: true }; + const res = await rq.get('/api/documents', { qs: { populate } }); + + expect(res.status).toBe(400); + }); + it.todo('Populates a nested relation'); it.todo('Populates a media'); diff --git a/packages/core/admin/server/src/services/permission/permissions-manager/sanitize.ts b/packages/core/admin/server/src/services/permission/permissions-manager/sanitize.ts index 54b1184821..b1ce238d98 100644 --- a/packages/core/admin/server/src/services/permission/permissions-manager/sanitize.ts +++ b/packages/core/admin/server/src/services/permission/permissions-manager/sanitize.ts @@ -21,7 +21,7 @@ import { contentTypes, traverseEntity, sanitize, pipeAsync, traverse } from '@st import { ADMIN_USER_ALLOWED_FIELDS } from '../../../domain/user'; const { - visitors: { removePassword }, + visitors: { removePassword, expandWildcardPopulate }, } = sanitize; const { @@ -86,6 +86,7 @@ export default ({ action, ability, model }: any) => { ); const sanitizePopulate = pipeAsync( + traverse.traverseQueryPopulate(expandWildcardPopulate, { schema }), traverse.traverseQueryPopulate(removeDisallowedFields(permittedFields), { schema }), traverse.traverseQueryPopulate(omitDisallowedAdminUserFields, { schema }), traverse.traverseQueryPopulate(omitHiddenFields, { schema }), diff --git a/packages/core/admin/server/src/services/permission/permissions-manager/validate.ts b/packages/core/admin/server/src/services/permission/permissions-manager/validate.ts index dedb6e2fa5..aef99d134f 100644 --- a/packages/core/admin/server/src/services/permission/permissions-manager/validate.ts +++ b/packages/core/admin/server/src/services/permission/permissions-manager/validate.ts @@ -37,8 +37,11 @@ const COMPONENT_FIELDS = ['__component']; const STATIC_FIELDS = [ID_ATTRIBUTE, DOC_ID_ATTRIBUTE]; -const throwInvalidParam = ({ key }: any) => { - throw new ValidationError(`Invalid parameter ${key}`); +const throwInvalidParam = ({ key, path }: { key: string; path?: string | null }) => { + const msg = + path && path !== key ? `Invalid parameter ${key} at ${path}` : `Invalid parameter ${key}`; + + throw new ValidationError(msg); }; export default ({ action, ability, model }: any) => { @@ -55,9 +58,9 @@ export default ({ action, ability, model }: any) => { traverse.traverseQueryFilters(throwDisallowedAdminUserFields, { schema }), traverse.traverseQueryFilters(throwPassword, { schema }), traverse.traverseQueryFilters( - ({ key, value }) => { + ({ key, value, path }) => { if (isObject(value) && isEmpty(value)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }, { schema } @@ -69,9 +72,9 @@ export default ({ action, ability, model }: any) => { traverse.traverseQuerySort(throwDisallowedAdminUserFields, { schema }), traverse.traverseQuerySort(throwPassword, { schema }), traverse.traverseQuerySort( - ({ key, attribute, value }) => { + ({ key, attribute, value, path }) => { if (!isScalarAttribute(attribute) && isEmpty(value)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }, { schema } @@ -83,6 +86,13 @@ export default ({ action, ability, model }: any) => { traverse.traverseQueryFields(throwPassword, { schema }) ); + const validatePopulate = pipeAsync( + traverse.traverseQueryPopulate(throwDisallowedFields(permittedFields), { schema }), + traverse.traverseQueryPopulate(throwDisallowedAdminUserFields, { schema }), + traverse.traverseQueryPopulate(throwHiddenFields, { schema }), + traverse.traverseQueryPopulate(throwPassword, { schema }) + ); + return async (query: any) => { if (query.filters) { await validateFilters(query.filters); @@ -96,6 +106,11 @@ export default ({ action, ability, model }: any) => { await validateFields(query.fields); } + // a wildcard is always valid; its conversion will be handled by the entity service and can be optimized with sanitizer + if (query.populate && query.populate !== '*') { + await validatePopulate(query.populate); + } + return true; }; }; @@ -165,20 +180,20 @@ export default ({ action, ability, model }: any) => { /** * Visitor used to remove hidden fields from the admin API responses */ - const throwHiddenFields = ({ key, schema }: any) => { + const throwHiddenFields = ({ key, schema, path }: any) => { const isHidden = getOr(false, ['config', 'attributes', key, 'hidden'], schema); if (isHidden) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }; /** * Visitor used to omit disallowed fields from the admin users entities & avoid leaking sensitive information */ - const throwDisallowedAdminUserFields = ({ key, attribute, schema }: any) => { + const throwDisallowedAdminUserFields = ({ key, attribute, schema, path }: any) => { if (schema.uid === 'admin::user' && attribute && !ADMIN_USER_ALLOWED_FIELDS.includes(key)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }; diff --git a/packages/core/utils/src/__tests__/query-populate.test.ts b/packages/core/utils/src/__tests__/query-populate.test.ts index aecf7e56c0..0315ba811d 100644 --- a/packages/core/utils/src/__tests__/query-populate.test.ts +++ b/packages/core/utils/src/__tests__/query-populate.test.ts @@ -1,22 +1,7 @@ import { traverseQueryPopulate } from '../traverse'; describe('traverseQueryPopulate', () => { - test('should return an empty object incase no populatable field exists', async () => { - const query = await traverseQueryPopulate(jest.fn(), { - schema: { - kind: 'collectionType', - attributes: { - title: { - type: 'string', - }, - }, - }, - })('*'); - - expect(query).toEqual({}); - }); - - test('should return all populatable fields', async () => { + test('should not modify wildcard', async () => { const strapi = { getModel: jest.fn((uid) => { return { @@ -33,7 +18,6 @@ describe('traverseQueryPopulate', () => { get: jest.fn(() => ({ columnToAttribute: { address: 'address', - some: 'some', }, })), }, @@ -63,7 +47,7 @@ describe('traverseQueryPopulate', () => { }, })('*'); - expect(query).toEqual({ address: true, some: true }); + expect(query).toEqual('*'); }); test('should return only selected populatable field', async () => { @@ -114,151 +98,4 @@ describe('traverseQueryPopulate', () => { expect(query).toEqual('address'); }); - - test('should populate dynamiczone', async () => { - const strapi = { - getModel: jest.fn((uid) => { - return { - uid, - attributes: { - street: { - type: 'string', - }, - }, - }; - }), - db: { - metadata: { - get: jest.fn(() => ({ - columnToAttribute: { - address: 'address', - }, - })), - }, - }, - } as any; - - global.strapi = strapi; - - const query = await traverseQueryPopulate(jest.fn(), { - schema: { - kind: 'collectionType', - attributes: { - title: { - type: 'string', - }, - address: { - type: 'relation', - relation: 'oneToOne', - target: 'api::address.address', - }, - some: { - type: 'relation', - relation: 'ManyToMany', - target: 'api::some.some', - }, - zone: { - type: 'dynamiczone', - components: ['blog.test-como', 'some.another-como'], - }, - }, - }, - })('*'); - - expect(query).toEqual({ - address: true, - some: true, - zone: true, - }); - }); - - test('should deep populate dynamiczone components', async () => { - const strapi = { - getModel: jest.fn((uid) => { - if (uid === 'blog.test-como') { - return { - uid, - attributes: { - street: { - type: 'string', - }, - address: { - type: 'relation', - relation: 'oneToOne', - target: 'api::address.address', - }, - }, - }; - } - if (uid === 'some.another-como') { - return { - uid, - attributes: { - street: { - type: 'string', - }, - some: { - type: 'relation', - relation: 'ManyToMany', - target: 'api::some.some', - }, - }, - }; - } - return { - uid, - attributes: { - street: { - type: 'string', - }, - }, - }; - }), - db: { - metadata: { - get: jest.fn(() => ({ - columnToAttribute: { - address: 'address', - }, - })), - }, - }, - } as any; - - global.strapi = strapi; - - const query = await traverseQueryPopulate(jest.fn(), { - schema: { - kind: 'collectionType', - attributes: { - title: { - type: 'string', - }, - address: { - type: 'relation', - relation: 'oneToOne', - target: 'api::address.address', - }, - some: { - type: 'relation', - relation: 'ManyToMany', - target: 'api::some.some', - }, - zone: { - type: 'dynamiczone', - components: ['blog.test-como', 'some.another-como'], - }, - }, - }, - })({ zone: { populate: '*' } }); - - expect(query).toEqual({ - zone: { - populate: { - address: true, - some: true, - }, - }, - }); - }); }); diff --git a/packages/core/utils/src/sanitize/sanitizers.ts b/packages/core/utils/src/sanitize/sanitizers.ts index 91dcadc0b6..38ee5f2bf0 100644 --- a/packages/core/utils/src/sanitize/sanitizers.ts +++ b/packages/core/utils/src/sanitize/sanitizers.ts @@ -16,6 +16,7 @@ import { removePrivate, removeDynamicZones, removeMorphToRelations, + expandWildcardPopulate, } from './visitors'; import { isOperator } from '../operators'; @@ -164,6 +165,7 @@ const defaultSanitizePopulate = curry((schema: Model, populate: unknown) => { throw new Error('Missing schema in defaultSanitizePopulate'); } return pipeAsync( + traverseQueryPopulate(expandWildcardPopulate, { schema }), traverseQueryPopulate( async ({ key, value, schema, attribute }, { set }) => { if (attribute) { @@ -181,6 +183,10 @@ const defaultSanitizePopulate = curry((schema: Model, populate: unknown) => { if (key === 'fields') { set(key, await defaultSanitizeFields(schema, value)); } + + if (key === 'populate') { + set(key, await defaultSanitizePopulate(schema, value)); + } }, { schema } ), diff --git a/packages/core/utils/src/sanitize/visitors/expand-wildcard-populate.ts b/packages/core/utils/src/sanitize/visitors/expand-wildcard-populate.ts new file mode 100644 index 0000000000..a7021ddb0b --- /dev/null +++ b/packages/core/utils/src/sanitize/visitors/expand-wildcard-populate.ts @@ -0,0 +1,17 @@ +import type { Visitor } from '../../traverse/factory'; + +const visitor: Visitor = ({ schema, key, value }, { set }) => { + if (key === '' && value === '*') { + const { attributes } = schema; + + const newPopulateQuery = Object.entries(attributes) + .filter(([, attribute]) => + ['relation', 'component', 'media', 'dynamiczone'].includes(attribute.type) + ) + .reduce>((acc, [key]) => ({ ...acc, [key]: true }), {}); + + set('', newPopulateQuery); + } +}; + +export default visitor; diff --git a/packages/core/utils/src/sanitize/visitors/index.ts b/packages/core/utils/src/sanitize/visitors/index.ts index d05d2247b8..024b88bc1f 100644 --- a/packages/core/utils/src/sanitize/visitors/index.ts +++ b/packages/core/utils/src/sanitize/visitors/index.ts @@ -5,3 +5,4 @@ export { default as removeMorphToRelations } from './remove-morph-to-relations'; export { default as removeDynamicZones } from './remove-dynamic-zones'; export { default as removeDisallowedFields } from './remove-disallowed-fields'; export { default as removeRestrictedFields } from './remove-restricted-fields'; +export { default as expandWildcardPopulate } from './expand-wildcard-populate'; diff --git a/packages/core/utils/src/traverse/factory.ts b/packages/core/utils/src/traverse/factory.ts index 7c985c0037..cc8ea2d3b9 100644 --- a/packages/core/utils/src/traverse/factory.ts +++ b/packages/core/utils/src/traverse/factory.ts @@ -51,7 +51,7 @@ interface Interceptor { interface ParseUtils { transform(data: T): unknown; remove(key: string, data: T): unknown; - set(key: string, valeu: unknown, data: T): unknown; + set(key: string, value: unknown, data: T): unknown; keys(data: T): string[]; get(key: string, data: T): unknown; } diff --git a/packages/core/utils/src/traverse/query-fields.ts b/packages/core/utils/src/traverse/query-fields.ts index b1283cab43..ab11bef5a5 100644 --- a/packages/core/utils/src/traverse/query-fields.ts +++ b/packages/core/utils/src/traverse/query-fields.ts @@ -6,7 +6,7 @@ const isStringArray = (value: unknown): value is string[] => isArray(value) && value.every(isString); const fields = traverseFactory() - // Interecept array of strings + // Intercept array of strings .intercept(isStringArray, async (visitor, options, fields, { recurse }) => { return Promise.all(fields.map((field) => recurse(visitor, options, field))); }) diff --git a/packages/core/utils/src/traverse/query-populate.ts b/packages/core/utils/src/traverse/query-populate.ts index 7660fe2437..9204c61a9c 100644 --- a/packages/core/utils/src/traverse/query-populate.ts +++ b/packages/core/utils/src/traverse/query-populate.ts @@ -6,7 +6,9 @@ import { split, isObject, trim, + constant, isNil, + identity, cloneDeep, join, first, @@ -27,8 +29,6 @@ const isKeyword = (keyword: string) => { const isStringArray = (value: unknown): value is string[] => isArray(value) && value.every(isString); -const isWildCardConstant = (value: unknown): value is '*' => value === '*'; - const isObj = (value: unknown): value is Record => isObject(value); const populate = traverseFactory() @@ -40,24 +40,40 @@ const populate = traverseFactory() return visitedPopulate.filter((item) => !isNil(item)); }) - // Transform wildcard populate to an exhaustive list of attributes to populate. - .intercept(isWildCardConstant, (visitor, options, _data, { recurse }) => { - const attributes = options.schema?.attributes; + // for wildcard, generate custom utilities to modify the values + .parse( + (value): value is '*' => value === '*', + () => ({ + /** + * Since value is '*', we don't need to transform it + */ + transform: identity, - // This should never happen, but adding the check in - // case this method is called with wrong parameters - if (!attributes) { - return '*'; - } + /** + * '*' isn't a key/value structure, so regardless + * of the given key, it returns the data ('*') + */ + get: (_key, data) => data, - const parsedPopulate = Object.entries(attributes) - // Get the list of all attributes that can be populated - .filter(([, value]) => ['relation', 'component', 'dynamiczone', 'media'].includes(value.type)) - // Only keep the attributes key - .reduce((acc, [key]) => ({ ...acc, [key]: true }), {}); + /** + * '*' isn't a key/value structure, so regardless + * of the given `key`, use `value` as the new `data` + */ + set: (_key, value) => value, + + /** + * '*' isn't a key/value structure, but we need to simulate at least one to enable + * the data traversal. We're using '' since it represents a falsy string value + */ + keys: constant(['']), + + /** + * Removing '*' means setting it to undefined, regardless of the given key + */ + remove: constant(undefined), + }) + ) - return recurse(visitor, options, parsedPopulate); - }) // Parse string values .parse(isString, () => { const tokenize = split('.'); diff --git a/packages/core/utils/src/validate/index.ts b/packages/core/utils/src/validate/index.ts index 5181cc2b6e..2c390f9829 100644 --- a/packages/core/utils/src/validate/index.ts +++ b/packages/core/utils/src/validate/index.ts @@ -9,7 +9,7 @@ import * as visitors from './visitors'; import * as validators from './validators'; import traverseEntity from '../traverse-entity'; -import { traverseQueryFilters, traverseQuerySort } from '../traverse'; +import { traverseQueryFilters, traverseQuerySort, traverseQueryPopulate } from '../traverse'; import { Model, Data } from '../types'; @@ -57,7 +57,7 @@ const createContentAPIValidators = () => { .get('content-api.input') .forEach((validator: Validator) => transforms.push(validator(schema))); - pipeAsync(...transforms)(data as Data); + await pipeAsync(...transforms)(data as Data); }; const validateQuery = async ( @@ -68,7 +68,7 @@ const createContentAPIValidators = () => { if (!schema) { throw new Error('Missing schema in validateQuery'); } - const { filters, sort, fields } = query; + const { filters, sort, fields, populate } = query; if (filters) { await validateFilters(filters, schema, { auth }); @@ -82,7 +82,10 @@ const createContentAPIValidators = () => { await validateFields(fields, schema); } - // TODO: validate populate + // a wildcard is always valid; its conversion will be handled by the entity service and can be optimized with sanitizer + if (populate && populate !== '*') { + await validatePopulate(populate, schema); + } }; const validateFilters: ValidateFunc = async (filters, schema: Model, { auth } = {}) => { @@ -100,7 +103,7 @@ const createContentAPIValidators = () => { transforms.push(traverseQueryFilters(visitors.throwRestrictedRelations(auth), { schema })); } - return pipeAsync(...transforms)(filters); + await pipeAsync(...transforms)(filters); }; const validateSort: ValidateFunc = async (sort, schema: Model, { auth } = {}) => { @@ -113,16 +116,29 @@ const createContentAPIValidators = () => { transforms.push(traverseQuerySort(visitors.throwRestrictedRelations(auth), { schema })); } - return pipeAsync(...transforms)(sort); + await pipeAsync(...transforms)(sort); }; - const validateFields: ValidateFunc = (fields, schema: Model) => { + const validateFields: ValidateFunc = async (fields, schema: Model) => { if (!schema) { throw new Error('Missing schema in validateFields'); } const transforms = [validators.defaultValidateFields(schema)]; - return pipeAsync(...transforms)(fields); + await pipeAsync(...transforms)(fields); + }; + + const validatePopulate: ValidateFunc = async (populate, schema: Model, { auth } = {}) => { + if (!schema) { + throw new Error('Missing schema in sanitizePopulate'); + } + const transforms = [validators.defaultValidatePopulate(schema)]; + + if (auth) { + transforms.push(traverseQueryPopulate(visitors.throwRestrictedRelations(auth), { schema })); + } + + await pipeAsync(...transforms)(populate); }; return { @@ -131,6 +147,7 @@ const createContentAPIValidators = () => { filters: validateFilters, sort: validateSort, fields: validateFields, + populate: validatePopulate, }; }; diff --git a/packages/core/utils/src/validate/utils.ts b/packages/core/utils/src/validate/utils.ts index 3c7de9df08..b144dd72a5 100644 --- a/packages/core/utils/src/validate/utils.ts +++ b/packages/core/utils/src/validate/utils.ts @@ -1,5 +1,8 @@ import { ValidationError } from '../errors'; -export const throwInvalidParam = ({ key }: { key: string }) => { - throw new ValidationError(`Invalid parameter ${key}`); +export const throwInvalidParam = ({ key, path }: { key: string; path?: string | null }) => { + const msg = + path && path !== key ? `Invalid parameter ${key} at ${path}` : `Invalid parameter ${key}`; + + throw new ValidationError(msg); }; diff --git a/packages/core/utils/src/validate/validators.ts b/packages/core/utils/src/validate/validators.ts index e7a895519d..467bef41f2 100644 --- a/packages/core/utils/src/validate/validators.ts +++ b/packages/core/utils/src/validate/validators.ts @@ -3,7 +3,12 @@ import { curry, isEmpty, isNil } from 'lodash/fp'; import { pipeAsync } from '../async'; import traverseEntity from '../traverse-entity'; import { isScalarAttribute } from '../content-types'; -import { traverseQueryFilters, traverseQuerySort, traverseQueryFields } from '../traverse'; +import { + traverseQueryFilters, + traverseQuerySort, + traverseQueryFields, + traverseQueryPopulate, +} from '../traverse'; import { throwPassword, throwPrivate, throwDynamicZones, throwMorphToRelations } from './visitors'; import { isOperator } from '../operators'; import { throwInvalidParam } from './utils'; @@ -25,7 +30,7 @@ const defaultValidateFilters = curry((schema: Model, filters: unknown) => { return pipeAsync( // keys that are not attributes or valid operators traverseQueryFilters( - ({ key, attribute }) => { + ({ key, attribute, path }) => { // ID is not an attribute per se, so we need to make // an extra check to ensure we're not removing it if (key === 'id') { @@ -35,7 +40,7 @@ const defaultValidateFilters = curry((schema: Model, filters: unknown) => { const isAttribute = !!attribute; if (!isAttribute && !isOperator(key)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }, { schema } @@ -60,7 +65,7 @@ const defaultValidateSort = curry((schema: Model, sort: unknown) => { return pipeAsync( // non attribute keys traverseQuerySort( - ({ key, attribute }) => { + ({ key, attribute, path }) => { // ID is not an attribute per se, so we need to make // an extra check to ensure we're not removing it if (key === 'id') { @@ -68,7 +73,7 @@ const defaultValidateSort = curry((schema: Model, sort: unknown) => { } if (!attribute) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }, { schema } @@ -83,7 +88,7 @@ const defaultValidateSort = curry((schema: Model, sort: unknown) => { traverseQuerySort(throwPassword, { schema }), // keys for empty non-scalar values traverseQuerySort( - ({ key, attribute, value }) => { + ({ key, attribute, value, path }) => { // ID is not an attribute per se, so we need to make // an extra check to ensure we're not removing it if (key === 'id') { @@ -91,7 +96,7 @@ const defaultValidateSort = curry((schema: Model, sort: unknown) => { } if (!isScalarAttribute(attribute) && isEmpty(value)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }, { schema } @@ -106,7 +111,7 @@ const defaultValidateFields = curry((schema: Model, fields: unknown) => { return pipeAsync( // Only allow scalar attributes traverseQueryFields( - ({ key, attribute }) => { + ({ key, attribute, path }) => { // ID is not an attribute per se, so we need to make // an extra check to ensure we're not removing it if (key === 'id') { @@ -114,7 +119,7 @@ const defaultValidateFields = curry((schema: Model, fields: unknown) => { } if (isNil(attribute) || !isScalarAttribute(attribute)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }, { schema } @@ -126,4 +131,44 @@ const defaultValidateFields = curry((schema: Model, fields: unknown) => { )(fields); }); -export { throwPasswords, defaultValidateFilters, defaultValidateSort, defaultValidateFields }; +const defaultValidatePopulate = curry((schema: Model, populate: unknown) => { + if (!schema) { + throw new Error('Missing schema in defaultValidatePopulate'); + } + + return pipeAsync( + traverseQueryPopulate( + async ({ key, value, schema, attribute }, { set }) => { + if (attribute) { + return; + } + + if (key === 'sort') { + set(key, await defaultValidateSort(schema, value)); + } + + if (key === 'filters') { + set(key, await defaultValidateFilters(schema, value)); + } + + if (key === 'fields') { + set(key, await defaultValidateFields(schema, value)); + } + + if (key === 'populate') { + set(key, await defaultValidatePopulate(schema, value)); + } + }, + { schema } + ), + // Remove private fields + traverseQueryPopulate(throwPrivate, { schema }) + )(populate); +}); +export { + throwPasswords, + defaultValidateFilters, + defaultValidateSort, + defaultValidateFields, + defaultValidatePopulate, +}; diff --git a/packages/core/utils/src/validate/visitors/throw-disallowed-fields.ts b/packages/core/utils/src/validate/visitors/throw-disallowed-fields.ts index bc67a7cac2..efb5b442e9 100644 --- a/packages/core/utils/src/validate/visitors/throw-disallowed-fields.ts +++ b/packages/core/utils/src/validate/visitors/throw-disallowed-fields.ts @@ -69,7 +69,7 @@ export default (allowedFields: string[] | null = null): Visitor => } // throw otherwise - throwInvalidParam({ key }); + throwInvalidParam({ key, path }); }; /** diff --git a/packages/core/utils/src/validate/visitors/throw-dynamic-zones.ts b/packages/core/utils/src/validate/visitors/throw-dynamic-zones.ts index 6f8c33248d..d6b290336a 100644 --- a/packages/core/utils/src/validate/visitors/throw-dynamic-zones.ts +++ b/packages/core/utils/src/validate/visitors/throw-dynamic-zones.ts @@ -2,9 +2,9 @@ import { isDynamicZoneAttribute } from '../../content-types'; import { throwInvalidParam } from '../utils'; import type { Visitor } from '../../traverse/factory'; -const visitor: Visitor = ({ key, attribute }) => { +const visitor: Visitor = ({ key, attribute, path }) => { if (isDynamicZoneAttribute(attribute)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }; diff --git a/packages/core/utils/src/validate/visitors/throw-morph-to-relations.ts b/packages/core/utils/src/validate/visitors/throw-morph-to-relations.ts index b932347d13..4b1d71a400 100644 --- a/packages/core/utils/src/validate/visitors/throw-morph-to-relations.ts +++ b/packages/core/utils/src/validate/visitors/throw-morph-to-relations.ts @@ -2,9 +2,9 @@ import { isMorphToRelationalAttribute } from '../../content-types'; import { throwInvalidParam } from '../utils'; import type { Visitor } from '../../traverse/factory'; -const visitor: Visitor = ({ key, attribute }) => { +const visitor: Visitor = ({ key, attribute, path }) => { if (isMorphToRelationalAttribute(attribute)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }; diff --git a/packages/core/utils/src/validate/visitors/throw-password.ts b/packages/core/utils/src/validate/visitors/throw-password.ts index e824a523c7..c2772888d3 100644 --- a/packages/core/utils/src/validate/visitors/throw-password.ts +++ b/packages/core/utils/src/validate/visitors/throw-password.ts @@ -1,9 +1,9 @@ import { throwInvalidParam } from '../utils'; import type { Visitor } from '../../traverse/factory'; -const visitor: Visitor = ({ key, attribute }) => { +const visitor: Visitor = ({ key, attribute, path }) => { if (attribute?.type === 'password') { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }; diff --git a/packages/core/utils/src/validate/visitors/throw-private.ts b/packages/core/utils/src/validate/visitors/throw-private.ts index 5b89741a53..3b0cce8185 100644 --- a/packages/core/utils/src/validate/visitors/throw-private.ts +++ b/packages/core/utils/src/validate/visitors/throw-private.ts @@ -2,7 +2,7 @@ import { isPrivateAttribute } from '../../content-types'; import { throwInvalidParam } from '../utils'; import type { Visitor } from '../../traverse/factory'; -const visitor: Visitor = ({ schema, key, attribute }) => { +const visitor: Visitor = ({ schema, key, attribute, path }) => { if (!attribute) { return; } @@ -10,7 +10,7 @@ const visitor: Visitor = ({ schema, key, attribute }) => { const isPrivate = attribute.private === true || isPrivateAttribute(schema, key); if (isPrivate) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } }; diff --git a/packages/core/utils/src/validate/visitors/throw-restricted-fields.ts b/packages/core/utils/src/validate/visitors/throw-restricted-fields.ts index b2c60ad0d2..d70c3448de 100644 --- a/packages/core/utils/src/validate/visitors/throw-restricted-fields.ts +++ b/packages/core/utils/src/validate/visitors/throw-restricted-fields.ts @@ -6,7 +6,7 @@ export default (restrictedFields: string[] | null = null): Visitor => ({ key, path: { attribute: path } }) => { // all fields if (restrictedFields === null) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path }); } // Throw on invalid formats @@ -18,7 +18,7 @@ export default (restrictedFields: string[] | null = null): Visitor => // if an exact match was found if (restrictedFields.includes(path as string)) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path }); } // nested matches @@ -26,6 +26,6 @@ export default (restrictedFields: string[] | null = null): Visitor => path?.toString().startsWith(`${allowedPath}.`) ); if (isRestrictedNested) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path }); } }; diff --git a/packages/core/utils/src/validate/visitors/throw-restricted-relations.ts b/packages/core/utils/src/validate/visitors/throw-restricted-relations.ts index 8b6e17eb50..91aa83842a 100644 --- a/packages/core/utils/src/validate/visitors/throw-restricted-relations.ts +++ b/packages/core/utils/src/validate/visitors/throw-restricted-relations.ts @@ -8,7 +8,7 @@ const { CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } = contentTypeUtils.constant type MorphArray = Array<{ __type: string }>; export default (auth: unknown): Visitor => - async ({ data, key, attribute, schema }) => { + async ({ data, key, attribute, schema, path }) => { if (!attribute) { return; } @@ -25,7 +25,7 @@ export default (auth: unknown): Visitor => const isAllowed = await hasAccessToSomeScopes(scopes, auth); if (!isAllowed) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } } }; @@ -37,7 +37,7 @@ export default (auth: unknown): Visitor => // If the authenticated user don't have access to any of the scopes if (!isAllowed) { - throwInvalidParam({ key }); + throwInvalidParam({ key, path: path.attribute }); } };