@reventlessdev/reventless-local 3.0.0-alpha.225 → 3.0.0-alpha.227
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 +19 -0
- package/package.json +11 -11
- package/src/Platform.res +5 -6
- package/src/Platform.res.mjs +3 -5
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +1 -1
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +1 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +223 -22
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +176 -18
- package/tests/UiHintsTest.res +33 -6
- package/tests/UiHintsTest.res.mjs +15 -2
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +1 -1
- package/tests/adapter/DcbEventLogStorageTest.res +1 -1
- package/tests/adapter/EventLogSnapshotParityTest.res +1 -1
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +9 -7
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +5 -5
- package/tests/adapter/QueryDbListResolverTest.res +82 -2
- package/tests/adapter/QueryDbListResolverTest.res.mjs +106 -11
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
4
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
5
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
6
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
@@ -16,6 +17,7 @@ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/s
|
|
|
16
17
|
import * as SortKey_Filter$ReventlessLocal from "./SortKey_Filter.res.mjs";
|
|
17
18
|
import * as QueryDbListQuery$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/QueryDbListQuery.res.mjs";
|
|
18
19
|
import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Callback.res.mjs";
|
|
20
|
+
import * as GraphQL_CallerError$ReventlessLocal from "../GraphQL_CallerError.res.mjs";
|
|
19
21
|
import * as DomainGraphQL_Server$ReventlessLocal from "../DomainGraphQL_Server.res.mjs";
|
|
20
22
|
import * as OwnerScopeDiagnostics$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/OwnerScopeDiagnostics.res.mjs";
|
|
21
23
|
import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
@@ -112,7 +114,6 @@ function Make(Bus) {
|
|
|
112
114
|
return true;
|
|
113
115
|
}
|
|
114
116
|
};
|
|
115
|
-
let cap = s => s.charAt(0).toUpperCase() + s.slice(1);
|
|
116
117
|
let registryEntry = Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name];
|
|
117
118
|
let singleQueryName = registryEntry !== undefined ? registryEntry.singleFieldName : name.charAt(0).toLowerCase() + name.slice(1);
|
|
118
119
|
let listQueryName = registryEntry !== undefined ? registryEntry.listFieldName : name + "s";
|
|
@@ -124,7 +125,7 @@ function Make(Bus) {
|
|
|
124
125
|
relay.registerNodeType(returnTypeName, name);
|
|
125
126
|
}
|
|
126
127
|
let byIdSdl = includeIdParam ? (
|
|
127
|
-
subIdField !== undefined ? ` ` + singleQueryName + `(id: ID!, ` + subIdField + `: String): ` + returnTypeName : ` ` + singleQueryName + `(id: ID
|
|
128
|
+
subIdField !== undefined ? ` ` + singleQueryName + `(id: ID!, ` + subIdField + `: String, includeRetired: Boolean): ` + returnTypeName : ` ` + singleQueryName + `(id: ID!, includeRetired: Boolean): ` + returnTypeName
|
|
128
129
|
) : ` ` + singleQueryName + `: ` + returnTypeName;
|
|
129
130
|
let byIdResolver = async (_root, args, ctx) => {
|
|
130
131
|
let match = await runInterceptor(ctx, args);
|
|
@@ -219,6 +220,101 @@ function Make(Bus) {
|
|
|
219
220
|
byIdsResolverEntry = undefined;
|
|
220
221
|
}
|
|
221
222
|
let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
|
|
223
|
+
let refsSdl = includeIdParam && subIdField === undefined ? [GraphQL_FragmentGenerator$ReventlessCore.deriveRefsQueryField(listQueryName, returnTypeName)] : [];
|
|
224
|
+
let refsResolverEntry;
|
|
225
|
+
if (includeIdParam && subIdField === undefined) {
|
|
226
|
+
server.registerTypes([GraphQL_FragmentGenerator$ReventlessCore.deriveRefTypeSdl(returnTypeName)]);
|
|
227
|
+
let resolver$1 = async (_root, args, ctx) => {
|
|
228
|
+
let match = await runInterceptor(ctx, args);
|
|
229
|
+
if (typeof match === "object") {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
let ids = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d["ids"]), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.string);
|
|
233
|
+
let spec = retiredSpecOf();
|
|
234
|
+
let namesRetired = Stdlib_Option.mapOr(spec, false, r => r.namedWhenRetired);
|
|
235
|
+
let retirementOf = item => {
|
|
236
|
+
if (spec === undefined) {
|
|
237
|
+
return [
|
|
238
|
+
false,
|
|
239
|
+
undefined
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
let cell = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[spec.field]);
|
|
243
|
+
let scope_field = spec.field;
|
|
244
|
+
let scope_values = spec.values;
|
|
245
|
+
let scope = {
|
|
246
|
+
field: scope_field,
|
|
247
|
+
values: scope_values
|
|
248
|
+
};
|
|
249
|
+
let retired = OwnerScope$Reventless.isRetiredValue(scope, cell);
|
|
250
|
+
return [
|
|
251
|
+
retired,
|
|
252
|
+
retired && Stdlib_Option.isSome(spec.values) ? Stdlib_Option.flatMap(cell, Stdlib_JSON.Decode.string) : undefined
|
|
253
|
+
];
|
|
254
|
+
};
|
|
255
|
+
let ops = Bus.getQueryDb(name);
|
|
256
|
+
if (ops === undefined) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
let load = key => Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream(key)), param => Effect.succeed([])));
|
|
260
|
+
let loaded = await Promise.all(ids.map(async id => {
|
|
261
|
+
let item = (await load(id))[0];
|
|
262
|
+
if (item !== undefined) {
|
|
263
|
+
return [
|
|
264
|
+
id,
|
|
265
|
+
item
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
let localId = Api_Ids$ReventlessCore.alternateKey(id);
|
|
269
|
+
if (localId !== undefined) {
|
|
270
|
+
return [
|
|
271
|
+
localId,
|
|
272
|
+
(await load(localId))[0]
|
|
273
|
+
];
|
|
274
|
+
} else {
|
|
275
|
+
return [
|
|
276
|
+
id,
|
|
277
|
+
undefined
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
}));
|
|
281
|
+
return Stdlib_Array.filterMap(loaded, param => {
|
|
282
|
+
let id = param[0];
|
|
283
|
+
return Stdlib_Option.flatMap(Stdlib_Option.filter(param[1], item => ownerAllows(ctx, item)), item => {
|
|
284
|
+
let match = retirementOf(item);
|
|
285
|
+
let retired = match[0];
|
|
286
|
+
if (retired && !namesRetired) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
let label = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[labelField]), Stdlib_JSON.Decode.string), id);
|
|
290
|
+
return Object.fromEntries([
|
|
291
|
+
[
|
|
292
|
+
"id",
|
|
293
|
+
id
|
|
294
|
+
],
|
|
295
|
+
[
|
|
296
|
+
"label",
|
|
297
|
+
label
|
|
298
|
+
],
|
|
299
|
+
[
|
|
300
|
+
"retired",
|
|
301
|
+
retired
|
|
302
|
+
],
|
|
303
|
+
[
|
|
304
|
+
"retiredState",
|
|
305
|
+
Stdlib_Option.mapOr(match[1], null, prim => prim)
|
|
306
|
+
]
|
|
307
|
+
]);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
refsResolverEntry = [
|
|
312
|
+
listQueryName + "Refs",
|
|
313
|
+
resolver$1
|
|
314
|
+
];
|
|
315
|
+
} else {
|
|
316
|
+
refsResolverEntry = undefined;
|
|
317
|
+
}
|
|
222
318
|
let stateSchemaOpt = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name];
|
|
223
319
|
let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(name, stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
|
|
224
320
|
let fetchAllItems = async () => {
|
|
@@ -250,7 +346,7 @@ function Make(Bus) {
|
|
|
250
346
|
endCursor: null
|
|
251
347
|
}
|
|
252
348
|
};
|
|
253
|
-
let resolver$
|
|
349
|
+
let resolver$2 = async (_root, args, ctx) => {
|
|
254
350
|
let match = await runInterceptor(ctx, args);
|
|
255
351
|
if (typeof match === "object") {
|
|
256
352
|
return emptyConnection;
|
|
@@ -277,11 +373,11 @@ function Make(Bus) {
|
|
|
277
373
|
};
|
|
278
374
|
match = [
|
|
279
375
|
sdl,
|
|
280
|
-
resolver$
|
|
376
|
+
resolver$2
|
|
281
377
|
];
|
|
282
378
|
} else {
|
|
283
379
|
let sdl$1 = [` ` + listQueryName + `(nextToken: String, limit: Int): ` + pluralTypeName + `!`];
|
|
284
|
-
let resolver$
|
|
380
|
+
let resolver$3 = async (_root, args, ctx) => {
|
|
285
381
|
let match = await runInterceptor(ctx, args);
|
|
286
382
|
if (typeof match === "object") {
|
|
287
383
|
return {
|
|
@@ -306,7 +402,7 @@ function Make(Bus) {
|
|
|
306
402
|
};
|
|
307
403
|
match = [
|
|
308
404
|
sdl$1,
|
|
309
|
-
resolver$
|
|
405
|
+
resolver$3
|
|
310
406
|
];
|
|
311
407
|
}
|
|
312
408
|
let listResolvers = [[
|
|
@@ -324,7 +420,7 @@ function Make(Bus) {
|
|
|
324
420
|
}
|
|
325
421
|
let itemsResolvers;
|
|
326
422
|
if (subIdField !== undefined) {
|
|
327
|
-
let resolver$
|
|
423
|
+
let resolver$4 = async (_root, args, ctx) => {
|
|
328
424
|
let emptyConn = {
|
|
329
425
|
edges: [],
|
|
330
426
|
pageInfo: {
|
|
@@ -423,22 +519,81 @@ function Make(Bus) {
|
|
|
423
519
|
};
|
|
424
520
|
itemsResolvers = [[
|
|
425
521
|
singleQueryName + "Items",
|
|
426
|
-
resolver$
|
|
522
|
+
resolver$4
|
|
427
523
|
]];
|
|
428
524
|
} else {
|
|
429
525
|
itemsResolvers = [];
|
|
430
526
|
}
|
|
431
|
-
let indexSdlFields = indexes.map(ic =>
|
|
527
|
+
let indexSdlFields = indexes.map(ic => GraphQL_FragmentGenerator$ReventlessCore.deriveIndexQueryField(singleQueryName, ic, returnTypeName + "Connection"));
|
|
528
|
+
let rejectBackwardPaging = args => {
|
|
529
|
+
let given = key => {
|
|
530
|
+
let match = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[key]);
|
|
531
|
+
if (match !== undefined) {
|
|
532
|
+
return match !== null;
|
|
533
|
+
} else {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
if (!(given("last") || given("before"))) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
throw GraphQL_CallerError$ReventlessLocal.badUserInput("Backward pagination (last/before) is not supported on by-index connections; use first/after.");
|
|
541
|
+
};
|
|
542
|
+
let indexConnection = (args, items) => {
|
|
543
|
+
let arg = key => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[key]);
|
|
544
|
+
let after = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(arg("after"), Stdlib_JSON.Decode.string), s => Stdlib_Int.fromString(s, undefined)), -1);
|
|
545
|
+
let first = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(arg("first"), Stdlib_JSON.Decode.float), prim => prim | 0), 50);
|
|
546
|
+
let start = after + 1 | 0;
|
|
547
|
+
let page = items.slice(start, start + first | 0);
|
|
548
|
+
let taken = page.length;
|
|
549
|
+
let edges = page.map((item, i) => Object.fromEntries([
|
|
550
|
+
[
|
|
551
|
+
"node",
|
|
552
|
+
item
|
|
553
|
+
],
|
|
554
|
+
[
|
|
555
|
+
"cursor",
|
|
556
|
+
(start + i | 0).toString()
|
|
557
|
+
]
|
|
558
|
+
]));
|
|
559
|
+
return Object.fromEntries([
|
|
560
|
+
[
|
|
561
|
+
"edges",
|
|
562
|
+
edges
|
|
563
|
+
],
|
|
564
|
+
[
|
|
565
|
+
"pageInfo",
|
|
566
|
+
Object.fromEntries([
|
|
567
|
+
[
|
|
568
|
+
"hasNextPage",
|
|
569
|
+
(start + taken | 0) < items.length
|
|
570
|
+
],
|
|
571
|
+
[
|
|
572
|
+
"hasPreviousPage",
|
|
573
|
+
start > 0
|
|
574
|
+
],
|
|
575
|
+
[
|
|
576
|
+
"startCursor",
|
|
577
|
+
taken > 0 ? start.toString() : null
|
|
578
|
+
],
|
|
579
|
+
[
|
|
580
|
+
"endCursor",
|
|
581
|
+
taken > 0 ? ((start + taken | 0) - 1 | 0).toString() : null
|
|
582
|
+
]
|
|
583
|
+
])
|
|
584
|
+
]
|
|
585
|
+
]);
|
|
586
|
+
};
|
|
432
587
|
let indexResolvers = indexes.map(ic => {
|
|
433
|
-
let
|
|
434
|
-
let
|
|
435
|
-
let filterField = Stdlib_Option.getOr(ic.idField, index);
|
|
588
|
+
let resolverName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(singleQueryName, ic.index);
|
|
589
|
+
let filterField = GraphQL_FragmentGenerator$ReventlessCore.indexKeyField(ic);
|
|
436
590
|
let resolver = async (_root, args, ctx) => {
|
|
437
591
|
let match = await runInterceptor(ctx, args);
|
|
438
592
|
if (typeof match === "object") {
|
|
439
|
-
return [];
|
|
593
|
+
return indexConnection(args, []);
|
|
440
594
|
}
|
|
441
|
-
|
|
595
|
+
rejectBackwardPaging(args);
|
|
596
|
+
let value = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[filterField]), Stdlib_JSON.Decode.string), "");
|
|
442
597
|
let scoped = rows => rows.filter(item => {
|
|
443
598
|
if (ownerAllows(ctx, item)) {
|
|
444
599
|
return retiredAllows(ctx, args, item);
|
|
@@ -448,13 +603,13 @@ function Make(Bus) {
|
|
|
448
603
|
});
|
|
449
604
|
let lookup = Bus.getQueryDbIndexLookup(name);
|
|
450
605
|
if (lookup !== undefined) {
|
|
451
|
-
return scoped(lookup(filterField, value));
|
|
606
|
+
return indexConnection(args, scoped(lookup(filterField, value)));
|
|
452
607
|
}
|
|
453
608
|
let scanAll = Bus.getQueryDbScan(name);
|
|
454
609
|
if (scanAll !== undefined) {
|
|
455
|
-
return scoped(scanAll().filter(item => Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[filterField]), Stdlib_JSON.Decode.string), v => v === value), false)));
|
|
610
|
+
return indexConnection(args, scoped(scanAll().filter(item => Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[filterField]), Stdlib_JSON.Decode.string), v => v === value), false))));
|
|
456
611
|
} else {
|
|
457
|
-
return [];
|
|
612
|
+
return indexConnection(args, []);
|
|
458
613
|
}
|
|
459
614
|
};
|
|
460
615
|
return [
|
|
@@ -462,12 +617,15 @@ function Make(Bus) {
|
|
|
462
617
|
resolver
|
|
463
618
|
];
|
|
464
619
|
});
|
|
465
|
-
let allSdl = [byIdSdl].concat(byIdsSdl).concat(match[0]).concat(itemsSdl).concat(indexSdlFields);
|
|
620
|
+
let allSdl = [byIdSdl].concat(byIdsSdl).concat(refsSdl).concat(match[0]).concat(itemsSdl).concat(indexSdlFields);
|
|
466
621
|
let resolvers = {};
|
|
467
622
|
resolvers[singleQueryName] = byIdResolver;
|
|
468
623
|
Stdlib_Option.forEach(byIdsResolverEntry, param => {
|
|
469
624
|
resolvers[param[0]] = param[1];
|
|
470
625
|
});
|
|
626
|
+
Stdlib_Option.forEach(refsResolverEntry, param => {
|
|
627
|
+
resolvers[param[0]] = param[1];
|
|
628
|
+
});
|
|
471
629
|
listResolvers.forEach(param => {
|
|
472
630
|
resolvers[param[0]] = param[1];
|
|
473
631
|
});
|
package/tests/UiHintsTest.res
CHANGED
|
@@ -103,30 +103,54 @@ describe("UiHints.emit", () => {
|
|
|
103
103
|
// point, while mid-session it is almost always an editor mid-save, and killing a
|
|
104
104
|
// running dev server over a keystroke is worse than the restart this replaces.
|
|
105
105
|
|
|
106
|
-
//
|
|
106
|
+
// The edit is repeated, and that is not belt-and-braces.
|
|
107
|
+
//
|
|
108
|
+
// `fs.watch` does not promise that a write landing near the watcher's creation
|
|
109
|
+
// is reported. The FSEvents stream behind it on macOS is armed asynchronously,
|
|
110
|
+
// and a write that gets there first is not reported late — it is not reported at
|
|
111
|
+
// all. Measured against this very module: once the stream is up the edit is seen
|
|
112
|
+
// in about 10ms, and when it is not, the same edit produces no event in four
|
|
113
|
+
// seconds, with which of the two happens varying by how many watchers the
|
|
114
|
+
// process has already opened rather than by anything the test does.
|
|
115
|
+
//
|
|
116
|
+
// So a single edit tests the arming, not the reload. Repeating it on an interval
|
|
117
|
+
// far longer than a reload takes — 50ms of debounce plus a file copy — leaves the
|
|
118
|
+
// case where the first write lands identical to what it always was, and lets the
|
|
119
|
+
// case where it is dropped recover on the next one instead of reporting that hot
|
|
120
|
+
// reload is broken. Re-writing the same bytes is what a developer saving twice
|
|
121
|
+
// does, so nothing here asserts less than it did.
|
|
122
|
+
let retryEvery = 1000
|
|
123
|
+
|
|
124
|
+
// All three exits close the watcher AND clear both timers. A watcher is `unref`ed
|
|
107
125
|
// and would not hold the run open on its own, but a live 4-second timer would —
|
|
108
126
|
// and "Jest did not exit" on a suite that passed is exactly the noise that
|
|
109
127
|
// teaches a reader to ignore it.
|
|
110
|
-
let waitForReload = (~timeoutMs: int=4000, ~
|
|
128
|
+
let waitForReload = (~timeoutMs: int=4000, ~edit: unit => unit, ~uiHintsFile, ~dir) =>
|
|
111
129
|
Promise.make((resolve, _) => {
|
|
112
130
|
let fired = ref(0)
|
|
113
131
|
let watcher = ref(None)
|
|
114
132
|
let deadline = ref(None)
|
|
133
|
+
let retry = ref(None)
|
|
115
134
|
// Resolves with a count rather than rejecting on the deadline: "nothing was
|
|
116
135
|
// re-served" is the expected answer in half these cases, and a rejection
|
|
117
136
|
// would make the assertion read as an infrastructure failure.
|
|
118
137
|
let finish = () => {
|
|
119
138
|
watcher.contents->Option.forEach(NodeFs.watcherClose)
|
|
120
139
|
deadline.contents->Option.forEach(clearTimeout)
|
|
140
|
+
retry.contents->Option.forEach(clearTimeout)
|
|
121
141
|
resolve(fired.contents)
|
|
122
142
|
}
|
|
143
|
+
let rec editUntilSeen = () => {
|
|
144
|
+
edit()
|
|
145
|
+
retry := Some(setTimeout(editUntilSeen, retryEvery))
|
|
146
|
+
}
|
|
123
147
|
watcher :=
|
|
124
148
|
UiHints.watch(~uiHintsFile, ~dir, ~onReload=() => {
|
|
125
149
|
fired := fired.contents + 1
|
|
126
150
|
finish()
|
|
127
151
|
})
|
|
128
152
|
deadline := Some(setTimeout(finish, timeoutMs))
|
|
129
|
-
|
|
153
|
+
editUntilSeen()
|
|
130
154
|
})
|
|
131
155
|
|
|
132
156
|
describe("UiHints.watch", () => {
|
|
@@ -147,7 +171,7 @@ describe("UiHints.watch", () => {
|
|
|
147
171
|
let fired = await waitForReload(
|
|
148
172
|
~uiHintsFile=Some(path),
|
|
149
173
|
~dir,
|
|
150
|
-
~
|
|
174
|
+
~edit=() => NodeFs.writeFileSync(path, edited),
|
|
151
175
|
)
|
|
152
176
|
expect((fired > 0, served(dir)))->toEqual((true, edited))
|
|
153
177
|
})
|
|
@@ -163,7 +187,7 @@ describe("UiHints.watch", () => {
|
|
|
163
187
|
~timeoutMs=1500,
|
|
164
188
|
~uiHintsFile=Some(path),
|
|
165
189
|
~dir,
|
|
166
|
-
~
|
|
190
|
+
~edit=() => NodeFs.writeFileSync(path, `{"Catalog": {"views":`),
|
|
167
191
|
)
|
|
168
192
|
expect((fired, served(dir)))->toEqual((0, declared))
|
|
169
193
|
})
|
|
@@ -178,7 +202,10 @@ describe("UiHints.watch", () => {
|
|
|
178
202
|
let fired = await waitForReload(
|
|
179
203
|
~uiHintsFile=Some(path),
|
|
180
204
|
~dir,
|
|
181
|
-
|
|
205
|
+
// Both halves of the save, so a repeat of this edit is the same story told
|
|
206
|
+
// again and still ends on the whole file — never a truncated write left
|
|
207
|
+
// standing after a complete one.
|
|
208
|
+
~edit=() => {
|
|
182
209
|
NodeFs.writeFileSync(path, `{"Catalog": {"views":`)
|
|
183
210
|
let _ = setTimeout(() => NodeFs.writeFileSync(path, whole), 200)
|
|
184
211
|
},
|
|
@@ -88,7 +88,7 @@ globalThis.describe("UiHints.emit", () => {
|
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
function waitForReload(timeoutMsOpt,
|
|
91
|
+
function waitForReload(timeoutMsOpt, edit, uiHintsFile, dir) {
|
|
92
92
|
let timeoutMs = timeoutMsOpt !== undefined ? timeoutMsOpt : 4000;
|
|
93
93
|
return new Promise((resolve, param) => {
|
|
94
94
|
let fired = {
|
|
@@ -100,6 +100,9 @@ function waitForReload(timeoutMsOpt, afterWatching, uiHintsFile, dir) {
|
|
|
100
100
|
let deadline = {
|
|
101
101
|
contents: undefined
|
|
102
102
|
};
|
|
103
|
+
let retry = {
|
|
104
|
+
contents: undefined
|
|
105
|
+
};
|
|
103
106
|
let finish = () => {
|
|
104
107
|
Stdlib_Option.forEach(watcher.contents, prim => {
|
|
105
108
|
prim.close();
|
|
@@ -107,14 +110,21 @@ function waitForReload(timeoutMsOpt, afterWatching, uiHintsFile, dir) {
|
|
|
107
110
|
Stdlib_Option.forEach(deadline.contents, prim => {
|
|
108
111
|
clearTimeout(prim);
|
|
109
112
|
});
|
|
113
|
+
Stdlib_Option.forEach(retry.contents, prim => {
|
|
114
|
+
clearTimeout(prim);
|
|
115
|
+
});
|
|
110
116
|
resolve(fired.contents);
|
|
111
117
|
};
|
|
118
|
+
let editUntilSeen = () => {
|
|
119
|
+
edit();
|
|
120
|
+
retry.contents = Primitive_option.some(setTimeout(editUntilSeen, 1000));
|
|
121
|
+
};
|
|
112
122
|
watcher.contents = UiHints$ReventlessLocal.watch(uiHintsFile, dir, () => {
|
|
113
123
|
fired.contents = fired.contents + 1 | 0;
|
|
114
124
|
finish();
|
|
115
125
|
});
|
|
116
126
|
deadline.contents = Primitive_option.some(setTimeout(finish, timeoutMs));
|
|
117
|
-
|
|
127
|
+
editUntilSeen();
|
|
118
128
|
});
|
|
119
129
|
}
|
|
120
130
|
|
|
@@ -178,6 +188,8 @@ globalThis.describe("UiHints.watch", () => {
|
|
|
178
188
|
});
|
|
179
189
|
});
|
|
180
190
|
|
|
191
|
+
let retryEvery = 1000;
|
|
192
|
+
|
|
181
193
|
export {
|
|
182
194
|
shipped,
|
|
183
195
|
declared,
|
|
@@ -187,6 +199,7 @@ export {
|
|
|
187
199
|
served,
|
|
188
200
|
baselinePath,
|
|
189
201
|
threw,
|
|
202
|
+
retryEvery,
|
|
190
203
|
waitForReload,
|
|
191
204
|
}
|
|
192
205
|
/* Not a pure module */
|
|
@@ -94,7 +94,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
|
|
|
94
94
|
// Backend contract: an empty tag value (an absent composite-partition member) is
|
|
95
95
|
// legitimate — it appends, is recorded verbatim, and keeps matching through both
|
|
96
96
|
// the partition read and a composite read. Held identically by the in-memory and
|
|
97
|
-
// DynamoDB backends. See docs/plans/dcb-empty-tag-values-break-append.md.
|
|
97
|
+
// DynamoDB backends. See docs/plans/done/dcb-empty-tag-values-break-append.md.
|
|
98
98
|
testPromise("an empty tag value appends and stays readable", async () => {
|
|
99
99
|
await runUnderSqlite(async () => {
|
|
100
100
|
module TestBus = LocalBus.Make()
|
|
@@ -145,7 +145,7 @@ describe("DcbEventLogStorage_InMemory", () => {
|
|
|
145
145
|
// An empty tag value is a legitimate model state — an absent member of a
|
|
146
146
|
// composite partition key. Every backend must accept it, record it verbatim,
|
|
147
147
|
// and keep matching the event through its partition and composite reads. See
|
|
148
|
-
// docs/plans/dcb-empty-tag-values-break-append.md.
|
|
148
|
+
// docs/plans/done/dcb-empty-tag-values-break-append.md.
|
|
149
149
|
testPromise("an empty tag value appends and stays readable", async () => {
|
|
150
150
|
let storage = makeStorage()
|
|
151
151
|
let ops = await storage.operations->TestRunner.resolve
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Backend-parity tests for the EventLog snapshot ops
|
|
2
|
-
// (docs/plans/aggregate-snapshotting.md): the in-memory and SQLite backends
|
|
2
|
+
// (docs/plans/done/aggregate-snapshotting.md): the in-memory and SQLite backends
|
|
3
3
|
// must behave identically for latestSnapshot / writeSnapshot (keep-one) and
|
|
4
4
|
// the replayStream ~fromSeq delta read, so GWT/local tests of
|
|
5
5
|
// snapshot-enabled aggregates give the same results under either backend.
|
|
@@ -271,7 +271,7 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
271
271
|
expect(result.typeDef->Option.isSome)->toBe(true)
|
|
272
272
|
let typeDef = result.typeDef->Option.getOrThrow
|
|
273
273
|
expect(typeDef->String.includes("type CatalogProduct"))->toBe(true)
|
|
274
|
-
expect(result.singleQuery->String.includes("Catalog_Product(id: ID
|
|
274
|
+
expect(result.singleQuery->String.includes("Catalog_Product(id: ID!, includeRetired: Boolean)"))->toBe(true)
|
|
275
275
|
expect(result.singleQuery->String.includes("CatalogProduct"))->toBe(true)
|
|
276
276
|
expect(result.listQuery->Option.isSome)->toBe(true)
|
|
277
277
|
let listQ = result.listQuery->Option.getOrThrow
|
|
@@ -298,7 +298,7 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
298
298
|
)
|
|
299
299
|
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
300
300
|
let sdl = inspection.sdlPreview
|
|
301
|
-
expect(sdl->String.includes("RM_Product(id: ID
|
|
301
|
+
expect(sdl->String.includes("RM_Product(id: ID!, includeRetired: Boolean): RMProduct"))->toBe(true)
|
|
302
302
|
expect(sdl->String.includes("type RMProduct"))->toBe(true)
|
|
303
303
|
// The type should have an injected id: ID! field (first field in the type)
|
|
304
304
|
let typeLines = sdl->String.split("\n")
|
|
@@ -357,7 +357,7 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
357
357
|
)
|
|
358
358
|
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
359
359
|
let sdl = inspection.sdlPreview
|
|
360
|
-
expect(sdl->String.includes("Default_Thing(id: ID
|
|
360
|
+
expect(sdl->String.includes("Default_Thing(id: ID!, includeRetired: Boolean): DefaultThing"))->toBe(true)
|
|
361
361
|
})
|
|
362
362
|
})
|
|
363
363
|
|
|
@@ -542,13 +542,15 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
542
542
|
],
|
|
543
543
|
)
|
|
544
544
|
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
545
|
-
// TestState, TestStateEdge, TestStateConnection, TestStateFilter (
|
|
545
|
+
// TestState, TestStateEdge, TestStateConnection, TestStateFilter (connection filter)
|
|
546
|
+
// + TestStateRef (the reference door's projection)
|
|
546
547
|
// + CommandResult union + CommandAccepted + CommandRejected + CommandPending
|
|
547
548
|
// (auto-injected whenever the fragment emits any mutation field).
|
|
548
|
-
expect(inspection.types->Array.length)->toBe(
|
|
549
|
+
expect(inspection.types->Array.length)->toBe(9)
|
|
549
550
|
expect(inspection.mutations->Array.length)->toBe(1)
|
|
550
|
-
// single Test_State(id), list Test_States(...),
|
|
551
|
-
|
|
551
|
+
// single Test_State(id), list Test_States(...), Test_StatesByIds(ids: [String!]!)
|
|
552
|
+
// and Test_StatesRefs(ids: [ID!]!) — the by-ids read projected to a reference.
|
|
553
|
+
expect(inspection.queries->Array.length)->toBe(4)
|
|
552
554
|
expect(inspection.sdlPreview->String.includes("type TestState"))->toBe(true)
|
|
553
555
|
expect(inspection.sdlPreview->String.includes("type TestStateEdge"))->toBe(true)
|
|
554
556
|
expect(inspection.sdlPreview->String.includes("type TestStateConnection"))->toBe(true)
|
|
@@ -239,7 +239,7 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
|
|
|
239
239
|
globalThis.expect(Stdlib_Option.isSome(result.typeDef)).toBe(true);
|
|
240
240
|
let typeDef = Stdlib_Option.getOrThrow(result.typeDef, undefined);
|
|
241
241
|
globalThis.expect(typeDef.includes("type CatalogProduct")).toBe(true);
|
|
242
|
-
globalThis.expect(result.singleQuery.includes("Catalog_Product(id: ID
|
|
242
|
+
globalThis.expect(result.singleQuery.includes("Catalog_Product(id: ID!, includeRetired: Boolean)")).toBe(true);
|
|
243
243
|
globalThis.expect(result.singleQuery.includes("CatalogProduct")).toBe(true);
|
|
244
244
|
globalThis.expect(Stdlib_Option.isSome(result.listQuery)).toBe(true);
|
|
245
245
|
let listQ = Stdlib_Option.getOrThrow(result.listQuery, undefined);
|
|
@@ -258,7 +258,7 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
|
|
|
258
258
|
}]);
|
|
259
259
|
let inspection = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment);
|
|
260
260
|
let sdl = inspection.sdlPreview;
|
|
261
|
-
globalThis.expect(sdl.includes("RM_Product(id: ID
|
|
261
|
+
globalThis.expect(sdl.includes("RM_Product(id: ID!, includeRetired: Boolean): RMProduct")).toBe(true);
|
|
262
262
|
globalThis.expect(sdl.includes("type RMProduct")).toBe(true);
|
|
263
263
|
let typeLines = sdl.split("\n");
|
|
264
264
|
let idFieldInType = typeLines.some(line => {
|
|
@@ -302,7 +302,7 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
|
|
|
302
302
|
}]);
|
|
303
303
|
let inspection = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment);
|
|
304
304
|
let sdl = inspection.sdlPreview;
|
|
305
|
-
globalThis.expect(sdl.includes("Default_Thing(id: ID
|
|
305
|
+
globalThis.expect(sdl.includes("Default_Thing(id: ID!, includeRetired: Boolean): DefaultThing")).toBe(true);
|
|
306
306
|
});
|
|
307
307
|
});
|
|
308
308
|
globalThis.describe("Relay compliance", () => {
|
|
@@ -424,9 +424,9 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
|
|
|
424
424
|
authorization: undefined
|
|
425
425
|
}]);
|
|
426
426
|
let inspection = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment);
|
|
427
|
-
globalThis.expect(inspection.types.length).toBe(
|
|
427
|
+
globalThis.expect(inspection.types.length).toBe(9);
|
|
428
428
|
globalThis.expect(inspection.mutations.length).toBe(1);
|
|
429
|
-
globalThis.expect(inspection.queries.length).toBe(
|
|
429
|
+
globalThis.expect(inspection.queries.length).toBe(4);
|
|
430
430
|
globalThis.expect(inspection.sdlPreview.includes("type TestState")).toBe(true);
|
|
431
431
|
globalThis.expect(inspection.sdlPreview.includes("type TestStateEdge")).toBe(true);
|
|
432
432
|
globalThis.expect(inspection.sdlPreview.includes("type TestStateConnection")).toBe(true);
|
|
@@ -102,7 +102,7 @@ let pageInfoString = (response: JSON.t, key: string): option<string> =>
|
|
|
102
102
|
// Per-test fixture — fresh Bus + storage + registry + resolver
|
|
103
103
|
// ─────────────────────────────────────────────────────────────
|
|
104
104
|
|
|
105
|
-
let buildFixture = async (~name: string) => {
|
|
105
|
+
let buildFixture = async (~name: string, ~indexes: array<Reventless.ReadModel.indexConfig>=[]) => {
|
|
106
106
|
module Bus = LocalBus.Make()
|
|
107
107
|
module Storage = LocalQueryDbStorage.Make(Bus)
|
|
108
108
|
module Resolvers = QueryDbResolvers_GraphQL.Make(Bus)
|
|
@@ -151,7 +151,7 @@ let buildFixture = async (~name: string) => {
|
|
|
151
151
|
~api=(),
|
|
152
152
|
~apiRole=(),
|
|
153
153
|
~dataSourceName=""->Pulumi.Output.make,
|
|
154
|
-
~indexes
|
|
154
|
+
~indexes,
|
|
155
155
|
~subIdField=None,
|
|
156
156
|
~idResolverConfigs=[],
|
|
157
157
|
~idsResolverConfigs=[],
|
|
@@ -440,3 +440,83 @@ describe("QueryDb list resolver — SQLite push-down path", () => {
|
|
|
440
440
|
})
|
|
441
441
|
)
|
|
442
442
|
})
|
|
443
|
+
|
|
444
|
+
// The by-index door answers a Relay connection, keyed on the index's column.
|
|
445
|
+
// Both halves used to be wrong here and in the opposite direction on AppSync:
|
|
446
|
+
// this backend promised `[String]` and returned whole rows, so every call failed
|
|
447
|
+
// to serialise before a caller could reach the rows at all.
|
|
448
|
+
describe("QueryDb by-index resolver", () => {
|
|
449
|
+
let statusIndex: array<Reventless.ReadModel.indexConfig> = [
|
|
450
|
+
{index: "status", type_: "S", projectionType: ALL},
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
let indexResolver = async (~name) => {
|
|
454
|
+
let _ = await buildFixture(~name, ~indexes=statusIndex)
|
|
455
|
+
switch DomainGraphQL_Server.getQueryResolver(name ++ "ByStatus") {
|
|
456
|
+
| Some(r) => r
|
|
457
|
+
| None => JsError.throwWithMessage("index resolver not registered: " ++ name ++ "ByStatus")
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
beforeEach(() => {
|
|
462
|
+
DomainGraphQL_Server.reset()
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
testPromise("answers the rows carrying the index value, as edges", async () => {
|
|
466
|
+
let resolver = await indexResolver(~name="IdxA")
|
|
467
|
+
let response = await resolver(JSON.Encode.null, argsOf([("status", strJson("active"))]), emptyCtx)
|
|
468
|
+
let edges = getEdges(response)
|
|
469
|
+
expect(edges->Array.length)->toBe(3)
|
|
470
|
+
expect(edgeNodeField(edges->Array.getUnsafe(0), "productId"))->toBe("p-1")
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
testPromise("pages forward on first/after like every other connection", async () => {
|
|
474
|
+
let resolver = await indexResolver(~name="IdxB")
|
|
475
|
+
let page1 =
|
|
476
|
+
await resolver(
|
|
477
|
+
JSON.Encode.null,
|
|
478
|
+
argsOf([("status", strJson("active")), ("first", numJson(2))]),
|
|
479
|
+
emptyCtx,
|
|
480
|
+
)
|
|
481
|
+
expect(getEdges(page1)->Array.length)->toBe(2)
|
|
482
|
+
expect(pageInfoBool(page1, "hasNextPage"))->toBe(true)
|
|
483
|
+
|
|
484
|
+
let page2 =
|
|
485
|
+
await resolver(
|
|
486
|
+
JSON.Encode.null,
|
|
487
|
+
argsOf([
|
|
488
|
+
("status", strJson("active")),
|
|
489
|
+
("first", numJson(2)),
|
|
490
|
+
("after", strJson(pageInfoString(page1, "endCursor")->Option.getOr(""))),
|
|
491
|
+
]),
|
|
492
|
+
emptyCtx,
|
|
493
|
+
)
|
|
494
|
+
let edges2 = getEdges(page2)
|
|
495
|
+
expect(edges2->Array.length)->toBe(1)
|
|
496
|
+
expect(edgeNodeField(edges2->Array.getUnsafe(0), "productId"))->toBe("p-4")
|
|
497
|
+
expect(pageInfoBool(page2, "hasNextPage"))->toBe(false)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
// Refused rather than ignored, and refused here because AppSync cannot do it:
|
|
501
|
+
// a door that paged backward on one backend and quietly returned the forward
|
|
502
|
+
// page on the other is the divergence this whole field was rebuilt to remove.
|
|
503
|
+
testPromise("refuses backward paging instead of answering forward", async () => {
|
|
504
|
+
let resolver = await indexResolver(~name="IdxC")
|
|
505
|
+
let outcome = try {
|
|
506
|
+
let _ =
|
|
507
|
+
await resolver(
|
|
508
|
+
JSON.Encode.null,
|
|
509
|
+
argsOf([("status", strJson("active")), ("last", numJson(2))]),
|
|
510
|
+
emptyCtx,
|
|
511
|
+
)
|
|
512
|
+
Ok()
|
|
513
|
+
} catch {
|
|
514
|
+
| e => Error(e->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr(""))
|
|
515
|
+
}
|
|
516
|
+
switch outcome {
|
|
517
|
+
| Ok() => expect("no error")->toBe("a BAD_USER_INPUT refusal")
|
|
518
|
+
| Error(message) =>
|
|
519
|
+
expect(message->String.includes("Backward pagination (last/before) is not supported"))->toBe(true)
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
})
|