@nymphjs/server 1.0.0-beta.12 → 1.0.0-beta.120
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/CHANGELOG.md +472 -0
- package/README.md +75 -3
- package/dist/cache.test.js +14 -18
- package/dist/cache.test.js.map +1 -1
- package/dist/createServer.d.ts +12 -0
- package/dist/createServer.js +298 -119
- package/dist/createServer.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -21
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +138 -24
- package/dist/index.test.js.map +1 -1
- package/dist/statusDescriptions.d.ts +3 -0
- package/dist/statusDescriptions.js +4 -4
- package/dist/statusDescriptions.js.map +1 -1
- package/dist/testArtifacts.d.ts +55 -7
- package/dist/testArtifacts.js +160 -74
- package/dist/testArtifacts.js.map +1 -1
- package/jest.config.js +31 -5
- package/package.json +29 -23
- package/src/cache.test.ts +5 -5
- package/src/createServer.ts +302 -128
- package/src/index.test.ts +171 -27
- package/src/index.ts +3 -4
- package/src/testArtifacts.ts +171 -42
- package/tsconfig.json +6 -3
- package/typedoc.json +4 -0
- package/dist/HttpError.d.ts +0 -5
- package/dist/HttpError.js +0 -13
- package/dist/HttpError.js.map +0 -1
- package/src/HttpError.ts +0 -12
package/src/createServer.ts
CHANGED
|
@@ -18,24 +18,32 @@ import {
|
|
|
18
18
|
} from '@nymphjs/nymph';
|
|
19
19
|
import { EntityInvalidDataError } from '@nymphjs/nymph';
|
|
20
20
|
|
|
21
|
-
import { statusDescriptions } from './statusDescriptions';
|
|
21
|
+
import { statusDescriptions } from './statusDescriptions.js';
|
|
22
22
|
|
|
23
23
|
type NymphResponse = Response<any, { nymph: Nymph }>;
|
|
24
24
|
|
|
25
25
|
const NOT_FOUND_ERROR = 'Entity is not found.';
|
|
26
26
|
|
|
27
|
+
export class ForbiddenClassError extends Error {
|
|
28
|
+
constructor(message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
|
|
31
|
+
this.name = 'ForbiddenClassError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
/**
|
|
28
36
|
* A REST server middleware creator for Nymph.
|
|
29
37
|
*
|
|
30
38
|
* Written by Hunter Perrin for SciActive.
|
|
31
39
|
*
|
|
32
|
-
* @author Hunter Perrin <hperrin@
|
|
40
|
+
* @author Hunter Perrin <hperrin@port87.com>
|
|
33
41
|
* @copyright SciActive Inc
|
|
34
42
|
* @see http://nymph.io/
|
|
35
43
|
*/
|
|
36
44
|
export function createServer(
|
|
37
45
|
nymph: Nymph,
|
|
38
|
-
{ jsonOptions = {} }: { jsonOptions?: OptionsJson } = {}
|
|
46
|
+
{ jsonOptions = {} }: { jsonOptions?: OptionsJson } = {},
|
|
39
47
|
) {
|
|
40
48
|
const rest = express();
|
|
41
49
|
rest.use(cookieParser());
|
|
@@ -44,22 +52,25 @@ export function createServer(
|
|
|
44
52
|
function instantiateNymph(
|
|
45
53
|
_request: Request,
|
|
46
54
|
response: NymphResponse,
|
|
47
|
-
next: NextFunction
|
|
55
|
+
next: NextFunction,
|
|
48
56
|
) {
|
|
49
57
|
response.locals.nymph = nymph.clone();
|
|
50
58
|
next();
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
function authenticateTilmeld(
|
|
61
|
+
async function authenticateTilmeld(
|
|
54
62
|
request: Request,
|
|
55
63
|
response: NymphResponse,
|
|
56
|
-
next: NextFunction
|
|
64
|
+
next: NextFunction,
|
|
57
65
|
) {
|
|
58
66
|
if (response.locals.nymph.tilmeld) {
|
|
59
67
|
response.locals.nymph.tilmeld.request = request;
|
|
60
68
|
response.locals.nymph.tilmeld.response = response;
|
|
61
69
|
try {
|
|
62
|
-
response.locals.nymph.tilmeld.authenticate(
|
|
70
|
+
await response.locals.nymph.tilmeld.authenticate(
|
|
71
|
+
false,
|
|
72
|
+
request.header('x-tilmeld-token-renewal') === 'off',
|
|
73
|
+
);
|
|
63
74
|
} catch (e: any) {
|
|
64
75
|
httpError(response, 500, e);
|
|
65
76
|
return;
|
|
@@ -71,7 +82,7 @@ export function createServer(
|
|
|
71
82
|
function unauthenticateTilmeld(
|
|
72
83
|
_request: Request,
|
|
73
84
|
response: NymphResponse,
|
|
74
|
-
next: NextFunction
|
|
85
|
+
next: NextFunction,
|
|
75
86
|
) {
|
|
76
87
|
if (response.locals.nymph.tilmeld) {
|
|
77
88
|
response.locals.nymph.tilmeld.request = null;
|
|
@@ -140,6 +151,10 @@ export function createServer(
|
|
|
140
151
|
let EntityClass;
|
|
141
152
|
try {
|
|
142
153
|
EntityClass = response.locals.nymph.getEntityClass(data[0].class);
|
|
154
|
+
if (!EntityClass.restEnabled) {
|
|
155
|
+
httpError(response, 403);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
143
158
|
} catch (e: any) {
|
|
144
159
|
httpError(response, 400, e);
|
|
145
160
|
return;
|
|
@@ -147,10 +162,24 @@ export function createServer(
|
|
|
147
162
|
options.class = EntityClass;
|
|
148
163
|
options.source = 'client';
|
|
149
164
|
options.skipAc = false;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
if (options.return === 'object') {
|
|
166
|
+
options.return = 'entity';
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
selectors = classNamesToEntityConstructors(
|
|
170
|
+
response.locals.nymph,
|
|
171
|
+
selectors,
|
|
172
|
+
true,
|
|
173
|
+
);
|
|
174
|
+
} catch (e: any) {
|
|
175
|
+
if (e?.message === 'Not accessible.') {
|
|
176
|
+
httpError(response, 403);
|
|
177
|
+
return;
|
|
178
|
+
} else {
|
|
179
|
+
httpError(response, 500, e);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
154
183
|
let result:
|
|
155
184
|
| EntityInterface
|
|
156
185
|
| EntityInterface[]
|
|
@@ -162,12 +191,12 @@ export function createServer(
|
|
|
162
191
|
if (action === 'entity') {
|
|
163
192
|
result = await response.locals.nymph.getEntity(
|
|
164
193
|
options,
|
|
165
|
-
...selectors
|
|
194
|
+
...selectors,
|
|
166
195
|
);
|
|
167
196
|
} else {
|
|
168
197
|
result = await response.locals.nymph.getEntities(
|
|
169
198
|
options,
|
|
170
|
-
...selectors
|
|
199
|
+
...selectors,
|
|
171
200
|
);
|
|
172
201
|
}
|
|
173
202
|
} catch (e: any) {
|
|
@@ -192,10 +221,10 @@ export function createServer(
|
|
|
192
221
|
}
|
|
193
222
|
if (response.locals.nymph.tilmeld) {
|
|
194
223
|
if (
|
|
195
|
-
!response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
224
|
+
!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
196
225
|
data,
|
|
197
|
-
TilmeldAccessLevels.READ_ACCESS
|
|
198
|
-
)
|
|
226
|
+
TilmeldAccessLevels.READ_ACCESS,
|
|
227
|
+
))
|
|
199
228
|
) {
|
|
200
229
|
httpError(response, 403);
|
|
201
230
|
return;
|
|
@@ -241,6 +270,7 @@ export function createServer(
|
|
|
241
270
|
let invalidRequest = false;
|
|
242
271
|
let conflict = false;
|
|
243
272
|
let notfound = false;
|
|
273
|
+
let forbidden = false;
|
|
244
274
|
let lastException = null;
|
|
245
275
|
for (let entData of data) {
|
|
246
276
|
if (entData.guid) {
|
|
@@ -254,6 +284,8 @@ export function createServer(
|
|
|
254
284
|
} catch (e: any) {
|
|
255
285
|
if (e instanceof EntityConflictError) {
|
|
256
286
|
conflict = true;
|
|
287
|
+
} else if (e instanceof ForbiddenClassError) {
|
|
288
|
+
forbidden = true;
|
|
257
289
|
} else if (e.message === NOT_FOUND_ERROR) {
|
|
258
290
|
notfound = true;
|
|
259
291
|
} else if (e instanceof InvalidParametersError) {
|
|
@@ -293,6 +325,9 @@ export function createServer(
|
|
|
293
325
|
} else if (conflict) {
|
|
294
326
|
httpError(response, 409);
|
|
295
327
|
return;
|
|
328
|
+
} else if (forbidden) {
|
|
329
|
+
httpError(response, 403);
|
|
330
|
+
return;
|
|
296
331
|
} else if (notfound) {
|
|
297
332
|
httpError(response, 404);
|
|
298
333
|
return;
|
|
@@ -313,92 +348,158 @@ export function createServer(
|
|
|
313
348
|
httpError(response, 400);
|
|
314
349
|
return;
|
|
315
350
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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;
|
|
351
|
+
try {
|
|
352
|
+
const params = referencesToEntities(
|
|
353
|
+
[...data.params],
|
|
354
|
+
response.locals.nymph,
|
|
355
|
+
);
|
|
356
|
+
if (data.static) {
|
|
357
|
+
let EntityClass: EntityConstructor;
|
|
358
|
+
try {
|
|
359
|
+
EntityClass = response.locals.nymph.getEntityClass(data.class);
|
|
360
|
+
if (!EntityClass.restEnabled) {
|
|
361
|
+
httpError(response, 403);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
} catch (e: any) {
|
|
365
|
+
httpError(response, 400);
|
|
366
|
+
return;
|
|
349
367
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
363
|
-
httpError(response,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
if (
|
|
369
|
+
EntityClass.clientEnabledStaticMethods.indexOf(data.method) === -1
|
|
370
|
+
) {
|
|
371
|
+
httpError(response, 403);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!(data.method in EntityClass)) {
|
|
375
|
+
httpError(response, 400);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// @ts-ignore Dynamic methods make TypeScript sad.
|
|
379
|
+
const method: Function = EntityClass[data.method];
|
|
380
|
+
if (typeof method !== 'function') {
|
|
381
|
+
httpError(response, 400);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (data.iterator) {
|
|
385
|
+
// Ping every 15 seconds to keep connection alive.
|
|
386
|
+
const interval = setInterval(() => {
|
|
387
|
+
if (response.headersSent) {
|
|
388
|
+
response.write('event: ping\n');
|
|
389
|
+
response.write(`data: ${new Date().toISOString()}\n\n`);
|
|
390
|
+
}
|
|
391
|
+
}, 15000);
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
response.set({
|
|
395
|
+
'Cache-Control': 'no-cache',
|
|
396
|
+
'Content-Type': 'text/event-stream',
|
|
397
|
+
Connection: 'keep-alive',
|
|
398
|
+
});
|
|
399
|
+
response.flushHeaders();
|
|
400
|
+
|
|
401
|
+
const result:
|
|
402
|
+
| Iterator<any, any, boolean>
|
|
403
|
+
| AsyncIterator<any, any, boolean> = method.call(
|
|
404
|
+
EntityClass,
|
|
405
|
+
...params,
|
|
406
|
+
);
|
|
407
|
+
let sequence = result;
|
|
408
|
+
if (result instanceof Promise) {
|
|
409
|
+
sequence = await result;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let { value, done } = await sequence.next(response.destroyed);
|
|
413
|
+
while (!done) {
|
|
414
|
+
response.write('event: next\n');
|
|
415
|
+
response.write(`data: ${JSON.stringify(value)}\n\n`);
|
|
416
|
+
// Wait for an event loop each iteration, to allow data to flush.
|
|
417
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
418
|
+
|
|
419
|
+
({ value, done } = await sequence.next(response.destroyed));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
clearInterval(interval);
|
|
423
|
+
|
|
424
|
+
response.write('event: finished\n');
|
|
425
|
+
response.write('data: \n\n');
|
|
426
|
+
response.end();
|
|
427
|
+
} catch (e: any) {
|
|
428
|
+
clearInterval(interval);
|
|
429
|
+
eventStreamError(response, 500, e);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
368
432
|
} else {
|
|
433
|
+
try {
|
|
434
|
+
const result = method.call(EntityClass, ...params);
|
|
435
|
+
let ret = result;
|
|
436
|
+
if (result instanceof Promise) {
|
|
437
|
+
ret = await result;
|
|
438
|
+
}
|
|
439
|
+
response.status(200);
|
|
440
|
+
response.setHeader('Content-Type', 'application/json');
|
|
441
|
+
response.send({ return: ret });
|
|
442
|
+
} catch (e: any) {
|
|
443
|
+
httpError(response, 500, e);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
let entity: EntityInterface;
|
|
449
|
+
try {
|
|
450
|
+
entity = await loadEntity(data.entity, response.locals.nymph);
|
|
451
|
+
} catch (e: any) {
|
|
452
|
+
if (e instanceof EntityConflictError) {
|
|
453
|
+
httpError(response, 409);
|
|
454
|
+
} else if (e instanceof ForbiddenClassError) {
|
|
455
|
+
httpError(response, 403);
|
|
456
|
+
} else if (e.message === NOT_FOUND_ERROR) {
|
|
457
|
+
httpError(response, 404, e);
|
|
458
|
+
} else if (e instanceof InvalidParametersError) {
|
|
459
|
+
httpError(response, 400, e);
|
|
460
|
+
} else {
|
|
461
|
+
httpError(response, 500, e);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (data.entity.guid && !entity.guid) {
|
|
466
|
+
httpError(response, 400);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (entity.$getClientEnabledMethods().indexOf(data.method) === -1) {
|
|
470
|
+
httpError(response, 403);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (
|
|
474
|
+
!(data.method in entity) ||
|
|
475
|
+
typeof entity[data.method] !== 'function'
|
|
476
|
+
) {
|
|
477
|
+
httpError(response, 400);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
const result = entity[data.method](...params);
|
|
482
|
+
let ret = result;
|
|
483
|
+
if (result instanceof Promise) {
|
|
484
|
+
ret = await result;
|
|
485
|
+
}
|
|
486
|
+
response.status(200);
|
|
487
|
+
response.setHeader('Content-Type', 'application/json');
|
|
488
|
+
if (data.stateless) {
|
|
489
|
+
response.send({ return: ret });
|
|
490
|
+
} else {
|
|
491
|
+
response.send({ entity: entity, return: ret });
|
|
492
|
+
}
|
|
493
|
+
} catch (e: any) {
|
|
369
494
|
httpError(response, 500, e);
|
|
495
|
+
return;
|
|
370
496
|
}
|
|
371
|
-
return;
|
|
372
497
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
if (entity.$getClientEnabledMethods().indexOf(data.method) === -1) {
|
|
498
|
+
} catch (e: any) {
|
|
499
|
+
if (e instanceof ForbiddenClassError) {
|
|
378
500
|
httpError(response, 403);
|
|
379
501
|
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) {
|
|
502
|
+
} else {
|
|
402
503
|
httpError(response, 500, e);
|
|
403
504
|
return;
|
|
404
505
|
}
|
|
@@ -410,10 +511,10 @@ export function createServer(
|
|
|
410
511
|
}
|
|
411
512
|
if (response.locals.nymph.tilmeld) {
|
|
412
513
|
if (
|
|
413
|
-
!response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
514
|
+
!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
414
515
|
data,
|
|
415
|
-
TilmeldAccessLevels.WRITE_ACCESS
|
|
416
|
-
)
|
|
516
|
+
TilmeldAccessLevels.WRITE_ACCESS,
|
|
517
|
+
))
|
|
417
518
|
) {
|
|
418
519
|
httpError(response, 403);
|
|
419
520
|
return;
|
|
@@ -472,7 +573,7 @@ export function createServer(
|
|
|
472
573
|
response: NymphResponse,
|
|
473
574
|
action: string,
|
|
474
575
|
data: any,
|
|
475
|
-
patch: boolean
|
|
576
|
+
patch: boolean,
|
|
476
577
|
) {
|
|
477
578
|
if (action === 'uid') {
|
|
478
579
|
if (typeof data.name !== 'string' || typeof data.value !== 'number') {
|
|
@@ -481,10 +582,10 @@ export function createServer(
|
|
|
481
582
|
}
|
|
482
583
|
if (response.locals.nymph.tilmeld) {
|
|
483
584
|
if (
|
|
484
|
-
!response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
585
|
+
!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
485
586
|
data.name,
|
|
486
|
-
TilmeldAccessLevels.FULL_ACCESS
|
|
487
|
-
)
|
|
587
|
+
TilmeldAccessLevels.FULL_ACCESS,
|
|
588
|
+
))
|
|
488
589
|
) {
|
|
489
590
|
httpError(response, 403);
|
|
490
591
|
return;
|
|
@@ -512,6 +613,7 @@ export function createServer(
|
|
|
512
613
|
let hadSuccess = false;
|
|
513
614
|
let invalidRequest = false;
|
|
514
615
|
let conflict = false;
|
|
616
|
+
let forbidden = false;
|
|
515
617
|
let notfound = false;
|
|
516
618
|
let lastException = null;
|
|
517
619
|
for (let entData of data) {
|
|
@@ -526,6 +628,8 @@ export function createServer(
|
|
|
526
628
|
} catch (e: any) {
|
|
527
629
|
if (e instanceof EntityConflictError) {
|
|
528
630
|
conflict = true;
|
|
631
|
+
} else if (e instanceof ForbiddenClassError) {
|
|
632
|
+
forbidden = true;
|
|
529
633
|
} else if (e.message === NOT_FOUND_ERROR) {
|
|
530
634
|
notfound = true;
|
|
531
635
|
} else if (e instanceof InvalidParametersError) {
|
|
@@ -561,6 +665,8 @@ export function createServer(
|
|
|
561
665
|
if (!hadSuccess) {
|
|
562
666
|
if (invalidRequest) {
|
|
563
667
|
httpError(response, 400, lastException);
|
|
668
|
+
} else if (forbidden) {
|
|
669
|
+
httpError(response, 403);
|
|
564
670
|
} else if (conflict) {
|
|
565
671
|
httpError(response, 409);
|
|
566
672
|
} else if (notfound) {
|
|
@@ -605,6 +711,10 @@ export function createServer(
|
|
|
605
711
|
let EntityClass: EntityConstructor;
|
|
606
712
|
try {
|
|
607
713
|
EntityClass = response.locals.nymph.getEntityClass(entData.class);
|
|
714
|
+
if (!EntityClass.restEnabled) {
|
|
715
|
+
httpError(response, 403);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
608
718
|
} catch (e: any) {
|
|
609
719
|
invalidRequest = true;
|
|
610
720
|
failures = true;
|
|
@@ -614,7 +724,7 @@ export function createServer(
|
|
|
614
724
|
try {
|
|
615
725
|
entity = await response.locals.nymph.getEntity(
|
|
616
726
|
{ class: EntityClass },
|
|
617
|
-
{ type: '&', guid: entData.guid }
|
|
727
|
+
{ type: '&', guid: entData.guid },
|
|
618
728
|
);
|
|
619
729
|
} catch (e: any) {
|
|
620
730
|
lastException = e;
|
|
@@ -661,10 +771,10 @@ export function createServer(
|
|
|
661
771
|
}
|
|
662
772
|
if (response.locals.nymph.tilmeld) {
|
|
663
773
|
if (
|
|
664
|
-
!response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
774
|
+
!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
665
775
|
data,
|
|
666
|
-
TilmeldAccessLevels.FULL_ACCESS
|
|
667
|
-
)
|
|
776
|
+
TilmeldAccessLevels.FULL_ACCESS,
|
|
777
|
+
))
|
|
668
778
|
) {
|
|
669
779
|
httpError(response, 403);
|
|
670
780
|
return;
|
|
@@ -698,15 +808,18 @@ export function createServer(
|
|
|
698
808
|
entityData: EntityJson | EntityPatch,
|
|
699
809
|
nymph: Nymph,
|
|
700
810
|
patch = false,
|
|
701
|
-
allowConflict = false
|
|
811
|
+
allowConflict = false,
|
|
702
812
|
): Promise<EntityInterface> {
|
|
703
813
|
if (entityData.class === 'Entity') {
|
|
704
814
|
// Don't let clients use the `Entity` class, since it has no validity/AC checks.
|
|
705
815
|
throw new InvalidParametersError(
|
|
706
|
-
"Can't use Entity class directly from the front end."
|
|
816
|
+
"Can't use Entity class directly from the front end.",
|
|
707
817
|
);
|
|
708
818
|
}
|
|
709
819
|
let EntityClass = nymph.getEntityClass(entityData.class);
|
|
820
|
+
if (!EntityClass.restEnabled) {
|
|
821
|
+
throw new ForbiddenClassError('Not accessible.');
|
|
822
|
+
}
|
|
710
823
|
let entity: EntityInterface | null;
|
|
711
824
|
if (entityData.guid) {
|
|
712
825
|
entity = await nymph.getEntity(
|
|
@@ -714,7 +827,7 @@ export function createServer(
|
|
|
714
827
|
{
|
|
715
828
|
type: '&',
|
|
716
829
|
guid: `${entityData['guid']}`,
|
|
717
|
-
}
|
|
830
|
+
},
|
|
718
831
|
);
|
|
719
832
|
if (entity === null) {
|
|
720
833
|
throw new Error(NOT_FOUND_ERROR);
|
|
@@ -739,10 +852,15 @@ export function createServer(
|
|
|
739
852
|
* @returns The item, converted.
|
|
740
853
|
*/
|
|
741
854
|
function referencesToEntities(item: any, nymph: Nymph): any {
|
|
742
|
-
if (
|
|
855
|
+
if (item == null) {
|
|
856
|
+
return item;
|
|
857
|
+
} else if (Array.isArray(item)) {
|
|
743
858
|
if (item.length === 3 && item[0] === 'nymph_entity_reference') {
|
|
744
859
|
try {
|
|
745
|
-
const EntityClass = nymph.getEntityClass(item[
|
|
860
|
+
const EntityClass = nymph.getEntityClass(item[2]);
|
|
861
|
+
if (!EntityClass.restEnabled) {
|
|
862
|
+
throw new ForbiddenClassError('Not accessible.');
|
|
863
|
+
}
|
|
746
864
|
return EntityClass.factoryReference(item as EntityReference);
|
|
747
865
|
} catch (e: any) {
|
|
748
866
|
return item;
|
|
@@ -770,7 +888,7 @@ export function createServer(
|
|
|
770
888
|
function httpError(
|
|
771
889
|
res: NymphResponse,
|
|
772
890
|
defaultStatusCode: number,
|
|
773
|
-
error?: Error & { status?: number; statusText?: string }
|
|
891
|
+
error?: Error & { status?: number; statusText?: string },
|
|
774
892
|
) {
|
|
775
893
|
const status = error?.status || defaultStatusCode;
|
|
776
894
|
const statusText =
|
|
@@ -780,26 +898,82 @@ export function createServer(
|
|
|
780
898
|
: error.status in statusDescriptions &&
|
|
781
899
|
statusDescriptions[error.status]) ||
|
|
782
900
|
'Internal Server Error';
|
|
901
|
+
|
|
902
|
+
const errorResponse = error
|
|
903
|
+
? {
|
|
904
|
+
textStatus: `${status} ${statusText}`,
|
|
905
|
+
statusText,
|
|
906
|
+
message: error.message,
|
|
907
|
+
error,
|
|
908
|
+
...(process.env.NODE_ENV !== 'production'
|
|
909
|
+
? { stack: error.stack }
|
|
910
|
+
: {}),
|
|
911
|
+
}
|
|
912
|
+
: {
|
|
913
|
+
textStatus: `${status} ${statusText}`,
|
|
914
|
+
statusText,
|
|
915
|
+
message: statusText,
|
|
916
|
+
};
|
|
917
|
+
|
|
783
918
|
if (!res.headersSent) {
|
|
784
919
|
res.status(status);
|
|
785
920
|
res.setHeader('Content-Type', 'application/json');
|
|
786
921
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
922
|
+
|
|
923
|
+
res.send(errorResponse);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* End the event stream with an HTTP error response.
|
|
928
|
+
*
|
|
929
|
+
* @param res The server response object.
|
|
930
|
+
* @param defaultStatusCode The HTTP status code to use if none is given in the error object.
|
|
931
|
+
* @param error An optional error object to report.
|
|
932
|
+
*/
|
|
933
|
+
function eventStreamError(
|
|
934
|
+
res: NymphResponse,
|
|
935
|
+
defaultStatusCode: number,
|
|
936
|
+
error?: Error & { status?: number; statusText?: string },
|
|
937
|
+
) {
|
|
938
|
+
const status = error?.status || defaultStatusCode;
|
|
939
|
+
const statusText =
|
|
940
|
+
error?.statusText ||
|
|
941
|
+
(error?.status == null
|
|
942
|
+
? statusDescriptions[defaultStatusCode]
|
|
943
|
+
: error.status in statusDescriptions &&
|
|
944
|
+
statusDescriptions[error.status]) ||
|
|
945
|
+
'Internal Server Error';
|
|
946
|
+
|
|
947
|
+
const errorResponse = error
|
|
948
|
+
? {
|
|
949
|
+
status,
|
|
950
|
+
textStatus: `${status} ${statusText}`,
|
|
951
|
+
statusText,
|
|
952
|
+
message: error.message,
|
|
953
|
+
error,
|
|
954
|
+
...(process.env.NODE_ENV !== 'production'
|
|
955
|
+
? { stack: error.stack }
|
|
956
|
+
: {}),
|
|
957
|
+
}
|
|
958
|
+
: {
|
|
959
|
+
status,
|
|
960
|
+
textStatus: `${status} ${statusText}`,
|
|
961
|
+
statusText,
|
|
962
|
+
message: statusText,
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
if (!res.headersSent) {
|
|
966
|
+
res.status(status);
|
|
967
|
+
res.setHeader('Content-Type', 'application/json');
|
|
968
|
+
|
|
969
|
+
res.send(errorResponse);
|
|
797
970
|
} else {
|
|
798
|
-
res.
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
971
|
+
res.write('event: error\n');
|
|
972
|
+
res.write(`data: ${JSON.stringify(errorResponse)}\n\n`);
|
|
973
|
+
|
|
974
|
+
res.write('event: finished\n');
|
|
975
|
+
res.write('data: \n\n');
|
|
976
|
+
res.end();
|
|
803
977
|
}
|
|
804
978
|
}
|
|
805
979
|
|