@opra/core 1.0.0-alpha.23 → 1.0.0-alpha.25
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/constants.js +1 -2
- package/cjs/execution-context.js +1 -0
- package/cjs/http/express-adapter.js +15 -18
- package/cjs/http/http-adapter.js +2 -5
- package/cjs/http/http-context.js +14 -10
- package/cjs/http/{impl/http-handler.js → http-handler.js} +159 -163
- package/cjs/http/impl/multipart-reader.js +132 -44
- package/cjs/index.js +1 -1
- package/cjs/platform-adapter.js +0 -3
- package/esm/constants.js +0 -1
- package/esm/execution-context.js +1 -0
- package/esm/http/express-adapter.js +15 -18
- package/esm/http/http-adapter.js +2 -5
- package/esm/http/http-context.js +14 -10
- package/esm/http/{impl/http-handler.js → http-handler.js} +152 -156
- package/esm/http/impl/multipart-reader.js +133 -45
- package/esm/index.js +1 -1
- package/esm/platform-adapter.js +0 -3
- package/package.json +5 -5
- package/types/constants.d.ts +0 -1
- package/types/execution-context.d.ts +2 -1
- package/types/http/http-adapter.d.ts +23 -5
- package/types/http/{impl/http-handler.d.ts → http-handler.d.ts} +8 -8
- package/types/http/impl/multipart-reader.d.ts +38 -19
- package/types/index.d.ts +1 -1
- package/types/platform-adapter.d.ts +0 -4
- package/cjs/helpers/logger.js +0 -35
- package/esm/helpers/logger.js +0 -31
- package/types/helpers/logger.d.ts +0 -14
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as process from 'node:process';
|
|
2
2
|
import typeIs from '@browsery/type-is';
|
|
3
|
-
import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema,
|
|
3
|
+
import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema, } from '@opra/common';
|
|
4
4
|
import { parse as parseContentType } from 'content-type';
|
|
5
5
|
import { splitString } from 'fast-tokenizer';
|
|
6
6
|
import { asMutable } from 'ts-gems';
|
|
7
7
|
import { toArray, ValidationError, vg } from 'valgen';
|
|
8
|
-
import { kAssetCache } from '
|
|
9
|
-
import { wrapException } from '
|
|
8
|
+
import { kAssetCache } from '../constants';
|
|
9
|
+
import { wrapException } from './utils/wrap-exception';
|
|
10
10
|
/**
|
|
11
11
|
* @class HttpHandler
|
|
12
12
|
*/
|
|
@@ -38,7 +38,7 @@ export class HttpHandler {
|
|
|
38
38
|
throw e;
|
|
39
39
|
if (e instanceof ValidationError) {
|
|
40
40
|
throw new BadRequestError({
|
|
41
|
-
message:
|
|
41
|
+
message: 'Response validation failed',
|
|
42
42
|
code: 'RESPONSE_VALIDATION',
|
|
43
43
|
details: e.issues,
|
|
44
44
|
}, e);
|
|
@@ -64,19 +64,17 @@ export class HttpHandler {
|
|
|
64
64
|
let e = error;
|
|
65
65
|
if (e instanceof ValidationError) {
|
|
66
66
|
e = new InternalServerError({
|
|
67
|
-
message:
|
|
67
|
+
message: 'Response validation failed',
|
|
68
68
|
code: 'RESPONSE_VALIDATION',
|
|
69
69
|
details: e.issues,
|
|
70
70
|
}, e);
|
|
71
71
|
}
|
|
72
72
|
else
|
|
73
73
|
e = wrapException(e);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
response.end();
|
|
79
|
-
});
|
|
74
|
+
if (this.onError)
|
|
75
|
+
await this.onError(context, error);
|
|
76
|
+
context.errors.push(e);
|
|
77
|
+
await this.sendResponse(context);
|
|
80
78
|
}
|
|
81
79
|
finally {
|
|
82
80
|
await context.emitAsync('finish');
|
|
@@ -210,6 +208,7 @@ export class HttpHandler {
|
|
|
210
208
|
}
|
|
211
209
|
}
|
|
212
210
|
for (const prm of paramsLeft) {
|
|
211
|
+
key = String(prm.name);
|
|
213
212
|
// Throw error for required parameters
|
|
214
213
|
if (prm.required) {
|
|
215
214
|
const decode = getDecoder(prm);
|
|
@@ -261,7 +260,7 @@ export class HttpHandler {
|
|
|
261
260
|
const responseValue = await context.operationHandler.call(context.controllerInstance, context);
|
|
262
261
|
const { response } = context;
|
|
263
262
|
if (!response.writableEnded) {
|
|
264
|
-
await this.
|
|
263
|
+
await this.sendResponse(context, responseValue).finally(() => {
|
|
265
264
|
if (!response.writableEnded)
|
|
266
265
|
response.end();
|
|
267
266
|
});
|
|
@@ -273,96 +272,156 @@ export class HttpHandler {
|
|
|
273
272
|
* @param responseValue
|
|
274
273
|
* @protected
|
|
275
274
|
*/
|
|
276
|
-
async
|
|
275
|
+
async sendResponse(context, responseValue) {
|
|
276
|
+
if (context.errors.length)
|
|
277
|
+
return this._sendErrorResponse(context);
|
|
277
278
|
const { response } = context;
|
|
278
279
|
const { document } = this.adapter;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
}
|
|
303
316
|
}
|
|
304
|
-
}
|
|
305
|
-
/** Encode body */
|
|
306
|
-
if (operationResponse.type.extendsFrom(operationResultType)) {
|
|
307
|
-
if (body instanceof OperationResult)
|
|
308
|
-
body = encode(body);
|
|
309
317
|
else {
|
|
310
|
-
body
|
|
311
|
-
|
|
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
|
+
}
|
|
312
327
|
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
328
|
if (body instanceof OperationResult &&
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
body.
|
|
319
|
-
body = operationResultEncoder(body);
|
|
329
|
+
operationResponse.type &&
|
|
330
|
+
operationResponse.type !== document.node.getDataType(OperationResult)) {
|
|
331
|
+
body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
|
|
320
332
|
}
|
|
321
|
-
else
|
|
322
|
-
body = encode(body);
|
|
323
|
-
}
|
|
324
|
-
if (body instanceof OperationResult &&
|
|
325
|
-
operationResponse.type &&
|
|
326
|
-
operationResponse.type !== document.node.getDataType(OperationResult)) {
|
|
327
|
-
body.type = operationResponse.type.name ? operationResponse.type.name : '#embedded';
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}
|
|
342
351
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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;
|
|
346
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);
|
|
347
370
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
371
|
+
catch (error) {
|
|
372
|
+
context.errors.push(error);
|
|
373
|
+
return this._sendErrorResponse(context);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async _sendErrorResponse(context) {
|
|
377
|
+
const { response, errors } = context;
|
|
378
|
+
if (response.headersSent) {
|
|
353
379
|
response.end();
|
|
354
380
|
return;
|
|
355
381
|
}
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
382
|
+
const wrappedErrors = errors.map(wrapException);
|
|
383
|
+
if (!wrappedErrors.length)
|
|
384
|
+
wrappedErrors.push(new InternalServerError());
|
|
385
|
+
// Sort errors from fatal to info
|
|
386
|
+
wrappedErrors.sort((a, b) => {
|
|
387
|
+
const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
|
|
388
|
+
if (i === 0)
|
|
389
|
+
return b.status - a.status;
|
|
390
|
+
return i;
|
|
391
|
+
});
|
|
392
|
+
context.errors = wrappedErrors;
|
|
393
|
+
let status = response.statusCode || 0;
|
|
394
|
+
if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
|
|
395
|
+
status = wrappedErrors[0].status;
|
|
396
|
+
if (status < Number(HttpStatusCode.BAD_REQUEST))
|
|
397
|
+
status = HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
398
|
+
}
|
|
399
|
+
response.statusCode = status;
|
|
400
|
+
this.adapter.emitAsync('error', wrappedErrors[0], context).catch(() => undefined);
|
|
401
|
+
const { document } = this.adapter;
|
|
402
|
+
const dt = document.node.getComplexType('OperationResult');
|
|
403
|
+
let encode = this[kAssetCache].get(dt, 'encode');
|
|
404
|
+
if (!encode) {
|
|
405
|
+
encode = dt.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
406
|
+
this[kAssetCache].set(dt, 'encode', encode);
|
|
407
|
+
}
|
|
408
|
+
const { i18n } = this.adapter;
|
|
409
|
+
const bodyObject = new OperationResult({
|
|
410
|
+
errors: wrappedErrors.map(x => {
|
|
411
|
+
const o = x.toJSON();
|
|
412
|
+
if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
|
|
413
|
+
delete o.stack;
|
|
414
|
+
return i18n.deep(o);
|
|
415
|
+
}),
|
|
416
|
+
});
|
|
417
|
+
const body = encode(bodyObject);
|
|
418
|
+
response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
|
|
419
|
+
response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
420
|
+
response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
|
|
421
|
+
response.setHeader(HttpHeaderCodes.Expires, '-1');
|
|
422
|
+
response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
|
|
423
|
+
response.send(JSON.stringify(body));
|
|
424
|
+
response.end();
|
|
366
425
|
}
|
|
367
426
|
/**
|
|
368
427
|
*
|
|
@@ -421,6 +480,8 @@ export class HttpHandler {
|
|
|
421
480
|
: operationResponse.contentType);
|
|
422
481
|
if (typeof ct === 'string')
|
|
423
482
|
responseArgs.contentType = contentType = ct;
|
|
483
|
+
else if (operationResponse.type)
|
|
484
|
+
responseArgs.contentType = MimeTypes.opra_response_json;
|
|
424
485
|
}
|
|
425
486
|
}
|
|
426
487
|
}
|
|
@@ -498,11 +559,10 @@ export class HttpHandler {
|
|
|
498
559
|
const documentId = searchParams.get('id');
|
|
499
560
|
const doc = documentId ? document.findDocument(documentId) : document;
|
|
500
561
|
if (!doc) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
]);
|
|
562
|
+
context.errors.push(new BadRequestError({
|
|
563
|
+
message: `Document with given id [${documentId}] does not exists`,
|
|
564
|
+
}));
|
|
565
|
+
return this.sendResponse(context);
|
|
506
566
|
}
|
|
507
567
|
/** Check if response cache exists */
|
|
508
568
|
let responseBody = this[kAssetCache].get(doc, `$schema`);
|
|
@@ -514,68 +574,4 @@ export class HttpHandler {
|
|
|
514
574
|
}
|
|
515
575
|
response.end(responseBody);
|
|
516
576
|
}
|
|
517
|
-
async sendErrorResponse(response, errors) {
|
|
518
|
-
if (response.headersSent) {
|
|
519
|
-
response.end();
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
if (!errors.length)
|
|
523
|
-
errors.push(wrapException({ status: response.statusCode || 500 }));
|
|
524
|
-
const { logger } = this.adapter;
|
|
525
|
-
errors.forEach(x => {
|
|
526
|
-
if (x instanceof OpraException) {
|
|
527
|
-
switch (x.severity) {
|
|
528
|
-
case 'fatal':
|
|
529
|
-
logger.fatal(x);
|
|
530
|
-
break;
|
|
531
|
-
case 'warning':
|
|
532
|
-
logger.warn(x);
|
|
533
|
-
break;
|
|
534
|
-
default:
|
|
535
|
-
logger.error(x);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
else
|
|
539
|
-
logger.fatal(x);
|
|
540
|
-
});
|
|
541
|
-
const wrappedErrors = errors.map(wrapException);
|
|
542
|
-
// Sort errors from fatal to info
|
|
543
|
-
wrappedErrors.sort((a, b) => {
|
|
544
|
-
const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
|
|
545
|
-
if (i === 0)
|
|
546
|
-
return b.status - a.status;
|
|
547
|
-
return i;
|
|
548
|
-
});
|
|
549
|
-
let status = response.statusCode || 0;
|
|
550
|
-
if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
|
|
551
|
-
status = wrappedErrors[0].status;
|
|
552
|
-
if (status < Number(HttpStatusCode.BAD_REQUEST))
|
|
553
|
-
status = HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
554
|
-
}
|
|
555
|
-
response.statusCode = status;
|
|
556
|
-
const { document } = this.adapter;
|
|
557
|
-
const dt = document.node.getComplexType('OperationResult');
|
|
558
|
-
let encode = this[kAssetCache].get(dt, 'encode');
|
|
559
|
-
if (!encode) {
|
|
560
|
-
encode = dt.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
561
|
-
this[kAssetCache].set(dt, 'encode', encode);
|
|
562
|
-
}
|
|
563
|
-
const { i18n } = this.adapter;
|
|
564
|
-
const bodyObject = new OperationResult({
|
|
565
|
-
errors: wrappedErrors.map(x => {
|
|
566
|
-
const o = x.toJSON();
|
|
567
|
-
if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
|
|
568
|
-
delete o.stack;
|
|
569
|
-
return i18n.deep(o);
|
|
570
|
-
}),
|
|
571
|
-
});
|
|
572
|
-
const body = encode(bodyObject);
|
|
573
|
-
response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
|
|
574
|
-
response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
575
|
-
response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
|
|
576
|
-
response.setHeader(HttpHeaderCodes.Expires, '-1');
|
|
577
|
-
response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
|
|
578
|
-
response.send(JSON.stringify(body));
|
|
579
|
-
response.end();
|
|
580
|
-
}
|
|
581
577
|
}
|
|
@@ -1,55 +1,146 @@
|
|
|
1
|
+
import { randomFillSync } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import nodePath from 'node:path';
|
|
5
|
+
import typeIs from '@browsery/type-is';
|
|
6
|
+
import { BadRequestError } from '@opra/common';
|
|
7
|
+
import busboy from 'busboy';
|
|
1
8
|
import { EventEmitter } from 'events';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
9
|
+
import fsPromise from 'fs/promises';
|
|
10
|
+
import { isNotNullish } from 'valgen';
|
|
4
11
|
export class MultipartReader extends EventEmitter {
|
|
5
|
-
constructor(
|
|
12
|
+
constructor(context, options, mediaType) {
|
|
6
13
|
super();
|
|
14
|
+
this.context = context;
|
|
15
|
+
this.mediaType = mediaType;
|
|
7
16
|
this._started = false;
|
|
17
|
+
this._finished = false;
|
|
8
18
|
this._cancelled = false;
|
|
9
19
|
this._items = [];
|
|
10
20
|
this._stack = [];
|
|
11
21
|
this.setMaxListeners(1000);
|
|
12
|
-
this.
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
form.once('error', () => {
|
|
22
|
+
this.tempDirectory = options?.tempDirectory || os.tmpdir();
|
|
23
|
+
const { request } = context;
|
|
24
|
+
const form = busboy({ headers: request.headers });
|
|
25
|
+
this._form = form;
|
|
26
|
+
form.once('error', (e) => {
|
|
18
27
|
this._cancelled = true;
|
|
28
|
+
this._finished = true;
|
|
19
29
|
if (this.listenerCount('error') > 0)
|
|
20
|
-
this.emit('error');
|
|
30
|
+
this.emit('error', e);
|
|
21
31
|
});
|
|
22
|
-
form.on('
|
|
23
|
-
|
|
32
|
+
form.on('close', () => {
|
|
33
|
+
this._finished = true;
|
|
34
|
+
});
|
|
35
|
+
form.on('field', (field, value, info) => {
|
|
36
|
+
const item = {
|
|
37
|
+
kind: 'field',
|
|
38
|
+
field,
|
|
39
|
+
value,
|
|
40
|
+
mimeType: info.mimeType,
|
|
41
|
+
encoding: info.encoding,
|
|
42
|
+
};
|
|
24
43
|
this._items.push(item);
|
|
25
44
|
this._stack.push(item);
|
|
26
45
|
this.emit('field', item);
|
|
27
46
|
this.emit('item', item);
|
|
28
47
|
});
|
|
29
|
-
form.on('file', (
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
form.on('file', (field, file, info) => {
|
|
49
|
+
const saveTo = nodePath.join(this.tempDirectory, `opra-${generateFileName()}`);
|
|
50
|
+
file.pipe(fs.createWriteStream(saveTo));
|
|
51
|
+
file.once('end', () => {
|
|
52
|
+
const item = {
|
|
53
|
+
kind: 'file',
|
|
54
|
+
field,
|
|
55
|
+
storedPath: saveTo,
|
|
56
|
+
filename: info.filename,
|
|
57
|
+
mimeType: info.mimeType,
|
|
58
|
+
encoding: info.encoding,
|
|
59
|
+
};
|
|
60
|
+
this._items.push(item);
|
|
61
|
+
this._stack.push(item);
|
|
62
|
+
this.emit('file', item);
|
|
63
|
+
this.emit('item', item);
|
|
64
|
+
});
|
|
35
65
|
});
|
|
36
66
|
}
|
|
37
67
|
get items() {
|
|
38
68
|
return this._items;
|
|
39
69
|
}
|
|
40
|
-
getNext() {
|
|
41
|
-
|
|
70
|
+
async getNext() {
|
|
71
|
+
let item = this._stack.shift();
|
|
72
|
+
if (!item && !this._finished) {
|
|
42
73
|
this.resume();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
item = await new Promise((resolve, reject) => {
|
|
75
|
+
if (this._stack.length)
|
|
76
|
+
return resolve(this._stack.shift());
|
|
77
|
+
if (this._form.ended)
|
|
78
|
+
return resolve(undefined);
|
|
79
|
+
this._form.once('close', () => {
|
|
80
|
+
resolve(this._stack.shift());
|
|
81
|
+
});
|
|
82
|
+
this.once('item', () => {
|
|
83
|
+
this.pause();
|
|
84
|
+
resolve(this._stack.shift());
|
|
85
|
+
});
|
|
86
|
+
this.once('error', e => reject(e));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (item && this.mediaType) {
|
|
90
|
+
const field = this.mediaType.findMultipartField(item.field);
|
|
91
|
+
if (!field)
|
|
92
|
+
throw new BadRequestError(`Unknown multipart field (${item.field})`);
|
|
93
|
+
if (item.kind === 'field') {
|
|
94
|
+
const decode = field.generateCodec('decode');
|
|
95
|
+
item.value = decode(item.value, {
|
|
96
|
+
onFail: issue => `Multipart field (${item.field}) validation failed: ` + issue.message,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else if (item.kind === 'file') {
|
|
100
|
+
if (field.contentType) {
|
|
101
|
+
const arr = Array.isArray(field.contentType) ? field.contentType : [field.contentType];
|
|
102
|
+
if (!(item.mimeType && arr.find(ct => typeIs.is(item.mimeType, [ct])))) {
|
|
103
|
+
throw new BadRequestError(`Multipart field (${item.field}) do not accept this content type`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** if all items received we check for required items */
|
|
109
|
+
if (this._finished && this.mediaType && this.mediaType.multipartFields?.length > 0) {
|
|
110
|
+
const fieldsLeft = new Set(this.mediaType.multipartFields);
|
|
111
|
+
for (const x of this._items) {
|
|
112
|
+
const field = this.mediaType.findMultipartField(x.field);
|
|
113
|
+
if (field)
|
|
114
|
+
fieldsLeft.delete(field);
|
|
115
|
+
}
|
|
116
|
+
let issues;
|
|
117
|
+
for (const field of fieldsLeft) {
|
|
118
|
+
try {
|
|
119
|
+
isNotNullish(null, { onFail: () => `Multi part field "${String(field.fieldName)}" is required` });
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
if (!issues) {
|
|
123
|
+
issues = e.issues;
|
|
124
|
+
this.context.errors.push(e);
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
issues.push(...e.issues);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (this.context.errors.length)
|
|
131
|
+
throw this.context.errors[0];
|
|
132
|
+
}
|
|
133
|
+
return item;
|
|
134
|
+
}
|
|
135
|
+
async getAll() {
|
|
136
|
+
const items = [];
|
|
137
|
+
let item;
|
|
138
|
+
while (!this._cancelled && (item = await this.getNext())) {
|
|
139
|
+
items.push(item);
|
|
140
|
+
}
|
|
141
|
+
return items;
|
|
51
142
|
}
|
|
52
|
-
|
|
143
|
+
getAll_() {
|
|
53
144
|
if (this._form.ended)
|
|
54
145
|
return Promise.resolve([...this._items]);
|
|
55
146
|
this.resume();
|
|
@@ -66,29 +157,26 @@ export class MultipartReader extends EventEmitter {
|
|
|
66
157
|
this.resume();
|
|
67
158
|
}
|
|
68
159
|
resume() {
|
|
69
|
-
if (!this._started)
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
160
|
+
if (!this._started) {
|
|
161
|
+
this._started = true;
|
|
162
|
+
this.context.request.pipe(this._form);
|
|
163
|
+
}
|
|
164
|
+
this.context.request.resume();
|
|
73
165
|
}
|
|
74
166
|
pause() {
|
|
75
|
-
|
|
76
|
-
this._form.pause();
|
|
167
|
+
this.context.request.pause();
|
|
77
168
|
}
|
|
78
|
-
async
|
|
169
|
+
async purge() {
|
|
79
170
|
const promises = [];
|
|
80
171
|
this._items.forEach(item => {
|
|
81
|
-
if (
|
|
172
|
+
if (item.kind !== 'file')
|
|
82
173
|
return;
|
|
83
|
-
|
|
84
|
-
promises.push(new Promise(resolve => {
|
|
85
|
-
if (file._writeStream.closed)
|
|
86
|
-
return resolve();
|
|
87
|
-
file._writeStream.once('close', resolve);
|
|
88
|
-
})
|
|
89
|
-
.then(() => fs.unlink(file.filepath))
|
|
90
|
-
.then(() => 0));
|
|
174
|
+
promises.push(fsPromise.unlink(item.storedPath));
|
|
91
175
|
});
|
|
92
176
|
return Promise.allSettled(promises);
|
|
93
177
|
}
|
|
94
178
|
}
|
|
179
|
+
function generateFileName() {
|
|
180
|
+
const buf = Buffer.alloc(10);
|
|
181
|
+
return new Date().toISOString().substring(0, 10).replaceAll('-', '') + randomFillSync(buf).toString('hex');
|
|
182
|
+
}
|
package/esm/index.js
CHANGED
|
@@ -6,11 +6,11 @@ import * as HttpOutgoingHost_ from './http/impl/http-outgoing.host.js';
|
|
|
6
6
|
import * as NodeIncomingMessageHost_ from './http/impl/node-incoming-message.host.js';
|
|
7
7
|
import * as NodeOutgoingMessageHost_ from './http/impl/node-outgoing-message.host.js';
|
|
8
8
|
export * from './execution-context.js';
|
|
9
|
-
export * from './helpers/logger.js';
|
|
10
9
|
export * from './helpers/service-base.js';
|
|
11
10
|
export * from './http/express-adapter.js';
|
|
12
11
|
export * from './http/http-adapter.js';
|
|
13
12
|
export * from './http/http-context.js';
|
|
13
|
+
export * from './http/http-handler.js';
|
|
14
14
|
export * from './http/impl/multipart-reader.js';
|
|
15
15
|
export * from './http/interfaces/http-incoming.interface.js';
|
|
16
16
|
export * from './http/interfaces/http-outgoing.interface.js';
|
package/esm/platform-adapter.js
CHANGED
|
@@ -2,7 +2,6 @@ import './augmentation/18n.augmentation.js';
|
|
|
2
2
|
import { I18n } from '@opra/common';
|
|
3
3
|
import { AsyncEventEmitter } from 'strict-typed-events';
|
|
4
4
|
import { kAssetCache } from './constants.js';
|
|
5
|
-
import { Logger } from './helpers/logger.js';
|
|
6
5
|
import { AssetCache } from './http/impl/asset-cache.js';
|
|
7
6
|
/**
|
|
8
7
|
* @class PlatformAdapter
|
|
@@ -12,8 +11,6 @@ export class PlatformAdapter extends AsyncEventEmitter {
|
|
|
12
11
|
super();
|
|
13
12
|
this[kAssetCache] = new AssetCache();
|
|
14
13
|
this.document = document;
|
|
15
|
-
this.logger =
|
|
16
|
-
options?.logger && options.logger instanceof Logger ? options.logger : new Logger({ instance: options?.logger });
|
|
17
14
|
this.i18n = options?.i18n || I18n.defaultInstance;
|
|
18
15
|
}
|
|
19
16
|
}
|