feat(data-plane): add bounded Gelios telemetry contract
This commit is contained in:
@@ -9,6 +9,7 @@ COPY packages/external-provider-contract/src/data-product.mjs ./packages/externa
|
||||
COPY packages/external-provider-contract/src/geometry.mjs ./packages/external-provider-contract/src/geometry.mjs
|
||||
COPY packages/external-provider-contract/src/intake-batch.mjs ./packages/external-provider-contract/src/intake-batch.mjs
|
||||
COPY packages/external-provider-contract/src/sensitive-field-policy.mjs ./packages/external-provider-contract/src/sensitive-field-policy.mjs
|
||||
COPY packages/external-provider-contract/src/telemetry-readings.mjs ./packages/external-provider-contract/src/telemetry-readings.mjs
|
||||
COPY services/external-data-plane/package.json services/external-data-plane/package-lock.json ./services/external-data-plane/
|
||||
|
||||
WORKDIR /workspace/services/external-data-plane
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"id": "fleet.positions.current.v5",
|
||||
"version": "5.0.0",
|
||||
"ontologyRevision": "ontology.map.moving_object.v3",
|
||||
"deliveryMode": "snapshot+patch",
|
||||
"semanticTypes": [
|
||||
"map.moving_object"
|
||||
],
|
||||
"fields": [
|
||||
"course_degrees",
|
||||
"display_name",
|
||||
"elevation_meters",
|
||||
"engine_hours",
|
||||
"geometry",
|
||||
"hdop",
|
||||
"horizontal_accuracy_meters",
|
||||
"mileage_km",
|
||||
"movement_state",
|
||||
"object_kind",
|
||||
"position_source",
|
||||
"satellite_count",
|
||||
"sensor_readings",
|
||||
"signal_state",
|
||||
"speed_kph"
|
||||
],
|
||||
"fieldContracts": {
|
||||
"course_degrees": { "type": "number", "required": false, "minimum": 0, "maximum": 360 },
|
||||
"display_name": { "type": "string", "required": true },
|
||||
"elevation_meters": { "type": "number", "required": false },
|
||||
"engine_hours": { "type": "number", "required": false, "minimum": 0 },
|
||||
"geometry": { "type": "point", "required": false },
|
||||
"hdop": { "type": "number", "required": false, "minimum": 0 },
|
||||
"horizontal_accuracy_meters": { "type": "number", "required": false, "minimum": 0 },
|
||||
"mileage_km": { "type": "number", "required": false, "minimum": 0 },
|
||||
"movement_state": { "type": "string", "required": true, "enum": ["moving", "stopped"] },
|
||||
"object_kind": { "type": "string", "required": true },
|
||||
"position_source": { "type": "string", "required": true },
|
||||
"satellite_count": { "type": "number", "required": false, "minimum": 0 },
|
||||
"sensor_readings": { "type": "telemetry_readings", "required": false },
|
||||
"signal_state": { "type": "string", "required": true, "enum": ["active", "inactive"] },
|
||||
"speed_kph": { "type": "number", "required": false, "minimum": 0 }
|
||||
},
|
||||
"history": {
|
||||
"mode": "sampled",
|
||||
"intervalMs": 60000,
|
||||
"strategy": "latest-per-entity-per-bucket",
|
||||
"retentionDays": 90
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DATA_PRODUCT_PATCH_SCHEMA_VERSION,
|
||||
DATA_PRODUCT_SNAPSHOT_SCHEMA_VERSION,
|
||||
isBoundedGeoJsonGeometry,
|
||||
isBoundedTelemetryReadings,
|
||||
} from "@nodedc/external-provider-contract/data-plane";
|
||||
|
||||
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||
@@ -854,6 +855,7 @@ function assertFieldContractValue(value, contract) {
|
||||
|
||||
function fieldContractValueMatchesType(value, type) {
|
||||
if (type === "string_array") return Array.isArray(value) && value.every((item) => typeof item === "string");
|
||||
if (type === "telemetry_readings") return isBoundedTelemetryReadings(value);
|
||||
if (type === "point") return isBoundedGeoJsonGeometry(value, new Set(["Point"]));
|
||||
if (type === "geometry") return isBoundedGeoJsonGeometry(value);
|
||||
return typeof value === type && (type !== "number" || Number.isFinite(value));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isBoundedGeoJsonGeometry } from "@nodedc/external-provider-contract/data-plane";
|
||||
import { isBoundedGeoJsonGeometry, isBoundedTelemetryReadings } from "@nodedc/external-provider-contract/data-plane";
|
||||
|
||||
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][a-z0-9.-]+)?$/i;
|
||||
const DELIVERY_MODES = new Set(["snapshot", "snapshot+patch", "query"]);
|
||||
const HISTORY_MODES = new Set(["none", "all", "sampled"]);
|
||||
const FIELD_CONTRACT_TYPES = new Set(["string", "number", "boolean", "string_array", "point", "geometry"]);
|
||||
const FIELD_CONTRACT_TYPES = new Set(["string", "number", "boolean", "string_array", "telemetry_readings", "point", "geometry"]);
|
||||
const DEFINITION_KEYS = new Set([
|
||||
"id", "version", "ontologyRevision", "deliveryMode", "semanticTypes", "fields", "fieldContracts", "history",
|
||||
]);
|
||||
@@ -65,7 +65,7 @@ function normalizeFieldContract(value) {
|
||||
if (value.enum !== undefined) {
|
||||
if (!Array.isArray(value.enum) || value.enum.length === 0
|
||||
|| new Set(value.enum.map(stableLiteral)).size !== value.enum.length
|
||||
|| new Set(["point", "geometry", "string_array"]).has(type)
|
||||
|| new Set(["point", "geometry", "string_array", "telemetry_readings"]).has(type)
|
||||
|| value.enum.some((item) => !fieldContractValueMatchesType(item, type))) {
|
||||
throw policyError("data_product_field_contract_enum_invalid");
|
||||
}
|
||||
@@ -174,6 +174,7 @@ function hasOnlyKeys(value, allowed) {
|
||||
|
||||
function fieldContractValueMatchesType(value, type) {
|
||||
if (type === "string_array") return Array.isArray(value) && value.every((item) => typeof item === "string");
|
||||
if (type === "telemetry_readings") return isBoundedTelemetryReadings(value);
|
||||
if (type === "point") return isBoundedGeoJsonGeometry(value, new Set(["Point"]));
|
||||
if (type === "geometry") return isBoundedGeoJsonGeometry(value);
|
||||
return typeof value === type && (type !== "number" || Number.isFinite(value));
|
||||
|
||||
@@ -65,6 +65,40 @@ assert.throws(
|
||||
}, definition),
|
||||
(error) => error?.status === 422 && error?.code === "data_product_field_type_invalid",
|
||||
);
|
||||
|
||||
const telemetryDefinition = normalizeDataProductDefinition({
|
||||
id: "test.telemetry.current.v1",
|
||||
version: "1.0.0",
|
||||
ontologyRevision: "ontology.test.telemetry.v1",
|
||||
deliveryMode: "snapshot+patch",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
fields: ["sensor_readings"],
|
||||
fieldContracts: {
|
||||
sensor_readings: { type: "telemetry_readings", required: false },
|
||||
},
|
||||
history: { mode: "none", retentionDays: 1 },
|
||||
});
|
||||
const validReadings = [{
|
||||
id: "battery.voltage",
|
||||
label: "Battery voltage",
|
||||
value: 51.7,
|
||||
unit: "V",
|
||||
observedAt: "2026-07-22T08:00:00.000Z",
|
||||
}];
|
||||
assert.doesNotThrow(() => assertPublishMatchesDefinition({ facts: [{
|
||||
sourceId: "unit-telemetry",
|
||||
semanticType: "map.moving_object",
|
||||
observedAt: "2026-07-22T08:00:00.000Z",
|
||||
attributes: { sensor_readings: validReadings },
|
||||
}] }, telemetryDefinition));
|
||||
assert.throws(
|
||||
() => assertPublishMatchesDefinition({ facts: [{
|
||||
sourceId: "unit-telemetry",
|
||||
semanticType: "map.moving_object",
|
||||
attributes: { sensor_readings: [{ ...validReadings[0], value: { raw: 51.7 } }] },
|
||||
}] }, telemetryDefinition),
|
||||
(error) => error?.status === 422 && error?.code === "data_product_field_type_invalid",
|
||||
);
|
||||
assert.throws(
|
||||
() => assertPublishMatchesDefinition({
|
||||
facts: [{ ...fact, attributes: { ...fact.attributes, speed_kph: -1 } }],
|
||||
|
||||
@@ -95,6 +95,15 @@ 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);
|
||||
|
||||
const telemetry = normalizeDataProductDefinition({
|
||||
...input,
|
||||
fields: ["sensor_readings"],
|
||||
fieldContracts: {
|
||||
sensor_readings: { type: "telemetry_readings", required: false },
|
||||
},
|
||||
});
|
||||
assert.equal(telemetry.fieldContracts.sensor_readings.type, "telemetry_readings");
|
||||
|
||||
assert.throws(
|
||||
() => normalizeDataProductDefinition({
|
||||
...input,
|
||||
|
||||
@@ -11,6 +11,7 @@ assert.deepEqual(bundled.map((definition) => definition.id), [
|
||||
"fleet.positions.current.v2",
|
||||
"fleet.positions.current.v3",
|
||||
"fleet.positions.current.v4",
|
||||
"fleet.positions.current.v5",
|
||||
"map.zones.current.v1",
|
||||
"map.zones.current.v2",
|
||||
]);
|
||||
@@ -47,19 +48,23 @@ assert.deepEqual(bundled[3].fieldContracts.movement_state, {
|
||||
required: true,
|
||||
enum: ["moving", "stopped"],
|
||||
});
|
||||
assert.equal(bundled[4].version, "1.0.0");
|
||||
assert.equal(bundled[4].ontologyRevision, "ontology.map.zone.v1");
|
||||
assert.deepEqual(bundled[4].semanticTypes, ["map.zone"]);
|
||||
assert.deepEqual(bundled[4].fieldContracts.geometry, { type: "geometry", required: true });
|
||||
assert.equal(bundled[4].fields.includes("dataset_authority"), false);
|
||||
assert.equal(bundled[5].version, "2.0.0");
|
||||
assert.equal(bundled[4].version, "5.0.0");
|
||||
assert.equal(bundled[4].ontologyRevision, "ontology.map.moving_object.v3");
|
||||
assert.equal(bundled[4].fieldContracts.sensor_readings.type, "telemetry_readings");
|
||||
assert.equal(bundled[5].version, "1.0.0");
|
||||
assert.equal(bundled[5].ontologyRevision, "ontology.map.zone.v1");
|
||||
assert.deepEqual(bundled[5].semanticTypes, ["map.zone"]);
|
||||
assert.deepEqual(bundled[5].fieldContracts.geometry, { type: "geometry", required: true });
|
||||
assert.deepEqual(bundled[5].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]);
|
||||
assert.equal(bundled[5].fields.includes("dataset_authority"), false);
|
||||
assert.equal(bundled[6].version, "2.0.0");
|
||||
assert.equal(bundled[6].ontologyRevision, "ontology.map.zone.v1");
|
||||
assert.deepEqual(bundled[6].semanticTypes, ["map.zone"]);
|
||||
assert.deepEqual(bundled[6].fieldContracts.geometry, { type: "geometry", required: true });
|
||||
assert.deepEqual(bundled[6].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]);
|
||||
assert.equal(Object.keys(bundled[3].fieldContracts).length, bundled[3].fields.length);
|
||||
assert.equal(Object.keys(bundled[4].fieldContracts).length, bundled[4].fields.length);
|
||||
assert.equal(Object.keys(bundled[5].fieldContracts).length, bundled[5].fields.length);
|
||||
assert.equal(Object.keys(bundled[6].fieldContracts).length, bundled[6].fields.length);
|
||||
|
||||
const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-definitions-"));
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user