feat(persistence): PR8 T8.4 — ApiKeyRow ORM + 3 tests

Adds deerflow/persistence/api_key/{__init__,model}.py with ApiKeyRow:
  - id: UUID36 PK
  - service_account_id: FK service_accounts ON DELETE CASCADE
  - key_prefix: String(16), UNIQUE — global uniqueness preserved even
    after revoke so audit logs never reference an ambiguous prefix
  - key_hash: String(128) sha-256 hex (plaintext returned only once at
    create time)
  - name / scopes / rate_limit_rpm / expires_at / last_used_at /
    revoked_at — all nullable or default-providing
  - created_at: UTC
  - Index idx_api_keys_sa (service_account_id) — list keys for an SA
  - Index idx_api_keys_active (key_prefix) WHERE revoked_at IS NULL —
    partial index, dual-dialect via sqlite_where + postgresql_where,
    shrinks the hot-path lookup index by excluding revoked keys

T8.4 tests:
  - test_unique_key_prefix_enforced: column-level UNIQUE blocks two
    rows from sharing key_prefix (active or revoked alike)
  - test_active_index_declares_both_dialect_where_clauses: schema
    introspection confirms idx_api_keys_active has both
    `dialect_options.sqlite.where` and `dialect_options.postgresql.where`
    set to `revoked_at IS NULL` — guards against accidental loss of the
    dual-driver hint when the Index is edited later
  - test_cascade_on_service_account_delete: deleting the parent SA
    removes all api_keys rows

Registered in deerflow/persistence/models/__init__.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1445043649
2026-05-14 14:10:32 +08:00
parent bb7289781e
commit 52e9999a61
4 changed files with 271 additions and 0 deletions
@@ -11,12 +11,14 @@ The actual ORM classes have moved to entity-specific subpackages:
- ``deerflow.persistence.workspace`` (Stage 0 PR3)
- ``deerflow.persistence.workspace_membership`` (Stage 0 PR3)
- ``deerflow.persistence.service_account`` (Stage 0 PR8)
- ``deerflow.persistence.api_key`` (Stage 0 PR8)
``RunEventRow`` remains in ``deerflow.persistence.models.run_event`` because
its storage implementation lives in ``deerflow.runtime.events.store.db`` and
there is no matching entity directory.
"""
from deerflow.persistence.api_key.model import ApiKeyRow
from deerflow.persistence.feedback.model import FeedbackRow
from deerflow.persistence.models.run_event import RunEventRow
from deerflow.persistence.run.model import RunRow
@@ -27,6 +29,7 @@ from deerflow.persistence.workspace.model import WorkspaceRow
from deerflow.persistence.workspace_membership.model import WorkspaceMembershipRow
__all__ = [
"ApiKeyRow",
"FeedbackRow",
"RunEventRow",
"RunRow",