claude-code-templates 1.15.0 → 1.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/bin/create-claude-config.js +15 -8
- package/package.json +2 -3
- package/src/analytics/core/AgentAnalyzer.js +17 -3
- package/src/analytics/core/ProcessDetector.js +23 -7
- package/src/analytics/core/StateCalculator.js +102 -33
- package/src/analytics/data/DataCache.js +7 -7
- package/src/analytics-web/chats_mobile.html +2590 -0
- package/src/analytics-web/components/App.js +10 -10
- package/src/analytics-web/components/SessionTimer.js +1 -1
- package/src/analytics-web/components/Sidebar.js +5 -14
- package/src/analytics-web/index.html +932 -78
- package/src/analytics.js +263 -5
- package/src/chats-mobile.js +682 -0
- package/src/claude-api-proxy.js +460 -0
- package/src/file-operations.js +422 -83
- package/src/health-check.js +310 -0
- package/src/index.js +944 -56
- package/src/tracking-service.js +31 -34
- package/components/agents/api-security-audit.md +0 -92
- package/components/agents/database-optimization.md +0 -94
- package/components/agents/react-performance-optimization.md +0 -64
- package/components/commands/check-file.md +0 -53
- package/components/commands/generate-tests.md +0 -68
- package/components/mcps/deepgraph-nextjs.json +0 -12
- package/components/mcps/deepgraph-react.json +0 -12
- package/components/mcps/deepgraph-typescript.json +0 -12
- package/components/mcps/deepgraph-vue.json +0 -12
- package/components/mcps/filesystem-access.json +0 -12
- package/components/mcps/github-integration.json +0 -11
- package/components/mcps/memory-integration.json +0 -8
- package/components/mcps/mysql-integration.json +0 -11
- package/components/mcps/postgresql-integration.json +0 -11
- package/components/mcps/web-fetch.json +0 -8
- package/src/analytics-web/components/AgentsPage.js +0 -4761
- package/templates/common/.claude/commands/git-workflow.md +0 -239
- package/templates/common/.claude/commands/project-setup.md +0 -316
- package/templates/common/.mcp.json +0 -41
- package/templates/common/CLAUDE.md +0 -109
- package/templates/common/README.md +0 -96
- package/templates/go/.mcp.json +0 -78
- package/templates/go/README.md +0 -25
- package/templates/javascript-typescript/.claude/commands/api-endpoint.md +0 -51
- package/templates/javascript-typescript/.claude/commands/debug.md +0 -52
- package/templates/javascript-typescript/.claude/commands/lint.md +0 -48
- package/templates/javascript-typescript/.claude/commands/npm-scripts.md +0 -48
- package/templates/javascript-typescript/.claude/commands/refactor.md +0 -55
- package/templates/javascript-typescript/.claude/commands/test.md +0 -61
- package/templates/javascript-typescript/.claude/commands/typescript-migrate.md +0 -51
- package/templates/javascript-typescript/.claude/settings.json +0 -142
- package/templates/javascript-typescript/.mcp.json +0 -80
- package/templates/javascript-typescript/CLAUDE.md +0 -185
- package/templates/javascript-typescript/README.md +0 -259
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/components.md +0 -63
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/services.md +0 -62
- package/templates/javascript-typescript/examples/node-api/.claude/commands/api-endpoint.md +0 -46
- package/templates/javascript-typescript/examples/node-api/.claude/commands/database.md +0 -56
- package/templates/javascript-typescript/examples/node-api/.claude/commands/middleware.md +0 -61
- package/templates/javascript-typescript/examples/node-api/.claude/commands/route.md +0 -57
- package/templates/javascript-typescript/examples/node-api/CLAUDE.md +0 -102
- package/templates/javascript-typescript/examples/react-app/.claude/commands/component.md +0 -29
- package/templates/javascript-typescript/examples/react-app/.claude/commands/hooks.md +0 -44
- package/templates/javascript-typescript/examples/react-app/.claude/commands/state-management.md +0 -45
- package/templates/javascript-typescript/examples/react-app/CLAUDE.md +0 -81
- package/templates/javascript-typescript/examples/react-app/agents/react-performance-optimization.md +0 -530
- package/templates/javascript-typescript/examples/react-app/agents/react-state-management.md +0 -295
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/components.md +0 -46
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/composables.md +0 -51
- package/templates/python/.claude/commands/lint.md +0 -111
- package/templates/python/.claude/commands/test.md +0 -73
- package/templates/python/.claude/settings.json +0 -153
- package/templates/python/.mcp.json +0 -78
- package/templates/python/CLAUDE.md +0 -276
- package/templates/python/examples/django-app/.claude/commands/admin.md +0 -264
- package/templates/python/examples/django-app/.claude/commands/django-model.md +0 -124
- package/templates/python/examples/django-app/.claude/commands/views.md +0 -222
- package/templates/python/examples/django-app/CLAUDE.md +0 -313
- package/templates/python/examples/django-app/agents/django-api-security.md +0 -642
- package/templates/python/examples/django-app/agents/django-database-optimization.md +0 -752
- package/templates/python/examples/fastapi-app/.claude/commands/api-endpoints.md +0 -513
- package/templates/python/examples/fastapi-app/.claude/commands/auth.md +0 -775
- package/templates/python/examples/fastapi-app/.claude/commands/database.md +0 -657
- package/templates/python/examples/fastapi-app/.claude/commands/deployment.md +0 -160
- package/templates/python/examples/fastapi-app/.claude/commands/testing.md +0 -927
- package/templates/python/examples/fastapi-app/CLAUDE.md +0 -229
- package/templates/python/examples/flask-app/.claude/commands/app-factory.md +0 -384
- package/templates/python/examples/flask-app/.claude/commands/blueprint.md +0 -243
- package/templates/python/examples/flask-app/.claude/commands/database.md +0 -410
- package/templates/python/examples/flask-app/.claude/commands/deployment.md +0 -620
- package/templates/python/examples/flask-app/.claude/commands/flask-route.md +0 -217
- package/templates/python/examples/flask-app/.claude/commands/testing.md +0 -559
- package/templates/python/examples/flask-app/CLAUDE.md +0 -391
- package/templates/ruby/.claude/commands/model.md +0 -360
- package/templates/ruby/.claude/commands/test.md +0 -480
- package/templates/ruby/.claude/settings.json +0 -146
- package/templates/ruby/.mcp.json +0 -83
- package/templates/ruby/CLAUDE.md +0 -284
- package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +0 -490
- package/templates/ruby/examples/rails-app/CLAUDE.md +0 -376
- package/templates/rust/.mcp.json +0 -78
- package/templates/rust/README.md +0 -26
|
@@ -1,657 +0,0 @@
|
|
|
1
|
-
# FastAPI Database Integration
|
|
2
|
-
|
|
3
|
-
Complete database setup with SQLAlchemy, Alembic, and async support for FastAPI.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Initialize Alembic
|
|
9
|
-
alembic init alembic
|
|
10
|
-
|
|
11
|
-
# Create migration
|
|
12
|
-
alembic revision --autogenerate -m "Initial migration"
|
|
13
|
-
|
|
14
|
-
# Apply migrations
|
|
15
|
-
alembic upgrade head
|
|
16
|
-
|
|
17
|
-
# Downgrade migration
|
|
18
|
-
alembic downgrade -1
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Database Configuration
|
|
22
|
-
|
|
23
|
-
```python
|
|
24
|
-
# app/core/config.py
|
|
25
|
-
from pydantic import BaseSettings, PostgresDsn, validator
|
|
26
|
-
from typing import Optional, Dict, Any
|
|
27
|
-
import os
|
|
28
|
-
|
|
29
|
-
class Settings(BaseSettings):
|
|
30
|
-
"""Application settings."""
|
|
31
|
-
|
|
32
|
-
# Database
|
|
33
|
-
POSTGRES_SERVER: str = "localhost"
|
|
34
|
-
POSTGRES_USER: str = "postgres"
|
|
35
|
-
POSTGRES_PASSWORD: str = "password"
|
|
36
|
-
POSTGRES_DB: str = "fastapi_app"
|
|
37
|
-
POSTGRES_PORT: str = "5432"
|
|
38
|
-
DATABASE_URL: Optional[PostgresDsn] = None
|
|
39
|
-
|
|
40
|
-
@validator("DATABASE_URL", pre=True)
|
|
41
|
-
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
|
|
42
|
-
if isinstance(v, str):
|
|
43
|
-
return v
|
|
44
|
-
return PostgresDsn.build(
|
|
45
|
-
scheme="postgresql+asyncpg",
|
|
46
|
-
user=values.get("POSTGRES_USER"),
|
|
47
|
-
password=values.get("POSTGRES_PASSWORD"),
|
|
48
|
-
host=values.get("POSTGRES_SERVER"),
|
|
49
|
-
port=values.get("POSTGRES_PORT"),
|
|
50
|
-
path=f"/{values.get('POSTGRES_DB') or ''}",
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# Redis
|
|
54
|
-
REDIS_URL: str = "redis://localhost:6379/0"
|
|
55
|
-
|
|
56
|
-
# Database settings
|
|
57
|
-
DATABASE_POOL_SIZE: int = 10
|
|
58
|
-
DATABASE_MAX_OVERFLOW: int = 20
|
|
59
|
-
DATABASE_POOL_RECYCLE: int = 3600
|
|
60
|
-
|
|
61
|
-
class Config:
|
|
62
|
-
env_file = ".env"
|
|
63
|
-
case_sensitive = True
|
|
64
|
-
|
|
65
|
-
settings = Settings()
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Database Setup
|
|
69
|
-
|
|
70
|
-
```python
|
|
71
|
-
# app/db/database.py
|
|
72
|
-
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
73
|
-
from sqlalchemy.ext.declarative import declarative_base
|
|
74
|
-
from sqlalchemy.orm import sessionmaker
|
|
75
|
-
from app.core.config import settings
|
|
76
|
-
|
|
77
|
-
# Create async engine
|
|
78
|
-
engine = create_async_engine(
|
|
79
|
-
str(settings.DATABASE_URL),
|
|
80
|
-
pool_size=settings.DATABASE_POOL_SIZE,
|
|
81
|
-
max_overflow=settings.DATABASE_MAX_OVERFLOW,
|
|
82
|
-
pool_recycle=settings.DATABASE_POOL_RECYCLE,
|
|
83
|
-
pool_pre_ping=True,
|
|
84
|
-
echo=False # Set to True for SQL debugging
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Create async session factory
|
|
88
|
-
AsyncSessionLocal = sessionmaker(
|
|
89
|
-
engine, class_=AsyncSession, expire_on_commit=False
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
# Base class for models
|
|
93
|
-
Base = declarative_base()
|
|
94
|
-
|
|
95
|
-
async def get_db() -> AsyncSession:
|
|
96
|
-
"""Dependency to get database session."""
|
|
97
|
-
async with AsyncSessionLocal() as session:
|
|
98
|
-
try:
|
|
99
|
-
yield session
|
|
100
|
-
finally:
|
|
101
|
-
await session.close()
|
|
102
|
-
|
|
103
|
-
async def create_tables():
|
|
104
|
-
"""Create database tables."""
|
|
105
|
-
async with engine.begin() as conn:
|
|
106
|
-
await conn.run_sync(Base.metadata.create_all)
|
|
107
|
-
|
|
108
|
-
async def drop_tables():
|
|
109
|
-
"""Drop database tables."""
|
|
110
|
-
async with engine.begin() as conn:
|
|
111
|
-
await conn.run_sync(Base.metadata.drop_all)
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Base Model
|
|
115
|
-
|
|
116
|
-
```python
|
|
117
|
-
# app/models/base.py
|
|
118
|
-
from sqlalchemy import Column, Integer, DateTime, func
|
|
119
|
-
from sqlalchemy.ext.declarative import declared_attr
|
|
120
|
-
from app.db.database import Base
|
|
121
|
-
from datetime import datetime
|
|
122
|
-
from typing import Any
|
|
123
|
-
|
|
124
|
-
class TimestampMixin:
|
|
125
|
-
"""Mixin for timestamp fields."""
|
|
126
|
-
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
127
|
-
updated_at = Column(
|
|
128
|
-
DateTime(timezone=True),
|
|
129
|
-
server_default=func.now(),
|
|
130
|
-
onupdate=func.now(),
|
|
131
|
-
nullable=False
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
class BaseModel(Base, TimestampMixin):
|
|
135
|
-
"""Base model with common functionality."""
|
|
136
|
-
__abstract__ = True
|
|
137
|
-
|
|
138
|
-
id = Column(Integer, primary_key=True, index=True)
|
|
139
|
-
|
|
140
|
-
@declared_attr
|
|
141
|
-
def __tablename__(cls) -> str:
|
|
142
|
-
return cls.__name__.lower()
|
|
143
|
-
|
|
144
|
-
def dict(self, exclude: set = None) -> dict[str, Any]:
|
|
145
|
-
"""Convert model to dictionary."""
|
|
146
|
-
exclude = exclude or set()
|
|
147
|
-
return {
|
|
148
|
-
column.name: getattr(self, column.name)
|
|
149
|
-
for column in self.__table__.columns
|
|
150
|
-
if column.name not in exclude
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
def __repr__(self) -> str:
|
|
154
|
-
return f"<{self.__class__.__name__}(id={self.id})>"
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Example Models
|
|
158
|
-
|
|
159
|
-
```python
|
|
160
|
-
# app/models/user.py
|
|
161
|
-
from sqlalchemy import Column, String, Boolean, Text, Index
|
|
162
|
-
from sqlalchemy.orm import relationship
|
|
163
|
-
from app.models.base import BaseModel
|
|
164
|
-
from passlib.context import CryptContext
|
|
165
|
-
|
|
166
|
-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
167
|
-
|
|
168
|
-
class User(BaseModel):
|
|
169
|
-
"""User model."""
|
|
170
|
-
__tablename__ = "users"
|
|
171
|
-
|
|
172
|
-
username = Column(String(50), unique=True, index=True, nullable=False)
|
|
173
|
-
email = Column(String(100), unique=True, index=True, nullable=False)
|
|
174
|
-
hashed_password = Column(String(255), nullable=False)
|
|
175
|
-
first_name = Column(String(50), nullable=False)
|
|
176
|
-
last_name = Column(String(50), nullable=False)
|
|
177
|
-
is_active = Column(Boolean, default=True, nullable=False)
|
|
178
|
-
is_superuser = Column(Boolean, default=False, nullable=False)
|
|
179
|
-
bio = Column(Text)
|
|
180
|
-
|
|
181
|
-
# Relationships
|
|
182
|
-
posts = relationship("Post", back_populates="author", cascade="all, delete-orphan")
|
|
183
|
-
|
|
184
|
-
# Indexes
|
|
185
|
-
__table_args__ = (
|
|
186
|
-
Index('idx_user_email_active', email, is_active),
|
|
187
|
-
Index('idx_user_username_active', username, is_active),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
def verify_password(self, password: str) -> bool:
|
|
191
|
-
"""Verify password against hash."""
|
|
192
|
-
return pwd_context.verify(password, self.hashed_password)
|
|
193
|
-
|
|
194
|
-
@staticmethod
|
|
195
|
-
def get_password_hash(password: str) -> str:
|
|
196
|
-
"""Generate password hash."""
|
|
197
|
-
return pwd_context.hash(password)
|
|
198
|
-
|
|
199
|
-
def set_password(self, password: str) -> None:
|
|
200
|
-
"""Set user password."""
|
|
201
|
-
self.hashed_password = self.get_password_hash(password)
|
|
202
|
-
|
|
203
|
-
@property
|
|
204
|
-
def full_name(self) -> str:
|
|
205
|
-
"""Get user's full name."""
|
|
206
|
-
return f"{self.first_name} {self.last_name}"
|
|
207
|
-
|
|
208
|
-
def dict(self, exclude: set = None) -> dict:
|
|
209
|
-
"""Convert to dict excluding sensitive data."""
|
|
210
|
-
exclude = exclude or set()
|
|
211
|
-
exclude.add('hashed_password')
|
|
212
|
-
return super().dict(exclude=exclude)
|
|
213
|
-
|
|
214
|
-
# app/models/post.py
|
|
215
|
-
from sqlalchemy import Column, String, Text, ForeignKey, Boolean, Index
|
|
216
|
-
from sqlalchemy.orm import relationship
|
|
217
|
-
from app.models.base import BaseModel
|
|
218
|
-
|
|
219
|
-
class Post(BaseModel):
|
|
220
|
-
"""Blog post model."""
|
|
221
|
-
__tablename__ = "posts"
|
|
222
|
-
|
|
223
|
-
title = Column(String(200), nullable=False, index=True)
|
|
224
|
-
content = Column(Text, nullable=False)
|
|
225
|
-
slug = Column(String(200), unique=True, nullable=False, index=True)
|
|
226
|
-
is_published = Column(Boolean, default=False, nullable=False)
|
|
227
|
-
|
|
228
|
-
# Foreign keys
|
|
229
|
-
author_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
230
|
-
|
|
231
|
-
# Relationships
|
|
232
|
-
author = relationship("User", back_populates="posts")
|
|
233
|
-
|
|
234
|
-
# Indexes
|
|
235
|
-
__table_args__ = (
|
|
236
|
-
Index('idx_post_published_created', is_published, 'created_at'),
|
|
237
|
-
Index('idx_post_author_published', author_id, is_published),
|
|
238
|
-
)
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
## Repository Pattern
|
|
242
|
-
|
|
243
|
-
```python
|
|
244
|
-
# app/repositories/base.py
|
|
245
|
-
from typing import Generic, TypeVar, Type, Optional, List, Dict, Any
|
|
246
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
247
|
-
from sqlalchemy import select, update, delete, func
|
|
248
|
-
from sqlalchemy.orm import selectinload
|
|
249
|
-
from app.models.base import BaseModel
|
|
250
|
-
|
|
251
|
-
ModelType = TypeVar("ModelType", bound=BaseModel)
|
|
252
|
-
|
|
253
|
-
class BaseRepository(Generic[ModelType]):
|
|
254
|
-
"""Base repository with common CRUD operations."""
|
|
255
|
-
|
|
256
|
-
def __init__(self, model: Type[ModelType], db: AsyncSession):
|
|
257
|
-
self.model = model
|
|
258
|
-
self.db = db
|
|
259
|
-
|
|
260
|
-
async def get(self, id: int) -> Optional[ModelType]:
|
|
261
|
-
"""Get model by ID."""
|
|
262
|
-
result = await self.db.execute(
|
|
263
|
-
select(self.model).where(self.model.id == id)
|
|
264
|
-
)
|
|
265
|
-
return result.scalar_one_or_none()
|
|
266
|
-
|
|
267
|
-
async def get_multi(
|
|
268
|
-
self,
|
|
269
|
-
skip: int = 0,
|
|
270
|
-
limit: int = 100,
|
|
271
|
-
filters: Dict[str, Any] = None
|
|
272
|
-
) -> List[ModelType]:
|
|
273
|
-
"""Get multiple models with pagination."""
|
|
274
|
-
query = select(self.model)
|
|
275
|
-
|
|
276
|
-
if filters:
|
|
277
|
-
for field, value in filters.items():
|
|
278
|
-
if hasattr(self.model, field):
|
|
279
|
-
query = query.where(getattr(self.model, field) == value)
|
|
280
|
-
|
|
281
|
-
query = query.offset(skip).limit(limit)
|
|
282
|
-
result = await self.db.execute(query)
|
|
283
|
-
return result.scalars().all()
|
|
284
|
-
|
|
285
|
-
async def create(self, obj_in: Dict[str, Any]) -> ModelType:
|
|
286
|
-
"""Create new model."""
|
|
287
|
-
db_obj = self.model(**obj_in)
|
|
288
|
-
self.db.add(db_obj)
|
|
289
|
-
await self.db.commit()
|
|
290
|
-
await self.db.refresh(db_obj)
|
|
291
|
-
return db_obj
|
|
292
|
-
|
|
293
|
-
async def update(
|
|
294
|
-
self,
|
|
295
|
-
id: int,
|
|
296
|
-
obj_in: Dict[str, Any]
|
|
297
|
-
) -> Optional[ModelType]:
|
|
298
|
-
"""Update model by ID."""
|
|
299
|
-
await self.db.execute(
|
|
300
|
-
update(self.model)
|
|
301
|
-
.where(self.model.id == id)
|
|
302
|
-
.values(**obj_in)
|
|
303
|
-
)
|
|
304
|
-
await self.db.commit()
|
|
305
|
-
return await self.get(id)
|
|
306
|
-
|
|
307
|
-
async def delete(self, id: int) -> bool:
|
|
308
|
-
"""Delete model by ID."""
|
|
309
|
-
result = await self.db.execute(
|
|
310
|
-
delete(self.model).where(self.model.id == id)
|
|
311
|
-
)
|
|
312
|
-
await self.db.commit()
|
|
313
|
-
return result.rowcount > 0
|
|
314
|
-
|
|
315
|
-
async def count(self, filters: Dict[str, Any] = None) -> int:
|
|
316
|
-
"""Count models with optional filters."""
|
|
317
|
-
query = select(func.count(self.model.id))
|
|
318
|
-
|
|
319
|
-
if filters:
|
|
320
|
-
for field, value in filters.items():
|
|
321
|
-
if hasattr(self.model, field):
|
|
322
|
-
query = query.where(getattr(self.model, field) == value)
|
|
323
|
-
|
|
324
|
-
result = await self.db.execute(query)
|
|
325
|
-
return result.scalar()
|
|
326
|
-
|
|
327
|
-
# app/repositories/user.py
|
|
328
|
-
from typing import Optional
|
|
329
|
-
from sqlalchemy import select
|
|
330
|
-
from app.models.user import User
|
|
331
|
-
from app.repositories.base import BaseRepository
|
|
332
|
-
|
|
333
|
-
class UserRepository(BaseRepository[User]):
|
|
334
|
-
"""User repository with custom methods."""
|
|
335
|
-
|
|
336
|
-
async def get_by_email(self, email: str) -> Optional[User]:
|
|
337
|
-
"""Get user by email."""
|
|
338
|
-
result = await self.db.execute(
|
|
339
|
-
select(User).where(User.email == email)
|
|
340
|
-
)
|
|
341
|
-
return result.scalar_one_or_none()
|
|
342
|
-
|
|
343
|
-
async def get_by_username(self, username: str) -> Optional[User]:
|
|
344
|
-
"""Get user by username."""
|
|
345
|
-
result = await self.db.execute(
|
|
346
|
-
select(User).where(User.username == username)
|
|
347
|
-
)
|
|
348
|
-
return result.scalar_one_or_none()
|
|
349
|
-
|
|
350
|
-
async def get_active_users(self, skip: int = 0, limit: int = 100):
|
|
351
|
-
"""Get active users."""
|
|
352
|
-
return await self.get_multi(
|
|
353
|
-
skip=skip,
|
|
354
|
-
limit=limit,
|
|
355
|
-
filters={'is_active': True}
|
|
356
|
-
)
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
## Alembic Configuration
|
|
360
|
-
|
|
361
|
-
```python
|
|
362
|
-
# alembic/env.py
|
|
363
|
-
from logging.config import fileConfig
|
|
364
|
-
from sqlalchemy import engine_from_config, pool
|
|
365
|
-
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
366
|
-
from alembic import context
|
|
367
|
-
import asyncio
|
|
368
|
-
|
|
369
|
-
# Import your models
|
|
370
|
-
from app.models.base import Base
|
|
371
|
-
from app.models.user import User
|
|
372
|
-
from app.models.post import Post
|
|
373
|
-
from app.core.config import settings
|
|
374
|
-
|
|
375
|
-
# Alembic Config object
|
|
376
|
-
config = context.config
|
|
377
|
-
|
|
378
|
-
# Override database URL
|
|
379
|
-
config.set_main_option("sqlalchemy.url", str(settings.DATABASE_URL))
|
|
380
|
-
|
|
381
|
-
# Interpret the config file for logging
|
|
382
|
-
if config.config_file_name is not None:
|
|
383
|
-
fileConfig(config.config_file_name)
|
|
384
|
-
|
|
385
|
-
# Add your model's MetaData object here
|
|
386
|
-
target_metadata = Base.metadata
|
|
387
|
-
|
|
388
|
-
def do_run_migrations(connection):
|
|
389
|
-
context.configure(
|
|
390
|
-
connection=connection,
|
|
391
|
-
target_metadata=target_metadata,
|
|
392
|
-
compare_type=True,
|
|
393
|
-
compare_server_default=True,
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
with context.begin_transaction():
|
|
397
|
-
context.run_migrations()
|
|
398
|
-
|
|
399
|
-
async def run_migrations_online():
|
|
400
|
-
"""Run migrations in 'online' mode."""
|
|
401
|
-
configuration = config.get_section(config.config_ini_section)
|
|
402
|
-
configuration["sqlalchemy.url"] = str(settings.DATABASE_URL)
|
|
403
|
-
|
|
404
|
-
connectable = AsyncEngine(
|
|
405
|
-
engine_from_config(
|
|
406
|
-
configuration,
|
|
407
|
-
prefix="sqlalchemy.",
|
|
408
|
-
poolclass=pool.NullPool,
|
|
409
|
-
)
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
async with connectable.connect() as connection:
|
|
413
|
-
await connection.run_sync(do_run_migrations)
|
|
414
|
-
|
|
415
|
-
await connectable.dispose()
|
|
416
|
-
|
|
417
|
-
if context.is_offline_mode():
|
|
418
|
-
run_migrations_offline()
|
|
419
|
-
else:
|
|
420
|
-
asyncio.run(run_migrations_online())
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
## Database Utilities
|
|
424
|
-
|
|
425
|
-
```python
|
|
426
|
-
# app/db/utils.py
|
|
427
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
428
|
-
from sqlalchemy import text
|
|
429
|
-
from app.db.database import engine, AsyncSessionLocal
|
|
430
|
-
from app.core.config import settings
|
|
431
|
-
import asyncio
|
|
432
|
-
|
|
433
|
-
async def check_database_connection() -> bool:
|
|
434
|
-
"""Check if database is accessible."""
|
|
435
|
-
try:
|
|
436
|
-
async with AsyncSessionLocal() as session:
|
|
437
|
-
await session.execute(text("SELECT 1"))
|
|
438
|
-
return True
|
|
439
|
-
except Exception:
|
|
440
|
-
return False
|
|
441
|
-
|
|
442
|
-
async def create_database_if_not_exists():
|
|
443
|
-
"""Create database if it doesn't exist."""
|
|
444
|
-
# This is PostgreSQL specific
|
|
445
|
-
import asyncpg
|
|
446
|
-
from urllib.parse import urlparse
|
|
447
|
-
|
|
448
|
-
url = urlparse(str(settings.DATABASE_URL))
|
|
449
|
-
|
|
450
|
-
try:
|
|
451
|
-
# Connect to postgres database to create our database
|
|
452
|
-
conn = await asyncpg.connect(
|
|
453
|
-
host=url.hostname,
|
|
454
|
-
port=url.port,
|
|
455
|
-
user=url.username,
|
|
456
|
-
password=url.password,
|
|
457
|
-
database='postgres'
|
|
458
|
-
)
|
|
459
|
-
|
|
460
|
-
# Check if database exists
|
|
461
|
-
exists = await conn.fetchval(
|
|
462
|
-
"SELECT 1 FROM pg_database WHERE datname = $1",
|
|
463
|
-
url.path[1:] # Remove leading slash
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
if not exists:
|
|
467
|
-
await conn.execute(f'CREATE DATABASE "{url.path[1:]}"')
|
|
468
|
-
print(f"Database {url.path[1:]} created.")
|
|
469
|
-
|
|
470
|
-
await conn.close()
|
|
471
|
-
|
|
472
|
-
except Exception as e:
|
|
473
|
-
print(f"Error creating database: {e}")
|
|
474
|
-
|
|
475
|
-
async def execute_raw_sql(sql: str, params: dict = None) -> list:
|
|
476
|
-
"""Execute raw SQL query."""
|
|
477
|
-
async with AsyncSessionLocal() as session:
|
|
478
|
-
result = await session.execute(text(sql), params or {})
|
|
479
|
-
return result.fetchall()
|
|
480
|
-
|
|
481
|
-
async def get_table_info(table_name: str) -> dict:
|
|
482
|
-
"""Get information about a table."""
|
|
483
|
-
sql = """
|
|
484
|
-
SELECT
|
|
485
|
-
column_name,
|
|
486
|
-
data_type,
|
|
487
|
-
is_nullable,
|
|
488
|
-
column_default
|
|
489
|
-
FROM information_schema.columns
|
|
490
|
-
WHERE table_name = :table_name
|
|
491
|
-
ORDER BY ordinal_position;
|
|
492
|
-
"""
|
|
493
|
-
|
|
494
|
-
result = await execute_raw_sql(sql, {'table_name': table_name})
|
|
495
|
-
return [
|
|
496
|
-
{
|
|
497
|
-
'column_name': row[0],
|
|
498
|
-
'data_type': row[1],
|
|
499
|
-
'is_nullable': row[2],
|
|
500
|
-
'column_default': row[3]
|
|
501
|
-
}
|
|
502
|
-
for row in result
|
|
503
|
-
]
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
## Database Initialization
|
|
507
|
-
|
|
508
|
-
```python
|
|
509
|
-
# app/db/init_db.py
|
|
510
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
511
|
-
from app.db.database import get_db, create_tables
|
|
512
|
-
from app.models.user import User
|
|
513
|
-
from app.repositories.user import UserRepository
|
|
514
|
-
from app.core.config import settings
|
|
515
|
-
import asyncio
|
|
516
|
-
|
|
517
|
-
async def init_db() -> None:
|
|
518
|
-
"""Initialize database with tables and default data."""
|
|
519
|
-
# Create tables
|
|
520
|
-
await create_tables()
|
|
521
|
-
print("Database tables created.")
|
|
522
|
-
|
|
523
|
-
# Create default superuser
|
|
524
|
-
async with AsyncSessionLocal() as session:
|
|
525
|
-
user_repo = UserRepository(User, session)
|
|
526
|
-
|
|
527
|
-
# Check if superuser exists
|
|
528
|
-
existing_user = await user_repo.get_by_email("admin@example.com")
|
|
529
|
-
|
|
530
|
-
if not existing_user:
|
|
531
|
-
superuser_data = {
|
|
532
|
-
"username": "admin",
|
|
533
|
-
"email": "admin@example.com",
|
|
534
|
-
"first_name": "Admin",
|
|
535
|
-
"last_name": "User",
|
|
536
|
-
"is_superuser": True,
|
|
537
|
-
"is_active": True
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
superuser = User(**superuser_data)
|
|
541
|
-
superuser.set_password("admin123")
|
|
542
|
-
|
|
543
|
-
session.add(superuser)
|
|
544
|
-
await session.commit()
|
|
545
|
-
print("Superuser created.")
|
|
546
|
-
else:
|
|
547
|
-
print("Superuser already exists.")
|
|
548
|
-
|
|
549
|
-
if __name__ == "__main__":
|
|
550
|
-
asyncio.run(init_db())
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
## Testing Database
|
|
554
|
-
|
|
555
|
-
```python
|
|
556
|
-
# tests/conftest.py
|
|
557
|
-
import pytest
|
|
558
|
-
import asyncio
|
|
559
|
-
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
560
|
-
from sqlalchemy.orm import sessionmaker
|
|
561
|
-
from app.db.database import Base, get_db
|
|
562
|
-
from app.main import app
|
|
563
|
-
import pytest_asyncio
|
|
564
|
-
|
|
565
|
-
# Test database URL
|
|
566
|
-
TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
|
|
567
|
-
|
|
568
|
-
@pytest.fixture(scope="session")
|
|
569
|
-
def event_loop():
|
|
570
|
-
"""Create event loop for async tests."""
|
|
571
|
-
loop = asyncio.get_event_loop_policy().new_event_loop()
|
|
572
|
-
yield loop
|
|
573
|
-
loop.close()
|
|
574
|
-
|
|
575
|
-
@pytest_asyncio.fixture(scope="session")
|
|
576
|
-
async def test_engine():
|
|
577
|
-
"""Create test database engine."""
|
|
578
|
-
engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
|
579
|
-
|
|
580
|
-
# Create tables
|
|
581
|
-
async with engine.begin() as conn:
|
|
582
|
-
await conn.run_sync(Base.metadata.create_all)
|
|
583
|
-
|
|
584
|
-
yield engine
|
|
585
|
-
|
|
586
|
-
# Drop tables
|
|
587
|
-
async with engine.begin() as conn:
|
|
588
|
-
await conn.run_sync(Base.metadata.drop_all)
|
|
589
|
-
|
|
590
|
-
await engine.dispose()
|
|
591
|
-
|
|
592
|
-
@pytest_asyncio.fixture
|
|
593
|
-
async def test_session(test_engine):
|
|
594
|
-
"""Create test database session."""
|
|
595
|
-
TestSessionLocal = sessionmaker(
|
|
596
|
-
test_engine, class_=AsyncSession, expire_on_commit=False
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
async with TestSessionLocal() as session:
|
|
600
|
-
yield session
|
|
601
|
-
|
|
602
|
-
@pytest.fixture
|
|
603
|
-
def override_get_db(test_session):
|
|
604
|
-
"""Override database dependency."""
|
|
605
|
-
async def _override_get_db():
|
|
606
|
-
yield test_session
|
|
607
|
-
|
|
608
|
-
app.dependency_overrides[get_db] = _override_get_db
|
|
609
|
-
yield
|
|
610
|
-
app.dependency_overrides = {}
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
## Database Health Check
|
|
614
|
-
|
|
615
|
-
```python
|
|
616
|
-
# app/api/health.py
|
|
617
|
-
from fastapi import APIRouter, Depends, HTTPException
|
|
618
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
619
|
-
from sqlalchemy import text
|
|
620
|
-
from app.db.database import get_db
|
|
621
|
-
import time
|
|
622
|
-
|
|
623
|
-
router = APIRouter()
|
|
624
|
-
|
|
625
|
-
@router.get("/health")
|
|
626
|
-
async def health_check(db: AsyncSession = Depends(get_db)):
|
|
627
|
-
"""Health check endpoint."""
|
|
628
|
-
checks = {
|
|
629
|
-
"status": "healthy",
|
|
630
|
-
"timestamp": time.time(),
|
|
631
|
-
"database": await check_database_health(db),
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
# Determine overall status
|
|
635
|
-
if checks["database"]["status"] != "ok":
|
|
636
|
-
checks["status"] = "unhealthy"
|
|
637
|
-
raise HTTPException(status_code=503, detail=checks)
|
|
638
|
-
|
|
639
|
-
return checks
|
|
640
|
-
|
|
641
|
-
async def check_database_health(db: AsyncSession) -> dict:
|
|
642
|
-
"""Check database connection."""
|
|
643
|
-
try:
|
|
644
|
-
start_time = time.time()
|
|
645
|
-
await db.execute(text("SELECT 1"))
|
|
646
|
-
response_time = (time.time() - start_time) * 1000 # milliseconds
|
|
647
|
-
|
|
648
|
-
return {
|
|
649
|
-
"status": "ok",
|
|
650
|
-
"response_time_ms": round(response_time, 2)
|
|
651
|
-
}
|
|
652
|
-
except Exception as e:
|
|
653
|
-
return {
|
|
654
|
-
"status": "error",
|
|
655
|
-
"error": str(e)
|
|
656
|
-
}
|
|
657
|
-
```
|