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

View file

@ -1,4 +1,5 @@
import datetime import datetime
from time import time
from loguru import logger from loguru import logger
from sqlalchemy.exc import OperationalError 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") await db.execute("ALTER TABLE accounts ADD COLUMN extra TEXT")
except OperationalError: except OperationalError:
pass 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] currency: Optional[str]
balance_msat: int balance_msat: int
deleted: bool deleted: bool
created_at: Optional[int] = None
updated_at: Optional[int] = None
@property @property
def balance(self) -> int: def balance(self) -> int:
@ -98,6 +100,8 @@ class User(BaseModel):
super_user: bool = False super_user: bool = False
has_password: bool = False has_password: bool = False
config: Optional[UserConfig] = None config: Optional[UserConfig] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
@property @property
def wallet_ids(self) -> List[str]: def wallet_ids(self) -> List[str]:

View file

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