add models, migrations, and CRUD

Schema mirrors the webapp tasks module's ScheduledEvent + EventCompletion
shape:

- tasks.tasks (NIP-52 kind 31922 cache): (pubkey, d_tag) is the
  parameterized-replaceable key. JSON-encoded participants / categories
  / recurrence columns are decoded on read via _parse_task_row so each
  model can keep clean field validators.
- tasks.completions (kind 31925 cache): unique on
  (task_address, pubkey, occurrence). occurrence is NULL for one-shot
  tasks; create_completion deletes any prior claim for the same triple
  so the latest event wins.
- tasks.settings: singleton row with public_listing toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-13 11:36:13 +02:00
commit 6fbb6d4a42
3 changed files with 521 additions and 3 deletions

View file

@ -1 +1,76 @@
# Migrations are filled in by the next commit.
async def m001_initial(db):
"""
Tasks table (NIP-52 kind 31922 cache) and completions table (kind 31925
cache). The (pubkey, d_tag) pair is the parameterized-replaceable key.
"""
await db.execute(
f"""
CREATE TABLE tasks.tasks (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
pubkey TEXT NOT NULL,
d_tag TEXT NOT NULL,
title TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT,
description TEXT NOT NULL DEFAULT '',
location TEXT,
status TEXT NOT NULL DEFAULT 'pending',
event_type TEXT NOT NULL DEFAULT 'task',
participants TEXT,
categories TEXT,
recurrence TEXT,
nostr_event_id TEXT,
nostr_event_created_at INTEGER,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
await db.execute(
"CREATE UNIQUE INDEX tasks_pubkey_dtag_idx "
"ON tasks.tasks (pubkey, d_tag)"
)
await db.execute(
f"""
CREATE TABLE tasks.completions (
id TEXT PRIMARY KEY,
task_address TEXT NOT NULL,
pubkey TEXT NOT NULL,
occurrence TEXT,
task_status TEXT NOT NULL DEFAULT 'claimed',
completed_at INTEGER,
notes TEXT NOT NULL DEFAULT '',
nostr_created_at INTEGER NOT NULL,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
# occurrence is NULL for one-shot tasks; the (task_address, pubkey,
# occurrence) tuple identifies "the latest claim by this user for this
# specific occurrence".
await db.execute(
"CREATE INDEX tasks_completions_address_idx "
"ON tasks.completions (task_address)"
)
await db.execute(
"CREATE INDEX tasks_completions_pubkey_idx "
"ON tasks.completions (pubkey)"
)
async def m002_settings(db):
"""Singleton settings row for the admin UI."""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS tasks.settings (
id INTEGER PRIMARY KEY DEFAULT 1,
public_listing BOOLEAN NOT NULL DEFAULT FALSE
)
"""
)
await db.execute(
"INSERT INTO tasks.settings (id, public_listing) "
"SELECT 1, FALSE WHERE NOT EXISTS "
"(SELECT 1 FROM tasks.settings WHERE id = 1)"
)