feat(platform): complete the Gelios external data loop
This commit is contained in:
@@ -2,10 +2,12 @@ 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"]);
|
||||
const DEFINITION_KEYS = new Set([
|
||||
"id", "version", "ontologyRevision", "deliveryMode", "semanticTypes", "fields", "history",
|
||||
"id", "version", "ontologyRevision", "deliveryMode", "semanticTypes", "fields", "fieldContracts", "history",
|
||||
]);
|
||||
const HISTORY_KEYS = new Set(["mode", "intervalMs", "retentionDays", "strategy"]);
|
||||
const FIELD_CONTRACT_KEYS = new Set(["type", "required", "enum", "minimum", "maximum"]);
|
||||
|
||||
export function normalizeDataProductDefinition(value) {
|
||||
if (!isPlainObject(value) || !hasOnlyKeys(value, DEFINITION_KEYS)) {
|
||||
@@ -30,8 +32,58 @@ export function normalizeDataProductDefinition(value) {
|
||||
}
|
||||
if (!semanticTypes.length || !fields.length) throw policyError("data_product_definition_shape_invalid");
|
||||
|
||||
const fieldContracts = normalizeFieldContracts(value.fieldContracts, fields);
|
||||
const history = normalizeHistoryPolicy(value.history);
|
||||
return Object.freeze({ id, version, ontologyRevision, deliveryMode, semanticTypes, fields, history });
|
||||
return Object.freeze({ id, version, ontologyRevision, deliveryMode, semanticTypes, fields, fieldContracts, history });
|
||||
}
|
||||
|
||||
export function normalizeFieldContracts(value, fields) {
|
||||
if (value === undefined) return Object.freeze({});
|
||||
if (!isPlainObject(value)) throw policyError("data_product_field_contracts_invalid");
|
||||
const names = Object.keys(value);
|
||||
if (names.length === 0) return Object.freeze({});
|
||||
if (names.length !== fields.length || fields.some((field) => !Object.hasOwn(value, field))) {
|
||||
throw policyError("data_product_field_contracts_must_match_fields");
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
for (const field of fields) normalized[field] = normalizeFieldContract(value[field]);
|
||||
return Object.freeze(normalized);
|
||||
}
|
||||
|
||||
function normalizeFieldContract(value) {
|
||||
if (!isPlainObject(value) || !hasOnlyKeys(value, FIELD_CONTRACT_KEYS)) {
|
||||
throw policyError("data_product_field_contract_invalid");
|
||||
}
|
||||
const type = string(value.type);
|
||||
if (!FIELD_CONTRACT_TYPES.has(type) || typeof value.required !== "boolean") {
|
||||
throw policyError("data_product_field_contract_shape_invalid");
|
||||
}
|
||||
const contract = { type, required: value.required };
|
||||
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", "string_array"]).has(type)
|
||||
|| value.enum.some((item) => !fieldContractValueMatchesType(item, type))) {
|
||||
throw policyError("data_product_field_contract_enum_invalid");
|
||||
}
|
||||
contract.enum = Object.freeze([...value.enum].sort((left, right) => stableLiteral(left).localeCompare(stableLiteral(right))));
|
||||
}
|
||||
if (value.minimum !== undefined || value.maximum !== undefined) {
|
||||
if (type !== "number"
|
||||
|| (value.minimum !== undefined && !Number.isFinite(value.minimum))
|
||||
|| (value.maximum !== undefined && !Number.isFinite(value.maximum))
|
||||
|| (Number.isFinite(value.minimum) && Number.isFinite(value.maximum) && value.minimum > value.maximum)) {
|
||||
throw policyError("data_product_field_contract_range_invalid");
|
||||
}
|
||||
if (value.minimum !== undefined) contract.minimum = value.minimum;
|
||||
if (value.maximum !== undefined) contract.maximum = value.maximum;
|
||||
if (contract.enum?.some((item) => (
|
||||
(contract.minimum !== undefined && item < contract.minimum)
|
||||
|| (contract.maximum !== undefined && item > contract.maximum)
|
||||
))) throw policyError("data_product_field_contract_enum_out_of_range");
|
||||
}
|
||||
return Object.freeze(contract);
|
||||
}
|
||||
|
||||
export function normalizeHistoryPolicy(value = { mode: "none" }) {
|
||||
@@ -62,6 +114,7 @@ export function safeDataProductDefinition(row) {
|
||||
deliveryMode: row.deliveryMode,
|
||||
semanticTypes: array(row.semanticTypes),
|
||||
fields: array(row.fields),
|
||||
fieldContracts: isPlainObject(row.fieldContracts) ? row.fieldContracts : {},
|
||||
history: isPlainObject(row.historyPolicy) ? row.historyPolicy : {},
|
||||
active: row.active === true,
|
||||
createdAt: iso(row.createdAt),
|
||||
@@ -116,3 +169,11 @@ function isPlainObject(value) {
|
||||
function hasOnlyKeys(value, allowed) {
|
||||
return Object.keys(value).every((key) => allowed.has(key));
|
||||
}
|
||||
|
||||
function fieldContractValueMatchesType(value, type) {
|
||||
return typeof value === type && (type !== "number" || Number.isFinite(value));
|
||||
}
|
||||
|
||||
function stableLiteral(value) {
|
||||
return `${typeof value}:${JSON.stringify(value)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user