feat(platform): complete the Gelios external data loop
This commit is contained in:
@@ -13,7 +13,7 @@ export async function loadDataProductDefinition(db, dataProductId, { activeOnly
|
||||
const result = await db.query(
|
||||
`select id, version, ontology_revision as "ontologyRevision",
|
||||
delivery_mode as "deliveryMode", semantic_types as "semanticTypes",
|
||||
fields, history_policy as "historyPolicy", active,
|
||||
fields, field_contracts as "fieldContracts", history_policy as "historyPolicy", active,
|
||||
created_at as "createdAt", updated_at as "updatedAt"
|
||||
from external_data_plane_products
|
||||
where id = $1 ${activeOnly ? "and active = true" : ""}`,
|
||||
@@ -25,8 +25,8 @@ export async function loadDataProductDefinition(db, dataProductId, { activeOnly
|
||||
export async function persistDataProductDefinition(db, definition) {
|
||||
const result = await db.query(
|
||||
`insert into external_data_plane_products (
|
||||
id, version, ontology_revision, delivery_mode, semantic_types, fields, history_policy
|
||||
) values ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7::jsonb)
|
||||
id, version, ontology_revision, delivery_mode, semantic_types, fields, field_contracts, history_policy
|
||||
) values ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7::jsonb, $8::jsonb)
|
||||
on conflict (id) do update set
|
||||
active = true,
|
||||
updated_at = now()
|
||||
@@ -35,10 +35,11 @@ export async function persistDataProductDefinition(db, definition) {
|
||||
and external_data_plane_products.delivery_mode = excluded.delivery_mode
|
||||
and external_data_plane_products.semantic_types = excluded.semantic_types
|
||||
and external_data_plane_products.fields = excluded.fields
|
||||
and external_data_plane_products.field_contracts = excluded.field_contracts
|
||||
and external_data_plane_products.history_policy = excluded.history_policy
|
||||
returning id, version, ontology_revision as "ontologyRevision",
|
||||
delivery_mode as "deliveryMode", semantic_types as "semanticTypes",
|
||||
fields, history_policy as "historyPolicy", active,
|
||||
fields, field_contracts as "fieldContracts", history_policy as "historyPolicy", active,
|
||||
created_at as "createdAt", updated_at as "updatedAt"`,
|
||||
[
|
||||
definition.id,
|
||||
@@ -47,6 +48,7 @@ export async function persistDataProductDefinition(db, definition) {
|
||||
definition.deliveryMode,
|
||||
JSON.stringify(definition.semanticTypes),
|
||||
JSON.stringify(definition.fields),
|
||||
JSON.stringify(definition.fieldContracts || {}),
|
||||
JSON.stringify(definition.history),
|
||||
],
|
||||
);
|
||||
@@ -658,6 +660,8 @@ function publishFingerprint(batch) {
|
||||
export function assertPublishMatchesDefinition(batch, definition) {
|
||||
const semanticTypes = new Set(Array.isArray(definition?.semanticTypes) ? definition.semanticTypes : []);
|
||||
const fields = new Set(Array.isArray(definition?.fields) ? definition.fields : []);
|
||||
const fieldContracts = definition?.fieldContracts && typeof definition.fieldContracts === "object"
|
||||
&& !Array.isArray(definition.fieldContracts) ? definition.fieldContracts : {};
|
||||
const entityKeys = new Set();
|
||||
for (const fact of batch?.facts || []) {
|
||||
if (!semanticTypes.has(fact.semanticType)) throw deliveryError("data_product_semantic_type_forbidden", 422);
|
||||
@@ -672,9 +676,55 @@ export function assertPublishMatchesDefinition(batch, definition) {
|
||||
if (fact.geometry !== undefined && !geometryDeclared(fields)) {
|
||||
throw deliveryError("data_product_geometry_forbidden", 422);
|
||||
}
|
||||
for (const [field, contract] of Object.entries(fieldContracts)) {
|
||||
const resolved = resolveContractField(fact, field);
|
||||
if (!resolved.present) {
|
||||
if (contract.required === true) throw deliveryError("data_product_field_required", 422);
|
||||
continue;
|
||||
}
|
||||
assertFieldContractValue(resolved.value, contract);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveContractField(fact, field) {
|
||||
if (field === "geometry") return { present: fact.geometry !== undefined, value: fact.geometry };
|
||||
if (field === "source_id") return { present: fact.sourceId !== undefined, value: fact.sourceId };
|
||||
if (field === "semantic_type") return { present: fact.semanticType !== undefined, value: fact.semanticType };
|
||||
if (field === "observed_at") return { present: fact.observedAt !== undefined, value: fact.observedAt };
|
||||
const attribute = field.startsWith("attributes.") ? field.slice("attributes.".length) : field;
|
||||
const attributes = fact.attributes || {};
|
||||
return { present: Object.hasOwn(attributes, attribute), value: attributes[attribute] };
|
||||
}
|
||||
|
||||
function assertFieldContractValue(value, contract) {
|
||||
if (!fieldContractValueMatchesType(value, contract.type)) {
|
||||
throw deliveryError("data_product_field_type_invalid", 422);
|
||||
}
|
||||
if (Array.isArray(contract.enum) && !contract.enum.some((allowed) => Object.is(allowed, value))) {
|
||||
throw deliveryError("data_product_field_value_forbidden", 422);
|
||||
}
|
||||
if (typeof value === "number" && (
|
||||
(Number.isFinite(contract.minimum) && value < contract.minimum)
|
||||
|| (Number.isFinite(contract.maximum) && value > contract.maximum)
|
||||
)) throw deliveryError("data_product_field_value_out_of_range", 422);
|
||||
}
|
||||
|
||||
function fieldContractValueMatchesType(value, type) {
|
||||
if (type === "string_array") return Array.isArray(value) && value.every((item) => typeof item === "string");
|
||||
if (type === "point") {
|
||||
return value?.type === "Point"
|
||||
&& Array.isArray(value.coordinates)
|
||||
&& value.coordinates.length === 2
|
||||
&& value.coordinates.every(Number.isFinite)
|
||||
&& value.coordinates[0] >= -180
|
||||
&& value.coordinates[0] <= 180
|
||||
&& value.coordinates[1] >= -90
|
||||
&& value.coordinates[1] <= 90;
|
||||
}
|
||||
return typeof value === type && (type !== "number" || Number.isFinite(value));
|
||||
}
|
||||
|
||||
function geometryDeclared(fields) {
|
||||
return fields.has("geometry")
|
||||
|| fields.has("coordinates")
|
||||
|
||||
Reference in New Issue
Block a user