jinaga-server 3.1.4 → 3.2.0
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/.github/workflows/main.yml +1 -1
- package/.github/workflows/publish.yml +3 -3
- package/dist/http/router.js +73 -121
- package/dist/http/router.js.map +1 -1
- package/dist/jinaga-server.d.ts +2 -1
- package/dist/jinaga-server.js +18 -4
- package/dist/jinaga-server.js.map +1 -1
- package/dist/postgres/postgres-store.d.ts +1 -0
- package/dist/postgres/postgres-store.js +17 -0
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/purge-sql.d.ts +6 -0
- package/dist/postgres/purge-sql.js +103 -0
- package/dist/postgres/purge-sql.js.map +1 -0
- package/dist/postgres/query-description.d.ts +1 -1
- package/dist/postgres/query-description.js +3 -3
- package/dist/postgres/query-description.js.map +1 -1
- package/dist/postgres/specification-result-sql.js +1 -1
- package/dist/postgres/specification-result-sql.js.map +1 -1
- package/dist/postgres/specification-sql.js +2 -2
- package/dist/postgres/specification-sql.js.map +1 -1
- package/integration-test/docker-compose.yaml +0 -1
- package/integration-test/jinaga-test/Dockerfile +1 -1
- package/integration-test/jinaga-test/package-lock.json +147 -86
- package/integration-test/jinaga-test/package.json +3 -3
- package/integration-test/postgres/1-setup.sql +2 -2
- package/integration-test/postgres/Dockerfile +1 -1
- package/package.json +4 -4
- package/replicator/package-lock.json +139 -71
- package/replicator/package.json +2 -2
- package/scripts/migrate_purge.sql +45 -0
- package/scripts/purge.sql +33 -0
- package/setup.sql +30 -2
- package/src/http/router.ts +72 -119
- package/src/jinaga-server.ts +27 -5
- package/src/postgres/postgres-store.ts +18 -0
- package/src/postgres/purge-sql.ts +125 -0
- package/src/postgres/query-description.ts +1 -1
- package/src/postgres/specification-result-sql.ts +3 -3
- package/src/postgres/specification-sql.ts +4 -4
- package/test/authorization/authorizationSpec.ts +1 -1
- package/test/models/blog.ts +92 -0
- package/test/postgres/purgeSqlSpec.ts +218 -0
package/replicator/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
GRANT DELETE ON TABLE public.edge TO appuser;
|
|
2
|
+
GRANT DELETE ON TABLE public.signature TO appuser;
|
|
3
|
+
GRANT DELETE ON TABLE public.ancestor TO appuser;
|
|
4
|
+
GRANT DELETE ON TABLE public.fact TO appuser;
|
|
5
|
+
|
|
6
|
+
-- Configure cascade delete from fact to edge
|
|
7
|
+
ALTER TABLE public.edge
|
|
8
|
+
DROP CONSTRAINT fk_predecessor_fact_id;
|
|
9
|
+
ALTER TABLE public.edge
|
|
10
|
+
ADD CONSTRAINT fk_predecessor_fact_id
|
|
11
|
+
FOREIGN KEY (predecessor_fact_id)
|
|
12
|
+
REFERENCES public.fact (fact_id)
|
|
13
|
+
ON DELETE CASCADE;
|
|
14
|
+
ALTER TABLE public.edge
|
|
15
|
+
DROP CONSTRAINT fk_successor_fact_id;
|
|
16
|
+
ALTER TABLE public.edge
|
|
17
|
+
ADD CONSTRAINT fk_successor_fact_id
|
|
18
|
+
FOREIGN KEY (successor_fact_id)
|
|
19
|
+
REFERENCES public.fact (fact_id)
|
|
20
|
+
ON DELETE CASCADE;
|
|
21
|
+
|
|
22
|
+
-- Configure cascade delete from fact to signature
|
|
23
|
+
ALTER TABLE public.signature
|
|
24
|
+
DROP CONSTRAINT fk_fact_id;
|
|
25
|
+
ALTER TABLE public.signature
|
|
26
|
+
ADD CONSTRAINT fk_fact_id
|
|
27
|
+
FOREIGN KEY (fact_id)
|
|
28
|
+
REFERENCES public.fact (fact_id)
|
|
29
|
+
ON DELETE CASCADE;
|
|
30
|
+
|
|
31
|
+
-- Configure cascade delete from fact to ancestor
|
|
32
|
+
ALTER TABLE public.ancestor
|
|
33
|
+
DROP CONSTRAINT fk_fact_id;
|
|
34
|
+
ALTER TABLE public.ancestor
|
|
35
|
+
ADD CONSTRAINT fk_fact_id
|
|
36
|
+
FOREIGN KEY (fact_id)
|
|
37
|
+
REFERENCES public.fact (fact_id)
|
|
38
|
+
ON DELETE CASCADE;
|
|
39
|
+
ALTER TABLE public.ancestor
|
|
40
|
+
DROP CONSTRAINT fk_ancestor_fact_id;
|
|
41
|
+
ALTER TABLE public.ancestor
|
|
42
|
+
ADD CONSTRAINT fk_ancestor_fact_id
|
|
43
|
+
FOREIGN KEY (ancestor_fact_id)
|
|
44
|
+
REFERENCES public.fact (fact_id)
|
|
45
|
+
ON DELETE CASCADE;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
ROLLBACK TRANSACTION;
|
|
2
|
+
|
|
3
|
+
BEGIN TRANSACTION;
|
|
4
|
+
|
|
5
|
+
WITH candidates AS (
|
|
6
|
+
SELECT
|
|
7
|
+
f1.fact_id as purge_root,
|
|
8
|
+
f2.fact_id as trigger1
|
|
9
|
+
FROM public.fact f1
|
|
10
|
+
JOIN public.edge e1
|
|
11
|
+
ON e1.predecessor_fact_id = f1.fact_id
|
|
12
|
+
AND e1.role_id = 26
|
|
13
|
+
JOIN public.fact f2
|
|
14
|
+
ON f2.fact_id = e1.successor_fact_id
|
|
15
|
+
WHERE f1.fact_type_id = 2
|
|
16
|
+
), targets AS (
|
|
17
|
+
SELECT a.fact_id
|
|
18
|
+
FROM public.ancestor a
|
|
19
|
+
JOIN candidates c ON c.purge_root = a.ancestor_fact_id
|
|
20
|
+
WHERE NOT EXISTS (
|
|
21
|
+
SELECT 1
|
|
22
|
+
FROM candidates c2
|
|
23
|
+
WHERE a.fact_id = c.trigger1
|
|
24
|
+
)
|
|
25
|
+
), facts AS (
|
|
26
|
+
DELETE
|
|
27
|
+
FROM public.fact f
|
|
28
|
+
USING targets t WHERE t.fact_id = f.fact_id
|
|
29
|
+
RETURNING f.fact_id
|
|
30
|
+
)
|
|
31
|
+
SELECT fact_id FROM facts;
|
|
32
|
+
|
|
33
|
+
ROLLBACK TRANSACTION;
|
package/setup.sql
CHANGED
|
@@ -56,7 +56,7 @@ IF (SELECT to_regclass('public.fact_type')) IS NULL THEN
|
|
|
56
56
|
|
|
57
57
|
CREATE TABLE fact_type (
|
|
58
58
|
fact_type_id serial PRIMARY KEY,
|
|
59
|
-
name character varying(
|
|
59
|
+
name character varying(200) NOT NULL
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
|
|
@@ -78,7 +78,7 @@ IF (SELECT to_regclass('public.role')) IS NULL THEN
|
|
|
78
78
|
CONSTRAINT fk_defining_fact_type_id
|
|
79
79
|
FOREIGN KEY (defining_fact_type_id)
|
|
80
80
|
REFERENCES fact_type (fact_type_id),
|
|
81
|
-
name character varying(
|
|
81
|
+
name character varying(200) NOT NULL
|
|
82
82
|
);
|
|
83
83
|
|
|
84
84
|
|
|
@@ -236,6 +236,34 @@ IF (SELECT to_regclass('public.user') IS NULL) THEN
|
|
|
236
236
|
|
|
237
237
|
END IF;
|
|
238
238
|
|
|
239
|
+
--
|
|
240
|
+
-- If the fact_type.name column is less than 200 characters, then increase it.
|
|
241
|
+
--
|
|
242
|
+
|
|
243
|
+
IF (SELECT character_maximum_length
|
|
244
|
+
FROM information_schema.columns
|
|
245
|
+
WHERE table_name = 'fact_type'
|
|
246
|
+
AND column_name = 'name') < 200 THEN
|
|
247
|
+
|
|
248
|
+
ALTER TABLE fact_type
|
|
249
|
+
ALTER COLUMN name TYPE character varying(200);
|
|
250
|
+
|
|
251
|
+
END IF;
|
|
252
|
+
|
|
253
|
+
--
|
|
254
|
+
-- If the role.name column is less than 200 characters, then increase it.
|
|
255
|
+
--
|
|
256
|
+
|
|
257
|
+
IF (SELECT character_maximum_length
|
|
258
|
+
FROM information_schema.columns
|
|
259
|
+
WHERE table_name = 'role'
|
|
260
|
+
AND column_name = 'name') < 200 THEN
|
|
261
|
+
|
|
262
|
+
ALTER TABLE role
|
|
263
|
+
ALTER COLUMN name TYPE character varying(200);
|
|
264
|
+
|
|
265
|
+
END IF;
|
|
266
|
+
|
|
239
267
|
IF (SELECT to_regclass('legacy.fact') IS NOT NULL) THEN
|
|
240
268
|
|
|
241
269
|
INSERT INTO public."user"
|
package/src/http/router.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
GraphDeserializer,
|
|
15
15
|
GraphSerializer,
|
|
16
16
|
GraphSource,
|
|
17
|
+
Invalid,
|
|
17
18
|
LoadMessage,
|
|
18
19
|
LoadResponse,
|
|
19
20
|
ProfileMessage,
|
|
@@ -72,17 +73,7 @@ function getOrStream<U>(
|
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
})
|
|
75
|
-
.catch(error =>
|
|
76
|
-
if (error instanceof Forbidden) {
|
|
77
|
-
res.type("text");
|
|
78
|
-
res.status(403).send(error.message);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
Trace.error(error);
|
|
82
|
-
res.status(500).send(error.message);
|
|
83
|
-
}
|
|
84
|
-
next();
|
|
85
|
-
});
|
|
76
|
+
.catch(error => handleError(error, res, next));
|
|
86
77
|
}
|
|
87
78
|
else {
|
|
88
79
|
getMethod(user, req.params, req.query)
|
|
@@ -97,17 +88,7 @@ function getOrStream<U>(
|
|
|
97
88
|
next();
|
|
98
89
|
}
|
|
99
90
|
})
|
|
100
|
-
.catch(error =>
|
|
101
|
-
if (error instanceof Forbidden) {
|
|
102
|
-
res.type("text");
|
|
103
|
-
res.status(403).send(error.message);
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
Trace.error(error);
|
|
107
|
-
res.status(500).send(error.message);
|
|
108
|
-
}
|
|
109
|
-
next();
|
|
110
|
-
});
|
|
91
|
+
.catch(error => handleError(error, res, next));
|
|
111
92
|
}
|
|
112
93
|
};
|
|
113
94
|
}
|
|
@@ -125,11 +106,7 @@ function getAuthenticate<U>(method: ((req: RequestUser, params?: { [key: string]
|
|
|
125
106
|
res.send(JSON.stringify(response));
|
|
126
107
|
next();
|
|
127
108
|
})
|
|
128
|
-
.catch(error =>
|
|
129
|
-
Trace.error(error);
|
|
130
|
-
res.sendStatus(500);
|
|
131
|
-
next();
|
|
132
|
-
});
|
|
109
|
+
.catch(error => handleError(error, res, next));
|
|
133
110
|
}
|
|
134
111
|
};
|
|
135
112
|
}
|
|
@@ -156,19 +133,7 @@ function post<T, U>(
|
|
|
156
133
|
next();
|
|
157
134
|
}
|
|
158
135
|
})
|
|
159
|
-
|
|
160
|
-
if (error instanceof Forbidden) {
|
|
161
|
-
Trace.warn(error.message);
|
|
162
|
-
res.type("text");
|
|
163
|
-
res.status(403).send(error.message);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
Trace.error(error);
|
|
167
|
-
res.type("text");
|
|
168
|
-
res.status(500).send(error.message);
|
|
169
|
-
}
|
|
170
|
-
next();
|
|
171
|
-
});
|
|
136
|
+
.catch(error => handleError(error, res, next));
|
|
172
137
|
};
|
|
173
138
|
}
|
|
174
139
|
|
|
@@ -187,19 +152,7 @@ function postString<U>(method: (user: RequestUser, message: string) => Promise<U
|
|
|
187
152
|
res.send(JSON.stringify(response, null, 2));
|
|
188
153
|
next();
|
|
189
154
|
})
|
|
190
|
-
.catch(error =>
|
|
191
|
-
if (error instanceof Forbidden) {
|
|
192
|
-
Trace.warn(error.message);
|
|
193
|
-
res.type("text");
|
|
194
|
-
res.status(403).send(error.message);
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
Trace.error(error);
|
|
198
|
-
res.type("text");
|
|
199
|
-
res.status(400).send(error.message);
|
|
200
|
-
}
|
|
201
|
-
next();
|
|
202
|
-
});
|
|
155
|
+
.catch(error => handleError(error, res, next));
|
|
203
156
|
}
|
|
204
157
|
};
|
|
205
158
|
}
|
|
@@ -216,19 +169,7 @@ function postCreate<T>(
|
|
|
216
169
|
res.sendStatus(201);
|
|
217
170
|
next();
|
|
218
171
|
})
|
|
219
|
-
.catch(error =>
|
|
220
|
-
if (error instanceof Forbidden) {
|
|
221
|
-
Trace.warn(error.message);
|
|
222
|
-
res.type("text");
|
|
223
|
-
res.status(403).send(error.message);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
Trace.error(error);
|
|
227
|
-
res.type("text");
|
|
228
|
-
res.status(500).send(error.message);
|
|
229
|
-
}
|
|
230
|
-
next();
|
|
231
|
-
});
|
|
172
|
+
.catch(error => handleError(error, res, next));
|
|
232
173
|
};
|
|
233
174
|
}
|
|
234
175
|
|
|
@@ -246,19 +187,7 @@ function postStringCreate(method: (user: RequestUser, message: string) => Promis
|
|
|
246
187
|
res.sendStatus(201);
|
|
247
188
|
next();
|
|
248
189
|
})
|
|
249
|
-
.catch(error =>
|
|
250
|
-
if (error instanceof Forbidden) {
|
|
251
|
-
Trace.warn(error.message);
|
|
252
|
-
res.type("text");
|
|
253
|
-
res.status(403).send(error.message);
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
Trace.error(error);
|
|
257
|
-
res.type("text");
|
|
258
|
-
res.status(400).send(error.message);
|
|
259
|
-
}
|
|
260
|
-
next();
|
|
261
|
-
});
|
|
190
|
+
.catch(error => handleError(error, res, next));
|
|
262
191
|
}
|
|
263
192
|
};
|
|
264
193
|
}
|
|
@@ -335,25 +264,26 @@ export class HttpRouter {
|
|
|
335
264
|
private allowedOrigin: string | string[] | ((origin: string, callback: (err: Error | null, allow?: boolean) => void) => void)
|
|
336
265
|
) {
|
|
337
266
|
const router = Router();
|
|
338
|
-
|
|
339
|
-
router.
|
|
267
|
+
const applyAllowOrigin = this.applyAllowOrigin.bind(this);
|
|
268
|
+
router.get('/login', applyAllowOrigin, getAuthenticate(user => this.login(user)));
|
|
269
|
+
router.post('/load', applyAllowOrigin, post(
|
|
340
270
|
parseLoadMessage,
|
|
341
271
|
(user, loadMessage) => this.load(user, loadMessage),
|
|
342
272
|
outputGraph
|
|
343
273
|
));
|
|
344
|
-
router.post('/save', postCreate(
|
|
274
|
+
router.post('/save', applyAllowOrigin, postCreate(
|
|
345
275
|
inputSaveMessage,
|
|
346
276
|
(user, graphSource) => this.save(user, graphSource)
|
|
347
277
|
));
|
|
348
278
|
|
|
349
|
-
router.post('/read', postString((user, input: string) => this.read(user, input)));
|
|
350
|
-
router.post('/write', postStringCreate((user, input: string) => this.write(user, input)));
|
|
351
|
-
router.post('/feeds', post(
|
|
279
|
+
router.post('/read', applyAllowOrigin, postString((user, input: string) => this.read(user, input)));
|
|
280
|
+
router.post('/write', applyAllowOrigin, postStringCreate((user, input: string) => this.write(user, input)));
|
|
281
|
+
router.post('/feeds', applyAllowOrigin, post(
|
|
352
282
|
parseString,
|
|
353
283
|
(user, input: string, accepts) => this.feeds(user, input),
|
|
354
284
|
outputFeeds
|
|
355
285
|
));
|
|
356
|
-
router.get('/feeds/:hash', getOrStream<FeedResponse>(
|
|
286
|
+
router.get('/feeds/:hash', applyAllowOrigin, getOrStream<FeedResponse>(
|
|
357
287
|
(user, params, query) => this.feed(user, params, query),
|
|
358
288
|
(user, params, query) => this.streamFeed(user, params, query)));
|
|
359
289
|
|
|
@@ -420,6 +350,11 @@ export class HttpRouter {
|
|
|
420
350
|
parser.expectEnd();
|
|
421
351
|
const start = this.selectStart(specification, declaration);
|
|
422
352
|
|
|
353
|
+
var failures: string[] = this.factManager.testSpecificationForCompliance(specification);
|
|
354
|
+
if (failures.length > 0) {
|
|
355
|
+
throw new Invalid(failures.join("\n"));
|
|
356
|
+
}
|
|
357
|
+
|
|
423
358
|
const userIdentity = serializeUserIdentity(user);
|
|
424
359
|
const results = await this.authorization.read(userIdentity, start, specification);
|
|
425
360
|
return extractResults(results);
|
|
@@ -435,7 +370,7 @@ export class HttpRouter {
|
|
|
435
370
|
const factRecords: FactRecord[] = [];
|
|
436
371
|
for (const value of declaration) {
|
|
437
372
|
if (!value.declared.fact) {
|
|
438
|
-
throw new
|
|
373
|
+
throw new Invalid("References are not allowed while saving.");
|
|
439
374
|
}
|
|
440
375
|
factRecords.push(value.declared.fact);
|
|
441
376
|
}
|
|
@@ -458,14 +393,19 @@ export class HttpRouter {
|
|
|
458
393
|
|
|
459
394
|
// Verify that the number of start facts equals the number of inputs
|
|
460
395
|
if (start.length !== specification.given.length) {
|
|
461
|
-
throw new
|
|
396
|
+
throw new Invalid(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
|
|
462
397
|
}
|
|
463
398
|
// Verify that the input type matches the start fact type
|
|
464
399
|
for (let i = 0; i < start.length; i++) {
|
|
465
400
|
if (start[i].type !== specification.given[i].type) {
|
|
466
|
-
throw new
|
|
401
|
+
throw new Invalid(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
|
|
467
402
|
}
|
|
468
403
|
}
|
|
404
|
+
// Verify that the specification is compliant with purge conditions
|
|
405
|
+
var failures: string[] = this.factManager.testSpecificationForCompliance(specification);
|
|
406
|
+
if (failures.length > 0) {
|
|
407
|
+
throw new Invalid(failures.join("\n"));
|
|
408
|
+
}
|
|
469
409
|
|
|
470
410
|
const namedStart = specification.given.reduce((map, label, index) => ({
|
|
471
411
|
...map,
|
|
@@ -596,7 +536,7 @@ export class HttpRouter {
|
|
|
596
536
|
return specification.given.map(input => {
|
|
597
537
|
const declaredFact = declaration.find(d => d.name === input.name);
|
|
598
538
|
if (!declaredFact) {
|
|
599
|
-
throw new
|
|
539
|
+
throw new Invalid(`No fact named ${input.name} was declared`);
|
|
600
540
|
}
|
|
601
541
|
return declaredFact.declared.reference;
|
|
602
542
|
});
|
|
@@ -604,7 +544,7 @@ export class HttpRouter {
|
|
|
604
544
|
|
|
605
545
|
private setOptions(router: Router, path: string): OptionsConfiguration {
|
|
606
546
|
const addOptions = (allowedMethods: string[], allowedHeaders: string[], allowedTypes: string[]) => {
|
|
607
|
-
router.options(path, this.applyAllowOrigin((req, res) => {
|
|
547
|
+
router.options(path, this.applyAllowOrigin.bind(this), (req: Request, res: Response) => {
|
|
608
548
|
res.set('Allow', allowedMethods.join(', '));
|
|
609
549
|
res.set('Access-Control-Allow-Methods', allowedMethods.join(', '));
|
|
610
550
|
res.set('Access-Control-Allow-Headers', allowedHeaders.join(', '));
|
|
@@ -612,7 +552,7 @@ export class HttpRouter {
|
|
|
612
552
|
res.set('Accept-Post', allowedTypes.join(', '));
|
|
613
553
|
}
|
|
614
554
|
res.status(204).send();
|
|
615
|
-
})
|
|
555
|
+
});
|
|
616
556
|
}
|
|
617
557
|
|
|
618
558
|
return {
|
|
@@ -642,41 +582,39 @@ export class HttpRouter {
|
|
|
642
582
|
};
|
|
643
583
|
}
|
|
644
584
|
|
|
645
|
-
private applyAllowOrigin(
|
|
585
|
+
private applyAllowOrigin(req: Request, res: Response, next: NextFunction) {
|
|
646
586
|
// Specify the allowed origins.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
587
|
+
let requestOrigin = req.get('Origin');
|
|
588
|
+
|
|
589
|
+
if (typeof this.allowedOrigin === 'string') {
|
|
590
|
+
// If allowedOrigin is a string, use it as the value of the Access-Control-Allow-Origin header
|
|
591
|
+
res.set('Access-Control-Allow-Origin', this.allowedOrigin);
|
|
592
|
+
next();
|
|
593
|
+
} else if (Array.isArray(this.allowedOrigin)) {
|
|
594
|
+
// If allowedOrigin is an array, check if the request's origin is in the array
|
|
595
|
+
if (requestOrigin && this.allowedOrigin.includes(requestOrigin)) {
|
|
596
|
+
res.set('Access-Control-Allow-Origin', requestOrigin);
|
|
597
|
+
}
|
|
598
|
+
next();
|
|
599
|
+
} else if (typeof this.allowedOrigin === 'function' && requestOrigin) {
|
|
600
|
+
// If allowedOrigin is a function, call it with the request's origin
|
|
601
|
+
this.allowedOrigin(requestOrigin, (err, allow) => {
|
|
602
|
+
if (err) {
|
|
603
|
+
next(err);
|
|
604
|
+
} else if (allow) {
|
|
657
605
|
res.set('Access-Control-Allow-Origin', requestOrigin);
|
|
658
606
|
}
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
next(err);
|
|
665
|
-
} else if (allow) {
|
|
666
|
-
res.set('Access-Control-Allow-Origin', requestOrigin);
|
|
667
|
-
}
|
|
668
|
-
sendResponse(req, res);
|
|
669
|
-
});
|
|
670
|
-
} else {
|
|
671
|
-
sendResponse(req, res);
|
|
672
|
-
}
|
|
673
|
-
};
|
|
607
|
+
next();
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
next();
|
|
611
|
+
}
|
|
674
612
|
}
|
|
675
613
|
}
|
|
676
614
|
|
|
677
615
|
function parseString(input: any): string {
|
|
678
616
|
if (typeof input !== 'string') {
|
|
679
|
-
throw new
|
|
617
|
+
throw new Invalid("Expected a string. Check the content type of the request.");
|
|
680
618
|
}
|
|
681
619
|
return input;
|
|
682
620
|
}
|
|
@@ -697,6 +635,21 @@ function extractResults(obj: any): any {
|
|
|
697
635
|
}
|
|
698
636
|
}
|
|
699
637
|
|
|
638
|
+
function handleError(error: any, res: Response, next: NextFunction) {
|
|
639
|
+
if (error instanceof Forbidden) {
|
|
640
|
+
res.type("text");
|
|
641
|
+
res.status(403).send(error.message);
|
|
642
|
+
} else if (error instanceof Invalid) {
|
|
643
|
+
res.type("text");
|
|
644
|
+
res.status(400).send(error.message);
|
|
645
|
+
} else {
|
|
646
|
+
Trace.error(error);
|
|
647
|
+
res.type("text");
|
|
648
|
+
res.status(500).send(error.message);
|
|
649
|
+
}
|
|
650
|
+
next();
|
|
651
|
+
}
|
|
652
|
+
|
|
700
653
|
interface OptionsConfiguration {
|
|
701
654
|
intendedForGet(): ResponseConfiguration;
|
|
702
655
|
intendedForPost(...contentTypes: string[]): ResponseConfiguration;
|
package/src/jinaga-server.ts
CHANGED
|
@@ -14,10 +14,13 @@ import {
|
|
|
14
14
|
ObservableSource,
|
|
15
15
|
ObservableSourceImpl,
|
|
16
16
|
PassThroughFork,
|
|
17
|
+
PurgeConditions,
|
|
18
|
+
Specification,
|
|
17
19
|
Storage,
|
|
18
20
|
SyncStatusNotifier,
|
|
19
21
|
Trace,
|
|
20
|
-
UserIdentity
|
|
22
|
+
UserIdentity,
|
|
23
|
+
validatePurgeSpecification
|
|
21
24
|
} from "jinaga";
|
|
22
25
|
import { Pool } from "pg";
|
|
23
26
|
|
|
@@ -38,6 +41,7 @@ export type JinagaServerConfig = {
|
|
|
38
41
|
model?: Model,
|
|
39
42
|
authorization?: (a: AuthorizationRules) => AuthorizationRules,
|
|
40
43
|
distribution?: (d: DistributionRules) => DistributionRules,
|
|
44
|
+
purgeConditions?: (p: PurgeConditions) => PurgeConditions,
|
|
41
45
|
origin?: string | string[] | ((origin: string, callback: (err: Error | null, allow?: boolean) => void) => void),
|
|
42
46
|
};
|
|
43
47
|
|
|
@@ -66,7 +70,8 @@ export class JinagaServer {
|
|
|
66
70
|
const feedCache = new FeedCache();
|
|
67
71
|
const authentication = createAuthentication(store, keystore, authorizationRules);
|
|
68
72
|
const network = new NetworkNoOp();
|
|
69
|
-
const
|
|
73
|
+
const purgeConditions = createPurgeConditions(config);
|
|
74
|
+
const factManager = new FactManager(fork, source, store, network, purgeConditions);
|
|
70
75
|
const authorization = createAuthorization(authorizationRules, distributionRules, factManager, store, keystore);
|
|
71
76
|
const router = new HttpRouter(factManager, authorization, feedCache, config.origin || '*');
|
|
72
77
|
const j: Jinaga = new Jinaga(authentication, factManager, syncStatusNotifier);
|
|
@@ -80,7 +85,7 @@ export class JinagaServer {
|
|
|
80
85
|
handler: router.handler,
|
|
81
86
|
j,
|
|
82
87
|
withSession: (req, callback) => {
|
|
83
|
-
return withSession(store, keystore, authorizationRules, req, callback);
|
|
88
|
+
return withSession(store, keystore, authorizationRules, purgeConditions, req, callback);
|
|
84
89
|
},
|
|
85
90
|
close
|
|
86
91
|
}
|
|
@@ -176,7 +181,7 @@ export function tracePool(postgresPool: Pool) {
|
|
|
176
181
|
}
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
async function withSession(store: Storage, keystore: Keystore | null, authorizationRules: AuthorizationRules | null, req: Request, callback: ((j: Jinaga) => Promise<void>)) {
|
|
184
|
+
async function withSession(store: Storage, keystore: Keystore | null, authorizationRules: AuthorizationRules | null, purgeConditions: Specification[], req: Request, callback: ((j: Jinaga) => Promise<void>)) {
|
|
180
185
|
const user = <RequestUser>req.user;
|
|
181
186
|
const userIdentity: UserIdentity = {
|
|
182
187
|
provider: user.provider,
|
|
@@ -187,7 +192,7 @@ async function withSession(store: Storage, keystore: Keystore | null, authorizat
|
|
|
187
192
|
const fork = new PassThroughFork(store);
|
|
188
193
|
const observableSource = new ObservableSource(store);
|
|
189
194
|
const network = new NetworkNoOp();
|
|
190
|
-
const factManager = new FactManager(fork, observableSource, store, network);
|
|
195
|
+
const factManager = new FactManager(fork, observableSource, store, network, purgeConditions);
|
|
191
196
|
const j = new Jinaga(authentication, factManager, syncStatusNotifier);
|
|
192
197
|
await callback(j);
|
|
193
198
|
}
|
|
@@ -205,3 +210,20 @@ function validateSchema(schema: string | undefined): string {
|
|
|
205
210
|
|
|
206
211
|
return schema;
|
|
207
212
|
}
|
|
213
|
+
|
|
214
|
+
function createPurgeConditions(
|
|
215
|
+
config: JinagaServerConfig
|
|
216
|
+
): Specification[] {
|
|
217
|
+
if (config.purgeConditions) {
|
|
218
|
+
var specifications = config.purgeConditions(new PurgeConditions([])).specifications;
|
|
219
|
+
var validationFailures: string[] = specifications.map(specification =>
|
|
220
|
+
validatePurgeSpecification(specification)).flat();
|
|
221
|
+
if (validationFailures.length > 0) {
|
|
222
|
+
throw new Error(validationFailures.join("\n"));
|
|
223
|
+
}
|
|
224
|
+
return specifications;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from "./maps";
|
|
45
45
|
import { ResultSetFact, ResultSetRow, ResultSetTree, resultSqlFromSpecification, SqlQueryTree } from "./specification-result-sql";
|
|
46
46
|
import { sqlFromFeed } from "./specification-sql";
|
|
47
|
+
import { purgeSqlFromSpecification } from "./purge-sql";
|
|
47
48
|
|
|
48
49
|
interface FactTypeResult {
|
|
49
50
|
rows: {
|
|
@@ -433,6 +434,23 @@ export class PostgresStore implements Storage {
|
|
|
433
434
|
return factTypes;
|
|
434
435
|
}
|
|
435
436
|
|
|
437
|
+
async purge(purgeConditions: Specification[]): Promise<void> {
|
|
438
|
+
for (const specification of purgeConditions) {
|
|
439
|
+
const factTypes = await this.loadFactTypesFromSpecification(specification);
|
|
440
|
+
const roleMap = await this.loadRolesFromSpecification(specification, factTypes);
|
|
441
|
+
|
|
442
|
+
const purgeCommand = purgeSqlFromSpecification(specification, factTypes, roleMap, this.schema);
|
|
443
|
+
if (!purgeCommand) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const { sql, parameters } = purgeCommand;
|
|
448
|
+
await this.connectionFactory.with(async (connection) => {
|
|
449
|
+
await connection.query(sql, parameters);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
436
454
|
loadBookmark(feed: string): Promise<string> {
|
|
437
455
|
throw new Error("Method not implemented.");
|
|
438
456
|
}
|