@reventlessdev/reventless-core 3.0.0-alpha.237 → 3.0.0-alpha.239
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 +24 -0
- package/package.json +8 -8
- package/src/Message.res +1 -1
- package/src/adapter/Monitoring/Monitoring.res +1 -1
- package/src/admin/Platform_Admin_Structure.res +1 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +1 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate_Callback.res +2 -2
- package/src/components/Api/ApiAllowedStatesHelpers.res +2 -3
- package/src/components/Api/ApiTargetStateHelpers.res +4 -3
- package/src/components/Api/GraphQL_FragmentGenerator.res +122 -12
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +41 -8
- package/src/components/Api/SuryToJsonSchema.res +8 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +8 -4
- package/src/components/Dcb/Dcb_Builder.res +79 -16
- package/src/components/Dcb/Dcb_Builder.res.mjs +41 -5
- package/src/components/EventLog/EventLog.res +1 -1
- package/src/plugin/component/Plugin_Builder.res +7 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
- package/src/plugin/component/Plugin_Structure.res +376 -27
- package/src/plugin/component/Plugin_Structure.res.mjs +175 -8
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +1 -1
- package/tests/admin/Platform_BakedManifestTest.res +1 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +1 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +3 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +6 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res +1 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +2 -0
- package/tests/aggregate/AggregateCacheTest.res +1 -1
- package/tests/aggregate/AggregateSnapshotTest.res +1 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res +171 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +108 -0
- package/tests/api/SuryToJsonSchemaTest.res +30 -3
- package/tests/api/SuryToJsonSchemaTest.res.mjs +49 -4
- package/tests/commandgenerator/OwnerStampingTest.res +57 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +19 -0
- package/tests/message/MessageTest.res +1 -1
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +1 -1
- package/tests/plugin/PluginStructureTest.res +268 -4
- package/tests/plugin/PluginStructureTest.res.mjs +284 -3
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +31 -0
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +73 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res +16 -1
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -2
- package/tests/plugin/StateViewSlice/PsShipmentsView.res +26 -0
- package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +101 -0
- package/tests/util/CsvStreamTest.res +1 -1
|
@@ -322,6 +322,111 @@ globalThis.describe("GraphQL_FragmentGenerator.mutationArgTypes", () => {
|
|
|
322
322
|
});
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
+
let stateSchema = S.schema(s => ({
|
|
326
|
+
id: s.m(S.string),
|
|
327
|
+
name: s.m(S.string)
|
|
328
|
+
}));
|
|
329
|
+
|
|
330
|
+
let RefDoorRow = {
|
|
331
|
+
stateSchema: stateSchema
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
function queryFor(fragment, needle) {
|
|
335
|
+
return GraphQL_Stitcher$ReventlessCore.decode(fragment).queries.find(q => q.includes(needle));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function refDoorFragment(subIdField) {
|
|
339
|
+
return GraphQL_FragmentGenerator$ReventlessCore.generate([], [{
|
|
340
|
+
singleFieldName: "Catalog_Product",
|
|
341
|
+
listFieldName: "Catalog_Products",
|
|
342
|
+
returnTypeName: "Catalog_Product",
|
|
343
|
+
stateSchema: stateSchema,
|
|
344
|
+
authorization: undefined,
|
|
345
|
+
connectionSpec: true,
|
|
346
|
+
subIdField: subIdField
|
|
347
|
+
}]);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
globalThis.describe("the reference door and the archive argument", () => {
|
|
351
|
+
globalThis.test("projects a reference to id, label and the state that retired it", () => {
|
|
352
|
+
let sdl = GraphQL_FragmentGenerator$ReventlessCore.deriveRefTypeSdl("Catalog_Product");
|
|
353
|
+
globalThis.expect([
|
|
354
|
+
sdl.includes("type Catalog_ProductRef"),
|
|
355
|
+
sdl.includes("label: String!"),
|
|
356
|
+
sdl.includes("retiredState: String"),
|
|
357
|
+
sdl.includes("price")
|
|
358
|
+
]).toEqual([
|
|
359
|
+
true,
|
|
360
|
+
true,
|
|
361
|
+
true,
|
|
362
|
+
false
|
|
363
|
+
]);
|
|
364
|
+
});
|
|
365
|
+
globalThis.test("names the door after the list field it resolves against", () => {
|
|
366
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.deriveRefsQueryField("Catalog_Products", "Catalog_Product")).toEqual(" Catalog_ProductsRefs(ids: [ID!]!): [Catalog_ProductRef!]!");
|
|
367
|
+
});
|
|
368
|
+
globalThis.test("emits the door from generate, not only from the helper", () => {
|
|
369
|
+
globalThis.expect([
|
|
370
|
+
Stdlib_Option.isSome(queryFor(refDoorFragment(undefined), "Catalog_ProductsRefs(ids: [ID!]!)")),
|
|
371
|
+
Stdlib_Option.isSome(typeDefFor(refDoorFragment(undefined), "type Catalog_ProductRef"))
|
|
372
|
+
]).toEqual([
|
|
373
|
+
true,
|
|
374
|
+
true
|
|
375
|
+
]);
|
|
376
|
+
});
|
|
377
|
+
globalThis.test("withholds the door from a composite-key view", () => {
|
|
378
|
+
globalThis.expect([
|
|
379
|
+
Stdlib_Option.isSome(queryFor(refDoorFragment("lineNo"), "Catalog_ProductsRefs")),
|
|
380
|
+
Stdlib_Option.isSome(typeDefFor(refDoorFragment("lineNo"), "type Catalog_ProductRef"))
|
|
381
|
+
]).toEqual([
|
|
382
|
+
false,
|
|
383
|
+
false
|
|
384
|
+
]);
|
|
385
|
+
});
|
|
386
|
+
globalThis.test("lets the single-entity and by-ids doors be asked for the archive", () => {
|
|
387
|
+
globalThis.expect([
|
|
388
|
+
GraphQL_FragmentGenerator$ReventlessCore.deriveObjectQueryField("Catalog_Product", "Catalog_Product", undefined, undefined).includes("includeRetired: Boolean"),
|
|
389
|
+
GraphQL_FragmentGenerator$ReventlessCore.deriveByIdsQueryField("Catalog_Products", "Catalog_Product").includes("includeRetired: Boolean")
|
|
390
|
+
]).toEqual([
|
|
391
|
+
true,
|
|
392
|
+
true
|
|
393
|
+
]);
|
|
394
|
+
});
|
|
395
|
+
globalThis.test("keeps the sub-id argument when the view has one", () => {
|
|
396
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.deriveObjectQueryField("Ordering_OrderLine", "Ordering_OrderLine", undefined, "lineNo")).toEqual(" Ordering_OrderLine(id: ID!, lineNo: String!, includeRetired: Boolean): Ordering_OrderLine");
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
globalThis.describe("the by-index door", () => {
|
|
401
|
+
let index = (index$1, idField, subIdField) => ({
|
|
402
|
+
index: index$1,
|
|
403
|
+
type_: "S",
|
|
404
|
+
idField: idField,
|
|
405
|
+
subIdField: subIdField,
|
|
406
|
+
projectionType: "ALL"
|
|
407
|
+
});
|
|
408
|
+
globalThis.test("takes the index value it filters on, and can be asked for the archive", () => {
|
|
409
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.deriveIndexQueryField("Catalog_Product", index("categoryId", undefined, undefined), "Catalog_ProductConnection")).toEqual(" Catalog_ProductByCategoryId(categoryId: String!, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): Catalog_ProductConnection!");
|
|
410
|
+
});
|
|
411
|
+
globalThis.test("names the field after the index and keys it on the column", () => {
|
|
412
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.deriveIndexQueryField("Ordering_Order", index("byOwner", "ownerId", undefined), "Ordering_OrderConnection")).toEqual(" Ordering_OrderByOwner(ownerId: String!, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): Ordering_OrderConnection!");
|
|
413
|
+
});
|
|
414
|
+
globalThis.test("offers the sort key the sort template has always read", () => {
|
|
415
|
+
globalThis.expect(GraphQL_FragmentGenerator$ReventlessCore.deriveIndexQueryField("Ordering_Order", index("byCustomer", "customerId", "placedAt"), "Ordering_OrderConnection")).toEqual(" Ordering_OrderByCustomer(customerId: String!, placedAt: String, first: Int, after: String, last: Int, before: String, includeRetired: Boolean): Ordering_OrderConnection!");
|
|
416
|
+
});
|
|
417
|
+
globalThis.test("drops a leading `by` exactly once", () => {
|
|
418
|
+
globalThis.expect([
|
|
419
|
+
GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName("X", "byOwner"),
|
|
420
|
+
GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName("X", "ownerId"),
|
|
421
|
+
GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName("X", "by")
|
|
422
|
+
]).toEqual([
|
|
423
|
+
"XByOwner",
|
|
424
|
+
"XByOwnerId",
|
|
425
|
+
"XByBy"
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
325
430
|
export {
|
|
326
431
|
OrderSliceCmd,
|
|
327
432
|
mutationFor,
|
|
@@ -330,5 +435,8 @@ export {
|
|
|
330
435
|
AddProductCmd,
|
|
331
436
|
ChangePriceCmd,
|
|
332
437
|
PlaceOrder,
|
|
438
|
+
RefDoorRow,
|
|
439
|
+
queryFor,
|
|
440
|
+
refDoorFragment,
|
|
333
441
|
}
|
|
334
442
|
/* commandSchema Not a pure module */
|
|
@@ -431,7 +431,7 @@ describe("SuryToJsonSchema:", () => {
|
|
|
431
431
|
let schema' =
|
|
432
432
|
schema->withSpec({
|
|
433
433
|
...emptySpec,
|
|
434
|
-
retired: Some({field: "archived", label: "Archived", showWhenFalse: true, values: None}),
|
|
434
|
+
retired: Some({field: "archived", label: "Archived", showWhenFalse: true, values: None, namedWhenRetired: false}),
|
|
435
435
|
})
|
|
436
436
|
let json = SuryToJsonSchema.deriveObjectSchema(schema')
|
|
437
437
|
let retiredObj =
|
|
@@ -464,6 +464,7 @@ describe("SuryToJsonSchema:", () => {
|
|
|
464
464
|
label: "",
|
|
465
465
|
showWhenFalse: false,
|
|
466
466
|
values: Some(values),
|
|
467
|
+
namedWhenRetired: false,
|
|
467
468
|
}),
|
|
468
469
|
})
|
|
469
470
|
->SuryToJsonSchema.deriveObjectSchema
|
|
@@ -522,7 +523,7 @@ describe("SuryToJsonSchema:", () => {
|
|
|
522
523
|
let schema' =
|
|
523
524
|
schema->withSpec({
|
|
524
525
|
...emptySpec,
|
|
525
|
-
retired: Some({field: "archived", label: "", showWhenFalse: false, values: None}),
|
|
526
|
+
retired: Some({field: "archived", label: "", showWhenFalse: false, values: None, namedWhenRetired: false}),
|
|
526
527
|
})
|
|
527
528
|
let json = SuryToJsonSchema.deriveObjectSchema(schema')
|
|
528
529
|
let retiredObj = json->retiredOn("archived")
|
|
@@ -532,12 +533,38 @@ describe("SuryToJsonSchema:", () => {
|
|
|
532
533
|
))->toEqual((None, None))
|
|
533
534
|
})
|
|
534
535
|
|
|
536
|
+
// The opt-in that lets a reference name a withheld row. Travels only when
|
|
537
|
+
// true, on the omit-the-default rule the keys around it follow: false is what
|
|
538
|
+
// every record said before it existed, and writing it would put a key on
|
|
539
|
+
// every retirement to report that nothing changed.
|
|
540
|
+
testSync("carries namedWhenRetired only when the record opted in", () => {
|
|
541
|
+
let build = (~named: bool) => {
|
|
542
|
+
let schema = S.schema(s => {"archived": s.matches(S.bool)})->S.castToUnknown
|
|
543
|
+
schema
|
|
544
|
+
->withSpec({
|
|
545
|
+
...emptySpec,
|
|
546
|
+
retired: Some({
|
|
547
|
+
field: "archived",
|
|
548
|
+
label: "",
|
|
549
|
+
showWhenFalse: false,
|
|
550
|
+
values: None,
|
|
551
|
+
namedWhenRetired: named,
|
|
552
|
+
}),
|
|
553
|
+
})
|
|
554
|
+
->SuryToJsonSchema.deriveObjectSchema
|
|
555
|
+
->retiredOn("archived")
|
|
556
|
+
->Option.flatMap(r => getProperty(r, "namedWhenRetired"))
|
|
557
|
+
->Option.flatMap(JSON.Decode.bool)
|
|
558
|
+
}
|
|
559
|
+
expect((build(~named=true), build(~named=false)))->toEqual((Some(true), None))
|
|
560
|
+
})
|
|
561
|
+
|
|
535
562
|
testSync("omits the retired label when empty but always states showWhenFalse", () => {
|
|
536
563
|
let schema = S.schema(s => {"deactivated": s.matches(S.bool)})->S.castToUnknown
|
|
537
564
|
let schema' =
|
|
538
565
|
schema->withSpec({
|
|
539
566
|
...emptySpec,
|
|
540
|
-
retired: Some({field: "deactivated", label: "", showWhenFalse: false, values: None}),
|
|
567
|
+
retired: Some({field: "deactivated", label: "", showWhenFalse: false, values: None, namedWhenRetired: false}),
|
|
541
568
|
})
|
|
542
569
|
let json = SuryToJsonSchema.deriveObjectSchema(schema')
|
|
543
570
|
let retiredObj =
|
|
@@ -737,7 +737,8 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
737
737
|
field: "archived",
|
|
738
738
|
label: "Archived",
|
|
739
739
|
showWhenFalse: true,
|
|
740
|
-
values: undefined
|
|
740
|
+
values: undefined,
|
|
741
|
+
namedWhenRetired: false
|
|
741
742
|
}
|
|
742
743
|
});
|
|
743
744
|
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
|
|
@@ -783,7 +784,8 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
783
784
|
field: "accountStatus",
|
|
784
785
|
label: "",
|
|
785
786
|
showWhenFalse: false,
|
|
786
|
-
values: values
|
|
787
|
+
values: values,
|
|
788
|
+
namedWhenRetired: false
|
|
787
789
|
}
|
|
788
790
|
}));
|
|
789
791
|
};
|
|
@@ -837,7 +839,8 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
837
839
|
field: "archived",
|
|
838
840
|
label: "",
|
|
839
841
|
showWhenFalse: false,
|
|
840
|
-
values: undefined
|
|
842
|
+
values: undefined,
|
|
843
|
+
namedWhenRetired: false
|
|
841
844
|
}
|
|
842
845
|
});
|
|
843
846
|
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
|
|
@@ -850,6 +853,47 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
850
853
|
undefined
|
|
851
854
|
]);
|
|
852
855
|
});
|
|
856
|
+
globalThis.test("carries namedWhenRetired only when the record opted in", () => {
|
|
857
|
+
let build = named => {
|
|
858
|
+
let schema = S.schema(s => ({
|
|
859
|
+
archived: s.m(S.bool)
|
|
860
|
+
}));
|
|
861
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(retiredOn(SuryToJsonSchema$ReventlessCore.deriveObjectSchema(withSpec(schema, {
|
|
862
|
+
ids: emptySpec_ids,
|
|
863
|
+
compositeIds: emptySpec_compositeIds,
|
|
864
|
+
subIds: emptySpec_subIds,
|
|
865
|
+
compositeSubIds: emptySpec_compositeSubIds,
|
|
866
|
+
indexes: emptySpec_indexes,
|
|
867
|
+
hidden: emptySpec_hidden,
|
|
868
|
+
summary: emptySpec_summary,
|
|
869
|
+
drillTargets: emptySpec_drillTargets,
|
|
870
|
+
drillTargetKeys: emptySpec_drillTargetKeys,
|
|
871
|
+
collapsed: emptySpec_collapsed,
|
|
872
|
+
scan: emptySpec_scan,
|
|
873
|
+
scanSort: emptySpec_scanSort,
|
|
874
|
+
semantic: emptySpec_semantic,
|
|
875
|
+
metric: emptySpec_metric,
|
|
876
|
+
lifecycle: undefined,
|
|
877
|
+
groupBy: undefined,
|
|
878
|
+
visibility: undefined,
|
|
879
|
+
live: undefined,
|
|
880
|
+
retired: {
|
|
881
|
+
field: "archived",
|
|
882
|
+
label: "",
|
|
883
|
+
showWhenFalse: false,
|
|
884
|
+
values: undefined,
|
|
885
|
+
namedWhenRetired: named
|
|
886
|
+
}
|
|
887
|
+
})), "archived"), r => getProperty(r, "namedWhenRetired")), Stdlib_JSON.Decode.bool);
|
|
888
|
+
};
|
|
889
|
+
globalThis.expect([
|
|
890
|
+
build(true),
|
|
891
|
+
build(false)
|
|
892
|
+
]).toEqual([
|
|
893
|
+
true,
|
|
894
|
+
undefined
|
|
895
|
+
]);
|
|
896
|
+
});
|
|
853
897
|
globalThis.test("omits the retired label when empty but always states showWhenFalse", () => {
|
|
854
898
|
let schema = S.schema(s => ({
|
|
855
899
|
deactivated: s.m(S.bool)
|
|
@@ -877,7 +921,8 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
877
921
|
field: "deactivated",
|
|
878
922
|
label: "",
|
|
879
923
|
showWhenFalse: false,
|
|
880
|
-
values: undefined
|
|
924
|
+
values: undefined,
|
|
925
|
+
namedWhenRetired: false
|
|
881
926
|
}
|
|
882
927
|
});
|
|
883
928
|
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
|
|
@@ -185,3 +185,60 @@ describe("@owner stamping on the command path:", () => {
|
|
|
185
185
|
})
|
|
186
186
|
})
|
|
187
187
|
})
|
|
188
|
+
|
|
189
|
+
// The schema is not only a validator, and the case below is the one that proves
|
|
190
|
+
// it. Two AppSync-side call sites passed `S.json` in place of the command schema
|
|
191
|
+
// — a defensible substitution while its only job was validation, since AppSync
|
|
192
|
+
// has already checked the input against the SDL, and per-slice decoding happens
|
|
193
|
+
// downstream anyway. Owner stamping then gave the same argument a second job:
|
|
194
|
+
// it is where the `@owner` fields are read from. Handed a permissive schema the
|
|
195
|
+
// lookup answers "no owner fields" for every command there is, and the write
|
|
196
|
+
// keeps whatever owner the client sent — no error, no warning, a wrong row.
|
|
197
|
+
//
|
|
198
|
+
// Nothing about a single call site was wrong, which is why per-path tests could
|
|
199
|
+
// not catch it. What was missing is a check on the RELATIONSHIP: a generator
|
|
200
|
+
// built for a command whose spec marks an owner must be able to find it.
|
|
201
|
+
describe("the schema a generator is built with must answer for its commands:", () => {
|
|
202
|
+
let permissive = CommandGenerator_Callback.makeGenerateCommand(
|
|
203
|
+
~publishJsons=async cmds => published := published.contents->Array.concat(cmds),
|
|
204
|
+
~serviceName="Ordering",
|
|
205
|
+
~commandSchema=S.json->S.castToUnknown,
|
|
206
|
+
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
207
|
+
~stripIdFromParams=false,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
testPromise("a permissive schema silently stamps nothing — the defect, pinned", async () => {
|
|
211
|
+
let _ =
|
|
212
|
+
await permissive(
|
|
213
|
+
placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
|
|
214
|
+
)->Effect.runPromise
|
|
215
|
+
// Not the behaviour we want anywhere — recorded so that a change making the
|
|
216
|
+
// permissive path stamp correctly is a deliberate one, and so the contrast
|
|
217
|
+
// with the case below cannot be read as incidental.
|
|
218
|
+
expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-B")))
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
testPromise("the real schema stamps, from the same payload and caller", async () => {
|
|
222
|
+
let _ =
|
|
223
|
+
await generate(
|
|
224
|
+
placeOrder(~customerId="cust-B", ~identity=cognito(~userId="cust-A", ~groups=["User"])),
|
|
225
|
+
)->Effect.runPromise
|
|
226
|
+
expect(lastPublished()->fieldOf("customerId"))->toEqual(Some(str("cust-A")))
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// The check a call site can run against itself, and the one the DCB routing
|
|
230
|
+
// maps now satisfy by construction: every command a spec exposes resolves to
|
|
231
|
+
// its own owner fields through the schema that command will be dispatched
|
|
232
|
+
// with. Reading `[]` here is exactly the state that stamps nothing.
|
|
233
|
+
testPromise("every command of a spec resolves its owner fields through that spec's schema", async () => {
|
|
234
|
+
let schema = commandSchema->S.castToUnknown
|
|
235
|
+
expect(Reventless.Owner.variantFieldNames(schema, ~variant="PlaceOrder"))->toEqual([
|
|
236
|
+
"customerId",
|
|
237
|
+
])
|
|
238
|
+
expect(Reventless.Owner.variantFieldNames(schema, ~variant="ImportProducts"))->toEqual([])
|
|
239
|
+
// The substitution, asked the same question.
|
|
240
|
+
expect(
|
|
241
|
+
Reventless.Owner.variantFieldNames(S.json->S.castToUnknown, ~variant="PlaceOrder"),
|
|
242
|
+
)->toEqual([])
|
|
243
|
+
})
|
|
244
|
+
})
|
|
@@ -192,6 +192,25 @@ globalThis.describe("@owner stamping on the command path:", () => {
|
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
+
globalThis.describe("the schema a generator is built with must answer for its commands:", () => {
|
|
196
|
+
let permissive = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(async cmds => {
|
|
197
|
+
published.contents = published.contents.concat(cmds);
|
|
198
|
+
}, undefined, "Ordering", S.json, "StateChangeSlice", false);
|
|
199
|
+
globalThis.test("a permissive schema silently stamps nothing — the defect, pinned", async () => {
|
|
200
|
+
await Effect.runPromise(permissive(placeOrder("cust-B", cognito("cust-A", ["User"]))));
|
|
201
|
+
globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "customerId")).toEqual("cust-B");
|
|
202
|
+
});
|
|
203
|
+
globalThis.test("the real schema stamps, from the same payload and caller", async () => {
|
|
204
|
+
await Effect.runPromise(generate(placeOrder("cust-B", cognito("cust-A", ["User"]))));
|
|
205
|
+
globalThis.expect(fieldOf(published.contents[published.contents.length - 1 | 0], "customerId")).toEqual("cust-A");
|
|
206
|
+
});
|
|
207
|
+
globalThis.test("every command of a spec resolves its owner fields through that spec's schema", async () => {
|
|
208
|
+
globalThis.expect(Owner$Reventless.variantFieldNames(commandSchema, "PlaceOrder")).toEqual(["customerId"]);
|
|
209
|
+
globalThis.expect(Owner$Reventless.variantFieldNames(commandSchema, "ImportProducts")).toEqual([]);
|
|
210
|
+
globalThis.expect(Owner$Reventless.variantFieldNames(S.json, "PlaceOrder")).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
195
214
|
export {
|
|
196
215
|
commandSchema,
|
|
197
216
|
published,
|
|
@@ -96,7 +96,7 @@ describe("Message should", () => {
|
|
|
96
96
|
// Schema-migration-on-read: a VersionConnected event persisted before `kind` (and
|
|
97
97
|
// before later pluginStructure fields like `events`/`chapter`) existed must still
|
|
98
98
|
// decode — otherwise the one stale event bricks the whole lifecycle aggregate. See
|
|
99
|
-
// docs/plans/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
99
|
+
// docs/plans/done/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
100
100
|
testSync("tolerantly decode a VersionConnected persisted before kind/structure fields existed", () => {
|
|
101
101
|
open PluginSpec
|
|
102
102
|
let def: Reventless.Plugin.pluginDefinition = {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// (which drives CreateDisconnectSchedule). A deployed plugin once ran with schedule
|
|
8
8
|
// rate(60min) but a Heartbeat(10) command (grace ~12min), so it reconnected for
|
|
9
9
|
// ~12min after each hourly beat then sat Disconnected for the rest of the hour —
|
|
10
|
-
// it flapped. See docs/plans/plugin-heartbeat-disconnect-margin-hardening.md.
|
|
10
|
+
// it flapped. See docs/plans/done/plugin-heartbeat-disconnect-margin-hardening.md.
|
|
11
11
|
//
|
|
12
12
|
// These tests pin the two guarantees that the fix provides at this layer:
|
|
13
13
|
// 1. the disconnect grace scales with the interval (large cadences keep headroom);
|