Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app/database/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async def create_generation_session(
generation_id = str(uuid.uuid4())
db.set(COL_GENERATION_SESSIONS, generation_id, {
"status": "pending",
"created_at": db.server_timestamp(),
"created_at": datetime.now(UTC),
**data.dict()
})
return {"id": generation_id}
Expand Down
4 changes: 0 additions & 4 deletions backend/app/database/firestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,6 @@ def list_subcollection(
out.append(row)
return out

def server_timestamp(self) -> Any:
"""Get server timestamp sentinel."""
return firestore.SERVER_TIMESTAMP

def get_api_key_by_uid(self, key_uid: str) -> Optional[Dict[str, Any]]:
"""Return the api_keys document whose key_uid field matches."""
results = self.query("api_keys", filters=[("key_uid", "==", key_uid)])
Expand Down
26 changes: 2 additions & 24 deletions backend/app/database/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ class IDatabase(ABC):
- CRUD operations (get, set, update, delete)
- Queries with filters and ordering
- Atomic transactions
- Server timestamps
- Array operations
"""

Expand Down Expand Up @@ -162,7 +161,7 @@ def set(self, collection: str, doc_id: str, data: Dict[str, Any]) -> None:
Example:
>>> db.set("generation_sessions", "est-123", {
... "status": "pending",
... "created_at": db.server_timestamp()
... "created_at": datetime.now(UTC)
... })
"""
pass
Expand Down Expand Up @@ -279,7 +278,7 @@ def run_transaction(self, callback: Callable[[ITransactionContext], T]) -> T:
... # 2. Then perform all writes
... tx.update("workspaces", ws_id, {
... "status": "allocated",
... "locked_at": db.server_timestamp()
... "locked_at": datetime.now(UTC)
... })
...
... return ws_id
Expand Down Expand Up @@ -322,27 +321,6 @@ def list_subcollection(
Each returned dict includes ``_id`` with the child document id.
"""

@abstractmethod
def server_timestamp(self) -> Any:
"""
Get a server timestamp value for use in set/update operations.

When this value is written to the database, it will be replaced with
the actual server timestamp. This ensures consistent timestamps across
all clients regardless of clock skew.

Returns:
A sentinel value representing server timestamp

Example:
>>> db.set("generation_sessions", "est-123", {
... "created_at": db.server_timestamp(),
... "status": "pending"
... })
"""
pass


@abstractmethod
def get_api_key_by_uid(self, key_uid: str) -> Optional[Dict[str, Any]]:
"""
Expand Down
23 changes: 4 additions & 19 deletions backend/app/database/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"""

import threading
from datetime import UTC, datetime
from typing import Any, Callable, Dict, List, Optional, TypeVar

from app.database.interface import (
Expand Down Expand Up @@ -43,12 +42,6 @@ def _apply_dot_notation_update(doc: Dict[str, Any], key: str, value: Any) -> Non
target[parts[-1]] = value


class _ServerTimestamp:
"""Sentinel class for server timestamps in memory database."""

pass


class InMemoryTransactionContext(ITransactionContext):
"""Transaction context for in-memory database."""

Expand Down Expand Up @@ -156,12 +149,10 @@ def _apply_writes(self) -> None:
self._data[collection].pop(doc_id, None)

def _process_timestamps(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Replace server timestamp sentinels with actual timestamps."""
"""Deep-copy the payload, recursing into nested maps."""
result = {}
for key, value in data.items():
if isinstance(value, _ServerTimestamp):
result[key] = datetime.now(UTC).replace(tzinfo=None)
elif isinstance(value, dict):
if isinstance(value, dict):
result[key] = self._process_timestamps(value)
else:
result[key] = value
Expand Down Expand Up @@ -351,10 +342,6 @@ def list_subcollection(
out.append(row)
return out

def server_timestamp(self) -> Any:
"""Get server timestamp sentinel."""
return _ServerTimestamp()

def get_api_key_by_uid(self, key_uid: str) -> Optional[Dict[str, Any]]:
"""Return the api_keys document whose key_uid field matches."""
with self._lock:
Expand All @@ -366,12 +353,10 @@ def get_api_key_by_uid(self, key_uid: str) -> Optional[Dict[str, Any]]:
return None

def _process_timestamps(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Replace server timestamp sentinels with actual timestamps."""
"""Deep-copy the payload, recursing into nested maps."""
result = {}
for key, value in data.items():
if isinstance(value, _ServerTimestamp):
result[key] = datetime.now(UTC).replace(tzinfo=None)
elif isinstance(value, dict):
if isinstance(value, dict):
result[key] = self._process_timestamps(value)
else:
result[key] = value
Expand Down
26 changes: 0 additions & 26 deletions backend/test/database/test_firestore_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import os
import pytest
from datetime import datetime

from app.database.emulator import EmulatorDatabase
from app.database.interface import DocumentNotFoundError
Expand Down Expand Up @@ -215,31 +214,6 @@ def test_array_union_no_duplicates(self, db):
assert "vue" in user["tags"]


class TestServerTimestamp:
"""Test server timestamp functionality against Firestore emulator."""

def test_server_timestamp_on_set(self, db):
"""Test server timestamp is replaced with actual time."""
db.set("users", "user-1", {
"name": "Alice",
"created_at": db.server_timestamp()
})

user = db.get("users", "user-1")
assert "created_at" in user
# Firestore returns datetime objects
assert isinstance(user["created_at"], datetime)

def test_server_timestamp_on_update(self, db):
"""Test server timestamp in update operation."""
db.set("users", "user-1", {"name": "Alice"})
db.update("users", "user-1", {"updated_at": db.server_timestamp()})

user = db.get("users", "user-1")
assert "updated_at" in user
assert isinstance(user["updated_at"], datetime)


class TestEmulatorConnection:
"""Test emulator-specific functionality."""

Expand Down
38 changes: 0 additions & 38 deletions backend/test/database/test_memory_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"""

import pytest
from datetime import datetime
from app.database.memory import InMemoryDatabase
from app.database.interface import DocumentNotFoundError

Expand Down Expand Up @@ -345,43 +344,6 @@ def test_array_union_nonexistent_doc(self, db):
db.array_union("users", "nonexistent", "tags", ["python"])


class TestServerTimestamp:
"""Test server timestamp functionality."""

def test_server_timestamp_on_set(self, db):
"""Test server timestamp is replaced with actual time."""
db.set("users", "user-1", {
"name": "Alice",
"created_at": db.server_timestamp()
})

user = db.get("users", "user-1")
assert "created_at" in user
assert isinstance(user["created_at"], datetime)

def test_server_timestamp_on_update(self, db):
"""Test server timestamp in update operation."""
db.set("users", "user-1", {"name": "Alice"})
db.update("users", "user-1", {"updated_at": db.server_timestamp()})

user = db.get("users", "user-1")
assert "updated_at" in user
assert isinstance(user["updated_at"], datetime)

def test_server_timestamp_in_transaction(self, db):
"""Test server timestamp in transaction."""
def create_with_timestamp(tx):
tx.set("users", "user-1", {
"name": "Alice",
"created_at": db.server_timestamp()
})

db.run_transaction(create_with_timestamp)

user = db.get("users", "user-1")
assert isinstance(user["created_at"], datetime)


class TestIsolation:
"""Test data isolation and cleanup."""

Expand Down