jinaga-server 2.6.2 → 3.0.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 (31) hide show
  1. package/dist/authentication/authentication-device.js +27 -6
  2. package/dist/authentication/authentication-device.js.map +1 -1
  3. package/dist/authentication/authentication-session.js +27 -6
  4. package/dist/authentication/authentication-session.js.map +1 -1
  5. package/dist/authorization/authorization-keystore.d.ts +4 -5
  6. package/dist/authorization/authorization-keystore.js +42 -18
  7. package/dist/authorization/authorization-keystore.js.map +1 -1
  8. package/dist/http/router.d.ts +1 -2
  9. package/dist/http/router.js +13 -17
  10. package/dist/http/router.js.map +1 -1
  11. package/dist/jinaga-server.js +5 -6
  12. package/dist/jinaga-server.js.map +1 -1
  13. package/dist/postgres/postgres-store.d.ts +2 -5
  14. package/dist/postgres/postgres-store.js +44 -121
  15. package/dist/postgres/postgres-store.js.map +1 -1
  16. package/integration-test/jinaga-test/jinaga.test.js +247 -231
  17. package/integration-test/jinaga-test/package-lock.json +9 -52
  18. package/integration-test/jinaga-test/package.json +2 -5
  19. package/package.json +2 -3
  20. package/src/authentication/authentication-device.ts +29 -7
  21. package/src/authentication/authentication-session.ts +29 -7
  22. package/src/authorization/authorization-keystore.ts +42 -22
  23. package/src/http/router.ts +13 -23
  24. package/src/jinaga-server.ts +5 -6
  25. package/src/postgres/postgres-store.ts +63 -149
  26. package/test/authorization/authorizationSpec.ts +75 -89
  27. package/dist/postgres/sql.d.ts +0 -10
  28. package/dist/postgres/sql.js +0 -408
  29. package/dist/postgres/sql.js.map +0 -1
  30. package/src/postgres/sql.ts +0 -498
  31. package/test/postgres/postgresSpec.ts +0 -391
@@ -17,11 +17,8 @@
17
17
  "express": "^4.17.2",
18
18
  "jest": "^29.7.0",
19
19
  "jest-compact-reporter": "^1.2.9",
20
- "jinaga": "^5.6.0",
20
+ "jinaga": "^6.0.0",
21
21
  "node-forge": "^1.3.0",
22
- "passport": "^0.6.0",
23
- "pg": "^8.11.3",
24
- "tweetnacl": "^1.0.3",
25
- "tweetnacl-util": "^0.15.1"
22
+ "pg": "^8.11.3"
26
23
  }
27
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinaga-server",
3
- "version": "2.6.2",
3
+ "version": "3.0.0",
4
4
  "description": "Data management for web and mobile applications.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,9 +48,8 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "express": "^4.18.2",
51
- "jinaga": "^5.6.0",
51
+ "jinaga": "^6.0.0",
52
52
  "node-forge": "^1.3.1",
53
- "passport": "^0.6.0",
54
53
  "pg": "^8.11.3"
55
54
  }
56
55
  }
@@ -1,4 +1,4 @@
1
- import { Authentication, AuthorizationEngine, AuthorizationRules, FactEnvelope, FactRecord, LoginResponse, Storage, UserIdentity } from "jinaga";
1
+ import { Authentication, AuthorizationEngine, AuthorizationRules, FactEnvelope, FactRecord, LoginResponse, Storage, UserIdentity, factReferenceEquals } from "jinaga";
2
2
 
3
3
  import { Keystore } from "../keystore";
4
4
 
@@ -33,11 +33,33 @@ export class AuthenticationDevice implements Authentication {
33
33
 
34
34
  async authorize(envelopes: FactEnvelope[]): Promise<FactEnvelope[]> {
35
35
  const deviceFact = await this.keystore.getOrCreateDeviceFact(this.localDeviceIdentity);
36
- const facts = envelopes.map(envelope => envelope.fact);
37
- const authorizedFacts = this.authorizationEngine
38
- ? await this.authorizationEngine.authorizeFacts(facts, deviceFact)
39
- : facts;
40
- const signedFacts = await this.keystore.signFacts(this.localDeviceIdentity, authorizedFacts);
41
- return signedFacts;
36
+
37
+ if (this.authorizationEngine) {
38
+ const results = await this.authorizationEngine.authorizeFacts(envelopes, deviceFact);
39
+ const authorizedEnvelopes: FactEnvelope[] = results.map(r => {
40
+ const isFact = factReferenceEquals(r.fact);
41
+ const envelope = envelopes.find(e => isFact(e.fact));
42
+ if (!envelope) {
43
+ throw new Error("Fact not found in envelopes.");
44
+ }
45
+ if (r.verdict === "Accept") {
46
+ return {
47
+ fact: r.fact,
48
+ signatures: envelope.signatures
49
+ .filter(s => r.newPublicKeys.includes(s.publicKey))
50
+ };
51
+ }
52
+ else if (r.verdict === "Existing") {
53
+ return envelope;
54
+ }
55
+ else {
56
+ throw new Error("Unexpected verdict.");
57
+ }
58
+ });
59
+ return authorizedEnvelopes;
60
+ }
61
+ else {
62
+ return envelopes;
63
+ }
42
64
  }
43
65
  }
@@ -1,4 +1,4 @@
1
- import { Authentication, AuthorizationEngine, AuthorizationRules, FactEnvelope, FactRecord, LoginResponse, Storage, UserIdentity } from "jinaga";
1
+ import { Authentication, AuthorizationEngine, AuthorizationRules, FactEnvelope, FactRecord, LoginResponse, Storage, UserIdentity, factReferenceEquals } from "jinaga";
2
2
 
3
3
  import { Keystore } from "../keystore";
4
4
 
@@ -41,11 +41,33 @@ export class AuthenticationSession implements Authentication {
41
41
 
42
42
  async authorize(envelopes: FactEnvelope[]): Promise<FactEnvelope[]> {
43
43
  const userFact = await this.keystore.getUserFact(this.userIdentity);
44
- const facts = envelopes.map(envelope => envelope.fact);
45
- const authorizedFacts = this.authorizationEngine
46
- ? await this.authorizationEngine.authorizeFacts(facts, userFact)
47
- : facts;
48
- const signedFacts = await this.keystore.signFacts(this.userIdentity, authorizedFacts);
49
- return signedFacts;
44
+
45
+ if (this.authorizationEngine) {
46
+ const results = await this.authorizationEngine.authorizeFacts(envelopes, userFact);
47
+ const authorizedEnvelopes: FactEnvelope[] = results.map(r => {
48
+ const isFact = factReferenceEquals(r.fact);
49
+ const envelope = envelopes.find(e => isFact(e.fact));
50
+ if (!envelope) {
51
+ throw new Error("Fact not found in envelopes.");
52
+ }
53
+ if (r.verdict === "Accept") {
54
+ return {
55
+ fact: r.fact,
56
+ signatures: envelope.signatures
57
+ .filter(s => r.newPublicKeys.includes(s.publicKey))
58
+ };
59
+ }
60
+ else if (r.verdict === "Existing") {
61
+ return envelope;
62
+ }
63
+ else {
64
+ throw new Error("Unexpected verdict.");
65
+ }
66
+ });
67
+ return authorizedEnvelopes;
68
+ }
69
+ else {
70
+ return envelopes;
71
+ }
50
72
  }
51
73
  }
@@ -7,10 +7,8 @@ import {
7
7
  FactEnvelope,
8
8
  FactFeed,
9
9
  FactManager,
10
- FactRecord,
11
10
  FactReference,
12
11
  Forbidden,
13
- Query,
14
12
  ReferencesByName,
15
13
  Specification,
16
14
  Storage,
@@ -52,10 +50,6 @@ export class AuthorizationKeystore implements Authorization {
52
50
  return userFact;
53
51
  }
54
52
 
55
- query(userIdentity: UserIdentity | null, start: FactReference, query: Query) {
56
- return this.factManager.query(start, query);
57
- }
58
-
59
53
  async read(userIdentity: UserIdentity | null, start: FactReference[], specification: Specification) {
60
54
  if (this.distributionEngine) {
61
55
  const userReference: FactReference | null = userIdentity
@@ -111,24 +105,50 @@ export class AuthorizationKeystore implements Authorization {
111
105
  return await this.factManager.load(references);
112
106
  }
113
107
 
114
- async save(userIdentity: UserIdentity | null, facts: FactRecord[]) {
115
- if (!this.authorizationEngine) {
116
- const envelopes = await this.factManager.save(facts.map(fact => ({
117
- fact,
118
- signatures: []
119
- })));
120
- return envelopes.map(envelope => envelope.fact);
121
- }
122
-
123
- const userFact = userIdentity ? await this.keystore.getUserFact(userIdentity) : null;
124
- const authorizedFacts = await this.authorizationEngine.authorizeFacts(facts, userFact);
125
- if (userIdentity) {
126
- const signedFacts = await this.keystore.signFacts(userIdentity, authorizedFacts);
127
- const envelopes = await this.factManager.save(signedFacts);
128
- return envelopes.map(envelope => envelope.fact);
108
+ async save(userIdentity: UserIdentity | null, envelopes: FactEnvelope[]): Promise<FactEnvelope[]> {
109
+ if (this.authorizationEngine) {
110
+ const userFact = userIdentity ? await this.keystore.getUserFact(userIdentity) : null;
111
+ const results = await this.authorizationEngine.authorizeFacts(envelopes, userFact);
112
+ const userKeys : string[] = (userFact && userFact.fields.hasOwnProperty("publicKey"))
113
+ ? [ userFact.fields.publicKey ]
114
+ : [];
115
+ const factsToSign = results
116
+ .filter(r => r.verdict === "Accept" && r.newPublicKeys.some(k => userKeys.includes(k)))
117
+ .map(r => r.fact);
118
+ const signedFacts = (userIdentity && factsToSign.length > 0)
119
+ ? await this.keystore.signFacts(userIdentity, factsToSign)
120
+ : [];
121
+ const authorizedEnvelopes: FactEnvelope[] = results.map(r => {
122
+ const isFact = factReferenceEquals(r.fact);
123
+ const envelope = envelopes.find(e => isFact(e.fact));
124
+ if (!envelope) {
125
+ throw new Error("Fact not found in envelopes.");
126
+ }
127
+ if (r.verdict === "Accept") {
128
+ const signedFact = signedFacts.find(f => isFact(f.fact));
129
+ const userSignatures = signedFact
130
+ ? signedFact.signatures
131
+ : [];
132
+ return {
133
+ fact: r.fact,
134
+ signatures: envelope.signatures
135
+ .filter(s => r.newPublicKeys.includes(s.publicKey))
136
+ .concat(userSignatures)
137
+ };
138
+ }
139
+ else if (r.verdict === "Existing") {
140
+ return envelope;
141
+ }
142
+ else {
143
+ throw new Error("Unexpected verdict.");
144
+ }
145
+ });
146
+ const savedEnvelopes = await this.factManager.save(authorizedEnvelopes);
147
+ return savedEnvelopes;
129
148
  }
130
149
  else {
131
- return authorizedFacts;
150
+ const savedEnvelopes = await this.factManager.save(envelopes);
151
+ return savedEnvelopes;
132
152
  }
133
153
  }
134
154
 
@@ -14,8 +14,6 @@ import {
14
14
  LoadResponse,
15
15
  ProfileMessage,
16
16
  ProjectedResult,
17
- QueryMessage,
18
- QueryResponse,
19
17
  ReferencesByName,
20
18
  SaveMessage,
21
19
  Specification,
@@ -25,10 +23,8 @@ import {
25
23
  buildFeeds,
26
24
  computeObjectHash,
27
25
  computeTupleSubsetHash,
28
- fromDescriptiveString,
29
26
  invertSpecification,
30
27
  parseLoadMessage,
31
- parseQueryMessage,
32
28
  parseSaveMessage
33
29
  } from "jinaga";
34
30
 
@@ -285,15 +281,9 @@ export interface RequestUser {
285
281
  export class HttpRouter {
286
282
  handler: Handler;
287
283
 
288
- constructor(private factManager: FactManager, private authorization: Authorization, private feedCache: FeedCache, backwardCompatible: boolean) {
284
+ constructor(private factManager: FactManager, private authorization: Authorization, private feedCache: FeedCache) {
289
285
  const router = Router();
290
286
  router.get('/login', getAuthenticate(user => this.login(user)));
291
- if (backwardCompatible) {
292
- router.post('/query', post(
293
- parseQueryMessage,
294
- (user, queryMessage) => this.query(user, queryMessage)
295
- ));
296
- }
297
287
  router.post('/load', post(
298
288
  parseLoadMessage,
299
289
  (user, loadMessage) => this.load(user, loadMessage)
@@ -327,26 +317,22 @@ export class HttpRouter {
327
317
  };
328
318
  }
329
319
 
330
- private async query(user: RequestUser | null, queryMessage: QueryMessage): Promise<QueryResponse> {
331
- const userIdentity = serializeUserIdentity(user);
332
- const query = fromDescriptiveString(queryMessage.query);
333
- const result = await this.authorization.query(userIdentity, queryMessage.start, query);
334
- return {
335
- results: result
336
- };
337
- }
338
-
339
320
  private async load(user: RequestUser, loadMessage: LoadMessage): Promise<LoadResponse> {
340
321
  const userIdentity = serializeUserIdentity(user);
341
322
  const result = await this.authorization.load(userIdentity, loadMessage.references);
323
+ const facts = result.map(r => r.fact);
342
324
  return {
343
- facts: result
325
+ facts
344
326
  };
345
327
  }
346
328
 
347
329
  private async save(user: RequestUser | null, saveMessage: SaveMessage): Promise<void> {
348
330
  const userIdentity = serializeUserIdentity(user);
349
- await this.authorization.save(userIdentity, saveMessage.facts);
331
+ await this.authorization.save(userIdentity, saveMessage.facts
332
+ .map(fact => ({
333
+ fact: fact,
334
+ signatures: []
335
+ })));
350
336
  }
351
337
 
352
338
  private async read(user: RequestUser | null, input: string): Promise<any[]> {
@@ -379,7 +365,11 @@ export class HttpRouter {
379
365
  }
380
366
 
381
367
  const userIdentity = serializeUserIdentity(user);
382
- await this.authorization.save(userIdentity, factRecords);
368
+ await this.authorization.save(userIdentity, factRecords
369
+ .map(fact => ({
370
+ fact: fact,
371
+ signatures: []
372
+ })));
383
373
  }
384
374
 
385
375
  private async feeds(user: RequestUser | null, input: string): Promise<FeedsResponse> {
@@ -65,11 +65,10 @@ export class JinagaServer {
65
65
  const feedCache = new FeedCache();
66
66
  const authentication = createAuthentication(store, keystore, authorizationRules);
67
67
  const network = new NetworkNoOp();
68
- const factManager = new FactManager(authentication, fork, source, store, network);
68
+ const factManager = new FactManager(fork, source, store, network);
69
69
  const authorization = createAuthorization(authorizationRules, distributionRules, factManager, store, keystore);
70
- // Retain backward compatibility until the user provides distribution rules.
71
- const router = new HttpRouter(factManager, authorization, feedCache, distributionRules === null);
72
- const j: Jinaga = new Jinaga(factManager, syncStatusNotifier);
70
+ const router = new HttpRouter(factManager, authorization, feedCache);
71
+ const j: Jinaga = new Jinaga(authentication, factManager, syncStatusNotifier);
73
72
 
74
73
  async function close() {
75
74
  for (const pool of Object.values(pools)) {
@@ -187,8 +186,8 @@ async function withSession(store: Storage, keystore: Keystore | null, authorizat
187
186
  const fork = new PassThroughFork(store);
188
187
  const observableSource = new ObservableSource(store);
189
188
  const network = new NetworkNoOp();
190
- const factManager = new FactManager(authentication, fork, observableSource, store, network);
191
- const j = new Jinaga(factManager, syncStatusNotifier);
189
+ const factManager = new FactManager(fork, observableSource, store, network);
190
+ const j = new Jinaga(authentication, factManager, syncStatusNotifier);
192
191
  await callback(j);
193
192
  }
194
193
 
@@ -1,25 +1,18 @@
1
1
  import {
2
2
  canonicalPredecessors,
3
- Direction,
4
- ExistentialCondition,
5
3
  FactEnvelope,
6
4
  FactFeed,
7
- FactPath,
8
5
  FactRecord,
9
6
  FactReference,
10
7
  factReferenceEquals,
8
+ FactSignature,
11
9
  FactTuple,
12
10
  getAllFactTypes,
13
11
  getAllRoles,
14
- Join,
15
12
  PredecessorCollection,
16
13
  ProjectedResult,
17
- PropertyCondition,
18
- Query,
19
14
  Specification,
20
- Step,
21
- Storage,
22
- TopologicalSorter
15
+ Storage
23
16
  } from "jinaga";
24
17
  import { Pool, PoolClient } from "pg";
25
18
 
@@ -51,7 +44,6 @@ import {
51
44
  } from "./maps";
52
45
  import { ResultSetFact, ResultSetRow, ResultSetTree, resultSqlFromSpecification, SqlQueryTree } from "./specification-result-sql";
53
46
  import { sqlFromFeed } from "./specification-sql";
54
- import { sqlFromSteps } from "./sql";
55
47
 
56
48
  interface FactTypeResult {
57
49
  rows: {
@@ -96,6 +88,8 @@ interface AncestorResult {
96
88
  name: string;
97
89
  hash: string;
98
90
  data: string;
91
+ public_key: string;
92
+ signature: string;
99
93
  }[];
100
94
  }
101
95
 
@@ -125,21 +119,6 @@ function loadFactTuple(labels: SpecificationLabel[], row: Row): FactTuple {
125
119
  };
126
120
  }
127
121
 
128
- function loadFactPath(pathLength: number, factTypeNames: string[], r: Row): FactPath {
129
- let path: FactPath = [];
130
- for (let i = 0; i < pathLength; i++) {
131
- const hash = r['hash' + (i + 2)];
132
- if (!hash) {
133
- throw new Error(`Cannot find column 'hash${i + 2}'`);
134
- }
135
- path.push({
136
- type: factTypeNames[i],
137
- hash: hash
138
- });
139
- }
140
- return path;
141
- }
142
-
143
122
  export class PostgresStore implements Storage {
144
123
  private connectionFactory: ConnectionFactory;
145
124
  private factTypeMap: FactTypeMap = emptyFactTypeMap();
@@ -204,32 +183,6 @@ export class PostgresStore implements Storage {
204
183
  }
205
184
  }
206
185
 
207
- async query(start: FactReference, query: Query): Promise<FactPath[]> {
208
- if (query.steps.length === 0) {
209
- return [[start]];
210
- }
211
-
212
- try {
213
- const factTypes = await this.loadFactTypesFromSteps(query.steps, start.type);
214
- const roleMap = await this.loadRolesFromSteps(query.steps, factTypes, start.type);
215
-
216
- const sqlQuery = sqlFromSteps(start, query.steps, factTypes, roleMap, this.schema);
217
- if (!sqlQuery) {
218
- throw new Error(`Could not generate SQL for query "${query.toDescriptiveString()}" starting at "${start.type}"`);
219
- }
220
- if (sqlQuery.empty) {
221
- return [];
222
- }
223
- const { rows } = await this.connectionFactory.with(async (connection) => {
224
- return await connection.query(sqlQuery.sql, sqlQuery.parameters);
225
- });
226
- return rows.map(row => loadFactPath(sqlQuery.pathLength, sqlQuery.factTypeNames, row));
227
- }
228
- catch (e) {
229
- throw new Error(`Could not generate SQL for query "${query.toDescriptiveString()}" starting at "${start.type}": ${e}`);
230
- }
231
- }
232
-
233
186
  async read(start: FactReference[], specification: Specification): Promise<ProjectedResult[]> {
234
187
  const factTypes = await this.loadFactTypesFromSpecification(specification);
235
188
  const roleMap = await this.loadRolesFromSpecification(specification, factTypes);
@@ -253,7 +206,8 @@ export class PostgresStore implements Storage {
253
206
  const factReferences: FactReference[] = composer.findFactReferences(resultSets);
254
207
 
255
208
  // Load the references into a fact tree
256
- const factRecords = await this.load(factReferences);
209
+ const factEnvelopes = await this.load(factReferences);
210
+ const factRecords = factEnvelopes.map(envelope => envelope.fact);
257
211
 
258
212
  return composer.compose(resultSets, factRecords);
259
213
  }
@@ -279,22 +233,6 @@ export class PostgresStore implements Storage {
279
233
  };
280
234
  }
281
235
 
282
- private async loadFactTypesFromSteps(steps: Step[], startType: string): Promise<FactTypeMap> {
283
- const factTypes = this.factTypeMap;
284
- const unknownFactTypes = [...allFactTypes(steps), startType]
285
- .filter(factType => !factTypes.has(factType))
286
- .filter(distinct);
287
- if (unknownFactTypes.length > 0) {
288
- const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
289
- return await loadFactTypes(unknownFactTypes, connection, this.schema);
290
- });
291
- const merged = mergeFactTypes(this.factTypeMap, loadedFactTypes);
292
- this.factTypeMap = merged;
293
- return merged;
294
- }
295
- return factTypes;
296
- }
297
-
298
236
  async loadFactTypesFromFeed(feed: Specification): Promise<FactTypeMap> {
299
237
  const factTypes = this.factTypeMap;
300
238
  const unknownFactTypes = getAllFactTypes(feed)
@@ -325,21 +263,6 @@ export class PostgresStore implements Storage {
325
263
  return factTypes;
326
264
  }
327
265
 
328
- private async loadRolesFromSteps(steps: Step[], factTypes: FactTypeMap, initialType: string) {
329
- const roleMap = this.roleMap;
330
- const unknownRoles = allRoles(steps, factTypes, getFactTypeId(factTypes, initialType))
331
- .filter(r => !hasRole(roleMap, r.successor_type_id, r.role));
332
- if (unknownRoles.length > 0) {
333
- const loadedRoles = await this.connectionFactory.with(async (connection) => {
334
- return await loadRoles(unknownRoles, roleMap, connection, this.schema);
335
- });
336
- const merged = mergeRoleMaps(this.roleMap, loadedRoles);
337
- this.roleMap = merged;
338
- return merged;
339
- }
340
- return roleMap;
341
- }
342
-
343
266
  async loadRolesFromSpecification(specification: Specification, factTypes: FactTypeMap): Promise<RoleMap> {
344
267
  const roleMap = this.roleMap;
345
268
  const unknownRoles = getAllRoles(specification)
@@ -407,7 +330,7 @@ export class PostgresStore implements Storage {
407
330
  return existing;
408
331
  }
409
332
 
410
- async load(references: FactReference[]): Promise<FactRecord[]> {
333
+ async load(references: FactReference[]): Promise<FactEnvelope[]> {
411
334
  if (references.length === 0) {
412
335
  return [];
413
336
  }
@@ -419,23 +342,30 @@ export class PostgresStore implements Storage {
419
342
  const factParameters = flatten(references, (f) =>
420
343
  [f.hash, factTypes.get(f.type)]);
421
344
  const sql =
422
- 'SELECT f.fact_type_id, t.name, f.hash, f.data ' +
423
- `FROM ${this.schema}.fact f ` +
424
- `JOIN ${this.schema}.fact_type t ` +
425
- ' ON f.fact_type_id = t.fact_type_id ' +
426
- 'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
427
- ' ON v.fact_type_id = f.fact_type_id AND v.hash = f.hash ' +
428
- 'UNION ' +
429
- 'SELECT f2.fact_type_id, t.name, f2.hash, f2.data ' +
430
- `FROM ${this.schema}.fact f1 ` +
431
- 'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
432
- ' ON v.fact_type_id = f1.fact_type_id AND v.hash = f1.hash ' +
433
- `JOIN ${this.schema}.ancestor a ` +
434
- ' ON a.fact_id = f1.fact_id ' +
435
- `JOIN ${this.schema}.fact f2 ` +
436
- ' ON f2.fact_id = a.ancestor_fact_id ' +
437
- `JOIN ${this.schema}.fact_type t ` +
438
- ' ON t.fact_type_id = f2.fact_type_id;';
345
+ 'SELECT f.fact_type_id, f.name, f.hash, f.data, s.public_key, s.signature ' +
346
+ 'FROM (SELECT f.fact_id, f.fact_type_id, t.name, f.hash, f.data ' +
347
+ `FROM ${this.schema}.fact f ` +
348
+ `JOIN ${this.schema}.fact_type t ` +
349
+ ' ON f.fact_type_id = t.fact_type_id ' +
350
+ 'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
351
+ ' ON v.fact_type_id = f.fact_type_id AND v.hash = f.hash ' +
352
+ 'UNION ' +
353
+ 'SELECT f2.fact_id, f2.fact_type_id, t.name, f2.hash, f2.data ' +
354
+ `FROM ${this.schema}.fact f1 ` +
355
+ 'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
356
+ ' ON v.fact_type_id = f1.fact_type_id AND v.hash = f1.hash ' +
357
+ `JOIN ${this.schema}.ancestor a ` +
358
+ ' ON a.fact_id = f1.fact_id ' +
359
+ `JOIN ${this.schema}.fact f2 ` +
360
+ ' ON f2.fact_id = a.ancestor_fact_id ' +
361
+ `JOIN ${this.schema}.fact_type t ` +
362
+ ' ON t.fact_type_id = f2.fact_type_id) as f ' +
363
+ 'LEFT JOIN (SELECT p.public_key, s.signature, s.fact_id ' +
364
+ `FROM ${this.schema}.signature s ` +
365
+ `JOIN ${this.schema}.public_key p ` +
366
+ ' ON p.public_key_id = s.public_key_id) as s ' +
367
+ 'ON s.fact_id = f.fact_id ' +
368
+ 'ORDER BY f.fact_id, s.public_key;';
439
369
  const result: AncestorResult = await this.connectionFactory.with(async (connection) => {
440
370
  return await connection.query(sql, factParameters);
441
371
  })
@@ -444,17 +374,46 @@ export class PostgresStore implements Storage {
444
374
  emptyFactTypeMap()
445
375
  );
446
376
  this.factTypeMap = mergeFactTypes(this.factTypeMap, resultFactTypes);
447
- const sorter = new TopologicalSorter<FactRecord>();
448
377
  const records = result.rows.map((r) => {
449
378
  const { fields, predecessors }: { fields: {}, predecessors: PredecessorCollection } = r.data as any;
450
- return <FactRecord>{
379
+ const fact: FactRecord = {
451
380
  type: r.name,
452
381
  hash: r.hash,
453
382
  fields,
454
383
  predecessors
384
+ };
385
+ const signature: FactSignature = {
386
+ publicKey: r.public_key,
387
+ signature: r.signature
388
+ };
389
+ return {
390
+ fact,
391
+ signature
455
392
  }
456
393
  });
457
- return sorter.sort(records, (p, r) => r);
394
+ // Group the records by fact type and hash.
395
+ const envelopes: FactEnvelope[] = records.reduce((acc: FactEnvelope[], record) => {
396
+ // Because the records are sorted by fact ID, we can just look at the last envelope.
397
+ const last = acc.length > 0 ? acc[acc.length - 1] : undefined;
398
+ if (last && last.fact.type === record.fact.type && last.fact.hash === record.fact.hash) {
399
+ last.signatures.push(record.signature);
400
+ }
401
+ else if (record.signature.signature === null) {
402
+ acc.push({
403
+ fact: record.fact,
404
+ signatures: []
405
+ });
406
+ }
407
+ else {
408
+ acc.push({
409
+ fact: record.fact,
410
+ signatures: [record.signature]
411
+ });
412
+ }
413
+ return acc;
414
+ }, []);
415
+ // Assuming that the facts were inserted in topological order, they don't need to be sorted.
416
+ return envelopes;
458
417
  }
459
418
 
460
419
  private async loadFactTypesFromReferences(references: FactReference[]): Promise<FactTypeMap> {
@@ -894,51 +853,6 @@ async function insertSignatures(envelopes: FactEnvelope[], allFacts: FactMap, fa
894
853
  }
895
854
  }
896
855
 
897
- function allFactTypes(steps: Step[]): string[] {
898
- const factTypes = steps
899
- .filter(step => step instanceof PropertyCondition && step.name === 'type')
900
- .map(step => (step as PropertyCondition).value);
901
- const childFactTypes = steps
902
- .filter(step => step instanceof ExistentialCondition)
903
- .flatMap(step => allFactTypes((step as ExistentialCondition).steps));
904
- return [...factTypes, ...childFactTypes].filter(distinct);
905
- }
906
-
907
- function allRoles(steps: Step[], factTypes: FactTypeMap, initialTypeId: number | undefined) {
908
- let roles: { successor_type_id: number; role: string }[] = [];
909
- let successor_type_id = initialTypeId;
910
- let role: string | undefined = undefined;
911
-
912
- for (const step of steps) {
913
- if (step instanceof PropertyCondition) {
914
- if (step.name === 'type') {
915
- successor_type_id = getFactTypeId(factTypes, step.value);
916
- if (successor_type_id && role) {
917
- roles.push({ successor_type_id: successor_type_id, role });
918
- }
919
- role = undefined;
920
- }
921
- }
922
- else if (step instanceof Join) {
923
- if (step.direction === Direction.Predecessor) {
924
- if (successor_type_id) {
925
- roles.push({ successor_type_id: successor_type_id, role: step.role });
926
- }
927
- role = undefined;
928
- }
929
- else {
930
- role = step.role;
931
- }
932
- successor_type_id = undefined;
933
- }
934
- else if (step instanceof ExistentialCondition) {
935
- roles = roles.concat(allRoles(step.steps, factTypes, successor_type_id));
936
- }
937
- }
938
-
939
- return roles;
940
- }
941
-
942
856
  function predecessorsOf(fact: FactRecord): FactReference[] {
943
857
  const references = Object.values(fact.predecessors).flatMap(predecessor =>
944
858
  Array.isArray(predecessor) ? predecessor : [predecessor]