@opra/core 0.26.4 → 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 +190 -169
- package/cjs/platform-adapter.host.js +20 -4
- package/cjs/request-context.js +1 -1
- package/esm/execution-context.host.js +0 -1
- package/esm/http/http-adapter-host.js +191 -170
- package/esm/platform-adapter.host.js +21 -5
- package/esm/request-context.js +1 -1
- package/i18n/en/error.json +2 -1
- package/package.json +4 -4
- package/types/augmentation/collection.augmentation.d.ts +32 -18
- package/types/augmentation/container.augmentation.d.ts +4 -3
- package/types/augmentation/singleton.augmentation.d.ts +20 -12
- package/types/augmentation/storage.augmentation.d.ts +16 -9
- 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/platform-adapter.host.d.ts +8 -3
- package/types/request.d.ts +2 -4
- package/types/request.host.d.ts +2 -0
- package/types/response.d.ts +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import os from 'os';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import typeIs from 'type-is';
|
|
4
|
+
import * as vg from 'valgen';
|
|
5
|
+
import { BadRequestError, Collection, Container, HttpHeaderCodes, HttpStatusCodes, InternalServerError, isReadable, IssueSeverity, MethodNotAllowedError, OperationResult, OpraException, OpraSchema, OpraURL, ResourceNotFoundError, Singleton, Storage, translate, uid, wrapException } from '@opra/common';
|
|
5
6
|
import { ExecutionContextHost } from '../execution-context.host.js';
|
|
6
7
|
import { PlatformAdapterHost } from '../platform-adapter.host.js';
|
|
7
8
|
import { RequestHost } from '../request.host.js';
|
|
@@ -28,54 +29,47 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
28
29
|
async handleHttp(incoming, outgoing) {
|
|
29
30
|
const context = new ExecutionContextHost(this.api, this.platform, { http: { incoming, outgoing } });
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
outgoing.setHeader('content-type', 'application/json');
|
|
46
|
-
outgoing.end(JSON.stringify(this.api.exportSchema({ webSafe: true })));
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
// Process Batch
|
|
50
|
-
if (incoming.method === 'POST' && incoming.headers['content-type'] === 'multipart/mixed') {
|
|
51
|
-
// todo Process Batch
|
|
52
|
-
}
|
|
53
|
-
throw new BadRequestError();
|
|
49
|
+
// Process Batch
|
|
50
|
+
if (incoming.method === 'POST' && incoming.headers['content-type'] === 'multipart/mixed') {
|
|
51
|
+
// todo Process Batch
|
|
54
52
|
}
|
|
55
|
-
|
|
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
|
-
context.errors.push(wrapException(error));
|
|
72
|
-
}
|
|
73
|
-
// If no response returned to the client we send an error
|
|
74
|
-
if (!outgoing.writableEnded) {
|
|
75
|
-
if (!context.errors.length)
|
|
76
|
-
context.errors.push(new BadRequestError(`Server can not process this request`));
|
|
77
|
-
await this.handleError(context);
|
|
53
|
+
throw new BadRequestError();
|
|
78
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]);
|
|
79
73
|
}
|
|
80
74
|
finally {
|
|
81
75
|
await context.emitAsync('finish');
|
|
@@ -83,22 +77,33 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
83
77
|
}
|
|
84
78
|
async handleExecution(executionContext) {
|
|
85
79
|
// Parse incoming message and create Request object
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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);
|
|
94
95
|
}
|
|
95
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);
|
|
96
101
|
await this.sendResponse(context);
|
|
97
102
|
}
|
|
98
103
|
catch (e) {
|
|
99
104
|
if (e instanceof OpraException)
|
|
100
105
|
throw e;
|
|
101
|
-
if (e instanceof
|
|
106
|
+
if (e instanceof vg.ValidationError) {
|
|
102
107
|
throw new InternalServerError({
|
|
103
108
|
message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
|
|
104
109
|
code: 'RESPONSE_VALIDATION',
|
|
@@ -146,7 +151,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
146
151
|
return request;
|
|
147
152
|
}
|
|
148
153
|
if (resource instanceof Storage)
|
|
149
|
-
request = await this._parseRequestStorage(executionContext, resource,
|
|
154
|
+
request = await this._parseRequestStorage(executionContext, resource, searchParams);
|
|
150
155
|
else if (urlPath.length === 1) { // Collection and Singleton resources should be last element in path
|
|
151
156
|
if (resource instanceof Collection)
|
|
152
157
|
request = await this._parseRequestCollection(executionContext, resource, urlPath, searchParams);
|
|
@@ -163,14 +168,12 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
163
168
|
}
|
|
164
169
|
async _parseRequestAction(executionContext, resource, urlPath, searchParams) {
|
|
165
170
|
const p = urlPath[0];
|
|
166
|
-
const { controller, endpoint, handler } = await this.
|
|
171
|
+
const { controller, endpoint, handler } = await this.getActionHandler(resource, p.resource);
|
|
167
172
|
const { incoming } = executionContext.switchToHttp();
|
|
168
173
|
const contentId = incoming.headers['content-id'];
|
|
169
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
174
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
170
175
|
return new RequestHost({
|
|
171
176
|
endpoint,
|
|
172
|
-
operation: 'action',
|
|
173
|
-
action: endpoint.name,
|
|
174
177
|
controller,
|
|
175
178
|
handler,
|
|
176
179
|
http: incoming,
|
|
@@ -190,11 +193,10 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
190
193
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'create');
|
|
191
194
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
192
195
|
let data = await jsonReader(incoming);
|
|
193
|
-
data = endpoint.
|
|
194
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
196
|
+
data = endpoint.decodeInput(data, { coerce: true });
|
|
197
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
195
198
|
return new RequestHost({
|
|
196
199
|
endpoint,
|
|
197
|
-
operation: 'create',
|
|
198
200
|
controller,
|
|
199
201
|
handler,
|
|
200
202
|
http: incoming,
|
|
@@ -213,10 +215,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
213
215
|
case 'DELETE': {
|
|
214
216
|
if (p.key != null) {
|
|
215
217
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
216
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
218
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
217
219
|
return new RequestHost({
|
|
218
220
|
endpoint,
|
|
219
|
-
operation: 'delete',
|
|
220
221
|
controller,
|
|
221
222
|
handler,
|
|
222
223
|
http: incoming,
|
|
@@ -226,10 +227,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
226
227
|
});
|
|
227
228
|
}
|
|
228
229
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'deleteMany');
|
|
229
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
230
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
230
231
|
return new RequestHost({
|
|
231
232
|
endpoint,
|
|
232
|
-
operation: 'deleteMany',
|
|
233
233
|
controller,
|
|
234
234
|
handler,
|
|
235
235
|
http: incoming,
|
|
@@ -243,10 +243,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
243
243
|
case 'GET': {
|
|
244
244
|
if (p.key != null) {
|
|
245
245
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
246
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
246
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
247
247
|
return new RequestHost({
|
|
248
248
|
endpoint,
|
|
249
|
-
operation: 'get',
|
|
250
249
|
controller,
|
|
251
250
|
handler,
|
|
252
251
|
http: incoming,
|
|
@@ -261,10 +260,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
261
260
|
});
|
|
262
261
|
}
|
|
263
262
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'findMany');
|
|
264
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
263
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
265
264
|
return new RequestHost({
|
|
266
265
|
endpoint,
|
|
267
|
-
operation: 'findMany',
|
|
268
266
|
controller,
|
|
269
267
|
handler,
|
|
270
268
|
http: incoming,
|
|
@@ -284,11 +282,10 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
284
282
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'update');
|
|
285
283
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
286
284
|
let data = await jsonReader(incoming);
|
|
287
|
-
data = endpoint.
|
|
288
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
285
|
+
data = endpoint.decodeInput(data, { coerce: true });
|
|
286
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
289
287
|
return new RequestHost({
|
|
290
288
|
endpoint,
|
|
291
|
-
operation: 'update',
|
|
292
289
|
controller,
|
|
293
290
|
handler,
|
|
294
291
|
http: incoming,
|
|
@@ -306,11 +303,10 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
306
303
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'updateMany');
|
|
307
304
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
308
305
|
let data = await jsonReader(incoming);
|
|
309
|
-
data = endpoint.
|
|
310
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
306
|
+
data = endpoint.decodeInput(data, { coerce: true });
|
|
307
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
311
308
|
return new RequestHost({
|
|
312
309
|
endpoint,
|
|
313
|
-
operation: 'updateMany',
|
|
314
310
|
controller,
|
|
315
311
|
handler,
|
|
316
312
|
http: incoming,
|
|
@@ -332,17 +328,15 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
332
328
|
if ((incoming.method === 'POST' || incoming.method === 'PATCH') && !incoming.is('json'))
|
|
333
329
|
throw new BadRequestError({ message: 'Unsupported Content-Type' });
|
|
334
330
|
const contentId = incoming.headers['content-id'];
|
|
335
|
-
const p = urlPath[0];
|
|
336
331
|
switch (incoming.method) {
|
|
337
332
|
case 'POST': {
|
|
338
333
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'create');
|
|
339
334
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
340
335
|
let data = await jsonReader(incoming);
|
|
341
|
-
data = endpoint.
|
|
342
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
336
|
+
data = endpoint.decodeInput(data, { coerce: true });
|
|
337
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
343
338
|
return new RequestHost({
|
|
344
339
|
endpoint,
|
|
345
|
-
operation: 'create',
|
|
346
340
|
controller,
|
|
347
341
|
handler,
|
|
348
342
|
http: incoming,
|
|
@@ -358,10 +352,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
358
352
|
}
|
|
359
353
|
case 'DELETE': {
|
|
360
354
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
361
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
355
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
362
356
|
return new RequestHost({
|
|
363
357
|
endpoint,
|
|
364
|
-
operation: 'delete',
|
|
365
358
|
controller,
|
|
366
359
|
handler,
|
|
367
360
|
http: incoming,
|
|
@@ -371,10 +364,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
371
364
|
}
|
|
372
365
|
case 'GET': {
|
|
373
366
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
374
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
367
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
375
368
|
return new RequestHost({
|
|
376
369
|
endpoint,
|
|
377
|
-
operation: 'get',
|
|
378
370
|
controller,
|
|
379
371
|
handler,
|
|
380
372
|
http: incoming,
|
|
@@ -391,11 +383,10 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
391
383
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'update');
|
|
392
384
|
const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
|
|
393
385
|
let data = await jsonReader(incoming);
|
|
394
|
-
data = endpoint.
|
|
395
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
386
|
+
data = endpoint.decodeInput(data, { coerce: true });
|
|
387
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
396
388
|
return new RequestHost({
|
|
397
389
|
endpoint,
|
|
398
|
-
operation: 'update',
|
|
399
390
|
controller,
|
|
400
391
|
handler,
|
|
401
392
|
http: incoming,
|
|
@@ -414,17 +405,15 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
414
405
|
message: `Singleton resource doesn't accept http "${incoming.method}" method`
|
|
415
406
|
});
|
|
416
407
|
}
|
|
417
|
-
async _parseRequestStorage(executionContext, resource,
|
|
408
|
+
async _parseRequestStorage(executionContext, resource, searchParams) {
|
|
418
409
|
const { incoming } = executionContext.switchToHttp();
|
|
419
410
|
const contentId = incoming.headers['content-id'];
|
|
420
|
-
const p = urlPath[0];
|
|
421
411
|
switch (incoming.method) {
|
|
422
412
|
case 'GET': {
|
|
423
413
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
|
|
424
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
414
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
425
415
|
return new RequestHost({
|
|
426
416
|
endpoint,
|
|
427
|
-
operation: 'get',
|
|
428
417
|
controller,
|
|
429
418
|
handler,
|
|
430
419
|
http: incoming,
|
|
@@ -435,10 +424,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
435
424
|
}
|
|
436
425
|
case 'DELETE': {
|
|
437
426
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
|
|
438
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
427
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
439
428
|
return new RequestHost({
|
|
440
429
|
endpoint,
|
|
441
|
-
operation: 'delete',
|
|
442
430
|
controller,
|
|
443
431
|
handler,
|
|
444
432
|
http: incoming,
|
|
@@ -449,7 +437,7 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
449
437
|
}
|
|
450
438
|
case 'POST': {
|
|
451
439
|
const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'post');
|
|
452
|
-
const params = this.parseParameters(endpoint.parameters,
|
|
440
|
+
const params = this.parseParameters(endpoint.parameters, searchParams);
|
|
453
441
|
await fs.mkdir(this._tempDir, { recursive: true });
|
|
454
442
|
const multipartIterator = new MultipartIterator(incoming, {
|
|
455
443
|
...endpoint.options,
|
|
@@ -464,7 +452,6 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
464
452
|
});
|
|
465
453
|
return new RequestHost({
|
|
466
454
|
endpoint,
|
|
467
|
-
operation: 'post',
|
|
468
455
|
controller,
|
|
469
456
|
handler,
|
|
470
457
|
http: incoming,
|
|
@@ -479,13 +466,15 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
479
466
|
message: `Storage resource doesn't accept http "${incoming.method}" method`
|
|
480
467
|
});
|
|
481
468
|
}
|
|
482
|
-
parseParameters(paramDefs,
|
|
469
|
+
parseParameters(paramDefs, searchParams) {
|
|
483
470
|
const out = {};
|
|
484
471
|
// Parse known parameters
|
|
485
472
|
for (const [k, prm] of paramDefs.entries()) {
|
|
486
473
|
const decode = prm.getDecoder();
|
|
487
474
|
let v = searchParams?.getAll(k);
|
|
488
475
|
try {
|
|
476
|
+
if (!v.length && prm.default != null)
|
|
477
|
+
v = [prm.default];
|
|
489
478
|
if (!prm.isArray) {
|
|
490
479
|
v = v[0];
|
|
491
480
|
v = decode(v, { coerce: true });
|
|
@@ -518,38 +507,51 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
518
507
|
async executeRequest(context) {
|
|
519
508
|
const { request } = context;
|
|
520
509
|
const { response } = context;
|
|
521
|
-
const { resource, handler } = request;
|
|
510
|
+
const { endpoint, resource, handler } = request;
|
|
522
511
|
// Call endpoint handler method
|
|
523
512
|
let value;
|
|
524
513
|
try {
|
|
525
514
|
value = await handler.call(request.controller, context);
|
|
526
515
|
if (response.value == null)
|
|
527
516
|
response.value = value;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
affected = value;
|
|
534
|
-
if (typeof value === 'boolean')
|
|
535
|
-
affected = value ? 1 : 0;
|
|
536
|
-
if (typeof value === 'object')
|
|
537
|
-
affected = value.affected || value.affectedRows ||
|
|
538
|
-
(operation === 'updateMany' ? value.updated : value.deleted);
|
|
539
|
-
response.value = affected;
|
|
517
|
+
// Normalize response value
|
|
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);
|
|
540
522
|
}
|
|
541
|
-
|
|
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;
|
|
542
540
|
// "get" and "update" endpoints must return the entity instance, otherwise it means resource not found
|
|
543
|
-
if (value == null && (
|
|
541
|
+
if (value == null && (operationName === 'get' || operationName === 'update'))
|
|
544
542
|
throw new ResourceNotFoundError(resource.name, request.key);
|
|
545
543
|
// "findMany" endpoint should return array of entity instances
|
|
546
|
-
if (
|
|
547
|
-
value = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
544
|
+
if (operationName === 'findMany')
|
|
545
|
+
value = (value == null ? [] : Array.isArray(value) ? value : [value]);
|
|
548
546
|
else
|
|
549
547
|
value = value == null ? {} : Array.isArray(value) ? value[0] : value;
|
|
548
|
+
value = endpoint.encodeReturning(value, { coerce: true });
|
|
550
549
|
response.value = value;
|
|
550
|
+
return;
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
|
+
if (response.value)
|
|
554
|
+
response.value = endpoint.encodeReturning(response.value, { coerce: true });
|
|
553
555
|
}
|
|
554
556
|
catch (error) {
|
|
555
557
|
response.errors.push(error);
|
|
@@ -557,77 +559,96 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
557
559
|
}
|
|
558
560
|
async sendResponse(context) {
|
|
559
561
|
const { request, response } = context;
|
|
562
|
+
const { endpoint, resource } = request;
|
|
560
563
|
const outgoing = response.switchToHttp();
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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' &&
|
|
579
|
+
(resource instanceof Collection || resource instanceof Singleton ||
|
|
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')
|
|
592
|
+
body.affected = response.value;
|
|
593
|
+
else {
|
|
594
|
+
outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
|
|
595
|
+
if (operationName === 'create')
|
|
596
|
+
outgoing.statusCode = 201;
|
|
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;
|
|
573
602
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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);
|
|
577
618
|
}
|
|
578
619
|
}
|
|
620
|
+
outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
|
|
621
|
+
outgoing.send(JSON.stringify(body));
|
|
579
622
|
outgoing.end();
|
|
580
623
|
return;
|
|
581
624
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (response.value != null)
|
|
594
|
-
responseValue = responseObject.data = request.endpoint.encode(response.value, { coerce: true });
|
|
595
|
-
}
|
|
596
|
-
if (request.operation === 'action') {
|
|
597
|
-
if (responseValue != null)
|
|
598
|
-
responseObject.data = responseValue;
|
|
599
|
-
}
|
|
600
|
-
else if (request.resource instanceof Collection || request.resource instanceof Singleton) {
|
|
601
|
-
if (request.operation === 'delete' || request.operation === 'deleteMany' ||
|
|
602
|
-
request.operation === 'updateMany') {
|
|
603
|
-
responseObject.affected = responseValue || 0;
|
|
625
|
+
outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
|
|
626
|
+
if (response.value != null) {
|
|
627
|
+
if (typeof response.value === 'string') {
|
|
628
|
+
if (!contentType)
|
|
629
|
+
outgoing.setHeader('content-type', 'text/plain');
|
|
630
|
+
outgoing.send(response.value);
|
|
631
|
+
}
|
|
632
|
+
else if (Buffer.isBuffer(response.value) || isReadable(response.value)) {
|
|
633
|
+
if (!contentType)
|
|
634
|
+
outgoing.setHeader('content-type', 'application/octet-stream');
|
|
635
|
+
outgoing.send(response.value);
|
|
604
636
|
}
|
|
605
637
|
else {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (request.operation === 'create')
|
|
609
|
-
outgoing.statusCode = 201;
|
|
610
|
-
if (request.operation === 'create' || request.operation === 'update')
|
|
611
|
-
responseObject.affected = 1;
|
|
612
|
-
else if (request.operation === 'get' || request.operation === 'update')
|
|
613
|
-
responseObject.key = request.key;
|
|
614
|
-
if (request.operation === 'findMany' && response.count != null && response.count >= 0)
|
|
615
|
-
responseObject.totalCount = response.count;
|
|
638
|
+
outgoing.setHeader('content-type', 'application/json; charset=utf-8');
|
|
639
|
+
outgoing.send(JSON.stringify(response.value));
|
|
616
640
|
}
|
|
617
|
-
outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
|
|
618
641
|
}
|
|
619
|
-
const body = this.i18n.deep(responseObject);
|
|
620
|
-
outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
|
|
621
|
-
outgoing.send(JSON.stringify(body));
|
|
622
642
|
outgoing.end();
|
|
623
643
|
}
|
|
624
|
-
async
|
|
625
|
-
const { errors } = context;
|
|
644
|
+
async sendErrorResponse(context, errors) {
|
|
626
645
|
const { outgoing } = context.switchToHttp();
|
|
627
646
|
if (outgoing.headersSent) {
|
|
628
647
|
outgoing.end();
|
|
629
648
|
return;
|
|
630
649
|
}
|
|
650
|
+
if (!errors.length)
|
|
651
|
+
errors.push(wrapException({ status: outgoing.statusCode || 500 }));
|
|
631
652
|
errors.forEach(x => {
|
|
632
653
|
if (x instanceof OpraException) {
|
|
633
654
|
switch (x.severity) {
|
|
@@ -659,9 +680,9 @@ export class HttpAdapterHost extends PlatformAdapterHost {
|
|
|
659
680
|
status = HttpStatusCodes.INTERNAL_SERVER_ERROR;
|
|
660
681
|
}
|
|
661
682
|
outgoing.statusCode = status;
|
|
662
|
-
const body = {
|
|
683
|
+
const body = new OperationResult({
|
|
663
684
|
errors: wrappedErrors.map(x => this._i18n.deep(x.toJSON()))
|
|
664
|
-
};
|
|
685
|
+
});
|
|
665
686
|
outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
|
|
666
687
|
outgoing.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
667
688
|
outgoing.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
|