@opra/core 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/adapter/execution-context.host.js +48 -0
- package/cjs/adapter/http/express-adapter.host.js +24 -0
- package/cjs/adapter/http/express-adapter.js +12 -45
- package/cjs/adapter/http/helpers/concat-readable.js +20 -0
- package/cjs/adapter/http/helpers/multipart-helper.js +96 -0
- package/cjs/adapter/http/helpers/query-parsers.js +16 -0
- package/cjs/adapter/http/http-adapter-base.js +127 -0
- package/cjs/adapter/http/http-adapter.host.js +57 -0
- package/cjs/adapter/http/http-adapter.js +11 -129
- package/cjs/adapter/http/{impl/http-server-request.js → http-server-request.js} +11 -5
- package/cjs/adapter/http/{impl/http-server-response.js → http-server-response.js} +22 -22
- package/cjs/adapter/http/impl/http-incoming-message.host.js +148 -0
- package/cjs/adapter/http/impl/{http-outgoing-message-host.js → http-outgoing-message.host.js} +26 -38
- package/cjs/adapter/http/request-handlers/entity-request-handler.js +378 -0
- package/cjs/adapter/http/request-handlers/request-handler-base.js +27 -0
- package/cjs/adapter/http/request-handlers/storage-request-handler.js +134 -0
- package/cjs/adapter/operation-context.js +16 -0
- package/cjs/adapter/platform-adapter.host.js +107 -0
- package/cjs/adapter/request.host.js +1 -2
- package/cjs/adapter/request.js +2 -0
- package/cjs/adapter/response.js +2 -0
- package/cjs/adapter/services/logger.js +36 -0
- package/cjs/augmentation/collection.augmentation.js +2 -0
- package/cjs/augmentation/singleton.augmentation.js +2 -0
- package/cjs/augmentation/storage.augmentation.js +2 -0
- package/cjs/index.js +15 -9
- package/esm/adapter/execution-context.host.js +44 -0
- package/esm/adapter/http/express-adapter.host.js +20 -0
- package/esm/adapter/http/express-adapter.js +11 -20
- package/esm/adapter/http/helpers/concat-readable.js +16 -0
- package/esm/adapter/http/helpers/multipart-helper.js +91 -0
- package/esm/adapter/http/helpers/query-parsers.js +12 -0
- package/esm/adapter/http/http-adapter-base.js +123 -0
- package/esm/adapter/http/http-adapter.host.js +52 -0
- package/esm/adapter/http/http-adapter.js +11 -128
- package/esm/adapter/http/{impl/http-server-request.js → http-server-request.js} +12 -6
- package/esm/adapter/http/{impl/http-server-response.js → http-server-response.js} +22 -22
- package/esm/adapter/http/impl/http-incoming-message.host.js +144 -0
- package/esm/adapter/http/impl/{http-outgoing-message-host.js → http-outgoing-message.host.js} +25 -36
- package/esm/adapter/http/request-handlers/entity-request-handler.js +373 -0
- package/esm/adapter/http/request-handlers/request-handler-base.js +23 -0
- package/esm/adapter/http/request-handlers/storage-request-handler.js +129 -0
- package/esm/adapter/operation-context.js +13 -0
- package/esm/adapter/platform-adapter.host.js +102 -0
- package/esm/adapter/request.host.js +1 -2
- package/esm/adapter/request.js +1 -0
- package/esm/adapter/response.js +1 -0
- package/esm/adapter/services/logger.js +32 -0
- package/esm/augmentation/collection.augmentation.js +1 -0
- package/esm/augmentation/singleton.augmentation.js +1 -0
- package/esm/augmentation/storage.augmentation.js +1 -0
- package/esm/index.js +15 -9
- package/i18n/en/error.json +5 -2
- package/package.json +8 -7
- package/types/adapter/execution-context.d.ts +31 -0
- package/types/adapter/execution-context.host.d.ts +27 -0
- package/types/adapter/http/express-adapter.d.ts +12 -8
- package/types/adapter/http/express-adapter.host.d.ts +11 -0
- package/types/adapter/http/helpers/concat-readable.d.ts +3 -0
- package/types/adapter/http/helpers/multipart-helper.d.ts +25 -0
- package/types/adapter/http/helpers/query-parsers.d.ts +1 -0
- package/types/adapter/http/http-adapter-base.d.ts +23 -0
- package/types/adapter/http/http-adapter.d.ts +13 -29
- package/types/adapter/http/http-adapter.host.d.ts +18 -0
- package/types/adapter/http/{impl/http-server-request.d.ts → http-server-request.d.ts} +7 -6
- package/types/adapter/http/{impl/http-server-response.d.ts → http-server-response.d.ts} +2 -2
- package/types/adapter/http/impl/{http-incoming-message-host.d.ts → http-incoming-message.host.d.ts} +16 -12
- package/types/adapter/http/impl/{http-outgoing-message-host.d.ts → http-outgoing-message.host.d.ts} +12 -16
- package/types/adapter/http/request-handlers/entity-request-handler.d.ts +24 -0
- package/types/adapter/http/request-handlers/request-handler-base.d.ts +15 -0
- package/types/adapter/http/request-handlers/storage-request-handler.d.ts +23 -0
- package/types/adapter/interfaces/logger.interface.d.ts +7 -6
- package/types/adapter/interfaces/request-handler.interface.d.ts +4 -0
- package/types/adapter/operation-context.d.ts +11 -0
- package/types/adapter/{adapter.d.ts → platform-adapter.d.ts} +18 -28
- package/types/adapter/platform-adapter.host.d.ts +31 -0
- package/types/adapter/request.d.ts +11 -0
- package/types/adapter/request.host.d.ts +12 -21
- package/types/adapter/{interfaces/response.interface.d.ts → response.d.ts} +2 -2
- package/types/adapter/response.host.d.ts +2 -2
- package/types/adapter/services/logger.d.ts +14 -0
- package/types/augmentation/collection.augmentation.d.ts +112 -0
- package/types/augmentation/singleton.augmentation.d.ts +64 -0
- package/types/augmentation/storage.augmentation.d.ts +39 -0
- package/types/index.d.ts +15 -9
- package/cjs/adapter/adapter.js +0 -118
- package/cjs/adapter/http/impl/http-incoming-message-host.js +0 -127
- package/cjs/adapter/http/request-parsers/parse-collection-request.js +0 -165
- package/cjs/adapter/http/request-parsers/parse-request.js +0 -24
- package/cjs/adapter/http/request-parsers/parse-singleton-request.js +0 -96
- package/cjs/adapter/internal/metadata.resource.js +0 -26
- package/cjs/adapter/request-context.host.js +0 -44
- package/cjs/shared/collection-resource-base.js +0 -20
- package/esm/adapter/adapter.js +0 -113
- package/esm/adapter/http/impl/http-incoming-message-host.js +0 -122
- package/esm/adapter/http/request-parsers/parse-collection-request.js +0 -161
- package/esm/adapter/http/request-parsers/parse-request.js +0 -20
- package/esm/adapter/http/request-parsers/parse-singleton-request.js +0 -92
- package/esm/adapter/internal/metadata.resource.js +0 -23
- package/esm/adapter/request-context.host.js +0 -40
- package/esm/shared/collection-resource-base.js +0 -16
- package/types/adapter/http/request-parsers/parse-collection-request.d.ts +0 -4
- package/types/adapter/http/request-parsers/parse-request.d.ts +0 -4
- package/types/adapter/http/request-parsers/parse-singleton-request.d.ts +0 -4
- package/types/adapter/interfaces/request-context.interface.d.ts +0 -31
- package/types/adapter/interfaces/request.interface.d.ts +0 -15
- package/types/adapter/internal/metadata.resource.d.ts +0 -7
- package/types/adapter/request-context.host.d.ts +0 -22
- package/types/shared/collection-resource-base.d.ts +0 -11
- /package/cjs/adapter/{interfaces/request-context.interface.js → execution-context.js} +0 -0
- /package/cjs/adapter/http/{request-parsers/batch-request-parser.js → request-handlers/parse-batch-request.js} +0 -0
- /package/cjs/adapter/interfaces/{request.interface.js → request-handler.interface.js} +0 -0
- /package/cjs/adapter/{interfaces/response.interface.js → platform-adapter.js} +0 -0
- /package/esm/adapter/{interfaces/request-context.interface.js → execution-context.js} +0 -0
- /package/esm/adapter/http/{request-parsers/batch-request-parser.js → request-handlers/parse-batch-request.js} +0 -0
- /package/esm/adapter/interfaces/{request.interface.js → request-handler.interface.js} +0 -0
- /package/esm/adapter/{interfaces/response.interface.js → platform-adapter.js} +0 -0
- /package/types/adapter/http/{request-parsers/batch-request-parser.d.ts → request-handlers/parse-batch-request.d.ts} +0 -0
|
@@ -12,20 +12,20 @@ import path from 'path';
|
|
|
12
12
|
import { toString } from 'putil-varhelpers';
|
|
13
13
|
import vary from 'vary';
|
|
14
14
|
import { HttpStatusCodes, isStream, mergePrototype } from '@opra/common';
|
|
15
|
-
import { HttpOutgoingMessageHost } from './http-outgoing-message
|
|
15
|
+
import { HttpOutgoingMessageHost } from './impl/http-outgoing-message.host.js';
|
|
16
16
|
const charsetRegExp = /;\s*charset\s*=/;
|
|
17
17
|
function isHttpIncomingMessage(v) {
|
|
18
18
|
return v && typeof v.getHeaders === 'function' && isStream(v);
|
|
19
19
|
}
|
|
20
20
|
export var HttpServerResponse;
|
|
21
21
|
(function (HttpServerResponse) {
|
|
22
|
-
function
|
|
22
|
+
function from(instance) {
|
|
23
23
|
if (!isHttpIncomingMessage(instance))
|
|
24
|
-
instance = HttpOutgoingMessageHost
|
|
24
|
+
instance = new HttpOutgoingMessageHost(instance);
|
|
25
25
|
mergePrototype(instance, HttpServerResponseHost.prototype);
|
|
26
26
|
return instance;
|
|
27
27
|
}
|
|
28
|
-
HttpServerResponse.
|
|
28
|
+
HttpServerResponse.from = from;
|
|
29
29
|
})(HttpServerResponse || (HttpServerResponse = {}));
|
|
30
30
|
class HttpServerResponseHost {
|
|
31
31
|
attachment(filename) {
|
|
@@ -43,28 +43,28 @@ class HttpServerResponseHost {
|
|
|
43
43
|
}
|
|
44
44
|
setHeader(field, val) {
|
|
45
45
|
const setHeader = Object.getPrototypeOf(this).setHeader;
|
|
46
|
-
if (typeof field === '
|
|
47
|
-
let value = Array.isArray(val)
|
|
48
|
-
? val.map(String)
|
|
49
|
-
: (val ? String(val) : '');
|
|
50
|
-
// add charset to content-type
|
|
51
|
-
if (field.toLowerCase() === 'content-type') {
|
|
52
|
-
if (Array.isArray(value)) {
|
|
53
|
-
throw new TypeError('Content-Type cannot be set to an Array');
|
|
54
|
-
}
|
|
55
|
-
if (!charsetRegExp.test(value)) {
|
|
56
|
-
const charset = mime.charsets.lookup(value.split(';')[0]);
|
|
57
|
-
if (charset)
|
|
58
|
-
value += '; charset=' + charset.toLowerCase();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
setHeader.call(this, field, value);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
46
|
+
if (typeof field === 'object') {
|
|
64
47
|
for (const [k, v] of Object.entries(field)) {
|
|
65
48
|
this.setHeader(k, v);
|
|
66
49
|
}
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
const fieldLower = field.toLowerCase();
|
|
53
|
+
let value = Array.isArray(val)
|
|
54
|
+
? val.map(String)
|
|
55
|
+
: (val ? String(val) : '');
|
|
56
|
+
// add charset to content-type
|
|
57
|
+
if (fieldLower === 'content-type') {
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
throw new TypeError('Content-Type cannot be set to an Array');
|
|
60
|
+
}
|
|
61
|
+
if (!charsetRegExp.test(value)) {
|
|
62
|
+
const charset = mime.charsets.lookup(value.split(';')[0]);
|
|
63
|
+
if (charset)
|
|
64
|
+
value += '; charset=' + charset.toLowerCase();
|
|
65
|
+
}
|
|
67
66
|
}
|
|
67
|
+
setHeader.call(this, field, value);
|
|
68
68
|
return this;
|
|
69
69
|
}
|
|
70
70
|
clearCookie(name, options) {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file contains code blocks from open source NodeJs project
|
|
3
|
+
https://github.com/nodejs/
|
|
4
|
+
*/
|
|
5
|
+
import { Duplex, Readable } from 'stream';
|
|
6
|
+
import { HTTPParser } from '@browsery/http-parser';
|
|
7
|
+
import { isAsyncIterable, isIterable } from '@opra/common';
|
|
8
|
+
import { concatReadable } from '../helpers/concat-readable.js';
|
|
9
|
+
import { convertToHeaders, convertToHeadersDistinct } from '../helpers/convert-to-headers.js';
|
|
10
|
+
import { convertToRawHeaders } from '../helpers/convert-to-raw-headers.js';
|
|
11
|
+
export const CRLF = Buffer.from('\r\n');
|
|
12
|
+
export const kHeaders = Symbol.for('kHeaders');
|
|
13
|
+
export const kHeadersDistinct = Symbol.for('kHeadersDistinct');
|
|
14
|
+
export const kTrailers = Symbol.for('kTrailers');
|
|
15
|
+
export const kTrailersDistinct = Symbol.for('kTrailersDistinct');
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @class HttpIncomingMessageHost
|
|
19
|
+
*/
|
|
20
|
+
export class HttpIncomingMessageHost extends Duplex {
|
|
21
|
+
constructor(init) {
|
|
22
|
+
super();
|
|
23
|
+
this.rawHeaders = [];
|
|
24
|
+
this.rawTrailers = [];
|
|
25
|
+
this.complete = false;
|
|
26
|
+
this.joinDuplicateHeaders = false;
|
|
27
|
+
if (init) {
|
|
28
|
+
this.complete = true;
|
|
29
|
+
this.httpVersionMajor = init.httpVersionMajor || 1;
|
|
30
|
+
this.httpVersionMinor = init.httpVersionMinor || 0;
|
|
31
|
+
this.method = (init.method || 'GET').toUpperCase();
|
|
32
|
+
this.url = init.url || '';
|
|
33
|
+
if (init.body != null) {
|
|
34
|
+
if (Buffer.isBuffer(init.body))
|
|
35
|
+
this.body = init.body;
|
|
36
|
+
else if (typeof init.body === 'string')
|
|
37
|
+
this.body = Buffer.from(init.body, 'utf-8');
|
|
38
|
+
else
|
|
39
|
+
this.body = Buffer.from(JSON.stringify(init.body), 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
if (init.headers)
|
|
42
|
+
this.rawHeaders = Array.isArray(init.headers) ? init.headers : convertToRawHeaders(init.headers);
|
|
43
|
+
if (init.trailers)
|
|
44
|
+
this.rawTrailers = Array.isArray(init.trailers) ? init.trailers : convertToRawHeaders(init.trailers);
|
|
45
|
+
this.ip = init.ip || '';
|
|
46
|
+
this.ips = init.ips || (this.ip ? [this.ip] : []);
|
|
47
|
+
if (this.body && !this.headers['content-length'])
|
|
48
|
+
this.headers['content-length'] = String(this.body.length);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
get httpVersion() {
|
|
52
|
+
return this.httpVersionMajor
|
|
53
|
+
? this.httpVersionMajor + '.' + this.httpVersionMinor
|
|
54
|
+
: '';
|
|
55
|
+
}
|
|
56
|
+
get headers() {
|
|
57
|
+
if (!this[kHeaders])
|
|
58
|
+
this[kHeaders] = convertToHeaders(this.rawHeaders, {}, this.joinDuplicateHeaders);
|
|
59
|
+
return this[kHeaders];
|
|
60
|
+
}
|
|
61
|
+
set headers(headers) {
|
|
62
|
+
this[kHeaders] = headers;
|
|
63
|
+
}
|
|
64
|
+
get headersDistinct() {
|
|
65
|
+
if (!this[kHeadersDistinct])
|
|
66
|
+
this[kHeadersDistinct] = convertToHeadersDistinct(this.rawHeaders, {});
|
|
67
|
+
return this[kHeadersDistinct];
|
|
68
|
+
}
|
|
69
|
+
get trailers() {
|
|
70
|
+
if (!this[kTrailers])
|
|
71
|
+
this[kTrailers] = convertToHeaders(this.rawTrailers, {}, this.joinDuplicateHeaders);
|
|
72
|
+
return this[kTrailers];
|
|
73
|
+
}
|
|
74
|
+
set trailers(trailers) {
|
|
75
|
+
this[kTrailers] = trailers;
|
|
76
|
+
}
|
|
77
|
+
get trailersDistinct() {
|
|
78
|
+
if (!this[kTrailersDistinct])
|
|
79
|
+
this[kTrailersDistinct] = convertToHeadersDistinct(this.rawTrailers, {});
|
|
80
|
+
return this[kTrailersDistinct];
|
|
81
|
+
}
|
|
82
|
+
_read(size) {
|
|
83
|
+
if (!this.body) {
|
|
84
|
+
this.push(null);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!this._readStream) {
|
|
88
|
+
if (isIterable(this.body) || isAsyncIterable(this.body))
|
|
89
|
+
this._readStream = Readable.from(this.body);
|
|
90
|
+
else if (typeof this.body === 'string') {
|
|
91
|
+
this._readStream = Readable.from(Buffer.from(this.body, 'utf-8'));
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
this._readStream = Readable.from(Buffer.from(JSON.stringify(this.body), 'utf-8'));
|
|
95
|
+
}
|
|
96
|
+
const chunk = this._readStream.read(size);
|
|
97
|
+
this.push(chunk);
|
|
98
|
+
// this.push(null);s
|
|
99
|
+
}
|
|
100
|
+
_write(chunk, encoding, callback) {
|
|
101
|
+
const error = this._httpParser?.execute(chunk);
|
|
102
|
+
if (error && typeof error === 'object')
|
|
103
|
+
callback(error);
|
|
104
|
+
else
|
|
105
|
+
callback();
|
|
106
|
+
}
|
|
107
|
+
static from(iterable) {
|
|
108
|
+
if (typeof iterable === 'object' && !(isIterable(iterable) || isAsyncIterable(iterable)))
|
|
109
|
+
return new HttpIncomingMessageHost(iterable);
|
|
110
|
+
const msg = new HttpIncomingMessageHost();
|
|
111
|
+
const parser = msg._httpParser = new HTTPParser(HTTPParser.REQUEST);
|
|
112
|
+
let bodyChunks;
|
|
113
|
+
parser[HTTPParser.kOnHeadersComplete] = (info) => {
|
|
114
|
+
msg.httpVersionMajor = info.versionMajor;
|
|
115
|
+
msg.httpVersionMinor = info.versionMinor;
|
|
116
|
+
msg.rawHeaders = info.headers;
|
|
117
|
+
msg.method = HTTPParser.methods[info.method];
|
|
118
|
+
msg.url = info.url;
|
|
119
|
+
};
|
|
120
|
+
parser[HTTPParser.kOnHeaders] = (trailers) => {
|
|
121
|
+
msg.rawTrailers = trailers;
|
|
122
|
+
};
|
|
123
|
+
parser[HTTPParser.kOnBody] = (chunk, offset, length) => {
|
|
124
|
+
bodyChunks = bodyChunks || [];
|
|
125
|
+
bodyChunks.push(chunk.subarray(offset, offset + length));
|
|
126
|
+
};
|
|
127
|
+
parser[HTTPParser.kOnMessageComplete] = () => {
|
|
128
|
+
msg.complete = true;
|
|
129
|
+
if (bodyChunks)
|
|
130
|
+
msg.body = Buffer.concat(bodyChunks);
|
|
131
|
+
};
|
|
132
|
+
const readable = concatReadable(Readable.from(iterable), Readable.from(CRLF));
|
|
133
|
+
msg.once('finish', () => parser.finish());
|
|
134
|
+
readable.pipe(msg);
|
|
135
|
+
return msg;
|
|
136
|
+
}
|
|
137
|
+
static async fromAsync(iterable) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const msg = this.from(iterable);
|
|
140
|
+
msg.once('finish', () => resolve(msg));
|
|
141
|
+
msg.once('error', (error) => reject(error));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
package/esm/adapter/http/impl/{http-outgoing-message-host.js → http-outgoing-message.host.js}
RENAMED
|
@@ -2,33 +2,40 @@
|
|
|
2
2
|
This file contains code blocks from open source NodeJs project
|
|
3
3
|
https://github.com/nodejs/
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
import * as stream from 'stream';
|
|
5
|
+
import { Duplex } from 'stream';
|
|
7
6
|
import { validateHeaderName, validateHeaderValue, validateString } from '../helpers/common.js';
|
|
8
|
-
export const kOutHeaders = Symbol('kOutHeaders');
|
|
9
|
-
export const kOutTrailers = Symbol('kOutTrailers');
|
|
10
|
-
export const kErrored = Symbol('kErrored');
|
|
7
|
+
export const kOutHeaders = Symbol.for('kOutHeaders');
|
|
8
|
+
export const kOutTrailers = Symbol.for('kOutTrailers');
|
|
11
9
|
// noinspection JSUnusedLocalSymbols
|
|
12
10
|
/**
|
|
13
11
|
*
|
|
14
12
|
* @class HttpOutgoingMessageHost
|
|
15
13
|
*/
|
|
16
|
-
export class HttpOutgoingMessageHost {
|
|
17
|
-
constructor() {
|
|
14
|
+
export class HttpOutgoingMessageHost extends Duplex {
|
|
15
|
+
constructor(init) {
|
|
16
|
+
super();
|
|
18
17
|
this._headersSent = false;
|
|
19
|
-
this._closed = false;
|
|
20
|
-
this[_a] = null;
|
|
21
18
|
this.finished = false;
|
|
22
|
-
|
|
19
|
+
if (init) {
|
|
20
|
+
this.req = init.req;
|
|
21
|
+
this.statusCode = init?.statusCode || 0;
|
|
22
|
+
this.statusMessage = init?.statusMessage || '';
|
|
23
|
+
this.chunkedEncoding = !!init?.chunkedEncoding;
|
|
24
|
+
this.sendDate = !!init?.sendDate;
|
|
25
|
+
this.strictContentLength = !!init?.strictContentLength;
|
|
26
|
+
if (init.headers)
|
|
27
|
+
this.setHeaders(Array.isArray(init.headers) ? new Map([init.headers]) : init.headers);
|
|
28
|
+
this.body = init.body;
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
|
-
get
|
|
25
|
-
return this.
|
|
31
|
+
get httpVersionMajor() {
|
|
32
|
+
return this.req?.httpVersionMajor || this._httpVersionMajor;
|
|
26
33
|
}
|
|
27
|
-
get
|
|
28
|
-
return this.
|
|
34
|
+
get httpVersionMinor() {
|
|
35
|
+
return this.req?.httpVersionMinor || this._httpVersionMinor;
|
|
29
36
|
}
|
|
30
|
-
get
|
|
31
|
-
return this
|
|
37
|
+
get headersSent() {
|
|
38
|
+
return this._headersSent;
|
|
32
39
|
}
|
|
33
40
|
appendHeader(name, value) {
|
|
34
41
|
if (this.headersSent)
|
|
@@ -181,25 +188,7 @@ export class HttpOutgoingMessageHost {
|
|
|
181
188
|
//
|
|
182
189
|
return this;
|
|
183
190
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.statusCode = init?.statusCode || 0;
|
|
187
|
-
this.statusMessage = init?.statusMessage || '';
|
|
188
|
-
this.chunkedEncoding = !!init?.chunkedEncoding;
|
|
189
|
-
this.sendDate = !!init?.sendDate;
|
|
190
|
-
this.strictContentLength = !!init?.strictContentLength;
|
|
191
|
-
if (init.headers) {
|
|
192
|
-
this.setHeaders(Array.isArray(init.headers) ? new Map([init.headers]) : init.headers);
|
|
193
|
-
}
|
|
194
|
-
this.body = init.body;
|
|
195
|
-
}
|
|
196
|
-
static create(init) {
|
|
197
|
-
const msg = new HttpOutgoingMessageHost();
|
|
198
|
-
if (init)
|
|
199
|
-
msg._readConfig(init);
|
|
200
|
-
return msg;
|
|
191
|
+
static from(init) {
|
|
192
|
+
return new HttpOutgoingMessageHost(init);
|
|
201
193
|
}
|
|
202
194
|
}
|
|
203
|
-
_a = kErrored;
|
|
204
|
-
// Apply mixins
|
|
205
|
-
Object.assign(HttpOutgoingMessageHost.prototype, stream.Stream.prototype);
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import bodyParser from 'body-parser';
|
|
2
|
+
import { toBoolean, toInt } from 'putil-varhelpers';
|
|
3
|
+
import { BadRequestError, Collection, HttpHeaderCodes, HttpStatusCodes, InternalServerError, MethodNotAllowedError, OpraException, ResourceNotFoundError, Singleton, } from '@opra/common';
|
|
4
|
+
import { OperationContext } from '../../operation-context.js';
|
|
5
|
+
import { RequestHost } from '../../request.host.js';
|
|
6
|
+
import { ResponseHost } from '../../response.host.js';
|
|
7
|
+
import { parseArrayParam } from '../helpers/query-parsers.js';
|
|
8
|
+
import { RequestHandlerBase } from './request-handler-base.js';
|
|
9
|
+
/**
|
|
10
|
+
* @class EntityRequestHandler
|
|
11
|
+
*/
|
|
12
|
+
export class EntityRequestHandler extends RequestHandlerBase {
|
|
13
|
+
constructor(adapter) {
|
|
14
|
+
super(adapter);
|
|
15
|
+
this.adapter = adapter;
|
|
16
|
+
this.bodyLoaders = new WeakMap();
|
|
17
|
+
}
|
|
18
|
+
async processRequest(executionContext) {
|
|
19
|
+
const { incoming, outgoing } = executionContext.switchToHttp();
|
|
20
|
+
// Parse incoming message and create Request object
|
|
21
|
+
const request = await this.parseRequest(incoming);
|
|
22
|
+
if (!request)
|
|
23
|
+
return;
|
|
24
|
+
const response = new ResponseHost({ http: outgoing });
|
|
25
|
+
const context = OperationContext.from(executionContext, request, response);
|
|
26
|
+
// Execute operation
|
|
27
|
+
await this.executeOperation(context);
|
|
28
|
+
if (response.errors.length) {
|
|
29
|
+
context.errors.push(...response.errors);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await this.sendResponse(context);
|
|
33
|
+
}
|
|
34
|
+
async parseRequest(incoming) {
|
|
35
|
+
const p = incoming.parsedUrl.path[0];
|
|
36
|
+
const resource = this.adapter.api.getResource(p.resource);
|
|
37
|
+
try {
|
|
38
|
+
if (resource instanceof Collection)
|
|
39
|
+
return await this.parseCollectionRequest(resource, incoming);
|
|
40
|
+
if (resource instanceof Singleton)
|
|
41
|
+
return await this.parseSingletonRequest(resource, incoming);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e instanceof OpraException)
|
|
45
|
+
throw e;
|
|
46
|
+
throw new BadRequestError(e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async executeOperation(context) {
|
|
50
|
+
const request = context.request;
|
|
51
|
+
const { response } = context;
|
|
52
|
+
const resource = request.resource;
|
|
53
|
+
// Call operation handler method
|
|
54
|
+
let value;
|
|
55
|
+
try {
|
|
56
|
+
value = await request.controller[request.operation].call(request.controller, context);
|
|
57
|
+
if (value == null)
|
|
58
|
+
value = response.value;
|
|
59
|
+
const { operation } = request;
|
|
60
|
+
if (operation === 'delete' || operation === 'deleteMany' || operation === 'updateMany') {
|
|
61
|
+
let affected = 0;
|
|
62
|
+
if (typeof value === 'number')
|
|
63
|
+
affected = value;
|
|
64
|
+
if (typeof value === 'boolean')
|
|
65
|
+
affected = value ? 1 : 0;
|
|
66
|
+
if (typeof value === 'object')
|
|
67
|
+
affected = value.affected || value.affectedRows ||
|
|
68
|
+
(operation === 'updateMany' ? value.updated : value.deleted);
|
|
69
|
+
response.value = affected;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// "get" and "update" operations must return the entity instance, otherwise it means resource not found
|
|
73
|
+
if (value == null && (request.operation === 'get' || request.operation === 'update'))
|
|
74
|
+
throw new ResourceNotFoundError(resource.name, request.key);
|
|
75
|
+
// "findMany" operation should return array of entity instances
|
|
76
|
+
if (request.operation === 'findMany')
|
|
77
|
+
value = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
78
|
+
else
|
|
79
|
+
value = value == null ? {} : Array.isArray(value) ? value[0] : value;
|
|
80
|
+
response.value = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
response.errors.push(error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async sendResponse(context) {
|
|
88
|
+
const { request, response } = context;
|
|
89
|
+
const resource = request.resource;
|
|
90
|
+
const outgoing = response.switchToHttp();
|
|
91
|
+
let responseObject;
|
|
92
|
+
if (request.operation === 'delete' || request.operation === 'deleteMany' || request.operation === 'updateMany') {
|
|
93
|
+
responseObject = {
|
|
94
|
+
resource: resource.name,
|
|
95
|
+
operation: request.operation,
|
|
96
|
+
affected: response.value
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (!response.value)
|
|
101
|
+
throw new InternalServerError(`"${request.operation}" operation should return value`);
|
|
102
|
+
const encode = resource.getEncoder(request.operation);
|
|
103
|
+
const data = encode(response.value, { coerce: true });
|
|
104
|
+
if (request.operation === 'create')
|
|
105
|
+
outgoing.statusCode = 201;
|
|
106
|
+
responseObject = {
|
|
107
|
+
resource: resource.name,
|
|
108
|
+
operation: request.operation,
|
|
109
|
+
data
|
|
110
|
+
};
|
|
111
|
+
if (request.operation === 'create' || request.operation === 'update')
|
|
112
|
+
responseObject.affected = 1;
|
|
113
|
+
if (request.operation === 'findMany' && response.count != null && response.count >= 0)
|
|
114
|
+
responseObject.totalCount = response.count;
|
|
115
|
+
}
|
|
116
|
+
outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
|
|
117
|
+
const body = this.adapter._i18n.deep(responseObject);
|
|
118
|
+
outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/json; charset=utf-8');
|
|
119
|
+
outgoing.send(JSON.stringify(body));
|
|
120
|
+
outgoing.end();
|
|
121
|
+
}
|
|
122
|
+
async parseCollectionRequest(resource, incoming) {
|
|
123
|
+
if ((incoming.method === 'POST' || incoming.method === 'PATCH') &&
|
|
124
|
+
incoming.headers['content-type'] !== 'application/json')
|
|
125
|
+
throw new BadRequestError({ message: 'Unsupported Content-Type' });
|
|
126
|
+
const contentId = incoming.headers['content-id'];
|
|
127
|
+
const p = incoming.parsedUrl.path[0];
|
|
128
|
+
const params = incoming.parsedUrl.searchParams;
|
|
129
|
+
switch (incoming.method) {
|
|
130
|
+
case 'POST': {
|
|
131
|
+
if (p.key == null) {
|
|
132
|
+
const operationMeta = await this.assertOperation(resource, 'create');
|
|
133
|
+
const jsonReader = this.getBodyLoader(operationMeta);
|
|
134
|
+
const decode = resource.getDecoder('create');
|
|
135
|
+
const data = decode(await jsonReader(incoming), { coerce: true });
|
|
136
|
+
const pick = parseArrayParam(params.get('$pick'));
|
|
137
|
+
const omit = parseArrayParam(params.get('$omit'));
|
|
138
|
+
const include = parseArrayParam(params.get('$include'));
|
|
139
|
+
return new RequestHost({
|
|
140
|
+
controller: operationMeta.controller,
|
|
141
|
+
http: incoming,
|
|
142
|
+
contentId,
|
|
143
|
+
resource,
|
|
144
|
+
operation: 'create',
|
|
145
|
+
data,
|
|
146
|
+
params: {
|
|
147
|
+
pick: pick && resource.normalizeFieldPath(pick),
|
|
148
|
+
omit: omit && resource.normalizeFieldPath(omit),
|
|
149
|
+
include: include && resource.normalizeFieldPath(include)
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'DELETE': {
|
|
156
|
+
if (p.key != null) {
|
|
157
|
+
const operationMeta = await this.assertOperation(resource, 'delete');
|
|
158
|
+
return new RequestHost({
|
|
159
|
+
controller: operationMeta.controller,
|
|
160
|
+
http: incoming,
|
|
161
|
+
contentId,
|
|
162
|
+
resource,
|
|
163
|
+
operation: 'delete',
|
|
164
|
+
key: resource.parseKeyValue(p.key)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const operationMeta = await this.assertOperation(resource, 'deleteMany');
|
|
168
|
+
const filter = resource.normalizeFilter(params.get('$filter'));
|
|
169
|
+
return new RequestHost({
|
|
170
|
+
controller: operationMeta.controller,
|
|
171
|
+
http: incoming,
|
|
172
|
+
contentId,
|
|
173
|
+
resource,
|
|
174
|
+
operation: 'deleteMany',
|
|
175
|
+
params: {
|
|
176
|
+
filter
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
case 'GET': {
|
|
181
|
+
const pick = parseArrayParam(params.get('$pick'));
|
|
182
|
+
const omit = parseArrayParam(params.get('$omit'));
|
|
183
|
+
const include = parseArrayParam(params.get('$include'));
|
|
184
|
+
if (p.key != null) {
|
|
185
|
+
const operationMeta = await this.assertOperation(resource, 'get');
|
|
186
|
+
return new RequestHost({
|
|
187
|
+
controller: operationMeta.controller,
|
|
188
|
+
http: incoming,
|
|
189
|
+
contentId,
|
|
190
|
+
resource,
|
|
191
|
+
operation: 'get',
|
|
192
|
+
key: resource.parseKeyValue(p.key),
|
|
193
|
+
params: {
|
|
194
|
+
pick: pick && resource.normalizeFieldPath(pick),
|
|
195
|
+
omit: omit && resource.normalizeFieldPath(omit),
|
|
196
|
+
include: include && resource.normalizeFieldPath(include)
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const operationMeta = await this.assertOperation(resource, 'findMany');
|
|
201
|
+
const filter = resource.normalizeFilter(params.get('$filter'));
|
|
202
|
+
const sort = parseArrayParam(params.get('$sort'));
|
|
203
|
+
return new RequestHost({
|
|
204
|
+
controller: operationMeta.controller,
|
|
205
|
+
http: incoming,
|
|
206
|
+
contentId,
|
|
207
|
+
resource,
|
|
208
|
+
operation: 'findMany',
|
|
209
|
+
params: {
|
|
210
|
+
pick: pick && resource.normalizeFieldPath(pick),
|
|
211
|
+
omit: omit && resource.normalizeFieldPath(omit),
|
|
212
|
+
include: include && resource.normalizeFieldPath(include),
|
|
213
|
+
sort: sort && resource.normalizeSortFields(sort),
|
|
214
|
+
filter,
|
|
215
|
+
limit: toInt(params.get('$limit')),
|
|
216
|
+
skip: toInt(params.get('$skip')),
|
|
217
|
+
distinct: toBoolean(params.get('$distinct')),
|
|
218
|
+
count: toBoolean(params.get('$count')),
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
case 'PATCH': {
|
|
223
|
+
if (p.key != null) {
|
|
224
|
+
const operationMeta = await this.assertOperation(resource, 'update');
|
|
225
|
+
const jsonReader = this.getBodyLoader(operationMeta);
|
|
226
|
+
const decode = resource.getDecoder('update');
|
|
227
|
+
const data = decode(await jsonReader(incoming), { coerce: true });
|
|
228
|
+
const pick = parseArrayParam(params.get('$pick'));
|
|
229
|
+
const omit = parseArrayParam(params.get('$omit'));
|
|
230
|
+
const include = parseArrayParam(params.get('$include'));
|
|
231
|
+
return new RequestHost({
|
|
232
|
+
controller: operationMeta.controller,
|
|
233
|
+
http: incoming,
|
|
234
|
+
contentId,
|
|
235
|
+
resource,
|
|
236
|
+
operation: 'update',
|
|
237
|
+
key: resource.parseKeyValue(p.key),
|
|
238
|
+
data,
|
|
239
|
+
params: {
|
|
240
|
+
pick: pick && resource.normalizeFieldPath(pick),
|
|
241
|
+
omit: omit && resource.normalizeFieldPath(omit),
|
|
242
|
+
include: include && resource.normalizeFieldPath(include),
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const operationMeta = await this.assertOperation(resource, 'updateMany');
|
|
247
|
+
const jsonReader = this.getBodyLoader(operationMeta);
|
|
248
|
+
const decode = resource.getDecoder('updateMany');
|
|
249
|
+
const data = decode(await jsonReader(incoming), { coerce: true });
|
|
250
|
+
const filter = resource.normalizeFilter(params.get('$filter'));
|
|
251
|
+
return new RequestHost({
|
|
252
|
+
controller: operationMeta.controller,
|
|
253
|
+
http: incoming,
|
|
254
|
+
contentId,
|
|
255
|
+
resource,
|
|
256
|
+
operation: 'updateMany',
|
|
257
|
+
data,
|
|
258
|
+
params: {
|
|
259
|
+
filter,
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
throw new MethodNotAllowedError({
|
|
265
|
+
message: `Collection resources do not accept http "${incoming.method}" method`
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async parseSingletonRequest(resource, incoming) {
|
|
269
|
+
if ((incoming.method === 'POST' || incoming.method === 'PATCH') &&
|
|
270
|
+
incoming.headers['content-type'] !== 'application/json')
|
|
271
|
+
throw new BadRequestError({ message: 'Unsupported Content-Type' });
|
|
272
|
+
const contentId = incoming.headers['content-id'];
|
|
273
|
+
const params = incoming.parsedUrl.searchParams;
|
|
274
|
+
switch (incoming.method) {
|
|
275
|
+
case 'POST': {
|
|
276
|
+
const operationMeta = await this.assertOperation(resource, 'create');
|
|
277
|
+
const jsonReader = this.getBodyLoader(operationMeta);
|
|
278
|
+
const decode = resource.getDecoder('create');
|
|
279
|
+
const data = decode(await jsonReader(incoming), { coerce: true });
|
|
280
|
+
const pick = parseArrayParam(params.get('$pick'));
|
|
281
|
+
const omit = parseArrayParam(params.get('$omit'));
|
|
282
|
+
const include = parseArrayParam(params.get('$include'));
|
|
283
|
+
return new RequestHost({
|
|
284
|
+
controller: operationMeta.controller,
|
|
285
|
+
http: incoming,
|
|
286
|
+
contentId,
|
|
287
|
+
resource,
|
|
288
|
+
operation: 'create',
|
|
289
|
+
data,
|
|
290
|
+
params: {
|
|
291
|
+
pick: pick && resource.normalizeFieldPath(pick),
|
|
292
|
+
omit: omit && resource.normalizeFieldPath(omit),
|
|
293
|
+
include: include && resource.normalizeFieldPath(include)
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
case 'DELETE': {
|
|
298
|
+
const operationMeta = await this.assertOperation(resource, 'delete');
|
|
299
|
+
return new RequestHost({
|
|
300
|
+
controller: operationMeta.controller,
|
|
301
|
+
http: incoming,
|
|
302
|
+
contentId,
|
|
303
|
+
resource,
|
|
304
|
+
operation: 'delete',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
case 'GET': {
|
|
308
|
+
const operationMeta = await this.assertOperation(resource, 'get');
|
|
309
|
+
const pick = parseArrayParam(params.get('$pick'));
|
|
310
|
+
const omit = parseArrayParam(params.get('$omit'));
|
|
311
|
+
const include = parseArrayParam(params.get('$include'));
|
|
312
|
+
return new RequestHost({
|
|
313
|
+
controller: operationMeta.controller,
|
|
314
|
+
http: incoming,
|
|
315
|
+
contentId,
|
|
316
|
+
resource,
|
|
317
|
+
operation: 'get',
|
|
318
|
+
params: {
|
|
319
|
+
pick: pick && resource.normalizeFieldPath(pick),
|
|
320
|
+
omit: omit && resource.normalizeFieldPath(omit),
|
|
321
|
+
include: include && resource.normalizeFieldPath(include)
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
case 'PATCH': {
|
|
326
|
+
const operationMeta = await this.assertOperation(resource, 'update');
|
|
327
|
+
const jsonReader = this.getBodyLoader(operationMeta);
|
|
328
|
+
const decode = resource.getDecoder('update');
|
|
329
|
+
const data = decode(await jsonReader(incoming), { coerce: true });
|
|
330
|
+
const pick = parseArrayParam(params.get('$pick'));
|
|
331
|
+
const omit = parseArrayParam(params.get('$omit'));
|
|
332
|
+
const include = parseArrayParam(params.get('$include'));
|
|
333
|
+
return new RequestHost({
|
|
334
|
+
controller: operationMeta.controller,
|
|
335
|
+
http: incoming,
|
|
336
|
+
contentId,
|
|
337
|
+
resource,
|
|
338
|
+
operation: 'update',
|
|
339
|
+
data,
|
|
340
|
+
params: {
|
|
341
|
+
pick: pick && resource.normalizeFieldPath(pick),
|
|
342
|
+
omit: omit && resource.normalizeFieldPath(omit),
|
|
343
|
+
include: include && resource.normalizeFieldPath(include),
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
throw new MethodNotAllowedError({
|
|
349
|
+
message: `Singleton resources do not accept http "${incoming.method}" method`
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
getBodyLoader(operation) {
|
|
353
|
+
let bodyLoader = this.bodyLoaders.get(operation);
|
|
354
|
+
if (!bodyLoader) {
|
|
355
|
+
const parser = bodyParser.json({
|
|
356
|
+
limit: operation.input?.maxContentSize,
|
|
357
|
+
type: 'json'
|
|
358
|
+
});
|
|
359
|
+
bodyLoader = (incoming) => {
|
|
360
|
+
return new Promise((resolve, reject) => {
|
|
361
|
+
const next = (error) => {
|
|
362
|
+
if (error)
|
|
363
|
+
return reject(error);
|
|
364
|
+
resolve(incoming.body);
|
|
365
|
+
};
|
|
366
|
+
parser(incoming, {}, next);
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
this.bodyLoaders.set(operation, bodyLoader);
|
|
370
|
+
}
|
|
371
|
+
return bodyLoader;
|
|
372
|
+
}
|
|
373
|
+
}
|