feat(knowledge-base): KnowledgeBase feature — models, CRUD API, RBAC
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>
This commit is contained in:
101
app/models/knowledge.py
Normal file
101
app/models/knowledge.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user