@opra/core 1.0.0-alpha.8 → 1.0.0-beta.1

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 (60) hide show
  1. package/cjs/augmentation/18n.augmentation.js +13 -13
  2. package/cjs/constants.js +1 -2
  3. package/cjs/execution-context.js +1 -0
  4. package/cjs/http/express-adapter.js +22 -21
  5. package/cjs/http/http-adapter.js +2 -5
  6. package/cjs/http/http-context.js +16 -31
  7. package/cjs/http/{impl/http-handler.js → http-handler.js} +194 -169
  8. package/cjs/http/impl/http-outgoing.host.js +2 -2
  9. package/cjs/http/impl/multipart-reader.js +141 -44
  10. package/cjs/http/utils/body-reader.js +0 -1
  11. package/cjs/http/utils/common.js +4 -4
  12. package/cjs/http/utils/concat-readable.js +1 -2
  13. package/cjs/http/utils/convert-to-headers.js +2 -3
  14. package/cjs/http/utils/convert-to-raw-headers.js +1 -2
  15. package/cjs/http/utils/match-known-fields.js +2 -2
  16. package/cjs/http/utils/wrap-exception.js +1 -2
  17. package/cjs/index.js +1 -1
  18. package/cjs/platform-adapter.js +0 -3
  19. package/cjs/type-guards.js +4 -5
  20. package/esm/augmentation/18n.augmentation.js +2 -2
  21. package/esm/constants.js +0 -1
  22. package/esm/execution-context.js +1 -0
  23. package/esm/http/express-adapter.js +22 -21
  24. package/esm/http/http-adapter.js +2 -5
  25. package/esm/http/http-context.js +17 -32
  26. package/esm/http/{impl/http-handler.js → http-handler.js} +195 -170
  27. package/esm/http/impl/http-outgoing.host.js +1 -1
  28. package/esm/http/impl/multipart-reader.js +142 -45
  29. package/esm/http/utils/body-reader.js +0 -1
  30. package/esm/index.js +1 -1
  31. package/esm/package.json +3 -0
  32. package/esm/platform-adapter.js +0 -3
  33. package/package.json +35 -63
  34. package/types/augmentation/http-controller.augmentation.d.ts +1 -2
  35. package/types/constants.d.ts +0 -1
  36. package/types/execution-context.d.ts +2 -1
  37. package/types/helpers/service-base.d.ts +1 -1
  38. package/types/http/express-adapter.d.ts +1 -1
  39. package/types/http/http-adapter.d.ts +35 -8
  40. package/types/http/http-context.d.ts +4 -4
  41. package/types/http/{impl/http-handler.d.ts → http-handler.d.ts} +11 -9
  42. package/types/http/impl/http-incoming.host.d.ts +1 -2
  43. package/types/http/impl/http-outgoing.host.d.ts +1 -1
  44. package/types/http/impl/multipart-reader.d.ts +38 -20
  45. package/types/http/impl/node-incoming-message.host.d.ts +3 -7
  46. package/types/http/impl/node-outgoing-message.host.d.ts +5 -8
  47. package/types/http/interfaces/http-incoming.interface.d.ts +2 -3
  48. package/types/http/interfaces/http-outgoing.interface.d.ts +2 -2
  49. package/types/http/interfaces/node-incoming-message.interface.d.ts +0 -2
  50. package/types/http/interfaces/node-outgoing-message.interface.d.ts +1 -3
  51. package/types/http/utils/body-reader.d.ts +1 -4
  52. package/types/http/utils/concat-readable.d.ts +0 -1
  53. package/types/http/utils/convert-to-raw-headers.d.ts +1 -2
  54. package/types/index.d.cts +28 -0
  55. package/types/index.d.ts +1 -1
  56. package/types/platform-adapter.d.ts +0 -4
  57. package/cjs/helpers/logger.js +0 -35
  58. package/esm/helpers/logger.js +0 -31
  59. package/i18n/i18n/en/error.json +0 -21
  60. package/types/helpers/logger.d.ts +0 -14
@@ -2,58 +2,158 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MultipartReader = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
7
+ const node_os_1 = tslib_1.__importDefault(require("node:os"));
8
+ const node_path_1 = tslib_1.__importDefault(require("node:path"));
9
+ const type_is_1 = tslib_1.__importDefault(require("@browsery/type-is"));
10
+ const common_1 = require("@opra/common");
11
+ const busboy_1 = tslib_1.__importDefault(require("busboy"));
5
12
  const events_1 = require("events");
6
- const formidable_1 = tslib_1.__importDefault(require("formidable"));
7
13
  const promises_1 = tslib_1.__importDefault(require("fs/promises"));
14
+ const valgen_1 = require("valgen");
8
15
  class MultipartReader extends events_1.EventEmitter {
9
- constructor(incoming, options) {
16
+ constructor(context, options, mediaType) {
10
17
  super();
18
+ this.context = context;
19
+ this.mediaType = mediaType;
11
20
  this._started = false;
21
+ this._finished = false;
12
22
  this._cancelled = false;
13
23
  this._items = [];
14
24
  this._stack = [];
15
25
  this.setMaxListeners(1000);
16
- this._incoming = incoming;
17
- const form = (this._form = (0, formidable_1.default)({
18
- ...options,
19
- filter: (part) => !this._cancelled && (!options?.filter || options.filter(part)),
20
- }));
21
- form.once('error', () => {
26
+ this.tempDirectory = options?.tempDirectory || node_os_1.default.tmpdir();
27
+ const { request } = context;
28
+ const form = (0, busboy_1.default)({ headers: request.headers });
29
+ this._form = form;
30
+ form.once('error', (e) => {
22
31
  this._cancelled = true;
32
+ this._finished = true;
23
33
  if (this.listenerCount('error') > 0)
24
- this.emit('error');
34
+ this.emit('error', e);
25
35
  });
26
- form.on('field', (fieldName, value) => {
27
- const item = { fieldName, type: 'field', value };
36
+ form.on('close', () => {
37
+ this._finished = true;
38
+ });
39
+ form.on('field', (field, value, info) => {
40
+ const item = {
41
+ kind: 'field',
42
+ field,
43
+ value,
44
+ mimeType: info.mimeType,
45
+ encoding: info.encoding,
46
+ };
28
47
  this._items.push(item);
29
48
  this._stack.push(item);
30
49
  this.emit('field', item);
31
50
  this.emit('item', item);
32
51
  });
33
- form.on('file', (fieldName, file) => {
34
- const item = { fieldName, type: 'file', file };
35
- this._items.push(item);
36
- this._stack.push(item);
37
- this.emit('file', item);
38
- this.emit('item', item);
52
+ form.on('file', (field, file, info) => {
53
+ const saveTo = node_path_1.default.join(this.tempDirectory, `opra-${generateFileName()}`);
54
+ file.pipe(node_fs_1.default.createWriteStream(saveTo));
55
+ file.once('end', () => {
56
+ const item = {
57
+ kind: 'file',
58
+ field,
59
+ storedPath: saveTo,
60
+ filename: info.filename,
61
+ mimeType: info.mimeType,
62
+ encoding: info.encoding,
63
+ };
64
+ this._items.push(item);
65
+ this._stack.push(item);
66
+ this.emit('file', item);
67
+ this.emit('item', item);
68
+ });
39
69
  });
40
70
  }
41
71
  get items() {
42
72
  return this._items;
43
73
  }
44
- getNext() {
45
- if (!this._form.ended)
74
+ async getNext() {
75
+ let item = this._stack.shift();
76
+ if (!item && !this._finished) {
46
77
  this.resume();
47
- return new Promise((resolve, reject) => {
48
- if (this._stack.length)
49
- return resolve(this._stack.shift());
50
- if (this._form.ended)
51
- return resolve(undefined);
52
- this.once('item', () => resolve(this._stack.shift()));
53
- this.once('error', e => reject(e));
54
- });
78
+ item = await new Promise((resolve, reject) => {
79
+ let resolved = false;
80
+ if (this._stack.length)
81
+ return resolve(this._stack.shift());
82
+ if (this._form.ended)
83
+ return resolve(undefined);
84
+ this._form.once('close', () => {
85
+ if (resolved)
86
+ return;
87
+ resolved = true;
88
+ resolve(this._stack.shift());
89
+ });
90
+ this.once('item', () => {
91
+ this.pause();
92
+ if (resolved)
93
+ return;
94
+ resolved = true;
95
+ resolve(this._stack.shift());
96
+ });
97
+ this.once('error', e => reject(e));
98
+ });
99
+ }
100
+ if (item && this.mediaType) {
101
+ const field = this.mediaType.findMultipartField(item.field);
102
+ if (!field)
103
+ throw new common_1.BadRequestError(`Unknown multipart field (${item.field})`);
104
+ if (item.kind === 'field') {
105
+ const decode = field.generateCodec('decode', { ignoreReadonlyFields: true, projection: '*' });
106
+ item.value = decode(item.value, {
107
+ onFail: issue => `Multipart field (${item.field}) validation failed: ` + issue.message,
108
+ });
109
+ }
110
+ else if (item.kind === 'file') {
111
+ if (field.contentType) {
112
+ const arr = Array.isArray(field.contentType) ? field.contentType : [field.contentType];
113
+ if (!(item.mimeType && arr.find(ct => type_is_1.default.is(item.mimeType, [ct])))) {
114
+ throw new common_1.BadRequestError(`Multipart field (${item.field}) do not accept this content type`);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ /** if all items received we check for required items */
120
+ if (this._finished && this.mediaType && this.mediaType.multipartFields?.length > 0) {
121
+ const fieldsLeft = new Set(this.mediaType.multipartFields);
122
+ for (const x of this._items) {
123
+ const field = this.mediaType.findMultipartField(x.field);
124
+ if (field)
125
+ fieldsLeft.delete(field);
126
+ }
127
+ let issues;
128
+ for (const field of fieldsLeft) {
129
+ if (!field.required)
130
+ continue;
131
+ try {
132
+ (0, valgen_1.isNotNullish)(null, { onFail: () => `Multi part field "${String(field.fieldName)}" is required` });
133
+ }
134
+ catch (e) {
135
+ if (!issues) {
136
+ issues = e.issues;
137
+ this.context.errors.push(e);
138
+ }
139
+ else
140
+ issues.push(...e.issues);
141
+ }
142
+ }
143
+ if (this.context.errors.length)
144
+ throw this.context.errors[0];
145
+ }
146
+ return item;
147
+ }
148
+ async getAll() {
149
+ const items = [...this._items];
150
+ let item;
151
+ while (!this._cancelled && (item = await this.getNext())) {
152
+ items.push(item);
153
+ }
154
+ return items;
55
155
  }
56
- getAll() {
156
+ getAll_() {
57
157
  if (this._form.ended)
58
158
  return Promise.resolve([...this._items]);
59
159
  this.resume();
@@ -70,30 +170,27 @@ class MultipartReader extends events_1.EventEmitter {
70
170
  this.resume();
71
171
  }
72
172
  resume() {
73
- if (!this._started)
74
- this._form.parse(this._incoming, () => undefined);
75
- if (this._form.req)
76
- this._form.resume();
173
+ if (!this._started) {
174
+ this._started = true;
175
+ this.context.request.pipe(this._form);
176
+ }
177
+ this.context.request.resume();
77
178
  }
78
179
  pause() {
79
- if (this._form.req)
80
- this._form.pause();
180
+ this.context.request.pause();
81
181
  }
82
- async deleteTempFiles() {
182
+ async purge() {
83
183
  const promises = [];
84
184
  this._items.forEach(item => {
85
- if (!item.file)
185
+ if (item.kind !== 'file')
86
186
  return;
87
- const file = item.file;
88
- promises.push(new Promise(resolve => {
89
- if (file._writeStream.closed)
90
- return resolve();
91
- file._writeStream.once('close', resolve);
92
- })
93
- .then(() => promises_1.default.unlink(file.filepath))
94
- .then(() => 0));
187
+ promises.push(promises_1.default.unlink(item.storedPath));
95
188
  });
96
189
  return Promise.allSettled(promises);
97
190
  }
98
191
  }
99
192
  exports.MultipartReader = MultipartReader;
193
+ function generateFileName() {
194
+ const buf = Buffer.alloc(10);
195
+ return new Date().toISOString().substring(0, 10).replace(/-/g, '') + (0, node_crypto_1.randomFillSync)(buf).toString('hex');
196
+ }
@@ -149,7 +149,6 @@ class BodyReader extends events_1.EventEmitter {
149
149
  message: 'request aborted',
150
150
  code: 'ECONNABORTED',
151
151
  details: {
152
- length,
153
152
  received: this._receivedSize,
154
153
  },
155
154
  }));
@@ -4,7 +4,10 @@
4
4
  https://github.com/nodejs/
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.validateString = exports.validateHeaderValue = exports.validateHeaderName = exports.hideStackFrames = exports.checkIsHttpToken = void 0;
7
+ exports.validateHeaderValue = exports.validateHeaderName = void 0;
8
+ exports.checkIsHttpToken = checkIsHttpToken;
9
+ exports.hideStackFrames = hideStackFrames;
10
+ exports.validateString = validateString;
8
11
  const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
9
12
  const nodeInternalPrefix = '__node_internal_';
10
13
  /**
@@ -17,7 +20,6 @@ const nodeInternalPrefix = '__node_internal_';
17
20
  function checkIsHttpToken(val) {
18
21
  return typeof val === 'string' && tokenRegExp.exec(val) !== null;
19
22
  }
20
- exports.checkIsHttpToken = checkIsHttpToken;
21
23
  const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
22
24
  /**
23
25
  * True if val contains an invalid field-vchar
@@ -44,7 +46,6 @@ function hideStackFrames(fn) {
44
46
  Object.defineProperty(fn, 'name', { __proto__: null, value: hidden });
45
47
  return fn;
46
48
  }
47
- exports.hideStackFrames = hideStackFrames;
48
49
  exports.validateHeaderName = hideStackFrames((name, label) => {
49
50
  // noinspection SuspiciousTypeOfGuard
50
51
  if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) {
@@ -64,4 +65,3 @@ function validateString(value, name) {
64
65
  throw new TypeError(`Invalid ${name ? name + ' ' : ''}argument. Value must be a string`);
65
66
  }
66
67
  }
67
- exports.validateString = validateString;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.concatReadable = void 0;
3
+ exports.concatReadable = concatReadable;
4
4
  const stream_1 = require("stream");
5
5
  function concatReadable(...streams) {
6
6
  const out = new stream_1.PassThrough();
@@ -17,4 +17,3 @@ function concatReadable(...streams) {
17
17
  pipeNext();
18
18
  return out;
19
19
  }
20
- exports.concatReadable = concatReadable;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.convertToHeadersDistinct = exports.convertToHeaders = void 0;
3
+ exports.convertToHeaders = convertToHeaders;
4
+ exports.convertToHeadersDistinct = convertToHeadersDistinct;
4
5
  const match_known_fields_js_1 = require("./match-known-fields.js");
5
6
  function convertToHeaders(src, dst, joinDuplicateHeaders) {
6
7
  for (let n = 0; n < src.length; n += 2) {
@@ -8,7 +9,6 @@ function convertToHeaders(src, dst, joinDuplicateHeaders) {
8
9
  }
9
10
  return dst;
10
11
  }
11
- exports.convertToHeaders = convertToHeaders;
12
12
  function convertToHeadersDistinct(src, dst) {
13
13
  const count = src.length % 2;
14
14
  for (let n = 0; n < count; n += 2) {
@@ -16,7 +16,6 @@ function convertToHeadersDistinct(src, dst) {
16
16
  }
17
17
  return dst;
18
18
  }
19
- exports.convertToHeadersDistinct = convertToHeadersDistinct;
20
19
  function addHeaderLine(field, value, dest, joinDuplicateHeaders) {
21
20
  if (value == null)
22
21
  return;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.convertToRawHeaders = void 0;
3
+ exports.convertToRawHeaders = convertToRawHeaders;
4
4
  const match_known_fields_js_1 = require("./match-known-fields.js");
5
5
  function convertToRawHeaders(src) {
6
6
  return Object.entries(src).reduce((a, [field, v]) => {
@@ -21,4 +21,3 @@ function convertToRawHeaders(src) {
21
21
  return a;
22
22
  }, []);
23
23
  }
24
- exports.convertToRawHeaders = convertToRawHeaders;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.matchKnownFields = exports.ARRAY_FIELD = exports.SEMICOLON_DELIMITED_FIELD = exports.COMMA_DELIMITED_FIELD = exports.NO_DUPLICATES_FIELD = void 0;
3
+ exports.ARRAY_FIELD = exports.SEMICOLON_DELIMITED_FIELD = exports.COMMA_DELIMITED_FIELD = exports.NO_DUPLICATES_FIELD = void 0;
4
+ exports.matchKnownFields = matchKnownFields;
4
5
  const common_1 = require("@opra/common");
5
6
  exports.NO_DUPLICATES_FIELD = 0;
6
7
  exports.COMMA_DELIMITED_FIELD = 1;
@@ -46,4 +47,3 @@ function matchKnownFields(field) {
46
47
  const x = KNOWN_FIELDS[field.toLowerCase()];
47
48
  return x ? x : [field, exports.COMMA_DELIMITED_FIELD];
48
49
  }
49
- exports.matchKnownFields = matchKnownFields;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.wrapException = void 0;
3
+ exports.wrapException = wrapException;
4
4
  const common_1 = require("@opra/common");
5
5
  function wrapException(error) {
6
6
  if (error instanceof common_1.OpraHttpError)
@@ -31,4 +31,3 @@ function wrapException(error) {
31
31
  return new common_1.InternalServerError(error);
32
32
  }
33
33
  }
34
- exports.wrapException = wrapException;
package/cjs/index.js CHANGED
@@ -10,11 +10,11 @@ const HttpOutgoingHost_ = tslib_1.__importStar(require("./http/impl/http-outgoin
10
10
  const NodeIncomingMessageHost_ = tslib_1.__importStar(require("./http/impl/node-incoming-message.host.js"));
11
11
  const NodeOutgoingMessageHost_ = tslib_1.__importStar(require("./http/impl/node-outgoing-message.host.js"));
12
12
  tslib_1.__exportStar(require("./execution-context.js"), exports);
13
- tslib_1.__exportStar(require("./helpers/logger.js"), exports);
14
13
  tslib_1.__exportStar(require("./helpers/service-base.js"), exports);
15
14
  tslib_1.__exportStar(require("./http/express-adapter.js"), exports);
16
15
  tslib_1.__exportStar(require("./http/http-adapter.js"), exports);
17
16
  tslib_1.__exportStar(require("./http/http-context.js"), exports);
17
+ tslib_1.__exportStar(require("./http/http-handler.js"), exports);
18
18
  tslib_1.__exportStar(require("./http/impl/multipart-reader.js"), exports);
19
19
  tslib_1.__exportStar(require("./http/interfaces/http-incoming.interface.js"), exports);
20
20
  tslib_1.__exportStar(require("./http/interfaces/http-outgoing.interface.js"), exports);
@@ -5,7 +5,6 @@ require("./augmentation/18n.augmentation.js");
5
5
  const common_1 = require("@opra/common");
6
6
  const strict_typed_events_1 = require("strict-typed-events");
7
7
  const constants_js_1 = require("./constants.js");
8
- const logger_js_1 = require("./helpers/logger.js");
9
8
  const asset_cache_js_1 = require("./http/impl/asset-cache.js");
10
9
  /**
11
10
  * @class PlatformAdapter
@@ -15,8 +14,6 @@ class PlatformAdapter extends strict_typed_events_1.AsyncEventEmitter {
15
14
  super();
16
15
  this[constants_js_1.kAssetCache] = new asset_cache_js_1.AssetCache();
17
16
  this.document = document;
18
- this.logger =
19
- options?.logger && options.logger instanceof logger_js_1.Logger ? options.logger : new logger_js_1.Logger({ instance: options?.logger });
20
17
  this.i18n = options?.i18n || common_1.I18n.defaultInstance;
21
18
  }
22
19
  }
@@ -1,23 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isHttpOutgoing = exports.isNodeOutgoingMessage = exports.isHttpIncoming = exports.isNodeIncomingMessage = void 0;
3
+ exports.isNodeIncomingMessage = isNodeIncomingMessage;
4
+ exports.isHttpIncoming = isHttpIncoming;
5
+ exports.isNodeOutgoingMessage = isNodeOutgoingMessage;
6
+ exports.isHttpOutgoing = isHttpOutgoing;
4
7
  const common_1 = require("@opra/common");
5
8
  function isNodeIncomingMessage(v) {
6
9
  return v && typeof v.method === 'string' && Array.isArray(v.rawHeaders) && (0, common_1.isReadable)(v);
7
10
  }
8
- exports.isNodeIncomingMessage = isNodeIncomingMessage;
9
11
  function isHttpIncoming(v) {
10
12
  return (isNodeIncomingMessage(v) &&
11
13
  typeof v.header === 'function' &&
12
14
  typeof v.acceptsLanguages === 'function' &&
13
15
  typeof v.readBody === 'function');
14
16
  }
15
- exports.isHttpIncoming = isHttpIncoming;
16
17
  function isNodeOutgoingMessage(v) {
17
18
  return v && typeof v.getHeaders === 'function' && (0, common_1.isStream)(v);
18
19
  }
19
- exports.isNodeOutgoingMessage = isNodeOutgoingMessage;
20
20
  function isHttpOutgoing(v) {
21
21
  return isNodeOutgoingMessage(v) && typeof v.clearCookie === 'function' && typeof v.cookie === 'function';
22
22
  }
23
- exports.isHttpOutgoing = isHttpOutgoing;
@@ -1,6 +1,6 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { getStackFileName, I18n as I18n_ } from '@opra/common';
2
- import fs from 'fs';
3
- import path from 'path';
4
4
  I18n_.load = async function (options) {
5
5
  const opts = {
6
6
  ...options,
package/esm/constants.js CHANGED
@@ -1,2 +1 @@
1
- export const kHandler = Symbol.for('kHandler');
2
1
  export const kAssetCache = Symbol.for('kAssetCache');
@@ -5,6 +5,7 @@ import { AsyncEventEmitter } from 'strict-typed-events';
5
5
  export class ExecutionContext extends AsyncEventEmitter {
6
6
  constructor(init) {
7
7
  super();
8
+ this.errors = [];
8
9
  this.document = init.document;
9
10
  this.protocol = init.protocol;
10
11
  this.platform = init.platform;
@@ -1,11 +1,11 @@
1
+ import * as nodePath from 'node:path';
1
2
  import { HttpApi, NotFoundError } from '@opra/common';
2
3
  import { Router } from 'express';
3
- import * as nodePath from 'path';
4
- import { kHandler } from '../constants.js';
5
4
  import { HttpAdapter } from './http-adapter.js';
6
5
  import { HttpContext } from './http-context.js';
7
6
  import { HttpIncoming } from './interfaces/http-incoming.interface.js';
8
7
  import { HttpOutgoing } from './interfaces/http-outgoing.interface.js';
8
+ import { wrapException } from './utils/wrap-exception.js';
9
9
  export class ExpressAdapter extends HttpAdapter {
10
10
  constructor(app, document, options) {
11
11
  super(document, options);
@@ -36,7 +36,8 @@ export class ExpressAdapter extends HttpAdapter {
36
36
  await resource.onShutdown.call(instance, resource);
37
37
  }
38
38
  catch (e) {
39
- this.logger.error(e);
39
+ if (this.listenerCount('error'))
40
+ this.emit('error', wrapException(e));
40
41
  }
41
42
  }
42
43
  }
@@ -59,10 +60,10 @@ export class ExpressAdapter extends HttpAdapter {
59
60
  }
60
61
  else
61
62
  this.app.use(router);
62
- const createContext = (_req, _res, args) => {
63
+ const createContext = async (_req, _res, args) => {
63
64
  const request = HttpIncoming.from(_req);
64
65
  const response = HttpOutgoing.from(_res);
65
- return new HttpContext({
66
+ const ctx = new HttpContext({
66
67
  adapter: this,
67
68
  platform: this.platform,
68
69
  request,
@@ -72,12 +73,14 @@ export class ExpressAdapter extends HttpAdapter {
72
73
  operation: args?.operation,
73
74
  operationHandler: args?.operationHandler,
74
75
  });
76
+ await this.emitAsync('createContext', ctx);
77
+ return ctx;
75
78
  };
76
79
  /** Add an endpoint that returns document schema */
77
80
  router.get('/\\$schema', (_req, _res, next) => {
78
- const context = createContext(_req, _res);
79
- this[kHandler].sendDocumentSchema(context).catch(next);
80
- return;
81
+ createContext(_req, _res)
82
+ .then(ctx => this.handler.sendDocumentSchema(ctx).catch(next))
83
+ .catch(next);
81
84
  });
82
85
  /** Add operation endpoints */
83
86
  if (this.api.controllers.size) {
@@ -91,19 +94,18 @@ export class ExpressAdapter extends HttpAdapter {
91
94
  continue;
92
95
  /** Define router callback */
93
96
  router[operation.method.toLowerCase()](routePath, (_req, _res, _next) => {
94
- const context = createContext(_req, _res, {
97
+ createContext(_req, _res, {
95
98
  controller,
96
99
  controllerInstance,
97
100
  operation,
98
101
  operationHandler,
99
- });
100
- this[kHandler]
101
- .handleRequest(context)
102
+ })
103
+ .then(ctx => this.handler.handleRequest(ctx))
102
104
  .then(() => {
103
105
  if (!_res.headersSent)
104
106
  _next();
105
107
  })
106
- .catch((e) => this.logger.fatal(e));
108
+ .catch((e) => this.emit('error', e));
107
109
  });
108
110
  }
109
111
  if (controller.controllers.size) {
@@ -116,18 +118,17 @@ export class ExpressAdapter extends HttpAdapter {
116
118
  }
117
119
  /** Add an endpoint that returns 404 error at last */
118
120
  router.use('*', (_req, _res, next) => {
119
- const res = HttpOutgoing.from(_res);
120
- // const url = new URL(_req.originalUrl, '')
121
- this[kHandler]
122
- .sendErrorResponse(res, [
123
- new NotFoundError({
124
- message: `No endpoint found for [${_req.method}]${_req.baseUrl}`,
121
+ createContext(_req, _res)
122
+ .then(ctx => {
123
+ ctx.errors.push(new NotFoundError({
124
+ message: `No endpoint found at [${_req.method}]${_req.baseUrl}`,
125
125
  details: {
126
126
  path: _req.baseUrl,
127
127
  method: _req.method,
128
128
  },
129
- }),
130
- ])
129
+ }));
130
+ this.handler.sendResponse(ctx).catch(next);
131
+ })
131
132
  .catch(next);
132
133
  });
133
134
  }
@@ -1,7 +1,6 @@
1
1
  import { HttpApi } from '@opra/common';
2
- import { kHandler } from '../constants.js';
3
2
  import { PlatformAdapter } from '../platform-adapter.js';
4
- import { HttpHandler } from './impl/http-handler.js';
3
+ import { HttpHandler } from './http-handler.js';
5
4
  /**
6
5
  *
7
6
  * @class HttpAdapter
@@ -12,10 +11,8 @@ export class HttpAdapter extends PlatformAdapter {
12
11
  this.protocol = 'http';
13
12
  if (!(document.api instanceof HttpApi))
14
13
  throw new TypeError(`The document does not expose an HTTP Api`);
15
- this[kHandler] = new HttpHandler(this);
14
+ this.handler = new HttpHandler(this);
16
15
  this.interceptors = [...(options?.interceptors || [])];
17
- if (options?.onRequest)
18
- this.on('request', options.onRequest);
19
16
  }
20
17
  get api() {
21
18
  return this.document.api;
@@ -1,5 +1,5 @@
1
1
  import typeIs from '@browsery/type-is';
2
- import { BadRequestError, InternalServerError, NotAcceptableError, } from '@opra/common';
2
+ import { InternalServerError, NotAcceptableError, } from '@opra/common';
3
3
  import { vg } from 'valgen';
4
4
  import { kAssetCache } from '../constants.js';
5
5
  import { ExecutionContext } from '../execution-context.js';
@@ -25,6 +25,10 @@ export class HttpContext extends ExecutionContext {
25
25
  this.pathParams = init.pathParams || {};
26
26
  this.queryParams = init.queryParams || {};
27
27
  this._body = init.body;
28
+ this.on('finish', () => {
29
+ if (this._multipartReader)
30
+ this._multipartReader.purge().catch(() => undefined);
31
+ });
28
32
  }
29
33
  get isMultipart() {
30
34
  return !!this.request.is('multipart');
@@ -34,21 +38,21 @@ export class HttpContext extends ExecutionContext {
34
38
  throw new InternalServerError('Request content is not a multipart content');
35
39
  if (this._multipartReader)
36
40
  return this._multipartReader;
37
- const { request, mediaType } = this;
41
+ const { mediaType } = this;
38
42
  if (mediaType?.contentType) {
39
43
  const arr = Array.isArray(mediaType.contentType) ? mediaType.contentType : [mediaType.contentType];
40
44
  const contentType = arr.find(ct => typeIs.is(ct, ['multipart']));
41
45
  if (!contentType)
42
46
  throw new NotAcceptableError('This endpoint does not accept multipart requests');
43
47
  }
44
- const reader = new MultipartReader(request, {
45
- maxFields: mediaType?.maxFields,
46
- maxFieldsSize: mediaType?.maxFieldsSize,
47
- maxFiles: mediaType?.maxFiles,
48
- maxFileSize: mediaType?.maxFileSize,
49
- maxTotalFileSize: mediaType?.maxTotalFileSize,
50
- minFileSize: mediaType?.minFileSize,
51
- });
48
+ const reader = new MultipartReader(this, {
49
+ limits: {
50
+ fields: mediaType?.maxFields,
51
+ fieldSize: mediaType?.maxFieldsSize,
52
+ files: mediaType?.maxFiles,
53
+ fileSize: mediaType?.maxFileSize,
54
+ },
55
+ }, mediaType);
52
56
  this._multipartReader = reader;
53
57
  return reader;
54
58
  }
@@ -61,29 +65,10 @@ export class HttpContext extends ExecutionContext {
61
65
  /** Retrieve all fields */
62
66
  const parts = await reader.getAll();
63
67
  /** Filter fields according to configuration */
64
- this._body = [];
65
- const multipartFields = mediaType?.multipartFields;
66
- if (mediaType && multipartFields?.length) {
67
- const fieldsFound = new Map();
68
- for (const item of parts) {
69
- const field = mediaType.findMultipartField(item.fieldName, item.type);
70
- if (field) {
71
- fieldsFound.set(field, true);
72
- this._body.push(item);
73
- }
74
- }
75
- /** Check required fields */
76
- for (const field of multipartFields) {
77
- if (field.required && !fieldsFound.get(field)) {
78
- throw new BadRequestError({
79
- message: `Multipart field (${field.fieldName}) is required`,
80
- });
81
- }
82
- }
83
- }
68
+ this._body = [...parts];
84
69
  return this._body;
85
70
  }
86
- this._body = await this.request.readBody({ limit: operation.requestBody?.maxContentSize });
71
+ this._body = await this.request.readBody({ limit: operation?.requestBody?.maxContentSize });
87
72
  if (this._body != null) {
88
73
  // Convert Buffer to string if media is text
89
74
  if (Buffer.isBuffer(this._body) && request.is(['json', 'xml', 'txt', 'text'])) {
@@ -100,7 +85,7 @@ export class HttpContext extends ExecutionContext {
100
85
  if (!decode) {
101
86
  decode =
102
87
  mediaType.type?.generateCodec('decode', {
103
- partial: operation.requestBody?.partial,
88
+ partial: operation?.requestBody?.partial,
104
89
  projection: '*',
105
90
  ignoreReadonlyFields: true,
106
91
  }) || vg.isAny();