@strapi/utils
Version:
Shared utilities for the Strapi packages
269 lines (266 loc) • 10.4 kB
JavaScript
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