Skip to content

feat(geoloc): expose per-entrance coordinates in GET /geoloc/networks#1712

Merged
ClemRz merged 2 commits into
developfrom
feat/geoloc-networks-entrances
Jun 28, 2026
Merged

feat(geoloc): expose per-entrance coordinates in GET /geoloc/networks#1712
ClemRz merged 2 commits into
developfrom
feat/geoloc-networks-entrances

Conversation

@ClemRz

@ClemRz ClemRz commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🤔 What

  • Enhance GET /api/v1/geoloc/networks to include per-entrance coordinates in its response
  • Each network object now contains an entrances array with id, name, latitude, longitude for each non-sensitive entrance
  • The top-level centroid (longitude/latitude) is now computed from ALL non-sensitive entrances of the network, making it stable across map pans
  • Closes Expose per-entrance coordinates in GET /geoloc/networks #1697

🤷‍♂️ Why

The main map uses this endpoint to display network markers. Currently it returns a single centroid per network computed only from entrances visible in the viewport — the centroid shifts as the user pans. The frontend needs individual entrance coordinates to render markers at each entrance location, and the centroid should remain stable.

🔍 How

  • Replaced the NETWORKS_IN_BOUNDS SQL with a CTE-based query:
    • qualifying_networks CTE identifies caves with >1 non-deleted entrances (sensitive ones count toward qualification) that have at least one non-sensitive entrance in the bbox
    • Main query returns flat rows with entrance-level data + network centroid via avg() OVER (PARTITION BY)
  • Rewrote formatNetworks to group flat rows by cave ID into network objects with entrances arrays
  • Network name uses COALESCE(cave_name, first_entrance_name) with a LATERAL subquery to avoid row multiplication
  • Backwards compatible: existing id, name, longitude, latitude fields preserved

🧪 Testing

  • All 3106 existing tests pass
  • Updated integration tests in GeoLocService.test.js to verify the new response shape (cave 1 with entrances 1 & 2)
  • Added centroid math assertion (avg of entrance coords = top-level coords)
  • Updated route tests in Geoloc.test.js to verify HTTP endpoint returns entrances array
  • Added property-based tests (GeoLocNetworks.property.test.js) for formatNetworks grouping logic and centroid correctness
  • Updated GeoLocNameFilter.test.js assertions for the new shape

Run: npm run test -- --grep "GeoLocService|GeoLocNetworks|find networks"

📸 Previews

N/A (API-only change)

- Rewrite NETWORKS_IN_BOUNDS SQL with CTE for stable centroid and entrance list
- Network qualification: caves with >1 non-deleted entrances (sensitive count)
- Response includes entrances array with id, name, latitude, longitude per entrance
- Centroid computed from all non-sensitive, non-deleted entrances (stable across pans)
- Add property-based tests for formatNetworks grouping and centroid math
- Update integration tests for new response shape
- Update Swagger documentation for /geoloc/networks

Closes #1697
@ClemRz ClemRz self-assigned this Jun 28, 2026
@ClemRz ClemRz requested a review from Paul-AUB June 28, 2026 20:23
Paul-AUB
Paul-AUB previously approved these changes Jun 28, 2026

@Paul-AUB Paul-AUB left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean implementation of a well-thought-out feature. The CTE + window function approach is the right tool here, the formatNetworks rewrite is clear, and the test coverage (integration + property-based) is exemplary for a service-layer change. A few items worth looking at before merge:

Suggestions (Should Consider)

  1. [GeoLocService.js:112] The LEFT JOIN t_name AS nc ON nc.id_cave = c.id AND nc.is_main = true AND nc.is_deleted = false has no SQL-level uniqueness guarantee. If a cave ever carries two active is_main = true name entries (e.g. a multilingual app storing one per language), each entrance row in the output would be silently duplicated. The old query's GROUP BY c.id, COALESCE(nc.name, ne.name) happened to collapse this as a side effect. The rest of the codebase follows the same JOIN pattern, so the data model likely enforces uniqueness today, but since the original GROUP BY was removed here, consider adding a defensive DISTINCT ON (c.id, en.id) or replacing the cave-name join with a LIMIT 1 lateral subquery (mirroring the nc_first_ent pattern already used for the fallback).

  2. [GeoLocNetworks.property.test.js:8] formatNetworks is inlined verbatim in the property test file rather than imported from the service. If the real function is refactored, these property tests will keep passing against the stale copy and won't catch the regression. Consider importing directly:

    // eslint-disable-next-line import/no-unresolved
    const { formatNetworks } = require('../../../api/services/GeoLocService');

    (Export it from the service, or extract it to a dedicated util if it needs to remain private.)

  3. [swaggerV1.yaml:5112] The network-level name field is documented as type: string without nullable: true, but COALESCE(nc.name, nc_first_ent.name) can return null when a cave has no name row in t_name and every one of its non-sensitive entrances also has no name. The entrance-level name on line 5132 correctly marks nullable: true — the same treatment should apply to the top-level name for consistency and to avoid client-side surprises.

Nitpicks (Optional)

  1. [GeoLocService.js:124] NETWORKS_IN_BOUNDS ends with a trailing semicolon (ORDER BY c.id, en.id;) that no other SQL constant in the file has. node-postgres accepts it, but it's inconsistent with the surrounding style.

  2. [GeoLocNetworks.property.test.js:45] The entrance-ID generator (fc.integer({ min: 1, max: 100000 })) does not enforce uniqueness across entries within the same network. The real SQL would never return duplicate entrance IDs (primary key), so the fixture slightly oversteps the production domain. Using fc.uniqueArray or sequential IDs would make the fixture a more faithful replica of what the service actually returns.

- Replace cave-name LEFT JOIN with LATERAL LIMIT 1 to prevent row duplication
- Export formatNetworks from GeoLocService and import in property tests
- Add nullable: true to network-level name in swagger spec
- Remove trailing semicolon from NETWORKS_IN_BOUNDS for style consistency
- Use fc.uniqueArray with selector for entrance ID uniqueness in property tests
@ClemRz

ClemRz commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @Paul-AUB! All items addressed in a939732:

  1. Cave-name JOIN duplication risk — Replaced the plain LEFT JOIN t_name AS nc with a LEFT JOIN LATERAL ... ORDER BY n.id ASC LIMIT 1 subquery (same pattern as the existing nc_first_ent lateral). This guarantees at most one cave name row regardless of data state.

  2. Inlined formatNetworks in property tests — Exported formatNetworks from GeoLocService.js and the property test now imports it directly. Regressions in the real function will be caught.

  3. Swagger name nullable — Added nullable: true to the network-level name field, consistent with the entrance-level name.

  4. Trailing semicolon — Removed for consistency with the other SQL constants in the file.

  5. Entrance ID uniqueness in arbitrary — Switched to fc.uniqueArray(..., { selector: (e) => e.id }) to faithfully mirror the primary key guarantee from SQL.

All 3106 tests pass, lint clean, and manual curl validation confirmed happy path + edge cases.

@ClemRz ClemRz merged commit ae5ad5b into develop Jun 28, 2026
5 checks passed
@ClemRz ClemRz deleted the feat/geoloc-networks-entrances branch June 28, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose per-entrance coordinates in GET /geoloc/networks

2 participants