@opra/http 1.0.0-beta.3

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/cjs/express-adapter.js +155 -0
  4. package/cjs/http-adapter.js +24 -0
  5. package/cjs/http-context.js +103 -0
  6. package/cjs/http-handler.js +609 -0
  7. package/cjs/impl/http-incoming.host.js +112 -0
  8. package/cjs/impl/http-outgoing.host.js +207 -0
  9. package/cjs/impl/multipart-reader.js +196 -0
  10. package/cjs/impl/node-incoming-message.host.js +109 -0
  11. package/cjs/impl/node-outgoing-message.host.js +195 -0
  12. package/cjs/index.js +27 -0
  13. package/cjs/interfaces/http-incoming.interface.js +25 -0
  14. package/cjs/interfaces/http-outgoing.interface.js +22 -0
  15. package/cjs/interfaces/node-incoming-message.interface.js +64 -0
  16. package/cjs/interfaces/node-outgoing-message.interface.js +15 -0
  17. package/cjs/package.json +3 -0
  18. package/cjs/type-guards.js +22 -0
  19. package/cjs/utils/body-reader.js +216 -0
  20. package/cjs/utils/common.js +67 -0
  21. package/cjs/utils/concat-readable.js +19 -0
  22. package/cjs/utils/convert-to-headers.js +64 -0
  23. package/cjs/utils/convert-to-raw-headers.js +23 -0
  24. package/cjs/utils/match-known-fields.js +49 -0
  25. package/cjs/utils/wrap-exception.js +33 -0
  26. package/esm/express-adapter.js +150 -0
  27. package/esm/http-adapter.js +20 -0
  28. package/esm/http-context.js +98 -0
  29. package/esm/http-handler.js +604 -0
  30. package/esm/impl/http-incoming.host.js +107 -0
  31. package/esm/impl/http-outgoing.host.js +202 -0
  32. package/esm/impl/multipart-reader.js +191 -0
  33. package/esm/impl/node-incoming-message.host.js +105 -0
  34. package/esm/impl/node-outgoing-message.host.js +191 -0
  35. package/esm/index.js +23 -0
  36. package/esm/interfaces/http-incoming.interface.js +22 -0
  37. package/esm/interfaces/http-outgoing.interface.js +19 -0
  38. package/esm/interfaces/node-incoming-message.interface.js +61 -0
  39. package/esm/interfaces/node-outgoing-message.interface.js +12 -0
  40. package/esm/package.json +3 -0
  41. package/esm/type-guards.js +16 -0
  42. package/esm/utils/body-reader.js +211 -0
  43. package/esm/utils/common.js +61 -0
  44. package/esm/utils/concat-readable.js +16 -0
  45. package/esm/utils/convert-to-headers.js +60 -0
  46. package/esm/utils/convert-to-raw-headers.js +20 -0
  47. package/esm/utils/match-known-fields.js +45 -0
  48. package/esm/utils/wrap-exception.js +30 -0
  49. package/i18n/en/error.json +21 -0
  50. package/package.json +89 -0
  51. package/types/express-adapter.d.ts +13 -0
  52. package/types/http-adapter.d.ts +32 -0
  53. package/types/http-context.d.ts +44 -0
  54. package/types/http-handler.d.ts +74 -0
  55. package/types/impl/http-incoming.host.d.ts +22 -0
  56. package/types/impl/http-outgoing.host.d.ts +17 -0
  57. package/types/impl/multipart-reader.d.ts +46 -0
  58. package/types/impl/node-incoming-message.host.d.ts +45 -0
  59. package/types/impl/node-outgoing-message.host.d.ts +49 -0
  60. package/types/index.d.cts +22 -0
  61. package/types/index.d.ts +22 -0
  62. package/types/interfaces/http-incoming.interface.d.ts +192 -0
  63. package/types/interfaces/http-outgoing.interface.d.ts +144 -0
  64. package/types/interfaces/node-incoming-message.interface.d.ts +36 -0
  65. package/types/interfaces/node-outgoing-message.interface.d.ts +27 -0
  66. package/types/type-guards.d.ts +8 -0
  67. package/types/utils/body-reader.d.ts +38 -0
  68. package/types/utils/common.d.ts +17 -0
  69. package/types/utils/concat-readable.d.ts +2 -0
  70. package/types/utils/convert-to-headers.d.ts +2 -0
  71. package/types/utils/convert-to-raw-headers.d.ts +2 -0
  72. package/types/utils/match-known-fields.d.ts +6 -0
  73. package/types/utils/wrap-exception.d.ts +2 -0
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.concatReadable = concatReadable;
4
+ const stream_1 = require("stream");
5
+ function concatReadable(...streams) {
6
+ const out = new stream_1.PassThrough();
7
+ const pipeNext = () => {
8
+ const nextStream = streams.shift();
9
+ if (nextStream) {
10
+ nextStream.pipe(out, { end: false });
11
+ nextStream.once('end', () => pipeNext());
12
+ }
13
+ else {
14
+ out.end();
15
+ }
16
+ };
17
+ pipeNext();
18
+ return out;
19
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertToHeaders = convertToHeaders;
4
+ exports.convertToHeadersDistinct = convertToHeadersDistinct;
5
+ const match_known_fields_js_1 = require("./match-known-fields.js");
6
+ function convertToHeaders(src, dst, joinDuplicateHeaders) {
7
+ for (let n = 0; n < src.length; n += 2) {
8
+ addHeaderLine(src[n], src[n + 1], dst, joinDuplicateHeaders);
9
+ }
10
+ return dst;
11
+ }
12
+ function convertToHeadersDistinct(src, dst) {
13
+ const count = src.length % 2;
14
+ for (let n = 0; n < count; n += 2) {
15
+ addHeaderLineDistinct(src[n], src[n + 1], dst);
16
+ }
17
+ return dst;
18
+ }
19
+ function addHeaderLine(field, value, dest, joinDuplicateHeaders) {
20
+ if (value == null)
21
+ return;
22
+ field = field.toLowerCase();
23
+ const [, flag] = (0, match_known_fields_js_1.matchKnownFields)(field);
24
+ // comma(0) or semicolon(2) delimited field
25
+ if (flag === match_known_fields_js_1.COMMA_DELIMITED_FIELD || flag === match_known_fields_js_1.SEMICOLON_DELIMITED_FIELD) {
26
+ // Make a delimited list
27
+ if (typeof dest[field] === 'string') {
28
+ dest[field] += (flag === match_known_fields_js_1.COMMA_DELIMITED_FIELD ? ', ' : '; ') + value;
29
+ }
30
+ else {
31
+ dest[field] = value;
32
+ }
33
+ }
34
+ else if (flag === match_known_fields_js_1.ARRAY_FIELD) {
35
+ // Array header -- only Set-Cookie at the moment
36
+ if (dest['set-cookie'] !== undefined) {
37
+ dest['set-cookie'].push(value);
38
+ }
39
+ else {
40
+ dest['set-cookie'] = [value];
41
+ }
42
+ }
43
+ else if (joinDuplicateHeaders) {
44
+ if (dest[field] === undefined) {
45
+ dest[field] = value;
46
+ }
47
+ else {
48
+ dest[field] += ', ' + value;
49
+ }
50
+ }
51
+ else if (dest[field] === undefined) {
52
+ // Drop duplicates
53
+ dest[field] = value;
54
+ }
55
+ }
56
+ function addHeaderLineDistinct(field, value, dest) {
57
+ field = field.toLowerCase();
58
+ if (!dest[field]) {
59
+ dest[field] = [value];
60
+ }
61
+ else {
62
+ dest[field].push(value);
63
+ }
64
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertToRawHeaders = convertToRawHeaders;
4
+ const match_known_fields_js_1 = require("./match-known-fields.js");
5
+ function convertToRawHeaders(src) {
6
+ return Object.entries(src).reduce((a, [field, v]) => {
7
+ const [name, flag] = (0, match_known_fields_js_1.matchKnownFields)(field);
8
+ if (flag === match_known_fields_js_1.ARRAY_FIELD) {
9
+ if (Array.isArray(v))
10
+ v.forEach(x => a.push(name, String(x)));
11
+ else
12
+ a.push(name, String(v));
13
+ return a;
14
+ }
15
+ if (flag === match_known_fields_js_1.COMMA_DELIMITED_FIELD || flag === match_known_fields_js_1.SEMICOLON_DELIMITED_FIELD) {
16
+ v = Array.isArray(v) ? v.join(flag === match_known_fields_js_1.COMMA_DELIMITED_FIELD ? ', ' : '; ') : String(v);
17
+ }
18
+ else
19
+ v = Array.isArray(v) ? String(v[0]) : String(v);
20
+ a.push(name, v);
21
+ return a;
22
+ }, []);
23
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ARRAY_FIELD = exports.SEMICOLON_DELIMITED_FIELD = exports.COMMA_DELIMITED_FIELD = exports.NO_DUPLICATES_FIELD = void 0;
4
+ exports.matchKnownFields = matchKnownFields;
5
+ const common_1 = require("@opra/common");
6
+ exports.NO_DUPLICATES_FIELD = 0;
7
+ exports.COMMA_DELIMITED_FIELD = 1;
8
+ exports.SEMICOLON_DELIMITED_FIELD = 2;
9
+ exports.ARRAY_FIELD = 3;
10
+ const ARRAY_HEADERS = ['set-cookie'];
11
+ const NO_DUPLICATES_HEADERS = [
12
+ 'age',
13
+ 'from',
14
+ 'etag',
15
+ 'server',
16
+ 'referer',
17
+ 'referrer',
18
+ 'expires',
19
+ 'location',
20
+ 'user-agent',
21
+ 'retry-after',
22
+ 'content-type',
23
+ 'content-length',
24
+ 'max-forwards',
25
+ 'last-modified',
26
+ 'authorization',
27
+ 'proxy-authorization',
28
+ 'if-modified-since',
29
+ 'if-unmodified-since',
30
+ ];
31
+ const SEMICOLON_DELIMITED_HEADERS = ['cookie'];
32
+ const KNOWN_FIELDS = Object.values(common_1.HttpHeaderCodes).reduce((o, k) => {
33
+ const n = k.toLowerCase();
34
+ o[n] = [
35
+ k,
36
+ NO_DUPLICATES_HEADERS.includes(n)
37
+ ? exports.NO_DUPLICATES_FIELD
38
+ : ARRAY_HEADERS.includes(n)
39
+ ? exports.ARRAY_FIELD
40
+ : SEMICOLON_DELIMITED_HEADERS.includes(n)
41
+ ? exports.SEMICOLON_DELIMITED_FIELD
42
+ : exports.COMMA_DELIMITED_FIELD,
43
+ ];
44
+ return o;
45
+ }, {});
46
+ function matchKnownFields(field) {
47
+ const x = KNOWN_FIELDS[field.toLowerCase()];
48
+ return x ? x : [field, exports.COMMA_DELIMITED_FIELD];
49
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrapException = wrapException;
4
+ const common_1 = require("@opra/common");
5
+ function wrapException(error) {
6
+ if (error instanceof common_1.OpraHttpError)
7
+ return error;
8
+ let status = 500;
9
+ if (typeof error.status === 'number')
10
+ status = error.status;
11
+ else if (typeof error.getStatus === 'function')
12
+ status = error.getStatus();
13
+ switch (status) {
14
+ case 400:
15
+ return new common_1.BadRequestError(error);
16
+ case 401:
17
+ return new common_1.UnauthorizedError(error);
18
+ case 403:
19
+ return new common_1.ForbiddenError(error);
20
+ case 404:
21
+ return new common_1.NotFoundError(error);
22
+ case 405:
23
+ return new common_1.MethodNotAllowedError(error);
24
+ case 406:
25
+ return new common_1.NotAcceptableError(error);
26
+ case 422:
27
+ return new common_1.UnprocessableEntityError(error);
28
+ case 424:
29
+ return new common_1.FailedDependencyError(error);
30
+ default:
31
+ return new common_1.InternalServerError(error);
32
+ }
33
+ }
@@ -0,0 +1,150 @@
1
+ import * as nodePath from 'node:path';
2
+ import { HttpApi, NotFoundError } from '@opra/common';
3
+ import { Router } from 'express';
4
+ import { HttpAdapter } from './http-adapter.js';
5
+ import { HttpContext } from './http-context.js';
6
+ import { HttpIncoming } from './interfaces/http-incoming.interface.js';
7
+ import { HttpOutgoing } from './interfaces/http-outgoing.interface.js';
8
+ import { wrapException } from './utils/wrap-exception.js';
9
+ export class ExpressAdapter extends HttpAdapter {
10
+ constructor(app, document, options) {
11
+ super(document, options);
12
+ this._controllerInstances = new Map();
13
+ this.app = app;
14
+ if (!(this.document.api instanceof HttpApi))
15
+ throw new TypeError('document.api must be instance of HttpApi');
16
+ for (const c of this.api.controllers.values())
17
+ this._createControllers(c);
18
+ this._initRouter(options?.basePath);
19
+ }
20
+ get platform() {
21
+ return 'express';
22
+ }
23
+ async close() {
24
+ const processInstance = async (controller) => {
25
+ if (controller.controllers.size) {
26
+ const subResources = Array.from(controller.controllers.values());
27
+ subResources.reverse();
28
+ for (const subResource of subResources) {
29
+ await processInstance(subResource);
30
+ }
31
+ }
32
+ if (controller.onShutdown) {
33
+ const instance = this._controllerInstances.get(controller) || controller.instance;
34
+ if (instance) {
35
+ try {
36
+ await controller.onShutdown.call(instance, controller);
37
+ }
38
+ catch (e) {
39
+ if (this.listenerCount('error'))
40
+ this.emit('error', wrapException(e));
41
+ }
42
+ }
43
+ }
44
+ };
45
+ for (const c of this.api.controllers.values())
46
+ await processInstance(c);
47
+ this._controllerInstances.clear();
48
+ }
49
+ getControllerInstance(controllerPath) {
50
+ const controller = this.api.findController(controllerPath);
51
+ return controller && this._controllerInstances.get(controller);
52
+ }
53
+ _initRouter(basePath) {
54
+ const router = Router();
55
+ if (basePath) {
56
+ if (!basePath.startsWith('/'))
57
+ basePath = '/' + basePath;
58
+ if (basePath)
59
+ this.app.use(basePath, router);
60
+ }
61
+ else
62
+ this.app.use(router);
63
+ const createContext = async (_req, _res, args) => {
64
+ const request = HttpIncoming.from(_req);
65
+ const response = HttpOutgoing.from(_res);
66
+ const ctx = new HttpContext({
67
+ adapter: this,
68
+ platform: this.platform,
69
+ request,
70
+ response,
71
+ controller: args?.controller,
72
+ controllerInstance: args?.controllerInstance,
73
+ operation: args?.operation,
74
+ operationHandler: args?.operationHandler,
75
+ });
76
+ await this.emitAsync('createContext', ctx);
77
+ return ctx;
78
+ };
79
+ /** Add an endpoint that returns document schema */
80
+ router.get('/\\$schema', (_req, _res, next) => {
81
+ createContext(_req, _res)
82
+ .then(ctx => this.handler.sendDocumentSchema(ctx).catch(next))
83
+ .catch(next);
84
+ });
85
+ /** Add operation endpoints */
86
+ if (this.api.controllers.size) {
87
+ const processResource = (controller, currentPath) => {
88
+ currentPath = nodePath.join(currentPath, controller.path);
89
+ for (const operation of controller.operations.values()) {
90
+ const routePath = currentPath + (operation.path || '');
91
+ const controllerInstance = this._controllerInstances.get(controller);
92
+ const operationHandler = controllerInstance[operation.name];
93
+ if (!operationHandler)
94
+ continue;
95
+ /** Define router callback */
96
+ router[operation.method.toLowerCase()](routePath, (_req, _res, _next) => {
97
+ createContext(_req, _res, {
98
+ controller,
99
+ controllerInstance,
100
+ operation,
101
+ operationHandler,
102
+ })
103
+ .then(ctx => this.handler.handleRequest(ctx))
104
+ .then(() => {
105
+ if (!_res.headersSent)
106
+ _next();
107
+ })
108
+ .catch((e) => this.emit('error', e));
109
+ });
110
+ }
111
+ if (controller.controllers.size) {
112
+ for (const child of controller.controllers.values())
113
+ processResource(child, currentPath);
114
+ }
115
+ };
116
+ for (const c of this.api.controllers.values())
117
+ processResource(c, '/');
118
+ }
119
+ /** Add an endpoint that returns 404 error at last */
120
+ router.use('*', (_req, _res, next) => {
121
+ createContext(_req, _res)
122
+ .then(ctx => {
123
+ ctx.errors.push(new NotFoundError({
124
+ message: `No endpoint found at [${_req.method}]${_req.baseUrl}`,
125
+ details: {
126
+ path: _req.baseUrl,
127
+ method: _req.method,
128
+ },
129
+ }));
130
+ this.handler.sendResponse(ctx).catch(next);
131
+ })
132
+ .catch(next);
133
+ });
134
+ }
135
+ _createControllers(controller) {
136
+ let instance = controller.instance;
137
+ if (!instance && controller.ctor)
138
+ instance = new controller.ctor();
139
+ if (instance) {
140
+ if (typeof controller.onInit === 'function')
141
+ controller.onInit.call(instance, controller);
142
+ this._controllerInstances.set(controller, instance);
143
+ // Initialize sub resources
144
+ for (const r of controller.controllers.values()) {
145
+ this._createControllers(r);
146
+ }
147
+ }
148
+ return instance;
149
+ }
150
+ }
@@ -0,0 +1,20 @@
1
+ import { HttpApi } from '@opra/common';
2
+ import { PlatformAdapter } from '@opra/core';
3
+ import { HttpHandler } from './http-handler.js';
4
+ /**
5
+ *
6
+ * @class HttpAdapter
7
+ */
8
+ export class HttpAdapter extends PlatformAdapter {
9
+ constructor(document, options) {
10
+ super(document, options);
11
+ this.protocol = 'http';
12
+ if (!(document.api instanceof HttpApi))
13
+ throw new TypeError(`The document does not expose an HTTP Api`);
14
+ this.handler = new HttpHandler(this);
15
+ this.interceptors = [...(options?.interceptors || [])];
16
+ }
17
+ get api() {
18
+ return this.document.httpApi;
19
+ }
20
+ }
@@ -0,0 +1,98 @@
1
+ import typeIs from '@browsery/type-is';
2
+ import { InternalServerError, NotAcceptableError, } from '@opra/common';
3
+ import { ExecutionContext, kAssetCache } from '@opra/core';
4
+ import { vg } from 'valgen';
5
+ import { MultipartReader } from './impl/multipart-reader.js';
6
+ export class HttpContext extends ExecutionContext {
7
+ constructor(init) {
8
+ super({ ...init, document: init.adapter.document, protocol: 'http' });
9
+ this.adapter = init.adapter;
10
+ this.protocol = 'http';
11
+ if (init.controller)
12
+ this.controller = init.controller;
13
+ if (init.controllerInstance)
14
+ this.controllerInstance = init.controllerInstance;
15
+ if (init.operation)
16
+ this.operation = init.operation;
17
+ if (init.operationHandler)
18
+ this.operationHandler = init.operationHandler;
19
+ this.request = init.request;
20
+ this.response = init.response;
21
+ this.mediaType = init.mediaType;
22
+ this.cookies = init.cookies || {};
23
+ this.headers = init.headers || {};
24
+ this.pathParams = init.pathParams || {};
25
+ this.queryParams = init.queryParams || {};
26
+ this._body = init.body;
27
+ this.on('finish', () => {
28
+ if (this._multipartReader)
29
+ this._multipartReader.purge().catch(() => undefined);
30
+ });
31
+ }
32
+ get isMultipart() {
33
+ return !!this.request.is('multipart');
34
+ }
35
+ async getMultipartReader() {
36
+ if (!this.isMultipart)
37
+ throw new InternalServerError('Request content is not a multipart content');
38
+ if (this._multipartReader)
39
+ return this._multipartReader;
40
+ const { mediaType } = this;
41
+ if (mediaType?.contentType) {
42
+ const arr = Array.isArray(mediaType.contentType) ? mediaType.contentType : [mediaType.contentType];
43
+ const contentType = arr.find(ct => typeIs.is(ct, ['multipart']));
44
+ if (!contentType)
45
+ throw new NotAcceptableError('This endpoint does not accept multipart requests');
46
+ }
47
+ const reader = new MultipartReader(this, {
48
+ limits: {
49
+ fields: mediaType?.maxFields,
50
+ fieldSize: mediaType?.maxFieldsSize,
51
+ files: mediaType?.maxFiles,
52
+ fileSize: mediaType?.maxFileSize,
53
+ },
54
+ }, mediaType);
55
+ this._multipartReader = reader;
56
+ return reader;
57
+ }
58
+ async getBody() {
59
+ if (this._body !== undefined)
60
+ return this._body;
61
+ const { request, operation, mediaType } = this;
62
+ if (this.isMultipart) {
63
+ const reader = await this.getMultipartReader();
64
+ /** Retrieve all fields */
65
+ const parts = await reader.getAll();
66
+ /** Filter fields according to configuration */
67
+ this._body = [...parts];
68
+ return this._body;
69
+ }
70
+ this._body = await this.request.readBody({ limit: operation?.requestBody?.maxContentSize });
71
+ if (this._body != null) {
72
+ // Convert Buffer to string if media is text
73
+ if (Buffer.isBuffer(this._body) && request.is(['json', 'xml', 'txt', 'text'])) {
74
+ this._body = this._body.toString('utf-8');
75
+ }
76
+ // Transform text to Object if media is JSON
77
+ if (typeof this._body === 'string' && request.is(['json']))
78
+ this._body = JSON.parse(this._body);
79
+ }
80
+ if (mediaType) {
81
+ // Decode/Validate the data object according to data model
82
+ if (this._body && mediaType.type) {
83
+ let decode = this.adapter[kAssetCache].get(mediaType, 'decode');
84
+ if (!decode) {
85
+ decode =
86
+ mediaType.type?.generateCodec('decode', {
87
+ partial: operation?.requestBody?.partial,
88
+ projection: '*',
89
+ ignoreReadonlyFields: true,
90
+ }) || vg.isAny();
91
+ this.adapter[kAssetCache].set(mediaType, 'decode', decode);
92
+ }
93
+ this._body = decode(this._body);
94
+ }
95
+ }
96
+ return this._body;
97
+ }
98
+ }