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:
bm-claudeai
2025-05-25 10:07:34 -05:00
committed by GitHub
parent 9437a5f83b
commit 020957cd76
169 changed files with 13405 additions and 3447 deletions
+3 -2
View File
@@ -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",
]
+16 -4
View File
@@ -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"
)
+80
View File
@@ -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)
+8 -5
View File
@@ -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