Move explored relationship from caves to entrances#1702
Conversation
Paul-AUB
left a comment
There was a problem hiding this comment.
Issues (Must Fix)
-
[api/controllers/v1/caver/add-explored-entrance.js:34-55] Race condition: the SELECT-then-INSERT pattern has a TOCTOU window. Two concurrent PUT requests for the same
(entranceId, caverId)pair can both find zero rows in the SELECT, then both attempt the INSERT. The second INSERT hits the composite PK constraint and throws an unhandled PostgreSQL error → 500 response to the caller instead of 409. Consider replacing the two-query flow with a single atomic statement:const insertQuery = ` INSERT INTO j_caver_entrance_explorer (id_entrance, id_caver) VALUES ($1, $2) ON CONFLICT (id_entrance, id_caver) DO NOTHING `; const result = await sails.sendNativeQuery(insertQuery, [entranceId, caverId]); if (result.rowCount === 0) { return res.conflict('Caver is already registered as an explorer of this entrance.'); } return res.status(204).send();
This eliminates the race entirely and removes the extra round-trip.
Suggestions (Should Consider)
- [test/integration/4_routes/Cavers/exploredEntrances.test.js:118] The
DELETEdescribe block has no test for a non-existingcaverId(expected: 404). ThePUTblock has this case at line 77. The remove-explored-entrance controller does checkcaver.isDeletedand returns 404, so the path exists — it's just untested. Adding a symmetric case would complete the DELETE coverage.
Nitpicks (Optional)
-
[api/controllers/v1/caver/delete.js:163-167] The
j_caver_cave_explorerraw SQL cleanup runs sequentially after thePromise.allon line 153. There is no data dependency between them, so it could be moved inside thePromise.allcall for a slight efficiency gain (one less sequential round-trip on caver delete/merge). -
[test/integration/0_models/JCaverEntranceExplorer.test.js:32-35] The outer
afterdestroystestEntrancebeforetestCaverwithout first clearing any lingering junction rows. If an innerafterhook fails mid-run, leftoverj_caver_entrance_explorerrows would prevent a hard delete oftestEntrance(FK constraint), leaving test state partially dirty. A safetyawait TCaver.removeFromCollection(testCaver.id, 'exploredEntrances', testEntrance.id)at the top of the outerafter(idempotent — safe to call when rows are already gone) would make cleanup resilient to inner-hook failures.
|
Thanks for the implementation of this feature @ClemRz :D |
- Add SQL migration for j_caver_entrance_explorer table - Add table to 0_tables.sql for fresh installs - Add indexes to test/customSQL.js - Create JCaverEntranceExplorer Waterline model - Add exploredEntrances association to TCaver - Add explorerCavers association to TEntrance - Add test fixture jcaverentranceexplorer.json - Add model association tests # Conflicts: # sql/0_tables.sql
- Add add-explored-entrance controller (PUT, 204/409/404/403) - Add remove-explored-entrance controller (DELETE, 204/404/403) - Wire new routes and policies - Remove cave-level explorer routes, policies, and controllers - Delete stale explored-entrance.test.js - Rewrite route integration tests for entrance endpoints
- Rewrite CaverService.getCaver to populate exploredEntrances directly - Remove exploredCaves association from TCaver and TCave models - Update caver hard-delete to clear j_caver_entrance_explorer - Add raw SQL cleanup for legacy j_caver_cave_explorer on caver delete - Rewrite service and route tests for new response shape - Add mock data SQL for j_caver_entrance_explorer - Add data migration SQL (all entrances per cave) - Update Swagger spec with new entrance explorer endpoints
…ement
- Remove dead listParser('exploredNetworks') from toCaver converter
- Remove exploredNetworks from Caver schema in Swagger spec
- Move entrance explorer routes to Entrance section in routes.js
- Replace TOCTOU SELECT+INSERT with atomic ON CONFLICT in add-explored-entrance - Add unique index to test DB for ON CONFLICT support - Add missing DELETE test for non-existing caverId - Move legacy j_caver_cave_explorer cleanup into Promise.all in delete - Add defensive junction row cleanup in model test outer after hook
49cc830 to
d495d57
Compare
|
All four review items addressed in d495d57:
All 3088 tests passing, lint clean. |
Paul-AUB
left a comment
There was a problem hiding this comment.
All four items from the previous review have been addressed in d495d57:
-
Race condition (must fix) — The two-query SELECT+INSERT has been replaced with a single atomic
INSERT ... ON CONFLICT (id_entrance, id_caver) DO NOTHING+rowCountcheck inadd-explored-entrance.js. The corresponding unique index was also added totest/customSQL.jsso the composite PK is enforced in the test environment. ✅ -
Missing DELETE test for non-existing caverId — A symmetric
should return 404 on non-existing cavertest was added to the DELETE describe block inexploredEntrances.test.js. ✅ -
Sequential cleanup in delete.js — The
j_caver_cave_explorerraw SQL cleanup is now inside the existingPromise.allcall alongside the other parallel operations. ✅ -
Cleanup resilience in model test — A defensive
TCaver.removeFromCollection()call was added at the top of the outerafterhook inJCaverEntranceExplorer.test.js. ✅
The implementation looks solid. The entrance-level model is correctly wired up in both directions (TCaver → exploredEntrances, TEntrance → explorerCavers), the data migration is idempotent, and test coverage is comprehensive.
🚀 Deployment ProcedureThis PR includes a database schema migration that must be executed on the production database before or during the deployment of the new code. Pre-deployment (run against production DB)Execute the migration scripts in order:
DeploymentDeploy the new application code. The old Post-deployment verification-- Confirm the new table has data
SELECT COUNT(*) FROM j_caver_entrance_explorer;
-- Spot-check: compare row counts (new should be >= old since multi-entrance caves expand)
SELECT COUNT(*) FROM j_caver_cave_explorer;Rollback planIf a rollback is needed:
API breaking changes
The Coordinate with the front-end to update API calls before or alongside this deployment. |
🤔 What
Move the "explored caves" relationship from the cave level (
j_caver_cave_explorer) to the entrance level (j_caver_entrance_explorer).Closes #1698
j_caver_entrance_explorerwith composite PK and indexesPUT/DELETE /api/v1/entrances/:entranceId/cavers/:caverIdendpoints replace the old cave-based onesCaverService.getCaver()now returns a flatexploredEntrancesarray (no moreexploredNetworkssplit)j_caver_cave_explorertable preserved (rows cleaned up on caver delete) for rollback safety🤷♂️ Why
The cave-level explorer relationship was an inaccurate model. A caver explores specific entrances, not abstract cave entities. For multi-entrance caves (networks), the old model couldn't express which entrance(s) the caver actually used. Moving to entrance-level granularity aligns the data model with physical reality and simplifies the API response (no more entrance/network bifurcation).
🔍 How
j_caver_entrance_explorertable (migration9_13), mock data (9_14), and data migration from the old table (9_15). Table also added to0_tables.sql.JCaverEntranceExplorerWaterline model.TCaver.exploredEntrancesnow points through it;TEntrance.explorerCaversadded as the inverse.add-explored-entrance.jsandremove-explored-entrance.jsuse raw SQL for precise conflict/existence checks (409 for duplicates, 404 for missing relationships).CaverService.getCaver()simplified — directly populates and returnsexploredEntranceswith names.exploredNetworksmapping removed fromtoCaver.🧪 Testing
The migration
9_15is idempotent (ON CONFLICT DO NOTHING) — safe to re-run.📸 Previews
N/A — backend-only change.