jinaga-server 3.1.5 → 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 (38) 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 +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/package-lock.json +147 -86
  23. package/integration-test/jinaga-test/package.json +3 -3
  24. package/package.json +4 -4
  25. package/replicator/package-lock.json +139 -71
  26. package/replicator/package.json +2 -2
  27. package/scripts/migrate_purge.sql +45 -0
  28. package/scripts/purge.sql +33 -0
  29. package/src/http/router.ts +38 -84
  30. package/src/jinaga-server.ts +27 -5
  31. package/src/postgres/postgres-store.ts +18 -0
  32. package/src/postgres/purge-sql.ts +125 -0
  33. package/src/postgres/query-description.ts +1 -1
  34. package/src/postgres/specification-result-sql.ts +3 -3
  35. package/src/postgres/specification-sql.ts +4 -4
  36. package/test/authorization/authorizationSpec.ts +1 -1
  37. package/test/models/blog.ts +92 -0
  38. package/test/postgres/purgeSqlSpec.ts +218 -0
@@ -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;
@@ -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 { 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
  }
@@ -0,0 +1,125 @@
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 purgeSqlFromSpecification(specification: Specification, factTypes: FactTypeMap, roleMap: RoleMap, schema: string):
6
+ { sql: string, parameters: (string | number)[] } | null {
7
+ const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
8
+
9
+ let queryDescription = QueryDescription.unsatisfiable;
10
+ let knownFacts: FactByLabel = {};
11
+ const given = specification.given;
12
+ const start: FactReference[] = specification.given.map(g => ({ type: g.type, hash: 'xxxxx' }));
13
+ ({ queryDescription, knownFacts } = queryDescriptionBuilder.addEdges(queryDescription, given, start, knownFacts, [], specification.matches));
14
+
15
+ if (!queryDescription.isSatisfiable()) {
16
+ return null;
17
+ }
18
+
19
+ const query = generatePurgeSqlQuery(queryDescription, schema);
20
+ return query;
21
+ }
22
+
23
+
24
+ function generatePurgeSqlQuery(queryDescription: QueryDescription, schema: string):
25
+ { sql: string, parameters: (string | number)[] } {
26
+ if (queryDescription.existentialConditions.length > 0) {
27
+ throw new Error("Purge conditions should not have existential conditions");
28
+ }
29
+
30
+ const columns = queryDescription.outputs
31
+ .map((label, index) => `f${label.factIndex}.fact_id as trigger${index + 1}`)
32
+ .join(",\n ");
33
+ const firstEdge = queryDescription.edges[0];
34
+ const predecessorFact = queryDescription.inputs.find(i => i.factIndex === firstEdge.predecessorFactIndex);
35
+ const successorFact = queryDescription.inputs.find(i => i.factIndex === firstEdge.successorFactIndex);
36
+ const firstFactIndex = predecessorFact ? predecessorFact.factIndex : successorFact!.factIndex;
37
+ const writtenFactIndexes = new Set<number>().add(firstFactIndex);
38
+ const joins: string[] = generateJoins(queryDescription.edges, writtenFactIndexes, schema);
39
+ const inputWhereClauses = queryDescription.inputs
40
+ .map(input => `f${input.factIndex}.fact_type_id = $${input.factTypeParameter}`)
41
+ .join(" AND ");
42
+ const triggerWhereClauses = queryDescription.outputs
43
+ .map((label, index) => `a.fact_id = c2.trigger${index + 1}`)
44
+ .join("\n OR ");
45
+
46
+ const sql =
47
+ `WITH candidates AS (\n` +
48
+ ` SELECT\n` +
49
+ ` f${firstFactIndex}.fact_id as purge_root,\n` +
50
+ ` ${columns}\n` +
51
+ ` FROM ${schema}.fact f${firstFactIndex}\n${joins.join("")}` +
52
+ ` WHERE ${inputWhereClauses}\n` +
53
+ `), targets AS (\n` +
54
+ ` SELECT a.fact_id\n` +
55
+ ` FROM public.ancestor a\n` +
56
+ ` JOIN candidates c ON c.purge_root = a.ancestor_fact_id\n` +
57
+ ` WHERE NOT EXISTS (\n` +
58
+ ` SELECT 1\n` +
59
+ ` FROM candidates c2\n` +
60
+ ` WHERE ${triggerWhereClauses}\n` +
61
+ ` )\n` +
62
+ `), facts AS (\n` +
63
+ ` DELETE\n` +
64
+ ` FROM public.fact f\n` +
65
+ ` USING targets t WHERE t.fact_id = f.fact_id\n` +
66
+ ` RETURNING f.fact_id\n` +
67
+ `)\n` +
68
+ `SELECT fact_id FROM facts\n`;
69
+
70
+ // Remove parameter 1, which is the hash of the first fact
71
+ const parameters = [
72
+ queryDescription.parameters[0],
73
+ ...queryDescription.parameters.slice(2)
74
+ ];
75
+
76
+
77
+ return {
78
+ sql,
79
+ parameters: parameters
80
+ };
81
+ }
82
+
83
+ function generateJoins(edges: EdgeDescription[], writtenFactIndexes: Set<number>, schema: string) {
84
+ const joins: string[] = [];
85
+ edges.forEach(edge => {
86
+ if (writtenFactIndexes.has(edge.predecessorFactIndex)) {
87
+ if (writtenFactIndexes.has(edge.successorFactIndex)) {
88
+ joins.push(
89
+ ` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
90
+ ` ON e${edge.edgeIndex}.predecessor_fact_id = f${edge.predecessorFactIndex}.fact_id\n` +
91
+ ` AND e${edge.edgeIndex}.successor_fact_id = f${edge.successorFactIndex}.fact_id\n` +
92
+ ` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
93
+ );
94
+ }
95
+ else {
96
+ joins.push(
97
+ ` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
98
+ ` ON e${edge.edgeIndex}.predecessor_fact_id = f${edge.predecessorFactIndex}.fact_id\n` +
99
+ ` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
100
+ );
101
+ joins.push(
102
+ ` JOIN ${schema}.fact f${edge.successorFactIndex}\n` +
103
+ ` ON f${edge.successorFactIndex}.fact_id = e${edge.edgeIndex}.successor_fact_id\n`
104
+ );
105
+ writtenFactIndexes.add(edge.successorFactIndex);
106
+ }
107
+ }
108
+ else if (writtenFactIndexes.has(edge.successorFactIndex)) {
109
+ joins.push(
110
+ ` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
111
+ ` ON e${edge.edgeIndex}.successor_fact_id = f${edge.successorFactIndex}.fact_id\n` +
112
+ ` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
113
+ );
114
+ joins.push(
115
+ ` JOIN ${schema}.fact f${edge.predecessorFactIndex}\n` +
116
+ ` ON f${edge.predecessorFactIndex}.fact_id = e${edge.edgeIndex}.predecessor_fact_id\n`
117
+ );
118
+ writtenFactIndexes.add(edge.predecessorFactIndex);
119
+ }
120
+ else {
121
+ throw new Error("Neither predecessor nor successor fact has been written");
122
+ }
123
+ });
124
+ return joins;
125
+ }
@@ -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
@@ -19,7 +19,7 @@ import {
19
19
  } from "jinaga";
20
20
 
21
21
  import { FactTypeMap, RoleMap } from "./maps";
22
- import { EdgeDescription, ExistentialConditionDescription, FactByLabel, QueryDescription, QueryDescriptionBuider } from "./query-description";
22
+ import { EdgeDescription, ExistentialConditionDescription, FactByLabel, QueryDescription, QueryDescriptionBuilder } from "./query-description";
23
23
 
24
24
  function generateResultSqlQuery(queryDescription: QueryDescription, schema: string): SpecificationSqlQuery {
25
25
  const allLabels = [ ...queryDescription.inputs, ...queryDescription.outputs ];
@@ -481,7 +481,7 @@ interface NamedResultComposer {
481
481
 
482
482
  class ResultDescriptionBuilder {
483
483
  constructor(
484
- private queryDescriptionBuilder: QueryDescriptionBuider
484
+ private queryDescriptionBuilder: QueryDescriptionBuilder
485
485
  ) { }
486
486
 
487
487
  buildDescription(start: FactReference[], specification: Specification): ResultDescription {
@@ -547,7 +547,7 @@ function idsEqual(a: number[], b: number[]) {
547
547
  }
548
548
 
549
549
  export function resultSqlFromSpecification(start: FactReference[], specification: Specification, factTypes: FactTypeMap, roleMap: RoleMap, schema: string): ResultComposer | null {
550
- const queryDescriptionBuilder = new QueryDescriptionBuider(factTypes, roleMap);
550
+ const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
551
551
  const descriptionBuilder = new ResultDescriptionBuilder(queryDescriptionBuilder);
552
552
  const description = descriptionBuilder.buildDescription(start, specification);
553
553
 
@@ -5,7 +5,7 @@ import {
5
5
  validateGiven
6
6
  } from "jinaga";
7
7
 
8
- import { EdgeDescription, ExistentialConditionDescription, QueryDescription, QueryDescriptionBuider } from "./query-description";
8
+ import { EdgeDescription, ExistentialConditionDescription, QueryDescription, QueryDescriptionBuilder } from "./query-description";
9
9
 
10
10
  interface SpecificationLabel {
11
11
  type: string;
@@ -173,7 +173,7 @@ export function sqlFromSpecification(start: FactReference[], schema: string, boo
173
173
  }, new Map<string, FactReference>());
174
174
 
175
175
  const feeds = buildFeeds(specification);
176
- const queryDescriptionBuilder = new QueryDescriptionBuider(factTypes, roleMap);
176
+ const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
177
177
  const feedAndBookmark = feeds.map((feed, index) => ({
178
178
  feed,
179
179
  bookmark: bookmarks[index]
@@ -193,7 +193,7 @@ export function sqlFromSpecification(start: FactReference[], schema: string, boo
193
193
  }
194
194
 
195
195
  export function sqlFromFeed(feed: Specification, start: FactReference[], schema: string, bookmark: string, limit: number, factTypes: Map<string, number>, roleMap: Map<number, Map<string, number>>): SpecificationSqlQuery | null {
196
- const queryDescriptionBuilder = new QueryDescriptionBuider(factTypes, roleMap);
196
+ const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
197
197
  const queryDescription = buildQueryDescription(queryDescriptionBuilder, feed, start);
198
198
  if (!queryDescription.isSatisfiable()) {
199
199
  return null;
@@ -202,7 +202,7 @@ export function sqlFromFeed(feed: Specification, start: FactReference[], schema:
202
202
  return sql;
203
203
  }
204
204
 
205
- function buildQueryDescription(queryDescriptionBuilder: QueryDescriptionBuider, specification: Specification, start: FactReference[]): QueryDescription {
205
+ function buildQueryDescription(queryDescriptionBuilder: QueryDescriptionBuilder, specification: Specification, start: FactReference[]): QueryDescription {
206
206
  const { queryDescription } = queryDescriptionBuilder.addEdges(
207
207
  QueryDescription.unsatisfiable,
208
208
  specification.given,
@@ -164,7 +164,7 @@ function givenAuthorizationWithStorage(storage: MemoryStore) {
164
164
  const fork = new PassThroughFork(storage);
165
165
  const observableSource = new ObservableSource(storage);
166
166
  const network = new NetworkNoOp();
167
- const factManager = new FactManager(fork, observableSource, storage, network);
167
+ const factManager = new FactManager(fork, observableSource, storage, network, []);
168
168
  const model = buildModel(b => b
169
169
  .type(Hashtag)
170
170
  .type(User)