@opra/core 1.0.0-alpha.3 → 1.0.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/augmentation/18n.augmentation.js +1 -1
- package/cjs/constants.js +1 -2
- package/cjs/execution-context.js +1 -1
- package/cjs/http/express-adapter.js +25 -34
- package/cjs/http/http-adapter.js +2 -5
- package/cjs/http/http-context.js +20 -32
- package/cjs/http/{impl/http-handler.js → http-handler.js} +249 -213
- package/cjs/http/impl/http-incoming.host.js +3 -3
- package/cjs/http/impl/http-outgoing.host.js +2 -2
- package/cjs/http/impl/multipart-reader.js +141 -50
- package/cjs/http/impl/node-incoming-message.host.js +5 -3
- package/cjs/http/interfaces/node-incoming-message.interface.js +3 -2
- package/cjs/http/utils/body-reader.js +6 -5
- package/cjs/http/utils/common.js +6 -5
- package/cjs/http/utils/concat-readable.js +1 -2
- package/cjs/http/utils/convert-to-headers.js +2 -3
- package/cjs/http/utils/convert-to-raw-headers.js +1 -2
- package/cjs/http/utils/match-known-fields.js +2 -2
- package/cjs/http/utils/wrap-exception.js +1 -2
- package/cjs/index.js +4 -4
- package/cjs/platform-adapter.js +1 -4
- package/cjs/type-guards.js +4 -5
- package/esm/augmentation/18n.augmentation.js +1 -1
- package/esm/constants.js +0 -1
- package/esm/execution-context.js +1 -1
- package/esm/http/express-adapter.js +25 -34
- package/esm/http/http-adapter.js +2 -5
- package/esm/http/http-context.js +21 -33
- package/esm/http/{impl/http-handler.js → http-handler.js} +243 -207
- package/esm/http/impl/http-incoming.host.js +3 -3
- package/esm/http/impl/http-outgoing.host.js +2 -2
- package/esm/http/impl/multipart-reader.js +142 -51
- package/esm/http/impl/node-incoming-message.host.js +5 -3
- package/esm/http/interfaces/node-incoming-message.interface.js +3 -2
- package/esm/http/utils/body-reader.js +6 -5
- package/esm/http/utils/common.js +2 -1
- package/esm/index.js +4 -4
- package/esm/platform-adapter.js +1 -4
- package/package.json +21 -14
- package/types/augmentation/18n.augmentation.d.ts +1 -1
- package/types/constants.d.ts +0 -1
- package/types/execution-context.d.ts +2 -3
- package/types/http/express-adapter.d.ts +1 -1
- package/types/http/http-adapter.d.ts +35 -8
- package/types/http/http-context.d.ts +4 -4
- package/types/http/{impl/http-handler.d.ts → http-handler.d.ts} +11 -9
- package/types/http/impl/http-incoming.host.d.ts +1 -2
- package/types/http/impl/http-outgoing.host.d.ts +1 -1
- package/types/http/impl/multipart-reader.d.ts +38 -20
- package/types/http/impl/node-incoming-message.host.d.ts +2 -6
- package/types/http/impl/node-outgoing-message.host.d.ts +4 -7
- package/types/http/interfaces/http-incoming.interface.d.ts +1 -2
- package/types/http/interfaces/http-outgoing.interface.d.ts +1 -1
- package/types/http/interfaces/node-incoming-message.interface.d.ts +0 -2
- package/types/http/interfaces/node-outgoing-message.interface.d.ts +0 -2
- package/types/http/utils/body-reader.d.ts +2 -5
- package/types/http/utils/concat-readable.d.ts +0 -1
- package/types/http/utils/convert-to-raw-headers.d.ts +0 -1
- package/types/index.d.ts +4 -4
- package/types/platform-adapter.d.ts +1 -5
- package/cjs/helpers/logger.js +0 -35
- package/esm/helpers/logger.js +0 -31
- package/types/helpers/logger.d.ts +0 -14
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import * as process from 'node:process';
|
|
2
|
+
import typeIs from '@browsery/type-is';
|
|
3
|
+
import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema, } from '@opra/common';
|
|
1
4
|
import { parse as parseContentType } from 'content-type';
|
|
2
5
|
import { splitString } from 'fast-tokenizer';
|
|
3
|
-
import * as process from 'node:process';
|
|
4
6
|
import { asMutable } from 'ts-gems';
|
|
5
7
|
import { toArray, ValidationError, vg } from 'valgen';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import { kAssetCache } from '../../constants.js';
|
|
9
|
-
import { wrapException } from '../utils/wrap-exception.js';
|
|
8
|
+
import { kAssetCache } from '../constants';
|
|
9
|
+
import { wrapException } from './utils/wrap-exception';
|
|
10
10
|
/**
|
|
11
11
|
* @class HttpHandler
|
|
12
12
|
*/
|
|
@@ -38,7 +38,7 @@ export class HttpHandler {
|
|
|
38
38
|
throw e;
|
|
39
39
|
if (e instanceof ValidationError) {
|
|
40
40
|
throw new BadRequestError({
|
|
41
|
-
message:
|
|
41
|
+
message: 'Response validation failed',
|
|
42
42
|
code: 'RESPONSE_VALIDATION',
|
|
43
43
|
details: e.issues,
|
|
44
44
|
}, e);
|
|
@@ -48,11 +48,14 @@ export class HttpHandler {
|
|
|
48
48
|
await this.adapter.emitAsync('request', context);
|
|
49
49
|
// Call interceptors than execute request
|
|
50
50
|
if (this.adapter.interceptors) {
|
|
51
|
+
const interceptors = this.adapter.interceptors;
|
|
51
52
|
let i = 0;
|
|
52
53
|
const next = async () => {
|
|
53
|
-
const interceptor =
|
|
54
|
-
if (interceptor)
|
|
54
|
+
const interceptor = interceptors[i++];
|
|
55
|
+
if (typeof interceptor === 'function')
|
|
55
56
|
await interceptor(context, next);
|
|
57
|
+
else if (typeof interceptor?.intercept === 'function')
|
|
58
|
+
await interceptor.intercept(context, next);
|
|
56
59
|
await this._executeRequest(context);
|
|
57
60
|
};
|
|
58
61
|
await next();
|
|
@@ -60,23 +63,21 @@ export class HttpHandler {
|
|
|
60
63
|
else
|
|
61
64
|
await this._executeRequest(context);
|
|
62
65
|
}
|
|
63
|
-
catch (
|
|
66
|
+
catch (error) {
|
|
67
|
+
let e = error;
|
|
64
68
|
if (e instanceof ValidationError) {
|
|
65
69
|
e = new InternalServerError({
|
|
66
|
-
message:
|
|
70
|
+
message: 'Response validation failed',
|
|
67
71
|
code: 'RESPONSE_VALIDATION',
|
|
68
72
|
details: e.issues,
|
|
69
73
|
}, e);
|
|
70
74
|
}
|
|
71
75
|
else
|
|
72
76
|
e = wrapException(e);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
response.end();
|
|
78
|
-
});
|
|
79
|
-
// if (!outgoing.writableEnded) await this._sendErrorResponse(context.response, [error]);
|
|
77
|
+
if (this.onError)
|
|
78
|
+
await this.onError(context, error);
|
|
79
|
+
context.errors.push(e);
|
|
80
|
+
await this.sendResponse(context);
|
|
80
81
|
}
|
|
81
82
|
finally {
|
|
82
83
|
await context.emitAsync('finish');
|
|
@@ -89,7 +90,7 @@ export class HttpHandler {
|
|
|
89
90
|
async parseRequest(context) {
|
|
90
91
|
await this._parseParameters(context);
|
|
91
92
|
await this._parseContentType(context);
|
|
92
|
-
if (context.operation
|
|
93
|
+
if (context.operation?.requestBody?.immediateFetch)
|
|
93
94
|
await context.getBody();
|
|
94
95
|
/** Set default status code as the first status code between 200 and 299 */
|
|
95
96
|
if (context.operation) {
|
|
@@ -109,17 +110,19 @@ export class HttpHandler {
|
|
|
109
110
|
*/
|
|
110
111
|
async _parseParameters(context) {
|
|
111
112
|
const { operation, request } = context;
|
|
112
|
-
|
|
113
|
+
if (!operation)
|
|
114
|
+
return;
|
|
115
|
+
let key = '';
|
|
113
116
|
try {
|
|
114
117
|
const onFail = (issue) => {
|
|
115
|
-
issue.location =
|
|
118
|
+
issue.location = key;
|
|
116
119
|
return issue;
|
|
117
120
|
};
|
|
118
121
|
/** prepare decoders */
|
|
119
122
|
const getDecoder = (prm) => {
|
|
120
123
|
let decode = this[kAssetCache].get(prm, 'decode');
|
|
121
124
|
if (!decode) {
|
|
122
|
-
decode = prm.type?.generateCodec('decode') || vg.isAny();
|
|
125
|
+
decode = prm.type?.generateCodec('decode', { ignoreReadonlyFields: true }) || vg.isAny();
|
|
123
126
|
this[kAssetCache].set(prm, 'decode', decode);
|
|
124
127
|
}
|
|
125
128
|
return decode;
|
|
@@ -127,9 +130,9 @@ export class HttpHandler {
|
|
|
127
130
|
const paramsLeft = new Set([...operation.parameters, ...operation.owner.parameters]);
|
|
128
131
|
/** parse cookie parameters */
|
|
129
132
|
if (request.cookies) {
|
|
130
|
-
for (
|
|
131
|
-
const oprPrm = operation.findParameter(
|
|
132
|
-
const cntPrm = operation.owner.findParameter(
|
|
133
|
+
for (key of Object.keys(request.cookies)) {
|
|
134
|
+
const oprPrm = operation.findParameter(key, 'cookie');
|
|
135
|
+
const cntPrm = operation.owner.findParameter(key, 'cookie');
|
|
133
136
|
const prm = oprPrm || cntPrm;
|
|
134
137
|
if (!prm)
|
|
135
138
|
continue;
|
|
@@ -138,16 +141,17 @@ export class HttpHandler {
|
|
|
138
141
|
if (cntPrm)
|
|
139
142
|
paramsLeft.delete(cntPrm);
|
|
140
143
|
const decode = getDecoder(prm);
|
|
141
|
-
const v = decode(request.cookies[
|
|
144
|
+
const v = decode(request.cookies[key], { coerce: true, label: key, onFail });
|
|
145
|
+
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
142
146
|
if (v !== undefined)
|
|
143
147
|
context.cookies[prmName] = v;
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
/** parse headers */
|
|
147
151
|
if (request.headers) {
|
|
148
|
-
for (
|
|
149
|
-
const oprPrm = operation.findParameter(
|
|
150
|
-
const cntPrm = operation.owner.findParameter(
|
|
152
|
+
for (key of Object.keys(request.headers)) {
|
|
153
|
+
const oprPrm = operation.findParameter(key, 'header');
|
|
154
|
+
const cntPrm = operation.owner.findParameter(key, 'header');
|
|
151
155
|
const prm = oprPrm || cntPrm;
|
|
152
156
|
if (!prm)
|
|
153
157
|
continue;
|
|
@@ -156,16 +160,17 @@ export class HttpHandler {
|
|
|
156
160
|
if (cntPrm)
|
|
157
161
|
paramsLeft.delete(cntPrm);
|
|
158
162
|
const decode = getDecoder(prm);
|
|
159
|
-
const v = decode(request.headers[
|
|
163
|
+
const v = decode(request.headers[key], { coerce: true, label: key, onFail });
|
|
164
|
+
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
160
165
|
if (v !== undefined)
|
|
161
166
|
context.headers[prmName] = v;
|
|
162
167
|
}
|
|
163
168
|
}
|
|
164
169
|
/** parse path parameters */
|
|
165
170
|
if (request.params) {
|
|
166
|
-
for (
|
|
167
|
-
const oprPrm = operation.findParameter(
|
|
168
|
-
const cntPrm = operation.owner.findParameter(
|
|
171
|
+
for (key of Object.keys(request.params)) {
|
|
172
|
+
const oprPrm = operation.findParameter(key, 'path');
|
|
173
|
+
const cntPrm = operation.owner.findParameter(key, 'path');
|
|
169
174
|
const prm = oprPrm || cntPrm;
|
|
170
175
|
if (!prm)
|
|
171
176
|
continue;
|
|
@@ -174,17 +179,17 @@ export class HttpHandler {
|
|
|
174
179
|
if (cntPrm)
|
|
175
180
|
paramsLeft.delete(cntPrm);
|
|
176
181
|
const decode = getDecoder(prm);
|
|
177
|
-
const v = decode(request.params[
|
|
182
|
+
const v = decode(request.params[key], { coerce: true, label: key, onFail });
|
|
178
183
|
if (v !== undefined)
|
|
179
|
-
context.pathParams[
|
|
184
|
+
context.pathParams[key] = v;
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
/** parse query parameters */
|
|
183
188
|
const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
|
|
184
189
|
const { searchParams } = url;
|
|
185
|
-
for (
|
|
186
|
-
const oprPrm = operation.findParameter(
|
|
187
|
-
const cntPrm = operation.owner.findParameter(
|
|
190
|
+
for (key of searchParams.keys()) {
|
|
191
|
+
const oprPrm = operation.findParameter(key, 'query');
|
|
192
|
+
const cntPrm = operation.owner.findParameter(key, 'query');
|
|
188
193
|
const prm = oprPrm || cntPrm;
|
|
189
194
|
if (!prm)
|
|
190
195
|
continue;
|
|
@@ -193,20 +198,22 @@ export class HttpHandler {
|
|
|
193
198
|
if (cntPrm)
|
|
194
199
|
paramsLeft.delete(cntPrm);
|
|
195
200
|
const decode = getDecoder(prm);
|
|
196
|
-
let values = searchParams?.getAll(
|
|
201
|
+
let values = searchParams?.getAll(key);
|
|
202
|
+
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
197
203
|
if (values?.length && prm.isArray) {
|
|
198
204
|
values = values.map(v => splitString(v, { delimiters: prm.arraySeparator, quotes: true })).flat();
|
|
199
|
-
values = values.map(v => decode(v, { coerce: true, label:
|
|
205
|
+
values = values.map(v => decode(v, { coerce: true, label: key, onFail }));
|
|
200
206
|
if (values.length)
|
|
201
207
|
context.queryParams[prmName] = values;
|
|
202
208
|
}
|
|
203
209
|
else {
|
|
204
|
-
const v = decode(values[0], { coerce: true, label:
|
|
210
|
+
const v = decode(values[0], { coerce: true, label: key, onFail });
|
|
205
211
|
if (values.length)
|
|
206
212
|
context.queryParams[prmName] = v;
|
|
207
213
|
}
|
|
208
214
|
}
|
|
209
215
|
for (const prm of paramsLeft) {
|
|
216
|
+
key = String(prm.name);
|
|
210
217
|
// Throw error for required parameters
|
|
211
218
|
if (prm.required) {
|
|
212
219
|
const decode = getDecoder(prm);
|
|
@@ -216,8 +223,8 @@ export class HttpHandler {
|
|
|
216
223
|
}
|
|
217
224
|
catch (e) {
|
|
218
225
|
if (e instanceof ValidationError) {
|
|
219
|
-
|
|
220
|
-
message: `Invalid parameter (${
|
|
226
|
+
throw new BadRequestError({
|
|
227
|
+
message: `Invalid parameter (${key}) value. ` + e.message,
|
|
221
228
|
code: 'REQUEST_VALIDATION',
|
|
222
229
|
details: e.issues,
|
|
223
230
|
}, e);
|
|
@@ -232,14 +239,15 @@ export class HttpHandler {
|
|
|
232
239
|
*/
|
|
233
240
|
async _parseContentType(context) {
|
|
234
241
|
const { request, operation } = context;
|
|
242
|
+
if (!operation)
|
|
243
|
+
return;
|
|
235
244
|
if (operation.requestBody?.content.length) {
|
|
236
245
|
let mediaType;
|
|
237
246
|
let contentType = request.header('content-type');
|
|
238
247
|
if (contentType) {
|
|
239
248
|
contentType = parseContentType(contentType).type;
|
|
240
|
-
mediaType = operation.requestBody.content.find(mc =>
|
|
241
|
-
|
|
242
|
-
});
|
|
249
|
+
mediaType = operation.requestBody.content.find(mc => mc.contentType &&
|
|
250
|
+
typeIs.is(contentType, Array.isArray(mc.contentType) ? mc.contentType : [mc.contentType]));
|
|
243
251
|
}
|
|
244
252
|
if (!mediaType) {
|
|
245
253
|
const contentTypes = operation.requestBody.content.map(mc => mc.contentType).flat();
|
|
@@ -258,11 +266,12 @@ export class HttpHandler {
|
|
|
258
266
|
throw new MethodNotAllowedError();
|
|
259
267
|
const responseValue = await context.operationHandler.call(context.controllerInstance, context);
|
|
260
268
|
const { response } = context;
|
|
261
|
-
if (!response.writableEnded)
|
|
262
|
-
await this.
|
|
269
|
+
if (!response.writableEnded) {
|
|
270
|
+
await this.sendResponse(context, responseValue).finally(() => {
|
|
263
271
|
if (!response.writableEnded)
|
|
264
272
|
response.end();
|
|
265
273
|
});
|
|
274
|
+
}
|
|
266
275
|
}
|
|
267
276
|
/**
|
|
268
277
|
*
|
|
@@ -270,93 +279,180 @@ export class HttpHandler {
|
|
|
270
279
|
* @param responseValue
|
|
271
280
|
* @protected
|
|
272
281
|
*/
|
|
273
|
-
async
|
|
282
|
+
async sendResponse(context, responseValue) {
|
|
283
|
+
if (context.errors.length)
|
|
284
|
+
return this._sendErrorResponse(context);
|
|
274
285
|
const { response } = context;
|
|
275
286
|
const { document } = this.adapter;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
287
|
+
try {
|
|
288
|
+
const responseArgs = this._determineResponseArgs(context, responseValue);
|
|
289
|
+
const { operationResponse, statusCode } = responseArgs;
|
|
290
|
+
let { contentType, body } = responseArgs;
|
|
291
|
+
const operationResultType = document.node.getDataType(OperationResult);
|
|
292
|
+
let operationResultEncoder = this[kAssetCache].get(operationResultType, 'encode');
|
|
293
|
+
if (!operationResultEncoder) {
|
|
294
|
+
operationResultEncoder = operationResultType.generateCodec('encode', {
|
|
295
|
+
ignoreWriteonlyFields: true,
|
|
296
|
+
ignoreHiddenFields: true,
|
|
297
|
+
});
|
|
298
|
+
this[kAssetCache].set(operationResultType, 'encode', operationResultEncoder);
|
|
299
|
+
}
|
|
300
|
+
/** Validate response */
|
|
301
|
+
if (operationResponse?.type) {
|
|
302
|
+
if (!(body == null && statusCode === HttpStatusCode.NO_CONTENT)) {
|
|
303
|
+
/** Generate encoder */
|
|
304
|
+
let encode = this[kAssetCache].get(operationResponse, 'encode');
|
|
305
|
+
if (!encode) {
|
|
306
|
+
encode = operationResponse.type.generateCodec('encode', {
|
|
307
|
+
partial: operationResponse.partial,
|
|
308
|
+
projection: responseArgs.projection || '*',
|
|
309
|
+
ignoreWriteonlyFields: true,
|
|
310
|
+
ignoreHiddenFields: true,
|
|
311
|
+
onFail: issue => `Response body validation failed: ` + issue.message,
|
|
312
|
+
});
|
|
313
|
+
if (operationResponse) {
|
|
314
|
+
if (operationResponse.isArray)
|
|
315
|
+
encode = vg.isArray(encode);
|
|
316
|
+
this[kAssetCache].set(operationResponse, 'encode', encode);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Encode body */
|
|
320
|
+
if (operationResponse.type.extendsFrom(operationResultType)) {
|
|
321
|
+
if (body instanceof OperationResult)
|
|
322
|
+
body = encode(body);
|
|
323
|
+
else {
|
|
324
|
+
body.payload = encode(body.payload);
|
|
325
|
+
body = operationResultEncoder(body);
|
|
326
|
+
}
|
|
299
327
|
}
|
|
300
|
-
}
|
|
301
|
-
/** Encode body */
|
|
302
|
-
if (operationResponse.type.extendsFrom(operationResultType)) {
|
|
303
|
-
if (body instanceof OperationResult)
|
|
304
|
-
body = encode(body);
|
|
305
328
|
else {
|
|
306
|
-
body
|
|
307
|
-
|
|
329
|
+
if (body instanceof OperationResult &&
|
|
330
|
+
contentType &&
|
|
331
|
+
typeIs.is(contentType, [MimeTypes.opra_response_json])) {
|
|
332
|
+
body.payload = encode(body.payload);
|
|
333
|
+
body = operationResultEncoder(body);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
body = encode(body);
|
|
337
|
+
}
|
|
308
338
|
}
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
339
|
if (body instanceof OperationResult &&
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
body.
|
|
315
|
-
body = operationResultEncoder(body);
|
|
340
|
+
operationResponse.type &&
|
|
341
|
+
operationResponse.type !== document.node.getDataType(OperationResult)) {
|
|
342
|
+
body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
|
|
316
343
|
}
|
|
317
|
-
else
|
|
318
|
-
body = encode(body);
|
|
319
|
-
}
|
|
320
|
-
if (body instanceof OperationResult && operationResponse.type) {
|
|
321
|
-
body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
|
|
322
344
|
}
|
|
323
345
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
346
|
+
else if (body != null) {
|
|
347
|
+
if (body instanceof OperationResult) {
|
|
348
|
+
body = operationResultEncoder(body);
|
|
349
|
+
contentType = MimeTypes.opra_response_json;
|
|
350
|
+
}
|
|
351
|
+
else if (Buffer.isBuffer(body))
|
|
352
|
+
contentType = MimeTypes.binary;
|
|
353
|
+
else if (typeof body === 'object') {
|
|
354
|
+
contentType = contentType || MimeTypes.json;
|
|
355
|
+
if (typeof body.toJSON === 'function')
|
|
356
|
+
body = body.toJSON();
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
contentType = contentType || MimeTypes.text;
|
|
360
|
+
body = String(body);
|
|
361
|
+
}
|
|
336
362
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
363
|
+
/** Set content-type header value if not set */
|
|
364
|
+
if (contentType && contentType !== responseArgs.contentType)
|
|
365
|
+
response.setHeader('content-type', contentType);
|
|
366
|
+
response.status(statusCode);
|
|
367
|
+
if (body == null) {
|
|
368
|
+
response.end();
|
|
369
|
+
return;
|
|
340
370
|
}
|
|
371
|
+
let x;
|
|
372
|
+
if (Buffer.isBuffer(body) || isReadableStream(body))
|
|
373
|
+
x = body;
|
|
374
|
+
else if (isBlob(body))
|
|
375
|
+
x = body.stream();
|
|
376
|
+
else if (typeof body === 'object')
|
|
377
|
+
x = JSON.stringify(body);
|
|
378
|
+
else
|
|
379
|
+
x = String(body);
|
|
380
|
+
response.end(x);
|
|
341
381
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
382
|
+
catch (error) {
|
|
383
|
+
context.errors.push(error);
|
|
384
|
+
return this._sendErrorResponse(context);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async _sendErrorResponse(context) {
|
|
388
|
+
context.errors = this._wrapExceptions(context.errors);
|
|
389
|
+
try {
|
|
390
|
+
await this.adapter.emitAsync('error', context);
|
|
391
|
+
context.errors = this._wrapExceptions(context.errors);
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
context.errors = this._wrapExceptions([e, ...context.errors]);
|
|
395
|
+
}
|
|
396
|
+
const { response, errors } = context;
|
|
397
|
+
if (response.headersSent) {
|
|
347
398
|
response.end();
|
|
348
399
|
return;
|
|
349
400
|
}
|
|
350
|
-
let
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
401
|
+
let status = response.statusCode || 0;
|
|
402
|
+
if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
|
|
403
|
+
status = errors[0].status;
|
|
404
|
+
if (status < Number(HttpStatusCode.BAD_REQUEST))
|
|
405
|
+
status = HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
406
|
+
}
|
|
407
|
+
response.statusCode = status;
|
|
408
|
+
const { document } = this.adapter;
|
|
409
|
+
const dt = document.node.getComplexType('OperationResult');
|
|
410
|
+
let encode = this[kAssetCache].get(dt, 'encode');
|
|
411
|
+
if (!encode) {
|
|
412
|
+
encode = dt.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
413
|
+
this[kAssetCache].set(dt, 'encode', encode);
|
|
414
|
+
}
|
|
415
|
+
const { i18n } = this.adapter;
|
|
416
|
+
const bodyObject = new OperationResult({
|
|
417
|
+
errors: errors.map(x => {
|
|
418
|
+
const o = x.toJSON();
|
|
419
|
+
if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
|
|
420
|
+
delete o.stack;
|
|
421
|
+
return i18n.deep(o);
|
|
422
|
+
}),
|
|
423
|
+
});
|
|
424
|
+
const body = encode(bodyObject);
|
|
425
|
+
response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
|
|
426
|
+
response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
427
|
+
response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
|
|
428
|
+
response.setHeader(HttpHeaderCodes.Expires, '-1');
|
|
429
|
+
response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
|
|
430
|
+
response.send(JSON.stringify(body));
|
|
431
|
+
response.end();
|
|
432
|
+
}
|
|
433
|
+
async sendDocumentSchema(context) {
|
|
434
|
+
const { request, response } = context;
|
|
435
|
+
const { document } = this.adapter;
|
|
436
|
+
response.setHeader('content-type', MimeTypes.json);
|
|
437
|
+
const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
|
|
438
|
+
const { searchParams } = url;
|
|
439
|
+
const documentId = searchParams.get('id');
|
|
440
|
+
const doc = documentId ? document.findDocument(documentId) : document;
|
|
441
|
+
if (!doc) {
|
|
442
|
+
context.errors.push(new BadRequestError({
|
|
443
|
+
message: `Document with given id [${documentId}] does not exists`,
|
|
444
|
+
}));
|
|
445
|
+
return this.sendResponse(context);
|
|
446
|
+
}
|
|
447
|
+
/** Check if response cache exists */
|
|
448
|
+
let responseBody = this[kAssetCache].get(doc, `$schema`);
|
|
449
|
+
/** Create response if response cache does not exists */
|
|
450
|
+
if (!responseBody) {
|
|
451
|
+
const schema = doc.export();
|
|
452
|
+
responseBody = JSON.stringify(schema);
|
|
453
|
+
this[kAssetCache].set(doc, `$schema`, responseBody);
|
|
454
|
+
}
|
|
455
|
+
response.end(responseBody);
|
|
360
456
|
}
|
|
361
457
|
/**
|
|
362
458
|
*
|
|
@@ -383,12 +479,13 @@ export class HttpHandler {
|
|
|
383
479
|
let responseArgs = this[kAssetCache].get(response, cacheKey);
|
|
384
480
|
if (!responseArgs) {
|
|
385
481
|
responseArgs = { statusCode, contentType };
|
|
386
|
-
if (operation
|
|
482
|
+
if (operation?.responses.length) {
|
|
387
483
|
/** Filter available HttpOperationResponse instances according to status code. */
|
|
388
484
|
const filteredResponses = operation.responses.filter(r => r.statusCode.find(sc => sc.start <= statusCode && sc.end >= statusCode));
|
|
389
485
|
/** Throw InternalServerError if controller returns non-configured status code */
|
|
390
|
-
if (!filteredResponses.length && statusCode < 400)
|
|
486
|
+
if (!filteredResponses.length && statusCode < 400) {
|
|
391
487
|
throw new InternalServerError(`No responses defined for status code ${statusCode} in operation "${operation.name}"`);
|
|
488
|
+
}
|
|
392
489
|
/** We search for content-type in filtered HttpOperationResponse array */
|
|
393
490
|
if (filteredResponses.length) {
|
|
394
491
|
/** If no response returned, and content-type has not been set (No response wants to be returned by operation) */
|
|
@@ -401,8 +498,9 @@ export class HttpHandler {
|
|
|
401
498
|
if (contentType) {
|
|
402
499
|
// Find HttpEndpointResponse instance according to content-type header
|
|
403
500
|
operationResponse = filteredResponses.find(r => typeIs.is(contentType, toArray(r.contentType)));
|
|
404
|
-
if (!operationResponse)
|
|
501
|
+
if (!operationResponse) {
|
|
405
502
|
throw new InternalServerError(`Operation didn't configured to return "${contentType}" content`);
|
|
503
|
+
}
|
|
406
504
|
}
|
|
407
505
|
else {
|
|
408
506
|
/** Select first HttpOperationResponse if content-type header has not been set */
|
|
@@ -413,6 +511,8 @@ export class HttpHandler {
|
|
|
413
511
|
: operationResponse.contentType);
|
|
414
512
|
if (typeof ct === 'string')
|
|
415
513
|
responseArgs.contentType = contentType = ct;
|
|
514
|
+
else if (operationResponse.type)
|
|
515
|
+
responseArgs.contentType = MimeTypes.opra_response_json;
|
|
416
516
|
}
|
|
417
517
|
}
|
|
418
518
|
}
|
|
@@ -424,6 +524,10 @@ export class HttpHandler {
|
|
|
424
524
|
}
|
|
425
525
|
if (!hasBody)
|
|
426
526
|
delete responseArgs.contentType;
|
|
527
|
+
if (operation?.composition?.startsWith('Entity.')) {
|
|
528
|
+
if (context.queryParams.projection)
|
|
529
|
+
responseArgs.projection = context.queryParams.projection;
|
|
530
|
+
}
|
|
427
531
|
this[kAssetCache].set(response, cacheKey, { ...responseArgs });
|
|
428
532
|
}
|
|
429
533
|
/** Fix response value according to composition */
|
|
@@ -434,23 +538,26 @@ export class HttpHandler {
|
|
|
434
538
|
case 'Entity.Get':
|
|
435
539
|
case 'Entity.FindMany':
|
|
436
540
|
case 'Entity.Update': {
|
|
437
|
-
if (!(body instanceof OperationResult))
|
|
541
|
+
if (!(body instanceof OperationResult)) {
|
|
438
542
|
body = new OperationResult({
|
|
439
543
|
payload: body,
|
|
440
544
|
});
|
|
545
|
+
}
|
|
441
546
|
if ((composition === 'Entity.Create' || composition === 'Entity.Update') &&
|
|
442
547
|
composition &&
|
|
443
|
-
body.affected == null)
|
|
548
|
+
body.affected == null) {
|
|
444
549
|
body.affected = 1;
|
|
550
|
+
}
|
|
445
551
|
break;
|
|
446
552
|
}
|
|
447
553
|
case 'Entity.Delete':
|
|
448
554
|
case 'Entity.DeleteMany':
|
|
449
555
|
case 'Entity.UpdateMany': {
|
|
450
|
-
if (!(body instanceof OperationResult))
|
|
556
|
+
if (!(body instanceof OperationResult)) {
|
|
451
557
|
body = new OperationResult({
|
|
452
558
|
affected: body,
|
|
453
559
|
});
|
|
560
|
+
}
|
|
454
561
|
body.affected =
|
|
455
562
|
typeof body.affected === 'number'
|
|
456
563
|
? body.affected
|
|
@@ -461,68 +568,27 @@ export class HttpHandler {
|
|
|
461
568
|
: undefined;
|
|
462
569
|
break;
|
|
463
570
|
}
|
|
571
|
+
default:
|
|
572
|
+
break;
|
|
464
573
|
}
|
|
465
574
|
}
|
|
466
|
-
if (responseArgs.contentType && responseArgs.contentType !== parsedContentType?.type)
|
|
575
|
+
if (responseArgs.contentType && responseArgs.contentType !== parsedContentType?.type) {
|
|
467
576
|
response.setHeader('content-type', responseArgs.contentType);
|
|
577
|
+
}
|
|
468
578
|
if (responseArgs.contentType &&
|
|
469
579
|
body != null &&
|
|
470
580
|
!(body instanceof OperationResult) &&
|
|
471
|
-
typeIs.is(responseArgs.contentType, [MimeTypes.opra_response_json]))
|
|
581
|
+
typeIs.is(responseArgs.contentType, [MimeTypes.opra_response_json])) {
|
|
472
582
|
body = new OperationResult({ payload: body });
|
|
583
|
+
}
|
|
473
584
|
if (hasBody)
|
|
474
585
|
responseArgs.body = body;
|
|
475
586
|
return responseArgs;
|
|
476
587
|
}
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
|
|
482
|
-
const { searchParams } = url;
|
|
483
|
-
const documentId = searchParams.get('id');
|
|
484
|
-
const doc = documentId ? document.findDocument(documentId) : document;
|
|
485
|
-
if (!doc)
|
|
486
|
-
return this.sendErrorResponse(response, [
|
|
487
|
-
new BadRequestError({
|
|
488
|
-
message: `Document with given id [${documentId}] does not exists`,
|
|
489
|
-
}),
|
|
490
|
-
]);
|
|
491
|
-
/** Check if response cache exists */
|
|
492
|
-
let responseBody = this[kAssetCache].get(doc, `$schema`);
|
|
493
|
-
/** Create response if response cache does not exists */
|
|
494
|
-
if (!responseBody) {
|
|
495
|
-
const schema = doc.export();
|
|
496
|
-
responseBody = JSON.stringify(schema);
|
|
497
|
-
this[kAssetCache].set(doc, `$schema`, responseBody);
|
|
498
|
-
}
|
|
499
|
-
response.end(responseBody);
|
|
500
|
-
}
|
|
501
|
-
async sendErrorResponse(response, errors) {
|
|
502
|
-
if (response.headersSent) {
|
|
503
|
-
response.end();
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
if (!errors.length)
|
|
507
|
-
errors.push(wrapException({ status: response.statusCode || 500 }));
|
|
508
|
-
const { logger } = this.adapter;
|
|
509
|
-
errors.forEach(x => {
|
|
510
|
-
if (x instanceof OpraException) {
|
|
511
|
-
switch (x.severity) {
|
|
512
|
-
case 'fatal':
|
|
513
|
-
logger.fatal(x);
|
|
514
|
-
break;
|
|
515
|
-
case 'warning':
|
|
516
|
-
logger.warn(x);
|
|
517
|
-
break;
|
|
518
|
-
default:
|
|
519
|
-
logger.error(x);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
else
|
|
523
|
-
logger.fatal(x);
|
|
524
|
-
});
|
|
525
|
-
const wrappedErrors = errors.map(wrapException);
|
|
588
|
+
_wrapExceptions(exceptions) {
|
|
589
|
+
const wrappedErrors = exceptions.map(wrapException);
|
|
590
|
+
if (!wrappedErrors.length)
|
|
591
|
+
wrappedErrors.push(new InternalServerError());
|
|
526
592
|
// Sort errors from fatal to info
|
|
527
593
|
wrappedErrors.sort((a, b) => {
|
|
528
594
|
const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
|
|
@@ -530,36 +596,6 @@ export class HttpHandler {
|
|
|
530
596
|
return b.status - a.status;
|
|
531
597
|
return i;
|
|
532
598
|
});
|
|
533
|
-
|
|
534
|
-
if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
|
|
535
|
-
status = wrappedErrors[0].status;
|
|
536
|
-
if (status < Number(HttpStatusCode.BAD_REQUEST))
|
|
537
|
-
status = HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
538
|
-
}
|
|
539
|
-
response.statusCode = status;
|
|
540
|
-
const { document } = this.adapter;
|
|
541
|
-
const dt = document.node.getComplexType('OperationResult');
|
|
542
|
-
let encode = this[kAssetCache].get(dt, 'encode');
|
|
543
|
-
if (!encode) {
|
|
544
|
-
encode = dt.generateCodec('encode');
|
|
545
|
-
this[kAssetCache].set(dt, 'encode', encode);
|
|
546
|
-
}
|
|
547
|
-
const { i18n } = this.adapter;
|
|
548
|
-
const bodyObject = new OperationResult({
|
|
549
|
-
errors: wrappedErrors.map(x => {
|
|
550
|
-
const o = x.toJSON();
|
|
551
|
-
if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
|
|
552
|
-
delete o.stack;
|
|
553
|
-
return i18n.deep(o);
|
|
554
|
-
}),
|
|
555
|
-
});
|
|
556
|
-
const body = encode(bodyObject);
|
|
557
|
-
response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
|
|
558
|
-
response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
559
|
-
response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
|
|
560
|
-
response.setHeader(HttpHeaderCodes.Expires, '-1');
|
|
561
|
-
response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
|
|
562
|
-
response.send(JSON.stringify(body));
|
|
563
|
-
response.end();
|
|
599
|
+
return wrappedErrors;
|
|
564
600
|
}
|
|
565
601
|
}
|