diff --git a/server/scripts/format-baseline.json b/server/scripts/format-baseline.json index 10980f381..e1ce926b3 100644 --- a/server/scripts/format-baseline.json +++ b/server/scripts/format-baseline.json @@ -1,6 +1,7 @@ [ "src/db/repositories/DayAssignments.repository.ts", "src/db/repositories/Places.repository.ts", + "src/db/repositories/Reservations.repository.ts", "src/db/repositories/Trips.repository.ts", "src/db/repositories/Users.repository.ts", "src/nest/atlas/atlas-geo.ts", diff --git a/server/src/db/repositories/Reservations.repository.ts b/server/src/db/repositories/Reservations.repository.ts index fb006d980..a61a5ce47 100644 --- a/server/src/db/repositories/Reservations.repository.ts +++ b/server/src/db/repositories/Reservations.repository.ts @@ -1,16 +1,14 @@ -import { castIntegerKysely, concatKysely, startsWithIsoDateKysely, substringKysely } from '../dialect/kysely-functions'; -import { coalesceOverride, columnRef, dayDistance } from '../dialect/sql-functions'; +import type { Reservations } from '../entities/Reservations.entity'; import { DayAssignments } from '../entities/DayAssignments.entity'; import { Days } from '../entities/Days.entity'; -import type { Reservations } from '../entities/Reservations.entity'; -import type { DB } from '../kysely/db'; -import { - publicReservationExpr, - publicStayExists, - type ReservationVisibilityKyselyDB, -} from './_shared/reservation-visibility'; -import { TrekRepository } from './_shared/trek-repository'; +import { coalesceOverride, columnRef, dayDistance } from '../dialect/sql-functions'; +import { castIntegerKysely, concatKysely, startsWithIsoDateKysely, substringKysely } from '../dialect/kysely-functions'; +import { publicReservationExpr, publicStayExists, type ReservationVisibilityKyselyDB } from './_shared/reservation-visibility'; import type { DayAssignmentRow } from './day-assignment-rows'; +import { TrekRepository } from './_shared/trek-repository'; +import { passContextQuery, type PassContextDB } from './_shared/reservation-pass-context'; +import type { DB } from '../kysely/db'; +import { ROADTRIP_RESERVATION_TYPES } from '@trek/shared'; import type { PublicApiAccommodationKyselyDB, PublicApiAccommodationRow, @@ -20,7 +18,6 @@ import type { PublicApiUnplannedPlaceRow, PublicApiUnscheduledReservationRow, } from './reservation-public-api-rows'; -import { ROADTRIP_RESERVATION_TYPES } from '@trek/shared'; // The public API row types moved to their own module; re-exported so callers keep one import path. export type { @@ -68,9 +65,7 @@ type ReservationJoinKyselyDB = Pick & { - accommodation_id: string | number | null; -}; +type ReservationsNumericAccommodationBind = Omit & { accommodation_id: string | number | null }; /** * Kysely typing for `restampLinkedReservation` (DY23). `accommodation_id: @@ -224,8 +219,7 @@ export interface CalendarStayRow { reservation_title: string | null; } -type CalendarStayKyselyDB = ReservationVisibilityKyselyDB & - Pick; +type CalendarStayKyselyDB = ReservationVisibilityKyselyDB & Pick; /** * RS20 (`ReservationsService.listUpcoming`) — the CTE + `UNION ALL` @@ -275,10 +269,7 @@ interface EntriesRow extends UpcomingReservationRow { at_time: string | null; } -type UpcomingReservationsKyselyDB = Pick< - DB, - 'trips' | 'trip_members' | 'reservations' | 'days' | 'places' | 'day_accommodations' ->; +type UpcomingReservationsKyselyDB = Pick; /** * `share.service.ts:251` (`publicEndpointsByReservation`) — `SELECT @@ -381,51 +372,12 @@ export interface SharePublicAccommodationRow { type SharePublicAccommodationKyselyDB = ReservationVisibilityKyselyDB & Pick; -/** - * Plan 3h Task 4 (`AirportsService.backfillFlightEndpoints`, AIR1) — - * additive. The `reservations`/`reservation_endpoints` tables the - * `NOT EXISTS` guard reads. - */ +/** `AirportsService.backfillFlightEndpoints` (AIR1): the tables its `NOT EXISTS` guard reads. */ type FlightsMissingEndpointsKyselyDB = Pick; -/** `findPassContext`: the booking's trip title and the coordinates of its place or stay. */ -type PassContextKyselyDB = Pick; - -/** What a booking's Wallet pass needs beyond the booking: where it is, and the trip's name. */ -export interface ReservationPassContext { - trip_title: string | null; - place_lat: number | null; - place_lng: number | null; - place_name: string | null; -} - export class ReservationsRepository extends TrekRepository { - /** - * The trip's title and the place a booking is at (its own place, else its - * stay's), for its Apple Wallet pass (WalletPassService). Scoped to the trip, - * so a booking of another trip reads as none. - */ - async findPassContext(id: number, trip_id: number): Promise { - const row = await this.kysely() - .selectFrom('reservations as r') - .innerJoin('trips as t', 't.id', 'r.trip_id') - .leftJoin('places as p', 'p.id', 'r.place_id') - // accommodation_id is TEXT; compared as a number so Postgres takes it as well as SQLite ("14" and "14.0" alike). - .leftJoin('day_accommodations as ap', (join) => - join.on((eb) => eb(eb.cast('r.accommodation_id', 'numeric'), '=', eb.ref('ap.id'))), - ) - .leftJoin('places as acc_p', 'acc_p.id', 'ap.place_id') - .where('r.id', '=', id) - .where('r.trip_id', '=', trip_id) - .select([ - 't.title as trip_title', - (eb) => eb.fn.coalesce('p.lat', 'acc_p.lat').as('place_lat'), - (eb) => eb.fn.coalesce('p.lng', 'acc_p.lng').as('place_lng'), - (eb) => eb.fn.coalesce('p.name', 'acc_p.name').as('place_name'), - ]) - .executeTakeFirst(); - return row as ReservationPassContext | undefined; - } + // For its Wallet pass (_shared/reservation-pass-context.ts). + findPassContext(id: number, tripId: number) { return passContextQuery(this.kysely(), id, tripId); } private joinedQuery() { return this.kysely() @@ -463,9 +415,7 @@ export class ReservationsRepository extends TrekRepository { * projection for one reservation, no `ORDER BY` (a single row). */ async findWithJoins(id: number | string): Promise { - const row = await this.joinedQuery() - .where('r.id', '=', id as number) - .executeTakeFirst(); + const row = await this.joinedQuery().where('r.id', '=', id as number).executeTakeFirst(); return row as ReservationJoinRow | undefined; } @@ -485,17 +435,11 @@ export class ReservationsRepository extends TrekRepository { } /** RS45 (`ReservationsService.remove`'s pre-image read) — `SELECT id, title, type, accommodation_id FROM reservations WHERE id = ? AND trip_id = ?`. */ - async findHeaderInTrip( - id: number, - trip_id: number, - ): Promise<{ id: number; title: string; type: string | null; accommodation_id: string | null } | undefined> { + async findHeaderInTrip(id: number, trip_id: number): Promise<{ id: number; title: string; type: string | null; accommodation_id: string | null } | undefined> { return this.qb('r') .select(['r.id', 'r.title', 'r.type', 'r.accommodation_id']) .where({ id, trip: trip_id }) - .execute<{ id: number; title: string; type: string | null; accommodation_id: string | null } | undefined>( - 'get', - false, - ); + .execute<{ id: number; title: string; type: string | null; accommodation_id: string | null } | undefined>('get', false); } /** @@ -504,38 +448,19 @@ export class ReservationsRepository extends TrekRepository { * reservations WHERE trip_id = ? AND (type != 'hotel' OR accommodation_id * IS NULL) AND reservation_time IS NOT NULL`. */ - async listResyncCandidates(trip_id: number): Promise< - { - id: number; - reservation_time: string | null; - reservation_end_time: string | null; - day_id: number | null; - end_day_id: number | null; - }[] - > { + async listResyncCandidates(trip_id: number): Promise<{ + id: number; reservation_time: string | null; reservation_end_time: string | null; + day_id: number | null; end_day_id: number | null; + }[]> { const platform = this.getEntityManager().getPlatform(); return this.qb('r') - .select([ - 'r.id', - 'r.reservation_time', - 'r.reservation_end_time', - columnRef(platform, 'r.day_id').as('day_id'), - columnRef(platform, 'r.end_day_id').as('end_day_id'), - ]) + .select(['r.id', 'r.reservation_time', 'r.reservation_end_time', columnRef(platform, 'r.day_id').as('day_id'), columnRef(platform, 'r.end_day_id').as('end_day_id')]) .where({ trip: trip_id, $or: [{ type: { $ne: 'hotel' } }, { accommodation_id: null }], reservation_time: { $ne: null }, }) - .execute< - { - id: number; - reservation_time: string | null; - reservation_end_time: string | null; - day_id: number | null; - end_day_id: number | null; - }[] - >('all', false); + .execute<{ id: number; reservation_time: string | null; reservation_end_time: string | null; day_id: number | null; end_day_id: number | null }[]>('all', false); } /** RS13 (`resyncReservationDays`'s per-row write) — `UPDATE reservations SET day_id = ?, end_day_id = ? WHERE id = ?`. */ @@ -605,48 +530,42 @@ export class ReservationsRepository extends TrekRepository { * is handed — no SQL-side COALESCE. `id: number` (rule 21 — the SAME * `toRowId`-parsed value `findInTrip`'s gate read used). */ - async updateReservation( - id: number, - write: { - title: string; - reservation_time: string | null; - reservation_end_time: string | null; - location: string | null; - confirmation_number: string | null; - notes: string | null; - url: string | null; - day_id: number | null; - end_day_id: number | null; - place_id: number | null; - assignment_id: number | null; - status: string; - type: string; - accommodation_id: string | null; - metadata: string | null; - needs_review: number; - }, - ): Promise { - await this.nativeUpdate( - { id }, - { - title: write.title, - reservation_time: write.reservation_time, - reservation_end_time: write.reservation_end_time, - location: write.location, - confirmation_number: write.confirmation_number, - notes: write.notes, - url: write.url, - day: write.day_id, - endDay: write.end_day_id, - place: write.place_id, - assignment: write.assignment_id, - status: write.status, - type: write.type, - accommodation_id: write.accommodation_id, - metadata: write.metadata, - needs_review: write.needs_review, - }, - ); + async updateReservation(id: number, write: { + title: string; + reservation_time: string | null; + reservation_end_time: string | null; + location: string | null; + confirmation_number: string | null; + notes: string | null; + url: string | null; + day_id: number | null; + end_day_id: number | null; + place_id: number | null; + assignment_id: number | null; + status: string; + type: string; + accommodation_id: string | null; + metadata: string | null; + needs_review: number; + }): Promise { + await this.nativeUpdate({ id }, { + title: write.title, + reservation_time: write.reservation_time, + reservation_end_time: write.reservation_end_time, + location: write.location, + confirmation_number: write.confirmation_number, + notes: write.notes, + url: write.url, + day: write.day_id, + endDay: write.end_day_id, + place: write.place_id, + assignment: write.assignment_id, + status: write.status, + type: write.type, + accommodation_id: write.accommodation_id, + metadata: write.metadata, + needs_review: write.needs_review, + }); } /** @@ -666,34 +585,15 @@ export class ReservationsRepository extends TrekRepository { } /** DY14 (`DaysService.restampReservationDates`) — `SELECT id, day_id, end_day_id, reservation_time, reservation_end_time FROM reservations WHERE trip_id = ?`. */ - async listForRestamp(trip_id: number): Promise< - { - id: number; - day_id: number | null; - end_day_id: number | null; - reservation_time: string | null; - reservation_end_time: string | null; - }[] - > { + async listForRestamp(trip_id: number): Promise<{ + id: number; day_id: number | null; end_day_id: number | null; + reservation_time: string | null; reservation_end_time: string | null; + }[]> { const platform = this.getEntityManager().getPlatform(); return this.qb('r') - .select([ - 'r.id', - columnRef(platform, 'r.day_id').as('day_id'), - columnRef(platform, 'r.end_day_id').as('end_day_id'), - 'r.reservation_time', - 'r.reservation_end_time', - ]) + .select(['r.id', columnRef(platform, 'r.day_id').as('day_id'), columnRef(platform, 'r.end_day_id').as('end_day_id'), 'r.reservation_time', 'r.reservation_end_time']) .where({ trip: trip_id }) - .execute< - { - id: number; - day_id: number | null; - end_day_id: number | null; - reservation_time: string | null; - reservation_end_time: string | null; - }[] - >('all', false); + .execute<{ id: number; day_id: number | null; end_day_id: number | null; reservation_time: string | null; reservation_end_time: string | null }[]>('all', false); } /** DY15 — `UPDATE reservations SET reservation_time = ? WHERE id = ?`. */ @@ -732,14 +632,7 @@ export class ReservationsRepository extends TrekRepository { .case() .when('reservation_time', 'is', null) .then(eb.val(date)) - .else( - concatKysely( - platform, - eb, - { value: date }, - { expression: substringKysely(platform, eb, 'reservation_time', 11) }, - ), - ) + .else(concatKysely(platform, eb, { value: date }, { expression: substringKysely(platform, eb, 'reservation_time', 11) })) .end(), })) .where('accommodation_id', '=', accommodation_id) @@ -1087,12 +980,7 @@ export class ReservationsRepository extends TrekRepository { } /** RS20 — see {@link UpcomingReservationRow}'s docstring. */ - async listUpcomingForUser( - user_id: number, - today: string, - now_hhmm: string, - limit: number, - ): Promise { + async listUpcomingForUser(user_id: number, today: string, now_hhmm: string, limit: number): Promise { const platform = this.getEntityManager().getPlatform(); // The `COALESCE(place name, nameless-stay's-linked-booking title, trip @@ -1107,15 +995,10 @@ export class ReservationsRepository extends TrekRepository { .with('visible_trips', (qb) => qb .selectFrom('trips as t') - .leftJoin('trip_members as tm', (join) => - join.onRef('tm.trip_id', '=', 't.id').on('tm.user_id', '=', user_id), - ) + .leftJoin('trip_members as tm', (join) => join.onRef('tm.trip_id', '=', 't.id').on('tm.user_id', '=', user_id)) .select(['t.id', 't.title', 't.cover_image']) .where((eb) => - eb.and([ - eb.or([eb('t.user_id', '=', user_id), eb('tm.user_id', 'is not', null)]), - eb('t.is_archived', '=', 0), - ]), + eb.and([eb.or([eb('t.user_id', '=', user_id), eb('tm.user_id', 'is not', null)]), eb('t.is_archived', '=', 0)]), ), ) .with('entries', (qb) => { @@ -1165,8 +1048,8 @@ export class ReservationsRepository extends TrekRepository { .select((eb) => [ 'a.id as id', 'a.trip_id as trip_id', - eb.fn - .coalesce( + eb + .fn.coalesce( eb.ref('p.name'), eb .selectFrom('reservations as res') @@ -1206,8 +1089,8 @@ export class ReservationsRepository extends TrekRepository { .select((eb) => [ 'a.id as id', 'a.trip_id as trip_id', - eb.fn - .coalesce( + eb + .fn.coalesce( eb.ref('p.name'), eb .selectFrom('reservations as res') @@ -1242,21 +1125,7 @@ export class ReservationsRepository extends TrekRepository { return bookings.unionAll(checkins).unionAll(checkouts); }) .selectFrom('entries') - .select([ - 'id', - 'trip_id', - 'title', - 'type', - 'status', - 'location', - 'reservation_time', - 'confirmation_number', - 'trip_title', - 'trip_cover', - 'day_date', - 'place_name', - 'place_image', - ]) + .select(['id', 'trip_id', 'title', 'type', 'status', 'location', 'reservation_time', 'confirmation_number', 'trip_title', 'trip_cover', 'day_date', 'place_name', 'place_image']) .where('at_date', 'is not', null) .where((eb) => eb.or([ @@ -1277,18 +1146,7 @@ export class ReservationsRepository extends TrekRepository { const rows = await this.kysely() .selectFrom('reservation_endpoints as e') .innerJoin('reservations as r', 'r.id', 'e.reservation_id') - .select([ - 'e.reservation_id', - 'e.role', - 'e.sequence', - 'e.name', - 'e.code', - 'e.lat', - 'e.lng', - 'e.timezone', - 'e.local_date', - 'e.local_time', - ]) + .select(['e.reservation_id', 'e.role', 'e.sequence', 'e.name', 'e.code', 'e.lat', 'e.lng', 'e.timezone', 'e.local_date', 'e.local_time']) .where('r.trip_id', '=', trip_id as number) .orderBy('e.reservation_id', 'asc') .orderBy('e.sequence', 'asc') @@ -1312,22 +1170,9 @@ export class ReservationsRepository extends TrekRepository { const rows = await this.kysely() .selectFrom('reservations as r') .select([ - 'r.id', - 'r.trip_id', - 'r.day_id', - 'r.end_day_id', - 'r.place_id', - 'r.accommodation_id', - 'r.title', - 'r.type', - 'r.status', - 'r.location', - 'r.reservation_time', - 'r.reservation_end_time', - 'r.notes', - 'r.url', - 'r.metadata', - 'r.created_at', + 'r.id', 'r.trip_id', 'r.day_id', 'r.end_day_id', 'r.place_id', 'r.accommodation_id', + 'r.title', 'r.type', 'r.status', 'r.location', 'r.reservation_time', 'r.reservation_end_time', + 'r.notes', 'r.url', 'r.metadata', 'r.created_at', ]) .where('r.trip_id', '=', trip_id as number) .where((eb) => publicReservationExpr(eb, 'r.ingest_state')) @@ -1344,19 +1189,9 @@ export class ReservationsRepository extends TrekRepository { .where((eb) => publicStayExists(eb)) .innerJoin('places as p', 'p.id', 'a.place_id') .select([ - 'a.id', - 'a.trip_id', - 'a.place_id', - 'a.start_day_id', - 'a.end_day_id', - 'a.check_in', - 'a.check_in_end', - 'a.check_out', - 'a.notes', - 'p.name as place_name', - 'p.address as place_address', - 'p.lat as place_lat', - 'p.lng as place_lng', + 'a.id', 'a.trip_id', 'a.place_id', 'a.start_day_id', 'a.end_day_id', + 'a.check_in', 'a.check_in_end', 'a.check_out', 'a.notes', + 'p.name as place_name', 'p.address as place_address', 'p.lat as place_lat', 'p.lng as place_lng', ]) .execute(); return rows as SharePublicAccommodationRow[]; @@ -1412,17 +1247,7 @@ export class ReservationsRepository extends TrekRepository { .leftJoin('places as p', 'p.id', 'a.place_id') .leftJoin('days as ds', 'ds.id', 'a.start_day_id') .leftJoin('days as de', 'de.id', 'a.end_day_id') - .select([ - 'p.name', - 'p.address', - 'p.lat', - 'p.lng', - 'ds.date as start_date', - 'de.date as end_date', - 'a.check_in', - 'a.check_out', - 'a.notes', - ]) + .select(['p.name', 'p.address', 'p.lat', 'p.lng', 'ds.date as start_date', 'de.date as end_date', 'a.check_in', 'a.check_out', 'a.notes']) .where('a.trip_id', '=', trip_id) .orderBy('ds.date', 'asc') .execute(); @@ -1435,24 +1260,12 @@ export class ReservationsRepository extends TrekRepository { .selectFrom('places as p') .leftJoin('categories as c', 'c.id', 'p.category_id') .select((eb) => [ - 'p.name', - 'p.address', - 'p.lat', - 'p.lng', - 'p.place_time', - 'p.end_time', - 'p.duration_minutes', - 'p.notes', - 'p.transport_mode', - 'c.name as category', + 'p.name', 'p.address', 'p.lat', 'p.lng', 'p.place_time', 'p.end_time', + 'p.duration_minutes', 'p.notes', 'p.transport_mode', 'c.name as category', ]) .where('p.trip_id', '=', trip_id) - .where((eb) => - eb.not(eb.exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.place_id', '=', 'p.id'))), - ) - .where((eb) => - eb.not(eb.exists(eb.selectFrom('day_accommodations as a').select('a.id').whereRef('a.place_id', '=', 'p.id'))), - ) + .where((eb) => eb.not(eb.exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.place_id', '=', 'p.id')))) + .where((eb) => eb.not(eb.exists(eb.selectFrom('day_accommodations as a').select('a.id').whereRef('a.place_id', '=', 'p.id')))) .orderBy('p.created_at', 'asc') .orderBy('p.id', 'asc') .execute(); @@ -1492,10 +1305,7 @@ export class ReservationsRepository extends TrekRepository { // --------------------------------------------------------------------------- /** BG54/BG85 — `SELECT id, metadata FROM reservations WHERE id = ? AND trip_id = ?`. */ - async getIdAndMetadata( - id: number | string, - trip_id: number | string, - ): Promise<{ id: number; metadata: string | null } | undefined> { + async getIdAndMetadata(id: number | string, trip_id: number | string): Promise<{ id: number; metadata: string | null } | undefined> { return await this.kysely>() .selectFrom('reservations') .select(['id', 'metadata']) @@ -1542,9 +1352,7 @@ export class ReservationsRepository extends TrekRepository { } /** ATI2 (`airtrail-import.service.ts#importAirtrailFlights`, existing-flight signature dedup) — `SELECT r.id, r.reservation_time, r.metadata FROM reservations r WHERE r.trip_id = ? AND r.type = 'flight'`. */ - async listFlightReservationsForTrip( - trip_id: number, - ): Promise<{ id: number; reservation_time: string | null; metadata: string | null }[]> { + async listFlightReservationsForTrip(trip_id: number): Promise<{ id: number; reservation_time: string | null; metadata: string | null }[]> { return this.qb('r') .select(['r.id', 'r.reservation_time', 'r.metadata']) .where({ trip: trip_id, type: 'flight' }) @@ -1562,16 +1370,8 @@ export class ReservationsRepository extends TrekRepository { * untracked reservation. Preserved exactly, not "fixed" with an added * `uow.transactional` wrap. */ - async linkAirtrailMultiLeg( - id: number, - external_id: string, - external_owner_user_id: number, - external_synced_at: string, - ): Promise { - await this.nativeUpdate( - { id }, - { external_source: 'airtrail', external_id, external_owner_user_id, sync_enabled: 0, external_synced_at }, - ); + async linkAirtrailMultiLeg(id: number, external_id: string, external_owner_user_id: number, external_synced_at: string): Promise { + await this.nativeUpdate({ id }, { external_source: 'airtrail', external_id, external_owner_user_id, sync_enabled: 0, external_synced_at }); } /** @@ -1581,24 +1381,8 @@ export class ReservationsRepository extends TrekRepository { * external_hash = ?, external_synced_at = ? WHERE id = ?`. Same * pre-existing tx gap as {@link linkAirtrailMultiLeg} — report don't fix. */ - async linkAirtrailSingleFlight( - id: number, - external_id: string, - external_owner_user_id: number, - external_hash: string, - external_synced_at: string, - ): Promise { - await this.nativeUpdate( - { id }, - { - external_source: 'airtrail', - external_id, - external_owner_user_id, - sync_enabled: 1, - external_hash, - external_synced_at, - }, - ); + async linkAirtrailSingleFlight(id: number, external_id: string, external_owner_user_id: number, external_hash: string, external_synced_at: string): Promise { + await this.nativeUpdate({ id }, { external_source: 'airtrail', external_id, external_owner_user_id, sync_enabled: 1, external_hash, external_synced_at }); } /** ATL2 (`airtrail-link.service.ts#detach`) — `UPDATE reservations SET sync_enabled = 0 WHERE id = ?`. */ @@ -1613,36 +1397,12 @@ export class ReservationsRepository extends TrekRepository { * via `columnRef` (the `listResyncCandidates` precedent above) — the * mirror column, not a bare select. */ - async findAirtrailLinked(id: number): Promise< - | { - id: number; - trip_id: number; - external_id: string | null; - external_owner_user_id: number | null; - sync_enabled: number | null; - } - | undefined - > { + async findAirtrailLinked(id: number): Promise<{ id: number; trip_id: number; external_id: string | null; external_owner_user_id: number | null; sync_enabled: number | null } | undefined> { const platform = this.getEntityManager().getPlatform(); return this.qb('r') - .select([ - 'r.id', - columnRef(platform, 'r.trip_id').as('trip_id'), - 'r.external_id', - 'r.external_owner_user_id', - 'r.sync_enabled', - ]) + .select(['r.id', columnRef(platform, 'r.trip_id').as('trip_id'), 'r.external_id', 'r.external_owner_user_id', 'r.sync_enabled']) .where({ id, external_source: 'airtrail' }) - .execute< - | { - id: number; - trip_id: number; - external_id: string | null; - external_owner_user_id: number | null; - sync_enabled: number | null; - } - | undefined - >('get', false); + .execute<{ id: number; trip_id: number; external_id: string | null; external_owner_user_id: number | null; sync_enabled: number | null } | undefined>('get', false); } /** @@ -1660,17 +1420,12 @@ export class ReservationsRepository extends TrekRepository { * external_id, external_hash FROM reservations WHERE external_source = * 'airtrail' AND sync_enabled = 1 AND external_owner_user_id = ?`. */ - async listAirtrailSyncCandidatesForOwner( - owner_user_id: number, - ): Promise<{ id: number; trip_id: number; external_id: string | null; external_hash: string | null }[]> { + async listAirtrailSyncCandidatesForOwner(owner_user_id: number): Promise<{ id: number; trip_id: number; external_id: string | null; external_hash: string | null }[]> { const platform = this.getEntityManager().getPlatform(); return this.qb('r') .select(['r.id', columnRef(platform, 'r.trip_id').as('trip_id'), 'r.external_id', 'r.external_hash']) .where({ external_source: 'airtrail', sync_enabled: 1, external_owner_user_id: owner_user_id }) - .execute<{ id: number; trip_id: number; external_id: string | null; external_hash: string | null }[]>( - 'all', - false, - ); + .execute<{ id: number; trip_id: number; external_id: string | null; external_hash: string | null }[]>('all', false); } /** @@ -1697,20 +1452,12 @@ export class ReservationsRepository extends TrekRepository { * EXISTS guard, same shape as `Reservations.repository.ts`'s own * `listUnplannedPlacesForPublicApi` NOT EXISTS precedent (`eb.not(eb.exists(...))`). */ - async listFlightsMissingEndpoints(): Promise< - { id: number; metadata: string | null; reservation_time: string | null; reservation_end_time: string | null }[] - > { + async listFlightsMissingEndpoints(): Promise<{ id: number; metadata: string | null; reservation_time: string | null; reservation_end_time: string | null }[]> { const rows = await this.kysely() .selectFrom('reservations as r') .select(['r.id', 'r.metadata', 'r.reservation_time', 'r.reservation_end_time']) .where('r.type', '=', 'flight') - .where((eb) => - eb.not( - eb.exists( - eb.selectFrom('reservation_endpoints as e').select('e.id').whereRef('e.reservation_id', '=', 'r.id'), - ), - ), - ) + .where((eb) => eb.not(eb.exists(eb.selectFrom('reservation_endpoints as e').select('e.id').whereRef('e.reservation_id', '=', 'r.id')))) .execute(); return rows; } @@ -1723,17 +1470,18 @@ export class ReservationsRepository extends TrekRepository { /** RPL4/RPL5's shared select list, the columns of `RoadtripCarrierRow`. */ private roadtripCarrierQuery() { const platform = this.getEntityManager().getPlatform(); - return this.qb('r').select([ - 'r.id', - 'r.type', - 'r.title', - columnRef(platform, 'r.day_id').as('day_id'), - columnRef(platform, 'r.end_day_id').as('end_day_id'), - 'r.reservation_time', - 'r.reservation_end_time', - 'r.metadata', - 'r.day_plan_position', - ]); + return this.qb('r') + .select([ + 'r.id', + 'r.type', + 'r.title', + columnRef(platform, 'r.day_id').as('day_id'), + columnRef(platform, 'r.end_day_id').as('end_day_id'), + 'r.reservation_time', + 'r.reservation_end_time', + 'r.metadata', + 'r.day_plan_position', + ]); } /** @@ -1756,7 +1504,9 @@ export class ReservationsRepository extends TrekRepository { * NULL`. Which of them count as rides is `undatedRides` (shared) alone. */ async listRoadtripUndated(trip_id: number): Promise { - return this.roadtripCarrierQuery().where({ trip: trip_id, day: null }).execute('all', false); + return this.roadtripCarrierQuery() + .where({ trip: trip_id, day: null }) + .execute('all', false); } } diff --git a/server/src/db/repositories/_shared/reservation-pass-context.ts b/server/src/db/repositories/_shared/reservation-pass-context.ts new file mode 100644 index 000000000..05da299e4 --- /dev/null +++ b/server/src/db/repositories/_shared/reservation-pass-context.ts @@ -0,0 +1,46 @@ +import type { DB } from '../../kysely/db'; + +import type { Kysely } from 'kysely'; + +/** The tables `passContextQuery` reads. */ +export type PassContextDB = Pick; + +/** What a booking's Wallet pass needs beyond the booking: where it is, and the trip's name. */ +export interface ReservationPassContext { + trip_title: string | null; + place_lat: number | null; + place_lng: number | null; + place_name: string | null; +} + +/** + * The trip's title and the place a booking is at (its own place, else its + * stay's), for its Apple Wallet pass (`ReservationsRepository.findPassContext`). + * Scoped to the trip, so a booking of another trip reads as none. Kept out of + * the repository file, which is at its size baseline. + */ +export async function passContextQuery( + kysely: Kysely, + id: number, + trip_id: number, +): Promise { + const row = await kysely + .selectFrom('reservations as r') + .innerJoin('trips as t', 't.id', 'r.trip_id') + .leftJoin('places as p', 'p.id', 'r.place_id') + // accommodation_id is TEXT; compared as a number so Postgres takes it as well as SQLite ("14" and "14.0" alike). + .leftJoin('day_accommodations as ap', (join) => + join.on((eb) => eb(eb.cast('r.accommodation_id', 'numeric'), '=', eb.ref('ap.id'))), + ) + .leftJoin('places as acc_p', 'acc_p.id', 'ap.place_id') + .where('r.id', '=', id) + .where('r.trip_id', '=', trip_id) + .select([ + 't.title as trip_title', + (eb) => eb.fn.coalesce('p.lat', 'acc_p.lat').as('place_lat'), + (eb) => eb.fn.coalesce('p.lng', 'acc_p.lng').as('place_lng'), + (eb) => eb.fn.coalesce('p.name', 'acc_p.name').as('place_name'), + ]) + .executeTakeFirst(); + return row as ReservationPassContext | undefined; +} diff --git a/server/src/nest/reservations/reservations-read.service.ts b/server/src/nest/reservations/reservations-read.service.ts index b2060a60a..79e1976d1 100644 --- a/server/src/nest/reservations/reservations-read.service.ts +++ b/server/src/nest/reservations/reservations-read.service.ts @@ -5,7 +5,8 @@ import { Reservations } from '../../db/entities/Reservations.entity'; import type { ReservationDayPositionsRepository } from '../../db/repositories/ReservationDayPositions.repository'; import type { ReservationEndpointsRepository } from '../../db/repositories/ReservationEndpoints.repository'; import type { ReservationTravelersRepository } from '../../db/repositories/ReservationTravelers.repository'; -import type { ReservationPassContext, ReservationsRepository } from '../../db/repositories/Reservations.repository'; +import type { ReservationsRepository } from '../../db/repositories/Reservations.repository'; +import type { ReservationPassContext } from '../../db/repositories/_shared/reservation-pass-context'; import { avatarUrl } from '../common/avatarUrl'; import type { ReservationRow, ReservationEndpoint, ReservationTraveler } from './reservations.service'; import { InjectRepository } from '@mikro-orm/nestjs';