mirror of
https://github.com/strapi/strapi.git
synced 2025-09-01 12:53:03 +00:00
fix: validate query populate
This commit is contained in:
parent
2c180844b3
commit
5c8ef69f82
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
|
@ -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 }),
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 }
|
||||
),
|
||||
|
@ -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<Record<string, true>>((acc, [key]) => ({ ...acc, [key]: true }), {});
|
||||
|
||||
set('', newPopulateQuery);
|
||||
}
|
||||
};
|
||||
|
||||
export default visitor;
|
@ -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';
|
||||
|
@ -51,7 +51,7 @@ interface Interceptor<T = unknown> {
|
||||
interface ParseUtils<T> {
|
||||
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;
|
||||
}
|
||||
|
@ -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)));
|
||||
})
|
||||
|
@ -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<string, unknown> => 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('.');
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -69,7 +69,7 @@ export default (allowedFields: string[] | null = null): Visitor =>
|
||||
}
|
||||
|
||||
// throw otherwise
|
||||
throwInvalidParam({ key });
|
||||
throwInvalidParam({ key, path });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user