UNPKG

8.21 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2018,2019. All Rights Reserved.
2// Node module: @loopback/rest
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {
7 isReferenceObject,
8 ParameterObject,
9 ReferenceObject,
10 SchemaObject,
11} from '@loopback/openapi-v3';
12import debugModule from 'debug';
13import {
14 RestHttpErrors,
15 validateValueAgainstSchema,
16 ValueValidationOptions,
17} from '../';
18import {parseJson, sanitizeJsonParse} from '../parse-json';
19import {ValidationOptions} from '../types';
20import {DEFAULT_AJV_VALIDATION_OPTIONS} from '../validation/ajv-factory.provider';
21import {
22 DateCoercionOptions,
23 getOAIPrimitiveType,
24 IntegerCoercionOptions,
25 isEmpty,
26 isFalse,
27 isTrue,
28 isValidDateTime,
29 matchDateFormat,
30} from './utils';
31import {Validator} from './validator';
32const isRFC3339 = require('validator/lib/isRFC3339');
33const debug = debugModule('loopback:rest:coercion');
34
35/**
36 * Coerce the http raw data to a JavaScript type data of a parameter
37 * according to its OpenAPI schema specification.
38 *
39 * @param data - The raw data get from http request
40 * @param schema - The parameter's schema defined in OpenAPI specification
41 * @param options - The ajv validation options
42 */
43export async function coerceParameter(
44 data: string | undefined | object,
45 spec: ParameterObject,
46 options?: ValueValidationOptions,
47) {
48 const schema = extractSchemaFromSpec(spec);
49
50 if (!schema || isReferenceObject(schema)) {
51 debug(
52 'The parameter with schema %s is not coerced since schema' +
53 'dereference is not supported yet.',
54 schema,
55 );
56 return data;
57 }
58 const OAIType = getOAIPrimitiveType(schema.type, schema.format);
59 const validator = new Validator({parameterSpec: spec});
60
61 validator.validateParamBeforeCoercion(data);
62 if (data === undefined) return data;
63
64 // eslint-disable-next-line @typescript-eslint/no-explicit-any
65 let result: any = data;
66
67 switch (OAIType) {
68 case 'byte':
69 result = coerceBuffer(data, spec);
70 break;
71 case 'date':
72 result = coerceDatetime(data, spec, {dateOnly: true});
73 break;
74 case 'date-time':
75 result = coerceDatetime(data, spec);
76 break;
77 case 'float':
78 case 'double':
79 case 'number':
80 result = coerceNumber(data, spec);
81 break;
82 case 'long':
83 result = coerceInteger(data, spec, {isLong: true});
84 break;
85 case 'integer':
86 result = coerceInteger(data, spec);
87 break;
88 case 'boolean':
89 result = coerceBoolean(data, spec);
90 break;
91 case 'object':
92 result = await coerceObject(data, spec, options);
93 break;
94 case 'string':
95 case 'password':
96 result = coerceString(data, spec);
97 break;
98 case 'array':
99 result = coerceArray(data, spec);
100 break;
101 }
102
103 if (result != null) {
104 // For date/date-time/byte, we need to pass the raw string value to AJV
105 if (OAIType === 'date' || OAIType === 'date-time' || OAIType === 'byte') {
106 await validateParam(spec, data, options);
107 return result;
108 }
109
110 result = await validateParam(spec, result, options);
111 }
112 return result;
113}
114
115function coerceString(data: string | object, spec: ParameterObject) {
116 if (typeof data !== 'string')
117 throw RestHttpErrors.invalidData(data, spec.name);
118
119 debug('data of type string is coerced to %s', data);
120 return data;
121}
122
123function coerceBuffer(data: string | object, spec: ParameterObject) {
124 if (typeof data === 'object')
125 throw RestHttpErrors.invalidData(data, spec.name);
126 return Buffer.from(data, 'base64');
127}
128
129function coerceDatetime(
130 data: string | object,
131 spec: ParameterObject,
132 options?: DateCoercionOptions,
133) {
134 if (typeof data === 'object' || isEmpty(data))
135 throw RestHttpErrors.invalidData(data, spec.name);
136
137 if (options?.dateOnly) {
138 if (!matchDateFormat(data))
139 throw RestHttpErrors.invalidData(data, spec.name);
140 } else {
141 if (!isRFC3339(data)) throw RestHttpErrors.invalidData(data, spec.name);
142 }
143
144 const coercedDate = new Date(data);
145 if (!isValidDateTime(coercedDate))
146 throw RestHttpErrors.invalidData(data, spec.name);
147 return coercedDate;
148}
149
150function coerceNumber(data: string | object, spec: ParameterObject) {
151 if (typeof data === 'object' || isEmpty(data))
152 throw RestHttpErrors.invalidData(data, spec.name);
153
154 const coercedNum = Number(data);
155 if (isNaN(coercedNum)) throw RestHttpErrors.invalidData(data, spec.name);
156
157 debug('data of type number is coerced to %s', coercedNum);
158 return coercedNum;
159}
160
161function coerceInteger(
162 data: string | object,
163 spec: ParameterObject,
164 options?: IntegerCoercionOptions,
165) {
166 if (typeof data === 'object' || isEmpty(data))
167 throw RestHttpErrors.invalidData(data, spec.name);
168
169 const coercedInt = Number(data);
170 if (isNaN(coercedInt!)) throw RestHttpErrors.invalidData(data, spec.name);
171
172 if (options?.isLong) {
173 if (!Number.isInteger(coercedInt))
174 throw RestHttpErrors.invalidData(data, spec.name);
175 } else {
176 if (!Number.isSafeInteger(coercedInt))
177 throw RestHttpErrors.invalidData(data, spec.name);
178 }
179
180 debug('data of type integer is coerced to %s', coercedInt);
181 return coercedInt;
182}
183
184function coerceBoolean(data: string | object, spec: ParameterObject) {
185 if (typeof data === 'object' || isEmpty(data))
186 throw RestHttpErrors.invalidData(data, spec.name);
187 if (isTrue(data)) return true;
188 if (isFalse(data)) return false;
189 throw RestHttpErrors.invalidData(data, spec.name);
190}
191
192async function coerceObject(
193 input: string | object,
194 spec: ParameterObject,
195 options?: ValidationOptions,
196) {
197 const data = parseJsonIfNeeded(input, spec, options);
198
199 if (data == null) {
200 // Skip any further checks and coercions, nothing we can do with `undefined`
201 return data;
202 }
203
204 if (typeof data !== 'object' || Array.isArray(data))
205 throw RestHttpErrors.invalidData(input, spec.name);
206
207 return data;
208}
209
210function coerceArray(data: string | object, spec: ParameterObject) {
211 if (spec.in === 'query') {
212 if (data == null || Array.isArray(data)) return data;
213 return [data];
214 }
215
216 return data;
217}
218
219function validateParam(
220 spec: ParameterObject,
221 // eslint-disable-next-line @typescript-eslint/no-explicit-any
222 data: any,
223 options: ValidationOptions = DEFAULT_AJV_VALIDATION_OPTIONS,
224) {
225 const schema = extractSchemaFromSpec(spec);
226 if (schema) {
227 // Apply coercion based on properties defined by spec.schema
228 return validateValueAgainstSchema(
229 data,
230 schema,
231 {},
232 {...options, coerceTypes: true, source: 'parameter', name: spec.name},
233 );
234 }
235 return data;
236}
237
238/**
239 * Extract the schema from an OpenAPI parameter specification. If the root level
240 * one not found, search from media type 'application/json'.
241 *
242 * @param spec The parameter specification
243 */
244function extractSchemaFromSpec(
245 spec: ParameterObject,
246): SchemaObject | ReferenceObject | undefined {
247 let schema = spec.schema;
248
249 // If a query parameter is a url encoded Json object,
250 // the schema is defined under content['application/json']
251 if (!schema && spec.in === 'query') {
252 schema = spec.content?.['application/json']?.schema;
253 }
254
255 return schema;
256}
257
258function parseJsonIfNeeded(
259 data: string | object,
260 spec: ParameterObject,
261 options?: ValidationOptions,
262): string | object | undefined {
263 if (typeof data !== 'string') return data;
264
265 if (spec.in !== 'query' || (spec.in === 'query' && !spec.content)) {
266 debug(
267 'Skipping JSON.parse, argument %s is not a url encoded json object query parameter (since content field is missing in parameter schema)',
268 spec.name,
269 );
270 return data;
271 }
272
273 if (data === '') {
274 debug('Converted empty string to object value `undefined`');
275 return undefined;
276 }
277
278 try {
279 const result = parseJson(
280 data,
281 sanitizeJsonParse(undefined, options?.prohibitedKeys),
282 );
283 debug('Parsed parameter %s as %j', spec.name, result);
284 return result;
285 } catch (err) {
286 debug('Cannot parse %s value %j as JSON: %s', spec.name, data, err.message);
287 throw RestHttpErrors.invalidData(data, spec.name, {
288 details: {
289 syntaxError: err.message,
290 },
291 });
292 }
293}