jinaga-server 3.1.5 → 3.3.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 +37 -84
- 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 +2 -0
- package/dist/postgres/postgres-store.js +34 -0
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/purge-sql.d.ts +7 -0
- package/dist/postgres/purge-sql.js +151 -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 +2 -0
- package/integration-test/jinaga-test/orderModel.js +104 -0
- package/integration-test/jinaga-test/package-lock.json +147 -86
- package/integration-test/jinaga-test/package.json +3 -3
- package/integration-test/jinaga-test/purge.test.js +224 -0
- package/integration-test/postgres/1-setup.sql +8 -3
- package/integration-test/postgres/2-grant.sh +4 -4
- package/integration-test/postgres/Dockerfile +1 -1
- package/package.json +5 -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.sh +4 -4
- package/setup.sql +59 -3
- package/src/http/router.ts +38 -84
- package/src/jinaga-server.ts +27 -5
- package/src/postgres/postgres-store.ts +37 -0
- package/src/postgres/purge-sql.ts +176 -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/purgeDescendantsSqlSpec.ts +41 -0
- package/test/postgres/purgeSqlSpec.ts +246 -0
|
@@ -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.sh
CHANGED
|
@@ -23,12 +23,12 @@ psql -v ON_ERROR_STOP=1 $JINAGA_ADMIN_CONNECTION_STRING <<-EOSQL
|
|
|
23
23
|
GRANT SELECT, USAGE ON SEQUENCE fact_type_fact_type_id_seq TO $JINAGA_POSTGRES_APP_USER;
|
|
24
24
|
GRANT SELECT,INSERT ON TABLE public.role TO $JINAGA_POSTGRES_APP_USER;
|
|
25
25
|
GRANT SELECT, USAGE ON SEQUENCE role_role_id_seq TO $JINAGA_POSTGRES_APP_USER;
|
|
26
|
-
GRANT SELECT,INSERT ON TABLE public.fact TO $JINAGA_POSTGRES_APP_USER;
|
|
26
|
+
GRANT SELECT,INSERT,DELETE ON TABLE public.fact TO $JINAGA_POSTGRES_APP_USER;
|
|
27
27
|
GRANT SELECT, USAGE ON SEQUENCE fact_fact_id_seq TO $JINAGA_POSTGRES_APP_USER;
|
|
28
|
-
GRANT SELECT,INSERT ON TABLE public.edge TO $JINAGA_POSTGRES_APP_USER;
|
|
29
|
-
GRANT SELECT,INSERT ON TABLE public.ancestor TO $JINAGA_POSTGRES_APP_USER;
|
|
28
|
+
GRANT SELECT,INSERT,DELETE ON TABLE public.edge TO $JINAGA_POSTGRES_APP_USER;
|
|
29
|
+
GRANT SELECT,INSERT,DELETE ON TABLE public.ancestor TO $JINAGA_POSTGRES_APP_USER;
|
|
30
30
|
GRANT SELECT,INSERT ON TABLE public.public_key TO $JINAGA_POSTGRES_APP_USER;
|
|
31
31
|
GRANT SELECT, USAGE ON SEQUENCE public_key_public_key_id_seq TO $JINAGA_POSTGRES_APP_USER;
|
|
32
|
-
GRANT SELECT,INSERT ON TABLE public.signature TO $JINAGA_POSTGRES_APP_USER;
|
|
32
|
+
GRANT SELECT,INSERT,DELETE ON TABLE public.signature TO $JINAGA_POSTGRES_APP_USER;
|
|
33
33
|
GRANT SELECT,INSERT ON TABLE public.user TO $JINAGA_POSTGRES_APP_USER;
|
|
34
34
|
EOSQL
|
package/setup.sql
CHANGED
|
@@ -127,11 +127,13 @@ IF (SELECT to_regclass('public.edge') IS NULL) THEN
|
|
|
127
127
|
successor_fact_id integer NOT NULL,
|
|
128
128
|
CONSTRAINT fk_successor_fact_id
|
|
129
129
|
FOREIGN KEY (successor_fact_id)
|
|
130
|
-
REFERENCES fact (fact_id)
|
|
130
|
+
REFERENCES fact (fact_id)
|
|
131
|
+
ON DELETE CASCADE,
|
|
131
132
|
predecessor_fact_id integer NOT NULL,
|
|
132
133
|
CONSTRAINT fk_predecessor_fact_id
|
|
133
134
|
FOREIGN KEY (predecessor_fact_id)
|
|
134
135
|
REFERENCES fact (fact_id)
|
|
136
|
+
ON DELETE CASCADE
|
|
135
137
|
);
|
|
136
138
|
|
|
137
139
|
|
|
@@ -156,11 +158,13 @@ IF (SELECT to_regclass('public.ancestor') IS NULL) THEN
|
|
|
156
158
|
fact_id integer NOT NULL,
|
|
157
159
|
CONSTRAINT fk_fact_id
|
|
158
160
|
FOREIGN KEY (fact_id)
|
|
159
|
-
REFERENCES fact (fact_id)
|
|
161
|
+
REFERENCES fact (fact_id)
|
|
162
|
+
ON DELETE CASCADE,
|
|
160
163
|
ancestor_fact_id integer NOT NULL,
|
|
161
164
|
CONSTRAINT fk_ancestor_fact_id
|
|
162
165
|
FOREIGN KEY (ancestor_fact_id)
|
|
163
166
|
REFERENCES fact (fact_id)
|
|
167
|
+
ON DELETE CASCADE
|
|
164
168
|
);
|
|
165
169
|
|
|
166
170
|
|
|
@@ -198,7 +202,8 @@ IF (SELECT to_regclass('public.signature') IS NULL) THEN
|
|
|
198
202
|
fact_id integer NOT NULL,
|
|
199
203
|
CONSTRAINT fk_fact_id
|
|
200
204
|
FOREIGN KEY (fact_id)
|
|
201
|
-
REFERENCES fact (fact_id)
|
|
205
|
+
REFERENCES fact (fact_id)
|
|
206
|
+
ON DELETE CASCADE,
|
|
202
207
|
public_key_id integer NOT NULL,
|
|
203
208
|
CONSTRAINT fk_public_key_id
|
|
204
209
|
FOREIGN KEY (public_key_id)
|
|
@@ -264,6 +269,57 @@ IF (SELECT character_maximum_length
|
|
|
264
269
|
|
|
265
270
|
END IF;
|
|
266
271
|
|
|
272
|
+
--
|
|
273
|
+
-- If the foreign keys on the edge table are not set to cascade delete, then set them.
|
|
274
|
+
--
|
|
275
|
+
|
|
276
|
+
IF (SELECT confdeltype
|
|
277
|
+
FROM pg_constraint
|
|
278
|
+
WHERE conname = 'fk_predecessor_fact_id') != 'c' THEN
|
|
279
|
+
|
|
280
|
+
-- Configure cascade delete from fact to edge
|
|
281
|
+
ALTER TABLE public.edge
|
|
282
|
+
DROP CONSTRAINT fk_predecessor_fact_id;
|
|
283
|
+
ALTER TABLE public.edge
|
|
284
|
+
ADD CONSTRAINT fk_predecessor_fact_id
|
|
285
|
+
FOREIGN KEY (predecessor_fact_id)
|
|
286
|
+
REFERENCES public.fact (fact_id)
|
|
287
|
+
ON DELETE CASCADE;
|
|
288
|
+
ALTER TABLE public.edge
|
|
289
|
+
DROP CONSTRAINT fk_successor_fact_id;
|
|
290
|
+
ALTER TABLE public.edge
|
|
291
|
+
ADD CONSTRAINT fk_successor_fact_id
|
|
292
|
+
FOREIGN KEY (successor_fact_id)
|
|
293
|
+
REFERENCES public.fact (fact_id)
|
|
294
|
+
ON DELETE CASCADE;
|
|
295
|
+
|
|
296
|
+
-- Configure cascade delete from fact to signature
|
|
297
|
+
ALTER TABLE public.signature
|
|
298
|
+
DROP CONSTRAINT fk_fact_id;
|
|
299
|
+
ALTER TABLE public.signature
|
|
300
|
+
ADD CONSTRAINT fk_fact_id
|
|
301
|
+
FOREIGN KEY (fact_id)
|
|
302
|
+
REFERENCES public.fact (fact_id)
|
|
303
|
+
ON DELETE CASCADE;
|
|
304
|
+
|
|
305
|
+
-- Configure cascade delete from fact to ancestor
|
|
306
|
+
ALTER TABLE public.ancestor
|
|
307
|
+
DROP CONSTRAINT fk_fact_id;
|
|
308
|
+
ALTER TABLE public.ancestor
|
|
309
|
+
ADD CONSTRAINT fk_fact_id
|
|
310
|
+
FOREIGN KEY (fact_id)
|
|
311
|
+
REFERENCES public.fact (fact_id)
|
|
312
|
+
ON DELETE CASCADE;
|
|
313
|
+
ALTER TABLE public.ancestor
|
|
314
|
+
DROP CONSTRAINT fk_ancestor_fact_id;
|
|
315
|
+
ALTER TABLE public.ancestor
|
|
316
|
+
ADD CONSTRAINT fk_ancestor_fact_id
|
|
317
|
+
FOREIGN KEY (ancestor_fact_id)
|
|
318
|
+
REFERENCES public.fact (fact_id)
|
|
319
|
+
ON DELETE CASCADE;
|
|
320
|
+
|
|
321
|
+
END IF;
|
|
322
|
+
|
|
267
323
|
IF (SELECT to_regclass('legacy.fact') IS NOT NULL) THEN
|
|
268
324
|
|
|
269
325
|
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
|
}
|
|
@@ -421,6 +350,11 @@ export class HttpRouter {
|
|
|
421
350
|
parser.expectEnd();
|
|
422
351
|
const start = this.selectStart(specification, declaration);
|
|
423
352
|
|
|
353
|
+
var failures: string[] = this.factManager.testSpecificationForCompliance(specification);
|
|
354
|
+
if (failures.length > 0) {
|
|
355
|
+
throw new Invalid(failures.join("\n"));
|
|
356
|
+
}
|
|
357
|
+
|
|
424
358
|
const userIdentity = serializeUserIdentity(user);
|
|
425
359
|
const results = await this.authorization.read(userIdentity, start, specification);
|
|
426
360
|
return extractResults(results);
|
|
@@ -436,7 +370,7 @@ export class HttpRouter {
|
|
|
436
370
|
const factRecords: FactRecord[] = [];
|
|
437
371
|
for (const value of declaration) {
|
|
438
372
|
if (!value.declared.fact) {
|
|
439
|
-
throw new
|
|
373
|
+
throw new Invalid("References are not allowed while saving.");
|
|
440
374
|
}
|
|
441
375
|
factRecords.push(value.declared.fact);
|
|
442
376
|
}
|
|
@@ -459,14 +393,19 @@ export class HttpRouter {
|
|
|
459
393
|
|
|
460
394
|
// Verify that the number of start facts equals the number of inputs
|
|
461
395
|
if (start.length !== specification.given.length) {
|
|
462
|
-
throw new
|
|
396
|
+
throw new Invalid(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
|
|
463
397
|
}
|
|
464
398
|
// Verify that the input type matches the start fact type
|
|
465
399
|
for (let i = 0; i < start.length; i++) {
|
|
466
400
|
if (start[i].type !== specification.given[i].type) {
|
|
467
|
-
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})`);
|
|
468
402
|
}
|
|
469
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
|
+
}
|
|
470
409
|
|
|
471
410
|
const namedStart = specification.given.reduce((map, label, index) => ({
|
|
472
411
|
...map,
|
|
@@ -597,7 +536,7 @@ export class HttpRouter {
|
|
|
597
536
|
return specification.given.map(input => {
|
|
598
537
|
const declaredFact = declaration.find(d => d.name === input.name);
|
|
599
538
|
if (!declaredFact) {
|
|
600
|
-
throw new
|
|
539
|
+
throw new Invalid(`No fact named ${input.name} was declared`);
|
|
601
540
|
}
|
|
602
541
|
return declaredFact.declared.reference;
|
|
603
542
|
});
|
|
@@ -675,7 +614,7 @@ export class HttpRouter {
|
|
|
675
614
|
|
|
676
615
|
function parseString(input: any): string {
|
|
677
616
|
if (typeof input !== 'string') {
|
|
678
|
-
throw new
|
|
617
|
+
throw new Invalid("Expected a string. Check the content type of the request.");
|
|
679
618
|
}
|
|
680
619
|
return input;
|
|
681
620
|
}
|
|
@@ -696,6 +635,21 @@ function extractResults(obj: any): any {
|
|
|
696
635
|
}
|
|
697
636
|
}
|
|
698
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
|
+
|
|
699
653
|
interface OptionsConfiguration {
|
|
700
654
|
intendedForGet(): ResponseConfiguration;
|
|
701
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 { purgeDescendantsSql, purgeSqlFromSpecification } from "./purge-sql";
|
|
47
48
|
|
|
48
49
|
interface FactTypeResult {
|
|
49
50
|
rows: {
|
|
@@ -433,6 +434,42 @@ 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
|
+
|
|
454
|
+
async purgeDescendants(purgeRoot: FactReference, triggers: FactReference[]): Promise<void> {
|
|
455
|
+
const factTypes = await this.loadFactTypesFromReferences([ purgeRoot, ...triggers ]);
|
|
456
|
+
if (!factTypes.has(purgeRoot.type) || triggers.some(t => !factTypes.has(t.type))) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const parameters = [
|
|
461
|
+
factTypes.get(purgeRoot.type),
|
|
462
|
+
purgeRoot.hash,
|
|
463
|
+
...triggers.map(t => [ factTypes.get(t.type), t.hash ]).flat()
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
const purgeCommand: string = purgeDescendantsSql(triggers.length, this.schema);
|
|
467
|
+
|
|
468
|
+
await this.connectionFactory.with(async (connection) => {
|
|
469
|
+
await connection.query(purgeCommand, parameters);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
436
473
|
loadBookmark(feed: string): Promise<string> {
|
|
437
474
|
throw new Error("Method not implemented.");
|
|
438
475
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { FactReference, Specification } from "jinaga";
|
|
2
|
+
import { FactTypeMap, RoleMap } from "./maps";
|
|
3
|
+
import { EdgeDescription, FactByLabel, QueryDescription, QueryDescriptionBuilder } from "./query-description";
|
|
4
|
+
|
|
5
|
+
export function purgeDescendantsSql(triggerCount: number, schema: string): string {
|
|
6
|
+
let whereClause = " WHERE (t.fact_type_id = $3 AND t.hash = $4)\n";
|
|
7
|
+
for (let i = 1; i < triggerCount; i++) {
|
|
8
|
+
whereClause += ` OR (t.fact_type_id = $${i * 2 + 3} AND t.hash = $${i * 2 + 4})\n`;
|
|
9
|
+
}
|
|
10
|
+
const sql =
|
|
11
|
+
`WITH purge_root AS (\n` +
|
|
12
|
+
` SELECT pr.fact_id\n` +
|
|
13
|
+
` FROM ${schema}.fact pr\n` +
|
|
14
|
+
` WHERE pr.fact_type_id = $1\n` +
|
|
15
|
+
` AND pr.hash = $2\n` +
|
|
16
|
+
`), triggers AS (\n` +
|
|
17
|
+
` SELECT t.fact_id\n` +
|
|
18
|
+
` FROM ${schema}.fact t\n` +
|
|
19
|
+
whereClause +
|
|
20
|
+
`), triggers_and_ancestors AS (\n` +
|
|
21
|
+
` SELECT t.fact_id\n` +
|
|
22
|
+
` FROM triggers t\n` +
|
|
23
|
+
` UNION\n` +
|
|
24
|
+
` SELECT a.ancestor_fact_id\n` +
|
|
25
|
+
` FROM ${schema}.ancestor a\n` +
|
|
26
|
+
` JOIN triggers t\n` +
|
|
27
|
+
` ON a.fact_id = t.fact_id\n` +
|
|
28
|
+
`), targets AS (\n` +
|
|
29
|
+
` SELECT a.fact_id\n` +
|
|
30
|
+
` FROM ${schema}.ancestor a\n` +
|
|
31
|
+
` JOIN purge_root pr\n` +
|
|
32
|
+
` ON a.ancestor_fact_id = pr.fact_id\n` +
|
|
33
|
+
` WHERE a.fact_id NOT IN (SELECT * FROM triggers_and_ancestors)\n` +
|
|
34
|
+
`), facts AS (\n` +
|
|
35
|
+
` DELETE\n` +
|
|
36
|
+
` FROM ${schema}.fact f\n` +
|
|
37
|
+
` USING targets t WHERE t.fact_id = f.fact_id\n` +
|
|
38
|
+
` RETURNING f.fact_id\n` +
|
|
39
|
+
`)\n` +
|
|
40
|
+
`SELECT fact_id FROM facts\n`;
|
|
41
|
+
return sql;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function purgeSqlFromSpecification(specification: Specification, factTypes: FactTypeMap, roleMap: RoleMap, schema: string):
|
|
45
|
+
{ sql: string, parameters: (string | number)[] } | null {
|
|
46
|
+
const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
|
|
47
|
+
|
|
48
|
+
let queryDescription = QueryDescription.unsatisfiable;
|
|
49
|
+
let knownFacts: FactByLabel = {};
|
|
50
|
+
const given = specification.given;
|
|
51
|
+
const start: FactReference[] = specification.given.map(g => ({ type: g.type, hash: 'xxxxx' }));
|
|
52
|
+
({ queryDescription, knownFacts } = queryDescriptionBuilder.addEdges(queryDescription, given, start, knownFacts, [], specification.matches));
|
|
53
|
+
|
|
54
|
+
if (!queryDescription.isSatisfiable()) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const query = generatePurgeSqlQuery(queryDescription, schema);
|
|
59
|
+
return query;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
function generatePurgeSqlQuery(queryDescription: QueryDescription, schema: string):
|
|
64
|
+
{ sql: string, parameters: (string | number)[] } {
|
|
65
|
+
if (queryDescription.existentialConditions.length > 0) {
|
|
66
|
+
throw new Error("Purge conditions should not have existential conditions");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const columns = queryDescription.outputs
|
|
70
|
+
.map((label, index) => `f${label.factIndex}.fact_id as trigger${index + 1}`)
|
|
71
|
+
.join(",\n ");
|
|
72
|
+
const firstEdge = queryDescription.edges[0];
|
|
73
|
+
const predecessorFact = queryDescription.inputs.find(i => i.factIndex === firstEdge.predecessorFactIndex);
|
|
74
|
+
const successorFact = queryDescription.inputs.find(i => i.factIndex === firstEdge.successorFactIndex);
|
|
75
|
+
const firstFactIndex = predecessorFact ? predecessorFact.factIndex : successorFact!.factIndex;
|
|
76
|
+
const writtenFactIndexes = new Set<number>().add(firstFactIndex);
|
|
77
|
+
const joins: string[] = generateJoins(queryDescription.edges, writtenFactIndexes, schema);
|
|
78
|
+
const inputWhereClauses = queryDescription.inputs
|
|
79
|
+
.map(input => `f${input.factIndex}.fact_type_id = $${input.factTypeParameter}`)
|
|
80
|
+
.join(" AND ");
|
|
81
|
+
const triggerWhereClauses = queryDescription.outputs
|
|
82
|
+
.map((label, index) => `a.fact_id = c2.trigger${index + 1}`)
|
|
83
|
+
.join("\n OR ");
|
|
84
|
+
const triggerAncestorClauses = queryDescription.outputs
|
|
85
|
+
.map((label, index) =>
|
|
86
|
+
` AND NOT EXISTS (\n` +
|
|
87
|
+
` SELECT 1\n` +
|
|
88
|
+
` FROM candidates c2\n` +
|
|
89
|
+
` JOIN ${schema}.ancestor a2\n` +
|
|
90
|
+
` ON a2.fact_id = c2.trigger${index + 1}\n` +
|
|
91
|
+
` WHERE a.fact_id = a2.ancestor_fact_id\n` +
|
|
92
|
+
` )\n`
|
|
93
|
+
)
|
|
94
|
+
.join("");
|
|
95
|
+
|
|
96
|
+
const sql =
|
|
97
|
+
`WITH candidates AS (\n` +
|
|
98
|
+
` SELECT\n` +
|
|
99
|
+
` f${firstFactIndex}.fact_id as purge_root,\n` +
|
|
100
|
+
` ${columns}\n` +
|
|
101
|
+
` FROM ${schema}.fact f${firstFactIndex}\n${joins.join("")}` +
|
|
102
|
+
` WHERE ${inputWhereClauses}\n` +
|
|
103
|
+
`), targets AS (\n` +
|
|
104
|
+
` SELECT a.fact_id\n` +
|
|
105
|
+
` FROM ${schema}.ancestor a\n` +
|
|
106
|
+
` JOIN candidates c ON c.purge_root = a.ancestor_fact_id\n` +
|
|
107
|
+
` WHERE NOT EXISTS (\n` +
|
|
108
|
+
` SELECT 1\n` +
|
|
109
|
+
` FROM candidates c2\n` +
|
|
110
|
+
` WHERE ${triggerWhereClauses}\n` +
|
|
111
|
+
` )\n` +
|
|
112
|
+
triggerAncestorClauses +
|
|
113
|
+
`), facts AS (\n` +
|
|
114
|
+
` DELETE\n` +
|
|
115
|
+
` FROM ${schema}.fact f\n` +
|
|
116
|
+
` USING targets t WHERE t.fact_id = f.fact_id\n` +
|
|
117
|
+
` RETURNING f.fact_id\n` +
|
|
118
|
+
`)\n` +
|
|
119
|
+
`SELECT fact_id FROM facts\n`;
|
|
120
|
+
|
|
121
|
+
// Remove parameter 1, which is the hash of the first fact
|
|
122
|
+
const parameters = [
|
|
123
|
+
queryDescription.parameters[0],
|
|
124
|
+
...queryDescription.parameters.slice(2)
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
sql,
|
|
130
|
+
parameters: parameters
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function generateJoins(edges: EdgeDescription[], writtenFactIndexes: Set<number>, schema: string) {
|
|
135
|
+
const joins: string[] = [];
|
|
136
|
+
edges.forEach(edge => {
|
|
137
|
+
if (writtenFactIndexes.has(edge.predecessorFactIndex)) {
|
|
138
|
+
if (writtenFactIndexes.has(edge.successorFactIndex)) {
|
|
139
|
+
joins.push(
|
|
140
|
+
` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
|
|
141
|
+
` ON e${edge.edgeIndex}.predecessor_fact_id = f${edge.predecessorFactIndex}.fact_id\n` +
|
|
142
|
+
` AND e${edge.edgeIndex}.successor_fact_id = f${edge.successorFactIndex}.fact_id\n` +
|
|
143
|
+
` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
joins.push(
|
|
148
|
+
` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
|
|
149
|
+
` ON e${edge.edgeIndex}.predecessor_fact_id = f${edge.predecessorFactIndex}.fact_id\n` +
|
|
150
|
+
` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
|
|
151
|
+
);
|
|
152
|
+
joins.push(
|
|
153
|
+
` JOIN ${schema}.fact f${edge.successorFactIndex}\n` +
|
|
154
|
+
` ON f${edge.successorFactIndex}.fact_id = e${edge.edgeIndex}.successor_fact_id\n`
|
|
155
|
+
);
|
|
156
|
+
writtenFactIndexes.add(edge.successorFactIndex);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (writtenFactIndexes.has(edge.successorFactIndex)) {
|
|
160
|
+
joins.push(
|
|
161
|
+
` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
|
|
162
|
+
` ON e${edge.edgeIndex}.successor_fact_id = f${edge.successorFactIndex}.fact_id\n` +
|
|
163
|
+
` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
|
|
164
|
+
);
|
|
165
|
+
joins.push(
|
|
166
|
+
` JOIN ${schema}.fact f${edge.predecessorFactIndex}\n` +
|
|
167
|
+
` ON f${edge.predecessorFactIndex}.fact_id = e${edge.edgeIndex}.predecessor_fact_id\n`
|
|
168
|
+
);
|
|
169
|
+
writtenFactIndexes.add(edge.predecessorFactIndex);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
throw new Error("Neither predecessor nor successor fact has been written");
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return joins;
|
|
176
|
+
}
|