UNPKG

@strapi/utils

Version:

Shared utilities for the Strapi packages

526 lines (522 loc) • 19 kB
'use strict'; var fp = require('lodash/fp'); var async = require('../async.js'); var contentTypes = require('../content-types.js'); var queryFilters = require('../traverse/query-filters.js'); var querySort = require('../traverse/query-sort.js'); var queryPopulate = require('../traverse/query-populate.js'); var queryFields = require('../traverse/query-fields.js'); var throwPassword = require('./visitors/throw-password.js'); var throwPrivate = require('./visitors/throw-private.js'); require('./visitors/throw-restricted-relations.js'); var throwMorphToRelations = require('./visitors/throw-morph-to-relations.js'); var throwDynamicZones = require('./visitors/throw-dynamic-zones.js'); var utils = require('./utils.js'); var operators = require('../operators.js'); var parseType = require('../parse-type.js'); const { ID_ATTRIBUTE, DOC_ID_ATTRIBUTE } = contentTypes.constants; const FILTER_TRAVERSALS = [ 'nonAttributesOperators', 'dynamicZones', 'morphRelations', 'passwords', 'private' ]; const validateFilters = utils.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(queryFilters(({ 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 && !operators.isOperator(key)) { utils.throwInvalidKey({ key, path: path.attribute }); } }, ctx)); } if (include.includes('dynamicZones')) { functionsToApply.push(queryFilters(throwDynamicZones, ctx)); } if (include.includes('morphRelations')) { functionsToApply.push(queryFilters(throwMorphToRelations, ctx)); } if (include.includes('passwords')) { functionsToApply.push(queryFilters(throwPassword, ctx)); } if (include.includes('private')) { functionsToApply.push(queryFilters(throwPrivate, ctx)); } // Return directly if no validation functions are provided if (functionsToApply.length === 0) { return filters; } return async.pipe(...functionsToApply)(filters); }); const defaultValidateFilters = utils.asyncCurry(async (ctx, filters)=>{ return validateFilters(ctx, filters, FILTER_TRAVERSALS); }); const SORT_TRAVERSALS = [ 'nonAttributesOperators', 'dynamicZones', 'morphRelations', 'passwords', 'private', 'nonScalarEmptyKeys' ]; const validateSort = utils.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(querySort(({ 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; } // status is a virtual sort key for D&P-enabled content types if (key === 'status' && ctx.schema?.options?.draftAndPublish) { return; } if (!attribute) { utils.throwInvalidKey({ key, path: path.attribute }); } }, ctx)); } // Validate dynamic zones from sort if (include.includes('dynamicZones')) { functionsToApply.push(querySort(throwDynamicZones, ctx)); } // Validate morphTo relations from sort if (include.includes('morphRelations')) { functionsToApply.push(querySort(throwMorphToRelations, ctx)); } // Validate passwords from sort if (include.includes('passwords')) { functionsToApply.push(querySort(throwPassword, ctx)); } // Validate private from sort if (include.includes('private')) { functionsToApply.push(querySort(throwPrivate, ctx)); } // Validate non-scalar empty keys if (include.includes('nonScalarEmptyKeys')) { functionsToApply.push(querySort(({ 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 (!contentTypes.isScalarAttribute(attribute) && fp.isEmpty(value)) { utils.throwInvalidKey({ key, path: path.attribute }); } }, ctx)); } // Return directly if no validation functions are provided if (functionsToApply.length === 0) { return sort; } return async.pipe(...functionsToApply)(sort); }); const defaultValidateSort = utils.asyncCurry(async (ctx, sort)=>{ return validateSort(ctx, sort, SORT_TRAVERSALS); }); const FIELDS_TRAVERSALS = [ 'scalarAttributes', 'privateFields', 'passwordFields' ]; const validateFields = utils.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(queryFields(({ 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 (fp.isNil(attribute) || !contentTypes.isScalarAttribute(attribute)) { utils.throwInvalidKey({ key, path: path.attribute }); } }, ctx)); } // Private fields if (include.includes('privateFields')) { functionsToApply.push(queryFields(throwPrivate, ctx)); } // Password fields if (include.includes('passwordFields')) { functionsToApply.push(queryFields(throwPassword, ctx)); } // Return directly if no validation functions are provided if (functionsToApply.length === 0) { return fields; } return async.pipe(...functionsToApply)(fields); }); const defaultValidateFields = utils.asyncCurry(async (ctx, fields)=>{ return validateFields(ctx, fields, FIELDS_TRAVERSALS); }); const isMorphLikeAttribute = (attribute)=>contentTypes.isDynamicZoneAttribute(attribute) || contentTypes.isMorphToRelationalAttribute(attribute); const isPopulatableAttribute = (attribute)=>!!attribute && [ 'relation', 'dynamiczone', 'component', 'media' ].includes(attribute.type); const isDotNotationPopulate = (populate)=>{ if (populate === '*') { return false; } if (typeof populate === 'string') { return true; } return Array.isArray(populate) && populate.length > 0 && populate.every((p)=>typeof p === 'string'); }; const flattenDotPopulatePaths = (populate)=>{ const items = Array.isArray(populate) ? populate : [ populate ]; return items.flatMap((item)=>item.split(',').map((segment)=>fp.trim(segment)).filter(Boolean)); }; const validatePopulateDotPathSegments = ({ schema, getModel }, segments, pathPrefix)=>{ if (!segments.length) { return; } const [head, ...tail] = segments; const fullPath = pathPrefix ? `${pathPrefix}.${head}` : head; const attribute = schema.attributes[head]; if (!attribute || !isPopulatableAttribute(attribute)) { utils.throwInvalidKey({ key: head, path: fullPath }); } if (!tail.length) { return; } if (attribute.type === 'component') { if (!attribute.component) { utils.throwInvalidKey({ key: head, path: fullPath }); } validatePopulateDotPathSegments({ schema: getModel(attribute.component), getModel }, tail, fullPath); return; } if (attribute.type === 'relation') { if (isMorphLikeAttribute(attribute)) { utils.throwInvalidKey({ key: head, path: fullPath }); } if (!attribute.target) { utils.throwInvalidKey({ key: head, path: fullPath }); } validatePopulateDotPathSegments({ schema: getModel(attribute.target), getModel }, tail, fullPath); return; } if (attribute.type === 'media') { utils.throwInvalidKey({ key: head, path: fullPath }); } if (attribute.type === 'dynamiczone') { const [nextSegment, ...rest] = tail; const nextPath = `${fullPath}.${nextSegment}`; const candidates = (attribute.components ?? []).map((uid)=>getModel(uid)).filter((model)=>model?.attributes?.[nextSegment]); if (!candidates.length) { utils.throwInvalidKey({ key: nextSegment, path: nextPath }); } let validated = false; for (const componentSchema of candidates){ const nestedAttribute = componentSchema.attributes[nextSegment]; try { if (!rest.length) { if (nestedAttribute) { validated = true; break; } } else if (nestedAttribute?.type === 'component' && nestedAttribute.component) { validatePopulateDotPathSegments({ schema: getModel(nestedAttribute.component), getModel }, rest, nextPath); validated = true; break; } else if (nestedAttribute?.type === 'relation' && !isMorphLikeAttribute(nestedAttribute) && nestedAttribute.target) { validatePopulateDotPathSegments({ schema: getModel(nestedAttribute.target), getModel }, rest, nextPath); validated = true; break; } } catch { // Try the next dynamic-zone component type. } } if (!validated) { utils.throwInvalidKey({ key: nextSegment, path: nextPath }); } } }; const validatePopulateDotPaths = (ctx, populate)=>{ for (const path of flattenDotPopulatePaths(populate)){ const segments = path.split('.').map((segment)=>fp.trim(segment)).filter(Boolean); validatePopulateDotPathSegments(ctx, segments, ''); } }; const validateMorphLikeNestedPopulate = (populateValue, { path })=>{ // Keep in sync with convert-query-params polymorphic nested populate handling. if (!fp.isNil(populateValue) && populateValue !== '*') { utils.throwInvalidKey({ key: 'populate', path }); } }; const POPULATE_TRAVERSALS = [ 'nonAttributesOperators', 'private' ]; const validatePopulate = utils.asyncCurry(async (ctx, populate, includes)=>{ if (!ctx.schema) { throw new Error('Missing schema in defaultValidatePopulate'); } if (isDotNotationPopulate(populate)) { validatePopulateDotPaths({ schema: ctx.schema, getModel: ctx.getModel }, populate); if (includes?.populate?.includes('private')) { return queryPopulate(throwPrivate, ctx)(populate); } return populate; } // Build the list of functions conditionally based on the include array const functionsToApply = []; // Always include the main traversal function functionsToApply.push(queryPopulate(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); if (isMorphLikeAttribute(attribute) && fp.isObject(value) && 'populate' in value) { validateMorphLikeNestedPopulate(value.populate, { path: path.raw }); } // Throw on non-populate attributes if (!isPopulatableAttribute) { utils.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 (!fp.isObject(value)) { return utils.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) { utils.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 { utils.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')) { utils.throwInvalidKey({ key, path: path.attribute }); } }, ctx)); // Conditionally traverse for private fields only if 'private' is included if (includes?.populate?.includes('private')) { functionsToApply.push(queryPopulate(throwPrivate, ctx)); } // Return directly if no validation functions are provided if (functionsToApply.length === 0) { return populate; } return async.pipe(...functionsToApply)(populate); }); const defaultValidatePopulate = utils.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 }); }); exports.FIELDS_TRAVERSALS = FIELDS_TRAVERSALS; exports.FILTER_TRAVERSALS = FILTER_TRAVERSALS; exports.POPULATE_TRAVERSALS = POPULATE_TRAVERSALS; exports.SORT_TRAVERSALS = SORT_TRAVERSALS; exports.defaultValidateFields = defaultValidateFields; exports.defaultValidateFilters = defaultValidateFilters; exports.defaultValidatePopulate = defaultValidatePopulate; exports.defaultValidateSort = defaultValidateSort; exports.validateFields = validateFields; exports.validateFilters = validateFilters; exports.validatePopulate = validatePopulate; exports.validateSort = validateSort; //# sourceMappingURL=validators.js.map