@reventlessdev/reventless-core 3.0.0-alpha.238 → 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.
Files changed (26) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +8 -8
  3. package/src/admin/Platform_Admin_Structure.res +1 -0
  4. package/src/admin/Platform_Admin_Structure.res.mjs +1 -0
  5. package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
  6. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
  7. package/src/components/Api/GraphQL_FragmentGenerator.res +122 -12
  8. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +41 -8
  9. package/src/components/Api/SuryToJsonSchema.res +8 -0
  10. package/src/components/Api/SuryToJsonSchema.res.mjs +8 -4
  11. package/src/plugin/component/Plugin_Builder.res +7 -0
  12. package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
  13. package/src/plugin/component/Plugin_Structure.res +109 -22
  14. package/src/plugin/component/Plugin_Structure.res.mjs +47 -7
  15. package/tests/admin/Platform_BakedManifestTest.res +1 -0
  16. package/tests/admin/Platform_BakedManifestTest.res.mjs +1 -0
  17. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +3 -0
  18. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +6 -0
  19. package/tests/admin/Platform_PluginStructuresApiTest.res +1 -0
  20. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +2 -0
  21. package/tests/api/GraphQL_FragmentGeneratorTest.res +171 -0
  22. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +108 -0
  23. package/tests/api/SuryToJsonSchemaTest.res +30 -3
  24. package/tests/api/SuryToJsonSchemaTest.res.mjs +49 -4
  25. package/tests/plugin/PluginStructureTest.res +104 -1
  26. package/tests/plugin/PluginStructureTest.res.mjs +114 -2
@@ -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);
@@ -938,7 +938,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
938
938
  groupBy: None,
939
939
  visibility: None,
940
940
  live: None,
941
- retired: Some({field, label: "", showWhenFalse: false, values: None}),
941
+ retired: Some({field, label: "", showWhenFalse: false, values: None, namedWhenRetired: false}),
942
942
  },
943
943
  )
944
944
 
@@ -973,6 +973,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
973
973
  label: "",
974
974
  showWhenFalse: false,
975
975
  values: Some(["Deactivated", "Closed"]),
976
+ namedWhenRetired: false,
976
977
  }),
977
978
  },
978
979
  )
@@ -1017,6 +1018,108 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1017
1018
  })
1018
1019
  })
1019
1020
 
1021
+ // A misspelled retirement state used to be fatal or merely logged depending on
1022
+ // where the enum happened to be declared — the PPX refuses it when the enum is
1023
+ // in the same file, and this pass, which sees the resolved schema, only warned.
1024
+ // File layout is not a defensible arbiter of whether a data-exposure bug stops
1025
+ // a build, so this rung was raised to match the other.
1026
+ describe("@retired state names are checked against the field's own enum", () => {
1027
+ let withRetiredStates = (schema, ~field, ~lifecycle, ~values) =>
1028
+ schema->S.castToUnknown->S.Metadata.set(
1029
+ ~id=Reventless.StateAnnotations.stateAnnotationsId,
1030
+ {
1031
+ ids: [],
1032
+ compositeIds: [],
1033
+ subIds: [],
1034
+ compositeSubIds: [],
1035
+ indexes: [],
1036
+ hidden: [],
1037
+ summary: [],
1038
+ drillTargets: [],
1039
+ drillTargetKeys: [],
1040
+ collapsed: [],
1041
+ scan: [],
1042
+ scanSort: [],
1043
+ semantic: [],
1044
+ metric: [],
1045
+ lifecycle,
1046
+ groupBy: None,
1047
+ visibility: None,
1048
+ live: None,
1049
+ retired: Some({field, label: "", showWhenFalse: false, values, namedWhenRetired: false}),
1050
+ },
1051
+ )
1052
+
1053
+ let shelf = values =>
1054
+ S.schema(s =>
1055
+ {
1056
+ "shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
1057
+ }
1058
+ )->withRetiredStates(~field="shelf", ~lifecycle=Some("shelf"), ~values=Some(values))
1059
+
1060
+ let check = schema => Plugin_Structure.checkRetiredValue(~entityName="Products", schema)
1061
+
1062
+ testSync("a declared state passes", () => {
1063
+ expect(check(shelf(["Archived"])))->toEqual(Plugin_Structure.Checked([]))
1064
+ })
1065
+
1066
+ // Reported per state rather than as a set: one wrong entry among several
1067
+ // still narrows something, so the symptom is a subset of rows leaking.
1068
+ testSync("each undeclared state is reported on its own", () => {
1069
+ switch check(shelf(["Archived", "Discontinud", "Withdrawn"])) {
1070
+ | Checked(failures) =>
1071
+ expect(failures->Array.length)->toBe(2)
1072
+ expect(failures->Array.some(f => f->String.includes("Discontinud")))->toBe(true)
1073
+ expect(failures->Array.some(f => f->String.includes("Withdrawn")))->toBe(true)
1074
+ // Naming what the field does declare is what turns the failure into a fix.
1075
+ expect(failures->Array.every(f => f->String.includes("Listed, Archived")))->toBe(true)
1076
+ | other => expect(other)->toEqual(Plugin_Structure.Checked([]))
1077
+ }
1078
+ })
1079
+
1080
+ // The condition that came with promoting this: a fatal rule that is silent
1081
+ // when it cannot run looks exactly like one that passed.
1082
+ testSync("a field with no cases to compare against says so instead of passing", () => {
1083
+ let schema =
1084
+ S.schema(s => {"shelf": s.matches(S.string)})->withRetiredStates(
1085
+ ~field="shelf",
1086
+ ~lifecycle=Some("shelf"),
1087
+ ~values=Some(["Archived"]),
1088
+ )
1089
+ switch check(schema) {
1090
+ | Unchecked(why) => expect(why->String.includes("no cases"))->toBe(true)
1091
+ | other => expect(other)->toEqual(Plugin_Structure.Unchecked(""))
1092
+ }
1093
+ })
1094
+
1095
+ // The boolean form names no state, so there is nothing to compare — which is
1096
+ // not the same as having failed to compare it.
1097
+ testSync("the boolean form is not a skip", () => {
1098
+ let schema =
1099
+ S.schema(s => {"archived": s.matches(S.bool)})->withRetiredStates(
1100
+ ~field="archived",
1101
+ ~lifecycle=None,
1102
+ ~values=None,
1103
+ )
1104
+ expect(Plugin_Structure.checkRetiredValue(~entityName="Products", schema))->toEqual(
1105
+ Plugin_Structure.NotDeclared,
1106
+ )
1107
+ })
1108
+
1109
+ // The second rule stays a warning: retirement still works, and which field
1110
+ // carries it is a modelling judgement rather than a name that is wrong.
1111
+ testSync("a state on a non-lifecycle field is not a failure", () => {
1112
+ let schema =
1113
+ S.schema(s =>
1114
+ {
1115
+ "shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
1116
+ "lifecycle": s.matches(S.union([S.literal("Draft"), S.literal("Live")])),
1117
+ }
1118
+ )->withRetiredStates(~field="shelf", ~lifecycle=Some("lifecycle"), ~values=Some(["Archived"]))
1119
+ expect(check(schema))->toEqual(Plugin_Structure.Checked([]))
1120
+ })
1121
+ })
1122
+
1020
1123
  // Shared by the read-side and write-side schema cases below, which ask the
1021
1124
  // same question of the same emitter.
1022
1125
  let getProperty = (json: JSON.t, key: string): option<JSON.t> =>
@@ -1053,7 +1053,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1053
1053
  field: field,
1054
1054
  label: "",
1055
1055
  showWhenFalse: false,
1056
- values: undefined
1056
+ values: undefined,
1057
+ namedWhenRetired: false
1057
1058
  }
1058
1059
  });
1059
1060
  globalThis.test("publishes the retirement state beside the field", () => {
@@ -1083,7 +1084,8 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1083
1084
  values: [
1084
1085
  "Deactivated",
1085
1086
  "Closed"
1086
- ]
1087
+ ],
1088
+ namedWhenRetired: false
1087
1089
  }
1088
1090
  });
1089
1091
  let schema = withState(S.schema(s => ({
@@ -1126,6 +1128,116 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1126
1128
  globalThis.expect(Plugin_Structure$ReventlessCore.retiredFieldFromStateSchema(schema)).toEqual(undefined);
1127
1129
  });
1128
1130
  });
1131
+ globalThis.describe("@retired state names are checked against the field's own enum", () => {
1132
+ let withRetiredStates = (schema, field, lifecycle, values) => S.Metadata.set(schema, StateAnnotations$Reventless.stateAnnotationsId, {
1133
+ ids: [],
1134
+ compositeIds: [],
1135
+ subIds: [],
1136
+ compositeSubIds: [],
1137
+ indexes: [],
1138
+ hidden: [],
1139
+ summary: [],
1140
+ drillTargets: [],
1141
+ drillTargetKeys: [],
1142
+ collapsed: [],
1143
+ scan: [],
1144
+ scanSort: [],
1145
+ semantic: [],
1146
+ metric: [],
1147
+ lifecycle: lifecycle,
1148
+ groupBy: undefined,
1149
+ visibility: undefined,
1150
+ live: undefined,
1151
+ retired: {
1152
+ field: field,
1153
+ label: "",
1154
+ showWhenFalse: false,
1155
+ values: values,
1156
+ namedWhenRetired: false
1157
+ }
1158
+ });
1159
+ let shelf = values => withRetiredStates(S.schema(s => ({
1160
+ shelf: s.m(S.union([
1161
+ S.literal("Listed"),
1162
+ S.literal("Archived")
1163
+ ]))
1164
+ })), "shelf", "shelf", values);
1165
+ globalThis.test("a declared state passes", () => {
1166
+ globalThis.expect(Plugin_Structure$ReventlessCore.checkRetiredValue("Products", shelf(["Archived"]))).toEqual({
1167
+ TAG: "Checked",
1168
+ _0: []
1169
+ });
1170
+ });
1171
+ globalThis.test("each undeclared state is reported on its own", () => {
1172
+ let failures = Plugin_Structure$ReventlessCore.checkRetiredValue("Products", shelf([
1173
+ "Archived",
1174
+ "Discontinud",
1175
+ "Withdrawn"
1176
+ ]));
1177
+ if (typeof failures !== "object") {
1178
+ globalThis.expect(failures).toEqual({
1179
+ TAG: "Checked",
1180
+ _0: []
1181
+ });
1182
+ return;
1183
+ }
1184
+ if (failures.TAG === "Unchecked") {
1185
+ globalThis.expect(failures).toEqual({
1186
+ TAG: "Checked",
1187
+ _0: []
1188
+ });
1189
+ return;
1190
+ }
1191
+ let failures$1 = failures._0;
1192
+ globalThis.expect(failures$1.length).toBe(2);
1193
+ globalThis.expect(failures$1.some(f => f.includes("Discontinud"))).toBe(true);
1194
+ globalThis.expect(failures$1.some(f => f.includes("Withdrawn"))).toBe(true);
1195
+ globalThis.expect(failures$1.every(f => f.includes("Listed, Archived"))).toBe(true);
1196
+ });
1197
+ globalThis.test("a field with no cases to compare against says so instead of passing", () => {
1198
+ let schema = withRetiredStates(S.schema(s => ({
1199
+ shelf: s.m(S.string)
1200
+ })), "shelf", "shelf", ["Archived"]);
1201
+ let why = Plugin_Structure$ReventlessCore.checkRetiredValue("Products", schema);
1202
+ if (typeof why !== "object") {
1203
+ globalThis.expect(why).toEqual({
1204
+ TAG: "Unchecked",
1205
+ _0: ""
1206
+ });
1207
+ return;
1208
+ }
1209
+ if (why.TAG === "Unchecked") {
1210
+ globalThis.expect(why._0.includes("no cases")).toBe(true);
1211
+ return;
1212
+ }
1213
+ globalThis.expect(why).toEqual({
1214
+ TAG: "Unchecked",
1215
+ _0: ""
1216
+ });
1217
+ });
1218
+ globalThis.test("the boolean form is not a skip", () => {
1219
+ let schema = withRetiredStates(S.schema(s => ({
1220
+ archived: s.m(S.bool)
1221
+ })), "archived", undefined, undefined);
1222
+ globalThis.expect(Plugin_Structure$ReventlessCore.checkRetiredValue("Products", schema)).toEqual("NotDeclared");
1223
+ });
1224
+ globalThis.test("a state on a non-lifecycle field is not a failure", () => {
1225
+ let schema = withRetiredStates(S.schema(s => ({
1226
+ shelf: s.m(S.union([
1227
+ S.literal("Listed"),
1228
+ S.literal("Archived")
1229
+ ])),
1230
+ lifecycle: s.m(S.union([
1231
+ S.literal("Draft"),
1232
+ S.literal("Live")
1233
+ ]))
1234
+ })), "shelf", "lifecycle", ["Archived"]);
1235
+ globalThis.expect(Plugin_Structure$ReventlessCore.checkRetiredValue("Products", schema)).toEqual({
1236
+ TAG: "Checked",
1237
+ _0: []
1238
+ });
1239
+ });
1240
+ });
1129
1241
  let getProperty = (json, key) => {
1130
1242
  let obj = Stdlib_JSON.Decode.object(json);
1131
1243
  if (obj !== undefined) {