Skip to content

feat(core): add entity module stubs, migration runner, and Storage wrapper#1059

Merged
geoffjay merged 38 commits into
issue-212from
issue-241
Apr 16, 2026
Merged

feat(core): add entity module stubs, migration runner, and Storage wrapper#1059
geoffjay merged 38 commits into
issue-212from
issue-241

Conversation

@geoffjay
Copy link
Copy Markdown
Owner

@geoffjay geoffjay commented Apr 8, 2026

Sets up the entity/migration/storage scaffold in crates/core that all downstream entity issues (#237, #238, #239) depend on.

  • entity/mod.rs — declares four sub-modules: user, organization, membership, session
  • entity/{user,organization,membership,session}.rs — minimal DeriveEntityModel stubs (id-only, correct table names) that compile and are ready to be filled in
  • migration/mod.rs — empty Migrator implementing MigratorTrait, ready for future migration files
  • storage.rsStorage struct wrapping DatabaseConnection; runs Migrator::up on construction; Clone + Send + Sync
  • Cargo.toml — adds sea-orm, sea-orm-migration, async-trait workspace/direct deps
  • lib.rs — declares entity, migration, storage modules

Closes #241

@geoffjay geoffjay added the review-agent Used to invoke a review by an agent tracking this label label Apr 8, 2026
Copy link
Copy Markdown
Owner Author

@geoffjay geoffjay left a comment

Choose a reason for hiding this comment

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

Review: feat(core): add entity module stubs, migration runner, and Storage wrapper

Clean scaffold. All four entity stubs, the migration runner, and the Storage wrapper follow the orchestrator's established patterns exactly. No correctness issues.


Stack context

issue-241 (#1059) is stacked on issue-212 (#1058), which was just approved and is in merge-queue. This branch cannot merge until #1058 lands. Once #1058 merges to main, this branch will need a restack to update its base — adding needs-restack accordingly.

Ordering note for the #240 worker: The triage plan defined the chain as #212 → #240 → #241, but this PR implements #241 directly on top of #212, ahead of #240. That's fine as a scaffold-first approach, but it changes what #240 needs to do: instead of creating migration/mod.rs from scratch, the #240 worker must stack on top of this branch and update the existing migration/mod.rs to register the actual migration file(s). Creating a fresh migration/mod.rs in a branch off main would conflict with this PR.


Entity stubs — correct

All four stubs follow the same minimal pattern:

  • #[sea_orm(table_name = "...")] uses the correct plural snake_case table names: users, organizations, memberships, sessions
  • auto_increment = false on the UUID id PK ✅
  • Empty Relation enum and impl ActiveModelBehavior for ActiveModel {} ready for #237/#238/#239 to fill in ✅
  • Doc comments reference the downstream issues that will complete each entity ✅

migration/mod.rs — correct scaffold

pub use sea_orm_migration::prelude::* + empty vec![] migration list matches the orchestrator's migration/mod.rs pattern exactly. The doc example showing Migrator::up(&db, None).await? is accurate. ✅


storage.rs — correct

Matches AgentStorage pattern:

  • Clone via DatabaseConnection (pool handle, cheap clone) ✅
  • Runs Migrator::up on construction before returning ✅
  • db() accessor for entity-specific operations ✅
  • anyhow::Result for error propagation ✅

The doc example references agentd_common::storage::{get_db_path, create_connection} — consistent with how AgentStorage is initialized. ✅


Dependencies — correct

  • sea-orm = { workspace = true } and sea-orm-migration = { workspace = true }
  • async-trait = "0.1" matches orchestrator's direct pinned dep (it's not in the workspace table) ✅

No tests

Acceptable for this scaffold — Storage::new has no logic beyond Migrator::up (which returns immediately with an empty migration list), and the entity stubs have no fields to exercise. Tests belong in the downstream issues (#237, #238, #239, #242).


Summary

Correct scaffold, consistent with existing patterns. Approving. Parent PR #1058 must merge first; #240 worker should stack on top of this branch.

geoffjay and others added 11 commits April 15, 2026 14:10
…apper

Sets up the structural scaffold that all core entity issues depend on:
- crates/core/src/entity/{mod,user,organization,membership,session}.rs — minimal
  DeriveEntityModel stubs for the four core tables
- crates/core/src/migration/mod.rs — empty Migrator ready for future migrations
- crates/core/src/storage.rs — Storage wrapping DatabaseConnection, runs
  Migrator::up on construction
- Adds sea-orm, sea-orm-migration, and async-trait to Cargo.toml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the stub entity/user.rs with the full DeriveEntityModel
implementation matching the users table schema from the initial migration:
id, email (unique), password_hash, display_name, role, timestamps.

Declares has_many relations to memberships and sessions, with Related<T>
impls for both. To satisfy the has_many bound (R: Related<Self>), the
membership and session stubs gain a user_id column and belongs_to User
relation — the minimum required for compilation before #238 and #239 fill
in their full schemas.

Adds tempfile dev-dependency for the test helper.

Test: insert a user via ActiveModel and retrieve it by primary key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds crates/core/src/migration/m20260305_000001_create_core_tables.rs
creating four tables in a single migration:
- users (id, email unique, password_hash, display_name, role, timestamps)
- organizations (id, name unique, slug unique, timestamps)
- memberships (id, user_id, organization_id, role, timestamps) with
  composite unique index on (user_id, organization_id)
- sessions (id, user_id, token_hash, expires_at, created_at) with
  indexes on token_hash and expires_at

All tables use if_not_exists; down() drops in reverse dependency order.
Registers the migration in Migrator and calls Storage::new() at startup
to apply pending migrations before the HTTP server begins accepting requests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…many relations

Replaces entity stubs with full DeriveEntityModel implementations:

organization.rs — id, name (unique), slug (unique), timestamps; has_many
Memberships; Related<user::Entity> via membership junction (via() pattern).

membership.rs — id, user_id, organization_id, role, timestamps; belongs_to
User and Organization; Related impls for both.

user.rs — adds Related<organization::Entity> with via() completing the
bidirectional many-to-many between User and Organization through Membership.

Four tests in organization.rs:
- test_org_insert_and_find: basic CRUD
- test_membership_links_user_and_org: junction row creation
- test_find_users_with_related_organizations: find_with_related(Org) from
  user side — one user, two orgs via two memberships
- test_find_org_with_related_users: find_with_related(User) from org side —
  one org, two member users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the stub entity/session.rs with the full DeriveEntityModel
implementation matching the sessions table schema: id, user_id,
token_hash, expires_at, created_at.

Declares belongs_to User relation and Related<user::Entity> impl.

Three tests covering the acceptance criteria:
- test_session_creation: insert via ActiveModel, retrieve by PK
- test_session_validation_with_related_user: find non-expired session by
  token_hash using find_also_related(user::Entity) for single-query auth
- test_expired_session_cleanup: delete_many with ExpiresAt.lt filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
geoffjay and others added 13 commits April 15, 2026 14:10
Adds crates/core/tests/entity_relations.rs with 9 tests covering all
acceptance criteria:

- test_crud_user: insert/find/update/delete via ActiveModel + delete_by_id
- test_user_has_many_memberships: find_related(membership::Entity) from a
  loaded user model
- test_many_to_many_user_organizations_from_user_side: find_with_related
  traversal user → organizations through membership junction
- test_many_to_many_organization_users_from_org_side: find_with_related
  traversal org → users through membership junction
- test_session_belongs_to_user: find_also_related(user::Entity) for
  single-query session validation with eager-loaded user
- test_unique_email_constraint: duplicate email insert returns Err
- test_unique_membership_constraint: duplicate (user_id, org_id) returns Err
- test_migration_idempotent: Migrator::up twice does not error
- test_migration_down_and_up: full rollback + re-apply yields empty tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… migration

Adds UserStorage with full CRUD, argon2id password hashing, get_by_username/email,
set_active_organization, and paginated listing. Adds migration to extend the users
table with nullable username (unique) and active_organization_id columns. Updates
all entity helpers and integration tests to include the new fields.
…oint

Adds TenantContext extractor (session + active org required, 403 if no
active org) and PUT /users/me/active-organization endpoint with membership
validation. Also adds api/users.rs module with 4 endpoint tests.
Implements POST /auth/register, POST /auth/login, POST /auth/logout, and
GET /auth/me. Adds SessionStorage with 256-bit random token generation,
expiry cleanup, and AuthUser extractor middleware. Registration creates a
default personal organization and sets it as the active organization.
Adds `agent auth register/login/logout/status` and `agent org list/create/switch`
commands communicating with the agentd-core service (port 17007). Session token
stored at ~/.config/agentd/session with 0600 permissions. Adds `dirs` dependency
for cross-platform config path resolution. Registers core service in status check.
Adds GET/PUT /api/v1/users/me, PUT /api/v1/users/me/password,
GET /api/v1/users/me/organizations, and full CRUD for organizations
with member management (add, remove, list). Enforces owner-only mutations,
last-owner protection, and clears active_organization_id on org deletion.
Adds update_email, update_password, and clear_active_organization_for_org
to UserStorage. 21 new endpoint tests covering auth and authorization checks.
Implements HTTP reverse proxy in crates/core that forwards /api/v1/{service}/*
requests to downstream agentd services with X-Tenant-ID and X-Request-ID header
injection. Includes concurrent health aggregation at /api/v1/health.
geoffjay added 12 commits April 15, 2026 17:27
feat(core): add API gateway proxy for downstream service routing
feat(cli): add auth and org commands for core service
feat(core): add user and organization management API endpoints
feat(core): add TenantContext middleware and active organization endpoint
feat(core): add authentication endpoints with SQLite-backed sessions
feat(core): add UserStorage with argon2 password hashing and username migration
feat(core): add OrganizationStorage and MembershipStorage
feat(core): add SeaORM integration tests for entity relations
feat(core): define Organization and Membership entities with many-to-many relations
feat(core): define SeaORM Session entity with belongs_to User
feat(core): define SeaORM User entity with has_many relations
feat(core): implement initial sea-orm migration for core service schema
@geoffjay geoffjay merged commit 493b830 into issue-212 Apr 16, 2026
@geoffjay geoffjay deleted the issue-241 branch April 16, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-restack Branch is behind its stack parent, needs git-spice restack

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Set up entity module structure and storage layer for core crate

1 participant