@nymphjs/server 1.0.0-beta.2 → 1.0.0-beta.21

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.
@@ -0,0 +1,807 @@
1
+ import type { OptionsJson } from 'body-parser';
2
+ import express, { NextFunction, Request, Response } from 'express';
3
+ import cookieParser from 'cookie-parser';
4
+ import {
5
+ Nymph,
6
+ Entity,
7
+ EntityConflictError,
8
+ EntityConstructor,
9
+ EntityInterface,
10
+ EntityJson,
11
+ EntityPatch,
12
+ EntityReference,
13
+ InvalidParametersError,
14
+ TilmeldAccessLevels,
15
+ classNamesToEntityConstructors,
16
+ Options,
17
+ Selector,
18
+ } from '@nymphjs/nymph';
19
+ import { EntityInvalidDataError } from '@nymphjs/nymph';
20
+
21
+ import { statusDescriptions } from './statusDescriptions';
22
+
23
+ type NymphResponse = Response<any, { nymph: Nymph }>;
24
+
25
+ const NOT_FOUND_ERROR = 'Entity is not found.';
26
+
27
+ /**
28
+ * A REST server middleware creator for Nymph.
29
+ *
30
+ * Written by Hunter Perrin for SciActive.
31
+ *
32
+ * @author Hunter Perrin <hperrin@gmail.com>
33
+ * @copyright SciActive Inc
34
+ * @see http://nymph.io/
35
+ */
36
+ export function createServer(
37
+ nymph: Nymph,
38
+ { jsonOptions = {} }: { jsonOptions?: OptionsJson } = {}
39
+ ) {
40
+ const rest = express();
41
+ rest.use(cookieParser());
42
+ rest.use(express.json(jsonOptions || {}));
43
+
44
+ function instantiateNymph(
45
+ _request: Request,
46
+ response: NymphResponse,
47
+ next: NextFunction
48
+ ) {
49
+ response.locals.nymph = nymph.clone();
50
+ next();
51
+ }
52
+
53
+ function authenticateTilmeld(
54
+ request: Request,
55
+ response: NymphResponse,
56
+ next: NextFunction
57
+ ) {
58
+ if (response.locals.nymph.tilmeld) {
59
+ response.locals.nymph.tilmeld.request = request;
60
+ response.locals.nymph.tilmeld.response = response;
61
+ try {
62
+ response.locals.nymph.tilmeld.authenticate();
63
+ } catch (e: any) {
64
+ httpError(response, 500, e);
65
+ return;
66
+ }
67
+ }
68
+ next();
69
+ }
70
+
71
+ function unauthenticateTilmeld(
72
+ _request: Request,
73
+ response: NymphResponse,
74
+ next: NextFunction
75
+ ) {
76
+ if (response.locals.nymph.tilmeld) {
77
+ response.locals.nymph.tilmeld.request = null;
78
+ response.locals.nymph.tilmeld.response = null;
79
+ try {
80
+ response.locals.nymph.tilmeld.clearSession();
81
+ } catch (e: any) {
82
+ httpError(response, 500, e);
83
+ return;
84
+ }
85
+ }
86
+ next();
87
+ }
88
+
89
+ function getActionData(request: Request): { action: string; data: any } {
90
+ if (request.method === 'GET') {
91
+ if (
92
+ typeof request.query?.action !== 'string' ||
93
+ typeof request.query?.data !== 'string'
94
+ ) {
95
+ return {
96
+ action: '',
97
+ data: {},
98
+ };
99
+ }
100
+ return {
101
+ action: JSON.parse(request.query.action) ?? '',
102
+ data: JSON.parse(request.query.data),
103
+ };
104
+ } else {
105
+ return {
106
+ action: request.body.action ?? '',
107
+ data: request.body.data,
108
+ };
109
+ }
110
+ }
111
+
112
+ // Create a new instance of Nymph for the request/response.
113
+ rest.use(instantiateNymph);
114
+
115
+ // Authenticate before the request.
116
+ rest.use(authenticateTilmeld);
117
+
118
+ rest.get('/', async (request, response: NymphResponse) => {
119
+ try {
120
+ const { action, data } = getActionData(request);
121
+ if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
122
+ httpError(response, 400);
123
+ return;
124
+ }
125
+ if (['entity', 'entities'].indexOf(action) !== -1) {
126
+ if (!Array.isArray(data)) {
127
+ httpError(response, 400);
128
+ return;
129
+ }
130
+ const count = data.length;
131
+ if (count < 1 || typeof data[0] !== 'object') {
132
+ httpError(response, 400);
133
+ return;
134
+ }
135
+ if (!('class' in data[0])) {
136
+ httpError(response, 400);
137
+ return;
138
+ }
139
+ let [options, ...selectors] = data as [Options, ...Selector[]];
140
+ let EntityClass;
141
+ try {
142
+ EntityClass = response.locals.nymph.getEntityClass(data[0].class);
143
+ } catch (e: any) {
144
+ httpError(response, 400, e);
145
+ return;
146
+ }
147
+ options.class = EntityClass;
148
+ options.source = 'client';
149
+ options.skipAc = false;
150
+ selectors = classNamesToEntityConstructors(
151
+ response.locals.nymph,
152
+ selectors
153
+ );
154
+ let result:
155
+ | EntityInterface
156
+ | EntityInterface[]
157
+ | string
158
+ | string[]
159
+ | number
160
+ | null;
161
+ try {
162
+ if (action === 'entity') {
163
+ result = await response.locals.nymph.getEntity(
164
+ options,
165
+ ...selectors
166
+ );
167
+ } else {
168
+ result = await response.locals.nymph.getEntities(
169
+ options,
170
+ ...selectors
171
+ );
172
+ }
173
+ } catch (e: any) {
174
+ httpError(response, 500, e);
175
+ return;
176
+ }
177
+ if (result == null || (Array.isArray(result) && result.length === 0)) {
178
+ if (
179
+ action === 'entity' ||
180
+ response.locals.nymph.config.emptyListError
181
+ ) {
182
+ httpError(response, 404);
183
+ return;
184
+ }
185
+ }
186
+ response.setHeader('Content-Type', 'application/json');
187
+ response.send(JSON.stringify(result));
188
+ } else {
189
+ if (typeof data !== 'string') {
190
+ httpError(response, 400);
191
+ return;
192
+ }
193
+ if (response.locals.nymph.tilmeld) {
194
+ if (
195
+ !response.locals.nymph.tilmeld.checkClientUIDPermissions(
196
+ data,
197
+ TilmeldAccessLevels.READ_ACCESS
198
+ )
199
+ ) {
200
+ httpError(response, 403);
201
+ return;
202
+ }
203
+ }
204
+ let result: number | null;
205
+ try {
206
+ result = await response.locals.nymph.getUID(data);
207
+ } catch (e: any) {
208
+ httpError(response, 500, e);
209
+ return;
210
+ }
211
+ if (result === null) {
212
+ httpError(response, 404);
213
+ return;
214
+ } else if (typeof result !== 'number') {
215
+ httpError(response, 500);
216
+ return;
217
+ }
218
+ response.setHeader('Content-Type', 'text/plain');
219
+ response.send(`${result}`);
220
+ }
221
+ } catch (e: any) {
222
+ httpError(response, 500, e);
223
+ return;
224
+ }
225
+ });
226
+
227
+ rest.post('/', async (request, response: NymphResponse) => {
228
+ try {
229
+ const { action, data: dataConst } = getActionData(request);
230
+ let data = dataConst;
231
+ if (['entity', 'entities', 'uid', 'method'].indexOf(action) === -1) {
232
+ httpError(response, 400);
233
+ return;
234
+ }
235
+ if (['entity', 'entities'].indexOf(action) !== -1) {
236
+ if (action === 'entity') {
237
+ data = [data];
238
+ }
239
+ const created = [];
240
+ let hadSuccess = false;
241
+ let invalidRequest = false;
242
+ let conflict = false;
243
+ let notfound = false;
244
+ let lastException = null;
245
+ for (let entData of data) {
246
+ if (entData.guid) {
247
+ invalidRequest = true;
248
+ created.push(null);
249
+ continue;
250
+ }
251
+ let entity: EntityInterface;
252
+ try {
253
+ entity = await loadEntity(entData, response.locals.nymph);
254
+ } catch (e: any) {
255
+ if (e instanceof EntityConflictError) {
256
+ conflict = true;
257
+ } else if (e.message === NOT_FOUND_ERROR) {
258
+ notfound = true;
259
+ } else if (e instanceof InvalidParametersError) {
260
+ invalidRequest = true;
261
+ lastException = e;
262
+ } else {
263
+ lastException = e;
264
+ }
265
+ created.push(null);
266
+ continue;
267
+ }
268
+ if (!entity) {
269
+ invalidRequest = true;
270
+ created.push(null);
271
+ continue;
272
+ }
273
+ try {
274
+ if (await entity.$save()) {
275
+ created.push(entity);
276
+ hadSuccess = true;
277
+ } else {
278
+ created.push(false);
279
+ }
280
+ } catch (e: any) {
281
+ if (e instanceof EntityInvalidDataError) {
282
+ invalidRequest = true;
283
+ } else {
284
+ lastException = e;
285
+ }
286
+ created.push(null);
287
+ }
288
+ }
289
+ if (!hadSuccess) {
290
+ if (invalidRequest) {
291
+ httpError(response, 400, lastException);
292
+ return;
293
+ } else if (conflict) {
294
+ httpError(response, 409);
295
+ return;
296
+ } else if (notfound) {
297
+ httpError(response, 404);
298
+ return;
299
+ } else {
300
+ httpError(response, 500, lastException);
301
+ return;
302
+ }
303
+ }
304
+ response.status(201);
305
+ response.setHeader('Content-Type', 'application/json');
306
+ if (action === 'entity') {
307
+ response.send(JSON.stringify(created[0]));
308
+ } else {
309
+ response.send(created);
310
+ }
311
+ } else if (action === 'method') {
312
+ if (!Array.isArray(data.params)) {
313
+ httpError(response, 400);
314
+ return;
315
+ }
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;
349
+ }
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);
368
+ } else {
369
+ httpError(response, 500, e);
370
+ }
371
+ return;
372
+ }
373
+ if (data.entity.guid && !entity.guid) {
374
+ httpError(response, 400);
375
+ return;
376
+ }
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);
386
+ 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) {
402
+ httpError(response, 500, e);
403
+ return;
404
+ }
405
+ }
406
+ } else {
407
+ if (typeof data !== 'string') {
408
+ httpError(response, 400);
409
+ return;
410
+ }
411
+ if (response.locals.nymph.tilmeld) {
412
+ if (
413
+ !response.locals.nymph.tilmeld.checkClientUIDPermissions(
414
+ data,
415
+ TilmeldAccessLevels.WRITE_ACCESS
416
+ )
417
+ ) {
418
+ httpError(response, 403);
419
+ return;
420
+ }
421
+ }
422
+ let result: number | null;
423
+ try {
424
+ result = await response.locals.nymph.newUID(data);
425
+ } catch (e: any) {
426
+ httpError(response, 500, e);
427
+ return;
428
+ }
429
+ if (typeof result !== 'number') {
430
+ httpError(response, 500);
431
+ return;
432
+ }
433
+ response.status(201);
434
+ response.setHeader('Content-Type', 'text/plain');
435
+ response.send(`${result}`);
436
+ }
437
+ } catch (e: any) {
438
+ httpError(response, 500, e);
439
+ return;
440
+ }
441
+ });
442
+
443
+ rest.put('/', async (request, response: NymphResponse) => {
444
+ try {
445
+ const { action, data } = getActionData(request);
446
+ if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
447
+ httpError(response, 400);
448
+ return;
449
+ }
450
+ await doPutOrPatch(response, action, data, false);
451
+ } catch (e: any) {
452
+ httpError(response, 500, e);
453
+ return;
454
+ }
455
+ });
456
+
457
+ rest.patch('/', async (request, response: NymphResponse) => {
458
+ try {
459
+ const { action, data } = getActionData(request);
460
+ if (['entity', 'entities'].indexOf(action) === -1) {
461
+ httpError(response, 400);
462
+ return;
463
+ }
464
+ await doPutOrPatch(response, action, data, true);
465
+ } catch (e: any) {
466
+ httpError(response, 500, e);
467
+ return;
468
+ }
469
+ });
470
+
471
+ async function doPutOrPatch(
472
+ response: NymphResponse,
473
+ action: string,
474
+ data: any,
475
+ patch: boolean
476
+ ) {
477
+ if (action === 'uid') {
478
+ if (typeof data.name !== 'string' || typeof data.value !== 'number') {
479
+ httpError(response, 400);
480
+ return;
481
+ }
482
+ if (response.locals.nymph.tilmeld) {
483
+ if (
484
+ !response.locals.nymph.tilmeld.checkClientUIDPermissions(
485
+ data.name,
486
+ TilmeldAccessLevels.FULL_ACCESS
487
+ )
488
+ ) {
489
+ httpError(response, 403);
490
+ return;
491
+ }
492
+ }
493
+ let result: boolean;
494
+ try {
495
+ result = await response.locals.nymph.setUID(data.name, data.value);
496
+ } catch (e: any) {
497
+ httpError(response, 500, e);
498
+ return;
499
+ }
500
+ if (!result) {
501
+ httpError(response, 500);
502
+ return;
503
+ }
504
+ response.status(200);
505
+ response.setHeader('Content-Type', 'text/plain');
506
+ response.send(`${result}`);
507
+ } else {
508
+ if (action === 'entity') {
509
+ data = [data];
510
+ }
511
+ const saved = [];
512
+ let hadSuccess = false;
513
+ let invalidRequest = false;
514
+ let conflict = false;
515
+ let notfound = false;
516
+ let lastException = null;
517
+ for (let entData of data) {
518
+ if (entData.guid && entData.guid.length != 24) {
519
+ invalidRequest = true;
520
+ saved.push(null);
521
+ continue;
522
+ }
523
+ let entity: EntityInterface;
524
+ try {
525
+ entity = await loadEntity(entData, response.locals.nymph, patch);
526
+ } catch (e: any) {
527
+ if (e instanceof EntityConflictError) {
528
+ conflict = true;
529
+ } else if (e.message === NOT_FOUND_ERROR) {
530
+ notfound = true;
531
+ } else if (e instanceof InvalidParametersError) {
532
+ invalidRequest = true;
533
+ lastException = e;
534
+ } else {
535
+ lastException = e;
536
+ }
537
+ saved.push(null);
538
+ continue;
539
+ }
540
+ if (!entity) {
541
+ invalidRequest = true;
542
+ saved.push(null);
543
+ continue;
544
+ }
545
+ try {
546
+ if (await entity.$save()) {
547
+ saved.push(entity);
548
+ hadSuccess = true;
549
+ } else {
550
+ saved.push(false);
551
+ }
552
+ } catch (e: any) {
553
+ if (e instanceof EntityInvalidDataError) {
554
+ invalidRequest = true;
555
+ } else {
556
+ lastException = e;
557
+ }
558
+ saved.push(null);
559
+ }
560
+ }
561
+ if (!hadSuccess) {
562
+ if (invalidRequest) {
563
+ httpError(response, 400, lastException);
564
+ } else if (conflict) {
565
+ httpError(response, 409);
566
+ } else if (notfound) {
567
+ httpError(response, 404);
568
+ } else {
569
+ httpError(response, 500, lastException);
570
+ }
571
+ return;
572
+ }
573
+ response.status(200);
574
+ response.setHeader('Content-Type', 'application/json');
575
+ if (action === 'entity') {
576
+ response.send(JSON.stringify(saved[0]));
577
+ } else {
578
+ response.send(saved);
579
+ }
580
+ }
581
+ }
582
+
583
+ rest.delete('/', async (request, response: NymphResponse) => {
584
+ try {
585
+ const { action, data: dataConst } = getActionData(request);
586
+ let data = dataConst;
587
+ if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
588
+ httpError(response, 400);
589
+ return;
590
+ }
591
+ if (['entity', 'entities'].indexOf(action) !== -1) {
592
+ if (action === 'entity') {
593
+ data = [data];
594
+ }
595
+ const deleted = [];
596
+ let failures = false;
597
+ let invalidRequest = false;
598
+ let notfound = false;
599
+ let lastException = null;
600
+ for (let entData of data) {
601
+ if (entData.guid && entData.guid.length != 24) {
602
+ invalidRequest = true;
603
+ continue;
604
+ }
605
+ let EntityClass: EntityConstructor;
606
+ try {
607
+ EntityClass = response.locals.nymph.getEntityClass(entData.class);
608
+ } catch (e: any) {
609
+ invalidRequest = true;
610
+ failures = true;
611
+ continue;
612
+ }
613
+ let entity: EntityInterface | null;
614
+ try {
615
+ entity = await response.locals.nymph.getEntity(
616
+ { class: EntityClass },
617
+ { type: '&', guid: entData.guid }
618
+ );
619
+ } catch (e: any) {
620
+ lastException = e;
621
+ failures = true;
622
+ continue;
623
+ }
624
+ if (!entity) {
625
+ notfound = true;
626
+ failures = true;
627
+ continue;
628
+ }
629
+ try {
630
+ if (await entity.$delete()) {
631
+ deleted.push(entData.guid);
632
+ } else {
633
+ failures = true;
634
+ }
635
+ } catch (e: any) {
636
+ lastException = e;
637
+ failures = true;
638
+ }
639
+ }
640
+ if (deleted.length === 0) {
641
+ if (invalidRequest || !failures) {
642
+ httpError(response, 400, lastException);
643
+ } else if (notfound) {
644
+ httpError(response, 404);
645
+ } else {
646
+ httpError(response, 500, lastException);
647
+ }
648
+ return;
649
+ }
650
+ response.status(200);
651
+ response.setHeader('Content-Type', 'application/json');
652
+ if (action === 'entity') {
653
+ response.send(JSON.stringify(deleted[0]));
654
+ } else {
655
+ response.send(deleted);
656
+ }
657
+ } else {
658
+ if (typeof data !== 'string') {
659
+ httpError(response, 400);
660
+ return;
661
+ }
662
+ if (response.locals.nymph.tilmeld) {
663
+ if (
664
+ !response.locals.nymph.tilmeld.checkClientUIDPermissions(
665
+ data,
666
+ TilmeldAccessLevels.FULL_ACCESS
667
+ )
668
+ ) {
669
+ httpError(response, 403);
670
+ return;
671
+ }
672
+ }
673
+ let result: boolean;
674
+ try {
675
+ result = await response.locals.nymph.deleteUID(data);
676
+ } catch (e: any) {
677
+ httpError(response, 500, e);
678
+ return;
679
+ }
680
+ if (!result) {
681
+ httpError(response, 500);
682
+ return;
683
+ }
684
+ response.status(200);
685
+ response.setHeader('Content-Type', 'application/json');
686
+ response.send(JSON.stringify(result));
687
+ }
688
+ } catch (e: any) {
689
+ httpError(response, 500, e);
690
+ return;
691
+ }
692
+ });
693
+
694
+ // Unauthenticate after the request.
695
+ rest.use(unauthenticateTilmeld);
696
+
697
+ async function loadEntity(
698
+ entityData: EntityJson | EntityPatch,
699
+ nymph: Nymph,
700
+ patch = false,
701
+ allowConflict = false
702
+ ): Promise<EntityInterface> {
703
+ if (entityData.class === 'Entity') {
704
+ // 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."
707
+ );
708
+ }
709
+ let EntityClass = nymph.getEntityClass(entityData.class);
710
+ let entity: EntityInterface | null;
711
+ if (entityData.guid) {
712
+ entity = await nymph.getEntity(
713
+ { class: EntityClass, source: 'client' },
714
+ {
715
+ type: '&',
716
+ guid: `${entityData['guid']}`,
717
+ }
718
+ );
719
+ if (entity === null) {
720
+ throw new Error(NOT_FOUND_ERROR);
721
+ }
722
+ } else {
723
+ entity = await EntityClass.factory();
724
+ }
725
+ if (patch) {
726
+ entity.$jsonAcceptPatch(entityData as EntityPatch, allowConflict);
727
+ } else {
728
+ entity.$jsonAcceptData(entityData as EntityJson, allowConflict);
729
+ }
730
+ return entity;
731
+ }
732
+
733
+ /**
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.
737
+ *
738
+ * @param item The item to check.
739
+ * @returns The item, converted.
740
+ */
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;
759
+ }
760
+ return item;
761
+ }
762
+
763
+ /**
764
+ * Return the request with an HTTP error response.
765
+ *
766
+ * @param res The server response object.
767
+ * @param defaultStatusCode The HTTP status code to use if none is given in the error object.
768
+ * @param error An optional error object to report.
769
+ */
770
+ function httpError(
771
+ res: NymphResponse,
772
+ defaultStatusCode: number,
773
+ error?: Error & { status?: number; statusText?: string }
774
+ ) {
775
+ const status = error?.status || defaultStatusCode;
776
+ const statusText =
777
+ error?.statusText ||
778
+ (error?.status == null
779
+ ? statusDescriptions[defaultStatusCode]
780
+ : error.status in statusDescriptions &&
781
+ statusDescriptions[error.status]) ||
782
+ 'Internal Server Error';
783
+ if (!res.headersSent) {
784
+ res.status(status);
785
+ 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
+ });
797
+ } else {
798
+ res.send({
799
+ textStatus: `${status} ${statusText}`,
800
+ statusText,
801
+ message: statusText,
802
+ });
803
+ }
804
+ }
805
+
806
+ return rest;
807
+ }