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.
Files changed (42) hide show
  1. package/.github/workflows/main.yml +1 -1
  2. package/.github/workflows/publish.yml +3 -3
  3. package/dist/http/router.js +73 -121
  4. package/dist/http/router.js.map +1 -1
  5. package/dist/jinaga-server.d.ts +2 -1
  6. package/dist/jinaga-server.js +18 -4
  7. package/dist/jinaga-server.js.map +1 -1
  8. package/dist/postgres/postgres-store.d.ts +1 -0
  9. package/dist/postgres/postgres-store.js +17 -0
  10. package/dist/postgres/postgres-store.js.map +1 -1
  11. package/dist/postgres/purge-sql.d.ts +6 -0
  12. package/dist/postgres/purge-sql.js +103 -0
  13. package/dist/postgres/purge-sql.js.map +1 -0
  14. package/dist/postgres/query-description.d.ts +1 -1
  15. package/dist/postgres/query-description.js +3 -3
  16. package/dist/postgres/query-description.js.map +1 -1
  17. package/dist/postgres/specification-result-sql.js +1 -1
  18. package/dist/postgres/specification-result-sql.js.map +1 -1
  19. package/dist/postgres/specification-sql.js +2 -2
  20. package/dist/postgres/specification-sql.js.map +1 -1
  21. package/integration-test/docker-compose.yaml +0 -1
  22. package/integration-test/jinaga-test/Dockerfile +1 -1
  23. package/integration-test/jinaga-test/package-lock.json +147 -86
  24. package/integration-test/jinaga-test/package.json +3 -3
  25. package/integration-test/postgres/1-setup.sql +2 -2
  26. package/integration-test/postgres/Dockerfile +1 -1
  27. package/package.json +4 -4
  28. package/replicator/package-lock.json +139 -71
  29. package/replicator/package.json +2 -2
  30. package/scripts/migrate_purge.sql +45 -0
  31. package/scripts/purge.sql +33 -0
  32. package/setup.sql +30 -2
  33. package/src/http/router.ts +72 -119
  34. package/src/jinaga-server.ts +27 -5
  35. package/src/postgres/postgres-store.ts +18 -0
  36. package/src/postgres/purge-sql.ts +125 -0
  37. package/src/postgres/query-description.ts +1 -1
  38. package/src/postgres/specification-result-sql.ts +3 -3
  39. package/src/postgres/specification-sql.ts +4 -4
  40. package/test/authorization/authorizationSpec.ts +1 -1
  41. package/test/models/blog.ts +92 -0
  42. package/test/postgres/purgeSqlSpec.ts +218 -0
@@ -15,8 +15,8 @@
15
15
  "author": "Michael L Perry",
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
- "express": "^4.19.2",
19
- "pg": "8.11.5"
18
+ "express": "^4.21.1",
19
+ "pg": "8.13.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/express": "^4.17.13",
@@ -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(50) NOT NULL
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(20) NOT NULL
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"
@@ -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
- .catch(error => {
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
- router.get('/login', getAuthenticate(user => this.login(user)));
339
- router.post('/load', post(
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 Error("References are not allowed while saving.");
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 Error(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
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 Error(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
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 Error(`No fact named ${input.name} was declared`);
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(sendResponse: (req: Request, res: Response) => void) {
585
+ private applyAllowOrigin(req: Request, res: Response, next: NextFunction) {
646
586
  // Specify the allowed origins.
647
- return (req: Request, res: Response, next: NextFunction) => {
648
- let requestOrigin = req.get('Origin');
649
-
650
- if (typeof this.allowedOrigin === 'string') {
651
- // If allowedOrigin is a string, use it as the value of the Access-Control-Allow-Origin header
652
- res.set('Access-Control-Allow-Origin', this.allowedOrigin);
653
- sendResponse(req, res);
654
- } else if (Array.isArray(this.allowedOrigin)) {
655
- // If allowedOrigin is an array, check if the request's origin is in the array
656
- if (requestOrigin && this.allowedOrigin.includes(requestOrigin)) {
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
- sendResponse(req, res);
660
- } else if (typeof this.allowedOrigin === 'function' && requestOrigin) {
661
- // If allowedOrigin is a function, call it with the request's origin
662
- this.allowedOrigin(requestOrigin, (err, allow) => {
663
- if (err) {
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 Error("Expected a string. Check the content type of the request.");
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;
@@ -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 factManager = new FactManager(fork, source, store, network);
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
  }