import os from typing import List, Optional, Dict, Any import psycopg2 from psycopg2.extras import RealDictCursor def get_db_connection(): """Get database connection""" return psycopg2.connect( host=os.getenv("DB_HOST", "localhost"), port=int(os.getenv("DB_PORT", "5432")), database=os.getenv("DB_NAME", "recipes_db"), user=os.getenv("DB_USER", "recipes_user"), password=os.getenv("DB_PASSWORD", "recipes_password"), ) def create_grocery_list(owner_id: int, name: str, items: List[str] = None) -> Dict[str, Any]: """Create a new grocery list""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: items = items or [] cur.execute( """ INSERT INTO grocery_lists (owner_id, name, items) VALUES (%s, %s, %s) RETURNING id, name, items, owner_id, created_at, updated_at """, (owner_id, name, items) ) grocery_list = cur.fetchone() conn.commit() return dict(grocery_list) finally: cur.close() conn.close() def get_user_grocery_lists(user_id: int) -> List[Dict[str, Any]]: """Get all grocery lists owned by or shared with a user""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( """ SELECT DISTINCT gl.id, gl.name, gl.items, gl.owner_id, gl.is_pinned, gl.created_at, gl.updated_at, u.display_name as owner_display_name, CASE WHEN gl.owner_id = %s THEN TRUE ELSE gls.can_edit END as can_edit, CASE WHEN gl.owner_id = %s THEN TRUE ELSE FALSE END as is_owner FROM grocery_lists gl LEFT JOIN users u ON gl.owner_id = u.id LEFT JOIN grocery_list_shares gls ON gl.id = gls.list_id AND gls.shared_with_user_id = %s WHERE gl.owner_id = %s OR gls.shared_with_user_id = %s ORDER BY gl.updated_at DESC """, (user_id, user_id, user_id, user_id, user_id) ) lists = cur.fetchall() return [dict(row) for row in lists] finally: cur.close() conn.close() def get_grocery_list_by_id(list_id: int, user_id: int) -> Optional[Dict[str, Any]]: """Get a specific grocery list if user has access""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( """ SELECT gl.id, gl.name, gl.items, gl.owner_id, gl.is_pinned, gl.created_at, gl.updated_at, u.display_name as owner_display_name, CASE WHEN gl.owner_id = %s THEN TRUE ELSE gls.can_edit END as can_edit, CASE WHEN gl.owner_id = %s THEN TRUE ELSE FALSE END as is_owner FROM grocery_lists gl LEFT JOIN users u ON gl.owner_id = u.id LEFT JOIN grocery_list_shares gls ON gl.id = gls.list_id AND gls.shared_with_user_id = %s WHERE gl.id = %s AND (gl.owner_id = %s OR gls.shared_with_user_id = %s) """, (user_id, user_id, user_id, list_id, user_id, user_id) ) grocery_list = cur.fetchone() return dict(grocery_list) if grocery_list else None finally: cur.close() conn.close() def update_grocery_list(list_id: int, name: str = None, items: List[str] = None) -> Optional[Dict[str, Any]]: """Update a grocery list""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: updates = [] params = [] if name is not None: updates.append("name = %s") params.append(name) if items is not None: updates.append("items = %s") params.append(items) if not updates: return None updates.append("updated_at = CURRENT_TIMESTAMP") params.append(list_id) query = f"UPDATE grocery_lists SET {', '.join(updates)} WHERE id = %s RETURNING id, name, items, owner_id, created_at, updated_at" cur.execute(query, params) grocery_list = cur.fetchone() conn.commit() return dict(grocery_list) if grocery_list else None finally: cur.close() conn.close() def delete_grocery_list(list_id: int) -> bool: """Delete a grocery list""" conn = get_db_connection() cur = conn.cursor() try: cur.execute("DELETE FROM grocery_lists WHERE id = %s", (list_id,)) deleted = cur.rowcount > 0 conn.commit() return deleted finally: cur.close() conn.close() def share_grocery_list(list_id: int, shared_with_user_id: int, can_edit: bool = False) -> Dict[str, Any]: """Share a grocery list with another user""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( """ INSERT INTO grocery_list_shares (list_id, shared_with_user_id, can_edit) VALUES (%s, %s, %s) ON CONFLICT (list_id, shared_with_user_id) DO UPDATE SET can_edit = EXCLUDED.can_edit, shared_at = CURRENT_TIMESTAMP RETURNING id, list_id, shared_with_user_id, can_edit, shared_at """, (list_id, shared_with_user_id, can_edit) ) share = cur.fetchone() conn.commit() return dict(share) finally: cur.close() conn.close() def unshare_grocery_list(list_id: int, user_id: int) -> bool: """Remove sharing access for a user""" conn = get_db_connection() cur = conn.cursor() try: cur.execute( "DELETE FROM grocery_list_shares WHERE list_id = %s AND shared_with_user_id = %s", (list_id, user_id) ) deleted = cur.rowcount > 0 conn.commit() return deleted finally: cur.close() conn.close() def get_grocery_list_shares(list_id: int) -> List[Dict[str, Any]]: """Get all users a grocery list is shared with""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( """ SELECT gls.id, gls.list_id, gls.shared_with_user_id, gls.can_edit, gls.shared_at, u.username, u.display_name, u.email FROM grocery_list_shares gls JOIN users u ON gls.shared_with_user_id = u.id WHERE gls.list_id = %s ORDER BY gls.shared_at DESC """, (list_id,) ) shares = cur.fetchall() return [dict(row) for row in shares] finally: cur.close() conn.close() def search_users(query: str, limit: int = 10) -> List[Dict[str, Any]]: """Search users by username or display_name for autocomplete""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( """ SELECT id, username, display_name, email FROM users WHERE username ILIKE %s OR display_name ILIKE %s ORDER BY username LIMIT %s """, (f"%{query}%", f"%{query}%", limit) ) users = cur.fetchall() return [dict(row) for row in users] finally: cur.close() conn.close() def toggle_grocery_list_pin(list_id: int, user_id: int) -> Optional[Dict[str, Any]]: """Toggle pin status for a grocery list (owner only)""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: # Check if user is owner cur.execute( "SELECT id, is_pinned FROM grocery_lists WHERE id = %s AND owner_id = %s", (list_id, user_id) ) result = cur.fetchone() if not result: return None # Toggle pin status new_pin_status = not result["is_pinned"] cur.execute( """ UPDATE grocery_lists SET is_pinned = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s RETURNING id, name, items, owner_id, is_pinned, created_at, updated_at """, (new_pin_status, list_id) ) updated = cur.fetchone() conn.commit() return dict(updated) if updated else None finally: cur.close() conn.close()