@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.
- package/CHANGELOG.md +524 -0
- package/README.md +78 -4
- 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 +317 -167
- 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 +139 -26
- 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 +56 -8
- package/dist/testArtifacts.js +160 -74
- package/dist/testArtifacts.js.map +1 -1
- package/jest.config.js +31 -5
- package/package.json +35 -25
- package/src/cache.test.ts +5 -5
- package/src/createServer.ts +370 -184
- package/src/index.test.ts +172 -29
- 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
|
@@ -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@
|
|
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
|
-
|
|
150
|
+
options.source = 'client';
|
|
151
|
+
options.skipAc = false;
|
|
152
|
+
if (options.return === 'object') {
|
|
153
|
+
options.return = 'entity';
|
|
154
|
+
}
|
|
141
155
|
try {
|
|
142
|
-
|
|
156
|
+
options = classNamesToEntityConstructors(
|
|
157
|
+
response.locals.nymph,
|
|
158
|
+
options,
|
|
159
|
+
true,
|
|
160
|
+
);
|
|
143
161
|
} catch (e: any) {
|
|
144
|
-
|
|
145
|
-
|
|
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(
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
363
|
-
httpError(response,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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(
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
786
|
+
try {
|
|
787
|
+
await entity.$delete();
|
|
631
788
|
deleted.push(entData.guid);
|
|
632
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* This function will recurse into deeper arrays and objects.
|
|
894
|
+
* Return the request with an HTTP error response.
|
|
737
895
|
*
|
|
738
|
-
* @param
|
|
739
|
-
* @
|
|
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
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
934
|
+
|
|
935
|
+
res.send(errorResponse);
|
|
761
936
|
}
|
|
762
937
|
|
|
763
938
|
/**
|
|
764
|
-
*
|
|
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
|
|
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
|
-
|
|
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.
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
|