refactor(trips): the trip span check and the feed token strip move into trips helpers

This commit is contained in:
Maurice
2026-10-12 21:13:57 +00:00
parent 7454c8f95f
commit f470ae029d
3 changed files with 73 additions and 30 deletions
+35
View File
@@ -0,0 +1,35 @@
import { ValidationError } from '../common/domain-errors';
import { MAX_TRIP_DAYS, tripSpanDays } from '@trek/shared';
/**
* Pure helpers of the trips domain, moved out of trips.service.ts. Nothing
* here touches the DB, so they stay plain exports rather than methods on
* TripsService.
*/
/**
* The date range is refused, not cut short: generateDays used to clip the day
* rows at the limit while the trip kept its full end date, so everything past
* the cut-off had dates but no day to go on (#2403). An inverted range is
* refused here as well, because a start date moved past the stored end date
* arrives on its own and would otherwise empty the trip.
*/
export function assertTripSpan(startDate: string, endDate: string) {
const span = tripSpanDays(startDate, endDate);
if (span < 1) throw new ValidationError('End date must be after start date');
if (span > MAX_TRIP_DAYS) throw new ValidationError(`A trip can span at most ${MAX_TRIP_DAYS} days`);
}
/**
* Strips `feed_token` from a trip row on its way out.
*
* The column is the sole credential for the anonymous /api/feed/trip/:token.ics
* route, and `SELECT t.*` hands it to every reader of the trip. Gating the
* token endpoint on `share_manage` means nothing while any member can read the
* same value out of the trip payload, so the two go together. No TREK client
* reads the field (it is absent from client/ and shared/ entirely).
*/
export function withoutFeedToken<T>(row: T): T {
if (row && typeof row === 'object') delete (row as { feed_token?: unknown }).feed_token;
return row;
}
+4 -30
View File
@@ -27,11 +27,9 @@ import { Tours } from '../../db/entities/Tours.entity';
import { TourWaypoints } from '../../db/entities/TourWaypoints.entity';
import type { TourWaypointRow } from '../../db/repositories/TourWaypoints.repository';
import {
MAX_TRIP_DAYS,
addIsoDays,
planDayGrid,
resolveDayGridRange,
tripSpanDays,
type ActiveTrip,
type DayGridPlan,
type DayGridRemoval,
@@ -53,34 +51,7 @@ import { NotFoundError, ValidationError } from '../common/domain-errors';
import { UnitOfWork } from '../database/unit-of-work';
import { legacyBoundIntegerText } from '../common/row-id';
import { appClock } from '../common/timezoneService';
/**
* The date range is refused, not cut short: generateDays used to clip the day
* rows at the limit while the trip kept its full end date, so everything past
* the cut-off had dates but no day to go on (#2403). An inverted range is
* refused here as well, because a start date moved past the stored end date
* arrives on its own and would otherwise empty the trip.
*/
function assertTripSpan(startDate: string, endDate: string) {
const span = tripSpanDays(startDate, endDate);
if (span < 1) throw new ValidationError('End date must be after start date');
if (span > MAX_TRIP_DAYS) throw new ValidationError(`A trip can span at most ${MAX_TRIP_DAYS} days`);
}
/**
* Strips `feed_token` from a trip row on its way out.
*
* The column is the sole credential for the anonymous /api/feed/trip/:token.ics
* route, and `SELECT t.*` hands it to every reader of the trip. Gating the
* token endpoint on `share_manage` means nothing while any member can read the
* same value out of the trip payload, so the two go together. No TREK client
* reads the field (it is absent from client/ and shared/ entirely).
*/
export function withoutFeedToken<T>(row: T): T {
if (row && typeof row === 'object') delete (row as Record<string, unknown>).feed_token;
return row;
}
import { assertTripSpan } from './trips.helpers';
interface CreateTripData {
title: string;
@@ -1003,3 +974,6 @@ export class TripsService {
// trip aggregate; re-exported here because nine files already import them from
// this module.
export { NotFoundError, ValidationError };
// Lives in trips.helpers.ts; re-exported for the importers that read it from here.
export { withoutFeedToken } from './trips.helpers';
@@ -0,0 +1,34 @@
/**
* The pure helpers of the trips domain: the date range a trip may span and the
* feed token a trip row loses on its way out. TRIP-HELPERS-001 to 003.
*/
import { assertTripSpan, withoutFeedToken } from '../../../src/nest/trips/trips.helpers';
import { MAX_TRIP_DAYS, addIsoDays } from '@trek/shared';
import { describe, expect, it } from 'vitest';
describe('assertTripSpan', () => {
it('TRIP-HELPERS-001: takes a range from a single day up to the limit', () => {
const lastAllowed = addIsoDays('2025-07-01', MAX_TRIP_DAYS - 1);
expect(() => assertTripSpan('2025-07-01', '2025-07-01')).not.toThrow();
expect(() => assertTripSpan('2025-07-01', lastAllowed)).not.toThrow();
});
it('TRIP-HELPERS-002: refuses an inverted range and one past the limit', () => {
const tooLate = addIsoDays('2025-07-01', MAX_TRIP_DAYS);
expect(() => assertTripSpan('2025-07-02', '2025-07-01')).toThrow('End date must be after start date');
expect(() => assertTripSpan('2025-07-01', tooLate)).toThrow(`A trip can span at most ${MAX_TRIP_DAYS} days`);
});
});
describe('withoutFeedToken', () => {
it('TRIP-HELPERS-003: drops feed_token from the row it was given and passes anything else through', () => {
const row = { id: 1, title: 'Japan', feed_token: 'feed-1' };
expect(withoutFeedToken(row)).toBe(row);
expect(row).toEqual({ id: 1, title: 'Japan' });
expect(row).not.toHaveProperty('feed_token');
expect(withoutFeedToken(undefined)).toBeUndefined();
expect(withoutFeedToken(null)).toBeNull();
expect(withoutFeedToken(42)).toBe(42);
});
});