From 23a5f8437caa8f9204e2a59d3b6be138103a2dd0 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 22 May 2026 11:46:41 +0200 Subject: [PATCH] fix: PSQL query regressions (#3748) * fix: PSQL query regressions - user.repository: replace MSSQL LIKE character-class with PostgreSQL regex operator in getNextRef; the [0-9] class matched 0 rows on Postgres, causing a null deref on every userData update and mail confirm with account merge - config: set relationLoadStrategy 'query' to avoid PostgreSQL's 1664-column target-list limit when finding deep relation trees (broke GET /v1/transaction/single) * fix: improve ref regex * fix: circular JSON * fix: split transaction reading * fix: linter --------- Co-authored-by: David May --- .../controllers/transaction.controller.ts | 42 ++++++++++++------- .../generic/kyc/entities/kyc-step.entity.ts | 6 +++ .../user/models/user/user.repository.ts | 4 +- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/subdomains/core/history/controllers/transaction.controller.ts b/src/subdomains/core/history/controllers/transaction.controller.ts index e00f0be68b..d62b29f3cc 100644 --- a/src/subdomains/core/history/controllers/transaction.controller.ts +++ b/src/subdomains/core/history/controllers/transaction.controller.ts @@ -710,16 +710,8 @@ export class TransactionController { }, accountId?: number, ): Promise { - const relations: FindOptionsRelations = { - buyCrypto: { - buy: true, - cryptoRoute: true, - cryptoInput: true, - bankTx: true, - chargebackOutput: true, - checkoutTx: true, - }, - buyFiat: { sell: true, cryptoInput: true, bankTx: true, fiatOutput: true }, + // Split into two queries to stay under PostgreSQL's 1664 column limit + const baseRelations: FindOptionsRelations = { refReward: true, bankTx: { transaction: true }, cryptoInput: true, @@ -731,25 +723,43 @@ export class TransactionController { }; let tx: Transaction | TransactionRequest; - if (id) tx = await this.transactionService.getTransactionById(+id, relations); + if (id) tx = await this.transactionService.getTransactionById(+id, baseRelations); const uidParam = uid ?? orderUid; if (uidParam) { tx = Config.formats.transactionUid.test(uidParam) - ? await this.transactionService.getTransactionByUid(uidParam, relations) - : ((await this.transactionService.getTransactionByRequestUid(uidParam, relations)) ?? + ? await this.transactionService.getTransactionByUid(uidParam, baseRelations) + : ((await this.transactionService.getTransactionByRequestUid(uidParam, baseRelations)) ?? (await this.transactionRequestService.getTransactionRequestByUid(uidParam, { user: { userData: true } }))); } if (orderId) tx = - (await this.transactionService.getTransactionByRequestId(+orderId, relations)) ?? + (await this.transactionService.getTransactionByRequestId(+orderId, baseRelations)) ?? (await this.transactionRequestService.getTransactionRequest(+orderId, { user: { userData: true } })); if (externalId && accountId) - tx = await this.transactionService.getTransactionByExternalId(externalId, accountId, relations); + tx = await this.transactionService.getTransactionByExternalId(externalId, accountId, baseRelations); + + if (ckoId) tx = await this.transactionService.getTransactionByCkoId(ckoId, baseRelations); - if (ckoId) tx = await this.transactionService.getTransactionByCkoId(ckoId, relations); + // Load buyCrypto/buyFiat separately + if (tx instanceof Transaction) { + tx.buyCrypto = await this.buyCryptoService.getBuyCryptoByTransactionId(tx.id, { + buy: true, + cryptoRoute: true, + cryptoInput: true, + bankTx: true, + chargebackOutput: true, + checkoutTx: true, + }); + tx.buyFiat = await this.buyFiatService.getBuyFiatByTransactionId(tx.id, { + sell: true, + cryptoInput: true, + bankTx: true, + fiatOutput: true, + }); + } return tx; } diff --git a/src/subdomains/generic/kyc/entities/kyc-step.entity.ts b/src/subdomains/generic/kyc/entities/kyc-step.entity.ts index e3354f0f78..f3a6852c4f 100644 --- a/src/subdomains/generic/kyc/entities/kyc-step.entity.ts +++ b/src/subdomains/generic/kyc/entities/kyc-step.entity.ts @@ -466,4 +466,10 @@ export class KycStep extends IEntity { get isManual(): boolean { return this.type === KycStepType.MANUAL; } + + // prevent circular reference: KycStep.userData -> UserData.kycSteps -> KycStep + toJSON(): any { + const { userData: _userData, ...rest } = this; + return rest; + } } diff --git a/src/subdomains/generic/user/models/user/user.repository.ts b/src/subdomains/generic/user/models/user/user.repository.ts index f97896ec9e..8d9b65ca7c 100644 --- a/src/subdomains/generic/user/models/user/user.repository.ts +++ b/src/subdomains/generic/user/models/user/user.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { BaseRepository } from 'src/shared/repositories/base.repository'; import { Util } from 'src/shared/utils/util'; -import { EntityManager, Like } from 'typeorm'; +import { EntityManager, Raw } from 'typeorm'; import { KycLevel } from '../user-data/user-data.enum'; import { User } from './user.entity'; @@ -28,7 +28,7 @@ export class UserRepository extends BaseRepository { // get highest numerical ref const nextRef = await this.findOne({ select: { id: true, ref: true }, - where: { ref: Like('%[0-9]-[0-9]%') }, + where: { ref: Raw((alias) => `${alias} ~ '^[0-9]{3}-[0-9]{3}$'`) }, order: { ref: 'DESC' }, }).then((u) => +u.ref.replace('-', '') + 1);