@reventlessdev/reventless-local 3.0.0-alpha.200 → 3.0.0-alpha.202
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/CHANGELOG.md +17 -0
- package/package.json +10 -9
- package/rescript.json +2 -1
- package/src/Platform.res +70 -37
- package/src/Platform.res.mjs +48 -34
- package/src/adapter/BackendState.res +13 -0
- package/src/adapter/BackendState.res.mjs +17 -1
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +1 -1
- package/src/adapter/DomainGraphQL_Server.res +5 -5
- package/src/adapter/DomainGraphQL_Server.res.mjs +1 -1
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +1 -1
- package/src/adapter/LocalBus.res +6 -46
- package/src/adapter/LocalBus.res.mjs +0 -29
- package/src/adapter/LocalStateChangeDescriptor.res +93 -0
- package/src/adapter/LocalStateChangeDescriptor.res.mjs +61 -0
- package/src/adapter/LocalUploadResolvers.res +16 -3
- package/src/adapter/LocalUploadResolvers.res.mjs +6 -4
- package/src/adapter/ObjectStore/LocalObjectStore.res +157 -0
- package/src/adapter/ObjectStore/LocalObjectStore.res.mjs +129 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +215 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res.mjs +286 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res +33 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res.mjs +47 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +4 -2
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +3 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +4 -2
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +3 -3
- package/src/reset/LocalSeedReset.res +552 -0
- package/src/reset/LocalSeedReset.res.mjs +533 -0
- package/tests/adapter/BackendParityTest.res +51 -0
- package/tests/adapter/BackendParityTest.res.mjs +44 -0
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +6 -4
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +4 -3
- package/tests/adapter/ObjectStorePersistenceTest.res +243 -0
- package/tests/adapter/ObjectStorePersistenceTest.res.mjs +159 -0
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +110 -0
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res.mjs +141 -0
- package/tests/reset/LocalSeedResetTest.res +252 -0
- package/tests/reset/LocalSeedResetTest.res.mjs +280 -0
- package/src/adapter/LocalObjectStore.res +0 -70
- package/src/adapter/LocalObjectStore.res.mjs +0 -60
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
5
|
+
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
6
|
+
import * as LocalBus$ReventlessLocal from "../../../src/adapter/LocalBus.res.mjs";
|
|
7
|
+
import * as TestRunner$ReventlessLocal from "../../../src/test/TestRunner.res.mjs";
|
|
8
|
+
import * as EventLog_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventLog/EventLog_Builder.res.mjs";
|
|
9
|
+
import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
|
|
10
|
+
import * as DcbEventLog_Builder$ReventlessLocal from "../../../src/components/DcbEventLog_Builder.res.mjs";
|
|
11
|
+
import * as EventLogProvisioning$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/EventLogProvisioning/EventLogProvisioning.res.mjs";
|
|
12
|
+
import * as EventLogStorage_InMemory$ReventlessLocal from "../../../src/adapter/EventLog/EventLogStorage_InMemory.res.mjs";
|
|
13
|
+
import * as LocalEventTopicPublisher$ReventlessLocal from "../../../src/adapter/EventTopic/LocalEventTopicPublisher.res.mjs";
|
|
14
|
+
|
|
15
|
+
let provisioned = [];
|
|
16
|
+
|
|
17
|
+
function onProvisioned(logStyle, name, owner, plugin, param, resources, param$1) {
|
|
18
|
+
provisioned.push({
|
|
19
|
+
logStyle: logStyle,
|
|
20
|
+
name: name,
|
|
21
|
+
owner: owner,
|
|
22
|
+
plugin: plugin,
|
|
23
|
+
resourceCount: resources.length
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let Recorder = {
|
|
28
|
+
onProvisioned: onProvisioned
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
EventLogProvisioning$ReventlessCore.use(Recorder);
|
|
32
|
+
|
|
33
|
+
let Bus = LocalBus$ReventlessLocal.Make({});
|
|
34
|
+
|
|
35
|
+
TestRunner$ReventlessLocal.setup();
|
|
36
|
+
|
|
37
|
+
let name = "SeamItemEventLog";
|
|
38
|
+
|
|
39
|
+
let eventSchema = S.schema(s => ({
|
|
40
|
+
TAG: "SeamItemCreated",
|
|
41
|
+
name: s.m(S.string)
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
let SeamItemSpec = {
|
|
45
|
+
Id: undefined,
|
|
46
|
+
name: name,
|
|
47
|
+
eventSchema: eventSchema
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let ClassicMaker = EventLog_Builder$ReventlessCore.Make({
|
|
51
|
+
Id: {
|
|
52
|
+
schema: Id$Reventless.StringPure.schema,
|
|
53
|
+
make: prim => prim,
|
|
54
|
+
makeFromString: prim => prim,
|
|
55
|
+
toString: prim => prim,
|
|
56
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
57
|
+
},
|
|
58
|
+
name: name,
|
|
59
|
+
eventSchema: eventSchema
|
|
60
|
+
})({
|
|
61
|
+
make: EventLogStorage_InMemory$ReventlessLocal.make
|
|
62
|
+
})(LocalEventTopicPublisher$ReventlessLocal.Make(Bus));
|
|
63
|
+
|
|
64
|
+
let previousContext = ResourceAttribution$ReventlessCore.enter("seam-platform", "SeamPlugin");
|
|
65
|
+
|
|
66
|
+
let classicLog = ClassicMaker.make("SeamItem", {
|
|
67
|
+
kind: "Aggregate",
|
|
68
|
+
name: "SeamItem"
|
|
69
|
+
}, undefined);
|
|
70
|
+
|
|
71
|
+
let DcbMaker = DcbEventLog_Builder$ReventlessLocal.Make(Bus);
|
|
72
|
+
|
|
73
|
+
let dcbLog = DcbMaker.make("SeamCatalog", undefined, {
|
|
74
|
+
TAG: "Simple",
|
|
75
|
+
_0: {
|
|
76
|
+
key: "id"
|
|
77
|
+
}
|
|
78
|
+
}, undefined);
|
|
79
|
+
|
|
80
|
+
ResourceAttribution$ReventlessCore.restore(previousContext);
|
|
81
|
+
|
|
82
|
+
globalThis.describe("EventLogProvisioning seam, driven by the real builders", () => {
|
|
83
|
+
globalThis.test("fires once per log, for both styles", () => {
|
|
84
|
+
globalThis.expect(provisioned.map(r => [
|
|
85
|
+
r.logStyle,
|
|
86
|
+
r.name
|
|
87
|
+
])).toEqual([
|
|
88
|
+
[
|
|
89
|
+
"Classic",
|
|
90
|
+
"SeamItemEventLog"
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
"Dcb",
|
|
94
|
+
"SeamCatalogDcbEventLog"
|
|
95
|
+
]
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
globalThis.test("carries the owning element: the aggregate for classic, the plugin for DCB", () => {
|
|
99
|
+
globalThis.expect(provisioned.map(r => r.owner)).toEqual([
|
|
100
|
+
{
|
|
101
|
+
kind: "Aggregate",
|
|
102
|
+
name: "SeamItem"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
kind: "Plugin",
|
|
106
|
+
name: "SeamCatalog"
|
|
107
|
+
}
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
globalThis.test("carries the ambient plugin attribution the builder published", () => {
|
|
111
|
+
globalThis.expect(provisioned.map(r => r.plugin)).toEqual([
|
|
112
|
+
"SeamPlugin",
|
|
113
|
+
"SeamPlugin"
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
globalThis.test("carries the adapter's resources — empty for in-memory storage", () => {
|
|
117
|
+
globalThis.expect(provisioned.map(r => r.resourceCount)).toEqual([
|
|
118
|
+
0,
|
|
119
|
+
0
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
globalThis.test("the components still build normally with a backend registered", async () => {
|
|
123
|
+
let classicOps = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(classicLog));
|
|
124
|
+
let dcbOps = await TestRunner$ReventlessLocal.resolve(DcbMaker.operations(dcbLog));
|
|
125
|
+
globalThis.expect(classicOps).toBeTruthy();
|
|
126
|
+
globalThis.expect(dcbOps).toBeTruthy();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
provisioned,
|
|
132
|
+
Recorder,
|
|
133
|
+
Bus,
|
|
134
|
+
SeamItemSpec,
|
|
135
|
+
ClassicMaker,
|
|
136
|
+
previousContext,
|
|
137
|
+
classicLog,
|
|
138
|
+
DcbMaker,
|
|
139
|
+
dcbLog,
|
|
140
|
+
}
|
|
141
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Classification tests for the scoped local reset.
|
|
2
|
+
//
|
|
3
|
+
// The fixture is not invented: it is the table set, checkpoint names and plugin
|
|
4
|
+
// structures captured from a live hybrid store on 2026-08-05 — including the four
|
|
5
|
+
// components (`qdb_ImportProductAudit` and the three `*Todo` tables) that appear in
|
|
6
|
+
// NO plugin structure, and `qdb_UiFragments`, which the platform's own structure
|
|
7
|
+
// omits. Those omissions are the reason classification is discovery-first rather
|
|
8
|
+
// than structure-driven, so a fixture without them would test the easy case only.
|
|
9
|
+
//
|
|
10
|
+
// This is the pinning test the module's header refers to: if core gains a
|
|
11
|
+
// platform-owned component and the allowlist is not updated with it, "platform
|
|
12
|
+
// claims exactly the platform set" fails.
|
|
13
|
+
|
|
14
|
+
@@warning("-44")
|
|
15
|
+
|
|
16
|
+
open JestGlobals
|
|
17
|
+
|
|
18
|
+
let tempRoot = (): string =>
|
|
19
|
+
NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-reset-"]))
|
|
20
|
+
|
|
21
|
+
// Every qdb table a live hybrid store holds.
|
|
22
|
+
let platformTables = ["qdb_Plugins", "qdb_UiFragments"]
|
|
23
|
+
let catalogTables = ["qdb_Categories", "qdb_ProductDemand", "qdb_Products"]
|
|
24
|
+
let orderingTables = ["qdb_AvailableProducts", "qdb_Customers", "qdb_Orders"]
|
|
25
|
+
// Claimed by no structure at all — reachable only through a domain-wide scope.
|
|
26
|
+
let unclaimedTables = [
|
|
27
|
+
"qdb_AutoShipOrderTodo",
|
|
28
|
+
"qdb_GeocodeCustomerAddressTodo",
|
|
29
|
+
"qdb_ImportProductAudit",
|
|
30
|
+
"qdb_SendOrderConfirmationTodo",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
let structureJson = (~queryables, ~writables, ~stores) =>
|
|
34
|
+
Dict.fromArray([
|
|
35
|
+
(
|
|
36
|
+
"readModels",
|
|
37
|
+
queryables
|
|
38
|
+
->Array.map(n => Dict.fromArray([("name", JSON.Encode.string(n))])->JSON.Encode.object)
|
|
39
|
+
->JSON.Encode.array,
|
|
40
|
+
),
|
|
41
|
+
("stateViewSlices", []->JSON.Encode.array),
|
|
42
|
+
(
|
|
43
|
+
"aggregates",
|
|
44
|
+
writables
|
|
45
|
+
->Array.map(n => Dict.fromArray([("name", JSON.Encode.string(n))])->JSON.Encode.object)
|
|
46
|
+
->JSON.Encode.array,
|
|
47
|
+
),
|
|
48
|
+
("stateChangeSlices", []->JSON.Encode.array),
|
|
49
|
+
("requiredStores", stores->Array.map(JSON.Encode.string)->JSON.Encode.array),
|
|
50
|
+
])->JSON.Encode.object
|
|
51
|
+
|
|
52
|
+
// A store shaped like the live one, with one row in every table so counts are
|
|
53
|
+
// non-zero and a plan that claims a table is visible as such.
|
|
54
|
+
let makeStore = (): (SqliteDriver.t, string) => {
|
|
55
|
+
let root = tempRoot()
|
|
56
|
+
let db = SqliteDriver.openDb(~path=NodePath.join([root, "local.db"]))
|
|
57
|
+
|
|
58
|
+
let allTables = [platformTables, catalogTables, orderingTables, unclaimedTables]->Array.flat
|
|
59
|
+
allTables->Array.forEach(t => {
|
|
60
|
+
db->SqliteDriver.exec(
|
|
61
|
+
`CREATE TABLE ${t} (partition_key TEXT NOT NULL, sub_key TEXT NOT NULL DEFAULT '', item TEXT NOT NULL, expires_at INTEGER, PRIMARY KEY (partition_key, sub_key))`,
|
|
62
|
+
)
|
|
63
|
+
// qdb_Plugins gets the two registry rows below and nothing else, so its count
|
|
64
|
+
// stays the number of connected plugins.
|
|
65
|
+
if t != "qdb_Plugins" {
|
|
66
|
+
db
|
|
67
|
+
->SqliteDriver.prepare(`INSERT INTO ${t}(partition_key, item) VALUES(?, ?)`)
|
|
68
|
+
->SqliteDriver.run([JSON.Encode.string("row1"), JSON.Encode.string("{}")])
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
db->SqliteDriver.exec(
|
|
73
|
+
"CREATE TABLE event_log (log_name TEXT NOT NULL, aggregate_id TEXT NOT NULL, seq_nr INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY (log_name, aggregate_id, seq_nr))",
|
|
74
|
+
)
|
|
75
|
+
db->SqliteDriver.exec(
|
|
76
|
+
"CREATE TABLE dcb_event (log_name TEXT NOT NULL, position INTEGER NOT NULL, event_type TEXT NOT NULL, data TEXT NOT NULL, meta TEXT NOT NULL, recorded_at TEXT NOT NULL, PRIMARY KEY (log_name, position))",
|
|
77
|
+
)
|
|
78
|
+
db->SqliteDriver.exec(
|
|
79
|
+
"CREATE TABLE projection_checkpoint (read_model TEXT NOT NULL PRIMARY KEY, position INTEGER NOT NULL)",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
[("PluginAggrEventLog", "plugin1"), ("CustomerAggrEventLog", "cust1")]->Array.forEach(((
|
|
83
|
+
log,
|
|
84
|
+
id,
|
|
85
|
+
)) =>
|
|
86
|
+
db
|
|
87
|
+
->SqliteDriver.prepare("INSERT INTO event_log VALUES(?, ?, 1, '{}')")
|
|
88
|
+
->SqliteDriver.run([JSON.Encode.string(log), JSON.Encode.string(id)])
|
|
89
|
+
)
|
|
90
|
+
db
|
|
91
|
+
->SqliteDriver.prepare("INSERT INTO dcb_event VALUES(?, 1, 'X', '{}', '{}', '')")
|
|
92
|
+
->SqliteDriver.run([JSON.Encode.string("CatalogDcbEventLog")])
|
|
93
|
+
|
|
94
|
+
// All four checkpoint shapes the live store holds.
|
|
95
|
+
[
|
|
96
|
+
"CategoriesEventColl",
|
|
97
|
+
"CustomersReadModelEventColl",
|
|
98
|
+
"UiFragmentsEventColl",
|
|
99
|
+
"PluginsReadModelEventColl",
|
|
100
|
+
"dcb:CategoriesEventColl",
|
|
101
|
+
]->Array.forEach(rm =>
|
|
102
|
+
db
|
|
103
|
+
->SqliteDriver.prepare("INSERT INTO projection_checkpoint VALUES(?, 1)")
|
|
104
|
+
->SqliteDriver.run([JSON.Encode.string(rm)])
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// The registry rows the reset reads its mapping from.
|
|
108
|
+
let plugin = (name, structure) =>
|
|
109
|
+
db
|
|
110
|
+
->SqliteDriver.prepare("INSERT INTO qdb_Plugins(partition_key, item) VALUES(?, ?)")
|
|
111
|
+
->SqliteDriver.run([
|
|
112
|
+
JSON.Encode.string(name),
|
|
113
|
+
JSON.Encode.string(
|
|
114
|
+
Dict.fromArray([("structure", structure)])->JSON.Encode.object->JSON.stringify,
|
|
115
|
+
),
|
|
116
|
+
])
|
|
117
|
+
plugin(
|
|
118
|
+
"Catalog",
|
|
119
|
+
structureJson(
|
|
120
|
+
~queryables=["Categories", "ProductDemand", "Products"],
|
|
121
|
+
~writables=[],
|
|
122
|
+
~stores=["Catalog.productImages"],
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
plugin(
|
|
126
|
+
"Ordering",
|
|
127
|
+
structureJson(
|
|
128
|
+
~queryables=["AvailableProducts", "Customers", "Orders"],
|
|
129
|
+
~writables=["Customer"],
|
|
130
|
+
~stores=[],
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
(db, root)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let labels = (p: LocalSeedReset.plan) => p.items->Array.map(i => i.label)
|
|
138
|
+
|
|
139
|
+
describe("checkpointComponent", () => {
|
|
140
|
+
testSync("recovers the component from every checkpoint shape the store holds", () => {
|
|
141
|
+
expect(LocalSeedReset.checkpointComponent("CategoriesEventColl"))->toEqual("Categories")
|
|
142
|
+
expect(LocalSeedReset.checkpointComponent("CustomersReadModelEventColl"))->toEqual("Customers")
|
|
143
|
+
expect(LocalSeedReset.checkpointComponent("UiFragmentsEventColl"))->toEqual("UiFragments")
|
|
144
|
+
expect(LocalSeedReset.checkpointComponent("PluginsReadModelEventColl"))->toEqual("Plugins")
|
|
145
|
+
expect(LocalSeedReset.checkpointComponent("dcb:CategoriesEventColl"))->toEqual("Categories")
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe("scope classification", () => {
|
|
150
|
+
testSync("domain claims every non-platform table, including what no structure lists", () => {
|
|
151
|
+
let (db, root) = makeStore()
|
|
152
|
+
let plan = LocalSeedReset.build(db, ~root, ~scope=Domain)
|
|
153
|
+
let claimed = labels(plan)
|
|
154
|
+
|
|
155
|
+
[catalogTables, orderingTables, unclaimedTables]
|
|
156
|
+
->Array.flat
|
|
157
|
+
->Array.forEach(t => expect(claimed->Array.includes(t))->toEqual(true))
|
|
158
|
+
platformTables->Array.forEach(t => expect(claimed->Array.includes(t))->toEqual(false))
|
|
159
|
+
db->SqliteDriver.close
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
testSync("domain leaves the plugin registry's own log and the offload store alone", () => {
|
|
163
|
+
let (db, root) = makeStore()
|
|
164
|
+
let claimed = labels(LocalSeedReset.build(db, ~root, ~scope=Domain))
|
|
165
|
+
expect(claimed->Array.includes("event_log (PluginAggrEventLog)"))->toEqual(false)
|
|
166
|
+
expect(claimed->Array.includes("offload/"))->toEqual(false)
|
|
167
|
+
// A domain aggregate's log is claimed.
|
|
168
|
+
expect(claimed->Array.includes("event_log (CustomerAggrEventLog)"))->toEqual(true)
|
|
169
|
+
expect(claimed->Array.includes("dcb_event (CatalogDcbEventLog)"))->toEqual(true)
|
|
170
|
+
db->SqliteDriver.close
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
testSync("platform claims exactly the platform set — the allowlist pin", () => {
|
|
174
|
+
let (db, root) = makeStore()
|
|
175
|
+
let claimed = labels(LocalSeedReset.build(db, ~root, ~scope=Platform))
|
|
176
|
+
platformTables->Array.forEach(t => expect(claimed->Array.includes(t))->toEqual(true))
|
|
177
|
+
expect(claimed->Array.includes("event_log (PluginAggrEventLog)"))->toEqual(true)
|
|
178
|
+
[catalogTables, orderingTables, unclaimedTables]
|
|
179
|
+
->Array.flat
|
|
180
|
+
->Array.forEach(t => expect(claimed->Array.includes(t))->toEqual(false))
|
|
181
|
+
db->SqliteDriver.close
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
testSync("checkpoints follow their component across scopes", () => {
|
|
185
|
+
let (db, root) = makeStore()
|
|
186
|
+
let domain = labels(LocalSeedReset.build(db, ~root, ~scope=Domain))
|
|
187
|
+
expect(domain->Array.includes("projection_checkpoint (CategoriesEventColl)"))->toEqual(true)
|
|
188
|
+
expect(domain->Array.includes("projection_checkpoint (dcb:CategoriesEventColl)"))->toEqual(true)
|
|
189
|
+
expect(domain->Array.includes("projection_checkpoint (UiFragmentsEventColl)"))->toEqual(false)
|
|
190
|
+
|
|
191
|
+
let platform = labels(LocalSeedReset.build(db, ~root, ~scope=Platform))
|
|
192
|
+
expect(platform->Array.includes("projection_checkpoint (UiFragmentsEventColl)"))->toEqual(true)
|
|
193
|
+
expect(platform->Array.includes("projection_checkpoint (PluginsReadModelEventColl)"))->toEqual(
|
|
194
|
+
true,
|
|
195
|
+
)
|
|
196
|
+
db->SqliteDriver.close
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
testSync("a plugin scope claims only its own components", () => {
|
|
200
|
+
let (db, root) = makeStore()
|
|
201
|
+
let claimed = labels(LocalSeedReset.build(db, ~root, ~scope=OnePlugin("Catalog")))
|
|
202
|
+
catalogTables->Array.forEach(t => expect(claimed->Array.includes(t))->toEqual(true))
|
|
203
|
+
orderingTables->Array.forEach(t => expect(claimed->Array.includes(t))->toEqual(false))
|
|
204
|
+
platformTables->Array.forEach(t => expect(claimed->Array.includes(t))->toEqual(false))
|
|
205
|
+
db->SqliteDriver.close
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
testSync("a plugin scope reports only what NO plugin claims, not the other plugin's", () => {
|
|
209
|
+
let (db, root) = makeStore()
|
|
210
|
+
let plan = LocalSeedReset.build(db, ~root, ~scope=OnePlugin("Catalog"))
|
|
211
|
+
expect(plan.unattributed->Array.toSorted(String.compare))->toEqual(unclaimedTables)
|
|
212
|
+
db->SqliteDriver.close
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe("execution", () => {
|
|
217
|
+
testSync("emptying the domain scope leaves the platform rows in place", () => {
|
|
218
|
+
let (db, root) = makeStore()
|
|
219
|
+
LocalSeedReset.execute(db, LocalSeedReset.build(db, ~root, ~scope=Domain))
|
|
220
|
+
|
|
221
|
+
let count = table =>
|
|
222
|
+
switch db->SqliteDriver.prepare(`SELECT COUNT(*) AS c FROM ${table}`)->SqliteDriver.get([]) {
|
|
223
|
+
| Some(row) =>
|
|
224
|
+
switch row->Dict.get("c") {
|
|
225
|
+
| Some(JSON.Number(n)) => n->Float.toInt
|
|
226
|
+
| _ => -1
|
|
227
|
+
}
|
|
228
|
+
| None => -1
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(count("qdb_Categories"))->toEqual(0)
|
|
232
|
+
expect(count("qdb_ImportProductAudit"))->toEqual(0)
|
|
233
|
+
expect(count("qdb_Plugins"))->toEqual(2)
|
|
234
|
+
expect(count("qdb_UiFragments"))->toEqual(1)
|
|
235
|
+
// The registry's own event log survives, so the plugins stay registered.
|
|
236
|
+
expect(count("event_log"))->toEqual(1)
|
|
237
|
+
db->SqliteDriver.close
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
testSync("the tables themselves survive — contents are deleted, not dropped", () => {
|
|
241
|
+
let (db, root) = makeStore()
|
|
242
|
+
LocalSeedReset.execute(db, LocalSeedReset.build(db, ~root, ~scope=Everything))
|
|
243
|
+
// A dropped table would make this throw rather than return 0.
|
|
244
|
+
expect(
|
|
245
|
+
db
|
|
246
|
+
->SqliteDriver.prepare("SELECT COUNT(*) AS c FROM qdb_Categories")
|
|
247
|
+
->SqliteDriver.get([])
|
|
248
|
+
->Option.isSome,
|
|
249
|
+
)->toEqual(true)
|
|
250
|
+
db->SqliteDriver.close
|
|
251
|
+
})
|
|
252
|
+
})
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodeos from "node:os";
|
|
5
|
+
import * as Nodepath from "node:path";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
|
+
import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.res.mjs";
|
|
9
|
+
import * as LocalSeedReset$ReventlessLocal from "../../src/reset/LocalSeedReset.res.mjs";
|
|
10
|
+
|
|
11
|
+
function tempRoot() {
|
|
12
|
+
return Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-reset-"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let platformTables = [
|
|
16
|
+
"qdb_Plugins",
|
|
17
|
+
"qdb_UiFragments"
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
let catalogTables = [
|
|
21
|
+
"qdb_Categories",
|
|
22
|
+
"qdb_ProductDemand",
|
|
23
|
+
"qdb_Products"
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
let orderingTables = [
|
|
27
|
+
"qdb_AvailableProducts",
|
|
28
|
+
"qdb_Customers",
|
|
29
|
+
"qdb_Orders"
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let unclaimedTables = [
|
|
33
|
+
"qdb_AutoShipOrderTodo",
|
|
34
|
+
"qdb_GeocodeCustomerAddressTodo",
|
|
35
|
+
"qdb_ImportProductAudit",
|
|
36
|
+
"qdb_SendOrderConfirmationTodo"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function structureJson(queryables, writables, stores) {
|
|
40
|
+
return Object.fromEntries([
|
|
41
|
+
[
|
|
42
|
+
"readModels",
|
|
43
|
+
queryables.map(n => Object.fromEntries([[
|
|
44
|
+
"name",
|
|
45
|
+
n
|
|
46
|
+
]]))
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"stateViewSlices",
|
|
50
|
+
[]
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"aggregates",
|
|
54
|
+
writables.map(n => Object.fromEntries([[
|
|
55
|
+
"name",
|
|
56
|
+
n
|
|
57
|
+
]]))
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"stateChangeSlices",
|
|
61
|
+
[]
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"requiredStores",
|
|
65
|
+
stores.map(prim => prim)
|
|
66
|
+
]
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeStore() {
|
|
71
|
+
let root = tempRoot();
|
|
72
|
+
let db = SqliteDriver$ReventlessLocal.openDb(Nodepath.join(root, "local.db"));
|
|
73
|
+
let allTables = [
|
|
74
|
+
platformTables,
|
|
75
|
+
catalogTables,
|
|
76
|
+
orderingTables,
|
|
77
|
+
unclaimedTables
|
|
78
|
+
].flat();
|
|
79
|
+
allTables.forEach(t => {
|
|
80
|
+
SqliteDriver$ReventlessLocal.exec(db, `CREATE TABLE ` + t + ` (partition_key TEXT NOT NULL, sub_key TEXT NOT NULL DEFAULT '', item TEXT NOT NULL, expires_at INTEGER, PRIMARY KEY (partition_key, sub_key))`);
|
|
81
|
+
if (t !== "qdb_Plugins") {
|
|
82
|
+
return SqliteDriver$ReventlessLocal.run(SqliteDriver$ReventlessLocal.prepare(db, `INSERT INTO ` + t + `(partition_key, item) VALUES(?, ?)`), [
|
|
83
|
+
"row1",
|
|
84
|
+
"{}"
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE event_log (log_name TEXT NOT NULL, aggregate_id TEXT NOT NULL, seq_nr INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY (log_name, aggregate_id, seq_nr))");
|
|
89
|
+
SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE dcb_event (log_name TEXT NOT NULL, position INTEGER NOT NULL, event_type TEXT NOT NULL, data TEXT NOT NULL, meta TEXT NOT NULL, recorded_at TEXT NOT NULL, PRIMARY KEY (log_name, position))");
|
|
90
|
+
SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE projection_checkpoint (read_model TEXT NOT NULL PRIMARY KEY, position INTEGER NOT NULL)");
|
|
91
|
+
[
|
|
92
|
+
[
|
|
93
|
+
"PluginAggrEventLog",
|
|
94
|
+
"plugin1"
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"CustomerAggrEventLog",
|
|
98
|
+
"cust1"
|
|
99
|
+
]
|
|
100
|
+
].forEach(param => SqliteDriver$ReventlessLocal.run(SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO event_log VALUES(?, ?, 1, '{}')"), [
|
|
101
|
+
param[0],
|
|
102
|
+
param[1]
|
|
103
|
+
]));
|
|
104
|
+
SqliteDriver$ReventlessLocal.run(SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO dcb_event VALUES(?, 1, 'X', '{}', '{}', '')"), ["CatalogDcbEventLog"]);
|
|
105
|
+
[
|
|
106
|
+
"CategoriesEventColl",
|
|
107
|
+
"CustomersReadModelEventColl",
|
|
108
|
+
"UiFragmentsEventColl",
|
|
109
|
+
"PluginsReadModelEventColl",
|
|
110
|
+
"dcb:CategoriesEventColl"
|
|
111
|
+
].forEach(rm => SqliteDriver$ReventlessLocal.run(SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO projection_checkpoint VALUES(?, 1)"), [rm]));
|
|
112
|
+
let plugin = (name, structure) => SqliteDriver$ReventlessLocal.run(SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO qdb_Plugins(partition_key, item) VALUES(?, ?)"), [
|
|
113
|
+
name,
|
|
114
|
+
JSON.stringify(Object.fromEntries([[
|
|
115
|
+
"structure",
|
|
116
|
+
structure
|
|
117
|
+
]]))
|
|
118
|
+
]);
|
|
119
|
+
plugin("Catalog", structureJson([
|
|
120
|
+
"Categories",
|
|
121
|
+
"ProductDemand",
|
|
122
|
+
"Products"
|
|
123
|
+
], [], ["Catalog.productImages"]));
|
|
124
|
+
plugin("Ordering", structureJson([
|
|
125
|
+
"AvailableProducts",
|
|
126
|
+
"Customers",
|
|
127
|
+
"Orders"
|
|
128
|
+
], ["Customer"], []));
|
|
129
|
+
return [
|
|
130
|
+
db,
|
|
131
|
+
root
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function labels(p) {
|
|
136
|
+
return p.items.map(i => i.label);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
globalThis.describe("checkpointComponent", () => {
|
|
140
|
+
globalThis.test("recovers the component from every checkpoint shape the store holds", () => {
|
|
141
|
+
globalThis.expect(LocalSeedReset$ReventlessLocal.checkpointComponent("CategoriesEventColl")).toEqual("Categories");
|
|
142
|
+
globalThis.expect(LocalSeedReset$ReventlessLocal.checkpointComponent("CustomersReadModelEventColl")).toEqual("Customers");
|
|
143
|
+
globalThis.expect(LocalSeedReset$ReventlessLocal.checkpointComponent("UiFragmentsEventColl")).toEqual("UiFragments");
|
|
144
|
+
globalThis.expect(LocalSeedReset$ReventlessLocal.checkpointComponent("PluginsReadModelEventColl")).toEqual("Plugins");
|
|
145
|
+
globalThis.expect(LocalSeedReset$ReventlessLocal.checkpointComponent("dcb:CategoriesEventColl")).toEqual("Categories");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
globalThis.describe("scope classification", () => {
|
|
150
|
+
globalThis.test("domain claims every non-platform table, including what no structure lists", () => {
|
|
151
|
+
let match = makeStore();
|
|
152
|
+
let db = match[0];
|
|
153
|
+
let plan = LocalSeedReset$ReventlessLocal.build(db, match[1], "Domain");
|
|
154
|
+
let claimed = labels(plan);
|
|
155
|
+
[
|
|
156
|
+
catalogTables,
|
|
157
|
+
orderingTables,
|
|
158
|
+
unclaimedTables
|
|
159
|
+
].flat().forEach(t => {
|
|
160
|
+
globalThis.expect(claimed.includes(t)).toEqual(true);
|
|
161
|
+
});
|
|
162
|
+
platformTables.forEach(t => {
|
|
163
|
+
globalThis.expect(claimed.includes(t)).toEqual(false);
|
|
164
|
+
});
|
|
165
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
166
|
+
});
|
|
167
|
+
globalThis.test("domain leaves the plugin registry's own log and the offload store alone", () => {
|
|
168
|
+
let match = makeStore();
|
|
169
|
+
let db = match[0];
|
|
170
|
+
let claimed = labels(LocalSeedReset$ReventlessLocal.build(db, match[1], "Domain"));
|
|
171
|
+
globalThis.expect(claimed.includes("event_log (PluginAggrEventLog)")).toEqual(false);
|
|
172
|
+
globalThis.expect(claimed.includes("offload/")).toEqual(false);
|
|
173
|
+
globalThis.expect(claimed.includes("event_log (CustomerAggrEventLog)")).toEqual(true);
|
|
174
|
+
globalThis.expect(claimed.includes("dcb_event (CatalogDcbEventLog)")).toEqual(true);
|
|
175
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
176
|
+
});
|
|
177
|
+
globalThis.test("platform claims exactly the platform set — the allowlist pin", () => {
|
|
178
|
+
let match = makeStore();
|
|
179
|
+
let db = match[0];
|
|
180
|
+
let claimed = labels(LocalSeedReset$ReventlessLocal.build(db, match[1], "Platform"));
|
|
181
|
+
platformTables.forEach(t => {
|
|
182
|
+
globalThis.expect(claimed.includes(t)).toEqual(true);
|
|
183
|
+
});
|
|
184
|
+
globalThis.expect(claimed.includes("event_log (PluginAggrEventLog)")).toEqual(true);
|
|
185
|
+
[
|
|
186
|
+
catalogTables,
|
|
187
|
+
orderingTables,
|
|
188
|
+
unclaimedTables
|
|
189
|
+
].flat().forEach(t => {
|
|
190
|
+
globalThis.expect(claimed.includes(t)).toEqual(false);
|
|
191
|
+
});
|
|
192
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
193
|
+
});
|
|
194
|
+
globalThis.test("checkpoints follow their component across scopes", () => {
|
|
195
|
+
let match = makeStore();
|
|
196
|
+
let root = match[1];
|
|
197
|
+
let db = match[0];
|
|
198
|
+
let domain = labels(LocalSeedReset$ReventlessLocal.build(db, root, "Domain"));
|
|
199
|
+
globalThis.expect(domain.includes("projection_checkpoint (CategoriesEventColl)")).toEqual(true);
|
|
200
|
+
globalThis.expect(domain.includes("projection_checkpoint (dcb:CategoriesEventColl)")).toEqual(true);
|
|
201
|
+
globalThis.expect(domain.includes("projection_checkpoint (UiFragmentsEventColl)")).toEqual(false);
|
|
202
|
+
let platform = labels(LocalSeedReset$ReventlessLocal.build(db, root, "Platform"));
|
|
203
|
+
globalThis.expect(platform.includes("projection_checkpoint (UiFragmentsEventColl)")).toEqual(true);
|
|
204
|
+
globalThis.expect(platform.includes("projection_checkpoint (PluginsReadModelEventColl)")).toEqual(true);
|
|
205
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
206
|
+
});
|
|
207
|
+
globalThis.test("a plugin scope claims only its own components", () => {
|
|
208
|
+
let match = makeStore();
|
|
209
|
+
let db = match[0];
|
|
210
|
+
let claimed = labels(LocalSeedReset$ReventlessLocal.build(db, match[1], {
|
|
211
|
+
TAG: "OnePlugin",
|
|
212
|
+
_0: "Catalog"
|
|
213
|
+
}));
|
|
214
|
+
catalogTables.forEach(t => {
|
|
215
|
+
globalThis.expect(claimed.includes(t)).toEqual(true);
|
|
216
|
+
});
|
|
217
|
+
orderingTables.forEach(t => {
|
|
218
|
+
globalThis.expect(claimed.includes(t)).toEqual(false);
|
|
219
|
+
});
|
|
220
|
+
platformTables.forEach(t => {
|
|
221
|
+
globalThis.expect(claimed.includes(t)).toEqual(false);
|
|
222
|
+
});
|
|
223
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
224
|
+
});
|
|
225
|
+
globalThis.test("a plugin scope reports only what NO plugin claims, not the other plugin's", () => {
|
|
226
|
+
let match = makeStore();
|
|
227
|
+
let db = match[0];
|
|
228
|
+
let plan = LocalSeedReset$ReventlessLocal.build(db, match[1], {
|
|
229
|
+
TAG: "OnePlugin",
|
|
230
|
+
_0: "Catalog"
|
|
231
|
+
});
|
|
232
|
+
globalThis.expect(plan.unattributed.toSorted(Primitive_string.compare)).toEqual(unclaimedTables);
|
|
233
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
globalThis.describe("execution", () => {
|
|
238
|
+
globalThis.test("emptying the domain scope leaves the platform rows in place", () => {
|
|
239
|
+
let match = makeStore();
|
|
240
|
+
let db = match[0];
|
|
241
|
+
LocalSeedReset$ReventlessLocal.execute(db, LocalSeedReset$ReventlessLocal.build(db, match[1], "Domain"));
|
|
242
|
+
let count = table => {
|
|
243
|
+
let row = SqliteDriver$ReventlessLocal.get(SqliteDriver$ReventlessLocal.prepare(db, `SELECT COUNT(*) AS c FROM ` + table), []);
|
|
244
|
+
if (row === undefined) {
|
|
245
|
+
return -1;
|
|
246
|
+
}
|
|
247
|
+
let match = row["c"];
|
|
248
|
+
if (typeof match === "number") {
|
|
249
|
+
return match | 0;
|
|
250
|
+
} else {
|
|
251
|
+
return -1;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
globalThis.expect(count("qdb_Categories")).toEqual(0);
|
|
255
|
+
globalThis.expect(count("qdb_ImportProductAudit")).toEqual(0);
|
|
256
|
+
globalThis.expect(count("qdb_Plugins")).toEqual(2);
|
|
257
|
+
globalThis.expect(count("qdb_UiFragments")).toEqual(1);
|
|
258
|
+
globalThis.expect(count("event_log")).toEqual(1);
|
|
259
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
260
|
+
});
|
|
261
|
+
globalThis.test("the tables themselves survive — contents are deleted, not dropped", () => {
|
|
262
|
+
let match = makeStore();
|
|
263
|
+
let db = match[0];
|
|
264
|
+
LocalSeedReset$ReventlessLocal.execute(db, LocalSeedReset$ReventlessLocal.build(db, match[1], "Everything"));
|
|
265
|
+
globalThis.expect(Stdlib_Option.isSome(SqliteDriver$ReventlessLocal.get(SqliteDriver$ReventlessLocal.prepare(db, "SELECT COUNT(*) AS c FROM qdb_Categories"), []))).toEqual(true);
|
|
266
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
export {
|
|
271
|
+
tempRoot,
|
|
272
|
+
platformTables,
|
|
273
|
+
catalogTables,
|
|
274
|
+
orderingTables,
|
|
275
|
+
unclaimedTables,
|
|
276
|
+
structureJson,
|
|
277
|
+
makeStore,
|
|
278
|
+
labels,
|
|
279
|
+
}
|
|
280
|
+
/* Not a pure module */
|