@opra/core 1.0.0-alpha.3 → 1.0.0-alpha.31

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/18n.augmentation.js +1 -1
  2. package/cjs/constants.js +1 -2
  3. package/cjs/execution-context.js +1 -1
  4. package/cjs/http/express-adapter.js +25 -34
  5. package/cjs/http/http-adapter.js +2 -5
  6. package/cjs/http/http-context.js +20 -32
  7. package/cjs/http/{impl/http-handler.js → http-handler.js} +249 -213
  8. package/cjs/http/impl/http-incoming.host.js +3 -3
  9. package/cjs/http/impl/http-outgoing.host.js +2 -2
  10. package/cjs/http/impl/multipart-reader.js +141 -50
  11. package/cjs/http/impl/node-incoming-message.host.js +5 -3
  12. package/cjs/http/interfaces/node-incoming-message.interface.js +3 -2
  13. package/cjs/http/utils/body-reader.js +6 -5
  14. package/cjs/http/utils/common.js +6 -5
  15. package/cjs/http/utils/concat-readable.js +1 -2
  16. package/cjs/http/utils/convert-to-headers.js +2 -3
  17. package/cjs/http/utils/convert-to-raw-headers.js +1 -2
  18. package/cjs/http/utils/match-known-fields.js +2 -2
  19. package/cjs/http/utils/wrap-exception.js +1 -2
  20. package/cjs/index.js +4 -4
  21. package/cjs/platform-adapter.js +1 -4
  22. package/cjs/type-guards.js +4 -5
  23. package/esm/augmentation/18n.augmentation.js +1 -1
  24. package/esm/constants.js +0 -1
  25. package/esm/execution-context.js +1 -1
  26. package/esm/http/express-adapter.js +25 -34
  27. package/esm/http/http-adapter.js +2 -5
  28. package/esm/http/http-context.js +21 -33
  29. package/esm/http/{impl/http-handler.js → http-handler.js} +243 -207
  30. package/esm/http/impl/http-incoming.host.js +3 -3
  31. package/esm/http/impl/http-outgoing.host.js +2 -2
  32. package/esm/http/impl/multipart-reader.js +142 -51
  33. package/esm/http/impl/node-incoming-message.host.js +5 -3
  34. package/esm/http/interfaces/node-incoming-message.interface.js +3 -2
  35. package/esm/http/utils/body-reader.js +6 -5
  36. package/esm/http/utils/common.js +2 -1
  37. package/esm/index.js +4 -4
  38. package/esm/platform-adapter.js +1 -4
  39. package/package.json +21 -14
  40. package/types/augmentation/18n.augmentation.d.ts +1 -1
  41. package/types/constants.d.ts +0 -1
  42. package/types/execution-context.d.ts +2 -3
  43. package/types/http/express-adapter.d.ts +1 -1
  44. package/types/http/http-adapter.d.ts +35 -8
  45. package/types/http/http-context.d.ts +4 -4
  46. package/types/http/{impl/http-handler.d.ts → http-handler.d.ts} +11 -9
  47. package/types/http/impl/http-incoming.host.d.ts +1 -2
  48. package/types/http/impl/http-outgoing.host.d.ts +1 -1
  49. package/types/http/impl/multipart-reader.d.ts +38 -20
  50. package/types/http/impl/node-incoming-message.host.d.ts +2 -6
  51. package/types/http/impl/node-outgoing-message.host.d.ts +4 -7
  52. package/types/http/interfaces/http-incoming.interface.d.ts +1 -2
  53. package/types/http/interfaces/http-outgoing.interface.d.ts +1 -1
  54. package/types/http/interfaces/node-incoming-message.interface.d.ts +0 -2
  55. package/types/http/interfaces/node-outgoing-message.interface.d.ts +0 -2
  56. package/types/http/utils/body-reader.d.ts +2 -5
  57. package/types/http/utils/concat-readable.d.ts +0 -1
  58. package/types/http/utils/convert-to-raw-headers.d.ts +0 -1
  59. package/types/index.d.ts +4 -4
  60. package/types/platform-adapter.d.ts +1 -5
  61. package/cjs/helpers/logger.js +0 -35
  62. package/esm/helpers/logger.js +0 -31
  63. package/types/helpers/logger.d.ts +0 -14
@@ -1,12 +1,12 @@
1
+ import * as process from 'node:process';
2
+ import typeIs from '@browsery/type-is';
3
+ import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema, } from '@opra/common';
1
4
  import { parse as parseContentType } from 'content-type';
2
5
  import { splitString } from 'fast-tokenizer';
3
- import * as process from 'node:process';
4
6
  import { asMutable } from 'ts-gems';
5
7
  import { toArray, ValidationError, vg } from 'valgen';
6
- import typeIs from '@browsery/type-is';
7
- import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema, translate, } from '@opra/common';
8
- import { kAssetCache } from '../../constants.js';
9
- import { wrapException } from '../utils/wrap-exception.js';
8
+ import { kAssetCache } from '../constants';
9
+ import { wrapException } from './utils/wrap-exception';
10
10
  /**
11
11
  * @class HttpHandler
12
12
  */
@@ -38,7 +38,7 @@ export class HttpHandler {
38
38
  throw e;
39
39
  if (e instanceof ValidationError) {
40
40
  throw new BadRequestError({
41
- message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
41
+ message: 'Response validation failed',
42
42
  code: 'RESPONSE_VALIDATION',
43
43
  details: e.issues,
44
44
  }, e);
@@ -48,11 +48,14 @@ export class HttpHandler {
48
48
  await this.adapter.emitAsync('request', context);
49
49
  // Call interceptors than execute request
50
50
  if (this.adapter.interceptors) {
51
+ const interceptors = this.adapter.interceptors;
51
52
  let i = 0;
52
53
  const next = async () => {
53
- const interceptor = this.adapter.interceptors[i++];
54
- if (interceptor)
54
+ const interceptor = interceptors[i++];
55
+ if (typeof interceptor === 'function')
55
56
  await interceptor(context, next);
57
+ else if (typeof interceptor?.intercept === 'function')
58
+ await interceptor.intercept(context, next);
56
59
  await this._executeRequest(context);
57
60
  };
58
61
  await next();
@@ -60,23 +63,21 @@ export class HttpHandler {
60
63
  else
61
64
  await this._executeRequest(context);
62
65
  }
63
- catch (e) {
66
+ catch (error) {
67
+ let e = error;
64
68
  if (e instanceof ValidationError) {
65
69
  e = new InternalServerError({
66
- message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
70
+ message: 'Response validation failed',
67
71
  code: 'RESPONSE_VALIDATION',
68
72
  details: e.issues,
69
73
  }, e);
70
74
  }
71
75
  else
72
76
  e = wrapException(e);
73
- response.status(e.statusCode || e.status || HttpStatusCode.INTERNAL_SERVER_ERROR);
74
- response.contentType(MimeTypes.opra_response_json);
75
- await this._sendResponse(context, new OperationResult({ errors: [e] })).finally(() => {
76
- if (!response.finished)
77
- response.end();
78
- });
79
- // if (!outgoing.writableEnded) await this._sendErrorResponse(context.response, [error]);
77
+ if (this.onError)
78
+ await this.onError(context, error);
79
+ context.errors.push(e);
80
+ await this.sendResponse(context);
80
81
  }
81
82
  finally {
82
83
  await context.emitAsync('finish');
@@ -89,7 +90,7 @@ export class HttpHandler {
89
90
  async parseRequest(context) {
90
91
  await this._parseParameters(context);
91
92
  await this._parseContentType(context);
92
- if (context.operation.requestBody?.immediateFetch)
93
+ if (context.operation?.requestBody?.immediateFetch)
93
94
  await context.getBody();
94
95
  /** Set default status code as the first status code between 200 and 299 */
95
96
  if (context.operation) {
@@ -109,17 +110,19 @@ export class HttpHandler {
109
110
  */
110
111
  async _parseParameters(context) {
111
112
  const { operation, request } = context;
112
- let prmName = '';
113
+ if (!operation)
114
+ return;
115
+ let key = '';
113
116
  try {
114
117
  const onFail = (issue) => {
115
- issue.location = prmName;
118
+ issue.location = key;
116
119
  return issue;
117
120
  };
118
121
  /** prepare decoders */
119
122
  const getDecoder = (prm) => {
120
123
  let decode = this[kAssetCache].get(prm, 'decode');
121
124
  if (!decode) {
122
- decode = prm.type?.generateCodec('decode') || vg.isAny();
125
+ decode = prm.type?.generateCodec('decode', { ignoreReadonlyFields: true }) || vg.isAny();
123
126
  this[kAssetCache].set(prm, 'decode', decode);
124
127
  }
125
128
  return decode;
@@ -127,9 +130,9 @@ export class HttpHandler {
127
130
  const paramsLeft = new Set([...operation.parameters, ...operation.owner.parameters]);
128
131
  /** parse cookie parameters */
129
132
  if (request.cookies) {
130
- for (prmName of Object.keys(request.cookies)) {
131
- const oprPrm = operation.findParameter(prmName, 'cookie');
132
- const cntPrm = operation.owner.findParameter(prmName, 'cookie');
133
+ for (key of Object.keys(request.cookies)) {
134
+ const oprPrm = operation.findParameter(key, 'cookie');
135
+ const cntPrm = operation.owner.findParameter(key, 'cookie');
133
136
  const prm = oprPrm || cntPrm;
134
137
  if (!prm)
135
138
  continue;
@@ -138,16 +141,17 @@ export class HttpHandler {
138
141
  if (cntPrm)
139
142
  paramsLeft.delete(cntPrm);
140
143
  const decode = getDecoder(prm);
141
- const v = decode(request.cookies[prmName], { coerce: true, label: prmName, onFail });
144
+ const v = decode(request.cookies[key], { coerce: true, label: key, onFail });
145
+ const prmName = typeof prm.name === 'string' ? prm.name : key;
142
146
  if (v !== undefined)
143
147
  context.cookies[prmName] = v;
144
148
  }
145
149
  }
146
150
  /** parse headers */
147
151
  if (request.headers) {
148
- for (prmName of Object.keys(request.headers)) {
149
- const oprPrm = operation.findParameter(prmName, 'header');
150
- const cntPrm = operation.owner.findParameter(prmName, 'header');
152
+ for (key of Object.keys(request.headers)) {
153
+ const oprPrm = operation.findParameter(key, 'header');
154
+ const cntPrm = operation.owner.findParameter(key, 'header');
151
155
  const prm = oprPrm || cntPrm;
152
156
  if (!prm)
153
157
  continue;
@@ -156,16 +160,17 @@ export class HttpHandler {
156
160
  if (cntPrm)
157
161
  paramsLeft.delete(cntPrm);
158
162
  const decode = getDecoder(prm);
159
- const v = decode(request.headers[prmName], { coerce: true, label: prmName, onFail });
163
+ const v = decode(request.headers[key], { coerce: true, label: key, onFail });
164
+ const prmName = typeof prm.name === 'string' ? prm.name : key;
160
165
  if (v !== undefined)
161
166
  context.headers[prmName] = v;
162
167
  }
163
168
  }
164
169
  /** parse path parameters */
165
170
  if (request.params) {
166
- for (prmName of Object.keys(request.params)) {
167
- const oprPrm = operation.findParameter(prmName, 'path');
168
- const cntPrm = operation.owner.findParameter(prmName, 'path');
171
+ for (key of Object.keys(request.params)) {
172
+ const oprPrm = operation.findParameter(key, 'path');
173
+ const cntPrm = operation.owner.findParameter(key, 'path');
169
174
  const prm = oprPrm || cntPrm;
170
175
  if (!prm)
171
176
  continue;
@@ -174,17 +179,17 @@ export class HttpHandler {
174
179
  if (cntPrm)
175
180
  paramsLeft.delete(cntPrm);
176
181
  const decode = getDecoder(prm);
177
- const v = decode(request.params[prmName], { coerce: true, label: prmName, onFail });
182
+ const v = decode(request.params[key], { coerce: true, label: key, onFail });
178
183
  if (v !== undefined)
179
- context.pathParams[prmName] = v;
184
+ context.pathParams[key] = v;
180
185
  }
181
186
  }
182
187
  /** parse query parameters */
183
188
  const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
184
189
  const { searchParams } = url;
185
- for (prmName of searchParams.keys()) {
186
- const oprPrm = operation.findParameter(prmName, 'query');
187
- const cntPrm = operation.owner.findParameter(prmName, 'query');
190
+ for (key of searchParams.keys()) {
191
+ const oprPrm = operation.findParameter(key, 'query');
192
+ const cntPrm = operation.owner.findParameter(key, 'query');
188
193
  const prm = oprPrm || cntPrm;
189
194
  if (!prm)
190
195
  continue;
@@ -193,20 +198,22 @@ export class HttpHandler {
193
198
  if (cntPrm)
194
199
  paramsLeft.delete(cntPrm);
195
200
  const decode = getDecoder(prm);
196
- let values = searchParams?.getAll(prmName);
201
+ let values = searchParams?.getAll(key);
202
+ const prmName = typeof prm.name === 'string' ? prm.name : key;
197
203
  if (values?.length && prm.isArray) {
198
204
  values = values.map(v => splitString(v, { delimiters: prm.arraySeparator, quotes: true })).flat();
199
- values = values.map(v => decode(v, { coerce: true, label: prmName, onFail }));
205
+ values = values.map(v => decode(v, { coerce: true, label: key, onFail }));
200
206
  if (values.length)
201
207
  context.queryParams[prmName] = values;
202
208
  }
203
209
  else {
204
- const v = decode(values[0], { coerce: true, label: prmName, onFail });
210
+ const v = decode(values[0], { coerce: true, label: key, onFail });
205
211
  if (values.length)
206
212
  context.queryParams[prmName] = v;
207
213
  }
208
214
  }
209
215
  for (const prm of paramsLeft) {
216
+ key = String(prm.name);
210
217
  // Throw error for required parameters
211
218
  if (prm.required) {
212
219
  const decode = getDecoder(prm);
@@ -216,8 +223,8 @@ export class HttpHandler {
216
223
  }
217
224
  catch (e) {
218
225
  if (e instanceof ValidationError) {
219
- e = new BadRequestError({
220
- message: `Invalid parameter (${prmName}) value. ` + e.message,
226
+ throw new BadRequestError({
227
+ message: `Invalid parameter (${key}) value. ` + e.message,
221
228
  code: 'REQUEST_VALIDATION',
222
229
  details: e.issues,
223
230
  }, e);
@@ -232,14 +239,15 @@ export class HttpHandler {
232
239
  */
233
240
  async _parseContentType(context) {
234
241
  const { request, operation } = context;
242
+ if (!operation)
243
+ return;
235
244
  if (operation.requestBody?.content.length) {
236
245
  let mediaType;
237
246
  let contentType = request.header('content-type');
238
247
  if (contentType) {
239
248
  contentType = parseContentType(contentType).type;
240
- mediaType = operation.requestBody.content.find(mc => {
241
- return (mc.contentType && typeIs.is(contentType, Array.isArray(mc.contentType) ? mc.contentType : [mc.contentType]));
242
- });
249
+ mediaType = operation.requestBody.content.find(mc => mc.contentType &&
250
+ typeIs.is(contentType, Array.isArray(mc.contentType) ? mc.contentType : [mc.contentType]));
243
251
  }
244
252
  if (!mediaType) {
245
253
  const contentTypes = operation.requestBody.content.map(mc => mc.contentType).flat();
@@ -258,11 +266,12 @@ export class HttpHandler {
258
266
  throw new MethodNotAllowedError();
259
267
  const responseValue = await context.operationHandler.call(context.controllerInstance, context);
260
268
  const { response } = context;
261
- if (!response.writableEnded)
262
- await this._sendResponse(context, responseValue).finally(() => {
269
+ if (!response.writableEnded) {
270
+ await this.sendResponse(context, responseValue).finally(() => {
263
271
  if (!response.writableEnded)
264
272
  response.end();
265
273
  });
274
+ }
266
275
  }
267
276
  /**
268
277
  *
@@ -270,93 +279,180 @@ export class HttpHandler {
270
279
  * @param responseValue
271
280
  * @protected
272
281
  */
273
- async _sendResponse(context, responseValue) {
282
+ async sendResponse(context, responseValue) {
283
+ if (context.errors.length)
284
+ return this._sendErrorResponse(context);
274
285
  const { response } = context;
275
286
  const { document } = this.adapter;
276
- const responseArgs = this._determineResponseArgs(context, responseValue);
277
- const { operationResponse, statusCode } = responseArgs;
278
- let { contentType, body } = responseArgs;
279
- const operationResultType = document.node.getDataType(OperationResult);
280
- let operationResultEncoder = this[kAssetCache].get(operationResultType, 'encode');
281
- if (!operationResultEncoder) {
282
- operationResultEncoder = operationResultType.generateCodec('encode');
283
- this[kAssetCache].set(operationResultType, 'encode', operationResultEncoder);
284
- }
285
- /** Validate response */
286
- if (operationResponse?.type) {
287
- if (!(body == null && statusCode === HttpStatusCode.NO_CONTENT)) {
288
- /** Generate encoder */
289
- let encode = this[kAssetCache].get(operationResponse, 'encode');
290
- if (!encode) {
291
- encode = operationResponse.type.generateCodec('encode', {
292
- partial: operationResponse.partial,
293
- projection: '*',
294
- });
295
- if (operationResponse) {
296
- if (operationResponse.isArray)
297
- encode = vg.isArray(encode);
298
- this[kAssetCache].set(operationResponse, 'encode', encode);
287
+ try {
288
+ const responseArgs = this._determineResponseArgs(context, responseValue);
289
+ const { operationResponse, statusCode } = responseArgs;
290
+ let { contentType, body } = responseArgs;
291
+ const operationResultType = document.node.getDataType(OperationResult);
292
+ let operationResultEncoder = this[kAssetCache].get(operationResultType, 'encode');
293
+ if (!operationResultEncoder) {
294
+ operationResultEncoder = operationResultType.generateCodec('encode', {
295
+ ignoreWriteonlyFields: true,
296
+ ignoreHiddenFields: true,
297
+ });
298
+ this[kAssetCache].set(operationResultType, 'encode', operationResultEncoder);
299
+ }
300
+ /** Validate response */
301
+ if (operationResponse?.type) {
302
+ if (!(body == null && statusCode === HttpStatusCode.NO_CONTENT)) {
303
+ /** Generate encoder */
304
+ let encode = this[kAssetCache].get(operationResponse, 'encode');
305
+ if (!encode) {
306
+ encode = operationResponse.type.generateCodec('encode', {
307
+ partial: operationResponse.partial,
308
+ projection: responseArgs.projection || '*',
309
+ ignoreWriteonlyFields: true,
310
+ ignoreHiddenFields: true,
311
+ onFail: issue => `Response body validation failed: ` + issue.message,
312
+ });
313
+ if (operationResponse) {
314
+ if (operationResponse.isArray)
315
+ encode = vg.isArray(encode);
316
+ this[kAssetCache].set(operationResponse, 'encode', encode);
317
+ }
318
+ }
319
+ /** Encode body */
320
+ if (operationResponse.type.extendsFrom(operationResultType)) {
321
+ if (body instanceof OperationResult)
322
+ body = encode(body);
323
+ else {
324
+ body.payload = encode(body.payload);
325
+ body = operationResultEncoder(body);
326
+ }
299
327
  }
300
- }
301
- /** Encode body */
302
- if (operationResponse.type.extendsFrom(operationResultType)) {
303
- if (body instanceof OperationResult)
304
- body = encode(body);
305
328
  else {
306
- body.payload = encode(body.payload);
307
- body = operationResultEncoder(body);
329
+ if (body instanceof OperationResult &&
330
+ contentType &&
331
+ typeIs.is(contentType, [MimeTypes.opra_response_json])) {
332
+ body.payload = encode(body.payload);
333
+ body = operationResultEncoder(body);
334
+ }
335
+ else {
336
+ body = encode(body);
337
+ }
308
338
  }
309
- }
310
- else {
311
339
  if (body instanceof OperationResult &&
312
- contentType &&
313
- typeIs.is(contentType, [MimeTypes.opra_response_json])) {
314
- body.payload = encode(body.payload);
315
- body = operationResultEncoder(body);
340
+ operationResponse.type &&
341
+ operationResponse.type !== document.node.getDataType(OperationResult)) {
342
+ body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
316
343
  }
317
- else
318
- body = encode(body);
319
- }
320
- if (body instanceof OperationResult && operationResponse.type) {
321
- body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
322
344
  }
323
345
  }
324
- }
325
- else if (body != null) {
326
- if (body instanceof OperationResult) {
327
- body = operationResultEncoder(body);
328
- contentType = MimeTypes.opra_response_json;
329
- }
330
- else if (Buffer.isBuffer(body))
331
- contentType = MimeTypes.binary;
332
- else if (typeof body === 'object') {
333
- contentType = contentType || MimeTypes.json;
334
- if (typeof body.toJSON === 'function')
335
- body = body.toJSON();
346
+ else if (body != null) {
347
+ if (body instanceof OperationResult) {
348
+ body = operationResultEncoder(body);
349
+ contentType = MimeTypes.opra_response_json;
350
+ }
351
+ else if (Buffer.isBuffer(body))
352
+ contentType = MimeTypes.binary;
353
+ else if (typeof body === 'object') {
354
+ contentType = contentType || MimeTypes.json;
355
+ if (typeof body.toJSON === 'function')
356
+ body = body.toJSON();
357
+ }
358
+ else {
359
+ contentType = contentType || MimeTypes.text;
360
+ body = String(body);
361
+ }
336
362
  }
337
- else {
338
- contentType = contentType || MimeTypes.text;
339
- body = String(body);
363
+ /** Set content-type header value if not set */
364
+ if (contentType && contentType !== responseArgs.contentType)
365
+ response.setHeader('content-type', contentType);
366
+ response.status(statusCode);
367
+ if (body == null) {
368
+ response.end();
369
+ return;
340
370
  }
371
+ let x;
372
+ if (Buffer.isBuffer(body) || isReadableStream(body))
373
+ x = body;
374
+ else if (isBlob(body))
375
+ x = body.stream();
376
+ else if (typeof body === 'object')
377
+ x = JSON.stringify(body);
378
+ else
379
+ x = String(body);
380
+ response.end(x);
341
381
  }
342
- /** Set content-type header value if not set */
343
- if (contentType && contentType !== responseArgs.contentType)
344
- response.setHeader('content-type', contentType);
345
- response.status(statusCode);
346
- if (body == null) {
382
+ catch (error) {
383
+ context.errors.push(error);
384
+ return this._sendErrorResponse(context);
385
+ }
386
+ }
387
+ async _sendErrorResponse(context) {
388
+ context.errors = this._wrapExceptions(context.errors);
389
+ try {
390
+ await this.adapter.emitAsync('error', context);
391
+ context.errors = this._wrapExceptions(context.errors);
392
+ }
393
+ catch (e) {
394
+ context.errors = this._wrapExceptions([e, ...context.errors]);
395
+ }
396
+ const { response, errors } = context;
397
+ if (response.headersSent) {
347
398
  response.end();
348
399
  return;
349
400
  }
350
- let x;
351
- if (Buffer.isBuffer(body) || isReadableStream(body))
352
- x = body;
353
- else if (isBlob(body))
354
- x = body.stream();
355
- else if (typeof body === 'object')
356
- x = JSON.stringify(body);
357
- else
358
- x = String(body);
359
- response.end(x);
401
+ let status = response.statusCode || 0;
402
+ if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
403
+ status = errors[0].status;
404
+ if (status < Number(HttpStatusCode.BAD_REQUEST))
405
+ status = HttpStatusCode.INTERNAL_SERVER_ERROR;
406
+ }
407
+ response.statusCode = status;
408
+ const { document } = this.adapter;
409
+ const dt = document.node.getComplexType('OperationResult');
410
+ let encode = this[kAssetCache].get(dt, 'encode');
411
+ if (!encode) {
412
+ encode = dt.generateCodec('encode', { ignoreWriteonlyFields: true });
413
+ this[kAssetCache].set(dt, 'encode', encode);
414
+ }
415
+ const { i18n } = this.adapter;
416
+ const bodyObject = new OperationResult({
417
+ errors: errors.map(x => {
418
+ const o = x.toJSON();
419
+ if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
420
+ delete o.stack;
421
+ return i18n.deep(o);
422
+ }),
423
+ });
424
+ const body = encode(bodyObject);
425
+ response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
426
+ response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
427
+ response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
428
+ response.setHeader(HttpHeaderCodes.Expires, '-1');
429
+ response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
430
+ response.send(JSON.stringify(body));
431
+ response.end();
432
+ }
433
+ async sendDocumentSchema(context) {
434
+ const { request, response } = context;
435
+ const { document } = this.adapter;
436
+ response.setHeader('content-type', MimeTypes.json);
437
+ const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
438
+ const { searchParams } = url;
439
+ const documentId = searchParams.get('id');
440
+ const doc = documentId ? document.findDocument(documentId) : document;
441
+ if (!doc) {
442
+ context.errors.push(new BadRequestError({
443
+ message: `Document with given id [${documentId}] does not exists`,
444
+ }));
445
+ return this.sendResponse(context);
446
+ }
447
+ /** Check if response cache exists */
448
+ let responseBody = this[kAssetCache].get(doc, `$schema`);
449
+ /** Create response if response cache does not exists */
450
+ if (!responseBody) {
451
+ const schema = doc.export();
452
+ responseBody = JSON.stringify(schema);
453
+ this[kAssetCache].set(doc, `$schema`, responseBody);
454
+ }
455
+ response.end(responseBody);
360
456
  }
361
457
  /**
362
458
  *
@@ -383,12 +479,13 @@ export class HttpHandler {
383
479
  let responseArgs = this[kAssetCache].get(response, cacheKey);
384
480
  if (!responseArgs) {
385
481
  responseArgs = { statusCode, contentType };
386
- if (operation.responses.length) {
482
+ if (operation?.responses.length) {
387
483
  /** Filter available HttpOperationResponse instances according to status code. */
388
484
  const filteredResponses = operation.responses.filter(r => r.statusCode.find(sc => sc.start <= statusCode && sc.end >= statusCode));
389
485
  /** Throw InternalServerError if controller returns non-configured status code */
390
- if (!filteredResponses.length && statusCode < 400)
486
+ if (!filteredResponses.length && statusCode < 400) {
391
487
  throw new InternalServerError(`No responses defined for status code ${statusCode} in operation "${operation.name}"`);
488
+ }
392
489
  /** We search for content-type in filtered HttpOperationResponse array */
393
490
  if (filteredResponses.length) {
394
491
  /** If no response returned, and content-type has not been set (No response wants to be returned by operation) */
@@ -401,8 +498,9 @@ export class HttpHandler {
401
498
  if (contentType) {
402
499
  // Find HttpEndpointResponse instance according to content-type header
403
500
  operationResponse = filteredResponses.find(r => typeIs.is(contentType, toArray(r.contentType)));
404
- if (!operationResponse)
501
+ if (!operationResponse) {
405
502
  throw new InternalServerError(`Operation didn't configured to return "${contentType}" content`);
503
+ }
406
504
  }
407
505
  else {
408
506
  /** Select first HttpOperationResponse if content-type header has not been set */
@@ -413,6 +511,8 @@ export class HttpHandler {
413
511
  : operationResponse.contentType);
414
512
  if (typeof ct === 'string')
415
513
  responseArgs.contentType = contentType = ct;
514
+ else if (operationResponse.type)
515
+ responseArgs.contentType = MimeTypes.opra_response_json;
416
516
  }
417
517
  }
418
518
  }
@@ -424,6 +524,10 @@ export class HttpHandler {
424
524
  }
425
525
  if (!hasBody)
426
526
  delete responseArgs.contentType;
527
+ if (operation?.composition?.startsWith('Entity.')) {
528
+ if (context.queryParams.projection)
529
+ responseArgs.projection = context.queryParams.projection;
530
+ }
427
531
  this[kAssetCache].set(response, cacheKey, { ...responseArgs });
428
532
  }
429
533
  /** Fix response value according to composition */
@@ -434,23 +538,26 @@ export class HttpHandler {
434
538
  case 'Entity.Get':
435
539
  case 'Entity.FindMany':
436
540
  case 'Entity.Update': {
437
- if (!(body instanceof OperationResult))
541
+ if (!(body instanceof OperationResult)) {
438
542
  body = new OperationResult({
439
543
  payload: body,
440
544
  });
545
+ }
441
546
  if ((composition === 'Entity.Create' || composition === 'Entity.Update') &&
442
547
  composition &&
443
- body.affected == null)
548
+ body.affected == null) {
444
549
  body.affected = 1;
550
+ }
445
551
  break;
446
552
  }
447
553
  case 'Entity.Delete':
448
554
  case 'Entity.DeleteMany':
449
555
  case 'Entity.UpdateMany': {
450
- if (!(body instanceof OperationResult))
556
+ if (!(body instanceof OperationResult)) {
451
557
  body = new OperationResult({
452
558
  affected: body,
453
559
  });
560
+ }
454
561
  body.affected =
455
562
  typeof body.affected === 'number'
456
563
  ? body.affected
@@ -461,68 +568,27 @@ export class HttpHandler {
461
568
  : undefined;
462
569
  break;
463
570
  }
571
+ default:
572
+ break;
464
573
  }
465
574
  }
466
- if (responseArgs.contentType && responseArgs.contentType !== parsedContentType?.type)
575
+ if (responseArgs.contentType && responseArgs.contentType !== parsedContentType?.type) {
467
576
  response.setHeader('content-type', responseArgs.contentType);
577
+ }
468
578
  if (responseArgs.contentType &&
469
579
  body != null &&
470
580
  !(body instanceof OperationResult) &&
471
- typeIs.is(responseArgs.contentType, [MimeTypes.opra_response_json]))
581
+ typeIs.is(responseArgs.contentType, [MimeTypes.opra_response_json])) {
472
582
  body = new OperationResult({ payload: body });
583
+ }
473
584
  if (hasBody)
474
585
  responseArgs.body = body;
475
586
  return responseArgs;
476
587
  }
477
- async sendDocumentSchema(context) {
478
- const { request, response } = context;
479
- const { document } = this.adapter;
480
- response.setHeader('content-type', MimeTypes.json);
481
- const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
482
- const { searchParams } = url;
483
- const documentId = searchParams.get('id');
484
- const doc = documentId ? document.findDocument(documentId) : document;
485
- if (!doc)
486
- return this.sendErrorResponse(response, [
487
- new BadRequestError({
488
- message: `Document with given id [${documentId}] does not exists`,
489
- }),
490
- ]);
491
- /** Check if response cache exists */
492
- let responseBody = this[kAssetCache].get(doc, `$schema`);
493
- /** Create response if response cache does not exists */
494
- if (!responseBody) {
495
- const schema = doc.export();
496
- responseBody = JSON.stringify(schema);
497
- this[kAssetCache].set(doc, `$schema`, responseBody);
498
- }
499
- response.end(responseBody);
500
- }
501
- async sendErrorResponse(response, errors) {
502
- if (response.headersSent) {
503
- response.end();
504
- return;
505
- }
506
- if (!errors.length)
507
- errors.push(wrapException({ status: response.statusCode || 500 }));
508
- const { logger } = this.adapter;
509
- errors.forEach(x => {
510
- if (x instanceof OpraException) {
511
- switch (x.severity) {
512
- case 'fatal':
513
- logger.fatal(x);
514
- break;
515
- case 'warning':
516
- logger.warn(x);
517
- break;
518
- default:
519
- logger.error(x);
520
- }
521
- }
522
- else
523
- logger.fatal(x);
524
- });
525
- const wrappedErrors = errors.map(wrapException);
588
+ _wrapExceptions(exceptions) {
589
+ const wrappedErrors = exceptions.map(wrapException);
590
+ if (!wrappedErrors.length)
591
+ wrappedErrors.push(new InternalServerError());
526
592
  // Sort errors from fatal to info
527
593
  wrappedErrors.sort((a, b) => {
528
594
  const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
@@ -530,36 +596,6 @@ export class HttpHandler {
530
596
  return b.status - a.status;
531
597
  return i;
532
598
  });
533
- let status = response.statusCode || 0;
534
- if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
535
- status = wrappedErrors[0].status;
536
- if (status < Number(HttpStatusCode.BAD_REQUEST))
537
- status = HttpStatusCode.INTERNAL_SERVER_ERROR;
538
- }
539
- response.statusCode = status;
540
- const { document } = this.adapter;
541
- const dt = document.node.getComplexType('OperationResult');
542
- let encode = this[kAssetCache].get(dt, 'encode');
543
- if (!encode) {
544
- encode = dt.generateCodec('encode');
545
- this[kAssetCache].set(dt, 'encode', encode);
546
- }
547
- const { i18n } = this.adapter;
548
- const bodyObject = new OperationResult({
549
- errors: wrappedErrors.map(x => {
550
- const o = x.toJSON();
551
- if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
552
- delete o.stack;
553
- return i18n.deep(o);
554
- }),
555
- });
556
- const body = encode(bodyObject);
557
- response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
558
- response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
559
- response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
560
- response.setHeader(HttpHeaderCodes.Expires, '-1');
561
- response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
562
- response.send(JSON.stringify(body));
563
- response.end();
599
+ return wrappedErrors;
564
600
  }
565
601
  }