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}/status → 400 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.
Summary
Several
CSAPIQueryBuildermethods for operating on individual commands and observations callassertResourceAvailable('commands')orassertResourceAvailable('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 throwEndpointError.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)
getCommands(options?)commands/commands?...getCommand(id, options?)commands/commands/{id}updateCommand(id)commands/commands/{id}deleteCommand(id)commands/commands/{id}getCommandStatus(id)commands/commands/{id}/statusupdateCommandStatus(id)commands/commands/{id}/statusgetCommandResult(id)commands/commands/{id}/resultcancelCommand(id)commands/commands/{id}/cancelCommands (2 methods — nested-aware ✅)
createCommand(controlStreamId)controlStreams/controlStreams/{csId}/commandscreateCommands(controlStreamId)controlStreams/controlStreams/{csId}/commandsObservations (8 methods — all top-level only)
getObservations(options?)observations/observations?...getObservation(id, options?)observations/observations/{id}updateObservation(id)observations/observations/{id}deleteObservation(id)observations/observations/{id}getObservationDatastream(id)observations/observations/{id}/datastreamgetObservationSamplingFeature(id, options?)observations/observations/{id}/samplingFeaturegetObservationSystem(id, options?)observations/observations/{id}/systemgetObservationHistory(id, options?)observations/observations/{id}/historyObservations (1 method — nested-aware ✅)
createObservation(datastreamId)datastreams/datastreams/{dsId}/observationsReal-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}/status→ 400 Bad RequestThe same applies to observations:
/datastreams/{dsId}/observations(collection)/observations/{obsId}→ 400 Bad Request (Invalid resource name: 'observations')Calling
builder.getCommandStatus('cmd-001')orbuilder.getObservation('obs-001')throws immediately becauseassertResourceAvailable()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@idfrom the raw command JSON and manually constructs the nested path: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 bycreateCommand(controlStreamId).Option A: Optional parent parameter overloads
Option B: Separate nested methods
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 optionalcontrolStreamIdupdateCommand(id)→ add optionalcontrolStreamIddeleteCommand(id)→ add optionalcontrolStreamIdgetCommandStatus(id)→ add optionalcontrolStreamIdupdateCommandStatus(id)→ add optionalcontrolStreamIdgetCommandResult(id)→ add optionalcontrolStreamIdcancelCommand(id)→ add optionalcontrolStreamIdObservations (parent:
datastreamId):getObservation(id, options?)→ add optionaldatastreamIdupdateObservation(id)→ add optionaldatastreamIddeleteObservation(id)→ add optionaldatastreamIdgetObservationDatastream(id)→ add optionaldatastreamIdgetObservationSamplingFeature(id, options?)→ add optionaldatastreamIdgetObservationSystem(id, options?)→ add optionaldatastreamIdgetObservationHistory(id, options?)→ add optionaldatastreamIdOGC Spec Reference
OGC 23-002 (Connected Systems Part 2) defines both access patterns:
/datastreams/{id}/observations(nested) AND optionally/observations(top-level)/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.