@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.
- package/CHANGELOG.md +36 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +16 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +14 -3
- package/src/admin/Platform_BakedManifest.res +154 -0
- package/src/admin/Platform_BakedManifest.res.mjs +156 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +19 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +30 -2
- package/src/components/Api/Api_Ids.res +63 -0
- package/src/components/Api/Api_Ids.res.mjs +43 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res +121 -28
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +97 -27
- package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
- package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
- package/src/components/Api/QueryDbListQuery.res +38 -6
- package/src/components/Api/QueryDbListQuery.res.mjs +14 -5
- package/src/components/Api/SchemaType.res +36 -2
- package/src/components/Api/SchemaType.res.mjs +31 -10
- package/src/components/Api/SuryToJsonSchema.res +19 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
- package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
- package/src/plugin/component/Plugin_Structure.res +78 -3
- package/src/plugin/component/Plugin_Structure.res.mjs +38 -9
- package/tests/admin/Platform_BakedManifestTest.res +244 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +73 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +135 -6
- package/tests/admin/Platform_PluginStructuresApiTest.res +12 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -2
- package/tests/api/Api_IdsTest.res +143 -0
- package/tests/api/Api_IdsTest.res.mjs +97 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res +169 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +179 -0
- package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
- package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
- package/tests/api/SuryToJsonSchemaTest.res +45 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
- package/tests/commandgenerator/OwnerStampingTest.res +174 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
- package/tests/plugin/PluginStructureAccessTest.res +110 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
- package/tests/plugin/PluginStructureTest.res +90 -0
- package/tests/plugin/PluginStructureTest.res.mjs +86 -8
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
- 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 */
|