@reventlessdev/reventless-local 3.0.0-alpha.199 → 3.0.0-alpha.201

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 (40) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +10 -9
  3. package/rescript.json +2 -1
  4. package/src/Platform.res +137 -86
  5. package/src/Platform.res.mjs +57 -44
  6. package/src/adapter/BackendState.res +13 -0
  7. package/src/adapter/BackendState.res.mjs +17 -1
  8. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +1 -1
  9. package/src/adapter/DomainGraphQL_Server.res +5 -5
  10. package/src/adapter/DomainGraphQL_Server.res.mjs +1 -1
  11. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +1 -1
  12. package/src/adapter/LocalBus.res +6 -46
  13. package/src/adapter/LocalBus.res.mjs +0 -29
  14. package/src/adapter/LocalStateChangeDescriptor.res +93 -0
  15. package/src/adapter/LocalStateChangeDescriptor.res.mjs +61 -0
  16. package/src/adapter/LocalUploadResolvers.res +16 -3
  17. package/src/adapter/LocalUploadResolvers.res.mjs +6 -4
  18. package/src/adapter/ObjectStore/LocalObjectStore.res +157 -0
  19. package/src/adapter/ObjectStore/LocalObjectStore.res.mjs +129 -0
  20. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +215 -0
  21. package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res.mjs +286 -0
  22. package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res +33 -0
  23. package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res.mjs +47 -0
  24. package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +1 -1
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +4 -2
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +3 -3
  27. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +4 -2
  28. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +3 -3
  29. package/src/reset/LocalSeedReset.res +552 -0
  30. package/src/reset/LocalSeedReset.res.mjs +533 -0
  31. package/tests/adapter/BackendParityTest.res +51 -0
  32. package/tests/adapter/BackendParityTest.res.mjs +44 -0
  33. package/tests/adapter/GraphQL_SubscriptionResolversTest.res +6 -4
  34. package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +4 -3
  35. package/tests/adapter/ObjectStorePersistenceTest.res +243 -0
  36. package/tests/adapter/ObjectStorePersistenceTest.res.mjs +159 -0
  37. package/tests/reset/LocalSeedResetTest.res +252 -0
  38. package/tests/reset/LocalSeedResetTest.res.mjs +280 -0
  39. package/src/adapter/LocalObjectStore.res +0 -70
  40. package/src/adapter/LocalObjectStore.res.mjs +0 -60
@@ -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 */
@@ -1,70 +0,0 @@
1
- // Process-local object store backing the dev platform's served-bucket routes.
2
- //
3
- // The AWS path fronts a private S3 bucket through CloudFront (served-buckets
4
- // plan); in dev there is no bucket, so uploaded and served objects live in this
5
- // process-local map. Ephemeral by design — contents are lost on restart, which
6
- // matches the plan's dev-only, non-durable stance. Held here (not in
7
- // EventLog/QueryDb storage) because it is raw bytes, not event/query state, and
8
- // has no SQLite arm — hence the `Local` prefix and no `_InMemory` suffix.
9
-
10
- // Node Buffer, opaque here — produced by the PUT handler (concat of request
11
- // chunks) and handed straight back to the response by the GET handler.
12
- type buffer
13
-
14
- @val @scope("Buffer") external concatBuffers: array<buffer> => buffer = "concat"
15
-
16
- type entry = {
17
- bytes: buffer,
18
- contentType: string,
19
- }
20
-
21
- let objects: dict<entry> = Dict.make()
22
-
23
- // The prefix the local upload route mints keys under. Mirrors the AWS presign
24
- // `SERVED_PREFIX` (default "uploads"); the hybrid example uses "uploads".
25
- let defaultUploadPrefix = "uploads"
26
-
27
- // Prefixes the dev server serves at `/{prefix}/*` (PUT stores, GET reads).
28
- // Seeded with the default upload prefix; a deployment serving another prefix
29
- // registers it before the servers start.
30
- let servedPrefixes: ref<array<string>> = ref([defaultUploadPrefix])
31
-
32
- let registerServedPrefix = (prefix: string): unit =>
33
- if !(servedPrefixes.contents->Array.includes(prefix)) {
34
- servedPrefixes.contents = servedPrefixes.contents->Array.concat([prefix])
35
- }
36
-
37
- // A request path is a served-object path when its first segment is a registered
38
- // served prefix and at least one key segment follows. Returns the storage key
39
- // (the path without its leading slash), or None for GraphQL / other paths.
40
- let servedKey = (path: string): option<string> => {
41
- let trimmed = path->String.startsWith("/") ? path->String.slice(~start=1, ~end=path->String.length) : path
42
- let prefix = trimmed->String.split("/")->Array.get(0)->Option.getOr("")
43
- if (
44
- prefix->String.length > 0 &&
45
- servedPrefixes.contents->Array.includes(prefix) &&
46
- trimmed->String.length > prefix->String.length + 1
47
- ) {
48
- Some(trimmed)
49
- } else {
50
- None
51
- }
52
- }
53
-
54
- let put = (~key: string, ~bytes: buffer, ~contentType: string): unit =>
55
- objects->Dict.set(key, {bytes, contentType})
56
-
57
- let get = (~key: string): option<entry> => objects->Dict.get(key)
58
-
59
- // Remove a stored object. Idempotent — deleting an absent key is a no-op, matching
60
- // the release contract (see docs/plans/done/upload-release-path.md, Step 3). The dev
61
- // store has no identities or clock, so the release resolver enforces only the
62
- // *shape* of the rule (key under a served prefix), not the identity/age conditions.
63
- let delete = (~key: string): unit => objects->Dict.delete(key)
64
-
65
- // Clear stored objects and restore the default served prefix — used between
66
- // isolated test suites (called from DomainGraphQL_Server.reset).
67
- let reset = (): unit => {
68
- objects->Dict.keysToArray->Array.forEach(k => objects->Dict.delete(k))
69
- servedPrefixes.contents = [defaultUploadPrefix]
70
- }
@@ -1,60 +0,0 @@
1
- // Generated by ReScript, PLEASE EDIT WITH CARE
2
-
3
- import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
-
6
- let objects = {};
7
-
8
- let defaultUploadPrefix = "uploads";
9
-
10
- let servedPrefixes = {
11
- contents: [defaultUploadPrefix]
12
- };
13
-
14
- function registerServedPrefix(prefix) {
15
- if (!servedPrefixes.contents.includes(prefix)) {
16
- servedPrefixes.contents = servedPrefixes.contents.concat([prefix]);
17
- return;
18
- }
19
- }
20
-
21
- function servedKey(path) {
22
- let trimmed = path.startsWith("/") ? path.slice(1, path.length) : path;
23
- let prefix = Stdlib_Option.getOr(trimmed.split("/")[0], "");
24
- if (prefix.length > 0 && servedPrefixes.contents.includes(prefix) && trimmed.length > (prefix.length + 1 | 0)) {
25
- return trimmed;
26
- }
27
- }
28
-
29
- function put(key, bytes, contentType) {
30
- objects[key] = {
31
- bytes: bytes,
32
- contentType: contentType
33
- };
34
- }
35
-
36
- function get(key) {
37
- return objects[key];
38
- }
39
-
40
- function $$delete(key) {
41
- Stdlib_Dict.$$delete(objects, key);
42
- }
43
-
44
- function reset() {
45
- Object.keys(objects).forEach(k => Stdlib_Dict.$$delete(objects, k));
46
- servedPrefixes.contents = [defaultUploadPrefix];
47
- }
48
-
49
- export {
50
- objects,
51
- defaultUploadPrefix,
52
- servedPrefixes,
53
- registerServedPrefix,
54
- servedKey,
55
- put,
56
- get,
57
- $$delete,
58
- reset,
59
- }
60
- /* No side effect */