first
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
from .mongo import (
|
||||
get_mongo_db,
|
||||
get_mongo_client,
|
||||
MongoCollections,
|
||||
create_indexes,
|
||||
get_collection,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"get_mongo_db",
|
||||
"get_mongo_client",
|
||||
"MongoCollections",
|
||||
"create_indexes",
|
||||
"get_collection",
|
||||
]
|
||||
@@ -0,0 +1,167 @@
|
||||
"""MongoDB client + collection provider.
|
||||
|
||||
Every query is (a) tenant-scoped and (b) backed by explicit indexes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator
|
||||
|
||||
from motor.motor_asyncio import (
|
||||
AsyncIOMotorClient,
|
||||
AsyncIOMotorCollection,
|
||||
AsyncIOMotorDatabase,
|
||||
)
|
||||
from pymongo import IndexModel
|
||||
import structlog
|
||||
|
||||
from ..config import get_settings
|
||||
|
||||
log = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
_client: AsyncIOMotorClient | None = None
|
||||
_db: AsyncIOMotorDatabase | None = None
|
||||
|
||||
|
||||
class MongoCollections(str):
|
||||
ORGANIZATIONS = "organizations"
|
||||
USERS = "users"
|
||||
TEAMS = "teams"
|
||||
PROJECTS = "projects"
|
||||
ARTIFACTS = "artifacts"
|
||||
ARTIFACT_RELATIONSHIPS = "artifact_relationships"
|
||||
INTEGRATIONS = "integrations"
|
||||
JOBS = "jobs"
|
||||
AI_EXECUTIONS = "ai_executions"
|
||||
AUDIT_EVENTS = "audit_events"
|
||||
|
||||
|
||||
def get_mongo_client() -> AsyncIOMotorClient:
|
||||
global _client
|
||||
if _client is None:
|
||||
s = get_settings()
|
||||
_client = AsyncIOMotorClient(
|
||||
s.mongo_uri,
|
||||
minPoolSize=s.mongo_min_pool_size,
|
||||
maxPoolSize=s.mongo_max_pool_size,
|
||||
uuidRepresentation="standard",
|
||||
tz_aware=True,
|
||||
)
|
||||
return _client
|
||||
|
||||
|
||||
def get_mongo_db() -> AsyncIOMotorDatabase:
|
||||
global _db
|
||||
if _db is None:
|
||||
s = get_settings()
|
||||
_db = get_mongo_client()[s.mongo_db]
|
||||
return _db
|
||||
|
||||
|
||||
def get_collection(name: MongoCollections | str) -> AsyncIOMotorCollection:
|
||||
return get_mongo_db()[name]
|
||||
|
||||
|
||||
async def create_indexes() -> None:
|
||||
"""Create (idempotent) indexes. Called once on service startup.
|
||||
|
||||
Rule of thumb for tenanted data: compound index with ``tenant_id`` first,
|
||||
then the most common filter / sort key.
|
||||
"""
|
||||
|
||||
db = get_mongo_db()
|
||||
|
||||
# --- organizations ---
|
||||
await db.organizations.create_indexes(
|
||||
[
|
||||
IndexModel("slug", unique=True, name="orgs_slug_unique"),
|
||||
]
|
||||
)
|
||||
|
||||
# --- users ---
|
||||
await db.users.create_indexes(
|
||||
[
|
||||
IndexModel([("tenant_id", 1), ("email", 1)], unique=True, name="users_tenant_email_unique"),
|
||||
]
|
||||
)
|
||||
|
||||
# --- projects ---
|
||||
await db.projects.create_indexes(
|
||||
[
|
||||
IndexModel([("tenant_id", 1), ("key", 1)], unique=True, name="projects_tenant_key_unique"),
|
||||
IndexModel([("tenant_id", 1), ("is_active", 1)], name="projects_tenant_active"),
|
||||
]
|
||||
)
|
||||
|
||||
# --- artifacts ---
|
||||
await db.artifacts.create_indexes(
|
||||
[
|
||||
IndexModel([("tenant_id", 1), ("project_id", 1)], name="arts_tenant_project"),
|
||||
IndexModel([("tenant_id", 1), ("project_id", 1), ("type", 1)], name="arts_tenant_project_type"),
|
||||
IndexModel([("tenant_id", 1), ("external_id", 1)], name="arts_tenant_external_id", sparse=True),
|
||||
IndexModel([("tenant_id", 1), ("created_at", -1)], name="arts_tenant_created_desc"),
|
||||
]
|
||||
)
|
||||
|
||||
# --- artifact_relationships ---
|
||||
# Two indexes: one for "where does this artifact point TO", one for "what points TO this artifact"
|
||||
await db.artifact_relationships.create_indexes(
|
||||
[
|
||||
IndexModel([("tenant_id", 1), ("source_artifact_id", 1), ("relationship_type", 1)],
|
||||
name="rels_tenant_source_type"),
|
||||
IndexModel([("tenant_id", 1), ("target_artifact_id", 1), ("relationship_type", 1)],
|
||||
name="rels_tenant_target_type"),
|
||||
IndexModel([("source_artifact_id", 1), ("target_artifact_id", 1), ("relationship_type", 1)],
|
||||
unique=True, name="rels_unique_edge"),
|
||||
]
|
||||
)
|
||||
|
||||
# --- jobs ---
|
||||
await db.jobs.create_indexes(
|
||||
[
|
||||
IndexModel([("tenant_id", 1), ("status", 1), ("priority", -1), ("created_at", 1)],
|
||||
name="jobs_tenant_status_priority"),
|
||||
IndexModel([("tenant_id", 1), ("project_id", 1), ("created_at", -1)],
|
||||
name="jobs_tenant_project_created_desc"),
|
||||
IndexModel([("correlation_id", 1)], name="jobs_correlation_id", sparse=True),
|
||||
]
|
||||
)
|
||||
|
||||
# --- audit_events (append-only, oldest-to-newest scans normal) ---
|
||||
await db.audit_events.create_indexes(
|
||||
[
|
||||
IndexModel([("tenant_id", 1), ("created_at", -1)], name="audit_tenant_created_desc"),
|
||||
IndexModel([("tenant_id", 1), ("actor_id", 1), ("created_at", -1)], name="audit_tenant_actor_desc"),
|
||||
IndexModel([("correlation_id", 1)], name="audit_correlation_id", sparse=True),
|
||||
]
|
||||
)
|
||||
|
||||
# --- ai_executions ---
|
||||
await db.ai_executions.create_indexes(
|
||||
[
|
||||
IndexModel([("tenant_id", 1), ("created_at", -1)], name="aie_tenant_created_desc"),
|
||||
IndexModel([("tenant_id", 1), ("job_id", 1)], name="aie_tenant_job", sparse=True),
|
||||
]
|
||||
)
|
||||
|
||||
await log.adebug("mongo_indexes_ready")
|
||||
|
||||
|
||||
async def close_mongo() -> None:
|
||||
global _client, _db
|
||||
if _client is not None:
|
||||
_client.close()
|
||||
_client = None
|
||||
_db = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def mongo_lifespan() -> AsyncIterator[None]:
|
||||
"""Async lifespan helper for FastAPI startup / shutdown."""
|
||||
await create_indexes()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await close_mongo()
|
||||
Reference in New Issue
Block a user