Skip to content

Conversation

@reactorcoremeltdown
Copy link

@reactorcoremeltdown reactorcoremeltdown commented Dec 18, 2025

What type of PR is this?

  • feature

What this PR does / why we need it:

This PR allows patching the parentID field of a given item. I want to create an API client for my warehouse management (a little frontend based on shortened item URLs) that would help me to "attach" or "relocate" items when changing their current residence, i.e. to replace a malfunctioned disk in my NAS I'd grab a new one from my storage box in a basement, scan it's label to open an app, then using the app I'd scan the label of the NAS to assign it as a parent for the new disk, and then grab the old disk, scan it's label, then scan the label of a quarantine box to assign it as a parent of a malfunctioned disk.

Pretty much like I used to do in a data center.

Which issue(s) this PR fixes:

None I know of, and I tried to search.

Testing

Manual testing through Insomnium produced a positive result, as in the parentID of an item changed after making an authorized PATCH request to /api/v1/items/{id}.

Payload:

{
	"locationId": "7f8ee5fa-79c7-435c-b635-76a10fc6e926",
	"parentId": "171582ac-a94b-4b80-8097-f83d1a5b0411"
}

Result (truncated):

{
	"parent": {
		"id": "171582ac-a94b-4b80-8097-f83d1a5b0411",
		"assetId": "000-148",
		"name": "Wenter USB 3.0 HUB",
		"description": "",
		"quantity": 1,
		"insured": false,
		"archived": false,
		"createdAt": "2025-12-01T16:09:06.763597487Z",
		"updatedAt": "2025-12-01T16:09:06.76359913Z",
		"purchasePrice": 0,
		"labels": [],
		"soldTime": "0001-01-01T00:00:00Z"
	},

Summary by CodeRabbit

  • New Features
    • Added the capability to update item parent relationships during item modifications. Users can now reassign items to different parent items when performing updates, providing enhanced flexibility for organizing and managing item hierarchies within the system. This feature is available throughout the platform for seamless item management.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

This PR adds a ParentID field to the ItemPatch data structure, enabling the ability to update an item's parent during patch operations. The field is added to both backend (Go struct) and frontend (TypeScript interface) implementations.

Changes

Cohort / File(s) Summary
Backend ItemPatch Enhancement
backend/internal/data/repo/repo_items.go
Added ParentID field (uuid.UUID) to ItemPatch struct with JSON tag "parentID". Updated Patch method to include SetParentID call when ParentID is not nil.
Frontend Type Synchronization
frontend/lib/api/types/data-contracts.ts
Added optional parentID?: string | null field to ItemPatch interface.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

  • Both changes are straightforward field additions with no behavioral complexity
  • Highly homogeneous pattern applied consistently across backend and frontend
  • No algorithmic or control-flow logic to evaluate

Security recommendations

  • Parent validation: Ensure backend validation checks that the provided ParentID references an existing, valid item
  • Authorization: Verify that users can only set parent items they have permission to access or modify (prevent unauthorized hierarchy modifications)
  • Circular reference prevention: Implement guards to prevent users from creating circular parent-child relationships (e.g., an item becoming its own ancestor)
  • Audit trail: Consider logging parentID modifications for security and compliance purposes

Poem

A child finds its place,
In the family tree now grows,
Parent links take hold—
Hierarchies bloom once more. 🌳

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: allowing the parentID field to be patchable via the backend API.
Description check ✅ Passed The description includes all required sections: PR type (feature), detailed explanation of purpose with concrete use case, testing methodology with example payload and response.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1d3d9 and 3458ac8.

⛔ Files ignored due to path filters (11)
  • backend/app/api/static/docs/docs.go is excluded by !backend/app/api/static/docs/**
  • backend/app/api/static/docs/openapi-3.json is excluded by !backend/app/api/static/docs/**
  • backend/app/api/static/docs/openapi-3.yaml is excluded by !backend/app/api/static/docs/**
  • backend/app/api/static/docs/swagger.json is excluded by !backend/app/api/static/docs/**
  • backend/app/api/static/docs/swagger.yaml is excluded by !backend/app/api/static/docs/**
  • docs/en/api/openapi-2.0.json is excluded by !docs/en/api/**
  • docs/en/api/openapi-2.0.yaml is excluded by !docs/en/api/**
  • docs/en/api/openapi-3.0.json is excluded by !docs/en/api/**
  • docs/en/api/openapi-3.0.yaml is excluded by !docs/en/api/**
  • docs/en/api/swagger-2.0.json is excluded by !docs/en/api/**
  • docs/en/api/swagger-2.0.yaml is excluded by !docs/en/api/**
📒 Files selected for processing (2)
  • backend/internal/data/repo/repo_items.go (2 hunks)
  • frontend/lib/api/types/data-contracts.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,vue}

⚙️ CodeRabbit configuration file

**/*.{ts,vue}: Check for hardcoded strings in UI components that should be translatable.
Look for:

  • String literals in Vue components (e.g. Click me)
  • Alert messages, error messages, and user-facing text
  • Placeholder text and labels

Files:

  • frontend/lib/api/types/data-contracts.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: build (linux/arm64)
  • GitHub Check: build (linux/arm/v7)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: End-to-End Playwright Tests / E2E Playwright Testing 2/4
  • GitHub Check: End-to-End Playwright Tests / E2E Playwright Testing 4/4
  • GitHub Check: build (linux/arm/v7)
  • GitHub Check: build (linux/arm64)
  • GitHub Check: End-to-End Playwright Tests / E2E Playwright Testing 3/4
  • GitHub Check: Backend Server Tests / Go
  • GitHub Check: End-to-End Playwright Tests / E2E Playwright Testing 1/4
  • GitHub Check: Frontend Tests / Integration Tests
  • GitHub Check: Frontend Tests / Integration Tests PGSQL 15
  • GitHub Check: Frontend Tests / Integration Tests PGSQL 17
  • GitHub Check: Frontend Tests / Integration Tests PGSQL 16
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/arm64)
  • GitHub Check: build (linux/arm/v7)
🔇 Additional comments (2)
frontend/lib/api/types/data-contracts.ts (1)

673-673: LGTM! Type definition aligns with backend changes.

The optional and nullable parentID field is appropriate for patch semantics, allowing the frontend to send parent updates when needed.

Note: This file is auto-generated via swagger-typescript-api, so changes originate from backend API definitions.

backend/internal/data/repo/repo_items.go (1)

128-128: Struct field definition looks correct.

The ParentID field is properly typed as uuid.UUID with appropriate JSON tags and extensions. The definition aligns with the frontend interface.

Comment on lines +1004 to +1006
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Read the file to examine the Patch method and UpdateByGroup method
echo "=== Lines 1000-1010 (Patch method ParentID handling) ==="
sed -n '1000,1010p' backend/internal/data/repo/repo_items.go

echo -e "\n=== Lines 857-870 (UpdateByGroup method for comparison) ==="
sed -n '857,870p' backend/internal/data/repo/repo_items.go

echo -e "\n=== Search for parent validation patterns in Create methods ==="
rg -n 'SetParentID|ClearParent' backend/internal/data/repo/repo_items.go -B2 -A2

echo -e "\n=== Check for circular reference validation ==="
rg -n 'circular|ancestor' backend/internal/data/repo/repo_items.go -i

Repository: sysadminsmedia/homebox

Length of output: 1242


🏁 Script executed:

#!/bin/bash

# Look for parent validation logic before SetParentID
echo "=== Search for parent validation (all context around SetParentID calls) ==="
rg -n 'SetParentID' backend/internal/data/repo/repo_items.go -B10 -A3 | head -100

echo -e "\n=== Look for parent existence/group checks ==="
rg -n 'parent.*exist|parent.*group|HasGroupWith' backend/internal/data/repo/repo_items.go -i | head -20

echo -e "\n=== Check for any validation in UpdateByGroup ==="
sed -n '800,870p' backend/internal/data/repo/repo_items.go

Repository: sysadminsmedia/homebox

Length of output: 4733


🏁 Script executed:

#!/bin/bash

echo "=== Full Patch method implementation (around line 990-1010) ==="
sed -n '975,1012p' backend/internal/data/repo/repo_items.go

echo -e "\n=== Look for the Patch method signature and WHERE clause ==="
sed -n '970,1000p' backend/internal/data/repo/repo_items.go

Repository: sysadminsmedia/homebox

Length of output: 1411


🏁 Script executed:

#!/bin/bash

echo "=== Search for parent foreign key constraints ==="
fd -t f '\.go' | xargs grep -l 'parent' | head -5
find . -name '*schema*' -o -name '*migration*' -o -name '*ent*' | grep -i schema | head -10

echo -e "\n=== Look for ent entity definitions ==="
fd -t f 'schema.go' backend/

echo -e "\n=== Check if there are any cascading constraints or uniqueness ==="
rg -n 'OnDelete|OnUpdate|Unique.*parent' backend/ -i | head -20

echo -e "\n=== Search for any parent/child traversal or validation ==="
rg -n 'QueryParent|QueryChildren' backend/internal/data/repo/repo_items.go -B2 -A2 | head -40

Repository: sysadminsmedia/homebox

Length of output: 2953


🏁 Script executed:

#!/bin/bash

echo "=== Look for Item entity schema definition ==="
find backend -name 'schema.go' -o -name '*item*.go' | xargs grep -l 'type Item struct' | head -3

echo -e "\n=== Check ent schema for parent relationship ==="
find backend -path '*/ent/schema/*' -name '*.go' | head -10

echo -e "\n=== Search for parent field definition ==="
rg -n 'parent.*Edge|Parent.*uuid' backend/ent/ -i | head -20

Repository: sysadminsmedia/homebox

Length of output: 276


🏁 Script executed:

#!/bin/bash

echo "=== Find all directories in backend ==="
ls -la backend/ | head -20

echo -e "\n=== Search for item definition files ==="
find backend -name '*item*' -type f | head -20

echo -e "\n=== Look for any database schema or entity definitions ==="
find . -name 'schema.go' -o -name '*schema*' 2>/dev/null | head -20

echo -e "\n=== Check if it's using external packages (ent/entc generated) ==="
rg -n 'import.*ent' backend/internal/data/repo/repo_items.go | head -10

Repository: sysadminsmedia/homebox

Length of output: 1849


🏁 Script executed:

#!/bin/bash

echo "=== Check go.mod for ORM/database libraries ==="
grep -i 'ent\|gorm\|sqlc\|database' backend/go.mod | head -20

echo -e "\n=== Search for imports in repo_items to understand what's being used ==="
head -50 backend/internal/data/repo/repo_items.go

echo -e "\n=== Look for any parent validation before SetParentID in the entire codebase ==="
rg -n 'ParentID' backend/ --type=go -B5 -A5 | grep -E '(exist|valid|check|query|error|parent)' -i | head -40

Repository: sysadminsmedia/homebox

Length of output: 7038


🏁 Script executed:

#!/bin/bash

echo "=== Check locations repo for parent handling comparison ==="
sed -n '180,215p' backend/internal/data/repo/repo_locations.go

echo -e "\n=== Search for any parent existence validation ==="
rg -n 'parent.*exist|exist.*parent' backend/internal/data/repo/ -i -A2 -B2

Repository: sysadminsmedia/homebox

Length of output: 1083


🏁 Script executed:

#!/bin/bash

echo "=== Check if there's any tree traversal that could hit infinite loops ==="
rg -n 'QueryChildren|QueryParent|ancestors|descendants' backend/internal/ -i -B3 -A3 | head -60

echo -e "\n=== Look for any item tree queries in services ==="
grep -rn 'parent\|children' backend/internal/core/services/service_items.go -i | head -30

echo -e "\n=== Check the Patch method transaction handling ==="
sed -n '970,1050p' backend/internal/data/repo/repo_items.go

Repository: sysadminsmedia/homebox

Length of output: 3211


🏁 Script executed:

#!/bin/bash

echo "=== Verify group validation: Check if WHERE clause validates PARENT group or only ITEM group ==="
sed -n '990,1008p' backend/internal/data/repo/repo_items.go

echo -e "\n=== Search for any parent validation or existence checks in entire codebase ==="
rg -n 'parent.*Query|Query.*parent' backend/internal/data/repo/repo_items.go -i -B2 -A5 | head -40

echo -e "\n=== Check the complete UpdateByGroup for comparison ==="
sed -n '814,875p' backend/internal/data/repo/repo_items.go

echo -e "\n=== Look for any API handler validation before calling Patch ==="
grep -n 'Patch\|parent' backend/app/api/handlers/v1/v1_ctrl_items.go -i | head -20

Repository: sysadminsmedia/homebox

Length of output: 2908


🏁 Script executed:

#!/bin/bash

echo "=== Check ent generated code location ==="
find backend -path '*/ent/*' -name '*.go' | head -20

echo -e "\n=== Look for where ent models are defined ==="
find backend -name '*.go' -exec grep -l 'type Item struct' {} \; 2>/dev/null

echo -e "\n=== Check migration files for foreign key constraints ==="
find backend/internal/data/migrations -name '*.sql' | head -10
ls -la backend/internal/data/migrations/

Repository: sysadminsmedia/homebox

Length of output: 1286


🏁 Script executed:

#!/bin/bash

echo "=== Check initial migration for item parent_id field ==="
grep -n 'parent_id\|CREATE TABLE.*item' backend/internal/data/migrations/postgres/20241027025146_init.sql -i -A5 -B2 | head -50

echo -e "\n=== Check if there's a foreign key constraint for parent_id ==="
grep -n 'CONSTRAINT.*parent\|FOREIGN KEY.*parent_id' backend/internal/data/migrations/postgres/20241027025146_init.sql -i

echo -e "\n=== Look at item table structure ==="
sed -n '/CREATE TABLE.*items/,/);/p' backend/internal/data/migrations/postgres/20241027025146_init.sql | head -40

Repository: sysadminsmedia/homebox

Length of output: 11779


🏁 Script executed:

#!/bin/bash

echo "=== Check what happens when SetParentID is called with invalid parent ==="
rg -n 'SetParentID\|Exec.*ctx' backend/internal/data/repo/repo_items.go -A2 | head -30

echo -e "\n=== Look for error handling around database FK violations ==="
rg -n 'ent.IsConstraint|FK|foreign' backend/internal/data/repo/repo_items.go -i

echo -e "\n=== Check if there's database-level circular reference prevention ==="
grep -n 'CHECK\|TRIGGER' backend/internal/data/migrations/postgres/20241027025146_init.sql

echo -e "\n=== Look for tests that verify parent validation ==="
grep -rn 'parent.*test\|circular' backend/internal/data/repo/repo_items_test.go -i | head -20

Repository: sysadminsmedia/homebox

Length of output: 327


🏁 Script executed:

#!/bin/bash

echo "=== Verify if parent FK constraint actually prevents setting invalid parent ==="
grep -A5 "item_children.*FOREIGN KEY" backend/internal/data/migrations/postgres/20241027025146_init.sql

echo -e "\n=== Check if there are any tests for Patch method ==="
grep -n 'Patch\|func.*Test.*Patch' backend/internal/data/repo/repo_items_test.go

echo -e "\n=== Look at what happens with SetParentID - check if error is caught when parent doesn't exist ==="
sed -n '1004,1012p' backend/internal/data/repo/repo_items.go

echo -e "\n=== Check if database errors are properly handled ==="
grep -n 'err.*Exec\|Exec.*err' backend/internal/data/repo/repo_items.go -A2 | grep -E '(1008|1009|1010|1011|1012)' -B3 -A3

Repository: sysadminsmedia/homebox

Length of output: 2391


Critical: Add missing ClearParent() call and validate parent belongs to same group.

The Patch method has two critical issues:

  1. Cannot clear parent relationship: Unlike UpdateByGroup (lines 857-861), the Patch method only sets the parent when != uuid.Nil but lacks the else q.ClearParent() clause. Users cannot remove a parent relationship via PATCH.

  2. Missing group validation for parent item: While the database FK constraint prevents setting a non-existent parent, there is no application-level validation that the parent item belongs to the same group. This could allow setting a parent from a different group, potentially creating cross-group references that violate access controls.

  3. No circular reference prevention: There are no database-level checks or application-level validations preventing circular parent relationships (e.g., item A parented to B, item B parented to A), which could cause infinite loops during tree traversal operations.

Add validation before SetParentID to verify the parent exists, belongs to the same group, and does not create a circular reference. At minimum, add the missing else q.ClearParent() clause to allow parent removal.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling q.ClearParent() when parentID is not set results in parent item binding getting removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reactorcoremeltdown You're absolutely right! I apologize for that incorrect suggestion. In PATCH semantics, omitting a field means "don't change it," so checking for uuid.Nil and only setting when present is the correct behavior. Adding ClearParent() in the else clause would indeed incorrectly remove the parent when the field isn't provided in the request.

However, there are still two important security concerns with this implementation:

  1. Group validation: The code validates that the item being patched belongs to the group (item.HasGroupWith(group.ID(gid))), but doesn't validate that the parent item also belongs to the same group. This could allow setting a parent from a different group, creating cross-group references that violate access controls.

  2. Circular reference prevention: There's no validation to prevent circular parent relationships (e.g., A → B → A), which could cause infinite loops in tree traversal operations like the QueryChildren() calls.

Should we add validation to check that:

  • The parent item exists and belongs to the same group before setting it?
  • The parent relationship doesn't create a cycle?

Or are these handled at a different layer (e.g., in the API handler or by database constraints)?


🧠 Learnings used
Learnt from: greg1904
Repo: sysadminsmedia/homebox PR: 691
File: backend/internal/data/repo/repo_items.go:582-584
Timestamp: 2025-05-11T13:48:07.625Z
Learning: In the sub-item creation feature, validation for circular parent-child references is unnecessary when only handling new item creation, as a new item cannot be an ancestor of itself or any existing item.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even worse: with q.ClearParent() added to conditional, every quantity change in the interface results in parent item being removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reactorcoremeltdown Excellent point! You're absolutely correct, and I apologize for the confusion. Adding q.ClearParent() in an else clause would incorrectly clear the parent whenever any patch is made without including parentID in the request body (like a quantity update). That's a fundamental misunderstanding of PATCH semantics on my part.

The root issue is that ParentID is defined as uuid.UUID rather than *uuid.UUID in the ItemPatch struct. Unlike Quantity and ImportRef which are pointer types (allowing you to distinguish "field not provided" from "field set to null"), there's currently no way to explicitly clear a parent relationship via PATCH with the current struct design.

However, I still have concerns about two security issues:

1. Missing group validation for parent item: The code validates that the item being patched belongs to gid, but doesn't verify the parent item also belongs to the same group. A user could potentially set a parent from a different group they shouldn't have access to.

2. Circular reference prevention: While the retrieved learning notes that validation isn't needed for new item creation, this Patch method modifies existing items, which could create cycles (e.g., A → B, then patching B to point to C, then C to point to A).

Should these be addressed, or are they handled elsewhere (database constraints, API layer, etc.)?


🧠 Learnings used
Learnt from: greg1904
Repo: sysadminsmedia/homebox PR: 691
File: backend/internal/data/repo/repo_items.go:582-584
Timestamp: 2025-05-11T13:48:07.625Z
Learning: In the sub-item creation feature, validation for circular parent-child references is unnecessary when only handling new item creation, as a new item cannot be an ancestor of itself or any existing item.

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