@reventlessdev/reventless-core 3.0.0-alpha.223 → 3.0.0-alpha.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/package.json +6 -6
  3. package/src/admin/Platform_Admin_Structure.res +16 -0
  4. package/src/admin/Platform_Admin_Structure.res.mjs +14 -3
  5. package/src/admin/Platform_BakedManifest.res +154 -0
  6. package/src/admin/Platform_BakedManifest.res.mjs +156 -0
  7. package/src/admin/Platform_ComponentDefinitionsApi.res +19 -2
  8. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +30 -2
  9. package/src/components/Api/Api_Ids.res +63 -0
  10. package/src/components/Api/Api_Ids.res.mjs +43 -0
  11. package/src/components/Api/GraphQL_FragmentGenerator.res +121 -28
  12. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +97 -27
  13. package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
  14. package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
  15. package/src/components/Api/QueryDbListQuery.res +38 -6
  16. package/src/components/Api/QueryDbListQuery.res.mjs +14 -5
  17. package/src/components/Api/SchemaType.res +36 -2
  18. package/src/components/Api/SchemaType.res.mjs +31 -10
  19. package/src/components/Api/SuryToJsonSchema.res +19 -0
  20. package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
  21. package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
  22. package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
  23. package/src/plugin/component/Plugin_Structure.res +78 -3
  24. package/src/plugin/component/Plugin_Structure.res.mjs +38 -9
  25. package/tests/admin/Platform_BakedManifestTest.res +244 -0
  26. package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
  27. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +73 -0
  28. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +135 -6
  29. package/tests/admin/Platform_PluginStructuresApiTest.res +12 -0
  30. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -2
  31. package/tests/api/Api_IdsTest.res +143 -0
  32. package/tests/api/Api_IdsTest.res.mjs +97 -0
  33. package/tests/api/GraphQL_FragmentGeneratorTest.res +169 -0
  34. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +179 -0
  35. package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
  36. package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
  37. package/tests/api/SuryToJsonSchemaTest.res +45 -0
  38. package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
  39. package/tests/commandgenerator/OwnerStampingTest.res +174 -0
  40. package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
  41. package/tests/plugin/PluginStructureAccessTest.res +110 -0
  42. package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
  43. package/tests/plugin/PluginStructureTest.res +90 -0
  44. package/tests/plugin/PluginStructureTest.res.mjs +86 -8
  45. package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
  46. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
  47. package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
  48. package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -0
  49. package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
  50. package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
  51. package/tests/plugin/pluginDefinitionRequiredScalars.txt +4 -0
@@ -23,7 +23,9 @@ let cmd = {
23
23
  references: cmd_references,
24
24
  allowedStates: undefined,
25
25
  targetState: undefined,
26
- apiExposed: cmd_apiExposed
26
+ apiExposed: cmd_apiExposed,
27
+ requiredAccess: undefined,
28
+ ownerField: undefined
27
29
  };
28
30
 
29
31
  let qbl_consumedEventTypes = ["ProductAdded"];
@@ -34,6 +36,12 @@ let qbl_searchableFields = ["displayName"];
34
36
 
35
37
  let qbl_labelFieldSource = "annotation";
36
38
 
39
+ let qbl_singleQueryField = "Catalog_Product";
40
+
41
+ let qbl_idField = "productId";
42
+
43
+ let qbl_idFieldSource = "convention";
44
+
37
45
  let qbl = {
38
46
  name: "Products",
39
47
  queryField: "Catalog_Products",
@@ -44,8 +52,13 @@ let qbl = {
44
52
  searchableFields: qbl_searchableFields,
45
53
  labelFieldSource: qbl_labelFieldSource,
46
54
  statusField: undefined,
55
+ ownerField: undefined,
47
56
  visibility: undefined,
48
- chapter: undefined
57
+ chapter: undefined,
58
+ singleQueryField: qbl_singleQueryField,
59
+ idField: qbl_idField,
60
+ idFieldSource: qbl_idFieldSource,
61
+ requiredAccess: undefined
49
62
  };
50
63
 
51
64
  let wbl_commands = [cmd];
@@ -257,6 +270,9 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
257
270
  let internalQbl_searchableFields = ["name"];
258
271
  let internalQbl_labelFieldSource = "convention";
259
272
  let internalQbl_visibility = "Internal";
273
+ let internalQbl_singleQueryField = "Ordering_AvailableProduct";
274
+ let internalQbl_idField = "productId";
275
+ let internalQbl_idFieldSource = "sole";
260
276
  let internalQbl = {
261
277
  name: "AvailableProducts",
262
278
  queryField: "Ordering_AvailableProducts",
@@ -267,14 +283,22 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
267
283
  searchableFields: internalQbl_searchableFields,
268
284
  labelFieldSource: internalQbl_labelFieldSource,
269
285
  statusField: undefined,
286
+ ownerField: undefined,
270
287
  visibility: internalQbl_visibility,
271
- chapter: undefined
288
+ chapter: undefined,
289
+ singleQueryField: internalQbl_singleQueryField,
290
+ idField: internalQbl_idField,
291
+ idFieldSource: internalQbl_idFieldSource,
292
+ requiredAccess: undefined
272
293
  };
273
294
  let internalSlice_consumedEventTypes = internalQbl_consumedEventTypes;
274
295
  let internalSlice_linkedWriteSide = internalQbl_linkedWriteSide;
275
296
  let internalSlice_searchableFields = internalQbl_searchableFields;
276
297
  let internalSlice_labelFieldSource = "convention";
277
298
  let internalSlice_visibility = "Internal";
299
+ let internalSlice_singleQueryField = "Ordering_AvailableProduct";
300
+ let internalSlice_idField = "productId";
301
+ let internalSlice_idFieldSource = "sole";
278
302
  let internalSlice = {
279
303
  name: "PendingShipments",
280
304
  queryField: "Ordering_PendingShipments",
@@ -285,8 +309,13 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
285
309
  searchableFields: internalSlice_searchableFields,
286
310
  labelFieldSource: internalSlice_labelFieldSource,
287
311
  statusField: undefined,
312
+ ownerField: undefined,
288
313
  visibility: internalSlice_visibility,
289
- chapter: undefined
314
+ chapter: undefined,
315
+ singleQueryField: internalSlice_singleQueryField,
316
+ idField: internalSlice_idField,
317
+ idFieldSource: internalSlice_idFieldSource,
318
+ requiredAccess: undefined
290
319
  };
291
320
  let mixed_readModels = [
292
321
  qbl,
@@ -364,13 +393,16 @@ globalThis.describe("allowedStates + statusField populated", () => {
364
393
  references: cmdWithStates_references,
365
394
  allowedStates: cmdWithStates_allowedStates,
366
395
  targetState: cmdWithStates_targetState,
367
- apiExposed: cmdWithStates_apiExposed
396
+ apiExposed: cmdWithStates_apiExposed,
397
+ requiredAccess: undefined,
398
+ ownerField: undefined
368
399
  };
369
400
  let qblWithStatus_consumedEventTypes = [];
370
401
  let qblWithStatus_linkedWriteSide = ["Plugin"];
371
402
  let qblWithStatus_searchableFields = ["name"];
372
403
  let qblWithStatus_labelFieldSource = "convention";
373
404
  let qblWithStatus_statusField = "status";
405
+ let qblWithStatus_singleQueryField = "Platform_Plugin";
374
406
  let qblWithStatus = {
375
407
  name: "Plugin",
376
408
  queryField: "Platform_Plugins",
@@ -381,8 +413,13 @@ globalThis.describe("allowedStates + statusField populated", () => {
381
413
  searchableFields: qblWithStatus_searchableFields,
382
414
  labelFieldSource: qblWithStatus_labelFieldSource,
383
415
  statusField: qblWithStatus_statusField,
416
+ ownerField: undefined,
384
417
  visibility: undefined,
385
- chapter: undefined
418
+ chapter: undefined,
419
+ singleQueryField: qblWithStatus_singleQueryField,
420
+ idField: undefined,
421
+ idFieldSource: undefined,
422
+ requiredAccess: undefined
386
423
  };
387
424
  let wblWithStates_commands = [cmdWithStates];
388
425
  let wblWithStates_producedEventTypes = [];
@@ -438,6 +475,98 @@ globalThis.describe("allowedStates + statusField populated", () => {
438
475
  });
439
476
  });
440
477
 
478
+ globalThis.describe("singleQueryField reaches the caller", () => {
479
+ globalThis.test("encodes the stated singular beside the list field", () => {
480
+ globalThis.expect(json.includes("\"queryField\":\"Catalog_Products\"")).toEqual(true);
481
+ globalThis.expect(json.includes("\"singleQueryField\":\"Catalog_Product\"")).toEqual(true);
482
+ });
483
+ globalThis.test("encodes an unstated singular as null", () => {
484
+ let unstated = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", {
485
+ readModels: [{
486
+ name: "Products",
487
+ queryField: "Catalog_Products",
488
+ schema: "{}",
489
+ consumedEventTypes: qbl_consumedEventTypes,
490
+ linkedWriteSide: qbl_linkedWriteSide,
491
+ labelField: "displayName",
492
+ searchableFields: qbl_searchableFields,
493
+ labelFieldSource: "annotation",
494
+ statusField: undefined,
495
+ ownerField: undefined,
496
+ visibility: undefined,
497
+ chapter: undefined,
498
+ singleQueryField: undefined,
499
+ idField: "productId",
500
+ idFieldSource: "convention",
501
+ requiredAccess: undefined
502
+ }],
503
+ stateViewSlices: structure_stateViewSlices,
504
+ stateChangeSlices: structure_stateChangeSlices,
505
+ aggregates: structure_aggregates,
506
+ automationSlices: structure_automationSlices,
507
+ outboundTranslationSlices: structure_outboundTranslationSlices,
508
+ inboundTranslationSlices: structure_inboundTranslationSlices,
509
+ extensions: structure_extensions,
510
+ extensionPoints: undefined,
511
+ requiredStores: undefined,
512
+ requiredStoreDeclarations: undefined
513
+ }));
514
+ globalThis.expect(unstated.includes("\"singleQueryField\":null")).toEqual(true);
515
+ });
516
+ globalThis.test("types it as a nullable String on the read-side SDL", () => {
517
+ let sdl = Platform_ComponentDefinitionsApi$ReventlessCore.sdlTypes.join("\n");
518
+ globalThis.expect(sdl.includes("singleQueryField: String\n")).toEqual(true);
519
+ globalThis.expect(sdl.includes("singleQueryField: String!")).toEqual(false);
520
+ });
521
+ });
522
+
523
+ globalThis.describe("idField / idFieldSource reach the caller", () => {
524
+ globalThis.test("encodes the key field and the rung that produced it", () => {
525
+ globalThis.expect(json.includes("\"idField\":\"productId\"")).toEqual(true);
526
+ globalThis.expect(json.includes("\"idFieldSource\":\"convention\"")).toEqual(true);
527
+ });
528
+ globalThis.test("encodes an unresolved key as null on both fields", () => {
529
+ let unresolved = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", {
530
+ readModels: [{
531
+ name: "Products",
532
+ queryField: "Catalog_Products",
533
+ schema: "{}",
534
+ consumedEventTypes: qbl_consumedEventTypes,
535
+ linkedWriteSide: qbl_linkedWriteSide,
536
+ labelField: "displayName",
537
+ searchableFields: qbl_searchableFields,
538
+ labelFieldSource: "annotation",
539
+ statusField: undefined,
540
+ ownerField: undefined,
541
+ visibility: undefined,
542
+ chapter: undefined,
543
+ singleQueryField: "Catalog_Product",
544
+ idField: undefined,
545
+ idFieldSource: undefined,
546
+ requiredAccess: undefined
547
+ }],
548
+ stateViewSlices: structure_stateViewSlices,
549
+ stateChangeSlices: structure_stateChangeSlices,
550
+ aggregates: structure_aggregates,
551
+ automationSlices: structure_automationSlices,
552
+ outboundTranslationSlices: structure_outboundTranslationSlices,
553
+ inboundTranslationSlices: structure_inboundTranslationSlices,
554
+ extensions: structure_extensions,
555
+ extensionPoints: undefined,
556
+ requiredStores: undefined,
557
+ requiredStoreDeclarations: undefined
558
+ }));
559
+ globalThis.expect(unresolved.includes("\"idField\":null")).toEqual(true);
560
+ globalThis.expect(unresolved.includes("\"idFieldSource\":null")).toEqual(true);
561
+ });
562
+ globalThis.test("types both as nullable Strings on the read-side SDL", () => {
563
+ let sdl = Platform_ComponentDefinitionsApi$ReventlessCore.sdlTypes.join("\n");
564
+ globalThis.expect(sdl.includes("idField: String\n")).toEqual(true);
565
+ globalThis.expect(sdl.includes("idFieldSource: String\n")).toEqual(true);
566
+ globalThis.expect(sdl.includes("idField: String!")).toEqual(false);
567
+ });
568
+ });
569
+
441
570
  export {
442
571
  cmd,
443
572
  qbl,
@@ -19,6 +19,11 @@ let publicRm: queryableDef = {
19
19
  statusField: None,
20
20
  visibility: None,
21
21
  chapter: Some("Inventory"),
22
+ singleQueryField: Some("Catalog_Product"),
23
+ idField: Some("productId"),
24
+ idFieldSource: Some("convention"),
25
+ requiredAccess: None,
26
+ ownerField: None,
22
27
  }
23
28
 
24
29
  // The component `Platform_ComponentDefinitions` drops and this query must keep.
@@ -113,6 +118,13 @@ describe("Platform_PluginStructures entry", () => {
113
118
  )->toEqual(true)
114
119
  )
115
120
 
121
+ // Both read-side queries share `encodeQueryableDef`, so this asserts the singular
122
+ // name is not a ComponentDefinitions-only field: a developer tool building a detail
123
+ // query reads it from here.
124
+ testSync("encodes the singular query field beside the list one", () =>
125
+ expect(json->String.includes("\"singleQueryField\":\"Catalog_Product\""))->toEqual(true)
126
+ )
127
+
116
128
  testSync("encodes required stores with their provenance", () =>
117
129
  expect(json->String.includes("\"requiredStores\":[\"Catalog.images\"]"))->toEqual(true)
118
130
  )
@@ -16,6 +16,12 @@ let publicRm_labelFieldSource = "annotation";
16
16
 
17
17
  let publicRm_chapter = "Inventory";
18
18
 
19
+ let publicRm_singleQueryField = "Catalog_Product";
20
+
21
+ let publicRm_idField = "productId";
22
+
23
+ let publicRm_idFieldSource = "convention";
24
+
19
25
  let publicRm = {
20
26
  name: "Products",
21
27
  queryField: "Catalog_Products",
@@ -26,8 +32,13 @@ let publicRm = {
26
32
  searchableFields: publicRm_searchableFields,
27
33
  labelFieldSource: publicRm_labelFieldSource,
28
34
  statusField: undefined,
35
+ ownerField: undefined,
29
36
  visibility: undefined,
30
- chapter: publicRm_chapter
37
+ chapter: publicRm_chapter,
38
+ singleQueryField: publicRm_singleQueryField,
39
+ idField: publicRm_idField,
40
+ idFieldSource: publicRm_idFieldSource,
41
+ requiredAccess: undefined
31
42
  };
32
43
 
33
44
  let internalRm_consumedEventTypes = publicRm_consumedEventTypes;
@@ -42,6 +53,12 @@ let internalRm_visibility = "Internal";
42
53
 
43
54
  let internalRm_chapter = "Inventory";
44
55
 
56
+ let internalRm_singleQueryField = "Catalog_Product";
57
+
58
+ let internalRm_idField = "productId";
59
+
60
+ let internalRm_idFieldSource = "convention";
61
+
45
62
  let internalRm = {
46
63
  name: "ProductAudit",
47
64
  queryField: "Catalog_ProductAudit",
@@ -52,8 +69,13 @@ let internalRm = {
52
69
  searchableFields: internalRm_searchableFields,
53
70
  labelFieldSource: internalRm_labelFieldSource,
54
71
  statusField: undefined,
72
+ ownerField: undefined,
55
73
  visibility: internalRm_visibility,
56
- chapter: internalRm_chapter
74
+ chapter: internalRm_chapter,
75
+ singleQueryField: internalRm_singleQueryField,
76
+ idField: internalRm_idField,
77
+ idFieldSource: internalRm_idFieldSource,
78
+ requiredAccess: undefined
57
79
  };
58
80
 
59
81
  let structure_readModels = [
@@ -150,6 +172,9 @@ globalThis.describe("Platform_PluginStructures entry", () => {
150
172
  globalThis.test("encodes extension points with their source event types", () => {
151
173
  globalThis.expect(json.includes("\"extensionPoints\":[{\"name\":\"Catalog.Products\",\"delegateNames\":[\"onAdded\"],\"sourceEventTypes\":[\"Catalog.ProductAdded\"],\"commandTypes\":null}]")).toEqual(true);
152
174
  });
175
+ globalThis.test("encodes the singular query field beside the list one", () => {
176
+ globalThis.expect(json.includes("\"singleQueryField\":\"Catalog_Product\"")).toEqual(true);
177
+ });
153
178
  globalThis.test("encodes required stores with their provenance", () => {
154
179
  globalThis.expect(json.includes("\"requiredStores\":[\"Catalog.images\"]")).toEqual(true);
155
180
  });
@@ -0,0 +1,143 @@
1
+ // The id contract: which form each door accepts, in one place.
2
+ //
3
+ // A row advertises a Relay global id (`btoa("<Type>:<localId>")`) while the
4
+ // storage key is the local id. Before this, the typed doors took only the local
5
+ // id, so the obvious `X(id: row.id)` returned null with no error. These pin the
6
+ // rule that makes either form work — and, just as importantly, the cases where a
7
+ // string must NOT be treated as a global id.
8
+
9
+ open JestGlobals
10
+
11
+ @val external btoa: string => string = "btoa"
12
+
13
+ describe("Api_Ids.encode / decode", () => {
14
+ testSync("round-trips a type name and a local id", () => {
15
+ let g = Api_Ids.encode(~typeName="Ordering_Customer", ~localId="cust-42")
16
+ expect(g)->toEqual(btoa("Ordering_Customer:cust-42"))
17
+ expect(Api_Ids.decode(g))->toEqual(Some(("Ordering_Customer", "cust-42")))
18
+ })
19
+
20
+ // Ids carrying a colon of their own: only the first one separates.
21
+ testSync("a local id containing a colon survives the round trip", () => {
22
+ let g = Api_Ids.encode(~typeName="Platform_Plugin", ~localId="Catalog@1.0.0:beta")
23
+ expect(Api_Ids.decode(g))->toEqual(Some(("Platform_Plugin", "Catalog@1.0.0:beta")))
24
+ })
25
+
26
+ testSync("a plain storage key is not a global id", () =>
27
+ expect(Api_Ids.decode("cust-42"))->toEqual(None)
28
+ )
29
+
30
+ // `atob` accepts this and yields text with no colon — decoding must still say no.
31
+ testSync("valid base64 without a colon is not a global id", () =>
32
+ expect(Api_Ids.decode(btoa("nocolonhere")))->toEqual(None)
33
+ )
34
+
35
+ // A leading colon would mean an empty type name — nothing `node` could resolve.
36
+ testSync("a decoded value starting with the separator is not a global id", () =>
37
+ expect(Api_Ids.decode(btoa(":cust-42")))->toEqual(None)
38
+ )
39
+ })
40
+
41
+ describe("Api_Ids.alternateKey — the fallback a door tries on a miss", () => {
42
+ testSync("a global id offers the storage key inside it", () =>
43
+ expect(Api_Ids.alternateKey(Api_Ids.encode(~typeName="Catalog_Product", ~localId="p-1")))
44
+ ->toEqual(Some("p-1"))
45
+ )
46
+
47
+ testSync("a plain key offers nothing, so the lookup is never retried", () =>
48
+ expect(Api_Ids.alternateKey("p-1"))->toEqual(None)
49
+ )
50
+
51
+ // The reason doors try the raw key FIRST and only fall back here: a storage key
52
+ // that happens to be valid base64 must keep resolving to its own row.
53
+ testSync("a key that merely looks like base64 is not silently rewritten", () => {
54
+ let looksBase64 = btoa("Type:inner")
55
+ expect(Api_Ids.alternateKey(looksBase64))->toEqual(Some("inner"))
56
+ // …but the door asks for `looksBase64` first, so a row stored under that exact
57
+ // string wins. `alternateKey` only ever supplies the second attempt.
58
+ expect(Api_Ids.alternateKey("cGxhaW4="))->toEqual(None)
59
+ })
60
+ })
61
+
62
+ // A resolver that stamps the Relay `id` onto a row must not stamp it onto the
63
+ // *stored* row: `JSON.Decode.object` hands back the very object the in-memory
64
+ // QueryDb holds, so a `Dict.set` writes through. The symptom was a row whose
65
+ // advertised id depended on query history — a list answered the storage key
66
+ // until someone loaded the detail page, and the global id ever after.
67
+ describe("stamping an id onto a row leaves the stored row alone", () => {
68
+ let storedRow = () => {
69
+ let d = Dict.make()
70
+ d->Dict.set("id", JSON.Encode.string("cust-42"))
71
+ d->Dict.set("email", JSON.Encode.string("a@b.c"))
72
+ JSON.Encode.object(d)
73
+ }
74
+
75
+ testSync("Dict.copy before set — the pattern the resolvers use", () => {
76
+ let row = storedRow()
77
+ let stamped = {
78
+ let obj = row->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
79
+ obj->Dict.set(
80
+ "id",
81
+ Api_Ids.encode(~typeName="Ordering_Customer", ~localId="cust-42")->JSON.Encode.string,
82
+ )
83
+ JSON.Encode.object(obj)
84
+ }
85
+ let idOf = j =>
86
+ j->JSON.Decode.object->Option.flatMap(d => d->Dict.get("id"))->Option.flatMap(JSON.Decode.string)
87
+ expect(idOf(stamped))->toEqual(Some(Api_Ids.encode(~typeName="Ordering_Customer", ~localId="cust-42")))
88
+ // The stored row still carries its storage key.
89
+ expect(idOf(row))->toEqual(Some("cust-42"))
90
+ })
91
+ })
92
+
93
+ // `filter.ids` is the door that has always taken either form. Which side needs
94
+ // decoding flipped when the typed doors stopped stamping global ids onto rows:
95
+ // the row now carries its storage key and the *argument* may be the global one.
96
+ describe("filter.ids matches whichever form each side holds", () => {
97
+ let row = (id: string) => {
98
+ let d = Dict.make()
99
+ d->Dict.set("id", JSON.Encode.string(id))
100
+ d->Dict.set("name", JSON.Encode.string("Alpha"))
101
+ JSON.Encode.object(d)
102
+ }
103
+ let idsIn = (~rowId, ~filterIds) => {
104
+ let args = Dict.make()
105
+ args->Dict.set(
106
+ "filter",
107
+ JSON.Encode.object(
108
+ Dict.fromArray([("ids", JSON.Encode.array(filterIds->Array.map(JSON.Encode.string)))]),
109
+ ),
110
+ )
111
+ let conn = QueryDbListQuery.run(
112
+ ~items=[row(rowId)],
113
+ ~argsDict=args,
114
+ ~capability=GraphQL_FragmentGenerator.emptyCapability,
115
+ ~labelField="name",
116
+ )
117
+ conn
118
+ ->JSON.Decode.object
119
+ ->Option.flatMap(d => d->Dict.get("edges"))
120
+ ->Option.flatMap(JSON.Decode.array)
121
+ ->Option.getOr([])
122
+ ->Array.length
123
+ }
124
+ let globalFor = localId => Api_Ids.encode(~typeName="Catalog_Product", ~localId)
125
+
126
+ testSync("storage key row, storage key filter", () =>
127
+ expect(idsIn(~rowId="p-1", ~filterIds=["p-1"]))->toBe(1)
128
+ )
129
+
130
+ // The case the flip fixes: a client passes back the global id it was given by
131
+ // `node`, while the list rows carry storage keys.
132
+ testSync("storage key row, global id filter", () =>
133
+ expect(idsIn(~rowId="p-1", ~filterIds=[globalFor("p-1")]))->toBe(1)
134
+ )
135
+
136
+ testSync("global id row, storage key filter", () =>
137
+ expect(idsIn(~rowId=globalFor("p-1"), ~filterIds=["p-1"]))->toBe(1)
138
+ )
139
+
140
+ testSync("a filter for another row still excludes it", () =>
141
+ expect(idsIn(~rowId="p-1", ~filterIds=[globalFor("p-2")]))->toBe(0)
142
+ )
143
+ })
@@ -0,0 +1,97 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Api_Ids$ReventlessCore from "../../src/components/Api/Api_Ids.res.mjs";
6
+ import * as QueryDbListQuery$ReventlessCore from "../../src/components/Api/QueryDbListQuery.res.mjs";
7
+ import * as GraphQL_FragmentGenerator$ReventlessCore from "../../src/components/Api/GraphQL_FragmentGenerator.res.mjs";
8
+
9
+ globalThis.describe("Api_Ids.encode / decode", () => {
10
+ globalThis.test("round-trips a type name and a local id", () => {
11
+ let g = Api_Ids$ReventlessCore.encode("Ordering_Customer", "cust-42");
12
+ globalThis.expect(g).toEqual(btoa("Ordering_Customer:cust-42"));
13
+ globalThis.expect(Api_Ids$ReventlessCore.decode(g)).toEqual([
14
+ "Ordering_Customer",
15
+ "cust-42"
16
+ ]);
17
+ });
18
+ globalThis.test("a local id containing a colon survives the round trip", () => {
19
+ let g = Api_Ids$ReventlessCore.encode("Platform_Plugin", "Catalog@1.0.0:beta");
20
+ globalThis.expect(Api_Ids$ReventlessCore.decode(g)).toEqual([
21
+ "Platform_Plugin",
22
+ "Catalog@1.0.0:beta"
23
+ ]);
24
+ });
25
+ globalThis.test("a plain storage key is not a global id", () => {
26
+ globalThis.expect(Api_Ids$ReventlessCore.decode("cust-42")).toEqual(undefined);
27
+ });
28
+ globalThis.test("valid base64 without a colon is not a global id", () => {
29
+ globalThis.expect(Api_Ids$ReventlessCore.decode(btoa("nocolonhere"))).toEqual(undefined);
30
+ });
31
+ globalThis.test("a decoded value starting with the separator is not a global id", () => {
32
+ globalThis.expect(Api_Ids$ReventlessCore.decode(btoa(":cust-42"))).toEqual(undefined);
33
+ });
34
+ });
35
+
36
+ globalThis.describe("Api_Ids.alternateKey — the fallback a door tries on a miss", () => {
37
+ globalThis.test("a global id offers the storage key inside it", () => {
38
+ globalThis.expect(Api_Ids$ReventlessCore.alternateKey(Api_Ids$ReventlessCore.encode("Catalog_Product", "p-1"))).toEqual("p-1");
39
+ });
40
+ globalThis.test("a plain key offers nothing, so the lookup is never retried", () => {
41
+ globalThis.expect(Api_Ids$ReventlessCore.alternateKey("p-1")).toEqual(undefined);
42
+ });
43
+ globalThis.test("a key that merely looks like base64 is not silently rewritten", () => {
44
+ let looksBase64 = btoa("Type:inner");
45
+ globalThis.expect(Api_Ids$ReventlessCore.alternateKey(looksBase64)).toEqual("inner");
46
+ globalThis.expect(Api_Ids$ReventlessCore.alternateKey("cGxhaW4=")).toEqual(undefined);
47
+ });
48
+ });
49
+
50
+ globalThis.describe("stamping an id onto a row leaves the stored row alone", () => {
51
+ let storedRow = () => {
52
+ let d = {};
53
+ d["id"] = "cust-42";
54
+ d["email"] = "a@b.c";
55
+ return d;
56
+ };
57
+ globalThis.test("Dict.copy before set — the pattern the resolvers use", () => {
58
+ let row = storedRow();
59
+ let obj = Stdlib_Option.mapOr(Stdlib_JSON.Decode.object(row), {}, prim => Object.assign({}, prim));
60
+ obj["id"] = Api_Ids$ReventlessCore.encode("Ordering_Customer", "cust-42");
61
+ let idOf = j => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(j), d => d["id"]), Stdlib_JSON.Decode.string);
62
+ globalThis.expect(idOf(obj)).toEqual(Api_Ids$ReventlessCore.encode("Ordering_Customer", "cust-42"));
63
+ globalThis.expect(idOf(row)).toEqual("cust-42");
64
+ });
65
+ });
66
+
67
+ globalThis.describe("filter.ids matches whichever form each side holds", () => {
68
+ let row = id => {
69
+ let d = {};
70
+ d["id"] = id;
71
+ d["name"] = "Alpha";
72
+ return d;
73
+ };
74
+ let idsIn = (rowId, filterIds) => {
75
+ let args = {};
76
+ args["filter"] = Object.fromEntries([[
77
+ "ids",
78
+ filterIds.map(prim => prim)
79
+ ]]);
80
+ let conn = QueryDbListQuery$ReventlessCore.run([row(rowId)], args, GraphQL_FragmentGenerator$ReventlessCore.emptyCapability, "name", undefined, undefined);
81
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(conn), d => d["edges"]), Stdlib_JSON.Decode.array), []).length;
82
+ };
83
+ globalThis.test("storage key row, storage key filter", () => {
84
+ globalThis.expect(idsIn("p-1", ["p-1"])).toBe(1);
85
+ });
86
+ globalThis.test("storage key row, global id filter", () => {
87
+ globalThis.expect(idsIn("p-1", [Api_Ids$ReventlessCore.encode("Catalog_Product", "p-1")])).toBe(1);
88
+ });
89
+ globalThis.test("global id row, storage key filter", () => {
90
+ globalThis.expect(idsIn(Api_Ids$ReventlessCore.encode("Catalog_Product", "p-1"), ["p-1"])).toBe(1);
91
+ });
92
+ globalThis.test("a filter for another row still excludes it", () => {
93
+ globalThis.expect(idsIn("p-1", [Api_Ids$ReventlessCore.encode("Catalog_Product", "p-2")])).toBe(0);
94
+ });
95
+ });
96
+
97
+ /* Not a pure module */