@reventlessdev/reventless-core 3.0.0-alpha.189 → 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 +17 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin.res +2 -2
- package/src/admin/Platform_Admin.res.mjs +2 -2
- package/src/admin/Platform_Admin_Structure.res +4 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +5 -1
- 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 +192 -45
- package/src/plugin/component/Plugin_Structure.res.mjs +139 -39
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +5 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +15 -4
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +2 -1
- package/tests/plugin/PluginStructureTest.res +292 -0
- package/tests/plugin/PluginStructureTest.res.mjs +268 -0
- package/tests/plugin/StateChangeSlice/PsAttachInvoice.res +39 -0
- package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +63 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
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
|
+
|
|
16
|
+
# 3.0.0-alpha.190 (2026-07-28)
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **platform:** provision object stores from the fields that declare them ([b5e2a1e](https://github.com/ReventlessDev/reventless-core/commit/b5e2a1ec88099941c113e4963f9c4b346b96b0d6))
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
# 3.0.0-alpha.189 (2026-07-28)
|
|
7
24
|
|
|
8
25
|
**Note:** Version bump only for package @reventlessdev/reventless-core
|
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",
|
|
34
|
-
"@reventlessdev/rescript-
|
|
33
|
+
"@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
|
|
35
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
36
|
-
"@reventlessdev/rescript-
|
|
35
|
+
"@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
|
|
37
36
|
"@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
|
|
37
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
38
38
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
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,4 +129,5 @@ let structure: pluginStructure = {
|
|
|
126
129
|
inboundTranslationSlices: [],
|
|
127
130
|
extensions: [],
|
|
128
131
|
extensionPoints: None,
|
|
132
|
+
requiredStores: None,
|
|
129
133
|
}
|
|
@@ -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
|
|
@@ -166,7 +169,8 @@ let structure = {
|
|
|
166
169
|
outboundTranslationSlices: structure_outboundTranslationSlices,
|
|
167
170
|
inboundTranslationSlices: structure_inboundTranslationSlices,
|
|
168
171
|
extensions: structure_extensions,
|
|
169
|
-
extensionPoints: undefined
|
|
172
|
+
extensionPoints: undefined,
|
|
173
|
+
requiredStores: undefined
|
|
170
174
|
};
|
|
171
175
|
|
|
172
176
|
let pluginName = pluginId;
|
|
@@ -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
|
|
|
@@ -387,6 +470,73 @@ let make = (
|
|
|
387
470
|
}
|
|
388
471
|
}
|
|
389
472
|
|
|
473
|
+
// ── Declared object stores ─────────────────────────────────────────────────
|
|
474
|
+
//
|
|
475
|
+
// A field typed as a storage ref states that the deployment needs the named
|
|
476
|
+
// store to exist. Read that declaration the same way `Reference.getTarget` is
|
|
477
|
+
// read for entity references, so the requirement travels with the plugin's
|
|
478
|
+
// structure instead of living only inside a field's schema, where the deploy
|
|
479
|
+
// would have to re-walk every component to find it.
|
|
480
|
+
//
|
|
481
|
+
// An unqualified store belongs to the declaring plugin, so every collected
|
|
482
|
+
// entry is qualified to `{plugin}.{store}` — one shape, and the string is
|
|
483
|
+
// directly the store's identity. Many fields legitimately name one store, so
|
|
484
|
+
// the result is deduplicated.
|
|
485
|
+
|
|
486
|
+
let storesFromProperties = (properties: dict<S.t<unknown>>): array<string> =>
|
|
487
|
+
properties
|
|
488
|
+
->Dict.toArray
|
|
489
|
+
->Array.filterMap(((_, fieldSchema)) =>
|
|
490
|
+
Reventless.StorageRef.getStore(fieldSchema)->Option.map(target =>
|
|
491
|
+
target.plugin->Option.getOr(name) ++ "." ++ target.store
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
// Commands and events are unions of per-variant objects; state is a single
|
|
496
|
+
// object. Both reduce to "the properties of every object in this schema".
|
|
497
|
+
let storesFromSchema = (schema: S.t<unknown>): array<string> => {
|
|
498
|
+
let fromVariant = v =>
|
|
499
|
+
switch v {
|
|
500
|
+
| S.Object({properties}) => storesFromProperties(properties)
|
|
501
|
+
| _ => []
|
|
502
|
+
}
|
|
503
|
+
switch schema {
|
|
504
|
+
| Union({anyOf}) => anyOf->Array.flatMap(fromVariant)
|
|
505
|
+
| other => fromVariant(other)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
let requiredStores =
|
|
510
|
+
[
|
|
511
|
+
aggregates->Array.flatMap((module(A: ReventlessInfra.Aggregate.T with type api = api)) =>
|
|
512
|
+
Array.concat(
|
|
513
|
+
storesFromSchema(A.Spec.commandSchema->S.castToUnknown),
|
|
514
|
+
storesFromSchema(A.Spec.eventSchema->S.castToUnknown),
|
|
515
|
+
)
|
|
516
|
+
),
|
|
517
|
+
stateChangeSlices->Array.flatMap((module(SCS: ReventlessInfra.StateChangeSlice.T)) =>
|
|
518
|
+
Array.concat(
|
|
519
|
+
storesFromSchema(SCS.Spec.commandSchema->S.castToUnknown),
|
|
520
|
+
storesFromSchema(SCS.Spec.eventSchema->S.castToUnknown),
|
|
521
|
+
)
|
|
522
|
+
),
|
|
523
|
+
stateViewSlices->Array.flatMap((module(SVS: ReventlessInfra.StateViewSlice.T)) =>
|
|
524
|
+
storesFromSchema(SVS.Spec.stateSchema->S.castToUnknown)
|
|
525
|
+
),
|
|
526
|
+
readModels->Array.flatMap((
|
|
527
|
+
module(R: ReventlessInfra.ReadModel.T with type api = api and type role = role),
|
|
528
|
+
) => storesFromSchema(R.Spec.stateSchema->S.castToUnknown)),
|
|
529
|
+
automationSlices->Array.flatMap((module(AS: ReventlessInfra.AutomationSlice.T)) =>
|
|
530
|
+
storesFromSchema(AS.Spec.commandSchema->S.castToUnknown)
|
|
531
|
+
),
|
|
532
|
+
inboundTranslationSlices->Array.flatMap((
|
|
533
|
+
module(ITS: ReventlessInfra.InboundTranslationSlice.T),
|
|
534
|
+
) => storesFromSchema(ITS.Spec.commandSchema->S.castToUnknown)),
|
|
535
|
+
]
|
|
536
|
+
->Array.flat
|
|
537
|
+
->Belt.Set.String.fromArray
|
|
538
|
+
->Belt.Set.String.toArray
|
|
539
|
+
|
|
390
540
|
// ── Build queryable defs ───────────────────────────────────────────────────
|
|
391
541
|
//
|
|
392
542
|
// Internal ReadModels and StateViewSlices (marked `@@reventless.visibility(Internal)`)
|
|
@@ -408,10 +558,7 @@ let make = (
|
|
|
408
558
|
) => {
|
|
409
559
|
let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
|
|
410
560
|
let stateSchema = R.Spec.stateSchema->S.castToUnknown
|
|
411
|
-
let
|
|
412
|
-
~entityName=R.Spec.name,
|
|
413
|
-
stateSchema,
|
|
414
|
-
)
|
|
561
|
+
let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
|
|
415
562
|
// The events this read model projects from, qualified to the plugin's event ids so
|
|
416
563
|
// they match the producers' nodes in the graph. Was empty — which dropped projection
|
|
417
564
|
// edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
|
|
@@ -423,9 +570,10 @@ let make = (
|
|
|
423
570
|
schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
424
571
|
consumedEventTypes: consumed,
|
|
425
572
|
linkedWriteSide: linkedWriteSideFor(consumed),
|
|
426
|
-
labelField,
|
|
427
|
-
searchableFields,
|
|
428
|
-
|
|
573
|
+
labelField: label.field,
|
|
574
|
+
searchableFields: label.searchableFields,
|
|
575
|
+
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
576
|
+
statusField: statusFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
|
|
429
577
|
visibility: visibilityTag(R.Spec.visibility),
|
|
430
578
|
chapter: chapterOf(R.Spec.name),
|
|
431
579
|
}: Reventless.Plugin.queryableDef)
|
|
@@ -436,19 +584,17 @@ let make = (
|
|
|
436
584
|
let qf = Api_Naming.queryFieldNamesForStateView(~plugin=name, ~viewName=SVS.Spec.name)
|
|
437
585
|
let (_, consumed) = svsConsumed->Array.getUnsafe(i)
|
|
438
586
|
let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
|
|
439
|
-
let
|
|
440
|
-
~entityName=SVS.Spec.name,
|
|
441
|
-
stateSchema,
|
|
442
|
-
)
|
|
587
|
+
let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
|
|
443
588
|
({
|
|
444
589
|
Reventless.Plugin.name: SVS.Spec.name,
|
|
445
590
|
queryField: qf.listFieldName,
|
|
446
591
|
schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
447
592
|
consumedEventTypes: consumed,
|
|
448
593
|
linkedWriteSide: linkedWriteSideFor(consumed),
|
|
449
|
-
labelField,
|
|
450
|
-
searchableFields,
|
|
451
|
-
|
|
594
|
+
labelField: label.field,
|
|
595
|
+
searchableFields: label.searchableFields,
|
|
596
|
+
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
597
|
+
statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
|
|
452
598
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
453
599
|
chapter: chapterOf(SVS.Spec.name),
|
|
454
600
|
}: Reventless.Plugin.queryableDef)
|
|
@@ -608,5 +754,6 @@ let make = (
|
|
|
608
754
|
inboundTranslationSlices: inboundTranslationSliceDefs,
|
|
609
755
|
extensions: extensionDefs,
|
|
610
756
|
extensionPoints: Some(extensionPointDefs),
|
|
757
|
+
requiredStores: Some(requiredStores),
|
|
611
758
|
}
|
|
612
759
|
}
|