@nymphjs/server 1.0.0-beta.12 → 1.0.0-beta.121

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.
@@ -1,29 +1,38 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createServer = void 0;
7
- const express_1 = __importDefault(require("express"));
8
- const cookie_parser_1 = __importDefault(require("cookie-parser"));
9
- const nymph_1 = require("@nymphjs/nymph");
10
- const nymph_2 = require("@nymphjs/nymph");
11
- const statusDescriptions_1 = require("./statusDescriptions");
1
+ import express from 'express';
2
+ import cookieParser from 'cookie-parser';
3
+ import { Entity, EntityConflictError, InvalidParametersError, TilmeldAccessLevels, classNamesToEntityConstructors, } from '@nymphjs/nymph';
4
+ import { EntityInvalidDataError } from '@nymphjs/nymph';
5
+ import { statusDescriptions } from './statusDescriptions.js';
12
6
  const NOT_FOUND_ERROR = 'Entity is not found.';
13
- function createServer(nymph, { jsonOptions = {} } = {}) {
14
- const rest = (0, express_1.default)();
15
- rest.use((0, cookie_parser_1.default)());
16
- rest.use(express_1.default.json(jsonOptions || {}));
7
+ export class ForbiddenClassError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'ForbiddenClassError';
11
+ }
12
+ }
13
+ /**
14
+ * A REST server middleware creator for Nymph.
15
+ *
16
+ * Written by Hunter Perrin for SciActive.
17
+ *
18
+ * @author Hunter Perrin <hperrin@port87.com>
19
+ * @copyright SciActive Inc
20
+ * @see http://nymph.io/
21
+ */
22
+ export function createServer(nymph, { jsonOptions = {} } = {}) {
23
+ const rest = express();
24
+ rest.use(cookieParser());
25
+ rest.use(express.json(jsonOptions || {}));
17
26
  function instantiateNymph(_request, response, next) {
18
27
  response.locals.nymph = nymph.clone();
19
28
  next();
20
29
  }
21
- function authenticateTilmeld(request, response, next) {
30
+ async function authenticateTilmeld(request, response, next) {
22
31
  if (response.locals.nymph.tilmeld) {
23
32
  response.locals.nymph.tilmeld.request = request;
24
33
  response.locals.nymph.tilmeld.response = response;
25
34
  try {
26
- response.locals.nymph.tilmeld.authenticate();
35
+ await response.locals.nymph.tilmeld.authenticate(false, request.header('x-tilmeld-token-renewal') === 'off');
27
36
  }
28
37
  catch (e) {
29
38
  httpError(response, 500, e);
@@ -67,7 +76,9 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
67
76
  };
68
77
  }
69
78
  }
79
+ // Create a new instance of Nymph for the request/response.
70
80
  rest.use(instantiateNymph);
81
+ // Authenticate before the request.
71
82
  rest.use(authenticateTilmeld);
72
83
  rest.get('/', async (request, response) => {
73
84
  try {
@@ -94,6 +105,10 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
94
105
  let EntityClass;
95
106
  try {
96
107
  EntityClass = response.locals.nymph.getEntityClass(data[0].class);
108
+ if (!EntityClass.restEnabled) {
109
+ httpError(response, 403);
110
+ return;
111
+ }
97
112
  }
98
113
  catch (e) {
99
114
  httpError(response, 400, e);
@@ -102,7 +117,22 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
102
117
  options.class = EntityClass;
103
118
  options.source = 'client';
104
119
  options.skipAc = false;
105
- selectors = (0, nymph_1.classNamesToEntityConstructors)(response.locals.nymph, selectors);
120
+ if (options.return === 'object') {
121
+ options.return = 'entity';
122
+ }
123
+ try {
124
+ selectors = classNamesToEntityConstructors(response.locals.nymph, selectors, true);
125
+ }
126
+ catch (e) {
127
+ if (e?.message === 'Not accessible.') {
128
+ httpError(response, 403);
129
+ return;
130
+ }
131
+ else {
132
+ httpError(response, 500, e);
133
+ return;
134
+ }
135
+ }
106
136
  let result;
107
137
  try {
108
138
  if (action === 'entity') {
@@ -132,7 +162,7 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
132
162
  return;
133
163
  }
134
164
  if (response.locals.nymph.tilmeld) {
135
- if (!response.locals.nymph.tilmeld.checkClientUIDPermissions(data, nymph_1.TilmeldAccessLevels.READ_ACCESS)) {
165
+ if (!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(data, TilmeldAccessLevels.READ_ACCESS))) {
136
166
  httpError(response, 403);
137
167
  return;
138
168
  }
@@ -179,6 +209,7 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
179
209
  let invalidRequest = false;
180
210
  let conflict = false;
181
211
  let notfound = false;
212
+ let forbidden = false;
182
213
  let lastException = null;
183
214
  for (let entData of data) {
184
215
  if (entData.guid) {
@@ -191,13 +222,16 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
191
222
  entity = await loadEntity(entData, response.locals.nymph);
192
223
  }
193
224
  catch (e) {
194
- if (e instanceof nymph_1.EntityConflictError) {
225
+ if (e instanceof EntityConflictError) {
195
226
  conflict = true;
196
227
  }
228
+ else if (e instanceof ForbiddenClassError) {
229
+ forbidden = true;
230
+ }
197
231
  else if (e.message === NOT_FOUND_ERROR) {
198
232
  notfound = true;
199
233
  }
200
- else if (e instanceof nymph_1.InvalidParametersError) {
234
+ else if (e instanceof InvalidParametersError) {
201
235
  invalidRequest = true;
202
236
  lastException = e;
203
237
  }
@@ -222,7 +256,7 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
222
256
  }
223
257
  }
224
258
  catch (e) {
225
- if (e instanceof nymph_2.EntityInvalidDataError) {
259
+ if (e instanceof EntityInvalidDataError) {
226
260
  invalidRequest = true;
227
261
  }
228
262
  else {
@@ -240,6 +274,10 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
240
274
  httpError(response, 409);
241
275
  return;
242
276
  }
277
+ else if (forbidden) {
278
+ httpError(response, 403);
279
+ return;
280
+ }
243
281
  else if (notfound) {
244
282
  httpError(response, 404);
245
283
  return;
@@ -263,93 +301,154 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
263
301
  httpError(response, 400);
264
302
  return;
265
303
  }
266
- const params = referencesToEntities([...data.params], response.locals.nymph);
267
- if (data.static) {
268
- let EntityClass;
269
- try {
270
- EntityClass = response.locals.nymph.getEntityClass(data.class);
271
- }
272
- catch (e) {
273
- httpError(response, 400);
274
- return;
275
- }
276
- if (EntityClass.clientEnabledStaticMethods.indexOf(data.method) === -1) {
277
- httpError(response, 403);
278
- return;
279
- }
280
- if (!(data.method in EntityClass)) {
281
- httpError(response, 400);
282
- return;
283
- }
284
- const method = EntityClass[data.method];
285
- if (typeof method !== 'function') {
286
- httpError(response, 400);
287
- return;
288
- }
289
- try {
290
- const result = method.call(EntityClass, ...params);
291
- let ret = result;
292
- if (result instanceof Promise) {
293
- ret = await result;
304
+ try {
305
+ const params = referencesToEntities([...data.params], response.locals.nymph);
306
+ if (data.static) {
307
+ let EntityClass;
308
+ try {
309
+ EntityClass = response.locals.nymph.getEntityClass(data.class);
310
+ if (!EntityClass.restEnabled) {
311
+ httpError(response, 403);
312
+ return;
313
+ }
294
314
  }
295
- response.status(200);
296
- response.setHeader('Content-Type', 'application/json');
297
- response.send({ return: ret });
298
- }
299
- catch (e) {
300
- httpError(response, 500, e);
301
- return;
302
- }
303
- }
304
- else {
305
- let entity;
306
- try {
307
- entity = await loadEntity(data.entity, response.locals.nymph);
308
- }
309
- catch (e) {
310
- if (e instanceof nymph_1.EntityConflictError) {
311
- httpError(response, 409);
315
+ catch (e) {
316
+ httpError(response, 400);
317
+ return;
312
318
  }
313
- else if (e.message === NOT_FOUND_ERROR) {
314
- httpError(response, 404, e);
319
+ if (EntityClass.clientEnabledStaticMethods.indexOf(data.method) === -1) {
320
+ httpError(response, 403);
321
+ return;
322
+ }
323
+ if (!(data.method in EntityClass)) {
324
+ httpError(response, 400);
325
+ return;
315
326
  }
316
- else if (e instanceof nymph_1.InvalidParametersError) {
317
- httpError(response, 400, e);
327
+ // @ts-ignore Dynamic methods make TypeScript sad.
328
+ const method = EntityClass[data.method];
329
+ if (typeof method !== 'function') {
330
+ httpError(response, 400);
331
+ return;
332
+ }
333
+ if (data.iterator) {
334
+ // Ping every 15 seconds to keep connection alive.
335
+ const interval = setInterval(() => {
336
+ if (response.headersSent) {
337
+ response.write('event: ping\n');
338
+ response.write(`data: ${new Date().toISOString()}\n\n`);
339
+ }
340
+ }, 15000);
341
+ try {
342
+ response.set({
343
+ 'Cache-Control': 'no-cache',
344
+ 'Content-Type': 'text/event-stream',
345
+ Connection: 'keep-alive',
346
+ });
347
+ response.flushHeaders();
348
+ const result = method.call(EntityClass, ...params);
349
+ let sequence = result;
350
+ if (result instanceof Promise) {
351
+ sequence = await result;
352
+ }
353
+ let { value, done } = await sequence.next(response.destroyed);
354
+ while (!done) {
355
+ response.write('event: next\n');
356
+ response.write(`data: ${JSON.stringify(value)}\n\n`);
357
+ // Wait for an event loop each iteration, to allow data to flush.
358
+ await new Promise((resolve) => setImmediate(resolve));
359
+ ({ value, done } = await sequence.next(response.destroyed));
360
+ }
361
+ clearInterval(interval);
362
+ response.write('event: finished\n');
363
+ response.write('data: \n\n');
364
+ response.end();
365
+ }
366
+ catch (e) {
367
+ clearInterval(interval);
368
+ eventStreamError(response, 500, e);
369
+ return;
370
+ }
318
371
  }
319
372
  else {
320
- httpError(response, 500, e);
373
+ try {
374
+ const result = method.call(EntityClass, ...params);
375
+ let ret = result;
376
+ if (result instanceof Promise) {
377
+ ret = await result;
378
+ }
379
+ response.status(200);
380
+ response.setHeader('Content-Type', 'application/json');
381
+ response.send({ return: ret });
382
+ }
383
+ catch (e) {
384
+ httpError(response, 500, e);
385
+ return;
386
+ }
321
387
  }
322
- return;
323
388
  }
324
- if (data.entity.guid && !entity.guid) {
325
- httpError(response, 400);
326
- return;
327
- }
328
- if (entity.$getClientEnabledMethods().indexOf(data.method) === -1) {
329
- httpError(response, 403);
330
- return;
331
- }
332
- if (!(data.method in entity) ||
333
- typeof entity[data.method] !== 'function') {
334
- httpError(response, 400);
335
- return;
336
- }
337
- try {
338
- const result = entity[data.method](...params);
339
- let ret = result;
340
- if (result instanceof Promise) {
341
- ret = await result;
389
+ else {
390
+ let entity;
391
+ try {
392
+ entity = await loadEntity(data.entity, response.locals.nymph);
342
393
  }
343
- response.status(200);
344
- response.setHeader('Content-Type', 'application/json');
345
- if (data.stateless) {
346
- response.send({ return: ret });
394
+ catch (e) {
395
+ if (e instanceof EntityConflictError) {
396
+ httpError(response, 409);
397
+ }
398
+ else if (e instanceof ForbiddenClassError) {
399
+ httpError(response, 403);
400
+ }
401
+ else if (e.message === NOT_FOUND_ERROR) {
402
+ httpError(response, 404, e);
403
+ }
404
+ else if (e instanceof InvalidParametersError) {
405
+ httpError(response, 400, e);
406
+ }
407
+ else {
408
+ httpError(response, 500, e);
409
+ }
410
+ return;
347
411
  }
348
- else {
349
- response.send({ entity: entity, return: ret });
412
+ if (data.entity.guid && !entity.guid) {
413
+ httpError(response, 400);
414
+ return;
415
+ }
416
+ if (entity.$getClientEnabledMethods().indexOf(data.method) === -1) {
417
+ httpError(response, 403);
418
+ return;
419
+ }
420
+ if (!(data.method in entity) ||
421
+ typeof entity[data.method] !== 'function') {
422
+ httpError(response, 400);
423
+ return;
424
+ }
425
+ try {
426
+ const result = entity[data.method](...params);
427
+ let ret = result;
428
+ if (result instanceof Promise) {
429
+ ret = await result;
430
+ }
431
+ response.status(200);
432
+ response.setHeader('Content-Type', 'application/json');
433
+ if (data.stateless) {
434
+ response.send({ return: ret });
435
+ }
436
+ else {
437
+ response.send({ entity: entity, return: ret });
438
+ }
439
+ }
440
+ catch (e) {
441
+ httpError(response, 500, e);
442
+ return;
350
443
  }
351
444
  }
352
- catch (e) {
445
+ }
446
+ catch (e) {
447
+ if (e instanceof ForbiddenClassError) {
448
+ httpError(response, 403);
449
+ return;
450
+ }
451
+ else {
353
452
  httpError(response, 500, e);
354
453
  return;
355
454
  }
@@ -361,7 +460,7 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
361
460
  return;
362
461
  }
363
462
  if (response.locals.nymph.tilmeld) {
364
- if (!response.locals.nymph.tilmeld.checkClientUIDPermissions(data, nymph_1.TilmeldAccessLevels.WRITE_ACCESS)) {
463
+ if (!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(data, TilmeldAccessLevels.WRITE_ACCESS))) {
365
464
  httpError(response, 403);
366
465
  return;
367
466
  }
@@ -423,7 +522,7 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
423
522
  return;
424
523
  }
425
524
  if (response.locals.nymph.tilmeld) {
426
- if (!response.locals.nymph.tilmeld.checkClientUIDPermissions(data.name, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
525
+ if (!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(data.name, TilmeldAccessLevels.FULL_ACCESS))) {
427
526
  httpError(response, 403);
428
527
  return;
429
528
  }
@@ -452,6 +551,7 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
452
551
  let hadSuccess = false;
453
552
  let invalidRequest = false;
454
553
  let conflict = false;
554
+ let forbidden = false;
455
555
  let notfound = false;
456
556
  let lastException = null;
457
557
  for (let entData of data) {
@@ -465,13 +565,16 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
465
565
  entity = await loadEntity(entData, response.locals.nymph, patch);
466
566
  }
467
567
  catch (e) {
468
- if (e instanceof nymph_1.EntityConflictError) {
568
+ if (e instanceof EntityConflictError) {
469
569
  conflict = true;
470
570
  }
571
+ else if (e instanceof ForbiddenClassError) {
572
+ forbidden = true;
573
+ }
471
574
  else if (e.message === NOT_FOUND_ERROR) {
472
575
  notfound = true;
473
576
  }
474
- else if (e instanceof nymph_1.InvalidParametersError) {
577
+ else if (e instanceof InvalidParametersError) {
475
578
  invalidRequest = true;
476
579
  lastException = e;
477
580
  }
@@ -496,7 +599,7 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
496
599
  }
497
600
  }
498
601
  catch (e) {
499
- if (e instanceof nymph_2.EntityInvalidDataError) {
602
+ if (e instanceof EntityInvalidDataError) {
500
603
  invalidRequest = true;
501
604
  }
502
605
  else {
@@ -509,6 +612,9 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
509
612
  if (invalidRequest) {
510
613
  httpError(response, 400, lastException);
511
614
  }
615
+ else if (forbidden) {
616
+ httpError(response, 403);
617
+ }
512
618
  else if (conflict) {
513
619
  httpError(response, 409);
514
620
  }
@@ -555,6 +661,10 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
555
661
  let EntityClass;
556
662
  try {
557
663
  EntityClass = response.locals.nymph.getEntityClass(entData.class);
664
+ if (!EntityClass.restEnabled) {
665
+ httpError(response, 403);
666
+ return;
667
+ }
558
668
  }
559
669
  catch (e) {
560
670
  invalidRequest = true;
@@ -615,7 +725,7 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
615
725
  return;
616
726
  }
617
727
  if (response.locals.nymph.tilmeld) {
618
- if (!response.locals.nymph.tilmeld.checkClientUIDPermissions(data, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
728
+ if (!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(data, TilmeldAccessLevels.FULL_ACCESS))) {
619
729
  httpError(response, 403);
620
730
  return;
621
731
  }
@@ -642,12 +752,17 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
642
752
  return;
643
753
  }
644
754
  });
755
+ // Unauthenticate after the request.
645
756
  rest.use(unauthenticateTilmeld);
646
757
  async function loadEntity(entityData, nymph, patch = false, allowConflict = false) {
647
758
  if (entityData.class === 'Entity') {
648
- throw new nymph_1.InvalidParametersError("Can't use Entity class directly from the front end.");
759
+ // Don't let clients use the `Entity` class, since it has no validity/AC checks.
760
+ throw new InvalidParametersError("Can't use Entity class directly from the front end.");
649
761
  }
650
762
  let EntityClass = nymph.getEntityClass(entityData.class);
763
+ if (!EntityClass.restEnabled) {
764
+ throw new ForbiddenClassError('Not accessible.');
765
+ }
651
766
  let entity;
652
767
  if (entityData.guid) {
653
768
  entity = await nymph.getEntity({ class: EntityClass, source: 'client' }, {
@@ -669,11 +784,25 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
669
784
  }
670
785
  return entity;
671
786
  }
787
+ /**
788
+ * Check if an item is a reference, and if it is, convert it to an entity.
789
+ *
790
+ * This function will recurse into deeper arrays and objects.
791
+ *
792
+ * @param item The item to check.
793
+ * @returns The item, converted.
794
+ */
672
795
  function referencesToEntities(item, nymph) {
673
- if (Array.isArray(item)) {
796
+ if (item == null) {
797
+ return item;
798
+ }
799
+ else if (Array.isArray(item)) {
674
800
  if (item.length === 3 && item[0] === 'nymph_entity_reference') {
675
801
  try {
676
- const EntityClass = nymph.getEntityClass(item[1]);
802
+ const EntityClass = nymph.getEntityClass(item[2]);
803
+ if (!EntityClass.restEnabled) {
804
+ throw new ForbiddenClassError('Not accessible.');
805
+ }
677
806
  return EntityClass.factoryReference(item);
678
807
  }
679
808
  catch (e) {
@@ -682,7 +811,8 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
682
811
  }
683
812
  return item.map((entry) => referencesToEntities(entry, nymph));
684
813
  }
685
- else if (typeof item === 'object' && !(item instanceof nymph_1.Entity)) {
814
+ else if (typeof item === 'object' && !(item instanceof Entity)) {
815
+ // Only do this for non-entity objects.
686
816
  const newItem = {};
687
817
  for (let curProperty in item) {
688
818
  newItem[curProperty] = referencesToEntities(item[curProperty], nymph);
@@ -691,20 +821,60 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
691
821
  }
692
822
  return item;
693
823
  }
824
+ /**
825
+ * Return the request with an HTTP error response.
826
+ *
827
+ * @param res The server response object.
828
+ * @param defaultStatusCode The HTTP status code to use if none is given in the error object.
829
+ * @param error An optional error object to report.
830
+ */
694
831
  function httpError(res, defaultStatusCode, error) {
695
832
  const status = error?.status || defaultStatusCode;
696
833
  const statusText = error?.statusText ||
697
834
  (error?.status == null
698
- ? statusDescriptions_1.statusDescriptions[defaultStatusCode]
699
- : error.status in statusDescriptions_1.statusDescriptions &&
700
- statusDescriptions_1.statusDescriptions[error.status]) ||
835
+ ? statusDescriptions[defaultStatusCode]
836
+ : error.status in statusDescriptions &&
837
+ statusDescriptions[error.status]) ||
701
838
  'Internal Server Error';
839
+ const errorResponse = error
840
+ ? {
841
+ textStatus: `${status} ${statusText}`,
842
+ statusText,
843
+ message: error.message,
844
+ error,
845
+ ...(process.env.NODE_ENV !== 'production'
846
+ ? { stack: error.stack }
847
+ : {}),
848
+ }
849
+ : {
850
+ textStatus: `${status} ${statusText}`,
851
+ statusText,
852
+ message: statusText,
853
+ };
702
854
  if (!res.headersSent) {
703
855
  res.status(status);
704
856
  res.setHeader('Content-Type', 'application/json');
705
857
  }
706
- if (error) {
707
- res.send({
858
+ res.send(errorResponse);
859
+ }
860
+ /**
861
+ * End the event stream with an HTTP error response.
862
+ *
863
+ * @param res The server response object.
864
+ * @param defaultStatusCode The HTTP status code to use if none is given in the error object.
865
+ * @param error An optional error object to report.
866
+ */
867
+ function eventStreamError(res, defaultStatusCode, error) {
868
+ const status = error?.status || defaultStatusCode;
869
+ const statusText = error?.statusText ||
870
+ (error?.status == null
871
+ ? statusDescriptions[defaultStatusCode]
872
+ : error.status in statusDescriptions &&
873
+ statusDescriptions[error.status]) ||
874
+ 'Internal Server Error';
875
+ const errorResponse = error
876
+ ? {
877
+ status,
708
878
  textStatus: `${status} ${statusText}`,
709
879
  statusText,
710
880
  message: error.message,
@@ -712,17 +882,26 @@ function createServer(nymph, { jsonOptions = {} } = {}) {
712
882
  ...(process.env.NODE_ENV !== 'production'
713
883
  ? { stack: error.stack }
714
884
  : {}),
715
- });
716
- }
717
- else {
718
- res.send({
885
+ }
886
+ : {
887
+ status,
719
888
  textStatus: `${status} ${statusText}`,
720
889
  statusText,
721
890
  message: statusText,
722
- });
891
+ };
892
+ if (!res.headersSent) {
893
+ res.status(status);
894
+ res.setHeader('Content-Type', 'application/json');
895
+ res.send(errorResponse);
896
+ }
897
+ else {
898
+ res.write('event: error\n');
899
+ res.write(`data: ${JSON.stringify(errorResponse)}\n\n`);
900
+ res.write('event: finished\n');
901
+ res.write('data: \n\n');
902
+ res.end();
723
903
  }
724
904
  }
725
905
  return rest;
726
906
  }
727
- exports.createServer = createServer;
728
907
  //# sourceMappingURL=createServer.js.map