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