diff --git a/lnbits/app.py b/lnbits/app.py index ecb0164c3..12292a64d 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -17,7 +17,7 @@ from slowapi.util import get_remote_address from starlette.middleware.sessions import SessionMiddleware from lnbits.core.crud import ( - get_dbversions, + get_db_version, get_installed_extensions, update_installed_extension_state, ) @@ -313,7 +313,7 @@ async def restore_installed_extension(app: FastAPI, ext: InstallableExtension): extension = Extension.from_installable_ext(ext) register_ext_routes(app, extension) - current_version = (await get_dbversions()).get(ext.id, 0) + current_version = await get_db_version(ext.id) await migrate_extension_database(ext, current_version) # mount routes for the new version diff --git a/lnbits/commands.py b/lnbits/commands.py index 49c9feeda..3b79b6131 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -17,7 +17,7 @@ from lnbits.core.crud import ( delete_unused_wallets, delete_wallet_by_id, delete_wallet_payment, - get_dbversions, + get_db_versions, get_installed_extension, get_installed_extensions, get_payments, @@ -122,7 +122,7 @@ def database_migrate(): async def db_versions(): """Show current database versions""" async with core_db.connect() as conn: - click.echo(await get_dbversions(conn)) + click.echo(await get_db_versions(conn)) @db.command("cleanup-wallets") diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 4b0b9fe07..bc082ef7a 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -25,6 +25,7 @@ from .models import ( AccountFilters, AccountOverview, CreatePayment, + DbVersion, Payment, PaymentFilters, PaymentHistoryPoint, @@ -842,35 +843,37 @@ async def check_internal( Returns the checking_id of the internal payment if it exists, otherwise None """ - row: dict = await (conn or db).fetchone( + payment = await (conn or db).fetchone( f""" - SELECT checking_id FROM apipayments + SELECT * FROM apipayments WHERE payment_hash = :hash AND status = '{PaymentState.PENDING}' AND amount > 0 """, {"hash": payment_hash}, + Payment, ) - if not row: + if not payment: return None else: - return row["checking_id"] + return payment.checking_id -async def check_internal_status( +async def is_internal_status_success( payment_hash: str, conn: Optional[Connection] = None ) -> bool: """ - Returns True if the internal payment was successful + Returns True if the internal payment was found and has the given status, """ - row: dict = await (conn or db).fetchone( + payment = await (conn or db).fetchone( """ - SELECT status FROM apipayments + SELECT * FROM apipayments WHERE payment_hash = :payment_hash AND amount > 0 """, {"payment_hash": payment_hash}, + Payment, ) - if not row: + if not payment: return True - return row["status"] == PaymentState.SUCCESS.value + return payment.status == PaymentState.SUCCESS.value async def mark_webhook_sent(payment_hash: str, status: int) -> None: @@ -950,12 +953,18 @@ async def create_admin_settings(super_user: str, new_settings: dict): # db versions # -------------- -async def get_dbversions(conn: Optional[Connection] = None) -> dict: - result = await (conn or db).execute("SELECT db, version FROM dbversions") - _dict = {} - for row in result.mappings().all(): - _dict[row["db"]] = row["version"] - return _dict +async def get_db_version( + ext_id: str, conn: Optional[Connection] = None +) -> Optional[DbVersion]: + return await (conn or db).fetchone( + "SELECT * FROM dbversions WHERE db = :ext_id", + {"ext_id": ext_id}, + model=DbVersion, + ) + + +async def get_db_versions(conn: Optional[Connection] = None) -> list[DbVersion]: + return await (conn or db).fetchall("SELECT * FROM dbversions", model=DbVersion) async def update_migration_version(conn, db_name, version): diff --git a/lnbits/core/extensions/extension_manager.py b/lnbits/core/extensions/extension_manager.py index c15469354..af4398489 100644 --- a/lnbits/core/extensions/extension_manager.py +++ b/lnbits/core/extensions/extension_manager.py @@ -7,7 +7,7 @@ from lnbits.core import core_app_extra from lnbits.core.crud import ( create_installed_extension, delete_installed_extension, - get_dbversions, + get_db_version, get_installed_extension, update_installed_extension_state, ) @@ -28,7 +28,7 @@ async def install_extension(ext_info: InstallableExtension) -> Extension: ext_info.extract_archive() - db_version = (await get_dbversions()).get(ext_id, 0) + db_version = await get_db_version(ext_id) await migrate_extension_database(ext_info, db_version) await create_installed_extension(ext_info) diff --git a/lnbits/core/helpers.py b/lnbits/core/helpers.py index 35087b2d8..d8a5c7f84 100644 --- a/lnbits/core/helpers.py +++ b/lnbits/core/helpers.py @@ -1,6 +1,6 @@ import importlib import re -from typing import Any +from typing import Any, Optional from urllib.parse import urlparse from uuid import UUID @@ -8,17 +8,20 @@ from loguru import logger from lnbits.core import migrations as core_migrations from lnbits.core.crud import ( - get_dbversions, + get_db_versions, get_installed_extensions, update_migration_version, ) from lnbits.core.db import db as core_db from lnbits.core.extensions.models import InstallableExtension +from lnbits.core.models import DbVersion from lnbits.db import COCKROACH, POSTGRES, SQLITE, Connection from lnbits.settings import settings -async def migrate_extension_database(ext: InstallableExtension, current_version: int): +async def migrate_extension_database( + ext: InstallableExtension, current_version: Optional[DbVersion] = None +): try: ext_migrations = importlib.import_module(f"{ext.module_name}.migrations") @@ -32,14 +35,17 @@ async def migrate_extension_database(ext: InstallableExtension, current_version: async def run_migration( - db: Connection, migrations_module: Any, db_name: str, current_version: int + db: Connection, + migrations_module: Any, + db_name: str, + current_version: Optional[DbVersion] = None, ): matcher = re.compile(r"^m(\d\d\d)_") for key, migrate in migrations_module.__dict__.items(): match = matcher.match(key) if match: version = int(match.group(1)) - if version > current_version: + if not current_version or version > current_version.version: logger.debug(f"running migration {db_name}.{version}") print(f"running migration {db_name}.{version}") await migrate(db) @@ -71,7 +77,7 @@ async def load_disabled_extension_list() -> None: async def migrate_databases(): """Creates the necessary databases if they don't exist already; or migrates them.""" - current_versions = await get_dbversions() + current_versions = await get_db_versions() async with core_db.connect() as conn: exists = False if conn.type == SQLITE: @@ -87,7 +93,10 @@ async def migrate_databases(): if not exists: await core_migrations.m000_create_migrations_table(conn) - core_version = current_versions.get("core", 0) + core_version = next( + (v for v in current_versions if v.db == "core"), + DbVersion(db="core", version=0), + ) await run_migration(conn, core_migrations, "core", core_version) # here is the first place we can be sure that the @@ -95,7 +104,10 @@ async def migrate_databases(): await load_disabled_extension_list() for ext in await get_installed_extensions(): - current_version = current_versions.get(ext.id) + current_version = next( + (v for v in current_versions if v.db == ext.id), + DbVersion(db=ext.id, version=0), + ) if current_version is None: logger.warning( f"Extension {ext.id} has no migration version. This should not happen." diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 0cd25e27e..81ab9f8b3 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -469,3 +469,8 @@ class BalanceDelta(BaseModel): class SimpleStatus(BaseModel): success: bool message: str + + +class DbVersion(BaseModel): + db: str + version: int diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 0d82cfc55..9fbe9c099 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -46,7 +46,6 @@ from lnbits.wallets.base import ( from .crud import ( check_internal, - check_internal_status, create_account, create_admin_settings, create_payment, @@ -62,6 +61,7 @@ from .crud import ( get_user, get_wallet, get_wallet_payment, + is_internal_status_success, update_admin_settings, update_payment_details, update_payment_status, @@ -247,9 +247,7 @@ async def pay_invoice( extra=extra, ) - # we check if an internal invoice exists that has already been paid - # (not pending anymore) - if await check_internal_status(invoice.payment_hash, conn=conn): + if await is_internal_status_success(invoice.payment_hash, conn=conn): raise PaymentError("Internal invoice already paid.", status="failed") # check_internal() returns the checking_id of the invoice we're waiting for diff --git a/lnbits/core/views/extension_api.py b/lnbits/core/views/extension_api.py index f41ea89f9..83afd20c7 100644 --- a/lnbits/core/views/extension_api.py +++ b/lnbits/core/views/extension_api.py @@ -40,7 +40,7 @@ from ..crud import ( create_user_extension, delete_dbversion, drop_extension_db, - get_dbversions, + get_db_version, get_installed_extension, get_installed_extensions, get_user_extension, @@ -474,7 +474,7 @@ async def get_extension_release(org: str, repo: str, tag_name: str): ) async def delete_extension_db(ext_id: str): try: - db_version = (await get_dbversions()).get(ext_id, None) + db_version = await get_db_version(ext_id) if not db_version: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index 6e8d91ab7..61d3b9042 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -23,7 +23,7 @@ from lnbits.wallets import get_funding_source from ...utils.exchange_rates import allowed_currencies, currencies from ..crud import ( create_wallet, - get_dbversions, + get_db_versions, get_installed_extensions, get_user_by_id, get_wallet, @@ -104,7 +104,8 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)): all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()] inactive_extensions = [e.id for e in await get_installed_extensions(active=False)] - db_version = await get_dbversions() + db_versions = await get_db_versions() + extensions = [ { "id": ext.id, @@ -115,7 +116,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)): "isFeatured": ext.meta.featured if ext.meta else False, "dependencies": ext.meta.dependencies if ext.meta else "", "isInstalled": ext.id in installed_exts_ids, - "hasDatabaseTables": ext.id in db_version, + "hasDatabaseTables": next( + (True for version in db_versions if version.db == ext.id), False + ), "isAvailable": ext.id in all_ext_ids, "isAdminOnly": ext.id in settings.lnbits_admin_extensions, "isActive": ext.id not in inactive_extensions,