@opra/core 1.0.0-alpha.24 → 1.0.0-alpha.26
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/http/http-context.js +1 -20
- package/cjs/http/http-handler.js +135 -117
- package/cjs/http/impl/multipart-reader.js +16 -12
- package/esm/http/http-context.js +2 -21
- package/esm/http/http-handler.js +135 -117
- package/esm/http/impl/multipart-reader.js +16 -12
- package/i18n/i18n/en/error.json +21 -0
- package/package.json +2 -2
- package/types/execution-context.d.ts +2 -2
- package/types/http/http-handler.d.ts +4 -3
- package/types/http/impl/multipart-reader.d.ts +1 -1
package/cjs/http/http-context.js
CHANGED
|
@@ -69,26 +69,7 @@ class HttpContext extends execution_context_js_1.ExecutionContext {
|
|
|
69
69
|
/** Retrieve all fields */
|
|
70
70
|
const parts = await reader.getAll();
|
|
71
71
|
/** Filter fields according to configuration */
|
|
72
|
-
this._body = [];
|
|
73
|
-
const multipartFields = mediaType?.multipartFields;
|
|
74
|
-
if (mediaType && multipartFields?.length) {
|
|
75
|
-
const fieldsFound = new Map();
|
|
76
|
-
for (const item of parts) {
|
|
77
|
-
const field = mediaType.findMultipartField(item.field, item.kind);
|
|
78
|
-
if (field) {
|
|
79
|
-
fieldsFound.set(field, true);
|
|
80
|
-
this._body.push(item);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/** Check required fields */
|
|
84
|
-
for (const field of multipartFields) {
|
|
85
|
-
if (field.required && !fieldsFound.get(field)) {
|
|
86
|
-
throw new common_1.BadRequestError({
|
|
87
|
-
message: `Multipart field (${field.fieldName}) is required`,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
72
|
+
this._body = [...parts];
|
|
92
73
|
return this._body;
|
|
93
74
|
}
|
|
94
75
|
this._body = await this.request.readBody({ limit: operation.requestBody?.maxContentSize });
|
package/cjs/http/http-handler.js
CHANGED
|
@@ -278,123 +278,126 @@ class HttpHandler {
|
|
|
278
278
|
*/
|
|
279
279
|
async sendResponse(context, responseValue) {
|
|
280
280
|
if (context.errors.length)
|
|
281
|
-
return this.
|
|
281
|
+
return this._sendErrorResponse(context);
|
|
282
282
|
const { response } = context;
|
|
283
283
|
const { document } = this.adapter;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
284
|
+
try {
|
|
285
|
+
const responseArgs = this._determineResponseArgs(context, responseValue);
|
|
286
|
+
const { operationResponse, statusCode } = responseArgs;
|
|
287
|
+
let { contentType, body } = responseArgs;
|
|
288
|
+
const operationResultType = document.node.getDataType(common_1.OperationResult);
|
|
289
|
+
let operationResultEncoder = this[constants_1.kAssetCache].get(operationResultType, 'encode');
|
|
290
|
+
if (!operationResultEncoder) {
|
|
291
|
+
operationResultEncoder = operationResultType.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
292
|
+
this[constants_1.kAssetCache].set(operationResultType, 'encode', operationResultEncoder);
|
|
293
|
+
}
|
|
294
|
+
/** Validate response */
|
|
295
|
+
if (operationResponse?.type) {
|
|
296
|
+
if (!(body == null && statusCode === common_1.HttpStatusCode.NO_CONTENT)) {
|
|
297
|
+
/** Generate encoder */
|
|
298
|
+
let encode = this[constants_1.kAssetCache].get(operationResponse, 'encode');
|
|
299
|
+
if (!encode) {
|
|
300
|
+
encode = operationResponse.type.generateCodec('encode', {
|
|
301
|
+
partial: operationResponse.partial,
|
|
302
|
+
projection: '*',
|
|
303
|
+
ignoreWriteonlyFields: true,
|
|
304
|
+
onFail: issue => `Response body validation failed: ` + issue.message,
|
|
305
|
+
});
|
|
306
|
+
if (operationResponse) {
|
|
307
|
+
if (operationResponse.isArray)
|
|
308
|
+
encode = valgen_1.vg.isArray(encode);
|
|
309
|
+
this[constants_1.kAssetCache].set(operationResponse, 'encode', encode);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** Encode body */
|
|
313
|
+
if (operationResponse.type.extendsFrom(operationResultType)) {
|
|
314
|
+
if (body instanceof common_1.OperationResult)
|
|
315
|
+
body = encode(body);
|
|
316
|
+
else {
|
|
317
|
+
body.payload = encode(body.payload);
|
|
318
|
+
body = operationResultEncoder(body);
|
|
319
|
+
}
|
|
308
320
|
}
|
|
309
|
-
}
|
|
310
|
-
/** Encode body */
|
|
311
|
-
if (operationResponse.type.extendsFrom(operationResultType)) {
|
|
312
|
-
if (body instanceof common_1.OperationResult)
|
|
313
|
-
body = encode(body);
|
|
314
321
|
else {
|
|
315
|
-
body
|
|
316
|
-
|
|
322
|
+
if (body instanceof common_1.OperationResult &&
|
|
323
|
+
contentType &&
|
|
324
|
+
type_is_1.default.is(contentType, [common_1.MimeTypes.opra_response_json])) {
|
|
325
|
+
body.payload = encode(body.payload);
|
|
326
|
+
body = operationResultEncoder(body);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
body = encode(body);
|
|
330
|
+
}
|
|
317
331
|
}
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
332
|
if (body instanceof common_1.OperationResult &&
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
body.
|
|
324
|
-
body = operationResultEncoder(body);
|
|
333
|
+
operationResponse.type &&
|
|
334
|
+
operationResponse.type !== document.node.getDataType(common_1.OperationResult)) {
|
|
335
|
+
body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
|
|
325
336
|
}
|
|
326
|
-
else
|
|
327
|
-
body = encode(body);
|
|
328
|
-
}
|
|
329
|
-
if (body instanceof common_1.OperationResult &&
|
|
330
|
-
operationResponse.type &&
|
|
331
|
-
operationResponse.type !== document.node.getDataType(common_1.OperationResult)) {
|
|
332
|
-
body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
|
|
333
337
|
}
|
|
334
338
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
339
|
+
else if (body != null) {
|
|
340
|
+
if (body instanceof common_1.OperationResult) {
|
|
341
|
+
body = operationResultEncoder(body);
|
|
342
|
+
contentType = common_1.MimeTypes.opra_response_json;
|
|
343
|
+
}
|
|
344
|
+
else if (Buffer.isBuffer(body))
|
|
345
|
+
contentType = common_1.MimeTypes.binary;
|
|
346
|
+
else if (typeof body === 'object') {
|
|
347
|
+
contentType = contentType || common_1.MimeTypes.json;
|
|
348
|
+
if (typeof body.toJSON === 'function')
|
|
349
|
+
body = body.toJSON();
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
contentType = contentType || common_1.MimeTypes.text;
|
|
353
|
+
body = String(body);
|
|
354
|
+
}
|
|
347
355
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
356
|
+
/** Set content-type header value if not set */
|
|
357
|
+
if (contentType && contentType !== responseArgs.contentType)
|
|
358
|
+
response.setHeader('content-type', contentType);
|
|
359
|
+
response.status(statusCode);
|
|
360
|
+
if (body == null) {
|
|
361
|
+
response.end();
|
|
362
|
+
return;
|
|
351
363
|
}
|
|
364
|
+
let x;
|
|
365
|
+
if (Buffer.isBuffer(body) || (0, common_1.isReadableStream)(body))
|
|
366
|
+
x = body;
|
|
367
|
+
else if ((0, common_1.isBlob)(body))
|
|
368
|
+
x = body.stream();
|
|
369
|
+
else if (typeof body === 'object')
|
|
370
|
+
x = JSON.stringify(body);
|
|
371
|
+
else
|
|
372
|
+
x = String(body);
|
|
373
|
+
response.end(x);
|
|
352
374
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
response.status(statusCode);
|
|
357
|
-
if (body == null) {
|
|
358
|
-
response.end();
|
|
359
|
-
return;
|
|
375
|
+
catch (error) {
|
|
376
|
+
context.errors.push(error);
|
|
377
|
+
return this._sendErrorResponse(context);
|
|
360
378
|
}
|
|
361
|
-
let x;
|
|
362
|
-
if (Buffer.isBuffer(body) || (0, common_1.isReadableStream)(body))
|
|
363
|
-
x = body;
|
|
364
|
-
else if ((0, common_1.isBlob)(body))
|
|
365
|
-
x = body.stream();
|
|
366
|
-
else if (typeof body === 'object')
|
|
367
|
-
x = JSON.stringify(body);
|
|
368
|
-
else
|
|
369
|
-
x = String(body);
|
|
370
|
-
response.end(x);
|
|
371
379
|
}
|
|
372
|
-
async
|
|
373
|
-
|
|
380
|
+
async _sendErrorResponse(context) {
|
|
381
|
+
context.errors = this._wrapExceptions(context.errors);
|
|
382
|
+
try {
|
|
383
|
+
await this.adapter.emitAsync('error', context.errors, context);
|
|
384
|
+
context.errors = this._wrapExceptions(context.errors);
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
context.errors = this._wrapExceptions([e, ...context.errors]);
|
|
388
|
+
}
|
|
389
|
+
const { response, errors } = context;
|
|
374
390
|
if (response.headersSent) {
|
|
375
391
|
response.end();
|
|
376
392
|
return;
|
|
377
393
|
}
|
|
378
|
-
errors = errors || context.errors;
|
|
379
|
-
const wrappedErrors = errors.map(wrap_exception_1.wrapException);
|
|
380
|
-
if (!wrappedErrors.length)
|
|
381
|
-
wrappedErrors.push(new common_1.InternalServerError());
|
|
382
|
-
// Sort errors from fatal to info
|
|
383
|
-
wrappedErrors.sort((a, b) => {
|
|
384
|
-
const i = common_1.IssueSeverity.Keys.indexOf(a.severity) - common_1.IssueSeverity.Keys.indexOf(b.severity);
|
|
385
|
-
if (i === 0)
|
|
386
|
-
return b.status - a.status;
|
|
387
|
-
return i;
|
|
388
|
-
});
|
|
389
|
-
context.errors = wrappedErrors;
|
|
390
394
|
let status = response.statusCode || 0;
|
|
391
395
|
if (!status || status < Number(common_1.HttpStatusCode.BAD_REQUEST)) {
|
|
392
|
-
status =
|
|
396
|
+
status = errors[0].status;
|
|
393
397
|
if (status < Number(common_1.HttpStatusCode.BAD_REQUEST))
|
|
394
398
|
status = common_1.HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
395
399
|
}
|
|
396
400
|
response.statusCode = status;
|
|
397
|
-
this.adapter.emitAsync('error', wrappedErrors[0], context).catch(() => undefined);
|
|
398
401
|
const { document } = this.adapter;
|
|
399
402
|
const dt = document.node.getComplexType('OperationResult');
|
|
400
403
|
let encode = this[constants_1.kAssetCache].get(dt, 'encode');
|
|
@@ -404,7 +407,7 @@ class HttpHandler {
|
|
|
404
407
|
}
|
|
405
408
|
const { i18n } = this.adapter;
|
|
406
409
|
const bodyObject = new common_1.OperationResult({
|
|
407
|
-
errors:
|
|
410
|
+
errors: errors.map(x => {
|
|
408
411
|
const o = x.toJSON();
|
|
409
412
|
if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
|
|
410
413
|
delete o.stack;
|
|
@@ -420,6 +423,30 @@ class HttpHandler {
|
|
|
420
423
|
response.send(JSON.stringify(body));
|
|
421
424
|
response.end();
|
|
422
425
|
}
|
|
426
|
+
async sendDocumentSchema(context) {
|
|
427
|
+
const { request, response } = context;
|
|
428
|
+
const { document } = this.adapter;
|
|
429
|
+
response.setHeader('content-type', common_1.MimeTypes.json);
|
|
430
|
+
const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
|
|
431
|
+
const { searchParams } = url;
|
|
432
|
+
const documentId = searchParams.get('id');
|
|
433
|
+
const doc = documentId ? document.findDocument(documentId) : document;
|
|
434
|
+
if (!doc) {
|
|
435
|
+
context.errors.push(new common_1.BadRequestError({
|
|
436
|
+
message: `Document with given id [${documentId}] does not exists`,
|
|
437
|
+
}));
|
|
438
|
+
return this.sendResponse(context);
|
|
439
|
+
}
|
|
440
|
+
/** Check if response cache exists */
|
|
441
|
+
let responseBody = this[constants_1.kAssetCache].get(doc, `$schema`);
|
|
442
|
+
/** Create response if response cache does not exists */
|
|
443
|
+
if (!responseBody) {
|
|
444
|
+
const schema = doc.export();
|
|
445
|
+
responseBody = JSON.stringify(schema);
|
|
446
|
+
this[constants_1.kAssetCache].set(doc, `$schema`, responseBody);
|
|
447
|
+
}
|
|
448
|
+
response.end(responseBody);
|
|
449
|
+
}
|
|
423
450
|
/**
|
|
424
451
|
*
|
|
425
452
|
* @param context
|
|
@@ -477,6 +504,8 @@ class HttpHandler {
|
|
|
477
504
|
: operationResponse.contentType);
|
|
478
505
|
if (typeof ct === 'string')
|
|
479
506
|
responseArgs.contentType = contentType = ct;
|
|
507
|
+
else if (operationResponse.type)
|
|
508
|
+
responseArgs.contentType = common_1.MimeTypes.opra_response_json;
|
|
480
509
|
}
|
|
481
510
|
}
|
|
482
511
|
}
|
|
@@ -545,29 +574,18 @@ class HttpHandler {
|
|
|
545
574
|
responseArgs.body = body;
|
|
546
575
|
return responseArgs;
|
|
547
576
|
}
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return this.sendResponse(context);
|
|
561
|
-
}
|
|
562
|
-
/** Check if response cache exists */
|
|
563
|
-
let responseBody = this[constants_1.kAssetCache].get(doc, `$schema`);
|
|
564
|
-
/** Create response if response cache does not exists */
|
|
565
|
-
if (!responseBody) {
|
|
566
|
-
const schema = doc.export();
|
|
567
|
-
responseBody = JSON.stringify(schema);
|
|
568
|
-
this[constants_1.kAssetCache].set(doc, `$schema`, responseBody);
|
|
569
|
-
}
|
|
570
|
-
response.end(responseBody);
|
|
577
|
+
_wrapExceptions(exceptions) {
|
|
578
|
+
const wrappedErrors = exceptions.map(wrap_exception_1.wrapException);
|
|
579
|
+
if (!wrappedErrors.length)
|
|
580
|
+
wrappedErrors.push(new common_1.InternalServerError());
|
|
581
|
+
// Sort errors from fatal to info
|
|
582
|
+
wrappedErrors.sort((a, b) => {
|
|
583
|
+
const i = common_1.IssueSeverity.Keys.indexOf(a.severity) - common_1.IssueSeverity.Keys.indexOf(b.severity);
|
|
584
|
+
if (i === 0)
|
|
585
|
+
return b.status - a.status;
|
|
586
|
+
return i;
|
|
587
|
+
});
|
|
588
|
+
return wrappedErrors;
|
|
571
589
|
}
|
|
572
590
|
}
|
|
573
591
|
exports.HttpHandler = HttpHandler;
|
|
@@ -76,15 +76,22 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
76
76
|
if (!item && !this._finished) {
|
|
77
77
|
this.resume();
|
|
78
78
|
item = await new Promise((resolve, reject) => {
|
|
79
|
+
let resolved = false;
|
|
79
80
|
if (this._stack.length)
|
|
80
81
|
return resolve(this._stack.shift());
|
|
81
82
|
if (this._form.ended)
|
|
82
83
|
return resolve(undefined);
|
|
83
84
|
this._form.once('close', () => {
|
|
85
|
+
if (resolved)
|
|
86
|
+
return;
|
|
87
|
+
resolved = true;
|
|
84
88
|
resolve(this._stack.shift());
|
|
85
89
|
});
|
|
86
90
|
this.once('item', () => {
|
|
87
91
|
this.pause();
|
|
92
|
+
if (resolved)
|
|
93
|
+
return;
|
|
94
|
+
resolved = true;
|
|
88
95
|
resolve(this._stack.shift());
|
|
89
96
|
});
|
|
90
97
|
this.once('error', e => reject(e));
|
|
@@ -95,8 +102,10 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
95
102
|
if (!field)
|
|
96
103
|
throw new common_1.BadRequestError(`Unknown multipart field (${item.field})`);
|
|
97
104
|
if (item.kind === 'field') {
|
|
98
|
-
const
|
|
99
|
-
item.value =
|
|
105
|
+
const decode = field.generateCodec('decode');
|
|
106
|
+
item.value = decode(item.value, {
|
|
107
|
+
onFail: issue => `Multipart field (${item.field}) validation failed: ` + issue.message,
|
|
108
|
+
});
|
|
100
109
|
}
|
|
101
110
|
else if (item.kind === 'file') {
|
|
102
111
|
if (field.contentType) {
|
|
@@ -108,7 +117,7 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
/** if all items received we check for required items */
|
|
111
|
-
if (
|
|
120
|
+
if (this._finished && this.mediaType && this.mediaType.multipartFields?.length > 0) {
|
|
112
121
|
const fieldsLeft = new Set(this.mediaType.multipartFields);
|
|
113
122
|
for (const x of this._items) {
|
|
114
123
|
const field = this.mediaType.findMultipartField(x.field);
|
|
@@ -117,6 +126,8 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
117
126
|
}
|
|
118
127
|
let issues;
|
|
119
128
|
for (const field of fieldsLeft) {
|
|
129
|
+
if (!field.required)
|
|
130
|
+
continue;
|
|
120
131
|
try {
|
|
121
132
|
(0, valgen_1.isNotNullish)(null, { onFail: () => `Multi part field "${String(field.fieldName)}" is required` });
|
|
122
133
|
}
|
|
@@ -135,7 +146,7 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
135
146
|
return item;
|
|
136
147
|
}
|
|
137
148
|
async getAll() {
|
|
138
|
-
const items = [];
|
|
149
|
+
const items = [...this._items];
|
|
139
150
|
let item;
|
|
140
151
|
while (!this._cancelled && (item = await this.getNext())) {
|
|
141
152
|
items.push(item);
|
|
@@ -173,14 +184,7 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
173
184
|
this._items.forEach(item => {
|
|
174
185
|
if (item.kind !== 'file')
|
|
175
186
|
return;
|
|
176
|
-
|
|
177
|
-
promises.push(new Promise(resolve => {
|
|
178
|
-
if (file._writeStream.closed)
|
|
179
|
-
return resolve();
|
|
180
|
-
file._writeStream.once('close', resolve);
|
|
181
|
-
})
|
|
182
|
-
.then(() => promises_1.default.unlink(file.filepath))
|
|
183
|
-
.then(() => 0));
|
|
187
|
+
promises.push(promises_1.default.unlink(item.storedPath));
|
|
184
188
|
});
|
|
185
189
|
return Promise.allSettled(promises);
|
|
186
190
|
}
|
package/esm/http/http-context.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import typeIs from '@browsery/type-is';
|
|
2
|
-
import {
|
|
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';
|
|
@@ -65,26 +65,7 @@ export class HttpContext extends ExecutionContext {
|
|
|
65
65
|
/** Retrieve all fields */
|
|
66
66
|
const parts = await reader.getAll();
|
|
67
67
|
/** Filter fields according to configuration */
|
|
68
|
-
this._body = [];
|
|
69
|
-
const multipartFields = mediaType?.multipartFields;
|
|
70
|
-
if (mediaType && multipartFields?.length) {
|
|
71
|
-
const fieldsFound = new Map();
|
|
72
|
-
for (const item of parts) {
|
|
73
|
-
const field = mediaType.findMultipartField(item.field, item.kind);
|
|
74
|
-
if (field) {
|
|
75
|
-
fieldsFound.set(field, true);
|
|
76
|
-
this._body.push(item);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/** Check required fields */
|
|
80
|
-
for (const field of multipartFields) {
|
|
81
|
-
if (field.required && !fieldsFound.get(field)) {
|
|
82
|
-
throw new BadRequestError({
|
|
83
|
-
message: `Multipart field (${field.fieldName}) is required`,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
68
|
+
this._body = [...parts];
|
|
88
69
|
return this._body;
|
|
89
70
|
}
|
|
90
71
|
this._body = await this.request.readBody({ limit: operation.requestBody?.maxContentSize });
|
package/esm/http/http-handler.js
CHANGED
|
@@ -274,123 +274,126 @@ export class HttpHandler {
|
|
|
274
274
|
*/
|
|
275
275
|
async sendResponse(context, responseValue) {
|
|
276
276
|
if (context.errors.length)
|
|
277
|
-
return this.
|
|
277
|
+
return this._sendErrorResponse(context);
|
|
278
278
|
const { response } = context;
|
|
279
279
|
const { document } = this.adapter;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
280
|
+
try {
|
|
281
|
+
const responseArgs = this._determineResponseArgs(context, responseValue);
|
|
282
|
+
const { operationResponse, statusCode } = responseArgs;
|
|
283
|
+
let { contentType, body } = responseArgs;
|
|
284
|
+
const operationResultType = document.node.getDataType(OperationResult);
|
|
285
|
+
let operationResultEncoder = this[kAssetCache].get(operationResultType, 'encode');
|
|
286
|
+
if (!operationResultEncoder) {
|
|
287
|
+
operationResultEncoder = operationResultType.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
288
|
+
this[kAssetCache].set(operationResultType, 'encode', operationResultEncoder);
|
|
289
|
+
}
|
|
290
|
+
/** Validate response */
|
|
291
|
+
if (operationResponse?.type) {
|
|
292
|
+
if (!(body == null && statusCode === HttpStatusCode.NO_CONTENT)) {
|
|
293
|
+
/** Generate encoder */
|
|
294
|
+
let encode = this[kAssetCache].get(operationResponse, 'encode');
|
|
295
|
+
if (!encode) {
|
|
296
|
+
encode = operationResponse.type.generateCodec('encode', {
|
|
297
|
+
partial: operationResponse.partial,
|
|
298
|
+
projection: '*',
|
|
299
|
+
ignoreWriteonlyFields: true,
|
|
300
|
+
onFail: issue => `Response body validation failed: ` + issue.message,
|
|
301
|
+
});
|
|
302
|
+
if (operationResponse) {
|
|
303
|
+
if (operationResponse.isArray)
|
|
304
|
+
encode = vg.isArray(encode);
|
|
305
|
+
this[kAssetCache].set(operationResponse, 'encode', encode);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/** Encode body */
|
|
309
|
+
if (operationResponse.type.extendsFrom(operationResultType)) {
|
|
310
|
+
if (body instanceof OperationResult)
|
|
311
|
+
body = encode(body);
|
|
312
|
+
else {
|
|
313
|
+
body.payload = encode(body.payload);
|
|
314
|
+
body = operationResultEncoder(body);
|
|
315
|
+
}
|
|
304
316
|
}
|
|
305
|
-
}
|
|
306
|
-
/** Encode body */
|
|
307
|
-
if (operationResponse.type.extendsFrom(operationResultType)) {
|
|
308
|
-
if (body instanceof OperationResult)
|
|
309
|
-
body = encode(body);
|
|
310
317
|
else {
|
|
311
|
-
body
|
|
312
|
-
|
|
318
|
+
if (body instanceof OperationResult &&
|
|
319
|
+
contentType &&
|
|
320
|
+
typeIs.is(contentType, [MimeTypes.opra_response_json])) {
|
|
321
|
+
body.payload = encode(body.payload);
|
|
322
|
+
body = operationResultEncoder(body);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
body = encode(body);
|
|
326
|
+
}
|
|
313
327
|
}
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
328
|
if (body instanceof OperationResult &&
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
body.
|
|
320
|
-
body = operationResultEncoder(body);
|
|
329
|
+
operationResponse.type &&
|
|
330
|
+
operationResponse.type !== document.node.getDataType(OperationResult)) {
|
|
331
|
+
body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
|
|
321
332
|
}
|
|
322
|
-
else
|
|
323
|
-
body = encode(body);
|
|
324
|
-
}
|
|
325
|
-
if (body instanceof OperationResult &&
|
|
326
|
-
operationResponse.type &&
|
|
327
|
-
operationResponse.type !== document.node.getDataType(OperationResult)) {
|
|
328
|
-
body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
|
|
329
333
|
}
|
|
330
334
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
335
|
+
else if (body != null) {
|
|
336
|
+
if (body instanceof OperationResult) {
|
|
337
|
+
body = operationResultEncoder(body);
|
|
338
|
+
contentType = MimeTypes.opra_response_json;
|
|
339
|
+
}
|
|
340
|
+
else if (Buffer.isBuffer(body))
|
|
341
|
+
contentType = MimeTypes.binary;
|
|
342
|
+
else if (typeof body === 'object') {
|
|
343
|
+
contentType = contentType || MimeTypes.json;
|
|
344
|
+
if (typeof body.toJSON === 'function')
|
|
345
|
+
body = body.toJSON();
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
contentType = contentType || MimeTypes.text;
|
|
349
|
+
body = String(body);
|
|
350
|
+
}
|
|
343
351
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
352
|
+
/** Set content-type header value if not set */
|
|
353
|
+
if (contentType && contentType !== responseArgs.contentType)
|
|
354
|
+
response.setHeader('content-type', contentType);
|
|
355
|
+
response.status(statusCode);
|
|
356
|
+
if (body == null) {
|
|
357
|
+
response.end();
|
|
358
|
+
return;
|
|
347
359
|
}
|
|
360
|
+
let x;
|
|
361
|
+
if (Buffer.isBuffer(body) || isReadableStream(body))
|
|
362
|
+
x = body;
|
|
363
|
+
else if (isBlob(body))
|
|
364
|
+
x = body.stream();
|
|
365
|
+
else if (typeof body === 'object')
|
|
366
|
+
x = JSON.stringify(body);
|
|
367
|
+
else
|
|
368
|
+
x = String(body);
|
|
369
|
+
response.end(x);
|
|
348
370
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
response.status(statusCode);
|
|
353
|
-
if (body == null) {
|
|
354
|
-
response.end();
|
|
355
|
-
return;
|
|
371
|
+
catch (error) {
|
|
372
|
+
context.errors.push(error);
|
|
373
|
+
return this._sendErrorResponse(context);
|
|
356
374
|
}
|
|
357
|
-
let x;
|
|
358
|
-
if (Buffer.isBuffer(body) || isReadableStream(body))
|
|
359
|
-
x = body;
|
|
360
|
-
else if (isBlob(body))
|
|
361
|
-
x = body.stream();
|
|
362
|
-
else if (typeof body === 'object')
|
|
363
|
-
x = JSON.stringify(body);
|
|
364
|
-
else
|
|
365
|
-
x = String(body);
|
|
366
|
-
response.end(x);
|
|
367
375
|
}
|
|
368
|
-
async
|
|
369
|
-
|
|
376
|
+
async _sendErrorResponse(context) {
|
|
377
|
+
context.errors = this._wrapExceptions(context.errors);
|
|
378
|
+
try {
|
|
379
|
+
await this.adapter.emitAsync('error', context.errors, context);
|
|
380
|
+
context.errors = this._wrapExceptions(context.errors);
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
context.errors = this._wrapExceptions([e, ...context.errors]);
|
|
384
|
+
}
|
|
385
|
+
const { response, errors } = context;
|
|
370
386
|
if (response.headersSent) {
|
|
371
387
|
response.end();
|
|
372
388
|
return;
|
|
373
389
|
}
|
|
374
|
-
errors = errors || context.errors;
|
|
375
|
-
const wrappedErrors = errors.map(wrapException);
|
|
376
|
-
if (!wrappedErrors.length)
|
|
377
|
-
wrappedErrors.push(new InternalServerError());
|
|
378
|
-
// Sort errors from fatal to info
|
|
379
|
-
wrappedErrors.sort((a, b) => {
|
|
380
|
-
const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
|
|
381
|
-
if (i === 0)
|
|
382
|
-
return b.status - a.status;
|
|
383
|
-
return i;
|
|
384
|
-
});
|
|
385
|
-
context.errors = wrappedErrors;
|
|
386
390
|
let status = response.statusCode || 0;
|
|
387
391
|
if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
|
|
388
|
-
status =
|
|
392
|
+
status = errors[0].status;
|
|
389
393
|
if (status < Number(HttpStatusCode.BAD_REQUEST))
|
|
390
394
|
status = HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
391
395
|
}
|
|
392
396
|
response.statusCode = status;
|
|
393
|
-
this.adapter.emitAsync('error', wrappedErrors[0], context).catch(() => undefined);
|
|
394
397
|
const { document } = this.adapter;
|
|
395
398
|
const dt = document.node.getComplexType('OperationResult');
|
|
396
399
|
let encode = this[kAssetCache].get(dt, 'encode');
|
|
@@ -400,7 +403,7 @@ export class HttpHandler {
|
|
|
400
403
|
}
|
|
401
404
|
const { i18n } = this.adapter;
|
|
402
405
|
const bodyObject = new OperationResult({
|
|
403
|
-
errors:
|
|
406
|
+
errors: errors.map(x => {
|
|
404
407
|
const o = x.toJSON();
|
|
405
408
|
if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
|
|
406
409
|
delete o.stack;
|
|
@@ -416,6 +419,30 @@ export class HttpHandler {
|
|
|
416
419
|
response.send(JSON.stringify(body));
|
|
417
420
|
response.end();
|
|
418
421
|
}
|
|
422
|
+
async sendDocumentSchema(context) {
|
|
423
|
+
const { request, response } = context;
|
|
424
|
+
const { document } = this.adapter;
|
|
425
|
+
response.setHeader('content-type', MimeTypes.json);
|
|
426
|
+
const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
|
|
427
|
+
const { searchParams } = url;
|
|
428
|
+
const documentId = searchParams.get('id');
|
|
429
|
+
const doc = documentId ? document.findDocument(documentId) : document;
|
|
430
|
+
if (!doc) {
|
|
431
|
+
context.errors.push(new BadRequestError({
|
|
432
|
+
message: `Document with given id [${documentId}] does not exists`,
|
|
433
|
+
}));
|
|
434
|
+
return this.sendResponse(context);
|
|
435
|
+
}
|
|
436
|
+
/** Check if response cache exists */
|
|
437
|
+
let responseBody = this[kAssetCache].get(doc, `$schema`);
|
|
438
|
+
/** Create response if response cache does not exists */
|
|
439
|
+
if (!responseBody) {
|
|
440
|
+
const schema = doc.export();
|
|
441
|
+
responseBody = JSON.stringify(schema);
|
|
442
|
+
this[kAssetCache].set(doc, `$schema`, responseBody);
|
|
443
|
+
}
|
|
444
|
+
response.end(responseBody);
|
|
445
|
+
}
|
|
419
446
|
/**
|
|
420
447
|
*
|
|
421
448
|
* @param context
|
|
@@ -473,6 +500,8 @@ export class HttpHandler {
|
|
|
473
500
|
: operationResponse.contentType);
|
|
474
501
|
if (typeof ct === 'string')
|
|
475
502
|
responseArgs.contentType = contentType = ct;
|
|
503
|
+
else if (operationResponse.type)
|
|
504
|
+
responseArgs.contentType = MimeTypes.opra_response_json;
|
|
476
505
|
}
|
|
477
506
|
}
|
|
478
507
|
}
|
|
@@ -541,28 +570,17 @@ export class HttpHandler {
|
|
|
541
570
|
responseArgs.body = body;
|
|
542
571
|
return responseArgs;
|
|
543
572
|
}
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
return this.sendResponse(context);
|
|
557
|
-
}
|
|
558
|
-
/** Check if response cache exists */
|
|
559
|
-
let responseBody = this[kAssetCache].get(doc, `$schema`);
|
|
560
|
-
/** Create response if response cache does not exists */
|
|
561
|
-
if (!responseBody) {
|
|
562
|
-
const schema = doc.export();
|
|
563
|
-
responseBody = JSON.stringify(schema);
|
|
564
|
-
this[kAssetCache].set(doc, `$schema`, responseBody);
|
|
565
|
-
}
|
|
566
|
-
response.end(responseBody);
|
|
573
|
+
_wrapExceptions(exceptions) {
|
|
574
|
+
const wrappedErrors = exceptions.map(wrapException);
|
|
575
|
+
if (!wrappedErrors.length)
|
|
576
|
+
wrappedErrors.push(new InternalServerError());
|
|
577
|
+
// Sort errors from fatal to info
|
|
578
|
+
wrappedErrors.sort((a, b) => {
|
|
579
|
+
const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
|
|
580
|
+
if (i === 0)
|
|
581
|
+
return b.status - a.status;
|
|
582
|
+
return i;
|
|
583
|
+
});
|
|
584
|
+
return wrappedErrors;
|
|
567
585
|
}
|
|
568
586
|
}
|
|
@@ -72,15 +72,22 @@ export class MultipartReader extends EventEmitter {
|
|
|
72
72
|
if (!item && !this._finished) {
|
|
73
73
|
this.resume();
|
|
74
74
|
item = await new Promise((resolve, reject) => {
|
|
75
|
+
let resolved = false;
|
|
75
76
|
if (this._stack.length)
|
|
76
77
|
return resolve(this._stack.shift());
|
|
77
78
|
if (this._form.ended)
|
|
78
79
|
return resolve(undefined);
|
|
79
80
|
this._form.once('close', () => {
|
|
81
|
+
if (resolved)
|
|
82
|
+
return;
|
|
83
|
+
resolved = true;
|
|
80
84
|
resolve(this._stack.shift());
|
|
81
85
|
});
|
|
82
86
|
this.once('item', () => {
|
|
83
87
|
this.pause();
|
|
88
|
+
if (resolved)
|
|
89
|
+
return;
|
|
90
|
+
resolved = true;
|
|
84
91
|
resolve(this._stack.shift());
|
|
85
92
|
});
|
|
86
93
|
this.once('error', e => reject(e));
|
|
@@ -91,8 +98,10 @@ export class MultipartReader extends EventEmitter {
|
|
|
91
98
|
if (!field)
|
|
92
99
|
throw new BadRequestError(`Unknown multipart field (${item.field})`);
|
|
93
100
|
if (item.kind === 'field') {
|
|
94
|
-
const
|
|
95
|
-
item.value =
|
|
101
|
+
const decode = field.generateCodec('decode');
|
|
102
|
+
item.value = decode(item.value, {
|
|
103
|
+
onFail: issue => `Multipart field (${item.field}) validation failed: ` + issue.message,
|
|
104
|
+
});
|
|
96
105
|
}
|
|
97
106
|
else if (item.kind === 'file') {
|
|
98
107
|
if (field.contentType) {
|
|
@@ -104,7 +113,7 @@ export class MultipartReader extends EventEmitter {
|
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
/** if all items received we check for required items */
|
|
107
|
-
if (
|
|
116
|
+
if (this._finished && this.mediaType && this.mediaType.multipartFields?.length > 0) {
|
|
108
117
|
const fieldsLeft = new Set(this.mediaType.multipartFields);
|
|
109
118
|
for (const x of this._items) {
|
|
110
119
|
const field = this.mediaType.findMultipartField(x.field);
|
|
@@ -113,6 +122,8 @@ export class MultipartReader extends EventEmitter {
|
|
|
113
122
|
}
|
|
114
123
|
let issues;
|
|
115
124
|
for (const field of fieldsLeft) {
|
|
125
|
+
if (!field.required)
|
|
126
|
+
continue;
|
|
116
127
|
try {
|
|
117
128
|
isNotNullish(null, { onFail: () => `Multi part field "${String(field.fieldName)}" is required` });
|
|
118
129
|
}
|
|
@@ -131,7 +142,7 @@ export class MultipartReader extends EventEmitter {
|
|
|
131
142
|
return item;
|
|
132
143
|
}
|
|
133
144
|
async getAll() {
|
|
134
|
-
const items = [];
|
|
145
|
+
const items = [...this._items];
|
|
135
146
|
let item;
|
|
136
147
|
while (!this._cancelled && (item = await this.getNext())) {
|
|
137
148
|
items.push(item);
|
|
@@ -169,14 +180,7 @@ export class MultipartReader extends EventEmitter {
|
|
|
169
180
|
this._items.forEach(item => {
|
|
170
181
|
if (item.kind !== 'file')
|
|
171
182
|
return;
|
|
172
|
-
|
|
173
|
-
promises.push(new Promise(resolve => {
|
|
174
|
-
if (file._writeStream.closed)
|
|
175
|
-
return resolve();
|
|
176
|
-
file._writeStream.once('close', resolve);
|
|
177
|
-
})
|
|
178
|
-
.then(() => fsPromise.unlink(file.filepath))
|
|
179
|
-
.then(() => 0));
|
|
183
|
+
promises.push(fsPromise.unlink(item.storedPath));
|
|
180
184
|
});
|
|
181
185
|
return Promise.allSettled(promises);
|
|
182
186
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"BAD_REQUEST": "Bad request",
|
|
3
|
+
"FAILED_DEPENDENCY": "The request failed due to failure of a previous request",
|
|
4
|
+
"FORBIDDEN": "You are not authorized to perform this action",
|
|
5
|
+
"INTERNAL_SERVER_ERROR": "Internal server error",
|
|
6
|
+
"METHOD_NOT_ALLOWED": "Method not allowed",
|
|
7
|
+
"NOT_ACCEPTABLE": "Not acceptable",
|
|
8
|
+
"NOT_FOUND": "Not found",
|
|
9
|
+
"UNAUTHORIZED": "You have not been authenticated to perform this action",
|
|
10
|
+
"UNPROCESSABLE_ENTITY": "Unprocessable entity",
|
|
11
|
+
"REQUEST_VALIDATION": "Request validation failed",
|
|
12
|
+
"RESPONSE_VALIDATION": "Response validation failed",
|
|
13
|
+
"RESOURCE_NOT_AVAILABLE": "Resource is not available or you dont have access",
|
|
14
|
+
"RESOURCE_CONFLICT": "There is already an other {{resource}} resource with same field values ({{fields}})",
|
|
15
|
+
"OPERATION_FORBIDDEN": "The {{resource}} resource does not accept '{{operation}}' operations",
|
|
16
|
+
"ACTION_NOT_FOUND": "The {{resource}} resource doesn't have an action named '{{action}}'",
|
|
17
|
+
"UNKNOWN_FIELD": "Unknown field '{{field}}'",
|
|
18
|
+
"UNACCEPTED_SORT_FIELD": "Field '{{field}}' is not available for sort operation",
|
|
19
|
+
"UNACCEPTED_FILTER_FIELD": "Field '{{field}}' is not available for filter operation",
|
|
20
|
+
"UNACCEPTED_FILTER_OPERATION": "'{{operation}}' for field '{{field}}' is not available for filter operation"
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opra/core",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.26",
|
|
4
4
|
"description": "Opra schema package",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@browsery/http-parser": "^0.5.8",
|
|
35
35
|
"@browsery/type-is": "^1.6.18-r2",
|
|
36
|
-
"@opra/common": "^1.0.0-alpha.
|
|
36
|
+
"@opra/common": "^1.0.0-alpha.26",
|
|
37
37
|
"accepts": "^1.3.8",
|
|
38
38
|
"base64-stream": "^1.0.0",
|
|
39
39
|
"busboy": "^1.6.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiDocument,
|
|
1
|
+
import { ApiDocument, OpraHttpError, OpraSchema } from '@opra/common';
|
|
2
2
|
import { AsyncEventEmitter } from 'strict-typed-events';
|
|
3
3
|
/**
|
|
4
4
|
* @namespace ExecutionContext
|
|
@@ -18,7 +18,7 @@ export declare abstract class ExecutionContext extends AsyncEventEmitter {
|
|
|
18
18
|
readonly document: ApiDocument;
|
|
19
19
|
readonly protocol: OpraSchema.Protocol;
|
|
20
20
|
readonly platform: string;
|
|
21
|
-
errors:
|
|
21
|
+
errors: OpraHttpError[];
|
|
22
22
|
protected constructor(init: ExecutionContext.Initiator);
|
|
23
23
|
addListener(event: 'finish', listener: ExecutionContext.OnFinishListener): this;
|
|
24
24
|
removeListener(event: 'finish', listener: ExecutionContext.OnFinishListener): this;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HttpOperationResponse, OpraException } from '@opra/common';
|
|
1
|
+
import { HttpOperationResponse, OpraException, OpraHttpError } from '@opra/common';
|
|
2
2
|
import { kAssetCache } from '../constants';
|
|
3
3
|
import type { HttpAdapter } from './http-adapter';
|
|
4
4
|
import { HttpContext } from './http-context';
|
|
@@ -61,7 +61,8 @@ export declare class HttpHandler {
|
|
|
61
61
|
* @protected
|
|
62
62
|
*/
|
|
63
63
|
sendResponse(context: HttpContext, responseValue?: any): Promise<void>;
|
|
64
|
-
|
|
64
|
+
protected _sendErrorResponse(context: HttpContext): Promise<void>;
|
|
65
|
+
sendDocumentSchema(context: HttpContext): Promise<void>;
|
|
65
66
|
/**
|
|
66
67
|
*
|
|
67
68
|
* @param context
|
|
@@ -69,5 +70,5 @@ export declare class HttpHandler {
|
|
|
69
70
|
* @protected
|
|
70
71
|
*/
|
|
71
72
|
protected _determineResponseArgs(context: HttpContext, body: any): HttpHandler.ResponseArgs;
|
|
72
|
-
|
|
73
|
+
protected _wrapExceptions(exceptions: any[]): OpraHttpError[];
|
|
73
74
|
}
|