CLAUDE: Complete PostgreSQL migration for custom commands
- Update CLAUDE.md to reflect PostgreSQL-only architecture - Add table_name Meta class to CustomCommand models for PostgreSQL - Remove SQLite-specific LIKE queries, use PostgreSQL ILIKE - Refactor custom command creator info handling - Add helper functions for database operations - Fix creator data serialization in execute endpoint 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4db6982bc5
commit
e75c1fbc7d
25
CLAUDE.md
25
CLAUDE.md
@ -57,10 +57,10 @@ app/
|
||||
```
|
||||
|
||||
### Database Configuration
|
||||
- **Primary**: PostgreSQL via `PooledPostgresqlDatabase` (production)
|
||||
- **Fallback**: SQLite with WAL mode (`storage/sba_master.db`)
|
||||
- **Database**: PostgreSQL via `PooledPostgresqlDatabase` (production and development)
|
||||
- **ORM**: Peewee with model-based schema definition
|
||||
- **Migrations**: Manual migrations via `playhouse.migrate`
|
||||
- **Migrations**: SQL migration files in `migrations/` directory
|
||||
- **Table Naming**: Peewee models must specify `Meta.table_name` to match PostgreSQL table names
|
||||
|
||||
### Authentication & Error Handling
|
||||
- **Authentication**: OAuth2 bearer token validation via `API_TOKEN` environment variable
|
||||
@ -76,10 +76,13 @@ app/
|
||||
### Environment Variables
|
||||
**Required**:
|
||||
- `API_TOKEN` - Authentication token for API access
|
||||
- `DATABASE_TYPE` - 'postgresql' or 'sqlite' (defaults to sqlite)
|
||||
|
||||
**PostgreSQL Configuration**:
|
||||
- `POSTGRES_HOST`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_PORT`
|
||||
**PostgreSQL Configuration** (required):
|
||||
- `POSTGRES_HOST` - PostgreSQL server hostname (default: 10.10.0.42)
|
||||
- `POSTGRES_DB` - Database name (default: sba_master)
|
||||
- `POSTGRES_USER` - Database user (default: sba_admin)
|
||||
- `POSTGRES_PASSWORD` - Database password
|
||||
- `POSTGRES_PORT` - Database port (default: 5432)
|
||||
|
||||
**Optional**:
|
||||
- `LOG_LEVEL` - INFO or WARNING (defaults to WARNING)
|
||||
@ -92,6 +95,8 @@ app/
|
||||
- **Draft System**: Draft picks, draft lists, keeper selections
|
||||
- **Game Data**: Schedules, results, Strat-o-Matic play-by-play data
|
||||
- **League Management**: Standings, transactions, injuries, decisions
|
||||
- **Custom Commands**: User-created Discord bot commands with creator tracking and usage statistics
|
||||
- **Help Commands**: Static help text for Discord bot commands
|
||||
|
||||
### Testing & Quality
|
||||
- **No formal test framework currently configured**
|
||||
@ -103,6 +108,8 @@ app/
|
||||
|
||||
- All active development occurs in the `/app` directory
|
||||
- Root directory files (`main.py`, `db_engine.py`, etc.) are legacy and not in use
|
||||
- The system supports both SQLite (development) and PostgreSQL (production) backends
|
||||
- Database migrations must be manually coded using Peewee's migration system
|
||||
- Authentication is required for all endpoints except documentation
|
||||
- **PostgreSQL migration completed**: System now uses PostgreSQL exclusively (no SQLite fallback)
|
||||
- Database migrations are SQL files in `migrations/` directory, applied manually via psql
|
||||
- Authentication is required for all endpoints except documentation and public read endpoints
|
||||
- **Peewee Models**: Always specify `Meta.table_name` to match PostgreSQL naming conventions
|
||||
- **Custom Commands**: Fully functional with creator tracking, usage statistics, and Discord bot integration
|
||||
@ -2235,6 +2235,9 @@ class CustomCommandCreator(BaseModel):
|
||||
total_commands = IntegerField(default=0)
|
||||
active_commands = IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
table_name = 'custom_command_creators'
|
||||
|
||||
|
||||
class CustomCommand(BaseModel):
|
||||
"""Model for custom commands created by users."""
|
||||
@ -2255,6 +2258,9 @@ class CustomCommand(BaseModel):
|
||||
is_active = BooleanField(default=True)
|
||||
tags = TextField(null=True) # JSON string for tags list
|
||||
|
||||
class Meta:
|
||||
table_name = 'custom_commands'
|
||||
|
||||
@staticmethod
|
||||
def get_by_name(name: str):
|
||||
"""Get a custom command by name (case-insensitive)."""
|
||||
|
||||
@ -92,6 +92,99 @@ def update_creator_stats(creator_id: int):
|
||||
creator.save()
|
||||
|
||||
|
||||
def get_custom_command_by_name(name: str):
|
||||
"""Get a custom command by name with creator info"""
|
||||
command = CustomCommand.select(
|
||||
CustomCommand,
|
||||
CustomCommandCreator
|
||||
).join(
|
||||
CustomCommandCreator, on=(CustomCommand.creator == CustomCommandCreator.id)
|
||||
).where(
|
||||
fn.LOWER(CustomCommand.name) == name.lower()
|
||||
).first()
|
||||
|
||||
if command:
|
||||
result = model_to_dict(command, recurse=False)
|
||||
# Ensure creator_id is in the result (it should be from model_to_dict, but make sure)
|
||||
result['creator_id'] = command.creator.id
|
||||
result['creator_discord_id'] = command.creator.discord_id
|
||||
result['creator_username'] = command.creator.username
|
||||
result['creator_display_name'] = command.creator.display_name
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
def get_custom_command_by_id(command_id: int):
|
||||
"""Get a custom command by ID with creator info"""
|
||||
command = CustomCommand.select(
|
||||
CustomCommand,
|
||||
CustomCommandCreator
|
||||
).join(
|
||||
CustomCommandCreator, on=(CustomCommand.creator == CustomCommandCreator.id)
|
||||
).where(
|
||||
CustomCommand.id == command_id
|
||||
).first()
|
||||
|
||||
if command:
|
||||
result = model_to_dict(command, recurse=False)
|
||||
result['creator_db_id'] = command.creator.id
|
||||
result['creator_discord_id'] = command.creator.discord_id
|
||||
result['creator_username'] = command.creator.username
|
||||
result['creator_display_name'] = command.creator.display_name
|
||||
result['creator_created_at'] = command.creator.created_at
|
||||
result['total_commands'] = command.creator.total_commands
|
||||
result['active_commands'] = command.creator.active_commands
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
def create_custom_command(command_data: dict) -> int:
|
||||
"""Create a new custom command and return its ID"""
|
||||
# Convert tags list to JSON if present
|
||||
if 'tags' in command_data and isinstance(command_data['tags'], list):
|
||||
command_data['tags'] = json.dumps(command_data['tags'])
|
||||
|
||||
# Set created_at if not provided
|
||||
if 'created_at' not in command_data:
|
||||
command_data['created_at'] = datetime.now()
|
||||
|
||||
command = CustomCommand.create(**command_data)
|
||||
return command.id
|
||||
|
||||
|
||||
def update_custom_command(command_id: int, update_data: dict):
|
||||
"""Update an existing custom command"""
|
||||
# Convert tags list to JSON if present
|
||||
if 'tags' in update_data and isinstance(update_data['tags'], list):
|
||||
update_data['tags'] = json.dumps(update_data['tags'])
|
||||
|
||||
query = CustomCommand.update(**update_data).where(CustomCommand.id == command_id)
|
||||
query.execute()
|
||||
|
||||
|
||||
def delete_custom_command(command_id: int):
|
||||
"""Delete a custom command"""
|
||||
query = CustomCommand.delete().where(CustomCommand.id == command_id)
|
||||
query.execute()
|
||||
|
||||
|
||||
def get_creator_by_discord_id(discord_id: int):
|
||||
"""Get a creator by Discord ID"""
|
||||
creator = CustomCommandCreator.get_or_none(CustomCommandCreator.discord_id == str(discord_id))
|
||||
if creator:
|
||||
return model_to_dict(creator)
|
||||
return None
|
||||
|
||||
|
||||
def create_creator(creator_data: dict) -> int:
|
||||
"""Create a new creator and return their ID"""
|
||||
if 'created_at' not in creator_data:
|
||||
creator_data['created_at'] = datetime.now()
|
||||
|
||||
creator = CustomCommandCreator.create(**creator_data)
|
||||
return creator.id
|
||||
|
||||
|
||||
# API Endpoints
|
||||
|
||||
@router.get('')
|
||||
@ -113,7 +206,7 @@ async def get_custom_commands(
|
||||
params = []
|
||||
|
||||
if name is not None:
|
||||
where_conditions.append("LOWER(cc.name) LIKE LOWER(%s)" if DATABASE_TYPE.lower() == 'sqlite' else "cc.name ILIKE %s")
|
||||
where_conditions.append("cc.name ILIKE %s")
|
||||
params.append(f"%{name}%")
|
||||
|
||||
if creator_discord_id is not None:
|
||||
@ -798,7 +891,7 @@ async def execute_custom_command(
|
||||
|
||||
update_custom_command(command_id, update_data)
|
||||
|
||||
# Return updated command
|
||||
# Return updated command - get_custom_command_by_id already has all creator info
|
||||
updated_result = get_custom_command_by_id(command_id)
|
||||
updated_dict = dict(updated_result)
|
||||
|
||||
@ -811,33 +904,21 @@ async def execute_custom_command(
|
||||
except:
|
||||
updated_dict['tags'] = []
|
||||
|
||||
# Add creator info - get full creator record
|
||||
creator_id = updated_dict['creator_id']
|
||||
creator_cursor = db.execute_sql("SELECT * FROM custom_command_creators WHERE id = %s", (creator_id,))
|
||||
creator_result = creator_cursor.fetchone()
|
||||
if creator_result:
|
||||
creator_columns = [desc[0] for desc in creator_cursor.description]
|
||||
creator_dict = dict(zip(creator_columns, creator_result))
|
||||
# Convert creator datetime to ISO string
|
||||
convert_datetime_to_iso(creator_dict, fields=['created_at'])
|
||||
updated_dict['creator'] = creator_dict
|
||||
else:
|
||||
# Fallback to basic info if full creator not found
|
||||
# Build creator object from the fields returned by get_custom_command_by_id
|
||||
creator_created_at = updated_dict.pop('creator_created_at')
|
||||
if hasattr(creator_created_at, 'isoformat'):
|
||||
creator_created_at = creator_created_at.isoformat()
|
||||
|
||||
updated_dict['creator'] = {
|
||||
'id': creator_id,
|
||||
'id': updated_dict.pop('creator_db_id'),
|
||||
'discord_id': updated_dict.pop('creator_discord_id'),
|
||||
'username': updated_dict.pop('creator_username'),
|
||||
'display_name': updated_dict.pop('creator_display_name'),
|
||||
'created_at': updated_dict['created_at'], # Use command creation as fallback
|
||||
'total_commands': 0,
|
||||
'active_commands': 0
|
||||
'created_at': creator_created_at,
|
||||
'total_commands': updated_dict.pop('total_commands'),
|
||||
'active_commands': updated_dict.pop('active_commands')
|
||||
}
|
||||
|
||||
# Remove the duplicate fields
|
||||
updated_dict.pop('creator_discord_id', None)
|
||||
updated_dict.pop('creator_username', None)
|
||||
updated_dict.pop('creator_display_name', None)
|
||||
|
||||
return updated_dict
|
||||
|
||||
except HTTPException:
|
||||
@ -858,10 +939,9 @@ async def get_command_names_for_autocomplete(
|
||||
"""Get command names for Discord autocomplete"""
|
||||
try:
|
||||
if partial_name:
|
||||
like_clause = "LOWER(name) LIKE LOWER(%s)" if DATABASE_TYPE.lower() == 'sqlite' else "name ILIKE %s"
|
||||
results = db.execute_sql(f"""
|
||||
results = db.execute_sql("""
|
||||
SELECT name FROM custom_commands
|
||||
WHERE is_active = TRUE AND {like_clause}
|
||||
WHERE is_active = TRUE AND name ILIKE %s
|
||||
ORDER BY name
|
||||
LIMIT %s
|
||||
""", (f"%{partial_name}%", limit)).fetchall()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user