@reventlessdev/reventless-local 3.0.0-alpha.210 → 3.0.0-alpha.212
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 +18 -0
- package/package.json +11 -11
- package/src/adapter/DomainGraphQL_Server.res +6 -18
- package/src/adapter/DomainGraphQL_Server.res.mjs +5 -22
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +49 -19
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +41 -13
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +47 -6
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +29 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.212 (2026-08-11)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **api:** one id contract across the read-side query doors ([c52fac2](https://github.com/ReventlessDev/reventless-core/commit/c52fac2bee87b3dd1fa33b4970a74f3cd3866280))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **api:** infer a queryable's key field and publish its provenance ([c835a42](https://github.com/ReventlessDev/reventless-core/commit/c835a42a0da07cdc4a3f010212e1f340a4a0ca27))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 3.0.0-alpha.211 (2026-08-10)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
# 3.0.0-alpha.210 (2026-08-10)
|
|
7
25
|
|
|
8
26
|
**Note:** Version bump only for package @reventlessdev/reventless-local
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.212",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
"sury": "11.0.0-alpha.4",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
38
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.
|
|
39
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
38
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
|
|
40
39
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
|
|
41
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.
|
|
40
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.4",
|
|
41
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.224",
|
|
44
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.72",
|
|
45
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.171",
|
|
46
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.88",
|
|
47
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.134",
|
|
48
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.11",
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.108"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.0",
|
|
@@ -257,26 +257,14 @@ type resolverFn = YG.resolverFn
|
|
|
257
257
|
|
|
258
258
|
// -- Relay Global ID encoding/decoding -------------------------------------
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
// The encoding itself lives in `ReventlessCore.Api_Ids` — one definition shared
|
|
261
|
+
// with the AWS adapter and with the doors that accept either id form. Kept as
|
|
262
|
+
// named bindings here because the resolver wiring below and the `relay` record
|
|
263
|
+
// passed to QueryDbResolvers_GraphQL both reference them.
|
|
263
264
|
let encodeGlobalId = (~typeName: string, ~localId: string): string =>
|
|
264
|
-
|
|
265
|
+
ReventlessCore.Api_Ids.encode(~typeName, ~localId)
|
|
265
266
|
|
|
266
|
-
let decodeGlobalId =
|
|
267
|
-
try {
|
|
268
|
-
let decoded = atob(globalId)
|
|
269
|
-
let idx = decoded->String.indexOf(":")
|
|
270
|
-
if idx > 0 {
|
|
271
|
-
let typeName = decoded->String.slice(~start=0, ~end=idx)
|
|
272
|
-
let localId = decoded->String.slice(~start=idx + 1, ~end=decoded->String.length)
|
|
273
|
-
Some((typeName, localId))
|
|
274
|
-
} else {
|
|
275
|
-
None
|
|
276
|
-
}
|
|
277
|
-
} catch {
|
|
278
|
-
| _ => None
|
|
279
|
-
}
|
|
267
|
+
let decodeGlobalId = ReventlessCore.Api_Ids.decode
|
|
280
268
|
|
|
281
269
|
// -- Scoped registry ---------------------------------------------------------
|
|
282
270
|
// Mirrors the AWS plugin=source-API, platform=merged-API model: every
|
|
@@ -15,6 +15,7 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
|
|
|
15
15
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
16
16
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
17
17
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
18
|
+
import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
|
|
18
19
|
import * as LocalAuth$ReventlessLocal from "./Auth/LocalAuth.res.mjs";
|
|
19
20
|
import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
20
21
|
import * as LocalObjectStore$ReventlessLocal from "./ObjectStore/LocalObjectStore.res.mjs";
|
|
@@ -193,27 +194,7 @@ function _dispatch(req, res, yoga, getSdl) {
|
|
|
193
194
|
});
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
|
|
197
|
-
return btoa(typeName + `:` + localId);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function decodeGlobalId(globalId) {
|
|
201
|
-
try {
|
|
202
|
-
let decoded = atob(globalId);
|
|
203
|
-
let idx = decoded.indexOf(":");
|
|
204
|
-
if (idx <= 0) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
let typeName = decoded.slice(0, idx);
|
|
208
|
-
let localId = decoded.slice(idx + 1 | 0, decoded.length);
|
|
209
|
-
return [
|
|
210
|
-
typeName,
|
|
211
|
-
localId
|
|
212
|
-
];
|
|
213
|
-
} catch (exn) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
197
|
+
let encodeGlobalId = Api_Ids$ReventlessCore.encode;
|
|
217
198
|
|
|
218
199
|
let platformScope = "platform";
|
|
219
200
|
|
|
@@ -368,7 +349,7 @@ function buildNodeResolver() {
|
|
|
368
349
|
}
|
|
369
350
|
let resolver = async (_root, args, _ctx) => {
|
|
370
351
|
let id = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d["id"]), Stdlib_JSON.Decode.string), "");
|
|
371
|
-
let match =
|
|
352
|
+
let match = Api_Ids$ReventlessCore.decode(id);
|
|
372
353
|
if (match === undefined) {
|
|
373
354
|
return null;
|
|
374
355
|
}
|
|
@@ -738,6 +719,8 @@ let YG;
|
|
|
738
719
|
|
|
739
720
|
let buildAuthContext = Auth_GraphqlContext$ReventlessLocal.buildAuthContext;
|
|
740
721
|
|
|
722
|
+
let decodeGlobalId = Api_Ids$ReventlessCore.decode;
|
|
723
|
+
|
|
741
724
|
export {
|
|
742
725
|
YG,
|
|
743
726
|
log,
|
|
@@ -85,6 +85,13 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
85
85
|
|
|
86
86
|
// Register the Relay node resolver callback once per QueryDb (domain only).
|
|
87
87
|
// Scans all QueryDb instances to resolve node(id: ID!) queries.
|
|
88
|
+
// `node` is the one door that speaks Relay global ids, on both sides: it needs
|
|
89
|
+
// the `<Type>:` prefix to know which read model to load, so a storage key alone
|
|
90
|
+
// could not be resolved here. Every other door — the list, `X(id:)`,
|
|
91
|
+
// `XsByIds`, `filter.ids` — reports the storage key, which is what a client
|
|
92
|
+
// reads off a row and passes back. Callers that hold a global id are still
|
|
93
|
+
// served by the typed doors (see `Api_Ids.alternateKey`); the reverse is not
|
|
94
|
+
// possible, which is why this door is opt-in rather than the default form.
|
|
88
95
|
switch relay {
|
|
89
96
|
| Some(r) =>
|
|
90
97
|
r.registerNodeResolverCallback(async (~typeName, ~localId) => {
|
|
@@ -101,7 +108,10 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
101
108
|
->Effect.runPromise
|
|
102
109
|
switch items->Array.get(0) {
|
|
103
110
|
| Some(item) =>
|
|
104
|
-
|
|
111
|
+
// Copied: `JSON.Decode.object` hands back the stored object itself, so
|
|
112
|
+
// setting a field here would rewrite the row inside the QueryDb — the
|
|
113
|
+
// in-memory backend keeps the very object it returns.
|
|
114
|
+
let obj = item->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
|
|
105
115
|
obj->Dict.set("__typename", JSON.Encode.string(typeName))
|
|
106
116
|
obj->Dict.set("id", r.encodeGlobalId(~typeName, ~localId)->JSON.Encode.string)
|
|
107
117
|
Some(JSON.Encode.object(obj))
|
|
@@ -169,11 +179,6 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
169
179
|
}
|
|
170
180
|
}
|
|
171
181
|
|
|
172
|
-
let encodeId = switch relay {
|
|
173
|
-
| Some(r) => (~typeName, ~localId) => r.encodeGlobalId(~typeName, ~localId)
|
|
174
|
-
| None => (~typeName as _, ~localId) => localId
|
|
175
|
-
}
|
|
176
|
-
|
|
177
182
|
// -- Main query: getById ---------------------------------------------------
|
|
178
183
|
let byIdSdl = if includeIdParam {
|
|
179
184
|
switch subIdField {
|
|
@@ -191,16 +196,34 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
191
196
|
args->JSON.Decode.object->Option.flatMap(d => d->Dict.get("id"))->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
192
197
|
switch Bus.getQueryDb(name) {
|
|
193
198
|
| Some(ops) =>
|
|
194
|
-
let
|
|
195
|
-
|
|
199
|
+
let load = key =>
|
|
200
|
+
ops.loadStream(key)
|
|
196
201
|
->Stream.runCollect
|
|
197
202
|
->Effect.catchAll(_ => Effect.succeed([]))
|
|
198
203
|
->Effect.runPromise
|
|
204
|
+
let firstAttempt = await load(id)
|
|
205
|
+
// The row advertises a Relay global id, so `X(id: row.id)` arrives here
|
|
206
|
+
// as one. Retried rather than decoded up front: the raw key is what this
|
|
207
|
+
// door has always taken, and a key that merely looks like base64 must
|
|
208
|
+
// keep resolving to its own row.
|
|
209
|
+
let (resolvedKey, items) = switch (
|
|
210
|
+
firstAttempt->Array.get(0),
|
|
211
|
+
Api_Ids.alternateKey(id),
|
|
212
|
+
) {
|
|
213
|
+
| (None, Some(localId)) => (localId, await load(localId))
|
|
214
|
+
| _ => (id, firstAttempt)
|
|
215
|
+
}
|
|
199
216
|
switch items->Array.get(0) {
|
|
200
217
|
| Some(item) =>
|
|
201
218
|
if includeIdParam {
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
// Copied — `JSON.Decode.object` hands back the stored object itself,
|
|
220
|
+
// so setting a field here would rewrite the row inside the QueryDb.
|
|
221
|
+
let obj = item->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
|
|
222
|
+
// The storage key, which is what the list answers and what this door
|
|
223
|
+
// takes back. `resolvedKey`, not the argument: a caller who passed a
|
|
224
|
+
// Relay global id gets the raw key returned, so a round trip through
|
|
225
|
+
// this door converges on the one form instead of alternating.
|
|
226
|
+
obj->Dict.set("id", JSON.Encode.string(resolvedKey))
|
|
204
227
|
JSON.Encode.object(obj)
|
|
205
228
|
} else {
|
|
206
229
|
item
|
|
@@ -237,21 +260,28 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
237
260
|
->Array.filterMap(JSON.Decode.string)
|
|
238
261
|
switch Bus.getQueryDb(name) {
|
|
239
262
|
| Some(ops) =>
|
|
240
|
-
let
|
|
241
|
-
ops.loadStream(
|
|
263
|
+
let load = key =>
|
|
264
|
+
ops.loadStream(key)
|
|
242
265
|
->Stream.runCollect
|
|
243
266
|
->Effect.catchAll(_ => Effect.succeed([]))
|
|
244
267
|
->Effect.runPromise
|
|
245
|
-
|
|
268
|
+
// Same either-form rule as the single-id door: raw key first, the
|
|
269
|
+
// key inside a Relay global id only on a miss.
|
|
270
|
+
let loaded = await ids->Array.map(async id =>
|
|
271
|
+
switch (await load(id))->Array.get(0) {
|
|
272
|
+
| Some(item) => (id, Some(item))
|
|
273
|
+
| None =>
|
|
274
|
+
switch Api_Ids.alternateKey(id) {
|
|
275
|
+
| Some(localId) => (localId, (await load(localId))->Array.get(0))
|
|
276
|
+
| None => (id, None)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
246
279
|
)->Promise.all
|
|
247
280
|
loaded
|
|
248
281
|
->Array.filterMap(((id, opt)) =>
|
|
249
282
|
opt->Option.map(item => {
|
|
250
|
-
let obj = item->JSON.Decode.object->Option.
|
|
251
|
-
obj->Dict.set(
|
|
252
|
-
"id",
|
|
253
|
-
encodeId(~typeName=returnTypeName, ~localId=id)->JSON.Encode.string,
|
|
254
|
-
)
|
|
283
|
+
let obj = item->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
|
|
284
|
+
obj->Dict.set("id", JSON.Encode.string(id))
|
|
255
285
|
JSON.Encode.object(obj)
|
|
256
286
|
})
|
|
257
287
|
)
|
|
@@ -279,7 +309,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
279
309
|
// so the resolver derives the same serverCapability the FragmentGenerator emitted.
|
|
280
310
|
let stateSchemaOpt = Plugin_Helpers.stateSchemaRegistry->Dict.get(name)
|
|
281
311
|
let capability = switch stateSchemaOpt {
|
|
282
|
-
| Some(s) => GraphQL_FragmentGenerator.deriveServerCapability(s)
|
|
312
|
+
| Some(s) => GraphQL_FragmentGenerator.deriveServerCapability(~entityName=name, s)
|
|
283
313
|
| None => GraphQL_FragmentGenerator.emptyCapability
|
|
284
314
|
}
|
|
285
315
|
|
|
@@ -7,6 +7,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
|
|
9
9
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
10
|
+
import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
|
|
10
11
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
11
12
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
12
13
|
import * as SortKey_Filter$ReventlessLocal from "./SortKey_Filter.res.mjs";
|
|
@@ -63,7 +64,7 @@ function Make(Bus) {
|
|
|
63
64
|
if (item === undefined) {
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
let obj = Stdlib_Option.
|
|
67
|
+
let obj = Stdlib_Option.mapOr(Stdlib_JSON.Decode.object(item), {}, prim => Object.assign({}, prim));
|
|
67
68
|
obj["__typename"] = typeName;
|
|
68
69
|
obj["id"] = relay.encodeGlobalId(typeName, localId);
|
|
69
70
|
return obj;
|
|
@@ -95,7 +96,6 @@ function Make(Bus) {
|
|
|
95
96
|
if (includeIdParam && relay !== undefined) {
|
|
96
97
|
relay.registerNodeType(returnTypeName, name);
|
|
97
98
|
}
|
|
98
|
-
let encodeId = relay !== undefined ? (typeName, localId) => relay.encodeGlobalId(typeName, localId) : (param, localId) => localId;
|
|
99
99
|
let byIdSdl = includeIdParam ? (
|
|
100
100
|
subIdField !== undefined ? ` ` + singleQueryName + `(id: ID!, ` + subIdField + `: String): ` + returnTypeName : ` ` + singleQueryName + `(id: ID!): ` + returnTypeName
|
|
101
101
|
) : ` ` + singleQueryName + `: ` + returnTypeName;
|
|
@@ -109,16 +109,26 @@ function Make(Bus) {
|
|
|
109
109
|
if (ops === undefined) {
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
|
-
let
|
|
113
|
-
let
|
|
112
|
+
let load = key => Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream(key)), param => Effect.succeed([])));
|
|
113
|
+
let firstAttempt = await load(id);
|
|
114
|
+
let match$1 = firstAttempt[0];
|
|
115
|
+
let match$2 = Api_Ids$ReventlessCore.alternateKey(id);
|
|
116
|
+
let match$3 = match$1 !== undefined || match$2 === undefined ? [
|
|
117
|
+
id,
|
|
118
|
+
firstAttempt
|
|
119
|
+
] : [
|
|
120
|
+
match$2,
|
|
121
|
+
await load(match$2)
|
|
122
|
+
];
|
|
123
|
+
let item = match$3[1][0];
|
|
114
124
|
if (item === undefined) {
|
|
115
125
|
return null;
|
|
116
126
|
}
|
|
117
127
|
if (!includeIdParam) {
|
|
118
128
|
return item;
|
|
119
129
|
}
|
|
120
|
-
let obj = Stdlib_Option.
|
|
121
|
-
obj["id"] =
|
|
130
|
+
let obj = Stdlib_Option.mapOr(Stdlib_JSON.Decode.object(item), {}, prim => Object.assign({}, prim));
|
|
131
|
+
obj["id"] = match$3[0];
|
|
122
132
|
return obj;
|
|
123
133
|
};
|
|
124
134
|
let byIdsSdl = includeIdParam && subIdField === undefined ? [GraphQL_FragmentGenerator$ReventlessCore.deriveByIdsQueryField(listQueryName, returnTypeName)] : [];
|
|
@@ -134,15 +144,33 @@ function Make(Bus) {
|
|
|
134
144
|
if (ops === undefined) {
|
|
135
145
|
return [];
|
|
136
146
|
}
|
|
137
|
-
let
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
let load = key => Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream(key)), param => Effect.succeed([])));
|
|
148
|
+
let loaded = await Promise.all(ids.map(async id => {
|
|
149
|
+
let item = (await load(id))[0];
|
|
150
|
+
if (item !== undefined) {
|
|
151
|
+
return [
|
|
152
|
+
id,
|
|
153
|
+
item
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
let localId = Api_Ids$ReventlessCore.alternateKey(id);
|
|
157
|
+
if (localId !== undefined) {
|
|
158
|
+
return [
|
|
159
|
+
localId,
|
|
160
|
+
(await load(localId))[0]
|
|
161
|
+
];
|
|
162
|
+
} else {
|
|
163
|
+
return [
|
|
164
|
+
id,
|
|
165
|
+
undefined
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
}));
|
|
141
169
|
return Stdlib_Array.filterMap(loaded, param => {
|
|
142
170
|
let id = param[0];
|
|
143
171
|
return Stdlib_Option.map(param[1], item => {
|
|
144
|
-
let obj = Stdlib_Option.
|
|
145
|
-
obj["id"] =
|
|
172
|
+
let obj = Stdlib_Option.mapOr(Stdlib_JSON.Decode.object(item), {}, prim => Object.assign({}, prim));
|
|
173
|
+
obj["id"] = id;
|
|
146
174
|
return obj;
|
|
147
175
|
});
|
|
148
176
|
});
|
|
@@ -156,7 +184,7 @@ function Make(Bus) {
|
|
|
156
184
|
}
|
|
157
185
|
let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
|
|
158
186
|
let stateSchemaOpt = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name];
|
|
159
|
-
let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
|
|
187
|
+
let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(name, stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
|
|
160
188
|
let fetchAllItems = async () => {
|
|
161
189
|
let makeStream = Bus.getQueryDbStream(name);
|
|
162
190
|
if (makeStream !== undefined) {
|
|
@@ -77,6 +77,14 @@ type plainState = {
|
|
|
77
77
|
name: string,
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// No `*Id` field at all, so the key ladder has nothing to nominate — the state
|
|
81
|
+
// that still produces an empty capability.
|
|
82
|
+
@schema
|
|
83
|
+
type keylessState = {
|
|
84
|
+
name: string,
|
|
85
|
+
note: string,
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
@schema
|
|
81
89
|
type indexedState = {
|
|
82
90
|
productId: @s.matches(Reventless.DcbTag.string) string,
|
|
@@ -582,7 +590,12 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
582
590
|
// ── Phase 1: server capability — auto-derived Filter / OrderBy ─────────
|
|
583
591
|
|
|
584
592
|
describe("server capability — auto-derived Filter / OrderBy", () => {
|
|
585
|
-
|
|
593
|
+
// This used to assert that a state with no annotations got a search-only
|
|
594
|
+
// Filter and no OrderBy at all. It no longer does: `plainState` has exactly
|
|
595
|
+
// one `*Id` field, so the key is knowable without `@id` and the queryable now
|
|
596
|
+
// gets that key's eq filter and sort. The search-only case is the one below,
|
|
597
|
+
// where nothing identifies the row.
|
|
598
|
+
testPromise("read model with no annotations gets its inferred key's Eq + OrderBy", async () => {
|
|
586
599
|
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
587
600
|
~mutationEntries=[],
|
|
588
601
|
~queryEntries=[
|
|
@@ -599,16 +612,44 @@ describe("GraphQL_SchemaInspector", () => {
|
|
|
599
612
|
)
|
|
600
613
|
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
601
614
|
let sdl = inspection.sdlPreview
|
|
602
|
-
//
|
|
615
|
+
// The legacy block is untouched …
|
|
603
616
|
expect(sdl->String.includes("input PlainViewFilter"))->toBe(true)
|
|
604
617
|
expect(sdl->String.includes("search: String"))->toBe(true)
|
|
605
618
|
expect(sdl->String.includes("searchPrefix: String"))->toBe(true)
|
|
606
619
|
expect(sdl->String.includes("ids: [ID!]"))->toBe(true)
|
|
607
|
-
//
|
|
620
|
+
// … and `productId`, the sole candidate, now narrows and sorts.
|
|
621
|
+
expect(sdl->String.includes("productIdEq: ID"))->toBe(true)
|
|
622
|
+
expect(sdl->String.includes("PlainViewOrderBy"))->toBe(true)
|
|
623
|
+
expect(sdl->String.includes("PlainViewOrderField"))->toBe(true)
|
|
624
|
+
expect(sdl->String.includes("orderBy:"))->toBe(true)
|
|
625
|
+
// Only the key — an inferred key never drags the rest of the record in.
|
|
626
|
+
expect(sdl->String.includes("nameEq:"))->toBe(false)
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
// The remaining empty-capability case: no `*Id` field, so nothing says which
|
|
630
|
+
// field identifies a row and the ladder declines rather than guessing.
|
|
631
|
+
testPromise("read model whose key cannot be resolved stays search-only", async () => {
|
|
632
|
+
let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
633
|
+
~mutationEntries=[],
|
|
634
|
+
~queryEntries=[
|
|
635
|
+
{
|
|
636
|
+
singleFieldName: "Keyless_View",
|
|
637
|
+
listFieldName: "Keyless_Views",
|
|
638
|
+
returnTypeName: "KeylessView",
|
|
639
|
+
stateSchema: keylessStateSchema->S.castToUnknown,
|
|
640
|
+
authorization: None,
|
|
641
|
+
includeIdParam: false,
|
|
642
|
+
connectionSpec: true,
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
)
|
|
646
|
+
let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
|
|
647
|
+
let sdl = inspection.sdlPreview
|
|
648
|
+
expect(sdl->String.includes("input KeylessViewFilter"))->toBe(true)
|
|
649
|
+
expect(sdl->String.includes("search: String"))->toBe(true)
|
|
608
650
|
expect(sdl->String.includes("Eq:"))->toBe(false)
|
|
609
|
-
|
|
610
|
-
expect(sdl->String.includes("
|
|
611
|
-
expect(sdl->String.includes("PlainViewOrderField"))->toBe(false)
|
|
651
|
+
expect(sdl->String.includes("KeylessViewOrderBy"))->toBe(false)
|
|
652
|
+
expect(sdl->String.includes("KeylessViewOrderField"))->toBe(false)
|
|
612
653
|
expect(sdl->String.includes("orderBy:"))->toBe(false)
|
|
613
654
|
})
|
|
614
655
|
|
|
@@ -84,6 +84,11 @@ let plainStateSchema = S.schema(s => ({
|
|
|
84
84
|
name: s.m(S.string)
|
|
85
85
|
}));
|
|
86
86
|
|
|
87
|
+
let keylessStateSchema = S.schema(s => ({
|
|
88
|
+
name: s.m(S.string),
|
|
89
|
+
note: s.m(S.string)
|
|
90
|
+
}));
|
|
91
|
+
|
|
87
92
|
let indexedStateSchema = S.schema(s => ({
|
|
88
93
|
productId: s.m(DcbTag$Reventless.string),
|
|
89
94
|
ownerId: s.m(S.string),
|
|
@@ -453,7 +458,7 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
|
|
|
453
458
|
});
|
|
454
459
|
});
|
|
455
460
|
globalThis.describe("server capability — auto-derived Filter / OrderBy", () => {
|
|
456
|
-
globalThis.test("read model with no annotations
|
|
461
|
+
globalThis.test("read model with no annotations gets its inferred key's Eq + OrderBy", async () => {
|
|
457
462
|
let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([], [{
|
|
458
463
|
singleFieldName: "Plain_View",
|
|
459
464
|
listFieldName: "Plain_Views",
|
|
@@ -469,9 +474,29 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
|
|
|
469
474
|
globalThis.expect(sdl.includes("search: String")).toBe(true);
|
|
470
475
|
globalThis.expect(sdl.includes("searchPrefix: String")).toBe(true);
|
|
471
476
|
globalThis.expect(sdl.includes("ids: [ID!]")).toBe(true);
|
|
477
|
+
globalThis.expect(sdl.includes("productIdEq: ID")).toBe(true);
|
|
478
|
+
globalThis.expect(sdl.includes("PlainViewOrderBy")).toBe(true);
|
|
479
|
+
globalThis.expect(sdl.includes("PlainViewOrderField")).toBe(true);
|
|
480
|
+
globalThis.expect(sdl.includes("orderBy:")).toBe(true);
|
|
481
|
+
globalThis.expect(sdl.includes("nameEq:")).toBe(false);
|
|
482
|
+
});
|
|
483
|
+
globalThis.test("read model whose key cannot be resolved stays search-only", async () => {
|
|
484
|
+
let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([], [{
|
|
485
|
+
singleFieldName: "Keyless_View",
|
|
486
|
+
listFieldName: "Keyless_Views",
|
|
487
|
+
returnTypeName: "KeylessView",
|
|
488
|
+
stateSchema: keylessStateSchema,
|
|
489
|
+
authorization: undefined,
|
|
490
|
+
includeIdParam: false,
|
|
491
|
+
connectionSpec: true
|
|
492
|
+
}]);
|
|
493
|
+
let inspection = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment);
|
|
494
|
+
let sdl = inspection.sdlPreview;
|
|
495
|
+
globalThis.expect(sdl.includes("input KeylessViewFilter")).toBe(true);
|
|
496
|
+
globalThis.expect(sdl.includes("search: String")).toBe(true);
|
|
472
497
|
globalThis.expect(sdl.includes("Eq:")).toBe(false);
|
|
473
|
-
globalThis.expect(sdl.includes("
|
|
474
|
-
globalThis.expect(sdl.includes("
|
|
498
|
+
globalThis.expect(sdl.includes("KeylessViewOrderBy")).toBe(false);
|
|
499
|
+
globalThis.expect(sdl.includes("KeylessViewOrderField")).toBe(false);
|
|
475
500
|
globalThis.expect(sdl.includes("orderBy:")).toBe(false);
|
|
476
501
|
});
|
|
477
502
|
globalThis.test("read model with @id + @index emits Eq filters + OrderBy + orderBy arg", async () => {
|
|
@@ -701,6 +726,7 @@ export {
|
|
|
701
726
|
dcbSingleCommandSchema,
|
|
702
727
|
dcbMultiCommandSchema,
|
|
703
728
|
plainStateSchema,
|
|
729
|
+
keylessStateSchema,
|
|
704
730
|
indexedStateSchema,
|
|
705
731
|
indexedStateSchemaWithAnnotations,
|
|
706
732
|
orderedStateSchema,
|