import {z} from "zod" import {optimizerServersSchema,optimizerProbesSchema} from "./route-optimizer-response" import {filtersGreResponseSchema} from "./filters-response" import {greResponseSchema} from "./gre-response" // Drizzle's string-mode timestamptz is returned as PostgreSQL text (+00), // while test/other API adapters may already return ISO. Preserve the explicit // offset and validate the resulting calendar date; never infer a missing zone. const mapObservedTime=z.preprocess(value=>typeof value==="string" ?value.replace(/^(\d{4}-\d{2}-\d{2}) /,"$1T").replace(/([+-]\d{2})$/,"$1:00") :value,z.string().datetime({offset:true})) export const networkMapServersSchema=z.array(optimizerServersSchema.element.extend({ status:z.enum(["online","offline","degraded"]).nullable(), asn:z.string().optional(),comment:z.string().optional(),lanSubnet:z.string().optional(),os:z.string().nullable().optional(), sessions:z.number().int().nonnegative().nullable().optional(),uptime:z.string().nullable().optional(), cpuLoad:z.number().finite().min(0).max(100).nullable().optional(),freeMemory:z.number().int().nonnegative().nullable().optional(),totalMemory:z.number().int().nonnegative().nullable().optional(), polledAt:mapObservedTime.nullable().optional(), })).refine(rows=>new Set(rows.map(row=>row.id)).size===rows.length && rows.every(row=>row.freeMemory==null||row.totalMemory==null||row.freeMemory<=row.totalMemory)) export type NetworkMapServerRow=z.infer[number] const mapGreSchema=z.object({tunnels:z.array(filtersGreResponseSchema.shape.tunnels.element).refine(rows=>new Set(rows.map(row=>JSON.stringify([row.serverId,row.id]))).size===rows.length),failures:greResponseSchema.shape.failures}).refine(data=>new Set(data.failures.map(row=>row.serverId)).size===data.failures.length&&!data.tunnels.some(tunnel=>data.failures.some(failure=>failure.serverId===tunnel.serverId))) export function validateNetworkMapSnapshot(servers:unknown,gre:unknown,probes:unknown){ const inventory=networkMapServersSchema.parse(servers),tunnels=mapGreSchema.parse(gre),measurements=optimizerProbesSchema.parse(probes) const ids=new Set(inventory.map(row=>String(row.id))) if(tunnels.tunnels.some(tunnel=>!ids.has(tunnel.serverId))||tunnels.failures.some(failure=>!ids.has(failure.serverId))||measurements.probes.some(probe=>!ids.has(probe.srcServerId)||!ids.has(probe.dstServerId)))throw new Error("Данные карты относятся к неизвестному устройству") if(tunnels.failures.length)throw new Error("Неполный каталог GRE: "+tunnels.failures.map(failure=>failure.serverName||failure.serverId).join(", ")) return {servers:inventory,tunnels:tunnels.tunnels,probes:measurements.probes} } import {flowMapHopsDtoSchema} from "@mmapp/contracts/traffic-flow" import {isIpv4String} from "./gre-endpoint-resolve" export const networkMapFlowSchema=flowMapHopsDtoSchema.required().refine(data=>data.rangeMinutes===5&&data.namedBytes<=data.totalBytes&&data.unclassifiedBytes<=data.totalBytes) export function validateNetworkMapDns(raw:unknown,hosts:string[]){ const data=z.object({results:z.record(z.string(),z.string().refine(isIpv4String).nullable())}).parse(raw) const requested=new Set(hosts.map(host=>host.toLowerCase())) if(Object.keys(data.results).length!==requested.size||Object.keys(data.results).some(host=>!requested.has(host)))throw new Error("DNS-ответ не соответствует запрошенным именам") return data.results }