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
5 changes: 1 addition & 4 deletions papyrus/services/library_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from papyrus.schemas.sync import PowerSyncCrudMutation
from papyrus.services import media as media_service
from papyrus.services.library_validation import convert_value, normalize_book_payload, uuid_value
from papyrus.services.library_validation import convert_value, uuid_value

MODELS: dict[str, Any] = {
"reading_goals": SyncReadingGoal,
Expand Down Expand Up @@ -233,9 +233,6 @@ async def apply_library_mutation(
payload = dict(mutation.op_data or {})
payload = {key: value for key, value in payload.items() if key not in {"owner_user_id", "updated_at"}}

if table == "books":
payload = normalize_book_payload(payload)

if is_membership:
payload = membership_values(table, mutation.id, payload)

Expand Down
45 changes: 1 addition & 44 deletions papyrus/services/library_validation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Type conversion and backwards compatibility for library queue payloads."""
"""Type conversion and validation for library queue payloads."""

from datetime import UTC, datetime
from math import isfinite
Expand All @@ -9,26 +9,6 @@
from sqlalchemy.dialects.postgresql import JSONB

from papyrus.core.exceptions import ValidationError
from papyrus.models.sync import SyncBook

PROMOTED_BOOK_FIELDS = frozenset(
{
"publication_date",
"file_format",
"file_size",
"file_hash",
"is_physical",
"physical_location",
"lent_to",
"lent_at",
"series_id",
"series_name",
"series_number",
"started_at",
"completed_at",
"last_read_at",
}
)


def uuid_value(value: object, name: str) -> UUID:
Expand All @@ -38,29 +18,6 @@ def uuid_value(value: object, name: str) -> UUID:
raise ValidationError(f"{name} must be a valid UUID") from exc


def normalize_book_payload(payload: dict[str, Any]) -> dict[str, Any]:
"""Keep the legacy envelope intact while promoting queued legacy values."""
envelope = payload.get("custom_metadata")

if isinstance(envelope, dict):
promoted = {}

for key, value in envelope.items():
if key not in PROMOTED_BOOK_FIELDS or key in payload:
continue

try:
convert_value(SyncBook.__table__.columns[key], value)
except ValidationError:
continue

promoted[key] = value

return {**promoted, **payload}

return payload


def finite_number(value: Any) -> bool:
if not isinstance(value, int | float) or isinstance(value, bool):
return False
Expand Down
23 changes: 10 additions & 13 deletions tests/api/routes/test_library_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,24 +155,21 @@ async def test_membership_remove_readd_and_pair_validation(client, auth_headers)
).status_code == 400


async def test_legacy_envelope_normalizes_and_preserves_metadata(client, auth_headers, db_session):
envelope = {
"publication_date": "2020-01-01T00:00:00Z",
"series_id": "old-id",
"file_size": 42,
"is_physical": True,
"custom_metadata": {"key": "value"},
}

async def test_custom_metadata_does_not_override_book_fields(client, auth_headers, db_session):
metadata = {"file_size": 42, "is_physical": True, "reader_locator": {"version": 1}, "key": "value"}
response = await upload(
client,
auth_headers,
mutation("books", uuid4(), {"title": "Book", "custom_metadata": envelope, "series_id": "explicit"}),
mutation(
"books",
uuid4(),
{"title": "Book", "custom_metadata": metadata, "series_id": "series", "file_size": 100},
),
)

assert response.status_code == 200, response.text
row = (await db_session.execute(text("SELECT series_id, file_size, is_physical, custom_metadata FROM books"))).one()
assert row == ("explicit", 42, True, envelope)
assert row == ("series", 100, False, metadata)


async def test_foreign_reference_rolls_back_mixed_batch(client, auth_headers, db_session):
Expand Down Expand Up @@ -286,7 +283,7 @@ async def test_concurrent_patches_preserve_unrelated_values(client, auth_headers
mutation(
"books",
book,
{"title": "Book", "author": "Author", "custom_metadata": {"custom_metadata": {"keep": True}}},
{"title": "Book", "author": "Author", "custom_metadata": {"keep": True}},
),
)
).status_code == 200
Expand Down Expand Up @@ -316,7 +313,7 @@ async def test_concurrent_patches_preserve_unrelated_values(client, auth_headers

assert [result.status_code for result in results] == [200, 200]
row = (await db_session.execute(text("SELECT title, author, custom_metadata FROM books"))).one()
assert row == ("New title", "New author", {"custom_metadata": {"keep": True}})
assert row == ("New title", "New author", {"keep": True})


async def test_library_field_validation_is_atomic(client, auth_headers, db_session):
Expand Down
16 changes: 1 addition & 15 deletions tests/services/test_library_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from papyrus.core.exceptions import ValidationError
from papyrus.models import SyncAnnotation, SyncBook
from papyrus.services.library_validation import convert_value, normalize_book_payload
from papyrus.services.library_validation import convert_value


@pytest.mark.parametrize(
Expand All @@ -16,17 +16,3 @@
def test_overflows_are_controlled_validation_errors(column, value):
with pytest.raises(ValidationError):
convert_value(column, value)


def test_legacy_invalid_values_remain_in_envelope_without_blocking_queue():
envelope = {"is_physical": None, "series_number": "invalid", "file_size": 10**500, "lent_to": "Reader"}
normalized = normalize_book_payload({"custom_metadata": envelope})
assert normalized == {"custom_metadata": envelope, "lent_to": "Reader"}


def test_explicit_promoted_value_wins_and_stays_subject_to_validation():
normalized = normalize_book_payload({"custom_metadata": {"is_physical": True}, "is_physical": None})
assert normalized["is_physical"] is None

with pytest.raises(ValidationError):
convert_value(SyncBook.__table__.c.is_physical, normalized["is_physical"])
Loading