@nymphjs/server 1.0.0-beta.11 → 1.0.0-beta.111
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 +438 -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 +17 -0
- package/dist/createServer.js +907 -0
- package/dist/createServer.js.map +1 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +4 -793
- 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 +6 -0
- package/dist/statusDescriptions.js +69 -0
- package/dist/statusDescriptions.js.map +1 -0
- package/dist/testArtifacts.d.ts +55 -7
- package/dist/testArtifacts.js +160 -74
- package/dist/testArtifacts.js.map +1 -1
- package/jest.config.js +11 -2
- package/package.json +20 -20
- package/src/cache.test.ts +5 -5
- package/src/createServer.ts +981 -0
- package/src/index.test.ts +171 -27
- package/src/index.ts +4 -873
- package/src/statusDescriptions.ts +68 -0
- package/src/testArtifacts.ts +171 -42
- package/tsconfig.json +5 -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
|
@@ -0,0 +1,907 @@
|
|
|
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';
|
|
6
|
+
const NOT_FOUND_ERROR = 'Entity is not found.';
|
|
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@gmail.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 || {}));
|
|
26
|
+
function instantiateNymph(_request, response, next) {
|
|
27
|
+
response.locals.nymph = nymph.clone();
|
|
28
|
+
next();
|
|
29
|
+
}
|
|
30
|
+
async function authenticateTilmeld(request, response, next) {
|
|
31
|
+
if (response.locals.nymph.tilmeld) {
|
|
32
|
+
response.locals.nymph.tilmeld.request = request;
|
|
33
|
+
response.locals.nymph.tilmeld.response = response;
|
|
34
|
+
try {
|
|
35
|
+
await response.locals.nymph.tilmeld.authenticate(false, request.header('x-tilmeld-token-renewal') === 'off');
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
httpError(response, 500, e);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
next();
|
|
43
|
+
}
|
|
44
|
+
function unauthenticateTilmeld(_request, response, next) {
|
|
45
|
+
if (response.locals.nymph.tilmeld) {
|
|
46
|
+
response.locals.nymph.tilmeld.request = null;
|
|
47
|
+
response.locals.nymph.tilmeld.response = null;
|
|
48
|
+
try {
|
|
49
|
+
response.locals.nymph.tilmeld.clearSession();
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
httpError(response, 500, e);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
next();
|
|
57
|
+
}
|
|
58
|
+
function getActionData(request) {
|
|
59
|
+
if (request.method === 'GET') {
|
|
60
|
+
if (typeof request.query?.action !== 'string' ||
|
|
61
|
+
typeof request.query?.data !== 'string') {
|
|
62
|
+
return {
|
|
63
|
+
action: '',
|
|
64
|
+
data: {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
action: JSON.parse(request.query.action) ?? '',
|
|
69
|
+
data: JSON.parse(request.query.data),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
return {
|
|
74
|
+
action: request.body.action ?? '',
|
|
75
|
+
data: request.body.data,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Create a new instance of Nymph for the request/response.
|
|
80
|
+
rest.use(instantiateNymph);
|
|
81
|
+
// Authenticate before the request.
|
|
82
|
+
rest.use(authenticateTilmeld);
|
|
83
|
+
rest.get('/', async (request, response) => {
|
|
84
|
+
try {
|
|
85
|
+
const { action, data } = getActionData(request);
|
|
86
|
+
if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
|
|
87
|
+
httpError(response, 400);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (['entity', 'entities'].indexOf(action) !== -1) {
|
|
91
|
+
if (!Array.isArray(data)) {
|
|
92
|
+
httpError(response, 400);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const count = data.length;
|
|
96
|
+
if (count < 1 || typeof data[0] !== 'object') {
|
|
97
|
+
httpError(response, 400);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!('class' in data[0])) {
|
|
101
|
+
httpError(response, 400);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let [options, ...selectors] = data;
|
|
105
|
+
let EntityClass;
|
|
106
|
+
try {
|
|
107
|
+
EntityClass = response.locals.nymph.getEntityClass(data[0].class);
|
|
108
|
+
if (!EntityClass.restEnabled) {
|
|
109
|
+
httpError(response, 403);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
httpError(response, 400, e);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
options.class = EntityClass;
|
|
118
|
+
options.source = 'client';
|
|
119
|
+
options.skipAc = false;
|
|
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
|
+
}
|
|
136
|
+
let result;
|
|
137
|
+
try {
|
|
138
|
+
if (action === 'entity') {
|
|
139
|
+
result = await response.locals.nymph.getEntity(options, ...selectors);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
result = await response.locals.nymph.getEntities(options, ...selectors);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
httpError(response, 500, e);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (result == null || (Array.isArray(result) && result.length === 0)) {
|
|
150
|
+
if (action === 'entity' ||
|
|
151
|
+
response.locals.nymph.config.emptyListError) {
|
|
152
|
+
httpError(response, 404);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
response.setHeader('Content-Type', 'application/json');
|
|
157
|
+
response.send(JSON.stringify(result));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
if (typeof data !== 'string') {
|
|
161
|
+
httpError(response, 400);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (response.locals.nymph.tilmeld) {
|
|
165
|
+
if (!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(data, TilmeldAccessLevels.READ_ACCESS))) {
|
|
166
|
+
httpError(response, 403);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let result;
|
|
171
|
+
try {
|
|
172
|
+
result = await response.locals.nymph.getUID(data);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
httpError(response, 500, e);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (result === null) {
|
|
179
|
+
httpError(response, 404);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
else if (typeof result !== 'number') {
|
|
183
|
+
httpError(response, 500);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
response.setHeader('Content-Type', 'text/plain');
|
|
187
|
+
response.send(`${result}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
httpError(response, 500, e);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
rest.post('/', async (request, response) => {
|
|
196
|
+
try {
|
|
197
|
+
const { action, data: dataConst } = getActionData(request);
|
|
198
|
+
let data = dataConst;
|
|
199
|
+
if (['entity', 'entities', 'uid', 'method'].indexOf(action) === -1) {
|
|
200
|
+
httpError(response, 400);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (['entity', 'entities'].indexOf(action) !== -1) {
|
|
204
|
+
if (action === 'entity') {
|
|
205
|
+
data = [data];
|
|
206
|
+
}
|
|
207
|
+
const created = [];
|
|
208
|
+
let hadSuccess = false;
|
|
209
|
+
let invalidRequest = false;
|
|
210
|
+
let conflict = false;
|
|
211
|
+
let notfound = false;
|
|
212
|
+
let forbidden = false;
|
|
213
|
+
let lastException = null;
|
|
214
|
+
for (let entData of data) {
|
|
215
|
+
if (entData.guid) {
|
|
216
|
+
invalidRequest = true;
|
|
217
|
+
created.push(null);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
let entity;
|
|
221
|
+
try {
|
|
222
|
+
entity = await loadEntity(entData, response.locals.nymph);
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
if (e instanceof EntityConflictError) {
|
|
226
|
+
conflict = true;
|
|
227
|
+
}
|
|
228
|
+
else if (e instanceof ForbiddenClassError) {
|
|
229
|
+
forbidden = true;
|
|
230
|
+
}
|
|
231
|
+
else if (e.message === NOT_FOUND_ERROR) {
|
|
232
|
+
notfound = true;
|
|
233
|
+
}
|
|
234
|
+
else if (e instanceof InvalidParametersError) {
|
|
235
|
+
invalidRequest = true;
|
|
236
|
+
lastException = e;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
lastException = e;
|
|
240
|
+
}
|
|
241
|
+
created.push(null);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!entity) {
|
|
245
|
+
invalidRequest = true;
|
|
246
|
+
created.push(null);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
if (await entity.$save()) {
|
|
251
|
+
created.push(entity);
|
|
252
|
+
hadSuccess = true;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
created.push(false);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
if (e instanceof EntityInvalidDataError) {
|
|
260
|
+
invalidRequest = true;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
lastException = e;
|
|
264
|
+
}
|
|
265
|
+
created.push(null);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!hadSuccess) {
|
|
269
|
+
if (invalidRequest) {
|
|
270
|
+
httpError(response, 400, lastException);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
else if (conflict) {
|
|
274
|
+
httpError(response, 409);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
else if (forbidden) {
|
|
278
|
+
httpError(response, 403);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
else if (notfound) {
|
|
282
|
+
httpError(response, 404);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
httpError(response, 500, lastException);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
response.status(201);
|
|
291
|
+
response.setHeader('Content-Type', 'application/json');
|
|
292
|
+
if (action === 'entity') {
|
|
293
|
+
response.send(JSON.stringify(created[0]));
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
response.send(created);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (action === 'method') {
|
|
300
|
+
if (!Array.isArray(data.params)) {
|
|
301
|
+
httpError(response, 400);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
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
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
httpError(response, 400);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
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;
|
|
326
|
+
}
|
|
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
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
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
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
let entity;
|
|
391
|
+
try {
|
|
392
|
+
entity = await loadEntity(data.entity, response.locals.nymph);
|
|
393
|
+
}
|
|
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;
|
|
411
|
+
}
|
|
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;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
if (e instanceof ForbiddenClassError) {
|
|
448
|
+
httpError(response, 403);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
httpError(response, 500, e);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
if (typeof data !== 'string') {
|
|
459
|
+
httpError(response, 400);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (response.locals.nymph.tilmeld) {
|
|
463
|
+
if (!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(data, TilmeldAccessLevels.WRITE_ACCESS))) {
|
|
464
|
+
httpError(response, 403);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
let result;
|
|
469
|
+
try {
|
|
470
|
+
result = await response.locals.nymph.newUID(data);
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
httpError(response, 500, e);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (typeof result !== 'number') {
|
|
477
|
+
httpError(response, 500);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
response.status(201);
|
|
481
|
+
response.setHeader('Content-Type', 'text/plain');
|
|
482
|
+
response.send(`${result}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
httpError(response, 500, e);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
rest.put('/', async (request, response) => {
|
|
491
|
+
try {
|
|
492
|
+
const { action, data } = getActionData(request);
|
|
493
|
+
if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
|
|
494
|
+
httpError(response, 400);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
await doPutOrPatch(response, action, data, false);
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
httpError(response, 500, e);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
rest.patch('/', async (request, response) => {
|
|
505
|
+
try {
|
|
506
|
+
const { action, data } = getActionData(request);
|
|
507
|
+
if (['entity', 'entities'].indexOf(action) === -1) {
|
|
508
|
+
httpError(response, 400);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
await doPutOrPatch(response, action, data, true);
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
httpError(response, 500, e);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
async function doPutOrPatch(response, action, data, patch) {
|
|
519
|
+
if (action === 'uid') {
|
|
520
|
+
if (typeof data.name !== 'string' || typeof data.value !== 'number') {
|
|
521
|
+
httpError(response, 400);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (response.locals.nymph.tilmeld) {
|
|
525
|
+
if (!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(data.name, TilmeldAccessLevels.FULL_ACCESS))) {
|
|
526
|
+
httpError(response, 403);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
let result;
|
|
531
|
+
try {
|
|
532
|
+
result = await response.locals.nymph.setUID(data.name, data.value);
|
|
533
|
+
}
|
|
534
|
+
catch (e) {
|
|
535
|
+
httpError(response, 500, e);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (!result) {
|
|
539
|
+
httpError(response, 500);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
response.status(200);
|
|
543
|
+
response.setHeader('Content-Type', 'text/plain');
|
|
544
|
+
response.send(`${result}`);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
if (action === 'entity') {
|
|
548
|
+
data = [data];
|
|
549
|
+
}
|
|
550
|
+
const saved = [];
|
|
551
|
+
let hadSuccess = false;
|
|
552
|
+
let invalidRequest = false;
|
|
553
|
+
let conflict = false;
|
|
554
|
+
let forbidden = false;
|
|
555
|
+
let notfound = false;
|
|
556
|
+
let lastException = null;
|
|
557
|
+
for (let entData of data) {
|
|
558
|
+
if (entData.guid && entData.guid.length != 24) {
|
|
559
|
+
invalidRequest = true;
|
|
560
|
+
saved.push(null);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
let entity;
|
|
564
|
+
try {
|
|
565
|
+
entity = await loadEntity(entData, response.locals.nymph, patch);
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
if (e instanceof EntityConflictError) {
|
|
569
|
+
conflict = true;
|
|
570
|
+
}
|
|
571
|
+
else if (e instanceof ForbiddenClassError) {
|
|
572
|
+
forbidden = true;
|
|
573
|
+
}
|
|
574
|
+
else if (e.message === NOT_FOUND_ERROR) {
|
|
575
|
+
notfound = true;
|
|
576
|
+
}
|
|
577
|
+
else if (e instanceof InvalidParametersError) {
|
|
578
|
+
invalidRequest = true;
|
|
579
|
+
lastException = e;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
lastException = e;
|
|
583
|
+
}
|
|
584
|
+
saved.push(null);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (!entity) {
|
|
588
|
+
invalidRequest = true;
|
|
589
|
+
saved.push(null);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
if (await entity.$save()) {
|
|
594
|
+
saved.push(entity);
|
|
595
|
+
hadSuccess = true;
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
saved.push(false);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
if (e instanceof EntityInvalidDataError) {
|
|
603
|
+
invalidRequest = true;
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
lastException = e;
|
|
607
|
+
}
|
|
608
|
+
saved.push(null);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (!hadSuccess) {
|
|
612
|
+
if (invalidRequest) {
|
|
613
|
+
httpError(response, 400, lastException);
|
|
614
|
+
}
|
|
615
|
+
else if (forbidden) {
|
|
616
|
+
httpError(response, 403);
|
|
617
|
+
}
|
|
618
|
+
else if (conflict) {
|
|
619
|
+
httpError(response, 409);
|
|
620
|
+
}
|
|
621
|
+
else if (notfound) {
|
|
622
|
+
httpError(response, 404);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
httpError(response, 500, lastException);
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
response.status(200);
|
|
630
|
+
response.setHeader('Content-Type', 'application/json');
|
|
631
|
+
if (action === 'entity') {
|
|
632
|
+
response.send(JSON.stringify(saved[0]));
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
response.send(saved);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
rest.delete('/', async (request, response) => {
|
|
640
|
+
try {
|
|
641
|
+
const { action, data: dataConst } = getActionData(request);
|
|
642
|
+
let data = dataConst;
|
|
643
|
+
if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
|
|
644
|
+
httpError(response, 400);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (['entity', 'entities'].indexOf(action) !== -1) {
|
|
648
|
+
if (action === 'entity') {
|
|
649
|
+
data = [data];
|
|
650
|
+
}
|
|
651
|
+
const deleted = [];
|
|
652
|
+
let failures = false;
|
|
653
|
+
let invalidRequest = false;
|
|
654
|
+
let notfound = false;
|
|
655
|
+
let lastException = null;
|
|
656
|
+
for (let entData of data) {
|
|
657
|
+
if (entData.guid && entData.guid.length != 24) {
|
|
658
|
+
invalidRequest = true;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
let EntityClass;
|
|
662
|
+
try {
|
|
663
|
+
EntityClass = response.locals.nymph.getEntityClass(entData.class);
|
|
664
|
+
if (!EntityClass.restEnabled) {
|
|
665
|
+
httpError(response, 403);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (e) {
|
|
670
|
+
invalidRequest = true;
|
|
671
|
+
failures = true;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
let entity;
|
|
675
|
+
try {
|
|
676
|
+
entity = await response.locals.nymph.getEntity({ class: EntityClass }, { type: '&', guid: entData.guid });
|
|
677
|
+
}
|
|
678
|
+
catch (e) {
|
|
679
|
+
lastException = e;
|
|
680
|
+
failures = true;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (!entity) {
|
|
684
|
+
notfound = true;
|
|
685
|
+
failures = true;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
if (await entity.$delete()) {
|
|
690
|
+
deleted.push(entData.guid);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
failures = true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
lastException = e;
|
|
698
|
+
failures = true;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (deleted.length === 0) {
|
|
702
|
+
if (invalidRequest || !failures) {
|
|
703
|
+
httpError(response, 400, lastException);
|
|
704
|
+
}
|
|
705
|
+
else if (notfound) {
|
|
706
|
+
httpError(response, 404);
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
httpError(response, 500, lastException);
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
response.status(200);
|
|
714
|
+
response.setHeader('Content-Type', 'application/json');
|
|
715
|
+
if (action === 'entity') {
|
|
716
|
+
response.send(JSON.stringify(deleted[0]));
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
response.send(deleted);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
if (typeof data !== 'string') {
|
|
724
|
+
httpError(response, 400);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (response.locals.nymph.tilmeld) {
|
|
728
|
+
if (!(await response.locals.nymph.tilmeld.checkClientUIDPermissions(data, TilmeldAccessLevels.FULL_ACCESS))) {
|
|
729
|
+
httpError(response, 403);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
let result;
|
|
734
|
+
try {
|
|
735
|
+
result = await response.locals.nymph.deleteUID(data);
|
|
736
|
+
}
|
|
737
|
+
catch (e) {
|
|
738
|
+
httpError(response, 500, e);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (!result) {
|
|
742
|
+
httpError(response, 500);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
response.status(200);
|
|
746
|
+
response.setHeader('Content-Type', 'application/json');
|
|
747
|
+
response.send(JSON.stringify(result));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch (e) {
|
|
751
|
+
httpError(response, 500, e);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
// Unauthenticate after the request.
|
|
756
|
+
rest.use(unauthenticateTilmeld);
|
|
757
|
+
async function loadEntity(entityData, nymph, patch = false, allowConflict = false) {
|
|
758
|
+
if (entityData.class === 'Entity') {
|
|
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.");
|
|
761
|
+
}
|
|
762
|
+
let EntityClass = nymph.getEntityClass(entityData.class);
|
|
763
|
+
if (!EntityClass.restEnabled) {
|
|
764
|
+
throw new ForbiddenClassError('Not accessible.');
|
|
765
|
+
}
|
|
766
|
+
let entity;
|
|
767
|
+
if (entityData.guid) {
|
|
768
|
+
entity = await nymph.getEntity({ class: EntityClass, source: 'client' }, {
|
|
769
|
+
type: '&',
|
|
770
|
+
guid: `${entityData['guid']}`,
|
|
771
|
+
});
|
|
772
|
+
if (entity === null) {
|
|
773
|
+
throw new Error(NOT_FOUND_ERROR);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
entity = await EntityClass.factory();
|
|
778
|
+
}
|
|
779
|
+
if (patch) {
|
|
780
|
+
entity.$jsonAcceptPatch(entityData, allowConflict);
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
entity.$jsonAcceptData(entityData, allowConflict);
|
|
784
|
+
}
|
|
785
|
+
return entity;
|
|
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
|
+
*/
|
|
795
|
+
function referencesToEntities(item, nymph) {
|
|
796
|
+
if (item == null) {
|
|
797
|
+
return item;
|
|
798
|
+
}
|
|
799
|
+
else if (Array.isArray(item)) {
|
|
800
|
+
if (item.length === 3 && item[0] === 'nymph_entity_reference') {
|
|
801
|
+
try {
|
|
802
|
+
const EntityClass = nymph.getEntityClass(item[1]);
|
|
803
|
+
if (!EntityClass.restEnabled) {
|
|
804
|
+
throw new ForbiddenClassError('Not accessible.');
|
|
805
|
+
}
|
|
806
|
+
return EntityClass.factoryReference(item);
|
|
807
|
+
}
|
|
808
|
+
catch (e) {
|
|
809
|
+
return item;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return item.map((entry) => referencesToEntities(entry, nymph));
|
|
813
|
+
}
|
|
814
|
+
else if (typeof item === 'object' && !(item instanceof Entity)) {
|
|
815
|
+
// Only do this for non-entity objects.
|
|
816
|
+
const newItem = {};
|
|
817
|
+
for (let curProperty in item) {
|
|
818
|
+
newItem[curProperty] = referencesToEntities(item[curProperty], nymph);
|
|
819
|
+
}
|
|
820
|
+
return newItem;
|
|
821
|
+
}
|
|
822
|
+
return item;
|
|
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
|
+
*/
|
|
831
|
+
function httpError(res, defaultStatusCode, error) {
|
|
832
|
+
const status = error?.status || defaultStatusCode;
|
|
833
|
+
const statusText = error?.statusText ||
|
|
834
|
+
(error?.status == null
|
|
835
|
+
? statusDescriptions[defaultStatusCode]
|
|
836
|
+
: error.status in statusDescriptions &&
|
|
837
|
+
statusDescriptions[error.status]) ||
|
|
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
|
+
};
|
|
854
|
+
if (!res.headersSent) {
|
|
855
|
+
res.status(status);
|
|
856
|
+
res.setHeader('Content-Type', 'application/json');
|
|
857
|
+
}
|
|
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,
|
|
878
|
+
textStatus: `${status} ${statusText}`,
|
|
879
|
+
statusText,
|
|
880
|
+
message: error.message,
|
|
881
|
+
error,
|
|
882
|
+
...(process.env.NODE_ENV !== 'production'
|
|
883
|
+
? { stack: error.stack }
|
|
884
|
+
: {}),
|
|
885
|
+
}
|
|
886
|
+
: {
|
|
887
|
+
status,
|
|
888
|
+
textStatus: `${status} ${statusText}`,
|
|
889
|
+
statusText,
|
|
890
|
+
message: statusText,
|
|
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();
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return rest;
|
|
906
|
+
}
|
|
907
|
+
//# sourceMappingURL=createServer.js.map
|