mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
refactor(db): JSON columns are decoded through one declared schema per column
decodeJson/decodeJsonResult/encodeJson read and write a JSON-in-TEXT column against the schema and fallback declared for it in db/json-columns.ts, and log a row that does not decode. reservations.metadata, the OAuth scope and redirect lists, consents, API token scopes, MFA backup codes, journey entry tags and pros/cons and poll options go through it. Readers that used to throw on a broken row (a hotel update, the ICS export, the price mirror, the OAuth token and session reads, a consent, a journey entry, a poll) now take the tolerant fallback the other readers of the same column already used.
This commit is contained in:
@@ -14,6 +14,8 @@ import { AirtrailRequestError, type AirtrailFlightRaw } from './airtrail.client'
|
||||
import { AirtrailClient } from './airtrail.client';
|
||||
import { AirtrailService } from './airtrail.service';
|
||||
import { canonicalHash, mapFlightToReservation, mapFlightsToMultiLegReservation, normalizeFlight } from './airtrail.mapper';
|
||||
import { RESERVATION_METADATA } from '../../db/json-columns';
|
||||
import { decodeJson } from '../../utils/json-column';
|
||||
|
||||
interface ExistingFlightRow {
|
||||
id: number;
|
||||
@@ -165,12 +167,8 @@ export class AirtrailImportService {
|
||||
const linkedRows = await this.reservationsRepo.listAirtrailLinkedForTrip(tripIdNum);
|
||||
for (const row of linkedRows) {
|
||||
if (row.external_id) linkedIds.add(row.external_id);
|
||||
try {
|
||||
const ids = row.metadata ? JSON.parse(row.metadata).airtrail_ids : null;
|
||||
if (Array.isArray(ids)) for (const id of ids) linkedIds.add(String(id));
|
||||
} catch {
|
||||
/* malformed metadata — ignore */
|
||||
}
|
||||
const ids = decodeJson(RESERVATION_METADATA, row.metadata, `airtrail ${row.external_id}`).airtrail_ids;
|
||||
if (Array.isArray(ids)) for (const id of ids) linkedIds.add(String(id));
|
||||
}
|
||||
|
||||
const existing = await this.reservationsRepo.listFlightReservationsForTrip(tripIdNum);
|
||||
@@ -194,12 +192,7 @@ export class AirtrailImportService {
|
||||
|
||||
const existingSigs = new Set<string>();
|
||||
for (const row of existing) {
|
||||
let meta: Record<string, any> = {};
|
||||
try {
|
||||
meta = row.metadata ? JSON.parse(row.metadata) : {};
|
||||
} catch {
|
||||
/* malformed metadata — ignore */
|
||||
}
|
||||
const meta = decodeJson(RESERVATION_METADATA, row.metadata, `reservation ${row.id}`);
|
||||
const eps = endpointsByReservation.get(row.id) ?? [];
|
||||
const legs: any[] | null = Array.isArray(meta.legs) ? meta.legs : null;
|
||||
if (legs && legs.length > 1) {
|
||||
@@ -218,7 +211,7 @@ export class AirtrailImportService {
|
||||
} else {
|
||||
const from = eps[0]?.code ?? null;
|
||||
const to = eps.length > 1 ? eps[eps.length - 1].code : null;
|
||||
const sig = softSignature(depDate(row.reservation_time), meta.flight_number ?? null, from, to);
|
||||
const sig = softSignature(depDate(row.reservation_time), (meta.flight_number as string | null | undefined) ?? null, from, to);
|
||||
if (sig) existingSigs.add(sig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import { AirtrailClient } from './airtrail.client';
|
||||
import { AirtrailService } from './airtrail.service';
|
||||
import { canonicalHash } from './airtrail.mapper';
|
||||
import { buildSavePayload } from './airtrail-sync.helpers';
|
||||
import { RESERVATION_METADATA } from '../../db/json-columns';
|
||||
import { decodeJson } from '../../utils/json-column';
|
||||
|
||||
/**
|
||||
* The AirTrail link lifecycle — the enablement gate, the detach policy, the
|
||||
@@ -70,12 +72,9 @@ export class AirtrailLinkService {
|
||||
* AirTrail flight to span the whole route (#1535).
|
||||
*/
|
||||
async hasLocalMultiLegShape(reservationId: number, metadataJson: string | null | undefined): Promise<boolean> {
|
||||
try {
|
||||
const meta = metadataJson ? JSON.parse(metadataJson) : {};
|
||||
if (Array.isArray(meta?.legs) && meta.legs.length > 1) return true;
|
||||
} catch {
|
||||
/* malformed metadata — fall through to the endpoint count */
|
||||
}
|
||||
// Malformed metadata falls through to the endpoint count.
|
||||
const legs = decodeJson(RESERVATION_METADATA, metadataJson, `reservation ${reservationId}`).legs;
|
||||
if (Array.isArray(legs) && legs.length > 1) return true;
|
||||
const n = await this.endpointsRepo.count({ reservation: reservationId });
|
||||
return n > 2;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { entityCode } from './airtrail.mapper';
|
||||
import { flightPassengers, ownPassenger } from './airtrail.client';
|
||||
import type { AirtrailFlightRaw, AirtrailPassengerWrite, AirtrailSavePayload } from './airtrail.client';
|
||||
import { RESERVATION_METADATA } from '../../db/json-columns';
|
||||
import { decodeJson } from '../../utils/json-column';
|
||||
|
||||
/**
|
||||
* The pure half of the AirTrail push: turning a TREK reservation plus the flight
|
||||
@@ -27,12 +29,7 @@ function splitLocal(dt: string | null | undefined): { date: string | null; time:
|
||||
* (#1240).
|
||||
*/
|
||||
export function buildSavePayload(reservation: any, existing: AirtrailFlightRaw): AirtrailSavePayload | null {
|
||||
let meta: Record<string, any>;
|
||||
try {
|
||||
meta = reservation.metadata ? JSON.parse(reservation.metadata) : {};
|
||||
} catch {
|
||||
meta = {};
|
||||
}
|
||||
const meta = decodeJson(RESERVATION_METADATA, reservation.metadata, `reservation ${reservation.id}`);
|
||||
const endpoints: any[] = reservation.endpoints || [];
|
||||
const fromEp = endpoints.find((e) => e.role === 'from');
|
||||
const toEp = endpoints.find((e) => e.role === 'to');
|
||||
|
||||
Reference in New Issue
Block a user