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 }