@reventlessdev/reventless-core 3.0.0-alpha.223 → 3.0.0-alpha.224
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 +22 -0
- package/package.json +7 -7
- package/src/admin/Platform_Admin_Structure.res +10 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +6 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res +11 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +13 -1
- 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/QueryDbListQuery.res +13 -5
- package/src/components/Api/QueryDbListQuery.res.mjs +5 -3
- package/src/components/Api/SchemaType.res +36 -2
- package/src/components/Api/SchemaType.res.mjs +31 -10
- package/src/plugin/component/Plugin_Structure.res +19 -0
- package/src/plugin/component/Plugin_Structure.res.mjs +11 -2
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +63 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +117 -4
- package/tests/admin/Platform_PluginStructuresApiTest.res +10 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +23 -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/plugin/PluginStructureTest.res +90 -0
- package/tests/plugin/PluginStructureTest.res.mjs +86 -8
- package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -0
|
@@ -5,6 +5,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
|
|
7
7
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
import * as Api_Ids$ReventlessCore from "./Api_Ids.res.mjs";
|
|
8
9
|
|
|
9
10
|
function encodeCursor(value) {
|
|
10
11
|
return btoa(value);
|
|
@@ -47,7 +48,8 @@ function buildConnection(pageItems, hasNextPage, hasPreviousPage, cursorValueOf)
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function run(items, argsDict, capability, labelField,
|
|
51
|
+
function run(items, argsDict, capability, labelField, decodeLocalIdOpt) {
|
|
52
|
+
let decodeLocalId = decodeLocalIdOpt !== undefined ? decodeLocalIdOpt : Api_Ids$ReventlessCore.toLocalId;
|
|
51
53
|
let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
|
|
52
54
|
let search = Stdlib_Option.flatMap(filterDict["search"], Stdlib_JSON.Decode.string);
|
|
53
55
|
let searchPrefix = Stdlib_Option.flatMap(filterDict["searchPrefix"], Stdlib_JSON.Decode.string);
|
|
@@ -83,10 +85,10 @@ function run(items, argsDict, capability, labelField, decodeLocalId) {
|
|
|
83
85
|
let itemId = getId(item);
|
|
84
86
|
let itemLocalId = decodeLocalId(itemId);
|
|
85
87
|
passIds = ids.some(i => {
|
|
86
|
-
if (i === itemId) {
|
|
88
|
+
if (i === itemId || Primitive_object.equal(itemLocalId, i)) {
|
|
87
89
|
return true;
|
|
88
90
|
} else {
|
|
89
|
-
return Primitive_object.equal(
|
|
91
|
+
return Primitive_object.equal(decodeLocalId(i), itemId);
|
|
90
92
|
}
|
|
91
93
|
});
|
|
92
94
|
} else {
|
|
@@ -32,8 +32,34 @@ let isIdsFieldName = (name: string): bool => {
|
|
|
32
32
|
len > 3 && String.endsWith(lower, "ids")
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
The GraphQL type name a semantic **composite** is emitted under.
|
|
37
|
+
|
|
38
|
+
The branded scalars are absent on purpose: they refine a `string` or a number
|
|
39
|
+
without changing its shape, so each is already a `String` or a `Float` in GraphQL
|
|
40
|
+
and has no definition that could be duplicated. Only the semantics that turn a
|
|
41
|
+
field into an object or an enum need a name of their own — without one, the same
|
|
42
|
+
type is emitted once per *field* that uses it, under the field path that reached
|
|
43
|
+
it (`Catalog_AddProductPriceCurrency`, `Ordering_SyncNewProductPriceCurrency`, …
|
|
44
|
+
six copies of ISO 4217 in one example schema).
|
|
45
|
+
|
|
46
|
+
Naming them is what makes the definitions identical across plugins, which is the
|
|
47
|
+
property merged-API composition needs: AppSync unions same-named types from
|
|
48
|
+
different source APIs, and a union of identical definitions is that definition.
|
|
49
|
+
The `Node` / `PageInfo` / `SortOrder` base types already rely on exactly this.
|
|
50
|
+
*/
|
|
51
|
+
let semanticCompositeNames = [
|
|
52
|
+
(Reventless.Semantic.Id.money, "Money"),
|
|
53
|
+
(Reventless.Semantic.Id.dateRange, "DateRange"),
|
|
54
|
+
(Reventless.Semantic.Id.geoPoint, "GeoPoint"),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
let canonicalName = (id: string): option<string> =>
|
|
58
|
+
semanticCompositeNames
|
|
59
|
+
->Array.find(((semanticId, _)) => semanticId == id)
|
|
60
|
+
->Option.map(((_, name)) => name)
|
|
61
|
+
|
|
35
62
|
let rec fromSury = (~parentName: string, ~fieldName: string, schema: S.t<unknown>): schemaType => {
|
|
36
|
-
let shape = shapeOf(~parentName, ~fieldName, schema)
|
|
37
63
|
// Semantics the IR already has a dedicated shape for keep it: `dateTime` and
|
|
38
64
|
// `reference` are read below via `isDateTime` / `getTarget`, both of which now
|
|
39
65
|
// consult the generic marker, and their `format` output is a published
|
|
@@ -42,8 +68,16 @@ let rec fromSury = (~parentName: string, ~fieldName: string, schema: S.t<unknown
|
|
|
42
68
|
switch Reventless.Semantic.get(schema) {
|
|
43
69
|
| Some({id} as sem)
|
|
44
70
|
if id !== Reventless.Semantic.Id.dateTime && id !== Reventless.Semantic.Id.reference =>
|
|
71
|
+
// A composite is one type wherever it appears, so its shape is walked under
|
|
72
|
+
// its own name rather than under the field path that reached it. That
|
|
73
|
+
// renames what is nested inside it too — `Money`'s currency enum becomes
|
|
74
|
+
// `MoneyCurrency` for every field, rather than one enum per price field.
|
|
75
|
+
let shape = switch canonicalName(id) {
|
|
76
|
+
| Some(name) => shapeOf(~parentName=name, ~fieldName="", schema)
|
|
77
|
+
| None => shapeOf(~parentName, ~fieldName, schema)
|
|
78
|
+
}
|
|
45
79
|
Semantic(sem, shape)
|
|
46
|
-
| _ =>
|
|
80
|
+
| _ => shapeOf(~parentName, ~fieldName, schema)
|
|
47
81
|
}
|
|
48
82
|
}
|
|
49
83
|
|
|
@@ -27,22 +27,41 @@ function isIdsFieldName(name) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
let semanticCompositeNames = [
|
|
31
|
+
[
|
|
32
|
+
Semantic$Reventless.Id.money,
|
|
33
|
+
"Money"
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
Semantic$Reventless.Id.dateRange,
|
|
37
|
+
"DateRange"
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
Semantic$Reventless.Id.geoPoint,
|
|
41
|
+
"GeoPoint"
|
|
42
|
+
]
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function canonicalName(id) {
|
|
46
|
+
return Stdlib_Option.map(semanticCompositeNames.find(param => param[0] === id), param => param[1]);
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
function fromSury(parentName, fieldName, schema) {
|
|
31
|
-
let shape = shapeOf(parentName, fieldName, schema);
|
|
32
50
|
let sem = Semantic$Reventless.get(schema);
|
|
33
51
|
if (sem === undefined) {
|
|
34
|
-
return
|
|
52
|
+
return shapeOf(parentName, fieldName, schema);
|
|
35
53
|
}
|
|
36
54
|
let id = sem.id;
|
|
37
|
-
if (id
|
|
38
|
-
return
|
|
39
|
-
TAG: "Semantic",
|
|
40
|
-
_0: sem,
|
|
41
|
-
_1: shape
|
|
42
|
-
};
|
|
43
|
-
} else {
|
|
44
|
-
return shape;
|
|
55
|
+
if (id === Semantic$Reventless.Id.dateTime || id === Semantic$Reventless.Id.reference) {
|
|
56
|
+
return shapeOf(parentName, fieldName, schema);
|
|
45
57
|
}
|
|
58
|
+
let name = canonicalName(id);
|
|
59
|
+
let shape = name !== undefined ? shapeOf(name, "", schema) : shapeOf(parentName, fieldName, schema);
|
|
60
|
+
return {
|
|
61
|
+
TAG: "Semantic",
|
|
62
|
+
_0: sem,
|
|
63
|
+
_1: shape
|
|
64
|
+
};
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
function shapeOf(parentName, fieldName, schema) {
|
|
@@ -195,6 +214,8 @@ export {
|
|
|
195
214
|
isDateTime,
|
|
196
215
|
isIdFieldName,
|
|
197
216
|
isIdsFieldName,
|
|
217
|
+
semanticCompositeNames,
|
|
218
|
+
canonicalName,
|
|
198
219
|
fromSury,
|
|
199
220
|
shapeOf,
|
|
200
221
|
optionalFieldNames,
|
|
@@ -650,6 +650,12 @@ let make = (
|
|
|
650
650
|
let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
|
|
651
651
|
let stateSchema = R.Spec.stateSchema->S.castToUnknown
|
|
652
652
|
let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
|
|
653
|
+
// The same call the capability deriver makes, so the published key and the
|
|
654
|
+
// key the generated filter/order-by is built from cannot disagree.
|
|
655
|
+
let keyField = GraphQL_FragmentGenerator.resolveKeyField(
|
|
656
|
+
~entityName=R.Spec.name,
|
|
657
|
+
stateSchema,
|
|
658
|
+
)
|
|
653
659
|
// The events this read model projects from, qualified to the plugin's event ids so
|
|
654
660
|
// they match the producers' nodes in the graph. Was empty — which dropped projection
|
|
655
661
|
// edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
|
|
@@ -667,6 +673,12 @@ let make = (
|
|
|
667
673
|
statusField: statusFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
|
|
668
674
|
visibility: visibilityTag(R.Spec.visibility),
|
|
669
675
|
chapter: chapterOf(R.Spec.name),
|
|
676
|
+
// Taken from the `qf` record, never re-derived: `Api_Naming` is the only
|
|
677
|
+
// place that decides how `Products`/`Categories` singularise, and the
|
|
678
|
+
// point of publishing the name is that a consumer stops guessing it.
|
|
679
|
+
singleQueryField: Some(qf.singleFieldName),
|
|
680
|
+
idField: keyField->Option.map(((f, _)) => f),
|
|
681
|
+
idFieldSource: keyField->Option.map(((_, rung)) => rung),
|
|
670
682
|
}: Reventless.Plugin.queryableDef)
|
|
671
683
|
})
|
|
672
684
|
|
|
@@ -676,6 +688,10 @@ let make = (
|
|
|
676
688
|
let (_, consumed) = svsConsumed->Array.getUnsafe(i)
|
|
677
689
|
let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
|
|
678
690
|
let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
|
|
691
|
+
let keyField = GraphQL_FragmentGenerator.resolveKeyField(
|
|
692
|
+
~entityName=SVS.Spec.name,
|
|
693
|
+
stateSchema,
|
|
694
|
+
)
|
|
679
695
|
({
|
|
680
696
|
Reventless.Plugin.name: SVS.Spec.name,
|
|
681
697
|
queryField: qf.listFieldName,
|
|
@@ -688,6 +704,9 @@ let make = (
|
|
|
688
704
|
statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
|
|
689
705
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
690
706
|
chapter: chapterOf(SVS.Spec.name),
|
|
707
|
+
singleQueryField: Some(qf.singleFieldName),
|
|
708
|
+
idField: keyField->Option.map(((f, _)) => f),
|
|
709
|
+
idFieldSource: keyField->Option.map(((_, rung)) => rung),
|
|
691
710
|
}: Reventless.Plugin.queryableDef)
|
|
692
711
|
})
|
|
693
712
|
|
|
@@ -19,6 +19,7 @@ import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJso
|
|
|
19
19
|
import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
|
|
20
20
|
import * as ApiTargetStateHelpers$ReventlessCore from "../../components/Api/ApiTargetStateHelpers.res.mjs";
|
|
21
21
|
import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/ApiAllowedStatesHelpers.res.mjs";
|
|
22
|
+
import * as GraphQL_FragmentGenerator$ReventlessCore from "../../components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
22
23
|
|
|
23
24
|
let log = Logger$ReventlessCore.fromEnv();
|
|
24
25
|
|
|
@@ -500,6 +501,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
500
501
|
let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
|
|
501
502
|
let stateSchema = R.Spec.stateSchema;
|
|
502
503
|
let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
|
|
504
|
+
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(R.Spec.name, stateSchema);
|
|
503
505
|
let consumed = qualify(name, R.consumedEventNames);
|
|
504
506
|
return {
|
|
505
507
|
name: R.Spec.name,
|
|
@@ -512,7 +514,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
512
514
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
513
515
|
statusField: statusFieldFromStateSchema(R.Spec.name, stateSchema),
|
|
514
516
|
visibility: visibilityTag(R.Spec.visibility),
|
|
515
|
-
chapter: componentChapters[R.Spec.name]
|
|
517
|
+
chapter: componentChapters[R.Spec.name],
|
|
518
|
+
singleQueryField: qf.singleFieldName,
|
|
519
|
+
idField: Stdlib_Option.map(keyField, param => param[0]),
|
|
520
|
+
idFieldSource: Stdlib_Option.map(keyField, param => param[1])
|
|
516
521
|
};
|
|
517
522
|
});
|
|
518
523
|
let stateViewDefs = stateViewSlices.map((SVS, i) => {
|
|
@@ -521,6 +526,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
521
526
|
let consumed = match[1];
|
|
522
527
|
let stateSchema = SVS.Spec.stateSchema;
|
|
523
528
|
let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
|
|
529
|
+
let keyField = GraphQL_FragmentGenerator$ReventlessCore.resolveKeyField(SVS.Spec.name, stateSchema);
|
|
524
530
|
return {
|
|
525
531
|
name: SVS.Spec.name,
|
|
526
532
|
queryField: qf.listFieldName,
|
|
@@ -532,7 +538,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
532
538
|
labelFieldSource: labelFieldSourceToString(label.source),
|
|
533
539
|
statusField: statusFieldFromStateSchema(SVS.Spec.name, stateSchema),
|
|
534
540
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
535
|
-
chapter: componentChapters[SVS.Spec.name]
|
|
541
|
+
chapter: componentChapters[SVS.Spec.name],
|
|
542
|
+
singleQueryField: qf.singleFieldName,
|
|
543
|
+
idField: Stdlib_Option.map(keyField, param => param[0]),
|
|
544
|
+
idFieldSource: Stdlib_Option.map(keyField, param => param[1])
|
|
536
545
|
};
|
|
537
546
|
});
|
|
538
547
|
let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
|
|
@@ -31,6 +31,9 @@ let qbl: queryableDef = {
|
|
|
31
31
|
statusField: None,
|
|
32
32
|
visibility: None,
|
|
33
33
|
chapter: None,
|
|
34
|
+
singleQueryField: Some("Catalog_Product"),
|
|
35
|
+
idField: Some("productId"),
|
|
36
|
+
idFieldSource: Some("convention"),
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
let wbl: writableDef = {
|
|
@@ -252,6 +255,9 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
|
|
|
252
255
|
statusField: None,
|
|
253
256
|
visibility: Some("Internal"),
|
|
254
257
|
chapter: None,
|
|
258
|
+
singleQueryField: Some("Ordering_AvailableProduct"),
|
|
259
|
+
idField: Some("productId"),
|
|
260
|
+
idFieldSource: Some("sole"),
|
|
255
261
|
}
|
|
256
262
|
// A distinct name per source array: the complement has to be fed by both, and
|
|
257
263
|
// reusing one def would hide a version that only walks `readModels`.
|
|
@@ -342,6 +348,9 @@ describe("allowedStates + statusField populated", () => {
|
|
|
342
348
|
statusField: Some("status"),
|
|
343
349
|
visibility: None,
|
|
344
350
|
chapter: None,
|
|
351
|
+
singleQueryField: Some("Platform_Plugin"),
|
|
352
|
+
idField: None,
|
|
353
|
+
idFieldSource: None,
|
|
345
354
|
}
|
|
346
355
|
|
|
347
356
|
let wblWithStates: writableDef = {
|
|
@@ -395,3 +404,57 @@ describe("allowedStates + statusField populated", () => {
|
|
|
395
404
|
expect(json->String.includes("\"errors\":null"))->toEqual(false)
|
|
396
405
|
})
|
|
397
406
|
})
|
|
407
|
+
|
|
408
|
+
// The name exists so a consumer can stop re-implementing `Api_Naming.singularize`;
|
|
409
|
+
// it is only worth publishing if it reaches the caller intact, so assert the wire
|
|
410
|
+
// value and the SDL that types it, not just the record field.
|
|
411
|
+
describe("singleQueryField reaches the caller", () => {
|
|
412
|
+
testSync("encodes the stated singular beside the list field", () => {
|
|
413
|
+
expect(json->String.includes("\"queryField\":\"Catalog_Products\""))->toEqual(true)
|
|
414
|
+
expect(json->String.includes("\"singleQueryField\":\"Catalog_Product\""))->toEqual(true)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// Nullable on purpose: a structure persisted before the field existed, or a
|
|
418
|
+
// hand-rolled def that declines to state it, must decode rather than fail.
|
|
419
|
+
testSync("encodes an unstated singular as null", () => {
|
|
420
|
+
let unstated =
|
|
421
|
+
{...structure, readModels: [{...qbl, singleQueryField: None}]}
|
|
422
|
+
->Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId="Catalog", _)
|
|
423
|
+
->JSON.stringify
|
|
424
|
+
expect(unstated->String.includes("\"singleQueryField\":null"))->toEqual(true)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
testSync("types it as a nullable String on the read-side SDL", () => {
|
|
428
|
+
let sdl = Platform_ComponentDefinitionsApi.sdlTypes->Array.join("\n")
|
|
429
|
+
expect(sdl->String.includes("singleQueryField: String\n"))->toEqual(true)
|
|
430
|
+
expect(sdl->String.includes("singleQueryField: String!"))->toEqual(false)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
// The key and its rung travel together: a consumer ranking its own guess against
|
|
435
|
+
// this one needs both, so a version that carried the field and dropped the
|
|
436
|
+
// provenance would be worse than useless.
|
|
437
|
+
describe("idField / idFieldSource reach the caller", () => {
|
|
438
|
+
testSync("encodes the key field and the rung that produced it", () => {
|
|
439
|
+
expect(json->String.includes("\"idField\":\"productId\""))->toEqual(true)
|
|
440
|
+
expect(json->String.includes("\"idFieldSource\":\"convention\""))->toEqual(true)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
// A state whose key cannot be resolved (several `*Id` fields and no name match,
|
|
444
|
+
// or none at all) has nothing to report — null, not an invented field name.
|
|
445
|
+
testSync("encodes an unresolved key as null on both fields", () => {
|
|
446
|
+
let unresolved =
|
|
447
|
+
{...structure, readModels: [{...qbl, idField: None, idFieldSource: None}]}
|
|
448
|
+
->Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId="Catalog", _)
|
|
449
|
+
->JSON.stringify
|
|
450
|
+
expect(unresolved->String.includes("\"idField\":null"))->toEqual(true)
|
|
451
|
+
expect(unresolved->String.includes("\"idFieldSource\":null"))->toEqual(true)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
testSync("types both as nullable Strings on the read-side SDL", () => {
|
|
455
|
+
let sdl = Platform_ComponentDefinitionsApi.sdlTypes->Array.join("\n")
|
|
456
|
+
expect(sdl->String.includes("idField: String\n"))->toEqual(true)
|
|
457
|
+
expect(sdl->String.includes("idFieldSource: String\n"))->toEqual(true)
|
|
458
|
+
expect(sdl->String.includes("idField: String!"))->toEqual(false)
|
|
459
|
+
})
|
|
460
|
+
})
|
|
@@ -34,6 +34,12 @@ let qbl_searchableFields = ["displayName"];
|
|
|
34
34
|
|
|
35
35
|
let qbl_labelFieldSource = "annotation";
|
|
36
36
|
|
|
37
|
+
let qbl_singleQueryField = "Catalog_Product";
|
|
38
|
+
|
|
39
|
+
let qbl_idField = "productId";
|
|
40
|
+
|
|
41
|
+
let qbl_idFieldSource = "convention";
|
|
42
|
+
|
|
37
43
|
let qbl = {
|
|
38
44
|
name: "Products",
|
|
39
45
|
queryField: "Catalog_Products",
|
|
@@ -45,7 +51,10 @@ let qbl = {
|
|
|
45
51
|
labelFieldSource: qbl_labelFieldSource,
|
|
46
52
|
statusField: undefined,
|
|
47
53
|
visibility: undefined,
|
|
48
|
-
chapter: undefined
|
|
54
|
+
chapter: undefined,
|
|
55
|
+
singleQueryField: qbl_singleQueryField,
|
|
56
|
+
idField: qbl_idField,
|
|
57
|
+
idFieldSource: qbl_idFieldSource
|
|
49
58
|
};
|
|
50
59
|
|
|
51
60
|
let wbl_commands = [cmd];
|
|
@@ -257,6 +266,9 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
257
266
|
let internalQbl_searchableFields = ["name"];
|
|
258
267
|
let internalQbl_labelFieldSource = "convention";
|
|
259
268
|
let internalQbl_visibility = "Internal";
|
|
269
|
+
let internalQbl_singleQueryField = "Ordering_AvailableProduct";
|
|
270
|
+
let internalQbl_idField = "productId";
|
|
271
|
+
let internalQbl_idFieldSource = "sole";
|
|
260
272
|
let internalQbl = {
|
|
261
273
|
name: "AvailableProducts",
|
|
262
274
|
queryField: "Ordering_AvailableProducts",
|
|
@@ -268,13 +280,19 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
268
280
|
labelFieldSource: internalQbl_labelFieldSource,
|
|
269
281
|
statusField: undefined,
|
|
270
282
|
visibility: internalQbl_visibility,
|
|
271
|
-
chapter: undefined
|
|
283
|
+
chapter: undefined,
|
|
284
|
+
singleQueryField: internalQbl_singleQueryField,
|
|
285
|
+
idField: internalQbl_idField,
|
|
286
|
+
idFieldSource: internalQbl_idFieldSource
|
|
272
287
|
};
|
|
273
288
|
let internalSlice_consumedEventTypes = internalQbl_consumedEventTypes;
|
|
274
289
|
let internalSlice_linkedWriteSide = internalQbl_linkedWriteSide;
|
|
275
290
|
let internalSlice_searchableFields = internalQbl_searchableFields;
|
|
276
291
|
let internalSlice_labelFieldSource = "convention";
|
|
277
292
|
let internalSlice_visibility = "Internal";
|
|
293
|
+
let internalSlice_singleQueryField = "Ordering_AvailableProduct";
|
|
294
|
+
let internalSlice_idField = "productId";
|
|
295
|
+
let internalSlice_idFieldSource = "sole";
|
|
278
296
|
let internalSlice = {
|
|
279
297
|
name: "PendingShipments",
|
|
280
298
|
queryField: "Ordering_PendingShipments",
|
|
@@ -286,7 +304,10 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
286
304
|
labelFieldSource: internalSlice_labelFieldSource,
|
|
287
305
|
statusField: undefined,
|
|
288
306
|
visibility: internalSlice_visibility,
|
|
289
|
-
chapter: undefined
|
|
307
|
+
chapter: undefined,
|
|
308
|
+
singleQueryField: internalSlice_singleQueryField,
|
|
309
|
+
idField: internalSlice_idField,
|
|
310
|
+
idFieldSource: internalSlice_idFieldSource
|
|
290
311
|
};
|
|
291
312
|
let mixed_readModels = [
|
|
292
313
|
qbl,
|
|
@@ -371,6 +392,7 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
371
392
|
let qblWithStatus_searchableFields = ["name"];
|
|
372
393
|
let qblWithStatus_labelFieldSource = "convention";
|
|
373
394
|
let qblWithStatus_statusField = "status";
|
|
395
|
+
let qblWithStatus_singleQueryField = "Platform_Plugin";
|
|
374
396
|
let qblWithStatus = {
|
|
375
397
|
name: "Plugin",
|
|
376
398
|
queryField: "Platform_Plugins",
|
|
@@ -382,7 +404,10 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
382
404
|
labelFieldSource: qblWithStatus_labelFieldSource,
|
|
383
405
|
statusField: qblWithStatus_statusField,
|
|
384
406
|
visibility: undefined,
|
|
385
|
-
chapter: undefined
|
|
407
|
+
chapter: undefined,
|
|
408
|
+
singleQueryField: qblWithStatus_singleQueryField,
|
|
409
|
+
idField: undefined,
|
|
410
|
+
idFieldSource: undefined
|
|
386
411
|
};
|
|
387
412
|
let wblWithStates_commands = [cmdWithStates];
|
|
388
413
|
let wblWithStates_producedEventTypes = [];
|
|
@@ -438,6 +463,94 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
438
463
|
});
|
|
439
464
|
});
|
|
440
465
|
|
|
466
|
+
globalThis.describe("singleQueryField reaches the caller", () => {
|
|
467
|
+
globalThis.test("encodes the stated singular beside the list field", () => {
|
|
468
|
+
globalThis.expect(json.includes("\"queryField\":\"Catalog_Products\"")).toEqual(true);
|
|
469
|
+
globalThis.expect(json.includes("\"singleQueryField\":\"Catalog_Product\"")).toEqual(true);
|
|
470
|
+
});
|
|
471
|
+
globalThis.test("encodes an unstated singular as null", () => {
|
|
472
|
+
let unstated = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", {
|
|
473
|
+
readModels: [{
|
|
474
|
+
name: "Products",
|
|
475
|
+
queryField: "Catalog_Products",
|
|
476
|
+
schema: "{}",
|
|
477
|
+
consumedEventTypes: qbl_consumedEventTypes,
|
|
478
|
+
linkedWriteSide: qbl_linkedWriteSide,
|
|
479
|
+
labelField: "displayName",
|
|
480
|
+
searchableFields: qbl_searchableFields,
|
|
481
|
+
labelFieldSource: "annotation",
|
|
482
|
+
statusField: undefined,
|
|
483
|
+
visibility: undefined,
|
|
484
|
+
chapter: undefined,
|
|
485
|
+
singleQueryField: undefined,
|
|
486
|
+
idField: "productId",
|
|
487
|
+
idFieldSource: "convention"
|
|
488
|
+
}],
|
|
489
|
+
stateViewSlices: structure_stateViewSlices,
|
|
490
|
+
stateChangeSlices: structure_stateChangeSlices,
|
|
491
|
+
aggregates: structure_aggregates,
|
|
492
|
+
automationSlices: structure_automationSlices,
|
|
493
|
+
outboundTranslationSlices: structure_outboundTranslationSlices,
|
|
494
|
+
inboundTranslationSlices: structure_inboundTranslationSlices,
|
|
495
|
+
extensions: structure_extensions,
|
|
496
|
+
extensionPoints: undefined,
|
|
497
|
+
requiredStores: undefined,
|
|
498
|
+
requiredStoreDeclarations: undefined
|
|
499
|
+
}));
|
|
500
|
+
globalThis.expect(unstated.includes("\"singleQueryField\":null")).toEqual(true);
|
|
501
|
+
});
|
|
502
|
+
globalThis.test("types it as a nullable String on the read-side SDL", () => {
|
|
503
|
+
let sdl = Platform_ComponentDefinitionsApi$ReventlessCore.sdlTypes.join("\n");
|
|
504
|
+
globalThis.expect(sdl.includes("singleQueryField: String\n")).toEqual(true);
|
|
505
|
+
globalThis.expect(sdl.includes("singleQueryField: String!")).toEqual(false);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
globalThis.describe("idField / idFieldSource reach the caller", () => {
|
|
510
|
+
globalThis.test("encodes the key field and the rung that produced it", () => {
|
|
511
|
+
globalThis.expect(json.includes("\"idField\":\"productId\"")).toEqual(true);
|
|
512
|
+
globalThis.expect(json.includes("\"idFieldSource\":\"convention\"")).toEqual(true);
|
|
513
|
+
});
|
|
514
|
+
globalThis.test("encodes an unresolved key as null on both fields", () => {
|
|
515
|
+
let unresolved = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", {
|
|
516
|
+
readModels: [{
|
|
517
|
+
name: "Products",
|
|
518
|
+
queryField: "Catalog_Products",
|
|
519
|
+
schema: "{}",
|
|
520
|
+
consumedEventTypes: qbl_consumedEventTypes,
|
|
521
|
+
linkedWriteSide: qbl_linkedWriteSide,
|
|
522
|
+
labelField: "displayName",
|
|
523
|
+
searchableFields: qbl_searchableFields,
|
|
524
|
+
labelFieldSource: "annotation",
|
|
525
|
+
statusField: undefined,
|
|
526
|
+
visibility: undefined,
|
|
527
|
+
chapter: undefined,
|
|
528
|
+
singleQueryField: "Catalog_Product",
|
|
529
|
+
idField: undefined,
|
|
530
|
+
idFieldSource: undefined
|
|
531
|
+
}],
|
|
532
|
+
stateViewSlices: structure_stateViewSlices,
|
|
533
|
+
stateChangeSlices: structure_stateChangeSlices,
|
|
534
|
+
aggregates: structure_aggregates,
|
|
535
|
+
automationSlices: structure_automationSlices,
|
|
536
|
+
outboundTranslationSlices: structure_outboundTranslationSlices,
|
|
537
|
+
inboundTranslationSlices: structure_inboundTranslationSlices,
|
|
538
|
+
extensions: structure_extensions,
|
|
539
|
+
extensionPoints: undefined,
|
|
540
|
+
requiredStores: undefined,
|
|
541
|
+
requiredStoreDeclarations: undefined
|
|
542
|
+
}));
|
|
543
|
+
globalThis.expect(unresolved.includes("\"idField\":null")).toEqual(true);
|
|
544
|
+
globalThis.expect(unresolved.includes("\"idFieldSource\":null")).toEqual(true);
|
|
545
|
+
});
|
|
546
|
+
globalThis.test("types both as nullable Strings on the read-side SDL", () => {
|
|
547
|
+
let sdl = Platform_ComponentDefinitionsApi$ReventlessCore.sdlTypes.join("\n");
|
|
548
|
+
globalThis.expect(sdl.includes("idField: String\n")).toEqual(true);
|
|
549
|
+
globalThis.expect(sdl.includes("idFieldSource: String\n")).toEqual(true);
|
|
550
|
+
globalThis.expect(sdl.includes("idField: String!")).toEqual(false);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
441
554
|
export {
|
|
442
555
|
cmd,
|
|
443
556
|
qbl,
|
|
@@ -19,6 +19,9 @@ 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"),
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
// The component `Platform_ComponentDefinitions` drops and this query must keep.
|
|
@@ -113,6 +116,13 @@ describe("Platform_PluginStructures entry", () => {
|
|
|
113
116
|
)->toEqual(true)
|
|
114
117
|
)
|
|
115
118
|
|
|
119
|
+
// Both read-side queries share `encodeQueryableDef`, so this asserts the singular
|
|
120
|
+
// name is not a ComponentDefinitions-only field: a developer tool building a detail
|
|
121
|
+
// query reads it from here.
|
|
122
|
+
testSync("encodes the singular query field beside the list one", () =>
|
|
123
|
+
expect(json->String.includes("\"singleQueryField\":\"Catalog_Product\""))->toEqual(true)
|
|
124
|
+
)
|
|
125
|
+
|
|
116
126
|
testSync("encodes required stores with their provenance", () =>
|
|
117
127
|
expect(json->String.includes("\"requiredStores\":[\"Catalog.images\"]"))->toEqual(true)
|
|
118
128
|
)
|
|
@@ -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",
|
|
@@ -27,7 +33,10 @@ let publicRm = {
|
|
|
27
33
|
labelFieldSource: publicRm_labelFieldSource,
|
|
28
34
|
statusField: undefined,
|
|
29
35
|
visibility: undefined,
|
|
30
|
-
chapter: publicRm_chapter
|
|
36
|
+
chapter: publicRm_chapter,
|
|
37
|
+
singleQueryField: publicRm_singleQueryField,
|
|
38
|
+
idField: publicRm_idField,
|
|
39
|
+
idFieldSource: publicRm_idFieldSource
|
|
31
40
|
};
|
|
32
41
|
|
|
33
42
|
let internalRm_consumedEventTypes = publicRm_consumedEventTypes;
|
|
@@ -42,6 +51,12 @@ let internalRm_visibility = "Internal";
|
|
|
42
51
|
|
|
43
52
|
let internalRm_chapter = "Inventory";
|
|
44
53
|
|
|
54
|
+
let internalRm_singleQueryField = "Catalog_Product";
|
|
55
|
+
|
|
56
|
+
let internalRm_idField = "productId";
|
|
57
|
+
|
|
58
|
+
let internalRm_idFieldSource = "convention";
|
|
59
|
+
|
|
45
60
|
let internalRm = {
|
|
46
61
|
name: "ProductAudit",
|
|
47
62
|
queryField: "Catalog_ProductAudit",
|
|
@@ -53,7 +68,10 @@ let internalRm = {
|
|
|
53
68
|
labelFieldSource: internalRm_labelFieldSource,
|
|
54
69
|
statusField: undefined,
|
|
55
70
|
visibility: internalRm_visibility,
|
|
56
|
-
chapter: internalRm_chapter
|
|
71
|
+
chapter: internalRm_chapter,
|
|
72
|
+
singleQueryField: internalRm_singleQueryField,
|
|
73
|
+
idField: internalRm_idField,
|
|
74
|
+
idFieldSource: internalRm_idFieldSource
|
|
57
75
|
};
|
|
58
76
|
|
|
59
77
|
let structure_readModels = [
|
|
@@ -150,6 +168,9 @@ globalThis.describe("Platform_PluginStructures entry", () => {
|
|
|
150
168
|
globalThis.test("encodes extension points with their source event types", () => {
|
|
151
169
|
globalThis.expect(json.includes("\"extensionPoints\":[{\"name\":\"Catalog.Products\",\"delegateNames\":[\"onAdded\"],\"sourceEventTypes\":[\"Catalog.ProductAdded\"],\"commandTypes\":null}]")).toEqual(true);
|
|
152
170
|
});
|
|
171
|
+
globalThis.test("encodes the singular query field beside the list one", () => {
|
|
172
|
+
globalThis.expect(json.includes("\"singleQueryField\":\"Catalog_Product\"")).toEqual(true);
|
|
173
|
+
});
|
|
153
174
|
globalThis.test("encodes required stores with their provenance", () => {
|
|
154
175
|
globalThis.expect(json.includes("\"requiredStores\":[\"Catalog.images\"]")).toEqual(true);
|
|
155
176
|
});
|