@strapi/utils
Version:
Shared utilities for the Strapi packages
357 lines (354 loc) • 13.1 kB
JavaScript
import { isEmpty, isNil, isObject } from 'lodash/fp';
import { pipe } from '../async.mjs';
import { isScalarAttribute, constants } from '../content-types.mjs';
import traverseQueryFilters from '../traverse/query-filters.mjs';
import traverseQuerySort from '../traverse/query-sort.mjs';
import traverseQueryPopulate from '../traverse/query-populate.mjs';
import traverseQueryFields from '../traverse/query-fields.mjs';
import visitor$2 from './visitors/throw-password.mjs';
import visitor$3 from './visitors/throw-private.mjs';
import { asyncCurry, throwInvalidKey } from './utils.mjs';
import visitor$1 from './visitors/throw-morph-to-relations.mjs';
import visitor from './visitors/throw-dynamic-zones.mjs';
import './visitors/throw-unrecognized-fields.mjs';
import { isOperator } from '../operators.mjs';
import parseType from '../parse-type.mjs';
const { ID_ATTRIBUTE, DOC_ID_ATTRIBUTE } = constants;
const FILTER_TRAVERSALS = [
'nonAttributesOperators',
'dynamicZones',
'morphRelations',
'passwords',
'private'
];
const validateFilters = asyncCurry(async (ctx, filters, include)=>{
// TODO: schema checks should check that it is a valid schema with yup
if (!ctx.schema) {
throw new Error('Missing schema in defaultValidateFilters');
}
// Build the list of functions conditionally
const functionsToApply = [];
// keys that are not attributes or valid operators
if (include.includes('nonAttributesOperators')) {
functionsToApply.push(traverseQueryFilters(({ 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 ([
ID_ATTRIBUTE,
DOC_ID_ATTRIBUTE
].includes(key)) {
return;
}
const isAttribute = !!attribute;
if (!isAttribute && !isOperator(key)) {
throwInvalidKey({
key,
path: path.attribute
});
}
}, ctx));
}
if (include.includes('dynamicZones')) {
functionsToApply.push(traverseQueryFilters(visitor, ctx));
}
if (include.includes('morphRelations')) {
functionsToApply.push(traverseQueryFilters(visitor$1, ctx));
}
if (include.includes('passwords')) {
functionsToApply.push(traverseQueryFilters(visitor$2, ctx));
}
if (include.includes('private')) {
functionsToApply.push(traverseQueryFilters(visitor$3, ctx));
}
// Return directly if no validation functions are provided
if (functionsToApply.length === 0) {
return filters;
}
return pipe(...functionsToApply)(filters);
});
const defaultValidateFilters = asyncCurry(async (ctx, filters)=>{
return validateFilters(ctx, filters, FILTER_TRAVERSALS);
});
const SORT_TRAVERSALS = [
'nonAttributesOperators',
'dynamicZones',
'morphRelations',
'passwords',
'private',
'nonScalarEmptyKeys'
];
const validateSort = asyncCurry(async (ctx, sort, include)=>{
if (!ctx.schema) {
throw new Error('Missing schema in defaultValidateSort');
}
// Build the list of functions conditionally based on the include array
const functionsToApply = [];
// Validate non attribute keys
if (include.includes('nonAttributesOperators')) {
functionsToApply.push(traverseQuerySort(({ 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 ([
ID_ATTRIBUTE,
DOC_ID_ATTRIBUTE
].includes(key)) {
return;
}
if (!attribute) {
throwInvalidKey({
key,
path: path.attribute
});
}
}, ctx));
}
// Validate dynamic zones from sort
if (include.includes('dynamicZones')) {
functionsToApply.push(traverseQuerySort(visitor, ctx));
}
// Validate morphTo relations from sort
if (include.includes('morphRelations')) {
functionsToApply.push(traverseQuerySort(visitor$1, ctx));
}
// Validate passwords from sort
if (include.includes('passwords')) {
functionsToApply.push(traverseQuerySort(visitor$2, ctx));
}
// Validate private from sort
if (include.includes('private')) {
functionsToApply.push(traverseQuerySort(visitor$3, ctx));
}
// Validate non-scalar empty keys
if (include.includes('nonScalarEmptyKeys')) {
functionsToApply.push(traverseQuerySort(({ 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 ([
ID_ATTRIBUTE,
DOC_ID_ATTRIBUTE
].includes(key)) {
return;
}
if (!isScalarAttribute(attribute) && isEmpty(value)) {
throwInvalidKey({
key,
path: path.attribute
});
}
}, ctx));
}
// Return directly if no validation functions are provided
if (functionsToApply.length === 0) {
return sort;
}
return pipe(...functionsToApply)(sort);
});
const defaultValidateSort = asyncCurry(async (ctx, sort)=>{
return validateSort(ctx, sort, SORT_TRAVERSALS);
});
const FIELDS_TRAVERSALS = [
'scalarAttributes',
'privateFields',
'passwordFields'
];
const validateFields = asyncCurry(async (ctx, fields, include)=>{
if (!ctx.schema) {
throw new Error('Missing schema in defaultValidateFields');
}
// Build the list of functions conditionally based on the include array
const functionsToApply = [];
// Only allow scalar attributes
if (include.includes('scalarAttributes')) {
functionsToApply.push(traverseQueryFields(({ key, attribute, path })=>{
// ID is not an attribute per se, so we need to make
// an extra check to ensure we're not throwing because of it
if ([
ID_ATTRIBUTE,
DOC_ID_ATTRIBUTE
].includes(key)) {
return;
}
if (isNil(attribute) || !isScalarAttribute(attribute)) {
throwInvalidKey({
key,
path: path.attribute
});
}
}, ctx));
}
// Private fields
if (include.includes('privateFields')) {
functionsToApply.push(traverseQueryFields(visitor$3, ctx));
}
// Password fields
if (include.includes('passwordFields')) {
functionsToApply.push(traverseQueryFields(visitor$2, ctx));
}
// Return directly if no validation functions are provided
if (functionsToApply.length === 0) {
return fields;
}
return pipe(...functionsToApply)(fields);
});
const defaultValidateFields = asyncCurry(async (ctx, fields)=>{
return validateFields(ctx, fields, FIELDS_TRAVERSALS);
});
const POPULATE_TRAVERSALS = [
'nonAttributesOperators',
'private'
];
const validatePopulate = asyncCurry(async (ctx, populate, includes)=>{
if (!ctx.schema) {
throw new Error('Missing schema in defaultValidatePopulate');
}
// Build the list of functions conditionally based on the include array
const functionsToApply = [];
// Always include the main traversal function
functionsToApply.push(traverseQueryPopulate(async ({ key, path, value, schema, attribute, getModel, parent }, { set })=>{
/**
* NOTE: The parent check is done to support "filters" (and the rest of keys) as valid attribute names.
*
* The parent will not be an attribute when its a "populate" / "filters" / "sort" ... key.
* Only in those scenarios the node will be an attribute.
*/ if (!parent?.attribute && attribute) {
const isPopulatableAttribute = [
'relation',
'dynamiczone',
'component',
'media'
].includes(attribute.type);
// Throw on non-populate attributes
if (!isPopulatableAttribute) {
throwInvalidKey({
key,
path: path.raw
});
}
// Valid populatable attribute, so return
return;
}
// If we're looking at a populate fragment, ensure its target is valid
if (key === 'on') {
// Populate fragment should always be an object
if (!isObject(value)) {
return throwInvalidKey({
key,
path: path.raw
});
}
const targets = Object.keys(value);
for (const target of targets){
const model = getModel(target);
// If a target is invalid (no matching model), then raise an error
if (!model) {
throwInvalidKey({
key: target,
path: `${path.raw}.${target}`
});
}
}
// If the fragment's target is fine, then let it pass
return;
}
// Ignore plain wildcards
if (key === '' && value === '*') {
return;
}
// Ensure count is a boolean
if (key === 'count') {
try {
parseType({
type: 'boolean',
value
});
return;
} catch {
throwInvalidKey({
key,
path: path.attribute
});
}
}
// Allowed boolean-like keywords should be ignored
try {
parseType({
type: 'boolean',
value: key
});
// Key is an allowed boolean-like keyword, skipping validation...
return;
} catch {
// Continue, because it's not a boolean-like
}
// Handle nested `sort` validation with custom or default traversals
if (key === 'sort') {
set(key, await validateSort({
schema,
getModel
}, value, includes?.sort || SORT_TRAVERSALS));
return;
}
// Handle nested `filters` validation with custom or default traversals
if (key === 'filters') {
set(key, await validateFilters({
schema,
getModel
}, value, includes?.filters || FILTER_TRAVERSALS));
return;
}
// Handle nested `fields` validation with custom or default traversals
if (key === 'fields') {
set(key, await validateFields({
schema,
getModel
}, value, includes?.fields || FIELDS_TRAVERSALS));
return;
}
// Handle recursive nested `populate` validation with the same include object
if (key === 'populate') {
set(key, await validatePopulate({
schema,
getModel,
parent: {
key,
path,
schema,
attribute
},
path
}, value, includes // pass down the same includes object
));
return;
}
// Throw an error if non-attribute operators are included in the populate array
if (includes?.populate?.includes('nonAttributesOperators')) {
throwInvalidKey({
key,
path: path.attribute
});
}
}, ctx));
// Conditionally traverse for private fields only if 'private' is included
if (includes?.populate?.includes('private')) {
functionsToApply.push(traverseQueryPopulate(visitor$3, ctx));
}
// Return directly if no validation functions are provided
if (functionsToApply.length === 0) {
return populate;
}
return pipe(...functionsToApply)(populate);
});
const defaultValidatePopulate = asyncCurry(async (ctx, populate)=>{
if (!ctx.schema) {
throw new Error('Missing schema in defaultValidatePopulate');
}
// Call validatePopulate and include all validations by passing in full traversal arrays
return validatePopulate(ctx, populate, {
filters: FILTER_TRAVERSALS,
sort: SORT_TRAVERSALS,
fields: FIELDS_TRAVERSALS,
populate: POPULATE_TRAVERSALS
});
});
export { FIELDS_TRAVERSALS, FILTER_TRAVERSALS, POPULATE_TRAVERSALS, SORT_TRAVERSALS, defaultValidateFields, defaultValidateFilters, defaultValidatePopulate, defaultValidateSort, validateFields, validateFilters, validatePopulate, validateSort };
//# sourceMappingURL=validators.mjs.map