Migration Guide
_subset_registry hack → add_virtual_entities() (Non-SQLModel Roots)
Before non-SQLModel root support landed, projects that needed a non-SQLModel root
(e.g. CurrentUser assembled from OIDC claims, page wrappers, third-party SDK DTOs)
worked around the limitation by mutating NexusX internals directly:
# ❌ Old hack — fragile, undocumented, breaks on version bumps
from nexusx.subset import _subset_registry
_subset_registry[CurrentUserRootDTO] = CurrentUserRoot
Replace with the official API. See Virtual Entities guide for the full contract. Quick mapping:
| Hack shape | Official replacement |
|---|---|
_subset_registry[X] = Y where Y has __relationships__ or should be ER-visible |
er.add_virtual_entities([Y]) after ErManager(...) |
_subset_registry[X] = Y where X is a subset of Y's schema |
class X(DefineSubset): __subset__ = (Y, ("fields",)) (Y may now be a BaseModel) |
_subset_registry[X] = Y where X is Y (root is its own schema) |
Make X a plain BaseModel and er.add_virtual_entities([X]) |
The migration is mechanical (search-and-replaceable). ErManager.__init__ signature is
unchanged; existing DTO hierarchies don't need to be rewritten.
1.6.0 → 1.7.0: Voyager ER Diagram Relationship Field Refactor
The Voyager ER diagram changed from FK-field-based to relationship-name-based display.
What Changed
| Aspect | Old Behavior | New Behavior |
|---|---|---|
| Relationship fields in node | Shows FK field (e.g. user_id) marked as is_object |
Shows relationship name + target type (e.g. owner: User) |
| Edge source | Starts from FK field (e.g. user_id) |
Starts from relationship field (e.g. owner) |
| Toggle "Object fields" | Shows FK fields | Shows relationship fields |
Impact
- Only affects
ErDiagramDotBuilder(Voyager visualization) ErDiagram(Mermaid generator) is not affected- No public API changes — no code migration needed
rpc → use_case Refactor
The RPC module has been fully refactored into the UseCase pattern.
Warning
This is a breaking change. Update all imports and class names before upgrading.
Name Changes
| Old Name | New Name |
|---|---|
RpcService |
UseCaseService |
create_rpc_mcp_server |
create_use_case_mcp_server |
create_rpc_voyager |
create_use_case_voyager |
RpcVoyager |
UseCaseVoyager |
MCP Tool Changes
Changed from three layers to four layers, adding list_apps for multi-app management:
| Old Tool | New Tool |
|---|---|
list_services() |
list_apps() → list_services(app_name) |
describe_service(service_name) |
describe_service(app_name, service_name) |
call_rpc(service_name, method_name, params) |
call_use_case(app_name, service_name, method_name, params) |
Code Migration
# Before (rpc)
from nexusx.rpc import RpcService, create_rpc_mcp_server
class SprintService(RpcService):
...
mcp = create_rpc_mcp_server(
services=[SprintService, TaskService],
name="Project API",
)
# After (use_case)
from nexusx.use_case import UseCaseService, UseCaseAppConfig, create_use_case_mcp_server
class SprintService(UseCaseService):
...
mcp = create_use_case_mcp_server(
apps=[
UseCaseAppConfig(
name="project",
services=[SprintService, TaskService],
description="Project management",
),
],
name="Project UseCase API",
)
New Features
- Multi-app management: Organize multiple apps via
UseCaseAppConfig - FromContext: Support parameter injection from MCP context
- Case-insensitive lookup: app_name lookup is case-insensitive
1.3.x → 1.4.0: RpcServiceConfig Removal (Historical)
create_rpc_mcp_server and create_rpc_voyager no longer accept RpcServiceConfig configuration dicts — pass RpcService subclass lists directly.
# Before (1.3.x)
from nexusx import RpcServiceConfig, create_rpc_mcp_server
mcp = create_rpc_mcp_server(
services=[
RpcServiceConfig(name="task", service=TaskService, description="..."),
RpcServiceConfig(name="sprint", service=SprintService, description="..."),
],
)
# After (1.4.0)
from nexusx import create_rpc_mcp_server
mcp = create_rpc_mcp_server(
services=[TaskService, SprintService],
)
1.3.2 → 1.3.3: Loader(str) Removal
The Loader('relationship_name') string lookup pattern has been removed.
# Before (1.3.2)
def resolve_owner(self, loader=Loader("owner")):
return loader.load(self.owner_id)
# After (1.3.3) — use DataLoader class or async function
def resolve_owner(self, loader=Loader(UserLoader)):
return loader.load(self.owner_id)
Tip
Implicit auto-loading already covers common scenarios. When the field name matches a relationship and the type is compatible, you don't need to write resolve_* methods: