Skip to content

Phase 8 / Task A4: Issue #167 — Pagination-contract JSDoc on list methods #167

@Sam-Bolling

Description

@Sam-Bolling

Phase 8 Task

Task ID: A4
Title: Issue #167 — Pagination-Contract JSDoc on List Methods
Source: Repo issue #167
Severity: P3-Minor
Category: Documentation / API Design
Ownership: Ours (CSAPI module — csapi/url_builder.ts)
Phase 8 phase: A — Documentation & Type-Hardening


Goal

Every list method on CSAPIQueryBuilder carries JSDoc that explicitly documents the pagination contract: server picks the default page size, and the consumer follows next HATEOAS links to retrieve subsequent pages. A centralized "Pagination" doc anchor on the module/class docblock is the single source of truth; per-method @remarks blocks point at it.

Acceptance criterion (from P8-contribution-goal-and-definition.md): B2 — pagination contract documented on every list method.

Locked Decision

⚠️ This decision is locked. Do not re-litigate in this issue. Surface deviations to the user; do not silently re-decide.

Decision: Adopt the docs-only fix from issue #167 — add a centralized "Pagination" anchor in the csapi/url_builder.ts module/class docblock, plus a @remarks block on every list method that points at the anchor. Do NOT add an auto-pagination helper or async-iterator in Phase 8 — those are deferred to issue #170 and explicitly out of scope here.

The @remarks block must call out, in plain language, that:

  1. The server picks the default limit (connected-systems-go defaults to 10; OpenSensorHub defaults to 100 — concrete servers cited).
  2. The consumer must follow rel: "next" HATEOAS links to retrieve subsequent pages.
  3. A future async-iterator helper is deferred (cite Future-enhancement (deferred): async-iterator helpers for paginated CSAPI list methods — out-of-scope until upstream broadens scope #170 by URL).

Locked in:


Problem Statement

connected-systems-go defaults to limit=10; OpenSensorHub defaults to limit=100. A consumer who only tested against the high-default server may silently process only the first page in production against a low-default server. The OGC API Common spec lets servers choose the default; the library's job is to make that contract impossible to miss in the JSDoc.

Affected code: src/ogc-api/csapi/url_builder.ts — every list method (getSystems, getDeployments, getProcedures, getSamplingFeatures, getDatastreams, getDatastreamObservations, getDatastreamSystems, getDatastreamProcedures, getDatastreamHistory, getSystemDatastreams, getSystemSubsystems, getProcedureDatastreams, getControlStreams, getCommands, getCommandStatus if it returns a list, getObservations if exposed) currently lacks the explicit pagination contract in its JSDoc.

Impact: Silent data loss in consumer code that processes only the first page. Pure DX/correctness-of-usage gap; no library-side runtime bug.

Files to Modify

File Action Est. Lines Purpose
src/ogc-api/csapi/url_builder.ts Modify ~80 Class/module-level "Pagination" doc anchor + @remarks Pagination block on each list method (~16 methods)

Cross-reference P8-implementation-guide §5.2 for the canonical files-modified list. If you find yourself adding a file that isn't in the implementation guide, stop — surface the question.

Implementation Approach

Pull the canonical sketches from P8-implementation-guide §5.2.

Centralized "Pagination" anchor (class/module docblock in csapi/url_builder.ts):

/**
 * ...
 *
 * ## Pagination
 *
 * All list methods (`get*` returning collection URLs) follow the
 * [OGC API Common](https://docs.ogc.org/is/19-072/19-072.html#_pagination)
 * pagination contract:
 *
 * - **The server chooses the default page size** if `limit` is unspecified.
 *   Defaults vary by implementation — `connected-systems-go` defaults to
 *   `limit=10`; OpenSensorHub defaults to `limit=100`. Code that processes
 *   only the first response may silently lose data on low-default servers.
 *
 * - **The server returns `next` HATEOAS links** in the response body's
 *   `links` array (`rel: "next"`) when more pages are available. The
 *   consumer is responsible for following them; this library does not
 *   auto-paginate.
 *
 * - **A future enhancement** (deferred — see issue
 *   [#170](https://github.com/OS4CSAPI/ogc-client-CSAPI_2/issues/170))
 *   may add an opt-in async-iterator / `followNext` helper. Until then,
 *   consumer code MUST follow `next` links explicitly to avoid data loss.
 *
 * ...
 */

Per-method @remarks tag — apply verbatim shape to every list method:

/**
 * Builds the URL for a paginated list of datastreams.
 *
 * @remarks
 * **Pagination:** server picks the default `limit` if unspecified; the
 * consumer must follow `next` HATEOAS links from the response body to
 * retrieve subsequent pages. See the
 * [Pagination section of this module's docs](#pagination).
 *
 * @param options - Optional query parameters (limit, bbox, datetime, etc.).
 * @returns The fully-formed URL string.
 */
public getDatastreams(options?: DatastreamQueryOptions): string { ... }

Sequencing note: A4 should be executed after Task B1 (the Datastream rename) so JSDoc bodies are written against the final method names. If A4 runs before B1, the JSDoc will be re-touched in B1; mechanically valid but creates churn. Either order works; post-B1 is recommended.

Test impact: none for behavior. An optional snapshot/lint test asserting every public list method's JSDoc matches /Pagination:.*next.*links/i MAY be added if it does not increase friction; otherwise skip.

Scope — What NOT to Touch

Acceptance Criteria

  • Class- or module-level "Pagination" doc anchor added to src/ogc-api/csapi/url_builder.ts covering: server-picks-default contract, concrete defaults (CS-Go=10, OSH=100), next-link-follow responsibility, deferred-helper note citing Future-enhancement (deferred): async-iterator helpers for paginated CSAPI list methods — out-of-scope until upstream broadens scope #170
  • Every public list method on CSAPIQueryBuilder carries a @remarks block with the Pagination note pointing at the anchor (target: ~16 methods after Task B1's renames)
  • All @remarks Pagination blocks use consistent wording (template above)
  • No source code changes (signatures, runtime behavior, return types) — docs only
  • All modified files pass npx prettier --check
  • npm run typecheck exits 0
  • npm run lint exits 0
  • npm run test:browser exits 0
  • npm run test:node exits 0

Acceptance Gate (verification command)

The Phase 8 roadmap defines a specific verification command for this task (P8-ROADMAP §Phase A Task A4). Paste the output of these commands on the issue before closing:

# 1. Manual review confirmation — paste a one-paragraph statement listing every
#    list method touched and confirming each carries the @remarks Pagination
#    block, plus citing the line range of the centralized anchor.

# 2. Spot-check via grep — every list method should have a Pagination remark.
git grep -n "Pagination:" -- src/ogc-api/csapi/url_builder.ts
# Expected: one match per list method + 1 anchor heading

# 3. Full upstream QA suite (must mirror .github/workflows/qa.yml)
npm run format:check
npm run typecheck
npm run lint
npm run test:browser
npm run test:node

Expected output:

  • git grep returns ≥ 16 matches (one per list method) plus the anchor section heading
  • All five upstream QA commands exit 0
  • Manual review paragraph confirms uniform coverage + anchor presence

Dependencies

Mandatory. Walk the P8-ROADMAP dependency graph and fill in every applicable row.

Blocked by: Nothing hard. Soft dependency on Task B1 (the Datastream rename) — recommended sequencing is to run A4 after B1 so JSDoc bodies are written against final method names. Either order works mechanically; post-B1 placement avoids re-touching the same JSDoc blocks.
Blocks: Nothing directly
Related: Task B1 (the rename — see soft dependency above); Task A1 (the module docblock added there can cross-link to the Pagination anchor); Issue #170 (the deferred async-iterator helper that this task's "deferred enhancement" note must cite)

Roadmap dependency row: P8-ROADMAP §Phase A Task A4: "Effort: Small (~1.5 hours, mostly mechanical). Risk: None. Dependencies: Logically depends on Task B1 (the rename) for final method names, but the JSDoc bodies can be written first against the old names and renamed in lockstep with B1. Recommended to run A4 after B1 to avoid double-touching the same methods."

References

# Document What It Provides
1 P8-contribution-goal-and-definition.md Phase 8 goal, scope, acceptance criteria, locked decisions
2 P8-implementation-guide.md §5.2 Authoritative execution-level guide (this task)
3 P8-ROADMAP.md §Phase A Task A4 Task ordering, dependencies, acceptance gate
4 Issue #167 Authoritative "why" for this task
5 Issue #170 Deferred async-iterator enhancement — cited in the anchor
6 OGC API Common §Pagination Spec authority for the contract
7 src/ogc-api/csapi/url_builder.ts Receives the anchor + per-method @remarks blocks

Original Finding (preserved from pre-Phase-8 issue body)

Below is the original investigation/assessment that motivated this Phase 8 task. Preserved verbatim — Phase 8 absorbs this issue in place rather than creating a wrapper.

📌 Status Update — 2026-04-28 (Phase 8 Triage)

This issue's diagnosis and proposed fix have been revised after deep evaluation. The original framing identified a real symptom (consumers receiving fewer results than expected on servers with low default page sizes) but misidentified the cause and recommended a fix that conflicts with both OGC spec philosophy and upstream precedent calibration.

Corrected diagnosis:

  1. The library is behaving correctly per OGC 23-001 §7.6. When limit is omitted, the server's default applies. This is spec-conformant. The cited "data loss" only occurs when a consumer fails to follow the next HATEOAS link — i.e., consumes only the first page of a paginated response. That is a consumer-side pagination defect, not a library defect.
  2. The cited consumer (find_by_uid() in OSHConnect-Python) implements single-page lookup and does not follow next links. Hardcoding &limit=1000 masks that defect; setting DEFAULT_LIMIT=100 in this library would only move the cutoff, not fix it. Any tenant with >100 systems would still fail.
  3. However, there is a real library-side gap: our public list methods do not document the pagination contract in their JSDoc. A consumer reading our API has no signal that single-call list methods may return partial results, or that they should follow the next link to retrieve subsequent pages. That is a documentation completeness issue we own.
  4. Upstream precedent calibration: Upstream getCollectionItems() (src/ogc-api/endpoint.ts:512) does set a client-side default of limit = 10 at the convenience-method layer (not the URL-builder layer). The lower-level getCollectionItemsUrl() keeps limit? optional with no default — same pattern as our buildQueryString(). So whether we also impose a client-side default on our public list methods is a separate, deferrable architectural decision; if we do, the upstream-aligned value is 10, not 100. This is recorded as an open question below — not part of the accepted scope.

Revised severity: P3-Suggestion (was P2-Important). The symptom requires three conditions to manifest: (a) caller omits limit, (b) caller does not follow next links, (c) server default page size is smaller than the result set. A spec-conformant consumer is unaffected. Compare to #166, where the bug silently corrupts parsed objects regardless of consumer behavior — that's genuine P1. This is "consumer that doesn't paginate has a problem," which is a different category.

Revised recommendation: Documentation-only fix — add JSDoc on every public list method explicitly stating the pagination contract (single page returned; consumers must follow next links to retrieve subsequent pages). No behavior change. No new code. ~30 lines of JSDoc additions across the affected methods.

Future enhancement (not in this issue's scope): An async-iterator helper (async function* iterateSystems()) that auto-follows next links would be the genuine ergonomic solution to the underlying user-experience problem. Upstream ogc-client does not provide this pattern (we audited — no async *, AsyncIterable, getAllItems, iterate, fetchAll matches anywhere in src/), so adding it would be a scope-broadening contribution. Per our governance model, that's appropriate to tee up in our queue and revisit only if upstream broadens scope. To be filed as a separate issue with explicit reasoning that (a) it has no upstream precedent, (b) we'd want upstream to lead, (c) we'd revisit if upstream takes the direction.

Out-of-scope follow-up (not our repo): The cited find_by_uid() defect lives in OS4CSAPI/OSHConnect-Python, not here. If that project wants to fix it, that's a separate conversation against that repo.

The original problem statement and proposed fixes are preserved below for audit. Items rendered with strikethrough are superseded by the corrected diagnosis above.

— see also: institutional-learning comment below for analysis of how this gap escaped 6 months of review.


Acceptance Criteria — Revised

  • Every public list method on CSAPIQueryBuilder (getSystems, getDataStreams, getObservations, getDeployments, getProcedures, getProperties, getControlStreams, getCommands, all get*History, get*Subsystems, get*Observations, etc.) has a JSDoc paragraph explicitly stating:
    • The method returns a URL for a single page of results.
    • The server applies its own default page size when limit is omitted (varies by implementation).
    • Consumers needing the full result set must follow the next HATEOAS link from the response.
  • One short note added to the CSAPI module-level documentation (e.g., src/ogc-api/csapi/index.ts or equivalent file we own) summarizing the pagination contract and pointing at the link-walking pattern used in our integration tests (e.g., src/ogc-api/csapi/integration/observation.spec.ts:253-267).
  • QueryOptions.limit JSDoc is expanded to clarify: "Optional. When omitted, the server applies its own default page size, which varies by implementation. To retrieve all results, follow the next link in the response."
  • No code/behavior changes. No public-API signature changes.
  • All modified files pass npx prettier --check. No lint or typecheck regressions.
  • Existing tests still pass.

Files to Modify — Revised

File Action Est. Lines Purpose
src/ogc-api/csapi/url_builder.ts JSDoc only ~30 Add pagination-contract paragraph to each public list method
src/ogc-api/csapi/model.ts JSDoc only ~3 Expand QueryOptions.limit doc
Module index (e.g., src/ogc-api/csapi/index.ts) JSDoc only ~10 Module-level pagination-contract note

Scope — What NOT to Touch

  • ❌ Do NOT modify buildQueryString() behavior. Do NOT add DEFAULT_LIMIT.
  • ❌ Do NOT modify validateLimit().
  • ❌ Do NOT add any new code, new methods, or new constants.
  • ❌ Do NOT modify the upstream-owned README.md.
  • ❌ Do NOT add auto-pagination / fetch-all logic — that's a separate scope-broadening concern, to be filed as a follow-up enhancement issue (not addressed here).

Open Question — Defer to Phase 8 Execution Plan

Should our public list methods (getSystems, etc.) also apply a client-side default limit at the convenience-method layer, mirroring upstream getCollectionItems(limit = 10)? Arguments either way:

  • For: Mirrors upstream pattern. Reduces likelihood of consumers tripping the silent-truncation symptom on servers with very small defaults.
  • Against: CSAPIQueryBuilder is closer to a URL-builder than a convenience class (its buildQueryString counterpart in upstream — getCollectionItemsUrl — has no default). The whole class returns URL strings, not data; pagination concerns arguably belong at a higher layer the consumer composes.

Decision: Out of scope for this issue. If accepted later, the upstream-aligned value is 10, not 100. To be revisited during Phase 8 execution planning if appropriate, or filed as a separate follow-up.


Original Issue (Below — for audit; superseded items struck through)

Finding

CSAPIQueryBuilder.buildQueryString() only appends the limit query parameter when explicitly provided by the caller — when omitted, the server applies its own default page size, which varies across implementations (SensorHub: 100, Go CSAPI: 10). Consumers calling any list method without { limit: N } silently receive truncated results on servers with low defaults.

Review Source: Live integration testing against a Go CSAPI server (PostGIS-backed, OGC-compliant) from the ogc-csapi-explorer project and OSHConnect-Python publisher fleet migration.
Severity: P2-ImportantP3-Suggestion (see status update)
Category: API DesignDocumentation
Ownership: Ours


Problem Statement

The OGC API — Connected Systems specification (OGC 23-001 §7.6) states that limit is an optional query parameter with a server-defined default. The spec does not mandate a minimum default — so any conformant server can return as few as 1 item per page if no limit is provided.

Our buildQueryString() method respects this literally: if the caller doesn't pass limit, no ?limit= appears in the URL, and the server picks its own default. This is technically correct but creates a practical interoperability trap — the library silently returns different result counts depending on which server it talks to, with no indication to the consumer that results were truncated.

Corrected (2026-04-28): This is technically correct and spec-conformant. The "interoperability trap" is real but is a documentation gap on our side (consumers aren't told to follow next links) plus a pagination defect in the cited consumer. The library is doing the right thing; our JSDoc just doesn't tell consumers what the right thing requires of them.

Affected code — buildQueryString() (line ~393):

// src/ogc-api/csapi/url_builder.ts — buildQueryString()
} else if (key === 'limit') {
  validateLimit(value);
  params.append(wireName, String(value));
}
// When limit is NOT in options, no ?limit= is appended → server default applies

Scenario:

const builder = await createCSAPIBuilder(endpoint, collectionId);

// Consumer expects "all systems" — gets only 10 on Go server
const url = builder.getSystems();
// SensorHub:  → .../systems  (returns up to 100 — usually sufficient)
// Go CSAPI:   → .../systems  (returns only 10 — silently truncated)

// Consumer must know to do this, but nothing in the API signals it:
const safeUrl = builder.getSystems({ limit: 100 });

Corrected (2026-04-28): What the consumer must know is to follow the next link in the response (per OGC pagination contract), not to guess a high limit. Setting a high limit is a workaround that fails as soon as the result set exceeds it. The next-link pattern is exhibited in our own integration tests at src/ogc-api/csapi/integration/observation.spec.ts:253-267.

Impact: Any consumer that calls a list method without an explicit limit will silently receive truncated results when connected to a server whose default page size is smaller than the total resource count. This was the root cause of the find_by_uid() failures in the OSHConnect-Python publisher fleet — systems, datastreams, and deployments were not found because the Go server's default limit=10 returned only the first page. The workaround was to hardcode &limit=1000 in the Python bootstrap helpers.

Corrected (2026-04-28): Any consumer that (a) calls a list method without an explicit limit AND (b) does not follow next links receives only the first page. The find_by_uid() failure in OSHConnect-Python is a consumer-side pagination defect (single-page lookup, doesn't follow next); the hardcoded &limit=1000 workaround masks the consumer defect, it doesn't fix it. Tenants with >1000 systems would still fail. The fix for that consumer belongs in that consumer's repo, not ours.

All 39 public list methods that accept QueryOptions are affected, including:

  • getSystems(), getDataStreams(), getObservations()
  • getDeployments(), getProcedures(), getProperties()
  • getControlStreams(), getCommands()
  • All get*History(), get*Subsystems(), get*Observations(), etc.

Ownership Verification

$ git diff upstream/main phase-7 -- src/ogc-api/csapi/url_builder.ts | grep -A2 "key === 'limit'"
+      } else if (key === 'limit') {
+        validateLimit(value);
+        params.append(wireName, String(value));

All affected code is in our diff. The buildQueryString() method, CSAPIQueryBuilder class, QueryOptions interface, and validateLimit() helper were all authored as part of our CSAPI contribution. The upstream camptocamp/ogc-client has zero CSAPI code.

Conclusion: This code is ours.

Files to Modify — Original (Superseded)

File Action Est. Lines Purpose
src/ogc-api/csapi/url_builder.ts Modify ~~~10~~ Add configurable default limit with fallback constant
src/ogc-api/csapi/model.ts Modify ~~~5~~ Add optional defaultLimit to builder config or QueryOptions JSDoc
src/ogc-api/csapi/url_builder.spec.ts Modify ~~~20~~ Test that default limit is applied when limit is omitted

Superseded. See the revised "Files to Modify — Revised" table above.

Proposed Solutions — Original (Superseded)

Superseded by status update. Both Option A (constant DEFAULT_LIMIT = 100) and Option B (configurable via constructor) impose opinionated client-side behavior that contradicts OGC 23-001 §7.6's explicit delegation of default page size to the server. The chosen 100 is OSH-flavored — there is no principled reason to pick 100 over 10, 1000, or any other value. Even if we later decide to mirror upstream's getCollectionItems(limit = 10) pattern at our convenience layer (see "Open Question" above), neither original Option A nor Option B is the right shape for that decision. The original options are preserved below struck through for audit.

Option A: Client-side default constant (Recommended)

Add a DEFAULT_LIMIT constant to url_builder.ts and apply it in buildQueryString() when no explicit limit is provided:

/** Default page size applied when the caller omits `limit`. */
const DEFAULT_LIMIT = 100;

private buildQueryString(options?: QueryOptions): string {
  // Apply default limit if caller didn't specify one
  const effectiveOptions = options?.limit !== undefined
    ? options
    : { ...options, limit: DEFAULT_LIMIT };

  // ... rest of existing logic, now always includes limit
}

Pros: Minimal diff; consistent behavior across all servers; matches SensorHub's default (100); no API surface change — callers can still override with { limit: N }
Cons: Adds a parameter the server may not expect (though limit is always a valid OGC parameter); opinionated default
Effort: Small | Risk: Low

Option B: Configurable default via constructor option

Add an optional defaultLimit property to the CSAPIQueryBuilder constructor config:

interface CSAPIQueryBuilderOptions {
  // ... existing fields ...
  /** Default limit applied to all list requests. Set to undefined to use server default. */
  defaultLimit?: number;
}

// In buildQueryString():
if (!options?.limit && this.defaultLimit_ !== undefined) {
  params.append('limit', String(this.defaultLimit_));
}

Pros: Per-instance configurable; consumers can opt out by passing undefined; works well when the same builder talks to different servers
Cons: Larger diff; requires factory function update; adds constructor complexity
Effort: Medium | Risk: Low

Scope — Original (Superseded)

Superseded. See "Scope — What NOT to Touch" in the revised section above.

Acceptance Criteria — Original (Superseded)

Superseded. See "Acceptance Criteria — Revised" above.

Dependencies

Blocked by: Nothing
Blocks: Nothing
Related: #166 (Part 2 parsers @link fallback — same Go server interop effort); #110 (@link resolution utilities — higher-level pagination concern)


Operational Constraints

⚠️ MANDATORY: Before starting work on this issue, review docs/governance/AI_OPERATIONAL_CONSTRAINTS.md.

Key constraints:

  • Precedence: OGC specifications → AI Collaboration Agreement → This issue description → Existing code → Conversational context
  • No scope expansion: Fix the finding, nothing more
  • Minimal diffs: Prefer the smallest change that satisfies the acceptance criteria
  • Ask when unclear: If intent is ambiguous, stop and ask for clarification
  • Respect ownership: This code is ours — fix on the current working branch

Ownership-Specific Constraints

If Ours:

  • Fix on the current working branch (phase-7)
  • Include in the next commit to clean-pr if the PR is still open
  • Add tests that cover the finding

References

# Document What It Provides
1 src/ogc-api/csapi/url_builder.ts L376-410 buildQueryString() — code referenced (no behavior change in revised scope)
2 src/ogc-api/csapi/model.ts L140-175 QueryOptions interface with optional limit — JSDoc to be expanded
3 src/ogc-api/csapi/helpers.ts L214-220 validateLimit() — existing validation (not modified)
4 OGC 23-001 §7.6 — Limit parameter Spec says limit is optional, server picks default — supports the corrected diagnosis
5 src/ogc-api/endpoint.ts L512 (upstream) Upstream precedent: getCollectionItems(limit = 10) — calibration for the open question
6 src/ogc-api/csapi/integration/observation.spec.ts L253-267 Example of correct next-link-walking pattern in our own tests
7 OSHConnect-Python bootstrap_helpers.py Real-world consumer with hardcoded &limit=1000 workaround — consumer-side defect, not ours
8 connected-systems-go#5 Go server ignores ?uid= — related but separate filtering gap

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions