Files
temp-history-api/types/contracts.js
2026-04-20 23:27:38 +07:00

247 lines
9.9 KiB
JavaScript

"use strict";
const SECTION_STATUSES = Object.freeze(["editing", "submitted", "approved", "rejected"]);
const SECTION_COMMIT_KINDS = Object.freeze(["manual", "restore"]);
const SECTION_SUBMISSION_STATUSES = Object.freeze(["pending", "approved", "rejected", "conflicted"]);
const SNAPSHOT_OPERATIONS = Object.freeze(["create", "update", "delete", "reference", "replace"]);
/**
* @typedef {"editing"|"submitted"|"approved"|"rejected"} SectionStatus
* @typedef {"manual"|"restore"} SectionCommitKind
* @typedef {"pending"|"approved"|"rejected"|"conflicted"} SectionSubmissionStatus
* @typedef {"create"|"update"|"delete"|"reference"|"replace"} SnapshotOperation
*/
function normalizeNullableString(value) {
if (value === undefined || value === null) return null;
const normalized = String(value);
return normalized.length ? normalized : null;
}
function normalizeRequiredString(value, fallback = "") {
if (value === undefined || value === null) return fallback;
return String(value);
}
function normalizeInteger(value, fallback = 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.trunc(parsed) : fallback;
}
function normalizeStatus(value, allowed, fallback) {
const normalized = String(value || "").trim().toLowerCase();
return allowed.includes(normalized) ? normalized : fallback;
}
function normalizeIdArray(value) {
if (!Array.isArray(value)) return [];
const seen = new Set();
const ids = [];
for (const item of value) {
if (typeof item !== "string" && typeof item !== "number") continue;
const id = String(item).trim();
if (!id || seen.has(id)) continue;
seen.add(id);
ids.push(id);
}
return ids;
}
function normalizeEntityContract(row) {
return {
id: normalizeRequiredString(row.id),
name: normalizeRequiredString(row.name),
slug: normalizeNullableString(row.slug),
description: normalizeNullableString(row.description),
type_id: normalizeNullableString(row.type_id),
status: row.status === undefined || row.status === null ? null : normalizeInteger(row.status, 1),
created_at: normalizeNullableString(row.created_at),
updated_at: normalizeNullableString(row.updated_at),
geometry_count: normalizeInteger(row.geometry_count, 0),
};
}
function normalizeFeaturePropertiesContract(properties) {
return {
id: properties.id,
type: normalizeNullableString(properties.type),
time_start: properties.time_start === undefined || properties.time_start === null
? null
: normalizeInteger(properties.time_start, 0),
time_end: properties.time_end === undefined || properties.time_end === null
? null
: normalizeInteger(properties.time_end, 0),
binding: normalizeIdArray(properties.binding),
entity_id: normalizeNullableString(properties.entity_id),
entity_ids: normalizeIdArray(properties.entity_ids),
entity_name: normalizeNullableString(properties.entity_name),
entity_names: Array.isArray(properties.entity_names)
? properties.entity_names
.filter((name) => typeof name === "string" && name.length > 0)
: [],
entity_type_id: normalizeNullableString(properties.entity_type_id),
};
}
function normalizeFeatureContract(feature) {
return {
type: "Feature",
properties: normalizeFeaturePropertiesContract(feature.properties || {}),
geometry: feature.geometry,
};
}
function normalizeFeatureCollectionContract(collection) {
return {
type: "FeatureCollection",
features: Array.isArray(collection?.features)
? collection.features.map(normalizeFeatureContract)
: [],
};
}
function normalizeSectionStateContract(row) {
return {
section_id: normalizeRequiredString(row.section_id),
status: normalizeStatus(row.status, SECTION_STATUSES, "editing"),
head_commit_id: normalizeNullableString(row.head_commit_id),
version: normalizeInteger(row.version, 0),
locked_by: normalizeNullableString(row.locked_by),
locked_at: normalizeNullableString(row.locked_at),
lock_expires_at: normalizeNullableString(row.lock_expires_at),
updated_at: normalizeRequiredString(row.updated_at),
};
}
function normalizeSectionContract(row) {
return {
id: normalizeRequiredString(row.id),
title: normalizeRequiredString(row.title),
description: normalizeNullableString(row.description),
user_id: normalizeNullableString(row.user_id),
created_by: normalizeNullableString(row.created_by),
created_at: normalizeRequiredString(row.created_at),
updated_at: normalizeRequiredString(row.updated_at),
state: {
status: normalizeStatus(row.status, SECTION_STATUSES, "editing"),
head_commit_id: normalizeNullableString(row.head_commit_id),
version: normalizeInteger(row.version, 0),
locked_by: normalizeNullableString(row.locked_by),
locked_at: normalizeNullableString(row.locked_at),
lock_expires_at: normalizeNullableString(row.lock_expires_at),
},
};
}
function normalizeSectionCommitContract(row, snapshot) {
const out = {
id: normalizeRequiredString(row.id),
section_id: normalizeRequiredString(row.section_id),
parent_commit_id: normalizeNullableString(row.parent_commit_id),
commit_no: normalizeInteger(row.commit_no, 0),
kind: normalizeStatus(row.kind, SECTION_COMMIT_KINDS, "manual"),
restored_from_commit_id: normalizeNullableString(row.restored_from_commit_id),
created_by: normalizeRequiredString(row.created_by),
created_at: normalizeRequiredString(row.created_at),
title: normalizeNullableString(row.title),
note: normalizeNullableString(row.note),
snapshot_hash: normalizeNullableString(row.snapshot_hash),
};
if (arguments.length >= 2) out.snapshot = normalizeEditorSnapshotContract(snapshot);
return out;
}
function normalizeSectionSubmissionContract(row, snapshot) {
const out = {
id: normalizeRequiredString(row.id),
section_id: normalizeRequiredString(row.section_id),
commit_id: normalizeRequiredString(row.commit_id),
submitted_by: normalizeRequiredString(row.submitted_by),
submitted_at: normalizeRequiredString(row.submitted_at),
status: normalizeStatus(row.status, SECTION_SUBMISSION_STATUSES, "pending"),
reviewed_by: normalizeNullableString(row.reviewed_by),
reviewed_at: normalizeNullableString(row.reviewed_at),
review_note: normalizeNullableString(row.review_note),
snapshot_hash: normalizeNullableString(row.snapshot_hash),
};
if (arguments.length >= 2) out.snapshot = normalizeEditorSnapshotContract(snapshot);
return out;
}
function normalizeSnapshotOperation(value) {
return normalizeStatus(value, SNAPSHOT_OPERATIONS, "");
}
function normalizeEditorSnapshotContract(snapshot) {
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return null;
return {
schema_version: normalizeInteger(snapshot.schema_version, 1),
section: {
id: normalizeRequiredString(snapshot.section?.id),
title: normalizeRequiredString(snapshot.section?.title),
},
editor_feature_collection: snapshot.editor_feature_collection
? normalizeFeatureCollectionContract(snapshot.editor_feature_collection)
: undefined,
entities: Array.isArray(snapshot.entities) ? snapshot.entities : [],
geometries: Array.isArray(snapshot.geometries) ? snapshot.geometries : [],
link_scopes: Array.isArray(snapshot.link_scopes) ? snapshot.link_scopes : [],
};
}
function assertEditorSnapshotContract(snapshot) {
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
throwContractError("snapshot must be an object");
}
if (!snapshot.section || typeof snapshot.section !== "object" || Array.isArray(snapshot.section)) {
throwContractError("snapshot.section must be an object");
}
if (!normalizeRequiredString(snapshot.section.id)) {
throwContractError("snapshot.section.id is required");
}
if (!normalizeRequiredString(snapshot.section.title)) {
throwContractError("snapshot.section.title is required");
}
for (const entity of Array.isArray(snapshot.entities) ? snapshot.entities : []) {
const operation = normalizeSnapshotOperation(entity?.operation);
if (!operation) throwContractError("Invalid entity operation");
if (!normalizeRequiredString(entity?.id)) throwContractError("Entity id is required");
}
for (const geometry of Array.isArray(snapshot.geometries) ? snapshot.geometries : []) {
const operation = normalizeSnapshotOperation(geometry?.operation);
if (!operation) throwContractError("Invalid geometry operation");
if (!normalizeRequiredString(geometry?.id)) throwContractError("Geometry id is required");
}
for (const scope of Array.isArray(snapshot.link_scopes) ? snapshot.link_scopes : []) {
if (!normalizeRequiredString(scope?.geometry_id)) throwContractError("link_scope geometry_id is required");
if (!Array.isArray(scope?.entity_ids)) throwContractError("link_scope entity_ids must be an array");
}
}
function throwContractError(message) {
const err = new Error(message);
err.status = 400;
throw err;
}
module.exports = {
SECTION_STATUSES,
SECTION_COMMIT_KINDS,
SECTION_SUBMISSION_STATUSES,
SNAPSHOT_OPERATIONS,
assertEditorSnapshotContract,
normalizeEditorSnapshotContract,
normalizeEntityContract,
normalizeFeatureCollectionContract,
normalizeFeatureContract,
normalizeIdArray,
normalizeSectionCommitContract,
normalizeSectionContract,
normalizeSectionStateContract,
normalizeSectionSubmissionContract,
normalizeSnapshotOperation,
};