# Phase 1 — Project Skeleton + Database **Status**: PENDING **Depends on**: nothing **Next phase**: `phase-02-auth-security.md` --- ## Goal Produce a runnable FastAPI app that starts, connects to SQLite, runs Alembic migrations, and responds to health checks. All ORM models and repositories must exist. No business logic yet. --- ## Java Source Files to Read `` = `E:\TestScript\ARMA-Server-Web-Gui\src\main\java\pl\bartlomiejstepien\armaserverwebgui\` | File | What to extract | |------|----------------| | `application/config/ASWGConfig.java` | Every `@Value` field name and default | | `ArmaServerWebGuiApplication.java` | Startup hooks | | `DefaultConfigGenerator.java` | How default config file is written on first run | | `domain/server/mod/model/InstalledModEntity.java` | JPA mapping, `ListOfLongsConverter` for dependencies_ids | | `domain/server/mod/model/ModPresetEntity.java` | JPA + relations | | `domain/server/mod/model/ModSettingsEntity.java` | JPA mapping | | `domain/server/mission/model/MissionEntity.java` | JPA mapping | | `domain/server/difficulty/model/DifficultyProfileEntity.java` | JPA mapping | | `domain/server/cdlc/model/CdlcEntity.java` | JPA mapping | | `application/security/jwt/model/InvalidJwtTokenEntity.java` | JPA mapping | | `application/scheduling/model/JobExecutionEntity.java` | JPA mapping | | `application/scheduling/model/JobExecutionStatus.java` | Enum values | | `repository/` (all .java files) | Custom query methods beyond findById/findAll | | `src/main/resources/application.properties` | server.port, multipart size limits, datasource | | `src/main/resources/aswg-default-config.properties` | All aswg.* keys and defaults | | `src/main/resources/db/changelog/` (all files) | Full DDL for all 14 migration sets | --- ## Output Files to Create Relative to `E:\TestScript\Arma_Server_Web_Manager\`: ``` pyproject.toml alembic.ini alembic/env.py alembic/versions/001_init.py alembic/versions/002_installed_mod_last_workshop_update.py alembic/versions/003_mission.py alembic/versions/004_mod_settings.py alembic/versions/005_invalid_jwt_token.py alembic/versions/006_aswg_user.py alembic/versions/007_overwrite_startup_params_authority.py alembic/versions/008_cdlc.py alembic/versions/009_aswg_user_last_login_column.py alembic/versions/010_job_last_execution_time.py alembic/versions/011_add_authorities.py alembic/versions/012_last_mod_update_attempt.py alembic/versions/013_job_execution_history.py alembic/versions/014_mod_dependencies.py config/settings.py config/aswg_default_config.properties (copy content from Java resources file) src/__init__.py src/main.py src/dependencies.py src/repository/__init__.py src/repository/base.py src/repository/models.py src/repository/installed_mod_repo.py src/repository/mod_preset_repo.py src/repository/mod_settings_repo.py src/repository/mission_repo.py src/repository/difficulty_profile_repo.py src/repository/cdlc_repo.py src/repository/user_repo.py src/repository/authority_repo.py src/repository/invalid_jwt_token_repo.py src/repository/job_execution_repo.py ``` --- ## Implementation Notes ### config/settings.py Use `pydantic-settings` `BaseSettings`. The properties file uses Java `.properties` format (key=value), so implement a custom `customise_sources` that reads via `jproperties.Properties`. Map every key in `aswg-default-config.properties` as a field with `alias="aswg.the-key-name"`. Key fields: ```python server_directory_path: str = Field(default="arma-server", alias="aswg.server-directory-path") security_enabled: bool = Field(default=True, alias="aswg.security.enabled") jwt_expiration_time: str = Field(default="PT2H", alias="aswg.security.jwt.expiration-time") default_user_username: str = Field(default="user", alias="aswg.default-user.username") default_user_password: str = Field(default="changeme", alias="aswg.default-user.password") steamcmd_path: str = Field(default="", alias="aswg.steamcmd.path") ``` ### src/repository/base.py ```python from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.orm import DeclarativeBase DATABASE_URL = "sqlite+aiosqlite:///./data/aswg.db" engine = create_async_engine(DATABASE_URL, echo=False) SessionLocal = async_sessionmaker(engine, expire_on_commit=False) class Base(DeclarativeBase): pass ``` ### src/repository/models.py — Key mapping notes - `InstalledModEntity.dependencies_ids`: Java stores as comma-separated long string. Create a SQLAlchemy `TypeDecorator` that serializes `list[int]` ↔ `"1,2,3"`. - `@GeneratedValue(IDENTITY)` → `Integer, primary_key=True, autoincrement=True` - `LocalDateTime` / `OffsetDateTime` → `DateTime(timezone=True)` - `@ManyToMany` user↔authority → explicit association table `aswg_user_authority` - `@Enumerated(STRING)` → store as `String`, convert in application layer ### Alembic migrations Replace H2-specific syntax: - `IDENTITY` / `BIGINT GENERATED BY DEFAULT AS IDENTITY` → `INTEGER PRIMARY KEY AUTOINCREMENT` - `TIMESTAMP` → `DATETIME` - No sequences needed Use `alembic/env.py` async pattern: ```python import asyncio from alembic import context from src.repository.base import engine, Base def run_migrations_online(): async def _run(): async with engine.begin() as conn: await conn.run_sync(context.run_migrations) asyncio.run(_run()) ``` ### src/main.py — minimal skeleton ```python from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from alembic.config import Config from alembic import command @asynccontextmanager async def lifespan(app: FastAPI): Config("alembic.ini") command.upgrade(Config("alembic.ini"), "head") # Phase 2 will add: create default user # Phase 6 will add: start scheduler yield # Phase 6 will add: stop scheduler app = FastAPI(lifespan=lifespan) @app.get("/api/v1/actuator/health") async def health(): return {"status": "UP"} @app.get("/api/v1/actuator/info") async def info(): return {"application": {"name": "ASWG"}} # Mount Angular SPA — must be last app.mount("/", StaticFiles(directory="static", html=True), name="static") ``` ### src/dependencies.py ```python from typing import Annotated from fastapi import Depends from sqlalchemy.ext.asyncio import AsyncSession from src.repository.base import SessionLocal async def get_db(): async with SessionLocal() as session: yield session DbSession = Annotated[AsyncSession, Depends(get_db)] ``` ### Repository pattern Plain async classes receiving `AsyncSession`. Port every Spring Data method name: - `findAll()` → `async def find_all()` - `findById(id)` → `async def find_by_id(id)` - `save(entity)` → `async def save(entity)` (uses `session.merge()`) - `deleteById(id)` → `async def delete_by_id(id)` - Custom `@Query` methods → SQLAlchemy `select()` with `.where()` --- ## Completion Checklist - [ ] `pyproject.toml` with all dependencies from `PLAN.md` - [ ] `config/settings.py` covers every `aswg.*` property with correct defaults - [ ] `src/repository/base.py` creates async SQLite engine - [ ] `src/repository/models.py` has all 12 ORM entity classes - [ ] All 14 Alembic migrations run cleanly (`alembic upgrade head`) - [ ] All 10 repository classes exist with full method set - [ ] `uvicorn src.main:app --port 8085` starts without errors - [ ] `GET /api/v1/actuator/health` → `{"status":"UP"}` - [ ] `GET /api/v1/actuator/info` → returns JSON ## Contract for Phase 2 Phase 2 will import: - `from config.settings import settings` - `from src.dependencies import DbSession` - `from src.repository.base import Base, engine, SessionLocal` - `from src.repository.models import AswgUserEntity, AuthorityEntity, InvalidJwtTokenEntity` - `from src.repository.user_repo import UserRepository` - `from src.repository.authority_repo import AuthorityRepository` - `from src.repository.invalid_jwt_token_repo import InvalidJwtTokenRepository` - `from src.main import app`