@reventlessdev/reventless-core 3.0.0-alpha.222 → 3.0.0-alpha.223
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 +4 -4
- package/src/admin/Platform_ComponentDefinitionsApi.res +22 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/plugin/component/Plugin_Structure.res +22 -24
- package/src/plugin/component/Plugin_Structure.res.mjs +14 -16
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +55 -7
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +52 -4
- package/tests/plugin/PluginStructureTest.res +69 -0
- package/tests/plugin/PluginStructureTest.res.mjs +76 -0
- package/tests/plugin/StateChangeSlice/PsReserveStock.res +44 -0
- package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +67 -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.223 (2026-08-10)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** collect [@ref](https://github.com/ref) declared on an array field ([6f9e2fe](https://github.com/ReventlessDev/reventless-core/commit/6f9e2fef2568ad57a8bf2efbaa9cd4830a947f26))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **admin:** carry Internal queryables on their own manifest field ([f5aa03d](https://github.com/ReventlessDev/reventless-core/commit/f5aa03d60b0b2554d116fe9b3784d30f2c4ef356))
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
# 3.0.0-alpha.222 (2026-08-10)
|
|
7
17
|
|
|
8
18
|
**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.223",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
34
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
35
35
|
"@reventlessdev/rescript-node": "2.0.0-alpha.3",
|
|
36
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.3",
|
|
36
37
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
37
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.132",
|
|
39
|
-
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.3",
|
|
40
39
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
41
|
-
"@reventlessdev/reventless-
|
|
40
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.133",
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.107"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -30,7 +30,14 @@ let sdlTypes: array<string> = [
|
|
|
30
30
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
31
31
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
32
32
|
`type Platform_ExtensionDef {\n name: String!\n delegateNames: [String!]!\n eventTypes: [String!]!\n commandTypes: [String!]!\n}`,
|
|
33
|
-
`
|
|
33
|
+
// `internalQueryables` is the complement of the `readModels` / `stateViewSlices`
|
|
34
|
+
// filter, not an addition to either: it carries exactly the components the
|
|
35
|
+
// filter removed. A consumer enumerating surfaces reads the two filtered lists
|
|
36
|
+
// and is structurally unable to turn an Internal view into a page; a consumer
|
|
37
|
+
// resolving a reference target reads all three. Keeping it a separate field is
|
|
38
|
+
// what makes that separation structural rather than a filter every enumeration
|
|
39
|
+
// site has to remember.
|
|
40
|
+
`type Platform_ComponentDefinitionEntry {\n pluginId: String!\n readModels: [Platform_ReadSideDef!]!\n stateViewSlices: [Platform_ReadSideDef!]!\n internalQueryables: [Platform_ReadSideDef!]!\n stateChangeSlices: [Platform_WriteSideDef!]!\n aggregates: [Platform_WriteSideDef!]!\n automationSlices: [Platform_AutomationSliceDef!]!\n outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!\n inboundTranslationSlices: [Platform_InboundTranslationSliceDef!]!\n extensions: [Platform_ExtensionDef!]!\n}`,
|
|
34
41
|
]
|
|
35
42
|
|
|
36
43
|
let sdlQueryField: string = ` Platform_ComponentDefinitions: [Platform_ComponentDefinitionEntry!]!`
|
|
@@ -75,6 +82,13 @@ let encodeCommandDef = (c: commandDef): JSON.t =>
|
|
|
75
82
|
// Internal ReadModels / StateViewSlices are carried in pluginStructure for developer
|
|
76
83
|
// tooling but must stay out of the deployed AutoUI — the menu, drill-down pages and
|
|
77
84
|
// queryable defs are all derived from this response, so filter them here.
|
|
85
|
+
//
|
|
86
|
+
// Hidden from the menu is not the same question as denied as a reference target: a
|
|
87
|
+
// denormalised lookup view is precisely the thing an author wants referenceable but
|
|
88
|
+
// not navigable. The filtered lists answer the first question, `internalQueryables`
|
|
89
|
+
// (below) answers the second. Neither is a security boundary — an Internal view
|
|
90
|
+
// keeps its resolver and stays queryable, so naming it here publishes a fact the
|
|
91
|
+
// caller is already entitled to.
|
|
78
92
|
let isPublicQueryable = (q: queryableDef): bool => q.visibility != Some("Internal")
|
|
79
93
|
|
|
80
94
|
let encodeQueryableDef = (r: queryableDef): JSON.t =>
|
|
@@ -190,6 +204,13 @@ let encodePluginStructureEntry = (~pluginId: string, def: pluginStructure): JSON
|
|
|
190
204
|
->Array.map(encodeQueryableDef)
|
|
191
205
|
->JSON.Encode.array,
|
|
192
206
|
),
|
|
207
|
+
(
|
|
208
|
+
"internalQueryables",
|
|
209
|
+
Array.concat(def.readModels, def.stateViewSlices)
|
|
210
|
+
->Array.filter(q => !isPublicQueryable(q))
|
|
211
|
+
->Array.map(encodeQueryableDef)
|
|
212
|
+
->JSON.Encode.array,
|
|
213
|
+
),
|
|
193
214
|
("stateChangeSlices", def.stateChangeSlices->Array.map(encodeWritableDef)->JSON.Encode.array),
|
|
194
215
|
("aggregates", def.aggregates->Array.map(encodeWritableDef)->JSON.Encode.array),
|
|
195
216
|
(
|
|
@@ -15,7 +15,7 @@ let sdlTypes = [
|
|
|
15
15
|
`type Platform_OutboundTranslationSliceDef {\n name: String!\n consumedEventTypes: [String!]!\n inboundCommandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
16
16
|
`type Platform_InboundTranslationSliceDef {\n name: String!\n commandTypes: [String!]!\n targetName: String\n externalSystem: String\n chapter: String\n}`,
|
|
17
17
|
`type Platform_ExtensionDef {\n name: String!\n delegateNames: [String!]!\n eventTypes: [String!]!\n commandTypes: [String!]!\n}`,
|
|
18
|
-
`type Platform_ComponentDefinitionEntry {\n pluginId: String!\n readModels: [Platform_ReadSideDef!]!\n stateViewSlices: [Platform_ReadSideDef!]!\n stateChangeSlices: [Platform_WriteSideDef!]!\n aggregates: [Platform_WriteSideDef!]!\n automationSlices: [Platform_AutomationSliceDef!]!\n outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!\n inboundTranslationSlices: [Platform_InboundTranslationSliceDef!]!\n extensions: [Platform_ExtensionDef!]!\n}`
|
|
18
|
+
`type Platform_ComponentDefinitionEntry {\n pluginId: String!\n readModels: [Platform_ReadSideDef!]!\n stateViewSlices: [Platform_ReadSideDef!]!\n internalQueryables: [Platform_ReadSideDef!]!\n stateChangeSlices: [Platform_WriteSideDef!]!\n aggregates: [Platform_WriteSideDef!]!\n automationSlices: [Platform_AutomationSliceDef!]!\n outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!\n inboundTranslationSlices: [Platform_InboundTranslationSliceDef!]!\n extensions: [Platform_ExtensionDef!]!\n}`
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
function encodeStrings(ss) {
|
|
@@ -325,6 +325,10 @@ function encodePluginStructureEntry(pluginId, def) {
|
|
|
325
325
|
"stateViewSlices",
|
|
326
326
|
def.stateViewSlices.filter(isPublicQueryable).map(encodeQueryableDef)
|
|
327
327
|
],
|
|
328
|
+
[
|
|
329
|
+
"internalQueryables",
|
|
330
|
+
def.readModels.concat(def.stateViewSlices).filter(q => !Primitive_object.notequal(q.visibility, "Internal")).map(encodeQueryableDef)
|
|
331
|
+
],
|
|
328
332
|
[
|
|
329
333
|
"stateChangeSlices",
|
|
330
334
|
def.stateChangeSlices.map(encodeWritableDef)
|
|
@@ -164,20 +164,29 @@ let labelFieldsFromStateSchema = (
|
|
|
164
164
|
// because its components never pass through `make` — has to derive its defs the
|
|
165
165
|
// same way, and a second copy of this walk would be free to drift from this one.
|
|
166
166
|
|
|
167
|
+
// A variant's declared cross-entity references. Shared by the command and event
|
|
168
|
+
// walks because the two ask the identical question of identical field dicts, and
|
|
169
|
+
// the question is `getFieldTarget` rather than `getTarget`: a reference declared
|
|
170
|
+
// on an `array<string>` field sits on the element schema. Collecting it in one
|
|
171
|
+
// place is what stops one walk from being taught that and the other not.
|
|
172
|
+
let extractReferences = (properties: dict<S.t<unknown>>): array<
|
|
173
|
+
Reventless.Plugin.fieldReference,
|
|
174
|
+
> =>
|
|
175
|
+
properties
|
|
176
|
+
->Dict.toArray
|
|
177
|
+
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
178
|
+
Reventless.Reference.getFieldTarget(fieldSchema)->Option.map(target => (
|
|
179
|
+
{
|
|
180
|
+
Reventless.Plugin.fieldName,
|
|
181
|
+
entity: target.entity,
|
|
182
|
+
plugin: target.plugin,
|
|
183
|
+
}: Reventless.Plugin.fieldReference
|
|
184
|
+
))
|
|
185
|
+
)
|
|
186
|
+
|
|
167
187
|
let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
168
188
|
let mkDef = (~variantName, ~properties) => {
|
|
169
|
-
let references =
|
|
170
|
-
properties
|
|
171
|
-
->Dict.toArray
|
|
172
|
-
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
173
|
-
Reventless.Reference.getTarget(fieldSchema)->Option.map(target => (
|
|
174
|
-
{
|
|
175
|
-
Reventless.Plugin.fieldName,
|
|
176
|
-
entity: target.entity,
|
|
177
|
-
plugin: target.plugin,
|
|
178
|
-
}: Reventless.Plugin.fieldReference
|
|
179
|
-
))
|
|
180
|
-
)
|
|
189
|
+
let references = extractReferences(properties)
|
|
181
190
|
({
|
|
182
191
|
Reventless.Plugin.name: variantName,
|
|
183
192
|
// Derived for the same reason as `commandDef.schema`: an event's field
|
|
@@ -301,18 +310,7 @@ let make = (
|
|
|
301
310
|
// `S.literal("Archive")` string rather than an `{TAG, ...}` object.
|
|
302
311
|
let mkDef = (~variantName, ~properties) => {
|
|
303
312
|
let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
|
|
304
|
-
let references =
|
|
305
|
-
properties
|
|
306
|
-
->Dict.toArray
|
|
307
|
-
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
308
|
-
Reventless.Reference.getTarget(fieldSchema)->Option.map(target => (
|
|
309
|
-
{
|
|
310
|
-
Reventless.Plugin.fieldName,
|
|
311
|
-
entity: target.entity,
|
|
312
|
-
plugin: target.plugin,
|
|
313
|
-
}: Reventless.Plugin.fieldReference
|
|
314
|
-
))
|
|
315
|
-
)
|
|
313
|
+
let references = extractReferences(properties)
|
|
316
314
|
// Per-variant `allowedStates` lives on the *parent* command schema
|
|
317
315
|
// (the PPX attaches a single dict<variantName, [|states|]> via
|
|
318
316
|
// markAllowedStates). Look it up by variant name; back-compat
|
|
@@ -140,16 +140,20 @@ function labelFieldsFromStateSchema(entityName, stateSchema) {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function extractReferences(properties) {
|
|
144
|
+
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
145
|
+
let fieldName = param[0];
|
|
146
|
+
return Stdlib_Option.map(Reference$Reventless.getFieldTarget(param[1]), target => ({
|
|
147
|
+
fieldName: fieldName,
|
|
148
|
+
entity: target.entity,
|
|
149
|
+
plugin: target.plugin
|
|
150
|
+
}));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
143
154
|
function toEventDef(v) {
|
|
144
155
|
let mkDef = (variantName, properties) => {
|
|
145
|
-
let references =
|
|
146
|
-
let fieldName = param[0];
|
|
147
|
-
return Stdlib_Option.map(Reference$Reventless.getTarget(param[1]), target => ({
|
|
148
|
-
fieldName: fieldName,
|
|
149
|
-
entity: target.entity,
|
|
150
|
-
plugin: target.plugin
|
|
151
|
-
}));
|
|
152
|
-
});
|
|
156
|
+
let references = extractReferences(properties);
|
|
153
157
|
return {
|
|
154
158
|
name: variantName,
|
|
155
159
|
schema: JSON.stringify(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v)),
|
|
@@ -267,14 +271,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
267
271
|
let toCommandDef = (isAggregate, mutationFieldFor, parentSchema, v) => {
|
|
268
272
|
let mkDef = (variantName, properties) => {
|
|
269
273
|
let match = commandLevelAndId(isAggregate, variantName, properties);
|
|
270
|
-
let references =
|
|
271
|
-
let fieldName = param[0];
|
|
272
|
-
return Stdlib_Option.map(Reference$Reventless.getTarget(param[1]), target => ({
|
|
273
|
-
fieldName: fieldName,
|
|
274
|
-
entity: target.entity,
|
|
275
|
-
plugin: target.plugin
|
|
276
|
-
}));
|
|
277
|
-
});
|
|
274
|
+
let references = extractReferences(properties);
|
|
278
275
|
let allowedStates = ApiAllowedStatesHelpers$ReventlessCore.getAllowedStates(parentSchema, variantName);
|
|
279
276
|
let targetState = ApiTargetStateHelpers$ReventlessCore.getTargetState(parentSchema, variantName);
|
|
280
277
|
let apiExposed = false;
|
|
@@ -653,6 +650,7 @@ export {
|
|
|
653
650
|
statusFieldFromStateSchema,
|
|
654
651
|
labelFieldSourceToString,
|
|
655
652
|
labelFieldsFromStateSchema,
|
|
653
|
+
extractReferences,
|
|
656
654
|
toEventDef,
|
|
657
655
|
extractEventDefs,
|
|
658
656
|
extractErrorDefs,
|
|
@@ -106,6 +106,7 @@ describe("encodePluginStructureEntry", () => {
|
|
|
106
106
|
"automationSlices",
|
|
107
107
|
"extensions",
|
|
108
108
|
"inboundTranslationSlices",
|
|
109
|
+
"internalQueryables",
|
|
109
110
|
"outboundTranslationSlices",
|
|
110
111
|
"pluginId",
|
|
111
112
|
"readModels",
|
|
@@ -252,21 +253,68 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
|
|
|
252
253
|
visibility: Some("Internal"),
|
|
253
254
|
chapter: None,
|
|
254
255
|
}
|
|
256
|
+
// A distinct name per source array: the complement has to be fed by both, and
|
|
257
|
+
// reusing one def would hide a version that only walks `readModels`.
|
|
258
|
+
let internalSlice: queryableDef = {
|
|
259
|
+
...internalQbl,
|
|
260
|
+
name: "PendingShipments",
|
|
261
|
+
queryField: "Ordering_PendingShipments",
|
|
262
|
+
}
|
|
255
263
|
let mixed: pluginStructure = {
|
|
256
264
|
...structure,
|
|
257
265
|
readModels: [qbl, internalQbl],
|
|
258
|
-
stateViewSlices: [
|
|
266
|
+
stateViewSlices: [internalSlice],
|
|
259
267
|
}
|
|
260
|
-
let
|
|
261
|
-
Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId="Ordering", mixed)
|
|
268
|
+
let mixedEntry =
|
|
269
|
+
Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId="Ordering", mixed)
|
|
270
|
+
|
|
271
|
+
// Names under one key of the encoded entry. `String.includes` over the whole
|
|
272
|
+
// JSON can no longer answer this: an Internal view is legitimately present
|
|
273
|
+
// under `internalQueryables`, so a whole-document search cannot tell "kept out
|
|
274
|
+
// of the surfaces" from "dropped entirely" — which is the distinction the
|
|
275
|
+
// feature is.
|
|
276
|
+
let namesAt = (entry: JSON.t, key: string): array<string> =>
|
|
277
|
+
entry
|
|
278
|
+
->JSON.Decode.object
|
|
279
|
+
->Option.flatMap(o => o->Dict.get(key))
|
|
280
|
+
->Option.flatMap(JSON.Decode.array)
|
|
281
|
+
->Option.getOr([])
|
|
282
|
+
->Array.filterMap(c =>
|
|
283
|
+
c->JSON.Decode.object->Option.flatMap(o => o->Dict.get("name"))->Option.flatMap(JSON.Decode.string)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
testSync("excludes an Internal queryableDef from the encoded read-side", () => {
|
|
287
|
+
expect(mixedEntry->namesAt("readModels"))->toEqual(["Products"])
|
|
288
|
+
expect(mixedEntry->namesAt("stateViewSlices"))->toEqual([])
|
|
289
|
+
})
|
|
262
290
|
|
|
263
|
-
testSync("
|
|
264
|
-
expect(
|
|
291
|
+
testSync("keeps a Public queryableDef", () =>
|
|
292
|
+
expect(mixedEntry->namesAt("readModels")->Array.includes("Products"))->toEqual(true)
|
|
265
293
|
)
|
|
266
294
|
|
|
267
|
-
|
|
268
|
-
|
|
295
|
+
// Both directions, deliberately: a change that also leaked Internal views back
|
|
296
|
+
// into the surface lists would pass a presence-only assertion.
|
|
297
|
+
testSync("carries what the filter removed on internalQueryables", () =>
|
|
298
|
+
expect(mixedEntry->namesAt("internalQueryables"))->toEqual([
|
|
299
|
+
"AvailableProducts",
|
|
300
|
+
"PendingShipments",
|
|
301
|
+
])
|
|
269
302
|
)
|
|
303
|
+
|
|
304
|
+
testSync("encodes internalQueryables as [] for an all-Public entry, not a missing key", () => {
|
|
305
|
+
let entry = Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
|
|
306
|
+
~pluginId="Ordering",
|
|
307
|
+
{...structure, readModels: [qbl], stateViewSlices: []},
|
|
308
|
+
)
|
|
309
|
+
expect(
|
|
310
|
+
entry->JSON.Decode.object->Option.flatMap(o => o->Dict.get("internalQueryables")),
|
|
311
|
+
)->toEqual(Some(JSON.Encode.array([])))
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
testSync("declares internalQueryables on the entry type in the SDL", () => {
|
|
315
|
+
let sdl = Platform_ComponentDefinitionsApi.sdlTypes->Array.join("\n")
|
|
316
|
+
expect(sdl->String.includes("internalQueryables: [Platform_ReadSideDef!]!"))->toEqual(true)
|
|
317
|
+
})
|
|
270
318
|
})
|
|
271
319
|
|
|
272
320
|
describe("allowedStates + statusField populated", () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
6
7
|
import * as Platform_ComponentDefinitionsApi$ReventlessCore from "../../src/admin/Platform_ComponentDefinitionsApi.res.mjs";
|
|
@@ -146,6 +147,7 @@ globalThis.describe("encodePluginStructureEntry", () => {
|
|
|
146
147
|
"automationSlices",
|
|
147
148
|
"extensions",
|
|
148
149
|
"inboundTranslationSlices",
|
|
150
|
+
"internalQueryables",
|
|
149
151
|
"outboundTranslationSlices",
|
|
150
152
|
"pluginId",
|
|
151
153
|
"readModels",
|
|
@@ -268,11 +270,29 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
268
270
|
visibility: internalQbl_visibility,
|
|
269
271
|
chapter: undefined
|
|
270
272
|
};
|
|
273
|
+
let internalSlice_consumedEventTypes = internalQbl_consumedEventTypes;
|
|
274
|
+
let internalSlice_linkedWriteSide = internalQbl_linkedWriteSide;
|
|
275
|
+
let internalSlice_searchableFields = internalQbl_searchableFields;
|
|
276
|
+
let internalSlice_labelFieldSource = "convention";
|
|
277
|
+
let internalSlice_visibility = "Internal";
|
|
278
|
+
let internalSlice = {
|
|
279
|
+
name: "PendingShipments",
|
|
280
|
+
queryField: "Ordering_PendingShipments",
|
|
281
|
+
schema: "{}",
|
|
282
|
+
consumedEventTypes: internalSlice_consumedEventTypes,
|
|
283
|
+
linkedWriteSide: internalSlice_linkedWriteSide,
|
|
284
|
+
labelField: "name",
|
|
285
|
+
searchableFields: internalSlice_searchableFields,
|
|
286
|
+
labelFieldSource: internalSlice_labelFieldSource,
|
|
287
|
+
statusField: undefined,
|
|
288
|
+
visibility: internalSlice_visibility,
|
|
289
|
+
chapter: undefined
|
|
290
|
+
};
|
|
271
291
|
let mixed_readModels = [
|
|
272
292
|
qbl,
|
|
273
293
|
internalQbl
|
|
274
294
|
];
|
|
275
|
-
let mixed_stateViewSlices = [
|
|
295
|
+
let mixed_stateViewSlices = [internalSlice];
|
|
276
296
|
let mixed_stateChangeSlices = structure_stateChangeSlices;
|
|
277
297
|
let mixed_aggregates = structure_aggregates;
|
|
278
298
|
let mixed_automationSlices = structure_automationSlices;
|
|
@@ -292,12 +312,40 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
292
312
|
requiredStores: undefined,
|
|
293
313
|
requiredStoreDeclarations: undefined
|
|
294
314
|
};
|
|
295
|
-
let
|
|
315
|
+
let mixedEntry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", mixed);
|
|
316
|
+
let namesAt = (entry, key) => Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), o => o[key]), Stdlib_JSON.Decode.array), []), c => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(c), o => o["name"]), Stdlib_JSON.Decode.string));
|
|
296
317
|
globalThis.test("excludes an Internal queryableDef from the encoded read-side", () => {
|
|
297
|
-
globalThis.expect(
|
|
318
|
+
globalThis.expect(namesAt(mixedEntry, "readModels")).toEqual(["Products"]);
|
|
319
|
+
globalThis.expect(namesAt(mixedEntry, "stateViewSlices")).toEqual([]);
|
|
298
320
|
});
|
|
299
321
|
globalThis.test("keeps a Public queryableDef", () => {
|
|
300
|
-
globalThis.expect(
|
|
322
|
+
globalThis.expect(namesAt(mixedEntry, "readModels").includes("Products")).toEqual(true);
|
|
323
|
+
});
|
|
324
|
+
globalThis.test("carries what the filter removed on internalQueryables", () => {
|
|
325
|
+
globalThis.expect(namesAt(mixedEntry, "internalQueryables")).toEqual([
|
|
326
|
+
"AvailableProducts",
|
|
327
|
+
"PendingShipments"
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
330
|
+
globalThis.test("encodes internalQueryables as [] for an all-Public entry, not a missing key", () => {
|
|
331
|
+
let entry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", {
|
|
332
|
+
readModels: [qbl],
|
|
333
|
+
stateViewSlices: [],
|
|
334
|
+
stateChangeSlices: structure_stateChangeSlices,
|
|
335
|
+
aggregates: structure_aggregates,
|
|
336
|
+
automationSlices: structure_automationSlices,
|
|
337
|
+
outboundTranslationSlices: structure_outboundTranslationSlices,
|
|
338
|
+
inboundTranslationSlices: structure_inboundTranslationSlices,
|
|
339
|
+
extensions: structure_extensions,
|
|
340
|
+
extensionPoints: undefined,
|
|
341
|
+
requiredStores: undefined,
|
|
342
|
+
requiredStoreDeclarations: undefined
|
|
343
|
+
});
|
|
344
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), o => o["internalQueryables"])).toEqual([]);
|
|
345
|
+
});
|
|
346
|
+
globalThis.test("declares internalQueryables on the entry type in the SDL", () => {
|
|
347
|
+
let sdl = Platform_ComponentDefinitionsApi$ReventlessCore.sdlTypes.join("\n");
|
|
348
|
+
globalThis.expect(sdl.includes("internalQueryables: [Platform_ReadSideDef!]!")).toEqual(true);
|
|
301
349
|
});
|
|
302
350
|
});
|
|
303
351
|
|
|
@@ -121,6 +121,20 @@ module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
121
121
|
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
module PsReserveStockSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
125
|
+
module Spec = PsReserveStock
|
|
126
|
+
module Behavior = {
|
|
127
|
+
type state = PsReserveStock.state
|
|
128
|
+
let initialState = PsReserveStock.initialState
|
|
129
|
+
let evolve = PsReserveStock.evolve
|
|
130
|
+
let decide = PsReserveStock.decide
|
|
131
|
+
let moduleUrl = PsReserveStock.moduleUrl
|
|
132
|
+
}
|
|
133
|
+
let isAsync = false
|
|
134
|
+
type component = scsComponent
|
|
135
|
+
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
136
|
+
}
|
|
137
|
+
|
|
124
138
|
let structure = Plugin_Structure.make(
|
|
125
139
|
~name="TestPlugin",
|
|
126
140
|
~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
|
|
@@ -960,4 +974,59 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
960
974
|
)->toEqual([true, true, true, false, false])
|
|
961
975
|
})
|
|
962
976
|
})
|
|
977
|
+
|
|
978
|
+
// A plural reference is declared exactly like a singular one, and the ppx puts
|
|
979
|
+
// the marker where `Reference.to_` returns it: on the element schema, one level
|
|
980
|
+
// inside the array. Reading the field's own schema therefore answers "no
|
|
981
|
+
// reference" for every `@ref` array — and the consuming side treats a missing
|
|
982
|
+
// declaration as licence to guess, so the field resolves to whatever its name
|
|
983
|
+
// suggests instead of what the author wrote.
|
|
984
|
+
describe("plural references reach the manifest", () => {
|
|
985
|
+
let withPluralRefs = Plugin_Structure.make(
|
|
986
|
+
~name="TestPlugin",
|
|
987
|
+
~stateChangeSlices=[module(PsReserveStockSlice)],
|
|
988
|
+
)
|
|
989
|
+
let slice = withPluralRefs.stateChangeSlices->Array.getUnsafe(0)
|
|
990
|
+
let referencesOf = refs =>
|
|
991
|
+
refs->Array.map(({Reventless.Plugin.fieldName: f, entity}) => (f, entity))
|
|
992
|
+
|
|
993
|
+
// Scalar and plural in one command: the plural forms are the regression, the
|
|
994
|
+
// scalar one is here so a fix that reaches them by breaking it cannot pass.
|
|
995
|
+
testSync("a command collects its array refs alongside its scalar one", () => {
|
|
996
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
997
|
+
expect(cmd.references->referencesOf)->toEqual([
|
|
998
|
+
("customerId", "Customers"),
|
|
999
|
+
("productIds", "AvailableProducts"),
|
|
1000
|
+
("warehouseIds", "Warehouses"),
|
|
1001
|
+
])
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
// Optionality is a statement about presence. An optional array puts the
|
|
1005
|
+
// element two wrappers down, which is the shape most likely to be missed by
|
|
1006
|
+
// an unwrap that only handles one.
|
|
1007
|
+
testSync("an optional array ref is collected under the field's own name", () => {
|
|
1008
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
1009
|
+
expect(
|
|
1010
|
+
cmd.references->Array.find(({fieldName}) => fieldName == "warehouseIds"),
|
|
1011
|
+
)->toEqual(Some({Reventless.Plugin.fieldName: "warehouseIds", entity: "Warehouses", plugin: None}))
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
// The event walk is a separate function over the same field dicts. Left to
|
|
1015
|
+
// itself it would be fixed by accident or not at all.
|
|
1016
|
+
testSync("an event collects its array ref too", () => {
|
|
1017
|
+
let evt = slice.events->Array.getUnsafe(0)
|
|
1018
|
+
expect(evt.references->referencesOf)->toEqual([("productIds", "AvailableProducts")])
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
// The declared entity, not the one `productIds` reads like. Before the
|
|
1022
|
+
// element unwrap this field resolved by heuristic to `Products`.
|
|
1023
|
+
testSync("the declared entity wins over the one the field name suggests", () => {
|
|
1024
|
+
let cmd = slice.commands->Array.getUnsafe(0)
|
|
1025
|
+
expect(
|
|
1026
|
+
cmd.references
|
|
1027
|
+
->Array.find(({fieldName}) => fieldName == "productIds")
|
|
1028
|
+
->Option.map(({entity}) => entity),
|
|
1029
|
+
)->toEqual(Some("AvailableProducts"))
|
|
1030
|
+
})
|
|
1031
|
+
})
|
|
963
1032
|
})
|
|
@@ -14,6 +14,7 @@ import * as PsOrdersView$ReventlessCore from "./StateViewSlice/PsOrdersView.res.
|
|
|
14
14
|
import * as PsPlaceOrder$ReventlessCore from "./StateChangeSlice/PsPlaceOrder.res.mjs";
|
|
15
15
|
import * as PsChangePhoto$ReventlessCore from "./StateChangeSlice/PsChangePhoto.res.mjs";
|
|
16
16
|
import * as CapabilityManifest$Reventless from "@reventlessdev/reventless-spec/src/components/CapabilityManifest.res.mjs";
|
|
17
|
+
import * as PsReserveStock$ReventlessCore from "./StateChangeSlice/PsReserveStock.res.mjs";
|
|
17
18
|
import * as PsUploadAvatar$ReventlessCore from "./StateChangeSlice/PsUploadAvatar.res.mjs";
|
|
18
19
|
import * as PsAnnotatedView$ReventlessCore from "./StateViewSlice/PsAnnotatedView.res.mjs";
|
|
19
20
|
import * as PsAttachInvoice$ReventlessCore from "./StateChangeSlice/PsAttachInvoice.res.mjs";
|
|
@@ -276,6 +277,36 @@ let PsChangePhotoSlice = {
|
|
|
276
277
|
make: make$8
|
|
277
278
|
};
|
|
278
279
|
|
|
280
|
+
let Behavior$5 = {
|
|
281
|
+
initialState: false,
|
|
282
|
+
evolve: PsReserveStock$ReventlessCore.evolve,
|
|
283
|
+
decide: PsReserveStock$ReventlessCore.decide,
|
|
284
|
+
moduleUrl: PsReserveStock$ReventlessCore.moduleUrl
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
function make$9(param, param$1, param$2, param$3, param$4, param$5) {
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let PsReserveStockSlice_Spec = {
|
|
292
|
+
name: PsReserveStock$ReventlessCore.name,
|
|
293
|
+
moduleUrl: PsReserveStock$ReventlessCore.moduleUrl,
|
|
294
|
+
Id: Id$Reventless.$$String,
|
|
295
|
+
consumedEventSchema: PsReserveStock$ReventlessCore.consumedEventSchema,
|
|
296
|
+
errorSchema: PsReserveStock$ReventlessCore.errorSchema,
|
|
297
|
+
eventSchema: PsReserveStock$ReventlessCore.eventSchema,
|
|
298
|
+
commandSchema: PsReserveStock$ReventlessCore.commandSchema,
|
|
299
|
+
commandAuthorization: PsReserveStock$ReventlessCore.commandAuthorization,
|
|
300
|
+
readConsistency: PsReserveStock$ReventlessCore.readConsistency
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
let PsReserveStockSlice = {
|
|
304
|
+
Spec: PsReserveStockSlice_Spec,
|
|
305
|
+
Behavior: Behavior$5,
|
|
306
|
+
isAsync: false,
|
|
307
|
+
make: make$9
|
|
308
|
+
};
|
|
309
|
+
|
|
279
310
|
let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [
|
|
280
311
|
PsOrdersViewSlice,
|
|
281
312
|
PsAvailableProductsViewSlice,
|
|
@@ -962,6 +993,50 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
962
993
|
]);
|
|
963
994
|
});
|
|
964
995
|
});
|
|
996
|
+
globalThis.describe("plural references reach the manifest", () => {
|
|
997
|
+
let withPluralRefs = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsReserveStockSlice], undefined, undefined, undefined, undefined, undefined, undefined);
|
|
998
|
+
let slice = withPluralRefs.stateChangeSlices[0];
|
|
999
|
+
let referencesOf = refs => refs.map(param => [
|
|
1000
|
+
param.fieldName,
|
|
1001
|
+
param.entity
|
|
1002
|
+
]);
|
|
1003
|
+
globalThis.test("a command collects its array refs alongside its scalar one", () => {
|
|
1004
|
+
let cmd = slice.commands[0];
|
|
1005
|
+
globalThis.expect(referencesOf(cmd.references)).toEqual([
|
|
1006
|
+
[
|
|
1007
|
+
"customerId",
|
|
1008
|
+
"Customers"
|
|
1009
|
+
],
|
|
1010
|
+
[
|
|
1011
|
+
"productIds",
|
|
1012
|
+
"AvailableProducts"
|
|
1013
|
+
],
|
|
1014
|
+
[
|
|
1015
|
+
"warehouseIds",
|
|
1016
|
+
"Warehouses"
|
|
1017
|
+
]
|
|
1018
|
+
]);
|
|
1019
|
+
});
|
|
1020
|
+
globalThis.test("an optional array ref is collected under the field's own name", () => {
|
|
1021
|
+
let cmd = slice.commands[0];
|
|
1022
|
+
globalThis.expect(cmd.references.find(param => param.fieldName === "warehouseIds")).toEqual({
|
|
1023
|
+
fieldName: "warehouseIds",
|
|
1024
|
+
entity: "Warehouses",
|
|
1025
|
+
plugin: undefined
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
globalThis.test("an event collects its array ref too", () => {
|
|
1029
|
+
let evt = slice.events[0];
|
|
1030
|
+
globalThis.expect(referencesOf(evt.references)).toEqual([[
|
|
1031
|
+
"productIds",
|
|
1032
|
+
"AvailableProducts"
|
|
1033
|
+
]]);
|
|
1034
|
+
});
|
|
1035
|
+
globalThis.test("the declared entity wins over the one the field name suggests", () => {
|
|
1036
|
+
let cmd = slice.commands[0];
|
|
1037
|
+
globalThis.expect(Stdlib_Option.map(cmd.references.find(param => param.fieldName === "productIds"), param => param.entity)).toEqual("AvailableProducts");
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
965
1040
|
});
|
|
966
1041
|
|
|
967
1042
|
export {
|
|
@@ -974,6 +1049,7 @@ export {
|
|
|
974
1049
|
PsAttachInvoiceSlice,
|
|
975
1050
|
PsUploadAvatarSlice,
|
|
976
1051
|
PsChangePhotoSlice,
|
|
1052
|
+
PsReserveStockSlice,
|
|
977
1053
|
structure,
|
|
978
1054
|
}
|
|
979
1055
|
/* structure Not a pure module */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Test fixture spec where every reference carrier is PLURAL.
|
|
2
|
+
//
|
|
3
|
+
// `Reference.to_` returns an *element* schema, so the ppx annotates the `string`
|
|
4
|
+
// inside `array<string>` and the field's own schema carries no marker. A walk
|
|
5
|
+
// that asks the field directly finds nothing and drops the declaration — and a
|
|
6
|
+
// dropped `@ref` is not the same as an absent one: the consuming side falls back
|
|
7
|
+
// to a naming heuristic and resolves the field to whatever entity the name
|
|
8
|
+
// suggests, so the author's declaration is both ignored and reported as missing.
|
|
9
|
+
//
|
|
10
|
+
// The command declares the scalar and plural forms side by side so a regression
|
|
11
|
+
// shows up as one of the two disappearing rather than as an empty list, and
|
|
12
|
+
// `warehouseIds?` covers the plural-inside-optional shape, where the array sits
|
|
13
|
+
// one wrapper further down still.
|
|
14
|
+
|
|
15
|
+
@@reventless.spec("ReserveStock")
|
|
16
|
+
|
|
17
|
+
type state = bool
|
|
18
|
+
let initialState = false
|
|
19
|
+
|
|
20
|
+
@schema
|
|
21
|
+
type consumedEvent = StockReserved({reservationId: string})
|
|
22
|
+
|
|
23
|
+
let evolve = (_state, _event) => true
|
|
24
|
+
|
|
25
|
+
@schema
|
|
26
|
+
type command =
|
|
27
|
+
| ReserveStock({
|
|
28
|
+
@partitionTag reservationId: string,
|
|
29
|
+
@ref("Customers") customerId: string,
|
|
30
|
+
@ref("AvailableProducts") productIds: array<string>,
|
|
31
|
+
@ref("Warehouses") warehouseIds?: array<string>,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
@schema
|
|
35
|
+
type error = OutOfStock
|
|
36
|
+
|
|
37
|
+
@schema
|
|
38
|
+
type event =
|
|
39
|
+
StockReserved({
|
|
40
|
+
@partitionTag reservationId: string,
|
|
41
|
+
@ref("AvailableProducts") productIds: array<string>,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
let decide = (_state, _command): result<array<event>, error> => Ok([])
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
5
|
+
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
6
|
+
|
|
7
|
+
let consumedEventSchema = S.schema(s => ({
|
|
8
|
+
TAG: "StockReserved",
|
|
9
|
+
reservationId: s.m(DcbTag$Reventless.string)
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
function evolve(_state, _event) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let commandSchema = S.schema(s => ({
|
|
17
|
+
TAG: "ReserveStock",
|
|
18
|
+
reservationId: s.m(DcbTag$Reventless.partition),
|
|
19
|
+
customerId: s.m(Reference$Reventless.to_(undefined, undefined, "Customers")),
|
|
20
|
+
productIds: s.m(S.array(Reference$Reventless.to_(undefined, "productId", "AvailableProducts"))),
|
|
21
|
+
warehouseIds: s.m(S.option(S.array(Reference$Reventless.to_(undefined, "warehouseId", "Warehouses"))))
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
let errorSchema = S.literal("OutOfStock");
|
|
25
|
+
|
|
26
|
+
let eventSchema = S.schema(s => ({
|
|
27
|
+
TAG: "StockReserved",
|
|
28
|
+
reservationId: s.m(DcbTag$Reventless.partition),
|
|
29
|
+
productIds: s.m(S.array(Reference$Reventless.to_(undefined, "productId", "AvailableProducts")))
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
function decide(_state, _command) {
|
|
33
|
+
return {
|
|
34
|
+
TAG: "Ok",
|
|
35
|
+
_0: []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function commandAuthorization(param) {
|
|
40
|
+
return "AllowAuthenticated";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let name = "ReserveStock";
|
|
44
|
+
|
|
45
|
+
let Id;
|
|
46
|
+
|
|
47
|
+
let initialState = false;
|
|
48
|
+
|
|
49
|
+
let moduleUrl = "@reventlessdev/reventless-core/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs";
|
|
50
|
+
|
|
51
|
+
let readConsistency = "EscalateOnRetry";
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
name,
|
|
55
|
+
Id,
|
|
56
|
+
initialState,
|
|
57
|
+
consumedEventSchema,
|
|
58
|
+
evolve,
|
|
59
|
+
commandSchema,
|
|
60
|
+
errorSchema,
|
|
61
|
+
eventSchema,
|
|
62
|
+
decide,
|
|
63
|
+
moduleUrl,
|
|
64
|
+
commandAuthorization,
|
|
65
|
+
readConsistency,
|
|
66
|
+
}
|
|
67
|
+
/* consumedEventSchema Not a pure module */
|