feat(api.iam): AIHCM-143 workspace repo infra#202
Conversation
…lembic - Add SQLAlchemy 2.0 with asyncpg for async database operations - Add Alembic for schema migrations - Add python-ulid for ULID support instead of UUID - Create read/write engine separation with connection pooling - Create FastAPI dependency injection for database sessions - Create SQLAlchemy declarative base with timestamp mixin - Initialize Alembic with async migration support - Create initial migration for teams table (ULID primary key) - Add comprehensive unit tests for engines and dependencies - Configure Alembic to use settings module for database URL - Enable ruff post-write hook for migration formatting Refs: AIHCM-121
- Add authzed library for SpiceDB integration - Add python-ulid for ULID support - Create ResourceType, RelationType, Permission enums (using Group not Team) - Create AuthorizationProvider protocol for swappable implementations - Implement SpiceDBClient with async methods for relationships and permissions - Create SpiceDB schema (.zed) with Tenant→Workspace→Group hierarchy - Create AuthorizationProbe for domain-oriented observability - Move ObservationContext to shared_kernel (fix architectural boundary) - Add 35 unit tests for types and probes - All 410 tests passing Refs: AIHCM-122
Resolved conflicts in authorization files by accepting remote changes: - shared_kernel/authorization/types.py (docstring fix) - shared_kernel/authorization/spicedb/client.py (_parse_reference helper)
…nstraints - Fix groups FK from CASCADE to RESTRICT to force application-level cascading and ensure domain events are emitted for SpiceDB cleanup - Create workspaces table with RESTRICT FK constraints and partial unique index for root workspace per tenant - Add WorkspaceModel with self-referential parent/child relationships - Define IWorkspaceRepository protocol with save, get_by_id, get_by_name, get_root_workspace, list_by_tenant, and delete methods - Implement WorkspaceRepository following TenantRepository pattern with transactional outbox for domain events - Add WorkspaceRepositoryProbe for domain-oriented observability - Add default_workspace_name setting to IAMSettings (defaults to None, falls back to tenant name) - Wire workspace repository into FastAPI dependency injection - Add 27 unit tests covering all repository methods, protocol compliance, outbox events, observability probes, and edge cases Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
WalkthroughThis PR adds workspace support to the IAM bounded context. It introduces new SQLAlchemy ORM model files (WorkspaceModel, TenantModel, GroupModel, UserModel, APIKeyModel) and a models package export; an Alembic migration to create the workspaces table and a migration to change groups FK behavior; a PostgreSQL-backed WorkspaceRepository that uses a transactional outbox to persist domain events; observability probes for workspace repository operations; a FastAPI dependency factory Sequence Diagram(s)mermaid Client->>WorkspaceRepo: save(workspace) mermaid Client->>WorkspaceRepo: get_by_id(id) Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/api/iam/infrastructure/models/workspace.py`:
- Around line 49-54: WorkspaceModel currently redefines created_at and
updated_at, which strips TimestampMixin's insert_default and onupdate behavior;
remove the created_at and updated_at mapped_column overrides from WorkspaceModel
so it inherits the mixin's timestamp defaults and callbacks (refer to
WorkspaceModel, created_at, updated_at, and
TimestampMixin/insert_default/onupdate) to restore consistent automatic
timestamp population like TenantModel and UserModel.
In `@src/api/iam/infrastructure/observability/repository_probe.py`:
- Around line 314-318: Update the module-level docstring to mention workspaces
in addition to groups, users, and tenants so it accurately reflects the new
WorkspaceRepositoryProbe; revise the top-level description to state that the
module covers group/user/tenant/workspace repository operations and that it
records domain events during workspace persistence operations (aligning with the
existing WorkspaceRepositoryProbe class and related probe implementations).
🧹 Nitpick comments (2)
src/api/tests/unit/iam/infrastructure/test_workspace_repository.py (1)
652-664: Consider avoiding direct access to private attribute_serializer.The test accesses
repository._serializer(line 664), which is an implementation detail. This couples the test to the internal structure ofWorkspaceRepository.Consider verifying the default serializer behavior indirectly, such as by checking that serialization produces expected output when saving a workspace, rather than inspecting the private attribute.
♻️ Alternative approach
def test_uses_default_serializer_when_not_injected( - self, mock_session, mock_outbox, mock_probe + self, mock_session, mock_outbox, mock_probe, tenant_id ): - """Should create default serializer when not injected.""" - from iam.infrastructure.outbox import IAMEventSerializer - + """Should use default serializer behavior when not injected.""" repository = WorkspaceRepository( session=mock_session, outbox=mock_outbox, probe=mock_probe, ) - assert isinstance(repository._serializer, IAMEventSerializer) + workspace = Workspace.create_root(name="Test", tenant_id=tenant_id) + + mock_result = MagicMock() + mock_result.scalar_one_or_none.return_value = None + mock_session.execute.return_value = mock_result + + # Save should succeed with default serializer + # (would fail if serializer was None or broken) + await repository.save(workspace) + mock_outbox.append.assert_called_once()src/api/iam/infrastructure/workspace_repository.py (1)
144-189: Consider probe consistency for not-found cases.
get_by_idcallsprobe.workspace_not_found()when the workspace doesn't exist, butget_by_nameandget_root_workspacesilently returnNone. This inconsistency may be intentional (these methods are often used for existence checks where not-found is expected), but worth confirming the observability requirements.
…e consistency Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
…m:openshift-hyperfleet/kartograph into jsell/feat/AIHCM-143-workspace-repo-infra
Summary by CodeRabbit