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,209 @@
1
+ import { AuthorizationRules, dehydrateFact, ensure, FactRecord, FeedImpl, hydrate, Jinaga as j, MemoryStore } from "jinaga";
2
+
3
+ import { AuthorizationKeystore } from "../../src/authorization/authorization-keystore";
4
+ import { MemoryKeystore } from "../../src/memory/memory-keystore";
5
+
6
+ describe('Authorization', () => {
7
+ it('should authorize empty save', async () => {
8
+ const authorization = givenAuthorization();
9
+ const result = await whenSave(authorization, []);
10
+ expect(result.length).toEqual(0);
11
+ });
12
+
13
+ it('should save a new fact', async () => {
14
+ const authorization = givenAuthorization();
15
+ const result = await whenSave(authorization, dehydrateFact({
16
+ type: 'Hashtag',
17
+ word: 'vorpal'
18
+ }));
19
+ expect(result.length).toEqual(1);
20
+ });
21
+
22
+ it('should save a fact once', async () => {
23
+ const authorization = givenAuthorization();
24
+ const facts = dehydrateFact({
25
+ type: 'Hashtag',
26
+ word: 'vorpal'
27
+ });
28
+ await whenSave(authorization, facts);
29
+ const result = await whenSave(authorization, facts);
30
+ expect(result.length).toEqual(0);
31
+ });
32
+
33
+ it('should reject a fact from an unauthorized user', async () => {
34
+ const authorization = givenAuthorization();
35
+ const mickeyMouse = givenOtherUser();
36
+ const promise = whenSave(authorization, dehydrateFact({
37
+ type: 'Tweet',
38
+ message: 'Twas brillig',
39
+ sender: mickeyMouse
40
+ }));
41
+ await expect(promise).rejects.not.toBeNull();
42
+ });
43
+
44
+ it('should accept a fact from an authorized user', async () => {
45
+ const authorization = givenAuthorization();
46
+ const lewiscarrol = await givenLoggedInUser(authorization);
47
+ const result = await whenSave(authorization, dehydrateFact({
48
+ type: 'Tweet',
49
+ message: 'Twas brillig',
50
+ sender: lewiscarrol
51
+ }));
52
+ expect(result.length).toBeGreaterThan(0);
53
+ });
54
+
55
+ it('should accept a predecessor from an authorized user', async () => {
56
+ const authorization = givenAuthorization();
57
+ const lewiscarrol = await givenLoggedInUser(authorization);
58
+ const result = await whenSave(authorization, dehydrateFact({
59
+ type: 'Like',
60
+ user: lewiscarrol,
61
+ tweet: {
62
+ type: 'Tweet',
63
+ message: 'Twas Brillig',
64
+ sender: lewiscarrol
65
+ }
66
+ }));
67
+ expect(result.filter(r => r.type === 'Tweet').length).toEqual(1);
68
+ });
69
+
70
+ it('should reject a predecessor from an unauthorized user', async () => {
71
+ const authorization = givenAuthorization();
72
+ const lewiscarrol = await givenLoggedInUser(authorization);
73
+ const mickeyMouse = givenOtherUser();
74
+ const promise = whenSave(authorization, dehydrateFact({
75
+ type: 'Like',
76
+ user: lewiscarrol,
77
+ tweet: {
78
+ type: 'Tweet',
79
+ message: 'Hiya, Pal!',
80
+ sender: mickeyMouse
81
+ }
82
+ }));
83
+ await expect(promise).rejects.not.toBeNull();
84
+ });
85
+
86
+ it('should accept a pre-existing predecessor from an unauthorized user', async () => {
87
+ const storage = givenStorage();
88
+ const mickeyMouse = givenOtherUser();
89
+ const tweet = {
90
+ type: 'Tweet',
91
+ message: 'Hiya, Pal!',
92
+ sender: mickeyMouse
93
+ };
94
+ await storage.save(dehydrateFact(tweet).map(f => ({ fact: f, signatures: [] })));
95
+
96
+ const authorization = givenAuthorizationWithStorage(storage);
97
+ const lewiscarrol = await givenLoggedInUser(authorization);
98
+ const result = await whenSave(authorization, dehydrateFact({
99
+ type: 'Like',
100
+ user: lewiscarrol,
101
+ tweet: tweet
102
+ }));
103
+ expect(result.length).toBeGreaterThan(0);
104
+ expect(result.filter(r => r.type === 'Tweet').length).toEqual(0);
105
+ });
106
+
107
+ it('should accept a fact authorized by predecessor', async () => {
108
+ const authorization = givenAuthorization();
109
+ const lewiscarrol = await givenLoggedInUser(authorization);
110
+ const tweet = {
111
+ type: 'Tweet',
112
+ message: 'Twas brillig',
113
+ sender: lewiscarrol
114
+ };
115
+ await whenSave(authorization, dehydrateFact(tweet));
116
+
117
+ const result = await whenSave(authorization, dehydrateFact({
118
+ type: 'Delete',
119
+ tweet: tweet
120
+ }));
121
+ expect(result.length).toBeGreaterThan(0);
122
+ });
123
+
124
+ it('should accept a fact based on in-flight predecessor', async () => {
125
+ const authorization = givenAuthorization();
126
+ const lewiscarrol = await givenLoggedInUser(authorization);
127
+ const tweet = {
128
+ type: 'Tweet',
129
+ message: 'Twas brillig',
130
+ sender: lewiscarrol
131
+ };
132
+ // Note that this is missing.
133
+ // await whenSave(authorization, dehydrateFact(tweet));
134
+
135
+ const result = await whenSave(authorization, dehydrateFact({
136
+ type: 'Delete',
137
+ tweet: tweet
138
+ }));
139
+ expect(result.length).toBeGreaterThan(0);
140
+ });
141
+ });
142
+
143
+ function givenAuthorization() {
144
+ const storage = givenStorage();
145
+ return givenAuthorizationWithStorage(storage);
146
+ }
147
+
148
+ function givenStorage() {
149
+ return new MemoryStore();
150
+ }
151
+
152
+ function givenAuthorizationWithStorage(storage: MemoryStore) {
153
+ const keystore = new MemoryKeystore();
154
+ keystore.getOrCreateUserFact(givenMockUserIdentity());
155
+ const feed = new FeedImpl(storage);
156
+ const authorizationRules = new AuthorizationRules()
157
+ .any('Hashtag')
158
+ .no('Jinaga.User')
159
+ .type('Tweet', j.for(tweetSender))
160
+ .type('Like', j.for(likeUser))
161
+ .type('Delete', j.for(deleteSender))
162
+ ;
163
+ return new AuthorizationKeystore(feed, keystore, authorizationRules);
164
+ }
165
+
166
+ function givenOtherUser() {
167
+ return {
168
+ type: 'Jinaga.User',
169
+ publicKey: 'other'
170
+ };
171
+ }
172
+
173
+ async function givenLoggedInUser(authorization: AuthorizationKeystore) {
174
+ const userIdentity = givenMockUserIdentity();
175
+ const userFact = await authorization.getOrCreateUserFact(userIdentity);
176
+ const user = hydrate<{}>(userFact);
177
+ return user;
178
+ }
179
+
180
+ function givenMockUserIdentity() {
181
+ return {
182
+ provider: 'mock',
183
+ id: 'user'
184
+ };
185
+ }
186
+
187
+ async function whenSave(authorization: AuthorizationKeystore, facts: FactRecord[]) {
188
+ const userIdentity = givenMockUserIdentity();
189
+ const result = await authorization.save(userIdentity, facts);
190
+ return result;
191
+ }
192
+
193
+ function tweetSender(t: { sender: {} }) {
194
+ ensure(t).has("sender", "Jinaga.User");
195
+
196
+ return j.match(t.sender);
197
+ }
198
+
199
+ function likeUser(l: { user: {} }) {
200
+ ensure(l).has("user", "Jinaga.User");
201
+
202
+ return j.match(l.user);
203
+ }
204
+
205
+ function deleteSender(d: { tweet: { sender: {} } }) {
206
+ ensure(d).has("tweet", "Tweet").has("sender", "Jinaga.User");
207
+
208
+ return j.match(d.tweet.sender);
209
+ }
@@ -0,0 +1,385 @@
1
+ import "source-map-support/register";
2
+
3
+ import {
4
+ dehydrateReference,
5
+ Direction,
6
+ ExistentialCondition,
7
+ fromDescriptiveString,
8
+ Join,
9
+ PropertyCondition,
10
+ Step,
11
+ } from "jinaga";
12
+
13
+ import { addFactType, addRole, emptyFactTypeMap, emptyRoleMap, getFactTypeId, getRoleId } from "../../src/postgres/maps";
14
+ import { sqlFromSteps } from "../../src/postgres/sql";
15
+ import { distinct } from "../../src/util/fn";
16
+
17
+ describe('Postgres', () => {
18
+
19
+ const start = dehydrateReference({ type: 'Root' });
20
+ const startHash = 'fSS1hK7OGAeSX4ocN3acuFF87jvzCdPN3vLFUtcej0lOAsVV859UIYZLRcHUoMbyd/J31TdVn5QuE7094oqUPg==';
21
+
22
+ function sqlFor(descriptiveString: string) {
23
+ const query = fromDescriptiveString(descriptiveString);
24
+ const factTypes = allFactTypes(query.steps).filter(t => t !== 'Unknown').reduce(
25
+ (f, factType, i) => addFactType(f, factType, i + 1),
26
+ emptyFactTypeMap());
27
+ let roleMap = allRoles(query.steps, 'Root').filter(r => r.role !== 'unknown').reduce(
28
+ (r, role, i) => addRole(r, getFactTypeId(factTypes, role.type), role.role, i + 1),
29
+ emptyRoleMap());
30
+ const sqlQuery = sqlFromSteps(start, query.steps, factTypes, roleMap);
31
+ return sqlQuery ? { sql: sqlQuery.sql, parameters: sqlQuery.parameters, pathLength: sqlQuery.pathLength, empty: sqlQuery.empty, factTypes, roleMap } : null;
32
+ }
33
+
34
+ it('should parse empty query', () => {
35
+ expect(sqlFor('')).toEqual(null);
36
+ });
37
+
38
+ it('should error on successor query', () => {
39
+ const parse = () => sqlFor('S.predecessor');
40
+ expect(parse).toThrow('Missing type for role predecessor');
41
+ });
42
+
43
+ it('should error on predecessor query', () => {
44
+ const parse = () => sqlFor('P.parent');
45
+ expect(parse).toThrow(/Missing type of "Root.parent"/);
46
+ });
47
+
48
+ it('should error on double predecessor query', () => {
49
+ const parse = () => sqlFor('P.parent P.grandparent');
50
+ expect(parse).toThrow(/Missing type of "Root.parent"/);
51
+ });
52
+
53
+ it('should parse successor query with type', () => {
54
+ const { sql, parameters, pathLength, factTypes, roleMap } = sqlFor('S.predecessor F.type="IntegrationTest.Successor"');
55
+ expect(sql).toEqual(
56
+ 'SELECT f2.hash as hash2 ' +
57
+ 'FROM public.fact f1 ' +
58
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
59
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
60
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2'
61
+ );
62
+ expect(parameters[0]).toEqual(getFactTypeId(factTypes, 'Root'));
63
+ expect(parameters[1]).toEqual(startHash);
64
+ expect(parameters[2]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'IntegrationTest.Successor'), 'predecessor'));
65
+ expect(pathLength).toEqual(1);
66
+ });
67
+
68
+ it('should parse predecessor query with type', () => {
69
+ const { sql, parameters, pathLength, factTypes, roleMap } = sqlFor('P.parent F.type="Parent"');
70
+ expect(sql).toEqual(
71
+ 'SELECT f2.hash as hash2 ' +
72
+ 'FROM public.fact f1 ' +
73
+ 'JOIN public.edge e1 ON e1.successor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
74
+ 'JOIN public.fact f2 ON f2.fact_id = e1.predecessor_fact_id ' +
75
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2'
76
+ );
77
+ expect(parameters[0]).toEqual(getFactTypeId(factTypes, 'Root'));
78
+ expect(parameters[1]).toEqual(startHash);
79
+ expect(parameters[2]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'Root'), 'parent'));
80
+ expect(pathLength).toEqual(1);
81
+ });
82
+
83
+ it('should parse successor query with existential', () => {
84
+ const { sql, parameters, pathLength, factTypes, roleMap } = sqlFor('S.predecessor F.type="IntegrationTest.Successor" E(S.successor F.type="IntegrationTest.Grandchild")');
85
+ expect(sql).toEqual(
86
+ 'SELECT f2.hash as hash2 ' +
87
+ 'FROM public.fact f1 ' +
88
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
89
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
90
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
91
+ 'AND EXISTS (SELECT 1 ' +
92
+ 'FROM public.edge e2 ' +
93
+ 'WHERE e2.predecessor_fact_id = e1.successor_fact_id AND e2.role_id = $4)'
94
+ );
95
+ expect(parameters[0]).toEqual(getFactTypeId(factTypes, 'Root'));
96
+ expect(parameters[1]).toEqual(startHash);
97
+ expect(parameters[2]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'IntegrationTest.Successor'), 'predecessor'));
98
+ expect(parameters[3]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'IntegrationTest.Grandchild'), 'successor'));
99
+ expect(pathLength).toEqual(1);
100
+ });
101
+
102
+ it('should parse successor query with negative existential', () => {
103
+ const { sql, parameters, pathLength, factTypes, roleMap } = sqlFor('S.predecessor F.type="IntegrationTest.Successor" N(S.successor F.type="IntegrationTest.Grandchild")');
104
+ expect(sql).toEqual(
105
+ 'SELECT f2.hash as hash2 ' +
106
+ 'FROM public.fact f1 ' +
107
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
108
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
109
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
110
+ 'AND NOT EXISTS (SELECT 1 ' +
111
+ 'FROM public.edge e2 ' +
112
+ 'WHERE e2.predecessor_fact_id = e1.successor_fact_id AND e2.role_id = $4)'
113
+ );
114
+ expect(parameters[0]).toEqual(getFactTypeId(factTypes, 'Root'));
115
+ expect(parameters[1]).toEqual(startHash);
116
+ expect(parameters[2]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'IntegrationTest.Successor'), 'predecessor'));
117
+ expect(parameters[3]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'IntegrationTest.Grandchild'), 'successor'));
118
+ expect(pathLength).toEqual(1);
119
+ });
120
+
121
+ it('should parse successor query with existential predecessor', () => {
122
+ const { sql, parameters, pathLength, factTypes, roleMap } = sqlFor('S.parent F.type="Child" E(P.uncle F.type="Uncle")');
123
+ expect(sql).toEqual(
124
+ 'SELECT f2.hash as hash2 ' +
125
+ 'FROM public.fact f1 ' +
126
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
127
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
128
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
129
+ 'AND EXISTS (SELECT 1 ' +
130
+ 'FROM public.edge e2 ' +
131
+ 'WHERE e2.successor_fact_id = e1.successor_fact_id AND e2.role_id = $4)'
132
+ );
133
+ expect(parameters[0]).toEqual(getFactTypeId(factTypes, 'Root'));
134
+ expect(parameters[1]).toEqual(startHash);
135
+ expect(parameters[2]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'Child'), 'parent'));
136
+ expect(parameters[3]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'Child'), 'uncle'));
137
+ expect(pathLength).toEqual(1);
138
+ });
139
+
140
+ it('should parse successor query with negative existential predecessor', () => {
141
+ const { sql, parameters, pathLength, factTypes, roleMap } = sqlFor('S.parent F.type="Child" N(P.uncle F.type="Uncle")');
142
+ expect(sql).toEqual(
143
+ 'SELECT f2.hash as hash2 ' +
144
+ 'FROM public.fact f1 ' +
145
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
146
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
147
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
148
+ 'AND NOT EXISTS (SELECT 1 ' +
149
+ 'FROM public.edge e2 ' +
150
+ 'WHERE e2.successor_fact_id = e1.successor_fact_id AND e2.role_id = $4)'
151
+ );
152
+ expect(parameters[0]).toEqual(getFactTypeId(factTypes, 'Root'));
153
+ expect(parameters[1]).toEqual(startHash);
154
+ expect(parameters[2]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'Child'), 'parent'));
155
+ expect(parameters[3]).toEqual(getRoleId(roleMap, getFactTypeId(factTypes, 'Child'), 'uncle'));
156
+ expect(pathLength).toEqual(1);
157
+ });
158
+
159
+ it('should parse consecutive existential queries', () => {
160
+ const { sql, parameters, factTypes, roleMap } = sqlFor('S.parent F.type="Child" N(S.condition F.type="Condition") N(S.other F.type="Other")');
161
+ expect(sql).toEqual(
162
+ 'SELECT f2.hash as hash2 ' +
163
+ 'FROM public.fact f1 ' +
164
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
165
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
166
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
167
+ 'AND NOT EXISTS (SELECT 1 ' +
168
+ 'FROM public.edge e2 ' +
169
+ 'WHERE e2.predecessor_fact_id = e1.successor_fact_id AND e2.role_id = $4) ' +
170
+ 'AND NOT EXISTS (SELECT 1 ' +
171
+ 'FROM public.edge e3 ' +
172
+ 'WHERE e3.predecessor_fact_id = e1.successor_fact_id AND e3.role_id = $5)'
173
+ );
174
+ expect(parameters).toEqual([
175
+ getFactTypeId(factTypes, 'Root'),
176
+ startHash,
177
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Child'), 'parent'),
178
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Condition'), 'condition'),
179
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Other'), 'other')
180
+ ]);
181
+ });
182
+
183
+ it('should parse existential query form predecessor', () => {
184
+ const { sql, parameters, factTypes, roleMap } = sqlFor('S.root F.type="Identifier" N(S.prior F.type="Identifier") P.identified F.type="Identified" N(S.identified F.type="Delete")');
185
+ expect(sql).toEqual(
186
+ 'SELECT f2.hash as hash2, f3.hash as hash3 ' +
187
+ 'FROM public.fact f1 ' +
188
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
189
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
190
+ 'JOIN public.edge e3 ON e3.successor_fact_id = e1.successor_fact_id AND e3.role_id = $5 ' +
191
+ 'JOIN public.fact f3 ON f3.fact_id = e3.predecessor_fact_id ' +
192
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
193
+ 'AND NOT EXISTS (SELECT 1 ' +
194
+ 'FROM public.edge e2 ' +
195
+ 'WHERE e2.predecessor_fact_id = e1.successor_fact_id AND e2.role_id = $4) ' +
196
+ 'AND NOT EXISTS (SELECT 1 ' +
197
+ 'FROM public.edge e4 ' +
198
+ 'WHERE e4.predecessor_fact_id = e3.predecessor_fact_id AND e4.role_id = $6)'
199
+ );
200
+ expect(parameters).toEqual([
201
+ getFactTypeId(factTypes, 'Root'),
202
+ startHash,
203
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Identifier'), 'root'),
204
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Identifier'), 'prior'),
205
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Identifier'), 'identified'),
206
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Delete'), 'identified')
207
+ ]);
208
+ });
209
+
210
+ it('should error on existential query on predecessor with no type', () => {
211
+ const parse = () => sqlFor('S.root F.type="Identifier" N(S.prior F.type="Identifier") P.identified N(S.identified F.type="Delete")');
212
+ expect(parse).toThrow(/Missing type of "Identifier.identified"/);
213
+ });
214
+
215
+ it('should parse zig-zag pipeline', () => {
216
+ const { sql, parameters, pathLength, factTypes, roleMap } = sqlFor('S.user F.type="Assignment" P.project F.type="Project" S.project F.type="Task" S.task F.type="Task.Title"');
217
+ expect(sql).toEqual(
218
+ 'SELECT f2.hash as hash2, f3.hash as hash3, f4.hash as hash4, f5.hash as hash5 ' +
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.successor_fact_id = e1.successor_fact_id AND e2.role_id = $4 ' +
223
+ 'JOIN public.fact f3 ON f3.fact_id = e2.predecessor_fact_id ' +
224
+ 'JOIN public.edge e3 ON e3.predecessor_fact_id = e2.predecessor_fact_id AND e3.role_id = $5 ' +
225
+ 'JOIN public.fact f4 ON f4.fact_id = e3.successor_fact_id ' +
226
+ 'JOIN public.edge e4 ON e4.predecessor_fact_id = e3.successor_fact_id AND e4.role_id = $6 ' +
227
+ 'JOIN public.fact f5 ON f5.fact_id = e4.successor_fact_id ' +
228
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2'
229
+ );
230
+ expect(parameters).toEqual([
231
+ getFactTypeId(factTypes, 'Root'),
232
+ startHash,
233
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Assignment'), 'user'),
234
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Assignment'), 'project'),
235
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Task'), 'project'),
236
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Task.Title'), 'task')
237
+ ]);
238
+ expect(pathLength).toEqual(4);
239
+ });
240
+
241
+ it('should parse query with unknown successor type', () => {
242
+ const { empty } = sqlFor('S.root F.type="Unknown"');
243
+ expect(empty).toBeTruthy();
244
+ });
245
+
246
+ it('should parse query with unknown predecessor type', () => {
247
+ const { empty } = sqlFor('S.root F.type="Assignment" P.pred F.type="Unknown" P.user F.type="Jinaga.User"');
248
+ expect(empty).toBeTruthy();
249
+ });
250
+
251
+ it('should parse query with some unknown types', () => {
252
+ const { empty } = sqlFor('S.root F.type="Unknown" P.user F.type="Jinaga.User"');
253
+ expect(empty).toBeTruthy();
254
+ });
255
+
256
+ it('should parse query with unknown successor role', () => {
257
+ const { empty } = sqlFor('S.unknown F.type="Successor"');
258
+ expect(empty).toBeTruthy();
259
+ });
260
+
261
+ it('should parse query with unknown predecessor role', () => {
262
+ const { empty } = sqlFor('S.root F.type="Assignment" P.unknown F.type="Jinaga.User"');
263
+ expect(empty).toBeTruthy();
264
+ });
265
+
266
+ it('should parse query with some unknown predecessor roles', () => {
267
+ const { empty } = sqlFor('S.root F.type="Assignment" P.predecessor F.type="Predecessor" P.unknown F.type="Jinaga.User"');
268
+ expect(empty).toBeTruthy();
269
+ });
270
+
271
+ it('should ignore negative existential with unknown type', () => {
272
+ const { empty, sql, parameters, factTypes, roleMap } = sqlFor('S.root F.type="Assignment" N(S.assignment F.type="Unknown")');
273
+ expect(empty).toBeFalsy();
274
+ expect(sql).toEqual(
275
+ 'SELECT f2.hash as hash2 ' +
276
+ 'FROM public.fact f1 ' +
277
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
278
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
279
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2'
280
+ );
281
+ expect(parameters).toEqual([
282
+ getFactTypeId(factTypes, 'Root'),
283
+ startHash,
284
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Assignment'), 'root')
285
+ ]);
286
+ });
287
+
288
+ it('should ignore negative existential with unknown role', () => {
289
+ const { empty, sql, parameters, factTypes, roleMap } = sqlFor('S.root F.type="Assignment" N(S.unknown F.type="Assignment.Delete")');
290
+ expect(empty).toBeFalsy();
291
+ expect(sql).toEqual(
292
+ 'SELECT f2.hash as hash2 ' +
293
+ 'FROM public.fact f1 ' +
294
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
295
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
296
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2'
297
+ );
298
+ expect(parameters).toEqual([
299
+ getFactTypeId(factTypes, 'Root'),
300
+ startHash,
301
+ getRoleId(roleMap, getFactTypeId(factTypes, 'Assignment'), 'root')
302
+ ]);
303
+ });
304
+
305
+ it('should elevate positive existential with unknown type', () => {
306
+ const { empty } = sqlFor('S.root F.type="Assignment" E(S.assignment F.type="Unknown")');
307
+ expect(empty).toBeTruthy();
308
+ });
309
+
310
+ it('should elevate positive existential with unknown role', () => {
311
+ const { empty } = sqlFor('S.root F.type="Assignment" E(S.unknown F.type="Assignment.Delete")');
312
+ expect(empty).toBeTruthy();
313
+ });
314
+
315
+ it('should parse nested existential condition', () => {
316
+ const { sql, parameters, factTypes, roleMap } = sqlFor('S.catalog F.type="ImprovingU.Idea" N(S.idea F.type="ImprovingU.Idea.Deletion" N(S.ideaDeletion F.type="ImprovingU.Idea.Restore"))');
317
+
318
+ expect(sql).toEqual(
319
+ 'SELECT f2.hash as hash2 ' +
320
+ 'FROM public.fact f1 ' +
321
+ 'JOIN public.edge e1 ON e1.predecessor_fact_id = f1.fact_id AND e1.role_id = $3 ' +
322
+ 'JOIN public.fact f2 ON f2.fact_id = e1.successor_fact_id ' +
323
+ 'WHERE f1.fact_type_id = $1 AND f1.hash = $2 ' +
324
+ 'AND NOT EXISTS (' +
325
+ 'SELECT 1 ' +
326
+ 'FROM public.edge e2 ' +
327
+ 'WHERE e2.predecessor_fact_id = e1.successor_fact_id AND e2.role_id = $4 ' +
328
+ 'AND NOT EXISTS (' +
329
+ 'SELECT 1 ' +
330
+ 'FROM public.edge e3 ' +
331
+ 'WHERE e3.predecessor_fact_id = e2.successor_fact_id AND e3.role_id = $5' +
332
+ ')' +
333
+ ')');
334
+ expect(parameters).toEqual([
335
+ getFactTypeId(factTypes, 'Catalog'),
336
+ startHash,
337
+ getRoleId(roleMap, getFactTypeId(factTypes, 'ImprovingU.Idea'), 'catalog'),
338
+ getRoleId(roleMap, getFactTypeId(factTypes, 'ImprovingU.Idea.Deletion'), 'idea'),
339
+ getRoleId(roleMap, getFactTypeId(factTypes, 'ImprovingU.Idea.Restore'), 'ideaDeletion')
340
+ ]);
341
+ });
342
+ });
343
+
344
+ function allFactTypes(steps: Step[]): string[] {
345
+ const factTypes = steps
346
+ .filter(step => step instanceof PropertyCondition && step.name === 'type')
347
+ .map(step => (step as PropertyCondition).value);
348
+ const childFactTypes = steps
349
+ .filter(step => step instanceof ExistentialCondition)
350
+ .flatMap(step => allFactTypes((step as ExistentialCondition).steps));
351
+ return [...factTypes, ...childFactTypes].filter(distinct);
352
+ }
353
+
354
+ function allRoles(steps: Step[], initialType: string) {
355
+ let roles: { type: string; role: string }[] = [];
356
+ let type: string = initialType;
357
+ let role: string = undefined;
358
+
359
+ for (const step of steps) {
360
+ if (step instanceof PropertyCondition) {
361
+ if (step.name === 'type') {
362
+ type = step.value;
363
+ if (role) {
364
+ roles.push({ type, role });
365
+ role = undefined;
366
+ }
367
+ }
368
+ }
369
+ else if (step instanceof Join) {
370
+ if (step.direction === Direction.Predecessor) {
371
+ roles.push({ type, role: step.role });
372
+ role = undefined;
373
+ }
374
+ else {
375
+ role = step.role;
376
+ }
377
+ type = undefined;
378
+ }
379
+ else if (step instanceof ExistentialCondition) {
380
+ roles = roles.concat(allRoles(step.steps, type));
381
+ }
382
+ }
383
+
384
+ return roles;
385
+ }