recoder-code
Version:
🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!
458 lines • 19.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RequestParameterMutator = void 0;
const types_1 = require("../../framework/types");
const url = require("url");
const util_1 = require("./util");
const mediaTypeParser = require("media-typer");
const contentTypeParser = require("content-type");
const qs_1 = require("qs");
const RESERVED_CHARS = /[\:\/\?#\[\]@!\$&\'()\*\+,;=]/;
const ARRAY_DELIMITER = {
simple: ',',
form: ',',
spaceDelimited: ' ',
pipeDelimited: '|',
};
const REQUEST_FIELDS = {
query: 'query',
header: 'headers',
path: 'params',
cookie: 'cookies',
};
/**
* A class top parse and mutate the incoming request parameters according to the openapi spec.
* the request is mutated to accomodate various styles and types e.g. form, explode, deepObject, etc
*/
class RequestParameterMutator {
constructor(ajv, apiDocs, path, parsedSchema) {
this.ajv = ajv;
this._apiDocs = apiDocs;
this.path = path;
this.parsedSchema = parsedSchema;
}
/**
* Modifies an incoming request object by applying the openapi schema
* req values may be parsed/mutated as a JSON object, JSON Exploded Object, JSON Array, or JSON Exploded Array
* @param req
*/
modifyRequest(req) {
const { parameters } = req.openapi.schema;
const rawQuery = this.parseQueryStringUndecoded(url.parse(req.originalUrl).query);
req.query = this.handleBracketNotationQueryFields(req.query);
(parameters || []).forEach((p) => {
const parameter = (0, util_1.dereferenceParameter)(this._apiDocs, p);
const { name, schema } = (0, util_1.normalizeParameter)(this.ajv, parameter);
const { type } = schema;
const { style, explode } = parameter;
const i = req.originalUrl.indexOf('?');
const queryString = req.originalUrl.substr(i + 1);
if (parameter.in === 'query' &&
!parameter.allowReserved &&
!!parameter.explode) {
//} && !!parameter.explode) {
this.validateReservedCharacters(name, rawQuery);
}
if (parameter.in === 'query' &&
!parameter.allowReserved &&
!parameter.explode) {
//} && !!parameter.explode) {
this.validateReservedCharacters(name, rawQuery, true);
}
if (parameter.content) {
this.handleContent(req, name, parameter);
}
else if (parameter.in === 'query' && this.isObjectOrXOf(schema)) {
// handle bracket notation and mutates query param
if (style === 'form' && explode) {
this.parseJsonAndMutateRequest(req, parameter.in, name);
this.handleFormExplode(req, name, schema, parameter);
}
else if (style === 'deepObject') {
this.handleDeepObject(req, queryString, name, schema);
}
else if (style === 'form' && !explode && schema.type === 'object') {
const value = req.query[name];
if (typeof value === 'string') {
const kvPairs = this.csvToKeyValuePairs(value);
if (kvPairs) {
req.query[name] = kvPairs;
return;
}
}
this.parseJsonAndMutateRequest(req, parameter.in, name);
}
else {
this.parseJsonAndMutateRequest(req, parameter.in, name);
}
}
else if (type === 'array' && !explode) {
const delimiter = ARRAY_DELIMITER[parameter.style];
this.validateArrayDelimiter(delimiter, parameter);
this.parseJsonArrayAndMutateRequest(req, parameter.in, name, delimiter, rawQuery);
}
else if (type === 'array' && explode) {
this.explodeJsonArrayAndMutateRequest(req, parameter.in, name);
}
else if (style === 'form' && explode) {
this.handleFormExplode(req, name, schema, parameter);
}
});
}
handleDeepObject(req, qs, name, schema) {
var _a;
const getDefaultSchemaValue = () => {
let defaultValue;
if (schema.default !== undefined) {
defaultValue = schema.default;
}
else if (schema.properties) {
Object.entries(schema.properties).forEach(([k, v]) => {
// Handle recursive objects
defaultValue !== null && defaultValue !== void 0 ? defaultValue : (defaultValue = {});
if (v['default']) {
defaultValue[k] = v['default'];
}
});
}
else {
['allOf', 'oneOf', 'anyOf'].forEach((key) => {
if (schema[key]) {
schema[key].forEach((s) => {
if (s.$ref) {
const compiledSchema = this.ajv.getSchema(s.$ref);
// as any -> https://stackoverflow.com/a/23553128
defaultValue =
defaultValue === undefined
? compiledSchema.schema.default
: defaultValue;
}
else {
defaultValue =
defaultValue === undefined ? s.default : defaultValue;
}
});
}
});
}
return defaultValue;
};
if (!((_a = req.query) === null || _a === void 0 ? void 0 : _a[name])) {
req.query[name] = getDefaultSchemaValue();
}
this.parseJsonAndMutateRequest(req, 'query', name);
// TODO handle url encoded?
}
handleContent(req, name, parameter) {
/**
* Per the OpenAPI3 spec:
* A map containing the representations for the parameter. The key is the media type
* and the value describes it. The map MUST only contain one entry.
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterContent
*/
const contentType = Object.keys(parameter.content)[0];
const parsedContentType = contentTypeParser.parse(contentType);
const parsedMediaType = mediaTypeParser.parse(parsedContentType.type);
const { subtype, suffix } = parsedMediaType;
const isMediaTypeJson = [subtype, suffix].includes('json');
if (isMediaTypeJson) {
const reqField = REQUEST_FIELDS[parameter.in];
this.parseJsonAndMutateRequest(req, reqField, name);
}
}
handleFormExplode(req, name, schema, parameter) {
var _a;
// fetch the keys used for this kind of explode
const type = schema.type;
const hasXOf = schema['allOf'] || schema['oneOf'] || schema['anyOf'];
const properties = hasXOf
? xOfProperties(schema)
: type === 'object'
? Object.keys((_a = schema.properties) !== null && _a !== void 0 ? _a : {})
: [];
this.explodedJsonObjectAndMutateRequest(req, parameter.in, name, properties, schema);
function xOfProperties(schema) {
return ['allOf', 'oneOf', 'anyOf'].reduce((acc, key) => {
if (!schema.hasOwnProperty(key)) {
return acc;
}
else {
const foundProperties = schema[key].reduce((acc2, obj) => {
return obj.type === 'object'
? acc2.concat(...Object.keys(obj.properties))
: acc2;
}, []);
return foundProperties.length > 0
? acc.concat(...foundProperties)
: acc;
}
}, []);
}
}
parseJsonAndMutateRequest(req, $in, name) {
var _a;
/**
* support json in request params, query, headers and cookies
* like this filter={"type":"t-shirt","color":"blue"}
*
* https://swagger.io/docs/specification/describing-parameters/#schema-vs-content
*/
const field = REQUEST_FIELDS[$in];
if ((_a = req[field]) === null || _a === void 0 ? void 0 : _a[name]) {
try {
const value = req[field][name];
const json = JSON.parse(value);
req[field][name] = json;
}
catch (e) {
// NOOP If parsing failed but _should_ contain JSON, validator will catch it.
// May contain falsely flagged parameter (e.g. input was object OR string)
}
}
}
/**
* used for !explode array parameters
* @param req
* @param $in
* @param name
* @param delimiter
* @param rawQuery
* @private
*/
parseJsonArrayAndMutateRequest(req, $in, name, delimiter, rawQuery) {
var _a, _b;
/**
* array deserialization for query and params
* filter=foo,bar,baz
* filter=foo|bar|baz
* filter=foo%20bar%20baz
*/
const field = REQUEST_FIELDS[$in];
const rawValues = [];
if (['query'].includes($in)) {
// perhaps split query from params
rawValues.concat((_a = rawQuery.get(name)) !== null && _a !== void 0 ? _a : []);
}
let i = 0;
if ((_b = req[field]) === null || _b === void 0 ? void 0 : _b[name]) {
if (Array.isArray(req[field][name]))
return;
const value = req[field][name].split(delimiter);
const rawValue = rawValues[i++];
if (rawValue === null || rawValue === void 0 ? void 0 : rawValue.includes(delimiter)) {
// TODO add && !allowReserved to improve performance. When allowReserved is true, commas are common and we do not need to do this extra work
// Currently, rawValue is only populated for query params
// if the raw value contains a delimiter, decode manually
// parse the decode value and update req[field][name]
const manuallyDecodedValues = rawValue
.split(delimiter)
.map((v) => decodeURIComponent(v));
req[field][name] = manuallyDecodedValues;
}
else {
req[field][name] = value;
}
}
}
// TODO is this method still necessary with the new qs processing introduced in the express-5 support
// (Try removing it)
explodedJsonObjectAndMutateRequest(req, $in, name, properties, schema) {
// forcing convert to object if scheme describes param as object + explode
// for easy validation, keep the schema but update whereabouts of its sub components
const field = REQUEST_FIELDS[$in];
if (req[field]) {
// check if there is at least one of the nested properties before creating the root property
const atLeastOne = properties.some((p) => {
return Object.prototype.hasOwnProperty.call(req[field], p);
});
if (atLeastOne) {
req[field][name] = {};
properties.forEach((property) => {
var _a, _b;
if (req[field][property]) {
const schema = this.parsedSchema[field];
const type = (_b = (_a = schema.properties[name].properties) === null || _a === void 0 ? void 0 : _a[property]) === null || _b === void 0 ? void 0 : _b.type;
const value = req[field][property];
const coercedValue = type === 'array' && !Array.isArray(value) ? [value] : value;
req[field][name][property] = coercedValue;
delete req[field][property];
}
});
}
}
}
explodeJsonArrayAndMutateRequest(req, $in, name) {
var _a;
/**
* forcing convert to array if scheme describes param as array + explode
*/
const field = REQUEST_FIELDS[$in];
if (((_a = req[field]) === null || _a === void 0 ? void 0 : _a[name]) && !Array.isArray(req[field][name])) {
const value = [req[field][name]];
req[field][name] = value;
}
}
isObjectOrXOf(schema) {
const schemaHasObject = (schema) => {
if (!schema)
return false;
if (schema.$ref)
return true;
const { type, allOf, oneOf, anyOf } = schema;
return (type === 'object' ||
[].concat(allOf, oneOf, anyOf).some(schemaHasObject));
};
return schemaHasObject(schema);
}
validateArrayDelimiter(delimiter, parameter) {
if (!delimiter) {
const message = `Parameter 'style' has incorrect value '${parameter.style}' for [${parameter.name}]`;
throw new types_1.BadRequest({
path: `.query.${parameter.name}`,
message: message,
});
}
}
validateReservedCharacters(name, pairs, allowComma = false) {
const vs = pairs.get(name);
if (!vs)
return;
for (const v of vs) {
const svs = allowComma ? v.split(',') : [v];
for (const sv of svs) {
if (sv === null || sv === void 0 ? void 0 : sv.match(RESERVED_CHARS)) {
const message = `Parameter '${name}' must be url encoded. Its value may not contain reserved characters.`;
throw new types_1.BadRequest({ path: `/query/${name}`, message: message });
}
}
}
}
parseQueryStringUndecoded(qs) {
if (!qs)
return new Map();
const q = qs.replace('?', '');
return q.split('&').reduce((m, p) => {
var _a;
const [k, v] = p.split('=');
m.set(k, (_a = m.get(k)) !== null && _a !== void 0 ? _a : []);
m.get(k).push(v);
return m;
}, new Map());
}
csvToKeyValuePairs(csvString) {
const hasBrace = csvString.split('{').length > 1;
const items = csvString.split(',');
if (hasBrace) {
// if it has a brace, we assume its JSON and skip creating k v pairs
// TODO improve json check, but ensure its cheap
return;
}
if (items.length % 2 !== 0) {
// if the number of elements is not event,
// then we do not have k v pairs, so return undefined
return;
}
const result = {};
for (let i = 0; i < items.length - 1; i += 2) {
result[items[i]] = items[i + 1];
}
return result;
}
/**
* Handles query parameters with bracket notation.
* @param query The query parameters object to process
* @returns The processed query parameters object
*/
handleBracketNotationQueryFields(query) {
const handler = new BracketNotationHandler(query);
return handler.process();
}
}
exports.RequestParameterMutator = RequestParameterMutator;
/**
* Handles parsing of query parameters with bracket notation.
* - If a parameter in the OpenAPI spec has literal brackets in its name (e.g., 'filter[name]'),
* it will be treated as a literal parameter name.
* - Otherwise, it will be parsed as a nested object using qs.
*/
class BracketNotationHandler {
constructor(query) {
this.query = query;
}
/**
* Process the query parameters to handle bracket notation
*/
process() {
const literalBracketParams = this.getLiteralBracketParams();
const query = this.query;
Object.keys(query).forEach((key) => {
// Only process keys that contain brackets
if (key.includes('[')) {
// Only process keys that do not contain literal bracket notation
if (!literalBracketParams.has(key)) {
// Use qs.parse to handle it as a nested object
const normalizedKey = key.split('[')[0];
const parsed = (0, qs_1.parse)(`${key}=${query[key]}`);
// Use the parsed value for the normalized key
if (parsed[normalizedKey] !== undefined) {
// If we already have a value for this key, merge the objects
if (query[normalizedKey] &&
typeof query[normalizedKey] === 'object' &&
typeof parsed[normalizedKey] === 'object' &&
!Array.isArray(parsed[normalizedKey])) {
query[normalizedKey] = Object.assign(Object.assign({}, query[normalizedKey]), parsed[normalizedKey]);
}
else {
query[normalizedKey] = parsed[normalizedKey];
}
}
// Remove the original bracketed key from the query
delete query[key];
}
}
});
return query;
}
/**
* Gets a cache key for the current request's OpenAPI schema
* Combines path, method, and operation ID to create a key
*/
getCacheKey() {
var _a, _b, _c, _d;
const schema = (_a = this.query._openapi) === null || _a === void 0 ? void 0 : _a.schema;
if (!schema)
return null;
// Use all available identifiers to ensure uniqueness
const path = (_b = schema.path) !== null && _b !== void 0 ? _b : '';
const method = (_c = schema.method) !== null && _c !== void 0 ? _c : '';
const operationId = (_d = schema.operationId) !== null && _d !== void 0 ? _d : '';
// Combine all parts with a consistent separator
return `${path}|${method}|${operationId}`;
}
/**
* Gets the set of parameter names that should be treated as literal bracket notation
*/
getLiteralBracketParams() {
var _a, _b;
const cacheKey = this.getCacheKey();
if (cacheKey &&
BracketNotationHandler.literalBracketParamsCache.has(cacheKey)) {
return BracketNotationHandler.literalBracketParamsCache.get(cacheKey);
}
// Get the OpenAPI parameters for the current request
const openApiParams = (((_b = (_a = this.query._openapi) === null || _a === void 0 ? void 0 : _a.schema) === null || _b === void 0 ? void 0 : _b.parameters) ||
[]);
// Create a Set of parameter names that have literal brackets in the spec
const literalBracketParams = new Set(openApiParams
.filter((p) => p.in === 'query' && p.name.includes('['))
.map((p) => p.name));
// Cache the result for future requests to this endpoint
if (cacheKey) {
BracketNotationHandler.literalBracketParamsCache.set(cacheKey, literalBracketParams);
}
return literalBracketParams;
}
}
// Cache for literal bracket parameters per endpoint
BracketNotationHandler.literalBracketParamsCache = new Map();
//# sourceMappingURL=req.parameter.mutator.js.map