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.
Files changed (47) hide show
  1. package/.github/workflows/main.yml +1 -1
  2. package/.github/workflows/publish.yml +3 -3
  3. package/dist/http/router.js +37 -84
  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 +2 -0
  9. package/dist/postgres/postgres-store.js +34 -0
  10. package/dist/postgres/postgres-store.js.map +1 -1
  11. package/dist/postgres/purge-sql.d.ts +7 -0
  12. package/dist/postgres/purge-sql.js +151 -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 +2 -0
  23. package/integration-test/jinaga-test/orderModel.js +104 -0
  24. package/integration-test/jinaga-test/package-lock.json +147 -86
  25. package/integration-test/jinaga-test/package.json +3 -3
  26. package/integration-test/jinaga-test/purge.test.js +224 -0
  27. package/integration-test/postgres/1-setup.sql +8 -3
  28. package/integration-test/postgres/2-grant.sh +4 -4
  29. package/integration-test/postgres/Dockerfile +1 -1
  30. package/package.json +5 -4
  31. package/replicator/package-lock.json +139 -71
  32. package/replicator/package.json +2 -2
  33. package/scripts/migrate_purge.sql +45 -0
  34. package/scripts/purge.sql +33 -0
  35. package/setup.sh +4 -4
  36. package/setup.sql +59 -3
  37. package/src/http/router.ts +38 -84
  38. package/src/jinaga-server.ts +27 -5
  39. package/src/postgres/postgres-store.ts +37 -0
  40. package/src/postgres/purge-sql.ts +176 -0
  41. package/src/postgres/query-description.ts +1 -1
  42. package/src/postgres/specification-result-sql.ts +3 -3
  43. package/src/postgres/specification-sql.ts +4 -4
  44. package/test/authorization/authorizationSpec.ts +1 -1
  45. package/test/models/blog.ts +92 -0
  46. package/test/postgres/purgeDescendantsSqlSpec.ts +41 -0
  47. 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"
@@ -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
  }
@@ -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 Error("References are not allowed while saving.");
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 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})`);
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 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})`);
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 Error(`No fact named ${input.name} was declared`);
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 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.");
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;
@@ -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 { 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
+ }
@@ -276,7 +276,7 @@ export type FactByLabel = {
276
276
  [label: string]: FactDescription;
277
277
  };
278
278
 
279
- export class QueryDescriptionBuider {
279
+ export class QueryDescriptionBuilder {
280
280
  constructor(
281
281
  private factTypes: FactTypeMap,
282
282
  private roleMap: RoleMap