Files
tesla-roadtrip/server/lib/tripStore.ts
T
tony 89b24d4c34 feat: wire build/test infra, trips API, and enriched journey stops
- Add tsconfig.json (server) + client/tsconfig.{json,app.json,node.json}
  so typecheck and tsc -b actually work.
- Fix npm test to run Playwright (was running vitest on Playwright specs);
  typecheck now covers both server and client.
- Mount routes before app.listen, add error handler, mount optional
  @tonycodes/auth-express middleware when AUTH_SECRET is set.
- Add /api/trips (GET/POST/PATCH/DELETE) backed by an in-memory store
  that gracefully degrades when DATABASE_URL is unset.
- Add prisma/seed.ts skeleton and server/types/express.d.ts for req.auth.
- Rewrite Grok prompt for combo-aware planning: charge+eat,
  stay+destination-charging, eat+viewpoint, etc., with amenities,
  cuisine, priceLevel, duration, day titles and trip highlights.
- Extend Stop schema + normalization to preserve all enrichment fields.
- New StopCard component renders combo pill, description, meta row
  (charge / stop / battery / cuisine / £-level) and amenity icons;
  map popups show the same enriched detail; timeline gains day titles
  and a HIGHLIGHTS sidebar.
- Fix server TS errors (vehicle accepted as string | {name,rangeKm},
  JSON parse results typed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:32:53 +01:00

89 lines
2.4 KiB
TypeScript

import { env } from '../config/env.js';
import { createLogger } from './logger.js';
const log = createLogger('trip-store');
export interface Trip {
id: string;
userId: string;
title: string;
vehicleModel: string;
rangeMi: number;
itinerary: unknown;
status: string;
isPublic: boolean;
shareSlug: string | null;
createdAt: Date;
updatedAt: Date;
}
export type TripInput = Omit<Trip, 'id' | 'createdAt' | 'updatedAt' | 'shareSlug'> & {
shareSlug?: string | null;
};
interface TripStore {
list(userId: string): Promise<Trip[]>;
get(id: string, userId: string): Promise<Trip | null>;
create(input: TripInput): Promise<Trip>;
update(id: string, userId: string, patch: Partial<TripInput>): Promise<Trip | null>;
remove(id: string, userId: string): Promise<boolean>;
}
class MemoryTripStore implements TripStore {
private trips = new Map<string, Trip>();
async list(userId: string): Promise<Trip[]> {
return [...this.trips.values()]
.filter(t => t.userId === userId)
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
}
async get(id: string, userId: string): Promise<Trip | null> {
const trip = this.trips.get(id);
if (!trip || trip.userId !== userId) return null;
return trip;
}
async create(input: TripInput): Promise<Trip> {
const id = crypto.randomUUID();
const now = new Date();
const trip: Trip = {
id,
...input,
shareSlug: input.shareSlug ?? null,
createdAt: now,
updatedAt: now,
};
this.trips.set(id, trip);
return trip;
}
async update(id: string, userId: string, patch: Partial<TripInput>): Promise<Trip | null> {
const trip = this.trips.get(id);
if (!trip || trip.userId !== userId) return null;
const updated: Trip = { ...trip, ...patch, updatedAt: new Date() };
this.trips.set(id, updated);
return updated;
}
async remove(id: string, userId: string): Promise<boolean> {
const trip = this.trips.get(id);
if (!trip || trip.userId !== userId) return false;
return this.trips.delete(id);
}
}
let store: TripStore | null = null;
export function getTripStore(): TripStore {
if (store) return store;
if (!env.databaseUrl) {
log.warn('DATABASE_URL not set — using in-memory trip store (data lost on restart)');
store = new MemoryTripStore();
return store;
}
log.info('Using in-memory trip store (Prisma adapter not yet wired)');
store = new MemoryTripStore();
return store;
}