@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.
@@ -7,7 +7,6 @@ class ExecutionContextHost extends strict_typed_events_1.AsyncEventEmitter {
7
7
  super();
8
8
  this.api = api;
9
9
  this.platform = platform;
10
- this.errors = [];
11
10
  this.ws = protocol.ws;
12
11
  this.rpc = protocol.rpc;
13
12
  if (protocol.http) {
@@ -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
- try {
37
- /* istanbul ignore next */
38
- if (!this._api)
39
- throw new common_1.InternalServerError(`${Object.getPrototypeOf(this).constructor.name} has not been initialized yet`);
40
- outgoing.setHeader(common_1.HttpHeaderCodes.X_Opra_Version, common_1.OpraSchema.SpecVersion);
41
- // Expose headers if cors enabled
42
- if (outgoing.getHeader(common_1.HttpHeaderCodes.Access_Control_Allow_Origin)) {
43
- // Expose X-Opra-* headers
44
- outgoing.appendHeader(common_1.HttpHeaderCodes.Access_Control_Expose_Headers, Object.values(common_1.HttpHeaderCodes)
45
- .filter(k => k.toLowerCase().startsWith('x-opra-')));
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
- const { parsedUrl } = incoming;
48
- if (!parsedUrl.path.length) {
49
- if (incoming.method === 'GET') {
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
- let i = 0;
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
- const request = await this.parseRequest(executionContext);
92
- const { outgoing } = executionContext.switchToHttp();
93
- const response = new response_host_js_1.ResponseHost({ http: outgoing });
94
- const context = request_context_js_1.RequestContext.from(executionContext, request, response);
95
- await this.executeRequest(context);
96
- if (response.errors.length) {
97
- context.errors.push(...response.errors);
98
- return;
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, urlPath.slice(1), searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, urlPath, searchParams) {
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, pathComponent, searchParams) {
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 (request.endpoint.kind === 'operation' &&
519
- (resource instanceof common_1.Collection || resource instanceof common_1.Singleton || resource instanceof common_1.Storage)) {
520
- const { endpoint } = request;
521
- const operationName = endpoint.name;
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
- // "get" and "update" endpoints must return the entity instance, otherwise it means resource not found
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 (endpoint.kind === 'operation' &&
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
- const returnType = endpoint.returnType;
564
- const operationName = endpoint.name;
565
- const body = {
566
- context: resource.getFullPath(),
567
- operation: operationName
568
- };
569
- if (operationName === 'delete' || operationName === 'deleteMany' || operationName === 'updateMany') {
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
- if (!response.value)
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 === 'create' || operationName === 'update')
581
- body.affected = 1;
582
- else if (operationName === 'get' || operationName === 'update')
583
- body.key = request.key;
584
- if (operationName === 'findMany' && response.count != null && response.count >= 0)
585
- body.totalCount = response.count;
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 (!outgoing.hasHeader('content-type'))
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 (!outgoing.hasHeader('content-type'))
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 handleError(context) {
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');
@@ -6,7 +6,7 @@ var RequestContext;
6
6
  function from(executionContext, request, response) {
7
7
  const out = {
8
8
  request,
9
- response,
9
+ response
10
10
  };
11
11
  Object.setPrototypeOf(out, executionContext);
12
12
  return out;
@@ -4,7 +4,6 @@ export class ExecutionContextHost extends AsyncEventEmitter {
4
4
  super();
5
5
  this.api = api;
6
6
  this.platform = platform;
7
- this.errors = [];
8
7
  this.ws = protocol.ws;
9
8
  this.rpc = protocol.rpc;
10
9
  if (protocol.http) {
@@ -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, ComplexType, Container, HttpHeaderCodes, HttpStatusCodes, InternalServerError, isReadable, IssueSeverity, MethodNotAllowedError, OpraException, OpraSchema, OpraURL, ResourceNotFoundError, Singleton, Storage, translate, uid, wrapException } from '@opra/common';
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
- try {
33
- /* istanbul ignore next */
34
- if (!this._api)
35
- throw new InternalServerError(`${Object.getPrototypeOf(this).constructor.name} has not been initialized yet`);
36
- outgoing.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
37
- // Expose headers if cors enabled
38
- if (outgoing.getHeader(HttpHeaderCodes.Access_Control_Allow_Origin)) {
39
- // Expose X-Opra-* headers
40
- outgoing.appendHeader(HttpHeaderCodes.Access_Control_Expose_Headers, Object.values(HttpHeaderCodes)
41
- .filter(k => k.toLowerCase().startsWith('x-opra-')));
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
- const { parsedUrl } = incoming;
44
- if (!parsedUrl.path.length) {
45
- if (incoming.method === 'GET') {
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
- let i = 0;
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
- const request = await this.parseRequest(executionContext);
88
- const { outgoing } = executionContext.switchToHttp();
89
- const response = new ResponseHost({ http: outgoing });
90
- const context = RequestContext.from(executionContext, request, response);
91
- await this.executeRequest(context);
92
- if (response.errors.length) {
93
- context.errors.push(...response.errors);
94
- return;
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, urlPath.slice(1), searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, urlPath, searchParams) {
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, pathComponent, searchParams) {
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 (request.endpoint.kind === 'operation' &&
515
- (resource instanceof Collection || resource instanceof Singleton || resource instanceof Storage)) {
516
- const { endpoint } = request;
517
- const operationName = endpoint.name;
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
- // "get" and "update" endpoints must return the entity instance, otherwise it means resource not found
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 (endpoint.kind === 'operation' &&
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
- const returnType = endpoint.returnType;
560
- const operationName = endpoint.name;
561
- const body = {
562
- context: resource.getFullPath(),
563
- operation: operationName
564
- };
565
- if (operationName === 'delete' || operationName === 'deleteMany' || operationName === 'updateMany') {
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
- if (!response.value)
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 === 'create' || operationName === 'update')
577
- body.affected = 1;
578
- else if (operationName === 'get' || operationName === 'update')
579
- body.key = request.key;
580
- if (operationName === 'findMany' && response.count != null && response.count >= 0)
581
- body.totalCount = response.count;
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 (!outgoing.hasHeader('content-type'))
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 (!outgoing.hasHeader('content-type'))
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 handleError(context) {
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');
@@ -3,7 +3,7 @@ export var RequestContext;
3
3
  function from(executionContext, request, response) {
4
4
  const out = {
5
5
  request,
6
- response,
6
+ response
7
7
  };
8
8
  Object.setPrototypeOf(out, executionContext);
9
9
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opra/core",
3
- "version": "0.26.5",
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.26.5",
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, OpraURLPathComponent, Parameter, Resource, Singleton, Storage } from '@opra/common';
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, urlPath: OpraURLPath, searchParams: URLSearchParams): Promise<RequestHost>;
30
- protected parseParameters(paramDefs: Map<string, Parameter>, pathComponent: OpraURLPathComponent, searchParams?: URLSearchParams): Record<string, any>;
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 handleError(context: ExecutionContext): Promise<void>;
33
+ protected sendErrorResponse(context: ExecutionContext, errors: any[]): Promise<void>;
34
34
  }
@@ -15,7 +15,7 @@ export interface Response {
15
15
  /**
16
16
  * Total count of matched entities. (Used in "search" endpoint with "count" option
17
17
  */
18
- count?: number;
18
+ totalMatches?: number;
19
19
  switchToHttp(): HttpServerResponse;
20
20
  switchToWs(): never;
21
21
  switchToRpc(): never;