@opra/core 0.26.5 → 0.27.0
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/execution-context.host.js +0 -1
- package/cjs/http/http-adapter-host.js +168 -135
- package/cjs/request-context.js +1 -1
- package/esm/execution-context.host.js +0 -1
- package/esm/http/http-adapter-host.js +169 -136
- package/esm/request-context.js +1 -1
- package/package.json +3 -3
- package/types/execution-context.d.ts +0 -1
- package/types/execution-context.host.d.ts +0 -1
- package/types/http/http-adapter-host.d.ts +4 -4
- package/types/response.d.ts +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HttpAdapterHost = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
const assert_1 = tslib_1.__importDefault(require("assert"));
|
|
6
5
|
const promises_1 = tslib_1.__importDefault(require("fs/promises"));
|
|
7
6
|
const os_1 = tslib_1.__importDefault(require("os"));
|
|
7
|
+
const type_is_1 = tslib_1.__importDefault(require("type-is"));
|
|
8
8
|
const vg = tslib_1.__importStar(require("valgen"));
|
|
9
9
|
const common_1 = require("@opra/common");
|
|
10
10
|
const execution_context_host_js_1 = require("../execution-context.host.js");
|
|
@@ -33,54 +33,47 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
33
33
|
async handleHttp(incoming, outgoing) {
|
|
34
34
|
const context = new execution_context_host_js_1.ExecutionContextHost(this.api, this.platform, { http: { incoming, outgoing } });
|
|
35
35
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
/* istanbul ignore next */
|
|
37
|
+
if (!this._api)
|
|
38
|
+
throw new common_1.InternalServerError(`${Object.getPrototypeOf(this).constructor.name} has not been initialized yet`);
|
|
39
|
+
outgoing.setHeader(common_1.HttpHeaderCodes.X_Opra_Version, common_1.OpraSchema.SpecVersion);
|
|
40
|
+
// Expose headers if cors enabled
|
|
41
|
+
if (outgoing.getHeader(common_1.HttpHeaderCodes.Access_Control_Allow_Origin)) {
|
|
42
|
+
// Expose X-Opra-* headers
|
|
43
|
+
outgoing.appendHeader(common_1.HttpHeaderCodes.Access_Control_Expose_Headers, Object.values(common_1.HttpHeaderCodes)
|
|
44
|
+
.filter(k => k.toLowerCase().startsWith('x-opra-')));
|
|
45
|
+
}
|
|
46
|
+
const { parsedUrl } = incoming;
|
|
47
|
+
if (!parsedUrl.path.length) {
|
|
48
|
+
if (incoming.method === 'GET') {
|
|
49
|
+
outgoing.setHeader('content-type', 'application/json');
|
|
50
|
+
outgoing.end(JSON.stringify(this.api.exportSchema({ webSafe: true })));
|
|
51
|
+
return;
|
|
46
52
|
}
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
outgoing.setHeader('content-type', 'application/json');
|
|
51
|
-
outgoing.end(JSON.stringify(this.api.exportSchema({ webSafe: true })));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
// Process Batch
|
|
55
|
-
if (incoming.method === 'POST' && incoming.headers['content-type'] === 'multipart/mixed') {
|
|
56
|
-
// todo Process Batch
|
|
57
|
-
}
|
|
58
|
-
throw new common_1.BadRequestError();
|
|
53
|
+
// Process Batch
|
|
54
|
+
if (incoming.method === 'POST' && incoming.headers['content-type'] === 'multipart/mixed') {
|
|
55
|
+
// todo Process Batch
|
|
59
56
|
}
|
|
60
|
-
|
|
61
|
-
let requestProcessed = false;
|
|
62
|
-
const next = async () => {
|
|
63
|
-
const interceptor = this._interceptors[i++];
|
|
64
|
-
if (interceptor) {
|
|
65
|
-
await interceptor(context, next);
|
|
66
|
-
await next();
|
|
67
|
-
}
|
|
68
|
-
else if (!requestProcessed) {
|
|
69
|
-
requestProcessed = true;
|
|
70
|
-
await this.handleExecution(context);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
await next();
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
context.errors.push((0, common_1.wrapException)(error));
|
|
77
|
-
}
|
|
78
|
-
// If no response returned to the client we send an error
|
|
79
|
-
if (!outgoing.writableEnded) {
|
|
80
|
-
if (!context.errors.length)
|
|
81
|
-
context.errors.push(new common_1.BadRequestError(`Server can not process this request`));
|
|
82
|
-
await this.handleError(context);
|
|
57
|
+
throw new common_1.BadRequestError();
|
|
83
58
|
}
|
|
59
|
+
let i = 0;
|
|
60
|
+
let requestProcessed = false;
|
|
61
|
+
const next = async () => {
|
|
62
|
+
const interceptor = this._interceptors[i++];
|
|
63
|
+
if (interceptor) {
|
|
64
|
+
await interceptor(context, next);
|
|
65
|
+
await next();
|
|
66
|
+
}
|
|
67
|
+
else if (!requestProcessed) {
|
|
68
|
+
requestProcessed = true;
|
|
69
|
+
await this.handleExecution(context);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
await next();
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!outgoing.writableEnded)
|
|
76
|
+
await this.sendErrorResponse(context, [error]);
|
|
84
77
|
}
|
|
85
78
|
finally {
|
|
86
79
|
await context.emitAsync('finish');
|
|
@@ -88,16 +81,27 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
88
81
|
}
|
|
89
82
|
async handleExecution(executionContext) {
|
|
90
83
|
// Parse incoming message and create Request object
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
let request;
|
|
85
|
+
try {
|
|
86
|
+
request = await this.parseRequest(executionContext);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (e instanceof common_1.OpraException)
|
|
90
|
+
throw e;
|
|
91
|
+
if (e instanceof vg.ValidationError) {
|
|
92
|
+
throw new common_1.BadRequestError({
|
|
93
|
+
message: (0, common_1.translate)('error:RESPONSE_VALIDATION,', 'Response validation failed'),
|
|
94
|
+
code: 'RESPONSE_VALIDATION',
|
|
95
|
+
details: e.issues
|
|
96
|
+
}, e);
|
|
97
|
+
}
|
|
98
|
+
throw new common_1.BadRequestError(e);
|
|
99
99
|
}
|
|
100
100
|
try {
|
|
101
|
+
const { outgoing } = executionContext.switchToHttp();
|
|
102
|
+
const response = new response_host_js_1.ResponseHost({ http: outgoing });
|
|
103
|
+
const context = request_context_js_1.RequestContext.from(executionContext, request, response);
|
|
104
|
+
await this.executeRequest(context);
|
|
101
105
|
await this.sendResponse(context);
|
|
102
106
|
}
|
|
103
107
|
catch (e) {
|
|
@@ -151,7 +155,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
151
155
|
return request;
|
|
152
156
|
}
|
|
153
157
|
if (resource instanceof common_1.Storage)
|
|
154
|
-
request = await this._parseRequestStorage(executionContext, resource,
|
|
158
|
+
request = await this._parseRequestStorage(executionContext, resource, searchParams);
|
|
155
159
|
else if (urlPath.length === 1) { // Collection and Singleton resources should be last element in path
|
|
156
160
|
if (resource instanceof common_1.Collection)
|
|
157
161
|
request = await this._parseRequestCollection(executionContext, resource, urlPath, searchParams);
|
|
@@ -171,7 +175,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
171
175
|
const { controller, endpoint, handler } = await this.getActionHandler(resource, p.resource);
|
|
172
176
|
const { incoming } = executionContext.switchToHttp();
|
|
173
177
|
const contentId = incoming.headers['content-id'];
|
|
174
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
178
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
175
179
|
return new request_host_js_1.RequestHost({
|
|
176
180
|
endpoint,
|
|
177
181
|
controller,
|
|
@@ -194,7 +198,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
194
198
|
const jsonReader = (0, json_body_loader_js_1.jsonBodyLoader)({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
195
199
|
let data = await jsonReader(incoming);
|
|
196
200
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
197
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
201
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
198
202
|
return new request_host_js_1.RequestHost({
|
|
199
203
|
endpoint,
|
|
200
204
|
controller,
|
|
@@ -215,7 +219,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
215
219
|
case 'DELETE': {
|
|
216
220
|
if (p.key != null) {
|
|
217
221
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
218
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
222
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
219
223
|
return new request_host_js_1.RequestHost({
|
|
220
224
|
endpoint,
|
|
221
225
|
controller,
|
|
@@ -227,7 +231,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
227
231
|
});
|
|
228
232
|
}
|
|
229
233
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'deleteMany');
|
|
230
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
234
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
231
235
|
return new request_host_js_1.RequestHost({
|
|
232
236
|
endpoint,
|
|
233
237
|
controller,
|
|
@@ -243,7 +247,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
243
247
|
case 'GET': {
|
|
244
248
|
if (p.key != null) {
|
|
245
249
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
246
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
250
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
247
251
|
return new request_host_js_1.RequestHost({
|
|
248
252
|
endpoint,
|
|
249
253
|
controller,
|
|
@@ -260,7 +264,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
260
264
|
});
|
|
261
265
|
}
|
|
262
266
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'findMany');
|
|
263
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
267
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
264
268
|
return new request_host_js_1.RequestHost({
|
|
265
269
|
endpoint,
|
|
266
270
|
controller,
|
|
@@ -283,7 +287,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
283
287
|
const jsonReader = (0, json_body_loader_js_1.jsonBodyLoader)({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
284
288
|
let data = await jsonReader(incoming);
|
|
285
289
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
286
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
290
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
287
291
|
return new request_host_js_1.RequestHost({
|
|
288
292
|
endpoint,
|
|
289
293
|
controller,
|
|
@@ -304,7 +308,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
304
308
|
const jsonReader = (0, json_body_loader_js_1.jsonBodyLoader)({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
305
309
|
let data = await jsonReader(incoming);
|
|
306
310
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
307
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
311
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
308
312
|
return new request_host_js_1.RequestHost({
|
|
309
313
|
endpoint,
|
|
310
314
|
controller,
|
|
@@ -328,14 +332,13 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
328
332
|
if ((incoming.method === 'POST' || incoming.method === 'PATCH') && !incoming.is('json'))
|
|
329
333
|
throw new common_1.BadRequestError({ message: 'Unsupported Content-Type' });
|
|
330
334
|
const contentId = incoming.headers['content-id'];
|
|
331
|
-
const p = urlPath[0];
|
|
332
335
|
switch (incoming.method) {
|
|
333
336
|
case 'POST': {
|
|
334
337
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'create');
|
|
335
338
|
const jsonReader = (0, json_body_loader_js_1.jsonBodyLoader)({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
336
339
|
let data = await jsonReader(incoming);
|
|
337
340
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
338
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
341
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
339
342
|
return new request_host_js_1.RequestHost({
|
|
340
343
|
endpoint,
|
|
341
344
|
controller,
|
|
@@ -353,7 +356,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
353
356
|
}
|
|
354
357
|
case 'DELETE': {
|
|
355
358
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
356
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
359
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
357
360
|
return new request_host_js_1.RequestHost({
|
|
358
361
|
endpoint,
|
|
359
362
|
controller,
|
|
@@ -365,7 +368,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
365
368
|
}
|
|
366
369
|
case 'GET': {
|
|
367
370
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
368
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
371
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
369
372
|
return new request_host_js_1.RequestHost({
|
|
370
373
|
endpoint,
|
|
371
374
|
controller,
|
|
@@ -385,7 +388,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
385
388
|
const jsonReader = (0, json_body_loader_js_1.jsonBodyLoader)({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
386
389
|
let data = await jsonReader(incoming);
|
|
387
390
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
388
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
391
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
389
392
|
return new request_host_js_1.RequestHost({
|
|
390
393
|
endpoint,
|
|
391
394
|
controller,
|
|
@@ -406,14 +409,13 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
406
409
|
message: `Singleton resource doesn't accept http "${incoming.method}" method`
|
|
407
410
|
});
|
|
408
411
|
}
|
|
409
|
-
async _parseRequestStorage(executionContext, resource,
|
|
412
|
+
async _parseRequestStorage(executionContext, resource, searchParams) {
|
|
410
413
|
const { incoming } = executionContext.switchToHttp();
|
|
411
414
|
const contentId = incoming.headers['content-id'];
|
|
412
|
-
const p = urlPath[0];
|
|
413
415
|
switch (incoming.method) {
|
|
414
416
|
case 'GET': {
|
|
415
417
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
416
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
418
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
417
419
|
return new request_host_js_1.RequestHost({
|
|
418
420
|
endpoint,
|
|
419
421
|
controller,
|
|
@@ -426,7 +428,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
426
428
|
}
|
|
427
429
|
case 'DELETE': {
|
|
428
430
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
429
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
431
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
430
432
|
return new request_host_js_1.RequestHost({
|
|
431
433
|
endpoint,
|
|
432
434
|
controller,
|
|
@@ -439,7 +441,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
439
441
|
}
|
|
440
442
|
case 'POST': {
|
|
441
443
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'post');
|
|
442
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
444
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
443
445
|
await promises_1.default.mkdir(this._tempDir, { recursive: true });
|
|
444
446
|
const multipartIterator = new multipart_helper_js_1.MultipartIterator(incoming, {
|
|
445
447
|
...endpoint.options,
|
|
@@ -468,13 +470,15 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
468
470
|
message: `Storage resource doesn't accept http "${incoming.method}" method`
|
|
469
471
|
});
|
|
470
472
|
}
|
|
471
|
-
parseParameters(paramDefs,
|
|
473
|
+
parseParameters(paramDefs, searchParams) {
|
|
472
474
|
const out = {};
|
|
473
475
|
// Parse known parameters
|
|
474
476
|
for (const [k, prm] of paramDefs.entries()) {
|
|
475
477
|
const decode = prm.getDecoder();
|
|
476
478
|
let v = searchParams?.getAll(k);
|
|
477
479
|
try {
|
|
480
|
+
if (!v.length && prm.default != null)
|
|
481
|
+
v = [prm.default];
|
|
478
482
|
if (!prm.isArray) {
|
|
479
483
|
v = v[0];
|
|
480
484
|
v = decode(v, { coerce: true });
|
|
@@ -507,7 +511,7 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
507
511
|
async executeRequest(context) {
|
|
508
512
|
const { request } = context;
|
|
509
513
|
const { response } = context;
|
|
510
|
-
const { resource, handler } = request;
|
|
514
|
+
const { endpoint, resource, handler } = request;
|
|
511
515
|
// Call endpoint handler method
|
|
512
516
|
let value;
|
|
513
517
|
try {
|
|
@@ -515,37 +519,41 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
515
519
|
if (response.value == null)
|
|
516
520
|
response.value = value;
|
|
517
521
|
// Normalize response value
|
|
518
|
-
if (
|
|
519
|
-
(resource instanceof common_1.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (operationName === 'delete' || operationName === 'deleteMany' || operationName === 'updateMany') {
|
|
523
|
-
let affected = 0;
|
|
524
|
-
if (typeof value === 'number')
|
|
525
|
-
affected = value;
|
|
526
|
-
if (typeof value === 'boolean')
|
|
527
|
-
affected = value ? 1 : 0;
|
|
528
|
-
if (typeof value === 'object')
|
|
529
|
-
affected = value.affected || value.affectedRows ||
|
|
530
|
-
(operationName === 'updateMany' ? value.updated : value.deleted);
|
|
531
|
-
response.value = affected;
|
|
532
|
-
return;
|
|
522
|
+
if (endpoint.kind === 'operation') {
|
|
523
|
+
if (resource instanceof common_1.Storage && endpoint.name === 'post') {
|
|
524
|
+
// Count file parts
|
|
525
|
+
value = context.request.parts.items.reduce((n, item) => item.file ? n + 1 : n, 0);
|
|
533
526
|
}
|
|
534
|
-
if (resource instanceof common_1.Storage)
|
|
527
|
+
if (resource instanceof common_1.Collection || resource instanceof common_1.Singleton || resource instanceof common_1.Storage) {
|
|
528
|
+
const operationName = endpoint.name;
|
|
529
|
+
if (operationName === 'delete' || operationName === 'deleteMany' ||
|
|
530
|
+
operationName === 'updateMany' || operationName === 'post') {
|
|
531
|
+
let affected = 0;
|
|
532
|
+
if (typeof value === 'number')
|
|
533
|
+
affected = value;
|
|
534
|
+
else if (typeof value === 'boolean')
|
|
535
|
+
affected = value ? 1 : 0;
|
|
536
|
+
else if (typeof value === 'object')
|
|
537
|
+
affected = value.affected || value.affectedRows ||
|
|
538
|
+
(operationName === 'updateMany' ? value.updated : value.deleted);
|
|
539
|
+
response.value = affected;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (resource instanceof common_1.Storage)
|
|
543
|
+
return;
|
|
544
|
+
// "get" and "update" endpoints must return the entity instance, otherwise it means resource not found
|
|
545
|
+
if (value == null && (operationName === 'get' || operationName === 'update'))
|
|
546
|
+
throw new common_1.ResourceNotFoundError(resource.name, request.key);
|
|
547
|
+
// "findMany" endpoint should return array of entity instances
|
|
548
|
+
if (operationName === 'findMany')
|
|
549
|
+
value = (value == null ? [] : Array.isArray(value) ? value : [value]);
|
|
550
|
+
else
|
|
551
|
+
value = value == null ? {} : Array.isArray(value) ? value[0] : value;
|
|
552
|
+
value = endpoint.encodeReturning(value, { coerce: true });
|
|
553
|
+
response.value = value;
|
|
535
554
|
return;
|
|
536
|
-
|
|
537
|
-
if (value == null && (operationName === 'get' || operationName === 'update'))
|
|
538
|
-
throw new common_1.ResourceNotFoundError(resource.name, request.key);
|
|
539
|
-
// "findMany" endpoint should return array of entity instances
|
|
540
|
-
if (operationName === 'findMany')
|
|
541
|
-
value = (value == null ? [] : Array.isArray(value) ? value : [value]);
|
|
542
|
-
else
|
|
543
|
-
value = value == null ? {} : Array.isArray(value) ? value[0] : value;
|
|
544
|
-
value = endpoint.encodeReturning(value, { coerce: true });
|
|
545
|
-
response.value = value;
|
|
546
|
-
return;
|
|
555
|
+
}
|
|
547
556
|
}
|
|
548
|
-
const { endpoint } = request;
|
|
549
557
|
if (response.value)
|
|
550
558
|
response.value = endpoint.encodeReturning(response.value, { coerce: true });
|
|
551
559
|
}
|
|
@@ -557,52 +565,76 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
557
565
|
const { request, response } = context;
|
|
558
566
|
const { endpoint, resource } = request;
|
|
559
567
|
const outgoing = response.switchToHttp();
|
|
560
|
-
if (
|
|
568
|
+
if (response.errors?.length || (outgoing.statusCode >= 400 && outgoing.statusCode <= 599))
|
|
569
|
+
return this.sendErrorResponse(context, response.errors || []);
|
|
570
|
+
// if response redirected we do not send any response
|
|
571
|
+
if (outgoing.statusCode >= 300 && outgoing.statusCode < 400) {
|
|
572
|
+
outgoing.end();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
let contentType = String(outgoing.getHeader('content-type') || '');
|
|
576
|
+
const returnType = endpoint.returnType;
|
|
577
|
+
if (endpoint.kind === 'action' && !contentType && endpoint.returnMime && response.value) {
|
|
578
|
+
contentType = endpoint.returnMime;
|
|
579
|
+
outgoing.setHeader('Content-Type', contentType);
|
|
580
|
+
}
|
|
581
|
+
// OperationResult response
|
|
582
|
+
if ((endpoint.kind === 'operation' &&
|
|
561
583
|
(resource instanceof common_1.Collection || resource instanceof common_1.Singleton ||
|
|
562
|
-
(resource instanceof common_1.Storage && endpoint.name !== 'get')))
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
584
|
+
(resource instanceof common_1.Storage && endpoint.name !== 'get'))) ||
|
|
585
|
+
(endpoint.kind === 'action' &&
|
|
586
|
+
(!contentType || type_is_1.default.is(contentType, ['application/opra+json'])))) {
|
|
587
|
+
const incoming = context.switchToHttp().incoming;
|
|
588
|
+
const apiUrl = new common_1.OpraURL(incoming.baseUrl, incoming.protocol + '://' + incoming.get('host')).toString();
|
|
589
|
+
const body = new common_1.OperationResult({
|
|
590
|
+
context: endpoint.getFullPath(false),
|
|
591
|
+
contextUrl: apiUrl + '/#' + endpoint.getFullPath(true)
|
|
592
|
+
});
|
|
593
|
+
const operationName = endpoint.kind === 'operation' ? endpoint.name : '';
|
|
594
|
+
if (operationName === 'delete' || operationName === 'deleteMany' ||
|
|
595
|
+
operationName === 'updateMany' || operationName === 'post')
|
|
570
596
|
body.affected = response.value;
|
|
571
|
-
}
|
|
572
597
|
else {
|
|
573
|
-
|
|
574
|
-
throw new common_1.InternalServerError(`"${request.endpoint.name}" endpoint should return value`);
|
|
575
|
-
(0, assert_1.default)(returnType instanceof common_1.ComplexType);
|
|
576
|
-
body.type = returnType.name || '#anonymous';
|
|
577
|
-
body.data = this.i18n.deep(response.value);
|
|
598
|
+
outgoing.statusCode = outgoing.statusCode || common_1.HttpStatusCodes.OK;
|
|
578
599
|
if (operationName === 'create')
|
|
579
600
|
outgoing.statusCode = 201;
|
|
580
|
-
if (operationName === '
|
|
581
|
-
body.affected = 1;
|
|
582
|
-
|
|
583
|
-
body.
|
|
584
|
-
|
|
585
|
-
|
|
601
|
+
if (operationName === 'update' || operationName === 'create')
|
|
602
|
+
body.affected = response.value ? 1 : 0;
|
|
603
|
+
if (operationName === 'findMany') {
|
|
604
|
+
body.count = response.value.length;
|
|
605
|
+
body.totalMatches = response.totalMatches;
|
|
606
|
+
}
|
|
607
|
+
if (returnType) {
|
|
608
|
+
if (response.value == null)
|
|
609
|
+
throw new common_1.InternalServerError(`"${request.endpoint.name}" endpoint should return value`);
|
|
610
|
+
if (returnType.name) {
|
|
611
|
+
const ns = this.api.getDataTypeNs(returnType);
|
|
612
|
+
// const isOpraSpec = returnType.document.url?.startsWith('https://oprajs.com/spec/v1.0')
|
|
613
|
+
body.type = (ns ? ns + ':' : '') + returnType.name;
|
|
614
|
+
body.typeUrl =
|
|
615
|
+
(ns
|
|
616
|
+
? new common_1.OpraURL('/#/types/' + returnType.name, returnType.document.url || 'http://tempuri.org').toString()
|
|
617
|
+
: apiUrl + '/#/types/' + returnType.name);
|
|
618
|
+
}
|
|
619
|
+
else
|
|
620
|
+
body.typeUrl = body.contextUrl + '/type';
|
|
621
|
+
body.payload = this.i18n.deep(response.value);
|
|
622
|
+
}
|
|
586
623
|
}
|
|
587
|
-
outgoing.statusCode = outgoing.statusCode || common_1.HttpStatusCodes.OK;
|
|
588
624
|
outgoing.setHeader(common_1.HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
|
|
589
625
|
outgoing.send(JSON.stringify(body));
|
|
590
626
|
outgoing.end();
|
|
591
627
|
return;
|
|
592
628
|
}
|
|
593
|
-
// Storage "get" resource
|
|
594
|
-
if (endpoint.kind === 'action') {
|
|
595
|
-
//
|
|
596
|
-
}
|
|
597
629
|
outgoing.statusCode = outgoing.statusCode || common_1.HttpStatusCodes.OK;
|
|
598
630
|
if (response.value != null) {
|
|
599
631
|
if (typeof response.value === 'string') {
|
|
600
|
-
if (!
|
|
632
|
+
if (!contentType)
|
|
601
633
|
outgoing.setHeader('content-type', 'text/plain');
|
|
602
634
|
outgoing.send(response.value);
|
|
603
635
|
}
|
|
604
636
|
else if (Buffer.isBuffer(response.value) || (0, common_1.isReadable)(response.value)) {
|
|
605
|
-
if (!
|
|
637
|
+
if (!contentType)
|
|
606
638
|
outgoing.setHeader('content-type', 'application/octet-stream');
|
|
607
639
|
outgoing.send(response.value);
|
|
608
640
|
}
|
|
@@ -613,13 +645,14 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
613
645
|
}
|
|
614
646
|
outgoing.end();
|
|
615
647
|
}
|
|
616
|
-
async
|
|
617
|
-
const { errors } = context;
|
|
648
|
+
async sendErrorResponse(context, errors) {
|
|
618
649
|
const { outgoing } = context.switchToHttp();
|
|
619
650
|
if (outgoing.headersSent) {
|
|
620
651
|
outgoing.end();
|
|
621
652
|
return;
|
|
622
653
|
}
|
|
654
|
+
if (!errors.length)
|
|
655
|
+
errors.push((0, common_1.wrapException)({ status: outgoing.statusCode || 500 }));
|
|
623
656
|
errors.forEach(x => {
|
|
624
657
|
if (x instanceof common_1.OpraException) {
|
|
625
658
|
switch (x.severity) {
|
|
@@ -651,9 +684,9 @@ class HttpAdapterHost extends platform_adapter_host_js_1.PlatformAdapterHost {
|
|
|
651
684
|
status = common_1.HttpStatusCodes.INTERNAL_SERVER_ERROR;
|
|
652
685
|
}
|
|
653
686
|
outgoing.statusCode = status;
|
|
654
|
-
const body = {
|
|
687
|
+
const body = new common_1.OperationResult({
|
|
655
688
|
errors: wrappedErrors.map(x => this._i18n.deep(x.toJSON()))
|
|
656
|
-
};
|
|
689
|
+
});
|
|
657
690
|
outgoing.setHeader(common_1.HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
|
|
658
691
|
outgoing.setHeader(common_1.HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
659
692
|
outgoing.setHeader(common_1.HttpHeaderCodes.Pragma, 'no-cache');
|
package/cjs/request-context.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import assert from 'assert';
|
|
2
1
|
import fs from 'fs/promises';
|
|
3
2
|
import os from 'os';
|
|
3
|
+
import typeIs from 'type-is';
|
|
4
4
|
import * as vg from 'valgen';
|
|
5
|
-
import { BadRequestError, Collection,
|
|
5
|
+
import { BadRequestError, Collection, Container, HttpHeaderCodes, HttpStatusCodes, InternalServerError, isReadable, IssueSeverity, MethodNotAllowedError, OperationResult, OpraException, OpraSchema, OpraURL, ResourceNotFoundError, Singleton, Storage, translate, uid, wrapException } from '@opra/common';
|
|
6
6
|
import { ExecutionContextHost } from '../execution-context.host.js';
|
|
7
7
|
import { PlatformAdapterHost } from '../platform-adapter.host.js';
|
|
8
8
|
import { RequestHost } from '../request.host.js';
|
|
@@ -29,54 +29,47 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
29
29
|
async handleHttp(incoming, outgoing) {
|
|
30
30
|
const context = new ExecutionContextHost(this.api, this.platform, { http: { incoming, outgoing } });
|
|
31
31
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
/* istanbul ignore next */
|
|
33
|
+
if (!this._api)
|
|
34
|
+
throw new InternalServerError(`${Object.getPrototypeOf(this).constructor.name} has not been initialized yet`);
|
|
35
|
+
outgoing.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
|
|
36
|
+
// Expose headers if cors enabled
|
|
37
|
+
if (outgoing.getHeader(HttpHeaderCodes.Access_Control_Allow_Origin)) {
|
|
38
|
+
// Expose X-Opra-* headers
|
|
39
|
+
outgoing.appendHeader(HttpHeaderCodes.Access_Control_Expose_Headers, Object.values(HttpHeaderCodes)
|
|
40
|
+
.filter(k => k.toLowerCase().startsWith('x-opra-')));
|
|
41
|
+
}
|
|
42
|
+
const { parsedUrl } = incoming;
|
|
43
|
+
if (!parsedUrl.path.length) {
|
|
44
|
+
if (incoming.method === 'GET') {
|
|
45
|
+
outgoing.setHeader('content-type', 'application/json');
|
|
46
|
+
outgoing.end(JSON.stringify(this.api.exportSchema({ webSafe: true })));
|
|
47
|
+
return;
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
outgoing.setHeader('content-type', 'application/json');
|
|
47
|
-
outgoing.end(JSON.stringify(this.api.exportSchema({ webSafe: true })));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
// Process Batch
|
|
51
|
-
if (incoming.method === 'POST' && incoming.headers['content-type'] === 'multipart/mixed') {
|
|
52
|
-
// todo Process Batch
|
|
53
|
-
}
|
|
54
|
-
throw new BadRequestError();
|
|
49
|
+
// Process Batch
|
|
50
|
+
if (incoming.method === 'POST' && incoming.headers['content-type'] === 'multipart/mixed') {
|
|
51
|
+
// todo Process Batch
|
|
55
52
|
}
|
|
56
|
-
|
|
57
|
-
let requestProcessed = false;
|
|
58
|
-
const next = async () => {
|
|
59
|
-
const interceptor = this._interceptors[i++];
|
|
60
|
-
if (interceptor) {
|
|
61
|
-
await interceptor(context, next);
|
|
62
|
-
await next();
|
|
63
|
-
}
|
|
64
|
-
else if (!requestProcessed) {
|
|
65
|
-
requestProcessed = true;
|
|
66
|
-
await this.handleExecution(context);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
await next();
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
context.errors.push(wrapException(error));
|
|
73
|
-
}
|
|
74
|
-
// If no response returned to the client we send an error
|
|
75
|
-
if (!outgoing.writableEnded) {
|
|
76
|
-
if (!context.errors.length)
|
|
77
|
-
context.errors.push(new BadRequestError(`Server can not process this request`));
|
|
78
|
-
await this.handleError(context);
|
|
53
|
+
throw new BadRequestError();
|
|
79
54
|
}
|
|
55
|
+
let i = 0;
|
|
56
|
+
let requestProcessed = false;
|
|
57
|
+
const next = async () => {
|
|
58
|
+
const interceptor = this._interceptors[i++];
|
|
59
|
+
if (interceptor) {
|
|
60
|
+
await interceptor(context, next);
|
|
61
|
+
await next();
|
|
62
|
+
}
|
|
63
|
+
else if (!requestProcessed) {
|
|
64
|
+
requestProcessed = true;
|
|
65
|
+
await this.handleExecution(context);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
await next();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!outgoing.writableEnded)
|
|
72
|
+
await this.sendErrorResponse(context, [error]);
|
|
80
73
|
}
|
|
81
74
|
finally {
|
|
82
75
|
await context.emitAsync('finish');
|
|
@@ -84,16 +77,27 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
84
77
|
}
|
|
85
78
|
async handleExecution(executionContext) {
|
|
86
79
|
// Parse incoming message and create Request object
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
80
|
+
let request;
|
|
81
|
+
try {
|
|
82
|
+
request = await this.parseRequest(executionContext);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
if (e instanceof OpraException)
|
|
86
|
+
throw e;
|
|
87
|
+
if (e instanceof vg.ValidationError) {
|
|
88
|
+
throw new BadRequestError({
|
|
89
|
+
message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
|
|
90
|
+
code: 'RESPONSE_VALIDATION',
|
|
91
|
+
details: e.issues
|
|
92
|
+
}, e);
|
|
93
|
+
}
|
|
94
|
+
throw new BadRequestError(e);
|
|
95
95
|
}
|
|
96
96
|
try {
|
|
97
|
+
const { outgoing } = executionContext.switchToHttp();
|
|
98
|
+
const response = new ResponseHost({ http: outgoing });
|
|
99
|
+
const context = RequestContext.from(executionContext, request, response);
|
|
100
|
+
await this.executeRequest(context);
|
|
97
101
|
await this.sendResponse(context);
|
|
98
102
|
}
|
|
99
103
|
catch (e) {
|
|
@@ -147,7 +151,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
147
151
|
return request;
|
|
148
152
|
}
|
|
149
153
|
if (resource instanceof Storage)
|
|
150
|
-
request = await this._parseRequestStorage(executionContext, resource,
|
|
154
|
+
request = await this._parseRequestStorage(executionContext, resource, searchParams);
|
|
151
155
|
else if (urlPath.length === 1) { // Collection and Singleton resources should be last element in path
|
|
152
156
|
if (resource instanceof Collection)
|
|
153
157
|
request = await this._parseRequestCollection(executionContext, resource, urlPath, searchParams);
|
|
@@ -167,7 +171,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
167
171
|
const { controller, endpoint, handler } = await this.getActionHandler(resource, p.resource);
|
|
168
172
|
const { incoming } = executionContext.switchToHttp();
|
|
169
173
|
const contentId = incoming.headers['content-id'];
|
|
170
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
174
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
171
175
|
return new RequestHost({
|
|
172
176
|
endpoint,
|
|
173
177
|
controller,
|
|
@@ -190,7 +194,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
190
194
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
191
195
|
let data = await jsonReader(incoming);
|
|
192
196
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
193
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
197
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
194
198
|
return new RequestHost({
|
|
195
199
|
endpoint,
|
|
196
200
|
controller,
|
|
@@ -211,7 +215,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
211
215
|
case 'DELETE': {
|
|
212
216
|
if (p.key != null) {
|
|
213
217
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
214
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
218
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
215
219
|
return new RequestHost({
|
|
216
220
|
endpoint,
|
|
217
221
|
controller,
|
|
@@ -223,7 +227,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
223
227
|
});
|
|
224
228
|
}
|
|
225
229
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'deleteMany');
|
|
226
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
230
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
227
231
|
return new RequestHost({
|
|
228
232
|
endpoint,
|
|
229
233
|
controller,
|
|
@@ -239,7 +243,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
239
243
|
case 'GET': {
|
|
240
244
|
if (p.key != null) {
|
|
241
245
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
242
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
246
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
243
247
|
return new RequestHost({
|
|
244
248
|
endpoint,
|
|
245
249
|
controller,
|
|
@@ -256,7 +260,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
256
260
|
});
|
|
257
261
|
}
|
|
258
262
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'findMany');
|
|
259
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
263
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
260
264
|
return new RequestHost({
|
|
261
265
|
endpoint,
|
|
262
266
|
controller,
|
|
@@ -279,7 +283,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
279
283
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
280
284
|
let data = await jsonReader(incoming);
|
|
281
285
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
282
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
286
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
283
287
|
return new RequestHost({
|
|
284
288
|
endpoint,
|
|
285
289
|
controller,
|
|
@@ -300,7 +304,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
300
304
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
301
305
|
let data = await jsonReader(incoming);
|
|
302
306
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
303
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
307
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
304
308
|
return new RequestHost({
|
|
305
309
|
endpoint,
|
|
306
310
|
controller,
|
|
@@ -324,14 +328,13 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
324
328
|
if ((incoming.method === 'POST' || incoming.method === 'PATCH') && !incoming.is('json'))
|
|
325
329
|
throw new BadRequestError({ message: 'Unsupported Content-Type' });
|
|
326
330
|
const contentId = incoming.headers['content-id'];
|
|
327
|
-
const p = urlPath[0];
|
|
328
331
|
switch (incoming.method) {
|
|
329
332
|
case 'POST': {
|
|
330
333
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'create');
|
|
331
334
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
332
335
|
let data = await jsonReader(incoming);
|
|
333
336
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
334
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
337
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
335
338
|
return new RequestHost({
|
|
336
339
|
endpoint,
|
|
337
340
|
controller,
|
|
@@ -349,7 +352,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
349
352
|
}
|
|
350
353
|
case 'DELETE': {
|
|
351
354
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
352
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
355
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
353
356
|
return new RequestHost({
|
|
354
357
|
endpoint,
|
|
355
358
|
controller,
|
|
@@ -361,7 +364,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
361
364
|
}
|
|
362
365
|
case 'GET': {
|
|
363
366
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
364
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
367
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
365
368
|
return new RequestHost({
|
|
366
369
|
endpoint,
|
|
367
370
|
controller,
|
|
@@ -381,7 +384,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
381
384
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
382
385
|
let data = await jsonReader(incoming);
|
|
383
386
|
data = endpoint.decodeInput(data, { coerce: true });
|
|
384
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
387
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
385
388
|
return new RequestHost({
|
|
386
389
|
endpoint,
|
|
387
390
|
controller,
|
|
@@ -402,14 +405,13 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
402
405
|
message: `Singleton resource doesn't accept http "${incoming.method}" method`
|
|
403
406
|
});
|
|
404
407
|
}
|
|
405
|
-
async _parseRequestStorage(executionContext, resource,
|
|
408
|
+
async _parseRequestStorage(executionContext, resource, searchParams) {
|
|
406
409
|
const { incoming } = executionContext.switchToHttp();
|
|
407
410
|
const contentId = incoming.headers['content-id'];
|
|
408
|
-
const p = urlPath[0];
|
|
409
411
|
switch (incoming.method) {
|
|
410
412
|
case 'GET': {
|
|
411
413
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
412
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
414
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
413
415
|
return new RequestHost({
|
|
414
416
|
endpoint,
|
|
415
417
|
controller,
|
|
@@ -422,7 +424,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
422
424
|
}
|
|
423
425
|
case 'DELETE': {
|
|
424
426
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
425
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
427
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
426
428
|
return new RequestHost({
|
|
427
429
|
endpoint,
|
|
428
430
|
controller,
|
|
@@ -435,7 +437,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
435
437
|
}
|
|
436
438
|
case 'POST': {
|
|
437
439
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'post');
|
|
438
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
440
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
439
441
|
await fs.mkdir(this._tempDir, { recursive: true });
|
|
440
442
|
const multipartIterator = new MultipartIterator(incoming, {
|
|
441
443
|
...endpoint.options,
|
|
@@ -464,13 +466,15 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
464
466
|
message: `Storage resource doesn't accept http "${incoming.method}" method`
|
|
465
467
|
});
|
|
466
468
|
}
|
|
467
|
-
parseParameters(paramDefs,
|
|
469
|
+
parseParameters(paramDefs, searchParams) {
|
|
468
470
|
const out = {};
|
|
469
471
|
// Parse known parameters
|
|
470
472
|
for (const [k, prm] of paramDefs.entries()) {
|
|
471
473
|
const decode = prm.getDecoder();
|
|
472
474
|
let v = searchParams?.getAll(k);
|
|
473
475
|
try {
|
|
476
|
+
if (!v.length && prm.default != null)
|
|
477
|
+
v = [prm.default];
|
|
474
478
|
if (!prm.isArray) {
|
|
475
479
|
v = v[0];
|
|
476
480
|
v = decode(v, { coerce: true });
|
|
@@ -503,7 +507,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
503
507
|
async executeRequest(context) {
|
|
504
508
|
const { request } = context;
|
|
505
509
|
const { response } = context;
|
|
506
|
-
const { resource, handler } = request;
|
|
510
|
+
const { endpoint, resource, handler } = request;
|
|
507
511
|
// Call endpoint handler method
|
|
508
512
|
let value;
|
|
509
513
|
try {
|
|
@@ -511,37 +515,41 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
511
515
|
if (response.value == null)
|
|
512
516
|
response.value = value;
|
|
513
517
|
// Normalize response value
|
|
514
|
-
if (
|
|
515
|
-
(resource instanceof
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (operationName === 'delete' || operationName === 'deleteMany' || operationName === 'updateMany') {
|
|
519
|
-
let affected = 0;
|
|
520
|
-
if (typeof value === 'number')
|
|
521
|
-
affected = value;
|
|
522
|
-
if (typeof value === 'boolean')
|
|
523
|
-
affected = value ? 1 : 0;
|
|
524
|
-
if (typeof value === 'object')
|
|
525
|
-
affected = value.affected || value.affectedRows ||
|
|
526
|
-
(operationName === 'updateMany' ? value.updated : value.deleted);
|
|
527
|
-
response.value = affected;
|
|
528
|
-
return;
|
|
518
|
+
if (endpoint.kind === 'operation') {
|
|
519
|
+
if (resource instanceof Storage && endpoint.name === 'post') {
|
|
520
|
+
// Count file parts
|
|
521
|
+
value = context.request.parts.items.reduce((n, item) => item.file ? n + 1 : n, 0);
|
|
529
522
|
}
|
|
530
|
-
if (resource instanceof Storage)
|
|
523
|
+
if (resource instanceof Collection || resource instanceof Singleton || resource instanceof Storage) {
|
|
524
|
+
const operationName = endpoint.name;
|
|
525
|
+
if (operationName === 'delete' || operationName === 'deleteMany' ||
|
|
526
|
+
operationName === 'updateMany' || operationName === 'post') {
|
|
527
|
+
let affected = 0;
|
|
528
|
+
if (typeof value === 'number')
|
|
529
|
+
affected = value;
|
|
530
|
+
else if (typeof value === 'boolean')
|
|
531
|
+
affected = value ? 1 : 0;
|
|
532
|
+
else if (typeof value === 'object')
|
|
533
|
+
affected = value.affected || value.affectedRows ||
|
|
534
|
+
(operationName === 'updateMany' ? value.updated : value.deleted);
|
|
535
|
+
response.value = affected;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (resource instanceof Storage)
|
|
539
|
+
return;
|
|
540
|
+
// "get" and "update" endpoints must return the entity instance, otherwise it means resource not found
|
|
541
|
+
if (value == null && (operationName === 'get' || operationName === 'update'))
|
|
542
|
+
throw new ResourceNotFoundError(resource.name, request.key);
|
|
543
|
+
// "findMany" endpoint should return array of entity instances
|
|
544
|
+
if (operationName === 'findMany')
|
|
545
|
+
value = (value == null ? [] : Array.isArray(value) ? value : [value]);
|
|
546
|
+
else
|
|
547
|
+
value = value == null ? {} : Array.isArray(value) ? value[0] : value;
|
|
548
|
+
value = endpoint.encodeReturning(value, { coerce: true });
|
|
549
|
+
response.value = value;
|
|
531
550
|
return;
|
|
532
|
-
|
|
533
|
-
if (value == null && (operationName === 'get' || operationName === 'update'))
|
|
534
|
-
throw new ResourceNotFoundError(resource.name, request.key);
|
|
535
|
-
// "findMany" endpoint should return array of entity instances
|
|
536
|
-
if (operationName === 'findMany')
|
|
537
|
-
value = (value == null ? [] : Array.isArray(value) ? value : [value]);
|
|
538
|
-
else
|
|
539
|
-
value = value == null ? {} : Array.isArray(value) ? value[0] : value;
|
|
540
|
-
value = endpoint.encodeReturning(value, { coerce: true });
|
|
541
|
-
response.value = value;
|
|
542
|
-
return;
|
|
551
|
+
}
|
|
543
552
|
}
|
|
544
|
-
const { endpoint } = request;
|
|
545
553
|
if (response.value)
|
|
546
554
|
response.value = endpoint.encodeReturning(response.value, { coerce: true });
|
|
547
555
|
}
|
|
@@ -553,52 +561,76 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
553
561
|
const { request, response } = context;
|
|
554
562
|
const { endpoint, resource } = request;
|
|
555
563
|
const outgoing = response.switchToHttp();
|
|
556
|
-
if (
|
|
564
|
+
if (response.errors?.length || (outgoing.statusCode >= 400 && outgoing.statusCode <= 599))
|
|
565
|
+
return this.sendErrorResponse(context, response.errors || []);
|
|
566
|
+
// if response redirected we do not send any response
|
|
567
|
+
if (outgoing.statusCode >= 300 && outgoing.statusCode < 400) {
|
|
568
|
+
outgoing.end();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
let contentType = String(outgoing.getHeader('content-type') || '');
|
|
572
|
+
const returnType = endpoint.returnType;
|
|
573
|
+
if (endpoint.kind === 'action' && !contentType && endpoint.returnMime && response.value) {
|
|
574
|
+
contentType = endpoint.returnMime;
|
|
575
|
+
outgoing.setHeader('Content-Type', contentType);
|
|
576
|
+
}
|
|
577
|
+
// OperationResult response
|
|
578
|
+
if ((endpoint.kind === 'operation' &&
|
|
557
579
|
(resource instanceof Collection || resource instanceof Singleton ||
|
|
558
|
-
(resource instanceof Storage && endpoint.name !== 'get')))
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
580
|
+
(resource instanceof Storage && endpoint.name !== 'get'))) ||
|
|
581
|
+
(endpoint.kind === 'action' &&
|
|
582
|
+
(!contentType || typeIs.is(contentType, ['application/opra+json'])))) {
|
|
583
|
+
const incoming = context.switchToHttp().incoming;
|
|
584
|
+
const apiUrl = new OpraURL(incoming.baseUrl, incoming.protocol + '://' + incoming.get('host')).toString();
|
|
585
|
+
const body = new OperationResult({
|
|
586
|
+
context: endpoint.getFullPath(false),
|
|
587
|
+
contextUrl: apiUrl + '/#' + endpoint.getFullPath(true)
|
|
588
|
+
});
|
|
589
|
+
const operationName = endpoint.kind === 'operation' ? endpoint.name : '';
|
|
590
|
+
if (operationName === 'delete' || operationName === 'deleteMany' ||
|
|
591
|
+
operationName === 'updateMany' || operationName === 'post')
|
|
566
592
|
body.affected = response.value;
|
|
567
|
-
}
|
|
568
593
|
else {
|
|
569
|
-
|
|
570
|
-
throw new InternalServerError(`"${request.endpoint.name}" endpoint should return value`);
|
|
571
|
-
assert(returnType instanceof ComplexType);
|
|
572
|
-
body.type = returnType.name || '#anonymous';
|
|
573
|
-
body.data = this.i18n.deep(response.value);
|
|
594
|
+
outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
|
|
574
595
|
if (operationName === 'create')
|
|
575
596
|
outgoing.statusCode = 201;
|
|
576
|
-
if (operationName === '
|
|
577
|
-
body.affected = 1;
|
|
578
|
-
|
|
579
|
-
body.
|
|
580
|
-
|
|
581
|
-
|
|
597
|
+
if (operationName === 'update' || operationName === 'create')
|
|
598
|
+
body.affected = response.value ? 1 : 0;
|
|
599
|
+
if (operationName === 'findMany') {
|
|
600
|
+
body.count = response.value.length;
|
|
601
|
+
body.totalMatches = response.totalMatches;
|
|
602
|
+
}
|
|
603
|
+
if (returnType) {
|
|
604
|
+
if (response.value == null)
|
|
605
|
+
throw new InternalServerError(`"${request.endpoint.name}" endpoint should return value`);
|
|
606
|
+
if (returnType.name) {
|
|
607
|
+
const ns = this.api.getDataTypeNs(returnType);
|
|
608
|
+
// const isOpraSpec = returnType.document.url?.startsWith('https://oprajs.com/spec/v1.0')
|
|
609
|
+
body.type = (ns ? ns + ':' : '') + returnType.name;
|
|
610
|
+
body.typeUrl =
|
|
611
|
+
(ns
|
|
612
|
+
? new OpraURL('/#/types/' + returnType.name, returnType.document.url || 'http://tempuri.org').toString()
|
|
613
|
+
: apiUrl + '/#/types/' + returnType.name);
|
|
614
|
+
}
|
|
615
|
+
else
|
|
616
|
+
body.typeUrl = body.contextUrl + '/type';
|
|
617
|
+
body.payload = this.i18n.deep(response.value);
|
|
618
|
+
}
|
|
582
619
|
}
|
|
583
|
-
outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
|
|
584
620
|
outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
|
|
585
621
|
outgoing.send(JSON.stringify(body));
|
|
586
622
|
outgoing.end();
|
|
587
623
|
return;
|
|
588
624
|
}
|
|
589
|
-
// Storage "get" resource
|
|
590
|
-
if (endpoint.kind === 'action') {
|
|
591
|
-
//
|
|
592
|
-
}
|
|
593
625
|
outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
|
|
594
626
|
if (response.value != null) {
|
|
595
627
|
if (typeof response.value === 'string') {
|
|
596
|
-
if (!
|
|
628
|
+
if (!contentType)
|
|
597
629
|
outgoing.setHeader('content-type', 'text/plain');
|
|
598
630
|
outgoing.send(response.value);
|
|
599
631
|
}
|
|
600
632
|
else if (Buffer.isBuffer(response.value) || isReadable(response.value)) {
|
|
601
|
-
if (!
|
|
633
|
+
if (!contentType)
|
|
602
634
|
outgoing.setHeader('content-type', 'application/octet-stream');
|
|
603
635
|
outgoing.send(response.value);
|
|
604
636
|
}
|
|
@@ -609,13 +641,14 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
609
641
|
}
|
|
610
642
|
outgoing.end();
|
|
611
643
|
}
|
|
612
|
-
async
|
|
613
|
-
const { errors } = context;
|
|
644
|
+
async sendErrorResponse(context, errors) {
|
|
614
645
|
const { outgoing } = context.switchToHttp();
|
|
615
646
|
if (outgoing.headersSent) {
|
|
616
647
|
outgoing.end();
|
|
617
648
|
return;
|
|
618
649
|
}
|
|
650
|
+
if (!errors.length)
|
|
651
|
+
errors.push(wrapException({ status: outgoing.statusCode || 500 }));
|
|
619
652
|
errors.forEach(x => {
|
|
620
653
|
if (x instanceof OpraException) {
|
|
621
654
|
switch (x.severity) {
|
|
@@ -647,9 +680,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
647
680
|
status = HttpStatusCodes.INTERNAL_SERVER_ERROR;
|
|
648
681
|
}
|
|
649
682
|
outgoing.statusCode = status;
|
|
650
|
-
const body = {
|
|
683
|
+
const body = new OperationResult({
|
|
651
684
|
errors: wrappedErrors.map(x => this._i18n.deep(x.toJSON()))
|
|
652
|
-
};
|
|
685
|
+
});
|
|
653
686
|
outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
|
|
654
687
|
outgoing.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
655
688
|
outgoing.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
|
package/esm/request-context.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opra/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Opra schema package",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"clean:cover": "rimraf ../../coverage/core"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@opra/common": "^0.
|
|
30
|
+
"@opra/common": "^0.27.0",
|
|
31
31
|
"accepts": "^1.3.8",
|
|
32
32
|
"content-disposition": "^0.5.4",
|
|
33
33
|
"content-type": "^1.0.5",
|
|
@@ -94,4 +94,4 @@
|
|
|
94
94
|
"swagger",
|
|
95
95
|
"raml"
|
|
96
96
|
]
|
|
97
|
-
}
|
|
97
|
+
}
|
|
@@ -4,7 +4,6 @@ import type { Protocol } from './platform-adapter.js';
|
|
|
4
4
|
export interface ExecutionContext {
|
|
5
5
|
readonly protocol: Protocol;
|
|
6
6
|
readonly platform: string;
|
|
7
|
-
errors: Error[];
|
|
8
7
|
switchToHttp(): HttpMessageContext;
|
|
9
8
|
switchToWs(): WsMessageContext;
|
|
10
9
|
switchToRpc(): RpcMessageContext;
|
|
@@ -11,7 +11,6 @@ export declare class ExecutionContextHost extends AsyncEventEmitter implements E
|
|
|
11
11
|
readonly http?: HttpMessageContext;
|
|
12
12
|
readonly ws?: WsMessageContext;
|
|
13
13
|
readonly rpc?: RpcMessageContext;
|
|
14
|
-
errors: Error[];
|
|
15
14
|
constructor(api: ApiDocument, platform: string, protocol: {
|
|
16
15
|
http?: {
|
|
17
16
|
incoming: HttpServerRequest;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Collection, OpraURLPath,
|
|
1
|
+
import { Collection, OpraURLPath, Parameter, Resource, Singleton, Storage } from '@opra/common';
|
|
2
2
|
import { ExecutionContext } from '../execution-context.js';
|
|
3
3
|
import { PlatformAdapterHost } from '../platform-adapter.host.js';
|
|
4
4
|
import type { Protocol } from '../platform-adapter.js';
|
|
@@ -26,9 +26,9 @@ export declare abstract class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
26
26
|
protected _parseRequestAction(executionContext: ExecutionContext, resource: Resource, urlPath: OpraURLPath, searchParams: URLSearchParams): Promise<RequestHost>;
|
|
27
27
|
protected _parseRequestCollection(executionContext: ExecutionContext, resource: Collection, urlPath: OpraURLPath, searchParams: URLSearchParams): Promise<RequestHost>;
|
|
28
28
|
protected _parseRequestSingleton(executionContext: ExecutionContext, resource: Singleton, urlPath: OpraURLPath, searchParams?: URLSearchParams): Promise<RequestHost>;
|
|
29
|
-
protected _parseRequestStorage(executionContext: ExecutionContext, resource: Storage,
|
|
30
|
-
protected parseParameters(paramDefs: Map<string, Parameter>,
|
|
29
|
+
protected _parseRequestStorage(executionContext: ExecutionContext, resource: Storage, searchParams: URLSearchParams): Promise<RequestHost>;
|
|
30
|
+
protected parseParameters(paramDefs: Map<string, Parameter>, searchParams?: URLSearchParams): Record<string, any>;
|
|
31
31
|
protected executeRequest(context: RequestContext): Promise<void>;
|
|
32
32
|
sendResponse(context: RequestContext): Promise<void>;
|
|
33
|
-
protected
|
|
33
|
+
protected sendErrorResponse(context: ExecutionContext, errors: any[]): Promise<void>;
|
|
34
34
|
}
|
package/types/response.d.ts
CHANGED