feat(platform): add replaceable geozone data layer
This commit is contained in:
@@ -20,7 +20,7 @@ const TOKEN_REFRESH_MODES = new Set(["not_applicable", "operator_managed", "runt
|
||||
const COLLECTION_MODES = new Set(["realtime", "manual", "history", "weekly"]);
|
||||
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 FIELD_CONTRACT_TYPES = new Set(["string", "number", "boolean", "string_array", "point", "geometry"]);
|
||||
const L2_STEP_KINDS = new Set([
|
||||
"collection_trigger",
|
||||
"provider_request",
|
||||
@@ -84,6 +84,10 @@ const CAPABILITY_KEYS = new Set([
|
||||
]);
|
||||
const REQUEST_KEYS = new Set(["method", "baseUrl", "path", "query", "response"]);
|
||||
const RESPONSE_KEYS = new Set(["collectionPaths", "pagination"]);
|
||||
const OFFSET_PAGINATION_KEYS = new Set([
|
||||
"mode", "limitParameter", "offsetParameter", "pageSize", "itemsPath", "totalPath",
|
||||
"maxPages", "maxItems", "maxResponseBytes",
|
||||
]);
|
||||
const ENTITY_SCOPE_KEYS = new Set(["mode", "refresh", "businessEntityFilter"]);
|
||||
const FIELD_POLICY_KEYS = new Set([
|
||||
"id",
|
||||
@@ -137,7 +141,9 @@ const EXPRESSION_KEYS = new Set([
|
||||
"strategy", "paths", "coerce", "prefix", "fallback", "constant", "derive",
|
||||
"omitIfMissing", "omitIfInvalid", "minimum", "maximum",
|
||||
]);
|
||||
const GEOMETRY_KEYS = new Set(["type", "longitude", "latitude", "omitIfInvalid"]);
|
||||
const GEOMETRY_KEYS = new Set([
|
||||
"type", "longitude", "latitude", "strategy", "paths", "allowedTypes", "omitIfInvalid",
|
||||
]);
|
||||
const DERIVATION_KEYS = new Set(["kind", "rules", "default", "parameters"]);
|
||||
const TEMPLATE_KEYS = new Set([
|
||||
"schemaVersion",
|
||||
@@ -496,12 +502,50 @@ function validateCapability(value, path, errors) {
|
||||
} else {
|
||||
rejectUnknownKeys(value.request.response, RESPONSE_KEYS, `${path}.request.response`, errors);
|
||||
requiredCollectionPathArray(value.request.response.collectionPaths, `${path}.request.response.collectionPaths`, errors);
|
||||
if (value.request.response.pagination !== "single_bounded_response") errors.push(`${path}.request.response.pagination_invalid`);
|
||||
validateResponsePagination(value.request.response.pagination, value.request.query, `${path}.request.response.pagination`, errors);
|
||||
}
|
||||
}
|
||||
validateCredentialVisibleScope(value.entityScope, `${path}.entityScope`, errors);
|
||||
}
|
||||
|
||||
function validateResponsePagination(value, query, path, errors) {
|
||||
if (value === "single_bounded_response") return;
|
||||
if (!isPlainObject(value)) {
|
||||
errors.push(`${path}_invalid`);
|
||||
return;
|
||||
}
|
||||
rejectUnknownKeys(value, OFFSET_PAGINATION_KEYS, path, errors);
|
||||
if (value.mode !== "offset") errors.push(`${path}.mode_must_be_offset`);
|
||||
for (const field of ["limitParameter", "offsetParameter"]) {
|
||||
if (typeof value[field] !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(value[field])) {
|
||||
errors.push(`${path}.${field}_invalid`);
|
||||
}
|
||||
}
|
||||
if (!Number.isInteger(value.pageSize) || value.pageSize < 1 || value.pageSize > 500) {
|
||||
errors.push(`${path}.pageSize_invalid`);
|
||||
}
|
||||
if (!Number.isInteger(value.maxPages) || value.maxPages < 1 || value.maxPages > 1000) {
|
||||
errors.push(`${path}.maxPages_invalid`);
|
||||
}
|
||||
if (!Number.isInteger(value.maxItems) || value.maxItems < 1 || value.maxItems > 5000) {
|
||||
errors.push(`${path}.maxItems_invalid`);
|
||||
}
|
||||
if (!Number.isInteger(value.maxResponseBytes) || value.maxResponseBytes < 1024 || value.maxResponseBytes > 32 * 1024 * 1024) {
|
||||
errors.push(`${path}.maxResponseBytes_invalid`);
|
||||
}
|
||||
for (const field of ["itemsPath", "totalPath"]) {
|
||||
if (!isCanonicalSourcePath(value[field])) errors.push(`${path}.${field}_invalid`);
|
||||
}
|
||||
if (isPlainObject(query)) {
|
||||
if (query[value.limitParameter] !== value.pageSize) errors.push(`${path}.pageSize_must_match_static_query_limit`);
|
||||
if (query[value.offsetParameter] !== 0) errors.push(`${path}.offset_query_must_start_at_zero`);
|
||||
}
|
||||
if (Number.isInteger(value.maxPages) && Number.isInteger(value.pageSize)
|
||||
&& Number.isInteger(value.maxItems) && value.maxPages * value.pageSize < value.maxItems) {
|
||||
errors.push(`${path}.maxPages_cannot_cover_maxItems`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateFieldPolicy(value, path, errors) {
|
||||
rejectUnknownKeys(value, FIELD_POLICY_KEYS, path, errors);
|
||||
requiredIdentifier(value.id, `${path}.id`, errors);
|
||||
@@ -619,10 +663,26 @@ function validateMappingContract(value, path, errors) {
|
||||
errors.push(`${path}.fact.geometry_must_be_object`);
|
||||
} else if (isPlainObject(value.fact.geometry)) {
|
||||
rejectUnknownKeys(value.fact.geometry, GEOMETRY_KEYS, `${path}.fact.geometry`, errors);
|
||||
if (value.fact.geometry.type !== "Point") errors.push(`${path}.fact.geometry.type_must_be_Point`);
|
||||
validateExpression(value.fact.geometry.longitude, `${path}.fact.geometry.longitude`, errors);
|
||||
validateExpression(value.fact.geometry.latitude, `${path}.fact.geometry.latitude`, errors);
|
||||
if (value.fact.geometry.omitIfInvalid !== true) errors.push(`${path}.fact.geometry.omitIfInvalid_must_be_true`);
|
||||
if (value.fact.geometry.type === "Point") {
|
||||
validateExpression(value.fact.geometry.longitude, `${path}.fact.geometry.longitude`, errors);
|
||||
validateExpression(value.fact.geometry.latitude, `${path}.fact.geometry.latitude`, errors);
|
||||
if (value.fact.geometry.omitIfInvalid !== true) errors.push(`${path}.fact.geometry.omitIfInvalid_must_be_true`);
|
||||
if (value.fact.geometry.strategy !== undefined || value.fact.geometry.paths !== undefined || value.fact.geometry.allowedTypes !== undefined) {
|
||||
errors.push(`${path}.fact.geometry.point_shape_invalid`);
|
||||
}
|
||||
} else if (value.fact.geometry.type === "GeoJSON") {
|
||||
if (value.fact.geometry.strategy !== "gelios_geozone_v1") errors.push(`${path}.fact.geometry.strategy_invalid`);
|
||||
requiredSourcePathArray(value.fact.geometry.paths, `${path}.fact.geometry.paths`, errors);
|
||||
if (!sameSet(value.fact.geometry.allowedTypes, ["Polygon", "MultiPolygon"])) {
|
||||
errors.push(`${path}.fact.geometry.allowedTypes_must_be_polygon_or_multipolygon`);
|
||||
}
|
||||
if (value.fact.geometry.omitIfInvalid !== false) errors.push(`${path}.fact.geometry.omitIfInvalid_must_be_false`);
|
||||
if (value.fact.geometry.longitude !== undefined || value.fact.geometry.latitude !== undefined) {
|
||||
errors.push(`${path}.fact.geometry.geojson_shape_invalid`);
|
||||
}
|
||||
} else {
|
||||
errors.push(`${path}.fact.geometry.type_invalid`);
|
||||
}
|
||||
}
|
||||
if (value.fact.attributes !== undefined && !isPlainObject(value.fact.attributes)) {
|
||||
errors.push(`${path}.fact.attributes_must_be_object`);
|
||||
@@ -773,7 +833,13 @@ function validateMappingAgainstProduct(mapping, product, errors) {
|
||||
for (const [field, contract] of Object.entries(contracts)) {
|
||||
if (!isPlainObject(contract)) continue;
|
||||
if (field === "geometry") {
|
||||
if (contract.type !== "point") errors.push(`${path}.fact.geometry_contract_must_be_point`);
|
||||
if (contract.type === "point" && mapping.fact?.geometry?.type !== "Point") {
|
||||
errors.push(`${path}.fact.geometry_contract_must_be_point`);
|
||||
}
|
||||
if (contract.type === "geometry" && mapping.fact?.geometry?.type !== "GeoJSON") {
|
||||
errors.push(`${path}.fact.geometry_contract_must_be_geojson`);
|
||||
}
|
||||
if (!new Set(["point", "geometry"]).has(contract.type)) errors.push(`${path}.fact.geometry_contract_invalid`);
|
||||
if (contract.required === true && mapping.fact?.geometry?.omitIfInvalid === true) {
|
||||
errors.push(`${path}.fact.geometry_required_but_mapping_can_omit`);
|
||||
}
|
||||
@@ -805,7 +871,7 @@ function validateFieldContract(value, path, errors) {
|
||||
if (value.enum !== undefined) {
|
||||
if (!Array.isArray(value.enum) || value.enum.length === 0 || new Set(value.enum.map(stableLiteral)).size !== value.enum.length) {
|
||||
errors.push(`${path}.enum_must_be_nonempty_unique_array`);
|
||||
} else if (new Set(["point", "string_array"]).has(value.type)
|
||||
} else if (new Set(["point", "geometry", "string_array"]).has(value.type)
|
||||
|| value.enum.some((item) => !fieldContractValueMatchesType(item, value.type))) {
|
||||
errors.push(`${path}.enum_value_type_invalid`);
|
||||
}
|
||||
@@ -880,6 +946,11 @@ function fieldContractValueMatchesType(value, type) {
|
||||
&& value.coordinates.length === 2
|
||||
&& value.coordinates.every(Number.isFinite);
|
||||
}
|
||||
if (type === "geometry") {
|
||||
return isPlainObject(value)
|
||||
&& new Set(["Point", "LineString", "Polygon", "MultiPolygon"]).has(value.type)
|
||||
&& Array.isArray(value.coordinates);
|
||||
}
|
||||
return typeof value === type && (type !== "number" || Number.isFinite(value));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user