jinaga-server 3.1.5 → 3.2.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 (38) 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 +1 -0
  9. package/dist/postgres/postgres-store.js +17 -0
  10. package/dist/postgres/postgres-store.js.map +1 -1
  11. package/dist/postgres/purge-sql.d.ts +6 -0
  12. package/dist/postgres/purge-sql.js +103 -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/package-lock.json +147 -86
  23. package/integration-test/jinaga-test/package.json +3 -3
  24. package/package.json +4 -4
  25. package/replicator/package-lock.json +139 -71
  26. package/replicator/package.json +2 -2
  27. package/scripts/migrate_purge.sql +45 -0
  28. package/scripts/purge.sql +33 -0
  29. package/src/http/router.ts +38 -84
  30. package/src/jinaga-server.ts +27 -5
  31. package/src/postgres/postgres-store.ts +18 -0
  32. package/src/postgres/purge-sql.ts +125 -0
  33. package/src/postgres/query-description.ts +1 -1
  34. package/src/postgres/specification-result-sql.ts +3 -3
  35. package/src/postgres/specification-sql.ts +4 -4
  36. package/test/authorization/authorizationSpec.ts +1 -1
  37. package/test/models/blog.ts +92 -0
  38. package/test/postgres/purgeSqlSpec.ts +218 -0
@@ -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,218 @@
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
+ ), facts AS (
45
+ DELETE
46
+ FROM public.fact f
47
+ USING targets t WHERE t.fact_id = f.fact_id
48
+ RETURNING f.fact_id
49
+ )
50
+ SELECT fact_id FROM facts
51
+ `;
52
+
53
+ expect(sql).toBe(expected);
54
+ expect(parameters).toEqual([
55
+ ensureGetFactTypeId(factTypes, Site.Type),
56
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site')
57
+ ]);
58
+ });
59
+
60
+ it("should handle two levels of successors", () => {
61
+ const specification = model.given(Site).match((site, facts) =>
62
+ facts.ofType(SitePurged)
63
+ .join(purged => purged.deleted.site, site)
64
+ ).specification;
65
+ const factTypes = buildFactTypeMap(
66
+ Site.Type,
67
+ SiteDeleted.Type,
68
+ SitePurged.Type
69
+ );
70
+ const roleMap = buildRoleMap(factTypes,
71
+ [Site.Type, ['creator']],
72
+ [SiteDeleted.Type, ['site']],
73
+ [SitePurged.Type, ['deleted']]
74
+ );
75
+ const schema = 'public';
76
+ const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
77
+
78
+ const expected =
79
+ `WITH candidates AS (
80
+ SELECT
81
+ f1.fact_id as purge_root,
82
+ f3.fact_id as trigger1
83
+ FROM public.fact f1
84
+ JOIN public.edge e1
85
+ ON e1.predecessor_fact_id = f1.fact_id
86
+ AND e1.role_id = $2
87
+ JOIN public.fact f2
88
+ ON f2.fact_id = e1.successor_fact_id
89
+ JOIN public.edge e2
90
+ ON e2.predecessor_fact_id = f2.fact_id
91
+ AND e2.role_id = $3
92
+ JOIN public.fact f3
93
+ ON f3.fact_id = e2.successor_fact_id
94
+ WHERE f1.fact_type_id = $1
95
+ ), targets AS (
96
+ SELECT a.fact_id
97
+ FROM public.ancestor a
98
+ JOIN candidates c ON c.purge_root = a.ancestor_fact_id
99
+ WHERE NOT EXISTS (
100
+ SELECT 1
101
+ FROM candidates c2
102
+ WHERE a.fact_id = c2.trigger1
103
+ )
104
+ ), facts AS (
105
+ DELETE
106
+ FROM public.fact f
107
+ USING targets t WHERE t.fact_id = f.fact_id
108
+ RETURNING f.fact_id
109
+ )
110
+ SELECT fact_id FROM facts
111
+ `;
112
+
113
+ expect(sql).toBe(expected);
114
+ expect(parameters).toEqual([
115
+ ensureGetFactTypeId(factTypes, Site.Type),
116
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
117
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SitePurged.Type), 'deleted')
118
+ ]);
119
+ });
120
+
121
+ it("should handle two labeled successors", () => {
122
+ const specification = model.given(Site).match((site, facts) =>
123
+ facts.ofType(SiteDeleted)
124
+ .join(deleted => deleted.site, site)
125
+ .selectMany(deleted =>
126
+ facts.ofType(SitePurged)
127
+ .join(purged => purged.deleted, deleted)
128
+ )).specification;
129
+ const factTypes = buildFactTypeMap(
130
+ Site.Type,
131
+ SiteDeleted.Type,
132
+ SitePurged.Type
133
+ );
134
+ const roleMap = buildRoleMap(factTypes,
135
+ [Site.Type, ['creator']],
136
+ [SiteDeleted.Type, ['site']],
137
+ [SitePurged.Type, ['deleted']]
138
+ );
139
+ const schema = 'public';
140
+ const { sql, parameters } = whenGeneratePurgeSql(specification, factTypes, roleMap, schema);
141
+
142
+ const expected =
143
+ `WITH candidates AS (
144
+ SELECT
145
+ f1.fact_id as purge_root,
146
+ f2.fact_id as trigger1,
147
+ f3.fact_id as trigger2
148
+ FROM public.fact f1
149
+ JOIN public.edge e1
150
+ ON e1.predecessor_fact_id = f1.fact_id
151
+ AND e1.role_id = $2
152
+ JOIN public.fact f2
153
+ ON f2.fact_id = e1.successor_fact_id
154
+ JOIN public.edge e2
155
+ ON e2.predecessor_fact_id = f2.fact_id
156
+ AND e2.role_id = $3
157
+ JOIN public.fact f3
158
+ ON f3.fact_id = e2.successor_fact_id
159
+ WHERE f1.fact_type_id = $1
160
+ ), targets AS (
161
+ SELECT a.fact_id
162
+ FROM public.ancestor a
163
+ JOIN candidates c ON c.purge_root = a.ancestor_fact_id
164
+ WHERE NOT EXISTS (
165
+ SELECT 1
166
+ FROM candidates c2
167
+ WHERE a.fact_id = c2.trigger1
168
+ OR a.fact_id = c2.trigger2
169
+ )
170
+ ), facts AS (
171
+ DELETE
172
+ FROM public.fact f
173
+ USING targets t WHERE t.fact_id = f.fact_id
174
+ RETURNING f.fact_id
175
+ )
176
+ SELECT fact_id FROM facts
177
+ `;
178
+
179
+ expect(sql).toBe(expected);
180
+ expect(parameters).toEqual([
181
+ ensureGetFactTypeId(factTypes, Site.Type),
182
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SiteDeleted.Type), 'site'),
183
+ getRoleId(roleMap, ensureGetFactTypeId(factTypes, SitePurged.Type), 'deleted')
184
+ ]);
185
+ });
186
+ });
187
+
188
+ function whenGeneratePurgeSql(specification: Specification, factTypes: FactTypeMap, roleMap: Map<number, Map<string, number>>, schema: string) {
189
+ const purgeCommand = purgeSqlFromSpecification(specification, factTypes, roleMap, schema);
190
+ if (purgeCommand === null) {
191
+ throw new Error("The specification was not satisfiable.");
192
+ }
193
+ return purgeCommand;
194
+ }
195
+
196
+ function buildFactTypeMap(...types: string[]): FactTypeMap {
197
+ const factTypeMap: FactTypeMap = new Map();
198
+ types.forEach((type, index) => {
199
+ factTypeMap.set(type, index + 1);
200
+ });
201
+ return factTypeMap;
202
+ }
203
+
204
+ function buildRoleMap(factTypes: FactTypeMap, ...args: [string, string[]][]): Map<number, Map<string, number>> {
205
+ const roleMap = new Map<number, Map<string, number>>();
206
+ args.forEach(([type, roles]) => {
207
+ const typeId = factTypes.get(type);
208
+ if (typeId === undefined) {
209
+ throw new Error(`Unknown fact type: ${type}`);
210
+ }
211
+ const roleIdMap = new Map<string, number>();
212
+ roles.forEach((role, roleIndex) => {
213
+ roleIdMap.set(role, typeId * 10 + roleIndex + 1);
214
+ });
215
+ roleMap.set(typeId, roleIdMap);
216
+ });
217
+ return roleMap;
218
+ }