@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.
@@ -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 });
@@ -278,123 +278,126 @@ class HttpHandler {
278
278
  */
279
279
  async sendResponse(context, responseValue) {
280
280
  if (context.errors.length)
281
- return this.sendErrorResponse(context, context.errors);
281
+ return this._sendErrorResponse(context);
282
282
  const { response } = context;
283
283
  const { document } = this.adapter;
284
- const responseArgs = this._determineResponseArgs(context, responseValue);
285
- const { operationResponse, statusCode } = responseArgs;
286
- let { contentType, body } = responseArgs;
287
- const operationResultType = document.node.getDataType(common_1.OperationResult);
288
- let operationResultEncoder = this[constants_1.kAssetCache].get(operationResultType, 'encode');
289
- if (!operationResultEncoder) {
290
- operationResultEncoder = operationResultType.generateCodec('encode', { ignoreWriteonlyFields: true });
291
- this[constants_1.kAssetCache].set(operationResultType, 'encode', operationResultEncoder);
292
- }
293
- /** Validate response */
294
- if (operationResponse?.type) {
295
- if (!(body == null && statusCode === common_1.HttpStatusCode.NO_CONTENT)) {
296
- /** Generate encoder */
297
- let encode = this[constants_1.kAssetCache].get(operationResponse, 'encode');
298
- if (!encode) {
299
- encode = operationResponse.type.generateCodec('encode', {
300
- partial: operationResponse.partial,
301
- projection: '*',
302
- ignoreWriteonlyFields: true,
303
- });
304
- if (operationResponse) {
305
- if (operationResponse.isArray)
306
- encode = valgen_1.vg.isArray(encode);
307
- this[constants_1.kAssetCache].set(operationResponse, 'encode', encode);
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.payload = encode(body.payload);
316
- body = operationResultEncoder(body);
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
- contentType &&
322
- type_is_1.default.is(contentType, [common_1.MimeTypes.opra_response_json])) {
323
- body.payload = encode(body.payload);
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
- else if (body != null) {
337
- if (body instanceof common_1.OperationResult) {
338
- body = operationResultEncoder(body);
339
- contentType = common_1.MimeTypes.opra_response_json;
340
- }
341
- else if (Buffer.isBuffer(body))
342
- contentType = common_1.MimeTypes.binary;
343
- else if (typeof body === 'object') {
344
- contentType = contentType || common_1.MimeTypes.json;
345
- if (typeof body.toJSON === 'function')
346
- body = body.toJSON();
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
- else {
349
- contentType = contentType || common_1.MimeTypes.text;
350
- body = String(body);
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
- /** Set content-type header value if not set */
354
- if (contentType && contentType !== responseArgs.contentType)
355
- response.setHeader('content-type', contentType);
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 sendErrorResponse(context, errors) {
373
- const { response } = context;
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 = wrappedErrors[0].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: wrappedErrors.map(x => {
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
- async sendDocumentSchema(context) {
549
- const { request, response } = context;
550
- const { document } = this.adapter;
551
- response.setHeader('content-type', common_1.MimeTypes.json);
552
- const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
553
- const { searchParams } = url;
554
- const documentId = searchParams.get('id');
555
- const doc = documentId ? document.findDocument(documentId) : document;
556
- if (!doc) {
557
- context.errors.push(new common_1.BadRequestError({
558
- message: `Document with given id [${documentId}] does not exists`,
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 codec = field.generateCodec('decode');
99
- item.value = codec(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 (!item && this.mediaType && this.mediaType.multipartFields?.length > 0) {
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
- const file = item.storedPath;
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
  }
@@ -1,5 +1,5 @@
1
1
  import typeIs from '@browsery/type-is';
2
- import { BadRequestError, InternalServerError, NotAcceptableError, } from '@opra/common';
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 });
@@ -274,123 +274,126 @@ export class HttpHandler {
274
274
  */
275
275
  async sendResponse(context, responseValue) {
276
276
  if (context.errors.length)
277
- return this.sendErrorResponse(context, context.errors);
277
+ return this._sendErrorResponse(context);
278
278
  const { response } = context;
279
279
  const { document } = this.adapter;
280
- const responseArgs = this._determineResponseArgs(context, responseValue);
281
- const { operationResponse, statusCode } = responseArgs;
282
- let { contentType, body } = responseArgs;
283
- const operationResultType = document.node.getDataType(OperationResult);
284
- let operationResultEncoder = this[kAssetCache].get(operationResultType, 'encode');
285
- if (!operationResultEncoder) {
286
- operationResultEncoder = operationResultType.generateCodec('encode', { ignoreWriteonlyFields: true });
287
- this[kAssetCache].set(operationResultType, 'encode', operationResultEncoder);
288
- }
289
- /** Validate response */
290
- if (operationResponse?.type) {
291
- if (!(body == null && statusCode === HttpStatusCode.NO_CONTENT)) {
292
- /** Generate encoder */
293
- let encode = this[kAssetCache].get(operationResponse, 'encode');
294
- if (!encode) {
295
- encode = operationResponse.type.generateCodec('encode', {
296
- partial: operationResponse.partial,
297
- projection: '*',
298
- ignoreWriteonlyFields: true,
299
- });
300
- if (operationResponse) {
301
- if (operationResponse.isArray)
302
- encode = vg.isArray(encode);
303
- this[kAssetCache].set(operationResponse, 'encode', encode);
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.payload = encode(body.payload);
312
- body = operationResultEncoder(body);
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
- contentType &&
318
- typeIs.is(contentType, [MimeTypes.opra_response_json])) {
319
- body.payload = encode(body.payload);
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
- else if (body != null) {
333
- if (body instanceof OperationResult) {
334
- body = operationResultEncoder(body);
335
- contentType = MimeTypes.opra_response_json;
336
- }
337
- else if (Buffer.isBuffer(body))
338
- contentType = MimeTypes.binary;
339
- else if (typeof body === 'object') {
340
- contentType = contentType || MimeTypes.json;
341
- if (typeof body.toJSON === 'function')
342
- body = body.toJSON();
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
- else {
345
- contentType = contentType || MimeTypes.text;
346
- body = String(body);
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
- /** Set content-type header value if not set */
350
- if (contentType && contentType !== responseArgs.contentType)
351
- response.setHeader('content-type', contentType);
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 sendErrorResponse(context, errors) {
369
- const { response } = context;
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 = wrappedErrors[0].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: wrappedErrors.map(x => {
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
- async sendDocumentSchema(context) {
545
- const { request, response } = context;
546
- const { document } = this.adapter;
547
- response.setHeader('content-type', MimeTypes.json);
548
- const url = new URL(request.originalUrl || request.url || '/', 'http://tempuri.org');
549
- const { searchParams } = url;
550
- const documentId = searchParams.get('id');
551
- const doc = documentId ? document.findDocument(documentId) : document;
552
- if (!doc) {
553
- context.errors.push(new BadRequestError({
554
- message: `Document with given id [${documentId}] does not exists`,
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 codec = field.generateCodec('decode');
95
- item.value = codec(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 (!item && this.mediaType && this.mediaType.multipartFields?.length > 0) {
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
- const file = item.storedPath;
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.24",
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.24",
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, OpraException, OpraSchema } from '@opra/common';
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: OpraException[];
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
- sendErrorResponse(context: HttpContext, errors: any[]): Promise<void>;
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
- sendDocumentSchema(context: HttpContext): Promise<void>;
73
+ protected _wrapExceptions(exceptions: any[]): OpraHttpError[];
73
74
  }
@@ -10,7 +10,7 @@ export declare namespace MultipartReader {
10
10
  interface FieldInfo {
11
11
  kind: 'field';
12
12
  field: string;
13
- value?: string;
13
+ value?: any;
14
14
  mimeType?: string;
15
15
  encoding?: string;
16
16
  }