@reventlessdev/reventless-core 3.0.0-alpha.190 → 3.0.0-alpha.192
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 +5 -5
- package/src/admin/Platform_Admin.res +2 -2
- package/src/admin/Platform_Admin.res.mjs +6 -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/Aggregate/Aggregate.res +7 -7
- package/src/components/Aggregate/Aggregate.res.mjs +7 -2
- package/src/components/Aggregate/Aggregate_Builder.res +4 -1
- package/src/components/Aggregate/Aggregate_Builder.res.mjs +11 -3
- package/src/components/Builder_Helpers.res +37 -4
- package/src/components/Builder_Helpers.res.mjs +24 -2
- package/src/components/Dcb/Dcb_Builder.res +2 -2
- package/src/components/Dcb/Dcb_Builder.res.mjs +4 -4
- package/src/components/SideEffectHandler/SideEffectHandler.res +11 -0
- package/src/components/SideEffectHandler/SideEffectHandler_Builder.res +2 -0
- package/src/components/SideEffectHandler/SideEffectHandler_Builder.res.mjs +3 -1
- package/src/components/Task/Task_Builder.res +11 -0
- package/src/components/Task/Task_Builder.res.mjs +2 -0
- package/src/plugin/component/Capability_Inference.res +68 -0
- package/src/plugin/component/Capability_Inference.res.mjs +79 -0
- package/src/plugin/component/Plugin_Builder.res +2 -2
- package/src/plugin/component/Plugin_Builder.res.mjs +4 -4
- package/src/plugin/component/Plugin_Helpers.res +18 -7
- package/src/plugin/component/Plugin_Helpers.res.mjs +20 -1
- package/src/plugin/component/Plugin_Structure.res +215 -79
- package/src/plugin/component/Plugin_Structure.res.mjs +195 -50
- 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 +391 -0
- package/tests/plugin/PluginStructureTest.res.mjs +355 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res +33 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +64 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
6
|
+
|
|
7
|
+
let refLikeNames = [
|
|
8
|
+
"file",
|
|
9
|
+
"attachment",
|
|
10
|
+
"upload"
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
let refLikeSuffixes = [
|
|
14
|
+
"storageref",
|
|
15
|
+
"fileref",
|
|
16
|
+
"attachmentref"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
let imageLikeNames = [
|
|
20
|
+
"image",
|
|
21
|
+
"imageurl",
|
|
22
|
+
"photo",
|
|
23
|
+
"photourl",
|
|
24
|
+
"avatar",
|
|
25
|
+
"avatarurl",
|
|
26
|
+
"thumbnail"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function nameMatches(field) {
|
|
30
|
+
let lower = field.toLowerCase();
|
|
31
|
+
if (refLikeNames.includes(lower) || imageLikeNames.includes(lower)) {
|
|
32
|
+
return true;
|
|
33
|
+
} else {
|
|
34
|
+
return refLikeSuffixes.some(suffix => lower.endsWith(suffix));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function warningsFromProperties(component, properties) {
|
|
39
|
+
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
40
|
+
let fieldSchema = param[1];
|
|
41
|
+
let field = param[0];
|
|
42
|
+
if (fieldSchema.type === "string" && field !== "TAG" && nameMatches(field) && Stdlib_Option.isNone(StorageRef$Reventless.getStore(fieldSchema))) {
|
|
43
|
+
return {
|
|
44
|
+
component: component,
|
|
45
|
+
field: field
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scanSchema(component, schema) {
|
|
52
|
+
let fromVariant = v => {
|
|
53
|
+
if (v.type === "object") {
|
|
54
|
+
return warningsFromProperties(component, v.properties);
|
|
55
|
+
} else {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
if (schema.type === "union") {
|
|
60
|
+
return schema.anyOf.flatMap(fromVariant);
|
|
61
|
+
} else {
|
|
62
|
+
return fromVariant(schema);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function message(w) {
|
|
67
|
+
return w.component + `.` + w.field + ` looks like a stored-object reference but declares no store — annotate it \`@storageRef("<store>")\` so the deployment provisions the store it needs, or rename the field if it holds an external URL. Heuristic matches are never provisioned.`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
refLikeNames,
|
|
72
|
+
refLikeSuffixes,
|
|
73
|
+
imageLikeNames,
|
|
74
|
+
nameMatches,
|
|
75
|
+
warningsFromProperties,
|
|
76
|
+
scanSchema,
|
|
77
|
+
message,
|
|
78
|
+
}
|
|
79
|
+
/* StorageRef-Reventless Not a pure module */
|
|
@@ -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";
|
|
@@ -434,6 +434,8 @@ let createTasks = (
|
|
|
434
434
|
~opts=Some(opts),
|
|
435
435
|
))
|
|
436
436
|
)
|
|
437
|
+
// Every task is constructed, so every side-effect handler has registered.
|
|
438
|
+
finishTasks()
|
|
437
439
|
tasksOutputs.contents
|
|
438
440
|
}
|
|
439
441
|
|
|
@@ -1246,23 +1248,32 @@ let serializeTasksOutputs = (pluginOutputs: Plugin.outputs): Pulumi.Output.t<JSO
|
|
|
1246
1248
|
// Serialize event mappers from aggregates to JSON array for the "eventMappers" stack export.
|
|
1247
1249
|
let serializeEventMappersOutputs = (pluginOutputs: Plugin.outputs): Pulumi.Output.t<JSON.t> =>
|
|
1248
1250
|
pluginOutputs.aggregates->Pulumi.Output.flatMap(aggregates =>
|
|
1251
|
+
// Resolve each `eventMapper` through its own Output before collecting, and only
|
|
1252
|
+
// then drop the absent ones. `Output.all` deeply unwraps the *contents* of its
|
|
1253
|
+
// elements, so collecting first would hand `toResolvedOutputs` a record whose
|
|
1254
|
+
// fields Pulumi has already resolved — see `serializePlainDictExport` above for
|
|
1255
|
+
// the same hazard. Absence is carried as `None` through the collection instead.
|
|
1249
1256
|
aggregates
|
|
1250
1257
|
->Dict.valuesToArray
|
|
1251
|
-
->Array.
|
|
1252
|
-
agg.eventMapper->
|
|
1253
|
-
|
|
1258
|
+
->Array.map((agg: Aggregate.outputs) =>
|
|
1259
|
+
agg.eventMapper->Pulumi.Output.flatMap(em =>
|
|
1260
|
+
switch em {
|
|
1261
|
+
| Some(em) =>
|
|
1254
1262
|
em
|
|
1255
1263
|
->EventMapper.toResolvedOutputs
|
|
1256
1264
|
->Pulumi.Output.apply(resolved =>
|
|
1257
|
-
|
|
1258
|
-
|
|
1265
|
+
Some(
|
|
1266
|
+
resolved->S.reverseConvertToJsonOrThrow(
|
|
1267
|
+
ReventlessInterop.EventMapper.resolvedOutputsSchema,
|
|
1268
|
+
),
|
|
1259
1269
|
)
|
|
1260
1270
|
)
|
|
1261
|
-
)
|
|
1271
|
+
| None => Pulumi.Output.make(None)
|
|
1272
|
+
}
|
|
1262
1273
|
)
|
|
1263
1274
|
)
|
|
1264
1275
|
->Pulumi.Output.all
|
|
1265
|
-
->Pulumi.Output.apply(arr => arr->JSON.Encode.array)
|
|
1276
|
+
->Pulumi.Output.apply(arr => arr->Array.keepSome->JSON.Encode.array)
|
|
1266
1277
|
)
|
|
1267
1278
|
|
|
1268
1279
|
// Export platform admin component outputs as individual top-level Pulumi stack exports.
|
|
@@ -248,6 +248,7 @@ function createTasks(tasks, aggregatesOutputs, scheduler, schedulerRoleUrn, publ
|
|
|
248
248
|
let bucketName = bucketNameOpt !== undefined ? bucketNameOpt : "Bucket";
|
|
249
249
|
return ResourceQueryRuntime$ReventlessCore.bucketNameOfTaskExn(tasksOutputs.contents, taskName, bucketName);
|
|
250
250
|
}, scheduler, schedulerRoleUrn, publishToAggregates, queryEngine, resourceNaming, aggregatesOutputs, componentRuntime[SpecificTask.Spec.name], opts)));
|
|
251
|
+
Builder_Helpers$ReventlessCore.finishTasks();
|
|
251
252
|
return tasksOutputs.contents;
|
|
252
253
|
}
|
|
253
254
|
|
|
@@ -627,7 +628,13 @@ function serializeTasksOutputs(pluginOutputs) {
|
|
|
627
628
|
}
|
|
628
629
|
|
|
629
630
|
function serializeEventMappersOutputs(pluginOutputs) {
|
|
630
|
-
return Output$Pulumi.flatMap(pluginOutputs.aggregates, aggregates => Pulumi.all(
|
|
631
|
+
return Output$Pulumi.flatMap(pluginOutputs.aggregates, aggregates => Pulumi.all(Object.values(aggregates).map(agg => Output$Pulumi.flatMap(agg.eventMapper, em => {
|
|
632
|
+
if (em !== undefined) {
|
|
633
|
+
return EventMapper$ReventlessCore.toResolvedOutputs(em).apply(resolved => S.reverseConvertToJsonOrThrow(resolved, EventMapper$ReventlessInterop.resolvedOutputsSchema));
|
|
634
|
+
} else {
|
|
635
|
+
return Pulumi.output(undefined);
|
|
636
|
+
}
|
|
637
|
+
}))).apply(Stdlib_Array.keepSome));
|
|
631
638
|
}
|
|
632
639
|
|
|
633
640
|
function exportPlatformOutputs(extensionPointsOutputs, aggregatesOutputs, readModelsOutputs, dcbEventLogOutputs, stateChangeSlicesOutputs, stateViewSlicesOutputs, automationSlicesOutputs, outboundTranslationSlicesOutputs, inboundTranslationSlicesOutputs) {
|
|
@@ -913,6 +920,14 @@ let finishAggregates = Builder_Helpers$ReventlessCore.finishAggregates;
|
|
|
913
920
|
|
|
914
921
|
let addEventMappers = Builder_Helpers$ReventlessCore.addEventMappers;
|
|
915
922
|
|
|
923
|
+
let taskSideEffectGates = Builder_Helpers$ReventlessCore.taskSideEffectGates;
|
|
924
|
+
|
|
925
|
+
let taskSideEffectFinishFns = Builder_Helpers$ReventlessCore.taskSideEffectFinishFns;
|
|
926
|
+
|
|
927
|
+
let registerTaskSideEffectHandler = Builder_Helpers$ReventlessCore.registerTaskSideEffectHandler;
|
|
928
|
+
|
|
929
|
+
let finishTasks = Builder_Helpers$ReventlessCore.finishTasks;
|
|
930
|
+
|
|
916
931
|
let readModelNamesForSourceName = Builder_Helpers$ReventlessCore.readModelNamesForSourceName;
|
|
917
932
|
|
|
918
933
|
let publishToReadModels = Builder_Helpers$ReventlessCore.publishToReadModels;
|
|
@@ -968,6 +983,10 @@ export {
|
|
|
968
983
|
createAggregatesWithoutEventMappers,
|
|
969
984
|
finishAggregates,
|
|
970
985
|
addEventMappers,
|
|
986
|
+
taskSideEffectGates,
|
|
987
|
+
taskSideEffectFinishFns,
|
|
988
|
+
registerTaskSideEffectHandler,
|
|
989
|
+
finishTasks,
|
|
971
990
|
readModelNamesForSourceName,
|
|
972
991
|
publishToReadModels,
|
|
973
992
|
finishReadModels,
|
|
@@ -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
|
|
|
@@ -398,23 +481,34 @@ let make = (
|
|
|
398
481
|
// An unqualified store belongs to the declaring plugin, so every collected
|
|
399
482
|
// entry is qualified to `{plugin}.{store}` — one shape, and the string is
|
|
400
483
|
// directly the store's identity. Many fields legitimately name one store, so
|
|
401
|
-
// the
|
|
402
|
-
|
|
403
|
-
|
|
484
|
+
// the key set is deduplicated.
|
|
485
|
+
//
|
|
486
|
+
// Each collected entry keeps its declaration site `(component, field)` as
|
|
487
|
+
// provenance, and `requiredStores` is derived from the collected entries —
|
|
488
|
+
// one walk, so the capability manifest's provenance and the deploy's key set
|
|
489
|
+
// cannot come from different readings of the schemas.
|
|
490
|
+
|
|
491
|
+
let storesFromProperties = (~component, properties: dict<S.t<unknown>>): array<
|
|
492
|
+
Reventless.Plugin.requiredStoreDeclaration,
|
|
493
|
+
> =>
|
|
404
494
|
properties
|
|
405
495
|
->Dict.toArray
|
|
406
|
-
->Array.filterMap(((
|
|
407
|
-
Reventless.StorageRef.getStore(fieldSchema)->Option.map(target =>
|
|
408
|
-
target.plugin->Option.getOr(name) ++ "." ++ target.store
|
|
409
|
-
|
|
496
|
+
->Array.filterMap(((field, fieldSchema)) =>
|
|
497
|
+
Reventless.StorageRef.getStore(fieldSchema)->Option.map(target => {
|
|
498
|
+
Reventless.Plugin.store: target.plugin->Option.getOr(name) ++ "." ++ target.store,
|
|
499
|
+
component,
|
|
500
|
+
field,
|
|
501
|
+
})
|
|
410
502
|
)
|
|
411
503
|
|
|
412
504
|
// Commands and events are unions of per-variant objects; state is a single
|
|
413
505
|
// object. Both reduce to "the properties of every object in this schema".
|
|
414
|
-
let storesFromSchema = (schema: S.t<unknown>): array<
|
|
506
|
+
let storesFromSchema = (~component, schema: S.t<unknown>): array<
|
|
507
|
+
Reventless.Plugin.requiredStoreDeclaration,
|
|
508
|
+
> => {
|
|
415
509
|
let fromVariant = v =>
|
|
416
510
|
switch v {
|
|
417
|
-
| S.Object({properties}) => storesFromProperties(properties)
|
|
511
|
+
| S.Object({properties}) => storesFromProperties(~component, properties)
|
|
418
512
|
| _ => []
|
|
419
513
|
}
|
|
420
514
|
switch schema {
|
|
@@ -423,37 +517,82 @@ let make = (
|
|
|
423
517
|
}
|
|
424
518
|
}
|
|
425
519
|
|
|
426
|
-
let
|
|
520
|
+
let compareDeclarations = (
|
|
521
|
+
a: Reventless.Plugin.requiredStoreDeclaration,
|
|
522
|
+
b: Reventless.Plugin.requiredStoreDeclaration,
|
|
523
|
+
) => {
|
|
524
|
+
let byStore = String.compare(a.store, b.store)
|
|
525
|
+
if byStore != Ordering.equal {
|
|
526
|
+
byStore
|
|
527
|
+
} else {
|
|
528
|
+
let byComponent = String.compare(a.component, b.component)
|
|
529
|
+
if byComponent != Ordering.equal {
|
|
530
|
+
byComponent
|
|
531
|
+
} else {
|
|
532
|
+
String.compare(a.field, b.field)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// The (component, schema) sites the store walk reads. One list feeds both the
|
|
538
|
+
// declared-store collection and the heuristic lint below, so a field is read
|
|
539
|
+
// exactly once: either its declaration is collected, or its name is eligible
|
|
540
|
+
// to warn — never a third reading that could disagree with both.
|
|
541
|
+
let storeDeclarationSites: array<(string, S.t<unknown>)> =
|
|
427
542
|
[
|
|
428
|
-
aggregates->Array.flatMap((module(A: ReventlessInfra.Aggregate.T with type api = api)) =>
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
),
|
|
440
|
-
|
|
441
|
-
storesFromSchema(SVS.Spec.stateSchema->S.castToUnknown)
|
|
442
|
-
),
|
|
443
|
-
readModels->Array.flatMap((
|
|
543
|
+
aggregates->Array.flatMap((module(A: ReventlessInfra.Aggregate.T with type api = api)) => [
|
|
544
|
+
(A.Spec.name, A.Spec.commandSchema->S.castToUnknown),
|
|
545
|
+
(A.Spec.name, A.Spec.eventSchema->S.castToUnknown),
|
|
546
|
+
]),
|
|
547
|
+
stateChangeSlices->Array.flatMap((module(SCS: ReventlessInfra.StateChangeSlice.T)) => [
|
|
548
|
+
(SCS.Spec.name, SCS.Spec.commandSchema->S.castToUnknown),
|
|
549
|
+
(SCS.Spec.name, SCS.Spec.eventSchema->S.castToUnknown),
|
|
550
|
+
]),
|
|
551
|
+
stateViewSlices->Array.map((module(SVS: ReventlessInfra.StateViewSlice.T)) => (
|
|
552
|
+
SVS.Spec.name,
|
|
553
|
+
SVS.Spec.stateSchema->S.castToUnknown,
|
|
554
|
+
)),
|
|
555
|
+
readModels->Array.map((
|
|
444
556
|
module(R: ReventlessInfra.ReadModel.T with type api = api and type role = role),
|
|
445
|
-
) =>
|
|
446
|
-
automationSlices->Array.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
557
|
+
) => (R.Spec.name, R.Spec.stateSchema->S.castToUnknown)),
|
|
558
|
+
automationSlices->Array.map((module(AS: ReventlessInfra.AutomationSlice.T)) => (
|
|
559
|
+
AS.Spec.name,
|
|
560
|
+
AS.Spec.commandSchema->S.castToUnknown,
|
|
561
|
+
)),
|
|
562
|
+
inboundTranslationSlices->Array.map((
|
|
450
563
|
module(ITS: ReventlessInfra.InboundTranslationSlice.T),
|
|
451
|
-
) =>
|
|
452
|
-
]
|
|
453
|
-
|
|
564
|
+
) => (ITS.Spec.name, ITS.Spec.commandSchema->S.castToUnknown)),
|
|
565
|
+
]->Array.flat
|
|
566
|
+
|
|
567
|
+
// Sorted so the emitted manifest is deterministic; identical triples collapse
|
|
568
|
+
// (one store named by a component's command field and again by its event
|
|
569
|
+
// field under the same field name is one declaration site).
|
|
570
|
+
let requiredStoreDeclarations =
|
|
571
|
+
storeDeclarationSites
|
|
572
|
+
->Array.flatMap(((component, schema)) => storesFromSchema(~component, schema))
|
|
573
|
+
->Array.toSorted(compareDeclarations)
|
|
574
|
+
->Array.reduce([], (acc, d) =>
|
|
575
|
+
switch acc->Array.last {
|
|
576
|
+
| Some(prev) if prev == d => acc
|
|
577
|
+
| _ => acc->Array.concat([d])
|
|
578
|
+
}
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
let requiredStores =
|
|
582
|
+
requiredStoreDeclarations
|
|
583
|
+
->Array.map(d => d.store)
|
|
454
584
|
->Belt.Set.String.fromArray
|
|
455
585
|
->Belt.Set.String.toArray
|
|
456
586
|
|
|
587
|
+
// Heuristic-only matches — a field *named* like a stored-object ref with no
|
|
588
|
+
// `@storageRef` — warn and provision nothing. Declaration outranks inference;
|
|
589
|
+
// the warning names the annotation that would settle it.
|
|
590
|
+
storeDeclarationSites->Array.forEach(((component, schema)) =>
|
|
591
|
+
Capability_Inference.scanSchema(~component, schema)->Array.forEach(w =>
|
|
592
|
+
log.warn(~comp="Plugin_Structure", Capability_Inference.message(w))
|
|
593
|
+
)
|
|
594
|
+
)
|
|
595
|
+
|
|
457
596
|
// ── Build queryable defs ───────────────────────────────────────────────────
|
|
458
597
|
//
|
|
459
598
|
// Internal ReadModels and StateViewSlices (marked `@@reventless.visibility(Internal)`)
|
|
@@ -475,10 +614,7 @@ let make = (
|
|
|
475
614
|
) => {
|
|
476
615
|
let qf = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
|
|
477
616
|
let stateSchema = R.Spec.stateSchema->S.castToUnknown
|
|
478
|
-
let
|
|
479
|
-
~entityName=R.Spec.name,
|
|
480
|
-
stateSchema,
|
|
481
|
-
)
|
|
617
|
+
let label = labelFieldsFromStateSchema(~entityName=R.Spec.name, stateSchema)
|
|
482
618
|
// The events this read model projects from, qualified to the plugin's event ids so
|
|
483
619
|
// they match the producers' nodes in the graph. Was empty — which dropped projection
|
|
484
620
|
// edges for any event reaching the read model via a DCB-log-sourced mapping (a classic
|
|
@@ -490,9 +626,10 @@ let make = (
|
|
|
490
626
|
schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
491
627
|
consumedEventTypes: consumed,
|
|
492
628
|
linkedWriteSide: linkedWriteSideFor(consumed),
|
|
493
|
-
labelField,
|
|
494
|
-
searchableFields,
|
|
495
|
-
|
|
629
|
+
labelField: label.field,
|
|
630
|
+
searchableFields: label.searchableFields,
|
|
631
|
+
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
632
|
+
statusField: statusFieldFromStateSchema(~entityName=R.Spec.name, stateSchema),
|
|
496
633
|
visibility: visibilityTag(R.Spec.visibility),
|
|
497
634
|
chapter: chapterOf(R.Spec.name),
|
|
498
635
|
}: Reventless.Plugin.queryableDef)
|
|
@@ -503,19 +640,17 @@ let make = (
|
|
|
503
640
|
let qf = Api_Naming.queryFieldNamesForStateView(~plugin=name, ~viewName=SVS.Spec.name)
|
|
504
641
|
let (_, consumed) = svsConsumed->Array.getUnsafe(i)
|
|
505
642
|
let stateSchema = SVS.Spec.stateSchema->S.castToUnknown
|
|
506
|
-
let
|
|
507
|
-
~entityName=SVS.Spec.name,
|
|
508
|
-
stateSchema,
|
|
509
|
-
)
|
|
643
|
+
let label = labelFieldsFromStateSchema(~entityName=SVS.Spec.name, stateSchema)
|
|
510
644
|
({
|
|
511
645
|
Reventless.Plugin.name: SVS.Spec.name,
|
|
512
646
|
queryField: qf.listFieldName,
|
|
513
647
|
schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
514
648
|
consumedEventTypes: consumed,
|
|
515
649
|
linkedWriteSide: linkedWriteSideFor(consumed),
|
|
516
|
-
labelField,
|
|
517
|
-
searchableFields,
|
|
518
|
-
|
|
650
|
+
labelField: label.field,
|
|
651
|
+
searchableFields: label.searchableFields,
|
|
652
|
+
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
653
|
+
statusField: statusFieldFromStateSchema(~entityName=SVS.Spec.name, stateSchema),
|
|
519
654
|
visibility: visibilityTag(SVS.Spec.visibility),
|
|
520
655
|
chapter: chapterOf(SVS.Spec.name),
|
|
521
656
|
}: Reventless.Plugin.queryableDef)
|
|
@@ -676,5 +811,6 @@ let make = (
|
|
|
676
811
|
extensions: extensionDefs,
|
|
677
812
|
extensionPoints: Some(extensionPointDefs),
|
|
678
813
|
requiredStores: Some(requiredStores),
|
|
814
|
+
requiredStoreDeclarations: Some(requiredStoreDeclarations),
|
|
679
815
|
}
|
|
680
816
|
}
|