jinaga-server 2.5.0 → 2.6.1

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 (59) hide show
  1. package/dist/authorization/authorization-keystore.d.ts +5 -4
  2. package/dist/authorization/authorization-keystore.js +16 -13
  3. package/dist/authorization/authorization-keystore.js.map +1 -1
  4. package/dist/http/router.d.ts +5 -3
  5. package/dist/http/router.js +125 -52
  6. package/dist/http/router.js.map +1 -1
  7. package/dist/http/stream.d.ts +10 -0
  8. package/dist/http/stream.js +35 -0
  9. package/dist/http/stream.js.map +1 -0
  10. package/dist/index.d.ts +1 -2
  11. package/dist/index.js +3 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/jinaga-server.js +7 -8
  14. package/dist/jinaga-server.js.map +1 -1
  15. package/dist/postgres/postgres-store.d.ts +4 -4
  16. package/dist/postgres/postgres-store.js +2 -2
  17. package/dist/postgres/postgres-store.js.map +1 -1
  18. package/dist/postgres/query-description.d.ts +75 -0
  19. package/dist/postgres/query-description.js +283 -0
  20. package/dist/postgres/query-description.js.map +1 -0
  21. package/dist/postgres/specification-result-sql.d.ts +2 -2
  22. package/dist/postgres/specification-result-sql.js +39 -324
  23. package/dist/postgres/specification-result-sql.js.map +1 -1
  24. package/dist/postgres/specification-sql.d.ts +2 -2
  25. package/dist/postgres/specification-sql.js +60 -163
  26. package/dist/postgres/specification-sql.js.map +1 -1
  27. package/http/sans-auth/.env.local +1 -0
  28. package/http/sans-auth/Create post sans auth.http +27 -0
  29. package/http/sans-auth/Create site sans auth.http +11 -0
  30. package/http/sans-auth/Feed sites.http +1 -0
  31. package/http/sans-auth/Feeds sites.http +12 -0
  32. package/http/sans-auth/Get posts sans auth.http +35 -0
  33. package/http/sans-auth/Get sites sans auth.http +12 -0
  34. package/http/sans-auth/stream.sh +2 -0
  35. package/integration-test/jinaga-test/Dockerfile +1 -1
  36. package/integration-test/jinaga-test/package-lock.json +917 -748
  37. package/integration-test/jinaga-test/package.json +3 -3
  38. package/package.json +2 -2
  39. package/replicator/package-lock.json +16 -16
  40. package/replicator/package.json +1 -1
  41. package/src/authorization/authorization-keystore.ts +23 -14
  42. package/src/http/router.ts +154 -64
  43. package/src/http/stream.ts +32 -0
  44. package/src/index.ts +1 -2
  45. package/src/jinaga-server.ts +8 -8
  46. package/src/postgres/postgres-store.ts +5 -8
  47. package/src/postgres/query-description.ts +435 -0
  48. package/src/postgres/specification-result-sql.ts +56 -489
  49. package/src/postgres/specification-sql.ts +71 -232
  50. package/test/authorization/authorizationSpec.ts +12 -2
  51. package/test/postgres/postgresSpecificationSpec.ts +15 -15
  52. package/dist/http/feed-cache.d.ts +0 -12
  53. package/dist/http/feed-cache.js +0 -3
  54. package/dist/http/feed-cache.js.map +0 -1
  55. package/dist/memory/memory-feed-cache.d.ts +0 -6
  56. package/dist/memory/memory-feed-cache.js +0 -17
  57. package/dist/memory/memory-feed-cache.js.map +0 -1
  58. package/src/http/feed-cache.ts +0 -14
  59. package/src/memory/memory-feed-cache.ts +0 -14
@@ -6,6 +6,7 @@ import {
6
6
  AuthorizationRules,
7
7
  DistributionRules,
8
8
  FactManager,
9
+ FeedCache,
9
10
  Jinaga,
10
11
  MemoryStore,
11
12
  Model,
@@ -25,7 +26,6 @@ import { AuthenticationSession } from "./authentication/authentication-session";
25
26
  import { AuthorizationKeystore } from "./authorization/authorization-keystore";
26
27
  import { HttpRouter, RequestUser } from "./http/router";
27
28
  import { Keystore } from "./keystore";
28
- import { MemoryFeedCache } from "./memory/memory-feed-cache";
29
29
  import { PostgresKeystore } from "./postgres/postgres-keystore";
30
30
  import { PostgresStore } from "./postgres/postgres-store";
31
31
 
@@ -62,13 +62,13 @@ export class JinagaServer {
62
62
  const keystore = createKeystore(config, pools);
63
63
  const authorizationRules = config.authorization ? config.authorization(new AuthorizationRules(config.model)) : null;
64
64
  const distributionRules = config.distribution ? config.distribution(new DistributionRules([])) : null;
65
- const authorization = createAuthorization(authorizationRules, distributionRules, store, keystore);
66
- const feedCache = new MemoryFeedCache();
67
- // Retain backward compatibility until the user provides distribution rules.
68
- const router = new HttpRouter(authorization, feedCache, distributionRules === null);
65
+ const feedCache = new FeedCache();
69
66
  const authentication = createAuthentication(store, keystore, authorizationRules);
70
67
  const network = new NetworkNoOp();
71
68
  const factManager = new FactManager(authentication, fork, source, store, network);
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
72
  const j: Jinaga = new Jinaga(factManager, syncStatusNotifier);
73
73
 
74
74
  async function close() {
@@ -110,13 +110,13 @@ function createKeystore(config: JinagaServerConfig, pools: { [uri: string]: Pool
110
110
  }
111
111
  }
112
112
 
113
- function createAuthorization(authorizationRules: AuthorizationRules | null, distributionRules: DistributionRules | null, store: Storage, keystore: Keystore | null): Authorization {
113
+ function createAuthorization(authorizationRules: AuthorizationRules | null, distributionRules: DistributionRules | null, factManager: FactManager, store: Storage, keystore: Keystore | null): Authorization {
114
114
  if (keystore) {
115
- const authorization = new AuthorizationKeystore(store, keystore, authorizationRules, distributionRules);
115
+ const authorization = new AuthorizationKeystore(factManager, store, keystore, authorizationRules, distributionRules);
116
116
  return authorization;
117
117
  }
118
118
  else {
119
- return new AuthorizationNoOp(store);
119
+ return new AuthorizationNoOp(factManager, store);
120
120
  }
121
121
  }
122
122
 
@@ -9,11 +9,8 @@ import {
9
9
  FactReference,
10
10
  factReferenceEquals,
11
11
  FactTuple,
12
- Feed,
13
12
  getAllFactTypes,
14
- getAllFactTypesFromFeed,
15
13
  getAllRoles,
16
- getAllRolesFromFeed,
17
14
  Join,
18
15
  PredecessorCollection,
19
16
  ProjectedResult,
@@ -261,7 +258,7 @@ export class PostgresStore implements Storage {
261
258
  return composer.compose(resultSets, factRecords);
262
259
  }
263
260
 
264
- async feed(feed: Feed, start: FactReference[], bookmark: string): Promise<FactFeed> {
261
+ async feed(feed: Specification, start: FactReference[], bookmark: string): Promise<FactFeed> {
265
262
  const factTypes: FactTypeMap = await this.loadFactTypesFromFeed(feed);
266
263
  const roleMap: RoleMap = await this.loadRolesFromFeed(feed, factTypes);
267
264
  const sql = sqlFromFeed(feed, start, this.schema, bookmark, 100, factTypes, roleMap);
@@ -298,9 +295,9 @@ export class PostgresStore implements Storage {
298
295
  return factTypes;
299
296
  }
300
297
 
301
- async loadFactTypesFromFeed(feed: Feed): Promise<FactTypeMap> {
298
+ async loadFactTypesFromFeed(feed: Specification): Promise<FactTypeMap> {
302
299
  const factTypes = this.factTypeMap;
303
- const unknownFactTypes = getAllFactTypesFromFeed(feed)
300
+ const unknownFactTypes = getAllFactTypes(feed)
304
301
  .filter(factType => !factTypes.has(factType));
305
302
  if (unknownFactTypes.length > 0) {
306
303
  const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
@@ -363,9 +360,9 @@ export class PostgresStore implements Storage {
363
360
  return roleMap;
364
361
  }
365
362
 
366
- async loadRolesFromFeed(feed: Feed, factTypes: FactTypeMap): Promise<RoleMap> {
363
+ async loadRolesFromFeed(feed: Specification, factTypes: FactTypeMap): Promise<RoleMap> {
367
364
  const roleMap = this.roleMap;
368
- const unknownRoles = getAllRolesFromFeed(feed)
365
+ const unknownRoles = getAllRoles(feed)
369
366
  .map(r => ({
370
367
  successor_type_id: getFactTypeId(factTypes, r.successorType)!,
371
368
  role: r.name
@@ -0,0 +1,435 @@
1
+ import { FactReference, Label, Match, PathCondition } from "jinaga";
2
+ import { FactTypeMap, RoleMap, ensureGetFactTypeId, getFactTypeId, getRoleId } from "./maps";
3
+
4
+ export interface FactDescription {
5
+ type: string;
6
+ factIndex: number;
7
+ }
8
+
9
+ export interface EdgeDescription {
10
+ edgeIndex: number;
11
+ predecessorFactIndex: number;
12
+ successorFactIndex: number;
13
+ roleParameter: number;
14
+ }
15
+
16
+ export interface ExistentialConditionDescription {
17
+ exists: boolean;
18
+ inputs: InputDescription[];
19
+ edges: EdgeDescription[];
20
+ existentialConditions: ExistentialConditionDescription[];
21
+ }
22
+
23
+ export interface InputDescription {
24
+ label: string;
25
+ type: string;
26
+ factIndex: number;
27
+ factTypeParameter: number;
28
+ factHashParameter: number;
29
+ }
30
+
31
+ export interface OutputDescription {
32
+ label: string;
33
+ type: string;
34
+ factIndex: number;
35
+ }
36
+
37
+ function countEdges(existentialConditions: ExistentialConditionDescription[]): number {
38
+ return existentialConditions.reduce((count, c) => count + c.edges.length + countEdges(c.existentialConditions),
39
+ 0);
40
+ }
41
+
42
+ export class QueryDescription {
43
+ // An unsatisfiable query description will produce no results.
44
+ static unsatisfiable: QueryDescription = new QueryDescription(
45
+ [], [], [], [], [], []
46
+ );
47
+
48
+ constructor(
49
+ public readonly inputs: InputDescription[],
50
+ public readonly parameters: (string | number)[],
51
+ public readonly outputs: OutputDescription[],
52
+ public readonly facts: FactDescription[],
53
+ public readonly edges: EdgeDescription[],
54
+ public readonly existentialConditions: ExistentialConditionDescription[] = []
55
+ ) {}
56
+
57
+ public withParameter(parameter: string | number): { query: QueryDescription; parameterIndex: number; } {
58
+ const parameterIndex = this.parameters.length + 1;
59
+ const query = new QueryDescription(
60
+ this.inputs,
61
+ this.parameters.concat(parameter),
62
+ this.outputs,
63
+ this.facts,
64
+ this.edges,
65
+ this.existentialConditions
66
+ );
67
+ return { query, parameterIndex };
68
+ }
69
+
70
+ public withInputParameter(label: Label, factTypeId: number, factHash: string, path: number[]): { queryDescription: QueryDescription; factDescription: FactDescription; } {
71
+ const factTypeParameter = this.parameters.length + 1;
72
+ const factHashParameter = factTypeParameter + 1;
73
+ const factIndex = this.facts.length + 1;
74
+ const factDescription: FactDescription = {
75
+ factIndex: factIndex,
76
+ type: label.type
77
+ };
78
+ const facts = [
79
+ ...this.facts,
80
+ factDescription
81
+ ];
82
+ const input: InputDescription = {
83
+ label: label.name,
84
+ type: label.type,
85
+ factIndex,
86
+ factTypeParameter,
87
+ factHashParameter
88
+ };
89
+ const parameters = this.parameters.concat(factTypeId, factHash);
90
+ if (path.length === 0) {
91
+ const inputs = [
92
+ ...this.inputs,
93
+ input
94
+ ];
95
+ const queryDescription = new QueryDescription(
96
+ inputs,
97
+ parameters,
98
+ this.outputs,
99
+ facts,
100
+ this.edges,
101
+ this.existentialConditions
102
+ );
103
+ return { queryDescription, factDescription };
104
+ }
105
+ else {
106
+ const existentialConditions = existentialsWithInput(this.existentialConditions, input, path);
107
+ const queryDescription = new QueryDescription(
108
+ this.inputs,
109
+ parameters,
110
+ this.outputs,
111
+ facts,
112
+ this.edges,
113
+ existentialConditions
114
+ );
115
+ return { queryDescription, factDescription };
116
+ }
117
+ }
118
+
119
+ public withFact(type: string): { query: QueryDescription; factIndex: number; } {
120
+ const factIndex = this.facts.length + 1;
121
+ const fact = { factIndex, type };
122
+ const query = new QueryDescription(
123
+ this.inputs,
124
+ this.parameters,
125
+ this.outputs,
126
+ this.facts.concat(fact),
127
+ this.edges,
128
+ this.existentialConditions
129
+ );
130
+ return { query, factIndex };
131
+ }
132
+
133
+ public withOutput(label: string, type: string, factIndex: number): QueryDescription {
134
+ const output = { label, type, factIndex };
135
+ const query = new QueryDescription(
136
+ this.inputs,
137
+ this.parameters,
138
+ this.outputs.concat(output),
139
+ this.facts,
140
+ this.edges,
141
+ this.existentialConditions
142
+ );
143
+ return query;
144
+ }
145
+
146
+ public withEdge(predecessorFactIndex: number, successorFactIndex: number, roleParameter: number, path: number[]) {
147
+ const edge = {
148
+ edgeIndex: this.edges.length + countEdges(this.existentialConditions) + 1,
149
+ predecessorFactIndex,
150
+ successorFactIndex,
151
+ roleParameter
152
+ };
153
+ const query = (path.length === 0)
154
+ ? new QueryDescription(
155
+ this.inputs,
156
+ this.parameters,
157
+ this.outputs,
158
+ this.facts,
159
+ this.edges.concat(edge),
160
+ this.existentialConditions
161
+ )
162
+ : new QueryDescription(
163
+ this.inputs,
164
+ this.parameters,
165
+ this.outputs,
166
+ this.facts,
167
+ this.edges,
168
+ existentialsWithEdge(this.existentialConditions, edge, path)
169
+ );
170
+ return query;
171
+ }
172
+
173
+ public withExistentialCondition(exists: boolean, path: number[]): { query: QueryDescription; path: number[]; } {
174
+ const { existentialConditions: newExistentialConditions, path: newPath } = existentialsWithNewCondition(this.existentialConditions, exists, path);
175
+ const query = new QueryDescription(
176
+ this.inputs,
177
+ this.parameters,
178
+ this.outputs,
179
+ this.facts,
180
+ this.edges,
181
+ newExistentialConditions
182
+ );
183
+ return { query, path: newPath };
184
+ }
185
+
186
+ isSatisfiable() {
187
+ return this.inputs.length > 0;
188
+ }
189
+
190
+ hasOutput(label: string) {
191
+ return this.outputs.some(o => o.label === label);
192
+ }
193
+
194
+ inputByLabel(label: string): InputDescription | undefined {
195
+ return this.inputs.find(i => i.label === label);
196
+ }
197
+
198
+ outputLength(): number {
199
+ return this.inputs.length + this.outputs.length;
200
+ }
201
+ }
202
+
203
+ function existentialsWithInput(existentialConditions: ExistentialConditionDescription[], input: InputDescription, path: number[]): ExistentialConditionDescription[] {
204
+ if (path.length === 1) {
205
+ return existentialConditions.map((c, i) => i === path[0] ?
206
+ {
207
+ ...c,
208
+ inputs: [...c.inputs, input]
209
+ } :
210
+ c
211
+ );
212
+ }
213
+ else {
214
+ return existentialConditions.map((c, i) => i === path[0] ?
215
+ {
216
+ ...c,
217
+ existentialConditions: existentialsWithInput(c.existentialConditions, input, path.slice(1))
218
+ } :
219
+ c
220
+ );
221
+ }
222
+ }
223
+
224
+ function existentialsWithEdge(existentialConditions: ExistentialConditionDescription[], edge: EdgeDescription, path: number[]): ExistentialConditionDescription[] {
225
+ if (path.length === 1) {
226
+ return existentialConditions.map((c, i) => i === path[0] ?
227
+ {
228
+ ...c,
229
+ edges: [...c.edges, edge]
230
+ } :
231
+ c
232
+ );
233
+ }
234
+ else {
235
+ return existentialConditions.map((c, i) => i === path[0] ?
236
+ {
237
+ ...c,
238
+ existentialConditions: existentialsWithEdge(c.existentialConditions, edge, path.slice(1))
239
+ } :
240
+ c
241
+ );
242
+ }
243
+ }
244
+
245
+ function existentialsWithNewCondition(existentialConditions: ExistentialConditionDescription[], exists: boolean, path: number[]): { existentialConditions: ExistentialConditionDescription[]; path: number[]; } {
246
+ if (path.length === 0) {
247
+ path = [existentialConditions.length];
248
+ existentialConditions = [
249
+ ...existentialConditions,
250
+ {
251
+ exists: exists,
252
+ inputs: [],
253
+ edges: [],
254
+ existentialConditions: []
255
+ }
256
+ ];
257
+ return { existentialConditions: existentialConditions, path };
258
+ }
259
+ else {
260
+ const { existentialConditions: newExistentialConditions, path: newPath } = existentialsWithNewCondition(existentialConditions[path[0]].existentialConditions, exists, path.slice(1));
261
+ existentialConditions = existentialConditions.map((c, i) => i === path[0] ?
262
+ {
263
+ exists: c.exists,
264
+ inputs: c.inputs,
265
+ edges: c.edges,
266
+ existentialConditions: newExistentialConditions
267
+ } :
268
+ c
269
+ );
270
+ path = [path[0], ...newPath];
271
+ return { existentialConditions: existentialConditions, path };
272
+ }
273
+ }
274
+
275
+ export type FactByLabel = {
276
+ [label: string]: FactDescription;
277
+ };
278
+
279
+ export class QueryDescriptionBuider {
280
+ constructor(
281
+ private factTypes: FactTypeMap,
282
+ private roleMap: RoleMap
283
+ ) { }
284
+
285
+ public addEdges(queryDescription: QueryDescription, given: Label[], start: FactReference[], knownFacts: FactByLabel, path: number[], matches: Match[]): { queryDescription: QueryDescription, knownFacts: FactByLabel } {
286
+ for (const match of matches) {
287
+ for (const condition of match.conditions) {
288
+ if (condition.type === "path") {
289
+ ({queryDescription, knownFacts} = this.addPathCondition(queryDescription, given, start, knownFacts, path, match.unknown, "", condition));
290
+ }
291
+ else if (condition.type === "existential") {
292
+ // Apply the where clause and continue with the tuple where it is true.
293
+ // The path describes which not-exists condition we are currently building on.
294
+ // Because the path is not empty, labeled facts will be included in the output.
295
+ const { query: queryDescriptionWithExistential, path: conditionalPath } = queryDescription.withExistentialCondition(condition.exists, path);
296
+ const { queryDescription: queryDescriptionConditional } = this.addEdges(queryDescriptionWithExistential, given, start, knownFacts, conditionalPath, condition.matches);
297
+
298
+ // If the negative existential condition is not satisfiable, then
299
+ // that means that the condition will always be true.
300
+ // We can therefore skip the branch for the negative existential condition.
301
+ if (queryDescriptionConditional.isSatisfiable()) {
302
+ queryDescription = queryDescriptionConditional;
303
+ }
304
+ }
305
+ if (!queryDescription.isSatisfiable()) {
306
+ break;
307
+ }
308
+ }
309
+ if (!queryDescription.isSatisfiable()) {
310
+ break;
311
+ }
312
+ }
313
+ return {
314
+ queryDescription,
315
+ knownFacts
316
+ };
317
+ }
318
+
319
+ private addPathCondition(queryDescription: QueryDescription, given: Label[], start: FactReference[], knownFacts: FactByLabel, path: number[], unknown: Label, prefix: string, condition: PathCondition): { queryDescription: QueryDescription, knownFacts: FactByLabel } {
320
+ // If no input parameter has been allocated, allocate one now.
321
+ if (!knownFacts.hasOwnProperty(condition.labelRight)) {
322
+ const givenIndex = given.findIndex(given => given.name === condition.labelRight);
323
+ if (givenIndex < 0) {
324
+ throw new Error(`No input parameter found for label ${condition.labelRight}`);
325
+ }
326
+ const { queryDescription: newQueryDescription, factDescription } = queryDescription.withInputParameter(
327
+ given[givenIndex],
328
+ ensureGetFactTypeId(this.factTypes, start[givenIndex].type),
329
+ start[givenIndex].hash,
330
+ path
331
+ );
332
+ queryDescription = newQueryDescription;
333
+ knownFacts = {
334
+ ...knownFacts,
335
+ [condition.labelRight]: factDescription
336
+ };
337
+ }
338
+
339
+ // Determine whether we have already written the output.
340
+ const knownFact = knownFacts[unknown.name];
341
+ const roleCount = condition.rolesLeft.length + condition.rolesRight.length;
342
+
343
+ // Walk up the right-hand side.
344
+ // This generates predecessor joins from a given or prior label.
345
+ let fact = knownFacts[condition.labelRight];
346
+ if (!fact) {
347
+ throw new Error(`Label ${condition.labelRight} not found. Known labels: ${Object.keys(knownFacts).join(", ")}`);
348
+ }
349
+ let type = fact.type;
350
+ let factIndex = fact.factIndex;
351
+ for (const [i, role] of condition.rolesRight.entries()) {
352
+ // If the type or role is not known, then no facts matching the condition can
353
+ // exist. The query is unsatisfiable.
354
+ const typeId = getFactTypeId(this.factTypes, type);
355
+ if (!typeId) {
356
+ return { queryDescription: QueryDescription.unsatisfiable, knownFacts };
357
+ }
358
+ const roleId = getRoleId(this.roleMap, typeId, role.name);
359
+ if (!roleId) {
360
+ return { queryDescription: QueryDescription.unsatisfiable, knownFacts };
361
+ }
362
+
363
+ const { query: queryWithParameter, parameterIndex: roleParameter } = queryDescription.withParameter(roleId);
364
+ if (i === roleCount - 1 && knownFact) {
365
+ // If we have already written the output, we can use the fact index.
366
+ queryDescription = queryWithParameter.withEdge(knownFact.factIndex, factIndex, roleParameter, path);
367
+ factIndex = knownFact.factIndex;
368
+ }
369
+ else {
370
+ // If we have not written the fact, we need to write it now.
371
+ const { query, factIndex: predecessorFactIndex } = queryWithParameter.withFact(role.predecessorType);
372
+ queryDescription = query.withEdge(predecessorFactIndex, factIndex, roleParameter, path);
373
+ factIndex = predecessorFactIndex;
374
+ }
375
+ type = role.predecessorType;
376
+ }
377
+
378
+ const rightType = type;
379
+
380
+ // Walk up the left-hand side.
381
+ // We will need to reverse this walk to generate successor joins.
382
+ type = unknown.type;
383
+ const newEdges: {
384
+ roleId: number;
385
+ declaringType: string;
386
+ }[] = [];
387
+ for (const role of condition.rolesLeft) {
388
+ // If the type or role is not known, then no facts matching the condition can
389
+ // exist. The query is unsatisfiable.
390
+ const typeId = getFactTypeId(this.factTypes, type);
391
+ if (!typeId) {
392
+ return { queryDescription: QueryDescription.unsatisfiable, knownFacts };
393
+ }
394
+ const roleId = getRoleId(this.roleMap, typeId, role.name);
395
+ if (!roleId) {
396
+ return { queryDescription: QueryDescription.unsatisfiable, knownFacts };
397
+ }
398
+
399
+ newEdges.push({
400
+ roleId,
401
+ declaringType: type
402
+ });
403
+ type = role.predecessorType;
404
+ }
405
+
406
+ if (type !== rightType) {
407
+ throw new Error(`Type mismatch: ${type} is compared to ${rightType}`);
408
+ }
409
+
410
+ newEdges.reverse().forEach(({ roleId, declaringType }, i) => {
411
+ const { query: queryWithParameter, parameterIndex: roleParameter } = queryDescription.withParameter(roleId);
412
+ if (condition.rolesRight.length + i === roleCount - 1 && knownFact) {
413
+ queryDescription = queryWithParameter.withEdge(factIndex, knownFact.factIndex, roleParameter, path);
414
+ factIndex = knownFact.factIndex;
415
+ }
416
+ else {
417
+ const { query: queryWithFact, factIndex: successorFactIndex } = queryWithParameter.withFact(declaringType);
418
+ queryDescription = queryWithFact.withEdge(factIndex, successorFactIndex, roleParameter, path);
419
+ factIndex = successorFactIndex;
420
+ }
421
+ });
422
+
423
+ // If we have not captured the known fact, add it now.
424
+ if (!knownFact) {
425
+ knownFacts = { ...knownFacts, [unknown.name]: { factIndex, type: unknown.type } };
426
+ // If we have not written the output, write it now.
427
+ // Only write the output if we are not inside of an existential condition.
428
+ // Use the prefix, which will be set for projections.
429
+ if (path.length === 0) {
430
+ queryDescription = queryDescription.withOutput(prefix + unknown.name, unknown.type, factIndex);
431
+ }
432
+ }
433
+ return { queryDescription, knownFacts };
434
+ }
435
+ }