jinaga-server 3.1.4 → 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 (42) hide show
  1. package/.github/workflows/main.yml +1 -1
  2. package/.github/workflows/publish.yml +3 -3
  3. package/dist/http/router.js +73 -121
  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/Dockerfile +1 -1
  23. package/integration-test/jinaga-test/package-lock.json +147 -86
  24. package/integration-test/jinaga-test/package.json +3 -3
  25. package/integration-test/postgres/1-setup.sql +2 -2
  26. package/integration-test/postgres/Dockerfile +1 -1
  27. package/package.json +4 -4
  28. package/replicator/package-lock.json +139 -71
  29. package/replicator/package.json +2 -2
  30. package/scripts/migrate_purge.sql +45 -0
  31. package/scripts/purge.sql +33 -0
  32. package/setup.sql +30 -2
  33. package/src/http/router.ts +72 -119
  34. package/src/jinaga-server.ts +27 -5
  35. package/src/postgres/postgres-store.ts +18 -0
  36. package/src/postgres/purge-sql.ts +125 -0
  37. package/src/postgres/query-description.ts +1 -1
  38. package/src/postgres/specification-result-sql.ts +3 -3
  39. package/src/postgres/specification-sql.ts +4 -4
  40. package/test/authorization/authorizationSpec.ts +1 -1
  41. package/test/models/blog.ts +92 -0
  42. package/test/postgres/purgeSqlSpec.ts +218 -0
@@ -0,0 +1,125 @@
1
+ import { FactReference, Specification } from "jinaga";
2
+ import { FactTypeMap, RoleMap } from "./maps";
3
+ import { EdgeDescription, FactByLabel, QueryDescription, QueryDescriptionBuilder } from "./query-description";
4
+
5
+ export function purgeSqlFromSpecification(specification: Specification, factTypes: FactTypeMap, roleMap: RoleMap, schema: string):
6
+ { sql: string, parameters: (string | number)[] } | null {
7
+ const queryDescriptionBuilder = new QueryDescriptionBuilder(factTypes, roleMap);
8
+
9
+ let queryDescription = QueryDescription.unsatisfiable;
10
+ let knownFacts: FactByLabel = {};
11
+ const given = specification.given;
12
+ const start: FactReference[] = specification.given.map(g => ({ type: g.type, hash: 'xxxxx' }));
13
+ ({ queryDescription, knownFacts } = queryDescriptionBuilder.addEdges(queryDescription, given, start, knownFacts, [], specification.matches));
14
+
15
+ if (!queryDescription.isSatisfiable()) {
16
+ return null;
17
+ }
18
+
19
+ const query = generatePurgeSqlQuery(queryDescription, schema);
20
+ return query;
21
+ }
22
+
23
+
24
+ function generatePurgeSqlQuery(queryDescription: QueryDescription, schema: string):
25
+ { sql: string, parameters: (string | number)[] } {
26
+ if (queryDescription.existentialConditions.length > 0) {
27
+ throw new Error("Purge conditions should not have existential conditions");
28
+ }
29
+
30
+ const columns = queryDescription.outputs
31
+ .map((label, index) => `f${label.factIndex}.fact_id as trigger${index + 1}`)
32
+ .join(",\n ");
33
+ const firstEdge = queryDescription.edges[0];
34
+ const predecessorFact = queryDescription.inputs.find(i => i.factIndex === firstEdge.predecessorFactIndex);
35
+ const successorFact = queryDescription.inputs.find(i => i.factIndex === firstEdge.successorFactIndex);
36
+ const firstFactIndex = predecessorFact ? predecessorFact.factIndex : successorFact!.factIndex;
37
+ const writtenFactIndexes = new Set<number>().add(firstFactIndex);
38
+ const joins: string[] = generateJoins(queryDescription.edges, writtenFactIndexes, schema);
39
+ const inputWhereClauses = queryDescription.inputs
40
+ .map(input => `f${input.factIndex}.fact_type_id = $${input.factTypeParameter}`)
41
+ .join(" AND ");
42
+ const triggerWhereClauses = queryDescription.outputs
43
+ .map((label, index) => `a.fact_id = c2.trigger${index + 1}`)
44
+ .join("\n OR ");
45
+
46
+ const sql =
47
+ `WITH candidates AS (\n` +
48
+ ` SELECT\n` +
49
+ ` f${firstFactIndex}.fact_id as purge_root,\n` +
50
+ ` ${columns}\n` +
51
+ ` FROM ${schema}.fact f${firstFactIndex}\n${joins.join("")}` +
52
+ ` WHERE ${inputWhereClauses}\n` +
53
+ `), targets AS (\n` +
54
+ ` SELECT a.fact_id\n` +
55
+ ` FROM public.ancestor a\n` +
56
+ ` JOIN candidates c ON c.purge_root = a.ancestor_fact_id\n` +
57
+ ` WHERE NOT EXISTS (\n` +
58
+ ` SELECT 1\n` +
59
+ ` FROM candidates c2\n` +
60
+ ` WHERE ${triggerWhereClauses}\n` +
61
+ ` )\n` +
62
+ `), facts AS (\n` +
63
+ ` DELETE\n` +
64
+ ` FROM public.fact f\n` +
65
+ ` USING targets t WHERE t.fact_id = f.fact_id\n` +
66
+ ` RETURNING f.fact_id\n` +
67
+ `)\n` +
68
+ `SELECT fact_id FROM facts\n`;
69
+
70
+ // Remove parameter 1, which is the hash of the first fact
71
+ const parameters = [
72
+ queryDescription.parameters[0],
73
+ ...queryDescription.parameters.slice(2)
74
+ ];
75
+
76
+
77
+ return {
78
+ sql,
79
+ parameters: parameters
80
+ };
81
+ }
82
+
83
+ function generateJoins(edges: EdgeDescription[], writtenFactIndexes: Set<number>, schema: string) {
84
+ const joins: string[] = [];
85
+ edges.forEach(edge => {
86
+ if (writtenFactIndexes.has(edge.predecessorFactIndex)) {
87
+ if (writtenFactIndexes.has(edge.successorFactIndex)) {
88
+ joins.push(
89
+ ` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
90
+ ` ON e${edge.edgeIndex}.predecessor_fact_id = f${edge.predecessorFactIndex}.fact_id\n` +
91
+ ` AND e${edge.edgeIndex}.successor_fact_id = f${edge.successorFactIndex}.fact_id\n` +
92
+ ` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
93
+ );
94
+ }
95
+ else {
96
+ joins.push(
97
+ ` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
98
+ ` ON e${edge.edgeIndex}.predecessor_fact_id = f${edge.predecessorFactIndex}.fact_id\n` +
99
+ ` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
100
+ );
101
+ joins.push(
102
+ ` JOIN ${schema}.fact f${edge.successorFactIndex}\n` +
103
+ ` ON f${edge.successorFactIndex}.fact_id = e${edge.edgeIndex}.successor_fact_id\n`
104
+ );
105
+ writtenFactIndexes.add(edge.successorFactIndex);
106
+ }
107
+ }
108
+ else if (writtenFactIndexes.has(edge.successorFactIndex)) {
109
+ joins.push(
110
+ ` JOIN ${schema}.edge e${edge.edgeIndex}\n` +
111
+ ` ON e${edge.edgeIndex}.successor_fact_id = f${edge.successorFactIndex}.fact_id\n` +
112
+ ` AND e${edge.edgeIndex}.role_id = $${edge.roleParameter - 1}\n`
113
+ );
114
+ joins.push(
115
+ ` JOIN ${schema}.fact f${edge.predecessorFactIndex}\n` +
116
+ ` ON f${edge.predecessorFactIndex}.fact_id = e${edge.edgeIndex}.predecessor_fact_id\n`
117
+ );
118
+ writtenFactIndexes.add(edge.predecessorFactIndex);
119
+ }
120
+ else {
121
+ throw new Error("Neither predecessor nor successor fact has been written");
122
+ }
123
+ });
124
+ return joins;
125
+ }
@@ -276,7 +276,7 @@ export type FactByLabel = {
276
276
  [label: string]: FactDescription;
277
277
  };
278
278
 
279
- export class QueryDescriptionBuider {
279
+ export class QueryDescriptionBuilder {
280
280
  constructor(
281
281
  private factTypes: FactTypeMap,
282
282
  private roleMap: RoleMap
@@ -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,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
+ }