@reventlessdev/reventless-core 3.0.0-alpha.190 → 3.0.0-alpha.191
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 +10 -0
- package/package.json +5 -5
- package/src/admin/Platform_Admin.res +2 -2
- package/src/admin/Platform_Admin.res.mjs +2 -2
- package/src/admin/Platform_Admin_Structure.res +3 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +3 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +5 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/components/Dcb/Dcb_Builder.res +2 -2
- package/src/components/Dcb/Dcb_Builder.res.mjs +4 -4
- package/src/plugin/component/Plugin_Builder.res +2 -2
- package/src/plugin/component/Plugin_Builder.res.mjs +4 -4
- package/src/plugin/component/Plugin_Structure.res +124 -45
- package/src/plugin/component/Plugin_Structure.res.mjs +113 -38
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +3 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +7 -0
- package/tests/plugin/PluginStructureTest.res +246 -0
- package/tests/plugin/PluginStructureTest.res.mjs +217 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.191 (2026-07-29)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **plugin:** the field a read model is named by ([c679bb9](https://github.com/ReventlessDev/reventless-core/commit/c679bb9f610be9947417174bc3dfceea38207ba0))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **plugin:** why a read model is named by that field ([699ef8e](https://github.com/ReventlessDev/reventless-core/commit/699ef8e72f1b1d82fb04672739e5e4a8db61ae9a))
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
# 3.0.0-alpha.190 (2026-07-28)
|
|
7
17
|
|
|
8
18
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.191",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
32
|
-
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
33
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
|
|
33
|
+
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
34
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
35
35
|
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
36
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
37
36
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
38
|
-
"@reventlessdev/
|
|
37
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
39
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.109",
|
|
40
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.84"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -252,7 +252,7 @@ module Make = (
|
|
|
252
252
|
// pushed SDL — CreateResolver then loops on
|
|
253
253
|
// `NotFoundException: No field named ...`.
|
|
254
254
|
let registryKey = entry.specName->Option.getOr(entityName)
|
|
255
|
-
let
|
|
255
|
+
let label: Plugin_Structure.labelResolution = Plugin_Structure.labelFieldsFromStateSchema(
|
|
256
256
|
~entityName,
|
|
257
257
|
entry.stateSchema,
|
|
258
258
|
)
|
|
@@ -263,7 +263,7 @@ module Make = (
|
|
|
263
263
|
pluralTypeName: entry.listFieldName,
|
|
264
264
|
includeIdParam: entry.includeIdParam->Option.getOr(true),
|
|
265
265
|
connectionSpec: entry.connectionSpec->Option.getOr(true),
|
|
266
|
-
labelField,
|
|
266
|
+
labelField: label.field,
|
|
267
267
|
connectionFilterTypeName: entry.returnTypeName ++ "Filter",
|
|
268
268
|
}
|
|
269
269
|
Plugin_Helpers.queryFieldNamesRegistry->Dict.set(registryKey, qn)
|
|
@@ -110,13 +110,13 @@ function Make(RuntimeEnvironment) {
|
|
|
110
110
|
let prefix = "Platform_";
|
|
111
111
|
let entityName = entry.returnTypeName.startsWith(prefix) ? entry.returnTypeName.slice(prefix.length, entry.returnTypeName.length) : entry.returnTypeName;
|
|
112
112
|
let registryKey = Stdlib_Option.getOr(entry.specName, entityName);
|
|
113
|
-
let
|
|
113
|
+
let label = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema(entityName, entry.stateSchema);
|
|
114
114
|
let qn_singleFieldName = entry.singleFieldName;
|
|
115
115
|
let qn_listFieldName = entry.listFieldName;
|
|
116
116
|
let qn_returnTypeName = entry.returnTypeName;
|
|
117
117
|
let qn_pluralTypeName = entry.listFieldName;
|
|
118
118
|
let qn_connectionFilterTypeName = entry.returnTypeName + "Filter";
|
|
119
|
-
let qn_labelField =
|
|
119
|
+
let qn_labelField = label.field;
|
|
120
120
|
let qn_includeIdParam = Stdlib_Option.getOr(entry.includeIdParam, true);
|
|
121
121
|
let qn_connectionSpec = Stdlib_Option.getOr(entry.connectionSpec, true);
|
|
122
122
|
let qn = {
|
|
@@ -111,6 +111,9 @@ let pluginReadModel: queryableDef = {
|
|
|
111
111
|
linkedWriteSide: ["Plugin"],
|
|
112
112
|
labelField: "name",
|
|
113
113
|
searchableFields: ["name"],
|
|
114
|
+
// Hand-rolled rather than resolved, but the rung is the same one
|
|
115
|
+
// `labelFieldsFromStateSchema` would report for a field literally named `name`.
|
|
116
|
+
labelFieldSource: Some("convention"),
|
|
114
117
|
statusField: Some("status"),
|
|
115
118
|
visibility: None,
|
|
116
119
|
chapter: None,
|
|
@@ -126,6 +126,8 @@ let pluginReadModel_linkedWriteSide = ["Plugin"];
|
|
|
126
126
|
|
|
127
127
|
let pluginReadModel_searchableFields = ["name"];
|
|
128
128
|
|
|
129
|
+
let pluginReadModel_labelFieldSource = "convention";
|
|
130
|
+
|
|
129
131
|
let pluginReadModel_statusField = "status";
|
|
130
132
|
|
|
131
133
|
let pluginReadModel = {
|
|
@@ -136,6 +138,7 @@ let pluginReadModel = {
|
|
|
136
138
|
linkedWriteSide: pluginReadModel_linkedWriteSide,
|
|
137
139
|
labelField: "name",
|
|
138
140
|
searchableFields: pluginReadModel_searchableFields,
|
|
141
|
+
labelFieldSource: pluginReadModel_labelFieldSource,
|
|
139
142
|
statusField: pluginReadModel_statusField,
|
|
140
143
|
visibility: undefined,
|
|
141
144
|
chapter: undefined
|
|
@@ -11,7 +11,7 @@ let sdlTypes: array<string> = [
|
|
|
11
11
|
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
|
|
12
12
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
13
13
|
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n}`,
|
|
14
|
-
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n statusField: String\n}`,
|
|
14
|
+
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n}`,
|
|
15
15
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n}`,
|
|
16
16
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n}`,
|
|
17
17
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n}`,
|
|
@@ -72,6 +72,10 @@ let encodeQueryableDef = (r: queryableDef): JSON.t =>
|
|
|
72
72
|
("linkedWriteSide", encodeStrings(r.linkedWriteSide)),
|
|
73
73
|
("labelField", JSON.Encode.string(r.labelField)),
|
|
74
74
|
("searchableFields", encodeStrings(r.searchableFields)),
|
|
75
|
+
(
|
|
76
|
+
"labelFieldSource",
|
|
77
|
+
r.labelFieldSource->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
|
|
78
|
+
),
|
|
75
79
|
("statusField", r.statusField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
76
80
|
("chapter", r.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
77
81
|
])->JSON.Encode.object
|
|
@@ -9,7 +9,7 @@ let sdlTypes = [
|
|
|
9
9
|
`type Platform_CommandDef {\n name: String!\n schema: String!\n level: String!\n aggregateIdField: String\n mutationField: String!\n references: [Platform_FieldReference!]!\n allowedStates: [String!]\n targetState: String\n apiExposed: Boolean\n}`,
|
|
10
10
|
`type Platform_EventDef {\n name: String!\n schema: String!\n references: [Platform_FieldReference!]!\n}`,
|
|
11
11
|
`type Platform_WriteSideDef {\n name: String!\n commands: [Platform_CommandDef!]!\n linkedViews: [String!]!\n consistencyRead: String\n producedEventTypes: [String!]!\n consumedEventTypes: [String!]!\n events: [Platform_EventDef!]!\n}`,
|
|
12
|
-
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n statusField: String\n}`,
|
|
12
|
+
`type Platform_ReadSideDef {\n name: String!\n queryField: String!\n schema: String!\n consumedEventTypes: [String!]!\n linkedWriteSide: [String!]!\n labelField: String!\n searchableFields: [String!]!\n labelFieldSource: String\n statusField: String\n}`,
|
|
13
13
|
`type Platform_AutomationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n producedCommandTypes: [String!]!\n}`,
|
|
14
14
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n}`,
|
|
15
15
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n}`,
|
|
@@ -116,6 +116,10 @@ function encodeQueryableDef(r) {
|
|
|
116
116
|
"searchableFields",
|
|
117
117
|
r.searchableFields.map(prim => prim)
|
|
118
118
|
],
|
|
119
|
+
[
|
|
120
|
+
"labelFieldSource",
|
|
121
|
+
Stdlib_Option.mapOr(r.labelFieldSource, null, prim => prim)
|
|
122
|
+
],
|
|
119
123
|
[
|
|
120
124
|
"statusField",
|
|
121
125
|
Stdlib_Option.mapOr(r.statusField, null, prim => prim)
|
|
@@ -526,13 +526,13 @@ module Make = (
|
|
|
526
526
|
module(V: StateViewSlice.T),
|
|
527
527
|
) => {
|
|
528
528
|
let qn = Api_Naming.queryFieldNamesForStateView(~plugin=name, ~viewName=V.Spec.name)
|
|
529
|
-
let
|
|
529
|
+
let label = Plugin_Structure.labelFieldsFromStateSchema(
|
|
530
530
|
~entityName=V.Spec.name,
|
|
531
531
|
V.Spec.stateSchema->S.castToUnknown,
|
|
532
532
|
)
|
|
533
533
|
let qn = {
|
|
534
534
|
...qn,
|
|
535
|
-
labelField,
|
|
535
|
+
labelField: label.field,
|
|
536
536
|
connectionFilterTypeName: qn.returnTypeName ++ "Filter",
|
|
537
537
|
}
|
|
538
538
|
let qn = switch V.Spec.subIdConfig {
|
|
@@ -276,13 +276,13 @@ function Make(DcbEventLogStorage) {
|
|
|
276
276
|
}
|
|
277
277
|
stateViewSlices.forEach(V => {
|
|
278
278
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForStateView(name, V.Spec.name, undefined);
|
|
279
|
-
let
|
|
279
|
+
let label = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema(V.Spec.name, V.Spec.stateSchema);
|
|
280
280
|
let newrecord = {...qn};
|
|
281
|
-
newrecord.labelField =
|
|
281
|
+
newrecord.labelField = label.field;
|
|
282
282
|
newrecord.connectionFilterTypeName = qn.returnTypeName + "Filter";
|
|
283
|
-
let match
|
|
283
|
+
let match = V.Spec.subIdConfig;
|
|
284
284
|
let qn$1;
|
|
285
|
-
if (match
|
|
285
|
+
if (match !== undefined) {
|
|
286
286
|
let newrecord$1 = {...newrecord};
|
|
287
287
|
newrecord$1.itemsFilterTypeName = newrecord.returnTypeName + "ItemsFilter";
|
|
288
288
|
newrecord$1.itemsFieldName = newrecord.singleFieldName + "Items";
|
|
@@ -267,13 +267,13 @@ module Make = (
|
|
|
267
267
|
module(R: ReventlessInfra.ReadModel.T with type api = api and type role = role),
|
|
268
268
|
) => {
|
|
269
269
|
let qn = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
|
|
270
|
-
let
|
|
270
|
+
let label = Plugin_Structure.labelFieldsFromStateSchema(
|
|
271
271
|
~entityName=R.Spec.name,
|
|
272
272
|
R.Spec.stateSchema->S.castToUnknown,
|
|
273
273
|
)
|
|
274
274
|
let qn = {
|
|
275
275
|
...qn,
|
|
276
|
-
labelField,
|
|
276
|
+
labelField: label.field,
|
|
277
277
|
connectionFilterTypeName: qn.returnTypeName ++ "Filter",
|
|
278
278
|
}
|
|
279
279
|
let qn = switch R.Spec.subIdConfig {
|
|
@@ -208,13 +208,13 @@ function Make(Spec) {
|
|
|
208
208
|
let eventLogEntries = eventLogEntriesFromAggregates.concat(dcbResult.eventLogEntries);
|
|
209
209
|
readModels.forEach(R => {
|
|
210
210
|
let qn = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(extra$1, R.Spec.name, undefined);
|
|
211
|
-
let
|
|
211
|
+
let label = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema(R.Spec.name, R.Spec.stateSchema);
|
|
212
212
|
let newrecord = {...qn};
|
|
213
|
-
newrecord.labelField =
|
|
213
|
+
newrecord.labelField = label.field;
|
|
214
214
|
newrecord.connectionFilterTypeName = qn.returnTypeName + "Filter";
|
|
215
|
-
let match
|
|
215
|
+
let match = R.Spec.subIdConfig;
|
|
216
216
|
let qn$1;
|
|
217
|
-
if (match
|
|
217
|
+
if (match !== undefined) {
|
|
218
218
|
let newrecord$1 = {...newrecord};
|
|
219
219
|
newrecord$1.itemsFilterTypeName = newrecord.returnTypeName + "ItemsFilter";
|
|
220
220
|
newrecord$1.itemsFieldName = newrecord.singleFieldName + "Items";
|
|
@@ -5,26 +5,53 @@
|
|
|
5
5
|
|
|
6
6
|
let log = Logger.fromEnv()
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// `
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
8
|
+
// What a field *is* is `SchemaType`'s question, and it is the IR every other
|
|
9
|
+
// schema consumer here reads. The two predicates below are the only two answers
|
|
10
|
+
// this module needs; both are stated over the IR rather than over sury shapes,
|
|
11
|
+
// so a field that is a date, a reference or an enum is one for the label picker
|
|
12
|
+
// too.
|
|
13
|
+
|
|
14
|
+
// Whether a field can name a record. `Nullable` unwraps — an optional name is
|
|
15
|
+
// still the entity's name, absent on some rows, which is a rendering question
|
|
16
|
+
// rather than a declaration one. A `Semantic` wrapper is refused: it is the
|
|
17
|
+
// schema saying the string means something other than prose (a storage ref, a
|
|
18
|
+
// URL), and a bucket key is not a name. `DateTime`, `EntityId`, `Enum` and every
|
|
19
|
+
// composite shape fall out of the catch-all without being named — which is the
|
|
20
|
+
// point of reading an IR rather than restating it: the next shape it grows is
|
|
21
|
+
// excluded before it exists.
|
|
22
|
+
let rec isLabelShape = (t: SchemaType.schemaType): bool =>
|
|
23
|
+
switch t {
|
|
24
|
+
| ScalarString => true
|
|
25
|
+
| Nullable(inner) => isLabelShape(inner)
|
|
26
|
+
| _ => false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Whether a field can hold a lifecycle status: a closed set of values, or an
|
|
30
|
+
// optional one. A free-text `status: string` is not a lifecycle — filtering a
|
|
31
|
+
// command menu against `allowedStates` needs states to compare with.
|
|
32
|
+
let rec isStatusShape = (t: SchemaType.schemaType): bool =>
|
|
33
|
+
switch t {
|
|
34
|
+
| Enum(_, _) => true
|
|
35
|
+
| Nullable(inner) => isStatusShape(inner)
|
|
36
|
+
| _ => false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Field names that say "this one is the record's name" in the only way
|
|
40
|
+
// available short of the `@displayName` annotation. Matched case-insensitively
|
|
41
|
+
// and *exactly*: `customerName` holds a customer's name, not this record's.
|
|
42
|
+
let conventionalLabelNames = ["name", "title", "label", "displayname"]
|
|
43
|
+
|
|
44
|
+
let shapeOfItem = (~entityName: string, item: S.item): SchemaType.schemaType =>
|
|
45
|
+
SchemaType.fromSury(~parentName=entityName, ~fieldName=item.location, item.schema)
|
|
46
|
+
|
|
47
|
+
// Resolve the field that holds the entity's lifecycle status, used to filter a
|
|
48
|
+
// per-row command menu against each command's `allowedStates`. Resolution order:
|
|
23
49
|
// 1. Field annotated `@status` (PPX-emitted; see StateAnnotations).
|
|
24
|
-
// 2. Field literally named `"status"` (convention;
|
|
25
|
-
// labelField falls back to a conventionally-named
|
|
50
|
+
// 2. Field literally named `"status"` whose IR shape is an enum (convention;
|
|
51
|
+
// mirrors how labelField falls back to a conventionally-named field).
|
|
26
52
|
// 3. None — filter is inert for this read model.
|
|
27
53
|
let statusFieldFromStateSchema = (
|
|
54
|
+
~entityName: string,
|
|
28
55
|
stateSchema: S.t<unknown>,
|
|
29
56
|
): option<string> => {
|
|
30
57
|
let annotated = switch Reventless.StateAnnotations.getSpec(stateSchema) {
|
|
@@ -37,40 +64,96 @@ let statusFieldFromStateSchema = (
|
|
|
37
64
|
switch stateSchema {
|
|
38
65
|
| Object({items}) =>
|
|
39
66
|
items
|
|
40
|
-
->Array.find(item =>
|
|
67
|
+
->Array.find(item =>
|
|
68
|
+
item.location == "status" && isStatusShape(shapeOfItem(~entityName, item))
|
|
69
|
+
)
|
|
41
70
|
->Option.map(item => item.location)
|
|
42
71
|
| _ => None
|
|
43
72
|
}
|
|
44
73
|
}
|
|
45
74
|
}
|
|
46
75
|
|
|
76
|
+
// Which rung of the ladder below produced the label. Published on `queryableDef`
|
|
77
|
+
// as `labelFieldSource`, because the four rungs are not equally believable and a
|
|
78
|
+
// consumer with a name rule of its own has to rank the declaration against it:
|
|
79
|
+
// rung 1 is the author saying which field names the record, rungs 2 and 3 are
|
|
80
|
+
// guesses this repo makes on their behalf, and rung 4 is the admission that
|
|
81
|
+
// there was nothing to guess from. Convention and position are both guesses and
|
|
82
|
+
// nothing here branches on the difference — but a conventional name is the one a
|
|
83
|
+
// client can independently arrive at, while a positional pick is a fact only
|
|
84
|
+
// this side knows.
|
|
85
|
+
type labelFieldSource =
|
|
86
|
+
| Annotation
|
|
87
|
+
| Convention
|
|
88
|
+
| Position
|
|
89
|
+
| Fallback
|
|
90
|
+
|
|
91
|
+
let labelFieldSourceToString = (s: labelFieldSource): string =>
|
|
92
|
+
switch s {
|
|
93
|
+
| Annotation => "annotation"
|
|
94
|
+
| Convention => "convention"
|
|
95
|
+
| Position => "position"
|
|
96
|
+
| Fallback => "fallback"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type labelResolution = {
|
|
100
|
+
field: string,
|
|
101
|
+
searchableFields: array<string>,
|
|
102
|
+
source: labelFieldSource,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Resolves the field a state is named by, from a state schema.
|
|
106
|
+
// Source ladder:
|
|
107
|
+
// 1. @displayName spec present → "displayName" + spec.fields (raw underlying fields)
|
|
108
|
+
// 2. A candidate field named `name`/`title`/`label`/`displayName`
|
|
109
|
+
// 3. The first candidate in declaration order
|
|
110
|
+
// 4. "id" fallback with a logged warning (no searchable fields)
|
|
111
|
+
//
|
|
112
|
+
// A *candidate* is a non-TAG field, not literally named `id`, whose IR shape can
|
|
113
|
+
// name a record (`isLabelShape`). Rungs 2 and 3 are both guesses, and the order
|
|
114
|
+
// between them matters: declaration order alone means a state gains a new name
|
|
115
|
+
// whenever a field is inserted above the old one — a `placedAt` added so date
|
|
116
|
+
// views have something to key off renames every order to a timestamp.
|
|
117
|
+
//
|
|
118
|
+
// `id` is excluded here rather than by the IR because `SchemaType` treats a name
|
|
119
|
+
// that short as an ordinary string; every other reference — `*Id`/`*Ids`, a DCB
|
|
120
|
+
// tag, a `Reference.to` — is already an `EntityId` there.
|
|
47
121
|
let labelFieldsFromStateSchema = (
|
|
48
122
|
~entityName: string,
|
|
49
123
|
stateSchema: S.t<unknown>,
|
|
50
|
-
):
|
|
124
|
+
): labelResolution =>
|
|
51
125
|
switch Reventless.DisplayName.getSpec(stateSchema) {
|
|
52
|
-
| Some(spec) =>
|
|
126
|
+
| Some(spec) => {field: "displayName", searchableFields: spec.fields, source: Annotation}
|
|
53
127
|
| None =>
|
|
54
|
-
let
|
|
128
|
+
let candidates = switch stateSchema {
|
|
55
129
|
| Object({items}) =>
|
|
56
|
-
items->Array.
|
|
130
|
+
items->Array.filter(item =>
|
|
57
131
|
item.location != "TAG" &&
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
| String(_) => true
|
|
61
|
-
| _ => false
|
|
62
|
-
}
|
|
132
|
+
item.location != "id" &&
|
|
133
|
+
isLabelShape(shapeOfItem(~entityName, item))
|
|
63
134
|
)
|
|
64
|
-
| _ =>
|
|
135
|
+
| _ => []
|
|
136
|
+
}
|
|
137
|
+
let conventional = candidates->Array.find(item => {
|
|
138
|
+
let lower = item.location->String.toLowerCase
|
|
139
|
+
conventionalLabelNames->Array.some(n => n == lower)
|
|
140
|
+
})
|
|
141
|
+
let picked = switch conventional {
|
|
142
|
+
| Some(item) => Some((item, Convention))
|
|
143
|
+
| None => candidates->Array.get(0)->Option.map(item => (item, Position))
|
|
65
144
|
}
|
|
66
|
-
switch
|
|
67
|
-
| Some(item) =>
|
|
145
|
+
switch picked {
|
|
146
|
+
| Some((item, source)) => {
|
|
147
|
+
field: item.location,
|
|
148
|
+
searchableFields: [item.location],
|
|
149
|
+
source,
|
|
150
|
+
}
|
|
68
151
|
| None =>
|
|
69
152
|
log.warn(
|
|
70
153
|
~comp="Plugin_Structure",
|
|
71
154
|
`${entityName}: no @displayName annotation and no suitable string field — labelField falls back to "id"`,
|
|
72
155
|
)
|
|
73
|
-
|
|
156
|
+
{field: "id", searchableFields: [], source: Fallback}
|
|
74
157
|
}
|
|
75
158
|
}
|
|
76
159
|
|
|
@@ -475,10 +558,7 @@ let make = (
|
|
|
475
558
|
) => {
|
|
476
559
|
let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
|
|
477
560
|
let stateSchema = R.Spec.stateSchema->S.castToUnknown
|
|
478
|
-
let
|
|
479
|
-
~entityName=R.Spec.name,
|
|
480
|
-
stateSchema,
|
|
481
|
-
)
|
|
561
|
+
let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
|
|
482
562
|
// The events this read model projects from, qualified to the plugin's event ids so
|
|
483
563
|
// they match the producers' nodes in the graph. Was empty — which dropped projection
|
|
484
564
|
// edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
|
|
@@ -490,9 +570,10 @@ let make = (
|
|
|
490
570
|
schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
491
571
|
consumedEventTypes: consumed,
|
|
492
572
|
linkedWriteSide: linkedWriteSideFor(consumed),
|
|
493
|
-
labelField,
|
|
494
|
-
searchableFields,
|
|
495
|
-
|
|
573
|
+
labelField: label.field,
|
|
574
|
+
searchableFields: label.searchableFields,
|
|
575
|
+
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
576
|
+
statusField: statusFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
|
|
496
577
|
visibility: visibilityTag(R.Spec.visibility),
|
|
497
578
|
chapter: chapterOf(R.Spec.name),
|
|
498
579
|
}: Reventless.Plugin.queryableDef)
|
|
@@ -503,19 +584,17 @@ let make = (
|
|
|
503
584
|
let qf = Api_Naming.queryFieldNamesForStateView(~plugin=name, ~viewName=SVS.Spec.name)
|
|
504
585
|
let (_, consumed) = svsConsumed->Array.getUnsafe(i)
|
|
505
586
|
let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
|
|
506
|
-
let
|
|
507
|
-
~entityName=SVS.Spec.name,
|
|
508
|
-
stateSchema,
|
|
509
|
-
)
|
|
587
|
+
let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
|
|
510
588
|
({
|
|
511
589
|
Reventless.Plugin.name: SVS.Spec.name,
|
|
512
590
|
queryField: qf.listFieldName,
|
|
513
591
|
schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
514
592
|
consumedEventTypes: consumed,
|
|
515
593
|
linkedWriteSide: linkedWriteSideFor(consumed),
|
|
516
|
-
labelField,
|
|
517
|
-
searchableFields,
|
|
518
|
-
|
|
594
|
+
labelField: label.field,
|
|
595
|
+
searchableFields: label.searchableFields,
|
|
596
|
+
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
597
|
+
statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
|
|
519
598
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
520
599
|
chapter: chapterOf(SVS.Spec.name),
|
|
521
600
|
}: Reventless.Plugin.queryableDef)
|
|
@@ -11,6 +11,7 @@ import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
|
11
11
|
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
12
12
|
import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
|
|
13
13
|
import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.mjs";
|
|
14
|
+
import * as SchemaType$ReventlessCore from "../../components/Api/SchemaType.res.mjs";
|
|
14
15
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
15
16
|
import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHelpers.res.mjs";
|
|
16
17
|
import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJsonSchema.res.mjs";
|
|
@@ -19,54 +20,122 @@ import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/Ap
|
|
|
19
20
|
|
|
20
21
|
let log = Logger$ReventlessCore.fromEnv();
|
|
21
22
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
function isLabelShape(_t) {
|
|
24
|
+
while (true) {
|
|
25
|
+
let t = _t;
|
|
26
|
+
if (typeof t !== "object") {
|
|
27
|
+
return t === "ScalarString";
|
|
28
|
+
}
|
|
29
|
+
if (t.TAG !== "Nullable") {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
_t = t._0;
|
|
33
|
+
continue;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isStatusShape(_t) {
|
|
38
|
+
while (true) {
|
|
39
|
+
let t = _t;
|
|
40
|
+
if (typeof t !== "object") {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
switch (t.TAG) {
|
|
44
|
+
case "Nullable" :
|
|
45
|
+
_t = t._0;
|
|
46
|
+
continue;
|
|
47
|
+
case "Enum" :
|
|
48
|
+
return true;
|
|
49
|
+
default:
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
28
53
|
}
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
let conventionalLabelNames = [
|
|
56
|
+
"name",
|
|
57
|
+
"title",
|
|
58
|
+
"label",
|
|
59
|
+
"displayname"
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
function shapeOfItem(entityName, item) {
|
|
63
|
+
return SchemaType$ReventlessCore.fromSury(entityName, item.location, item.schema);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function statusFieldFromStateSchema(entityName, stateSchema) {
|
|
31
67
|
let spec = StateAnnotations$Reventless.getSpec(stateSchema);
|
|
32
68
|
let annotated = spec !== undefined ? spec.status : undefined;
|
|
33
69
|
if (annotated !== undefined) {
|
|
34
70
|
return annotated;
|
|
35
71
|
} else if (stateSchema.type === "object") {
|
|
36
|
-
return Stdlib_Option.map(stateSchema.items.find(item =>
|
|
72
|
+
return Stdlib_Option.map(stateSchema.items.find(item => {
|
|
73
|
+
if (item.location === "status") {
|
|
74
|
+
return isStatusShape(shapeOfItem(entityName, item));
|
|
75
|
+
} else {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}), item => item.location);
|
|
37
79
|
} else {
|
|
38
80
|
return;
|
|
39
81
|
}
|
|
40
82
|
}
|
|
41
83
|
|
|
84
|
+
function labelFieldSourceToString(s) {
|
|
85
|
+
switch (s) {
|
|
86
|
+
case "Annotation" :
|
|
87
|
+
return "annotation";
|
|
88
|
+
case "Convention" :
|
|
89
|
+
return "convention";
|
|
90
|
+
case "Position" :
|
|
91
|
+
return "position";
|
|
92
|
+
case "Fallback" :
|
|
93
|
+
return "fallback";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
42
97
|
function labelFieldsFromStateSchema(entityName, stateSchema) {
|
|
43
98
|
let spec = DisplayName$Reventless.getSpec(stateSchema);
|
|
44
99
|
if (spec !== undefined) {
|
|
45
|
-
return
|
|
46
|
-
"displayName",
|
|
47
|
-
spec.fields
|
|
48
|
-
|
|
100
|
+
return {
|
|
101
|
+
field: "displayName",
|
|
102
|
+
searchableFields: spec.fields,
|
|
103
|
+
source: "Annotation"
|
|
104
|
+
};
|
|
49
105
|
}
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
if (item.location
|
|
106
|
+
let candidates;
|
|
107
|
+
candidates = stateSchema.type === "object" ? stateSchema.items.filter(item => {
|
|
108
|
+
if (item.location !== "TAG" && item.location !== "id") {
|
|
109
|
+
return isLabelShape(shapeOfItem(entityName, item));
|
|
110
|
+
} else {
|
|
53
111
|
return false;
|
|
54
112
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
];
|
|
113
|
+
}) : [];
|
|
114
|
+
let conventional = candidates.find(item => {
|
|
115
|
+
let lower = item.location.toLowerCase();
|
|
116
|
+
return conventionalLabelNames.some(n => n === lower);
|
|
117
|
+
});
|
|
118
|
+
let picked = conventional !== undefined ? [
|
|
119
|
+
conventional,
|
|
120
|
+
"Convention"
|
|
121
|
+
] : Stdlib_Option.map(candidates[0], item => [
|
|
122
|
+
item,
|
|
123
|
+
"Position"
|
|
124
|
+
]);
|
|
125
|
+
if (picked !== undefined) {
|
|
126
|
+
let item = picked[0];
|
|
127
|
+
return {
|
|
128
|
+
field: item.location,
|
|
129
|
+
searchableFields: [item.location],
|
|
130
|
+
source: picked[1]
|
|
131
|
+
};
|
|
69
132
|
}
|
|
133
|
+
log.warn("Plugin_Structure", undefined, entityName + `: no @displayName annotation and no suitable string field — labelField falls back to "id"`);
|
|
134
|
+
return {
|
|
135
|
+
field: "id",
|
|
136
|
+
searchableFields: [],
|
|
137
|
+
source: "Fallback"
|
|
138
|
+
};
|
|
70
139
|
}
|
|
71
140
|
|
|
72
141
|
function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt, componentChaptersOpt) {
|
|
@@ -350,7 +419,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
350
419
|
let readModelDefs = readModels.map(R => {
|
|
351
420
|
let qf = Api_Naming$ReventlessCore.queryFieldNamesForReadModel(name, R.Spec.name, undefined);
|
|
352
421
|
let stateSchema = R.Spec.stateSchema;
|
|
353
|
-
let
|
|
422
|
+
let label = labelFieldsFromStateSchema(R.Spec.name, stateSchema);
|
|
354
423
|
let consumed = qualify(name, R.consumedEventNames);
|
|
355
424
|
return {
|
|
356
425
|
name: R.Spec.name,
|
|
@@ -358,9 +427,10 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
358
427
|
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(stateSchema)),
|
|
359
428
|
consumedEventTypes: consumed,
|
|
360
429
|
linkedWriteSide: linkedWriteSideFor(consumed),
|
|
361
|
-
labelField:
|
|
362
|
-
searchableFields:
|
|
363
|
-
|
|
430
|
+
labelField: label.field,
|
|
431
|
+
searchableFields: label.searchableFields,
|
|
432
|
+
labelFieldSource: labelFieldSourceToString(label.source),
|
|
433
|
+
statusField: statusFieldFromStateSchema(R.Spec.name, stateSchema),
|
|
364
434
|
visibility: visibilityTag(R.Spec.visibility),
|
|
365
435
|
chapter: componentChapters[R.Spec.name]
|
|
366
436
|
};
|
|
@@ -370,16 +440,17 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
370
440
|
let match = svsConsumed[i];
|
|
371
441
|
let consumed = match[1];
|
|
372
442
|
let stateSchema = SVS.Spec.stateSchema;
|
|
373
|
-
let
|
|
443
|
+
let label = labelFieldsFromStateSchema(SVS.Spec.name, stateSchema);
|
|
374
444
|
return {
|
|
375
445
|
name: SVS.Spec.name,
|
|
376
446
|
queryField: qf.listFieldName,
|
|
377
447
|
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(stateSchema)),
|
|
378
448
|
consumedEventTypes: consumed,
|
|
379
449
|
linkedWriteSide: linkedWriteSideFor(consumed),
|
|
380
|
-
labelField:
|
|
381
|
-
searchableFields:
|
|
382
|
-
|
|
450
|
+
labelField: label.field,
|
|
451
|
+
searchableFields: label.searchableFields,
|
|
452
|
+
labelFieldSource: labelFieldSourceToString(label.source),
|
|
453
|
+
statusField: statusFieldFromStateSchema(SVS.Spec.name, stateSchema),
|
|
383
454
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
384
455
|
chapter: componentChapters[SVS.Spec.name]
|
|
385
456
|
};
|
|
@@ -489,8 +560,12 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
489
560
|
|
|
490
561
|
export {
|
|
491
562
|
log,
|
|
492
|
-
|
|
563
|
+
isLabelShape,
|
|
564
|
+
isStatusShape,
|
|
565
|
+
conventionalLabelNames,
|
|
566
|
+
shapeOfItem,
|
|
493
567
|
statusFieldFromStateSchema,
|
|
568
|
+
labelFieldSourceToString,
|
|
494
569
|
labelFieldsFromStateSchema,
|
|
495
570
|
make,
|
|
496
571
|
}
|
|
@@ -27,6 +27,7 @@ let qbl: queryableDef = {
|
|
|
27
27
|
linkedWriteSide: ["Product"],
|
|
28
28
|
labelField: "displayName",
|
|
29
29
|
searchableFields: ["displayName"],
|
|
30
|
+
labelFieldSource: Some("annotation"),
|
|
30
31
|
statusField: None,
|
|
31
32
|
visibility: None,
|
|
32
33
|
chapter: None,
|
|
@@ -228,6 +229,7 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
|
|
|
228
229
|
linkedWriteSide: [],
|
|
229
230
|
labelField: "name",
|
|
230
231
|
searchableFields: ["name"],
|
|
232
|
+
labelFieldSource: Some("convention"),
|
|
231
233
|
statusField: None,
|
|
232
234
|
visibility: Some("Internal"),
|
|
233
235
|
chapter: None,
|
|
@@ -270,6 +272,7 @@ describe("allowedStates + statusField populated", () => {
|
|
|
270
272
|
linkedWriteSide: ["Plugin"],
|
|
271
273
|
labelField: "name",
|
|
272
274
|
searchableFields: ["name"],
|
|
275
|
+
labelFieldSource: Some("convention"),
|
|
273
276
|
statusField: Some("status"),
|
|
274
277
|
visibility: None,
|
|
275
278
|
chapter: None,
|
|
@@ -31,6 +31,8 @@ let qbl_linkedWriteSide = ["Product"];
|
|
|
31
31
|
|
|
32
32
|
let qbl_searchableFields = ["displayName"];
|
|
33
33
|
|
|
34
|
+
let qbl_labelFieldSource = "annotation";
|
|
35
|
+
|
|
34
36
|
let qbl = {
|
|
35
37
|
name: "Products",
|
|
36
38
|
queryField: "Catalog_Products",
|
|
@@ -39,6 +41,7 @@ let qbl = {
|
|
|
39
41
|
linkedWriteSide: qbl_linkedWriteSide,
|
|
40
42
|
labelField: "displayName",
|
|
41
43
|
searchableFields: qbl_searchableFields,
|
|
44
|
+
labelFieldSource: qbl_labelFieldSource,
|
|
42
45
|
statusField: undefined,
|
|
43
46
|
visibility: undefined,
|
|
44
47
|
chapter: undefined
|
|
@@ -233,6 +236,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
233
236
|
let internalQbl_consumedEventTypes = ["CatalogProductSynced"];
|
|
234
237
|
let internalQbl_linkedWriteSide = [];
|
|
235
238
|
let internalQbl_searchableFields = ["name"];
|
|
239
|
+
let internalQbl_labelFieldSource = "convention";
|
|
236
240
|
let internalQbl_visibility = "Internal";
|
|
237
241
|
let internalQbl = {
|
|
238
242
|
name: "AvailableProducts",
|
|
@@ -242,6 +246,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
242
246
|
linkedWriteSide: internalQbl_linkedWriteSide,
|
|
243
247
|
labelField: "name",
|
|
244
248
|
searchableFields: internalQbl_searchableFields,
|
|
249
|
+
labelFieldSource: internalQbl_labelFieldSource,
|
|
245
250
|
statusField: undefined,
|
|
246
251
|
visibility: internalQbl_visibility,
|
|
247
252
|
chapter: undefined
|
|
@@ -298,6 +303,7 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
298
303
|
let qblWithStatus_consumedEventTypes = [];
|
|
299
304
|
let qblWithStatus_linkedWriteSide = ["Plugin"];
|
|
300
305
|
let qblWithStatus_searchableFields = ["name"];
|
|
306
|
+
let qblWithStatus_labelFieldSource = "convention";
|
|
301
307
|
let qblWithStatus_statusField = "status";
|
|
302
308
|
let qblWithStatus = {
|
|
303
309
|
name: "Plugin",
|
|
@@ -307,6 +313,7 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
307
313
|
linkedWriteSide: qblWithStatus_linkedWriteSide,
|
|
308
314
|
labelField: "name",
|
|
309
315
|
searchableFields: qblWithStatus_searchableFields,
|
|
316
|
+
labelFieldSource: qblWithStatus_labelFieldSource,
|
|
310
317
|
statusField: qblWithStatus_statusField,
|
|
311
318
|
visibility: undefined,
|
|
312
319
|
chapter: undefined
|
|
@@ -275,6 +275,252 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
275
275
|
})
|
|
276
276
|
})
|
|
277
277
|
|
|
278
|
+
// What a field *is* comes from `SchemaType`, the IR every other schema
|
|
279
|
+
// consumer reads, rather than from a `String(_)` match here. These pin the
|
|
280
|
+
// shapes that separates the two: a date, a reference, a semantic-carrying
|
|
281
|
+
// string and an optional one all match `String(_)` loosely or not at all, and
|
|
282
|
+
// each was answered wrong before.
|
|
283
|
+
describe("labelField — the shape rule", () => {
|
|
284
|
+
let labelOf = (~entityName="Test", schema) => {
|
|
285
|
+
let r = Plugin_Structure.labelFieldsFromStateSchema(~entityName, schema->S.castToUnknown)
|
|
286
|
+
(r.field, r.searchableFields)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
testSync("a DateTime is not a name — a state whose only string is one falls to id", () => {
|
|
290
|
+
// The shipped `Orders` case: `placedAt` was added so date views have a
|
|
291
|
+
// field to key off, and a positional rule let it rename the entity.
|
|
292
|
+
let schema = S.schema(s =>
|
|
293
|
+
{
|
|
294
|
+
"orderId": s.matches(Reventless.Reference.to_("Order")),
|
|
295
|
+
"placedAt": s.matches(Reventless.DateTime.string),
|
|
296
|
+
"shippedAt": s.matches(Reventless.DateTime.string),
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
expect(labelOf(schema))->toEqual(("id", []))
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
testSync("a DateTime is skipped, not fatal — a later plain string still wins", () => {
|
|
303
|
+
let schema = S.schema(s =>
|
|
304
|
+
{
|
|
305
|
+
"placedAt": s.matches(Reventless.DateTime.string),
|
|
306
|
+
"reference": s.matches(S.string),
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
expect(labelOf(schema))->toEqual(("reference", ["reference"]))
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
testSync("an optional string is still the entity's name", () => {
|
|
313
|
+
let schema = S.schema(s =>
|
|
314
|
+
{
|
|
315
|
+
"nickname": s.matches(S.option(S.string)),
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
expect(labelOf(schema))->toEqual(("nickname", ["nickname"]))
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
testSync("a reference is not a name even when its field is not called `*Id`", () => {
|
|
322
|
+
let schema = S.schema(s =>
|
|
323
|
+
{
|
|
324
|
+
"customer": s.matches(Reventless.Reference.to_("Customer")),
|
|
325
|
+
"note": s.matches(S.string),
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
testSync("a DCB-tagged string is not a name", () => {
|
|
332
|
+
let schema = S.schema(s =>
|
|
333
|
+
{
|
|
334
|
+
"partition": s.matches(Reventless.DcbTag.string),
|
|
335
|
+
"note": s.matches(S.string),
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
testSync("the `*Id` test is case-insensitive — `productID` is a reference", () => {
|
|
342
|
+
let schema = S.schema(s =>
|
|
343
|
+
{
|
|
344
|
+
"productID": s.matches(S.string),
|
|
345
|
+
"note": s.matches(S.string),
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
testSync("a semantic-carrying string is a value, not a name", () => {
|
|
352
|
+
// `imageUrl` names a stored object. A bucket key read as a product's name
|
|
353
|
+
// is what the previous rule did whenever no plain string preceded it.
|
|
354
|
+
let schema = S.schema(s =>
|
|
355
|
+
{
|
|
356
|
+
"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
|
|
357
|
+
"note": s.matches(S.string),
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
testSync("a state whose only string is a semantic falls to id", () => {
|
|
364
|
+
let schema = S.schema(s =>
|
|
365
|
+
{
|
|
366
|
+
"imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
expect(labelOf(schema))->toEqual(("id", []))
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
testSync("`id` itself is still excluded by name", () => {
|
|
373
|
+
// `SchemaType` reads a name that short as an ordinary string, so the
|
|
374
|
+
// exclusion cannot come from the IR.
|
|
375
|
+
let schema = S.schema(s =>
|
|
376
|
+
{
|
|
377
|
+
"id": s.matches(S.string),
|
|
378
|
+
"note": s.matches(S.string),
|
|
379
|
+
}
|
|
380
|
+
)
|
|
381
|
+
expect(labelOf(schema))->toEqual(("note", ["note"]))
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
describe("labelField — the conventional-name rung", () => {
|
|
386
|
+
let labelOf = schema => {
|
|
387
|
+
let r = Plugin_Structure.labelFieldsFromStateSchema(~entityName="Test", schema->S.castToUnknown)
|
|
388
|
+
(r.field, r.searchableFields)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
testSync("a field named `name` beats an earlier string", () => {
|
|
392
|
+
let schema = S.schema(s =>
|
|
393
|
+
{
|
|
394
|
+
"sku": s.matches(S.string),
|
|
395
|
+
"name": s.matches(S.string),
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
expect(labelOf(schema))->toEqual(("name", ["name"]))
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
testSync("`title` counts too, case-insensitively", () => {
|
|
402
|
+
let schema = S.schema(s =>
|
|
403
|
+
{
|
|
404
|
+
"body": s.matches(S.string),
|
|
405
|
+
"Title": s.matches(S.string),
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
expect(labelOf(schema))->toEqual(("Title", ["Title"]))
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
testSync("the match is exact — `customerName` is a customer's name, not this record's", () => {
|
|
412
|
+
// The shipped `OrdersView` fixture depends on this: it has exactly one
|
|
413
|
+
// eligible field and must be picked by position, not by convention.
|
|
414
|
+
let schema = S.schema(s =>
|
|
415
|
+
{
|
|
416
|
+
"customerName": s.matches(S.string),
|
|
417
|
+
"note": s.matches(S.string),
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
expect(labelOf(schema))->toEqual(("customerName", ["customerName"]))
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
testSync("an optional `name` still outranks an earlier string", () => {
|
|
424
|
+
let schema = S.schema(s =>
|
|
425
|
+
{
|
|
426
|
+
"sku": s.matches(S.string),
|
|
427
|
+
"name": s.matches(S.option(S.string)),
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
expect(labelOf(schema))->toEqual(("name", ["name"]))
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
// The rung is published because the four are not equally believable: a
|
|
435
|
+
// consumer with a name rule of its own has to know whether it is ranking
|
|
436
|
+
// against a declaration or against a guess.
|
|
437
|
+
describe("labelFieldSource — which rung answered", () => {
|
|
438
|
+
let sourceOf = schema =>
|
|
439
|
+
Plugin_Structure.labelFieldsFromStateSchema(
|
|
440
|
+
~entityName="Test",
|
|
441
|
+
schema->S.castToUnknown,
|
|
442
|
+
).source->Plugin_Structure.labelFieldSourceToString
|
|
443
|
+
|
|
444
|
+
testSync("a @displayName spec is a declaration", () => {
|
|
445
|
+
let customers = structure.stateViewSlices->Array.getUnsafe(2)
|
|
446
|
+
expect((customers.labelField, customers.labelFieldSource))->toEqual((
|
|
447
|
+
"displayName",
|
|
448
|
+
Some("annotation"),
|
|
449
|
+
))
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
testSync("a field named `name` is a guess the consumer can also make", () => {
|
|
453
|
+
let schema = S.schema(s =>
|
|
454
|
+
{
|
|
455
|
+
"sku": s.matches(S.string),
|
|
456
|
+
"name": s.matches(S.string),
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
expect(sourceOf(schema))->toEqual("convention")
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
testSync("declaration order is a guess only this side can make", () => {
|
|
463
|
+
// `customerName` is eligible and unconventional, so it wins by position —
|
|
464
|
+
// and a consumer told only "customerName" could not tell that from a
|
|
465
|
+
// declaration.
|
|
466
|
+
let schema = S.schema(s =>
|
|
467
|
+
{
|
|
468
|
+
"customerName": s.matches(S.string),
|
|
469
|
+
"note": s.matches(S.string),
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
expect(sourceOf(schema))->toEqual("position")
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
testSync("no candidate at all is the state saying it has no human field", () => {
|
|
476
|
+
let schema = S.schema(s =>
|
|
477
|
+
{
|
|
478
|
+
"orderId": s.matches(Reventless.Reference.to_("Order")),
|
|
479
|
+
"placedAt": s.matches(Reventless.DateTime.string),
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
expect(sourceOf(schema))->toEqual("fallback")
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
testSync("every built def states one", () => {
|
|
486
|
+
let stated =
|
|
487
|
+
Array.concat(structure.readModels, structure.stateViewSlices)->Array.every(q =>
|
|
488
|
+
q.labelFieldSource->Option.isSome
|
|
489
|
+
)
|
|
490
|
+
expect(stated)->toEqual(true)
|
|
491
|
+
})
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
describe("statusField — the shape rule", () => {
|
|
495
|
+
let statusOf = schema =>
|
|
496
|
+
Plugin_Structure.statusFieldFromStateSchema(~entityName="Test", schema->S.castToUnknown)
|
|
497
|
+
|
|
498
|
+
testSync("a `status` field holding a closed set of values is the lifecycle field", () => {
|
|
499
|
+
let schema = S.schema(s =>
|
|
500
|
+
{
|
|
501
|
+
"status": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
|
|
502
|
+
}
|
|
503
|
+
)
|
|
504
|
+
expect(statusOf(schema))->toEqual(Some("status"))
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
testSync("an optional one counts too", () => {
|
|
508
|
+
let schema = S.schema(s =>
|
|
509
|
+
{
|
|
510
|
+
"status": s.matches(S.option(S.union([S.literal("Placed"), S.literal("Shipped")]))),
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
expect(statusOf(schema))->toEqual(Some("status"))
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
testSync("free text named `status` is not a lifecycle", () => {
|
|
517
|
+
// `allowedStates` filtering needs states to compare against; a string
|
|
518
|
+
// field named `status` gives a command menu nothing to match.
|
|
519
|
+
let schema = S.schema(s => {"status": s.matches(S.string)})
|
|
520
|
+
expect(statusOf(schema))->toEqual(None)
|
|
521
|
+
})
|
|
522
|
+
})
|
|
523
|
+
|
|
278
524
|
// Phase 4: queryableDef.schema must carry x-reventless-* extension keys for
|
|
279
525
|
// annotated state types. Plugin_Structure now uses SuryToJsonSchema.deriveObjectSchema
|
|
280
526
|
// (annotation-aware) instead of S.toJSONSchema (metadata-blind).
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
3
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
5
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
5
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
8
|
+
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
|
|
9
|
+
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
10
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
6
11
|
import * as PsShipOrder$ReventlessCore from "./StateChangeSlice/PsShipOrder.res.mjs";
|
|
7
12
|
import * as PsOrdersView$ReventlessCore from "./StateViewSlice/PsOrdersView.res.mjs";
|
|
8
13
|
import * as PsPlaceOrder$ReventlessCore from "./StateChangeSlice/PsPlaceOrder.res.mjs";
|
|
@@ -395,6 +400,218 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
395
400
|
]);
|
|
396
401
|
});
|
|
397
402
|
});
|
|
403
|
+
globalThis.describe("labelField — the shape rule", () => {
|
|
404
|
+
let labelOf = (entityNameOpt, schema) => {
|
|
405
|
+
let entityName = entityNameOpt !== undefined ? entityNameOpt : "Test";
|
|
406
|
+
let r = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema(entityName, schema);
|
|
407
|
+
return [
|
|
408
|
+
r.field,
|
|
409
|
+
r.searchableFields
|
|
410
|
+
];
|
|
411
|
+
};
|
|
412
|
+
globalThis.test("a DateTime is not a name — a state whose only string is one falls to id", () => {
|
|
413
|
+
let schema = S.schema(s => ({
|
|
414
|
+
orderId: s.m(Reference$Reventless.to_(undefined, undefined, "Order")),
|
|
415
|
+
placedAt: s.m(DateTime$Reventless.string),
|
|
416
|
+
shippedAt: s.m(DateTime$Reventless.string)
|
|
417
|
+
}));
|
|
418
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
419
|
+
"id",
|
|
420
|
+
[]
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
globalThis.test("a DateTime is skipped, not fatal — a later plain string still wins", () => {
|
|
424
|
+
let schema = S.schema(s => ({
|
|
425
|
+
placedAt: s.m(DateTime$Reventless.string),
|
|
426
|
+
reference: s.m(S.string)
|
|
427
|
+
}));
|
|
428
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
429
|
+
"reference",
|
|
430
|
+
["reference"]
|
|
431
|
+
]);
|
|
432
|
+
});
|
|
433
|
+
globalThis.test("an optional string is still the entity's name", () => {
|
|
434
|
+
let schema = S.schema(s => ({
|
|
435
|
+
nickname: s.m(S.option(S.string))
|
|
436
|
+
}));
|
|
437
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
438
|
+
"nickname",
|
|
439
|
+
["nickname"]
|
|
440
|
+
]);
|
|
441
|
+
});
|
|
442
|
+
globalThis.test("a reference is not a name even when its field is not called `*Id`", () => {
|
|
443
|
+
let schema = S.schema(s => ({
|
|
444
|
+
customer: s.m(Reference$Reventless.to_(undefined, undefined, "Customer")),
|
|
445
|
+
note: s.m(S.string)
|
|
446
|
+
}));
|
|
447
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
448
|
+
"note",
|
|
449
|
+
["note"]
|
|
450
|
+
]);
|
|
451
|
+
});
|
|
452
|
+
globalThis.test("a DCB-tagged string is not a name", () => {
|
|
453
|
+
let schema = S.schema(s => ({
|
|
454
|
+
partition: s.m(DcbTag$Reventless.string),
|
|
455
|
+
note: s.m(S.string)
|
|
456
|
+
}));
|
|
457
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
458
|
+
"note",
|
|
459
|
+
["note"]
|
|
460
|
+
]);
|
|
461
|
+
});
|
|
462
|
+
globalThis.test("the `*Id` test is case-insensitive — `productID` is a reference", () => {
|
|
463
|
+
let schema = S.schema(s => ({
|
|
464
|
+
productID: s.m(S.string),
|
|
465
|
+
note: s.m(S.string)
|
|
466
|
+
}));
|
|
467
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
468
|
+
"note",
|
|
469
|
+
["note"]
|
|
470
|
+
]);
|
|
471
|
+
});
|
|
472
|
+
globalThis.test("a semantic-carrying string is a value, not a name", () => {
|
|
473
|
+
let schema = S.schema(s => ({
|
|
474
|
+
imageUrl: s.m(StorageRef$Reventless.forStore(undefined, "productImages")),
|
|
475
|
+
note: s.m(S.string)
|
|
476
|
+
}));
|
|
477
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
478
|
+
"note",
|
|
479
|
+
["note"]
|
|
480
|
+
]);
|
|
481
|
+
});
|
|
482
|
+
globalThis.test("a state whose only string is a semantic falls to id", () => {
|
|
483
|
+
let schema = S.schema(s => ({
|
|
484
|
+
imageUrl: s.m(StorageRef$Reventless.forStore(undefined, "productImages"))
|
|
485
|
+
}));
|
|
486
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
487
|
+
"id",
|
|
488
|
+
[]
|
|
489
|
+
]);
|
|
490
|
+
});
|
|
491
|
+
globalThis.test("`id` itself is still excluded by name", () => {
|
|
492
|
+
let schema = S.schema(s => ({
|
|
493
|
+
id: s.m(S.string),
|
|
494
|
+
note: s.m(S.string)
|
|
495
|
+
}));
|
|
496
|
+
globalThis.expect(labelOf(undefined, schema)).toEqual([
|
|
497
|
+
"note",
|
|
498
|
+
["note"]
|
|
499
|
+
]);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
globalThis.describe("labelField — the conventional-name rung", () => {
|
|
503
|
+
let labelOf = schema => {
|
|
504
|
+
let r = Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema("Test", schema);
|
|
505
|
+
return [
|
|
506
|
+
r.field,
|
|
507
|
+
r.searchableFields
|
|
508
|
+
];
|
|
509
|
+
};
|
|
510
|
+
globalThis.test("a field named `name` beats an earlier string", () => {
|
|
511
|
+
let schema = S.schema(s => ({
|
|
512
|
+
sku: s.m(S.string),
|
|
513
|
+
name: s.m(S.string)
|
|
514
|
+
}));
|
|
515
|
+
globalThis.expect(labelOf(schema)).toEqual([
|
|
516
|
+
"name",
|
|
517
|
+
["name"]
|
|
518
|
+
]);
|
|
519
|
+
});
|
|
520
|
+
globalThis.test("`title` counts too, case-insensitively", () => {
|
|
521
|
+
let schema = S.schema(s => ({
|
|
522
|
+
body: s.m(S.string),
|
|
523
|
+
Title: s.m(S.string)
|
|
524
|
+
}));
|
|
525
|
+
globalThis.expect(labelOf(schema)).toEqual([
|
|
526
|
+
"Title",
|
|
527
|
+
["Title"]
|
|
528
|
+
]);
|
|
529
|
+
});
|
|
530
|
+
globalThis.test("the match is exact — `customerName` is a customer's name, not this record's", () => {
|
|
531
|
+
let schema = S.schema(s => ({
|
|
532
|
+
customerName: s.m(S.string),
|
|
533
|
+
note: s.m(S.string)
|
|
534
|
+
}));
|
|
535
|
+
globalThis.expect(labelOf(schema)).toEqual([
|
|
536
|
+
"customerName",
|
|
537
|
+
["customerName"]
|
|
538
|
+
]);
|
|
539
|
+
});
|
|
540
|
+
globalThis.test("an optional `name` still outranks an earlier string", () => {
|
|
541
|
+
let schema = S.schema(s => ({
|
|
542
|
+
sku: s.m(S.string),
|
|
543
|
+
name: s.m(S.option(S.string))
|
|
544
|
+
}));
|
|
545
|
+
globalThis.expect(labelOf(schema)).toEqual([
|
|
546
|
+
"name",
|
|
547
|
+
["name"]
|
|
548
|
+
]);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
globalThis.describe("labelFieldSource — which rung answered", () => {
|
|
552
|
+
let sourceOf = schema => Plugin_Structure$ReventlessCore.labelFieldSourceToString(Plugin_Structure$ReventlessCore.labelFieldsFromStateSchema("Test", schema).source);
|
|
553
|
+
globalThis.test("a @displayName spec is a declaration", () => {
|
|
554
|
+
let customers = structure.stateViewSlices[2];
|
|
555
|
+
globalThis.expect([
|
|
556
|
+
customers.labelField,
|
|
557
|
+
customers.labelFieldSource
|
|
558
|
+
]).toEqual([
|
|
559
|
+
"displayName",
|
|
560
|
+
"annotation"
|
|
561
|
+
]);
|
|
562
|
+
});
|
|
563
|
+
globalThis.test("a field named `name` is a guess the consumer can also make", () => {
|
|
564
|
+
let schema = S.schema(s => ({
|
|
565
|
+
sku: s.m(S.string),
|
|
566
|
+
name: s.m(S.string)
|
|
567
|
+
}));
|
|
568
|
+
globalThis.expect(sourceOf(schema)).toEqual("convention");
|
|
569
|
+
});
|
|
570
|
+
globalThis.test("declaration order is a guess only this side can make", () => {
|
|
571
|
+
let schema = S.schema(s => ({
|
|
572
|
+
customerName: s.m(S.string),
|
|
573
|
+
note: s.m(S.string)
|
|
574
|
+
}));
|
|
575
|
+
globalThis.expect(sourceOf(schema)).toEqual("position");
|
|
576
|
+
});
|
|
577
|
+
globalThis.test("no candidate at all is the state saying it has no human field", () => {
|
|
578
|
+
let schema = S.schema(s => ({
|
|
579
|
+
orderId: s.m(Reference$Reventless.to_(undefined, undefined, "Order")),
|
|
580
|
+
placedAt: s.m(DateTime$Reventless.string)
|
|
581
|
+
}));
|
|
582
|
+
globalThis.expect(sourceOf(schema)).toEqual("fallback");
|
|
583
|
+
});
|
|
584
|
+
globalThis.test("every built def states one", () => {
|
|
585
|
+
let stated = structure.readModels.concat(structure.stateViewSlices).every(q => Stdlib_Option.isSome(q.labelFieldSource));
|
|
586
|
+
globalThis.expect(stated).toEqual(true);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
globalThis.describe("statusField — the shape rule", () => {
|
|
590
|
+
globalThis.test("a `status` field holding a closed set of values is the lifecycle field", () => {
|
|
591
|
+
let schema = S.schema(s => ({
|
|
592
|
+
status: s.m(S.union([
|
|
593
|
+
S.literal("Placed"),
|
|
594
|
+
S.literal("Shipped")
|
|
595
|
+
]))
|
|
596
|
+
}));
|
|
597
|
+
globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual("status");
|
|
598
|
+
});
|
|
599
|
+
globalThis.test("an optional one counts too", () => {
|
|
600
|
+
let schema = S.schema(s => ({
|
|
601
|
+
status: s.m(S.option(S.union([
|
|
602
|
+
S.literal("Placed"),
|
|
603
|
+
S.literal("Shipped")
|
|
604
|
+
])))
|
|
605
|
+
}));
|
|
606
|
+
globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual("status");
|
|
607
|
+
});
|
|
608
|
+
globalThis.test("free text named `status` is not a lifecycle", () => {
|
|
609
|
+
let schema = S.schema(s => ({
|
|
610
|
+
status: s.m(S.string)
|
|
611
|
+
}));
|
|
612
|
+
globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual(undefined);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
398
615
|
globalThis.describe("queryableDef.schema propagates x-reventless-* annotations", () => {
|
|
399
616
|
let getProperty = (json, key) => {
|
|
400
617
|
let obj = Stdlib_JSON.Decode.object(json);
|