From 089d75f953ff63271cecebf225be3378890dd0ca Mon Sep 17 00:00:00 2001 From: zhi Date: Sun, 29 Mar 2026 16:33:00 +0000 Subject: [PATCH] BE-PR-003: Add Essential SQLAlchemy model - New app/models/essential.py with Essential model and EssentialType enum (feature, improvement, refactor) - Fields: id, essential_code (unique), proposal_id (FK to proposes), type, title, description, created_by_id (FK to users), created_at, updated_at - Added essentials relationship to Proposal model (cascade delete-orphan) - Added essentials table auto-migration in main.py _migrate_schema() - Registered essential module import in startup() --- app/main.py | 23 +++++++++++++++- app/models/essential.py | 59 +++++++++++++++++++++++++++++++++++++++++ app/models/proposal.py | 9 +++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 app/models/essential.py diff --git a/app/main.py b/app/main.py index 4cbe6df..698fbe4 100644 --- a/app/main.py +++ b/app/main.py @@ -259,6 +259,27 @@ def _migrate_schema(): if _has_table(db, "server_states") and not _has_column(db, "server_states", "nginx_sites_json"): db.execute(text("ALTER TABLE server_states ADD COLUMN nginx_sites_json TEXT NULL")) + # --- essentials table (BE-PR-003) --- + if not _has_table(db, "essentials"): + db.execute(text(""" + CREATE TABLE essentials ( + id INTEGER NOT NULL AUTO_INCREMENT, + essential_code VARCHAR(64) NOT NULL, + proposal_id INTEGER NOT NULL, + type ENUM('feature','improvement','refactor') NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT NULL, + created_by_id INTEGER NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NULL ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE INDEX idx_essentials_code (essential_code), + INDEX idx_essentials_proposal_id (proposal_id), + CONSTRAINT fk_essentials_proposal_id FOREIGN KEY (proposal_id) REFERENCES proposes(id), + CONSTRAINT fk_essentials_created_by_id FOREIGN KEY (created_by_id) REFERENCES users(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + """)) + db.commit() except Exception as e: db.rollback() @@ -293,7 +314,7 @@ def _sync_default_user_roles(db): @app.on_event("startup") def startup(): from app.core.config import Base, engine, SessionLocal - from app.models import models, webhook, apikey, activity, milestone, notification, worklog, monitor, role_permission, task, support, meeting, proposal, propose + from app.models import models, webhook, apikey, activity, milestone, notification, worklog, monitor, role_permission, task, support, meeting, proposal, propose, essential Base.metadata.create_all(bind=engine) _migrate_schema() diff --git a/app/models/essential.py b/app/models/essential.py new file mode 100644 index 0000000..1bb934e --- /dev/null +++ b/app/models/essential.py @@ -0,0 +1,59 @@ +"""Essential model — actionable items under a Proposal. + +Each Essential represents one deliverable scope item (feature, improvement, +or refactor). When a Proposal is accepted, every Essential is converted into +a corresponding ``story/*`` task under the chosen Milestone. + +See: NEXT_WAVE_DEV_DIRECTION.md §8.5 +""" + +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum +from sqlalchemy.sql import func +from app.core.config import Base +import enum + + +class EssentialType(str, enum.Enum): + FEATURE = "feature" + IMPROVEMENT = "improvement" + REFACTOR = "refactor" + + +class Essential(Base): + __tablename__ = "essentials" + + id = Column(Integer, primary_key=True, index=True) + + essential_code = Column( + String(64), + nullable=False, + unique=True, + index=True, + comment="Unique human-readable code, e.g. PROJ:E00001", + ) + + proposal_id = Column( + Integer, + ForeignKey("proposes.id"), # FK targets the actual DB table name + nullable=False, + comment="Owning Proposal", + ) + + type = Column( + Enum(EssentialType, values_callable=lambda x: [e.value for e in x]), + nullable=False, + comment="Essential type: feature | improvement | refactor", + ) + + title = Column(String(255), nullable=False, comment="Short title") + description = Column(Text, nullable=True, comment="Detailed description") + + created_by_id = Column( + Integer, + ForeignKey("users.id"), + nullable=True, + comment="Author of the essential", + ) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) diff --git a/app/models/proposal.py b/app/models/proposal.py index d3730f7..49eb0a4 100644 --- a/app/models/proposal.py +++ b/app/models/proposal.py @@ -1,5 +1,6 @@ from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.core.config import Base import enum @@ -69,6 +70,14 @@ class Proposal(Base): created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + # ---- relationships ----------------------------------------------------- + essentials = relationship( + "Essential", + foreign_keys="Essential.proposal_id", + cascade="all, delete-orphan", + lazy="select", + ) + # ---- convenience alias ------------------------------------------------ @hybrid_property def proposal_code(self) -> str | None: