From 683305918cef1a6605a8c035a854806adf1b564e Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Wed, 10 Sep 2025 16:21:32 +0900 Subject: [PATCH] Step 3: MongoDB and Redis integration complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added MongoDB and Redis containers to docker-compose - Integrated Users service with MongoDB using Beanie ODM - Replaced in-memory storage with persistent MongoDB - Added proper data models with email validation - Verified data persistence with MongoDB ObjectIDs Services running: - MongoDB: Port 27017 (with health checks) - Redis: Port 6379 (with health checks) - Users service: Connected to MongoDB - Console: API Gateway routing working Test: Users now stored in MongoDB with persistence 🤖 Generated with Claude Code Co-Authored-By: Claude --- docker-compose.yml | 46 +++++- docs/PROGRESS.md | 9 +- services/users/backend/database.py | 22 +++ services/users/backend/main.py | 183 +++++++++++++++--------- services/users/backend/models.py | 24 ++++ services/users/backend/requirements.txt | 5 +- 6 files changed, 218 insertions(+), 71 deletions(-) create mode 100644 services/users/backend/database.py create mode 100644 services/users/backend/models.py diff --git a/docker-compose.yml b/docker-compose.yml index 9b47d71..bcafb57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,13 +28,57 @@ services: environment: - ENV=development - PORT=8000 + - MONGODB_URL=mongodb://mongodb:27017 + - DB_NAME=users_db volumes: - ./services/users/backend:/app networks: - site11_network restart: unless-stopped + depends_on: + - mongodb + + mongodb: + image: mongo:7.0 + container_name: site11_mongodb + environment: + - MONGO_INITDB_DATABASE=site11_db + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + networks: + - site11_network + restart: unless-stopped + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + container_name: site11_redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - site11_network + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 networks: site11_network: driver: bridge - name: site11_network \ No newline at end of file + name: site11_network + +volumes: + mongodb_data: + mongodb_config: + redis_data: \ No newline at end of file diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 0e49369..e1d2872 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -5,8 +5,8 @@ ## Current Status - **Date Started**: 2025-09-09 -- **Current Phase**: Step 2 Complete ✅ -- **Next Action**: Step 3 - Database Integration (MongoDB) +- **Current Phase**: Step 3 Complete ✅ +- **Next Action**: Step 4 - Frontend Skeleton ## Completed Checkpoints ✅ Project structure planning (CLAUDE.md) @@ -21,6 +21,11 @@ - Console API Gateway routing to Users - Service communication verified - Test: curl http://localhost:8011/api/users/users +✅ Step 3: Database Integration + - MongoDB and Redis containers added + - Users service using MongoDB with Beanie ODM + - Data persistence verified + - MongoDB IDs: 68c126c0bbbe52be68495933 ## Active Working Files ``` diff --git a/services/users/backend/database.py b/services/users/backend/database.py new file mode 100644 index 0000000..2409c81 --- /dev/null +++ b/services/users/backend/database.py @@ -0,0 +1,22 @@ +from motor.motor_asyncio import AsyncIOMotorClient +from beanie import init_beanie +import os +from models import User + + +async def init_db(): + """Initialize database connection""" + # Get MongoDB URL from environment + mongodb_url = os.getenv("MONGODB_URL", "mongodb://mongodb:27017") + db_name = os.getenv("DB_NAME", "users_db") + + # Create Motor client + client = AsyncIOMotorClient(mongodb_url) + + # Initialize beanie with the User model + await init_beanie( + database=client[db_name], + document_models=[User] + ) + + print(f"Connected to MongoDB: {mongodb_url}/{db_name}") \ No newline at end of file diff --git a/services/users/backend/main.py b/services/users/backend/main.py index 652d2ef..2996f08 100644 --- a/services/users/backend/main.py +++ b/services/users/backend/main.py @@ -4,11 +4,46 @@ from pydantic import BaseModel from typing import List, Optional from datetime import datetime import uvicorn +from contextlib import asynccontextmanager +from database import init_db +from models import User +from beanie import PydanticObjectId + + +# Pydantic models for requests +class UserCreate(BaseModel): + username: str + email: str + full_name: Optional[str] = None + +class UserUpdate(BaseModel): + username: Optional[str] = None + email: Optional[str] = None + full_name: Optional[str] = None + +class UserResponse(BaseModel): + id: str + username: str + email: str + full_name: Optional[str] = None + created_at: datetime + updated_at: datetime + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + await init_db() + yield + # Shutdown + pass + app = FastAPI( title="Users Service", - description="User management microservice", - version="0.1.0" + description="User management microservice with MongoDB", + version="0.2.0", + lifespan=lifespan ) # CORS middleware @@ -20,29 +55,6 @@ app.add_middleware( allow_headers=["*"], ) -# In-memory storage for now (will be replaced with MongoDB later) -users_db = {} -user_id_counter = 1 - -# Pydantic models -class UserCreate(BaseModel): - username: str - email: str - full_name: Optional[str] = None - -class UserUpdate(BaseModel): - username: Optional[str] = None - email: Optional[str] = None - full_name: Optional[str] = None - -class User(BaseModel): - id: int - username: str - email: str - full_name: Optional[str] = None - created_at: datetime - updated_at: datetime - # Health check @app.get("/health") async def health_check(): @@ -53,70 +65,107 @@ async def health_check(): } # CRUD Operations -@app.get("/users", response_model=List[User]) +@app.get("/users", response_model=List[UserResponse]) async def get_users(): - return list(users_db.values()) + users = await User.find_all().to_list() + return [UserResponse( + id=str(user.id), + username=user.username, + email=user.email, + full_name=user.full_name, + created_at=user.created_at, + updated_at=user.updated_at + ) for user in users] -@app.get("/users/{user_id}", response_model=User) -async def get_user(user_id: int): - if user_id not in users_db: +@app.get("/users/{user_id}", response_model=UserResponse) +async def get_user(user_id: str): + try: + user = await User.get(PydanticObjectId(user_id)) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return UserResponse( + id=str(user.id), + username=user.username, + email=user.email, + full_name=user.full_name, + created_at=user.created_at, + updated_at=user.updated_at + ) + except Exception: raise HTTPException(status_code=404, detail="User not found") - return users_db[user_id] -@app.post("/users", response_model=User, status_code=201) -async def create_user(user: UserCreate): - global user_id_counter - +@app.post("/users", response_model=UserResponse, status_code=201) +async def create_user(user_data: UserCreate): # Check if username already exists - for existing_user in users_db.values(): - if existing_user["username"] == user.username: - raise HTTPException(status_code=400, detail="Username already exists") + existing_user = await User.find_one(User.username == user_data.username) + if existing_user: + raise HTTPException(status_code=400, detail="Username already exists") - new_user = { - "id": user_id_counter, - "username": user.username, - "email": user.email, - "full_name": user.full_name, - "created_at": datetime.now(), - "updated_at": datetime.now() - } + # Create new user + user = User( + username=user_data.username, + email=user_data.email, + full_name=user_data.full_name + ) - users_db[user_id_counter] = new_user - user_id_counter += 1 + await user.create() - return new_user + return UserResponse( + id=str(user.id), + username=user.username, + email=user.email, + full_name=user.full_name, + created_at=user.created_at, + updated_at=user.updated_at + ) -@app.put("/users/{user_id}", response_model=User) -async def update_user(user_id: int, user_update: UserUpdate): - if user_id not in users_db: +@app.put("/users/{user_id}", response_model=UserResponse) +async def update_user(user_id: str, user_update: UserUpdate): + try: + user = await User.get(PydanticObjectId(user_id)) + if not user: + raise HTTPException(status_code=404, detail="User not found") + except Exception: raise HTTPException(status_code=404, detail="User not found") - user = users_db[user_id] - if user_update.username is not None: # Check if new username already exists - for uid, existing_user in users_db.items(): - if uid != user_id and existing_user["username"] == user_update.username: - raise HTTPException(status_code=400, detail="Username already exists") - user["username"] = user_update.username + existing_user = await User.find_one( + User.username == user_update.username, + User.id != user.id + ) + if existing_user: + raise HTTPException(status_code=400, detail="Username already exists") + user.username = user_update.username if user_update.email is not None: - user["email"] = user_update.email + user.email = user_update.email if user_update.full_name is not None: - user["full_name"] = user_update.full_name + user.full_name = user_update.full_name - user["updated_at"] = datetime.now() + user.updated_at = datetime.now() + await user.save() - return user + return UserResponse( + id=str(user.id), + username=user.username, + email=user.email, + full_name=user.full_name, + created_at=user.created_at, + updated_at=user.updated_at + ) @app.delete("/users/{user_id}") -async def delete_user(user_id: int): - if user_id not in users_db: +async def delete_user(user_id: str): + try: + user = await User.get(PydanticObjectId(user_id)) + if not user: + raise HTTPException(status_code=404, detail="User not found") + await user.delete() + return {"message": "User deleted successfully"} + except Exception: raise HTTPException(status_code=404, detail="User not found") - - del users_db[user_id] - return {"message": "User deleted successfully"} if __name__ == "__main__": uvicorn.run( diff --git a/services/users/backend/models.py b/services/users/backend/models.py new file mode 100644 index 0000000..8e31e23 --- /dev/null +++ b/services/users/backend/models.py @@ -0,0 +1,24 @@ +from beanie import Document +from pydantic import EmailStr, Field +from datetime import datetime +from typing import Optional + + +class User(Document): + username: str = Field(..., unique=True) + email: EmailStr + full_name: Optional[str] = None + created_at: datetime = Field(default_factory=datetime.now) + updated_at: datetime = Field(default_factory=datetime.now) + + class Settings: + collection = "users" + + class Config: + json_schema_extra = { + "example": { + "username": "john_doe", + "email": "john@example.com", + "full_name": "John Doe" + } + } \ No newline at end of file diff --git a/services/users/backend/requirements.txt b/services/users/backend/requirements.txt index 7da3bfd..5be6a5f 100644 --- a/services/users/backend/requirements.txt +++ b/services/users/backend/requirements.txt @@ -1,3 +1,6 @@ fastapi==0.109.0 uvicorn[standard]==0.27.0 -pydantic==2.5.3 \ No newline at end of file +pydantic[email]==2.5.3 +pymongo==4.6.1 +motor==3.3.2 +beanie==1.23.6 \ No newline at end of file