Skip to content

[DEFERRED] URL builder: command/observation CRUD methods require top-level endpoints, fail on nested-only servers #102

@Sam-Bolling

Description

@Sam-Bolling

Summary

Several CSAPIQueryBuilder methods for operating on individual commands and observations call assertResourceAvailable('commands') or assertResourceAvailable('observations'), requiring these resource types to be exposed as top-level endpoints in the API root document. Servers that only expose commands/observations as nested sub-resources under their parents (control streams / datastreams) cannot use these methods — they throw EndpointError.

This is an asymmetry: the create methods correctly accept a parent ID and use the nested path pattern, but the read/update/delete/sub-resource methods assume top-level access.

Affected Methods

Commands (8 methods — all top-level only)

Method Line Asserts Builds
getCommands(options?) L2074 commands /commands?...
getCommand(id, options?) L2095 commands /commands/{id}
updateCommand(id) L2202 commands /commands/{id}
deleteCommand(id) L2222 commands /commands/{id}
getCommandStatus(id) L2245 commands /commands/{id}/status
updateCommandStatus(id) L2277 commands /commands/{id}/status
getCommandResult(id) L2300 commands /commands/{id}/result
cancelCommand(id) L2326 commands /commands/{id}/cancel

Commands (2 methods — nested-aware ✅)

Method Line Asserts Builds
createCommand(controlStreamId) L2142 controlStreams /controlStreams/{csId}/commands
createCommands(controlStreamId) L2173 controlStreams /controlStreams/{csId}/commands

Observations (8 methods — all top-level only)

Method Line Asserts Builds
getObservations(options?) L1685 observations /observations?...
getObservation(id, options?) L1706 observations /observations/{id}
updateObservation(id) L1735 observations /observations/{id}
deleteObservation(id) L1755 observations /observations/{id}
getObservationDatastream(id) L1778 observations /observations/{id}/datastream
getObservationSamplingFeature(id, options?) L1802 observations /observations/{id}/samplingFeature
getObservationSystem(id, options?) L1826 observations /observations/{id}/system
getObservationHistory(id, options?) L1847 observations /observations/{id}/history

Observations (1 method — nested-aware ✅)

Method Line Asserts Builds
createObservation(datastreamId) L1594 datastreams /datastreams/{dsId}/observations

Real-World Impact

OSH SensorHub (http://45.55.99.236:8080/sensorhub/api) exposes commands and observations only as nested sub-resources:

  • /controlstreams/{csId}/commands (collection)
  • /controlstreams/{csId}/commands/{cmdId} (individual)
  • /controlstreams/{csId}/commands/{cmdId}/status (status)
  • /commands/{cmdId}400 Bad Request (Invalid resource name: 'commands')
  • /commands/{cmdId}/status400 Bad Request

The same applies to observations:

  • /datastreams/{dsId}/observations (collection)
  • /observations/{obsId}400 Bad Request (Invalid resource name: 'observations')

Calling builder.getCommandStatus('cmd-001') or builder.getObservation('obs-001') throws immediately because assertResourceAvailable() fails — these resource types were never discovered as top-level links.

Discovery Context

This was found while implementing ogc-csapi-explorer#32 (CommandStatus history panel). The workaround in the explorer demo extracts controlstream@id from the raw command JSON and manually constructs the nested path:

// Workaround in csapi-bridge.ts
export function getCommandStatusUrl(commandId: string, controlStreamId?: string | null): string | null {
  if (controlStreamId) {
    return `/controlstreams/${controlStreamId}/commands/${commandId}/status`
  }
  // Top-level fallback (servers that expose commands at root)
  const b = builder.value
  if (!b) return `/commands/${commandId}/status`
  try {
    return b.getCommandStatus(commandId)
  } catch {
    return `/commands/${commandId}/status`
  }
}

Proposed Fix

Add optional parent ID parameters to the affected methods so callers can build nested paths when the top-level endpoint isn't available. The existing buildResourceUrl() private method already supports the pattern — it's used by createCommand(controlStreamId).

Option A: Optional parent parameter overloads

// Current (top-level only):
getCommandStatus(id: string): string {
  this.assertResourceAvailable('commands');
  return this.buildResourceUrl('commands', id, 'status');
}

// Proposed (with nested fallback):
getCommandStatus(id: string, controlStreamId?: string): string {
  if (controlStreamId) {
    this.assertResourceAvailable('controlStreams');
    return this.buildResourceUrl('controlStreams', controlStreamId, `commands/${id}/status`);
  }
  this.assertResourceAvailable('commands');
  return this.buildResourceUrl('commands', id, 'status');
}

Option B: Separate nested methods

getNestedCommandStatus(controlStreamId: string, commandId: string): string {
  this.assertResourceAvailable('controlStreams');
  return this.buildResourceUrl('controlStreams', controlStreamId, `commands/${commandId}/status`);
}

Option A is preferred — it's backward-compatible and follows the existing pattern where callers provide context when they have it.

Full list of methods needing the parent parameter

Commands (parent: controlStreamId):

  • getCommand(id, options?) → add optional controlStreamId
  • updateCommand(id) → add optional controlStreamId
  • deleteCommand(id) → add optional controlStreamId
  • getCommandStatus(id) → add optional controlStreamId
  • updateCommandStatus(id) → add optional controlStreamId
  • getCommandResult(id) → add optional controlStreamId
  • cancelCommand(id) → add optional controlStreamId

Observations (parent: datastreamId):

  • getObservation(id, options?) → add optional datastreamId
  • updateObservation(id) → add optional datastreamId
  • deleteObservation(id) → add optional datastreamId
  • getObservationDatastream(id) → add optional datastreamId
  • getObservationSamplingFeature(id, options?) → add optional datastreamId
  • getObservationSystem(id, options?) → add optional datastreamId
  • getObservationHistory(id, options?) → add optional datastreamId

OGC Spec Reference

OGC 23-002 (Connected Systems Part 2) defines both access patterns:

  • §7.5 — Observations accessible via /datastreams/{id}/observations (nested) AND optionally /observations (top-level)
  • §7.9 — Commands accessible via /controlstreams/{id}/commands (nested) AND optionally /commands (top-level)

The top-level endpoints are optional per spec — servers may only implement the nested pattern. The URL builder should support both.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions