@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.
@@ -1,7 +1,8 @@
1
1
  import fs from 'fs/promises';
2
2
  import os from 'os';
3
- import * as valgen from 'valgen';
4
- import { BadRequestError, Collection, Container, HttpHeaderCodes, HttpStatusCodes, InternalServerError, isReadable, IssueSeverity, MethodNotAllowedError, OpraException, OpraSchema, OpraURL, ResourceNotFoundError, Singleton, Storage, translate, uid, wrapException } from '@opra/common';
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
- try {
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-')));
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
- 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;
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
- 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
- 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
- const request = await this.parseRequest(executionContext);
87
- const { outgoing } = executionContext.switchToHttp();
88
- const response = new ResponseHost({ http: outgoing });
89
- const context = RequestContext.from(executionContext, request, response);
90
- await this.executeRequest(context);
91
- if (response.errors.length) {
92
- context.errors.push(...response.errors);
93
- 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);
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 valgen.ValidationError) {
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, urlPath.slice(1), searchParams);
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.getOperationHandler(resource, p.resource);
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, p, searchParams);
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.decode(data, { coerce: true });
194
- const params = this.parseParameters(endpoint.parameters, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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.decode(data, { coerce: true });
288
- const params = this.parseParameters(endpoint.parameters, p, searchParams);
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.decode(data, { coerce: true });
310
- const params = this.parseParameters(endpoint.parameters, p, searchParams);
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.decode(data, { coerce: true });
342
- const params = this.parseParameters(endpoint.parameters, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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.decode(data, { coerce: true });
395
- const params = this.parseParameters(endpoint.parameters, p, searchParams);
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, urlPath, searchParams) {
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, p, searchParams);
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, p, searchParams);
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, p, searchParams);
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, pathComponent, searchParams) {
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
- if (request.resource instanceof Collection || request.resource instanceof Singleton) {
529
- const { operation } = request;
530
- if (operation === 'delete' || operation === 'deleteMany' || operation === 'updateMany') {
531
- let affected = 0;
532
- if (typeof value === 'number')
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
- else {
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 && (request.operation === 'get' || request.operation === 'update'))
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 (request.operation === 'findMany')
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 (request.resource instanceof Storage) {
562
- outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
563
- if (response.value != null) {
564
- if (typeof response.value === 'string') {
565
- if (!outgoing.hasHeader('content-type'))
566
- outgoing.setHeader('content-type', 'text/plain');
567
- outgoing.send(response.value);
568
- }
569
- else if (Buffer.isBuffer(response.value) || isReadable(response.value)) {
570
- if (!outgoing.hasHeader('content-type'))
571
- outgoing.setHeader('content-type', 'application/octet-stream');
572
- outgoing.send(response.value);
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
- else {
575
- outgoing.setHeader('content-type', 'application/json; charset=utf-8');
576
- outgoing.send(JSON.stringify(response.value));
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
- const responseObject = {
583
- context: request.resource.getFullPath()
584
- };
585
- if (request.operation === 'action')
586
- responseObject.action = request.action;
587
- else
588
- responseObject.operation = request.operation;
589
- const returnType = request.endpoint.returnType;
590
- let responseValue = response.value;
591
- if (returnType) {
592
- responseObject.type = returnType.name || '#anonymous';
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
- if (!responseValue)
607
- throw new InternalServerError(`"${request.operation}" endpoint should return value`);
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 handleError(context) {
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');