@strapi/utils
Version:
Shared utilities for the Strapi packages
300 lines (296 loc) • 11.7 kB
JavaScript
;
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