mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
feat: Multi-project support, OAuth authentication, and major improvements (#119)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,13 @@
|
||||
import basic_memory
|
||||
from basic_memory.models.base import Base
|
||||
from basic_memory.models.knowledge import Entity, Observation, Relation
|
||||
|
||||
SCHEMA_VERSION = basic_memory.__version__ + "-" + "003"
|
||||
from basic_memory.models.project import Project
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
"Entity",
|
||||
"Observation",
|
||||
"Relation",
|
||||
"Project",
|
||||
"basic_memory",
|
||||
]
|
||||
|
||||
@@ -17,7 +17,6 @@ from sqlalchemy import (
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from basic_memory.models.base import Base
|
||||
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@@ -29,6 +28,7 @@ class Entity(Base):
|
||||
- Maps to a file on disk
|
||||
- Maintains a checksum for change detection
|
||||
- Tracks both source file and semantic properties
|
||||
- Belongs to a specific project
|
||||
"""
|
||||
|
||||
__tablename__ = "entity"
|
||||
@@ -38,13 +38,21 @@ class Entity(Base):
|
||||
Index("ix_entity_title", "title"),
|
||||
Index("ix_entity_created_at", "created_at"), # For timeline queries
|
||||
Index("ix_entity_updated_at", "updated_at"), # For timeline queries
|
||||
# Unique index only for markdown files with non-null permalinks
|
||||
Index("ix_entity_project_id", "project_id"), # For project filtering
|
||||
# Project-specific uniqueness constraints
|
||||
Index(
|
||||
"uix_entity_permalink",
|
||||
"uix_entity_permalink_project",
|
||||
"permalink",
|
||||
"project_id",
|
||||
unique=True,
|
||||
sqlite_where=text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
|
||||
),
|
||||
Index(
|
||||
"uix_entity_file_path_project",
|
||||
"file_path",
|
||||
"project_id",
|
||||
unique=True,
|
||||
),
|
||||
)
|
||||
|
||||
# Core identity
|
||||
@@ -54,10 +62,13 @@ class Entity(Base):
|
||||
entity_metadata: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
||||
content_type: Mapped[str] = mapped_column(String)
|
||||
|
||||
# Project reference
|
||||
project_id: Mapped[int] = mapped_column(Integer, ForeignKey("project.id"), nullable=False)
|
||||
|
||||
# Normalized path for URIs - required for markdown files only
|
||||
permalink: Mapped[Optional[str]] = mapped_column(String, nullable=True, index=True)
|
||||
# Actual filesystem relative path
|
||||
file_path: Mapped[str] = mapped_column(String, unique=True, index=True)
|
||||
file_path: Mapped[str] = mapped_column(String, index=True)
|
||||
# checksum of file
|
||||
checksum: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
||||
|
||||
@@ -66,6 +77,7 @@ class Entity(Base):
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
|
||||
# Relationships
|
||||
project = relationship("Project", back_populates="entities")
|
||||
observations = relationship(
|
||||
"Observation", back_populates="entity", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Project model for Basic Memory."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
Boolean,
|
||||
DateTime,
|
||||
Index,
|
||||
event,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from basic_memory.models.base import Base
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
class Project(Base):
|
||||
"""Project model for Basic Memory.
|
||||
|
||||
A project represents a collection of knowledge entities that are grouped together.
|
||||
Projects are stored in the app-level database and provide context for all knowledge
|
||||
operations.
|
||||
"""
|
||||
|
||||
__tablename__ = "project"
|
||||
__table_args__ = (
|
||||
# Regular indexes
|
||||
Index("ix_project_name", "name", unique=True),
|
||||
Index("ix_project_permalink", "permalink", unique=True),
|
||||
Index("ix_project_path", "path"),
|
||||
Index("ix_project_created_at", "created_at"),
|
||||
Index("ix_project_updated_at", "updated_at"),
|
||||
)
|
||||
|
||||
# Core identity
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String, unique=True)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# URL-friendly identifier generated from name
|
||||
permalink: Mapped[str] = mapped_column(String, unique=True)
|
||||
|
||||
# Filesystem path to project directory
|
||||
path: Mapped[str] = mapped_column(String)
|
||||
|
||||
# Status flags
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_default: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean, default=None, unique=True, nullable=True
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||
)
|
||||
|
||||
# Define relationships to entities, observations, and relations
|
||||
# These relationships will be established once we add project_id to those models
|
||||
entities = relationship("Entity", back_populates="project", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"Project(id={self.id}, name='{self.name}', permalink='{self.permalink}', path='{self.path}')"
|
||||
|
||||
|
||||
@event.listens_for(Project, "before_insert")
|
||||
@event.listens_for(Project, "before_update")
|
||||
def set_project_permalink(mapper, connection, project):
|
||||
"""Generate URL-friendly permalink for the project if needed.
|
||||
|
||||
This event listener ensures the permalink is always derived from the name,
|
||||
even if the name changes.
|
||||
"""
|
||||
# If the name changed or permalink is empty, regenerate permalink
|
||||
if not project.permalink or project.permalink != generate_permalink(project.name):
|
||||
project.permalink = generate_permalink(project.name)
|
||||
@@ -13,21 +13,24 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
|
||||
permalink, -- Stable identifier (now indexed for path search)
|
||||
file_path UNINDEXED, -- Physical location
|
||||
type UNINDEXED, -- entity/relation/observation
|
||||
|
||||
-- Relation fields
|
||||
|
||||
-- Project context
|
||||
project_id UNINDEXED, -- Project identifier
|
||||
|
||||
-- Relation fields
|
||||
from_id UNINDEXED, -- Source entity
|
||||
to_id UNINDEXED, -- Target entity
|
||||
relation_type UNINDEXED, -- Type of relation
|
||||
|
||||
|
||||
-- Observation fields
|
||||
entity_id UNINDEXED, -- Parent entity
|
||||
category UNINDEXED, -- Observation category
|
||||
|
||||
|
||||
-- Common fields
|
||||
metadata UNINDEXED, -- JSON metadata
|
||||
created_at UNINDEXED, -- Creation timestamp
|
||||
updated_at UNINDEXED, -- Last update
|
||||
|
||||
|
||||
-- Configuration
|
||||
tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
|
||||
prefix='1,2,3,4' -- Support longer prefixes for paths
|
||||
|
||||
Reference in New Issue
Block a user