UNPKG

@strapi/utils

Version:

Shared utilities for the Strapi packages

300 lines (296 loc) • 11.7 kB
'use strict'; var fp = require('lodash/fp'); var contentTypes = require('../content-types.js'); var contentApiConstants = require('../content-api-constants.js'); var contentApiRouteParams = require('../content-api-route-params.js'); var async = require('../async.js'); var utils = require('./utils.js'); var index = require('./visitors/index.js'); var validators = require('./validators.js'); var traverseEntity = require('../traverse-entity.js'); var queryFilters = require('../traverse/query-filters.js'); var querySort = require('../traverse/query-sort.js'); var queryPopulate = require('../traverse/query-populate.js'); require('../traverse/query-fields.js'); var errors = require('../errors.js'); var publicationFilter = require('../publication-filter.js'); var throwUnrecognizedFields = require('./visitors/throw-unrecognized-fields.js'); var throwRestrictedRelations = require('./visitors/throw-restricted-relations.js'); var throwRestrictedFields = require('./visitors/throw-restricted-fields.js'); const { ID_ATTRIBUTE, DOC_ID_ATTRIBUTE } = contentTypes.constants; const createAPIValidators = (opts)=>{ const { getModel } = opts || {}; const validateInput = async (data, schema, options = {})=>{ const { auth, route } = options; if (!schema) { throw new Error('Missing schema in validateInput'); } if (fp.isArray(data)) { await Promise.all(data.map((entry)=>validateInput(entry, schema, options))); return; } const allowedExtraRootKeys = contentApiRouteParams.getExtraRootKeysFromRouteBody(route); const nonWritableAttributes = contentTypes.getNonWritableAttributes(schema); const transforms = [ (data)=>{ if (fp.isObject(data)) { if (ID_ATTRIBUTE in data) { utils.throwInvalidKey({ key: ID_ATTRIBUTE }); } if (DOC_ID_ATTRIBUTE in data) { utils.throwInvalidKey({ key: DOC_ID_ATTRIBUTE }); } } return data; }, // non-writable attributes traverseEntity(throwRestrictedFields(nonWritableAttributes), { schema, getModel }), // unrecognized attributes (allowedExtraRootKeys = registered input param keys) traverseEntity(throwUnrecognizedFields, { schema, getModel, allowedExtraRootKeys }) ]; if (auth) { // restricted relations transforms.push(traverseEntity(throwRestrictedRelations(auth), { schema, getModel })); } // Apply validators from registry if exists opts?.validators?.input?.forEach((validator)=>transforms.push(validator(schema))); try { await async.pipe(...transforms)(data); // Validate extra root keys from route's body schema with Zod (throw on failure). // // Content-api sends the document payload as body.data; the controller calls validateInput(body.data, ctx), // so the input we receive here is the inner payload (keys like "relatedMedia", "name"), not the full body. // The route's body schema is z.object({ data: ... }), so its shape includes "data". We skip "data" because // the main document payload is already validated above by traverseEntity (throwUnrecognizedFields, etc.); // relation ops (connect/disconnect/set) are handled there, not by the route's Zod schema. We only run // Zod here for truly extra root keys added via addInputParams (e.g. clientMutationId). if (fp.isObject(data) && route?.request?.body?.['application/json']) { const bodySchema = route.request.body['application/json']; if (typeof bodySchema === 'object' && 'shape' in bodySchema) { const shape = bodySchema.shape; const dataObj = data; for (const key of Object.keys(shape)){ if (key === 'data' || !(key in dataObj)) continue; const zodSchema = shape[key]; if (zodSchema && typeof zodSchema.parse === 'function') { const result = zodSchema.safeParse(dataObj[key]); if (!result.success) { throw new errors.ValidationError(result.error?.message ?? 'Validation failed', { key, path: null, source: 'body', param: key }); } } } } } } catch (e) { if (e instanceof errors.ValidationError) { e.details.source = 'body'; } throw e; } }; const validateQuery = async (query, schema, { auth, strictParams = false, route } = {})=>{ if (!schema) { throw new Error('Missing schema in validateQuery'); } // Core allowlisted params are not covered by route.request.query Zod (see getExtraQueryKeysFromRoute). if ('publicationFilter' in query) { publicationFilter.validatePublicationFilterQueryParam(query.publicationFilter); } if (strictParams) { const extraQueryKeys = contentApiRouteParams.getExtraQueryKeysFromRoute(route); const allowedKeys = [ ...contentApiConstants.ALLOWED_QUERY_PARAM_KEYS, ...extraQueryKeys ]; for (const key of Object.keys(query)){ if (!allowedKeys.includes(key)) { try { utils.throwInvalidKey({ key, path: null }); } catch (e) { if (e instanceof errors.ValidationError) { e.details.source = 'query'; e.details.param = key; } throw e; } } } // Validate extra query keys from route's request schema with Zod (throw on failure) const routeQuerySchema = route?.request?.query; if (routeQuerySchema) { for (const key of extraQueryKeys){ if (key in query) { const zodSchema = routeQuerySchema[key]; if (zodSchema && typeof zodSchema.parse === 'function') { const result = zodSchema.safeParse(query[key]); if (!result.success) { throw new errors.ValidationError(result.error?.message ?? 'Invalid query param', { key, path: null, source: 'query', param: key }); } } } } } } const { filters, sort, fields, populate } = query; if (filters) { await validateFilters(filters, schema, { auth }); } if (sort) { await validateSort(sort, schema, { auth }); } if (fields) { await validateFields(fields, schema); } // 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 = async (filters, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in validateFilters'); } if (fp.isArray(filters)) { await Promise.all(filters.map((filter)=>validateFilters(filter, schema, { auth }))); return; } const transforms = [ validators.defaultValidateFilters({ schema, getModel }) ]; if (auth) { transforms.push(queryFilters(throwRestrictedRelations(auth), { schema, getModel })); } try { await async.pipe(...transforms)(filters); } catch (e) { if (e instanceof errors.ValidationError) { e.details.source = 'query'; e.details.param = 'filters'; } throw e; } }; const validateSort = async (sort, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in validateSort'); } const transforms = [ validators.defaultValidateSort({ schema, getModel }) ]; if (auth) { transforms.push(querySort(throwRestrictedRelations(auth), { schema, getModel })); } try { await async.pipe(...transforms)(sort); } catch (e) { if (e instanceof errors.ValidationError) { e.details.source = 'query'; e.details.param = 'sort'; } throw e; } }; const validateFields = async (fields, schema)=>{ if (!schema) { throw new Error('Missing schema in validateFields'); } const transforms = [ validators.defaultValidateFields({ schema, getModel }) ]; try { await async.pipe(...transforms)(fields); } catch (e) { if (e instanceof errors.ValidationError) { e.details.source = 'query'; e.details.param = 'fields'; } throw e; } }; const validatePopulate = async (populate, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizePopulate'); } const transforms = [ validators.defaultValidatePopulate({ schema, getModel }) ]; if (auth) { transforms.push(queryPopulate(throwRestrictedRelations(auth), { schema, getModel })); } try { await async.pipe(...transforms)(populate); } catch (e) { if (e instanceof errors.ValidationError) { e.details.source = 'query'; e.details.param = 'populate'; } throw e; } }; return { input: validateInput, query: validateQuery, filters: validateFilters, sort: validateSort, fields: validateFields, populate: validatePopulate }; }; exports.visitors = index; exports.validators = validators; exports.createAPIValidators = createAPIValidators; //# sourceMappingURL=index.js.map