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,742 @@
1
+ import { dehydrateReference, FactBookmark, getAllFactTypes, getAllRoles, SpecificationParser } from "jinaga";
2
+
3
+ import { addFactType, addRole, emptyFactTypeMap, emptyRoleMap, getFactTypeId, getRoleId } from "../../src/postgres/maps";
4
+ import { SpecificationSqlQuery } from "../../src/postgres/query-description";
5
+ import { sqlFromSpecification } from "../../src/postgres/specification-sql";
6
+
7
+ const root = dehydrateReference({ type: 'Root' });
8
+ const rootHash = root.hash;
9
+ const user = dehydrateReference({ type: "Jinaga.User", publicKey: "PUBLIC KEY"});
10
+ const userHash = user.hash;
11
+
12
+ function parseSpecification(input: string) {
13
+ const parser = new SpecificationParser(input);
14
+ parser.skipWhitespace();
15
+ return parser.parseSpecification();
16
+ }
17
+
18
+ function sqlFor(descriptiveString: string, bookmarks: FactBookmark[] = []) {
19
+ const specification = parseSpecification(descriptiveString);
20
+ const factTypeNames = getAllFactTypes(specification);
21
+
22
+ // Build a fact type map containing all fact types in the specification.
23
+ // Filter out fact types named "Unknown".
24
+ const factTypes = factTypeNames.filter(t => t !== 'Unknown').reduce(
25
+ (f, factType, i) => addFactType(f, factType, i + 1),
26
+ emptyFactTypeMap());
27
+
28
+ // Build a role map containing all roles in the specification.
29
+ // Filter out the roles named "unknown", and those of unknown fact types.
30
+ let roleMap = getAllRoles(specification).filter(r => r.name !== 'unknown').reduce(
31
+ (r, role, i) => {
32
+ const factTypeId = getFactTypeId(factTypes, role.definingFactType);
33
+ if (!factTypeId) {
34
+ return r;
35
+ }
36
+ return addRole(r, factTypeId, role.name, i + 1);
37
+ },
38
+ emptyRoleMap());
39
+ const start = specification.given.map(input => {
40
+ if (input.type === 'Root') {
41
+ return root;
42
+ }
43
+ if (input.type === 'Jinaga.User') {
44
+ return user;
45
+ }
46
+ throw new Error(`Unknown input type ${input.type}`);
47
+ });
48
+ const sqlQueries: SpecificationSqlQuery[] = sqlFromSpecification(start, bookmarks, 100, specification, factTypes, roleMap);
49
+ return { sqlQueries, factTypes, roleMap };
50
+ }
51
+
52
+ function roleParameter(roleMap: Map<number, Map<string, number>>, factTypes: Map<string, number>, factTypeName: string, roleName: string): number {
53
+ const factTypeId = getFactTypeId(factTypes, factTypeName);
54
+ if (!factTypeId) {
55
+ throw new Error(`Unknown fact type ${factTypeName}`);
56
+ }
57
+ const roleId = getRoleId(roleMap, factTypeId, roleName);
58
+ if (!roleId) {
59
+ throw new Error(`Unknown role ${roleName} in fact type ${factTypeName}`);
60
+ }
61
+ return roleId;
62
+ }
63
+
64
+ describe("Postgres query generator", () => {
65
+ it("should generate a join to successors", () => {
66
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
67
+ (predecessor: Root) {
68
+ successor: IntegrationTest.Successor [
69
+ successor->predecessor:Root = predecessor
70
+ ]
71
+ }`);
72
+ expect(sqlQueries.length).toBe(1);
73
+ const query = sqlQueries[0];
74
+ expect(query.sql).toEqual(
75
+ 'SELECT f2.hash as hash2, ' +
76
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
77
+ 'FROM public.fact f1 ' +
78
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
79
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
80
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
81
+ 'AND sort(array[f2.fact_id], \'desc\') > $4 ' +
82
+ 'ORDER BY bookmark ASC ' +
83
+ 'LIMIT $5'
84
+ );
85
+ expect(query.parameters).toEqual([
86
+ getFactTypeId(factTypes, 'Root'),
87
+ rootHash,
88
+ roleParameter(roleMap, factTypes, 'IntegrationTest.Successor', 'predecessor'),
89
+ [],
90
+ 100
91
+ ]);
92
+ expect(query.labels).toEqual([
93
+ {
94
+ name: 'successor',
95
+ type: 'IntegrationTest.Successor',
96
+ index: 2
97
+ }
98
+ ]);
99
+ });
100
+
101
+ it("should generate a join to successor and then predecessor", () => {
102
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
103
+ (predecessor: Root) {
104
+ successor: IntegrationTest.Successor [
105
+ successor->predecessor:Root = predecessor
106
+ ]
107
+ other: IntegrationTest.OtherPredecessor [
108
+ other = successor->other:IntegrationTest.OtherPredecessor
109
+ ]
110
+ }`);
111
+
112
+ expect(sqlQueries.length).toEqual(1)
113
+ expect(sqlQueries[0].sql).toEqual(
114
+ 'SELECT f2.hash as hash2, ' +
115
+ 'f3.hash as hash3, ' +
116
+ 'sort(array[f2.fact_id, f3.fact_id], \'desc\') as bookmark ' +
117
+ 'FROM public.fact f1 ' +
118
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
119
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
120
+ 'JOIN public.edge e2 ON e2.successor_fact_id = f2.fact_id AND e2.role_id = $4 ' +
121
+ 'JOIN public.fact f3 ON f3.fact_id = e2.predecessor_fact_id ' +
122
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
123
+ 'AND sort(array[f2.fact_id, f3.fact_id], \'desc\') > $5 ' +
124
+ 'ORDER BY bookmark ASC ' +
125
+ 'LIMIT $6'
126
+ );
127
+ expect(sqlQueries[0].parameters).toEqual([
128
+ getFactTypeId(factTypes, "Root"),
129
+ rootHash,
130
+ roleParameter(roleMap, factTypes, "IntegrationTest.Successor", "predecessor"),
131
+ roleParameter(roleMap, factTypes, "IntegrationTest.Successor", "other"),
132
+ [],
133
+ 100
134
+ ]);
135
+ expect(sqlQueries[0].labels).toEqual([
136
+ {
137
+ name: "successor",
138
+ type: "IntegrationTest.Successor",
139
+ index: 2
140
+ },
141
+ {
142
+ name: "other",
143
+ type: "IntegrationTest.OtherPredecessor",
144
+ index: 3
145
+ }
146
+ ]);
147
+ });
148
+
149
+ it("should generate positive existential conditions", () => {
150
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
151
+ (root: Root) {
152
+ project: MyApplication.Project [
153
+ project->root: Root = root
154
+ E {
155
+ assignment: MyApplication.Assignment [
156
+ assignment->project: MyApplication.Project = project
157
+ ]
158
+ }
159
+ ]
160
+ }
161
+ `);
162
+
163
+ expect(sqlQueries.length).toEqual(1);
164
+ expect(sqlQueries[0].sql).toEqual(
165
+ 'SELECT f2.hash as hash2, ' +
166
+ 'f3.hash as hash3, ' +
167
+ 'sort(array[f2.fact_id, f3.fact_id], \'desc\') as bookmark ' +
168
+ 'FROM public.fact f1 ' +
169
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
170
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
171
+ 'JOIN public.edge e2 ON e2.predecessor_fact_id = f2.fact_id AND e2.role_id = $4 ' +
172
+ 'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
173
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
174
+ 'AND sort(array[f2.fact_id, f3.fact_id], \'desc\') > $5 ' +
175
+ 'ORDER BY bookmark ASC ' +
176
+ 'LIMIT $6'
177
+ );
178
+ expect(sqlQueries[0].parameters).toEqual([
179
+ getFactTypeId(factTypes, "Root"),
180
+ rootHash,
181
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
182
+ roleParameter(roleMap, factTypes, "MyApplication.Assignment", "project"),
183
+ [],
184
+ 100
185
+ ]);
186
+ expect(sqlQueries[0].labels).toEqual([
187
+ {
188
+ name: "project",
189
+ type: "MyApplication.Project",
190
+ index: 2
191
+ },
192
+ {
193
+ name: "assignment",
194
+ type: "MyApplication.Assignment",
195
+ index: 3
196
+ }
197
+ ]);
198
+ });
199
+
200
+ it("should generate negative existential conditions", () => {
201
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
202
+ (root: Root) {
203
+ project: MyApplication.Project [
204
+ project->root: Root = root
205
+ !E {
206
+ deleted: MyApplication.Project.Deleted [
207
+ deleted->project: MyApplication.Project = project
208
+ ]
209
+ }
210
+ ]
211
+ }
212
+ `);
213
+
214
+ expect(sqlQueries.length).toEqual(2);
215
+ expect(sqlQueries[0].sql).toEqual(
216
+ 'SELECT f2.hash as hash2, ' +
217
+ 'f3.hash as hash3, ' +
218
+ 'sort(array[f2.fact_id, f3.fact_id], \'desc\') as bookmark ' +
219
+ 'FROM public.fact f1 ' +
220
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
221
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
222
+ 'JOIN public.edge e2 ON e2.predecessor_fact_id = f2.fact_id AND e2.role_id = $4 ' +
223
+ 'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
224
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
225
+ 'AND sort(array[f2.fact_id, f3.fact_id], \'desc\') > $5 ' +
226
+ 'ORDER BY bookmark ASC ' +
227
+ 'LIMIT $6'
228
+ );
229
+ expect(sqlQueries[0].parameters).toEqual([
230
+ getFactTypeId(factTypes, "Root"),
231
+ rootHash,
232
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
233
+ roleParameter(roleMap, factTypes, "MyApplication.Project.Deleted", "project"),
234
+ [],
235
+ 100
236
+ ]);
237
+ expect(sqlQueries[0].labels).toEqual([
238
+ {
239
+ name: "project",
240
+ type: "MyApplication.Project",
241
+ index: 2
242
+ },
243
+ {
244
+ name: "deleted",
245
+ type: "MyApplication.Project.Deleted",
246
+ index: 3
247
+ }
248
+ ]);
249
+
250
+ expect(sqlQueries[1].sql).toEqual(
251
+ 'SELECT f2.hash as hash2, ' +
252
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
253
+ 'FROM public.fact f1 ' +
254
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
255
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
256
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
257
+ 'AND NOT EXISTS (' +
258
+ 'SELECT 1 ' +
259
+ 'FROM public.edge e2 ' +
260
+ 'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
261
+ 'WHERE e2.predecessor_fact_id = f2.fact_id AND e2.role_id = $4' +
262
+ ') ' +
263
+ 'AND sort(array[f2.fact_id], \'desc\') > $5 ' +
264
+ 'ORDER BY bookmark ASC ' +
265
+ 'LIMIT $6'
266
+ );
267
+ expect(sqlQueries[1].parameters).toEqual([
268
+ getFactTypeId(factTypes, "Root"),
269
+ rootHash,
270
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
271
+ roleParameter(roleMap, factTypes, "MyApplication.Project.Deleted", "project"),
272
+ [],
273
+ 100
274
+ ]);
275
+ expect(sqlQueries[1].labels).toEqual([
276
+ {
277
+ name: "project",
278
+ type: "MyApplication.Project",
279
+ index: 2
280
+ }
281
+ ]);
282
+ });
283
+
284
+ it("should accept multiple givens", () => {
285
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
286
+ (root: Root, user: Jinaga.User) {
287
+ project: MyApplication.Project [
288
+ project->root: Root = root
289
+ ]
290
+ assignment: MyApplication.Assignment [
291
+ assignment->project: MyApplication.Project = project
292
+ assignment->user: Jinaga.User = user
293
+ ]
294
+ }
295
+ `);
296
+
297
+ expect(sqlQueries.length).toEqual(1);
298
+ expect(sqlQueries[0].sql).toEqual(
299
+ 'SELECT f3.hash as hash3, ' +
300
+ 'f4.hash as hash4, ' +
301
+ 'sort(array[f3.fact_id, f4.fact_id], \'desc\') as bookmark ' +
302
+ 'FROM public.fact f1 ' +
303
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
304
+ 'JOIN public.fact f3 ON f3.fact_id = e1.successor_fact_id ' +
305
+ 'JOIN public.edge e2 ON e2.predecessor_fact_id = f3.fact_id AND e2.role_id = $4 ' +
306
+ 'JOIN public.fact f4 ON f4.fact_id = e2.successor_fact_id ' +
307
+ 'JOIN public.edge e3 ON e3.successor_fact_id = f4.fact_id AND e3.role_id = $7 ' +
308
+ 'JOIN public.fact f2 ON f2.fact_id = e3.predecessor_fact_id ' +
309
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
310
+ 'AND f2.fact_type_id = $5 AND f2.hash = $6 ' +
311
+ 'AND sort(array[f3.fact_id, f4.fact_id], \'desc\') > $8 ' +
312
+ 'ORDER BY bookmark ASC ' +
313
+ 'LIMIT $9'
314
+ );
315
+ expect(sqlQueries[0].parameters).toEqual([
316
+ getFactTypeId(factTypes, "Root"),
317
+ rootHash,
318
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
319
+ roleParameter(roleMap, factTypes, "MyApplication.Assignment", "project"),
320
+ getFactTypeId(factTypes, "Jinaga.User"),
321
+ userHash,
322
+ roleParameter(roleMap, factTypes, "MyApplication.Assignment", "user"),
323
+ [],
324
+ 100
325
+ ]);
326
+ expect(sqlQueries[0].labels).toEqual([
327
+ {
328
+ name: "project",
329
+ type: "MyApplication.Project",
330
+ index: 3
331
+ },
332
+ {
333
+ name: "assignment",
334
+ type: "MyApplication.Assignment",
335
+ index: 4
336
+ }
337
+ ]);
338
+ });
339
+
340
+ it("should accept givens in any order", () => {
341
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
342
+ (user: Jinaga.User, root: Root) {
343
+ project: MyApplication.Project [
344
+ project->root: Root = root
345
+ ]
346
+ assignment: MyApplication.Assignment [
347
+ assignment->project: MyApplication.Project = project
348
+ assignment->user: Jinaga.User = user
349
+ ]
350
+ }
351
+ `);
352
+
353
+ expect(sqlQueries.length).toEqual(1);
354
+ expect(sqlQueries[0].sql).toEqual(
355
+ 'SELECT f3.hash as hash3, ' +
356
+ 'f4.hash as hash4, ' +
357
+ 'sort(array[f3.fact_id, f4.fact_id], \'desc\') as bookmark ' +
358
+ 'FROM public.fact f2 ' +
359
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f2.fact_id AND e1.role_id = $3 ' +
360
+ 'JOIN public.fact f3 ON f3.fact_id = e1.successor_fact_id ' +
361
+ 'JOIN public.edge e2 ON e2.predecessor_fact_id = f3.fact_id AND e2.role_id = $4 ' +
362
+ 'JOIN public.fact f4 ON f4.fact_id = e2.successor_fact_id ' +
363
+ 'JOIN public.edge e3 ON e3.successor_fact_id = f4.fact_id AND e3.role_id = $7 ' +
364
+ 'JOIN public.fact f1 ON f1.fact_id = e3.predecessor_fact_id ' +
365
+ 'WHERE f1.fact_type_id = $5 AND f1.hash = $6 ' +
366
+ 'AND f2.fact_type_id = $1 AND f2.hash = $2 ' +
367
+ 'AND sort(array[f3.fact_id, f4.fact_id], \'desc\') > $8 ' +
368
+ 'ORDER BY bookmark ASC ' +
369
+ 'LIMIT $9'
370
+ );
371
+ expect(sqlQueries[0].parameters).toEqual([
372
+ getFactTypeId(factTypes, "Root"),
373
+ rootHash,
374
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
375
+ roleParameter(roleMap, factTypes, "MyApplication.Assignment", "project"),
376
+ getFactTypeId(factTypes, "Jinaga.User"),
377
+ userHash,
378
+ roleParameter(roleMap, factTypes, "MyApplication.Assignment", "user"),
379
+ [],
380
+ 100
381
+ ]);
382
+ expect(sqlQueries[0].labels).toEqual([
383
+ {
384
+ name: "project",
385
+ type: "MyApplication.Project",
386
+ index: 3
387
+ },
388
+ {
389
+ name: "assignment",
390
+ type: "MyApplication.Assignment",
391
+ index: 4
392
+ }
393
+ ]);
394
+ });
395
+
396
+ it("should accept a projection", () => {
397
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
398
+ (root: Root) {
399
+ project: MyApplication.Project [
400
+ project->root: Root = root
401
+ ]
402
+ } => {
403
+ names = {
404
+ name: MyApplication.Project.Name [
405
+ name->project: MyApplication.Project = project
406
+ ]
407
+ }
408
+ }`);
409
+
410
+ expect(sqlQueries.length).toEqual(2);
411
+ expect(sqlQueries[0].sql).toEqual(
412
+ 'SELECT f2.hash as hash2, ' +
413
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
414
+ 'FROM public.fact f1 ' +
415
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
416
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
417
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
418
+ 'AND sort(array[f2.fact_id], \'desc\') > $4 ' +
419
+ 'ORDER BY bookmark ASC ' +
420
+ 'LIMIT $5'
421
+ );
422
+ expect(sqlQueries[0].parameters).toEqual([
423
+ getFactTypeId(factTypes, "Root"),
424
+ rootHash,
425
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
426
+ [],
427
+ 100
428
+ ]);
429
+ expect(sqlQueries[0].labels).toEqual([
430
+ {
431
+ name: "project",
432
+ type: "MyApplication.Project",
433
+ index: 2
434
+ }
435
+ ]);
436
+ expect(sqlQueries[1].sql).toEqual(
437
+ 'SELECT f2.hash as hash2, f3.hash as hash3, ' +
438
+ 'sort(array[f2.fact_id, f3.fact_id], \'desc\') as bookmark ' +
439
+ 'FROM public.fact f1 ' +
440
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
441
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
442
+ 'JOIN public.edge e2 ON e2.predecessor_fact_id = f2.fact_id AND e2.role_id = $4 ' +
443
+ 'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
444
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
445
+ 'AND sort(array[f2.fact_id, f3.fact_id], \'desc\') > $5 ' +
446
+ 'ORDER BY bookmark ASC ' +
447
+ 'LIMIT $6'
448
+ );
449
+ expect(sqlQueries[1].parameters).toEqual([
450
+ getFactTypeId(factTypes, "Root"),
451
+ rootHash,
452
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
453
+ roleParameter(roleMap, factTypes, "MyApplication.Project.Name", "project"),
454
+ [],
455
+ 100
456
+ ]);
457
+ expect(sqlQueries[1].labels).toEqual([
458
+ {
459
+ name: "project",
460
+ type: "MyApplication.Project",
461
+ index: 2
462
+ },
463
+ {
464
+ name: "names.name",
465
+ type: "MyApplication.Project.Name",
466
+ index: 3
467
+ }
468
+ ]);
469
+ });
470
+
471
+ it("should apply the bookmark for each feed", () => {
472
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
473
+ (root: Root) {
474
+ project: MyApplication.Project [
475
+ project->root: Root = root
476
+ ]
477
+ } => {
478
+ names = {
479
+ name: MyApplication.Project.Name [
480
+ name->project: MyApplication.Project = project
481
+ !E {
482
+ next: MyApplication.Project.Name [
483
+ next->prior: MyApplication.Project.Name = name
484
+ ]
485
+ }
486
+ ]
487
+ }
488
+ }`, [
489
+ {
490
+ labels: [ "project" ],
491
+ bookmark: "123"
492
+ },
493
+ {
494
+ labels: [ "project", "names.name", "names.next" ],
495
+ bookmark: "456.345.234"
496
+ },
497
+ {
498
+ labels: [ "project", "names.name" ],
499
+ bookmark: "789.678"
500
+ }
501
+ ]);
502
+
503
+ expect(sqlQueries.length).toEqual(3);
504
+ expect(sqlQueries[0].sql).toEqual(
505
+ 'SELECT f2.hash as hash2, ' +
506
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
507
+ 'FROM public.fact f1 ' +
508
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
509
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
510
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
511
+ 'AND sort(array[f2.fact_id], \'desc\') > $4 ' +
512
+ 'ORDER BY bookmark ASC ' +
513
+ 'LIMIT $5'
514
+ );
515
+ expect(sqlQueries[0].parameters).toEqual([
516
+ getFactTypeId(factTypes, "Root"),
517
+ rootHash,
518
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
519
+ [ 123 ],
520
+ 100
521
+ ]);
522
+ expect(sqlQueries[0].labels).toEqual([
523
+ {
524
+ name: "project",
525
+ type: "MyApplication.Project",
526
+ index: 2
527
+ }
528
+ ]);
529
+ });
530
+
531
+ it("should accept overconstrained specification", () => {
532
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
533
+ (root: Root) {
534
+ project: MyApplication.Project [
535
+ project->root: Root = root
536
+ project->root2: Root = root
537
+ ]
538
+ }`);
539
+
540
+ expect(sqlQueries.length).toEqual(1);
541
+ expect(sqlQueries[0].sql).toEqual(
542
+ 'SELECT f2.hash as hash2, ' +
543
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
544
+ 'FROM public.fact f1 ' +
545
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
546
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
547
+ 'JOIN public.edge e2 ON e2.predecessor_fact_id = f1.fact_id AND e2.successor_fact_id = f2.fact_id AND e2.role_id = $4 ' +
548
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
549
+ 'AND sort(array[f2.fact_id], \'desc\') > $5 ' +
550
+ 'ORDER BY bookmark ASC ' +
551
+ 'LIMIT $6'
552
+ );
553
+ expect(sqlQueries[0].parameters).toEqual([
554
+ getFactTypeId(factTypes, "Root"),
555
+ rootHash,
556
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
557
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root2"),
558
+ [],
559
+ 100
560
+ ]);
561
+ expect(sqlQueries[0].labels).toEqual([
562
+ {
563
+ name: "project",
564
+ type: "MyApplication.Project",
565
+ index: 2
566
+ }
567
+ ]);
568
+ });
569
+
570
+ it("should accept overconstrained existential query", () => {
571
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
572
+ (root: Root) {
573
+ project: MyApplication.Project [
574
+ project->root: Root = root
575
+ ]
576
+ } => {
577
+ names = {
578
+ name: MyApplication.Project.Name [
579
+ name->project: MyApplication.Project = project
580
+ !E {
581
+ next: MyApplication.Project.Name [
582
+ next->prior: MyApplication.Project.Name = name
583
+ next->project: MyApplication.Project = project
584
+ ]
585
+ }
586
+ ]
587
+ }
588
+ }`);
589
+ expect(sqlQueries.length).toEqual(3);
590
+ expect(sqlQueries[2].sql).toEqual(
591
+ 'SELECT f2.hash as hash2, f3.hash as hash3, ' +
592
+ 'sort(array[f2.fact_id, f3.fact_id], \'desc\') as bookmark ' +
593
+ 'FROM public.fact f1 ' +
594
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
595
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
596
+ 'JOIN public.edge e2 ON e2.predecessor_fact_id = f2.fact_id AND e2.role_id = $4 ' +
597
+ 'JOIN public.fact f3 ON f3.fact_id = e2.successor_fact_id ' +
598
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
599
+ 'AND NOT EXISTS (' +
600
+ 'SELECT 1 ' +
601
+ 'FROM public.edge e3 ' +
602
+ 'JOIN public.fact f4 ON f4.fact_id = e3.successor_fact_id ' +
603
+ 'JOIN public.edge e4 ON e4.predecessor_fact_id = f2.fact_id AND e4.successor_fact_id = f4.fact_id AND e4.role_id = $6 ' +
604
+ 'WHERE e3.predecessor_fact_id = f3.fact_id AND e3.role_id = $5' +
605
+ ') ' +
606
+ 'AND sort(array[f2.fact_id, f3.fact_id], \'desc\') > $7 ' +
607
+ 'ORDER BY bookmark ASC ' +
608
+ 'LIMIT $8'
609
+ );
610
+ expect(sqlQueries[1].parameters).toEqual([
611
+ getFactTypeId(factTypes, "Root"),
612
+ rootHash,
613
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
614
+ roleParameter(roleMap, factTypes, "MyApplication.Project.Name", "project"),
615
+ roleParameter(roleMap, factTypes, "MyApplication.Project.Name", "prior"),
616
+ roleParameter(roleMap, factTypes, "MyApplication.Project.Name", "project"),
617
+ [],
618
+ 100
619
+ ]);
620
+ });
621
+
622
+ it("skips unknown types in matches", () => {
623
+ const { sqlQueries } = sqlFor(`
624
+ (root: Root) {
625
+ successor: Unknown [
626
+ successor->root: Root = root
627
+ ]
628
+ }`);
629
+
630
+ expect(sqlQueries.length).toEqual(0);
631
+ });
632
+
633
+ it("skips unknown types on left in paths", () => {
634
+ const { sqlQueries } = sqlFor(`
635
+ (root: Root) {
636
+ successor: MyApplication.Project [
637
+ successor->intermediate: Unknown->root: Root = root
638
+ ]
639
+ }`);
640
+
641
+ expect(sqlQueries.length).toEqual(0);
642
+ });
643
+
644
+ it("skips unknown types on right in paths", () => {
645
+ const { sqlQueries } = sqlFor(`
646
+ (root: Root) {
647
+ successor: MyApplication.Successor [
648
+ successor = root->intermediate: Unknown->root: Root
649
+ ]
650
+ }`);
651
+
652
+ expect(sqlQueries.length).toEqual(0);
653
+ });
654
+
655
+ it("skips unknown roles on left in paths", () => {
656
+ const { sqlQueries } = sqlFor(`
657
+ (root: Root) {
658
+ successor: MyApplication.Project [
659
+ successor->unknown: Root = root
660
+ ]
661
+ }`);
662
+
663
+ expect(sqlQueries.length).toEqual(0);
664
+ });
665
+
666
+ it("skips unknown roles on right in paths", () => {
667
+ const { sqlQueries } = sqlFor(`
668
+ (root: Root) {
669
+ successor: MyApplication.Successor [
670
+ successor = root->unknown: Intermediate->root: Root
671
+ ]
672
+ }`);
673
+
674
+ expect(sqlQueries.length).toEqual(0);
675
+ });
676
+
677
+ it("skips entire query when second path contains unknown role", () => {
678
+ const { sqlQueries } = sqlFor(`
679
+ (root: Root) {
680
+ successor: MyApplication.Project [
681
+ successor->root: Root = root
682
+ successor->unknown: Root = root
683
+ ]
684
+ }`);
685
+
686
+ expect(sqlQueries.length).toEqual(0);
687
+ });
688
+
689
+ it("skips entire query when first path contains unknown role", () => {
690
+ const { sqlQueries } = sqlFor(`
691
+ (root: Root) {
692
+ successor: MyApplication.Project [
693
+ successor->unknown: Root = root
694
+ successor->root: Root = root
695
+ ]
696
+ }`);
697
+
698
+ expect(sqlQueries.length).toEqual(0);
699
+ });
700
+
701
+ it("skips existential conditions that are unsatisfiable", () => {
702
+ const { sqlQueries, factTypes, roleMap } = sqlFor(`
703
+ (root: Root) {
704
+ project: MyApplication.Project [
705
+ project->root: Root = root
706
+ !E {
707
+ delete: Unknown [
708
+ delete->project: MyApplication.Project = project
709
+ ]
710
+ }
711
+ ]
712
+ }`);
713
+
714
+ expect(sqlQueries.length).toEqual(1);
715
+
716
+ expect(sqlQueries[0].sql).toEqual(
717
+ 'SELECT f2.hash as hash2, ' +
718
+ 'sort(array[f2.fact_id], \'desc\') as bookmark ' +
719
+ 'FROM public.fact f1 ' +
720
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
721
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
722
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
723
+ 'AND sort(array[f2.fact_id], \'desc\') > $4 ' +
724
+ 'ORDER BY bookmark ASC ' +
725
+ 'LIMIT $5'
726
+ );
727
+ expect(sqlQueries[0].parameters).toEqual([
728
+ getFactTypeId(factTypes, "Root"),
729
+ rootHash,
730
+ roleParameter(roleMap, factTypes, "MyApplication.Project", "root"),
731
+ [],
732
+ 100
733
+ ]);
734
+ expect(sqlQueries[0].labels).toEqual([
735
+ {
736
+ name: "project",
737
+ type: "MyApplication.Project",
738
+ index: 2
739
+ }
740
+ ])
741
+ });
742
+ });