@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.
package/src/index.ts CHANGED
@@ -1,795 +1,7 @@
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';
1
+ import { createServer } from './createServer';
20
2
 
21
- type NymphResponse = Response<any, { nymph: Nymph }>;
3
+ export * from './HttpError';
4
+ export * from './statusDescriptions';
5
+ export * from './createServer';
22
6
 
23
- const NOT_FOUND_ERROR = 'Entity is not found.';
24
-
25
- /**
26
- * A REST server middleware creator for Nymph.
27
- *
28
- * Written by Hunter Perrin for SciActive.
29
- *
30
- * @author Hunter Perrin <hperrin@gmail.com>
31
- * @copyright SciActive Inc
32
- * @see http://nymph.io/
33
- */
34
- export default function createServer(
35
- nymph: Nymph,
36
- { jsonOptions = {} }: { jsonOptions?: OptionsJson } = {}
37
- ) {
38
- const rest = express();
39
- rest.use(cookieParser());
40
- rest.use(express.json(jsonOptions || {}));
41
-
42
- function instantiateNymph(
43
- _request: Request,
44
- response: NymphResponse,
45
- next: NextFunction
46
- ) {
47
- response.locals.nymph = nymph.clone();
48
- next();
49
- }
50
-
51
- function authenticateTilmeld(
52
- request: Request,
53
- response: NymphResponse,
54
- next: NextFunction
55
- ) {
56
- if (response.locals.nymph.tilmeld) {
57
- response.locals.nymph.tilmeld.request = request;
58
- response.locals.nymph.tilmeld.response = response;
59
- try {
60
- response.locals.nymph.tilmeld.authenticate();
61
- } catch (e: any) {
62
- httpError(response, 500, 'Internal Server Error', e);
63
- return;
64
- }
65
- }
66
- next();
67
- }
68
-
69
- function unauthenticateTilmeld(
70
- _request: Request,
71
- response: NymphResponse,
72
- next: NextFunction
73
- ) {
74
- if (response.locals.nymph.tilmeld) {
75
- response.locals.nymph.tilmeld.request = null;
76
- response.locals.nymph.tilmeld.response = null;
77
- try {
78
- response.locals.nymph.tilmeld.clearSession();
79
- } catch (e: any) {
80
- httpError(response, 500, 'Internal Server Error', e);
81
- return;
82
- }
83
- }
84
- next();
85
- }
86
-
87
- function getActionData(request: Request): { action: string; data: any } {
88
- if (request.method === 'GET') {
89
- if (
90
- typeof request.query?.action !== 'string' ||
91
- typeof request.query?.data !== 'string'
92
- ) {
93
- return {
94
- action: '',
95
- data: {},
96
- };
97
- }
98
- return {
99
- action: JSON.parse(request.query.action) ?? '',
100
- data: JSON.parse(request.query.data),
101
- };
102
- } else {
103
- return {
104
- action: request.body.action ?? '',
105
- data: request.body.data,
106
- };
107
- }
108
- }
109
-
110
- // Create a new instance of Nymph for the request/response.
111
- rest.use(instantiateNymph);
112
-
113
- // Authenticate before the request.
114
- rest.use(authenticateTilmeld);
115
-
116
- rest.get('/', async (request, response: NymphResponse) => {
117
- try {
118
- const { action, data } = getActionData(request);
119
- if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
120
- httpError(response, 400, 'Bad Request');
121
- return;
122
- }
123
- if (['entity', 'entities'].indexOf(action) !== -1) {
124
- if (!Array.isArray(data)) {
125
- httpError(response, 400, 'Bad Request');
126
- return;
127
- }
128
- const count = data.length;
129
- if (count < 1 || typeof data[0] !== 'object') {
130
- httpError(response, 400, 'Bad Request');
131
- return;
132
- }
133
- if (!('class' in data[0])) {
134
- httpError(response, 400, 'Bad Request');
135
- return;
136
- }
137
- let [options, ...selectors] = data as [Options, ...Selector[]];
138
- let EntityClass;
139
- try {
140
- EntityClass = response.locals.nymph.getEntityClass(data[0].class);
141
- } catch (e: any) {
142
- httpError(response, 400, 'Bad Request', e);
143
- return;
144
- }
145
- options.class = EntityClass;
146
- options.source = 'client';
147
- options.skipAc = false;
148
- selectors = classNamesToEntityConstructors(
149
- response.locals.nymph,
150
- selectors
151
- );
152
- let result:
153
- | EntityInterface
154
- | EntityInterface[]
155
- | string
156
- | string[]
157
- | number
158
- | null;
159
- try {
160
- if (action === 'entity') {
161
- result = await response.locals.nymph.getEntity(
162
- options,
163
- ...selectors
164
- );
165
- } else {
166
- result = await response.locals.nymph.getEntities(
167
- options,
168
- ...selectors
169
- );
170
- }
171
- } catch (e: any) {
172
- httpError(response, 500, 'Internal Server Error', e);
173
- return;
174
- }
175
- if (result == null || (Array.isArray(result) && result.length === 0)) {
176
- if (
177
- action === 'entity' ||
178
- response.locals.nymph.config.emptyListError
179
- ) {
180
- httpError(response, 404, 'Not Found');
181
- return;
182
- }
183
- }
184
- response.setHeader('Content-Type', 'application/json');
185
- response.send(JSON.stringify(result));
186
- } else {
187
- if (typeof data !== 'string') {
188
- httpError(response, 400, 'Bad Request');
189
- return;
190
- }
191
- if (response.locals.nymph.tilmeld) {
192
- if (
193
- !response.locals.nymph.tilmeld.checkClientUIDPermissions(
194
- data,
195
- TilmeldAccessLevels.READ_ACCESS
196
- )
197
- ) {
198
- httpError(response, 403, 'Forbidden');
199
- return;
200
- }
201
- }
202
- let result: number | null;
203
- try {
204
- result = await response.locals.nymph.getUID(data);
205
- } catch (e: any) {
206
- httpError(response, 500, 'Internal Server Error', e);
207
- return;
208
- }
209
- if (result === null) {
210
- httpError(response, 404, 'Not Found');
211
- return;
212
- } else if (typeof result !== 'number') {
213
- httpError(response, 500, 'Internal Server Error');
214
- return;
215
- }
216
- response.setHeader('Content-Type', 'text/plain');
217
- response.send(`${result}`);
218
- }
219
- } catch (e: any) {
220
- httpError(response, 500, 'Internal Server Error', e);
221
- return;
222
- }
223
- });
224
-
225
- rest.post('/', async (request, response: NymphResponse) => {
226
- try {
227
- const { action, data: dataConst } = getActionData(request);
228
- let data = dataConst;
229
- if (['entity', 'entities', 'uid', 'method'].indexOf(action) === -1) {
230
- httpError(response, 400, 'Bad Request');
231
- return;
232
- }
233
- if (['entity', 'entities'].indexOf(action) !== -1) {
234
- if (action === 'entity') {
235
- data = [data];
236
- }
237
- const created = [];
238
- let hadSuccess = false;
239
- let invalidRequest = false;
240
- let conflict = false;
241
- let notfound = false;
242
- let lastException = null;
243
- for (let entData of data) {
244
- if (entData.guid) {
245
- invalidRequest = true;
246
- created.push(null);
247
- continue;
248
- }
249
- let entity: EntityInterface;
250
- try {
251
- entity = await loadEntity(entData, response.locals.nymph);
252
- } catch (e: any) {
253
- if (e instanceof EntityConflictError) {
254
- conflict = true;
255
- } else if (e.message === NOT_FOUND_ERROR) {
256
- notfound = true;
257
- } else if (e instanceof InvalidParametersError) {
258
- invalidRequest = true;
259
- lastException = e;
260
- } else {
261
- lastException = e;
262
- }
263
- created.push(null);
264
- continue;
265
- }
266
- if (!entity) {
267
- invalidRequest = true;
268
- created.push(null);
269
- continue;
270
- }
271
- try {
272
- if (await entity.$save()) {
273
- created.push(entity);
274
- hadSuccess = true;
275
- } else {
276
- created.push(false);
277
- }
278
- } catch (e: any) {
279
- if (e instanceof EntityInvalidDataError) {
280
- invalidRequest = true;
281
- } else {
282
- lastException = e;
283
- }
284
- created.push(null);
285
- }
286
- }
287
- if (!hadSuccess) {
288
- if (invalidRequest) {
289
- httpError(response, 400, 'Bad Request', lastException);
290
- return;
291
- } else if (conflict) {
292
- httpError(response, 409, 'Conflict');
293
- return;
294
- } else if (notfound) {
295
- httpError(response, 404, 'Not Found');
296
- return;
297
- } else {
298
- httpError(response, 500, 'Internal Server Error', lastException);
299
- return;
300
- }
301
- }
302
- response.status(201);
303
- response.setHeader('Content-Type', 'application/json');
304
- if (action === 'entity') {
305
- response.send(JSON.stringify(created[0]));
306
- } else {
307
- response.send(created);
308
- }
309
- } else if (action === 'method') {
310
- if (!Array.isArray(data.params)) {
311
- httpError(response, 400, 'Bad Request');
312
- return;
313
- }
314
- const params = referencesToEntities(
315
- [...data.params],
316
- response.locals.nymph
317
- );
318
- if (data.static) {
319
- let EntityClass: EntityConstructor;
320
- try {
321
- EntityClass = response.locals.nymph.getEntityClass(data.class);
322
- } catch (e: any) {
323
- httpError(response, 400, 'Bad Request');
324
- return;
325
- }
326
- if (
327
- EntityClass.clientEnabledStaticMethods.indexOf(data.method) === -1
328
- ) {
329
- httpError(response, 403, 'Forbidden');
330
- return;
331
- }
332
- if (!(data.method in EntityClass)) {
333
- httpError(response, 400, 'Bad Request');
334
- return;
335
- }
336
- // @ts-ignore Dynamic methods make TypeScript sad.
337
- const method: Function = EntityClass[data.method];
338
- if (typeof method !== 'function') {
339
- httpError(response, 400, 'Bad Request');
340
- return;
341
- }
342
- try {
343
- const result = method.call(EntityClass, ...params);
344
- let ret = result;
345
- if (result instanceof Promise) {
346
- ret = await result;
347
- }
348
- response.status(200);
349
- response.setHeader('Content-Type', 'application/json');
350
- response.send({ return: ret });
351
- } catch (e: any) {
352
- httpError(response, 500, 'Internal Server Error', e);
353
- return;
354
- }
355
- } else {
356
- let entity: EntityInterface;
357
- try {
358
- entity = await loadEntity(data.entity, response.locals.nymph);
359
- } catch (e: any) {
360
- if (e instanceof EntityConflictError) {
361
- httpError(response, 409, 'Conflict');
362
- } else if (e.message === NOT_FOUND_ERROR) {
363
- httpError(response, 404, 'Not Found', e);
364
- } else if (e instanceof InvalidParametersError) {
365
- httpError(response, 400, 'Bad Request', e);
366
- } else {
367
- httpError(response, 500, 'Internal Server Error', e);
368
- }
369
- return;
370
- }
371
- if (data.entity.guid && !entity.guid) {
372
- httpError(response, 400, 'Bad Request');
373
- return;
374
- }
375
- if (entity.$getClientEnabledMethods().indexOf(data.method) === -1) {
376
- httpError(response, 403, 'Forbidden');
377
- return;
378
- }
379
- if (
380
- !(data.method in entity) ||
381
- typeof entity[data.method] !== 'function'
382
- ) {
383
- httpError(response, 400, 'Bad Request');
384
- return;
385
- }
386
- try {
387
- const result = entity[data.method](...params);
388
- let ret = result;
389
- if (result instanceof Promise) {
390
- ret = await result;
391
- }
392
- response.status(200);
393
- response.setHeader('Content-Type', 'application/json');
394
- if (data.stateless) {
395
- response.send({ return: ret });
396
- } else {
397
- response.send({ entity: entity, return: ret });
398
- }
399
- } catch (e: any) {
400
- httpError(response, 500, 'Internal Server Error', e);
401
- return;
402
- }
403
- }
404
- } else {
405
- if (typeof data !== 'string') {
406
- httpError(response, 400, 'Bad Request');
407
- return;
408
- }
409
- if (response.locals.nymph.tilmeld) {
410
- if (
411
- !response.locals.nymph.tilmeld.checkClientUIDPermissions(
412
- data,
413
- TilmeldAccessLevels.WRITE_ACCESS
414
- )
415
- ) {
416
- httpError(response, 403, 'Forbidden');
417
- return;
418
- }
419
- }
420
- let result: number | null;
421
- try {
422
- result = await response.locals.nymph.newUID(data);
423
- } catch (e: any) {
424
- httpError(response, 500, 'Internal Server Error', e);
425
- return;
426
- }
427
- if (typeof result !== 'number') {
428
- httpError(response, 500, 'Internal Server Error');
429
- return;
430
- }
431
- response.status(201);
432
- response.setHeader('Content-Type', 'text/plain');
433
- response.send(`${result}`);
434
- }
435
- } catch (e: any) {
436
- httpError(response, 500, 'Internal Server Error', e);
437
- return;
438
- }
439
- });
440
-
441
- rest.put('/', async (request, response: NymphResponse) => {
442
- try {
443
- const { action, data } = getActionData(request);
444
- if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
445
- httpError(response, 400, 'Bad Request');
446
- return;
447
- }
448
- await doPutOrPatch(response, action, data, false);
449
- } catch (e: any) {
450
- httpError(response, 500, 'Internal Server Error', e);
451
- return;
452
- }
453
- });
454
-
455
- rest.patch('/', async (request, response: NymphResponse) => {
456
- try {
457
- const { action, data } = getActionData(request);
458
- if (['entity', 'entities'].indexOf(action) === -1) {
459
- httpError(response, 400, 'Bad Request');
460
- return;
461
- }
462
- await doPutOrPatch(response, action, data, true);
463
- } catch (e: any) {
464
- httpError(response, 500, 'Internal Server Error', e);
465
- return;
466
- }
467
- });
468
-
469
- async function doPutOrPatch(
470
- response: NymphResponse,
471
- action: string,
472
- data: any,
473
- patch: boolean
474
- ) {
475
- if (action === 'uid') {
476
- if (typeof data.name !== 'string' || typeof data.value !== 'number') {
477
- httpError(response, 400, 'Bad Request');
478
- return;
479
- }
480
- if (response.locals.nymph.tilmeld) {
481
- if (
482
- !response.locals.nymph.tilmeld.checkClientUIDPermissions(
483
- data.name,
484
- TilmeldAccessLevels.FULL_ACCESS
485
- )
486
- ) {
487
- httpError(response, 403, 'Forbidden');
488
- return;
489
- }
490
- }
491
- let result: boolean;
492
- try {
493
- result = await response.locals.nymph.setUID(data.name, data.value);
494
- } catch (e: any) {
495
- httpError(response, 500, 'Internal Server Error', e);
496
- return;
497
- }
498
- if (!result) {
499
- httpError(response, 500, 'Internal Server Error');
500
- return;
501
- }
502
- response.status(200);
503
- response.setHeader('Content-Type', 'text/plain');
504
- response.send(`${result}`);
505
- } else {
506
- if (action === 'entity') {
507
- data = [data];
508
- }
509
- const saved = [];
510
- let hadSuccess = false;
511
- let invalidRequest = false;
512
- let conflict = false;
513
- let notfound = false;
514
- let lastException = null;
515
- for (let entData of data) {
516
- if (entData.guid && entData.guid.length != 24) {
517
- invalidRequest = true;
518
- saved.push(null);
519
- continue;
520
- }
521
- let entity: EntityInterface;
522
- try {
523
- entity = await loadEntity(entData, response.locals.nymph, patch);
524
- } catch (e: any) {
525
- if (e instanceof EntityConflictError) {
526
- conflict = true;
527
- } else if (e.message === NOT_FOUND_ERROR) {
528
- notfound = true;
529
- } else if (e instanceof InvalidParametersError) {
530
- invalidRequest = true;
531
- lastException = e;
532
- } else {
533
- lastException = e;
534
- }
535
- saved.push(null);
536
- continue;
537
- }
538
- if (!entity) {
539
- invalidRequest = true;
540
- saved.push(null);
541
- continue;
542
- }
543
- try {
544
- if (await entity.$save()) {
545
- saved.push(entity);
546
- hadSuccess = true;
547
- } else {
548
- saved.push(false);
549
- }
550
- } catch (e: any) {
551
- if (e instanceof EntityInvalidDataError) {
552
- invalidRequest = true;
553
- } else {
554
- lastException = e;
555
- }
556
- saved.push(null);
557
- }
558
- }
559
- if (!hadSuccess) {
560
- if (invalidRequest) {
561
- httpError(response, 400, 'Bad Request', lastException);
562
- } else if (conflict) {
563
- httpError(response, 409, 'Conflict');
564
- } else if (notfound) {
565
- httpError(response, 404, 'Not Found');
566
- } else {
567
- httpError(response, 500, 'Internal Server Error', lastException);
568
- }
569
- return;
570
- }
571
- response.status(200);
572
- response.setHeader('Content-Type', 'application/json');
573
- if (action === 'entity') {
574
- response.send(JSON.stringify(saved[0]));
575
- } else {
576
- response.send(saved);
577
- }
578
- }
579
- }
580
-
581
- rest.delete('/', async (request, response: NymphResponse) => {
582
- try {
583
- const { action, data: dataConst } = getActionData(request);
584
- let data = dataConst;
585
- if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
586
- httpError(response, 400, 'Bad Request');
587
- return;
588
- }
589
- if (['entity', 'entities'].indexOf(action) !== -1) {
590
- if (action === 'entity') {
591
- data = [data];
592
- }
593
- const deleted = [];
594
- let failures = false;
595
- let hadSuccess = false;
596
- let invalidRequest = false;
597
- let notfound = false;
598
- let lastException = null;
599
- for (let entData of data) {
600
- if (entData.guid && entData.guid.length != 24) {
601
- invalidRequest = true;
602
- continue;
603
- }
604
- let EntityClass: EntityConstructor;
605
- try {
606
- EntityClass = response.locals.nymph.getEntityClass(entData.class);
607
- } catch (e: any) {
608
- invalidRequest = true;
609
- failures = true;
610
- continue;
611
- }
612
- let entity: EntityInterface | null;
613
- try {
614
- entity = await response.locals.nymph.getEntity(
615
- { class: EntityClass },
616
- { type: '&', guid: entData.guid }
617
- );
618
- } catch (e: any) {
619
- lastException = e;
620
- failures = true;
621
- continue;
622
- }
623
- if (!entity) {
624
- notfound = true;
625
- failures = true;
626
- continue;
627
- }
628
- try {
629
- if (await entity.$delete()) {
630
- deleted.push(entData.guid);
631
- hadSuccess = true;
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, 'Bad Request', lastException);
643
- } else if (notfound) {
644
- httpError(response, 404, 'Not Found');
645
- } else {
646
- httpError(response, 500, 'Internal Server Error', 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, 'Bad Request');
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, 'Forbidden');
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, 'Internal Server Error', e);
678
- return;
679
- }
680
- if (!result) {
681
- httpError(response, 500, 'Internal Server Error');
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, 'Internal Server Error', 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 errorCode The HTTP status code.
767
- * @param message The message to place on the HTTP status header line.
768
- * @param error An optional exception object to report.
769
- */
770
- function httpError(
771
- res: NymphResponse,
772
- errorCode: number,
773
- message: string,
774
- error?: Error
775
- ) {
776
- if (!res.headersSent) {
777
- res.status(errorCode);
778
- res.setHeader('Content-Type', 'application/json');
779
- }
780
- if (error) {
781
- res.send({
782
- textStatus: `${errorCode} ${message}`,
783
- message: error.message,
784
- error,
785
- ...(process.env.NODE_ENV !== 'production'
786
- ? { stack: error.stack }
787
- : {}),
788
- });
789
- } else {
790
- res.send({ textStatus: `${errorCode} ${message}` });
791
- }
792
- }
793
-
794
- return rest;
795
- }
7
+ export default createServer;