@reventlessdev/reventless-core 3.0.0-alpha.191 → 3.0.0-alpha.193
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 +19 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin.res.mjs +4 -0
- package/src/admin/Platform_Admin_Structure.res +1 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +2 -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/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_Helpers.res +18 -7
- package/src/plugin/component/Plugin_Helpers.res.mjs +20 -1
- package/src/plugin/component/Plugin_Structure.res +91 -34
- package/src/plugin/component/Plugin_Structure.res.mjs +82 -12
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +2 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +8 -4
- package/tests/api/SuryToJsonSchemaTest.res +42 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +36 -0
- package/tests/message/MessageTest.res +1 -0
- package/tests/message/MessageTest.res.mjs +2 -1
- package/tests/plugin/PluginStructureTest.res +145 -0
- package/tests/plugin/PluginStructureTest.res.mjs +138 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res +33 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +64 -0
|
@@ -481,23 +481,34 @@ let make = (
|
|
|
481
481
|
// An unqualified store belongs to the declaring plugin, so every collected
|
|
482
482
|
// entry is qualified to `{plugin}.{store}` — one shape, and the string is
|
|
483
483
|
// directly the store's identity. Many fields legitimately name one store, so
|
|
484
|
-
// the
|
|
485
|
-
|
|
486
|
-
|
|
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
|
+
> =>
|
|
487
494
|
properties
|
|
488
495
|
->Dict.toArray
|
|
489
|
-
->Array.filterMap(((
|
|
490
|
-
Reventless.StorageRef.getStore(fieldSchema)->Option.map(target =>
|
|
491
|
-
target.plugin->Option.getOr(name) ++ "." ++ target.store
|
|
492
|
-
|
|
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
|
+
})
|
|
493
502
|
)
|
|
494
503
|
|
|
495
504
|
// Commands and events are unions of per-variant objects; state is a single
|
|
496
505
|
// object. Both reduce to "the properties of every object in this schema".
|
|
497
|
-
let storesFromSchema = (schema: S.t<unknown>): array<
|
|
506
|
+
let storesFromSchema = (~component, schema: S.t<unknown>): array<
|
|
507
|
+
Reventless.Plugin.requiredStoreDeclaration,
|
|
508
|
+
> => {
|
|
498
509
|
let fromVariant = v =>
|
|
499
510
|
switch v {
|
|
500
|
-
| S.Object({properties}) => storesFromProperties(properties)
|
|
511
|
+
| S.Object({properties}) => storesFromProperties(~component, properties)
|
|
501
512
|
| _ => []
|
|
502
513
|
}
|
|
503
514
|
switch schema {
|
|
@@ -506,37 +517,82 @@ let make = (
|
|
|
506
517
|
}
|
|
507
518
|
}
|
|
508
519
|
|
|
509
|
-
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>)> =
|
|
510
542
|
[
|
|
511
|
-
aggregates->Array.flatMap((module(A: ReventlessInfra.Aggregate.T with type api = api)) =>
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
),
|
|
523
|
-
|
|
524
|
-
storesFromSchema(SVS.Spec.stateSchema->S.castToUnknown)
|
|
525
|
-
),
|
|
526
|
-
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((
|
|
527
556
|
module(R: ReventlessInfra.ReadModel.T with type api = api and type role = role),
|
|
528
|
-
) =>
|
|
529
|
-
automationSlices->Array.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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((
|
|
533
563
|
module(ITS: ReventlessInfra.InboundTranslationSlice.T),
|
|
534
|
-
) =>
|
|
535
|
-
]
|
|
536
|
-
|
|
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)
|
|
537
584
|
->Belt.Set.String.fromArray
|
|
538
585
|
->Belt.Set.String.toArray
|
|
539
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
|
+
|
|
540
596
|
// ── Build queryable defs ───────────────────────────────────────────────────
|
|
541
597
|
//
|
|
542
598
|
// Internal ReadModels and StateViewSlices (marked `@@reventless.visibility(Internal)`)
|
|
@@ -755,5 +811,6 @@ let make = (
|
|
|
755
811
|
extensions: extensionDefs,
|
|
756
812
|
extensionPoints: Some(extensionPointDefs),
|
|
757
813
|
requiredStores: Some(requiredStores),
|
|
814
|
+
requiredStoreDeclarations: Some(requiredStoreDeclarations),
|
|
758
815
|
}
|
|
759
816
|
}
|
|
@@ -5,6 +5,8 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
5
5
|
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
6
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
7
|
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
8
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
9
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
10
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
9
11
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
10
12
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
@@ -15,6 +17,7 @@ import * as SchemaType$ReventlessCore from "../../components/Api/SchemaType.res.
|
|
|
15
17
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
16
18
|
import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHelpers.res.mjs";
|
|
17
19
|
import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJsonSchema.res.mjs";
|
|
20
|
+
import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
|
|
18
21
|
import * as ApiTargetStateHelpers$ReventlessCore from "../../components/Api/ApiTargetStateHelpers.res.mjs";
|
|
19
22
|
import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/ApiAllowedStatesHelpers.res.mjs";
|
|
20
23
|
|
|
@@ -386,11 +389,70 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
386
389
|
let match$3 = scored[0];
|
|
387
390
|
return match$3[0];
|
|
388
391
|
};
|
|
389
|
-
let
|
|
392
|
+
let compareDeclarations = (a, b) => {
|
|
393
|
+
let byStore = Primitive_string.compare(a.store, b.store);
|
|
394
|
+
if (byStore !== 0) {
|
|
395
|
+
return byStore;
|
|
396
|
+
}
|
|
397
|
+
let byComponent = Primitive_string.compare(a.component, b.component);
|
|
398
|
+
if (byComponent !== 0) {
|
|
399
|
+
return byComponent;
|
|
400
|
+
} else {
|
|
401
|
+
return Primitive_string.compare(a.field, b.field);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
let storeDeclarationSites = [
|
|
405
|
+
aggregates.flatMap(A => [
|
|
406
|
+
[
|
|
407
|
+
A.Spec.name,
|
|
408
|
+
A.Spec.commandSchema
|
|
409
|
+
],
|
|
410
|
+
[
|
|
411
|
+
A.Spec.name,
|
|
412
|
+
A.Spec.eventSchema
|
|
413
|
+
]
|
|
414
|
+
]),
|
|
415
|
+
stateChangeSlices.flatMap(SCS => [
|
|
416
|
+
[
|
|
417
|
+
SCS.Spec.name,
|
|
418
|
+
SCS.Spec.commandSchema
|
|
419
|
+
],
|
|
420
|
+
[
|
|
421
|
+
SCS.Spec.name,
|
|
422
|
+
SCS.Spec.eventSchema
|
|
423
|
+
]
|
|
424
|
+
]),
|
|
425
|
+
stateViewSlices.map(SVS => [
|
|
426
|
+
SVS.Spec.name,
|
|
427
|
+
SVS.Spec.stateSchema
|
|
428
|
+
]),
|
|
429
|
+
readModels.map(R => [
|
|
430
|
+
R.Spec.name,
|
|
431
|
+
R.Spec.stateSchema
|
|
432
|
+
]),
|
|
433
|
+
automationSlices.map(AS => [
|
|
434
|
+
AS.Spec.name,
|
|
435
|
+
AS.Spec.commandSchema
|
|
436
|
+
]),
|
|
437
|
+
inboundTranslationSlices.map(ITS => [
|
|
438
|
+
ITS.Spec.name,
|
|
439
|
+
ITS.Spec.commandSchema
|
|
440
|
+
])
|
|
441
|
+
].flat();
|
|
442
|
+
let requiredStoreDeclarations = Stdlib_Array.reduce(storeDeclarationSites.flatMap(param => {
|
|
443
|
+
let component = param[0];
|
|
444
|
+
let schema = param[1];
|
|
390
445
|
let fromVariant = v => {
|
|
391
446
|
if (v.type === "object") {
|
|
392
447
|
let properties = v.properties;
|
|
393
|
-
return Stdlib_Array.filterMap(Object.entries(properties), param =>
|
|
448
|
+
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
449
|
+
let field = param[0];
|
|
450
|
+
return Stdlib_Option.map(StorageRef$Reventless.getStore(param[1]), target => ({
|
|
451
|
+
store: Stdlib_Option.getOr(target.plugin, name) + "." + target.store,
|
|
452
|
+
component: component,
|
|
453
|
+
field: field
|
|
454
|
+
}));
|
|
455
|
+
});
|
|
394
456
|
} else {
|
|
395
457
|
return [];
|
|
396
458
|
}
|
|
@@ -400,15 +462,22 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
400
462
|
} else {
|
|
401
463
|
return fromVariant(schema);
|
|
402
464
|
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
465
|
+
}).toSorted(compareDeclarations), [], (acc, d) => {
|
|
466
|
+
let prev = Stdlib_Array.last(acc);
|
|
467
|
+
if (prev !== undefined) {
|
|
468
|
+
if (Primitive_object.equal(prev, d)) {
|
|
469
|
+
return acc;
|
|
470
|
+
} else {
|
|
471
|
+
return acc.concat([d]);
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
return acc.concat([d]);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
let requiredStores = Belt_SetString.toArray(Belt_SetString.fromArray(requiredStoreDeclarations.map(d => d.store)));
|
|
478
|
+
storeDeclarationSites.forEach(param => {
|
|
479
|
+
Capability_Inference$ReventlessCore.scanSchema(param[0], param[1]).forEach(w => log.warn("Plugin_Structure", undefined, Capability_Inference$ReventlessCore.message(w)));
|
|
480
|
+
});
|
|
412
481
|
let visibilityTag = v => {
|
|
413
482
|
if (v === "Public") {
|
|
414
483
|
return;
|
|
@@ -554,7 +623,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
|
|
|
554
623
|
inboundTranslationSlices: inboundTranslationSliceDefs,
|
|
555
624
|
extensions: extensionDefs,
|
|
556
625
|
extensionPoints: extensionPointDefs,
|
|
557
|
-
requiredStores: requiredStores
|
|
626
|
+
requiredStores: requiredStores,
|
|
627
|
+
requiredStoreDeclarations: requiredStoreDeclarations
|
|
558
628
|
};
|
|
559
629
|
}
|
|
560
630
|
|
|
@@ -87,6 +87,7 @@ let structure: pluginStructure = {
|
|
|
87
87
|
],
|
|
88
88
|
extensionPoints: None,
|
|
89
89
|
requiredStores: None,
|
|
90
|
+
requiredStoreDeclarations: None,
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
let encoded = Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
|
|
@@ -300,6 +301,7 @@ describe("allowedStates + statusField populated", () => {
|
|
|
300
301
|
extensions: [],
|
|
301
302
|
extensionPoints: None,
|
|
302
303
|
requiredStores: None,
|
|
304
|
+
requiredStoreDeclarations: None,
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
let json =
|
|
@@ -122,7 +122,8 @@ let structure = {
|
|
|
122
122
|
inboundTranslationSlices: structure_inboundTranslationSlices,
|
|
123
123
|
extensions: structure_extensions,
|
|
124
124
|
extensionPoints: undefined,
|
|
125
|
-
requiredStores: undefined
|
|
125
|
+
requiredStores: undefined,
|
|
126
|
+
requiredStoreDeclarations: undefined
|
|
126
127
|
};
|
|
127
128
|
|
|
128
129
|
let encoded = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", structure);
|
|
@@ -221,7 +222,8 @@ globalThis.describe("translation-slice externalSystem round-trip", () => {
|
|
|
221
222
|
inboundTranslationSlices: externalStructure_inboundTranslationSlices,
|
|
222
223
|
extensions: externalStructure_extensions,
|
|
223
224
|
extensionPoints: undefined,
|
|
224
|
-
requiredStores: undefined
|
|
225
|
+
requiredStores: undefined,
|
|
226
|
+
requiredStoreDeclarations: undefined
|
|
225
227
|
};
|
|
226
228
|
let externalJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", externalStructure));
|
|
227
229
|
globalThis.test("surfaces Some outbound externalSystem as its inner string", () => {
|
|
@@ -272,7 +274,8 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
272
274
|
inboundTranslationSlices: mixed_inboundTranslationSlices,
|
|
273
275
|
extensions: mixed_extensions,
|
|
274
276
|
extensionPoints: undefined,
|
|
275
|
-
requiredStores: undefined
|
|
277
|
+
requiredStores: undefined,
|
|
278
|
+
requiredStoreDeclarations: undefined
|
|
276
279
|
};
|
|
277
280
|
let mixedJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", mixed));
|
|
278
281
|
globalThis.test("excludes an Internal queryableDef from the encoded read-side", () => {
|
|
@@ -351,7 +354,8 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
351
354
|
inboundTranslationSlices: structureWithStates_inboundTranslationSlices,
|
|
352
355
|
extensions: structureWithStates_extensions,
|
|
353
356
|
extensionPoints: undefined,
|
|
354
|
-
requiredStores: undefined
|
|
357
|
+
requiredStores: undefined,
|
|
358
|
+
requiredStoreDeclarations: undefined
|
|
355
359
|
};
|
|
356
360
|
let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Platform", structureWithStates));
|
|
357
361
|
globalThis.test("encodes populated allowedStates as a JSON array", () => {
|
|
@@ -619,4 +619,46 @@ describe("SuryToJsonSchema:", () => {
|
|
|
619
619
|
expect(semanticOf(json, "imageUrl"))->toEqual(Some(JSON.Encode.string("storageRef")))
|
|
620
620
|
})
|
|
621
621
|
})
|
|
622
|
+
|
|
623
|
+
// The claim the branded scalars rest on: marking a field adds an annotation,
|
|
624
|
+
// not a shape. A string stays a string and a number stays a number on the
|
|
625
|
+
// wire, which is what makes them retrofittable onto a log that already has
|
|
626
|
+
// events in it. One of each is enough — the emission path is the same for all
|
|
627
|
+
// seven, and it is the *shape* half that is the load-bearing assertion here.
|
|
628
|
+
describe("branded scalars keep their underlying shape:", () => {
|
|
629
|
+
let fieldOf = (json, name) => getPropertyOf(json, name)
|
|
630
|
+
let keyOf = (json, name, key) => fieldOf(json, name)->Option.flatMap(s => getProperty(s, key))
|
|
631
|
+
|
|
632
|
+
let json = SuryToJsonSchema.deriveObjectSchema(
|
|
633
|
+
S.schema(s =>
|
|
634
|
+
{
|
|
635
|
+
"contactEmail": s.matches(Reventless.Email.schema),
|
|
636
|
+
"taxRate": s.matches(Reventless.Percent.schema),
|
|
637
|
+
}
|
|
638
|
+
)->S.castToUnknown,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
testSync("a string scalar stays a string and carries its id", () =>
|
|
642
|
+
expect((
|
|
643
|
+
keyOf(json, "contactEmail", "type"),
|
|
644
|
+
keyOf(json, "contactEmail", "x-reventless-semantic"),
|
|
645
|
+
))->toEqual((Some(JSON.Encode.string("string")), Some(JSON.Encode.string("email"))))
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
testSync("a numeric scalar stays a number and carries its id", () =>
|
|
649
|
+
expect((
|
|
650
|
+
keyOf(json, "taxRate", "type"),
|
|
651
|
+
keyOf(json, "taxRate", "x-reventless-semantic"),
|
|
652
|
+
))->toEqual((Some(JSON.Encode.string("number")), Some(JSON.Encode.string("percent"))))
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
// The provenance half: a typed field outranks an annotated one, and this is
|
|
656
|
+
// the key a consumer reads to tell them apart.
|
|
657
|
+
testSync("both name the type as their source", () =>
|
|
658
|
+
expect((
|
|
659
|
+
keyOf(json, "contactEmail", "x-reventless-semantic-source"),
|
|
660
|
+
keyOf(json, "taxRate", "x-reventless-semantic-source"),
|
|
661
|
+
))->toEqual((Some(JSON.Encode.string("type")), Some(JSON.Encode.string("type"))))
|
|
662
|
+
)
|
|
663
|
+
})
|
|
622
664
|
})
|
|
@@ -4,8 +4,10 @@ import * as S from "sury/src/S.res.mjs";
|
|
|
4
4
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Email$Reventless from "@reventlessdev/reventless-spec/src/semantic/Email.res.mjs";
|
|
7
8
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
9
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
10
|
+
import * as Percent$Reventless from "@reventlessdev/reventless-spec/src/semantic/Percent.res.mjs";
|
|
9
11
|
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
|
|
10
12
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
11
13
|
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
@@ -937,6 +939,40 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
937
939
|
globalThis.expect(semanticOf(json, "imageUrl")).toEqual("storageRef");
|
|
938
940
|
});
|
|
939
941
|
});
|
|
942
|
+
globalThis.describe("branded scalars keep their underlying shape:", () => {
|
|
943
|
+
let keyOf = (json, name, key) => Stdlib_Option.flatMap(getPropertyOf(json, name), s => getProperty(s, key));
|
|
944
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(S.schema(s => ({
|
|
945
|
+
contactEmail: s.m(Email$Reventless.schema),
|
|
946
|
+
taxRate: s.m(Percent$Reventless.schema)
|
|
947
|
+
})));
|
|
948
|
+
globalThis.test("a string scalar stays a string and carries its id", () => {
|
|
949
|
+
globalThis.expect([
|
|
950
|
+
keyOf(json, "contactEmail", "type"),
|
|
951
|
+
keyOf(json, "contactEmail", "x-reventless-semantic")
|
|
952
|
+
]).toEqual([
|
|
953
|
+
"string",
|
|
954
|
+
"email"
|
|
955
|
+
]);
|
|
956
|
+
});
|
|
957
|
+
globalThis.test("a numeric scalar stays a number and carries its id", () => {
|
|
958
|
+
globalThis.expect([
|
|
959
|
+
keyOf(json, "taxRate", "type"),
|
|
960
|
+
keyOf(json, "taxRate", "x-reventless-semantic")
|
|
961
|
+
]).toEqual([
|
|
962
|
+
"number",
|
|
963
|
+
"percent"
|
|
964
|
+
]);
|
|
965
|
+
});
|
|
966
|
+
globalThis.test("both name the type as their source", () => {
|
|
967
|
+
globalThis.expect([
|
|
968
|
+
keyOf(json, "contactEmail", "x-reventless-semantic-source"),
|
|
969
|
+
keyOf(json, "taxRate", "x-reventless-semantic-source")
|
|
970
|
+
]).toEqual([
|
|
971
|
+
"type",
|
|
972
|
+
"type"
|
|
973
|
+
]);
|
|
974
|
+
});
|
|
975
|
+
});
|
|
940
976
|
});
|
|
941
977
|
|
|
942
978
|
export {
|
|
@@ -118,7 +118,8 @@ globalThis.describe("Message should", () => {
|
|
|
118
118
|
inboundTranslationSlices: [],
|
|
119
119
|
extensions: [],
|
|
120
120
|
extensionPoints: undefined,
|
|
121
|
-
requiredStores: undefined
|
|
121
|
+
requiredStores: undefined,
|
|
122
|
+
requiredStoreDeclarations: undefined
|
|
122
123
|
},
|
|
123
124
|
dcbEventLog: undefined,
|
|
124
125
|
kind: "Domain"
|
|
@@ -93,6 +93,20 @@ module PsAttachInvoiceSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
93
93
|
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
97
|
+
module Spec = PsChangePhoto
|
|
98
|
+
module Behavior = {
|
|
99
|
+
type state = PsChangePhoto.state
|
|
100
|
+
let initialState = PsChangePhoto.initialState
|
|
101
|
+
let evolve = PsChangePhoto.evolve
|
|
102
|
+
let decide = PsChangePhoto.decide
|
|
103
|
+
let moduleUrl = PsChangePhoto.moduleUrl
|
|
104
|
+
}
|
|
105
|
+
let isAsync = false
|
|
106
|
+
type component = scsComponent
|
|
107
|
+
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
let structure = Plugin_Structure.make(
|
|
97
111
|
~name="TestPlugin",
|
|
98
112
|
~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
|
|
@@ -675,5 +689,136 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
675
689
|
testSync("a plugin declaring no stores collects none — not None", () =>
|
|
676
690
|
expect(structure.requiredStores)->toEqual(Some([]))
|
|
677
691
|
)
|
|
692
|
+
|
|
693
|
+
testSync("each requirement keeps its declaring (component, field) sites", () => {
|
|
694
|
+
// `documents` is declared on a command field AND an event field of the
|
|
695
|
+
// same component under the same field name — one declaration site, so
|
|
696
|
+
// the identical triples collapse to one entry.
|
|
697
|
+
let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
|
|
698
|
+
{store: "TestPlugin.documents", component: "AttachInvoice", field: "documentUrl"},
|
|
699
|
+
{store: "branding.logos", component: "AttachInvoice", field: "logoUrl"},
|
|
700
|
+
]
|
|
701
|
+
expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
// The manifest is a rendering of the structure. Its key set must be
|
|
705
|
+
// byte-identical to what the deployed plugin reports as `requiredStores`,
|
|
706
|
+
// because the deploy-time coverage assertion compares against exactly that
|
|
707
|
+
// — this equality is the whole contract of `capabilities.json`.
|
|
708
|
+
describe("capability manifest", () => {
|
|
709
|
+
let manifest = Reventless.CapabilityManifest.fromStructure(withStores)
|
|
710
|
+
|
|
711
|
+
testSync("keys are byte-identical to requiredStores", () =>
|
|
712
|
+
expect(manifest.capabilities->Array.map(c => c.key))->toEqual(
|
|
713
|
+
withStores.requiredStores->Option.getOr([]),
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
testSync("each entry carries its provenance", () => {
|
|
718
|
+
let entry =
|
|
719
|
+
manifest.capabilities->Array.find(c => c.key == "TestPlugin.documents")->Option.getOrThrow
|
|
720
|
+
let expected: array<Reventless.CapabilityManifest.provenance> = [
|
|
721
|
+
{component: "AttachInvoice", field: "documentUrl"},
|
|
722
|
+
]
|
|
723
|
+
expect(entry.declaredBy)->toEqual(expected)
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
testSync("a structure declaring nothing yields an empty list, not a missing manifest", () =>
|
|
727
|
+
expect(Reventless.CapabilityManifest.fromStructure(structure).capabilities)->toEqual([])
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
testSync("rendering is deterministic and newline-terminated", () => {
|
|
731
|
+
let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
|
|
732
|
+
expect(rendered)->toBe(
|
|
733
|
+
`{
|
|
734
|
+
"capabilities": [
|
|
735
|
+
{
|
|
736
|
+
"kind": "ObjectStore",
|
|
737
|
+
"key": "TestPlugin.documents",
|
|
738
|
+
"declaredBy": [
|
|
739
|
+
{
|
|
740
|
+
"component": "AttachInvoice",
|
|
741
|
+
"field": "documentUrl"
|
|
742
|
+
}
|
|
743
|
+
]
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
"kind": "ObjectStore",
|
|
747
|
+
"key": "branding.logos",
|
|
748
|
+
"declaredBy": [
|
|
749
|
+
{
|
|
750
|
+
"component": "AttachInvoice",
|
|
751
|
+
"field": "logoUrl"
|
|
752
|
+
}
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
]
|
|
756
|
+
}
|
|
757
|
+
`,
|
|
758
|
+
)
|
|
759
|
+
})
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
// Declarations are authoritative; the name heuristics are a lint. A
|
|
764
|
+
// heuristic-only match warns and never reaches the manifest, so nothing is
|
|
765
|
+
// provisioned from a guess — `imageUrl` is genuinely ambiguous between an
|
|
766
|
+
// uploaded object and an external URL, and only the author can settle it.
|
|
767
|
+
describe("capability inference", () => {
|
|
768
|
+
let commandWarnings = Capability_Inference.scanSchema(
|
|
769
|
+
~component="ChangePhoto",
|
|
770
|
+
PsChangePhoto.commandSchema->S.castToUnknown,
|
|
771
|
+
)
|
|
772
|
+
let eventWarnings = Capability_Inference.scanSchema(
|
|
773
|
+
~component="ChangePhoto",
|
|
774
|
+
PsChangePhoto.eventSchema->S.castToUnknown,
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
testSync("a heuristic-only match produces a warning", () => {
|
|
778
|
+
let expected: array<Capability_Inference.warning> = [
|
|
779
|
+
{component: "ChangePhoto", field: "photoUrl"},
|
|
780
|
+
]
|
|
781
|
+
expect(commandWarnings)->toEqual(expected)
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
testSync("a declared field with a heuristic name does not warn", () => {
|
|
785
|
+
// The event's `thumbnail` matches the name heuristic but carries
|
|
786
|
+
// @storageRef — only the undeclared `photoUrl` warns.
|
|
787
|
+
let expected: array<Capability_Inference.warning> = [
|
|
788
|
+
{component: "ChangePhoto", field: "photoUrl"},
|
|
789
|
+
]
|
|
790
|
+
expect(eventWarnings)->toEqual(expected)
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
testSync("a heuristic-only match reaches no manifest entry", () => {
|
|
794
|
+
let withHeuristicField = Plugin_Structure.make(
|
|
795
|
+
~name="TestPlugin",
|
|
796
|
+
~stateChangeSlices=[module(PsChangePhotoSlice)],
|
|
797
|
+
)
|
|
798
|
+
// Only the *declared* `thumbnail` store is collected; `photoUrl` — the
|
|
799
|
+
// heuristic-only match — provisions nothing.
|
|
800
|
+
expect(withHeuristicField.requiredStores)->toEqual(Some(["TestPlugin.productPhotos"]))
|
|
801
|
+
let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
|
|
802
|
+
expect(
|
|
803
|
+
manifest.capabilities->Array.every(entry =>
|
|
804
|
+
entry.declaredBy->Array.every(site => site.field != "photoUrl")
|
|
805
|
+
),
|
|
806
|
+
)->toBe(true)
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
testSync("the warning names the field and the settling annotation", () => {
|
|
810
|
+
let text =
|
|
811
|
+
commandWarnings->Array.getUnsafe(0)->Capability_Inference.message
|
|
812
|
+
expect(text->String.includes("ChangePhoto.photoUrl"))->toBe(true)
|
|
813
|
+
expect(text->String.includes("@storageRef"))->toBe(true)
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
testSync("suffix and exact name rules match; ordinary names do not", () => {
|
|
817
|
+
expect(
|
|
818
|
+
["invoiceFileRef", "upload", "thumbnail", "customerName", "documentUrl"]->Array.map(
|
|
819
|
+
Capability_Inference.nameMatches,
|
|
820
|
+
),
|
|
821
|
+
)->toEqual([true, true, true, false, false])
|
|
822
|
+
})
|
|
678
823
|
})
|
|
679
824
|
})
|