jinaga-server 1.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 (127) hide show
  1. package/LICENSE +22 -0
  2. package/dist/authentication/authentication-device.d.ts +20 -0
  3. package/dist/authentication/authentication-device.js +61 -0
  4. package/dist/authentication/authentication-device.js.map +1 -0
  5. package/dist/authentication/authentication-impl.d.ts +21 -0
  6. package/dist/authentication/authentication-impl.js +61 -0
  7. package/dist/authentication/authentication-impl.js.map +1 -0
  8. package/dist/authentication/authentication-session.d.ts +23 -0
  9. package/dist/authentication/authentication-session.js +85 -0
  10. package/dist/authentication/authentication-session.js.map +1 -0
  11. package/dist/authentication/authentication.d.ts +5 -0
  12. package/dist/authentication/authentication.js +3 -0
  13. package/dist/authentication/authentication.js.map +1 -0
  14. package/dist/authentication.d.ts +17 -0
  15. package/dist/authentication.js +52 -0
  16. package/dist/authentication.js.map +1 -0
  17. package/dist/authorization/authorization-keystore.d.ts +13 -0
  18. package/dist/authorization/authorization-keystore.js +61 -0
  19. package/dist/authorization/authorization-keystore.js.map +1 -0
  20. package/dist/http/node-http.d.ts +7 -0
  21. package/dist/http/node-http.js +126 -0
  22. package/dist/http/node-http.js.map +1 -0
  23. package/dist/http/router.d.ts +20 -0
  24. package/dist/http/router.js +223 -0
  25. package/dist/http/router.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +6 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/jinaga-server.d.ts +18 -0
  30. package/dist/jinaga-server.js +101 -0
  31. package/dist/jinaga-server.js.map +1 -0
  32. package/dist/keystore.d.ts +9 -0
  33. package/dist/keystore.js +3 -0
  34. package/dist/keystore.js.map +1 -0
  35. package/dist/memory/memory-keystore.d.ts +16 -0
  36. package/dist/memory/memory-keystore.js +86 -0
  37. package/dist/memory/memory-keystore.js.map +1 -0
  38. package/dist/postgres/connection.d.ts +12 -0
  39. package/dist/postgres/connection.js +83 -0
  40. package/dist/postgres/connection.js.map +1 -0
  41. package/dist/postgres/edge-record.d.ts +9 -0
  42. package/dist/postgres/edge-record.js +30 -0
  43. package/dist/postgres/edge-record.js.map +1 -0
  44. package/dist/postgres/maps.d.ts +21 -0
  45. package/dist/postgres/maps.js +86 -0
  46. package/dist/postgres/maps.js.map +1 -0
  47. package/dist/postgres/postgres-keystore.d.ts +20 -0
  48. package/dist/postgres/postgres-keystore.js +168 -0
  49. package/dist/postgres/postgres-keystore.js.map +1 -0
  50. package/dist/postgres/postgres-store.d.ts +20 -0
  51. package/dist/postgres/postgres-store.js +717 -0
  52. package/dist/postgres/postgres-store.js.map +1 -0
  53. package/dist/postgres/query-description-builder.d.ts +13 -0
  54. package/dist/postgres/query-description-builder.js +104 -0
  55. package/dist/postgres/query-description-builder.js.map +1 -0
  56. package/dist/postgres/query-description.d.ts +71 -0
  57. package/dist/postgres/query-description.js +256 -0
  58. package/dist/postgres/query-description.js.map +1 -0
  59. package/dist/postgres/specification-result-sql.d.ts +41 -0
  60. package/dist/postgres/specification-result-sql.js +260 -0
  61. package/dist/postgres/specification-result-sql.js.map +1 -0
  62. package/dist/postgres/specification-sql.d.ts +3 -0
  63. package/dist/postgres/specification-sql.js +119 -0
  64. package/dist/postgres/specification-sql.js.map +1 -0
  65. package/dist/postgres/sql.d.ts +10 -0
  66. package/dist/postgres/sql.js +396 -0
  67. package/dist/postgres/sql.js.map +1 -0
  68. package/dist/util/fn.d.ts +6 -0
  69. package/dist/util/fn.js +81 -0
  70. package/dist/util/fn.js.map +1 -0
  71. package/dist/util/promise.d.ts +1 -0
  72. package/dist/util/promise.js +10 -0
  73. package/dist/util/promise.js.map +1 -0
  74. package/docs/registrations.sql +22 -0
  75. package/docs/specification.txt +41 -0
  76. package/docs/sql-generator.gv +25 -0
  77. package/integration-test/docker-compose.yaml +13 -0
  78. package/integration-test/jinaga-test/Dockerfile +12 -0
  79. package/integration-test/jinaga-test/jinaga.test.js +425 -0
  80. package/integration-test/jinaga-test/package-lock.json +6767 -0
  81. package/integration-test/jinaga-test/package.json +25 -0
  82. package/integration-test/postgres/0-init-app-db.sh +16 -0
  83. package/integration-test/postgres/1-setup.sql +219 -0
  84. package/integration-test/postgres/2-grant.sh +17 -0
  85. package/integration-test/postgres/Dockerfile +5 -0
  86. package/integration-test/run.sh +17 -0
  87. package/integration-test/start_db.sh +2 -0
  88. package/integration-test/stop_db.sh +3 -0
  89. package/integration-test/test.sh +8 -0
  90. package/interactive/index.js +56 -0
  91. package/jest.config.js +12 -0
  92. package/package.json +51 -0
  93. package/scripts/combined.sql +46 -0
  94. package/scripts/cte.sql +55 -0
  95. package/scripts/separate.sql +27 -0
  96. package/setup.sh +34 -0
  97. package/setup.sql +329 -0
  98. package/src/authentication/authentication-device.ts +68 -0
  99. package/src/authentication/authentication-impl.ts +59 -0
  100. package/src/authentication/authentication-session.ts +95 -0
  101. package/src/authentication/authentication.ts +6 -0
  102. package/src/authentication.ts +45 -0
  103. package/src/authorization/authorization-keystore.ts +67 -0
  104. package/src/http/node-http.ts +106 -0
  105. package/src/http/router.ts +237 -0
  106. package/src/index.ts +1 -0
  107. package/src/jinaga-server.ts +125 -0
  108. package/src/keystore.ts +10 -0
  109. package/src/memory/memory-keystore.ts +93 -0
  110. package/src/postgres/connection.ts +73 -0
  111. package/src/postgres/edge-record.ts +36 -0
  112. package/src/postgres/maps.ts +89 -0
  113. package/src/postgres/postgres-keystore.ts +167 -0
  114. package/src/postgres/postgres-store.ts +874 -0
  115. package/src/postgres/query-description-builder.ts +116 -0
  116. package/src/postgres/query-description.ts +385 -0
  117. package/src/postgres/specification-result-sql.ts +362 -0
  118. package/src/postgres/specification-sql.ts +138 -0
  119. package/src/postgres/sql.ts +482 -0
  120. package/src/util/fn.ts +63 -0
  121. package/src/util/promise.ts +5 -0
  122. package/test/authorization/authorizationSpec.ts +209 -0
  123. package/test/postgres/postgresSpec.ts +385 -0
  124. package/test/postgres/postgresSpecificationSpec.ts +742 -0
  125. package/test/reporter.js +25 -0
  126. package/tsconfig.json +25 -0
  127. package/upgrade.sh +21 -0
@@ -0,0 +1,874 @@
1
+ import {
2
+ canonicalPredecessors,
3
+ Direction,
4
+ ExistentialCondition,
5
+ FactBookmark,
6
+ FactEnvelope,
7
+ FactPath,
8
+ FactRecord,
9
+ FactReference,
10
+ factReferenceEquals,
11
+ FactStream,
12
+ FactTuple,
13
+ getAllFactTypes,
14
+ getAllRoles,
15
+ Join,
16
+ PredecessorCollection,
17
+ PropertyCondition,
18
+ Query,
19
+ Specification,
20
+ Step,
21
+ Storage,
22
+ } from "jinaga";
23
+ import { PoolClient } from "pg";
24
+
25
+ import { distinct, flatten } from "../util/fn";
26
+ import { ConnectionFactory, Row } from "./connection";
27
+ import { EdgeRecord, makeEdgeRecords } from "./edge-record";
28
+ import {
29
+ addFact,
30
+ addFactType,
31
+ addRole,
32
+ copyRoleMap,
33
+ emptyFactMap,
34
+ emptyFactTypeMap,
35
+ emptyPublicKeyMap,
36
+ emptyRoleMap,
37
+ FactMap,
38
+ FactTypeMap,
39
+ getFactId,
40
+ getFactTypeId,
41
+ getPublicKeyId,
42
+ getRoleId,
43
+ hasFact,
44
+ hasRole,
45
+ mergeFactTypes,
46
+ mergeRoleMaps,
47
+ PublicKeyMap,
48
+ RoleMap,
49
+ } from "./maps";
50
+ import { SpecificationLabel } from "./query-description";
51
+ import { ResultSetTree, resultSqlFromSpecification, SqlQueryTree } from "./specification-result-sql";
52
+ import { sqlFromSpecification } from "./specification-sql";
53
+ import { sqlFromSteps } from "./sql";
54
+
55
+ interface FactTypeResult {
56
+ rows: {
57
+ fact_type_id: number;
58
+ name: string;
59
+ }[];
60
+ }
61
+
62
+ interface RoleResult {
63
+ rows: {
64
+ role_id: number;
65
+ name: string;
66
+ defining_fact_type_id: number;
67
+ }[];
68
+ }
69
+
70
+ interface FactResult {
71
+ rows: {
72
+ fact_id: number;
73
+ fact_type_id: number;
74
+ hash: string;
75
+ }[];
76
+ }
77
+
78
+ interface ExistsResult {
79
+ rows: {
80
+ fact_type_id: number;
81
+ hash: string;
82
+ }[];
83
+ }
84
+
85
+ interface PublicKeyResult {
86
+ rows: {
87
+ public_key_id: number;
88
+ public_key: string;
89
+ }[];
90
+ }
91
+
92
+ interface AncestorResult {
93
+ rows: {
94
+ fact_type_id: number;
95
+ name: string;
96
+ hash: string;
97
+ data: string;
98
+ }[];
99
+ }
100
+
101
+ function loadFactReference(r: Row): FactReference {
102
+ return {
103
+ type: r.type,
104
+ hash: r.hash
105
+ };
106
+ }
107
+
108
+ function loadFactTuple(labels: SpecificationLabel[], row: Row): FactTuple {
109
+ const facts = labels.map(label => {
110
+ const hashColumn = `hash${label.index}`;
111
+ const hash = row[hashColumn];
112
+ if (hash === null) {
113
+ const columns = Object.keys(row);
114
+ throw new Error(`Cannot find column '${hashColumn}'. Available columns: ${columns.join(', ')}`);
115
+ }
116
+ const fact: FactReference = {
117
+ type: label.type,
118
+ hash
119
+ }
120
+ return fact;
121
+ });
122
+ const bookmark: number[] = row.bookmark;
123
+ return {
124
+ facts,
125
+ bookmark: bookmark.join(".")
126
+ };
127
+ }
128
+
129
+ function loadFactPath(pathLength: number, factTypeNames: string[], r: Row): FactPath {
130
+ let path: FactPath = [];
131
+ for (let i = 0; i < pathLength; i++) {
132
+ const hash = r['hash' + (i + 2)];
133
+ if (!hash) {
134
+ throw new Error(`Cannot find column 'hash${i + 2}'`);
135
+ }
136
+ path.push({
137
+ type: factTypeNames[i],
138
+ hash: hash
139
+ });
140
+ }
141
+ return path;
142
+ }
143
+
144
+ export class PostgresStore implements Storage {
145
+ private connectionFactory: ConnectionFactory;
146
+ private factTypeMap: FactTypeMap = emptyFactTypeMap();
147
+ private roleMap: RoleMap = emptyRoleMap();
148
+
149
+ constructor (postgresUri: string) {
150
+ this.connectionFactory = new ConnectionFactory(postgresUri);
151
+ }
152
+
153
+ async close() {
154
+ await this.connectionFactory.close();
155
+ }
156
+
157
+ async save(envelopes: FactEnvelope[]): Promise<FactEnvelope[]> {
158
+ if (envelopes.length > 0) {
159
+ const facts = envelopes.map(e => e.fact);
160
+ if (facts.some(f => !f.hash || !f.type)) {
161
+ throw new Error('Attempted to save a fact with no hash or type.');
162
+ }
163
+ const { newEnvelopes, factTypes, roles } : {
164
+ newEnvelopes: FactEnvelope[];
165
+ factTypes: FactTypeMap;
166
+ roles: RoleMap;
167
+ } = await this.connectionFactory.withTransaction(async (connection) => {
168
+ const factTypes = await storeFactTypes(facts, this.factTypeMap, connection);
169
+ const existingFacts = await findExistingFacts(facts, factTypes, connection);
170
+ const newFacts = facts.filter(f => !hasFact(existingFacts, f.hash, factTypes.get(f.type)));
171
+ if (newFacts.length === 0) {
172
+ return {
173
+ newEnvelopes: [],
174
+ factTypes,
175
+ roles: emptyRoleMap()
176
+ };
177
+ }
178
+
179
+ const roles = await storeRoles(newFacts, factTypes, copyRoleMap(this.roleMap), connection);
180
+ const allFacts = await insertFactsEdgesAndAncestors(newFacts, factTypes, existingFacts, connection, roles);
181
+ const newEnvelopes = envelopes.filter(envelope => newFacts.some(
182
+ factReferenceEquals(envelope.fact)));
183
+ if (newEnvelopes.length === 0) {
184
+ return {
185
+ newEnvelopes,
186
+ factTypes,
187
+ roles
188
+ };
189
+ }
190
+
191
+ const publicKeys = await storePublicKeys(newEnvelopes, connection);
192
+ await insertSignatures(newEnvelopes, allFacts, factTypes, publicKeys, connection);
193
+ return {
194
+ newEnvelopes,
195
+ factTypes,
196
+ roles
197
+ };
198
+ });
199
+ this.factTypeMap = mergeFactTypes(this.factTypeMap, factTypes);
200
+ this.roleMap = mergeRoleMaps(this.roleMap, roles);
201
+ return newEnvelopes;
202
+ }
203
+ else {
204
+ return [];
205
+ }
206
+ }
207
+
208
+ async query(start: FactReference, query: Query): Promise<FactPath[]> {
209
+ if (query.steps.length === 0) {
210
+ return [[start]];
211
+ }
212
+
213
+ try {
214
+ const factTypes = await this.loadFactTypesFromSteps(query.steps, start.type);
215
+ const roleMap = await this.loadRolesFromSteps(query.steps, factTypes, start.type);
216
+
217
+ const sqlQuery = sqlFromSteps(start, query.steps, factTypes, roleMap);
218
+ if (!sqlQuery) {
219
+ throw new Error(`Could not generate SQL for query "${query.toDescriptiveString()}" starting at "${start.type}"`);
220
+ }
221
+ if (sqlQuery.empty) {
222
+ return [];
223
+ }
224
+ const { rows } = await this.connectionFactory.with(async (connection) => {
225
+ return await connection.query(sqlQuery.sql, sqlQuery.parameters);
226
+ });
227
+ return rows.map(row => loadFactPath(sqlQuery.pathLength, sqlQuery.factTypeNames, row));
228
+ }
229
+ catch (e) {
230
+ throw new Error(`Could not generate SQL for query "${query.toDescriptiveString()}" starting at "${start.type}": ${e}`);
231
+ }
232
+ }
233
+
234
+ async read(start: FactReference[], specification: Specification): Promise<any[]> {
235
+ const factTypes = await this.loadFactTypesFromSpecification(specification);
236
+ const roleMap = await this.loadRolesFromSpecification(specification, factTypes);
237
+
238
+ const composer = resultSqlFromSpecification(start, specification, factTypes, roleMap);
239
+ if (composer === null) {
240
+ return [];
241
+ }
242
+
243
+ const sqlQueryTree = composer.getSqlQueries();
244
+ const resultSets = await this.connectionFactory.with(async (connection) => {
245
+ return await executeQueryTree(sqlQueryTree, connection);
246
+ });
247
+ return composer.compose(resultSets);
248
+ }
249
+
250
+ async streamsFromSpecification(start: FactReference[], bookmarks: FactBookmark[], limit: number, specification: Specification): Promise<FactStream[]> {
251
+ const factTypes = await this.loadFactTypesFromSpecification(specification);
252
+ const roleMap = await this.loadRolesFromSpecification(specification, factTypes);
253
+
254
+ const sqlQueries = sqlFromSpecification(start, bookmarks, limit, specification, factTypes, roleMap);
255
+ const streams = await this.connectionFactory.with(async (connection) => {
256
+ const streams: FactStream[] = [];
257
+ for (const sqlQuery of sqlQueries) {
258
+ try {
259
+ const { rows } = await connection.query(sqlQuery.sql, sqlQuery.parameters);
260
+ const tuples = rows.map(row => loadFactTuple(sqlQuery.labels, row));
261
+ streams.push({
262
+ labels: sqlQuery.labels.map(l => l.name),
263
+ tuples,
264
+ bookmark: tuples.length > 0 ? tuples[tuples.length - 1].bookmark : sqlQuery.bookmark
265
+ });
266
+ }
267
+ catch (error) {
268
+ throw new Error(`Could not execute query "${sqlQuery.sql}", parameters ${sqlQuery.parameters}:\n${error}`);
269
+ }
270
+ }
271
+ return streams;
272
+ });
273
+ return streams;
274
+ }
275
+
276
+ private async loadFactTypesFromSteps(steps: Step[], startType: string): Promise<FactTypeMap> {
277
+ const factTypes = this.factTypeMap;
278
+ const unknownFactTypes = [...allFactTypes(steps), startType]
279
+ .filter(factType => !factTypes.has(factType))
280
+ .filter(distinct);
281
+ if (unknownFactTypes.length > 0) {
282
+ const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
283
+ return await loadFactTypes(unknownFactTypes, connection);
284
+ });
285
+ const merged = mergeFactTypes(this.factTypeMap, loadedFactTypes);
286
+ this.factTypeMap = merged;
287
+ return merged;
288
+ }
289
+ return factTypes;
290
+ }
291
+
292
+ async loadFactTypesFromSpecification(specification: Specification): Promise<FactTypeMap> {
293
+ const factTypes = this.factTypeMap;
294
+ const unknownFactTypes = getAllFactTypes(specification)
295
+ .filter(factType => !factTypes.has(factType));
296
+ if (unknownFactTypes.length > 0) {
297
+ const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
298
+ return await loadFactTypes(unknownFactTypes, connection);
299
+ });
300
+ const merged = mergeFactTypes(this.factTypeMap, loadedFactTypes);
301
+ this.factTypeMap = merged;
302
+ return merged;
303
+ }
304
+ return factTypes;
305
+ }
306
+
307
+ private async loadRolesFromSteps(steps: Step[], factTypes: FactTypeMap, initialType: string) {
308
+ const roleMap = this.roleMap;
309
+ const unknownRoles = allRoles(steps, factTypes, getFactTypeId(factTypes, initialType))
310
+ .filter(r => !hasRole(roleMap, r.defining_fact_type_id, r.role));
311
+ if (unknownRoles.length > 0) {
312
+ const loadedRoles = await this.connectionFactory.with(async (connection) => {
313
+ return await loadRoles(unknownRoles, roleMap, connection);
314
+ });
315
+ const merged = mergeRoleMaps(this.roleMap, loadedRoles);
316
+ this.roleMap = merged;
317
+ return merged;
318
+ }
319
+ return roleMap;
320
+ }
321
+
322
+ async loadRolesFromSpecification(specification: Specification, factTypes: FactTypeMap): Promise<RoleMap> {
323
+ const roleMap = this.roleMap;
324
+ const unknownRoles = getAllRoles(specification)
325
+ .map(r => ({
326
+ defining_fact_type_id: getFactTypeId(factTypes, r.definingFactType),
327
+ role: r.name
328
+ }))
329
+ .filter(r => !hasRole(roleMap, r.defining_fact_type_id, r.role));
330
+ if (unknownRoles.length > 0) {
331
+ const loadedRoles = await this.connectionFactory.with(async (connection) => {
332
+ return await loadRoles(unknownRoles, roleMap, connection);
333
+ });
334
+ const merged = mergeRoleMaps(this.roleMap, loadedRoles);
335
+ this.roleMap = merged;
336
+ return merged;
337
+ }
338
+ return roleMap;
339
+ }
340
+
341
+ async whichExist(references: FactReference[]): Promise<FactReference[]> {
342
+ if (references.length === 0) {
343
+ return [];
344
+ }
345
+
346
+ const factTypes = await this.loadFactTypesFromReferences(references);
347
+
348
+ const factValues = references.map((f, i) =>
349
+ `(\$${i * 2 + 1}, \$${i * 2 + 2}::integer)`);
350
+ const factParameters = flatten(references, (f) =>
351
+ [f.hash, factTypes.get(f.type)]);
352
+ const sql =
353
+ 'SELECT f.fact_type_id, f.hash ' +
354
+ 'FROM public.fact f ' +
355
+ 'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
356
+ ' ON v.fact_type_id = f.fact_type_id AND v.hash = f.hash ';
357
+ const result: ExistsResult = await this.connectionFactory.with(async (connection) => {
358
+ return await connection.query(sql, factParameters);
359
+ });
360
+
361
+ const existing = references.filter(r =>
362
+ result.rows.some(row =>
363
+ row.fact_type_id === factTypes.get(r.type) && row.hash === r.hash
364
+ )
365
+ );
366
+ return existing;
367
+ }
368
+
369
+ async load(references: FactReference[]): Promise<FactRecord[]> {
370
+ if (references.length === 0) {
371
+ return [];
372
+ }
373
+
374
+ const factTypes = await this.loadFactTypesFromReferences(references);
375
+
376
+ const factValues = references.map((f, i) =>
377
+ `(\$${i * 2 + 1}, \$${i * 2 + 2}::integer)`);
378
+ const factParameters = flatten(references, (f) =>
379
+ [f.hash, factTypes.get(f.type)]);
380
+ const sql =
381
+ 'SELECT f.fact_type_id, t.name, f.hash, f.data ' +
382
+ 'FROM public.fact f ' +
383
+ 'JOIN public.fact_type t ' +
384
+ ' ON f.fact_type_id = t.fact_type_id ' +
385
+ 'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
386
+ ' ON v.fact_type_id = f.fact_type_id AND v.hash = f.hash ' +
387
+ 'UNION ' +
388
+ 'SELECT f2.fact_type_id, t.name, f2.hash, f2.data ' +
389
+ 'FROM public.fact f1 ' +
390
+ 'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
391
+ ' ON v.fact_type_id = f1.fact_type_id AND v.hash = f1.hash ' +
392
+ 'JOIN public.ancestor a ' +
393
+ ' ON a.fact_id = f1.fact_id ' +
394
+ 'JOIN public.fact f2 ' +
395
+ ' ON f2.fact_id = a.ancestor_fact_id ' +
396
+ 'JOIN public.fact_type t ' +
397
+ ' ON t.fact_type_id = f2.fact_type_id;';
398
+ const result: AncestorResult = await this.connectionFactory.with(async (connection) => {
399
+ return await connection.query(sql, factParameters);
400
+ })
401
+ const resultFactTypes = result.rows.reduce(
402
+ (factTypes, r) => addFactType(factTypes, r.name, r.fact_type_id),
403
+ emptyFactTypeMap()
404
+ );
405
+ this.factTypeMap = mergeFactTypes(this.factTypeMap, resultFactTypes);
406
+ return result.rows.map((r) => {
407
+ const { fields, predecessors }: { fields: {}, predecessors: PredecessorCollection } = r.data as any;
408
+ return <FactRecord>{
409
+ type: r.name,
410
+ hash: r.hash,
411
+ fields,
412
+ predecessors
413
+ }
414
+ });
415
+ }
416
+
417
+ private async loadFactTypesFromReferences(references: FactReference[]): Promise<FactTypeMap> {
418
+ const factTypes = this.factTypeMap;
419
+ const newFactTypes = references
420
+ .map(reference => reference.type)
421
+ .filter(type => !factTypes.has(type))
422
+ .filter(distinct);
423
+ if (newFactTypes.length > 0) {
424
+ const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
425
+ return await loadFactTypes(newFactTypes, connection);
426
+ });
427
+ const mergedFactTypes = mergeFactTypes(factTypes, loadedFactTypes);
428
+ this.factTypeMap = mergedFactTypes;
429
+ return mergedFactTypes;
430
+ }
431
+ return factTypes;
432
+ }
433
+ }
434
+
435
+ async function executeQueryTree(sqlQueryTree: SqlQueryTree, connection: PoolClient): Promise<ResultSetTree> {
436
+ const sqlQuery = sqlQueryTree.sqlQuery;
437
+ const { rows } = await connection.query(sqlQuery.sql, sqlQuery.parameters);
438
+ const resultSets: ResultSetTree = {
439
+ resultSet: rows,
440
+ childResultSets: []
441
+ };
442
+ for (const child of sqlQueryTree.childQueries) {
443
+ const childResultSet = await executeQueryTree(child, connection);
444
+ resultSets.childResultSets.push({
445
+ name: child.name,
446
+ ...childResultSet
447
+ });
448
+ }
449
+ return resultSets;
450
+ }
451
+
452
+ function predecessorTypes(predecessor: FactReference[] | FactReference): string[] {
453
+ if (Array.isArray(predecessor)) {
454
+ return predecessor.map(p => p.type);
455
+ }
456
+ return [predecessor.type];
457
+ }
458
+
459
+ function predecessorCollectionTypes(predecessors: PredecessorCollection): string[] {
460
+ return Object.keys(predecessors).flatMap(key => predecessorTypes(predecessors[key]));
461
+ }
462
+
463
+ async function storeFactTypes(facts: FactRecord[], factTypes: FactTypeMap, connection: PoolClient) {
464
+ const newFactTypes = facts
465
+ .flatMap(fact => [fact.type, ...predecessorCollectionTypes(fact.predecessors)])
466
+ .filter(type => !factTypes.has(type))
467
+ .filter(distinct);
468
+ if (newFactTypes.length === 0) {
469
+ return factTypes;
470
+ }
471
+
472
+ // Look up existing fact types
473
+ const loadedFactTypes = await loadFactTypes(newFactTypes, connection);
474
+ const remainingNames = newFactTypes.filter(type => !loadedFactTypes.has(type));
475
+ if (remainingNames.length === 0) {
476
+ return mergeFactTypes(loadedFactTypes, factTypes);
477
+ }
478
+
479
+ // Insert new fact types
480
+ const values = remainingNames.map((name, index) => `($${index + 1})`);
481
+ const insertSql = 'INSERT INTO public.fact_type (name) VALUES ' + values.join(', ') +
482
+ ' RETURNING fact_type_id, name;';
483
+ const { rows: newRows }: FactTypeResult = await connection.query(insertSql, remainingNames);
484
+ if (newRows.length !== remainingNames.length) {
485
+ throw new Error('Failed to insert all new fact types.');
486
+ }
487
+ const allFactTypes = newRows.reduce(
488
+ (map, row) => addFactType(map, row.name, row.fact_type_id),
489
+ mergeFactTypes(loadedFactTypes, factTypes)
490
+ );
491
+ return allFactTypes;
492
+ }
493
+
494
+ async function loadFactTypes(factTypeNames: string[], connection: PoolClient) {
495
+ const lookUpSql = 'SELECT name, fact_type_id FROM public.fact_type WHERE name=ANY($1);';
496
+ const { rows: existingRows }: FactTypeResult = await connection.query(lookUpSql, [factTypeNames]);
497
+ const factTypeIds = existingRows.reduce(
498
+ (map, row) => addFactType(map, row.name, row.fact_type_id),
499
+ emptyFactTypeMap()
500
+ );
501
+ return factTypeIds;
502
+ }
503
+
504
+ async function storeRoles(facts: FactRecord[], factTypes: FactTypeMap, roleMap: RoleMap, connection: PoolClient) {
505
+ // Find distinct roles
506
+ const roles = flatten(facts, fact => {
507
+ const defining_fact_type_id = factTypes.get(fact.type);
508
+ return Object.keys(fact.predecessors).map(role => ({
509
+ role,
510
+ defining_fact_type_id
511
+ }));
512
+ }).filter((role, index, array) => array.findIndex(r =>
513
+ r.role === role.role &&
514
+ r.defining_fact_type_id === role.defining_fact_type_id
515
+ ) === index);
516
+
517
+ if (roles.length > 0) {
518
+ // Look up existing roles
519
+ const roleIds = await loadRoles(roles, roleMap, connection);
520
+ const remainingRoles = roles.filter(role => !hasRole(
521
+ roleIds, role.defining_fact_type_id, role.role));
522
+ if (remainingRoles.length === 0) {
523
+ return roleIds;
524
+ }
525
+
526
+ // Insert new roles
527
+ const remainingRoleValues = remainingRoles.map((role, index) =>
528
+ `($${index * 2 + 1}, $${index * 2 + 2}::integer)`);
529
+ const insertSql = 'INSERT INTO public.role (name, defining_fact_type_id) VALUES ' +
530
+ remainingRoleValues.join(', ') +
531
+ ' RETURNING role_id, name, defining_fact_type_id;';
532
+ const remainingRoleParameters = flatten(remainingRoles, (role) => [
533
+ role.role,
534
+ role.defining_fact_type_id
535
+ ]);
536
+ const { rows: newRows }: RoleResult = await connection.query(insertSql, remainingRoleParameters);
537
+ if (newRows.length !== remainingRoles.length) {
538
+ throw new Error('Failed to insert all new roles.');
539
+ }
540
+ const allRoleIds = newRows.reduce(
541
+ (map, row) => addRole(map, row.defining_fact_type_id, row.name, row.role_id),
542
+ roleIds
543
+ );
544
+ return allRoleIds;
545
+ }
546
+ else {
547
+ return roleMap;
548
+ }
549
+ }
550
+
551
+ async function loadRoles(roles: { role: string; defining_fact_type_id: number; }[], roleMap: RoleMap, connection: PoolClient) {
552
+ const roleValues = roles.map((role, index) =>
553
+ `($${index * 2 + 1}, $${index * 2 + 2}::integer)`);
554
+ const roleParameters = flatten(roles, (role) => [
555
+ role.role,
556
+ role.defining_fact_type_id
557
+ ]);
558
+
559
+ const lookUpSql = 'SELECT role.name, role.defining_fact_type_id, role.role_id' +
560
+ ' FROM public.role' +
561
+ ' JOIN (VALUES ' + roleValues.join(', ') + ') AS v (name, defining_fact_type_id)' +
562
+ ' ON v.name = role.name AND v.defining_fact_type_id = role.defining_fact_type_id;';
563
+ const { rows }: RoleResult = await connection.query(lookUpSql, roleParameters);
564
+ const roleIds = rows.reduce(
565
+ (map, row) => addRole(map, row.defining_fact_type_id, row.name, row.role_id),
566
+ roleMap
567
+ );
568
+ return roleIds;
569
+ }
570
+
571
+ async function findExistingFacts(facts: FactRecord[], factTypes: FactTypeMap, connection: PoolClient) {
572
+ if (facts.length > 0) {
573
+ const factReferences: FactReference[] = facts.flatMap(fact => [
574
+ ...predecessorsOf(fact),
575
+ fact
576
+ ]);
577
+ const factValues = factReferences.map((f, i) =>
578
+ `(\$${i * 2 + 1}, \$${i * 2 + 2}::integer)`);
579
+ const factParameters = factReferences.flatMap((f) =>
580
+ [f.hash, factTypes.get(f.type)]);
581
+
582
+ const sql = 'SELECT fact_id, fact.fact_type_id, fact.hash' +
583
+ ' FROM public.fact' +
584
+ ' JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id)' +
585
+ ' ON v.fact_type_id = fact.fact_type_id AND v.hash = fact.hash;';
586
+ const { rows }: FactResult = await connection.query(sql, factParameters);
587
+ const existingFacts = rows.reduce(
588
+ (map, row) => addFact(map, row.hash, row.fact_type_id, row.fact_id),
589
+ emptyFactMap()
590
+ );
591
+ return existingFacts;
592
+ }
593
+ else {
594
+ return emptyFactMap();
595
+ }
596
+ }
597
+
598
+ async function insertFactsEdgesAndAncestors(facts: FactRecord[], factTypes: FactTypeMap, existingFacts: Map<string, Map<number, number>>, connection: PoolClient, roles: RoleMap) {
599
+ if (facts.length === 0) {
600
+ return emptyFactMap();
601
+ }
602
+
603
+ const { sql, parameters } = sqlInsertFacts(facts, roles, factTypes);
604
+
605
+ const { rows }: FactResult = await connection.query(sql, parameters);
606
+ if (rows.length !== facts.length) {
607
+ throw new Error('Failed to insert all new facts.');
608
+ }
609
+ const allFacts = rows.reduce(
610
+ (map, row) => addFact(map, row.hash, row.fact_type_id, row.fact_id),
611
+ existingFacts
612
+ );
613
+ return allFacts;
614
+ }
615
+
616
+ function sqlInsertFacts(facts: FactRecord[], roles: RoleMap, factTypes: FactTypeMap) {
617
+ const factValues = facts.map((f, i) => `(\$${i * 3 + 1}, \$${i * 3 + 2}::integer, \$${i * 3 + 3}::jsonb)`);
618
+ const factParameters = flatten(facts, (f) => [f.hash, factTypes.get(f.type), {
619
+ fields: f.fields,
620
+ predecessors: canonicalPredecessors(f.predecessors)
621
+ }]);
622
+
623
+ const edgeRecords = flatten(facts, fact => makeEdgeRecords(fact));
624
+ if (edgeRecords.length > 0) {
625
+ return sqlInsertFactsEdgesAndAncestors(facts, factParameters, edgeRecords, factTypes, roles, factValues);
626
+ }
627
+ else {
628
+ return {
629
+ sql: 'INSERT INTO public.fact (hash, fact_type_id, data) VALUES ' +
630
+ factValues.join(', ') +
631
+ ' RETURNING fact_id, hash, fact_type_id;',
632
+ parameters: factParameters
633
+ };
634
+ }
635
+ }
636
+
637
+ function sqlInsertFactsEdgesAndAncestors(facts: FactRecord[], factParameters: (string | number | { fields: {}; predecessors: PredecessorCollection; })[], edgeRecords: EdgeRecord[], factTypes: FactTypeMap, roles: RoleMap, factValues: string[]) {
638
+ let parameterOffset = factParameters.length;
639
+ 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)`);
640
+ const edgeParameters = flatten(edgeRecords, (e) => {
641
+ const successor_fact_type_id = getFactTypeId(factTypes, e.successor_type);
642
+ const predecessor_fact_type_id = getFactTypeId(factTypes, e.predecessor_type);
643
+ return [
644
+ e.successor_hash,
645
+ successor_fact_type_id,
646
+ e.predecessor_hash,
647
+ predecessor_fact_type_id,
648
+ getRoleId(roles, successor_fact_type_id, e.role)
649
+ ];
650
+ });
651
+
652
+ parameterOffset += edgeParameters.length;
653
+ const ancestors = ancestorRecords(facts);
654
+ const ancestorValues = ancestors.map((a, i) => `(\$${i * 4 + 1 + parameterOffset}, \$${i * 4 + 2 + parameterOffset}::integer, \$${i * 4 + 3 + parameterOffset}, \$${i * 4 + 4 + parameterOffset}::integer)`);
655
+ const ancestorParameters = flatten(ancestors, (a) => [
656
+ a.fact.hash,
657
+ factTypes.get(a.fact.type),
658
+ a.ancestor.hash,
659
+ factTypes.get(a.ancestor.type)
660
+ ]);
661
+
662
+ const sql =
663
+ `WITH new_fact AS (
664
+ SELECT hash, fact_type_id, data
665
+ FROM (VALUES ${factValues.join(', ')})
666
+ AS fv (hash, fact_type_id, data)
667
+ ),
668
+ new_edge AS (
669
+ SELECT successor_hash, successor_fact_type_id, predecessor_hash, predecessor_fact_type_id, role_id
670
+ FROM (VALUES ${edgeValues.join(', ')})
671
+ AS ev (successor_hash, successor_fact_type_id, predecessor_hash, predecessor_fact_type_id, role_id)
672
+ ),
673
+ new_ancestor AS (
674
+ SELECT hash, fact_type_id, ancestor_hash, ancestor_fact_type_id
675
+ FROM (VALUES ${ancestorValues.join(', ')})
676
+ AS ev (hash, fact_type_id, ancestor_hash, ancestor_fact_type_id)
677
+ ),
678
+ inserted_fact AS (
679
+ INSERT INTO public.fact (hash, fact_type_id, data)
680
+ SELECT hash, fact_type_id, data
681
+ FROM new_fact
682
+ RETURNING fact_id, fact_type_id, hash
683
+ ),
684
+ inserted_edge AS (
685
+ INSERT INTO public.edge
686
+ (role_id, successor_fact_id, predecessor_fact_id)
687
+ SELECT
688
+ new_edge.role_id,
689
+ successor.fact_id,
690
+ predecessor.fact_id
691
+ FROM new_edge
692
+ JOIN inserted_fact AS successor
693
+ ON successor.hash = new_edge.successor_hash
694
+ AND successor.fact_type_id = new_edge.successor_fact_type_id
695
+ JOIN (
696
+ SELECT fact_id, fact_type_id, hash
697
+ FROM inserted_fact
698
+ UNION ALL
699
+ SELECT fact_id, fact_type_id, hash
700
+ FROM public.fact
701
+ ) AS predecessor
702
+ ON predecessor.hash = new_edge.predecessor_hash
703
+ AND predecessor.fact_type_id = new_edge.predecessor_fact_type_id
704
+ ON CONFLICT DO NOTHING
705
+ ),
706
+ ancestor_id AS (
707
+ SELECT
708
+ fact.fact_id,
709
+ ancestor.fact_id AS ancestor_fact_id
710
+ FROM new_ancestor
711
+ JOIN inserted_fact AS fact
712
+ ON fact.hash = new_ancestor.hash
713
+ AND fact.fact_type_id = new_ancestor.fact_type_id
714
+ JOIN (
715
+ SELECT fact_id, fact_type_id, hash
716
+ FROM inserted_fact
717
+ UNION ALL
718
+ SELECT fact_id, fact_type_id, hash
719
+ FROM public.fact
720
+ ) AS ancestor
721
+ ON ancestor.hash = new_ancestor.ancestor_hash
722
+ AND ancestor.fact_type_id = new_ancestor.ancestor_fact_type_id
723
+ ),
724
+ inserted_ancestor AS (
725
+ INSERT INTO public.ancestor
726
+ (fact_id, ancestor_fact_id)
727
+ SELECT ancestor_id.fact_id, ancestor_id.ancestor_fact_id
728
+ FROM ancestor_id
729
+ UNION ALL
730
+ SELECT ancestor_id.fact_id, ancestor.ancestor_fact_id
731
+ FROM ancestor_id
732
+ JOIN public.ancestor
733
+ ON ancestor.fact_id = ancestor_id.ancestor_fact_id
734
+ ON CONFLICT DO NOTHING
735
+ )
736
+ SELECT fact_id, fact_type_id, hash
737
+ FROM inserted_fact;`;
738
+ const parameters = [...factParameters, ...edgeParameters, ...ancestorParameters];
739
+ return { sql, parameters };
740
+ }
741
+
742
+ function ancestorRecords(facts: FactRecord[]): { fact: FactReference, ancestor: FactReference }[] {
743
+ return facts.flatMap(fact => {
744
+ const factReference: FactReference = { hash: fact.hash, type: fact.type };
745
+ const ancestorReferences: FactReference[] = recursivePredecessors(factReference, facts);
746
+ return ancestorReferences.map(ancestor => (
747
+ {
748
+ fact: factReference,
749
+ ancestor
750
+ }
751
+ ));
752
+ });
753
+ }
754
+
755
+ function recursivePredecessors(factReference: FactReference, facts: FactRecord[]): FactReference[] {
756
+ return facts
757
+ .filter(f => f.hash === factReference.hash && f.type === factReference.type)
758
+ .flatMap(fact => {
759
+ const predecessorReferences = Object.keys(fact.predecessors).flatMap(role => {
760
+ const predecessors = fact.predecessors[role];
761
+ if (Array.isArray(predecessors)) {
762
+ return predecessors;
763
+ }
764
+ else {
765
+ return [predecessors];
766
+ }
767
+ });
768
+ const ancestorReferences = predecessorReferences.flatMap(predecessor => recursivePredecessors(predecessor, facts));
769
+ return [...predecessorReferences, ...ancestorReferences];
770
+ });
771
+ }
772
+
773
+ async function storePublicKeys(envelopes: FactEnvelope[], connection: PoolClient) {
774
+ // Look up existing fact types
775
+ const publicKeys = flatten(envelopes, e => e.signatures.map(s => s.publicKey))
776
+ .filter(distinct);
777
+ const lookUpSql = 'SELECT public_key, public_key_id FROM public.public_key WHERE public_key=ANY($1);';
778
+ const { rows: existingRows }: PublicKeyResult = await connection.query(lookUpSql, [publicKeys]);
779
+ const publicKeyIds = existingRows.reduce(
780
+ (map, row) => addFactType(map, row.public_key, row.public_key_id),
781
+ emptyPublicKeyMap()
782
+ );
783
+ const remainingPublicKeys = publicKeys.filter(pk => !publicKeyIds.has(pk));
784
+ if (remainingPublicKeys.length === 0) {
785
+ return publicKeyIds;
786
+ }
787
+
788
+ // Insert new fact types
789
+ const values = remainingPublicKeys.map((name, index) => `($${index + 1})`);
790
+ const insertSql = 'INSERT INTO public.public_key (public_key) VALUES ' + values.join(', ') +
791
+ ' RETURNING public_key, public_key_id;';
792
+ const { rows: newRows }: PublicKeyResult = await connection.query(insertSql, remainingPublicKeys);
793
+ if (newRows.length !== remainingPublicKeys.length) {
794
+ throw new Error('Failed to insert all new public keys.');
795
+ }
796
+ const allPublicKeyIds = newRows.reduce(
797
+ (map, row) => addFactType(map, row.public_key, row.public_key_id),
798
+ publicKeyIds
799
+ );
800
+ return allPublicKeyIds;
801
+ }
802
+
803
+ async function insertSignatures(envelopes: FactEnvelope[], allFacts: FactMap, factTypes: FactTypeMap, publicKey: PublicKeyMap, connection: PoolClient) {
804
+ const signatureRecords = flatten(envelopes, envelope => envelope.signatures.map(signature => ({
805
+ factId: getFactId(allFacts, envelope.fact.hash, getFactTypeId(factTypes, envelope.fact.type)),
806
+ publicKeyId: getPublicKeyId(publicKey, signature.publicKey),
807
+ signature: signature.signature
808
+ })));
809
+ if (signatureRecords.length > 0) {
810
+ const signatureValues = signatureRecords.map((s, i) =>
811
+ `($${i * 3 + 1}::integer, $${i * 3 + 2}::integer, $${i * 3 + 3})`);
812
+ const signatureParameters = flatten(signatureRecords, s =>
813
+ [s.factId, s.publicKeyId, s.signature]);
814
+
815
+ const sql = `INSERT INTO public.signature
816
+ (fact_id, public_key_id, signature)
817
+ (SELECT fact_id, public_key_id, signature
818
+ FROM (VALUES ${signatureValues.join(', ')}) AS v(fact_id, public_key_id, signature))
819
+ ON CONFLICT DO NOTHING`;
820
+ await connection.query(sql, signatureParameters);
821
+ }
822
+ }
823
+
824
+ function allFactTypes(steps: Step[]): string[] {
825
+ const factTypes = steps
826
+ .filter(step => step instanceof PropertyCondition && step.name === 'type')
827
+ .map(step => (step as PropertyCondition).value);
828
+ const childFactTypes = steps
829
+ .filter(step => step instanceof ExistentialCondition)
830
+ .flatMap(step => allFactTypes((step as ExistentialCondition).steps));
831
+ return [...factTypes, ...childFactTypes].filter(distinct);
832
+ }
833
+
834
+ function allRoles(steps: Step[], factTypes: FactTypeMap, initialTypeId: number) {
835
+ let roles: { defining_fact_type_id: number; role: string }[] = [];
836
+ let defining_fact_type_id = initialTypeId;
837
+ let role: string | undefined = undefined;
838
+
839
+ for (const step of steps) {
840
+ if (step instanceof PropertyCondition) {
841
+ if (step.name === 'type') {
842
+ defining_fact_type_id = getFactTypeId(factTypes, step.value);
843
+ if (defining_fact_type_id && role) {
844
+ roles.push({ defining_fact_type_id, role });
845
+ }
846
+ role = undefined;
847
+ }
848
+ }
849
+ else if (step instanceof Join) {
850
+ if (step.direction === Direction.Predecessor) {
851
+ if (defining_fact_type_id) {
852
+ roles.push({ defining_fact_type_id, role: step.role });
853
+ }
854
+ role = undefined;
855
+ }
856
+ else {
857
+ role = step.role;
858
+ }
859
+ defining_fact_type_id = undefined;
860
+ }
861
+ else if (step instanceof ExistentialCondition) {
862
+ roles = roles.concat(allRoles(step.steps, factTypes, defining_fact_type_id));
863
+ }
864
+ }
865
+
866
+ return roles;
867
+ }
868
+
869
+ function predecessorsOf(fact: FactRecord): FactReference[] {
870
+ const references = Object.values(fact.predecessors).flatMap(predecessor =>
871
+ Array.isArray(predecessor) ? predecessor : [predecessor]
872
+ );
873
+ return references;
874
+ }