@nymphjs/server 1.0.0-beta.2 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +77 -5
- package/dist/HttpError.d.ts +5 -0
- package/dist/HttpError.js +13 -0
- package/dist/HttpError.js.map +1 -0
- package/dist/cache.test.js +5 -5
- package/dist/cache.test.js.map +1 -1
- package/dist/createServer.d.ts +5 -0
- package/dist/createServer.js +728 -0
- package/dist/createServer.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +18 -712
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +67 -39
- package/dist/index.test.js.map +1 -1
- package/dist/statusDescriptions.d.ts +3 -0
- package/dist/statusDescriptions.js +69 -0
- package/dist/statusDescriptions.js.map +1 -0
- package/dist/testArtifacts.d.ts +4 -0
- package/dist/testArtifacts.js +20 -5
- package/dist/testArtifacts.js.map +1 -1
- package/package.json +12 -12
- package/src/HttpError.ts +12 -0
- package/src/cache.test.ts +6 -3
- package/src/createServer.ts +807 -0
- package/src/index.test.ts +38 -5
- package/src/index.ts +5 -793
- package/src/statusDescriptions.ts +68 -0
- package/src/testArtifacts.ts +23 -3
package/src/index.ts
CHANGED
|
@@ -1,795 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import express, { NextFunction, Request, Response } from 'express';
|
|
3
|
-
import cookieParser from 'cookie-parser';
|
|
4
|
-
import {
|
|
5
|
-
Nymph,
|
|
6
|
-
Entity,
|
|
7
|
-
EntityConflictError,
|
|
8
|
-
EntityConstructor,
|
|
9
|
-
EntityInterface,
|
|
10
|
-
EntityJson,
|
|
11
|
-
EntityPatch,
|
|
12
|
-
EntityReference,
|
|
13
|
-
InvalidParametersError,
|
|
14
|
-
TilmeldAccessLevels,
|
|
15
|
-
classNamesToEntityConstructors,
|
|
16
|
-
Options,
|
|
17
|
-
Selector,
|
|
18
|
-
} from '@nymphjs/nymph';
|
|
19
|
-
import { EntityInvalidDataError } from '@nymphjs/nymph';
|
|
1
|
+
import { createServer } from './createServer';
|
|
20
2
|
|
|
21
|
-
|
|
3
|
+
export * from './HttpError';
|
|
4
|
+
export * from './statusDescriptions';
|
|
5
|
+
export * from './createServer';
|
|
22
6
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* A REST server middleware creator for Nymph.
|
|
27
|
-
*
|
|
28
|
-
* Written by Hunter Perrin for SciActive.
|
|
29
|
-
*
|
|
30
|
-
* @author Hunter Perrin <hperrin@gmail.com>
|
|
31
|
-
* @copyright SciActive Inc
|
|
32
|
-
* @see http://nymph.io/
|
|
33
|
-
*/
|
|
34
|
-
export default function createServer(
|
|
35
|
-
nymph: Nymph,
|
|
36
|
-
{ jsonOptions = {} }: { jsonOptions?: OptionsJson } = {}
|
|
37
|
-
) {
|
|
38
|
-
const rest = express();
|
|
39
|
-
rest.use(cookieParser());
|
|
40
|
-
rest.use(express.json(jsonOptions || {}));
|
|
41
|
-
|
|
42
|
-
function instantiateNymph(
|
|
43
|
-
_request: Request,
|
|
44
|
-
response: NymphResponse,
|
|
45
|
-
next: NextFunction
|
|
46
|
-
) {
|
|
47
|
-
response.locals.nymph = nymph.clone();
|
|
48
|
-
next();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function authenticateTilmeld(
|
|
52
|
-
request: Request,
|
|
53
|
-
response: NymphResponse,
|
|
54
|
-
next: NextFunction
|
|
55
|
-
) {
|
|
56
|
-
if (response.locals.nymph.tilmeld) {
|
|
57
|
-
response.locals.nymph.tilmeld.request = request;
|
|
58
|
-
response.locals.nymph.tilmeld.response = response;
|
|
59
|
-
try {
|
|
60
|
-
response.locals.nymph.tilmeld.authenticate();
|
|
61
|
-
} catch (e: any) {
|
|
62
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
next();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function unauthenticateTilmeld(
|
|
70
|
-
_request: Request,
|
|
71
|
-
response: NymphResponse,
|
|
72
|
-
next: NextFunction
|
|
73
|
-
) {
|
|
74
|
-
if (response.locals.nymph.tilmeld) {
|
|
75
|
-
response.locals.nymph.tilmeld.request = null;
|
|
76
|
-
response.locals.nymph.tilmeld.response = null;
|
|
77
|
-
try {
|
|
78
|
-
response.locals.nymph.tilmeld.clearSession();
|
|
79
|
-
} catch (e: any) {
|
|
80
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
next();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getActionData(request: Request): { action: string; data: any } {
|
|
88
|
-
if (request.method === 'GET') {
|
|
89
|
-
if (
|
|
90
|
-
typeof request.query?.action !== 'string' ||
|
|
91
|
-
typeof request.query?.data !== 'string'
|
|
92
|
-
) {
|
|
93
|
-
return {
|
|
94
|
-
action: '',
|
|
95
|
-
data: {},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
action: JSON.parse(request.query.action) ?? '',
|
|
100
|
-
data: JSON.parse(request.query.data),
|
|
101
|
-
};
|
|
102
|
-
} else {
|
|
103
|
-
return {
|
|
104
|
-
action: request.body.action ?? '',
|
|
105
|
-
data: request.body.data,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Create a new instance of Nymph for the request/response.
|
|
111
|
-
rest.use(instantiateNymph);
|
|
112
|
-
|
|
113
|
-
// Authenticate before the request.
|
|
114
|
-
rest.use(authenticateTilmeld);
|
|
115
|
-
|
|
116
|
-
rest.get('/', async (request, response: NymphResponse) => {
|
|
117
|
-
try {
|
|
118
|
-
const { action, data } = getActionData(request);
|
|
119
|
-
if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
|
|
120
|
-
httpError(response, 400, 'Bad Request');
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (['entity', 'entities'].indexOf(action) !== -1) {
|
|
124
|
-
if (!Array.isArray(data)) {
|
|
125
|
-
httpError(response, 400, 'Bad Request');
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const count = data.length;
|
|
129
|
-
if (count < 1 || typeof data[0] !== 'object') {
|
|
130
|
-
httpError(response, 400, 'Bad Request');
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (!('class' in data[0])) {
|
|
134
|
-
httpError(response, 400, 'Bad Request');
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
let [options, ...selectors] = data as [Options, ...Selector[]];
|
|
138
|
-
let EntityClass;
|
|
139
|
-
try {
|
|
140
|
-
EntityClass = response.locals.nymph.getEntityClass(data[0].class);
|
|
141
|
-
} catch (e: any) {
|
|
142
|
-
httpError(response, 400, 'Bad Request', e);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
options.class = EntityClass;
|
|
146
|
-
options.source = 'client';
|
|
147
|
-
options.skipAc = false;
|
|
148
|
-
selectors = classNamesToEntityConstructors(
|
|
149
|
-
response.locals.nymph,
|
|
150
|
-
selectors
|
|
151
|
-
);
|
|
152
|
-
let result:
|
|
153
|
-
| EntityInterface
|
|
154
|
-
| EntityInterface[]
|
|
155
|
-
| string
|
|
156
|
-
| string[]
|
|
157
|
-
| number
|
|
158
|
-
| null;
|
|
159
|
-
try {
|
|
160
|
-
if (action === 'entity') {
|
|
161
|
-
result = await response.locals.nymph.getEntity(
|
|
162
|
-
options,
|
|
163
|
-
...selectors
|
|
164
|
-
);
|
|
165
|
-
} else {
|
|
166
|
-
result = await response.locals.nymph.getEntities(
|
|
167
|
-
options,
|
|
168
|
-
...selectors
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
} catch (e: any) {
|
|
172
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (result == null || (Array.isArray(result) && result.length === 0)) {
|
|
176
|
-
if (
|
|
177
|
-
action === 'entity' ||
|
|
178
|
-
response.locals.nymph.config.emptyListError
|
|
179
|
-
) {
|
|
180
|
-
httpError(response, 404, 'Not Found');
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
response.setHeader('Content-Type', 'application/json');
|
|
185
|
-
response.send(JSON.stringify(result));
|
|
186
|
-
} else {
|
|
187
|
-
if (typeof data !== 'string') {
|
|
188
|
-
httpError(response, 400, 'Bad Request');
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (response.locals.nymph.tilmeld) {
|
|
192
|
-
if (
|
|
193
|
-
!response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
194
|
-
data,
|
|
195
|
-
TilmeldAccessLevels.READ_ACCESS
|
|
196
|
-
)
|
|
197
|
-
) {
|
|
198
|
-
httpError(response, 403, 'Forbidden');
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
let result: number | null;
|
|
203
|
-
try {
|
|
204
|
-
result = await response.locals.nymph.getUID(data);
|
|
205
|
-
} catch (e: any) {
|
|
206
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (result === null) {
|
|
210
|
-
httpError(response, 404, 'Not Found');
|
|
211
|
-
return;
|
|
212
|
-
} else if (typeof result !== 'number') {
|
|
213
|
-
httpError(response, 500, 'Internal Server Error');
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
response.setHeader('Content-Type', 'text/plain');
|
|
217
|
-
response.send(`${result}`);
|
|
218
|
-
}
|
|
219
|
-
} catch (e: any) {
|
|
220
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
rest.post('/', async (request, response: NymphResponse) => {
|
|
226
|
-
try {
|
|
227
|
-
const { action, data: dataConst } = getActionData(request);
|
|
228
|
-
let data = dataConst;
|
|
229
|
-
if (['entity', 'entities', 'uid', 'method'].indexOf(action) === -1) {
|
|
230
|
-
httpError(response, 400, 'Bad Request');
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (['entity', 'entities'].indexOf(action) !== -1) {
|
|
234
|
-
if (action === 'entity') {
|
|
235
|
-
data = [data];
|
|
236
|
-
}
|
|
237
|
-
const created = [];
|
|
238
|
-
let hadSuccess = false;
|
|
239
|
-
let invalidRequest = false;
|
|
240
|
-
let conflict = false;
|
|
241
|
-
let notfound = false;
|
|
242
|
-
let lastException = null;
|
|
243
|
-
for (let entData of data) {
|
|
244
|
-
if (entData.guid) {
|
|
245
|
-
invalidRequest = true;
|
|
246
|
-
created.push(null);
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
let entity: EntityInterface;
|
|
250
|
-
try {
|
|
251
|
-
entity = await loadEntity(entData, response.locals.nymph);
|
|
252
|
-
} catch (e: any) {
|
|
253
|
-
if (e instanceof EntityConflictError) {
|
|
254
|
-
conflict = true;
|
|
255
|
-
} else if (e.message === NOT_FOUND_ERROR) {
|
|
256
|
-
notfound = true;
|
|
257
|
-
} else if (e instanceof InvalidParametersError) {
|
|
258
|
-
invalidRequest = true;
|
|
259
|
-
lastException = e;
|
|
260
|
-
} else {
|
|
261
|
-
lastException = e;
|
|
262
|
-
}
|
|
263
|
-
created.push(null);
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
if (!entity) {
|
|
267
|
-
invalidRequest = true;
|
|
268
|
-
created.push(null);
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
if (await entity.$save()) {
|
|
273
|
-
created.push(entity);
|
|
274
|
-
hadSuccess = true;
|
|
275
|
-
} else {
|
|
276
|
-
created.push(false);
|
|
277
|
-
}
|
|
278
|
-
} catch (e: any) {
|
|
279
|
-
if (e instanceof EntityInvalidDataError) {
|
|
280
|
-
invalidRequest = true;
|
|
281
|
-
} else {
|
|
282
|
-
lastException = e;
|
|
283
|
-
}
|
|
284
|
-
created.push(null);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (!hadSuccess) {
|
|
288
|
-
if (invalidRequest) {
|
|
289
|
-
httpError(response, 400, 'Bad Request', lastException);
|
|
290
|
-
return;
|
|
291
|
-
} else if (conflict) {
|
|
292
|
-
httpError(response, 409, 'Conflict');
|
|
293
|
-
return;
|
|
294
|
-
} else if (notfound) {
|
|
295
|
-
httpError(response, 404, 'Not Found');
|
|
296
|
-
return;
|
|
297
|
-
} else {
|
|
298
|
-
httpError(response, 500, 'Internal Server Error', lastException);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
response.status(201);
|
|
303
|
-
response.setHeader('Content-Type', 'application/json');
|
|
304
|
-
if (action === 'entity') {
|
|
305
|
-
response.send(JSON.stringify(created[0]));
|
|
306
|
-
} else {
|
|
307
|
-
response.send(created);
|
|
308
|
-
}
|
|
309
|
-
} else if (action === 'method') {
|
|
310
|
-
if (!Array.isArray(data.params)) {
|
|
311
|
-
httpError(response, 400, 'Bad Request');
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
const params = referencesToEntities(
|
|
315
|
-
[...data.params],
|
|
316
|
-
response.locals.nymph
|
|
317
|
-
);
|
|
318
|
-
if (data.static) {
|
|
319
|
-
let EntityClass: EntityConstructor;
|
|
320
|
-
try {
|
|
321
|
-
EntityClass = response.locals.nymph.getEntityClass(data.class);
|
|
322
|
-
} catch (e: any) {
|
|
323
|
-
httpError(response, 400, 'Bad Request');
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (
|
|
327
|
-
EntityClass.clientEnabledStaticMethods.indexOf(data.method) === -1
|
|
328
|
-
) {
|
|
329
|
-
httpError(response, 403, 'Forbidden');
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
if (!(data.method in EntityClass)) {
|
|
333
|
-
httpError(response, 400, 'Bad Request');
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
// @ts-ignore Dynamic methods make TypeScript sad.
|
|
337
|
-
const method: Function = EntityClass[data.method];
|
|
338
|
-
if (typeof method !== 'function') {
|
|
339
|
-
httpError(response, 400, 'Bad Request');
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
try {
|
|
343
|
-
const result = method.call(EntityClass, ...params);
|
|
344
|
-
let ret = result;
|
|
345
|
-
if (result instanceof Promise) {
|
|
346
|
-
ret = await result;
|
|
347
|
-
}
|
|
348
|
-
response.status(200);
|
|
349
|
-
response.setHeader('Content-Type', 'application/json');
|
|
350
|
-
response.send({ return: ret });
|
|
351
|
-
} catch (e: any) {
|
|
352
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
} else {
|
|
356
|
-
let entity: EntityInterface;
|
|
357
|
-
try {
|
|
358
|
-
entity = await loadEntity(data.entity, response.locals.nymph);
|
|
359
|
-
} catch (e: any) {
|
|
360
|
-
if (e instanceof EntityConflictError) {
|
|
361
|
-
httpError(response, 409, 'Conflict');
|
|
362
|
-
} else if (e.message === NOT_FOUND_ERROR) {
|
|
363
|
-
httpError(response, 404, 'Not Found', e);
|
|
364
|
-
} else if (e instanceof InvalidParametersError) {
|
|
365
|
-
httpError(response, 400, 'Bad Request', e);
|
|
366
|
-
} else {
|
|
367
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
368
|
-
}
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (data.entity.guid && !entity.guid) {
|
|
372
|
-
httpError(response, 400, 'Bad Request');
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
if (entity.$getClientEnabledMethods().indexOf(data.method) === -1) {
|
|
376
|
-
httpError(response, 403, 'Forbidden');
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (
|
|
380
|
-
!(data.method in entity) ||
|
|
381
|
-
typeof entity[data.method] !== 'function'
|
|
382
|
-
) {
|
|
383
|
-
httpError(response, 400, 'Bad Request');
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
try {
|
|
387
|
-
const result = entity[data.method](...params);
|
|
388
|
-
let ret = result;
|
|
389
|
-
if (result instanceof Promise) {
|
|
390
|
-
ret = await result;
|
|
391
|
-
}
|
|
392
|
-
response.status(200);
|
|
393
|
-
response.setHeader('Content-Type', 'application/json');
|
|
394
|
-
if (data.stateless) {
|
|
395
|
-
response.send({ return: ret });
|
|
396
|
-
} else {
|
|
397
|
-
response.send({ entity: entity, return: ret });
|
|
398
|
-
}
|
|
399
|
-
} catch (e: any) {
|
|
400
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
} else {
|
|
405
|
-
if (typeof data !== 'string') {
|
|
406
|
-
httpError(response, 400, 'Bad Request');
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
if (response.locals.nymph.tilmeld) {
|
|
410
|
-
if (
|
|
411
|
-
!response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
412
|
-
data,
|
|
413
|
-
TilmeldAccessLevels.WRITE_ACCESS
|
|
414
|
-
)
|
|
415
|
-
) {
|
|
416
|
-
httpError(response, 403, 'Forbidden');
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
let result: number | null;
|
|
421
|
-
try {
|
|
422
|
-
result = await response.locals.nymph.newUID(data);
|
|
423
|
-
} catch (e: any) {
|
|
424
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (typeof result !== 'number') {
|
|
428
|
-
httpError(response, 500, 'Internal Server Error');
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
response.status(201);
|
|
432
|
-
response.setHeader('Content-Type', 'text/plain');
|
|
433
|
-
response.send(`${result}`);
|
|
434
|
-
}
|
|
435
|
-
} catch (e: any) {
|
|
436
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
rest.put('/', async (request, response: NymphResponse) => {
|
|
442
|
-
try {
|
|
443
|
-
const { action, data } = getActionData(request);
|
|
444
|
-
if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
|
|
445
|
-
httpError(response, 400, 'Bad Request');
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
await doPutOrPatch(response, action, data, false);
|
|
449
|
-
} catch (e: any) {
|
|
450
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
rest.patch('/', async (request, response: NymphResponse) => {
|
|
456
|
-
try {
|
|
457
|
-
const { action, data } = getActionData(request);
|
|
458
|
-
if (['entity', 'entities'].indexOf(action) === -1) {
|
|
459
|
-
httpError(response, 400, 'Bad Request');
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
await doPutOrPatch(response, action, data, true);
|
|
463
|
-
} catch (e: any) {
|
|
464
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
async function doPutOrPatch(
|
|
470
|
-
response: NymphResponse,
|
|
471
|
-
action: string,
|
|
472
|
-
data: any,
|
|
473
|
-
patch: boolean
|
|
474
|
-
) {
|
|
475
|
-
if (action === 'uid') {
|
|
476
|
-
if (typeof data.name !== 'string' || typeof data.value !== 'number') {
|
|
477
|
-
httpError(response, 400, 'Bad Request');
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
if (response.locals.nymph.tilmeld) {
|
|
481
|
-
if (
|
|
482
|
-
!response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
483
|
-
data.name,
|
|
484
|
-
TilmeldAccessLevels.FULL_ACCESS
|
|
485
|
-
)
|
|
486
|
-
) {
|
|
487
|
-
httpError(response, 403, 'Forbidden');
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
let result: boolean;
|
|
492
|
-
try {
|
|
493
|
-
result = await response.locals.nymph.setUID(data.name, data.value);
|
|
494
|
-
} catch (e: any) {
|
|
495
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
if (!result) {
|
|
499
|
-
httpError(response, 500, 'Internal Server Error');
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
response.status(200);
|
|
503
|
-
response.setHeader('Content-Type', 'text/plain');
|
|
504
|
-
response.send(`${result}`);
|
|
505
|
-
} else {
|
|
506
|
-
if (action === 'entity') {
|
|
507
|
-
data = [data];
|
|
508
|
-
}
|
|
509
|
-
const saved = [];
|
|
510
|
-
let hadSuccess = false;
|
|
511
|
-
let invalidRequest = false;
|
|
512
|
-
let conflict = false;
|
|
513
|
-
let notfound = false;
|
|
514
|
-
let lastException = null;
|
|
515
|
-
for (let entData of data) {
|
|
516
|
-
if (entData.guid && entData.guid.length != 24) {
|
|
517
|
-
invalidRequest = true;
|
|
518
|
-
saved.push(null);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
let entity: EntityInterface;
|
|
522
|
-
try {
|
|
523
|
-
entity = await loadEntity(entData, response.locals.nymph, patch);
|
|
524
|
-
} catch (e: any) {
|
|
525
|
-
if (e instanceof EntityConflictError) {
|
|
526
|
-
conflict = true;
|
|
527
|
-
} else if (e.message === NOT_FOUND_ERROR) {
|
|
528
|
-
notfound = true;
|
|
529
|
-
} else if (e instanceof InvalidParametersError) {
|
|
530
|
-
invalidRequest = true;
|
|
531
|
-
lastException = e;
|
|
532
|
-
} else {
|
|
533
|
-
lastException = e;
|
|
534
|
-
}
|
|
535
|
-
saved.push(null);
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
if (!entity) {
|
|
539
|
-
invalidRequest = true;
|
|
540
|
-
saved.push(null);
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
try {
|
|
544
|
-
if (await entity.$save()) {
|
|
545
|
-
saved.push(entity);
|
|
546
|
-
hadSuccess = true;
|
|
547
|
-
} else {
|
|
548
|
-
saved.push(false);
|
|
549
|
-
}
|
|
550
|
-
} catch (e: any) {
|
|
551
|
-
if (e instanceof EntityInvalidDataError) {
|
|
552
|
-
invalidRequest = true;
|
|
553
|
-
} else {
|
|
554
|
-
lastException = e;
|
|
555
|
-
}
|
|
556
|
-
saved.push(null);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
if (!hadSuccess) {
|
|
560
|
-
if (invalidRequest) {
|
|
561
|
-
httpError(response, 400, 'Bad Request', lastException);
|
|
562
|
-
} else if (conflict) {
|
|
563
|
-
httpError(response, 409, 'Conflict');
|
|
564
|
-
} else if (notfound) {
|
|
565
|
-
httpError(response, 404, 'Not Found');
|
|
566
|
-
} else {
|
|
567
|
-
httpError(response, 500, 'Internal Server Error', lastException);
|
|
568
|
-
}
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
response.status(200);
|
|
572
|
-
response.setHeader('Content-Type', 'application/json');
|
|
573
|
-
if (action === 'entity') {
|
|
574
|
-
response.send(JSON.stringify(saved[0]));
|
|
575
|
-
} else {
|
|
576
|
-
response.send(saved);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
rest.delete('/', async (request, response: NymphResponse) => {
|
|
582
|
-
try {
|
|
583
|
-
const { action, data: dataConst } = getActionData(request);
|
|
584
|
-
let data = dataConst;
|
|
585
|
-
if (['entity', 'entities', 'uid'].indexOf(action) === -1) {
|
|
586
|
-
httpError(response, 400, 'Bad Request');
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
if (['entity', 'entities'].indexOf(action) !== -1) {
|
|
590
|
-
if (action === 'entity') {
|
|
591
|
-
data = [data];
|
|
592
|
-
}
|
|
593
|
-
const deleted = [];
|
|
594
|
-
let failures = false;
|
|
595
|
-
let hadSuccess = false;
|
|
596
|
-
let invalidRequest = false;
|
|
597
|
-
let notfound = false;
|
|
598
|
-
let lastException = null;
|
|
599
|
-
for (let entData of data) {
|
|
600
|
-
if (entData.guid && entData.guid.length != 24) {
|
|
601
|
-
invalidRequest = true;
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
let EntityClass: EntityConstructor;
|
|
605
|
-
try {
|
|
606
|
-
EntityClass = response.locals.nymph.getEntityClass(entData.class);
|
|
607
|
-
} catch (e: any) {
|
|
608
|
-
invalidRequest = true;
|
|
609
|
-
failures = true;
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
let entity: EntityInterface | null;
|
|
613
|
-
try {
|
|
614
|
-
entity = await response.locals.nymph.getEntity(
|
|
615
|
-
{ class: EntityClass },
|
|
616
|
-
{ type: '&', guid: entData.guid }
|
|
617
|
-
);
|
|
618
|
-
} catch (e: any) {
|
|
619
|
-
lastException = e;
|
|
620
|
-
failures = true;
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
if (!entity) {
|
|
624
|
-
notfound = true;
|
|
625
|
-
failures = true;
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
try {
|
|
629
|
-
if (await entity.$delete()) {
|
|
630
|
-
deleted.push(entData.guid);
|
|
631
|
-
hadSuccess = true;
|
|
632
|
-
} else {
|
|
633
|
-
failures = true;
|
|
634
|
-
}
|
|
635
|
-
} catch (e: any) {
|
|
636
|
-
lastException = e;
|
|
637
|
-
failures = true;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (deleted.length === 0) {
|
|
641
|
-
if (invalidRequest || !failures) {
|
|
642
|
-
httpError(response, 400, 'Bad Request', lastException);
|
|
643
|
-
} else if (notfound) {
|
|
644
|
-
httpError(response, 404, 'Not Found');
|
|
645
|
-
} else {
|
|
646
|
-
httpError(response, 500, 'Internal Server Error', lastException);
|
|
647
|
-
}
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
response.status(200);
|
|
651
|
-
response.setHeader('Content-Type', 'application/json');
|
|
652
|
-
if (action === 'entity') {
|
|
653
|
-
response.send(JSON.stringify(deleted[0]));
|
|
654
|
-
} else {
|
|
655
|
-
response.send(deleted);
|
|
656
|
-
}
|
|
657
|
-
} else {
|
|
658
|
-
if (typeof data !== 'string') {
|
|
659
|
-
httpError(response, 400, 'Bad Request');
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
if (response.locals.nymph.tilmeld) {
|
|
663
|
-
if (
|
|
664
|
-
!response.locals.nymph.tilmeld.checkClientUIDPermissions(
|
|
665
|
-
data,
|
|
666
|
-
TilmeldAccessLevels.FULL_ACCESS
|
|
667
|
-
)
|
|
668
|
-
) {
|
|
669
|
-
httpError(response, 403, 'Forbidden');
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
let result: boolean;
|
|
674
|
-
try {
|
|
675
|
-
result = await response.locals.nymph.deleteUID(data);
|
|
676
|
-
} catch (e: any) {
|
|
677
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
if (!result) {
|
|
681
|
-
httpError(response, 500, 'Internal Server Error');
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
response.status(200);
|
|
685
|
-
response.setHeader('Content-Type', 'application/json');
|
|
686
|
-
response.send(JSON.stringify(result));
|
|
687
|
-
}
|
|
688
|
-
} catch (e: any) {
|
|
689
|
-
httpError(response, 500, 'Internal Server Error', e);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// Unauthenticate after the request.
|
|
695
|
-
rest.use(unauthenticateTilmeld);
|
|
696
|
-
|
|
697
|
-
async function loadEntity(
|
|
698
|
-
entityData: EntityJson | EntityPatch,
|
|
699
|
-
nymph: Nymph,
|
|
700
|
-
patch = false,
|
|
701
|
-
allowConflict = false
|
|
702
|
-
): Promise<EntityInterface> {
|
|
703
|
-
if (entityData.class === 'Entity') {
|
|
704
|
-
// Don't let clients use the `Entity` class, since it has no validity/AC checks.
|
|
705
|
-
throw new InvalidParametersError(
|
|
706
|
-
"Can't use Entity class directly from the front end."
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
let EntityClass = nymph.getEntityClass(entityData.class);
|
|
710
|
-
let entity: EntityInterface | null;
|
|
711
|
-
if (entityData.guid) {
|
|
712
|
-
entity = await nymph.getEntity(
|
|
713
|
-
{ class: EntityClass, source: 'client' },
|
|
714
|
-
{
|
|
715
|
-
type: '&',
|
|
716
|
-
guid: `${entityData['guid']}`,
|
|
717
|
-
}
|
|
718
|
-
);
|
|
719
|
-
if (entity === null) {
|
|
720
|
-
throw new Error(NOT_FOUND_ERROR);
|
|
721
|
-
}
|
|
722
|
-
} else {
|
|
723
|
-
entity = await EntityClass.factory();
|
|
724
|
-
}
|
|
725
|
-
if (patch) {
|
|
726
|
-
entity.$jsonAcceptPatch(entityData as EntityPatch, allowConflict);
|
|
727
|
-
} else {
|
|
728
|
-
entity.$jsonAcceptData(entityData as EntityJson, allowConflict);
|
|
729
|
-
}
|
|
730
|
-
return entity;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Check if an item is a reference, and if it is, convert it to an entity.
|
|
735
|
-
*
|
|
736
|
-
* This function will recurse into deeper arrays and objects.
|
|
737
|
-
*
|
|
738
|
-
* @param item The item to check.
|
|
739
|
-
* @returns The item, converted.
|
|
740
|
-
*/
|
|
741
|
-
function referencesToEntities(item: any, nymph: Nymph): any {
|
|
742
|
-
if (Array.isArray(item)) {
|
|
743
|
-
if (item.length === 3 && item[0] === 'nymph_entity_reference') {
|
|
744
|
-
try {
|
|
745
|
-
const EntityClass = nymph.getEntityClass(item[1]);
|
|
746
|
-
return EntityClass.factoryReference(item as EntityReference);
|
|
747
|
-
} catch (e: any) {
|
|
748
|
-
return item;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
return item.map((entry) => referencesToEntities(entry, nymph));
|
|
752
|
-
} else if (typeof item === 'object' && !(item instanceof Entity)) {
|
|
753
|
-
// Only do this for non-entity objects.
|
|
754
|
-
const newItem: { [k: string]: any } = {};
|
|
755
|
-
for (let curProperty in item) {
|
|
756
|
-
newItem[curProperty] = referencesToEntities(item[curProperty], nymph);
|
|
757
|
-
}
|
|
758
|
-
return newItem;
|
|
759
|
-
}
|
|
760
|
-
return item;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
/**
|
|
764
|
-
* Return the request with an HTTP error response.
|
|
765
|
-
*
|
|
766
|
-
* @param errorCode The HTTP status code.
|
|
767
|
-
* @param message The message to place on the HTTP status header line.
|
|
768
|
-
* @param error An optional exception object to report.
|
|
769
|
-
*/
|
|
770
|
-
function httpError(
|
|
771
|
-
res: NymphResponse,
|
|
772
|
-
errorCode: number,
|
|
773
|
-
message: string,
|
|
774
|
-
error?: Error
|
|
775
|
-
) {
|
|
776
|
-
if (!res.headersSent) {
|
|
777
|
-
res.status(errorCode);
|
|
778
|
-
res.setHeader('Content-Type', 'application/json');
|
|
779
|
-
}
|
|
780
|
-
if (error) {
|
|
781
|
-
res.send({
|
|
782
|
-
textStatus: `${errorCode} ${message}`,
|
|
783
|
-
message: error.message,
|
|
784
|
-
error,
|
|
785
|
-
...(process.env.NODE_ENV !== 'production'
|
|
786
|
-
? { stack: error.stack }
|
|
787
|
-
: {}),
|
|
788
|
-
});
|
|
789
|
-
} else {
|
|
790
|
-
res.send({ textStatus: `${errorCode} ${message}` });
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return rest;
|
|
795
|
-
}
|
|
7
|
+
export default createServer;
|