Finding
DeploymentRepository.applyFilters() explicitly excludes subdeployments from top-level GET /deployments responses with WHERE parent_deployment_id IS NULL OR parent_deployment_id = '', making subdeployments undiscoverable without prior knowledge of the parent deployment ID.
Review Source: Live integration testing against the Go CSAPI server during OSHConnect-Python publisher fleet migration to the Go server.
Severity: P2-Important
Category: API Design
Ownership: Upstream (SomethingCreativeStudios/connected-systems-go)
Problem Statement
When a client requests GET /deployments, the repository layer explicitly filters to only top-level deployments (parent_deployment_id IS NULL). Subdeployments created via POST /deployments/{parentId}/subdeployments are completely absent from the top-level listing. The only way to discover them is to already know the parent deployment ID and query /deployments/{parentId}/subdeployments.
This makes it impossible for a client to enumerate all deployments in a collection without first fetching every top-level deployment and recursively querying each for subdeployments.
Affected code — internal/repository/deployment_repository.go — applyFilters():
func (r *DeploymentRepository) applyFilters(query *gorm.DB, params *queryparams.DeploymentsQueryParams, parentId *string) *gorm.DB {
// ... other filters ...
if parentId != nil {
if params.Recursive {
// Recursive traversal from parent to include all descendants.
query = query.Where(`id IN (
WITH RECURSIVE deployment_descendants AS (
SELECT id FROM deployments WHERE parent_deployment_id = ?
UNION ALL
SELECT d.id FROM deployments d
JOIN deployment_descendants dd ON d.parent_deployment_id = dd.id
)
SELECT id FROM deployment_descendants
)`, *parentId)
} else {
// Direct children only
query = query.Where("parent_deployment_id = ?", *parentId)
}
} else {
if params.Recursive {
// Canonical recursive search should include top-level deployments and all descendants.
// ← Currently a no-op — does NOT actually include descendants
} else {
// ← THIS LINE: explicitly excludes all subdeployments from top-level listing
query = query.Where("parent_deployment_id IS NULL OR parent_deployment_id = ''")
}
}
return query
}
Affected code — internal/api/deployment_handler.go — ListDeployments():
func (h *DeploymentHandler) ListDeployments(w http.ResponseWriter, r *http.Request) {
params := queryparams.DeploymentsQueryParams{}.BuildFromRequest(r)
deployments, total, err := h.repo.List(params, nil) // parentId = nil → top-level only
// ...
}
Scenario:
# Create a top-level deployment
POST /deployments
{ "type": "Feature", "properties": { "uid": "urn:test:dep:parent", "name": "Parent" } }
# → 201, Location: /deployments/aaa-111
# Create a subdeployment
POST /deployments/aaa-111/subdeployments
{ "type": "Feature", "properties": { "uid": "urn:test:dep:child", "name": "Child" } }
# → 201, Location: /deployments/bbb-222
# List all deployments
GET /deployments
# Expected: both "Parent" and "Child" returned
# Actual: only "Parent" returned — "Child" is invisible
# Also note: the `recursive=true` path for parentId==nil is a no-op:
GET /deployments?recursive=true
# Expected: all deployments (top-level + descendants)
# Actual: same as without recursive — the `if params.Recursive` branch does nothing
Impact: The OSHConnect-Python bootstrap helpers use find_by_uid() against /deployments to check if a subdeployment already exists before creating it. Because subdeployments are not in the top-level listing, the lookup always returned None, causing duplicate creation attempts that failed on the unique UID constraint. The workaround adds special-case logic to search under the parent:
# Workaround in bootstrap_helpers.py
def ensure_deployment(session, base_url, collection_id, deployment, parent_id=None):
existing = find_by_uid(session, deps_url, deployment["properties"]["uid"])
if existing:
return existing["id"]
# Subdeployment not in top-level listing — try under parent
if parent_id:
sub_url = f"{deps_url}/{parent_id}/subdeployments"
existing = find_by_uid(session, sub_url, deployment["properties"]["uid"])
if existing:
return existing["id"]
# ... create it
Additionally, the recursive=true code path for top-level listings is a no-op comment with no actual implementation:
if params.Recursive {
// Canonical recursive search should include top-level deployments and all descendants.
// ← This is just a comment — no filter is applied, so it returns same as non-recursive
}
Ownership Verification
This code is pre-existing in the upstream SomethingCreativeStudios/connected-systems-go repository. The OS4CSAPI fork is currently synced with upstream (main branch is up to date).
Conclusion: This code is upstream.
Files to Modify
| File |
Action |
Est. Lines |
Purpose |
internal/repository/deployment_repository.go |
Modify |
~15 |
Change default listing to include subdeployments (flat view), or implement recursive=true at top level |
e2e/ |
Modify |
~25 |
Add E2E test verifying subdeployments appear in top-level listing |
Proposed Solutions
Option A: Flat listing — include all deployments at top level (Recommended)
Remove the parent_deployment_id IS NULL filter from the default (non-recursive, no-parent) path. All deployments appear in GET /deployments regardless of nesting depth. Each subdeployment carries its parent_deployment_id so clients can reconstruct the hierarchy if needed.
} else {
if params.Recursive {
// No filter needed — return everything
} else {
// REMOVED: query = query.Where("parent_deployment_id IS NULL OR parent_deployment_id = ''")
// Now returns all deployments, matching SensorHub behavior
}
}
Pros: Matches SensorHub behavior; simplest fix; clients can discover all deployments with a single request; consistent with how /systems returns all systems (including subsystems)
Cons: Changes existing behavior — clients relying on top-level-only might receive more results
Effort: Small | Risk: Low
Option B: Implement recursive=true at top level
Keep the default behavior (top-level only) but implement the currently-no-op recursive=true path:
} else {
if params.Recursive {
// Return all deployments (top-level + all descendants)
// No WHERE clause needed — just omit the parent filter
} else {
query = query.Where("parent_deployment_id IS NULL OR parent_deployment_id = ''")
}
}
Pros: Preserves existing default behavior; adds opt-in flat listing; the code comment already suggests this was intended
Cons: Clients must know to pass ?recursive=true; still breaks discovery for clients that don't know about the parameter
Effort: Small | Risk: None
Scope — What NOT to Touch
- ❌ Do NOT modify files outside the "Files to Modify" table above
- ❌ Do NOT refactor adjacent code that isn't part of this finding
- ❌ Do NOT change the
ListSubdeployments() handler behavior — it correctly lists children of a specific parent
- ❌ Do NOT change the
AddSubdeployment() creation flow
- ❌ Do NOT modify the closure table logic in
findAllChildren()
Acceptance Criteria
Dependencies
Blocked by: Nothing
Blocks: Nothing
Related: #7 — ?uid= filter ignored (compounds this problem — can't filter subdeployments by UID even if listed); #9 — Default limit of 10 (expanded listing increases the importance of reasonable pagination defaults)
Operational Constraints
⚠️ MANDATORY: Before starting work on this issue, review docs/governance/AI_OPERATIONAL_CONSTRAINTS.md if available.
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 upstream — coordinate with the maintainer if contributing back
Ownership-Specific Constraints
If Upstream:
- Track the issue for potential future contribution or discussion with the maintainer
- If the fix is trivial and clearly beneficial, note in the issue that it could be offered as a separate upstream PR
References
| # |
Document |
What It Provides |
| 1 |
internal/repository/deployment_repository.go — applyFilters() |
Root cause — WHERE parent_deployment_id IS NULL excludes subdeployments |
| 2 |
internal/api/deployment_handler.go — ListDeployments() |
Handler passes nil parentId → triggers top-level-only filter |
| 3 |
OGC 23-001 §8.5 — Deployment resources |
Spec defines deployment listing endpoint |
| 4 |
OSHConnect-Python publishers/bootstrap_helpers.py — ensure_deployment() |
Real-world workaround demonstrating impact |
| 5 |
SensorHub API behavior |
Comparison — SensorHub returns all deployments (flat) at /deployments |
Finding
DeploymentRepository.applyFilters()explicitly excludes subdeployments from top-levelGET /deploymentsresponses withWHERE parent_deployment_id IS NULL OR parent_deployment_id = '', making subdeployments undiscoverable without prior knowledge of the parent deployment ID.Review Source: Live integration testing against the Go CSAPI server during OSHConnect-Python publisher fleet migration to the Go server.
Severity: P2-Important
Category: API Design
Ownership: Upstream (SomethingCreativeStudios/connected-systems-go)
Problem Statement
When a client requests
GET /deployments, the repository layer explicitly filters to only top-level deployments (parent_deployment_id IS NULL). Subdeployments created viaPOST /deployments/{parentId}/subdeploymentsare completely absent from the top-level listing. The only way to discover them is to already know the parent deployment ID and query/deployments/{parentId}/subdeployments.This makes it impossible for a client to enumerate all deployments in a collection without first fetching every top-level deployment and recursively querying each for subdeployments.
Affected code —
internal/repository/deployment_repository.go—applyFilters():Affected code —
internal/api/deployment_handler.go—ListDeployments():Scenario:
Impact: The OSHConnect-Python bootstrap helpers use
find_by_uid()against/deploymentsto check if a subdeployment already exists before creating it. Because subdeployments are not in the top-level listing, the lookup always returnedNone, causing duplicate creation attempts that failed on the unique UID constraint. The workaround adds special-case logic to search under the parent:Additionally, the
recursive=truecode path for top-level listings is a no-op comment with no actual implementation:Ownership Verification
This code is pre-existing in the upstream
SomethingCreativeStudios/connected-systems-gorepository. TheOS4CSAPIfork is currently synced with upstream (mainbranch is up to date).Conclusion: This code is upstream.
Files to Modify
internal/repository/deployment_repository.gorecursive=trueat top levele2e/Proposed Solutions
Option A: Flat listing — include all deployments at top level (Recommended)
Remove the
parent_deployment_id IS NULLfilter from the default (non-recursive, no-parent) path. All deployments appear inGET /deploymentsregardless of nesting depth. Each subdeployment carries itsparent_deployment_idso clients can reconstruct the hierarchy if needed.Pros: Matches SensorHub behavior; simplest fix; clients can discover all deployments with a single request; consistent with how
/systemsreturns all systems (including subsystems)Cons: Changes existing behavior — clients relying on top-level-only might receive more results
Effort: Small | Risk: Low
Option B: Implement
recursive=trueat top levelKeep the default behavior (top-level only) but implement the currently-no-op
recursive=truepath:Pros: Preserves existing default behavior; adds opt-in flat listing; the code comment already suggests this was intended
Cons: Clients must know to pass
?recursive=true; still breaks discovery for clients that don't know about the parameterEffort: Small | Risk: None
Scope — What NOT to Touch
ListSubdeployments()handler behavior — it correctly lists children of a specific parentAddSubdeployment()creation flowfindAllChildren()Acceptance Criteria
GET /deploymentsreturns both top-level deployments and subdeployments (Option A), orGET /deployments?recursive=truereturns all deployments (Option B)parent_deployment_idin the response so clients can reconstruct hierarchyGET /deployments/{id}/subdeploymentsstill works correctly (direct children of a parent)limit,offset) works correctly with the expanded result setmake test)Dependencies
Blocked by: Nothing
Blocks: Nothing
Related: #7 —
?uid=filter ignored (compounds this problem — can't filter subdeployments by UID even if listed); #9 — Default limit of 10 (expanded listing increases the importance of reasonable pagination defaults)Operational Constraints
Key constraints:
Ownership-Specific Constraints
If Upstream:
References
internal/repository/deployment_repository.go—applyFilters()WHERE parent_deployment_id IS NULLexcludes subdeploymentsinternal/api/deployment_handler.go—ListDeployments()nilparentId → triggers top-level-only filterpublishers/bootstrap_helpers.py—ensure_deployment()/deployments