@nymphjs/server 1.0.0-beta.13 → 1.0.0-beta.131

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.
@@ -3,39 +3,46 @@ import express, { NextFunction, Request, Response } from 'express';
3
3
  import cookieParser from 'cookie-parser';
4
4
  import {
5
5
  Nymph,
6
- Entity,
7
6
  EntityConflictError,
8
7
  EntityConstructor,
9
8
  EntityInterface,
10
9
  EntityJson,
11
10
  EntityPatch,
12
- EntityReference,
13
11
  InvalidParametersError,
14
12
  TilmeldAccessLevels,
15
13
  classNamesToEntityConstructors,
14
+ referencesToEntities,
16
15
  Options,
17
16
  Selector,
18
17
  } from '@nymphjs/nymph';
19
18
  import { EntityInvalidDataError } from '@nymphjs/nymph';
20
19
 
21
- import { statusDescriptions } from './statusDescriptions';
20
+ import { statusDescriptions } from './statusDescriptions.js';
22
21
 
23
22
  type NymphResponse = Response<any, { nymph: Nymph }>;
24
23
 
25
24
  const NOT_FOUND_ERROR = 'Entity is not found.';
26
25
 
26
+ export class ForbiddenClassError extends Error {
27
+ constructor(message: string) {
28
+ super(message);
29
+
30
+ this.name = 'ForbiddenClassError';
31
+ }
32
+ }
33
+
27
34
  /**
28
35
  * A REST server middleware creator for Nymph.
29
36
  *
30
37
  * Written by Hunter Perrin for SciActive.
31
38
  *
32
- * @author Hunter Perrin <hperrin@gmail.com>
39
+ * @author Hunter Perrin <hperrin@port87.com>
33
40
  * @copyright SciActive Inc
34
41
  * @see http://nymph.io/
35
42
  */
36
43
  export function createServer(
37
44
  nymph: Nymph,
38
- { jsonOptions = {} }: { jsonOptions?: OptionsJson } = {}
45
+ { jsonOptions = {} }: { jsonOptions?: OptionsJson } = {},
39
46
  ) {
40
47
  const rest = express();
41
48
  rest.use(cookieParser());
@@ -44,22 +51,25 @@ export function createServer(
44
51
  function instantiateNymph(
45
52
  _request: Request,
46
53
  response: NymphResponse,
47
- next: NextFunction
54
+ next: NextFunction,
48
55
  ) {
49
56
  response.locals.nymph = nymph.clone();
50
57
  next();
51
58
  }
52
59
 
53
- function authenticateTilmeld(
60
+ async function authenticateTilmeld(
54
61
  request: Request,
55
62
  response: NymphResponse,
56
- next: NextFunction
63
+ next: NextFunction,
57
64
  ) {
58
65
  if (response.locals.nymph.tilmeld) {
59
66
  response.locals.nymph.tilmeld.request = request;
60
67
  response.locals.nymph.tilmeld.response = response;
61
68
  try {
62
- response.locals.nymph.tilmeld.authenticate();
69
+ await response.locals.nymph.tilmeld.authenticate(
70
+ false,
71
+ request.header('x-tilmeld-token-renewal') === 'off',
72
+ );
63
73
  } catch (e: any) {
64
74
  httpError(response, 500, e);
65
75
  return;
@@ -71,7 +81,7 @@ export function createServer(
71
81
  function unauthenticateTilmeld(
72
82
  _request: Request,
73
83
  response: NymphResponse,
74
- next: NextFunction
84
+ next: NextFunction,
75
85
  ) {
76
86
  if (response.locals.nymph.tilmeld) {
77
87
  response.locals.nymph.tilmeld.request = null;
@@ -137,20 +147,41 @@ export function createServer(
137
147
  return;
138
148
  }
139
149
  let [options, ...selectors] = data as [Options, ...Selector[]];
140
- let EntityClass;
150
+ options.source = 'client';
151
+ options.skipAc = false;
152
+ if (options.return === 'object') {
153
+ options.return = 'entity';
154
+ }
141
155
  try {
142
- EntityClass = response.locals.nymph.getEntityClass(data[0].class);
156
+ options = classNamesToEntityConstructors(
157
+ response.locals.nymph,
158
+ options,
159
+ true,
160
+ );
143
161
  } catch (e: any) {
144
- httpError(response, 400, e);
145
- return;
162
+ if (e?.message === 'Not accessible.') {
163
+ httpError(response, 403);
164
+ return;
165
+ } else {
166
+ httpError(response, 500, e);
167
+ return;
168
+ }
169
+ }
170
+ try {
171
+ selectors = classNamesToEntityConstructors(
172
+ response.locals.nymph,
173
+ referencesToEntities(selectors, response.locals.nymph, false, true),
174
+ true,
175
+ );
176
+ } catch (e: any) {
177
+ if (e?.message === 'Not accessible.') {
178
+ httpError(response, 403);
179
+ return;
180
+ } else {
181
+ httpError(response, 500, e);
182
+ return;
183
+ }
146
184
  }
147
- options.class = EntityClass;
148
- options.source = 'client';
149
- options.skipAc = false;
150
- selectors = classNamesToEntityConstructors(
151
- response.locals.nymph,
152
- selectors
153
- );
154
185
  let result:
155
186
  | EntityInterface
156
187
  | EntityInterface[]
@@ -162,12 +193,12 @@ export function createServer(
162
193
  if (action === 'entity') {
163
194
  result = await response.locals.nymph.getEntity(
164
195
  options,
165
- ...selectors
196
+ ...selectors,
166
197
  );
167
198
  } else {
168
199
  result = await response.locals.nymph.getEntities(
169
200
  options,
170
- ...selectors
201
+ ...selectors,
171
202
  );
172
203
  }
173
204
  } catch (e: any) {
@@ -192,10 +223,10 @@ export function createServer(
192
223
  }
193
224
  if (response.locals.nymph.tilmeld) {
194
225
  if (
195
- !response.locals.nymph.tilmeld.checkClientUIDPermissions(
226
+ !(await response.locals.nymph.tilmeld.checkClientUIDPermissions(
196
227
  data,
197
- TilmeldAccessLevels.READ_ACCESS
198
- )
228
+ TilmeldAccessLevels.READ_ACCESS,
229
+ ))
199
230
  ) {
200
231
  httpError(response, 403);
201
232
  return;
@@ -241,6 +272,7 @@ export function createServer(
241
272
  let invalidRequest = false;
242
273
  let conflict = false;
243
274
  let notfound = false;
275
+ let forbidden = false;
244
276
  let lastException = null;
245
277
  for (let entData of data) {
246
278
  if (entData.guid) {
@@ -250,15 +282,34 @@ export function createServer(
250
282
  }
251
283
  let entity: EntityInterface;
252
284
  try {
253
- entity = await loadEntity(entData, response.locals.nymph);
285
+ entity = await loadEntity(
286
+ classNamesToEntityConstructors(
287
+ response.locals.nymph,
288
+ referencesToEntities(
289
+ entData,
290
+ response.locals.nymph,
291
+ false,
292
+ true,
293
+ ),
294
+ true,
295
+ ),
296
+ response.locals.nymph,
297
+ );
254
298
  } catch (e: any) {
255
299
  if (e instanceof EntityConflictError) {
256
300
  conflict = true;
301
+ lastException = e;
302
+ } else if (e instanceof ForbiddenClassError) {
303
+ forbidden = true;
304
+ lastException = e;
257
305
  } else if (e.message === NOT_FOUND_ERROR) {
258
306
  notfound = true;
259
307
  } else if (e instanceof InvalidParametersError) {
260
308
  invalidRequest = true;
261
309
  lastException = e;
310
+ } else if (e.message === 'Not accessible.') {
311
+ forbidden = true;
312
+ lastException = e;
262
313
  } else {
263
314
  lastException = e;
264
315
  }
@@ -271,12 +322,9 @@ export function createServer(
271
322
  continue;
272
323
  }
273
324
  try {
274
- if (await entity.$save()) {
275
- created.push(entity);
276
- hadSuccess = true;
277
- } else {
278
- created.push(false);
279
- }
325
+ await entity.$save();
326
+ created.push(entity);
327
+ hadSuccess = true;
280
328
  } catch (e: any) {
281
329
  if (e instanceof EntityInvalidDataError) {
282
330
  invalidRequest = true;
@@ -291,7 +339,10 @@ export function createServer(
291
339
  httpError(response, 400, lastException);
292
340
  return;
293
341
  } else if (conflict) {
294
- httpError(response, 409);
342
+ httpError(response, 409, lastException);
343
+ return;
344
+ } else if (forbidden) {
345
+ httpError(response, 403, lastException);
295
346
  return;
296
347
  } else if (notfound) {
297
348
  httpError(response, 404);
@@ -313,92 +364,178 @@ export function createServer(
313
364
  httpError(response, 400);
314
365
  return;
315
366
  }
316
- const params = referencesToEntities(
317
- [...data.params],
318
- response.locals.nymph
319
- );
320
- if (data.static) {
321
- let EntityClass: EntityConstructor;
322
- try {
323
- EntityClass = response.locals.nymph.getEntityClass(data.class);
324
- } catch (e: any) {
325
- httpError(response, 400);
326
- return;
327
- }
328
- if (
329
- EntityClass.clientEnabledStaticMethods.indexOf(data.method) === -1
330
- ) {
331
- httpError(response, 403);
332
- return;
333
- }
334
- if (!(data.method in EntityClass)) {
335
- httpError(response, 400);
336
- return;
337
- }
338
- // @ts-ignore Dynamic methods make TypeScript sad.
339
- const method: Function = EntityClass[data.method];
340
- if (typeof method !== 'function') {
341
- httpError(response, 400);
342
- return;
343
- }
344
- try {
345
- const result = method.call(EntityClass, ...params);
346
- let ret = result;
347
- if (result instanceof Promise) {
348
- ret = await result;
367
+ try {
368
+ const params = classNamesToEntityConstructors(
369
+ response.locals.nymph,
370
+ referencesToEntities(
371
+ [...data.params],
372
+ response.locals.nymph,
373
+ false,
374
+ true,
375
+ ),
376
+ true,
377
+ );
378
+ if (data.static) {
379
+ let EntityClass: EntityConstructor;
380
+ try {
381
+ EntityClass = response.locals.nymph.getEntityClass(data.class);
382
+ if (!EntityClass.restEnabled) {
383
+ httpError(response, 403);
384
+ return;
385
+ }
386
+ } catch (e: any) {
387
+ httpError(response, 400);
388
+ return;
349
389
  }
350
- response.status(200);
351
- response.setHeader('Content-Type', 'application/json');
352
- response.send({ return: ret });
353
- } catch (e: any) {
354
- httpError(response, 500, e);
355
- return;
356
- }
357
- } else {
358
- let entity: EntityInterface;
359
- try {
360
- entity = await loadEntity(data.entity, response.locals.nymph);
361
- } catch (e: any) {
362
- if (e instanceof EntityConflictError) {
363
- httpError(response, 409);
364
- } else if (e.message === NOT_FOUND_ERROR) {
365
- httpError(response, 404, e);
366
- } else if (e instanceof InvalidParametersError) {
367
- httpError(response, 400, e);
390
+ if (
391
+ EntityClass.clientEnabledStaticMethods.indexOf(data.method) === -1
392
+ ) {
393
+ httpError(response, 403);
394
+ return;
395
+ }
396
+ if (!(data.method in EntityClass)) {
397
+ httpError(response, 400);
398
+ return;
399
+ }
400
+ // @ts-ignore Dynamic methods make TypeScript sad.
401
+ const method: Function = EntityClass[data.method];
402
+ if (typeof method !== 'function') {
403
+ httpError(response, 400);
404
+ return;
405
+ }
406
+ if (data.iterator) {
407
+ // Ping every 15 seconds to keep connection alive.
408
+ const interval = setInterval(() => {
409
+ if (response.headersSent) {
410
+ response.write('event: ping\n');
411
+ response.write(`data: ${new Date().toISOString()}\n\n`);
412
+ }
413
+ }, 15000);
414
+
415
+ try {
416
+ response.set({
417
+ 'Cache-Control': 'no-cache',
418
+ 'Content-Type': 'text/event-stream',
419
+ Connection: 'keep-alive',
420
+ });
421
+ response.flushHeaders();
422
+
423
+ const result:
424
+ | Iterator<any, any, boolean>
425
+ | AsyncIterator<any, any, boolean> = method.call(
426
+ EntityClass,
427
+ ...params,
428
+ );
429
+ let sequence = result;
430
+ if (result instanceof Promise) {
431
+ sequence = await result;
432
+ }
433
+
434
+ let { value, done } = await sequence.next(response.destroyed);
435
+ while (!done) {
436
+ response.write('event: next\n');
437
+ response.write(`data: ${JSON.stringify(value)}\n\n`);
438
+ // Wait for an event loop each iteration, to allow data to flush.
439
+ await new Promise((resolve) => setImmediate(resolve));
440
+
441
+ ({ value, done } = await sequence.next(response.destroyed));
442
+ }
443
+
444
+ clearInterval(interval);
445
+
446
+ response.write('event: finished\n');
447
+ response.write('data: \n\n');
448
+ response.end();
449
+ } catch (e: any) {
450
+ clearInterval(interval);
451
+ eventStreamError(response, 500, e);
452
+ return;
453
+ }
368
454
  } else {
455
+ try {
456
+ const result = method.call(EntityClass, ...params);
457
+ let ret = result;
458
+ if (result instanceof Promise) {
459
+ ret = await result;
460
+ }
461
+ response.status(200);
462
+ response.setHeader('Content-Type', 'application/json');
463
+ response.send({ return: ret });
464
+ } catch (e: any) {
465
+ httpError(response, 500, e);
466
+ return;
467
+ }
468
+ }
469
+ } else {
470
+ let entity: EntityInterface;
471
+ try {
472
+ entity = await loadEntity(
473
+ classNamesToEntityConstructors(
474
+ response.locals.nymph,
475
+ referencesToEntities(
476
+ data.entity,
477
+ response.locals.nymph,
478
+ false,
479
+ true,
480
+ ),
481
+ true,
482
+ ),
483
+ response.locals.nymph,
484
+ );
485
+ } catch (e: any) {
486
+ if (e instanceof EntityConflictError) {
487
+ httpError(response, 409, e);
488
+ } else if (e instanceof ForbiddenClassError) {
489
+ httpError(response, 403, e);
490
+ } else if (e.message === NOT_FOUND_ERROR) {
491
+ httpError(response, 404, e);
492
+ } else if (e instanceof InvalidParametersError) {
493
+ httpError(response, 400, e);
494
+ } else if (e.message === 'Not accessible.') {
495
+ httpError(response, 403);
496
+ } else {
497
+ httpError(response, 500, e);
498
+ }
499
+ return;
500
+ }
501
+ if (data.entity.guid && !entity.guid) {
502
+ httpError(response, 400);
503
+ return;
504
+ }
505
+ if (entity.$getClientEnabledMethods().indexOf(data.method) === -1) {
506
+ httpError(response, 403);
507
+ return;
508
+ }
509
+ if (
510
+ !(data.method in entity) ||
511
+ typeof entity[data.method] !== 'function'
512
+ ) {
513
+ httpError(response, 400);
514
+ return;
515
+ }
516
+ try {
517
+ const result = entity[data.method](...params);
518
+ let ret = result;
519
+ if (result instanceof Promise) {
520
+ ret = await result;
521
+ }
522
+ response.status(200);
523
+ response.setHeader('Content-Type', 'application/json');
524
+ if (data.stateless) {
525
+ response.send({ return: ret });
526
+ } else {
527
+ response.send({ entity: entity, return: ret });
528
+ }
529
+ } catch (e: any) {
369
530
  httpError(response, 500, e);
531
+ return;
370
532
  }
371
- return;
372
- }
373
- if (data.entity.guid && !entity.guid) {
374
- httpError(response, 400);
375
- return;
376
533
  }
377
- if (entity.$getClientEnabledMethods().indexOf(data.method) === -1) {
378
- httpError(response, 403);
379
- return;
380
- }
381
- if (
382
- !(data.method in entity) ||
383
- typeof entity[data.method] !== 'function'
384
- ) {
385
- httpError(response, 400);
534
+ } catch (e: any) {
535
+ if (e instanceof ForbiddenClassError) {
536
+ httpError(response, 403, e);
386
537
  return;
387
- }
388
- try {
389
- const result = entity[data.method](...params);
390
- let ret = result;
391
- if (result instanceof Promise) {
392
- ret = await result;
393
- }
394
- response.status(200);
395
- response.setHeader('Content-Type', 'application/json');
396
- if (data.stateless) {
397
- response.send({ return: ret });
398
- } else {
399
- response.send({ entity: entity, return: ret });
400
- }
401
- } catch (e: any) {
538
+ } else {
402
539
  httpError(response, 500, e);
403
540
  return;
404
541
  }
@@ -410,10 +547,10 @@ export function createServer(
410
547
  }
411
548
  if (response.locals.nymph.tilmeld) {
412
549
  if (
413
- !response.locals.nymph.tilmeld.checkClientUIDPermissions(
550
+ !(await response.locals.nymph.tilmeld.checkClientUIDPermissions(
414
551
  data,
415
- TilmeldAccessLevels.WRITE_ACCESS
416
- )
552
+ TilmeldAccessLevels.WRITE_ACCESS,
553
+ ))
417
554
  ) {
418
555
  httpError(response, 403);
419
556
  return;
@@ -472,7 +609,7 @@ export function createServer(
472
609
  response: NymphResponse,
473
610
  action: string,
474
611
  data: any,
475
- patch: boolean
612
+ patch: boolean,
476
613
  ) {
477
614
  if (action === 'uid') {
478
615
  if (typeof data.name !== 'string' || typeof data.value !== 'number') {
@@ -481,10 +618,10 @@ export function createServer(
481
618
  }
482
619
  if (response.locals.nymph.tilmeld) {
483
620
  if (
484
- !response.locals.nymph.tilmeld.checkClientUIDPermissions(
621
+ !(await response.locals.nymph.tilmeld.checkClientUIDPermissions(
485
622
  data.name,
486
- TilmeldAccessLevels.FULL_ACCESS
487
- )
623
+ TilmeldAccessLevels.FULL_ACCESS,
624
+ ))
488
625
  ) {
489
626
  httpError(response, 403);
490
627
  return;
@@ -512,6 +649,7 @@ export function createServer(
512
649
  let hadSuccess = false;
513
650
  let invalidRequest = false;
514
651
  let conflict = false;
652
+ let forbidden = false;
515
653
  let notfound = false;
516
654
  let lastException = null;
517
655
  for (let entData of data) {
@@ -522,15 +660,30 @@ export function createServer(
522
660
  }
523
661
  let entity: EntityInterface;
524
662
  try {
525
- entity = await loadEntity(entData, response.locals.nymph, patch);
663
+ entity = await loadEntity(
664
+ classNamesToEntityConstructors(
665
+ response.locals.nymph,
666
+ referencesToEntities(entData, response.locals.nymph, false, true),
667
+ true,
668
+ ),
669
+ response.locals.nymph,
670
+ patch,
671
+ );
526
672
  } catch (e: any) {
527
673
  if (e instanceof EntityConflictError) {
528
674
  conflict = true;
675
+ lastException = e;
676
+ } else if (e instanceof ForbiddenClassError) {
677
+ forbidden = true;
678
+ lastException = e;
529
679
  } else if (e.message === NOT_FOUND_ERROR) {
530
680
  notfound = true;
531
681
  } else if (e instanceof InvalidParametersError) {
532
682
  invalidRequest = true;
533
683
  lastException = e;
684
+ } else if (e.message === 'Not accessible.') {
685
+ forbidden = true;
686
+ lastException = e;
534
687
  } else {
535
688
  lastException = e;
536
689
  }
@@ -543,12 +696,9 @@ export function createServer(
543
696
  continue;
544
697
  }
545
698
  try {
546
- if (await entity.$save()) {
547
- saved.push(entity);
548
- hadSuccess = true;
549
- } else {
550
- saved.push(false);
551
- }
699
+ await entity.$save();
700
+ saved.push(entity);
701
+ hadSuccess = true;
552
702
  } catch (e: any) {
553
703
  if (e instanceof EntityInvalidDataError) {
554
704
  invalidRequest = true;
@@ -561,8 +711,10 @@ export function createServer(
561
711
  if (!hadSuccess) {
562
712
  if (invalidRequest) {
563
713
  httpError(response, 400, lastException);
714
+ } else if (forbidden) {
715
+ httpError(response, 403, lastException);
564
716
  } else if (conflict) {
565
- httpError(response, 409);
717
+ httpError(response, 409, lastException);
566
718
  } else if (notfound) {
567
719
  httpError(response, 404);
568
720
  } else {
@@ -605,6 +757,10 @@ export function createServer(
605
757
  let EntityClass: EntityConstructor;
606
758
  try {
607
759
  EntityClass = response.locals.nymph.getEntityClass(entData.class);
760
+ if (!EntityClass.restEnabled) {
761
+ httpError(response, 403);
762
+ return;
763
+ }
608
764
  } catch (e: any) {
609
765
  invalidRequest = true;
610
766
  failures = true;
@@ -614,7 +770,7 @@ export function createServer(
614
770
  try {
615
771
  entity = await response.locals.nymph.getEntity(
616
772
  { class: EntityClass },
617
- { type: '&', guid: entData.guid }
773
+ { type: '&', guid: entData.guid },
618
774
  );
619
775
  } catch (e: any) {
620
776
  lastException = e;
@@ -627,9 +783,10 @@ export function createServer(
627
783
  continue;
628
784
  }
629
785
  try {
630
- if (await entity.$delete()) {
786
+ try {
787
+ await entity.$delete();
631
788
  deleted.push(entData.guid);
632
- } else {
789
+ } catch (e: any) {
633
790
  failures = true;
634
791
  }
635
792
  } catch (e: any) {
@@ -661,10 +818,10 @@ export function createServer(
661
818
  }
662
819
  if (response.locals.nymph.tilmeld) {
663
820
  if (
664
- !response.locals.nymph.tilmeld.checkClientUIDPermissions(
821
+ !(await response.locals.nymph.tilmeld.checkClientUIDPermissions(
665
822
  data,
666
- TilmeldAccessLevels.FULL_ACCESS
667
- )
823
+ TilmeldAccessLevels.FULL_ACCESS,
824
+ ))
668
825
  ) {
669
826
  httpError(response, 403);
670
827
  return;
@@ -698,15 +855,18 @@ export function createServer(
698
855
  entityData: EntityJson | EntityPatch,
699
856
  nymph: Nymph,
700
857
  patch = false,
701
- allowConflict = false
858
+ allowConflict = false,
702
859
  ): Promise<EntityInterface> {
703
- if (entityData.class === 'Entity') {
860
+ let EntityClass = entityData.class;
861
+ if (EntityClass.class === 'Entity') {
704
862
  // Don't let clients use the `Entity` class, since it has no validity/AC checks.
705
- throw new InvalidParametersError(
706
- "Can't use Entity class directly from the front end."
863
+ throw new ForbiddenClassError(
864
+ "Can't use Entity class directly from the front end.",
707
865
  );
708
866
  }
709
- let EntityClass = nymph.getEntityClass(entityData.class);
867
+ if (!EntityClass.restEnabled) {
868
+ throw new ForbiddenClassError('Not accessible.');
869
+ }
710
870
  let entity: EntityInterface | null;
711
871
  if (entityData.guid) {
712
872
  entity = await nymph.getEntity(
@@ -714,7 +874,7 @@ export function createServer(
714
874
  {
715
875
  type: '&',
716
876
  guid: `${entityData['guid']}`,
717
- }
877
+ },
718
878
  );
719
879
  if (entity === null) {
720
880
  throw new Error(NOT_FOUND_ERROR);
@@ -731,46 +891,61 @@ export function createServer(
731
891
  }
732
892
 
733
893
  /**
734
- * Check if an item is a reference, and if it is, convert it to an entity.
735
- *
736
- * This function will recurse into deeper arrays and objects.
894
+ * Return the request with an HTTP error response.
737
895
  *
738
- * @param item The item to check.
739
- * @returns The item, converted.
896
+ * @param res The server response object.
897
+ * @param defaultStatusCode The HTTP status code to use if none is given in the error object.
898
+ * @param error An optional error object to report.
740
899
  */
741
- function referencesToEntities(item: any, nymph: Nymph): any {
742
- if (Array.isArray(item)) {
743
- if (item.length === 3 && item[0] === 'nymph_entity_reference') {
744
- try {
745
- const EntityClass = nymph.getEntityClass(item[1]);
746
- return EntityClass.factoryReference(item as EntityReference);
747
- } catch (e: any) {
748
- return item;
749
- }
750
- }
751
- return item.map((entry) => referencesToEntities(entry, nymph));
752
- } else if (typeof item === 'object' && !(item instanceof Entity)) {
753
- // Only do this for non-entity objects.
754
- const newItem: { [k: string]: any } = {};
755
- for (let curProperty in item) {
756
- newItem[curProperty] = referencesToEntities(item[curProperty], nymph);
757
- }
758
- return newItem;
900
+ function httpError(
901
+ res: NymphResponse,
902
+ defaultStatusCode: number,
903
+ error?: Error & { status?: number; statusText?: string },
904
+ ) {
905
+ const status = error?.status || defaultStatusCode;
906
+ const statusText =
907
+ error?.statusText ||
908
+ (error?.status == null
909
+ ? statusDescriptions[defaultStatusCode]
910
+ : error.status in statusDescriptions &&
911
+ statusDescriptions[error.status]) ||
912
+ 'Internal Server Error';
913
+
914
+ const errorResponse = error
915
+ ? {
916
+ textStatus: `${status} ${statusText}`,
917
+ statusText,
918
+ message: error.message,
919
+ error,
920
+ ...(process.env.NODE_ENV !== 'production'
921
+ ? { stack: error.stack }
922
+ : {}),
923
+ }
924
+ : {
925
+ textStatus: `${status} ${statusText}`,
926
+ statusText,
927
+ message: statusText,
928
+ };
929
+
930
+ if (!res.headersSent) {
931
+ res.status(status);
932
+ res.setHeader('Content-Type', 'application/json');
759
933
  }
760
- return item;
934
+
935
+ res.send(errorResponse);
761
936
  }
762
937
 
763
938
  /**
764
- * Return the request with an HTTP error response.
939
+ * End the event stream with an HTTP error response.
765
940
  *
766
941
  * @param res The server response object.
767
942
  * @param defaultStatusCode The HTTP status code to use if none is given in the error object.
768
943
  * @param error An optional error object to report.
769
944
  */
770
- function httpError(
945
+ function eventStreamError(
771
946
  res: NymphResponse,
772
947
  defaultStatusCode: number,
773
- error?: Error & { status?: number; statusText?: string }
948
+ error?: Error & { status?: number; statusText?: string },
774
949
  ) {
775
950
  const status = error?.status || defaultStatusCode;
776
951
  const statusText =
@@ -780,26 +955,37 @@ export function createServer(
780
955
  : error.status in statusDescriptions &&
781
956
  statusDescriptions[error.status]) ||
782
957
  'Internal Server Error';
958
+
959
+ const errorResponse = error
960
+ ? {
961
+ status,
962
+ textStatus: `${status} ${statusText}`,
963
+ statusText,
964
+ message: error.message,
965
+ error,
966
+ ...(process.env.NODE_ENV !== 'production'
967
+ ? { stack: error.stack }
968
+ : {}),
969
+ }
970
+ : {
971
+ status,
972
+ textStatus: `${status} ${statusText}`,
973
+ statusText,
974
+ message: statusText,
975
+ };
976
+
783
977
  if (!res.headersSent) {
784
978
  res.status(status);
785
979
  res.setHeader('Content-Type', 'application/json');
786
- }
787
- if (error) {
788
- res.send({
789
- textStatus: `${status} ${statusText}`,
790
- statusText,
791
- message: error.message,
792
- error,
793
- ...(process.env.NODE_ENV !== 'production'
794
- ? { stack: error.stack }
795
- : {}),
796
- });
980
+
981
+ res.send(errorResponse);
797
982
  } else {
798
- res.send({
799
- textStatus: `${status} ${statusText}`,
800
- statusText,
801
- message: statusText,
802
- });
983
+ res.write('event: error\n');
984
+ res.write(`data: ${JSON.stringify(errorResponse)}\n\n`);
985
+
986
+ res.write('event: finished\n');
987
+ res.write('data: \n\n');
988
+ res.end();
803
989
  }
804
990
  }
805
991