@loopback/rest
Version:
Expose controllers as REST endpoints and route REST API requests to controller methods
217 lines • 8.5 kB
JavaScript
// Copyright IBM Corp. and LoopBack contributors 2018,2019. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
Object.defineProperty(exports, "__esModule", { value: true });
exports.coerceParameter = void 0;
const tslib_1 = require("tslib");
const openapi_v3_1 = require("@loopback/openapi-v3");
const debug_1 = tslib_1.__importDefault(require("debug"));
const __1 = require("../");
const parse_json_1 = require("../parse-json");
const ajv_factory_provider_1 = require("../validation/ajv-factory.provider");
const utils_1 = require("./utils");
const validator_1 = require("./validator");
const isRFC3339 = require('validator/lib/isRFC3339');
const debug = (0, debug_1.default)('loopback:rest:coercion');
/**
* Coerce the http raw data to a JavaScript type data of a parameter
* according to its OpenAPI schema specification.
*
* @param data - The raw data get from http request
* @param schema - The parameter's schema defined in OpenAPI specification
* @param options - The ajv validation options
*/
async function coerceParameter(data, spec, options) {
const schema = extractSchemaFromSpec(spec);
if (!schema || (0, openapi_v3_1.isReferenceObject)(schema)) {
debug('The parameter with schema %s is not coerced since schema' +
'dereference is not supported yet.', schema);
return data;
}
const OAIType = (0, utils_1.getOAIPrimitiveType)(schema.type, schema.format);
const validator = new validator_1.Validator({ parameterSpec: spec });
validator.validateParamBeforeCoercion(data);
if (data === undefined)
return data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let result = data;
switch (OAIType) {
case 'byte':
result = coerceBuffer(data, spec);
break;
case 'date':
result = coerceDatetime(data, spec, { dateOnly: true });
break;
case 'date-time':
result = coerceDatetime(data, spec);
break;
case 'float':
case 'double':
case 'number':
result = coerceNumber(data, spec);
break;
case 'long':
result = coerceInteger(data, spec, { isLong: true });
break;
case 'integer':
result = coerceInteger(data, spec);
break;
case 'boolean':
result = coerceBoolean(data, spec);
break;
case 'object':
result = await coerceObject(data, spec, options);
break;
case 'string':
case 'password':
result = coerceString(data, spec);
break;
case 'array':
result = coerceArray(data, spec);
break;
}
if (result != null) {
// For date/date-time/byte, we need to pass the raw string value to AJV
if (OAIType === 'date' || OAIType === 'date-time' || OAIType === 'byte') {
await validateParam(spec, data, options);
return result;
}
result = await validateParam(spec, result, options);
}
return result;
}
exports.coerceParameter = coerceParameter;
function coerceString(data, spec) {
if (typeof data !== 'string')
throw __1.RestHttpErrors.invalidData(data, spec.name);
debug('data of type string is coerced to %s', data);
return data;
}
function coerceBuffer(data, spec) {
if (typeof data === 'object')
throw __1.RestHttpErrors.invalidData(data, spec.name);
return Buffer.from(data, 'base64');
}
function coerceDatetime(data, spec, options) {
if (typeof data === 'object' || (0, utils_1.isEmpty)(data))
throw __1.RestHttpErrors.invalidData(data, spec.name);
if (options === null || options === void 0 ? void 0 : options.dateOnly) {
if (!(0, utils_1.matchDateFormat)(data))
throw __1.RestHttpErrors.invalidData(data, spec.name);
}
else {
if (!isRFC3339(data))
throw __1.RestHttpErrors.invalidData(data, spec.name);
}
const coercedDate = new Date(data);
if (!(0, utils_1.isValidDateTime)(coercedDate))
throw __1.RestHttpErrors.invalidData(data, spec.name);
return coercedDate;
}
function coerceNumber(data, spec) {
if (typeof data === 'object' || (0, utils_1.isEmpty)(data))
throw __1.RestHttpErrors.invalidData(data, spec.name);
const coercedNum = Number(data);
if (isNaN(coercedNum))
throw __1.RestHttpErrors.invalidData(data, spec.name);
debug('data of type number is coerced to %s', coercedNum);
return coercedNum;
}
function coerceInteger(data, spec, options) {
if (typeof data === 'object' || (0, utils_1.isEmpty)(data))
throw __1.RestHttpErrors.invalidData(data, spec.name);
const coercedInt = Number(data);
if (isNaN(coercedInt))
throw __1.RestHttpErrors.invalidData(data, spec.name);
if (options === null || options === void 0 ? void 0 : options.isLong) {
if (!Number.isInteger(coercedInt))
throw __1.RestHttpErrors.invalidData(data, spec.name);
}
else {
if (!Number.isSafeInteger(coercedInt))
throw __1.RestHttpErrors.invalidData(data, spec.name);
}
debug('data of type integer is coerced to %s', coercedInt);
return coercedInt;
}
function coerceBoolean(data, spec) {
if (typeof data === 'object' || (0, utils_1.isEmpty)(data))
throw __1.RestHttpErrors.invalidData(data, spec.name);
if ((0, utils_1.isTrue)(data))
return true;
if ((0, utils_1.isFalse)(data))
return false;
throw __1.RestHttpErrors.invalidData(data, spec.name);
}
async function coerceObject(input, spec, options) {
const data = parseJsonIfNeeded(input, spec, options);
if (data == null) {
// Skip any further checks and coercions, nothing we can do with `undefined`
return data;
}
if (typeof data !== 'object' || Array.isArray(data))
throw __1.RestHttpErrors.invalidData(input, spec.name);
return data;
}
function coerceArray(data, spec) {
if (spec.in === 'query') {
if (data == null || Array.isArray(data))
return data;
return [data];
}
return data;
}
function validateParam(spec,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data, options = ajv_factory_provider_1.DEFAULT_AJV_VALIDATION_OPTIONS) {
const schema = extractSchemaFromSpec(spec);
if (schema) {
// Apply coercion based on properties defined by spec.schema
return (0, __1.validateValueAgainstSchema)(data, schema, {}, { ...options, coerceTypes: true, source: 'parameter', name: spec.name });
}
return data;
}
/**
* Extract the schema from an OpenAPI parameter specification. If the root level
* one not found, search from media type 'application/json'.
*
* @param spec The parameter specification
*/
function extractSchemaFromSpec(spec) {
var _a, _b;
let schema = spec.schema;
// If a query parameter is a url encoded Json object,
// the schema is defined under content['application/json']
if (!schema && spec.in === 'query') {
schema = (_b = (_a = spec.content) === null || _a === void 0 ? void 0 : _a['application/json']) === null || _b === void 0 ? void 0 : _b.schema;
}
return schema;
}
function parseJsonIfNeeded(data, spec, options) {
if (typeof data !== 'string')
return data;
if (spec.in !== 'query' || (spec.in === 'query' && !spec.content)) {
debug('Skipping JSON.parse, argument %s is not a url encoded json object query parameter (since content field is missing in parameter schema)', spec.name);
return data;
}
if (data === '') {
debug('Converted empty string to object value `undefined`');
return undefined;
}
try {
const result = (0, parse_json_1.parseJson)(data, (0, parse_json_1.sanitizeJsonParse)(undefined, options === null || options === void 0 ? void 0 : options.prohibitedKeys));
debug('Parsed parameter %s as %j', spec.name, result);
return result;
}
catch (err) {
debug('Cannot parse %s value %j as JSON: %s', spec.name, data, err.message);
throw __1.RestHttpErrors.invalidData(data, spec.name, {
details: {
syntaxError: err.message,
},
});
}
}
//# sourceMappingURL=coerce-parameter.js.map
;