UNPKG

@strapi/utils

Version:

Shared utilities for the Strapi packages

270 lines (266 loc) • 10.4 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 index = require('./visitors/index.js'); var sanitizers = require('./sanitizers.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 publicationFilter = require('../publication-filter.js'); var removeUnrecognizedFields = require('./visitors/remove-unrecognized-fields.js'); var removeRestrictedRelations = require('./visitors/remove-restricted-relations.js'); var removeRestrictedFields = require('./visitors/remove-restricted-fields.js'); const createAPISanitizers = (opts)=>{ const { getModel } = opts; const sanitizeInput = (data, schema, { auth, strictParams = false, route } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeInput'); } if (fp.isArray(data)) { return Promise.all(data.map((entry)=>sanitizeInput(entry, schema, { auth, strictParams, route }))); } const allowedExtraRootKeys = contentApiRouteParams.getExtraRootKeysFromRouteBody(route); const nonWritableAttributes = contentTypes.getNonWritableAttributes(schema); const transforms = [ // Remove first level ID in inputs fp.omit(contentTypes.constants.ID_ATTRIBUTE), fp.omit(contentTypes.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 async.pipe(...transforms)(data); }; const sanitizeOutput = async (data, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeOutput'); } if (fp.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)=>sanitizers.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 async.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 = fp.cloneDeep(query); if ('publicationFilter' in sanitizedQuery) { publicationFilter.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 = contentApiRouteParams.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 = [ ...contentApiConstants.ALLOWED_QUERY_PARAM_KEYS, ...extraQueryKeys ]; return fp.pick(allowedKeys, sanitizedQuery); } return sanitizedQuery; }; const sanitizeFilters = (filters, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeFilters'); } if (fp.isArray(filters)) { return Promise.all(filters.map((filter)=>sanitizeFilters(filter, schema, { auth }))); } const transforms = [ sanitizers.defaultSanitizeFilters({ schema, getModel }) ]; if (auth) { transforms.push(queryFilters(removeRestrictedRelations(auth), { schema, getModel })); } return async.pipe(...transforms)(filters); }; const sanitizeSort = (sort, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizeSort'); } const transforms = [ sanitizers.defaultSanitizeSort({ schema, getModel }) ]; if (auth) { transforms.push(querySort(removeRestrictedRelations(auth), { schema, getModel })); } return async.pipe(...transforms)(sort); }; const sanitizeFields = (fields, schema)=>{ if (!schema) { throw new Error('Missing schema in sanitizeFields'); } const transforms = [ sanitizers.defaultSanitizeFields({ schema, getModel }) ]; return async.pipe(...transforms)(fields); }; const sanitizePopulate = (populate, schema, { auth } = {})=>{ if (!schema) { throw new Error('Missing schema in sanitizePopulate'); } const transforms = [ sanitizers.defaultSanitizePopulate({ schema, getModel }) ]; if (auth) { transforms.push(queryPopulate(removeRestrictedRelations(auth), { schema, getModel })); } return async.pipe(...transforms)(populate); }; return { input: sanitizeInput, output: sanitizeOutput, query: sanitizeQuery, filters: sanitizeFilters, sort: sanitizeSort, fields: sanitizeFields, populate: sanitizePopulate }; }; exports.visitors = index; exports.sanitizers = sanitizers; exports.createAPISanitizers = createAPISanitizers; //# sourceMappingURL=index.js.map