UNPKG

@strapi/utils

Version:

Shared utilities for the Strapi packages

269 lines (266 loc) • 10.4 kB
import { isArray, cloneDeep, pick, omit } from 'lodash/fp'; import { getNonWritableAttributes, constants } from '../content-types.mjs'; import { ALLOWED_QUERY_PARAM_KEYS } from '../content-api-constants.mjs'; import { getExtraQueryKeysFromRoute, getExtraRootKeysFromRouteBody } from '../content-api-route-params.mjs'; import { pipe } from '../async.mjs'; import * as index from './visitors/index.mjs'; export { index as visitors }; import { defaultSanitizePopulate, defaultSanitizeFields, defaultSanitizeSort, defaultSanitizeFilters, defaultSanitizeOutput } from './sanitizers.mjs'; import * as sanitizers from './sanitizers.mjs'; export { sanitizers }; import traverseEntity from '../traverse-entity.mjs'; import traverseQueryFilters from '../traverse/query-filters.mjs'; import traverseQuerySort from '../traverse/query-sort.mjs'; import traverseQueryPopulate from '../traverse/query-populate.mjs'; import '../traverse/query-fields.mjs'; import { validatePublicationFilterQueryParam } from '../publication-filter.mjs'; import removeUnrecognizedFields from './visitors/remove-unrecognized-fields.mjs'; import removeRestrictedRelations from './visitors/remove-restricted-relations.mjs'; import removeRestrictedFields from './visitors/remove-restricted-fields.mjs'; const createAPISanitizers = (opts)=>{ const { getModel } = opts; const sanitizeInput = (data, schema, { auth, strictParams = false, route } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeInput'); } if (isArray(data)) { return Promise.all(data.map((entry)=>sanitizeInput(entry, schema, { auth, strictParams, route }))); } const allowedExtraRootKeys = getExtraRootKeysFromRouteBody(route); const nonWritableAttributes = getNonWritableAttributes(schema); const transforms = [ // Remove first level ID in inputs omit(constants.ID_ATTRIBUTE), omit(constants.DOC_ID_ATTRIBUTE), // Remove non-writable attributes traverseEntity(removeRestrictedFields(nonWritableAttributes), { schema, getModel }) ]; if (strictParams) { // Remove unrecognized fields (allowedExtraRootKeys = registered input param keys) transforms.push(traverseEntity(removeUnrecognizedFields, { schema, getModel, allowedExtraRootKeys })); } if (auth) { // Remove restricted relations transforms.push(traverseEntity(removeRestrictedRelations(auth), { schema, getModel })); } // Apply sanitizers from registry if exists opts?.sanitizers?.input?.forEach((sanitizer)=>transforms.push(sanitizer(schema))); /** * For each extra root key from the route's body schema present in data, run Zod safeParse. * If parsing fails, the key is removed from the output. * * Content-api sends the document payload as body.data; the controller calls sanitizeInput(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 sanitized above by traverseEntity (removeUnrecognizedFields, 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). */ const routeBodySanitizeTransform = async (data)=>{ if (!data || typeof data !== 'object' || Array.isArray(data)) return data; const obj = data; const bodySchema = route?.request?.body?.['application/json']; if (bodySchema && typeof bodySchema === 'object' && 'shape' in bodySchema) { const shape = bodySchema.shape; for (const key of Object.keys(shape)){ if (key === 'data' || !(key in obj)) continue; const zodSchema = shape[key]; if (zodSchema && typeof zodSchema.safeParse === 'function') { const result = zodSchema.safeParse(obj[key]); if (result.success) { obj[key] = result.data; } else { delete obj[key]; } } } } return data; }; transforms.push(routeBodySanitizeTransform); return pipe(...transforms)(data); }; const sanitizeOutput = async (data, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeOutput'); } if (isArray(data)) { const res = new Array(data.length); for(let i = 0; i < data.length; i += 1){ res[i] = await sanitizeOutput(data[i], schema, { auth }); } return res; } const transforms = [ (data)=>defaultSanitizeOutput({ schema, getModel }, data) ]; if (auth) { transforms.push(traverseEntity(removeRestrictedRelations(auth), { schema, getModel })); } // Apply sanitizers from registry if exists opts?.sanitizers?.output?.forEach((sanitizer)=>transforms.push(sanitizer(schema))); return pipe(...transforms)(data); }; const sanitizeQuery = async (query, schema, { auth, strictParams = false, route } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeQuery'); } const { filters, sort, fields, populate } = query; const sanitizedQuery = cloneDeep(query); if ('publicationFilter' in sanitizedQuery) { validatePublicationFilterQueryParam(sanitizedQuery.publicationFilter); } if (filters) { Object.assign(sanitizedQuery, { filters: await sanitizeFilters(filters, schema, { auth }) }); } if (sort) { Object.assign(sanitizedQuery, { sort: await sanitizeSort(sort, schema, { auth }) }); } if (fields) { Object.assign(sanitizedQuery, { fields: await sanitizeFields(fields, schema) }); } if (populate) { Object.assign(sanitizedQuery, { populate: await sanitizePopulate(populate, schema) }); } const extraQueryKeys = getExtraQueryKeysFromRoute(route); const routeQuerySchema = route?.request?.query; if (routeQuerySchema) { for (const key of extraQueryKeys){ if (key in query) { const zodSchema = routeQuerySchema[key]; if (zodSchema && typeof zodSchema.safeParse === 'function') { const result = zodSchema.safeParse(query[key]); if (result.success) { sanitizedQuery[key] = result.data; } else { delete sanitizedQuery[key]; } } } } } if (strictParams) { const allowedKeys = [ ...ALLOWED_QUERY_PARAM_KEYS, ...extraQueryKeys ]; return pick(allowedKeys, sanitizedQuery); } return sanitizedQuery; }; const sanitizeFilters = (filters, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeFilters'); } if (isArray(filters)) { return Promise.all(filters.map((filter)=>sanitizeFilters(filter, schema, { auth }))); } const transforms = [ defaultSanitizeFilters({ schema, getModel }) ]; if (auth) { transforms.push(traverseQueryFilters(removeRestrictedRelations(auth), { schema, getModel })); } return pipe(...transforms)(filters); }; const sanitizeSort = (sort, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeSort'); } const transforms = [ defaultSanitizeSort({ schema, getModel }) ]; if (auth) { transforms.push(traverseQuerySort(removeRestrictedRelations(auth), { schema, getModel })); } return pipe(...transforms)(sort); }; const sanitizeFields = (fields, schema)=>{ if (!schema) { throw new Error('Missing schema in sanitizeFields'); } const transforms = [ defaultSanitizeFields({ schema, getModel }) ]; return pipe(...transforms)(fields); }; const sanitizePopulate = (populate, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizePopulate'); } const transforms = [ defaultSanitizePopulate({ schema, getModel }) ]; if (auth) { transforms.push(traverseQueryPopulate(removeRestrictedRelations(auth), { schema, getModel })); } return pipe(...transforms)(populate); }; return { input: sanitizeInput, output: sanitizeOutput, query: sanitizeQuery, filters: sanitizeFilters, sort: sanitizeSort, fields: sanitizeFields, populate: sanitizePopulate }; }; export { createAPISanitizers }; //# sourceMappingURL=index.mjs.map