Skip to content

Commit 73c2e89

Browse files
committed
Refactor Account and Currency repositories with caching and enhanced member operations
- Added Redis-based caching for entities like account IDs, currencies, and default accounts. - Updated AccountRepository with improved member add/remove handling and safer default account operations. - Introduced new result type for account deletion to handle default account restrictions. - Enhanced handling of currency updates by invalidating relevant caches. - Adjusted UUID handling for better serialization and consistency. - Improved documentation for repository interfaces and reduced reliance on EntityID.
1 parent 1e2b51a commit 73c2e89

File tree

12 files changed

+179
-49
lines changed

12 files changed

+179
-49
lines changed

surf-transaction-api/src/main/kotlin/dev/slne/surf/transaction/api/account/result/AccountDeleteResult.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ package dev.slne.surf.transaction.api.account.result
22

33
enum class AccountDeleteResult {
44
SUCCESS,
5+
DEFAULT_ACCOUNT_CANNOT_BE_DELETED,
56
ACCOUNT_NOT_FOUND;
67
}

surf-transaction-core/src/main/kotlin/dev/slne/surf/transaction/core/TransactionInstance.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import kotlinx.coroutines.withContext
1010
import java.nio.file.Path
1111

1212
abstract class TransactionInstance {
13-
1413
val databaseApi = DatabaseApi.create(dataPath)
1514

1615
abstract val dataPath: Path

surf-transaction-core/src/main/kotlin/dev/slne/surf/transaction/core/account/AccountImpl.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
package dev.slne.surf.transaction.core.account
22

3+
import dev.slne.surf.surfapi.core.api.serializer.java.uuid.SerializableStringUUID
34
import dev.slne.surf.transaction.api.account.Account
45
import dev.slne.surf.transaction.api.account.member.AccountMemberOperations
56
import dev.slne.surf.transaction.api.transactional.Transactional
67
import dev.slne.surf.transaction.core.account.member.AccountMemberOperationsImpl
78
import dev.slne.surf.transaction.core.component.Components
89
import dev.slne.surf.transaction.core.transactional.TransactionalImpl
9-
import java.util.*
10+
import kotlinx.serialization.Serializable
1011

12+
@Serializable
1113
class AccountImpl(
12-
override val accountId: UUID,
14+
override val accountId: SerializableStringUUID,
1315
override val name: String,
14-
override val ownerUuid: UUID,
16+
override val ownerUuid: SerializableStringUUID,
1517
override val defaultAccount: Boolean = false,
16-
override val members: Set<UUID>
18+
override val members: Set<SerializableStringUUID>
1719
) : Account,
1820
Transactional by TransactionalImpl(),
1921
AccountMemberOperations by AccountMemberOperationsImpl(accountId, members) {

surf-transaction-core/src/main/kotlin/dev/slne/surf/transaction/core/account/AccountServiceImpl.kt

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ import dev.slne.surf.transaction.api.account.member.results.AccountMemberResult
77
import dev.slne.surf.transaction.api.account.result.AccountCreationResult
88
import dev.slne.surf.transaction.api.account.result.AccountDeleteResult
99
import dev.slne.surf.transaction.core.db.account.AccountRepository
10+
import dev.slne.surf.transaction.core.redis.RedisService
1011
import java.util.*
12+
import kotlin.time.Duration.Companion.minutes
1113

1214
@AutoService(AccountService::class)
1315
class AccountServiceImpl : AccountService {
16+
private val defaultAccountCache =
17+
RedisService.cache<UUID, AccountImpl>("default_account", 10.minutes)
18+
1419
override suspend fun getAccountByAccountId(accountId: UUID): Account? {
1520
return AccountRepository.findAccountByAccountId(accountId)
1621
}
@@ -25,13 +30,13 @@ class AccountServiceImpl : AccountService {
2530
): AccountCreationResult {
2631
val name = name.trim().replace(" ", "_").lowercase()
2732

28-
if (name.length < 3) {
33+
if (name.length < Account.MIN_NAME_LENGTH) {
2934
return AccountCreationResult.Failed(
3035
AccountCreationResult.FailureReason.NAME_TOO_SHORT
3136
)
3237
}
3338

34-
if (name.length > 32) {
39+
if (name.length > Account.MAX_NAME_LENGTH) {
3540
return AccountCreationResult.Failed(
3641
AccountCreationResult.FailureReason.NAME_TOO_LONG
3742
)
@@ -64,10 +69,16 @@ class AccountServiceImpl : AccountService {
6469
}
6570

6671
suspend fun getDefaultAccount(playerUuid: UUID): Account {
67-
return AccountRepository.findOrCreateDefaultAccount(playerUuid)
72+
return defaultAccountCache.cachedOrLoad(playerUuid) {
73+
AccountRepository.findOrCreateDefaultAccount(playerUuid)
74+
}
6875
}
6976

7077
suspend fun deleteAccount(account: Account): AccountDeleteResult {
78+
if (account.defaultAccount) {
79+
return AccountDeleteResult.DEFAULT_ACCOUNT_CANNOT_BE_DELETED
80+
}
81+
7182
val count = AccountRepository.deleteAccount(account.accountId)
7283
if (count == 0) {
7384
return AccountDeleteResult.ACCOUNT_NOT_FOUND
@@ -81,15 +92,35 @@ class AccountServiceImpl : AccountService {
8192
executor: UUID,
8293
target: UUID
8394
): AccountMemberResult {
84-
return AccountRepository.addMemberToAccount(accountId, executor, target)
95+
val (result, accountOwnerUuid) = AccountRepository.addMemberToAccount(
96+
accountId,
97+
executor,
98+
target
99+
)
100+
101+
if (result == AccountMemberResult.SUCCESS && accountOwnerUuid != null) {
102+
defaultAccountCache.invalidate(accountOwnerUuid)
103+
}
104+
105+
return result
85106
}
86107

87108
suspend fun removeMemberFromAccount(
88109
accountId: UUID,
89110
executor: UUID,
90111
target: UUID
91112
): AccountMemberResult {
92-
return AccountRepository.removeMemberFromAccount(accountId, executor, target)
113+
val (result, accountOwnerUuid) = AccountRepository.removeMemberFromAccount(
114+
accountId,
115+
executor,
116+
target
117+
)
118+
119+
if (result == AccountMemberResult.SUCCESS && accountOwnerUuid != null) {
120+
defaultAccountCache.invalidate(accountOwnerUuid)
121+
}
122+
123+
return result
93124
}
94125

95126
suspend fun completeAccountNameSuggestions(input: String, maxSuggestions: Int): List<String> {

surf-transaction-core/src/main/kotlin/dev/slne/surf/transaction/core/component/Components.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ object Components {
6060
fun formatDeletionResult(result: AccountDeleteResult) = buildText {
6161
when (result) {
6262
AccountDeleteResult.SUCCESS -> success("Das Konto wurde erfolgreich gelöscht")
63+
AccountDeleteResult.DEFAULT_ACCOUNT_CANNOT_BE_DELETED -> error("Das Standardkonto kann nicht gelöscht werden")
6364
AccountDeleteResult.ACCOUNT_NOT_FOUND -> error("Das Konto konnte nicht gefunden werden")
6465
}
6566
}

surf-transaction-core/src/main/kotlin/dev/slne/surf/transaction/core/currency/CurrencyServiceImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.slne.surf.transaction.core.currency
22

3+
import com.google.auto.service.AutoService
34
import dev.slne.surf.surfapi.core.api.util.toObjectSet
45
import dev.slne.surf.transaction.api.currency.Currency.Companion.CURRENCY_NAME_MAX_LENGTH
56
import dev.slne.surf.transaction.api.currency.Currency.Companion.CURRENCY_SYMBOL_MAX_LENGTH
@@ -11,6 +12,7 @@ import dev.slne.surf.transaction.core.redis.events.currency.CurrencyCreatedEvent
1112
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
1213
import kotlin.properties.Delegates
1314

15+
@AutoService(CurrencyService::class)
1416
class CurrencyServiceImpl : CurrencyService {
1517
override var defaultCurrency: CurrencyImpl by Delegates.notNull()
1618
override var currencies: Set<CurrencyImpl> by Delegates.notNull()

surf-transaction-core/src/main/kotlin/dev/slne/surf/transaction/core/db/account/AccountRepository.kt

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,96 @@ package dev.slne.surf.transaction.core.db.account
22

33
import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.core.dao.id.EntityID
44
import dev.slne.surf.transaction.api.account.member.results.AccountMemberResult
5-
import dev.slne.surf.transaction.api.account.result.AccountDeleteResult
65
import dev.slne.surf.transaction.core.account.AccountImpl
76
import java.util.*
87

8+
/**
9+
* Internal persistence abstraction for reading and mutating accounts and their members.
10+
*
11+
* Backed by [AccountRepositoryImpl] / Exposed tables.
12+
*/
913
interface AccountRepository {
1014

11-
suspend fun findAccountIDByAccountId(accountId: UUID): EntityID<Long>?
15+
/**
16+
* Resolves the internal database id for the given public [accountId].
17+
*
18+
* This method is not executed inside a database transaction.
19+
* The caller is responsible for ensuring transactional safety if required.
20+
*
21+
* @return the entity id or `null` if no account exists.
22+
*/
23+
suspend fun findAccountIDByAccountId(accountId: UUID): Long?
24+
25+
/**
26+
* Loads an account by its public [accountId] including member UUIDs.
27+
*
28+
* @return the account or `null` if not found.
29+
*/
1230
suspend fun findAccountByAccountId(accountId: UUID): AccountImpl?
31+
32+
/**
33+
* Loads an account by its unique [name] including member UUIDs.
34+
*
35+
* @return the account or `null` if not found.
36+
*/
1337
suspend fun findAccountByName(name: String): AccountImpl?
38+
39+
/**
40+
* Loads all accounts owned by [ownerUuid] including their members.
41+
*/
1442
suspend fun findAccountsByOwner(ownerUuid: UUID): List<AccountImpl>
1543

44+
/**
45+
* Checks whether an account with [name] exists.
46+
*/
1647
suspend fun existsByAccountName(name: String): Boolean
48+
49+
/**
50+
* Creates a new account for [ownerId].
51+
*
52+
* If [defaultAccount] is `true`, any existing default account for that owner will be unset.
53+
*/
1754
suspend fun createAccount(ownerId: UUID, name: String, defaultAccount: Boolean): AccountImpl
1855

56+
/**
57+
* Returns the current default account for [ownerId] or creates one if missing.
58+
*
59+
* The created default account uses the owner UUID string as name.
60+
*/
1961
suspend fun findOrCreateDefaultAccount(ownerId: UUID): AccountImpl
2062

63+
/**
64+
* Adds [target] as a member to the account identified by [accountId].
65+
*
66+
* @return a pair of the operation result and the account owner UUID (if available).
67+
*/
2168
suspend fun addMemberToAccount(
2269
accountId: UUID,
2370
executor: UUID,
2471
target: UUID
25-
): AccountMemberResult
72+
): Pair<AccountMemberResult, UUID?>
2673

74+
/**
75+
* Removes [target] from the members of the account identified by [accountId].
76+
*
77+
* @return a pair of the operation result and the account owner UUID (if available).
78+
*/
2779
suspend fun removeMemberFromAccount(
2880
accountId: UUID,
2981
executor: UUID,
3082
target: UUID
31-
): AccountMemberResult
83+
): Pair<AccountMemberResult, UUID?>
3284

85+
/**
86+
* Deletes the account identified by [accountId].
87+
*
88+
* @return number of deleted rows.
89+
*/
3390
suspend fun deleteAccount(accountId: UUID): Int
3491

92+
/**
93+
* Returns up to [maxSuggestions] account names containing [input], ordered by last update.
94+
*/
3595
suspend fun completeAccountNameSuggestions(input: String, maxSuggestions: Int): List<String>
3696

3797
companion object : AccountRepository by AccountRepositoryImpl()

surf-transaction-core/src/main/kotlin/dev/slne/surf/transaction/core/db/account/AccountRepositoryImpl.kt

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.r2dbc.*
99
import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.r2dbc.transactions.suspendTransaction
1010
import dev.slne.surf.transaction.api.account.member.results.AccountMemberResult
1111
import dev.slne.surf.transaction.core.account.AccountImpl
12+
import dev.slne.surf.transaction.core.redis.RedisService
1213
import kotlinx.coroutines.flow.*
1314
import java.util.*
15+
import kotlin.time.Duration.Companion.minutes
1416

1517
class AccountRepositoryImpl : AccountRepository {
16-
override suspend fun findAccountIDByAccountId(accountId: UUID): EntityID<Long>? {
17-
return AccountTable.select(AccountTable.id)
18-
.where { AccountTable.accountId eq accountId }
19-
.singleOrNull()
20-
?.get(AccountTable.id)
18+
private val accountIdCache = RedisService.cache<UUID, Long>("account_id", 10.minutes)
19+
20+
override suspend fun findAccountIDByAccountId(accountId: UUID): Long? {
21+
return accountIdCache.cachedOrLoadNullable(accountId) {
22+
AccountTable.select(AccountTable.id)
23+
.where { AccountTable.accountId eq accountId }
24+
.singleOrNull()
25+
?.get(AccountTable.id)?.value
26+
}
2127
}
2228

2329
override suspend fun findAccountByAccountId(
@@ -119,11 +125,11 @@ class AccountRepositoryImpl : AccountRepository {
119125
accountId: UUID,
120126
executor: UUID,
121127
target: UUID
122-
): AccountMemberResult = suspendTransaction {
128+
): Pair<AccountMemberResult, UUID?> = suspendTransaction {
123129
val accountID = findAccountIDByAccountId(accountId)
124130

125131
if (accountID == null) {
126-
return@suspendTransaction AccountMemberResult.ACCOUNT_NOT_FOUND
132+
return@suspendTransaction AccountMemberResult.ACCOUNT_NOT_FOUND to null
127133
}
128134

129135
val alreadyMember = AccountMemberTable.select(AccountMemberTable.id)
@@ -133,42 +139,50 @@ class AccountRepositoryImpl : AccountRepository {
133139
}.count() > 0
134140

135141
if (alreadyMember) {
136-
return@suspendTransaction AccountMemberResult.NOTHING_CHANGED
142+
return@suspendTransaction AccountMemberResult.NOTHING_CHANGED to null
137143
}
138144

139145
AccountMemberTable.insert {
140146
it[this.accountId] = accountID
141147
it[this.memberId] = target
142148
}
143149

144-
AccountMemberResult.SUCCESS
150+
val accountOwnerUuid = AccountTable.select(AccountTable.ownerId)
151+
.where { AccountTable.id eq accountID }
152+
.single()[AccountTable.ownerId]
153+
154+
AccountMemberResult.SUCCESS to accountOwnerUuid
145155
}
146156

147157
override suspend fun removeMemberFromAccount(
148158
accountId: UUID,
149159
executor: UUID,
150160
target: UUID
151-
): AccountMemberResult = suspendTransaction {
161+
): Pair<AccountMemberResult, UUID?> = suspendTransaction {
152162
val accountID = findAccountIDByAccountId(accountId)
153163

154164
if (accountID == null) {
155-
return@suspendTransaction AccountMemberResult.ACCOUNT_NOT_FOUND
165+
return@suspendTransaction AccountMemberResult.ACCOUNT_NOT_FOUND to null
156166
}
157167

158168
val count = AccountMemberTable.deleteWhere {
159169
(AccountMemberTable.accountId eq accountID) and (AccountMemberTable.memberId eq target)
160170
}
161171

162172
if (count == 0) {
163-
return@suspendTransaction AccountMemberResult.NOTHING_CHANGED
173+
return@suspendTransaction AccountMemberResult.NOTHING_CHANGED to null
164174
}
165175

166-
AccountMemberResult.SUCCESS
176+
val accountOwnerUuid = AccountTable.select(AccountTable.ownerId)
177+
.where { AccountTable.id eq accountID }
178+
.single()[AccountTable.ownerId]
179+
180+
AccountMemberResult.SUCCESS to accountOwnerUuid
167181
}
168182

169183
override suspend fun deleteAccount(accountId: UUID): Int = suspendTransaction {
170184
AccountTable.deleteWhere { AccountTable.accountId eq accountId }
171-
}
185+
}.also { accountIdCache.invalidate(accountId) }
172186

173187
override suspend fun completeAccountNameSuggestions(
174188
input: String,

surf-transaction-core/src/main/kotlin/dev/slne/surf/transaction/core/db/currency/CurrencyRepository.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package dev.slne.surf.transaction.core.db.currency
22

3-
import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.core.dao.id.EntityID
43
import dev.slne.surf.transaction.core.currency.CurrencyCreateResult
54
import dev.slne.surf.transaction.core.currency.CurrencyDefaultResult
65
import dev.slne.surf.transaction.core.currency.CurrencyImpl
76

87
interface CurrencyRepository {
98

109
suspend fun findAllAndCreateDefaultCurrencyIfMissing(): List<CurrencyImpl>
11-
suspend fun findCurrencyIDByName(name: String): EntityID<Long>?
10+
suspend fun findCurrencyIDByName(name: String): Long?
1211

1312
suspend fun createCurrency(currency: CurrencyImpl): CurrencyCreateResult
1413
suspend fun makeDefaultCurrency(currency: CurrencyImpl): CurrencyDefaultResult

0 commit comments

Comments
 (0)