@reventlessdev/reventless-gwt 1.0.0-alpha.74
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 +698 -0
- package/LICENSE +202 -0
- package/bin/reventless-dev.mjs +21 -0
- package/package.json +54 -0
- package/rescript.json +36 -0
- package/src/.gwtignore +10 -0
- package/src/Automation_GWT.res +207 -0
- package/src/Automation_GWT.res.mjs +160 -0
- package/src/Behavior_GWT.res +603 -0
- package/src/Behavior_GWT.res.mjs +426 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +137 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +31 -0
- package/src/Cancellation.res.mjs +45 -0
- package/src/ChildProcess.res +84 -0
- package/src/ChildProcess.res.mjs +92 -0
- package/src/Cli.res +671 -0
- package/src/Cli.res.mjs +754 -0
- package/src/Collector.res +201 -0
- package/src/Collector.res.mjs +215 -0
- package/src/ComponentMeta.res +104 -0
- package/src/ComponentMeta.res.mjs +120 -0
- package/src/ComponentScan.res +103 -0
- package/src/ComponentScan.res.mjs +105 -0
- package/src/Delegate_GWT.res +303 -0
- package/src/Delegate_GWT.res.mjs +372 -0
- package/src/Diff.res +91 -0
- package/src/Diff.res.mjs +98 -0
- package/src/Discovery.res +105 -0
- package/src/Discovery.res.mjs +103 -0
- package/src/DomainDeadCode.res +93 -0
- package/src/DomainDeadCode.res.mjs +69 -0
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/EventMapping_GWT.res +29 -0
- package/src/EventMapping_GWT.res.mjs +44 -0
- package/src/Filter.res +36 -0
- package/src/Filter.res.mjs +40 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +138 -0
- package/src/FormatterHuman.res.mjs +122 -0
- package/src/FormatterJson.res +257 -0
- package/src/FormatterJson.res.mjs +320 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +139 -0
- package/src/FormatterTap.res.mjs +149 -0
- package/src/FormatterVsCode.res +362 -0
- package/src/FormatterVsCode.res.mjs +564 -0
- package/src/Hint.res +119 -0
- package/src/Hint.res.mjs +124 -0
- package/src/InboundTranslation_GWT.res +83 -0
- package/src/InboundTranslation_GWT.res.mjs +94 -0
- package/src/JestBind.res +70 -0
- package/src/JestBind.res.mjs +63 -0
- package/src/Loader.res +24 -0
- package/src/Loader.res.mjs +23 -0
- package/src/LocalHost.res +135 -0
- package/src/LocalHost.res.mjs +127 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -0
- package/src/MultiSourceProjection_GWT.res +316 -0
- package/src/MultiSourceProjection_GWT.res.mjs +354 -0
- package/src/OutboundTranslation_GWT.res +238 -0
- package/src/OutboundTranslation_GWT.res.mjs +186 -0
- package/src/Outcome.res +161 -0
- package/src/Outcome.res.mjs +140 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +160 -0
- package/src/PlatformRunner.res.mjs +155 -0
- package/src/PlatformScan.res +138 -0
- package/src/PlatformScan.res.mjs +131 -0
- package/src/ProcessManager.res +91 -0
- package/src/ProcessManager.res.mjs +80 -0
- package/src/Projection_GWT.res +312 -0
- package/src/Projection_GWT.res.mjs +332 -0
- package/src/Query_GWT.res +379 -0
- package/src/Query_GWT.res.mjs +367 -0
- package/src/RenderRescript.res +125 -0
- package/src/RenderRescript.res.mjs +141 -0
- package/src/RunnerTypes.res +68 -0
- package/src/RunnerTypes.res.mjs +60 -0
- package/src/SideEffect_GWT.res +157 -0
- package/src/SideEffect_GWT.res.mjs +113 -0
- package/src/StateChange/ExternalAddCategorySlice.res +25 -0
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
- package/src/StateChange/WithFixtures.res +22 -0
- package/src/StateChange/WithFixtures.res.mjs +31 -0
- package/src/StateChange/WithFixtures_Behavior.res +17 -0
- package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
- package/src/StateChange/WithManualOpen.res +23 -0
- package/src/StateChange/WithManualOpen.res.mjs +31 -0
- package/src/StateChange/WithManualOpen_Behavior.res +17 -0
- package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
- package/src/StubRuntime.res +53 -0
- package/src/StubRuntime.res.mjs +54 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +92 -0
- package/src/Watch.res.mjs +83 -0
- package/src/WatcherProbe.res +48 -0
- package/src/WatcherProbe.res.mjs +46 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +94 -0
- package/tests/BuildClassifierTest.res.mjs +132 -0
- package/tests/ComponentMetaTest.res +52 -0
- package/tests/ComponentMetaTest.res.mjs +57 -0
- package/tests/DelegateGwtTest.res +487 -0
- package/tests/DelegateGwtTest.res.mjs +891 -0
- package/tests/DiscoveryTest.res +42 -0
- package/tests/DiscoveryTest.res.mjs +43 -0
- package/tests/DomainDeadCodeTest.res +116 -0
- package/tests/DomainDeadCodeTest.res.mjs +155 -0
- package/tests/DomainGraphTest.res +365 -0
- package/tests/DomainGraphTest.res.mjs +413 -0
- package/tests/FlowAggregateGwtTest.res +169 -0
- package/tests/FlowAggregateGwtTest.res.mjs +269 -0
- package/tests/FlowCrossPluginGwtTest.res +340 -0
- package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
- package/tests/FlowGwtTest.res +222 -0
- package/tests/FlowGwtTest.res.mjs +432 -0
- package/tests/FormatterVsCodeMessageTest.res +41 -0
- package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
- package/tests/InboundTranslationGwtTest.res +34 -0
- package/tests/InboundTranslationGwtTest.res.mjs +98 -0
- package/tests/LocalHostIntegration.res +70 -0
- package/tests/LocalHostIntegration.res.mjs +64 -0
- package/tests/LocalHostTest.res +71 -0
- package/tests/LocalHostTest.res.mjs +63 -0
- package/tests/MappingGwtTest.res +348 -0
- package/tests/MappingGwtTest.res.mjs +592 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +138 -0
- package/tests/PlatformRunnerTest.res +66 -0
- package/tests/PlatformRunnerTest.res.mjs +85 -0
- package/tests/PlatformScanTest.res +61 -0
- package/tests/PlatformScanTest.res.mjs +56 -0
- package/tests/QueryGwtTest.res +161 -0
- package/tests/QueryGwtTest.res.mjs +314 -0
- package/tests/QueryResolverGwtTest.res +71 -0
- package/tests/QueryResolverGwtTest.res.mjs +136 -0
- package/tests/RunnerUnitTest.res +219 -0
- package/tests/RunnerUnitTest.res.mjs +244 -0
- package/tests/SideEffect/FakeOrderNotification.res +23 -0
- package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
- package/tests/SideEffect/MockEmail.res +18 -0
- package/tests/SideEffect/MockEmail.res.mjs +47 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
- package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
- package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithFixtures_GWT.res +23 -0
- package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithManualOpen_GWT.res +17 -0
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
- package/tests/StateChangeSliceGwtTest.res +160 -0
- package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
- package/tests/StateViewSliceGwtTest.res +64 -0
- package/tests/StateViewSliceGwtTest.res.mjs +161 -0
- package/tests/WatchDebounceTest.res +78 -0
- package/tests/WatchDebounceTest.res.mjs +97 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
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 Stream from "effect/Stream";
|
|
8
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
9
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
10
|
+
import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
11
|
+
import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
|
|
12
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
13
|
+
import * as Projection$ReventlessCore from "@reventlessdev/reventless-core/src/Projection.res.mjs";
|
|
14
|
+
import * as TestFixtures$ReventlessGwt from "./TestFixtures.res.mjs";
|
|
15
|
+
|
|
16
|
+
function Make(Spec) {
|
|
17
|
+
return Projection => {
|
|
18
|
+
S.enableJson();
|
|
19
|
+
let testId = {
|
|
20
|
+
contents: TestFixtures$ReventlessGwt.id
|
|
21
|
+
};
|
|
22
|
+
let describeWithId = (description, id, fn) => {
|
|
23
|
+
testId.contents = id;
|
|
24
|
+
JestBind$ReventlessGwt.describe(description, fn);
|
|
25
|
+
};
|
|
26
|
+
let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
|
|
27
|
+
let getSubId = state => Stdlib_Option.map(Spec.subIdConfig, param => param.getSubId(state));
|
|
28
|
+
let hasSubId = (state, subId) => Stdlib_Option.getOrThrow(getSubId(state), undefined) === subId;
|
|
29
|
+
let states = (store, id) => Stdlib_Option.getOr(store[id], []);
|
|
30
|
+
let setStates = (store, id, states) => {
|
|
31
|
+
store[id] = states;
|
|
32
|
+
};
|
|
33
|
+
let updateState = (store, id, subId, newState) => states(store, id).map(state => {
|
|
34
|
+
if (hasSubId(state, subId)) {
|
|
35
|
+
return newState;
|
|
36
|
+
} else {
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
let addState = (store, id, newState) => {
|
|
41
|
+
let subId = getSubId(newState);
|
|
42
|
+
let match = subId !== undefined ? (
|
|
43
|
+
states(store, id).some(state => hasSubId(state, subId)) ? [
|
|
44
|
+
updateState(store, id, subId, newState),
|
|
45
|
+
[]
|
|
46
|
+
] : [
|
|
47
|
+
states(store, id),
|
|
48
|
+
[newState]
|
|
49
|
+
]
|
|
50
|
+
) : [
|
|
51
|
+
states(store, id),
|
|
52
|
+
[newState]
|
|
53
|
+
];
|
|
54
|
+
store[id] = match[0].concat(match[1]);
|
|
55
|
+
};
|
|
56
|
+
let deleteStates = (store, id) => {
|
|
57
|
+
store[id] = [];
|
|
58
|
+
};
|
|
59
|
+
let deleteSubState = (store, id, subId, getSubId) => {
|
|
60
|
+
store[id] = Stdlib_Option.getOr(Stdlib_Option.map(store[id], states => states.filter(state => Primitive_object.notequal(getSubId(state), subId))), []);
|
|
61
|
+
};
|
|
62
|
+
let update = async (store, events) => {
|
|
63
|
+
let actions = events.map(ev => Projection.project(ev)).flat();
|
|
64
|
+
await Projection$ReventlessCore.handleActions(actions, {
|
|
65
|
+
load: extra => Promise.resolve({
|
|
66
|
+
TAG: "Ok",
|
|
67
|
+
_0: states(store, extra)
|
|
68
|
+
}),
|
|
69
|
+
loadStream: id => Stream.fromIterable(states(store, id)),
|
|
70
|
+
save: (extra, extra$1, extra$2, extra$3) => {
|
|
71
|
+
let match = states(store, extra);
|
|
72
|
+
let match$1 = Spec.subIdConfig;
|
|
73
|
+
let exit = 0;
|
|
74
|
+
switch (extra$2) {
|
|
75
|
+
case "Init" :
|
|
76
|
+
if (match$1 !== undefined) {
|
|
77
|
+
addState(store, extra, extra$1);
|
|
78
|
+
return Promise.resolve({
|
|
79
|
+
TAG: "Ok",
|
|
80
|
+
_0: undefined
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
exit = 2;
|
|
84
|
+
break;
|
|
85
|
+
case "Overwrite" :
|
|
86
|
+
exit = 2;
|
|
87
|
+
break;
|
|
88
|
+
case "Any" :
|
|
89
|
+
if (match$1 !== undefined) {
|
|
90
|
+
addState(store, extra, extra$1);
|
|
91
|
+
return Promise.resolve({
|
|
92
|
+
TAG: "Ok",
|
|
93
|
+
_0: undefined
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (exit === 2) {
|
|
99
|
+
let len = match.length;
|
|
100
|
+
if (len !== 1) {
|
|
101
|
+
if (len !== 0) {
|
|
102
|
+
return Promise.resolve({
|
|
103
|
+
TAG: "Error",
|
|
104
|
+
_0: "StaleState"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (extra$2 !== "Init") {
|
|
108
|
+
return Promise.resolve({
|
|
109
|
+
TAG: "Error",
|
|
110
|
+
_0: "StaleState"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} else if (extra$2 === "Init") {
|
|
114
|
+
return Promise.resolve({
|
|
115
|
+
TAG: "Error",
|
|
116
|
+
_0: "StaleState"
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
setStates(store, extra, [extra$1]);
|
|
121
|
+
return Promise.resolve({
|
|
122
|
+
TAG: "Ok",
|
|
123
|
+
_0: undefined
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
saveBatch: extra => {
|
|
127
|
+
extra.forEach(param => addState(store, param[0], param[1]));
|
|
128
|
+
return Promise.resolve({
|
|
129
|
+
TAG: "Ok",
|
|
130
|
+
_0: undefined
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
count: async (param, param$1, param$2) => ({
|
|
134
|
+
TAG: "Ok",
|
|
135
|
+
_0: 0
|
|
136
|
+
}),
|
|
137
|
+
delete: (extra, extra$1) => {
|
|
138
|
+
let match = Spec.subIdConfig;
|
|
139
|
+
if (extra$1 !== undefined) {
|
|
140
|
+
if (match !== undefined) {
|
|
141
|
+
deleteSubState(store, extra, extra$1[1], match.getSubId);
|
|
142
|
+
return Promise.resolve({
|
|
143
|
+
TAG: "Ok",
|
|
144
|
+
_0: undefined
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
return Promise.resolve({
|
|
148
|
+
TAG: "Ok",
|
|
149
|
+
_0: undefined
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
deleteStates(store, extra);
|
|
154
|
+
return Promise.resolve({
|
|
155
|
+
TAG: "Ok",
|
|
156
|
+
_0: undefined
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
deleteBatch: extra => {
|
|
161
|
+
extra.forEach(param => {
|
|
162
|
+
let subId = param[1];
|
|
163
|
+
let id = param[0];
|
|
164
|
+
let match = Spec.subIdConfig;
|
|
165
|
+
if (subId !== undefined) {
|
|
166
|
+
if (match !== undefined) {
|
|
167
|
+
return deleteSubState(store, id, subId[1], match.getSubId);
|
|
168
|
+
} else {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
return deleteStates(store, id);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return Promise.resolve({
|
|
176
|
+
TAG: "Ok",
|
|
177
|
+
_0: undefined
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}, Spec.subIdConfig);
|
|
181
|
+
if (Spec.subIdConfig !== undefined) {
|
|
182
|
+
return Stdlib_Dict.mapValues(store, states => states.toSorted((s1, s2) => Primitive_string.compare(getSubId(s1), getSubId(s2))));
|
|
183
|
+
} else {
|
|
184
|
+
return store;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
let givenEvents = events => update({}, events);
|
|
188
|
+
let whenEvent = (store, event) => (() => (async () => await update(await store, [event]))());
|
|
189
|
+
let whenEvents = (store, events) => (() => (async () => await update(await store, events))());
|
|
190
|
+
let encState = s => Message$ReventlessCore.encode(s, Spec.stateSchema);
|
|
191
|
+
let statesEq = (a, b) => {
|
|
192
|
+
if (a.length === b.length) {
|
|
193
|
+
return Stdlib_Array.zip(a, b).every(param => {
|
|
194
|
+
let a = param[0];
|
|
195
|
+
let b = param[1];
|
|
196
|
+
return JSON.stringify(Message$ReventlessCore.encode(a, Spec.stateSchema)) === JSON.stringify(Message$ReventlessCore.encode(b, Spec.stateSchema));
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
let storeEq = (a, b) => {
|
|
203
|
+
let ka = Object.keys(a).toSorted(Primitive_string.compare);
|
|
204
|
+
let kb = Object.keys(b).toSorted(Primitive_string.compare);
|
|
205
|
+
if (Primitive_object.equal(ka, kb)) {
|
|
206
|
+
return ka.every(key => statesEq(Stdlib_Option.getOr(a[key], []), Stdlib_Option.getOr(b[key], [])));
|
|
207
|
+
} else {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
let encStore = s => {
|
|
212
|
+
let obj = {};
|
|
213
|
+
Object.entries(s).forEach(param => {
|
|
214
|
+
obj[param[0]] = param[1].map(encState);
|
|
215
|
+
});
|
|
216
|
+
return obj;
|
|
217
|
+
};
|
|
218
|
+
let thenStates = async (thunk, expectedStates) => {
|
|
219
|
+
let store = await thunk();
|
|
220
|
+
let keys = Object.keys(store);
|
|
221
|
+
let actualId = keys[0];
|
|
222
|
+
let actualStates = Stdlib_Option.getOr(Object.values(store)[0], []);
|
|
223
|
+
if (keys.length === 1 && Primitive_object.equal(actualId, testId.contents) && statesEq(actualStates, expectedStates)) {
|
|
224
|
+
return Outcome$ReventlessGwt.pass;
|
|
225
|
+
} else {
|
|
226
|
+
return Outcome$ReventlessGwt.fail({
|
|
227
|
+
TAG: "StateMismatch",
|
|
228
|
+
key: testId.contents,
|
|
229
|
+
expected: expectedStates.map(encState),
|
|
230
|
+
actual: encStore(store)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
let thenStatesWithId = async (thunk, id, expectedStates) => {
|
|
235
|
+
let store = await thunk();
|
|
236
|
+
let keys = Object.keys(store);
|
|
237
|
+
let actualId = keys[0];
|
|
238
|
+
let actualStates = Stdlib_Option.getOr(Object.values(store)[0], []);
|
|
239
|
+
if (keys.length === 1 && Primitive_object.equal(actualId, id) && statesEq(actualStates, expectedStates)) {
|
|
240
|
+
return Outcome$ReventlessGwt.pass;
|
|
241
|
+
} else {
|
|
242
|
+
return Outcome$ReventlessGwt.fail({
|
|
243
|
+
TAG: "StateMismatch",
|
|
244
|
+
key: id,
|
|
245
|
+
expected: expectedStates.map(encState),
|
|
246
|
+
actual: encStore(store)
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
let thenAllStates = async (thunk, expectedStore) => {
|
|
251
|
+
let store = await thunk();
|
|
252
|
+
if (storeEq(store, expectedStore)) {
|
|
253
|
+
return Outcome$ReventlessGwt.pass;
|
|
254
|
+
} else {
|
|
255
|
+
return Outcome$ReventlessGwt.fail({
|
|
256
|
+
TAG: "StateMismatch",
|
|
257
|
+
key: "<all>",
|
|
258
|
+
expected: encStore(expectedStore),
|
|
259
|
+
actual: encStore(store)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
let thenState = (thunk, expectedState) => thenStates(thunk, [expectedState]);
|
|
264
|
+
let thenStateWithId = (thunk, id, expectedState) => thenStatesWithId(thunk, id, [expectedState]);
|
|
265
|
+
let thenNoState = async thunk => {
|
|
266
|
+
let store = await thunk();
|
|
267
|
+
let total = Stdlib_Array.reduce(Object.values(store), 0, (acc, states) => acc + states.length | 0);
|
|
268
|
+
if (total === 0) {
|
|
269
|
+
return Outcome$ReventlessGwt.pass;
|
|
270
|
+
} else {
|
|
271
|
+
return Outcome$ReventlessGwt.fail({
|
|
272
|
+
TAG: "StateMismatch",
|
|
273
|
+
key: "<any>",
|
|
274
|
+
expected: undefined,
|
|
275
|
+
actual: encStore(store)
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
let thenThrow = async thunk => {
|
|
280
|
+
let val;
|
|
281
|
+
try {
|
|
282
|
+
val = await thunk();
|
|
283
|
+
} catch (exn) {
|
|
284
|
+
return Outcome$ReventlessGwt.pass;
|
|
285
|
+
}
|
|
286
|
+
return Outcome$ReventlessGwt.fail({
|
|
287
|
+
TAG: "Throw",
|
|
288
|
+
error: "Expected thunk to throw but it returned normally",
|
|
289
|
+
stack: ""
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
let thenFail = async thunk => {
|
|
293
|
+
let val;
|
|
294
|
+
try {
|
|
295
|
+
val = await thunk();
|
|
296
|
+
} catch (exn) {
|
|
297
|
+
return Outcome$ReventlessGwt.pass;
|
|
298
|
+
}
|
|
299
|
+
return Outcome$ReventlessGwt.fail({
|
|
300
|
+
TAG: "Throw",
|
|
301
|
+
error: "Expected failure but thunk returned normally",
|
|
302
|
+
stack: ""
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
Spec: Spec,
|
|
307
|
+
describe: JestBind$ReventlessGwt.describe,
|
|
308
|
+
describeWithId: describeWithId,
|
|
309
|
+
todo: JestBind$ReventlessGwt.todo,
|
|
310
|
+
test: test,
|
|
311
|
+
givenEvents: givenEvents,
|
|
312
|
+
whenEvent: whenEvent,
|
|
313
|
+
whenEvents: whenEvents,
|
|
314
|
+
thenStates: thenStates,
|
|
315
|
+
thenStatesWithId: thenStatesWithId,
|
|
316
|
+
thenAllStates: thenAllStates,
|
|
317
|
+
thenState: thenState,
|
|
318
|
+
thenStateWithId: thenStateWithId,
|
|
319
|
+
thenNoState: thenNoState,
|
|
320
|
+
thenThrow: thenThrow,
|
|
321
|
+
thenFail: thenFail
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let handleActions = Projection$ReventlessCore.handleActions;
|
|
327
|
+
|
|
328
|
+
export {
|
|
329
|
+
handleActions,
|
|
330
|
+
Make,
|
|
331
|
+
}
|
|
332
|
+
/* S Not a pure module */
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// Stage 6 — query-pattern GWT for ReadModels and StateViewSlices.
|
|
2
|
+
//
|
|
3
|
+
// Where `MultiSourceProjection_GWT` (multi-source ReadModel) and `Projection_GWT`
|
|
4
|
+
// (single-source StateViewSlice) cover the projection *write*
|
|
5
|
+
// side (event → state actions), `Query_GWT` covers the read side: what queries
|
|
6
|
+
// must the projected state support? Indexes, sub-IDs, and GraphQL resolvers
|
|
7
|
+
// all exist to answer specific query patterns — none of which are derivable
|
|
8
|
+
// from the projection function alone.
|
|
9
|
+
//
|
|
10
|
+
// The DSL accepts the minimal intersection of `ReadModel.Spec` and
|
|
11
|
+
// `StateViewSlice.Spec` (`name`, `state`, `stateSchema`, `config`,
|
|
12
|
+
// `subIdConfig`), so both kinds of consumers use the same combinator set.
|
|
13
|
+
// Adapter functors `FromReadModel` and `FromStateViewSlice` lift their
|
|
14
|
+
// respective specs into the shared shape — mirroring how `Mapping_GWT` lifts
|
|
15
|
+
// `Aggregate.Spec`/`StateChangeSlice.Spec` via `FromBehavior`/`FromStateChangeSlice`.
|
|
16
|
+
|
|
17
|
+
// -- Unified spec shape ------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
module type QueryableSpec = {
|
|
20
|
+
let name: string
|
|
21
|
+
|
|
22
|
+
@schema
|
|
23
|
+
type state
|
|
24
|
+
let stateSchema: S.t<state>
|
|
25
|
+
|
|
26
|
+
let config: Reventless.ReadModel.config
|
|
27
|
+
let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module FromReadModel = (Spec: Reventless.ReadModel.Spec) => {
|
|
31
|
+
let name = Spec.name
|
|
32
|
+
type state = Spec.state
|
|
33
|
+
let stateSchema = Spec.stateSchema
|
|
34
|
+
let config = Spec.config
|
|
35
|
+
let subIdConfig = Spec.subIdConfig
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module FromStateViewSlice = (Spec: Reventless.StateViewSlice.Spec) => {
|
|
39
|
+
let name = Spec.name
|
|
40
|
+
type state = Spec.state
|
|
41
|
+
let stateSchema = Spec.stateSchema
|
|
42
|
+
let config = Spec.config
|
|
43
|
+
let subIdConfig = Spec.subIdConfig
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// -- Query argument shapes ---------------------------------------------------
|
|
47
|
+
|
|
48
|
+
type compositeId = {id: string, subId: string}
|
|
49
|
+
|
|
50
|
+
// A `whenQuery` scenario — `by` names the state field being filtered, `value`
|
|
51
|
+
// the expected value. `index` names a GSI when the field isn't the primary
|
|
52
|
+
// key. `filter` lets tests constrain the result set after the index lookup;
|
|
53
|
+
// `limit` truncates (used with `thenRowCount` for pagination).
|
|
54
|
+
type queryArgs<'state> = {
|
|
55
|
+
by: string,
|
|
56
|
+
value: string,
|
|
57
|
+
index?: string,
|
|
58
|
+
filter?: 'state => bool,
|
|
59
|
+
limit?: int,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// -- Query_GWT DSL ----------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
module type T = {
|
|
65
|
+
module Spec: QueryableSpec
|
|
66
|
+
|
|
67
|
+
let describe: (string, unit => unit) => unit
|
|
68
|
+
let test: (string, unit => Outcome.outcome) => unit
|
|
69
|
+
|
|
70
|
+
type store
|
|
71
|
+
let givenStore: array<(string, Spec.state)> => store
|
|
72
|
+
let givenCompositeStore: array<(string, string, Spec.state)> => store
|
|
73
|
+
|
|
74
|
+
let whenQueryById: (store, string) => option<Spec.state>
|
|
75
|
+
let whenQueryByCompositeId: (store, compositeId) => result<option<Spec.state>, Outcome.mismatch>
|
|
76
|
+
let whenQuery: (store, queryArgs<Spec.state>) => result<array<Spec.state>, Outcome.mismatch>
|
|
77
|
+
|
|
78
|
+
let thenRow: (option<Spec.state>, option<Spec.state>) => Outcome.outcome
|
|
79
|
+
let thenRows: (result<array<Spec.state>, Outcome.mismatch>, array<Spec.state>) => Outcome.outcome
|
|
80
|
+
let thenRowCount: (result<array<Spec.state>, Outcome.mismatch>, int) => Outcome.outcome
|
|
81
|
+
let thenRowFromComposite: (
|
|
82
|
+
result<option<Spec.state>, Outcome.mismatch>,
|
|
83
|
+
option<Spec.state>,
|
|
84
|
+
) => Outcome.outcome
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module Make = (Spec: QueryableSpec): (T with module Spec = Spec) => {
|
|
88
|
+
module Spec = Spec
|
|
89
|
+
|
|
90
|
+
S.enableJson()
|
|
91
|
+
|
|
92
|
+
let describe = JestBind.describe
|
|
93
|
+
let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
|
|
94
|
+
|
|
95
|
+
type row = {id: string, subId: option<string>, state: Spec.state}
|
|
96
|
+
type store = array<row>
|
|
97
|
+
|
|
98
|
+
let encState = (s: Spec.state): JSON.t => s->S.reverseConvertToJsonOrThrow(Spec.stateSchema)
|
|
99
|
+
let encStates = arr => arr->Array.map(encState)
|
|
100
|
+
|
|
101
|
+
let givenStore = pairs =>
|
|
102
|
+
pairs->Array.map(((id, state)) => {id, subId: None, state})
|
|
103
|
+
|
|
104
|
+
let givenCompositeStore = triples =>
|
|
105
|
+
triples->Array.map(((id, subId, state)) => {id, subId: Some(subId), state})
|
|
106
|
+
|
|
107
|
+
// `whenQueryById` ignores `subId` — returns the first row with the given id.
|
|
108
|
+
// Callers who want composite-id lookup go through `whenQueryByCompositeId`.
|
|
109
|
+
let whenQueryById = (s: store, id) =>
|
|
110
|
+
s->Array.findMap(r => if r.id == id {Some(r.state)} else {None})
|
|
111
|
+
|
|
112
|
+
let whenQueryByCompositeId = (s: store, {id, subId}: compositeId): result<
|
|
113
|
+
option<Spec.state>,
|
|
114
|
+
Outcome.mismatch,
|
|
115
|
+
> =>
|
|
116
|
+
switch Spec.subIdConfig {
|
|
117
|
+
| None =>
|
|
118
|
+
Error(
|
|
119
|
+
Outcome.QueryRowsMismatch({
|
|
120
|
+
expected: [JSON.Encode.string(
|
|
121
|
+
`composite-id lookup requires subIdConfig = Some({subIdField, getSubId}) in ${Spec.name}.config`,
|
|
122
|
+
)],
|
|
123
|
+
actual: [],
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
| Some(_) =>
|
|
127
|
+
Ok(
|
|
128
|
+
s->Array.findMap(r =>
|
|
129
|
+
if r.id == id && r.subId == Some(subId) {
|
|
130
|
+
Some(r.state)
|
|
131
|
+
} else {
|
|
132
|
+
None
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Does the schema declare an index with this name whose pk covers `field`?
|
|
139
|
+
let indexCovers = (indexName: string, field: string): bool =>
|
|
140
|
+
Spec.config.indexes->Array.some(idx =>
|
|
141
|
+
idx.index == indexName &&
|
|
142
|
+
(idx.idField == Some(field) ||
|
|
143
|
+
switch idx.pkFields {
|
|
144
|
+
| Some(fields) => fields->Array.includes(field)
|
|
145
|
+
| None => false
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
let whenQuery = (s: store, args: queryArgs<Spec.state>): result<
|
|
150
|
+
array<Spec.state>,
|
|
151
|
+
Outcome.mismatch,
|
|
152
|
+
> => {
|
|
153
|
+
// Config validation: if the caller named an `index`, verify the schema
|
|
154
|
+
// actually declares it. Without this, the test could "pass" against an
|
|
155
|
+
// in-memory store while the production config would reject the query.
|
|
156
|
+
let indexError = switch args.index {
|
|
157
|
+
| Some(name) if !indexCovers(name, args.by) =>
|
|
158
|
+
Some(
|
|
159
|
+
Outcome.QueryRowsMismatch({
|
|
160
|
+
expected: [
|
|
161
|
+
JSON.Encode.string(
|
|
162
|
+
`index "${name}" covering field "${args.by}" is missing from ${Spec.name}.config.indexes`,
|
|
163
|
+
),
|
|
164
|
+
],
|
|
165
|
+
actual: [],
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
| _ => None
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
switch indexError {
|
|
172
|
+
| Some(err) => Error(err)
|
|
173
|
+
| None =>
|
|
174
|
+
let field = args.by
|
|
175
|
+
let expected = args.value
|
|
176
|
+
let matches = s->Array.filter(r => {
|
|
177
|
+
let json = r.state->S.reverseConvertToJsonOrThrow(Spec.stateSchema)
|
|
178
|
+
switch json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field)) {
|
|
179
|
+
| Some(v) =>
|
|
180
|
+
let actualStr = switch v {
|
|
181
|
+
| JSON.String(s) => s
|
|
182
|
+
| JSON.Number(n) => n->Float.toString
|
|
183
|
+
| JSON.Boolean(b) => b ? "true" : "false"
|
|
184
|
+
| _ => v->JSON.stringify
|
|
185
|
+
}
|
|
186
|
+
actualStr == expected
|
|
187
|
+
| None => false
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
let afterFilter = switch args.filter {
|
|
191
|
+
| Some(p) => matches->Array.filter(r => p(r.state))
|
|
192
|
+
| None => matches
|
|
193
|
+
}
|
|
194
|
+
let afterLimit = switch args.limit {
|
|
195
|
+
| Some(n) => afterFilter->Array.slice(~start=0, ~end=n)
|
|
196
|
+
| None => afterFilter
|
|
197
|
+
}
|
|
198
|
+
Ok(afterLimit->Array.map(r => r.state))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// -- Then combinators ----------------------------------------------------
|
|
203
|
+
|
|
204
|
+
let thenRow = (actual: option<Spec.state>, expected: option<Spec.state>): Outcome.outcome =>
|
|
205
|
+
if actual == expected {
|
|
206
|
+
Outcome.pass
|
|
207
|
+
} else {
|
|
208
|
+
let actualJson = actual->Option.mapOr([], s => [encState(s)])
|
|
209
|
+
let expectedJson = expected->Option.mapOr([], s => [encState(s)])
|
|
210
|
+
Outcome.fail(QueryRowsMismatch({expected: expectedJson, actual: actualJson}))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let thenRows = (
|
|
214
|
+
result: result<array<Spec.state>, Outcome.mismatch>,
|
|
215
|
+
expected: array<Spec.state>,
|
|
216
|
+
): Outcome.outcome =>
|
|
217
|
+
switch result {
|
|
218
|
+
| Error(m) => Outcome.fail(m)
|
|
219
|
+
| Ok(actual) =>
|
|
220
|
+
if actual == expected {
|
|
221
|
+
Outcome.pass
|
|
222
|
+
} else {
|
|
223
|
+
Outcome.fail(
|
|
224
|
+
QueryRowsMismatch({expected: encStates(expected), actual: encStates(actual)}),
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let thenRowCount = (
|
|
230
|
+
result: result<array<Spec.state>, Outcome.mismatch>,
|
|
231
|
+
expected: int,
|
|
232
|
+
): Outcome.outcome =>
|
|
233
|
+
switch result {
|
|
234
|
+
| Error(m) => Outcome.fail(m)
|
|
235
|
+
| Ok(actual) =>
|
|
236
|
+
if actual->Array.length == expected {
|
|
237
|
+
Outcome.pass
|
|
238
|
+
} else {
|
|
239
|
+
Outcome.fail(
|
|
240
|
+
QueryRowsMismatch({
|
|
241
|
+
expected: [JSON.Encode.string(`row count = ${expected->Int.toString}`)],
|
|
242
|
+
actual: [JSON.Encode.string(`row count = ${actual->Array.length->Int.toString}`)],
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let thenRowFromComposite = (
|
|
249
|
+
result: result<option<Spec.state>, Outcome.mismatch>,
|
|
250
|
+
expected: option<Spec.state>,
|
|
251
|
+
): Outcome.outcome =>
|
|
252
|
+
switch result {
|
|
253
|
+
| Error(m) => Outcome.fail(m)
|
|
254
|
+
| Ok(actual) => thenRow(actual, expected)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// -- Cross-spec resolvers ----------------------------------------------------
|
|
259
|
+
//
|
|
260
|
+
// `@resolves` / `@resolvesMany` declare GraphQL resolvers that join a row in one
|
|
261
|
+
// read model to row(s) in a foreign table by a `*Id` / `*Ids` field. The
|
|
262
|
+
// single-spec `Make` above can't exercise these — the join target lives in a
|
|
263
|
+
// different spec. `MakeResolver(Primary, Target)` lifts both into one scenario:
|
|
264
|
+
// a primary store with the foreign-key field, a target store keyed by id, and
|
|
265
|
+
// `whenResolve` / `whenResolveMany` that follow the key across the boundary.
|
|
266
|
+
//
|
|
267
|
+
// The DSL validates that `Primary.config` actually declares the resolver for the
|
|
268
|
+
// field — so a test fails if the `@resolves` annotation is missing, exactly as
|
|
269
|
+
// `whenQuery` validates a named index.
|
|
270
|
+
module MakeResolver = (Primary: QueryableSpec, Target: QueryableSpec) => {
|
|
271
|
+
S.enableJson()
|
|
272
|
+
|
|
273
|
+
let describe = JestBind.describe
|
|
274
|
+
let sliceName = `${Primary.name}→${Target.name}`
|
|
275
|
+
let test = (name, body) => JestBind.test(~slice=sliceName, name, body)
|
|
276
|
+
|
|
277
|
+
type scenario = {
|
|
278
|
+
primary: array<(string, Primary.state)>,
|
|
279
|
+
target: array<(string, Target.state)>,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let givenStores = (primary, target): scenario => {primary, target}
|
|
283
|
+
|
|
284
|
+
let encTarget = (s: Target.state): JSON.t => s->S.reverseConvertToJsonOrThrow(Target.stateSchema)
|
|
285
|
+
let encTargets = arr => arr->Array.map(encTarget)
|
|
286
|
+
|
|
287
|
+
let resolverFor = field =>
|
|
288
|
+
Primary.config.idResolvers->Array.find(r => r.source.idField == field)
|
|
289
|
+
let resolverManyFor = field =>
|
|
290
|
+
Primary.config.idsResolvers->Array.find(r => r.source.idsField == field)
|
|
291
|
+
|
|
292
|
+
let missing = msg =>
|
|
293
|
+
Outcome.QueryRowsMismatch({expected: [JSON.Encode.string(msg)], actual: []})
|
|
294
|
+
|
|
295
|
+
// Read a JSON field off the primary row's encoded state.
|
|
296
|
+
let readField = (state: Primary.state, field): option<JSON.t> =>
|
|
297
|
+
state
|
|
298
|
+
->S.reverseConvertToJsonOrThrow(Primary.stateSchema)
|
|
299
|
+
->JSON.Decode.object
|
|
300
|
+
->Option.flatMap(d => d->Dict.get(field))
|
|
301
|
+
|
|
302
|
+
let jsonToStr = v =>
|
|
303
|
+
switch v {
|
|
304
|
+
| JSON.String(s) => s
|
|
305
|
+
| JSON.Number(n) => n->Float.toString
|
|
306
|
+
| _ => v->JSON.stringify
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let lookupTarget = (target, fkId) =>
|
|
310
|
+
target->Array.findMap(((id, st)) => id == fkId ? Some(st) : None)
|
|
311
|
+
|
|
312
|
+
let whenResolve = (scenario: scenario, ~field, primaryId): result<
|
|
313
|
+
option<Target.state>,
|
|
314
|
+
Outcome.mismatch,
|
|
315
|
+
> =>
|
|
316
|
+
switch resolverFor(field) {
|
|
317
|
+
| None =>
|
|
318
|
+
Error(missing(`@resolves on field "${field}" is missing from ${Primary.name}.config.idResolvers`))
|
|
319
|
+
| Some(_) =>
|
|
320
|
+
switch scenario.primary->Array.findMap(((id, st)) => id == primaryId ? Some(st) : None) {
|
|
321
|
+
| None => Ok(None)
|
|
322
|
+
| Some(pstate) =>
|
|
323
|
+
switch readField(pstate, field) {
|
|
324
|
+
| None => Ok(None)
|
|
325
|
+
| Some(fk) => Ok(lookupTarget(scenario.target, jsonToStr(fk)))
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let whenResolveMany = (scenario: scenario, ~field, primaryId): result<
|
|
331
|
+
array<Target.state>,
|
|
332
|
+
Outcome.mismatch,
|
|
333
|
+
> =>
|
|
334
|
+
switch resolverManyFor(field) {
|
|
335
|
+
| None =>
|
|
336
|
+
Error(
|
|
337
|
+
missing(`@resolvesMany on field "${field}" is missing from ${Primary.name}.config.idsResolvers`),
|
|
338
|
+
)
|
|
339
|
+
| Some(_) =>
|
|
340
|
+
switch scenario.primary->Array.findMap(((id, st)) => id == primaryId ? Some(st) : None) {
|
|
341
|
+
| None => Ok([])
|
|
342
|
+
| Some(pstate) =>
|
|
343
|
+
switch readField(pstate, field) {
|
|
344
|
+
| Some(JSON.Array(arr)) =>
|
|
345
|
+
Ok(arr->Array.map(jsonToStr)->Array.filterMap(fkId => lookupTarget(scenario.target, fkId)))
|
|
346
|
+
| _ => Ok([])
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let thenResolved = (
|
|
352
|
+
result: result<option<Target.state>, Outcome.mismatch>,
|
|
353
|
+
expected: option<Target.state>,
|
|
354
|
+
): Outcome.outcome =>
|
|
355
|
+
switch result {
|
|
356
|
+
| Error(m) => Outcome.fail(m)
|
|
357
|
+
| Ok(actual) =>
|
|
358
|
+
actual == expected
|
|
359
|
+
? Outcome.pass
|
|
360
|
+
: Outcome.fail(
|
|
361
|
+
QueryRowsMismatch({
|
|
362
|
+
expected: expected->Option.mapOr([], s => [encTarget(s)]),
|
|
363
|
+
actual: actual->Option.mapOr([], s => [encTarget(s)]),
|
|
364
|
+
}),
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let thenResolvedMany = (
|
|
369
|
+
result: result<array<Target.state>, Outcome.mismatch>,
|
|
370
|
+
expected: array<Target.state>,
|
|
371
|
+
): Outcome.outcome =>
|
|
372
|
+
switch result {
|
|
373
|
+
| Error(m) => Outcome.fail(m)
|
|
374
|
+
| Ok(actual) =>
|
|
375
|
+
actual == expected
|
|
376
|
+
? Outcome.pass
|
|
377
|
+
: Outcome.fail(QueryRowsMismatch({expected: encTargets(expected), actual: encTargets(actual)}))
|
|
378
|
+
}
|
|
379
|
+
}
|