@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.
- package/CHANGELOG.md +17 -0
- package/package.json +10 -9
- package/rescript.json +2 -1
- package/src/Platform.res +137 -86
- package/src/Platform.res.mjs +57 -44
- 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/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,533 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Offload$Reventless from "@reventlessdev/reventless-spec/src/semantic/Offload.res.mjs";
|
|
8
|
+
import * as Backend$ReventlessLocal from "../adapter/Backend.res.mjs";
|
|
9
|
+
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
10
|
+
import * as Seed_Prompt$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Prompt.res.mjs";
|
|
11
|
+
import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs";
|
|
12
|
+
import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/src/ComponentType.res.mjs";
|
|
13
|
+
import * as SqliteDriver$ReventlessLocal from "../adapter/SqliteDriver.res.mjs";
|
|
14
|
+
import * as LocalObjectStore$ReventlessLocal from "../adapter/ObjectStore/LocalObjectStore.res.mjs";
|
|
15
|
+
import * as PluginsReadModelSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
|
|
16
|
+
import * as ObjectStoreStorage_FileSystem$ReventlessLocal from "../adapter/ObjectStore/ObjectStoreStorage_FileSystem.res.mjs";
|
|
17
|
+
|
|
18
|
+
function scopeLabel(s) {
|
|
19
|
+
if (typeof s === "object") {
|
|
20
|
+
return s._0;
|
|
21
|
+
}
|
|
22
|
+
switch (s) {
|
|
23
|
+
case "Domain" :
|
|
24
|
+
return "domain";
|
|
25
|
+
case "Platform" :
|
|
26
|
+
return "platform";
|
|
27
|
+
case "Everything" :
|
|
28
|
+
return "everything";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let platformQueryables = [
|
|
33
|
+
PluginsReadModelSpec$ReventlessCore.name,
|
|
34
|
+
UiFragments$ReventlessCore.name
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
let platformWritables = [PluginSpec$ReventlessCore.name];
|
|
38
|
+
|
|
39
|
+
function qdbTableName(name) {
|
|
40
|
+
return "qdb_" + name.replaceAll("-", "_");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function aggregateLogName(name) {
|
|
44
|
+
return ComponentType$ReventlessCore.name(ComponentType$ReventlessCore.name(name, "Aggregate"), "EventLog");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function checkpointComponent(readModel) {
|
|
48
|
+
let withoutDcb = readModel.startsWith("dcb:") ? readModel.slice("dcb:".length, readModel.length) : readModel;
|
|
49
|
+
let withoutColl = withoutDcb.endsWith("EventColl") ? withoutDcb.slice(0, withoutDcb.length - "EventColl".length | 0) : withoutDcb;
|
|
50
|
+
if (withoutColl.endsWith("ReadModel")) {
|
|
51
|
+
return withoutColl.slice(0, withoutColl.length - "ReadModel".length | 0);
|
|
52
|
+
} else {
|
|
53
|
+
return withoutColl;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tableExists(db, name) {
|
|
58
|
+
return Stdlib_Option.isSome(SqliteDriver$ReventlessLocal.get(SqliteDriver$ReventlessLocal.prepare(db, "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"), [name]));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function strings(rows, column) {
|
|
62
|
+
return Stdlib_Array.filterMap(rows, row => {
|
|
63
|
+
let match = row[column];
|
|
64
|
+
if (typeof match === "string") {
|
|
65
|
+
return match;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function qdbTables(db) {
|
|
71
|
+
return strings(SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'qdb\\_%' ESCAPE '\\' ORDER BY name"), []), "name");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function distinct(db, table, column) {
|
|
75
|
+
if (tableExists(db, table)) {
|
|
76
|
+
return strings(SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, `SELECT DISTINCT ` + column + ` AS v FROM ` + table + ` ORDER BY v`), []), "v");
|
|
77
|
+
} else {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function countWhere(db, table, column, value) {
|
|
83
|
+
if (!tableExists(db, table)) {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
let row = SqliteDriver$ReventlessLocal.get(SqliteDriver$ReventlessLocal.prepare(db, `SELECT COUNT(*) AS c FROM ` + table + ` WHERE ` + column + ` = ?`), [value]);
|
|
87
|
+
if (row === undefined) {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
let match = row["c"];
|
|
91
|
+
if (typeof match === "number") {
|
|
92
|
+
return match | 0;
|
|
93
|
+
} else {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function countAll(db, table) {
|
|
99
|
+
if (!tableExists(db, table)) {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
let row = SqliteDriver$ReventlessLocal.get(SqliteDriver$ReventlessLocal.prepare(db, `SELECT COUNT(*) AS c FROM ` + table), []);
|
|
103
|
+
if (row === undefined) {
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
let match = row["c"];
|
|
107
|
+
if (typeof match === "number") {
|
|
108
|
+
return match | 0;
|
|
109
|
+
} else {
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function namesIn(structure, field) {
|
|
115
|
+
let match = structure[field];
|
|
116
|
+
if (match !== undefined) {
|
|
117
|
+
if (Array.isArray(match)) {
|
|
118
|
+
return Stdlib_Array.filterMap(match, item => {
|
|
119
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let match = item["name"];
|
|
123
|
+
if (typeof match === "string") {
|
|
124
|
+
return match;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveStructure(root, json) {
|
|
136
|
+
if (typeof json !== "object" || json === null || Array.isArray(json)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let match = json[Offload$Reventless.sentinelKey];
|
|
140
|
+
if (match === undefined) {
|
|
141
|
+
return json;
|
|
142
|
+
}
|
|
143
|
+
if (typeof match !== "object" || match === null || Array.isArray(match)) {
|
|
144
|
+
return json;
|
|
145
|
+
}
|
|
146
|
+
let match$1 = match["key"];
|
|
147
|
+
if (typeof match$1 === "string") {
|
|
148
|
+
return Stdlib_Option.flatMap(ObjectStoreStorage_FileSystem$ReventlessLocal.getOffload(root, match$1), bytes => {
|
|
149
|
+
let resolved;
|
|
150
|
+
try {
|
|
151
|
+
resolved = JSON.parse(bytes);
|
|
152
|
+
} catch (exn) {
|
|
153
|
+
resolved = null;
|
|
154
|
+
}
|
|
155
|
+
if (typeof resolved === "object" && resolved !== null && !Array.isArray(resolved)) {
|
|
156
|
+
return resolved;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readPlugins(db, root) {
|
|
163
|
+
let table = qdbTableName(PluginsReadModelSpec$ReventlessCore.name);
|
|
164
|
+
if (tableExists(db, table)) {
|
|
165
|
+
return Stdlib_Array.filterMap(SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, `SELECT partition_key, item FROM ` + table), []), row => {
|
|
166
|
+
let match = row["partition_key"];
|
|
167
|
+
let match$1 = row["item"];
|
|
168
|
+
if (match === undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (typeof match !== "string") {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (match$1 === undefined) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (typeof match$1 !== "string") {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let o;
|
|
181
|
+
try {
|
|
182
|
+
o = JSON.parse(match$1);
|
|
183
|
+
} catch (exn) {
|
|
184
|
+
o = null;
|
|
185
|
+
}
|
|
186
|
+
if (typeof o === "object" && o !== null && !Array.isArray(o)) {
|
|
187
|
+
return Stdlib_Option.map(Stdlib_Option.flatMap(o["structure"], s => resolveStructure(root, s)), structure => {
|
|
188
|
+
let match$2 = structure["requiredStores"];
|
|
189
|
+
let tmp;
|
|
190
|
+
tmp = match$2 !== undefined ? (
|
|
191
|
+
Array.isArray(match$2) ? Stdlib_Array.filterMap(match$2, i => {
|
|
192
|
+
if (typeof i === "string") {
|
|
193
|
+
return i;
|
|
194
|
+
}
|
|
195
|
+
}) : []
|
|
196
|
+
) : [];
|
|
197
|
+
return {
|
|
198
|
+
plugin: match,
|
|
199
|
+
queryables: namesIn(structure, "readModels").concat(namesIn(structure, "stateViewSlices")),
|
|
200
|
+
writables: namesIn(structure, "aggregates").concat(namesIn(structure, "stateChangeSlices")),
|
|
201
|
+
stores: tmp
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function total(p) {
|
|
212
|
+
return Stdlib_Array.reduce(p.items, 0, (sum, i) => sum + i.count | 0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function deleteWhere(db, table, column, value) {
|
|
216
|
+
if (tableExists(db, table)) {
|
|
217
|
+
return SqliteDriver$ReventlessLocal.run(SqliteDriver$ReventlessLocal.prepare(db, `DELETE FROM ` + table + ` WHERE ` + column + ` = ?`), [value]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function clearTable(db, table) {
|
|
222
|
+
if (tableExists(db, table)) {
|
|
223
|
+
return SqliteDriver$ReventlessLocal.exec(db, `DELETE FROM ` + table);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function build(db, root, scope) {
|
|
228
|
+
let plugins = readPlugins(db, root);
|
|
229
|
+
let platformQdb = platformQueryables.map(qdbTableName);
|
|
230
|
+
let platformLogs = platformWritables.map(aggregateLogName);
|
|
231
|
+
let selectedPlugins;
|
|
232
|
+
if (typeof scope !== "object") {
|
|
233
|
+
switch (scope) {
|
|
234
|
+
case "Platform" :
|
|
235
|
+
selectedPlugins = [];
|
|
236
|
+
break;
|
|
237
|
+
case "Domain" :
|
|
238
|
+
case "Everything" :
|
|
239
|
+
selectedPlugins = plugins;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
let name = scope._0;
|
|
244
|
+
selectedPlugins = plugins.filter(p => p.plugin === name);
|
|
245
|
+
}
|
|
246
|
+
let claimsQdb = table => {
|
|
247
|
+
if (typeof scope === "object") {
|
|
248
|
+
return selectedPlugins.some(p => p.queryables.some(q => qdbTableName(q) === table));
|
|
249
|
+
}
|
|
250
|
+
switch (scope) {
|
|
251
|
+
case "Domain" :
|
|
252
|
+
return !platformQdb.includes(table);
|
|
253
|
+
case "Platform" :
|
|
254
|
+
return platformQdb.includes(table);
|
|
255
|
+
case "Everything" :
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
let claimsLog = log => {
|
|
260
|
+
if (typeof scope === "object") {
|
|
261
|
+
return selectedPlugins.some(p => p.writables.some(w => {
|
|
262
|
+
if (aggregateLogName(w) === log) {
|
|
263
|
+
return true;
|
|
264
|
+
} else {
|
|
265
|
+
return w === log;
|
|
266
|
+
}
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
switch (scope) {
|
|
270
|
+
case "Domain" :
|
|
271
|
+
return !platformLogs.includes(log);
|
|
272
|
+
case "Platform" :
|
|
273
|
+
return platformLogs.includes(log);
|
|
274
|
+
case "Everything" :
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
let claimedQdb = qdbTables(db).filter(claimsQdb);
|
|
279
|
+
let claimedComponents = claimedQdb.map(t => t.slice("qdb_".length, t.length));
|
|
280
|
+
let items = [];
|
|
281
|
+
claimedQdb.forEach(table => {
|
|
282
|
+
items.push({
|
|
283
|
+
label: table,
|
|
284
|
+
count: countAll(db, table),
|
|
285
|
+
run: () => clearTable(db, table)
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
[
|
|
289
|
+
"event_log",
|
|
290
|
+
"snapshot"
|
|
291
|
+
].forEach(table => {
|
|
292
|
+
distinct(db, table, "log_name").filter(claimsLog).forEach(log => {
|
|
293
|
+
items.push({
|
|
294
|
+
label: table + ` (` + log + `)`,
|
|
295
|
+
count: countWhere(db, table, "log_name", log),
|
|
296
|
+
run: () => deleteWhere(db, table, "log_name", log)
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
[
|
|
301
|
+
"dcb_event",
|
|
302
|
+
"dcb_tag"
|
|
303
|
+
].forEach(table => {
|
|
304
|
+
distinct(db, table, "log_name").filter(claimsLog).forEach(log => {
|
|
305
|
+
items.push({
|
|
306
|
+
label: table + ` (` + log + `)`,
|
|
307
|
+
count: countWhere(db, table, "log_name", log),
|
|
308
|
+
run: () => deleteWhere(db, table, "log_name", log)
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
distinct(db, "projection_checkpoint", "read_model").filter(rm => claimedComponents.includes(checkpointComponent(rm))).forEach(rm => {
|
|
313
|
+
items.push({
|
|
314
|
+
label: `projection_checkpoint (` + rm + `)`,
|
|
315
|
+
count: countWhere(db, "projection_checkpoint", "read_model", rm),
|
|
316
|
+
run: () => deleteWhere(db, "projection_checkpoint", "read_model", rm)
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
let claimedPrefixes;
|
|
320
|
+
if (typeof scope !== "object") {
|
|
321
|
+
switch (scope) {
|
|
322
|
+
case "Platform" :
|
|
323
|
+
claimedPrefixes = [];
|
|
324
|
+
break;
|
|
325
|
+
case "Domain" :
|
|
326
|
+
case "Everything" :
|
|
327
|
+
claimedPrefixes = ObjectStoreStorage_FileSystem$ReventlessLocal.topLevelPrefixes(root);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
claimedPrefixes = selectedPlugins.flatMap(p => p.stores.map(LocalObjectStore$ReventlessLocal.localPrefixFor));
|
|
332
|
+
}
|
|
333
|
+
claimedPrefixes.forEach(prefix => {
|
|
334
|
+
let count = ObjectStoreStorage_FileSystem$ReventlessLocal.keysUnder(root, prefix).length;
|
|
335
|
+
if (count > 0) {
|
|
336
|
+
items.push({
|
|
337
|
+
label: `objects/` + prefix + `/`,
|
|
338
|
+
count: count,
|
|
339
|
+
run: () => {
|
|
340
|
+
ObjectStoreStorage_FileSystem$ReventlessLocal.deleteUnder(root, prefix);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
let exit = 0;
|
|
347
|
+
if (typeof scope !== "object") {
|
|
348
|
+
switch (scope) {
|
|
349
|
+
case "Domain" :
|
|
350
|
+
break;
|
|
351
|
+
case "Platform" :
|
|
352
|
+
case "Everything" :
|
|
353
|
+
exit = 1;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (exit === 1) {
|
|
358
|
+
let count = ObjectStoreStorage_FileSystem$ReventlessLocal.offloadKeys(root).length;
|
|
359
|
+
if (count > 0) {
|
|
360
|
+
items.push({
|
|
361
|
+
label: "offload/",
|
|
362
|
+
count: count,
|
|
363
|
+
run: () => {
|
|
364
|
+
ObjectStoreStorage_FileSystem$ReventlessLocal.deleteOffloadAll(root);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
let unattributed;
|
|
370
|
+
if (typeof scope !== "object") {
|
|
371
|
+
unattributed = [];
|
|
372
|
+
} else {
|
|
373
|
+
let claimedByAnyPlugin = plugins.flatMap(p => p.queryables.map(qdbTableName));
|
|
374
|
+
unattributed = qdbTables(db).filter(t => {
|
|
375
|
+
if (platformQdb.includes(t)) {
|
|
376
|
+
return false;
|
|
377
|
+
} else {
|
|
378
|
+
return !claimedByAnyPlugin.includes(t);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
scope: scope,
|
|
384
|
+
items: items,
|
|
385
|
+
unattributed: unattributed
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function describe(p) {
|
|
390
|
+
console.log("");
|
|
391
|
+
console.log(`Reset scope: ` + scopeLabel(p.scope));
|
|
392
|
+
console.log("");
|
|
393
|
+
if (p.items.length === 0) {
|
|
394
|
+
console.log(" (nothing — this scope already reads empty)");
|
|
395
|
+
} else {
|
|
396
|
+
p.items.forEach(i => {
|
|
397
|
+
console.log(` ` + i.count.toString().padStart(7, " ") + ` ` + i.label);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (p.unattributed.length !== 0) {
|
|
401
|
+
console.log("");
|
|
402
|
+
console.log(` Left alone — no connected plugin's structure claims them (widen the scope to include them):`);
|
|
403
|
+
p.unattributed.forEach(t => {
|
|
404
|
+
console.log(` ` + t);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
console.log("");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function execute(db, p) {
|
|
411
|
+
SqliteDriver$ReventlessLocal.transaction(db, () => {
|
|
412
|
+
p.items.forEach(i => i.run());
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function scopeOptions(plugins) {
|
|
417
|
+
return [[
|
|
418
|
+
"domain",
|
|
419
|
+
"Domain"
|
|
420
|
+
]].concat(plugins.map(p => [
|
|
421
|
+
p,
|
|
422
|
+
{
|
|
423
|
+
TAG: "OnePlugin",
|
|
424
|
+
_0: p
|
|
425
|
+
}
|
|
426
|
+
])).concat([
|
|
427
|
+
[
|
|
428
|
+
"platform",
|
|
429
|
+
"Platform"
|
|
430
|
+
],
|
|
431
|
+
[
|
|
432
|
+
"everything",
|
|
433
|
+
"Everything"
|
|
434
|
+
]
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function run(dbPath) {
|
|
439
|
+
let go = async () => {
|
|
440
|
+
let resolved;
|
|
441
|
+
if (dbPath !== undefined) {
|
|
442
|
+
resolved = dbPath;
|
|
443
|
+
} else {
|
|
444
|
+
let match = Backend$ReventlessLocal.fromEnv();
|
|
445
|
+
if (typeof match !== "object") {
|
|
446
|
+
console.log("Nothing to reset — REVENTLESS_LOCAL_BACKEND selects an in-memory store, which a restart already empties.");
|
|
447
|
+
resolved = undefined;
|
|
448
|
+
} else if (match.TAG === "Sqlite") {
|
|
449
|
+
let path = match.path;
|
|
450
|
+
if (path !== ":memory:") {
|
|
451
|
+
resolved = path;
|
|
452
|
+
} else {
|
|
453
|
+
console.log("Nothing to reset — REVENTLESS_LOCAL_BACKEND selects an in-memory store, which a restart already empties.");
|
|
454
|
+
resolved = undefined;
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
console.log("Nothing to reset here — the Postgres backend keeps its event logs off this machine. Reset it against the database.");
|
|
458
|
+
resolved = undefined;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (resolved === undefined) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (Nodefs.existsSync(resolved)) {
|
|
465
|
+
let root = Nodepath.dirname(resolved);
|
|
466
|
+
let db = SqliteDriver$ReventlessLocal.openDb(resolved);
|
|
467
|
+
let plugins = readPlugins(db, root).map(p => p.plugin);
|
|
468
|
+
let scope = await Seed_Prompt$ReventlessSeed.select("Reset scope:", scopeOptions(plugins), "SEED_RESET_SCOPE");
|
|
469
|
+
let plan = build(db, root, scope);
|
|
470
|
+
describe(plan);
|
|
471
|
+
if (total(plan) === 0) {
|
|
472
|
+
console.log("Nothing to do.");
|
|
473
|
+
} else {
|
|
474
|
+
let match$1 = Seed_Prompt$ReventlessSeed.envValue("SEED_RESET_CONFIRM");
|
|
475
|
+
let confirmed;
|
|
476
|
+
let exit = 0;
|
|
477
|
+
if (match$1 !== undefined) {
|
|
478
|
+
switch (match$1) {
|
|
479
|
+
case "1" :
|
|
480
|
+
case "yes" :
|
|
481
|
+
confirmed = true;
|
|
482
|
+
break;
|
|
483
|
+
default:
|
|
484
|
+
exit = 1;
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
exit = 1;
|
|
488
|
+
}
|
|
489
|
+
if (exit === 1) {
|
|
490
|
+
let answer = await Seed_Prompt$ReventlessSeed.ask(`Empty ` + total(plan).toString() + ` row(s)/object(s) in the "` + scopeLabel(plan.scope) + `" scope? [y/N]: `);
|
|
491
|
+
confirmed = answer.trim().toLowerCase() === "y";
|
|
492
|
+
}
|
|
493
|
+
if (confirmed) {
|
|
494
|
+
execute(db, plan);
|
|
495
|
+
console.log(`Reset complete — the "` + scopeLabel(plan.scope) + `" scope reads empty and is re-seedable.`);
|
|
496
|
+
} else {
|
|
497
|
+
console.log("Nothing was deleted.");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
Seed_Prompt$ReventlessSeed.close();
|
|
501
|
+
return SqliteDriver$ReventlessLocal.close(db);
|
|
502
|
+
}
|
|
503
|
+
console.log(`Nothing to reset — no store at ` + resolved + `.`);
|
|
504
|
+
};
|
|
505
|
+
go();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export {
|
|
509
|
+
scopeLabel,
|
|
510
|
+
platformQueryables,
|
|
511
|
+
platformWritables,
|
|
512
|
+
qdbTableName,
|
|
513
|
+
aggregateLogName,
|
|
514
|
+
checkpointComponent,
|
|
515
|
+
tableExists,
|
|
516
|
+
strings,
|
|
517
|
+
qdbTables,
|
|
518
|
+
distinct,
|
|
519
|
+
countWhere,
|
|
520
|
+
countAll,
|
|
521
|
+
namesIn,
|
|
522
|
+
resolveStructure,
|
|
523
|
+
readPlugins,
|
|
524
|
+
total,
|
|
525
|
+
deleteWhere,
|
|
526
|
+
clearTable,
|
|
527
|
+
build,
|
|
528
|
+
describe,
|
|
529
|
+
execute,
|
|
530
|
+
scopeOptions,
|
|
531
|
+
run,
|
|
532
|
+
}
|
|
533
|
+
/* node:fs Not a pure module */
|
|
@@ -205,6 +205,57 @@ describe("Backend parity (Memory vs Sqlite)", () => {
|
|
|
205
205
|
await runUnderSqlite(scenario)
|
|
206
206
|
})
|
|
207
207
|
|
|
208
|
+
testPromise("QueryDb: a descriptor carries the saved row and a rising seq under both", async () => {
|
|
209
|
+
let scenario = async () => {
|
|
210
|
+
module TestBus = LocalBus.Make()
|
|
211
|
+
module Storage = LocalQueryDbStorage.Make(TestBus)
|
|
212
|
+
let s = Storage.make(
|
|
213
|
+
~name="parity-payload",
|
|
214
|
+
~indexes=[],
|
|
215
|
+
~api=(),
|
|
216
|
+
~apiRole=(),
|
|
217
|
+
~owner=None,
|
|
218
|
+
~opts,
|
|
219
|
+
)
|
|
220
|
+
let ops = await s.operations->TestRunner.resolve
|
|
221
|
+
|
|
222
|
+
let descriptors = []
|
|
223
|
+
TestBus.subscribeToStateChanges("parity-payload", d => descriptors->Array.push(d))
|
|
224
|
+
|
|
225
|
+
let field = (d, key) => d->JSON.Decode.object->Option.flatMap(o => o->Dict.get(key))
|
|
226
|
+
let item = (k, v) =>
|
|
227
|
+
JSON.Encode.object(
|
|
228
|
+
Dict.fromArray([("id", JSON.Encode.string(k)), ("v", JSON.Encode.string(v))]),
|
|
229
|
+
)
|
|
230
|
+
let stateAt = i => descriptors->Array.get(i)->Option.flatMap(d => field(d, "state"))
|
|
231
|
+
|
|
232
|
+
let _ = await ops.save("p1", item("p1", "one"), ReventlessCore.QueryDb.Any, None)
|
|
233
|
+
let _ = await ops.save("p1", item("p1", "two"), ReventlessCore.QueryDb.Any, None)
|
|
234
|
+
let _ = await ops.delete("p1", None)
|
|
235
|
+
|
|
236
|
+
// The point of the payload: a subscriber can apply the row it was handed
|
|
237
|
+
// instead of spending a round-trip to fetch what the platform already had.
|
|
238
|
+
expect(stateAt(0))->toEqual(Some(item("p1", "one")))
|
|
239
|
+
expect(stateAt(1))->toEqual(Some(item("p1", "two")))
|
|
240
|
+
// A delete has no new row to carry.
|
|
241
|
+
expect(stateAt(2))->toEqual(None)
|
|
242
|
+
|
|
243
|
+
// seq only has to rise — it is monotonic, not consecutive, so a client can
|
|
244
|
+
// reject a stale payload but cannot count gaps.
|
|
245
|
+
let seqs =
|
|
246
|
+
descriptors->Array.filterMap(d =>
|
|
247
|
+
field(d, "seq")->Option.flatMap(JSON.Decode.string)->Option.flatMap(Float.fromString)
|
|
248
|
+
)
|
|
249
|
+
expect(seqs->Array.length)->toBe(3)
|
|
250
|
+
let rising =
|
|
251
|
+
seqs->Array.everyWithIndex((v, i) => i == 0 || v > seqs->Array.getUnsafe(i - 1))
|
|
252
|
+
expect(rising)->toBe(true)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await runUnderMemory(scenario)
|
|
256
|
+
await runUnderSqlite(scenario)
|
|
257
|
+
})
|
|
258
|
+
|
|
208
259
|
testPromise("QueryDb: saveBatch reports Added then Updated for a repeated key under both", async () => {
|
|
209
260
|
let scenario = async () => {
|
|
210
261
|
module TestBus = LocalBus.Make()
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
4
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Stdlib_Float from "@rescript/runtime/lib/es6/Stdlib_Float.js";
|
|
5
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
8
|
import * as Effect from "effect/Effect";
|
|
7
9
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
@@ -173,6 +175,48 @@ globalThis.describe("Backend parity (Memory vs Sqlite)", () => {
|
|
|
173
175
|
await runUnderMemory(scenario);
|
|
174
176
|
return await runUnderSqlite(scenario);
|
|
175
177
|
});
|
|
178
|
+
globalThis.test("QueryDb: a descriptor carries the saved row and a rising seq under both", async () => {
|
|
179
|
+
let scenario = async () => {
|
|
180
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
181
|
+
let Storage = LocalQueryDbStorage$ReventlessLocal.Make(TestBus);
|
|
182
|
+
let s = Storage.make("parity-payload", [], undefined, undefined, undefined, undefined, undefined, opts);
|
|
183
|
+
let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
|
|
184
|
+
let descriptors = [];
|
|
185
|
+
TestBus.subscribeToStateChanges("parity-payload", d => {
|
|
186
|
+
descriptors.push(d);
|
|
187
|
+
});
|
|
188
|
+
let field = (d, key) => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(d), o => o[key]);
|
|
189
|
+
let item = (k, v) => Object.fromEntries([
|
|
190
|
+
[
|
|
191
|
+
"id",
|
|
192
|
+
k
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
"v",
|
|
196
|
+
v
|
|
197
|
+
]
|
|
198
|
+
]);
|
|
199
|
+
let stateAt = i => Stdlib_Option.flatMap(descriptors[i], d => field(d, "state"));
|
|
200
|
+
await ops.save("p1", item("p1", "one"), "Any", undefined);
|
|
201
|
+
await ops.save("p1", item("p1", "two"), "Any", undefined);
|
|
202
|
+
await ops.delete("p1", undefined);
|
|
203
|
+
globalThis.expect(stateAt(0)).toEqual(item("p1", "one"));
|
|
204
|
+
globalThis.expect(stateAt(1)).toEqual(item("p1", "two"));
|
|
205
|
+
globalThis.expect(stateAt(2)).toEqual(undefined);
|
|
206
|
+
let seqs = Stdlib_Array.filterMap(descriptors, d => Stdlib_Option.flatMap(Stdlib_Option.flatMap(field(d, "seq"), Stdlib_JSON.Decode.string), Stdlib_Float.fromString));
|
|
207
|
+
globalThis.expect(seqs.length).toBe(3);
|
|
208
|
+
let rising = seqs.every((v, i) => {
|
|
209
|
+
if (i === 0) {
|
|
210
|
+
return true;
|
|
211
|
+
} else {
|
|
212
|
+
return v > seqs[i - 1 | 0];
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
globalThis.expect(rising).toBe(true);
|
|
216
|
+
};
|
|
217
|
+
await runUnderMemory(scenario);
|
|
218
|
+
return await runUnderSqlite(scenario);
|
|
219
|
+
});
|
|
176
220
|
globalThis.test("QueryDb: saveBatch reports Added then Updated for a repeated key under both", async () => {
|
|
177
221
|
let scenario = async () => {
|
|
178
222
|
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
@@ -77,8 +77,8 @@ describe("LocalGraphQL_SubscriptionResolvers", () => {
|
|
|
77
77
|
let consumerPromise = startConsumer(iter, 1000)
|
|
78
78
|
await yieldTick() // ensure listener is registered before publish
|
|
79
79
|
|
|
80
|
-
// Bus payload is
|
|
81
|
-
//
|
|
80
|
+
// Bus payload is the change descriptor matching the AWS StateTopic Lambda
|
|
81
|
+
// output shape: {changeKind, id, sortKeyValue?, seq, state?}.
|
|
82
82
|
let state = JSON.Encode.object(
|
|
83
83
|
Dict.fromArray([
|
|
84
84
|
("id", JSON.Encode.string("prod-1")),
|
|
@@ -86,10 +86,11 @@ describe("LocalGraphQL_SubscriptionResolvers", () => {
|
|
|
86
86
|
("updatedAt", JSON.Encode.string("2026-05-19T12:00:00Z")),
|
|
87
87
|
]),
|
|
88
88
|
)
|
|
89
|
-
let descriptor =
|
|
89
|
+
let descriptor = LocalStateChangeDescriptor.make(
|
|
90
90
|
~changeKind="Updated",
|
|
91
91
|
~id="prod-1",
|
|
92
92
|
~state=Some(state),
|
|
93
|
+
~seq=LocalStateChangeDescriptor.nextSequence(),
|
|
93
94
|
)
|
|
94
95
|
TestBus.publishStateChange(~name="Product", ~descriptor)
|
|
95
96
|
|
|
@@ -115,10 +116,11 @@ describe("LocalGraphQL_SubscriptionResolvers", () => {
|
|
|
115
116
|
await yieldTick()
|
|
116
117
|
|
|
117
118
|
// Publish for a DIFFERENT ReadModel — should NOT reach our topic.
|
|
118
|
-
let foreignDescriptor =
|
|
119
|
+
let foreignDescriptor = LocalStateChangeDescriptor.make(
|
|
119
120
|
~changeKind="Updated",
|
|
120
121
|
~id="cat-1",
|
|
121
122
|
~state=Some(JSON.Encode.object(Dict.fromArray([("id", JSON.Encode.string("cat-1"))]))),
|
|
123
|
+
~seq=LocalStateChangeDescriptor.nextSequence(),
|
|
122
124
|
)
|
|
123
125
|
TestBus.publishStateChange(~name="Category", ~descriptor=foreignDescriptor)
|
|
124
126
|
|