fix(translator): run auto-translate inside the save that triggered it - #141
Merged
SearheiParkhamchuk merged 1 commit intoSep 15, 2026
Merged
Conversation
An auto-translation now joins the transaction of the save that triggered it, so the two commit or roll back together. Before this, auto-translate did nothing at all on PostgreSQL. The hook fires while the save's transaction is still open, and the translation's first act is to read the document back. That read opened its own transaction, where the row did not exist yet, so Payload answered "Not Found" and the translation stopped. SQLite and MongoDB do not isolate the uncommitted row the same way, which is why the suite stayed green. The transaction travels as `TransactionScope`, a slice carrying the one field an operation needs to join it, from the hook to every operation call on the path. Passing the whole request instead would have dragged a god type through every handler and risked `createLocalReq` mutating a request Payload shares across every document of a bulk update. Reads and writes both join it: the write targets the very document the caller has not committed, so from outside there is nothing to update. A failure inside that transaction is now surfaced instead of swallowed. Payload's `killTransaction` fires from the catch of every operation and rolls back whenever a transaction id is present, without checking whose it is — so a translation Payload rejects takes the editor's save with it. Rethrowing cannot save the edit, which is already gone; it stops the translator reporting a save that did not happen. Narrowed to `APIError`, because a provider outage throws before any operation runs and leaves the save intact. The provenance receipt joins the same transaction, or it would outlive the translation it certifies and report a document translated at a fingerprint that never committed. Two writes deliberately stay outside: the jobs runner, whose job runs after the commit and never had this defect, and the provenance cleanup on document delete, which must never roll back the delete that triggered it. `LifecycleNotifier` now logs a failed translation when the host configured no `onFailed` callback. Without it this defect left no trace anywhere, which is most of why it survived so long. Closes #124
SearheiParkhamchuk
requested review from
ChiefCreator and
dogfrogfog
as code owners
September 15, 2026 10:19
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🎉 This PR is included in version 0.13.2 🎉 The release is available on npm package (@latest dist-tag) Your semantic-release bot 📦🚀 |
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.
Closes #124.
The defect
The auto-translate hook fires from
afterChange, while the save's transaction is still open. The translation's first act is to read the document back — and that read opened a transaction of its own, where the row does not exist yet. Payload answersNotFound, the sync runner swallowed it, and nothing was translated.SQLite and MongoDB do not isolate the uncommitted row the same way, so the whole integration suite stayed green while the feature was dead on PostgreSQL.
The failure was also completely silent:
LifecycleNotifier.failedonly reported through the host'sonFailedcallback, and a host that configured none saw nothing at all.The fix
TransactionScope— a slice carrying the one field an operation needs to join a transaction — travels from the hook to every operation call on the path:Reads and writes join it. The write targets the very document the caller has not committed, so from outside the transaction there is nothing to update — measured, not assumed: an intermediate design that kept writes outside reproduced the original bug on the write instead of the read.
Passing the whole
PayloadRequestwas rejected twice over: it breaches the package rule against god types in leaf helpers, andcreateLocalReqfills its argument in place while Payload shares one request across every document of a bulk update — so the translator's skip-context would have leaked to the next document and stopped it translating. That regression now has a test.The trade, and why it is visible
Payload's
killTransactionfires from the catch of every operation and rolls back whenever a transaction id is present, without checking whose transaction it is. So a translation Payload rejects takes the editor's save with it, and the first version of this change reported success over a save that had already been thrown away.killedTheCallersTransactionnow rethrows such a failure. It cannot save the edit — that is already gone — it only stops the plugin claiming otherwise. Narrowed toAPIError, because a provider outage throws before any operation runs and leaves the save intact.Both halves are pinned by integration tests on PostgreSQL:
Deliberately outside the transaction
killTransactionexposure to buy only the removal of an orphan job row.The provenance receipt does join, or it would outlive the translation it certifies and report a document translated at a fingerprint that never committed.
Not a breaking change
Both new parameters are optional, so a third-party
TaskRunnerstill satisfies the interface.TaskRunnerProvider.createis untouched.Verification
Design record, including the abandoned reads-only variant and why it cannot work:
packages/payload-plugin-translator/docs/plans/2026-09-14-auto-translate-in-the-triggering-transaction.task.md.