jinaga-server 2.1.8 → 2.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 (36) hide show
  1. package/dist/authorization/authorization-keystore.d.ts +2 -2
  2. package/dist/authorization/authorization-keystore.js +10 -10
  3. package/dist/authorization/authorization-keystore.js.map +1 -1
  4. package/dist/jinaga-server.d.ts +2 -2
  5. package/dist/jinaga-server.js +17 -29
  6. package/dist/jinaga-server.js.map +1 -1
  7. package/dist/postgres/postgres-keystore.d.ts +2 -1
  8. package/dist/postgres/postgres-keystore.js +5 -4
  9. package/dist/postgres/postgres-keystore.js.map +1 -1
  10. package/dist/postgres/postgres-store.d.ts +4 -1
  11. package/dist/postgres/postgres-store.js +60 -53
  12. package/dist/postgres/postgres-store.js.map +1 -1
  13. package/dist/postgres/specification-result-sql.d.ts +1 -1
  14. package/dist/postgres/specification-result-sql.js +21 -21
  15. package/dist/postgres/specification-result-sql.js.map +1 -1
  16. package/dist/postgres/specification-sql.d.ts +2 -2
  17. package/dist/postgres/specification-sql.js +19 -19
  18. package/dist/postgres/specification-sql.js.map +1 -1
  19. package/dist/postgres/sql.d.ts +1 -1
  20. package/dist/postgres/sql.js +14 -14
  21. package/dist/postgres/sql.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/authorization/authorization-keystore.ts +11 -11
  24. package/src/jinaga-server.ts +26 -40
  25. package/src/postgres/postgres-keystore.ts +4 -4
  26. package/src/postgres/postgres-store.ts +61 -53
  27. package/src/postgres/specification-result-sql.ts +21 -21
  28. package/src/postgres/specification-sql.ts +19 -19
  29. package/src/postgres/sql.ts +14 -14
  30. package/test/postgres/postgresReadSpec.ts +1 -1
  31. package/test/postgres/postgresSpec.ts +1 -1
  32. package/test/postgres/postgresSpecificationSpec.ts +1 -1
  33. package/dist/http/node-http.d.ts +0 -7
  34. package/dist/http/node-http.js +0 -135
  35. package/dist/http/node-http.js.map +0 -1
  36. package/src/http/node-http.ts +0 -115
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinaga-server",
3
- "version": "2.1.8",
3
+ "version": "2.3.0",
4
4
  "description": "Application-agnostic back end for web applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "express": "^4.18.1",
46
- "jinaga": "^5.1.4",
46
+ "jinaga": "^5.3.0",
47
47
  "node-forge": "^1.3.1",
48
48
  "passport": "^0.6.0",
49
49
  "pg": "^8.7.1"
@@ -19,12 +19,12 @@ export class AuthorizationKeystore implements Authorization {
19
19
  private authorizationEngine: AuthorizationEngine | null;
20
20
 
21
21
  constructor(
22
- private observableSource: Storage,
22
+ private store: Storage,
23
23
  private keystore: Keystore,
24
- authorizationRules: AuthorizationRules | null) {
25
-
24
+ authorizationRules: AuthorizationRules | null
25
+ ) {
26
26
  this.authorizationEngine = authorizationRules &&
27
- new AuthorizationEngine(authorizationRules, observableSource);
27
+ new AuthorizationEngine(authorizationRules, store);
28
28
  }
29
29
 
30
30
  async getOrCreateUserFact(userIdentity: UserIdentity) {
@@ -35,29 +35,29 @@ export class AuthorizationKeystore implements Authorization {
35
35
  signatures: []
36
36
  }
37
37
  ];
38
- await this.observableSource.save(envelopes);
38
+ await this.store.save(envelopes);
39
39
  return userFact;
40
40
  }
41
41
 
42
42
  query(userIdentity: UserIdentity, start: FactReference, query: Query) {
43
- return this.observableSource.query(start, query);
43
+ return this.store.query(start, query);
44
44
  }
45
45
 
46
46
  read(userIdentity: UserIdentity, start: FactReference[], specification: Specification) {
47
- return this.observableSource.read(start, specification);
47
+ return this.store.read(start, specification);
48
48
  }
49
49
 
50
50
  feed(userIdentity: UserIdentity, feed: Feed, bookmark: string): Promise<FactFeed> {
51
- return this.observableSource.feed(feed, bookmark);
51
+ return this.store.feed(feed, bookmark);
52
52
  }
53
53
 
54
54
  load(userIdentity: UserIdentity, references: FactReference[]) {
55
- return this.observableSource.load(references);
55
+ return this.store.load(references);
56
56
  }
57
57
 
58
58
  async save(userIdentity: UserIdentity | null, facts: FactRecord[]) {
59
59
  if (!this.authorizationEngine) {
60
- const envelopes = await this.observableSource.save(facts.map(fact => ({
60
+ const envelopes = await this.store.save(facts.map(fact => ({
61
61
  fact,
62
62
  signatures: []
63
63
  })));
@@ -68,7 +68,7 @@ export class AuthorizationKeystore implements Authorization {
68
68
  const authorizedFacts = await this.authorizationEngine.authorizeFacts(facts, userFact);
69
69
  if (userIdentity) {
70
70
  const signedFacts = await this.keystore.signFacts(userIdentity, authorizedFacts);
71
- const envelopes = await this.observableSource.save(signedFacts);
71
+ const envelopes = await this.store.save(signedFacts);
72
72
  return envelopes.map(envelope => envelope.fact);
73
73
  }
74
74
  else {
@@ -5,9 +5,7 @@ import {
5
5
  AuthorizationNoOp,
6
6
  AuthorizationRules,
7
7
  FactManager,
8
- Fork,
9
- HttpNetwork,
10
- Jinaga,
8
+ Fork, Jinaga,
11
9
  MemoryStore,
12
10
  Model,
13
11
  Network,
@@ -17,17 +15,13 @@ import {
17
15
  PassThroughFork,
18
16
  Storage,
19
17
  SyncStatusNotifier,
20
- Trace,
21
- TransientFork,
22
- UserIdentity,
23
- WebClient
18
+ Trace, UserIdentity
24
19
  } from "jinaga";
25
20
  import { Pool } from "pg";
26
21
 
27
22
  import { AuthenticationDevice } from "./authentication/authentication-device";
28
23
  import { AuthenticationSession } from "./authentication/authentication-session";
29
24
  import { AuthorizationKeystore } from "./authorization/authorization-keystore";
30
- import { NodeHttpConnection } from "./http/node-http";
31
25
  import { HttpRouter, RequestUser } from "./http/router";
32
26
  import { Keystore } from "./keystore";
33
27
  import { MemoryFeedCache } from "./memory/memory-feed-cache";
@@ -37,11 +31,11 @@ import { PostgresStore } from "./postgres/postgres-store";
37
31
 
38
32
  export type JinagaServerConfig = {
39
33
  pgStore?: string | Pool,
34
+ pgStoreSchema?: string,
40
35
  pgKeystore?: string | Pool,
41
- httpEndpoint?: string,
36
+ pgKeystoreSchema?: string,
42
37
  model?: Model,
43
- authorization?: (a: AuthorizationRules) => AuthorizationRules,
44
- httpTimeoutSeconds?: number
38
+ authorization?: (a: AuthorizationRules) => AuthorizationRules
45
39
  };
46
40
 
47
41
  export type JinagaServerInstance = {
@@ -62,14 +56,14 @@ export class JinagaServer {
62
56
  const pools: { [uri: string]: Pool } = {};
63
57
  const store = createStore(config, pools);
64
58
  const source = new ObservableSourceImpl(store);
65
- const fork = createFork(config, store, syncStatusNotifier);
59
+ const fork = new PassThroughFork(store);
66
60
  const keystore = createKeystore(config, pools);
67
61
  const authorizationRules = config.authorization ? config.authorization(new AuthorizationRules(config.model)) : null;
68
62
  const authorization = createAuthorization(authorizationRules, store, keystore);
69
63
  const feedCache = new MemoryFeedCache();
70
64
  const router = new HttpRouter(authorization, feedCache);
71
65
  const authentication = createAuthentication(store, keystore, authorizationRules);
72
- const network = createNetwork(config);
66
+ const network = new NetworkNoOp();
73
67
  const factManager = new FactManager(authentication, fork, source, store, network);
74
68
  const j: Jinaga = new Jinaga(factManager, syncStatusNotifier);
75
69
 
@@ -92,7 +86,7 @@ export class JinagaServer {
92
86
  function createStore(config: JinagaServerConfig, pools: { [uri: string]: Pool }): Storage {
93
87
  if (config.pgStore) {
94
88
  const pool = getPool(config.pgStore, pools);
95
- const store = new PostgresStore(pool);
89
+ const store = new PostgresStore(pool, validateSchema(config.pgStoreSchema));
96
90
  return store;
97
91
  }
98
92
  else {
@@ -101,25 +95,14 @@ function createStore(config: JinagaServerConfig, pools: { [uri: string]: Pool })
101
95
  }
102
96
 
103
97
  function createFork(config: JinagaServerConfig, store: Storage, syncStatusNotifier: SyncStatusNotifier): Fork {
104
- if (config.httpEndpoint) {
105
- const httpConnection = new NodeHttpConnection(config.httpEndpoint);
106
- const httpTimeoutSeconds = config.httpTimeoutSeconds || 5;
107
- const webClient = new WebClient(httpConnection, syncStatusNotifier, {
108
- timeoutSeconds: httpTimeoutSeconds
109
- });
110
- const fork = new TransientFork(store, webClient);
111
- return fork;
112
- }
113
- else {
114
- return new PassThroughFork(store);
115
- }
98
+ return new PassThroughFork(store);
116
99
  }
117
100
 
118
101
  function createKeystore(config: JinagaServerConfig, pools: { [uri: string]: Pool }): Keystore | null {
119
102
  const uriOrPool = config.pgKeystore;
120
103
  if (uriOrPool) {
121
104
  const pool = getPool(uriOrPool, pools);
122
- const keystore = new PostgresKeystore(pool);
105
+ const keystore = new PostgresKeystore(pool, validateSchema(config.pgKeystoreSchema));
123
106
  return keystore;
124
107
  }
125
108
  else {
@@ -142,18 +125,7 @@ function createAuthentication(store: Storage, keystore: Keystore | null, authori
142
125
  }
143
126
 
144
127
  function createNetwork(config: JinagaServerConfig): Network {
145
- if (config.httpEndpoint) {
146
- const httpConnection = new NodeHttpConnection(config.httpEndpoint);
147
- const httpTimeoutSeconds = config.httpTimeoutSeconds || 5;
148
- const syncStatusNotifier = new SyncStatusNotifier();
149
- const webClient = new WebClient(httpConnection, syncStatusNotifier, {
150
- timeoutSeconds: httpTimeoutSeconds
151
- });
152
- return new HttpNetwork(webClient);
153
- }
154
- else {
155
- return new NetworkNoOp();
156
- }
128
+ return new NetworkNoOp();
157
129
  }
158
130
 
159
131
  function getPool(uriOrPool: string | Pool, pools: { [uri: string]: Pool; }): Pool {
@@ -222,4 +194,18 @@ async function withSession(store: Storage, keystore: Keystore | null, authorizat
222
194
  const factManager = new FactManager(authentication, fork, observableSource, store, network);
223
195
  const j = new Jinaga(factManager, syncStatusNotifier);
224
196
  await callback(j);
225
- }
197
+ }
198
+
199
+ function validateSchema(schema: string | undefined): string {
200
+ if (!schema) {
201
+ return "public";
202
+ }
203
+
204
+ // Verify that the schema is a valid Postgres schema name.
205
+ // https://www.postgresql.org/docs/9.1/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
206
+ if (!/^[a-z_][a-z0-9_$]*$/.test(schema)) {
207
+ throw new Error(`Invalid schema name: ${schema}. Schema names must start with a letter or underscore, and contain only letters, numbers, and underscores.`);
208
+ }
209
+
210
+ return schema;
211
+ }
@@ -14,7 +14,7 @@ export class PostgresKeystore implements Keystore {
14
14
  private connectionFactory: ConnectionFactory;
15
15
  private cache: Map<string, KeyPair> = new Map();
16
16
 
17
- constructor (pool: Pool) {
17
+ constructor (pool: Pool, private schema: string) {
18
18
  this.connectionFactory = new ConnectionFactory(pool);
19
19
  }
20
20
 
@@ -77,7 +77,7 @@ export class PostgresKeystore implements Keystore {
77
77
  }
78
78
 
79
79
  private async selectKeyPair(connection: PoolClient, userIdentity: UserIdentity): Promise<KeyPair> {
80
- const { rows } = await connection.query('SELECT public_key, private_key FROM public.user WHERE provider = $1 AND user_identifier = $2',
80
+ const { rows } = await connection.query(`SELECT public_key, private_key FROM ${this.schema}.user WHERE provider = $1 AND user_identifier = $2`,
81
81
  [userIdentity.provider, userIdentity.id]);
82
82
  if (rows.length > 1) {
83
83
  throw new Error('Duplicate entries found in the keystore');
@@ -104,7 +104,7 @@ export class PostgresKeystore implements Keystore {
104
104
  }
105
105
 
106
106
  private async selectOrInsertKeyPair(connection: PoolClient, userIdentity: UserIdentity): Promise<KeyPair> {
107
- const { rows } = await connection.query('SELECT public_key, private_key FROM public.user WHERE provider = $1 AND user_identifier = $2',
107
+ const { rows } = await connection.query(`SELECT public_key, private_key FROM ${this.schema}.user WHERE provider = $1 AND user_identifier = $2`,
108
108
  [userIdentity.provider, userIdentity.id]);
109
109
  if (rows.length > 1) {
110
110
  throw new Error('Duplicate entries found in the keystore');
@@ -124,7 +124,7 @@ export class PostgresKeystore implements Keystore {
124
124
  const keypair = pki.rsa.generateKeyPair({ bits: 2048 });
125
125
  const privatePem = pki.privateKeyToPem(keypair.privateKey);
126
126
  const publicPem = pki.publicKeyToPem(keypair.publicKey);
127
- await connection.query('INSERT INTO public.user (provider, user_identifier, private_key, public_key) VALUES ($1, $2, $3, $4)',
127
+ await connection.query(`INSERT INTO ${this.schema}.user (provider, user_identifier, private_key, public_key) VALUES ($1, $2, $3, $4)`,
128
128
  [userIdentity.provider, userIdentity.id, privatePem, publicPem]);
129
129
  return { publicPem, privatePem };
130
130
  }
@@ -147,7 +147,7 @@ export class PostgresStore implements Storage {
147
147
  private factTypeMap: FactTypeMap = emptyFactTypeMap();
148
148
  private roleMap: RoleMap = emptyRoleMap();
149
149
 
150
- constructor (pool: Pool) {
150
+ constructor (pool: Pool, private schema: string) {
151
151
  this.connectionFactory = new ConnectionFactory(pool);
152
152
  }
153
153
 
@@ -166,8 +166,8 @@ export class PostgresStore implements Storage {
166
166
  factTypes: FactTypeMap;
167
167
  roles: RoleMap;
168
168
  } = await this.connectionFactory.withTransaction(async (connection) => {
169
- const factTypes = await storeFactTypes(facts, this.factTypeMap, connection);
170
- const existingFacts = await findExistingFacts(facts, factTypes, connection);
169
+ const factTypes = await storeFactTypes(facts, this.factTypeMap, connection, this.schema);
170
+ const existingFacts = await findExistingFacts(facts, factTypes, connection, this.schema);
171
171
  const newFacts = facts.filter(f => !hasFact(existingFacts, f.hash, ensureGetFactTypeId(factTypes, f.type)));
172
172
  if (newFacts.length === 0) {
173
173
  return {
@@ -177,8 +177,8 @@ export class PostgresStore implements Storage {
177
177
  };
178
178
  }
179
179
 
180
- const roles = await storeRoles(newFacts, factTypes, copyRoleMap(this.roleMap), connection);
181
- const allFacts = await insertFactsEdgesAndAncestors(newFacts, factTypes, existingFacts, connection, roles);
180
+ const roles = await storeRoles(newFacts, factTypes, copyRoleMap(this.roleMap), connection, this.schema);
181
+ const allFacts = await insertFactsEdgesAndAncestors(newFacts, factTypes, existingFacts, connection, roles, this.schema);
182
182
  const newEnvelopes = envelopes.filter(envelope => newFacts.some(
183
183
  factReferenceEquals(envelope.fact)));
184
184
  if (newEnvelopes.length === 0) {
@@ -189,8 +189,8 @@ export class PostgresStore implements Storage {
189
189
  };
190
190
  }
191
191
 
192
- const publicKeys = await storePublicKeys(newEnvelopes, connection);
193
- await insertSignatures(newEnvelopes, allFacts, factTypes, publicKeys, connection);
192
+ const publicKeys = await storePublicKeys(newEnvelopes, connection, this.schema);
193
+ await insertSignatures(newEnvelopes, allFacts, factTypes, publicKeys, connection, this.schema);
194
194
  return {
195
195
  newEnvelopes,
196
196
  factTypes,
@@ -215,7 +215,7 @@ export class PostgresStore implements Storage {
215
215
  const factTypes = await this.loadFactTypesFromSteps(query.steps, start.type);
216
216
  const roleMap = await this.loadRolesFromSteps(query.steps, factTypes, start.type);
217
217
 
218
- const sqlQuery = sqlFromSteps(start, query.steps, factTypes, roleMap);
218
+ const sqlQuery = sqlFromSteps(start, query.steps, factTypes, roleMap, this.schema);
219
219
  if (!sqlQuery) {
220
220
  throw new Error(`Could not generate SQL for query "${query.toDescriptiveString()}" starting at "${start.type}"`);
221
221
  }
@@ -241,7 +241,7 @@ export class PostgresStore implements Storage {
241
241
  return [];
242
242
  }
243
243
 
244
- const composer = resultSqlFromSpecification(start, specification, factTypes, roleMap);
244
+ const composer = resultSqlFromSpecification(start, specification, factTypes, roleMap, this.schema);
245
245
  if (composer === null) {
246
246
  return [];
247
247
  }
@@ -256,7 +256,7 @@ export class PostgresStore implements Storage {
256
256
  async feed(feed: Feed, bookmark: string): Promise<FactFeed> {
257
257
  const factTypes: FactTypeMap = await this.loadFactTypesFromFeed(feed);
258
258
  const roleMap: RoleMap = await this.loadRolesFromFeed(feed, factTypes);
259
- const sql = sqlFromFeed(feed, bookmark, 100, factTypes, roleMap);
259
+ const sql = sqlFromFeed(feed, this.schema, bookmark, 100, factTypes, roleMap);
260
260
  if (!sql) {
261
261
  return {
262
262
  tuples: [],
@@ -281,7 +281,7 @@ export class PostgresStore implements Storage {
281
281
  .filter(distinct);
282
282
  if (unknownFactTypes.length > 0) {
283
283
  const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
284
- return await loadFactTypes(unknownFactTypes, connection);
284
+ return await loadFactTypes(unknownFactTypes, connection, this.schema);
285
285
  });
286
286
  const merged = mergeFactTypes(this.factTypeMap, loadedFactTypes);
287
287
  this.factTypeMap = merged;
@@ -296,7 +296,7 @@ export class PostgresStore implements Storage {
296
296
  .filter(factType => !factTypes.has(factType));
297
297
  if (unknownFactTypes.length > 0) {
298
298
  const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
299
- return await loadFactTypes(unknownFactTypes, connection);
299
+ return await loadFactTypes(unknownFactTypes, connection, this.schema);
300
300
  });
301
301
  const merged = mergeFactTypes(this.factTypeMap, loadedFactTypes);
302
302
  this.factTypeMap = merged;
@@ -311,7 +311,7 @@ export class PostgresStore implements Storage {
311
311
  .filter(factType => !factTypes.has(factType));
312
312
  if (unknownFactTypes.length > 0) {
313
313
  const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
314
- return await loadFactTypes(unknownFactTypes, connection);
314
+ return await loadFactTypes(unknownFactTypes, connection, this.schema);
315
315
  });
316
316
  const merged = mergeFactTypes(this.factTypeMap, loadedFactTypes);
317
317
  this.factTypeMap = merged;
@@ -326,7 +326,7 @@ export class PostgresStore implements Storage {
326
326
  .filter(r => !hasRole(roleMap, r.successor_type_id, r.role));
327
327
  if (unknownRoles.length > 0) {
328
328
  const loadedRoles = await this.connectionFactory.with(async (connection) => {
329
- return await loadRoles(unknownRoles, roleMap, connection);
329
+ return await loadRoles(unknownRoles, roleMap, connection, this.schema);
330
330
  });
331
331
  const merged = mergeRoleMaps(this.roleMap, loadedRoles);
332
332
  this.roleMap = merged;
@@ -346,7 +346,7 @@ export class PostgresStore implements Storage {
346
346
  .filter(r => r.successor_type_id && !hasRole(roleMap, r.successor_type_id, r.role));
347
347
  if (unknownRoles.length > 0) {
348
348
  const loadedRoles = await this.connectionFactory.with(async (connection) => {
349
- return await loadRoles(unknownRoles, roleMap, connection);
349
+ return await loadRoles(unknownRoles, roleMap, connection, this.schema);
350
350
  });
351
351
  const merged = mergeRoleMaps(this.roleMap, loadedRoles);
352
352
  this.roleMap = merged;
@@ -365,7 +365,7 @@ export class PostgresStore implements Storage {
365
365
  .filter(r => r.successor_type_id && !hasRole(roleMap, r.successor_type_id, r.role));
366
366
  if (unknownRoles.length > 0) {
367
367
  const loadedRoles = await this.connectionFactory.with(async (connection) => {
368
- return await loadRoles(unknownRoles, roleMap, connection);
368
+ return await loadRoles(unknownRoles, roleMap, connection, this.schema);
369
369
  });
370
370
  const merged = mergeRoleMaps(this.roleMap, loadedRoles);
371
371
  this.roleMap = merged;
@@ -387,7 +387,7 @@ export class PostgresStore implements Storage {
387
387
  [f.hash, factTypes.get(f.type)]);
388
388
  const sql =
389
389
  'SELECT f.fact_type_id, f.hash ' +
390
- 'FROM public.fact f ' +
390
+ `FROM ${this.schema}.fact f ` +
391
391
  'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
392
392
  ' ON v.fact_type_id = f.fact_type_id AND v.hash = f.hash ';
393
393
  const result: ExistsResult = await this.connectionFactory.with(async (connection) => {
@@ -415,21 +415,21 @@ export class PostgresStore implements Storage {
415
415
  [f.hash, factTypes.get(f.type)]);
416
416
  const sql =
417
417
  'SELECT f.fact_type_id, t.name, f.hash, f.data ' +
418
- 'FROM public.fact f ' +
419
- 'JOIN public.fact_type t ' +
418
+ `FROM ${this.schema}.fact f ` +
419
+ `JOIN ${this.schema}.fact_type t ` +
420
420
  ' ON f.fact_type_id = t.fact_type_id ' +
421
421
  'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
422
422
  ' ON v.fact_type_id = f.fact_type_id AND v.hash = f.hash ' +
423
423
  'UNION ' +
424
424
  'SELECT f2.fact_type_id, t.name, f2.hash, f2.data ' +
425
- 'FROM public.fact f1 ' +
425
+ `FROM ${this.schema}.fact f1 ` +
426
426
  'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
427
427
  ' ON v.fact_type_id = f1.fact_type_id AND v.hash = f1.hash ' +
428
- 'JOIN public.ancestor a ' +
428
+ `JOIN ${this.schema}.ancestor a ` +
429
429
  ' ON a.fact_id = f1.fact_id ' +
430
- 'JOIN public.fact f2 ' +
430
+ `JOIN ${this.schema}.fact f2 ` +
431
431
  ' ON f2.fact_id = a.ancestor_fact_id ' +
432
- 'JOIN public.fact_type t ' +
432
+ `JOIN ${this.schema}.fact_type t ` +
433
433
  ' ON t.fact_type_id = f2.fact_type_id;';
434
434
  const result: AncestorResult = await this.connectionFactory.with(async (connection) => {
435
435
  return await connection.query(sql, factParameters);
@@ -460,7 +460,7 @@ export class PostgresStore implements Storage {
460
460
  .filter(distinct);
461
461
  if (newFactTypes.length > 0) {
462
462
  const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
463
- return await loadFactTypes(newFactTypes, connection);
463
+ return await loadFactTypes(newFactTypes, connection, this.schema);
464
464
  });
465
465
  const mergedFactTypes = mergeFactTypes(factTypes, loadedFactTypes);
466
466
  this.factTypeMap = mergedFactTypes;
@@ -476,6 +476,14 @@ export class PostgresStore implements Storage {
476
476
  saveBookmark(feed: string, bookmark: string): Promise<void> {
477
477
  throw new Error("Method not implemented.");
478
478
  }
479
+
480
+ getMruDate(specificationHash: string): Promise<Date | null> {
481
+ throw new Error("Method not implemented.");
482
+ }
483
+
484
+ setMruDate(specificationHash: string, mruDate: Date): Promise<void> {
485
+ throw new Error("Method not implemented.");
486
+ }
479
487
  }
480
488
 
481
489
  async function executeQueryTree(sqlQueryTree: SqlQueryTree, connection: PoolClient): Promise<ResultSetTree> {
@@ -506,7 +514,7 @@ function predecessorCollectionTypes(predecessors: PredecessorCollection): string
506
514
  return Object.keys(predecessors).flatMap(key => predecessorTypes(predecessors[key]));
507
515
  }
508
516
 
509
- async function storeFactTypes(facts: FactRecord[], factTypes: FactTypeMap, connection: PoolClient) {
517
+ async function storeFactTypes(facts: FactRecord[], factTypes: FactTypeMap, connection: PoolClient, schema: string) {
510
518
  const newFactTypes = facts
511
519
  .flatMap(fact => [fact.type, ...predecessorCollectionTypes(fact.predecessors)])
512
520
  .filter(type => !factTypes.has(type))
@@ -516,7 +524,7 @@ async function storeFactTypes(facts: FactRecord[], factTypes: FactTypeMap, conne
516
524
  }
517
525
 
518
526
  // Look up existing fact types
519
- const loadedFactTypes = await loadFactTypes(newFactTypes, connection);
527
+ const loadedFactTypes = await loadFactTypes(newFactTypes, connection, schema);
520
528
  const remainingNames = newFactTypes.filter(type => !loadedFactTypes.has(type));
521
529
  if (remainingNames.length === 0) {
522
530
  return mergeFactTypes(loadedFactTypes, factTypes);
@@ -524,7 +532,7 @@ async function storeFactTypes(facts: FactRecord[], factTypes: FactTypeMap, conne
524
532
 
525
533
  // Insert new fact types
526
534
  const values = remainingNames.map((name, index) => `($${index + 1})`);
527
- const insertSql = 'INSERT INTO public.fact_type (name) VALUES ' + values.join(', ') +
535
+ const insertSql = `INSERT INTO ${schema}.fact_type (name) VALUES ` + values.join(', ') +
528
536
  ' RETURNING fact_type_id, name;';
529
537
  const { rows: newRows }: FactTypeResult = await connection.query(insertSql, remainingNames);
530
538
  if (newRows.length !== remainingNames.length) {
@@ -537,8 +545,8 @@ async function storeFactTypes(facts: FactRecord[], factTypes: FactTypeMap, conne
537
545
  return allFactTypes;
538
546
  }
539
547
 
540
- async function loadFactTypes(factTypeNames: string[], connection: PoolClient) {
541
- const lookUpSql = 'SELECT name, fact_type_id FROM public.fact_type WHERE name=ANY($1);';
548
+ async function loadFactTypes(factTypeNames: string[], connection: PoolClient, schema: string) {
549
+ const lookUpSql = `SELECT name, fact_type_id FROM ${schema}.fact_type WHERE name=ANY($1);`;
542
550
  const { rows: existingRows }: FactTypeResult = await connection.query(lookUpSql, [factTypeNames]);
543
551
  const factTypeIds = existingRows.reduce(
544
552
  (map, row) => addFactType(map, row.name, row.fact_type_id),
@@ -547,7 +555,7 @@ async function loadFactTypes(factTypeNames: string[], connection: PoolClient) {
547
555
  return factTypeIds;
548
556
  }
549
557
 
550
- async function storeRoles(facts: FactRecord[], factTypes: FactTypeMap, roleMap: RoleMap, connection: PoolClient) {
558
+ async function storeRoles(facts: FactRecord[], factTypes: FactTypeMap, roleMap: RoleMap, connection: PoolClient, schema: string) {
551
559
  // Find distinct roles
552
560
  const roles = flatten(facts, fact => {
553
561
  const successor_type_id = ensureGetFactTypeId(factTypes, fact.type);
@@ -562,7 +570,7 @@ async function storeRoles(facts: FactRecord[], factTypes: FactTypeMap, roleMap:
562
570
 
563
571
  if (roles.length > 0) {
564
572
  // Look up existing roles
565
- const roleIds = await loadRoles(roles, roleMap, connection);
573
+ const roleIds = await loadRoles(roles, roleMap, connection, schema);
566
574
  const remainingRoles = roles.filter(role => !hasRole(
567
575
  roleIds, role.successor_type_id, role.role));
568
576
  if (remainingRoles.length === 0) {
@@ -572,7 +580,7 @@ async function storeRoles(facts: FactRecord[], factTypes: FactTypeMap, roleMap:
572
580
  // Insert new roles
573
581
  const remainingRoleValues = remainingRoles.map((role, index) =>
574
582
  `($${index * 2 + 1}, $${index * 2 + 2}::integer)`);
575
- const insertSql = 'INSERT INTO public.role (name, defining_fact_type_id) VALUES ' +
583
+ const insertSql = `INSERT INTO ${schema}.role (name, defining_fact_type_id) VALUES ` +
576
584
  remainingRoleValues.join(', ') +
577
585
  ' RETURNING role_id, name, defining_fact_type_id;';
578
586
  const remainingRoleParameters = flatten(remainingRoles, (role) => [
@@ -594,7 +602,7 @@ async function storeRoles(facts: FactRecord[], factTypes: FactTypeMap, roleMap:
594
602
  }
595
603
  }
596
604
 
597
- async function loadRoles(roles: { role: string; successor_type_id: number; }[], roleMap: RoleMap, connection: PoolClient) {
605
+ async function loadRoles(roles: { role: string; successor_type_id: number; }[], roleMap: RoleMap, connection: PoolClient, schema: string) {
598
606
  const roleValues = roles.map((role, index) =>
599
607
  `($${index * 2 + 1}, $${index * 2 + 2}::integer)`);
600
608
  const roleParameters = flatten(roles, (role) => [
@@ -603,7 +611,7 @@ async function loadRoles(roles: { role: string; successor_type_id: number; }[],
603
611
  ]);
604
612
 
605
613
  const lookUpSql = 'SELECT role.name, role.defining_fact_type_id, role.role_id' +
606
- ' FROM public.role' +
614
+ ` FROM ${schema}.role` +
607
615
  ' JOIN (VALUES ' + roleValues.join(', ') + ') AS v (name, defining_fact_type_id)' +
608
616
  ' ON v.name = role.name AND v.defining_fact_type_id = role.defining_fact_type_id;';
609
617
  const { rows }: RoleResult = await connection.query(lookUpSql, roleParameters);
@@ -614,7 +622,7 @@ async function loadRoles(roles: { role: string; successor_type_id: number; }[],
614
622
  return roleIds;
615
623
  }
616
624
 
617
- async function findExistingFacts(facts: FactRecord[], factTypes: FactTypeMap, connection: PoolClient) {
625
+ async function findExistingFacts(facts: FactRecord[], factTypes: FactTypeMap, connection: PoolClient, schema: string) {
618
626
  if (facts.length > 0) {
619
627
  const factReferences: FactReference[] = facts.flatMap(fact => [
620
628
  ...predecessorsOf(fact),
@@ -626,7 +634,7 @@ async function findExistingFacts(facts: FactRecord[], factTypes: FactTypeMap, co
626
634
  [f.hash, factTypes.get(f.type)]);
627
635
 
628
636
  const sql = 'SELECT fact_id, fact.fact_type_id, fact.hash' +
629
- ' FROM public.fact' +
637
+ ` FROM ${schema}.fact` +
630
638
  ' JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id)' +
631
639
  ' ON v.fact_type_id = fact.fact_type_id AND v.hash = fact.hash;';
632
640
  const { rows }: FactResult = await connection.query(sql, factParameters);
@@ -641,12 +649,12 @@ async function findExistingFacts(facts: FactRecord[], factTypes: FactTypeMap, co
641
649
  }
642
650
  }
643
651
 
644
- async function insertFactsEdgesAndAncestors(facts: FactRecord[], factTypes: FactTypeMap, existingFacts: Map<string, Map<number, number>>, connection: PoolClient, roles: RoleMap) {
652
+ async function insertFactsEdgesAndAncestors(facts: FactRecord[], factTypes: FactTypeMap, existingFacts: Map<string, Map<number, number>>, connection: PoolClient, roles: RoleMap, schema: string) {
645
653
  if (facts.length === 0) {
646
654
  return emptyFactMap();
647
655
  }
648
656
 
649
- const { sql, parameters } = sqlInsertFacts(facts, roles, factTypes);
657
+ const { sql, parameters } = sqlInsertFacts(facts, roles, factTypes, schema);
650
658
 
651
659
  const { rows }: FactResult = await connection.query(sql, parameters);
652
660
  if (rows.length !== facts.length) {
@@ -659,7 +667,7 @@ async function insertFactsEdgesAndAncestors(facts: FactRecord[], factTypes: Fact
659
667
  return allFacts;
660
668
  }
661
669
 
662
- function sqlInsertFacts(facts: FactRecord[], roles: RoleMap, factTypes: FactTypeMap) {
670
+ function sqlInsertFacts(facts: FactRecord[], roles: RoleMap, factTypes: FactTypeMap, schema: string) {
663
671
  const factValues = facts.map((f, i) => `(\$${i * 3 + 1}, \$${i * 3 + 2}::integer, \$${i * 3 + 3}::jsonb)`);
664
672
  const factParameters = flatten(facts, (f) => [f.hash, ensureGetFactTypeId(factTypes, f.type), {
665
673
  fields: f.fields,
@@ -668,11 +676,11 @@ function sqlInsertFacts(facts: FactRecord[], roles: RoleMap, factTypes: FactType
668
676
 
669
677
  const edgeRecords = flatten(facts, fact => makeEdgeRecords(fact));
670
678
  if (edgeRecords.length > 0) {
671
- return sqlInsertFactsEdgesAndAncestors(facts, factParameters, edgeRecords, factTypes, roles, factValues);
679
+ return sqlInsertFactsEdgesAndAncestors(facts, factParameters, edgeRecords, factTypes, roles, factValues, schema);
672
680
  }
673
681
  else {
674
682
  return {
675
- sql: 'INSERT INTO public.fact (hash, fact_type_id, data) VALUES ' +
683
+ sql: `INSERT INTO ${schema}.fact (hash, fact_type_id, data) VALUES ` +
676
684
  factValues.join(', ') +
677
685
  ' RETURNING fact_id, hash, fact_type_id;',
678
686
  parameters: factParameters
@@ -680,7 +688,7 @@ function sqlInsertFacts(facts: FactRecord[], roles: RoleMap, factTypes: FactType
680
688
  }
681
689
  }
682
690
 
683
- function sqlInsertFactsEdgesAndAncestors(facts: FactRecord[], factParameters: (string | number | { fields: {}; predecessors: PredecessorCollection; })[], edgeRecords: EdgeRecord[], factTypes: FactTypeMap, roles: RoleMap, factValues: string[]) {
691
+ function sqlInsertFactsEdgesAndAncestors(facts: FactRecord[], factParameters: (string | number | { fields: {}; predecessors: PredecessorCollection; })[], edgeRecords: EdgeRecord[], factTypes: FactTypeMap, roles: RoleMap, factValues: string[], schema: string) {
684
692
  let parameterOffset = factParameters.length;
685
693
  const edgeValues = edgeRecords.map((e, i) => `(\$${i * 5 + 1 + parameterOffset}, \$${i * 5 + 2 + parameterOffset}::integer, \$${i * 5 + 3 + parameterOffset}, \$${i * 5 + 4 + parameterOffset}::integer, \$${i * 5 + 5 + parameterOffset}::integer)`);
686
694
  const edgeParameters = flatten(edgeRecords, (e) => {
@@ -722,13 +730,13 @@ new_ancestor AS (
722
730
  AS ev (hash, fact_type_id, ancestor_hash, ancestor_fact_type_id)
723
731
  ),
724
732
  inserted_fact AS (
725
- INSERT INTO public.fact (hash, fact_type_id, data)
733
+ INSERT INTO ${schema}.fact (hash, fact_type_id, data)
726
734
  SELECT hash, fact_type_id, data
727
735
  FROM new_fact
728
736
  RETURNING fact_id, fact_type_id, hash
729
737
  ),
730
738
  inserted_edge AS (
731
- INSERT INTO public.edge
739
+ INSERT INTO ${schema}.edge
732
740
  (role_id, successor_fact_id, predecessor_fact_id)
733
741
  SELECT
734
742
  new_edge.role_id,
@@ -743,7 +751,7 @@ inserted_edge AS (
743
751
  FROM inserted_fact
744
752
  UNION ALL
745
753
  SELECT fact_id, fact_type_id, hash
746
- FROM public.fact
754
+ FROM ${schema}.fact
747
755
  ) AS predecessor
748
756
  ON predecessor.hash = new_edge.predecessor_hash
749
757
  AND predecessor.fact_type_id = new_edge.predecessor_fact_type_id
@@ -762,20 +770,20 @@ ancestor_id AS (
762
770
  FROM inserted_fact
763
771
  UNION ALL
764
772
  SELECT fact_id, fact_type_id, hash
765
- FROM public.fact
773
+ FROM ${schema}.fact
766
774
  ) AS ancestor
767
775
  ON ancestor.hash = new_ancestor.ancestor_hash
768
776
  AND ancestor.fact_type_id = new_ancestor.ancestor_fact_type_id
769
777
  ),
770
778
  inserted_ancestor AS (
771
- INSERT INTO public.ancestor
779
+ INSERT INTO ${schema}.ancestor
772
780
  (fact_id, ancestor_fact_id)
773
781
  SELECT ancestor_id.fact_id, ancestor_id.ancestor_fact_id
774
782
  FROM ancestor_id
775
783
  UNION ALL
776
784
  SELECT ancestor_id.fact_id, ancestor.ancestor_fact_id
777
785
  FROM ancestor_id
778
- JOIN public.ancestor
786
+ JOIN ${schema}.ancestor
779
787
  ON ancestor.fact_id = ancestor_id.ancestor_fact_id
780
788
  ON CONFLICT DO NOTHING
781
789
  )
@@ -816,11 +824,11 @@ function recursivePredecessors(factReference: FactReference, facts: FactRecord[]
816
824
  });
817
825
  }
818
826
 
819
- async function storePublicKeys(envelopes: FactEnvelope[], connection: PoolClient) {
827
+ async function storePublicKeys(envelopes: FactEnvelope[], connection: PoolClient, schema: string) {
820
828
  // Look up existing fact types
821
829
  const publicKeys = flatten(envelopes, e => e.signatures.map(s => s.publicKey))
822
830
  .filter(distinct);
823
- const lookUpSql = 'SELECT public_key, public_key_id FROM public.public_key WHERE public_key=ANY($1);';
831
+ const lookUpSql = `SELECT public_key, public_key_id FROM ${schema}.public_key WHERE public_key=ANY($1);`;
824
832
  const { rows: existingRows }: PublicKeyResult = await connection.query(lookUpSql, [publicKeys]);
825
833
  const publicKeyIds = existingRows.reduce(
826
834
  (map, row) => addFactType(map, row.public_key, row.public_key_id),
@@ -833,7 +841,7 @@ async function storePublicKeys(envelopes: FactEnvelope[], connection: PoolClient
833
841
 
834
842
  // Insert new fact types
835
843
  const values = remainingPublicKeys.map((name, index) => `($${index + 1})`);
836
- const insertSql = 'INSERT INTO public.public_key (public_key) VALUES ' + values.join(', ') +
844
+ const insertSql = `INSERT INTO ${schema}.public_key (public_key) VALUES ` + values.join(', ') +
837
845
  ' RETURNING public_key, public_key_id;';
838
846
  const { rows: newRows }: PublicKeyResult = await connection.query(insertSql, remainingPublicKeys);
839
847
  if (newRows.length !== remainingPublicKeys.length) {
@@ -846,7 +854,7 @@ async function storePublicKeys(envelopes: FactEnvelope[], connection: PoolClient
846
854
  return allPublicKeyIds;
847
855
  }
848
856
 
849
- async function insertSignatures(envelopes: FactEnvelope[], allFacts: FactMap, factTypes: FactTypeMap, publicKey: PublicKeyMap, connection: PoolClient) {
857
+ async function insertSignatures(envelopes: FactEnvelope[], allFacts: FactMap, factTypes: FactTypeMap, publicKey: PublicKeyMap, connection: PoolClient, schema: string) {
850
858
  const signatureRecords = flatten(envelopes, envelope => envelope.signatures.map(signature => ({
851
859
  factId: getFactId(allFacts, envelope.fact.hash, ensureGetFactTypeId(factTypes, envelope.fact.type)),
852
860
  publicKeyId: getPublicKeyId(publicKey, signature.publicKey),
@@ -858,7 +866,7 @@ async function insertSignatures(envelopes: FactEnvelope[], allFacts: FactMap, fa
858
866
  const signatureParameters = flatten(signatureRecords, s =>
859
867
  [s.factId, s.publicKeyId, s.signature]);
860
868
 
861
- const sql = `INSERT INTO public.signature
869
+ const sql = `INSERT INTO ${schema}.signature
862
870
  (fact_id, public_key_id, signature)
863
871
  (SELECT fact_id, public_key_id, signature
864
872
  FROM (VALUES ${signatureValues.join(', ')}) AS v(fact_id, public_key_id, signature))