jinaga-server 3.1.5 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.github/workflows/main.yml +1 -1
  2. package/.github/workflows/publish.yml +3 -3
  3. package/dist/http/router.js +37 -84
  4. package/dist/http/router.js.map +1 -1
  5. package/dist/jinaga-server.d.ts +2 -1
  6. package/dist/jinaga-server.js +18 -4
  7. package/dist/jinaga-server.js.map +1 -1
  8. package/dist/postgres/postgres-store.d.ts +2 -0
  9. package/dist/postgres/postgres-store.js +34 -0
  10. package/dist/postgres/postgres-store.js.map +1 -1
  11. package/dist/postgres/purge-sql.d.ts +7 -0
  12. package/dist/postgres/purge-sql.js +151 -0
  13. package/dist/postgres/purge-sql.js.map +1 -0
  14. package/dist/postgres/query-description.d.ts +1 -1
  15. package/dist/postgres/query-description.js +3 -3
  16. package/dist/postgres/query-description.js.map +1 -1
  17. package/dist/postgres/specification-result-sql.js +1 -1
  18. package/dist/postgres/specification-result-sql.js.map +1 -1
  19. package/dist/postgres/specification-sql.js +2 -2
  20. package/dist/postgres/specification-sql.js.map +1 -1
  21. package/integration-test/docker-compose.yaml +0 -1
  22. package/integration-test/jinaga-test/Dockerfile +2 -0
  23. package/integration-test/jinaga-test/orderModel.js +104 -0
  24. package/integration-test/jinaga-test/package-lock.json +147 -86
  25. package/integration-test/jinaga-test/package.json +3 -3
  26. package/integration-test/jinaga-test/purge.test.js +224 -0
  27. package/integration-test/postgres/1-setup.sql +8 -3
  28. package/integration-test/postgres/2-grant.sh +4 -4
  29. package/integration-test/postgres/Dockerfile +1 -1
  30. package/package.json +5 -4
  31. package/replicator/package-lock.json +139 -71
  32. package/replicator/package.json +2 -2
  33. package/scripts/migrate_purge.sql +45 -0
  34. package/scripts/purge.sql +33 -0
  35. package/setup.sh +4 -4
  36. package/setup.sql +59 -3
  37. package/src/http/router.ts +38 -84
  38. package/src/jinaga-server.ts +27 -5
  39. package/src/postgres/postgres-store.ts +37 -0
  40. package/src/postgres/purge-sql.ts +176 -0
  41. package/src/postgres/query-description.ts +1 -1
  42. package/src/postgres/specification-result-sql.ts +3 -3
  43. package/src/postgres/specification-sql.ts +4 -4
  44. package/test/authorization/authorizationSpec.ts +1 -1
  45. package/test/models/blog.ts +92 -0
  46. package/test/postgres/purgeDescendantsSqlSpec.ts +41 -0
  47. package/test/postgres/purgeSqlSpec.ts +246 -0
@@ -19,7 +19,7 @@ import {
19
19
  } from "jinaga";
20
20
 
21
21
  import { FactTypeMap, RoleMap } from "./maps";
22
- import { EdgeDescription, ExistentialConditionDescription, FactByLabel, QueryDescription, QueryDescriptionBuider } from "./query-description";
22
+ import { EdgeDescription, ExistentialConditionDescription, FactByLabel, QueryDescription, QueryDescriptionBuilder } from "./query-description";
23
23
 
24
24
  function generateResultSqlQuery(queryDescription: QueryDescription, schema: string): SpecificationSqlQuery {
25
25
  const allLabels = [ ...queryDescription.inputs, ...queryDescription.outputs ];
@@ -481,7 +481,7 @@ interface NamedResultComposer {
481
481
 
482
482
  class ResultDescriptionBuilder {
483
483
  constructor(
484
- private queryDescriptionBuilder: QueryDescriptionBuider
484
+ private queryDescriptionBuilder: QueryDescriptionBuilder
485
485
  ) { }
486
486
 
487
487
  buildDescription(start: FactReference[], specification: Specification): ResultDescription {
@@ -547,7 +547,7 @@ function idsEqual(a: number[], b: number[]) {
547
547
  }
548
548
 
549
549
  export function resultSqlFromSpecification(start: FactReference[], specification: Specification, factTypes: FactTypeMap, roleMap: RoleMap, schema: string): ResultComposer | null {
550
- const queryDescriptionBuilder = new QueryDescriptionBuider(factTypes, roleMap);
550
+ const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
551
551
  const descriptionBuilder = new ResultDescriptionBuilder(queryDescriptionBuilder);
552
552
  const description = descriptionBuilder.buildDescription(start, specification);
553
553
 
@@ -5,7 +5,7 @@ import {
5
5
  validateGiven
6
6
  } from "jinaga";
7
7
 
8
- import { EdgeDescription, ExistentialConditionDescription, QueryDescription, QueryDescriptionBuider } from "./query-description";
8
+ import { EdgeDescription, ExistentialConditionDescription, QueryDescription, QueryDescriptionBuilder } from "./query-description";
9
9
 
10
10
  interface SpecificationLabel {
11
11
  type: string;
@@ -173,7 +173,7 @@ export function sqlFromSpecification(start: FactReference[], schema: string, boo
173
173
  }, new Map<string, FactReference>());
174
174
 
175
175
  const feeds = buildFeeds(specification);
176
- const queryDescriptionBuilder = new QueryDescriptionBuider(factTypes, roleMap);
176
+ const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
177
177
  const feedAndBookmark = feeds.map((feed, index) => ({
178
178
  feed,
179
179
  bookmark: bookmarks[index]
@@ -193,7 +193,7 @@ export function sqlFromSpecification(start: FactReference[], schema: string, boo
193
193
  }
194
194
 
195
195
  export function sqlFromFeed(feed: Specification, start: FactReference[], schema: string, bookmark: string, limit: number, factTypes: Map<string, number>, roleMap: Map<number, Map<string, number>>): SpecificationSqlQuery | null {
196
- const queryDescriptionBuilder = new QueryDescriptionBuider(factTypes, roleMap);
196
+ const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
197
197
  const queryDescription = buildQueryDescription(queryDescriptionBuilder, feed, start);
198
198
  if (!queryDescription.isSatisfiable()) {
199
199
  return null;
@@ -202,7 +202,7 @@ export function sqlFromFeed(feed: Specification, start: FactReference[], schema:
202
202
  return sql;
203
203
  }
204
204
 
205
- function buildQueryDescription(queryDescriptionBuilder: QueryDescriptionBuider, specification: Specification, start: FactReference[]): QueryDescription {
205
+ function buildQueryDescription(queryDescriptionBuilder: QueryDescriptionBuilder, specification: Specification, start: FactReference[]): QueryDescription {
206
206
  const { queryDescription } = queryDescriptionBuilder.addEdges(
207
207
  QueryDescription.unsatisfiable,
208
208
  specification.given,
@@ -164,7 +164,7 @@ function givenAuthorizationWithStorage(storage: MemoryStore) {
164
164
  const fork = new PassThroughFork(storage);
165
165
  const observableSource = new ObservableSource(storage);
166
166
  const network = new NetworkNoOp();
167
- const factManager = new FactManager(fork, observableSource, storage, network);
167
+ const factManager = new FactManager(fork, observableSource, storage, network, []);
168
168
  const model = buildModel(b => b
169
169
  .type(Hashtag)
170
170
  .type(User)
@@ -0,0 +1,92 @@
1
+ import { buildModel, PurgeConditions, User } from "jinaga";
2
+
3
+ export class Site {
4
+ static Type = 'Blog.Site' as const;
5
+ type = Site.Type;
6
+
7
+ constructor(
8
+ public readonly creator: User,
9
+ public readonly domain: string
10
+ ) { }
11
+ }
12
+
13
+ export class SiteDeleted {
14
+ static Type = 'Blog.Site.Deleted' as const;
15
+ type = SiteDeleted.Type;
16
+
17
+ constructor(
18
+ public readonly site: Site
19
+ ) { }
20
+ }
21
+
22
+ export class SitePurged {
23
+ static Type = 'Blog.Site.Purged' as const;
24
+ type = SitePurged.Type;
25
+
26
+ constructor(
27
+ public readonly deleted: SiteDeleted
28
+ ) { }
29
+ }
30
+
31
+ export class Post {
32
+ static Type = 'Blog.Post' as const;
33
+ type = Post.Type;
34
+
35
+ constructor(
36
+ public readonly site: Site,
37
+ public readonly createdAt: Date | string
38
+ ) { }
39
+ }
40
+
41
+ export class PostDeleted {
42
+ static Type = 'Blog.Post.Deleted' as const;
43
+ type = PostDeleted.Type;
44
+
45
+ constructor(
46
+ public readonly post: Post
47
+ ) { }
48
+ }
49
+
50
+ export class Title {
51
+ static Type = 'Blog.Post.Title' as const;
52
+ type = Title.Type;
53
+
54
+ constructor(
55
+ public readonly post: Post,
56
+ public readonly value: string,
57
+ public readonly prior: Title[]
58
+ ) { }
59
+ }
60
+
61
+ export const model = buildModel(b => b
62
+ .type(Site, f => f
63
+ .predecessor('creator', User)
64
+ )
65
+ .type(Post, f => f
66
+ .predecessor('site', Site)
67
+ )
68
+ .type(SiteDeleted, f => f
69
+ .predecessor('site', Site)
70
+ )
71
+ .type(SitePurged, f => f
72
+ .predecessor('deleted', SiteDeleted)
73
+ )
74
+ .type(Title, f => f
75
+ .predecessor('post', Post)
76
+ .predecessor('prior', Title)
77
+ )
78
+ .type(PostDeleted, f => f
79
+ .predecessor('post', Post)
80
+ )
81
+ )
82
+
83
+ export const purgeConditions = (p: PurgeConditions) => p
84
+ .whenExists(model.given(Site).match((site, facts) =>
85
+ facts.ofType(SiteDeleted)
86
+ .join(deleted => deleted.site, site)
87
+ ))
88
+ .whenExists(model.given(Post).match((post, facts) =>
89
+ facts.ofType(PostDeleted)
90
+ .join(deleted => deleted.post, post)
91
+ ))
92
+ ;
@@ -0,0 +1,41 @@
1
+ import { purgeDescendantsSql } from "../../src/postgres/purge-sql";
2
+
3
+ describe("Purge Descendants SQL", () => {
4
+ it("should handle single trigger", () => {
5
+ const sql = purgeDescendantsSql(1, 'public');
6
+
7
+ const expected =
8
+ `WITH purge_root AS (\n` +
9
+ ` SELECT pr.fact_id\n` +
10
+ ` FROM public.fact pr\n` +
11
+ ` WHERE pr.fact_type_id = $1\n` +
12
+ ` AND pr.hash = $2\n` +
13
+ `), triggers AS (\n` +
14
+ ` SELECT t.fact_id\n` +
15
+ ` FROM public.fact t\n` +
16
+ ` WHERE (t.fact_type_id = $3 AND t.hash = $4)\n` +
17
+ `), triggers_and_ancestors AS (\n` +
18
+ ` SELECT t.fact_id\n` +
19
+ ` FROM triggers t\n` +
20
+ ` UNION\n` +
21
+ ` SELECT a.ancestor_fact_id\n` +
22
+ ` FROM public.ancestor a\n` +
23
+ ` JOIN triggers t\n` +
24
+ ` ON a.fact_id = t.fact_id\n` +
25
+ `), targets AS (\n` +
26
+ ` SELECT a.fact_id\n` +
27
+ ` FROM public.ancestor a\n` +
28
+ ` JOIN purge_root pr\n` +
29
+ ` ON a.ancestor_fact_id = pr.fact_id\n` +
30
+ ` WHERE a.fact_id NOT IN (SELECT * FROM triggers_and_ancestors)\n` +
31
+ `), facts AS (\n` +
32
+ ` DELETE\n` +
33
+ ` FROM public.fact f\n` +
34
+ ` USING targets t WHERE t.fact_id = f.fact_id\n` +
35
+ ` RETURNING f.fact_id\n` +
36
+ `)\n` +
37
+ `SELECT fact_id FROM facts\n`;
38
+
39
+ expect(sql).toBe(expected);
40
+ });
41
+ });
@@ -0,0 +1,246 @@
1
+ import { Specification } from "jinaga";
2
+ import { ensureGetFactTypeId, FactTypeMap, getRoleId } from "../../src/postgres/maps";
3
+ import { purgeSqlFromSpecification } from "../../src/postgres/purge-sql";
4
+ import { model, Site, SiteDeleted, SitePurged } from "../models/blog";
5
+
6
+ describe("Purge SQL", () => {
7
+ it("should handle direct successor", () => {
8
+ const specification = model.given(Site).match((site, facts) =>
9
+ facts.ofType(SiteDeleted)
10
+ .join(deleted => deleted.site, site)
11
+ ).specification;
12
+ const factTypes = buildFactTypeMap(
13
+ Site.Type,
14
+ SiteDeleted.Type
15
+ );
16
+ const roleMap = buildRoleMap(factTypes,
17
+ [Site.Type, ['creator']],
18
+ [SiteDeleted.Type, ['site']]
19
+ );
20
+ const schema = 'public';
21
+ const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
22
+
23
+ const expected =
24
+ `WITH candidates AS (
25
+ SELECT
26
+ f1.fact_id as purge_root,
27
+ f2.fact_id as trigger1
28
+ FROM public.fact f1
29
+ JOIN public.edge e1
30
+ ON e1.predecessor_fact_id = f1.fact_id
31
+ AND e1.role_id = $2
32
+ JOIN public.fact f2
33
+ ON f2.fact_id = e1.successor_fact_id
34
+ WHERE f1.fact_type_id = $1
35
+ ), targets AS (
36
+ SELECT a.fact_id
37
+ FROM public.ancestor a
38
+ JOIN candidates c ON c.purge_root = a.ancestor_fact_id
39
+ WHERE NOT EXISTS (
40
+ SELECT 1
41
+ FROM candidates c2
42
+ WHERE a.fact_id = c2.trigger1
43
+ )
44
+ AND NOT EXISTS (
45
+ SELECT 1
46
+ FROM candidates c2
47
+ JOIN public.ancestor a2
48
+ ON a2.fact_id = c2.trigger1
49
+ WHERE a.fact_id = a2.ancestor_fact_id
50
+ )
51
+ ), facts AS (
52
+ DELETE
53
+ FROM public.fact f
54
+ USING targets t WHERE t.fact_id = f.fact_id
55
+ RETURNING f.fact_id
56
+ )
57
+ SELECT fact_id FROM facts
58
+ `;
59
+
60
+ expect(sql).toBe(expected);
61
+ expect(parameters).toEqual([
62
+ ensureGetFactTypeId(factTypes, Site.Type),
63
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site')
64
+ ]);
65
+ });
66
+
67
+ it("should handle two levels of successors", () => {
68
+ const specification = model.given(Site).match((site, facts) =>
69
+ facts.ofType(SitePurged)
70
+ .join(purged => purged.deleted.site, site)
71
+ ).specification;
72
+ const factTypes = buildFactTypeMap(
73
+ Site.Type,
74
+ SiteDeleted.Type,
75
+ SitePurged.Type
76
+ );
77
+ const roleMap = buildRoleMap(factTypes,
78
+ [Site.Type, ['creator']],
79
+ [SiteDeleted.Type, ['site']],
80
+ [SitePurged.Type, ['deleted']]
81
+ );
82
+ const schema = 'public';
83
+ const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
84
+
85
+ const expected =
86
+ `WITH candidates AS (
87
+ SELECT
88
+ f1.fact_id as purge_root,
89
+ f3.fact_id as trigger1
90
+ FROM public.fact f1
91
+ JOIN public.edge e1
92
+ ON e1.predecessor_fact_id = f1.fact_id
93
+ AND e1.role_id = $2
94
+ JOIN public.fact f2
95
+ ON f2.fact_id = e1.successor_fact_id
96
+ JOIN public.edge e2
97
+ ON e2.predecessor_fact_id = f2.fact_id
98
+ AND e2.role_id = $3
99
+ JOIN public.fact f3
100
+ ON f3.fact_id = e2.successor_fact_id
101
+ WHERE f1.fact_type_id = $1
102
+ ), targets AS (
103
+ SELECT a.fact_id
104
+ FROM public.ancestor a
105
+ JOIN candidates c ON c.purge_root = a.ancestor_fact_id
106
+ WHERE NOT EXISTS (
107
+ SELECT 1
108
+ FROM candidates c2
109
+ WHERE a.fact_id = c2.trigger1
110
+ )
111
+ AND NOT EXISTS (
112
+ SELECT 1
113
+ FROM candidates c2
114
+ JOIN public.ancestor a2
115
+ ON a2.fact_id = c2.trigger1
116
+ WHERE a.fact_id = a2.ancestor_fact_id
117
+ )
118
+ ), facts AS (
119
+ DELETE
120
+ FROM public.fact f
121
+ USING targets t WHERE t.fact_id = f.fact_id
122
+ RETURNING f.fact_id
123
+ )
124
+ SELECT fact_id FROM facts
125
+ `;
126
+
127
+ expect(sql).toBe(expected);
128
+ expect(parameters).toEqual([
129
+ ensureGetFactTypeId(factTypes, Site.Type),
130
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
131
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SitePurged.Type), 'deleted')
132
+ ]);
133
+ });
134
+
135
+ it("should handle two labeled successors", () => {
136
+ const specification = model.given(Site).match((site, facts) =>
137
+ facts.ofType(SiteDeleted)
138
+ .join(deleted => deleted.site, site)
139
+ .selectMany(deleted =>
140
+ facts.ofType(SitePurged)
141
+ .join(purged => purged.deleted, deleted)
142
+ )).specification;
143
+ const factTypes = buildFactTypeMap(
144
+ Site.Type,
145
+ SiteDeleted.Type,
146
+ SitePurged.Type
147
+ );
148
+ const roleMap = buildRoleMap(factTypes,
149
+ [Site.Type, ['creator']],
150
+ [SiteDeleted.Type, ['site']],
151
+ [SitePurged.Type, ['deleted']]
152
+ );
153
+ const schema = 'public';
154
+ const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
155
+
156
+ const expected =
157
+ `WITH candidates AS (
158
+ SELECT
159
+ f1.fact_id as purge_root,
160
+ f2.fact_id as trigger1,
161
+ f3.fact_id as trigger2
162
+ FROM public.fact f1
163
+ JOIN public.edge e1
164
+ ON e1.predecessor_fact_id = f1.fact_id
165
+ AND e1.role_id = $2
166
+ JOIN public.fact f2
167
+ ON f2.fact_id = e1.successor_fact_id
168
+ JOIN public.edge e2
169
+ ON e2.predecessor_fact_id = f2.fact_id
170
+ AND e2.role_id = $3
171
+ JOIN public.fact f3
172
+ ON f3.fact_id = e2.successor_fact_id
173
+ WHERE f1.fact_type_id = $1
174
+ ), targets AS (
175
+ SELECT a.fact_id
176
+ FROM public.ancestor a
177
+ JOIN candidates c ON c.purge_root = a.ancestor_fact_id
178
+ WHERE NOT EXISTS (
179
+ SELECT 1
180
+ FROM candidates c2
181
+ WHERE a.fact_id = c2.trigger1
182
+ OR a.fact_id = c2.trigger2
183
+ )
184
+ AND NOT EXISTS (
185
+ SELECT 1
186
+ FROM candidates c2
187
+ JOIN public.ancestor a2
188
+ ON a2.fact_id = c2.trigger1
189
+ WHERE a.fact_id = a2.ancestor_fact_id
190
+ )
191
+ AND NOT EXISTS (
192
+ SELECT 1
193
+ FROM candidates c2
194
+ JOIN public.ancestor a2
195
+ ON a2.fact_id = c2.trigger2
196
+ WHERE a.fact_id = a2.ancestor_fact_id
197
+ )
198
+ ), facts AS (
199
+ DELETE
200
+ FROM public.fact f
201
+ USING targets t WHERE t.fact_id = f.fact_id
202
+ RETURNING f.fact_id
203
+ )
204
+ SELECT fact_id FROM facts
205
+ `;
206
+
207
+ expect(sql).toBe(expected);
208
+ expect(parameters).toEqual([
209
+ ensureGetFactTypeId(factTypes, Site.Type),
210
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
211
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SitePurged.Type), 'deleted')
212
+ ]);
213
+ });
214
+ });
215
+
216
+ function whenGeneratePurgeSql(specification: Specification, factTypes: FactTypeMap, roleMap: Map<number, Map<string, number>>, schema: string) {
217
+ const purgeCommand = purgeSqlFromSpecification(specification, factTypes, roleMap, schema);
218
+ if (purgeCommand === null) {
219
+ throw new Error("The specification was not satisfiable.");
220
+ }
221
+ return purgeCommand;
222
+ }
223
+
224
+ function buildFactTypeMap(...types: string[]): FactTypeMap {
225
+ const factTypeMap: FactTypeMap = new Map();
226
+ types.forEach((type, index) => {
227
+ factTypeMap.set(type, index + 1);
228
+ });
229
+ return factTypeMap;
230
+ }
231
+
232
+ function buildRoleMap(factTypes: FactTypeMap, ...args: [string, string[]][]): Map<number, Map<string, number>> {
233
+ const roleMap = new Map<number, Map<string, number>>();
234
+ args.forEach(([type, roles]) => {
235
+ const typeId = factTypes.get(type);
236
+ if (typeId === undefined) {
237
+ throw new Error(`Unknown fact type: ${type}`);
238
+ }
239
+ const roleIdMap = new Map<string, number>();
240
+ roles.forEach((role, roleIndex) => {
241
+ roleIdMap.set(role, typeId * 10 + roleIndex + 1);
242
+ });
243
+ roleMap.set(typeId, roleIdMap);
244
+ });
245
+ return roleMap;
246
+ }