feat(foundry): support restricted subject aspects
This commit is contained in:
@@ -132,6 +132,73 @@ test("subject details compose declared aspect bindings by stable source id", ()
|
||||
assert.equal(card.tabs[0].sections[0].rows[0].value, "cargo_trike");
|
||||
});
|
||||
|
||||
test("restricted joined aspects render declared identifiers and remain fail-closed otherwise", () => {
|
||||
const profile = {
|
||||
id: "map.subject-detail.restricted",
|
||||
version: "1.0.0",
|
||||
title: "Restricted",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
defaultTabId: "contacts",
|
||||
tabs: [{
|
||||
id: "contacts",
|
||||
label: "Контакты",
|
||||
emptyMessage: "Нет данных",
|
||||
sections: [{
|
||||
id: "device",
|
||||
label: "Контакты устройства",
|
||||
fields: [
|
||||
{
|
||||
id: "contacts.phone",
|
||||
aspectId: "unit_contacts",
|
||||
source: "attribute",
|
||||
field: "device_phone_primary",
|
||||
label: "Телефон 1",
|
||||
format: "text",
|
||||
dataClass: "restricted",
|
||||
},
|
||||
{
|
||||
id: "contacts.imei",
|
||||
aspectId: "unit_contacts",
|
||||
source: "attribute",
|
||||
field: "device_imei",
|
||||
label: "IMEI",
|
||||
format: "text",
|
||||
dataClass: "restricted",
|
||||
},
|
||||
],
|
||||
}],
|
||||
}],
|
||||
};
|
||||
const contactAspect = {
|
||||
bindingId: "trike-unit-contacts",
|
||||
dataProductId: "fleet.units.contacts.current.v1",
|
||||
dataClass: "restricted",
|
||||
fact: {
|
||||
sourceId: fact.sourceId,
|
||||
semanticType: "map.moving_object",
|
||||
attributes: {
|
||||
device_phone_primary: "+79991234567",
|
||||
device_imei: "867236078012345",
|
||||
},
|
||||
},
|
||||
};
|
||||
const card = buildMapSubjectCardModel(fact, {
|
||||
profile,
|
||||
aspects: { unit_contacts: contactAspect },
|
||||
});
|
||||
assert.deepEqual(
|
||||
card.tabs[0].sections[0].rows.map((row) => row.value),
|
||||
["+79991234567", "867236078012345"],
|
||||
);
|
||||
const rejected = buildMapSubjectCardModel(fact, {
|
||||
profile,
|
||||
aspects: {
|
||||
unit_contacts: { ...contactAspect, dataClass: "operational" },
|
||||
},
|
||||
});
|
||||
assert.equal(rejected.tabs[0].empty, true);
|
||||
});
|
||||
|
||||
test("Cesium pin and label share one entity selection that opens the subject card", async () => {
|
||||
const [renderer, preview] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
|
||||
|
||||
@@ -80,6 +80,7 @@ for (const policy of consumerPolicies) {
|
||||
"terminalStatuses",
|
||||
"statusContract",
|
||||
"consumerContract",
|
||||
"dataClass",
|
||||
"removeMode",
|
||||
]);
|
||||
requireValue(Object.keys(policy).every((key) => allowedPolicyKeys.has(key)), `unsupported data product consumer policy key: ${policy.id ?? "unknown"}`);
|
||||
@@ -89,6 +90,7 @@ for (const policy of consumerPolicies) {
|
||||
requireValue(typeof policy.productVersion === "string" && /^\d+\.\d+\.\d+$/.test(policy.productVersion), `invalid data product consumer product version: ${policy.id ?? "unknown"}`);
|
||||
requireValue(Array.isArray(policy.terminalStatuses) && policy.terminalStatuses.every((value) => typeof value === "string" && /^[a-z0-9_-]{1,64}$/.test(value)), `invalid terminal statuses: ${policy.id ?? "unknown"}`);
|
||||
requireValue(policy.removeMode === "canonical-tombstone-or-snapshot-rebase", `invalid remove mode: ${policy.id ?? "unknown"}`);
|
||||
requireValue(["operational", "restricted"].includes(policy.dataClass ?? "operational"), `invalid data class: ${policy.id ?? "unknown"}`);
|
||||
requireValue(["none", "observed-at"].includes(freshness), `invalid freshness mode: ${policy.id ?? "unknown"}`);
|
||||
if (policy.consumerContract !== undefined) {
|
||||
const contract = policy.consumerContract;
|
||||
@@ -205,6 +207,42 @@ requireValue(JSON.stringify(unitProfileV1Policy?.consumerContract?.fieldProjecti
|
||||
requireValue(JSON.stringify(unitProfileV1Policy?.consumerContract?.geometryTypes) === JSON.stringify([]), "fleet.units.profile.current.v1 must remain a non-geometric aspect");
|
||||
requireValue(!/(provider|tenant|endpoint|credential|token|secret|authorization)/i.test(JSON.stringify(unitProfileV1Policy)), "fleet.units.profile.current.v1 consumer policy crosses the transport/secret boundary");
|
||||
|
||||
const unitContactsV1Policy = consumerPolicies.find(
|
||||
(policy) => policy.dataProductId === "fleet.units.contacts.current.v1"
|
||||
&& policy.productVersion === "1.0.0",
|
||||
);
|
||||
const unitContactsV1Projection = [
|
||||
"device_imei",
|
||||
"device_phone_primary",
|
||||
"device_phone_secondary",
|
||||
"display_name",
|
||||
"provider_creator_login",
|
||||
];
|
||||
requireValue(
|
||||
unitContactsV1Policy?.id === "map-moving-object-unit-contacts-current-v1"
|
||||
&& unitContactsV1Policy?.version === "1.0.0",
|
||||
"fleet.units.contacts.current.v1 consumer policy is missing",
|
||||
);
|
||||
requireValue(
|
||||
unitContactsV1Policy?.dataClass === "restricted",
|
||||
"fleet.units.contacts.current.v1 must be explicitly restricted",
|
||||
);
|
||||
requireValue(
|
||||
JSON.stringify(unitContactsV1Policy?.consumerContract?.fieldProjection)
|
||||
=== JSON.stringify(unitContactsV1Projection),
|
||||
"fleet.units.contacts.current.v1 field projection must be exact",
|
||||
);
|
||||
requireValue(
|
||||
JSON.stringify(unitContactsV1Policy?.consumerContract?.geometryTypes) === JSON.stringify([]),
|
||||
"fleet.units.contacts.current.v1 must remain a non-geometric aspect",
|
||||
);
|
||||
requireValue(
|
||||
!/(tenant|endpoint|credential|token|secret|authorization)/i.test(
|
||||
JSON.stringify(unitContactsV1Policy),
|
||||
),
|
||||
"fleet.units.contacts.current.v1 consumer policy crosses the transport/secret boundary",
|
||||
);
|
||||
|
||||
const ids = components.components.map((component) => component.id);
|
||||
requireValue(new Set(ids).size === ids.length, "component ids must be unique");
|
||||
const candidateIds = candidates.candidates.map((component) => component.id);
|
||||
|
||||
Reference in New Issue
Block a user