Skip to content

refactor(custom_fields): per-key endpoints, drop from PUT /devices/:uid#6286

Merged
gustavosbarreto merged 4 commits into
masterfrom
refactor/custom-fields-per-key-endpoints
May 8, 2026
Merged

refactor(custom_fields): per-key endpoints, drop from PUT /devices/:uid#6286
gustavosbarreto merged 4 commits into
masterfrom
refactor/custom-fields-per-key-endpoints

Conversation

@gustavosbarreto
Copy link
Copy Markdown
Member

Summary

Move custom_fields off PUT /devices/:uid and into dedicated per-key endpoints, mirroring the tag attach/detach pattern.

PR #6248 added custom_fields as a *map[string]string field on DeviceUpdate, sharing the body with rename. That forced three smells:

  • Tri-state pointer (nil = no change, {} = clear, {k:v} = replace) instead of explicit operations.
  • Bulk replace requires the client to know the current map, opening a race when two tabs edit different keys at the same time.
  • The validator order on Name (device_name,omitempty) made any payload that omitted name fail with 400. The UI worked around it by sending name: "", which silently reverted external renames.

After this change:

  • PUT /devices/:uid/custom_fields/:key with {value} and DELETE /devices/:uid/custom_fields/:key are atomic per-key. Storage stays as the same JSONB column, so GET /devices and the custom_fields contains filter are unchanged.
  • PUT /devices/:uid is rename-only. Returns early when name is empty or unchanged. The validator order is corrected.
  • New DeviceCustomFieldUpdate permission gates the new endpoints (Operator and up).
  • The 20-entries-per-device cap moves into the service layer, enforced when adding a new key.

UI now calls useSetDeviceCustomField / useDeleteDeviceCustomField instead of replaying the full map.

Decisions

  • Per-key over bulk PATCH/PUT: matches the tag attach/detach pattern already in the repo, keeps UI calls free of full-state knowledge.
  • PUT (not POST) for set: idempotent set/upsert of the resource at the URL.
  • Mongo store implements the new methods symmetrically; nothing else in the mongo path evolves.
  • No data migration: devices.custom_fields JSONB column is untouched; only the write paths change.
  • Filter capability is unchanged on this PR. custom_fields contains keeps working over all values; per-key filtering and discovery are out of scope.

@gustavosbarreto gustavosbarreto requested review from a team as code owners May 7, 2026 22:03
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Claude finished @gustavosbarreto's task in 2m 42s —— View job


Code Review Complete

Reviewed 21 files across code quality, security, testing, Go/TypeScript patterns, and architecture. 2 inline issues posted (2 high).

Additional notes

  • Missing tests: The PR removes 133 lines of test coverage for custom_fields via UpdateDevice but adds zero tests for the new SetDeviceCustomField and DeleteDeviceCustomField service methods. The limit-enforcement logic (max 20 fields, with the existing-key bypass) especially warrants coverage.

  • Cloud mock regeneration: The new DeviceSetCustomField and DeviceDeleteCustomField methods on the community DeviceStore interface will require regenerating the cloud repo's MockStore (via mockery) before cloud tests compile. This is expected — just coordinate it as part of the merge process.


To request another review round, comment /review.

@gustavosbarreto gustavosbarreto force-pushed the refactor/custom-fields-per-key-endpoints branch from bf5bde1 to bcd6952 Compare May 7, 2026 22:13
Comment thread api/services/device.go
Comment thread pkg/api/requests/device.go
@gustavosbarreto gustavosbarreto force-pushed the refactor/custom-fields-per-key-endpoints branch from bcd6952 to cd93243 Compare May 7, 2026 22:16
Replace the single CustomFields field on DeviceUpdate with two dedicated
requests, DeviceSetCustomField and DeviceDeleteCustomField, each parameterized
by the field key. Add DeviceCustomFieldUpdate permission so role gates can
distinguish custom_fields edits from device rename. Fix the validator order
on DeviceUpdate.Name so omitempty short-circuits on empty input.
@gustavosbarreto gustavosbarreto force-pushed the refactor/custom-fields-per-key-endpoints branch from cd93243 to da156c4 Compare May 8, 2026 02:32
@gustavosbarreto
Copy link
Copy Markdown
Member Author

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Code Review

  • Gathered PR context
  • Reviewing with 5 specialized agents
  • Posting feedback

Adds PUT /devices/:uid/custom_fields/:key and DELETE
/devices/:uid/custom_fields/:key for atomic per-key edits. Service enforces
the per-device entry cap (defined by maxCustomFieldsPerDevice) when adding a
new key. PG store uses jsonb_set / jsonb subtraction; Mongo store uses
dot-notation $set / $unset. Both are atomic, removing the read-modify-write
race that the previous bulk replace had.

UpdateDevice now handles only rename, returning early when the name is empty
or unchanged. The DeviceConflicts and DeviceUpdate calls only run when the
name actually changes. Drops the CustomFields branch.

Adds ErrDeviceCustomFieldLimitReached for the cap.
Adds /api/devices/{uid}/custom_fields/{key} with PUT and DELETE. PUT requires
{value} body; both responses are 200 only. Removes custom_fields and the
required:[name] from PUT /api/devices/{uid}, which is now strictly a rename.
Replaces useUpdateDeviceCustomFields with useSetDeviceCustomField and
useDeleteDeviceCustomField. CustomFieldsSection now PUTs a single key on
add and DELETEs a single key on remove, dropping the bulk map replace
that forced sending name in the body. Gates the section on the new
device:customField:update permission.
@gustavosbarreto gustavosbarreto force-pushed the refactor/custom-fields-per-key-endpoints branch from da156c4 to 3c5e1ed Compare May 8, 2026 02:38
@gustavosbarreto
Copy link
Copy Markdown
Member Author

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Claude finished @gustavosbarreto's task in 10m 1s —— View job


Code Review Complete

Reviewed 21 files across code quality, security, testing, Go/TypeScript patterns, and architecture — no issues found. The code looks good as-is.

Notes from this round

  • Previous findings addressed: The cross-tenant scoping issue from round 1 is correctly fixed — both SetDeviceCustomField and DeleteDeviceCustomField now pass InNamespace(req.TenantID) to DeviceResolve, matching the pattern used by UpdateDevice and RenameDevice.
  • Tests added: TestSetDeviceCustomField (5 cases) and TestDeleteDeviceCustomField (3 cases) cover resolve failures, limit enforcement, store errors, and success paths. The limit-bypass-for-existing-key logic is explicitly tested.
  • Cross-repo impact: Cloud store types embed the community Store concrete type, so the two new DeviceStore methods are promoted automatically. Only cloud/pkg/store/mock_store.go needs regeneration (expected — coordinate as part of merge).

To request another review round, comment /review.

@gustavosbarreto
Copy link
Copy Markdown
Member Author

Cloud mock regen tracked in shellhub-io/cloud#2360.

@gustavosbarreto gustavosbarreto merged commit ff35f4f into master May 8, 2026
12 checks passed
@gustavosbarreto gustavosbarreto deleted the refactor/custom-fields-per-key-endpoints branch May 8, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant