Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions service/src/nats/parsers/aircraft.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AircraftMessagePayload, CoverageEvent } from '../../types/events.js';
import { toStationId } from './station-id.js';

/**
* Parse an aircraft message NATS event into a CoverageEvent.
Expand All @@ -9,7 +10,8 @@ export function parseAircraftMessage(data: AircraftMessagePayload): CoverageEven
const station = data.station;
const message = data.message;

if (!station?.id || !station?.uuid) return null;
const stationId = toStationId(station?.id);
if (stationId === null || !station?.uuid) return null;

// Try to get target position from message first, then flight
let targetLat: number | null = null;
Expand All @@ -29,7 +31,7 @@ export function parseAircraftMessage(data: AircraftMessagePayload): CoverageEven
const hasTarget = targetLat !== null && targetLon !== null;

return {
stationId: station.id,
stationId,
stationUuid: station.uuid,
targetLat,
targetLon,
Expand All @@ -55,11 +57,12 @@ export function parseAircraftFlight(data: AircraftMessagePayload): CoverageEvent
const station = data.station;
const flight = data.flight;

if (!station?.id || !station?.uuid) return null;
const stationId = toStationId(station?.id);
if (stationId === null || !station?.uuid) return null;
if (!flight?.latitude || !flight?.longitude) return null;

return {
stationId: station.id,
stationId,
stationUuid: station.uuid,
targetLat: flight.latitude,
targetLon: flight.longitude,
Expand Down
16 changes: 10 additions & 6 deletions service/src/nats/parsers/marine.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toStationId } from './station-id.js';
import type {
MarineVoyagePositionPayload,
MarineAisMessagePayload,
Expand All @@ -12,7 +13,8 @@ export function parseVoyagePosition(data: MarineVoyagePositionPayload): Coverage
const station = data.station;
const pos = data.voyagePosition;

if (!station?.id || !station?.uuid) return null;
const stationId = toStationId(station?.id);
if (stationId === null || !station?.uuid) return null;

let targetLat: number | null = null;
let targetLon: number | null = null;
Expand All @@ -28,7 +30,7 @@ export function parseVoyagePosition(data: MarineVoyagePositionPayload): Coverage
const hasTarget = targetLat !== null && targetLon !== null;

return {
stationId: station.id,
stationId,
stationUuid: station.uuid,
targetLat,
targetLon,
Expand All @@ -53,14 +55,15 @@ export function parseVoyage(data: MarineVoyagePositionPayload): CoverageEvent |
const station = data.station;
const voyage = data.voyage;

if (!station?.id || !station?.uuid) return null;
const stationId = toStationId(station?.id);
if (stationId === null || !station?.uuid) return null;

const targetLat = voyage?.last_latitude ?? null;
const targetLon = voyage?.last_longitude ?? null;
const hasTarget = targetLat !== null && targetLon !== null;

return {
stationId: station.id,
stationId,
stationUuid: station.uuid,
targetLat,
targetLon,
Expand All @@ -85,10 +88,11 @@ export function parseVoyage(data: MarineVoyagePositionPayload): CoverageEvent |
export function parseAisMessage(data: MarineAisMessagePayload): CoverageEvent | null {
const station = data.station;

if (!station?.id || !station?.uuid) return null;
const stationId = toStationId(station?.id);
if (stationId === null || !station?.uuid) return null;

return {
stationId: station.id,
stationId,
stationUuid: station.uuid,
targetLat: null,
targetLon: null,
Expand Down
15 changes: 15 additions & 0 deletions service/src/nats/parsers/station-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* The aggregation server publishes row ids as JSON strings ("1234"), while the
* main API returns them as numbers. Station metadata is keyed by number, so
* normalize here or every event looks like it came from an unknown station.
*/
export function toStationId(value: unknown): number | null {
if (typeof value === 'number') {
return Number.isSafeInteger(value) && value > 0 ? value : null;
}
if (typeof value === 'string' && /^\d+$/.test(value)) {
const id = Number(value);
return Number.isSafeInteger(id) && id > 0 ? id : null;
}
return null;
}
8 changes: 5 additions & 3 deletions service/src/sync/station-sync.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { config } from '../config.js';
import type { StationMeta } from '../types/events.js';
import { toStationId } from '../nats/parsers/station-id.js';

/**
* Periodically syncs station metadata from the main API.
Expand Down Expand Up @@ -84,7 +85,8 @@ export class StationSync {
let enriched = 0;

for (const s of stations) {
if (!s.id) continue;
const id = toStationId(s.id);
if (id === null) continue;

let lat = parseFloat(s.latitude) || parseFloat(s.fuzzedLatitude) || 0;
let lon = parseFloat(s.longitude) || parseFloat(s.fuzzedLongitude) || 0;
Expand All @@ -99,8 +101,8 @@ export class StationSync {
}
}

this.stationMap.set(s.id, {
id: s.id,
this.stationMap.set(id, {
id,
uuid: s.uuid ?? '',
ident: s.ident ?? '',
latitude: lat,
Expand Down
20 changes: 10 additions & 10 deletions service/src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export interface AircraftMessagePayload {
transmissionType?: string;
};
station?: {
id?: number;
id?: number | string;
uuid?: string;
ident?: string;
latitude?: number;
Expand All @@ -63,7 +63,7 @@ export interface AircraftMessagePayload {
source_type?: string;
};
message?: {
id?: number;
id?: number | string;
latitude?: number;
longitude?: number;
altitude?: number;
Expand All @@ -76,18 +76,18 @@ export interface AircraftMessagePayload {
source_type?: string;
};
airframe?: {
id?: number;
id?: number | string;
tail?: string;
};
flight?: {
id?: number;
id?: number | string;
latitude?: number;
longitude?: number;
altitude?: number;
track?: number;
};
messageReport?: {
id?: number;
id?: number | string;
};
}

Expand All @@ -99,7 +99,7 @@ export interface MarineVoyagePositionPayload {
transmissionType?: string;
};
station?: {
id?: number;
id?: number | string;
uuid?: string;
latitude?: number;
longitude?: number;
Expand All @@ -114,12 +114,12 @@ export interface MarineVoyagePositionPayload {
position_accuracy?: boolean;
};
voyage?: {
id?: number;
id?: number | string;
last_latitude?: number;
last_longitude?: number;
};
vessel?: {
id?: number;
id?: number | string;
mmsi?: string;
name?: string;
};
Expand All @@ -132,13 +132,13 @@ export interface MarineAisMessagePayload {
application?: string;
};
station?: {
id?: number;
id?: number | string;
uuid?: string;
latitude?: number;
longitude?: number;
source_type?: string;
};
aisMessage?: {
id?: number;
id?: number | string;
};
}
56 changes: 56 additions & 0 deletions service/test/parsers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { afterEach, describe, expect, it } from 'vitest';
import { CoverageStore } from '../src/aggregation/coverage-store.js';
import { parseAircraftMessage, parseAircraftFlight } from '../src/nats/parsers/aircraft.js';
import { toStationId } from '../src/nats/parsers/station-id.js';
import type { StationMeta } from '../src/types/events.js';

describe('toStationId', () => {
it('accepts numbers and digit strings', () => {
expect(toStationId(1234)).toBe(1234);
expect(toStationId('1234')).toBe(1234);
});

it('rejects anything that is not a positive integer id', () => {
for (const bad of [0, -1, 1.5, '', '0', '12a', ' 12', '1e3', null, undefined, {}, NaN]) {
expect(toStationId(bad)).toBeNull();
}
});
});

describe('aircraft parsers with production payloads', () => {
// The aggregation server serializes ids as strings.
const payload = {
source: { name: 'dumpvdl2' },
station: { id: '1234', uuid: 'uuid-1234', ident: 'TEST-1234' },
message: { id: '987654321', latitude: 48.5, longitude: 11.2, altitude: 36000, timestamp: new Date().toISOString() },
flight: { id: '555', latitude: 48.6, longitude: 11.3, altitude: 36000 },
};

it('normalizes a string station id to a number', () => {
expect(parseAircraftMessage(payload)?.stationId).toBe(1234);
expect(parseAircraftFlight(payload)?.stationId).toBe(1234);
});

it('drops events with no usable station id', () => {
expect(parseAircraftMessage({ ...payload, station: { ...payload.station, id: 'abc' } })).toBeNull();
expect(parseAircraftMessage({ ...payload, station: { uuid: 'uuid-1234' } })).toBeNull();
});

describe('ingest', () => {
let store: CoverageStore;
afterEach(() => store.destroy());

it('accepts an event whose station id arrived as a string', () => {
const meta: StationMeta = {
id: 1234, uuid: 'uuid-1234', ident: 'TEST-1234', latitude: 48.0, longitude: 11.0,
sourceType: 'vdl', status: 'active', flagged: false, blocked: false, trustScore: 0.5,
lastSyncedAt: new Date(),
};
store = new CoverageStore();
store.stationMap.set(1234, meta);
store.ingest(parseAircraftMessage(payload)!);
expect(store.eventsProcessed).toBe(1);
expect(store.eventsRejected).toBe(0);
});
});
});
Loading