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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions