refactor(tours): the region repository reads the status enum of its entity, unused exports go

PaddleRegionsRepository types and writes the status through the generated PaddleRegionsStatus instead of a copy of its four values; the paint tokens, a fact key type and a minutes input stay private to their modules, the wind test pins its 502 text, and the planner page's size entry follows it down.
This commit is contained in:
Maurice
2026-10-16 23:24:20 +00:00
parent 1e2ba0f842
commit c246877f30
6 changed files with 19 additions and 15 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
"src/mobile/screens/admin/MAdminPluginsPanel.tsx": 1507,
"src/pages/JourneyDetailPage.test.tsx": 4030,
"src/pages/LoginPage.tsx": 1348,
"src/pages/TripPlannerPage.tsx": 1147,
"src/pages/TripPlannerPage.tsx": 1142,
"src/pages/atlas/useAtlas.ts": 1287,
"src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx": 3518,
"src/pages/tripPlanner/useTripPlanner.test.tsx": 3309,
@@ -36,7 +36,7 @@ export function PaddleRolePill({ role }: { role: PaddleRole }) {
* whole number of minutes in range is dropped on leaving the field, and the
* value before it stays.
*/
export function MinutesInput({
function MinutesInput({
label,
value,
placeholder,
@@ -26,7 +26,7 @@ export const OBSTACLE_FACT_KEYS = [
'fee',
'operator',
] as const;
export type ObstacleFactKey = (typeof OBSTACLE_FACT_KEYS)[number];
type ObstacleFactKey = (typeof OBSTACLE_FACT_KEYS)[number];
/** The access values the card puts into words; any other value is shown as OSM has it. */
export const KNOWN_ACCESS_VALUES = ['yes', 'no', 'private', 'permissive', 'designated'] as const;
@@ -7,7 +7,7 @@ import { useMapPaint } from '../../Map/mapPaint';
* reads them resolved (`usePaddlePaint`); markup on the page takes the CSS
* variable itself (`paddleKindCss`), which follows the theme on its own.
*/
export const PADDLE_PAINT_TOKENS = {
const PADDLE_PAINT_TOKENS = {
obstacle: '--map-paddle-obstacle',
access: '--map-paddle-access',
landing: '--map-paddle-landing',
@@ -1,10 +1,8 @@
import type { PaddleRegions } from '../entities/PaddleRegions.entity';
import { type PaddleRegions, PaddleRegionsStatus, type TPaddleRegionsStatus } from '../entities/PaddleRegions.entity';
import type { DB } from '../kysely/db';
import type { Box } from './PaddleFeatures.repository';
import { TrekRepository } from './_shared/trek-repository';
export type PaddleRegionStatus = 'queued' | 'importing' | 'ready' | 'failed';
/** One region an admin chose, as stored; the boundary is still GeoJSON text. */
export interface PaddleRegionRow {
id: string;
@@ -14,7 +12,7 @@ export interface PaddleRegionRow {
min_lng: number | null;
max_lat: number | null;
max_lng: number | null;
status: PaddleRegionStatus;
status: TPaddleRegionsStatus;
source_timestamp: string | null;
imported_at: string | null;
feature_count: number;
@@ -23,7 +21,7 @@ export interface PaddleRegionRow {
/** `status` narrowed to the four values the column's CHECK constraint allows. */
type RegionsDB = {
paddle_regions: Omit<DB['paddle_regions'], 'status'> & { status: PaddleRegionStatus };
paddle_regions: Omit<DB['paddle_regions'], 'status'> & { status: TPaddleRegionsStatus };
};
const COLUMNS = [
@@ -78,7 +76,7 @@ export class PaddleRegionsRepository extends TrekRepository<PaddleRegions> {
return await this.db()
.selectFrom('paddle_regions')
.select(COLUMNS)
.where('status', '=', 'queued')
.where('status', '=', PaddleRegionsStatus.QUEUED)
.orderBy('created_at', 'asc')
.executeTakeFirst();
}
@@ -88,7 +86,7 @@ export class PaddleRegionsRepository extends TrekRepository<PaddleRegions> {
): Promise<void> {
await this.db()
.insertInto('paddle_regions')
.values({ ...row, status: 'queued', feature_count: 0 })
.values({ ...row, status: PaddleRegionsStatus.QUEUED, feature_count: 0 })
.onConflict((oc) => oc.column('id').doNothing())
.execute();
}
@@ -117,15 +115,19 @@ export class PaddleRegionsRepository extends TrekRepository<PaddleRegions> {
async queueReady(): Promise<number> {
const result = await this.db()
.updateTable('paddle_regions')
.set({ status: 'queued', error: null })
.where('status', 'in', ['ready', 'failed'])
.set({ status: PaddleRegionsStatus.QUEUED, error: null })
.where('status', 'in', [PaddleRegionsStatus.READY, PaddleRegionsStatus.FAILED])
.executeTakeFirst();
return Number(result.numUpdatedRows ?? 0);
}
/** A region left half read by a restart goes back to the queue. */
async requeueInterrupted(): Promise<void> {
await this.db().updateTable('paddle_regions').set({ status: 'queued' }).where('status', '=', 'importing').execute();
await this.db()
.updateTable('paddle_regions')
.set({ status: PaddleRegionsStatus.QUEUED })
.where('status', '=', PaddleRegionsStatus.IMPORTING)
.execute();
}
async deleteRegion(id: string): Promise<boolean> {
@@ -6,7 +6,7 @@
* request is a 502 of its own rather than an empty forecast.
*/
import { DomainError } from '../../../../src/nest/common/domain-error';
import { WindService } from '../../../../src/nest/weather/wind.service';
import { WIND_UNAVAILABLE_MESSAGE, WindService } from '../../../../src/nest/weather/wind.service';
import { afterEach, describe, expect, it, vi } from 'vitest';
@@ -97,6 +97,8 @@ describe('WindService', () => {
vi.stubGlobal('fetch', answer({ hourly: 'nonsense' }));
await expect(service.forecast({ lat: 47, lng: 9, date: '2026-10-19' }, NOW)).rejects.toMatchObject({
code: 'wind_unavailable',
status: 502,
publicMessage: WIND_UNAVAILABLE_MESSAGE,
});
});