@opra/core 0.25.5 → 0.26.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.
Files changed (63) hide show
  1. package/cjs/augmentation/container.augmentation.js +2 -0
  2. package/cjs/http/adapters/express-adapter.host.js +34 -0
  3. package/cjs/http/{express-adapter.js → adapters/express-adapter.js} +1 -3
  4. package/cjs/http/{http-adapter.host.js → adapters/node-http-adapter.host.js} +30 -22
  5. package/cjs/http/adapters/node-http-adapter.js +14 -0
  6. package/cjs/http/helpers/json-body-loader.js +29 -0
  7. package/cjs/http/http-adapter-host.js +678 -0
  8. package/cjs/index.js +4 -3
  9. package/cjs/platform-adapter.host.js +74 -45
  10. package/cjs/{endpoint-context.js → request-context.js} +5 -5
  11. package/cjs/request.host.js +3 -0
  12. package/esm/augmentation/container.augmentation.js +1 -0
  13. package/esm/http/adapters/express-adapter.host.js +30 -0
  14. package/esm/http/{express-adapter.js → adapters/express-adapter.js} +1 -3
  15. package/esm/http/{http-adapter.host.js → adapters/node-http-adapter.host.js} +28 -20
  16. package/esm/http/adapters/node-http-adapter.js +11 -0
  17. package/esm/http/helpers/json-body-loader.js +24 -0
  18. package/esm/http/http-adapter-host.js +673 -0
  19. package/esm/index.js +4 -3
  20. package/esm/platform-adapter.host.js +75 -46
  21. package/esm/{endpoint-context.js → request-context.js} +4 -4
  22. package/esm/request.host.js +3 -0
  23. package/i18n/en/error.json +1 -2
  24. package/package.json +3 -3
  25. package/types/augmentation/collection.augmentation.d.ts +19 -16
  26. package/types/augmentation/container.augmentation.d.ts +13 -0
  27. package/types/augmentation/resource.augmentation.d.ts +2 -2
  28. package/types/augmentation/singleton.augmentation.d.ts +13 -9
  29. package/types/augmentation/storage.augmentation.d.ts +11 -14
  30. package/types/http/{express-adapter.d.ts → adapters/express-adapter.d.ts} +3 -3
  31. package/types/http/adapters/express-adapter.host.d.ts +12 -0
  32. package/types/http/{http-adapter.d.ts → adapters/node-http-adapter.d.ts} +5 -5
  33. package/types/http/adapters/node-http-adapter.host.d.ts +19 -0
  34. package/types/http/helpers/json-body-loader.d.ts +5 -0
  35. package/types/http/http-adapter-host.d.ts +34 -0
  36. package/types/index.d.ts +4 -3
  37. package/types/interfaces/request-handler.interface.d.ts +1 -1
  38. package/types/platform-adapter.d.ts +2 -2
  39. package/types/platform-adapter.host.d.ts +18 -14
  40. package/types/{endpoint-context.d.ts → request-context.d.ts} +3 -3
  41. package/types/request.d.ts +7 -2
  42. package/types/request.host.d.ts +5 -2
  43. package/cjs/http/express-adapter.host.js +0 -24
  44. package/cjs/http/http-adapter-base.js +0 -138
  45. package/cjs/http/http-adapter.js +0 -16
  46. package/cjs/http/request-handlers/entity-request-handler.js +0 -429
  47. package/cjs/http/request-handlers/parse-batch-request.js +0 -169
  48. package/cjs/http/request-handlers/request-handler-base.js +0 -37
  49. package/cjs/http/request-handlers/storage-request-handler.js +0 -139
  50. package/esm/http/express-adapter.host.js +0 -20
  51. package/esm/http/http-adapter-base.js +0 -134
  52. package/esm/http/http-adapter.js +0 -13
  53. package/esm/http/request-handlers/entity-request-handler.js +0 -424
  54. package/esm/http/request-handlers/parse-batch-request.js +0 -169
  55. package/esm/http/request-handlers/request-handler-base.js +0 -33
  56. package/esm/http/request-handlers/storage-request-handler.js +0 -134
  57. package/types/http/express-adapter.host.d.ts +0 -11
  58. package/types/http/http-adapter-base.d.ts +0 -23
  59. package/types/http/http-adapter.host.d.ts +0 -18
  60. package/types/http/request-handlers/entity-request-handler.d.ts +0 -24
  61. package/types/http/request-handlers/parse-batch-request.d.ts +0 -0
  62. package/types/http/request-handlers/request-handler-base.d.ts +0 -16
  63. package/types/http/request-handlers/storage-request-handler.d.ts +0 -23
@@ -0,0 +1,673 @@
1
+ import fs from 'fs/promises';
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';
5
+ import { ExecutionContextHost } from '../execution-context.host.js';
6
+ import { PlatformAdapterHost } from '../platform-adapter.host.js';
7
+ import { RequestHost } from '../request.host.js';
8
+ import { RequestContext } from '../request-context.js';
9
+ import { ResponseHost } from '../response.host.js';
10
+ import { jsonBodyLoader } from './helpers/json-body-loader.js';
11
+ import { MultipartIterator } from './helpers/multipart-helper.js';
12
+ /**
13
+ *
14
+ * @class HttpAdapterHost
15
+ */
16
+ export class HttpAdapterHost extends PlatformAdapterHost {
17
+ constructor() {
18
+ super(...arguments);
19
+ this._protocol = 'http';
20
+ this._tempDir = os.tmpdir();
21
+ }
22
+ /**
23
+ * Main http request handler
24
+ * @param incoming
25
+ * @param outgoing
26
+ * @protected
27
+ */
28
+ async handleHttp(incoming, outgoing) {
29
+ const context = new ExecutionContextHost(this.api, this.platform, { http: { incoming, outgoing } });
30
+ 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-')));
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;
48
+ }
49
+ // Process Batch
50
+ if (incoming.method === 'POST' && incoming.headers['content-type'] === 'multipart/mixed') {
51
+ // todo Process Batch
52
+ }
53
+ throw new BadRequestError();
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
+ 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);
78
+ }
79
+ }
80
+ finally {
81
+ await context.emitAsync('finish');
82
+ }
83
+ }
84
+ async handleExecution(executionContext) {
85
+ // 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;
94
+ }
95
+ try {
96
+ await this.sendResponse(context);
97
+ }
98
+ catch (e) {
99
+ if (e instanceof OpraException)
100
+ throw e;
101
+ if (e instanceof valgen.ValidationError) {
102
+ throw new InternalServerError({
103
+ message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
104
+ code: 'RESPONSE_VALIDATION',
105
+ details: e.issues
106
+ }, e);
107
+ }
108
+ throw new InternalServerError(e);
109
+ }
110
+ }
111
+ async parseRequest(executionContext) {
112
+ const { incoming } = executionContext.switchToHttp();
113
+ const parsedUrl = new OpraURL(incoming.url);
114
+ let i = 0;
115
+ let p;
116
+ let resource = this.api.root;
117
+ let request;
118
+ // Walk through container
119
+ while (resource instanceof Container) {
120
+ p = parsedUrl.path[i];
121
+ const r = resource.resources.get(p.resource);
122
+ if (r) {
123
+ resource = r;
124
+ if (resource instanceof Container) {
125
+ i++;
126
+ }
127
+ else
128
+ break;
129
+ }
130
+ else
131
+ break;
132
+ }
133
+ const urlPath = i > 0 ? parsedUrl.path.slice(i) : parsedUrl.path;
134
+ const searchParams = parsedUrl.searchParams;
135
+ // If there is one more element in the path it may be an action
136
+ if (resource instanceof Container) {
137
+ if (urlPath.length === 1 && resource.actions.has(urlPath[0].resource)) {
138
+ request = await this._parseRequestAction(executionContext, resource, urlPath, searchParams);
139
+ if (request)
140
+ return request;
141
+ }
142
+ }
143
+ else if (urlPath.length === 2 && resource.actions.has(urlPath[1].resource)) {
144
+ request = await this._parseRequestAction(executionContext, resource, urlPath.slice(1), searchParams);
145
+ if (request)
146
+ return request;
147
+ }
148
+ if (resource instanceof Storage)
149
+ request = await this._parseRequestStorage(executionContext, resource, urlPath.slice(1), searchParams);
150
+ else if (urlPath.length === 1) { // Collection and Singleton resources should be last element in path
151
+ if (resource instanceof Collection)
152
+ request = await this._parseRequestCollection(executionContext, resource, urlPath, searchParams);
153
+ else if (resource instanceof Singleton)
154
+ request = await this._parseRequestSingleton(executionContext, resource, urlPath, searchParams);
155
+ }
156
+ if (request)
157
+ return request;
158
+ const path = urlPath.toString();
159
+ throw new BadRequestError({
160
+ message: 'No resource or endpoint found at ' + path,
161
+ details: { path }
162
+ });
163
+ }
164
+ async _parseRequestAction(executionContext, resource, urlPath, searchParams) {
165
+ const p = urlPath[0];
166
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, p.resource);
167
+ const { incoming } = executionContext.switchToHttp();
168
+ const contentId = incoming.headers['content-id'];
169
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
170
+ return new RequestHost({
171
+ endpoint,
172
+ operation: 'action',
173
+ action: p.resource,
174
+ controller,
175
+ handler,
176
+ http: incoming,
177
+ contentId,
178
+ params
179
+ });
180
+ }
181
+ async _parseRequestCollection(executionContext, resource, urlPath, searchParams) {
182
+ const { incoming } = executionContext.switchToHttp();
183
+ if ((incoming.method === 'POST' || incoming.method === 'PATCH') && !incoming.is('json'))
184
+ throw new BadRequestError({ message: 'Unsupported Content-Type' });
185
+ const contentId = incoming.headers['content-id'];
186
+ const p = urlPath[0];
187
+ switch (incoming.method) {
188
+ case 'POST': {
189
+ if (p.key == null) {
190
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'create');
191
+ const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
192
+ let data = await jsonReader(incoming);
193
+ data = endpoint.decode(data, { coerce: true });
194
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
195
+ return new RequestHost({
196
+ endpoint,
197
+ operation: 'create',
198
+ controller,
199
+ handler,
200
+ http: incoming,
201
+ contentId,
202
+ data,
203
+ params: {
204
+ ...params,
205
+ pick: params.pick && resource.normalizeFieldPath(params.pick),
206
+ omit: params.omit && resource.normalizeFieldPath(params.omit),
207
+ include: params.include && resource.normalizeFieldPath(params.include)
208
+ }
209
+ });
210
+ }
211
+ break;
212
+ }
213
+ case 'DELETE': {
214
+ if (p.key != null) {
215
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
216
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
217
+ return new RequestHost({
218
+ endpoint,
219
+ operation: 'delete',
220
+ controller,
221
+ handler,
222
+ http: incoming,
223
+ contentId,
224
+ key: resource.parseKeyValue(p.key),
225
+ params
226
+ });
227
+ }
228
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'deleteMany');
229
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
230
+ return new RequestHost({
231
+ endpoint,
232
+ operation: 'deleteMany',
233
+ controller,
234
+ handler,
235
+ http: incoming,
236
+ contentId,
237
+ params: {
238
+ ...params,
239
+ filter: params.filter && resource.normalizeFilter(params.filter)
240
+ }
241
+ });
242
+ }
243
+ case 'GET': {
244
+ if (p.key != null) {
245
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
246
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
247
+ return new RequestHost({
248
+ endpoint,
249
+ operation: 'get',
250
+ controller,
251
+ handler,
252
+ http: incoming,
253
+ contentId,
254
+ key: resource.parseKeyValue(p.key),
255
+ params: {
256
+ ...params,
257
+ pick: params.pick && resource.normalizeFieldPath(params.pick),
258
+ omit: params.omit && resource.normalizeFieldPath(params.omit),
259
+ include: params.include && resource.normalizeFieldPath(params.include)
260
+ }
261
+ });
262
+ }
263
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'findMany');
264
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
265
+ return new RequestHost({
266
+ endpoint,
267
+ operation: 'findMany',
268
+ controller,
269
+ handler,
270
+ http: incoming,
271
+ contentId,
272
+ params: {
273
+ ...params,
274
+ pick: params.pick && resource.normalizeFieldPath(params.pick),
275
+ omit: params.omit && resource.normalizeFieldPath(params.omit),
276
+ include: params.include && resource.normalizeFieldPath(params.include),
277
+ sort: params.sort && resource.normalizeSortFields(params.sort),
278
+ filter: params.filter && resource.normalizeFilter(params.filter)
279
+ }
280
+ });
281
+ }
282
+ case 'PATCH': {
283
+ if (p.key != null) {
284
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'update');
285
+ const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
286
+ let data = await jsonReader(incoming);
287
+ data = endpoint.decode(data, { coerce: true });
288
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
289
+ return new RequestHost({
290
+ endpoint,
291
+ operation: 'update',
292
+ controller,
293
+ handler,
294
+ http: incoming,
295
+ contentId,
296
+ key: resource.parseKeyValue(p.key),
297
+ data,
298
+ params: {
299
+ ...params,
300
+ pick: params.pick && resource.normalizeFieldPath(params.pick),
301
+ omit: params.omit && resource.normalizeFieldPath(params.omit),
302
+ include: params.include && resource.normalizeFieldPath(params.include),
303
+ }
304
+ });
305
+ }
306
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'updateMany');
307
+ const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
308
+ let data = await jsonReader(incoming);
309
+ data = endpoint.decode(data, { coerce: true });
310
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
311
+ return new RequestHost({
312
+ endpoint,
313
+ operation: 'updateMany',
314
+ controller,
315
+ handler,
316
+ http: incoming,
317
+ contentId,
318
+ data,
319
+ params: {
320
+ ...params,
321
+ filter: params.filter && resource.normalizeFilter(params.filter)
322
+ }
323
+ });
324
+ }
325
+ }
326
+ throw new MethodNotAllowedError({
327
+ message: `Collection resource doesn't accept http "${incoming.method}" method`
328
+ });
329
+ }
330
+ async _parseRequestSingleton(executionContext, resource, urlPath, searchParams) {
331
+ const { incoming } = executionContext.switchToHttp();
332
+ if ((incoming.method === 'POST' || incoming.method === 'PATCH') && !incoming.is('json'))
333
+ throw new BadRequestError({ message: 'Unsupported Content-Type' });
334
+ const contentId = incoming.headers['content-id'];
335
+ const p = urlPath[0];
336
+ switch (incoming.method) {
337
+ case 'POST': {
338
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'create');
339
+ const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
340
+ let data = await jsonReader(incoming);
341
+ data = endpoint.decode(data, { coerce: true });
342
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
343
+ return new RequestHost({
344
+ endpoint,
345
+ operation: 'create',
346
+ controller,
347
+ handler,
348
+ http: incoming,
349
+ contentId,
350
+ data,
351
+ params: {
352
+ ...params,
353
+ pick: params.pick && resource.normalizeFieldPath(params.pick),
354
+ omit: params.omit && resource.normalizeFieldPath(params.omit),
355
+ include: params.include && resource.normalizeFieldPath(params.include)
356
+ }
357
+ });
358
+ }
359
+ case 'DELETE': {
360
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
361
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
362
+ return new RequestHost({
363
+ endpoint,
364
+ operation: 'delete',
365
+ controller,
366
+ handler,
367
+ http: incoming,
368
+ contentId,
369
+ params
370
+ });
371
+ }
372
+ case 'GET': {
373
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
374
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
375
+ return new RequestHost({
376
+ endpoint,
377
+ operation: 'get',
378
+ controller,
379
+ handler,
380
+ http: incoming,
381
+ contentId,
382
+ params: {
383
+ ...params,
384
+ pick: params.pick && resource.normalizeFieldPath(params.pick),
385
+ omit: params.omit && resource.normalizeFieldPath(params.omit),
386
+ include: params.include && resource.normalizeFieldPath(params.include)
387
+ }
388
+ });
389
+ }
390
+ case 'PATCH': {
391
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'update');
392
+ const jsonReader = jsonBodyLoader({ limit: endpoint.inputMaxContentSize }, endpoint);
393
+ let data = await jsonReader(incoming);
394
+ data = endpoint.decode(data, { coerce: true });
395
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
396
+ return new RequestHost({
397
+ endpoint,
398
+ operation: 'update',
399
+ controller,
400
+ handler,
401
+ http: incoming,
402
+ contentId,
403
+ data,
404
+ params: {
405
+ ...params,
406
+ pick: params.pick && resource.normalizeFieldPath(params.pick),
407
+ omit: params.omit && resource.normalizeFieldPath(params.omit),
408
+ include: params.include && resource.normalizeFieldPath(params.include),
409
+ }
410
+ });
411
+ }
412
+ }
413
+ throw new MethodNotAllowedError({
414
+ message: `Singleton resource doesn't accept http "${incoming.method}" method`
415
+ });
416
+ }
417
+ async _parseRequestStorage(executionContext, resource, urlPath, searchParams) {
418
+ const { incoming } = executionContext.switchToHttp();
419
+ const contentId = incoming.headers['content-id'];
420
+ const p = urlPath[0];
421
+ switch (incoming.method) {
422
+ case 'GET': {
423
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
424
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
425
+ return new RequestHost({
426
+ endpoint,
427
+ operation: 'get',
428
+ controller,
429
+ handler,
430
+ http: incoming,
431
+ contentId,
432
+ path: incoming.parsedUrl.path.slice(1).toString().substring(1),
433
+ params
434
+ });
435
+ }
436
+ case 'DELETE': {
437
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
438
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
439
+ return new RequestHost({
440
+ endpoint,
441
+ operation: 'delete',
442
+ controller,
443
+ handler,
444
+ http: incoming,
445
+ contentId,
446
+ path: incoming.parsedUrl.path.slice(1).toString().substring(1),
447
+ params
448
+ });
449
+ }
450
+ case 'POST': {
451
+ const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'post');
452
+ const params = this.parseParameters(endpoint.parameters, p, searchParams);
453
+ await fs.mkdir(this._tempDir, { recursive: true });
454
+ const multipartIterator = new MultipartIterator(incoming, {
455
+ ...endpoint.options,
456
+ filename: () => this.serviceName + '_p' + process.pid +
457
+ 't' + String(Date.now()).substring(8) + 'r' + uid(12)
458
+ });
459
+ multipartIterator.pause();
460
+ // Add an hook to clean up files after request finished
461
+ executionContext.on('finish', async () => {
462
+ multipartIterator.cancel();
463
+ await multipartIterator.deleteFiles().catch(() => void 0);
464
+ });
465
+ return new RequestHost({
466
+ endpoint,
467
+ operation: 'post',
468
+ controller,
469
+ handler,
470
+ http: incoming,
471
+ contentId,
472
+ parts: multipartIterator,
473
+ path: incoming.parsedUrl.path.slice(1).toString().substring(1),
474
+ params
475
+ });
476
+ }
477
+ }
478
+ throw new MethodNotAllowedError({
479
+ message: `Storage resource doesn't accept http "${incoming.method}" method`
480
+ });
481
+ }
482
+ parseParameters(paramDefs, pathComponent, searchParams) {
483
+ const out = {};
484
+ // Parse known parameters
485
+ for (const [k, prm] of paramDefs.entries()) {
486
+ const decode = prm.getDecoder();
487
+ let v = searchParams?.getAll(k);
488
+ try {
489
+ if (!prm.isArray) {
490
+ v = v[0];
491
+ v = decode(v, { coerce: true });
492
+ }
493
+ else {
494
+ v = v.map(x => decode(x, { coerce: true })).flat();
495
+ if (!v.length)
496
+ v = undefined;
497
+ }
498
+ if (v !== undefined)
499
+ out[k] = v;
500
+ }
501
+ catch (e) {
502
+ e.message = `Error parsing parameter ${k}. ` + e.message;
503
+ throw e;
504
+ }
505
+ }
506
+ // Add unknown parameters
507
+ if (searchParams) {
508
+ for (const k of searchParams.keys()) {
509
+ let v = searchParams.getAll(k);
510
+ if (v.length < 2)
511
+ v = v[0];
512
+ if (!paramDefs.has(k))
513
+ out[k] = v;
514
+ }
515
+ }
516
+ return out;
517
+ }
518
+ async executeRequest(context) {
519
+ const { request } = context;
520
+ const { response } = context;
521
+ const { resource, handler } = request;
522
+ // Call endpoint handler method
523
+ let value;
524
+ try {
525
+ value = await handler.call(request.controller, context);
526
+ if (response.value == null)
527
+ 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;
540
+ }
541
+ else {
542
+ // "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'))
544
+ throw new ResourceNotFoundError(resource.name, request.key);
545
+ // "findMany" endpoint should return array of entity instances
546
+ if (request.operation === 'findMany')
547
+ value = value == null ? [] : Array.isArray(value) ? value : [value];
548
+ else
549
+ value = value == null ? {} : Array.isArray(value) ? value[0] : value;
550
+ response.value = value;
551
+ }
552
+ }
553
+ }
554
+ catch (error) {
555
+ response.errors.push(error);
556
+ }
557
+ }
558
+ async sendResponse(context) {
559
+ const { request, response } = context;
560
+ 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);
573
+ }
574
+ else {
575
+ outgoing.setHeader('content-type', 'application/json; charset=utf-8');
576
+ outgoing.send(JSON.stringify(response.value));
577
+ }
578
+ }
579
+ outgoing.end();
580
+ return;
581
+ }
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;
604
+ }
605
+ 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;
616
+ }
617
+ outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
618
+ }
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
+ outgoing.end();
623
+ }
624
+ async handleError(context) {
625
+ const { errors } = context;
626
+ const { outgoing } = context.switchToHttp();
627
+ if (outgoing.headersSent) {
628
+ outgoing.end();
629
+ return;
630
+ }
631
+ errors.forEach(x => {
632
+ if (x instanceof OpraException) {
633
+ switch (x.severity) {
634
+ case "fatal":
635
+ this._logger.fatal(x);
636
+ break;
637
+ case "warning":
638
+ this._logger.warn(x);
639
+ break;
640
+ default:
641
+ this._logger.error(x);
642
+ }
643
+ }
644
+ else
645
+ this._logger.fatal(x);
646
+ });
647
+ const wrappedErrors = errors.map(wrapException);
648
+ // Sort errors from fatal to info
649
+ wrappedErrors.sort((a, b) => {
650
+ const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
651
+ if (i === 0)
652
+ return b.status - a.status;
653
+ return i;
654
+ });
655
+ let status = outgoing.statusCode || 0;
656
+ if (!status || status < Number(HttpStatusCodes.BAD_REQUEST)) {
657
+ status = wrappedErrors[0].status;
658
+ if (status < Number(HttpStatusCodes.BAD_REQUEST))
659
+ status = HttpStatusCodes.INTERNAL_SERVER_ERROR;
660
+ }
661
+ outgoing.statusCode = status;
662
+ const body = {
663
+ errors: wrappedErrors.map(x => this._i18n.deep(x.toJSON()))
664
+ };
665
+ outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
666
+ outgoing.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
667
+ outgoing.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
668
+ outgoing.setHeader(HttpHeaderCodes.Expires, '-1');
669
+ outgoing.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
670
+ outgoing.send(JSON.stringify(body));
671
+ outgoing.end();
672
+ }
673
+ }