Release: develop -> main - #4546
Merged
Merged
Conversation
* fix(transaction): load statement relations as separate queries PUT /v1/transaction/:id/invoice and /receipt returned 500 for every request: QueryFailedError "target lists can have at most 1664 entries". getTxStatementDetails' relation tree resolves to 61 joined nodes. TypeORM 0.3 expands eager relations recursively from the root entity and one level deep for each explicitly requested relation, so Asset ends up joined 10 times and Country 9 times — 537 columns between them. Measured against real entity metadata the tree selects exactly 1664 columns, precisely Postgres' MaxTupleAttributeNumber, which means a single new @column anywhere in it overflows. settlementEventId on TransactionRequest (#4469) was that column: TransactionRequest appears once in the tree, taking it to 1665 and failing deterministically. Pass relationLoadStrategy 'query' so each relation is fetched by its own query instead of one 60-way join: 1664 columns/60 joins becomes 98 columns/2 joins. Eager relations are preserved — under this strategy each relation becomes the main alias of its own query, where eager expansion is recursive, so sub-queries load strictly more than the join did (verified per branch against the metadata). Also prune three branches the statement path never dereferences (cryptoRoute, buyFiat.sell, refReward.user) and drop the explicit userData.organization, which is already eager and so cost a redundant round-trip under the new strategy. Under 'query' an unread branch costs a dependent query rather than just join columns; pruning also leaves the tree at 1219 columns as a plain join, so the fix no longer depends on the strategy alone. * fix(payment-link): reject non-integer ids before they reach Postgres GET /v1/paymentLink/payment, /v1/plp and /v1/paymentLink/recipient returned 500 with QueryFailedError "invalid input syntax for type integer" for NaN, Infinity, 1.9 and 1e+21. All three are reachable without authentication, and the filter echoes the raw Postgres message — including the parameter — back to the caller. The cause is coercing a request string with !isNaN(+x) or Number.isInteger(+x) and handing the result to a query. Neither is sufficient: +'Infinity' is Infinity, Number.isInteger(1e21) is true, and +'abc' is NaN, all of which pg serialises straight into an int4 comparison. Add Util.isDbId — digits only, within 1..2147483647 — as the single predicate, and apply it where each value is actually consumed: - deposit-route.service.getPaymentRoute: a non-id is a route label, so it now goes to the label lookup instead of the id branch (/paymentLink/recipient). - payment-link.service.createInvoice: validated at the point of use rather than on the DTO, because `route` wins over `routeId` and the `r` alias is resolved after validation — so a junk routeId is only an error when it is the value actually used (/paymentLink/payment, /plp). - payment-link.controller.parseLinkId: linkId is one of several optional lookup keys, so a malformed value is dropped and the request still resolves via externalLinkId/externalPaymentId/key. That is what the falsy NaN did implicitly; the point is only to keep the value out of SQL. - generateOcpStickers: the ids list is the primary selector, so it still 400s, but on the correct predicate — Number.isInteger accepted 1e+21 and 2147483648 and 500'd on an endpoint anonymous callers reach with no route knowledge. - transaction.controller: the same coercion on :id/invoice and :id/receipt. Transaction UIDs are ^T[A-Za-z0-9]{16}$, so a digit-only split is unambiguous. Also closes the 1e+21 hole in the existing Number.isInteger guards, which turned the error into a 22003 overflow rather than fixing it. Relocate PG_INTEGER_MAX to shared and point the two custody copies of this predicate at Util.isDbId, so the bound is expressed once rather than in three places kept in step by a comment. * fix(payment-link): require a usable identifier before assigning a link Round-2 review of this branch found that the previous commit introduced a hole in PUT /v1/paymentLink/assign, which has no auth guard. assignPaymentLink builds `where: { id, externalId, status: UNASSIGNED }` with no dispatch and no user scoping. Dropping a malformed linkId to undefined — correct for the other call sites, where linkId is one of several lookup keys — left both identifiers absent, and TypeORM omits undefined keys, so the query degenerated to `WHERE status = 'Unassigned'` and matched an arbitrary merchant's unassigned link, which was then re-pointed at the caller's route and returned to them. `?linkId=abc` reached that state because the presence check tested the raw query string rather than the parsed id. The check the previous commit removed had blocked it by accident. Guard in the service, where the dangerous query lives, so no caller can reach it, and check the parsed id in the controller so the endpoint answers 400. Also replace Util.isDbId with Util.toDbId, which returns the parsed id instead of a boolean. Callers previously re-derived the value with `+x` after the check, and whitespace was handled inconsistently — parseLinkId and the sticker id list trimmed, getPaymentRoute and the transaction routes did not, so `?id=+42` (a query string decodes `+` to a space) resolved on one endpoint and 404'd on another. Trimming now happens once, inside the parse, and there is no boolean form to pair with a laxer coercion. Adds coverage for the three behaviours the review found unpinned: the assign guard, the createInvoice routeId guard, and the pruned relation tree, which was asserted only as expect.any(Object) so any branch could have reappeared. * test: pin the four behaviours mutation testing found unguarded Round-3 review mutation-tested every production change on this branch: 11 of 15 failed loudly when reverted, 4 did not. - TransactionService.getTransactionById/ByUid forwarding relationLoadStrategy to the repository. The helper spec asserted the argument reaching the service, so the service dropping it on the floor would not have failed anything — and that forwarding is the whole fix for the 1664-column overflow. - createInvoicePayment classifying the `r` alias. Reverting it to !isNaN(+r) sent 'Infinity' and '1e+21' back down the routeId branch with nothing failing. - The id and order-id guards in getTransaction, which the earlier commit rewrote from Number.isInteger to Util.toDbId with no test on either. No production code changes.
github-actions
Bot
requested review from
TaprootFreak and
davidleomay
as code owners
July 31, 2026 19:48
TaprootFreak
approved these changes
Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automatic Release PR
This PR was automatically created after changes were pushed to develop.
Commits: 1 new commit(s)
Checklist