feat: scaffold FastAPI backend and Vue 3 frontend
Backend (backend/):
- FastAPI app with layered app/{api,core,crud,db,models,schemas} structure
- TiDB connection via SQLAlchemy. The URL is built with URL.create rather
than string formatting: the password contains '@', which would otherwise be
parsed as the user/host separator and silently truncate the credential.
- Alembic environment wired to Base.metadata and the app settings, so
backend/.env stays the single source of truth for credentials
- /api/health endpoint reporting database reachability
- models/ and crud/ are intentionally empty: no model classes are defined and
no tables are created, at import time or otherwise
Frontend (frontend/):
- Vue 3 + Vite + TypeScript scaffold (create-vue, --bare)
- axios instance with a normalized error shape
- vue-router with a home route and a catch-all 404
- pinia + pinia-plugin-persistedstate; the app store persists selected keys
- Element Plus with its icons registered globally
- dev proxy forwards /api to the FastAPI service on port 8000
The database paper_doc was created in TiDB out of band; no table exists yet.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Cross-cutting configuration and infrastructure."""
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Application settings, loaded from ``backend/.env`` and the environment.
|
||||
|
||||
The database URL is assembled with :func:`sqlalchemy.engine.URL.create` rather
|
||||
than string formatting. That matters here: the TiDB password contains ``@``,
|
||||
and interpolating it into a DSN by hand silently truncates the password at the
|
||||
first ``@`` (it gets parsed as the user/host separator). ``URL.create``
|
||||
percent-encodes each component correctly.
|
||||
"""
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from sqlalchemy import URL
|
||||
|
||||
# backend/app/core/config.py -> backend/
|
||||
BASE_DIR = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Runtime configuration for the API service."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=BASE_DIR / ".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
app_name: str = "paper-doc API"
|
||||
api_prefix: str = "/api"
|
||||
debug: bool = True
|
||||
|
||||
# The Vite dev server proxies /api to this service, so CORS is normally not
|
||||
# exercised during development; it is configured for direct-origin setups.
|
||||
cors_origins: list[str] = [
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:5173",
|
||||
]
|
||||
|
||||
# --- Database (TiDB) -------------------------------------------------
|
||||
db_host: str = "127.0.0.1"
|
||||
db_port: int = 4000
|
||||
db_user: str = "root"
|
||||
db_password: str = ""
|
||||
db_name: str = "paper_doc"
|
||||
|
||||
# When set, this wins over the DB_* parts above and is used verbatim.
|
||||
database_url: str | None = None
|
||||
|
||||
# --- Connection pool -------------------------------------------------
|
||||
db_echo: bool = False
|
||||
db_pool_pre_ping: bool = True
|
||||
db_pool_recycle: int = 3600
|
||||
|
||||
@property
|
||||
def sqlalchemy_url(self) -> Union[URL, str]:
|
||||
"""The SQLAlchemy connection URL.
|
||||
|
||||
Returns a :class:`~sqlalchemy.engine.URL` unless ``DATABASE_URL`` was
|
||||
supplied explicitly, in which case that string is passed through.
|
||||
"""
|
||||
if self.database_url:
|
||||
return self.database_url
|
||||
return URL.create(
|
||||
drivername="mysql+pymysql",
|
||||
username=self.db_user,
|
||||
password=self.db_password,
|
||||
host=self.db_host,
|
||||
port=self.db_port,
|
||||
database=self.db_name,
|
||||
)
|
||||
|
||||
@property
|
||||
def safe_database_url(self) -> str:
|
||||
"""The URL rendered with the password masked, for logs and diagnostics."""
|
||||
url = self.sqlalchemy_url
|
||||
if isinstance(url, str):
|
||||
return URL.make_url(url).render_as_string(hide_password=True)
|
||||
return url.render_as_string(hide_password=True)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
"""Return the process-wide settings singleton."""
|
||||
return Settings()
|
||||
Reference in New Issue
Block a user