@opra/core 1.0.0-alpha.1 → 1.0.0-alpha.10
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/augmentation/18n.augmentation.js +1 -1
- package/cjs/execution-context.js +0 -1
- package/cjs/http/express-adapter.js +6 -17
- package/cjs/http/http-context.js +6 -3
- package/cjs/http/impl/http-handler.js +70 -61
- package/cjs/http/impl/http-incoming.host.js +3 -3
- package/cjs/http/impl/http-outgoing.host.js +2 -2
- package/cjs/http/impl/multipart-reader.js +4 -10
- package/cjs/http/impl/node-incoming-message.host.js +5 -3
- package/cjs/http/interfaces/node-incoming-message.interface.js +3 -2
- package/cjs/http/utils/body-reader.js +6 -4
- package/cjs/http/utils/common.js +2 -1
- package/cjs/index.js +3 -3
- package/cjs/platform-adapter.js +1 -1
- package/esm/augmentation/18n.augmentation.js +1 -1
- package/esm/execution-context.js +0 -1
- package/esm/http/express-adapter.js +6 -17
- package/esm/http/http-context.js +6 -3
- package/esm/http/impl/http-handler.js +70 -61
- package/esm/http/impl/http-incoming.host.js +3 -3
- package/esm/http/impl/http-outgoing.host.js +2 -2
- package/esm/http/impl/multipart-reader.js +4 -10
- package/esm/http/impl/node-incoming-message.host.js +5 -3
- package/esm/http/interfaces/node-incoming-message.interface.js +3 -2
- package/esm/http/utils/body-reader.js +6 -4
- package/esm/http/utils/common.js +2 -1
- package/esm/index.js +3 -3
- package/esm/platform-adapter.js +1 -1
- package/package.json +13 -7
- package/types/augmentation/18n.augmentation.d.ts +1 -1
- package/types/execution-context.d.ts +1 -3
- package/types/http/express-adapter.d.ts +1 -1
- package/types/http/impl/node-incoming-message.host.d.ts +1 -1
- package/types/http/utils/body-reader.d.ts +1 -1
- package/types/index.d.ts +3 -3
- package/types/platform-adapter.d.ts +1 -1
package/cjs/index.js
CHANGED
|
@@ -10,20 +10,20 @@ 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("./platform-adapter.js"), exports);
|
|
14
|
-
tslib_1.__exportStar(require("./type-guards.js"), exports);
|
|
15
13
|
tslib_1.__exportStar(require("./helpers/logger.js"), exports);
|
|
16
14
|
tslib_1.__exportStar(require("./helpers/service-base.js"), exports);
|
|
17
15
|
tslib_1.__exportStar(require("./http/express-adapter.js"), exports);
|
|
18
16
|
tslib_1.__exportStar(require("./http/http-adapter.js"), exports);
|
|
19
17
|
tslib_1.__exportStar(require("./http/http-context.js"), exports);
|
|
18
|
+
tslib_1.__exportStar(require("./http/impl/multipart-reader.js"), exports);
|
|
20
19
|
tslib_1.__exportStar(require("./http/interfaces/http-incoming.interface.js"), exports);
|
|
21
20
|
tslib_1.__exportStar(require("./http/interfaces/http-outgoing.interface.js"), exports);
|
|
22
21
|
tslib_1.__exportStar(require("./http/interfaces/node-incoming-message.interface.js"), exports);
|
|
23
22
|
tslib_1.__exportStar(require("./http/interfaces/node-outgoing-message.interface.js"), exports);
|
|
24
|
-
tslib_1.__exportStar(require("./http/impl/multipart-reader.js"), exports);
|
|
25
23
|
tslib_1.__exportStar(require("./http/utils/wrap-exception.js"), exports);
|
|
26
24
|
tslib_1.__exportStar(require("./interfaces/logger.interface.js"), exports);
|
|
25
|
+
tslib_1.__exportStar(require("./platform-adapter.js"), exports);
|
|
26
|
+
tslib_1.__exportStar(require("./type-guards.js"), exports);
|
|
27
27
|
var classes;
|
|
28
28
|
(function (classes) {
|
|
29
29
|
classes.HttpIncomingHost = HttpIncomingHost_.HttpIncomingHost;
|
package/cjs/platform-adapter.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PlatformAdapter = void 0;
|
|
4
4
|
require("./augmentation/18n.augmentation.js");
|
|
5
|
-
const strict_typed_events_1 = require("strict-typed-events");
|
|
6
5
|
const common_1 = require("@opra/common");
|
|
6
|
+
const strict_typed_events_1 = require("strict-typed-events");
|
|
7
7
|
const constants_js_1 = require("./constants.js");
|
|
8
8
|
const logger_js_1 = require("./helpers/logger.js");
|
|
9
9
|
const asset_cache_js_1 = require("./http/impl/asset-cache.js");
|
package/esm/execution-context.js
CHANGED
|
@@ -8,7 +8,6 @@ export class ExecutionContext extends AsyncEventEmitter {
|
|
|
8
8
|
this.document = init.document;
|
|
9
9
|
this.protocol = init.protocol;
|
|
10
10
|
this.platform = init.platform;
|
|
11
|
-
this.platformArgs = init.platformArgs;
|
|
12
11
|
}
|
|
13
12
|
addListener(event, listener) {
|
|
14
13
|
return super.addListener(event, listener);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { HttpApi, NotFoundError } from '@opra/common';
|
|
1
2
|
import { Router } from 'express';
|
|
2
3
|
import * as nodePath from 'path';
|
|
3
|
-
import { HttpApi, NotFoundError } from '@opra/common';
|
|
4
4
|
import { kHandler } from '../constants.js';
|
|
5
5
|
import { HttpAdapter } from './http-adapter.js';
|
|
6
6
|
import { HttpContext } from './http-context.js';
|
|
@@ -31,13 +31,14 @@ export class ExpressAdapter extends HttpAdapter {
|
|
|
31
31
|
}
|
|
32
32
|
if (resource.onShutdown) {
|
|
33
33
|
const instance = this._controllerInstances.get(resource) || resource.instance;
|
|
34
|
-
if (instance)
|
|
34
|
+
if (instance) {
|
|
35
35
|
try {
|
|
36
36
|
await resource.onShutdown.call(instance, resource);
|
|
37
37
|
}
|
|
38
38
|
catch (e) {
|
|
39
39
|
this.logger.error(e);
|
|
40
40
|
}
|
|
41
|
+
}
|
|
41
42
|
}
|
|
42
43
|
};
|
|
43
44
|
for (const c of this.api.controllers.values())
|
|
@@ -61,14 +62,9 @@ export class ExpressAdapter extends HttpAdapter {
|
|
|
61
62
|
const createContext = (_req, _res, args) => {
|
|
62
63
|
const request = HttpIncoming.from(_req);
|
|
63
64
|
const response = HttpOutgoing.from(_res);
|
|
64
|
-
const platformArgs = {
|
|
65
|
-
request: _req,
|
|
66
|
-
response: _res,
|
|
67
|
-
};
|
|
68
65
|
return new HttpContext({
|
|
69
66
|
adapter: this,
|
|
70
67
|
platform: this.platform,
|
|
71
|
-
platformArgs,
|
|
72
68
|
request,
|
|
73
69
|
response,
|
|
74
70
|
controller: args?.controller,
|
|
@@ -78,16 +74,9 @@ export class ExpressAdapter extends HttpAdapter {
|
|
|
78
74
|
});
|
|
79
75
|
};
|
|
80
76
|
/** Add an endpoint that returns document schema */
|
|
81
|
-
router.get('
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (url === '/$schema') {
|
|
85
|
-
const context = createContext(_req, _res);
|
|
86
|
-
this[kHandler].sendDocumentSchema(context).catch(next);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
next();
|
|
77
|
+
router.get('/\\$schema', (_req, _res, next) => {
|
|
78
|
+
const context = createContext(_req, _res);
|
|
79
|
+
this[kHandler].sendDocumentSchema(context).catch(next);
|
|
91
80
|
});
|
|
92
81
|
/** Add operation endpoints */
|
|
93
82
|
if (this.api.controllers.size) {
|
package/esm/http/http-context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { vg } from 'valgen';
|
|
2
1
|
import typeIs from '@browsery/type-is';
|
|
3
2
|
import { BadRequestError, InternalServerError, NotAcceptableError, } from '@opra/common';
|
|
3
|
+
import { vg } from 'valgen';
|
|
4
4
|
import { kAssetCache } from '../constants.js';
|
|
5
5
|
import { ExecutionContext } from '../execution-context.js';
|
|
6
6
|
import { MultipartReader } from './impl/multipart-reader.js';
|
|
@@ -74,10 +74,11 @@ export class HttpContext extends ExecutionContext {
|
|
|
74
74
|
}
|
|
75
75
|
/** Check required fields */
|
|
76
76
|
for (const field of multipartFields) {
|
|
77
|
-
if (field.required && !fieldsFound.get(field))
|
|
77
|
+
if (field.required && !fieldsFound.get(field)) {
|
|
78
78
|
throw new BadRequestError({
|
|
79
79
|
message: `Multipart field (${field.fieldName}) is required`,
|
|
80
80
|
});
|
|
81
|
+
}
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
return this._body;
|
|
@@ -85,8 +86,9 @@ export class HttpContext extends ExecutionContext {
|
|
|
85
86
|
this._body = await this.request.readBody({ limit: operation.requestBody?.maxContentSize });
|
|
86
87
|
if (this._body != null) {
|
|
87
88
|
// Convert Buffer to string if media is text
|
|
88
|
-
if (Buffer.isBuffer(this._body) && request.is(['json', 'xml', 'txt', 'text']))
|
|
89
|
+
if (Buffer.isBuffer(this._body) && request.is(['json', 'xml', 'txt', 'text'])) {
|
|
89
90
|
this._body = this._body.toString('utf-8');
|
|
91
|
+
}
|
|
90
92
|
// Transform text to Object if media is JSON
|
|
91
93
|
if (typeof this._body === 'string' && request.is(['json']))
|
|
92
94
|
this._body = JSON.parse(this._body);
|
|
@@ -100,6 +102,7 @@ export class HttpContext extends ExecutionContext {
|
|
|
100
102
|
mediaType.type?.generateCodec('decode', {
|
|
101
103
|
partial: operation.requestBody?.partial,
|
|
102
104
|
projection: '*',
|
|
105
|
+
ignoreReadonlyFields: true,
|
|
103
106
|
}) || vg.isAny();
|
|
104
107
|
this.adapter[kAssetCache].set(mediaType, 'decode', decode);
|
|
105
108
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import * as process from 'node:process';
|
|
2
|
+
import typeIs from '@browsery/type-is';
|
|
3
|
+
import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema, translate, } from '@opra/common';
|
|
1
4
|
import { parse as parseContentType } from 'content-type';
|
|
2
5
|
import { splitString } from 'fast-tokenizer';
|
|
3
|
-
import * as process from 'node:process';
|
|
4
6
|
import { asMutable } from 'ts-gems';
|
|
5
7
|
import { toArray, ValidationError, vg } from 'valgen';
|
|
6
|
-
import typeIs from '@browsery/type-is';
|
|
7
|
-
import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema, translate, } from '@opra/common';
|
|
8
8
|
import { kAssetCache } from '../../constants.js';
|
|
9
9
|
import { wrapException } from '../utils/wrap-exception.js';
|
|
10
10
|
/**
|
|
@@ -60,7 +60,8 @@ export class HttpHandler {
|
|
|
60
60
|
else
|
|
61
61
|
await this._executeRequest(context);
|
|
62
62
|
}
|
|
63
|
-
catch (
|
|
63
|
+
catch (error) {
|
|
64
|
+
let e = error;
|
|
64
65
|
if (e instanceof ValidationError) {
|
|
65
66
|
e = new InternalServerError({
|
|
66
67
|
message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
|
|
@@ -72,11 +73,10 @@ export class HttpHandler {
|
|
|
72
73
|
e = wrapException(e);
|
|
73
74
|
response.status(e.statusCode || e.status || HttpStatusCode.INTERNAL_SERVER_ERROR);
|
|
74
75
|
response.contentType(MimeTypes.opra_response_json);
|
|
75
|
-
await this._sendResponse(context, new OperationResult({ errors: [e] })).finally(() => {
|
|
76
|
+
await this._sendResponse(context, new OperationResult({ errors: [e.toJSON()] })).finally(() => {
|
|
76
77
|
if (!response.finished)
|
|
77
78
|
response.end();
|
|
78
79
|
});
|
|
79
|
-
// if (!outgoing.writableEnded) await this._sendErrorResponse(context.response, [error]);
|
|
80
80
|
}
|
|
81
81
|
finally {
|
|
82
82
|
await context.emitAsync('finish');
|
|
@@ -109,17 +109,17 @@ export class HttpHandler {
|
|
|
109
109
|
*/
|
|
110
110
|
async _parseParameters(context) {
|
|
111
111
|
const { operation, request } = context;
|
|
112
|
-
let
|
|
112
|
+
let key = '';
|
|
113
113
|
try {
|
|
114
114
|
const onFail = (issue) => {
|
|
115
|
-
issue.location =
|
|
115
|
+
issue.location = key;
|
|
116
116
|
return issue;
|
|
117
117
|
};
|
|
118
118
|
/** prepare decoders */
|
|
119
119
|
const getDecoder = (prm) => {
|
|
120
120
|
let decode = this[kAssetCache].get(prm, 'decode');
|
|
121
121
|
if (!decode) {
|
|
122
|
-
decode = prm.type?.generateCodec('decode') || vg.isAny();
|
|
122
|
+
decode = prm.type?.generateCodec('decode', { ignoreReadonlyFields: true }) || vg.isAny();
|
|
123
123
|
this[kAssetCache].set(prm, 'decode', decode);
|
|
124
124
|
}
|
|
125
125
|
return decode;
|
|
@@ -127,9 +127,9 @@ export class HttpHandler {
|
|
|
127
127
|
const paramsLeft = new Set([...operation.parameters, ...operation.owner.parameters]);
|
|
128
128
|
/** parse cookie parameters */
|
|
129
129
|
if (request.cookies) {
|
|
130
|
-
for (
|
|
131
|
-
const oprPrm = operation.findParameter(
|
|
132
|
-
const cntPrm = operation.owner.findParameter(
|
|
130
|
+
for (key of Object.keys(request.cookies)) {
|
|
131
|
+
const oprPrm = operation.findParameter(key, 'cookie');
|
|
132
|
+
const cntPrm = operation.owner.findParameter(key, 'cookie');
|
|
133
133
|
const prm = oprPrm || cntPrm;
|
|
134
134
|
if (!prm)
|
|
135
135
|
continue;
|
|
@@ -138,16 +138,17 @@ export class HttpHandler {
|
|
|
138
138
|
if (cntPrm)
|
|
139
139
|
paramsLeft.delete(cntPrm);
|
|
140
140
|
const decode = getDecoder(prm);
|
|
141
|
-
const v = decode(request.cookies[
|
|
141
|
+
const v = decode(request.cookies[key], { coerce: true, label: key, onFail });
|
|
142
|
+
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
142
143
|
if (v !== undefined)
|
|
143
144
|
context.cookies[prmName] = v;
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
/** parse headers */
|
|
147
148
|
if (request.headers) {
|
|
148
|
-
for (
|
|
149
|
-
const oprPrm = operation.findParameter(
|
|
150
|
-
const cntPrm = operation.owner.findParameter(
|
|
149
|
+
for (key of Object.keys(request.headers)) {
|
|
150
|
+
const oprPrm = operation.findParameter(key, 'header');
|
|
151
|
+
const cntPrm = operation.owner.findParameter(key, 'header');
|
|
151
152
|
const prm = oprPrm || cntPrm;
|
|
152
153
|
if (!prm)
|
|
153
154
|
continue;
|
|
@@ -156,16 +157,17 @@ export class HttpHandler {
|
|
|
156
157
|
if (cntPrm)
|
|
157
158
|
paramsLeft.delete(cntPrm);
|
|
158
159
|
const decode = getDecoder(prm);
|
|
159
|
-
const v = decode(request.headers[
|
|
160
|
+
const v = decode(request.headers[key], { coerce: true, label: key, onFail });
|
|
161
|
+
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
160
162
|
if (v !== undefined)
|
|
161
163
|
context.headers[prmName] = v;
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
/** parse path parameters */
|
|
165
167
|
if (request.params) {
|
|
166
|
-
for (
|
|
167
|
-
const oprPrm = operation.findParameter(
|
|
168
|
-
const cntPrm = operation.owner.findParameter(
|
|
168
|
+
for (key of Object.keys(request.params)) {
|
|
169
|
+
const oprPrm = operation.findParameter(key, 'path');
|
|
170
|
+
const cntPrm = operation.owner.findParameter(key, 'path');
|
|
169
171
|
const prm = oprPrm || cntPrm;
|
|
170
172
|
if (!prm)
|
|
171
173
|
continue;
|
|
@@ -174,17 +176,17 @@ export class HttpHandler {
|
|
|
174
176
|
if (cntPrm)
|
|
175
177
|
paramsLeft.delete(cntPrm);
|
|
176
178
|
const decode = getDecoder(prm);
|
|
177
|
-
const v = decode(request.params[
|
|
179
|
+
const v = decode(request.params[key], { coerce: true, label: key, onFail });
|
|
178
180
|
if (v !== undefined)
|
|
179
|
-
context.pathParams[
|
|
181
|
+
context.pathParams[key] = v;
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
/** parse query parameters */
|
|
183
185
|
const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
|
|
184
186
|
const { searchParams } = url;
|
|
185
|
-
for (
|
|
186
|
-
const oprPrm = operation.findParameter(
|
|
187
|
-
const cntPrm = operation.owner.findParameter(
|
|
187
|
+
for (key of searchParams.keys()) {
|
|
188
|
+
const oprPrm = operation.findParameter(key, 'query');
|
|
189
|
+
const cntPrm = operation.owner.findParameter(key, 'query');
|
|
188
190
|
const prm = oprPrm || cntPrm;
|
|
189
191
|
if (!prm)
|
|
190
192
|
continue;
|
|
@@ -193,15 +195,16 @@ export class HttpHandler {
|
|
|
193
195
|
if (cntPrm)
|
|
194
196
|
paramsLeft.delete(cntPrm);
|
|
195
197
|
const decode = getDecoder(prm);
|
|
196
|
-
let values = searchParams?.getAll(
|
|
198
|
+
let values = searchParams?.getAll(key);
|
|
199
|
+
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
197
200
|
if (values?.length && prm.isArray) {
|
|
198
201
|
values = values.map(v => splitString(v, { delimiters: prm.arraySeparator, quotes: true })).flat();
|
|
199
|
-
values = values.map(v => decode(v, { coerce: true, label:
|
|
202
|
+
values = values.map(v => decode(v, { coerce: true, label: key, onFail }));
|
|
200
203
|
if (values.length)
|
|
201
204
|
context.queryParams[prmName] = values;
|
|
202
205
|
}
|
|
203
206
|
else {
|
|
204
|
-
const v = decode(values[0], { coerce: true, label:
|
|
207
|
+
const v = decode(values[0], { coerce: true, label: key, onFail });
|
|
205
208
|
if (values.length)
|
|
206
209
|
context.queryParams[prmName] = v;
|
|
207
210
|
}
|
|
@@ -216,8 +219,8 @@ export class HttpHandler {
|
|
|
216
219
|
}
|
|
217
220
|
catch (e) {
|
|
218
221
|
if (e instanceof ValidationError) {
|
|
219
|
-
|
|
220
|
-
message: `Invalid parameter (${
|
|
222
|
+
throw new BadRequestError({
|
|
223
|
+
message: `Invalid parameter (${key}) value. ` + e.message,
|
|
221
224
|
code: 'REQUEST_VALIDATION',
|
|
222
225
|
details: e.issues,
|
|
223
226
|
}, e);
|
|
@@ -237,9 +240,8 @@ export class HttpHandler {
|
|
|
237
240
|
let contentType = request.header('content-type');
|
|
238
241
|
if (contentType) {
|
|
239
242
|
contentType = parseContentType(contentType).type;
|
|
240
|
-
mediaType = operation.requestBody.content.find(mc =>
|
|
241
|
-
|
|
242
|
-
});
|
|
243
|
+
mediaType = operation.requestBody.content.find(mc => mc.contentType &&
|
|
244
|
+
typeIs.is(contentType, Array.isArray(mc.contentType) ? mc.contentType : [mc.contentType]));
|
|
243
245
|
}
|
|
244
246
|
if (!mediaType) {
|
|
245
247
|
const contentTypes = operation.requestBody.content.map(mc => mc.contentType).flat();
|
|
@@ -258,11 +260,12 @@ export class HttpHandler {
|
|
|
258
260
|
throw new MethodNotAllowedError();
|
|
259
261
|
const responseValue = await context.operationHandler.call(context.controllerInstance, context);
|
|
260
262
|
const { response } = context;
|
|
261
|
-
if (!response.writableEnded)
|
|
263
|
+
if (!response.writableEnded) {
|
|
262
264
|
await this._sendResponse(context, responseValue).finally(() => {
|
|
263
265
|
if (!response.writableEnded)
|
|
264
266
|
response.end();
|
|
265
267
|
});
|
|
268
|
+
}
|
|
266
269
|
}
|
|
267
270
|
/**
|
|
268
271
|
*
|
|
@@ -279,7 +282,7 @@ export class HttpHandler {
|
|
|
279
282
|
const operationResultType = document.node.getDataType(OperationResult);
|
|
280
283
|
let operationResultEncoder = this[kAssetCache].get(operationResultType, 'encode');
|
|
281
284
|
if (!operationResultEncoder) {
|
|
282
|
-
operationResultEncoder = operationResultType.generateCodec('encode');
|
|
285
|
+
operationResultEncoder = operationResultType.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
283
286
|
this[kAssetCache].set(operationResultType, 'encode', operationResultEncoder);
|
|
284
287
|
}
|
|
285
288
|
/** Validate response */
|
|
@@ -291,6 +294,7 @@ export class HttpHandler {
|
|
|
291
294
|
encode = operationResponse.type.generateCodec('encode', {
|
|
292
295
|
partial: operationResponse.partial,
|
|
293
296
|
projection: '*',
|
|
297
|
+
ignoreWriteonlyFields: true,
|
|
294
298
|
});
|
|
295
299
|
if (operationResponse) {
|
|
296
300
|
if (operationResponse.isArray)
|
|
@@ -387,8 +391,9 @@ export class HttpHandler {
|
|
|
387
391
|
/** Filter available HttpOperationResponse instances according to status code. */
|
|
388
392
|
const filteredResponses = operation.responses.filter(r => r.statusCode.find(sc => sc.start <= statusCode && sc.end >= statusCode));
|
|
389
393
|
/** Throw InternalServerError if controller returns non-configured status code */
|
|
390
|
-
if (!filteredResponses.length && statusCode < 400)
|
|
394
|
+
if (!filteredResponses.length && statusCode < 400) {
|
|
391
395
|
throw new InternalServerError(`No responses defined for status code ${statusCode} in operation "${operation.name}"`);
|
|
396
|
+
}
|
|
392
397
|
/** We search for content-type in filtered HttpOperationResponse array */
|
|
393
398
|
if (filteredResponses.length) {
|
|
394
399
|
/** If no response returned, and content-type has not been set (No response wants to be returned by operation) */
|
|
@@ -401,8 +406,9 @@ export class HttpHandler {
|
|
|
401
406
|
if (contentType) {
|
|
402
407
|
// Find HttpEndpointResponse instance according to content-type header
|
|
403
408
|
operationResponse = filteredResponses.find(r => typeIs.is(contentType, toArray(r.contentType)));
|
|
404
|
-
if (!operationResponse)
|
|
409
|
+
if (!operationResponse) {
|
|
405
410
|
throw new InternalServerError(`Operation didn't configured to return "${contentType}" content`);
|
|
411
|
+
}
|
|
406
412
|
}
|
|
407
413
|
else {
|
|
408
414
|
/** Select first HttpOperationResponse if content-type header has not been set */
|
|
@@ -434,23 +440,26 @@ export class HttpHandler {
|
|
|
434
440
|
case 'Entity.Get':
|
|
435
441
|
case 'Entity.FindMany':
|
|
436
442
|
case 'Entity.Update': {
|
|
437
|
-
if (!(body instanceof OperationResult))
|
|
443
|
+
if (!(body instanceof OperationResult)) {
|
|
438
444
|
body = new OperationResult({
|
|
439
445
|
payload: body,
|
|
440
446
|
});
|
|
447
|
+
}
|
|
441
448
|
if ((composition === 'Entity.Create' || composition === 'Entity.Update') &&
|
|
442
449
|
composition &&
|
|
443
|
-
body.affected == null)
|
|
450
|
+
body.affected == null) {
|
|
444
451
|
body.affected = 1;
|
|
452
|
+
}
|
|
445
453
|
break;
|
|
446
454
|
}
|
|
447
455
|
case 'Entity.Delete':
|
|
448
456
|
case 'Entity.DeleteMany':
|
|
449
457
|
case 'Entity.UpdateMany': {
|
|
450
|
-
if (!(body instanceof OperationResult))
|
|
458
|
+
if (!(body instanceof OperationResult)) {
|
|
451
459
|
body = new OperationResult({
|
|
452
460
|
affected: body,
|
|
453
461
|
});
|
|
462
|
+
}
|
|
454
463
|
body.affected =
|
|
455
464
|
typeof body.affected === 'number'
|
|
456
465
|
? body.affected
|
|
@@ -461,15 +470,19 @@ export class HttpHandler {
|
|
|
461
470
|
: undefined;
|
|
462
471
|
break;
|
|
463
472
|
}
|
|
473
|
+
default:
|
|
474
|
+
break;
|
|
464
475
|
}
|
|
465
476
|
}
|
|
466
|
-
if (responseArgs.contentType && responseArgs.contentType !== parsedContentType?.type)
|
|
477
|
+
if (responseArgs.contentType && responseArgs.contentType !== parsedContentType?.type) {
|
|
467
478
|
response.setHeader('content-type', responseArgs.contentType);
|
|
479
|
+
}
|
|
468
480
|
if (responseArgs.contentType &&
|
|
469
481
|
body != null &&
|
|
470
482
|
!(body instanceof OperationResult) &&
|
|
471
|
-
typeIs.is(responseArgs.contentType, [MimeTypes.opra_response_json]))
|
|
483
|
+
typeIs.is(responseArgs.contentType, [MimeTypes.opra_response_json])) {
|
|
472
484
|
body = new OperationResult({ payload: body });
|
|
485
|
+
}
|
|
473
486
|
if (hasBody)
|
|
474
487
|
responseArgs.body = body;
|
|
475
488
|
return responseArgs;
|
|
@@ -478,28 +491,24 @@ export class HttpHandler {
|
|
|
478
491
|
const { request, response } = context;
|
|
479
492
|
const { document } = this.adapter;
|
|
480
493
|
response.setHeader('content-type', MimeTypes.json);
|
|
494
|
+
const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
|
|
495
|
+
const { searchParams } = url;
|
|
496
|
+
const documentId = searchParams.get('id');
|
|
497
|
+
const doc = documentId ? document.findDocument(documentId) : document;
|
|
498
|
+
if (!doc) {
|
|
499
|
+
return this.sendErrorResponse(response, [
|
|
500
|
+
new BadRequestError({
|
|
501
|
+
message: `Document with given id [${documentId}] does not exists`,
|
|
502
|
+
}),
|
|
503
|
+
]);
|
|
504
|
+
}
|
|
481
505
|
/** Check if response cache exists */
|
|
482
|
-
let responseBody = this[kAssetCache].get(
|
|
506
|
+
let responseBody = this[kAssetCache].get(doc, `$schema`);
|
|
483
507
|
/** Create response if response cache does not exists */
|
|
484
508
|
if (!responseBody) {
|
|
485
|
-
const
|
|
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
|
-
}
|
|
509
|
+
const schema = doc.export();
|
|
501
510
|
responseBody = JSON.stringify(schema);
|
|
502
|
-
this[kAssetCache].set(
|
|
511
|
+
this[kAssetCache].set(doc, `$schema`, responseBody);
|
|
503
512
|
}
|
|
504
513
|
response.end(responseBody);
|
|
505
514
|
}
|
|
@@ -546,7 +555,7 @@ export class HttpHandler {
|
|
|
546
555
|
const dt = document.node.getComplexType('OperationResult');
|
|
547
556
|
let encode = this[kAssetCache].get(dt, 'encode');
|
|
548
557
|
if (!encode) {
|
|
549
|
-
encode = dt.generateCodec('encode');
|
|
558
|
+
encode = dt.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
550
559
|
this[kAssetCache].set(dt, 'encode', encode);
|
|
551
560
|
}
|
|
552
561
|
const { i18n } = this.adapter;
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
Some parts of this file contains codes from open source express library
|
|
3
3
|
https://github.com/expressjs
|
|
4
4
|
*/
|
|
5
|
+
import typeIs from '@browsery/type-is';
|
|
5
6
|
import accepts from 'accepts';
|
|
6
7
|
import fresh from 'fresh';
|
|
7
8
|
import parseRange from 'range-parser';
|
|
8
|
-
import typeIs from '@browsery/type-is';
|
|
9
9
|
import { BodyReader } from '../utils/body-reader.js';
|
|
10
10
|
export class HttpIncomingHost {
|
|
11
11
|
get protocol() {
|
|
@@ -37,11 +37,11 @@ export class HttpIncomingHost {
|
|
|
37
37
|
get fresh() {
|
|
38
38
|
const method = this.method;
|
|
39
39
|
// GET or HEAD for weak freshness validation only
|
|
40
|
-
if ('GET'
|
|
40
|
+
if (method !== 'GET' && method !== 'HEAD')
|
|
41
41
|
return false;
|
|
42
42
|
const status = this.res?.statusCode;
|
|
43
43
|
// 2xx or 304 as per rfc2616 14.26
|
|
44
|
-
if ((status >= 200 && status < 300) ||
|
|
44
|
+
if ((status >= 200 && status < 300) || status === 304) {
|
|
45
45
|
return fresh(this.headers, {
|
|
46
46
|
etag: this.res.getHeader('ETag'),
|
|
47
47
|
'last-modified': this.res.getHeader('Last-Modified'),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Some parts of this file contains codes from open source express library
|
|
3
3
|
https://github.com/expressjs
|
|
4
4
|
*/
|
|
5
|
+
import { HttpStatusCode } from '@opra/common';
|
|
5
6
|
import contentDisposition from 'content-disposition';
|
|
6
7
|
import contentType from 'content-type';
|
|
7
8
|
import cookie from 'cookie';
|
|
@@ -11,7 +12,6 @@ import mime from 'mime-types';
|
|
|
11
12
|
import path from 'path';
|
|
12
13
|
import { toString } from 'putil-varhelpers';
|
|
13
14
|
import vary from 'vary';
|
|
14
|
-
import { HttpStatusCode } from '@opra/common';
|
|
15
15
|
const charsetRegExp = /;\s*charset\s*=/;
|
|
16
16
|
export class HttpOutgoingHost {
|
|
17
17
|
attachment(filename) {
|
|
@@ -160,7 +160,7 @@ export class HttpOutgoingHost {
|
|
|
160
160
|
if (req?.fresh)
|
|
161
161
|
this.statusCode = 304;
|
|
162
162
|
// strip irrelevant headers
|
|
163
|
-
if (
|
|
163
|
+
if (this.statusCode === 204 || this.statusCode === 304) {
|
|
164
164
|
this.removeHeader('Content-Type');
|
|
165
165
|
this.removeHeader('Content-Length');
|
|
166
166
|
this.removeHeader('Transfer-Encoding');
|
|
@@ -12,9 +12,7 @@ export class MultipartReader extends EventEmitter {
|
|
|
12
12
|
this._incoming = incoming;
|
|
13
13
|
const form = (this._form = formidable({
|
|
14
14
|
...options,
|
|
15
|
-
filter: (part) =>
|
|
16
|
-
return !this._cancelled && (!options?.filter || options.filter(part));
|
|
17
|
-
},
|
|
15
|
+
filter: (part) => !this._cancelled && (!options?.filter || options.filter(part)),
|
|
18
16
|
}));
|
|
19
17
|
form.once('error', () => {
|
|
20
18
|
this._cancelled = true;
|
|
@@ -69,7 +67,7 @@ export class MultipartReader extends EventEmitter {
|
|
|
69
67
|
}
|
|
70
68
|
resume() {
|
|
71
69
|
if (!this._started)
|
|
72
|
-
this._form.parse(this._incoming, () =>
|
|
70
|
+
this._form.parse(this._incoming, () => undefined);
|
|
73
71
|
if (this._form.req)
|
|
74
72
|
this._form.resume();
|
|
75
73
|
}
|
|
@@ -88,12 +86,8 @@ export class MultipartReader extends EventEmitter {
|
|
|
88
86
|
return resolve();
|
|
89
87
|
file._writeStream.once('close', resolve);
|
|
90
88
|
})
|
|
91
|
-
.then(() =>
|
|
92
|
-
|
|
93
|
-
})
|
|
94
|
-
.then(() => {
|
|
95
|
-
return 0;
|
|
96
|
-
}));
|
|
89
|
+
.then(() => fs.unlink(file.filepath))
|
|
90
|
+
.then(() => 0));
|
|
97
91
|
});
|
|
98
92
|
return Promise.allSettled(promises);
|
|
99
93
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Duplex, Readable } from 'stream';
|
|
2
1
|
import { isAsyncIterable, isIterable } from '@opra/common';
|
|
2
|
+
import { Duplex, Readable } from 'stream';
|
|
3
3
|
import { convertToHeaders, convertToHeadersDistinct } from '../utils/convert-to-headers.js';
|
|
4
4
|
import { convertToRawHeaders } from '../utils/convert-to-raw-headers.js';
|
|
5
5
|
export const CRLF = Buffer.from('\r\n');
|
|
@@ -33,10 +33,12 @@ export class NodeIncomingMessageHost extends Duplex {
|
|
|
33
33
|
else
|
|
34
34
|
this.body = Buffer.from(JSON.stringify(init.body), 'utf-8');
|
|
35
35
|
}
|
|
36
|
-
if (init.headers)
|
|
36
|
+
if (init.headers) {
|
|
37
37
|
this.rawHeaders = Array.isArray(init.headers) ? init.headers : convertToRawHeaders(init.headers);
|
|
38
|
-
|
|
38
|
+
}
|
|
39
|
+
if (init.trailers) {
|
|
39
40
|
this.rawTrailers = Array.isArray(init.trailers) ? init.trailers : convertToRawHeaders(init.trailers);
|
|
41
|
+
}
|
|
40
42
|
this.ip = init.ip || '';
|
|
41
43
|
this.ips = init.ips || (this.ip ? [this.ip] : []);
|
|
42
44
|
if (this.body && !this.headers['content-length'])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Readable } from 'stream';
|
|
2
1
|
import { HTTPParser } from '@browsery/http-parser';
|
|
3
2
|
import { isAsyncIterable, isIterable } from '@opra/common';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
4
|
import { CRLF, kHttpParser, NodeIncomingMessageHost } from '../impl/node-incoming-message.host.js';
|
|
5
5
|
import { concatReadable } from '../utils/concat-readable.js';
|
|
6
6
|
/**
|
|
@@ -14,8 +14,9 @@ export var NodeIncomingMessage;
|
|
|
14
14
|
* @param iterable
|
|
15
15
|
*/
|
|
16
16
|
function from(iterable) {
|
|
17
|
-
if (typeof iterable === 'object' && !(isIterable(iterable) || isAsyncIterable(iterable)))
|
|
17
|
+
if (typeof iterable === 'object' && !(isIterable(iterable) || isAsyncIterable(iterable))) {
|
|
18
18
|
return new NodeIncomingMessageHost(iterable);
|
|
19
|
+
}
|
|
19
20
|
const msg = new NodeIncomingMessageHost();
|
|
20
21
|
const parser = (msg[kHttpParser] = new HTTPParser(HTTPParser.REQUEST));
|
|
21
22
|
let bodyChunks;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import typeIs from '@browsery/type-is';
|
|
2
|
+
import { BadRequestError, InternalServerError, OpraHttpError } from '@opra/common';
|
|
1
3
|
import { Base64Decode } from 'base64-stream';
|
|
2
4
|
import byteParser from 'bytes';
|
|
3
5
|
import { parse as parseContentType } from 'content-type';
|
|
@@ -5,8 +7,6 @@ import { EventEmitter } from 'events';
|
|
|
5
7
|
import iconv from 'iconv-lite';
|
|
6
8
|
import { Writable } from 'stream';
|
|
7
9
|
import * as zlib from 'zlib';
|
|
8
|
-
import typeIs from '@browsery/type-is';
|
|
9
|
-
import { BadRequestError, InternalServerError, OpraHttpError } from '@opra/common';
|
|
10
10
|
/**
|
|
11
11
|
*
|
|
12
12
|
* @class BodyReader
|
|
@@ -29,11 +29,12 @@ export class BodyReader extends EventEmitter {
|
|
|
29
29
|
}
|
|
30
30
|
async read() {
|
|
31
31
|
/* istanbul ignore next */
|
|
32
|
-
if (this._completed)
|
|
32
|
+
if (this._completed) {
|
|
33
33
|
throw new InternalServerError({
|
|
34
34
|
message: 'Stream already read',
|
|
35
35
|
code: 'STREAM_ALREADY_READ',
|
|
36
36
|
});
|
|
37
|
+
}
|
|
37
38
|
if (!this.req.readable) {
|
|
38
39
|
throw new InternalServerError({
|
|
39
40
|
message: 'Stream is not readable',
|
|
@@ -57,8 +58,9 @@ export class BodyReader extends EventEmitter {
|
|
|
57
58
|
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
|
|
58
59
|
*/
|
|
59
60
|
const contentLength = parseInt(this.req.headers['content-length'] || '0', 10);
|
|
60
|
-
if (this.req.headers['transfer-encoding'] === undefined && !(contentLength && !isNaN(contentLength)))
|
|
61
|
+
if (this.req.headers['transfer-encoding'] === undefined && !(contentLength && !isNaN(contentLength))) {
|
|
61
62
|
return this.onEnd();
|
|
63
|
+
}
|
|
62
64
|
// check the length and limit options.
|
|
63
65
|
// note: we intentionally leave the stream paused,
|
|
64
66
|
// so users should handle the stream themselves.
|