libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

helpers.kt (5241B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024-2025 Taler Systems S.A.
      4  *
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9  *
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14  *
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.common.db
     21 
     22 import kotlinx.coroutines.coroutineScope
     23 import kotlinx.coroutines.flow.Flow
     24 import kotlinx.coroutines.flow.first
     25 import kotlinx.coroutines.launch
     26 import kotlinx.coroutines.withTimeoutOrNull
     27 import tech.libeufin.common.HistoryParams
     28 import tech.libeufin.common.PageParams
     29 import java.sql.PreparedStatement
     30 import java.sql.ResultSet
     31 import kotlin.math.abs
     32 import kotlin.math.min
     33 
     34 /**
     35  * Hard upper bound on the number of records returned by a single
     36  * query, regardless of the limit requested by the client.
     37  */
     38 private const val MAX_RECORDS: Long = 50_000
     39 
     40 /** Apply paging logic to a sql query */
     41 suspend fun <T> DbPool.page(
     42     params: PageParams,
     43     idName: String,
     44     query: String,
     45     args: TalerStatement.() -> Unit = {},
     46     map: (ResultSet) -> T
     47 ): List<T> {
     48     val backward = params.limit < 0
     49     val pageQuery = """
     50         $query
     51         $idName ${if (backward) '<' else '>'} ?
     52         ORDER BY $idName ${if (backward) "DESC" else "ASC"}
     53         LIMIT ?
     54     """
     55     return serializable(pageQuery) {
     56         args()
     57         bind(params.offset)
     58         // Widen before abs(): abs(Int.MIN_VALUE) is Int.MIN_VALUE, which would
     59         // reach Postgres as a negative LIMIT.
     60         bind(min(MAX_RECORDS, abs(params.limit.toLong())))
     61         all { map(it) }
     62     }
     63 }
     64 
     65 /**
     66 * The following function returns the list of transactions, according
     67 * to the history parameters and perform long polling when necessary
     68 */
     69 suspend fun <T> DbPool.poolHistory(
     70     params: HistoryParams, 
     71     bankAccountId: Long,
     72     listen: suspend (Long, suspend (Flow<Long>) -> List<T>) -> List<T>,
     73     query: String,
     74     accountColumn: String = "bank_account_id",
     75     map: (ResultSet) -> T
     76 ): List<T> {
     77 
     78     suspend fun load(): List<T> = page(
     79         params.page, 
     80         "bank_transaction_id", 
     81         "$query $accountColumn=? AND", 
     82         {
     83             bind(bankAccountId)
     84         },
     85         map
     86     )
     87     
     88     // When going backward there is always at least one transaction or none
     89     return if (params.page.limit >= 0 && params.polling.timeout_ms > 0) {
     90         listen(bankAccountId) { flow ->
     91             coroutineScope {
     92                 // Start buffering notification before loading transactions to not miss any
     93                 val polling = launch {
     94                     withTimeoutOrNull(params.polling.timeout_ms) {
     95                         flow.first { it > params.page.offset } // Always forward so >
     96                     }
     97                 }    
     98                 // Initial loading
     99                 val init = load()
    100                 // Long polling if we found no transactions
    101                 if (init.isEmpty()) {
    102                     if (polling.join() != null) {
    103                         load()
    104                     } else {
    105                         init
    106                     }
    107                 } else {
    108                     polling.cancel()
    109                     init
    110                 }
    111             }
    112         }
    113     } else {
    114         load()
    115     }
    116 }
    117 
    118 /**
    119 * The following function returns the list of transactions, according
    120 * to the history parameters and perform long polling when necessary
    121 */
    122 suspend fun <T> DbPool.poolHistoryGlobal(
    123     params: HistoryParams, 
    124     listen: suspend (suspend (Flow<Long>) -> List<T>) -> List<T>,
    125     query: String,
    126     idColumnValue: String,
    127     map: (ResultSet) -> T
    128 ): List<T> {
    129 
    130     suspend fun load(): List<T> = page(
    131         params.page, 
    132         idColumnValue,
    133         query,
    134         map=map
    135     )
    136 
    137     // When going backward there is always at least one transaction or none
    138     return if (params.page.limit >= 0 && params.polling.timeout_ms > 0) {
    139         listen { flow ->
    140             coroutineScope {
    141                 // Start buffering notification before loading transactions to not miss any
    142                 val polling = launch {
    143                     withTimeoutOrNull(params.polling.timeout_ms) {
    144                         flow.first { it > params.page.offset } // Always forward so >
    145                     }
    146                 }    
    147                 // Initial loading
    148                 val init = load()
    149                 // Long polling if we found no transactions
    150                 if (init.isEmpty()) {
    151                     if (polling.join() != null) {
    152                         load()
    153                     } else {
    154                         init
    155                     }
    156                 } else {
    157                     polling.cancel()
    158                     init
    159                 }
    160             }
    161         }
    162     } else {
    163         load()
    164     }
    165 }