Compare commits

...

2 commits
v0.0.1 ... main

Author SHA1 Message Date
68e6e3d02e fix: Parse JSON extra field when reading tickets from database
Some checks failed
lint / lint (push) Has been cancelled
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
The previous fix used db.insert() which serializes the extra field to JSON.
However, the read functions (get_ticket, get_tickets, etc.) use fetchone/fetchall
without a model parameter, so the extra field comes back as a JSON string.

Added _parse_ticket_row() helper that:
- Converts empty strings to None for name/email (existing logic)
- Parses extra field from JSON string if needed (new)

The isinstance(extra, str) check ensures compatibility with both:
- SQLite: returns JSON as string
- PostgreSQL/CockroachDB: may return native JSONB as dict

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 18:03:34 +01:00
a77145e08e fix: Use db.insert() for ticket creation to fix SQLite serialization
Some checks failed
lint / lint (push) Waiting to run
lint / lint (pull_request) Has been cancelled
The previous implementation used db.execute() with a raw dict, which
failed on SQLite because the 'extra' field (TicketExtra model) was
passed as a Python dict that SQLite cannot serialize.

Using db.insert() with the Pydantic model ensures proper JSON
serialization of the extra field across all database backends
(SQLite, PostgreSQL, CockroachDB).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 17:48:46 +01:00

114
crud.py
View file

@ -1,3 +1,4 @@
import json
from datetime import datetime, timedelta, timezone
from typing import Optional
@ -6,6 +7,30 @@ from lnbits.helpers import urlsafe_short_hash
from .models import CreateEvent, Event, Ticket, TicketExtra
def _parse_ticket_row(row) -> dict:
"""
Parse a database row into a dict suitable for Ticket model creation.
Handles:
- Empty string to None conversion for name/email
- JSON string to dict conversion for extra field
"""
ticket_data = dict(row)
# Convert empty strings back to None for the model
if ticket_data.get("name") == "":
ticket_data["name"] = None
if ticket_data.get("email") == "":
ticket_data["email"] = None
# Parse extra field from JSON string if needed
# (db.insert() serializes to JSON, but manual fetchone/fetchall returns string)
extra = ticket_data.get("extra")
if isinstance(extra, str):
ticket_data["extra"] = json.loads(extra)
return ticket_data
db = Database("ext_events")
@ -20,7 +45,12 @@ async def create_ticket(
) -> Ticket:
now = datetime.now(timezone.utc)
# Handle database constraints: if user_id is provided, use empty strings for name/email
# TODO: Check if this empty string workaround is still needed.
# This converts None to empty strings for database storage because:
# 1. Database may have NOT NULL constraints on name/email columns
# 2. When user_id is provided, name/email are not used (mutually exclusive)
# 3. The get_ticket() functions convert empty strings back to None when reading
# Consider using nullable columns instead of this empty string pattern.
if user_id:
db_name = ""
db_email = ""
@ -28,7 +58,28 @@ async def create_ticket(
db_name = name or ""
db_email = email or ""
ticket = Ticket(
# Create ticket with database-compatible values for insertion
# Using db.insert() ensures proper serialization of the extra field (TicketExtra)
# across all database backends (SQLite, PostgreSQL, CockroachDB)
db_ticket = Ticket(
id=payment_hash,
wallet=wallet,
event=event,
name=db_name,
email=db_email,
user_id=user_id,
registered=False,
paid=False,
reg_timestamp=now,
time=now,
extra=TicketExtra(**extra) if extra else TicketExtra(),
)
await db.insert("events.ticket", db_ticket)
# Return ticket with original name/email values (not empty strings)
# This maintains consistency with how get_ticket() converts empty strings back to None
return Ticket(
id=payment_hash,
wallet=wallet,
event=event,
@ -42,20 +93,6 @@ async def create_ticket(
extra=TicketExtra(**extra) if extra else TicketExtra(),
)
# Create a dict for database insertion with proper handling of constraints
ticket_dict = ticket.dict()
ticket_dict["name"] = db_name
ticket_dict["email"] = db_email
await db.execute(
"""
INSERT INTO events.ticket (id, wallet, event, name, email, user_id, registered, paid, time, reg_timestamp, extra)
VALUES (:id, :wallet, :event, :name, :email, :user_id, :registered, :paid, :time, :reg_timestamp, :extra)
""",
ticket_dict
)
return ticket
async def update_ticket(ticket: Ticket) -> Ticket:
# Create a new Ticket object with corrected values for database constraints
@ -82,14 +119,7 @@ async def get_ticket(payment_hash: str) -> Optional[Ticket]:
if not row:
return None
# Convert empty strings back to None for the model
ticket_data = dict(row)
if ticket_data.get("name") == "":
ticket_data["name"] = None
if ticket_data.get("email") == "":
ticket_data["email"] = None
return Ticket(**ticket_data)
return Ticket(**_parse_ticket_row(row))
async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]:
@ -98,17 +128,7 @@ async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]:
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})")
tickets = []
for row in rows:
# Convert empty strings back to None for the model
ticket_data = dict(row)
if ticket_data.get("name") == "":
ticket_data["name"] = None
if ticket_data.get("email") == "":
ticket_data["email"] = None
tickets.append(Ticket(**ticket_data))
return tickets
return [Ticket(**_parse_ticket_row(row)) for row in rows]
async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
@ -118,17 +138,7 @@ async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
{"user_id": user_id}
)
tickets = []
for row in rows:
# Convert empty strings back to None for the model
ticket_data = dict(row)
if ticket_data.get("name") == "":
ticket_data["name"] = None
if ticket_data.get("email") == "":
ticket_data["email"] = None
tickets.append(Ticket(**ticket_data))
return tickets
return [Ticket(**_parse_ticket_row(row)) for row in rows]
async def delete_ticket(payment_hash: str) -> None:
@ -200,14 +210,4 @@ async def get_event_tickets(event_id: str) -> list[Ticket]:
{"event": event_id},
)
tickets = []
for row in rows:
# Convert empty strings back to None for the model
ticket_data = dict(row)
if ticket_data.get("name") == "":
ticket_data["name"] = None
if ticket_data.get("email") == "":
ticket_data["email"] = None
tickets.append(Ticket(**ticket_data))
return tickets
return [Ticket(**_parse_ticket_row(row)) for row in rows]