@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.
- package/CHANGELOG.md +12 -0
- package/package.json +8 -8
- 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/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/plugin/component/Plugin_Builder.res +7 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
- package/src/plugin/component/Plugin_Structure.res +109 -22
- package/src/plugin/component/Plugin_Structure.res.mjs +47 -7
- 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/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/plugin/PluginStructureTest.res +104 -1
- 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) {
|