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.
- package/.github/workflows/main.yml +1 -1
- package/.github/workflows/publish.yml +3 -3
- package/dist/http/router.js +37 -84
- package/dist/http/router.js.map +1 -1
- package/dist/jinaga-server.d.ts +2 -1
- package/dist/jinaga-server.js +18 -4
- package/dist/jinaga-server.js.map +1 -1
- package/dist/postgres/postgres-store.d.ts +1 -0
- package/dist/postgres/postgres-store.js +17 -0
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/purge-sql.d.ts +6 -0
- package/dist/postgres/purge-sql.js +103 -0
- package/dist/postgres/purge-sql.js.map +1 -0
- package/dist/postgres/query-description.d.ts +1 -1
- package/dist/postgres/query-description.js +3 -3
- package/dist/postgres/query-description.js.map +1 -1
- package/dist/postgres/specification-result-sql.js +1 -1
- package/dist/postgres/specification-result-sql.js.map +1 -1
- package/dist/postgres/specification-sql.js +2 -2
- package/dist/postgres/specification-sql.js.map +1 -1
- package/integration-test/docker-compose.yaml +0 -1
- package/integration-test/jinaga-test/package-lock.json +147 -86
- package/integration-test/jinaga-test/package.json +3 -3
- package/package.json +4 -4
- package/replicator/package-lock.json +139 -71
- package/replicator/package.json +2 -2
- package/scripts/migrate_purge.sql +45 -0
- package/scripts/purge.sql +33 -0
- package/src/http/router.ts +38 -84
- package/src/jinaga-server.ts +27 -5
- package/src/postgres/postgres-store.ts +18 -0
- package/src/postgres/purge-sql.ts +125 -0
- package/src/postgres/query-description.ts +1 -1
- package/src/postgres/specification-result-sql.ts +3 -3
- package/src/postgres/specification-sql.ts +4 -4
- package/test/authorization/authorizationSpec.ts +1 -1
- package/test/models/blog.ts +92 -0
- 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
|
+
}
|