1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import {
|
7 | isReferenceObject,
|
8 | ParameterObject,
|
9 | ReferenceObject,
|
10 | SchemaObject,
|
11 | } from '@loopback/openapi-v3';
|
12 | import debugModule from 'debug';
|
13 | import {
|
14 | RestHttpErrors,
|
15 | validateValueAgainstSchema,
|
16 | ValueValidationOptions,
|
17 | } from '../';
|
18 | import {parseJson, sanitizeJsonParse} from '../parse-json';
|
19 | import {ValidationOptions} from '../types';
|
20 | import {DEFAULT_AJV_VALIDATION_OPTIONS} from '../validation/ajv-factory.provider';
|
21 | import {
|
22 | DateCoercionOptions,
|
23 | getOAIPrimitiveType,
|
24 | IntegerCoercionOptions,
|
25 | isEmpty,
|
26 | isFalse,
|
27 | isTrue,
|
28 | isValidDateTime,
|
29 | matchDateFormat,
|
30 | } from './utils';
|
31 | import {Validator} from './validator';
|
32 | const isRFC3339 = require('validator/lib/isRFC3339');
|
33 | const debug = debugModule('loopback:rest:coercion');
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | export 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 |
|
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 |
|
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 |
|
115 | function 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 |
|
123 | function 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 |
|
129 | function 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 |
|
150 | function 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 |
|
161 | function 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 |
|
184 | function 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 |
|
192 | async 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 |
|
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 |
|
210 | function 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 |
|
219 | function validateParam(
|
220 | spec: ParameterObject,
|
221 |
|
222 | data: any,
|
223 | options: ValidationOptions = DEFAULT_AJV_VALIDATION_OPTIONS,
|
224 | ) {
|
225 | const schema = extractSchemaFromSpec(spec);
|
226 | if (schema) {
|
227 |
|
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 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 | function extractSchemaFromSpec(
|
245 | spec: ParameterObject,
|
246 | ): SchemaObject | ReferenceObject | undefined {
|
247 | let schema = spec.schema;
|
248 |
|
249 |
|
250 |
|
251 | if (!schema && spec.in === 'query') {
|
252 | schema = spec.content?.['application/json']?.schema;
|
253 | }
|
254 |
|
255 | return schema;
|
256 | }
|
257 |
|
258 | function 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 | }
|