feat(platform): complete the Gelios external data loop
This commit is contained in:
@@ -38,6 +38,12 @@ try {
|
||||
deliveryMode: "snapshot+patch",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
fields: ["source_id", "observed_at", "geometry", "status"],
|
||||
fieldContracts: {
|
||||
source_id: { type: "string", required: true },
|
||||
observed_at: { type: "string", required: true },
|
||||
geometry: { type: "point", required: true },
|
||||
status: { type: "string", required: true },
|
||||
},
|
||||
history: {
|
||||
mode: "sampled",
|
||||
intervalMs: 60_000,
|
||||
@@ -47,6 +53,7 @@ try {
|
||||
});
|
||||
await persistDataProductDefinition(pool, definition);
|
||||
const storedDefinition = await loadDataProductDefinition(pool, definition.id);
|
||||
assert.deepEqual(storedDefinition.fieldContracts, definition.fieldContracts);
|
||||
const binding = {
|
||||
id: "reader-binding-test",
|
||||
bindingKey: "managed-reader-binding-test",
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { assertPublishMatchesDefinition } from "../src/data-product-delivery.mjs";
|
||||
import { normalizeDataProductDefinition } from "../src/data-product-policy.mjs";
|
||||
|
||||
const definition = normalizeDataProductDefinition({
|
||||
id: "test.positions.current.v2",
|
||||
version: "2.0.0",
|
||||
ontologyRevision: "ontology.test.positions.v1",
|
||||
deliveryMode: "snapshot+patch",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
fields: ["display_name", "geometry", "movement_state", "signal_state", "speed_kph"],
|
||||
fieldContracts: {
|
||||
display_name: { type: "string", required: true },
|
||||
geometry: { type: "point", required: false },
|
||||
movement_state: { type: "string", required: true, enum: ["moving", "stopped"] },
|
||||
signal_state: { type: "string", required: true, enum: ["active", "inactive"] },
|
||||
speed_kph: { type: "number", required: false, minimum: 0 },
|
||||
},
|
||||
history: { mode: "none", retentionDays: 1 },
|
||||
});
|
||||
|
||||
const fact = {
|
||||
sourceId: "unit-1",
|
||||
semanticType: "map.moving_object",
|
||||
observedAt: "2026-07-20T10:00:00.000Z",
|
||||
geometry: { type: "Point", coordinates: [37.6173, 55.7558] },
|
||||
attributes: {
|
||||
display_name: "Unit 1",
|
||||
movement_state: "moving",
|
||||
signal_state: "active",
|
||||
speed_kph: 12,
|
||||
},
|
||||
};
|
||||
const batch = { facts: [fact] };
|
||||
assert.doesNotThrow(() => assertPublishMatchesDefinition(batch, definition));
|
||||
|
||||
assert.throws(
|
||||
() => assertPublishMatchesDefinition({
|
||||
facts: [{ ...fact, attributes: { ...fact.attributes, movement_state: "teleporting" } }],
|
||||
}, definition),
|
||||
(error) => error?.status === 422 && error?.code === "data_product_field_value_forbidden",
|
||||
);
|
||||
assert.throws(
|
||||
() => assertPublishMatchesDefinition({
|
||||
facts: [{ ...fact, attributes: { ...fact.attributes, signal_state: "invented" } }],
|
||||
}, definition),
|
||||
(error) => error?.status === 422 && error?.code === "data_product_field_value_forbidden",
|
||||
);
|
||||
assert.throws(
|
||||
() => assertPublishMatchesDefinition({
|
||||
facts: [{
|
||||
...fact,
|
||||
attributes: Object.fromEntries(Object.entries(fact.attributes).filter(([field]) => field !== "signal_state")),
|
||||
}],
|
||||
}, definition),
|
||||
(error) => error?.status === 422 && error?.code === "data_product_field_required",
|
||||
);
|
||||
assert.throws(
|
||||
() => assertPublishMatchesDefinition({
|
||||
facts: [{ ...fact, attributes: { ...fact.attributes, speed_kph: "12" } }],
|
||||
}, definition),
|
||||
(error) => error?.status === 422 && error?.code === "data_product_field_type_invalid",
|
||||
);
|
||||
assert.throws(
|
||||
() => assertPublishMatchesDefinition({
|
||||
facts: [{ ...fact, attributes: { ...fact.attributes, speed_kph: -1 } }],
|
||||
}, definition),
|
||||
(error) => error?.status === 422 && error?.code === "data_product_field_value_out_of_range",
|
||||
);
|
||||
assert.throws(
|
||||
() => assertPublishMatchesDefinition({
|
||||
facts: [{ ...fact, geometry: { type: "Point", coordinates: [181, 55.7558] } }],
|
||||
}, definition),
|
||||
(error) => error?.status === 422 && error?.code === "data_product_field_type_invalid",
|
||||
);
|
||||
|
||||
const legacyDefinition = normalizeDataProductDefinition({
|
||||
id: "test.positions.current.v1",
|
||||
version: "1.0.0",
|
||||
ontologyRevision: "ontology.test.positions.v1",
|
||||
deliveryMode: "snapshot+patch",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
fields: ["movement_state", "signal_state"],
|
||||
history: { mode: "none", retentionDays: 1 },
|
||||
});
|
||||
assert.doesNotThrow(() => assertPublishMatchesDefinition({
|
||||
facts: [{
|
||||
sourceId: "unit-legacy",
|
||||
semanticType: "map.moving_object",
|
||||
attributes: { movement_state: "teleporting", signal_state: "invented" },
|
||||
}],
|
||||
}, legacyDefinition));
|
||||
|
||||
console.log("external-data-plane data product delivery policy: ok");
|
||||
@@ -24,6 +24,8 @@ assert.deepEqual(input.semanticTypes, ["vehicle.trike", "map.moving_object"]);
|
||||
assert.deepEqual(input.fields, ["status", "source_id", "observed_at", "geometry"]);
|
||||
assert.equal(Object.isFrozen(definition.semanticTypes), true);
|
||||
assert.equal(Object.isFrozen(definition.fields), true);
|
||||
assert.deepEqual(definition.fieldContracts, {});
|
||||
assert.equal(Object.isFrozen(definition.fieldContracts), true);
|
||||
|
||||
const reordered = normalizeDataProductDefinition({
|
||||
...input,
|
||||
@@ -78,4 +80,49 @@ assert.throws(
|
||||
/data_product_definition_fields_duplicate/,
|
||||
);
|
||||
|
||||
const strict = normalizeDataProductDefinition({
|
||||
...input,
|
||||
fields: ["geometry", "movement_state", "signal_state", "speed_kph"],
|
||||
fieldContracts: {
|
||||
geometry: { type: "point", required: false },
|
||||
movement_state: { type: "string", required: true, enum: ["stopped", "moving"] },
|
||||
signal_state: { type: "string", required: true, enum: ["inactive", "active"] },
|
||||
speed_kph: { type: "number", required: false, minimum: 0 },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(strict.fieldContracts.movement_state.enum, ["moving", "stopped"]);
|
||||
assert.equal(Object.isFrozen(strict.fieldContracts), true);
|
||||
assert.equal(Object.isFrozen(strict.fieldContracts.signal_state), true);
|
||||
assert.equal(Object.isFrozen(strict.fieldContracts.signal_state.enum), true);
|
||||
|
||||
assert.throws(
|
||||
() => normalizeDataProductDefinition({
|
||||
...input,
|
||||
fieldContracts: { status: { type: "string", required: true } },
|
||||
}),
|
||||
/data_product_field_contracts_must_match_fields/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeDataProductDefinition({
|
||||
...input,
|
||||
fieldContracts: Object.fromEntries(input.fields.map((field) => [
|
||||
field,
|
||||
field === "status"
|
||||
? { type: "string", required: true, enum: ["active", 1] }
|
||||
: { type: field === "geometry" ? "point" : "string", required: false },
|
||||
])),
|
||||
}),
|
||||
/data_product_field_contract_enum_invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeDataProductDefinition({
|
||||
...strict,
|
||||
fieldContracts: {
|
||||
...strict.fieldContracts,
|
||||
speed_kph: { type: "number", required: false, minimum: 10, maximum: 1 },
|
||||
},
|
||||
}),
|
||||
/data_product_field_contract_range_invalid/,
|
||||
);
|
||||
|
||||
console.log("external-data-plane data product policy: ok");
|
||||
|
||||
@@ -6,7 +6,12 @@ import { join } from "node:path";
|
||||
import { loadDataProductDefinitions } from "../src/definitions.mjs";
|
||||
|
||||
const bundled = await loadDataProductDefinitions();
|
||||
assert.deepEqual(bundled.map((definition) => definition.id), ["fleet.positions.current.v1"]);
|
||||
assert.deepEqual(bundled.map((definition) => definition.id), [
|
||||
"fleet.positions.current.v1",
|
||||
"fleet.positions.current.v2",
|
||||
"fleet.positions.current.v3",
|
||||
"fleet.positions.current.v4",
|
||||
]);
|
||||
assert.deepEqual(bundled[0].semanticTypes, ["map.moving_object"]);
|
||||
assert.equal(bundled[0].ontologyRevision, "ontology.map.moving_object.v1");
|
||||
assert.equal(bundled[0].history.mode, "sampled");
|
||||
@@ -15,6 +20,32 @@ assert.equal(bundled[0].history.retentionDays, 90);
|
||||
assert.equal(bundled[0].fields.includes("geometry"), true);
|
||||
assert.equal(bundled[0].fields.includes("providerUnitId"), false);
|
||||
assert.equal(bundled[0].fields.includes("provider_unit_id"), false);
|
||||
assert.equal(bundled[1].version, "2.0.0");
|
||||
assert.equal(bundled[1].ontologyRevision, "ontology.map.moving_object.v2");
|
||||
assert.deepEqual(
|
||||
bundled[1].fields.filter((field) => field.endsWith("_state")),
|
||||
["availability_state", "freshness_state", "motion_state", "position_state"],
|
||||
);
|
||||
assert.equal(bundled[2].version, "3.0.0");
|
||||
assert.equal(bundled[2].ontologyRevision, "ontology.map.moving_object.v3");
|
||||
assert.deepEqual(
|
||||
bundled[2].fields.filter((field) => field.endsWith("_state")),
|
||||
["movement_state", "signal_state"],
|
||||
);
|
||||
assert.equal(bundled[2].fields.includes("operational_status"), false);
|
||||
assert.equal(bundled[3].version, "4.0.0");
|
||||
assert.equal(bundled[3].ontologyRevision, "ontology.map.moving_object.v3");
|
||||
assert.deepEqual(bundled[3].fieldContracts.signal_state, {
|
||||
type: "string",
|
||||
required: true,
|
||||
enum: ["active", "inactive"],
|
||||
});
|
||||
assert.deepEqual(bundled[3].fieldContracts.movement_state, {
|
||||
type: "string",
|
||||
required: true,
|
||||
enum: ["moving", "stopped"],
|
||||
});
|
||||
assert.equal(Object.keys(bundled[3].fieldContracts).length, bundled[3].fields.length);
|
||||
|
||||
const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-definitions-"));
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user