feat: add created_at and updated_at to wallets and accounts (#2177)

* feat: add `created_at` and `updated_at` to wallets and accounts

the title says it all :)

* fixup!

* nitpicks :)

* fixup!

* sqlite fix

* sqlite compat

* fixup!

* mypy

* revert db py

* motorinas suggestions

* int(time()) proper default values in migration

* uncomment migration

* use now = int(time()) idiom to make code more readable

also this fixes the issue where time() is called multiple times
providing different return values for multiple invocations

---------

Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
This commit is contained in:
dni ⚡ 2023-12-21 13:37:56 +01:00 committed by GitHub
parent 4e55ea18e5
commit 815c3e61e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 20 deletions

View File

@ -1,5 +1,6 @@
import datetime
import json
from time import time
from typing import Any, Dict, List, Literal, Optional
from urllib.parse import urlparse
from uuid import UUID, uuid4
@ -51,10 +52,13 @@ async def create_user(
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
user_id = uuid4().hex
tsph = db.timestamp_placeholder
now = int(time())
await db.execute(
"""
INSERT INTO accounts (id, email, username, pass, extra)
VALUES (?, ?, ?, ?, ?)
f"""
INSERT INTO accounts
(id, email, username, pass, extra, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, {tsph}, {tsph})
""",
(
user_id,
@ -62,6 +66,8 @@ async def create_user(
data.username,
pwd_context.hash(data.password),
json.dumps(dict(user_config)) if user_config else "{}",
now,
now,
),
)
new_account = await get_account(user_id=user_id)
@ -82,9 +88,13 @@ async def create_account(
user_id = uuid4().hex
extra = json.dumps(dict(user_config)) if user_config else "{}"
now = int(time())
await (conn or db).execute(
"INSERT INTO accounts (id, email, extra) VALUES (?, ?, ?)",
(user_id, email, extra),
f"""
INSERT INTO accounts (id, email, extra, created_at, updated_at)
VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
""",
(user_id, email, extra, now, now),
)
new_account = await get_account(user_id=user_id, conn=conn)
@ -116,12 +126,20 @@ async def update_account(
email = user.email or email
extra = user_config or user.config
now = int(time())
await db.execute(
"""
UPDATE accounts SET (username, email, extra) = (?, ?, ?)
f"""
UPDATE accounts SET (username, email, extra, updated_at) =
(?, ?, ?, {db.timestamp_placeholder})
WHERE id = ?
""",
(username, email, json.dumps(dict(extra)) if extra else "{}", user_id),
(
username,
email,
json.dumps(dict(extra)) if extra else "{}",
now,
user_id,
),
)
user = await get_user(user_id)
@ -133,7 +151,7 @@ async def get_account(
user_id: str, conn: Optional[Connection] = None
) -> Optional[User]:
row = await (conn or db).fetchone(
"SELECT id, email, username FROM accounts WHERE id = ?",
"SELECT id, email, username, created_at, updated_at FROM accounts WHERE id = ?",
(user_id,),
)
@ -172,10 +190,15 @@ async def update_user_password(data: UpdateUserPassword) -> Optional[User]:
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
now = int(time())
await db.execute(
"UPDATE accounts SET pass = ? WHERE id = ?",
f"""
UPDATE accounts SET pass = ?, updated_at = {db.timestamp_placeholder}
WHERE id = ?
""",
(
pwd_context.hash(data.password),
now,
data.user_id,
),
)
@ -189,7 +212,10 @@ async def get_account_by_username(
username: str, conn: Optional[Connection] = None
) -> Optional[User]:
row = await (conn or db).fetchone(
"SELECT id, username, email FROM accounts WHERE username = ?",
"""
SELECT id, username, email, created_at, updated_at
FROM accounts WHERE username = ?
""",
(username,),
)
@ -200,7 +226,10 @@ async def get_account_by_email(
email: str, conn: Optional[Connection] = None
) -> Optional[User]:
row = await (conn or db).fetchone(
"SELECT id, username, email FROM accounts WHERE email = ?",
"""
SELECT id, username, email, created_at, updated_at
FROM accounts WHERE email = ?
""",
(email,),
)
@ -218,7 +247,11 @@ async def get_account_by_username_or_email(
async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]:
user = await (conn or db).fetchone(
"SELECT id, email, username, pass, extra FROM accounts WHERE id = ?", (user_id,)
"""
SELECT id, email, username, pass, extra, created_at, updated_at
FROM accounts WHERE id = ?
""",
(user_id,),
)
if user:
@ -392,10 +425,11 @@ async def create_wallet(
conn: Optional[Connection] = None,
) -> Wallet:
wallet_id = uuid4().hex
now = int(time())
await (conn or db).execute(
"""
INSERT INTO wallets (id, name, "user", adminkey, inkey)
VALUES (?, ?, ?, ?, ?)
f"""
INSERT INTO wallets (id, name, "user", adminkey, inkey, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
""",
(
wallet_id,
@ -403,6 +437,8 @@ async def create_wallet(
user_id,
uuid4().hex,
uuid4().hex,
now,
now,
),
)
@ -419,7 +455,10 @@ async def update_wallet(
conn: Optional[Connection] = None,
) -> Optional[Wallet]:
set_clause = []
values = []
values: list = []
set_clause.append(f"updated_at = {db.timestamp_placeholder}")
now = int(time())
values.append(now)
if name:
set_clause.append("name = ?")
values.append(name)
@ -441,13 +480,14 @@ async def update_wallet(
async def delete_wallet(
*, user_id: str, wallet_id: str, conn: Optional[Connection] = None
) -> None:
now = int(time())
await (conn or db).execute(
"""
f"""
UPDATE wallets
SET deleted = true
SET deleted = true, updated_at = {db.timestamp_placeholder}
WHERE id = ? AND "user" = ?
""",
(wallet_id, user_id),
(now, wallet_id, user_id),
)

View File

@ -1,4 +1,5 @@
import datetime
from time import time
from loguru import logger
from sqlalchemy.exc import OperationalError
@ -404,3 +405,69 @@ async def m016_add_username_column_to_accounts(db):
await db.execute("ALTER TABLE accounts ADD COLUMN extra TEXT")
except OperationalError:
pass
async def m017_add_timestamp_columns_to_accounts_and_wallets(db):
"""
Adds created_at and updated_at column to accounts and wallets.
"""
try:
await db.execute(
"ALTER TABLE accounts "
f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)
await db.execute(
"ALTER TABLE accounts "
f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)
await db.execute(
"ALTER TABLE wallets "
f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)
await db.execute(
"ALTER TABLE wallets "
f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)
# set their wallets created_at with the first payment
await db.execute(
"""
UPDATE wallets SET created_at = (
SELECT time FROM apipayments
WHERE apipayments.wallet = wallets.id
ORDER BY time ASC LIMIT 1
)
"""
)
# then set their accounts created_at with the wallet
await db.execute(
"""
UPDATE accounts SET created_at = (
SELECT created_at FROM wallets
WHERE wallets.user = accounts.id
ORDER BY created_at ASC LIMIT 1
)
"""
)
# set all to now where they are null
now = int(time())
await db.execute(
f"""
UPDATE wallets SET created_at = {db.timestamp_placeholder}
WHERE created_at IS NULL
""",
(now,),
)
await db.execute(
f"""
UPDATE accounts SET created_at = {db.timestamp_placeholder}
WHERE created_at IS NULL
""",
(now,),
)
except OperationalError as exc:
logger.error(f"Migration 17 failed: {exc}")
pass

View File

@ -30,6 +30,8 @@ class Wallet(BaseModel):
currency: Optional[str]
balance_msat: int
deleted: bool
created_at: Optional[int] = None
updated_at: Optional[int] = None
@property
def balance(self) -> int:
@ -98,6 +100,8 @@ class User(BaseModel):
super_user: bool = False
has_password: bool = False
config: Optional[UserConfig] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
@property
def wallet_ids(self) -> List[str]:

View File

@ -91,6 +91,12 @@ class Compat:
return "(strftime('%s', 'now'))"
return "<nothing>"
@property
def timestamp_column_default(self) -> str:
if self.type in {POSTGRES, COCKROACH}:
return self.timestamp_now
return "NULL"
@property
def serial_primary_key(self) -> str:
if self.type in {POSTGRES, COCKROACH}: