Hierarchy Kit
Overview
Purpose: Transform flat group data into hierarchical tree structures and query ancestor/descendant relationships for organizational charts, permission inheritance, and navigation.
Key Features:
- Build complete hierarchy trees from root groups
- Query ancestors (parents, grandparents, etc.) of any group
- Query descendants (children, grandchildren, etc.) of any group
- Support multi-parent groups (matrix organizations)
- Configurable depth limits for large hierarchies
- Cycle detection for multi-parent graphs
Dependencies:
- Injected services: GroupService (for accessing group data)
- Port dependencies: None (uses GroupService methods)
- Note: Kits cannot directly import from other kits (enforced by import-linter contract #6). Dependencies are injected via constructor in
compose.py.
Quick Start
from portico import compose
# Basic configuration
app = compose.webapp(
database_url="postgresql://localhost/myapp",
kits=[
compose.group(), # Required - provides group data
compose.hierarchy(), # Uses GroupService
]
)
# Access the hierarchy kit
hierarchy_kit = app.kits["hierarchy"]
hierarchy_service = hierarchy_kit.service
# Build complete hierarchy
roots = await hierarchy_service.build_hierarchy()
# Get ancestors of a group
ancestors = await hierarchy_service.get_ancestors(group_id)
# Get descendants of a group
descendants = await hierarchy_service.get_descendants(group_id)
Core Concepts
Tree Building
The Hierarchy Kit builds tree structures from flat group data by following parent-child relationships. Each node in the tree is represented as a HierarchyNode containing the group data plus its children.
# Build entire hierarchy from all roots
roots = await hierarchy_service.build_hierarchy()
# Build from specific root
division_id = UUID("...")
roots = await hierarchy_service.build_hierarchy(root_ids=[division_id])
# Limit depth for performance
roots = await hierarchy_service.build_hierarchy(max_depth=2)
Ancestor/Descendant Queries
Efficiently query relationships in the hierarchy using breadth-first search:
# Get all parent groups (walking up the hierarchy)
ancestors = await hierarchy_service.get_ancestors(team_id)
# Returns: [Division, Company]
# Get all child groups (walking down the hierarchy)
descendants = await hierarchy_service.get_descendants(division_id)
# Returns: [Team1, Team2, Project1, Project2, ...]
# Include the group itself in results
ancestors = await hierarchy_service.get_ancestors(team_id, include_self=True)
# Returns: [Team, Division, Company]
Multi-Parent Support
The Hierarchy Kit supports matrix organizations where groups can have multiple parents:
# Create cross-functional project with multiple parents
launch_project = await group_service.create_group(
CreateGroupRequest(
name="Product Launch 2024",
group_type="project",
parent_ids=[engineering.id, marketing.id] # Multiple parents
)
)
# When building hierarchy, the project appears under both parents
# When getting ancestors, both parents are returned
Multi-parent traversal uses BFS with cycle detection and deduplication to ensure each group appears once in results.
Configuration
The Hierarchy Kit has no configuration options:
from portico import compose
app = compose.webapp(
kits=[
compose.group(),
compose.hierarchy(), # No configuration needed
]
)
Usage Examples
Example 1: Displaying Organizational Chart
@app.get("/org/chart")
async def get_org_chart():
"""Get complete organizational hierarchy."""
hierarchy_service = app.kits["hierarchy"].service
# Build full tree with depth limit
roots = await hierarchy_service.build_hierarchy(max_depth=3)
# Return as JSON for frontend rendering
return {"hierarchy": [root.dict() for root in roots]}
Example 2: Permission Inheritance Chain
async def get_permission_chain(group_id: UUID) -> List[Group]:
"""Get groups to check for cascading permissions."""
hierarchy_service = app.kits["hierarchy"].service
# Check this group + all ancestors
return await hierarchy_service.get_ancestors(
group_id,
include_self=True
)
# Usage with RBAC
groups_to_check = await get_permission_chain(team_id)
for group in groups_to_check:
if await rbac_service.check_permission(user_id, "documents.read", group.id):
return True
Example 3: Breadcrumb Navigation
@app.get("/groups/{group_id}/breadcrumbs")
async def get_breadcrumbs(group_id: UUID):
"""Get breadcrumb navigation for a group."""
hierarchy_service = app.kits["hierarchy"].service
# Get path from root to this group
ancestors = await hierarchy_service.get_ancestors(group_id, include_self=True)
ancestors.reverse() # Root → group order
return {
"breadcrumbs": [
{"id": str(g.id), "name": g.name, "type": g.group_type}
for g in ancestors
]
}
Domain Models
HierarchyNode
Represents a node in a hierarchy tree with children.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
UUID |
Yes | - | Group identifier |
name |
str |
Yes | - | Group name |
group_type |
str |
Yes | - | Group type (company, division, team, etc.) |
description |
Optional[str] |
No | None |
Group description |
metadata |
Dict[str, Any] |
Yes | {} |
Custom metadata |
parent_ids |
List[UUID] |
Yes | [] |
Parent group IDs (supports multi-parent) |
children |
List[HierarchyNode] |
Yes | [] |
Child nodes in tree |
depth |
int |
Yes | 0 |
Depth in tree (0 for roots) |
Example:
# Build hierarchy returns trees of HierarchyNode objects
roots = await hierarchy_service.build_hierarchy()
# Recursively traverse tree
def print_tree(node: HierarchyNode, indent: int = 0):
print(" " * indent + f"- {node.name} ({node.group_type})")
for child in node.children:
print_tree(child, indent + 1)
print_tree(roots[0])
Best Practices
1. Use Depth Limits for Large Hierarchies
For organizations with hundreds of groups, always use depth limits to prevent loading entire trees:
# ✅ GOOD - Limit depth for performance
roots = await hierarchy_service.build_hierarchy(max_depth=3)
subtree = await hierarchy_service.get_subtree(division_id, max_depth=2)
# ❌ BAD - Loading entire hierarchy without limits
roots = await hierarchy_service.build_hierarchy() # Could load 1000+ groups
2. Load Subtrees Instead of Full Hierarchy
When displaying a specific section of the org chart, load only the needed subtree:
# ✅ GOOD - Load only relevant subtree
subtree = await hierarchy_service.get_subtree(division_id)
# ❌ BAD - Load full hierarchy and filter client-side
roots = await hierarchy_service.build_hierarchy()
# Then search for division node...
3. Cache Hierarchy Results
Hierarchy queries can be expensive for large organizations. Cache results in your application:
# ✅ GOOD - Cache hierarchy results
from functools import lru_cache
@lru_cache(maxsize=100)
async def get_cached_hierarchy(root_id: str):
return await hierarchy_service.get_subtree(UUID(root_id))
# ❌ BAD - Rebuild hierarchy on every request
@app.get("/org/chart")
async def get_org_chart():
roots = await hierarchy_service.build_hierarchy() # Expensive!
return {"hierarchy": [r.dict() for r in roots]}
4. Use Cascading Permissions Instead of Manual Traversal
For permission checks, use RBAC's cascading permissions instead of manually walking the hierarchy:
# ✅ GOOD - Let RBAC handle cascading
can_read = await rbac_service.check_permission(
user_id,
"documents.read",
team_id # Automatically checks ancestors
)
# ❌ BAD - Manually walk hierarchy for permissions
groups = await hierarchy_service.get_ancestors(team_id, include_self=True)
for group in groups:
if await rbac_service.check_permission(user_id, "documents.read", group.id):
return True
5. Include Self When Building Permission Chains
When checking permissions, always include the group itself in the ancestor list:
# ✅ GOOD - Include group in permission chain
groups = await hierarchy_service.get_ancestors(group_id, include_self=True)
# ❌ BAD - Missing direct group permissions
groups = await hierarchy_service.get_ancestors(group_id) # Skips group itself
6. Handle Multi-Parent Groups Carefully
When working with multi-parent groups, remember that ancestors/descendants may contain duplicates from different paths:
# ✅ GOOD - BFS ensures closest ancestors first, no duplicates
ancestors = await hierarchy_service.get_ancestors(multi_parent_group_id)
# Returns each unique ancestor once, ordered by distance
# ❌ BAD - Assuming single-parent structure
parent_id = group.parent_ids[0] # Breaks for multi-parent groups!
7. Use get_subtree for Tree Rendering
When rendering hierarchical UIs, use get_subtree() to get pre-built tree structures:
# ✅ GOOD - Get tree structure ready for rendering
subtree = await hierarchy_service.get_subtree(division_id)
return subtree.dict() # Frontend can render directly
# ❌ BAD - Get descendants and rebuild tree manually
descendants = await hierarchy_service.get_descendants(division_id)
# Then manually reconstruct tree structure...
Integration with RBAC
Cascading Permissions
The Hierarchy Kit works seamlessly with RBAC's cascading permissions. When permissions have cascades=True, they automatically flow down the hierarchy:
from portico.kits.rbac.models import CreatePermissionRequest
rbac_service = app.kits["rbac"].service
# Create cascading permission
await rbac_service.create_permission(
CreatePermissionRequest(
name="documents.read",
cascades=True # Flows down hierarchy
)
)
# Assign at company level
await rbac_service.assign_group_role(
user_id=ceo_user_id,
group_id=company_id,
role_id=admin_role_id
)
# CEO automatically has permission in all child groups
can_read = await rbac_service.check_permission(
ceo_user_id,
"documents.read",
team_id # Deep in hierarchy - returns True!
)
The RBAC service internally uses HierarchyService.get_ancestors() to walk up the hierarchy when checking cascading permissions.
When to Use Manual Traversal
Use manual get_ancestors() / get_descendants() calls for:
- Building UIs that display hierarchy structures
- Custom business logic that needs to inspect the hierarchy
- Non-permission queries (e.g., finding all teams under a division)
- Breadcrumbs and navigation trees
Use cascading permissions for:
- Checking permissions (let RBAC handle it)
- Access control (simpler and more maintainable)
- Standard inheritance patterns
FAQs
Q: What's the difference between get_descendants and get_subtree?
A: get_descendants() returns a flat list of all child groups (children, grandchildren, etc.) ordered by distance. get_subtree() returns a tree structure (HierarchyNode) with nested children, ready for rendering hierarchical UIs.
# Flat list of all descendants
descendants = await hierarchy_service.get_descendants(division_id)
# Returns: [Team1, Team2, Project1, Project2]
# Tree structure
subtree = await hierarchy_service.get_subtree(division_id)
# Returns: HierarchyNode with nested children property
Q: How do I handle multi-parent groups?
A: The Hierarchy Kit fully supports multi-parent groups using breadth-first search with cycle detection. Each group appears once in results, ordered by distance from the starting point. When building trees, multi-parent groups appear under each parent.
Q: What happens if there's a cycle in the hierarchy?
A: The Hierarchy Kit detects cycles during traversal and prevents infinite loops. Each group is visited only once, even if it appears multiple times in different paths.
Q: How do I improve performance for large hierarchies?
A: Use depth limits (max_depth) when building trees, load subtrees instead of full hierarchies, and cache results in your application. For very large organizations (1000+ groups), consider lazy loading and pagination.
Q: Can I customize how the tree is built?
A: The Hierarchy Kit uses a fixed BFS algorithm for consistency and correctness. For custom tree structures, use get_descendants() to get all groups and build your own tree structure.
Q: Do I need to manually walk the hierarchy for permission checks?
A: No! Use RBAC's cascading permissions instead. When a permission has cascades=True, the RBAC service automatically checks ancestors. Manual traversal is only needed for non-permission use cases like building UIs.
Q: How do I find the root groups in my hierarchy?
A: Call build_hierarchy() without parameters - it automatically finds all groups without parents and uses them as roots:
roots = await hierarchy_service.build_hierarchy()
# Returns HierarchyNode objects for each root group
Q: What's the order of results from get_ancestors and get_descendants?
A: Both methods return results ordered by distance from the starting group. get_ancestors() returns immediate parents first, then grandparents, etc. get_descendants() returns immediate children first, then grandchildren, etc.