Eliminates the silent-failure pattern where a route/activity row could be committed with gpx IS NOT NULL but geom IS NULL if the PostGIS write failed after the row insert. - New gpx-save.server.ts owns all GPX validation (GpxValidationError, validateGpx) and PostGIS geometry writes (writeGeom, tx-aware) - createRoute, updateRoute, createActivity, createRouteFromActivity all wrapped in db.transaction() covering row + geom + version snapshot - demo-bot uses createRoute/createActivity instead of raw inserts; errors propagate loudly - Callback endpoint returns 400 for GpxValidationError instead of 401 - ADR-0006 documents the invariant for future explorers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 KiB
3 KiB
1. New gpx-save module
- 1.1 Create
apps/journal/app/lib/gpx-save.server.tswithGpxValidationErrorclass andvalidateGpx(gpx: string): Promise<ParsedGpx>— parses, checks ≥2 track points, checks coordinate ranges, throws on failure, returns parsed result - 1.2 Move
setGeomFromGpxlogic intogpx-save.server.tsas an internalwriteGeom(tx, id, table, coords)function that accepts a Drizzle transaction client and throws on failure (no try/catch) - 1.3 Write unit tests in
gpx-save.server.test.tscovering: valid GPX passes, <2 track points throws, out-of-range coordinates throw, unparseable GPX throws
2. Migrate routes.server.ts
- 2.1 Import
validateGpxandwriteGeomfromgpx-save.server.ts; remove localsetGeomFromGpximplementation and its export - 2.2 Wrap
createRouteindb.transaction(): validate GPX → insert row →writeGeom→ insertrouteVersions, all within the transaction client - 2.3 Wrap
updateRouteindb.transaction(): validate GPX → update row →writeGeom→ insert newrouteVersions, all within the transaction client - 2.4 Verify
computeRouteStatsis called with theParsedGpxreturned byvalidateGpx(avoid re-parsing)
3. Migrate activities.server.ts
- 3.1 Replace inline
parseGpxAsynccall increateActivitywithvalidateGpx; extract stats from the returnedParsedGpx - 3.2 Wrap
createActivityindb.transaction(): validate GPX → insert row →writeGeom, all within the transaction client - 3.3 Wrap
createRouteFromActivityindb.transaction(): insert route row →writeGeom→ update activity'srouteId, all within the transaction client - 3.4 Remove
import { setGeomFromGpx } from "./routes.server.ts"— no longer needed
4. Migrate demo-bot.server.ts
- 4.1 Replace the raw
db.insert(routes).values(...)+setGeomFromGpx(routeId, ...)block with acreateRoute(ownerId, input)call; pass pre-computed stats viaRouteInputfields socreateRoutedoesn't re-parse BRouter's output - 4.2 Replace the raw
db.insert(activities).values(...)+setGeomFromGpx(activityId, ...)block with acreateActivity(ownerId, input)call - 4.3 Remove
import { setGeomFromGpx } from "./routes.server.ts"fromdemo-bot.server.ts
5. Callback endpoint error handling
- 5.1 In
api.routes.$id.callback.ts, catchGpxValidationErrorfromupdateRouteand return a 400 response with the validation message (currently the catch block returns 401 for all errors — narrow it)
6. ADR
- 6.1 Write
docs/adr/0006-atomic-gpx-save.mddocumenting that PostGIS geometry writes are always transactional with row writes and thatgpx-save.server.tsis the sole owner of geometry persistence
7. Verification
- 7.1 Run
pnpm typecheck— no new type errors - 7.2 Run
pnpm test— all existing tests pass; newgpx-save.server.test.tstests pass - 7.3 Confirm
setGeomFromGpxis no longer exported from any file (grep -r "setGeomFromGpx" apps/journal/appreturns no results outsidegpx-save.server.ts)