@opra/core 0.33.13 → 1.0.0-alpha.2

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 (156) hide show
  1. package/cjs/augmentation/18n.augmentation.js +17 -4
  2. package/cjs/augmentation/http-controller.augmentation.js +25 -0
  3. package/cjs/constants.js +5 -0
  4. package/cjs/execution-context.js +25 -12
  5. package/cjs/{services → helpers}/logger.js +1 -2
  6. package/cjs/{services/api-service.js → helpers/service-base.js} +8 -8
  7. package/cjs/http/express-adapter.js +164 -0
  8. package/cjs/http/http-adapter.js +27 -0
  9. package/cjs/http/http-context.js +116 -0
  10. package/cjs/http/impl/asset-cache.js +21 -0
  11. package/cjs/http/impl/http-handler.js +575 -0
  12. package/cjs/http/{http-server-request.js → impl/http-incoming.host.js} +21 -46
  13. package/cjs/http/{http-server-response.js → impl/http-outgoing.host.js} +7 -26
  14. package/cjs/http/{helpers/multipart-helper.js → impl/multipart-reader.js} +24 -22
  15. package/cjs/http/impl/{http-incoming-message.host.js → node-incoming-message.host.js} +13 -54
  16. package/cjs/http/impl/{http-outgoing-message.host.js → node-outgoing-message.host.js} +11 -14
  17. package/cjs/http/interfaces/http-incoming.interface.js +25 -0
  18. package/cjs/http/interfaces/http-outgoing.interface.js +22 -0
  19. package/cjs/http/interfaces/node-incoming-message.interface.js +63 -0
  20. package/cjs/http/interfaces/node-outgoing-message.interface.js +15 -0
  21. package/cjs/http/utils/body-reader.js +215 -0
  22. package/cjs/http/{helpers → utils}/convert-to-raw-headers.js +1 -2
  23. package/cjs/http/{helpers → utils}/match-known-fields.js +11 -9
  24. package/cjs/http/utils/wrap-exception.js +34 -0
  25. package/cjs/index.js +25 -25
  26. package/cjs/platform-adapter.js +21 -0
  27. package/cjs/type-guards.js +23 -0
  28. package/esm/augmentation/18n.augmentation.js +20 -7
  29. package/esm/augmentation/http-controller.augmentation.js +23 -0
  30. package/esm/constants.js +2 -0
  31. package/esm/execution-context.js +25 -13
  32. package/esm/{services → helpers}/logger.js +1 -2
  33. package/esm/{services/api-service.js → helpers/service-base.js} +6 -6
  34. package/esm/http/express-adapter.js +159 -0
  35. package/esm/http/http-adapter.js +23 -0
  36. package/esm/http/http-context.js +111 -0
  37. package/esm/http/impl/asset-cache.js +17 -0
  38. package/esm/http/impl/http-handler.js +570 -0
  39. package/esm/http/{http-server-request.js → impl/http-incoming.host.js} +17 -43
  40. package/esm/http/{http-server-response.js → impl/http-outgoing.host.js} +6 -26
  41. package/esm/http/{helpers/multipart-helper.js → impl/multipart-reader.js} +22 -20
  42. package/esm/http/impl/{http-incoming-message.host.js → node-incoming-message.host.js} +11 -52
  43. package/esm/http/impl/{http-outgoing-message.host.js → node-outgoing-message.host.js} +9 -12
  44. package/esm/http/interfaces/http-incoming.interface.js +22 -0
  45. package/esm/http/interfaces/http-outgoing.interface.js +19 -0
  46. package/esm/http/interfaces/node-incoming-message.interface.js +60 -0
  47. package/esm/http/interfaces/node-outgoing-message.interface.js +12 -0
  48. package/esm/http/utils/body-reader.js +210 -0
  49. package/esm/http/{helpers → utils}/convert-to-headers.js +1 -1
  50. package/esm/http/{helpers → utils}/convert-to-raw-headers.js +2 -3
  51. package/esm/http/{helpers → utils}/match-known-fields.js +11 -9
  52. package/esm/http/utils/wrap-exception.js +30 -0
  53. package/esm/index.js +25 -26
  54. package/esm/platform-adapter.js +19 -1
  55. package/esm/type-guards.js +16 -0
  56. package/package.json +22 -10
  57. package/types/augmentation/18n.augmentation.d.ts +32 -2
  58. package/types/augmentation/http-controller.augmentation.d.ts +21 -0
  59. package/types/constants.d.ts +2 -0
  60. package/types/execution-context.d.ts +24 -26
  61. package/types/helpers/service-base.d.ts +10 -0
  62. package/types/http/express-adapter.d.ts +13 -0
  63. package/types/http/http-adapter.d.ts +27 -0
  64. package/types/http/http-context.d.ts +44 -0
  65. package/types/http/impl/asset-cache.d.ts +5 -0
  66. package/types/http/impl/http-handler.d.ts +73 -0
  67. package/types/http/impl/http-incoming.host.d.ts +23 -0
  68. package/types/http/impl/http-outgoing.host.d.ts +17 -0
  69. package/types/http/{helpers/multipart-helper.d.ts → impl/multipart-reader.d.ts} +8 -6
  70. package/types/http/impl/{http-incoming-message.host.d.ts → node-incoming-message.host.d.ts} +9 -22
  71. package/types/http/impl/{http-outgoing-message.host.d.ts → node-outgoing-message.host.d.ts} +11 -27
  72. package/types/http/{http-server-request.d.ts → interfaces/http-incoming.interface.d.ts} +28 -17
  73. package/types/http/{http-server-response.d.ts → interfaces/http-outgoing.interface.d.ts} +17 -10
  74. package/types/http/interfaces/node-incoming-message.interface.d.ts +38 -0
  75. package/types/http/interfaces/node-outgoing-message.interface.d.ts +29 -0
  76. package/types/http/utils/body-reader.d.ts +41 -0
  77. package/types/http/utils/wrap-exception.d.ts +2 -0
  78. package/types/index.d.ts +24 -26
  79. package/types/platform-adapter.d.ts +20 -48
  80. package/types/type-guards.d.ts +8 -0
  81. package/cjs/augmentation/collection.augmentation.js +0 -2
  82. package/cjs/augmentation/container.augmentation.js +0 -2
  83. package/cjs/augmentation/resource.augmentation.js +0 -26
  84. package/cjs/augmentation/singleton.augmentation.js +0 -2
  85. package/cjs/augmentation/storage.augmentation.js +0 -2
  86. package/cjs/execution-context.host.js +0 -46
  87. package/cjs/http/adapters/express-adapter.host.js +0 -34
  88. package/cjs/http/adapters/express-adapter.js +0 -14
  89. package/cjs/http/adapters/node-http-adapter.host.js +0 -70
  90. package/cjs/http/adapters/node-http-adapter.js +0 -14
  91. package/cjs/http/helpers/json-body-loader.js +0 -29
  92. package/cjs/http/helpers/query-parsers.js +0 -16
  93. package/cjs/http/http-adapter-host.js +0 -715
  94. package/cjs/interfaces/interceptor.interface.js +0 -2
  95. package/cjs/interfaces/request-handler.interface.js +0 -2
  96. package/cjs/platform-adapter.host.js +0 -154
  97. package/cjs/request-context.js +0 -25
  98. package/cjs/request.host.js +0 -24
  99. package/cjs/request.js +0 -2
  100. package/cjs/response.host.js +0 -22
  101. package/cjs/response.js +0 -2
  102. package/esm/augmentation/collection.augmentation.js +0 -1
  103. package/esm/augmentation/container.augmentation.js +0 -1
  104. package/esm/augmentation/resource.augmentation.js +0 -24
  105. package/esm/augmentation/singleton.augmentation.js +0 -1
  106. package/esm/augmentation/storage.augmentation.js +0 -1
  107. package/esm/execution-context.host.js +0 -42
  108. package/esm/http/adapters/express-adapter.host.js +0 -30
  109. package/esm/http/adapters/express-adapter.js +0 -11
  110. package/esm/http/adapters/node-http-adapter.host.js +0 -65
  111. package/esm/http/adapters/node-http-adapter.js +0 -11
  112. package/esm/http/helpers/json-body-loader.js +0 -24
  113. package/esm/http/helpers/query-parsers.js +0 -12
  114. package/esm/http/http-adapter-host.js +0 -710
  115. package/esm/interfaces/interceptor.interface.js +0 -1
  116. package/esm/interfaces/request-handler.interface.js +0 -1
  117. package/esm/platform-adapter.host.js +0 -149
  118. package/esm/request-context.js +0 -22
  119. package/esm/request.host.js +0 -20
  120. package/esm/request.js +0 -1
  121. package/esm/response.host.js +0 -18
  122. package/esm/response.js +0 -1
  123. package/i18n/i18n/en/error.json +0 -21
  124. package/types/augmentation/collection.augmentation.d.ts +0 -146
  125. package/types/augmentation/container.augmentation.d.ts +0 -14
  126. package/types/augmentation/resource.augmentation.d.ts +0 -38
  127. package/types/augmentation/singleton.augmentation.d.ts +0 -83
  128. package/types/augmentation/storage.augmentation.d.ts +0 -50
  129. package/types/execution-context.host.d.ts +0 -25
  130. package/types/http/adapters/express-adapter.d.ts +0 -15
  131. package/types/http/adapters/express-adapter.host.d.ts +0 -12
  132. package/types/http/adapters/node-http-adapter.d.ts +0 -17
  133. package/types/http/adapters/node-http-adapter.host.d.ts +0 -19
  134. package/types/http/helpers/json-body-loader.d.ts +0 -5
  135. package/types/http/helpers/query-parsers.d.ts +0 -1
  136. package/types/http/http-adapter-host.d.ts +0 -34
  137. package/types/interfaces/interceptor.interface.d.ts +0 -2
  138. package/types/interfaces/request-handler.interface.d.ts +0 -4
  139. package/types/platform-adapter.host.d.ts +0 -43
  140. package/types/request-context.d.ts +0 -13
  141. package/types/request.d.ts +0 -14
  142. package/types/request.host.d.ts +0 -27
  143. package/types/response.d.ts +0 -22
  144. package/types/response.host.d.ts +0 -22
  145. package/types/services/api-service.d.ts +0 -10
  146. /package/cjs/http/{helpers → utils}/common.js +0 -0
  147. /package/cjs/http/{helpers → utils}/concat-readable.js +0 -0
  148. /package/cjs/http/{helpers → utils}/convert-to-headers.js +0 -0
  149. /package/esm/http/{helpers → utils}/common.js +0 -0
  150. /package/esm/http/{helpers → utils}/concat-readable.js +0 -0
  151. /package/types/{services → helpers}/logger.d.ts +0 -0
  152. /package/types/http/{helpers → utils}/common.d.ts +0 -0
  153. /package/types/http/{helpers → utils}/concat-readable.d.ts +0 -0
  154. /package/types/http/{helpers → utils}/convert-to-headers.d.ts +0 -0
  155. /package/types/http/{helpers → utils}/convert-to-raw-headers.d.ts +0 -0
  156. /package/types/http/{helpers → utils}/match-known-fields.d.ts +0 -0
@@ -0,0 +1,570 @@
1
+ import { parse as parseContentType } from 'content-type';
2
+ import { splitString } from 'fast-tokenizer';
3
+ import * as process from 'node:process';
4
+ import { asMutable } from 'ts-gems';
5
+ 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';
10
+ /**
11
+ * @class HttpHandler
12
+ */
13
+ export class HttpHandler {
14
+ constructor(adapter) {
15
+ this.adapter = adapter;
16
+ this[kAssetCache] = adapter[kAssetCache];
17
+ }
18
+ /**
19
+ * Main http request handler
20
+ * @param context
21
+ * @protected
22
+ */
23
+ async handleRequest(context) {
24
+ const { response } = context;
25
+ try {
26
+ response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
27
+ // Expose headers if cors enabled
28
+ if (response.getHeader(HttpHeaderCodes.Access_Control_Allow_Origin)) {
29
+ // Expose X-Opra-* headers
30
+ response.appendHeader(HttpHeaderCodes.Access_Control_Expose_Headers, Object.values(HttpHeaderCodes).filter(k => k.toLowerCase().startsWith('x-opra-')));
31
+ }
32
+ // Parse request
33
+ try {
34
+ await this.parseRequest(context);
35
+ }
36
+ catch (e) {
37
+ if (e instanceof OpraException)
38
+ throw e;
39
+ if (e instanceof ValidationError) {
40
+ throw new BadRequestError({
41
+ message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
42
+ code: 'RESPONSE_VALIDATION',
43
+ details: e.issues,
44
+ }, e);
45
+ }
46
+ throw new BadRequestError(e);
47
+ }
48
+ await this.adapter.emitAsync('request', context);
49
+ // Call interceptors than execute request
50
+ if (this.adapter.interceptors) {
51
+ let i = 0;
52
+ const next = async () => {
53
+ const interceptor = this.adapter.interceptors[i++];
54
+ if (interceptor)
55
+ await interceptor(context, next);
56
+ await this._executeRequest(context);
57
+ };
58
+ await next();
59
+ }
60
+ else
61
+ await this._executeRequest(context);
62
+ }
63
+ catch (e) {
64
+ if (e instanceof ValidationError) {
65
+ e = new InternalServerError({
66
+ message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
67
+ code: 'RESPONSE_VALIDATION',
68
+ details: e.issues,
69
+ }, e);
70
+ }
71
+ else
72
+ 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]);
80
+ }
81
+ finally {
82
+ await context.emitAsync('finish');
83
+ }
84
+ }
85
+ /**
86
+ *
87
+ * @param context
88
+ */
89
+ async parseRequest(context) {
90
+ await this._parseParameters(context);
91
+ await this._parseContentType(context);
92
+ if (context.operation.requestBody?.immediateFetch)
93
+ await context.getBody();
94
+ /** Set default status code as the first status code between 200 and 299 */
95
+ if (context.operation) {
96
+ for (const r of context.operation.responses) {
97
+ const st = r.statusCode.find(sc => sc.start <= 299 && sc.end >= 200);
98
+ if (st) {
99
+ context.response.status(st.start);
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ /**
106
+ *
107
+ * @param context
108
+ * @protected
109
+ */
110
+ async _parseParameters(context) {
111
+ const { operation, request } = context;
112
+ let prmName = '';
113
+ try {
114
+ const onFail = (issue) => {
115
+ issue.location = prmName;
116
+ return issue;
117
+ };
118
+ /** prepare decoders */
119
+ const getDecoder = (prm) => {
120
+ let decode = this[kAssetCache].get(prm, 'decode');
121
+ if (!decode) {
122
+ decode = prm.type?.generateCodec('decode') || vg.isAny();
123
+ this[kAssetCache].set(prm, 'decode', decode);
124
+ }
125
+ return decode;
126
+ };
127
+ const paramsLeft = new Set([...operation.parameters, ...operation.owner.parameters]);
128
+ /** parse cookie parameters */
129
+ 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
+ const prm = oprPrm || cntPrm;
134
+ if (!prm)
135
+ continue;
136
+ if (oprPrm)
137
+ paramsLeft.delete(oprPrm);
138
+ if (cntPrm)
139
+ paramsLeft.delete(cntPrm);
140
+ const decode = getDecoder(prm);
141
+ const v = decode(request.cookies[prmName], { coerce: true, label: prmName, onFail });
142
+ if (v !== undefined)
143
+ context.cookies[prmName] = v;
144
+ }
145
+ }
146
+ /** parse headers */
147
+ 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');
151
+ const prm = oprPrm || cntPrm;
152
+ if (!prm)
153
+ continue;
154
+ if (oprPrm)
155
+ paramsLeft.delete(oprPrm);
156
+ if (cntPrm)
157
+ paramsLeft.delete(cntPrm);
158
+ const decode = getDecoder(prm);
159
+ const v = decode(request.headers[prmName], { coerce: true, label: prmName, onFail });
160
+ if (v !== undefined)
161
+ context.headers[prmName] = v;
162
+ }
163
+ }
164
+ /** parse path parameters */
165
+ 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');
169
+ const prm = oprPrm || cntPrm;
170
+ if (!prm)
171
+ continue;
172
+ if (oprPrm)
173
+ paramsLeft.delete(oprPrm);
174
+ if (cntPrm)
175
+ paramsLeft.delete(cntPrm);
176
+ const decode = getDecoder(prm);
177
+ const v = decode(request.params[prmName], { coerce: true, label: prmName, onFail });
178
+ if (v !== undefined)
179
+ context.pathParams[prmName] = v;
180
+ }
181
+ }
182
+ /** parse query parameters */
183
+ const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
184
+ const { searchParams } = url;
185
+ for (prmName of searchParams.keys()) {
186
+ const oprPrm = operation.findParameter(prmName, 'query');
187
+ const cntPrm = operation.owner.findParameter(prmName, 'query');
188
+ const prm = oprPrm || cntPrm;
189
+ if (!prm)
190
+ continue;
191
+ if (oprPrm)
192
+ paramsLeft.delete(oprPrm);
193
+ if (cntPrm)
194
+ paramsLeft.delete(cntPrm);
195
+ const decode = getDecoder(prm);
196
+ let values = searchParams?.getAll(prmName);
197
+ if (values?.length && prm.isArray) {
198
+ values = values.map(v => splitString(v, { delimiters: prm.arraySeparator, quotes: true })).flat();
199
+ values = values.map(v => decode(v, { coerce: true, label: prmName, onFail }));
200
+ if (values.length)
201
+ context.queryParams[prmName] = values;
202
+ }
203
+ else {
204
+ const v = decode(values[0], { coerce: true, label: prmName, onFail });
205
+ if (values.length)
206
+ context.queryParams[prmName] = v;
207
+ }
208
+ }
209
+ for (const prm of paramsLeft) {
210
+ // Throw error for required parameters
211
+ if (prm.required) {
212
+ const decode = getDecoder(prm);
213
+ decode(undefined, { coerce: true, label: String(prm.name), onFail });
214
+ }
215
+ }
216
+ }
217
+ catch (e) {
218
+ if (e instanceof ValidationError) {
219
+ e = new BadRequestError({
220
+ message: `Invalid parameter (${prmName}) value. ` + e.message,
221
+ code: 'REQUEST_VALIDATION',
222
+ details: e.issues,
223
+ }, e);
224
+ }
225
+ throw e;
226
+ }
227
+ }
228
+ /**
229
+ *
230
+ * @param context
231
+ * @protected
232
+ */
233
+ async _parseContentType(context) {
234
+ const { request, operation } = context;
235
+ if (operation.requestBody?.content.length) {
236
+ let mediaType;
237
+ let contentType = request.header('content-type');
238
+ if (contentType) {
239
+ 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
+ });
243
+ }
244
+ if (!mediaType) {
245
+ const contentTypes = operation.requestBody.content.map(mc => mc.contentType).flat();
246
+ throw new BadRequestError(`Request body should be one of required content types (${contentTypes.join(', ')})`);
247
+ }
248
+ asMutable(context).mediaType = mediaType;
249
+ }
250
+ }
251
+ /**
252
+ *
253
+ * @param context
254
+ * @protected
255
+ */
256
+ async _executeRequest(context) {
257
+ if (!context.operationHandler)
258
+ throw new MethodNotAllowedError();
259
+ const responseValue = await context.operationHandler.call(context.controllerInstance, context);
260
+ const { response } = context;
261
+ if (!response.writableEnded)
262
+ await this._sendResponse(context, responseValue).finally(() => {
263
+ if (!response.writableEnded)
264
+ response.end();
265
+ });
266
+ }
267
+ /**
268
+ *
269
+ * @param context
270
+ * @param responseValue
271
+ * @protected
272
+ */
273
+ async _sendResponse(context, responseValue) {
274
+ const { response } = context;
275
+ 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);
299
+ }
300
+ }
301
+ /** Encode body */
302
+ if (operationResponse.type.extendsFrom(operationResultType)) {
303
+ if (body instanceof OperationResult)
304
+ body = encode(body);
305
+ else {
306
+ body.payload = encode(body.payload);
307
+ body = operationResultEncoder(body);
308
+ }
309
+ }
310
+ else {
311
+ if (body instanceof OperationResult &&
312
+ contentType &&
313
+ typeIs.is(contentType, [MimeTypes.opra_response_json])) {
314
+ body.payload = encode(body.payload);
315
+ body = operationResultEncoder(body);
316
+ }
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
+ }
323
+ }
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();
336
+ }
337
+ else {
338
+ contentType = contentType || MimeTypes.text;
339
+ body = String(body);
340
+ }
341
+ }
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) {
347
+ response.end();
348
+ return;
349
+ }
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);
360
+ }
361
+ /**
362
+ *
363
+ * @param context
364
+ * @param body
365
+ * @protected
366
+ */
367
+ _determineResponseArgs(context, body) {
368
+ const { response, operation } = context;
369
+ const hasBody = body != null;
370
+ const statusCode = !hasBody && response.statusCode === HttpStatusCode.OK ? HttpStatusCode.NO_CONTENT : response.statusCode;
371
+ /** Parse content-type header */
372
+ const parsedContentType = hasBody && response.hasHeader('content-type') ? parseContentType(response) : undefined;
373
+ let contentType = parsedContentType?.type;
374
+ /** Estimate content type if not defined */
375
+ if (hasBody && !contentType) {
376
+ if (body instanceof OperationResult)
377
+ contentType = MimeTypes.opra_response_json;
378
+ else if (Buffer.isBuffer(body))
379
+ contentType = MimeTypes.binary;
380
+ }
381
+ let operationResponse;
382
+ const cacheKey = `HttpOperationResponse:${statusCode}${contentType ? ':' + contentType : ''}`;
383
+ let responseArgs = this[kAssetCache].get(response, cacheKey);
384
+ if (!responseArgs) {
385
+ responseArgs = { statusCode, contentType };
386
+ if (operation.responses.length) {
387
+ /** Filter available HttpOperationResponse instances according to status code. */
388
+ const filteredResponses = operation.responses.filter(r => r.statusCode.find(sc => sc.start <= statusCode && sc.end >= statusCode));
389
+ /** Throw InternalServerError if controller returns non-configured status code */
390
+ if (!filteredResponses.length && statusCode < 400)
391
+ throw new InternalServerError(`No responses defined for status code ${statusCode} in operation "${operation.name}"`);
392
+ /** We search for content-type in filtered HttpOperationResponse array */
393
+ if (filteredResponses.length) {
394
+ /** If no response returned, and content-type has not been set (No response wants to be returned by operation) */
395
+ if (!hasBody) {
396
+ /** Find HttpOperationResponse with no content-type */
397
+ operationResponse = filteredResponses.find(r => !r.contentType);
398
+ }
399
+ if (!operationResponse) {
400
+ /** Find HttpOperationResponse according to content-type */
401
+ if (contentType) {
402
+ // Find HttpEndpointResponse instance according to content-type header
403
+ operationResponse = filteredResponses.find(r => typeIs.is(contentType, toArray(r.contentType)));
404
+ if (!operationResponse)
405
+ throw new InternalServerError(`Operation didn't configured to return "${contentType}" content`);
406
+ }
407
+ else {
408
+ /** Select first HttpOperationResponse if content-type header has not been set */
409
+ operationResponse = filteredResponses[0];
410
+ if (operationResponse.contentType) {
411
+ const ct = typeIs.normalize(Array.isArray(operationResponse.contentType)
412
+ ? operationResponse.contentType[0]
413
+ : operationResponse.contentType);
414
+ if (typeof ct === 'string')
415
+ responseArgs.contentType = contentType = ct;
416
+ }
417
+ }
418
+ }
419
+ responseArgs.operationResponse = operationResponse;
420
+ if (!operationResponse.statusCode.find(sc => sc.start <= statusCode && sc.end >= statusCode)) {
421
+ responseArgs.statusCode = operationResponse.statusCode[0].start;
422
+ }
423
+ }
424
+ }
425
+ if (!hasBody)
426
+ delete responseArgs.contentType;
427
+ this[kAssetCache].set(response, cacheKey, { ...responseArgs });
428
+ }
429
+ /** Fix response value according to composition */
430
+ const composition = operationResponse?.owner.composition;
431
+ if (composition && body != null) {
432
+ switch (composition) {
433
+ case 'Entity.Create':
434
+ case 'Entity.Get':
435
+ case 'Entity.FindMany':
436
+ case 'Entity.Update': {
437
+ if (!(body instanceof OperationResult))
438
+ body = new OperationResult({
439
+ payload: body,
440
+ });
441
+ if ((composition === 'Entity.Create' || composition === 'Entity.Update') &&
442
+ composition &&
443
+ body.affected == null)
444
+ body.affected = 1;
445
+ break;
446
+ }
447
+ case 'Entity.Delete':
448
+ case 'Entity.DeleteMany':
449
+ case 'Entity.UpdateMany': {
450
+ if (!(body instanceof OperationResult))
451
+ body = new OperationResult({
452
+ affected: body,
453
+ });
454
+ body.affected =
455
+ typeof body.affected === 'number'
456
+ ? body.affected
457
+ : typeof body.affected === 'boolean'
458
+ ? body.affected
459
+ ? 1
460
+ : 0
461
+ : undefined;
462
+ break;
463
+ }
464
+ }
465
+ }
466
+ if (responseArgs.contentType && responseArgs.contentType !== parsedContentType?.type)
467
+ response.setHeader('content-type', responseArgs.contentType);
468
+ if (responseArgs.contentType &&
469
+ body != null &&
470
+ !(body instanceof OperationResult) &&
471
+ typeIs.is(responseArgs.contentType, [MimeTypes.opra_response_json]))
472
+ body = new OperationResult({ payload: body });
473
+ if (hasBody)
474
+ responseArgs.body = body;
475
+ return responseArgs;
476
+ }
477
+ async sendDocumentSchema(context) {
478
+ const { request, response } = context;
479
+ const { document } = this.adapter;
480
+ response.setHeader('content-type', MimeTypes.json);
481
+ /** Check if response cache exists */
482
+ let responseBody = this[kAssetCache].get(document, '$schema-response');
483
+ /** Create response if response cache does not exists */
484
+ if (!responseBody) {
485
+ const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
486
+ const { searchParams } = url;
487
+ // const nsPath = searchParams.get('ns');
488
+ // if (nsPath) {
489
+ // const arr = nsPath.split('/');
490
+ // let doc = document;
491
+ // for (const a of arr) {
492
+ // }
493
+ // }
494
+ const schema = document.export({ references: searchParams.get('references') });
495
+ const dt = document.node.getComplexType('OperationResult');
496
+ let encode = this[kAssetCache].get(dt, 'encode');
497
+ if (!encode) {
498
+ encode = dt.generateCodec('encode');
499
+ this[kAssetCache].set(dt, 'encode', encode);
500
+ }
501
+ responseBody = JSON.stringify(schema);
502
+ this[kAssetCache].set(document, '$schema-response', responseBody);
503
+ }
504
+ response.end(responseBody);
505
+ }
506
+ async sendErrorResponse(response, errors) {
507
+ if (response.headersSent) {
508
+ response.end();
509
+ return;
510
+ }
511
+ if (!errors.length)
512
+ errors.push(wrapException({ status: response.statusCode || 500 }));
513
+ const { logger } = this.adapter;
514
+ errors.forEach(x => {
515
+ if (x instanceof OpraException) {
516
+ switch (x.severity) {
517
+ case 'fatal':
518
+ logger.fatal(x);
519
+ break;
520
+ case 'warning':
521
+ logger.warn(x);
522
+ break;
523
+ default:
524
+ logger.error(x);
525
+ }
526
+ }
527
+ else
528
+ logger.fatal(x);
529
+ });
530
+ const wrappedErrors = errors.map(wrapException);
531
+ // Sort errors from fatal to info
532
+ wrappedErrors.sort((a, b) => {
533
+ const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
534
+ if (i === 0)
535
+ return b.status - a.status;
536
+ return i;
537
+ });
538
+ let status = response.statusCode || 0;
539
+ if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
540
+ status = wrappedErrors[0].status;
541
+ if (status < Number(HttpStatusCode.BAD_REQUEST))
542
+ status = HttpStatusCode.INTERNAL_SERVER_ERROR;
543
+ }
544
+ response.statusCode = status;
545
+ const { document } = this.adapter;
546
+ const dt = document.node.getComplexType('OperationResult');
547
+ let encode = this[kAssetCache].get(dt, 'encode');
548
+ if (!encode) {
549
+ encode = dt.generateCodec('encode');
550
+ this[kAssetCache].set(dt, 'encode', encode);
551
+ }
552
+ const { i18n } = this.adapter;
553
+ const bodyObject = new OperationResult({
554
+ errors: wrappedErrors.map(x => {
555
+ const o = x.toJSON();
556
+ if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
557
+ delete o.stack;
558
+ return i18n.deep(o);
559
+ }),
560
+ });
561
+ const body = encode(bodyObject);
562
+ response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
563
+ response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
564
+ response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
565
+ response.setHeader(HttpHeaderCodes.Expires, '-1');
566
+ response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
567
+ response.send(JSON.stringify(body));
568
+ response.end();
569
+ }
570
+ }
@@ -6,40 +6,12 @@ import accepts from 'accepts';
6
6
  import fresh from 'fresh';
7
7
  import parseRange from 'range-parser';
8
8
  import typeIs from '@browsery/type-is';
9
- import { isReadable, mergePrototype, OpraURL } from '@opra/common';
10
- import { HttpIncomingMessageHost } from './impl/http-incoming-message.host.js';
11
- function isHttpIncomingMessage(v) {
12
- return v && Array.isArray(v.rawHeaders) && isReadable(v);
13
- }
14
- export var HttpServerRequest;
15
- (function (HttpServerRequest) {
16
- function from(instance) {
17
- if (!isHttpIncomingMessage(instance))
18
- instance = HttpIncomingMessageHost.from(instance);
19
- mergePrototype(instance, HttpServerRequestHost.prototype);
20
- const req = instance;
21
- req.baseUrl = req.baseUrl || '';
22
- req.parsedUrl = req.parsedUrl || new OpraURL(req.url);
23
- if (!req.searchParams)
24
- Object.defineProperty(req, 'searchParams', {
25
- get() {
26
- return req.parsedUrl.searchParams;
27
- }
28
- });
29
- return req;
30
- }
31
- HttpServerRequest.from = from;
32
- })(HttpServerRequest || (HttpServerRequest = {}));
33
- class HttpServerRequestHost {
34
- constructor() {
35
- this.basePath = '/';
36
- }
9
+ import { BodyReader } from '../utils/body-reader.js';
10
+ export class HttpIncomingHost {
37
11
  get protocol() {
38
12
  const proto = this.header('X-Forwarded-Proto') || 'http';
39
13
  const index = proto.indexOf(',');
40
- return index !== -1
41
- ? proto.substring(0, index).trim()
42
- : proto.trim();
14
+ return index !== -1 ? proto.substring(0, index).trim() : proto.trim();
43
15
  }
44
16
  get secure() {
45
17
  return this.protocol === 'https';
@@ -54,16 +26,13 @@ class HttpServerRequestHost {
54
26
  // single value, but this is to be safe.
55
27
  host = host.substring(0, host.indexOf(',')).trim();
56
28
  }
57
- if (!host)
58
- return;
59
- // IPv6 literal support
60
- const offset = host[0] === '['
61
- ? host.indexOf(']') + 1
62
- : 0;
63
- const index = host.indexOf(':', offset);
64
- return index !== -1
65
- ? host.substring(0, index)
66
- : host;
29
+ if (host) {
30
+ // IPv6 literal support
31
+ const offset = host[0] === '[' ? host.indexOf(']') + 1 : 0;
32
+ const index = host.indexOf(':', offset);
33
+ return index !== -1 ? host.substring(0, index) : host;
34
+ }
35
+ return '';
67
36
  }
68
37
  get fresh() {
69
38
  const method = this.method;
@@ -74,8 +43,8 @@ class HttpServerRequestHost {
74
43
  // 2xx or 304 as per rfc2616 14.26
75
44
  if ((status >= 200 && status < 300) || 304 === status) {
76
45
  return fresh(this.headers, {
77
- 'etag': this.res.getHeader('ETag'),
78
- 'last-modified': this.res.getHeader('Last-Modified')
46
+ etag: this.res.getHeader('ETag'),
47
+ 'last-modified': this.res.getHeader('Last-Modified'),
79
48
  });
80
49
  }
81
50
  return false;
@@ -130,4 +99,9 @@ class HttpServerRequestHost {
130
99
  return;
131
100
  return parseRange(size, range, options);
132
101
  }
102
+ async readBody(options) {
103
+ if (!this.complete)
104
+ this.body = await BodyReader.read(this, options);
105
+ return this.body;
106
+ }
133
107
  }