Skip to content

feat: expose read-only layer queries to external plugins - #1784

Merged
giswqs merged 3 commits into
opengeos:mainfrom
Jihkk:feat/external-plugin-query-api
Aug 14, 2026
Merged

feat: expose read-only layer queries to external plugins#1784
giswqs merged 3 commits into
opengeos:mainfrom
Jihkk:feat/external-plugin-query-api

Conversation

@Jihkk

@Jihkk Jihkk commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expose optional read-only layer and feature query methods to external plugins
  • expose the selected layer, all selected features, sketch features, and selection-change subscriptions
  • document compatibility and cleanup behavior for plugin authors
  • add focused tests for selection, layer lookup, sketch filtering, missing IDs, and unsubscribe behavior

Why

External plugins can add layers, but they currently cannot inspect GeoLibre's current layer list or feature selection through the public plugin API. Plugins that need to run an analysis for a user-selected feature therefore have no supported read-only path to obtain that context.

The new methods remain optional so existing plugins can continue to run against older GeoLibre hosts. The query surface does not mutate the application store, and subscription callers receive an unsubscribe function for plugin cleanup.

API

  • listLayers()
  • getLayerFeatures(layerId)
  • getSelectedFeatures()
  • getSelectedLayerId()
  • getDrawnFeatures()
  • onSelectionChange(callback)

Validation

  • node --import tsx --test tests/plugin-query-api.test.ts — 7 passed
  • npm run lint — 0 errors (40 existing warnings)
  • npm run build — passed

The API was also exercised from an external hydrology plugin in the Tauri desktop during a synthetic end-to-end workflow.

Summary by CodeRabbit

  • New Features

    • Added read-only plugin APIs for listing layers and retrieving layer, selected, and drawn features.
    • Plugins can now identify the selected layer and subscribe to selection changes.
    • Added layer visibility, opacity, type, and name details to layer information.
    • Documented selection behavior, empty results, error handling, and subscription cleanup.
  • Tests

    • Added coverage for layer and feature queries, selection updates, drawn features, errors, and immutable results.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The plugin API now provides read-only layer, feature, drawing, and selection queries. It also supports selection-change subscriptions. Documentation and tests define and verify the new behavior.

Changes

Plugin query API

Layer / File(s) Summary
Query API contracts and documentation
packages/plugins/src/types.ts, docs/plugin-api.md
Defines layer summaries, selections, feature query methods, selected-layer access, drawn-feature access, and selection subscriptions. Documents empty results, unknown-layer errors, unsubscribe behavior, and read-only semantics.
Desktop API implementation
apps/geolibre-desktop/src/hooks/usePlugins.ts
Resolves selected features by layer and feature IDs. Adds layer listing, feature retrieval, selected-feature and selected-layer access, sketch-layer feature collection, and selection-change callbacks.
Query API validation
tests/plugin-query-api.test.ts
Tests selection queries, subscriptions, layer and feature listing, state preservation, missing-layer errors, index-based selection, empty selections, detached results, and sketch filtering.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Plugin
  participant createAppAPI
  participant AppStore
  participant readPluginSelection
  Plugin->>createAppAPI: Register onSelectionChange callback
  createAppAPI->>AppStore: Subscribe to selected layer and feature IDs
  AppStore-->>createAppAPI: Report selection change
  createAppAPI->>readPluginSelection: Resolve selected features
  readPluginSelection-->>createAppAPI: Return layer ID and features
  createAppAPI-->>Plugin: Invoke callback
Loading

Possibly related PRs

Suggested reviewers: giswqs

Poem

I’m a rabbit with layers to query,
Features hop back, neat and spry.
Selections change; callbacks cheer,
Sketches appear when drawing is near.
Read-only paws keep data clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: exposing read-only layer and selection query APIs to external plugins.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Jihkk
Jihkk marked this pull request as ready for review August 11, 2026 17:10

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/hooks/usePlugins.ts`:
- Around line 834-844: The plugin query APIs must return detached feature data
rather than Zustand-owned Feature objects. Update getLayerFeatures,
getSelectedFeatures, getDrawnFeatures, and selection callbacks such as
readPluginSelection to deep-copy features, including nested properties and
geometry coordinates; add a regression test that mutates a returned feature and
verifies the store state is unchanged.

In `@packages/plugins/src/types.ts`:
- Around line 342-355: Update GeoLibreSelection.features and every feature-query
method in packages/plugins/src/types.ts to use Feature<Geometry | null>[],
importing Geometry as needed; mirror the same nullable geometry types for all
corresponding APIs in docs/plugin-api.md and add the Geometry import there.
Ensure the public contracts match runtime features that may have geometry: null.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bfdd86d9-2922-4e54-8282-5f5225dc35f5

📥 Commits

Reviewing files that changed from the base of the PR and between f2142fc and 7903a3f.

📒 Files selected for processing (4)
  • apps/geolibre-desktop/src/hooks/usePlugins.ts
  • docs/plugin-api.md
  • packages/plugins/src/types.ts
  • tests/plugin-query-api.test.ts

Comment thread apps/geolibre-desktop/src/hooks/usePlugins.ts
Comment thread packages/plugins/src/types.ts

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/hooks/usePlugins.ts (1)

860-867: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include detached metadata in each layer summary.

GeoLibreLayerSummary includes layer metadata, but listLayers() omits it. Plugins that use the declared metadata field receive undefined.

Return metadata: structuredClone(metadata) with each summary. This also preserves the read-only API boundary.

Proposed fix
-      useAppStore.getState().layers.map(({ id, name, type, visible, opacity }) => ({
+      useAppStore.getState().layers.map(({ id, name, type, visible, opacity, metadata }) => ({
         id,
         name,
         type,
         visible,
         opacity,
+        metadata: structuredClone(metadata),
       })),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/hooks/usePlugins.ts` around lines 860 - 867, Update
listLayers in usePlugins.ts to destructure each layer’s metadata and include
metadata: structuredClone(metadata) in every returned layer summary, preserving
the read-only API boundary and the existing summary fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/hooks/usePlugins.ts`:
- Around line 860-867: Update listLayers in usePlugins.ts to destructure each
layer’s metadata and include metadata: structuredClone(metadata) in every
returned layer summary, preserving the read-only API boundary and the existing
summary fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0f975e57-8375-4547-9a6a-e0a783df0681

📥 Commits

Reviewing files that changed from the base of the PR and between 7903a3f and a6b788a.

📒 Files selected for processing (4)
  • apps/geolibre-desktop/src/hooks/usePlugins.ts
  • docs/plugin-api.md
  • packages/plugins/src/types.ts
  • tests/plugin-query-api.test.ts

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://b45c60a3.geolibre-preview.pages.dev
Demo app https://b45c60a3.geolibre-preview.pages.dev/demo/
Commit a6b788a

@giswqs giswqs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thank you for your contribution.

@giswqs
giswqs merged commit 6093aa8 into opengeos:main Aug 14, 2026
19 of 20 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1784/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1784/demo/
Commit a6b788a

Note

GitHub Pages built this preview successfully, but its serving edge returned HTTP 403 when checked. The links may still be propagating.

giswqs added a commit that referenced this pull request Aug 14, 2026
)

The read-only query API added in #1784 is exercised through
`createAppAPI`, which lives in `usePlugins.ts`. That module imports the
whole built-in plugin registry, so loading it in a Node test pulled in
MapCanvas, CesiumCanvas, and every `maplibre-*` plugin: 39 browser-only
modules, none of them meaningfully exercised, all of them newly counted
by the coverage reporter.

Coverage is reported only over files a test actually imports, so those 39
files landed in the denominator at 1-30% function coverage and dropped
the total from 72.90% to 60.36%, under the 63% floor. CI has been red on
`main` since, and the report is measuring module reachability rather than
how well the code is tested.

Move the six query methods and `readPluginSelection` into
`lib/plugin-layer-queries.ts`, which needs only the store, and have
`createAppAPI` spread them in. The test imports that module directly and
no longer stubs `maplibre-gl`, `window`, `sessionStorage`, or
`localStorage` to get off the ground. Same reasoning, and the same shape,
as `geo-editor-geometry.ts` in `@geolibre/plugins`, which is already kept
free of the Geoman/MapLibre runtime so it can be unit-tested under Node.

`SKETCHES_SOURCE_KIND` gets a subpath export alongside the five already
in that package, so the test can share the constant instead of repeating
the string literal as it did before.

The wiring into the plugin-facing API is now a typed spread rather than
something this test asserts; `tsc` covers it on every build.

Coverage returns to 82.84% lines / 84.45% branches / 72.55% functions
(baseline before #1784: 82.89 / 84.46 / 72.90), with the counted file set
back from 444 to 406. The one addition is the extracted module itself, at
100% lines and 95.45% functions. All 8 tests still pass.
giswqs added a commit that referenced this pull request Aug 14, 2026
)

* fix(ci): re-measure a line-only coverage shortfall before failing

Line coverage is nondeterministic on CI. Two runs over byte-identical
sources reported 81.82% and 76.47%, with 114 of 444 files differing on
lines and zero differing on branches or functions, the same 5935 tests
passing in both. The low run failed a floor the identical tree had
cleared minutes earlier, which is how a release PR touching only version
strings and docs came to fail CI.

It does not reproduce locally. Eleven runs across Node 26 and CI's exact
Node 22.23.2, at 24 cores and pinned to 4 with taskset, and at both
default and serial test concurrency, all landed within 0.05 points.
Serial execution produced identical numbers to parallel at 2.3x the wall
time, so pinning concurrency buys nothing measurable.

Since the trigger is unknown but the signature is specific, mitigate it
narrowly instead of lowering the floor. `test:frontend:coverage` now runs
through `scripts/coverage-check.mjs`, following the `audit-check.mjs`
precedent of wrapping a tool that cannot express the policy we want. Node
still enforces all three floors; the wrapper re-measures once when lines
alone come up short with every test passing, and fails if the second run
is short too. Branch and function shortfalls, and any test failure, fail
immediately with no retry, so a real regression still fails fast and a
line regression fails on the second run rather than being retried away.

`classify()` is exported and covered by tests/coverage-check.test.ts:
a retry that swallowed a genuine regression would be worse than the flake
it works around, so which failures earn a second run is pinned by tests
rather than trusted. Verified end to end with impossible floors: a
line-only shortfall runs the suite twice and fails; a function shortfall
runs it once and fails.

CLAUDE.md documents the wrapper, and separately the denominator trap that
caused the real regression in #1784, since "first test for a big module
looks like a regression" will recur.

* fix(ci): stream the wrapped suite instead of buffering it

The first version of the wrapper captured the run with spawnSync and
wrote it out afterwards, then called process.exit. `process.stdout` is
asynchronous when it is a pipe, which is what CI gives it, so the exit
discarded everything still queued: the CI run for this branch lost about
42,000 lines of test output and the entire coverage summary, cut off
mid-line, while still reporting success. Redirecting to a file locally
made stdout synchronous, which is why it passed here first.

Stream the child's stdout and stderr through as they arrive, and return
an exit code for the caller to assign to process.exitCode rather than
calling process.exit anywhere, so Node exits only once the output has
drained. Classification is unchanged; it reads the accumulated string
either way.

Verified with stdout on a pipe, the CI shape rather than the one that
hid it: 45,384 lines through the wrapper ending in "end of coverage
report" with the summary intact, against 2,044 truncated lines before.
The retry paths still behave, checked with impossible floors: line-only
short runs the suite twice and exits 1, function short runs it once and
exits 1.

Pins the regression at the source, since a truncated log looks green and
no assertion downstream would notice.
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.

2 participants