New entities mirroring the Project shape: - knowledge_bases (human code, title, description, created_by, timestamps) - knowledge_topics (UNIQUE(topic, knowledge_base_id)) - knowledge_categories (self-referential parent; UNIQUE(topic_id, parent, name), with an app-level check for the NULL-parent case MySQL can't enforce) - knowledge_facts (category_id NULL → fact lives directly on the topic) - project_knowledge_bases (M2M project ↔ knowledge base) Adds full CRUD for KB/topic/category/fact, a nested /tree aggregate, project link/unlink/list, KB-code generation (same algorithm as project codes), and category cycle-prevention. Four global permissions (knowledge-base.create/read/update/delete) seeded in init_bootstrap and granted to admin/mgr/dev/general-agent/guest as appropriate. New tables auto-create via Base.metadata.create_all; router wired in main.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
"""Knowledge Base models.
|
|
|
|
Mirrors the Project feature's shape (human-friendly *code*, creator FK,
|
|
created/updated timestamps). Hierarchy is:
|
|
|
|
knowledge_base
|
|
└─ knowledge_topic (unique per (topic, knowledge_base_id))
|
|
├─ knowledge_fact (category_id NULL → fact lives on the topic)
|
|
└─ knowledge_category (parent NULL → top-level category in topic)
|
|
├─ knowledge_fact
|
|
└─ knowledge_category (parent → nested)
|
|
|
|
`project_knowledge_base` is the M2M link between projects and knowledge bases.
|
|
|
|
Relationships are intentionally kept minimal (no ORM cascade on the
|
|
self-referential category tree); deletion ordering is handled explicitly in
|
|
the router to stay clear of FK-ordering surprises under MySQL.
|
|
"""
|
|
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
|
|
from app.core.config import Base
|
|
|
|
|
|
class KnowledgeBase(Base):
|
|
__tablename__ = "knowledge_bases"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
knowledge_base_code = Column(String(16), unique=True, index=True, nullable=True)
|
|
title = Column(String(200), nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
last_updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
creator = relationship("User", foreign_keys=[created_by])
|
|
|
|
|
|
class KnowledgeTopic(Base):
|
|
__tablename__ = "knowledge_topics"
|
|
__table_args__ = (
|
|
UniqueConstraint("topic", "knowledge_base_id", name="uq_knowledge_topic_kb"),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
topic = Column(String(200), nullable=False)
|
|
knowledge_base_id = Column(Integer, ForeignKey("knowledge_bases.id"), nullable=False, index=True)
|
|
description = Column(Text, nullable=True)
|
|
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
last_updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
creator = relationship("User", foreign_keys=[created_by])
|
|
|
|
|
|
class KnowledgeCategory(Base):
|
|
__tablename__ = "knowledge_categories"
|
|
__table_args__ = (
|
|
# NOTE: MySQL treats NULLs as distinct in a UNIQUE index, so this only
|
|
# enforces uniqueness for non-NULL `parent`. Top-level categories
|
|
# (parent IS NULL) are de-duped in the router (application-level check).
|
|
UniqueConstraint("topic_id", "parent", "name", name="uq_knowledge_category_triple"),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
name = Column(String(200), nullable=False)
|
|
parent = Column(Integer, ForeignKey("knowledge_categories.id"), nullable=True, index=True)
|
|
topic_id = Column(Integer, ForeignKey("knowledge_topics.id"), nullable=False, index=True)
|
|
description = Column(Text, nullable=True)
|
|
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
last_updated_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
|
|
|
|
class KnowledgeFact(Base):
|
|
__tablename__ = "knowledge_facts"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
category_id = Column(Integer, ForeignKey("knowledge_categories.id"), nullable=True, index=True)
|
|
topic_id = Column(Integer, ForeignKey("knowledge_topics.id"), nullable=False, index=True)
|
|
fact = Column(Text, nullable=False)
|
|
last_updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
|
|
class ProjectKnowledgeBase(Base):
|
|
__tablename__ = "project_knowledge_bases"
|
|
__table_args__ = (
|
|
UniqueConstraint("project_id", "knowledge_base_id", name="uq_project_knowledge_base"),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False, index=True)
|
|
knowledge_base_id = Column(Integer, ForeignKey("knowledge_bases.id"), nullable=False, index=True)
|
|
|
|
|
|
class KnowledgeBaseCodeCounter(Base):
|
|
__tablename__ = "knowledge_base_code_counters"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
prefix = Column(String(16), unique=True, index=True, nullable=False)
|
|
next_value = Column(Integer, default=0)
|