How the Domain Layer Implements the Repository Pattern for SurrealDB Operations

The Open-Notebook domain layer separates business logic from SurrealDB driver specifics by delegating all database operations to repository helpers in open_notebook/database/repository.py while exposing clean CRUD methods through a generic ObjectModel base class.

This architecture keeps the domain layer framework‑agnostic and testable. Instead of embedding SurrealQL queries inside business logic, the repository pattern centralizes connection handling, query construction, and result parsing in dedicated utilities, while domain models in open_notebook/domain/ focus purely on validation and business rules.

Repository Layer: Low‑Level SurrealDB Abstractions

All direct interaction with SurrealDB resides in open_notebook/database/repository.py. This module exposes async helper functions that wrap the official driver, handle RecordID parsing, and manage timestamps.

Query Execution and Record Handling

The repo_query() function executes arbitrary SurrealQL statements and transforms the raw response into plain Python dictionaries. Located at lines 65–73, it parses RecordID values to strings and returns clean data structures that the domain layer can consume without driver dependencies.

from open_notebook.database.repository import repo_query

# Execute a custom SurrealQL statement

results = await repo_query("SELECT * FROM notebook WHERE name = 'Research'")

Atomic CRUD Operations

The repository provides explicit functions for persistence operations, each handling specific SurrealDB commands:

  • repo_create() (lines 85–99): Wraps INSERT statements and injects created_at timestamps
  • repo_update() (lines 124–131): Handles UPDATE operations with error logging
  • repo_upsert() (lines 135–144): Implements UPSERT for idempotent writes
  • repo_delete() (lines 158–166): Executes DELETE commands with validation
  • repo_relate() (lines 6–15): Creates graph edges between records using the RELATE statement

These functions ensure every database interaction includes proper error handling and timestamp management, eliminating repetitive boilerplate from domain models.

Domain Layer: Generic CRUD via ObjectModel

Domain models inherit from ObjectModel in open_notebook/domain/base.py, which implements the repository pattern’s abstraction layer by delegating to the helpers above. Concrete entities only declare a table_name and field definitions to gain full persistence capabilities.

Retrieval and Hydration

The ObjectModel.get(id) method at lines 102–124 constructs a table‑aware SELECT query, validates the record exists, and automatically instantiates the correct subclass via _get_class_by_table_name. For bulk operations, ObjectModel.get_all(order_by) at lines 38–50 builds a SELECT * FROM <table> query with optional ordering validation.

from open_notebook.domain.notebook import Notebook

# Single record retrieval

notebook = await Notebook.get("notebook:abcd1234")

# List all notebooks ordered by creation date

notebooks = await Notebook.get_all(order_by="created_at DESC")

Persistence Lifecycle

The save() method (lines 46–74) orchestrates the persistence workflow:

  1. Validates the model instance
  2. Prepares a dictionary of non‑null fields via _prepare_save_data() (lines 75–84)
  3. Calls repo_create() for new objects or repo_update() for existing records
  4. Copies the returned fields back onto the instance to reflect database state
nb = Notebook(name="AI Safety Research", description="Collection of papers")
await nb.save()  # Calls repo_create() internally

print(nb.id)     # Now populated with RecordID string

Relationship Management

Graph relationships are handled through relate() at lines 117–124, which wraps repo_relate() to create edges between entities. Domain models expose semantic methods like add_to_notebook() that internally call relate() with the appropriate edge type and direction.

Concrete Implementations: Domain‑Specific Models

Concrete entities leverage the inherited CRUD behavior by defining minimal configuration. For example, Notebook in open_notebook/domain/notebook.py (lines 16–18) declares only table_name = "notebook" and gains automatic persistence, retrieval, and deletion capabilities.

class Notebook(ObjectModel):
    table_name: str = "notebook"
    name: str
    description: Optional[str] = None

This declarative approach means adding a new entity requires only field definitions and business logic, with zero boilerplate for database operations.

Advanced Patterns: Complex Queries and Domain Logic

Higher‑level domain methods combine repository calls to implement rich operations. In Notebook.get_sources() (lines 29–42), the method executes a repo_query() with a FETCH clause to retrieve related sources, then hydrates Source objects from the raw rows.


# Fetch notebook with related sources

notebook = await Notebook.get("notebook:xyz789")
sources = await notebook.get_sources(include_full_text=False)

for src in sources:
    print(src.title)

Similar patterns appear throughout the domain, such as Source.get_insights() and ChatSession.relate_to_notebook(), demonstrating how the repository pattern enables complex business logic while keeping SurrealQL encapsulated in the infrastructure layer.

Summary

  • Repository isolation: All SurrealDB driver code lives in open_notebook/database/repository.py, providing repo_query(), repo_create(), repo_update(), and repo_relate() as the sole database interaction points.
  • Domain abstraction: ObjectModel in open_notebook/domain/base.py implements generic CRUD by delegating to repository functions, allowing concrete models to inherit persistence behavior automatically.
  • Minimal configuration: Domain entities like Notebook declare only table_name and fields, with business logic completely decoupled from SurrealQL syntax.
  • Relationship support: Graph edges are created via ObjectModel.relate(), enabling rich domain relationships without exposing the underlying RELATE statement complexity.
  • Testability: The separation allows unit testing domain logic with mocked repository functions, ensuring business rules remain independent of database availability.

Frequently Asked Questions

What is the repository pattern and why does Open-Notebook use it for SurrealDB?

The repository pattern abstracts data access by placing all database operations behind a collection of helper functions. Open‑Notebook uses this pattern to prevent SurrealDB‑specific code (SurrealQL, RecordID handling, connection management) from leaking into business logic. This keeps the domain layer agnostic to the underlying database technology, making it easier to test, maintain, or potentially migrate to different storage backends.

How does ObjectModel handle database record IDs?

ObjectModel automatically manages SurrealDB RecordID objects through the repository layer. When repo_query() executes at lines 65–73, it parses RecordID values to strings before returning them to the domain layer. The get() method at lines 102–124 uses these string IDs to construct table‑aware queries, while save() updates the instance’s id field after creation to reflect the database‑generated identifier.

Can I use the repository functions directly without inheriting from ObjectModel?

Yes. The repository functions in open_notebook/database/repository.py are designed as standalone async utilities that can be imported and used anywhere in the application. While ObjectModel provides convenient CRUD methods for standard entities, direct repository access is appropriate for complex queries, bulk operations, or transactions that don’t map cleanly to single‑model persistence patterns.

How are relationships between entities implemented in the domain layer?

Relationships are created through the relate() method at lines 117–124 of open_notebook/domain/base.py, which delegates to repo_relate() to execute SurrealDB’s RELATE statement. Domain models expose semantic wrapper methods (like Source.add_to_notebook()) that call relate() with the appropriate edge type, keeping the graph structure encapsulated while providing a clean, object‑oriented API for linking entities.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →