-
Notifications
You must be signed in to change notification settings - Fork 1.4k
OrderService translates ProductVariant without guaranteed translations relation #4566
Description
Describe the bug
OrderService.findOne() / getOrderOrThrow() can end up translating OrderLine.productVariant even when ProductVariant.translations was not loaded by the split order-line query. This causes order mutations to fail with error.entity-has-no-translation-in-language even though the translation row exists in the database.
In our case, the failure happens during active order edits that call OrderService.addItemToOrder() on an active order. We verified the failing ProductVariant and Product both have valid en translation rows in Postgres, so this does not appear to be bad data.
At minimum, getOrderOrThrow() appears to have incomplete default relations: it includes lines.productVariant but not lines.productVariant.translations, and findOne() later translates the loaded variants.
To Reproduce
Steps we can reproduce in our app:
- Create an active order with at least one existing order line.
- Ensure the order line's
ProductVarianthas a valid translation row. - Trigger an order mutation that goes through
OrderService.addItemToOrder()or another path that reloads the active order viagetOrderOrThrow(). - Vendure reloads the order with
lines.productVariantbut withoutlines.productVariant.translations. findOne()then attempts to translateline.productVariant.- Observe
error.entity-has-no-translation-in-language.
Narrowed core-level reproduction target:
- Call
OrderService.findOne(ctx, orderId, ['lines', 'lines.productVariant', 'lines.productVariant.productVariantPrices', 'shippingLines', 'surcharges', 'customer'])on an active order with lines. - Observe that
findOne()later translates eachline.productVariant. - If
translationswas not loaded on the split line query, Vendure throws even though translation rows exist in the DB.
Expected behavior
If Vendure is going to translate line.productVariant, it should ensure lines.productVariant.translations is loaded first. Order mutations should not fail when the variant's translation row exists in the database.
Actual behavior
Order mutations intermittently fail with INTERNAL_SERVER_ERROR, and the underlying error is:
error.entity-has-no-translation-in-languageentityName: ProductVariantlanguageCode: en,en,en
We verified the failing variant's translation row exists, so the exception appears to be caused by missing loaded relations rather than missing DB data.
Screenshots/Videos
N/A
Error logs
INTERNAL_SERVER_ERROR
error.entity-has-no-translation-in-language
entityName: ProductVariant
languageCode: en,en,en
Environment
@vendure/coreversion:3.5.5- Nodejs version:
20.18.0in production container - Database:
PostgreSQL - Operating System:
Linux(Cloud Run container) - Browser: Not browser-specific; triggered via Shop API order mutations
- Package manager:
yarn
Configuration
Relevant shape only:
const config: VendureConfig = {
dbConnectionOptions: {
type: 'postgres',
},
orderOptions: {
// standard order service flows; issue appears during active-order reloads
},
};Minimal reproduction
I do not yet have a standalone public reproduction repo, but the issue has been narrowed to Vendure core order loading:
await orderService.findOne(ctx, orderId, [
'lines',
'lines.productVariant',
'lines.productVariant.productVariantPrices',
'shippingLines',
'surcharges',
'customer',
]);findOne() later translates each line.productVariant, but the split line-loading path does not appear to guarantee lines.productVariant.translations is present.
In our production case, this surfaced through orderService.addItemToOrder(), which internally calls getOrderOrThrow().
Workaround
We originally worked around this in application code by widening relation lists anywhere we loaded orders with lines.productVariant… (injecting lines.productVariant.translations and, when needed, lines.productVariant.product.translations). That seemed to work but was easy to miss on new call sites.
We replaced that with a patch-package patch against @vendure/core so the fix lives in one place and tracks Vendure’s own loading logic.
- Tooling:
patch-packagewith"postinstall": "patch-package"inbackend/package.json, so the patch reapplies after everynpm install/ CI install. - Patch file:
backend/patches/@vendure+core+3.5.5.patch(must be regenerated if@vendure/coreis upgraded past 3.5.5; the filename matches the patched package version).
What the patch does (targets compiled dist/service/services/order.service.js for 3.5.5):
-
findOnerelation injection — Where core only auto-addedlines.productVariant.taxCategorywhenlines.productVariantwas present, the patch also ensures:lines.productVariantwhen anylines.productVariant.*relation is requestedlines.productVariant.taxCategory(unchanged intent, slightly broader detection)lines.productVariant.translationslines.productVariant.productandlines.productVariant.product.translationswhen product relations under the line variant are used
So the split line query loads the data
translator.translate()needs. -
getOrderOrThrowdefaults — Addslines.productVariant.translationsto the default relation list alongsidelines.productVariantandlines.productVariant.productVariantPrices, so active-order paths that rely on those defaults also load variant translations.
After upgrading @vendure/core, either drop the patch if upstream fixes the issue, or re-diff against the new order.service.js and run npx patch-package @vendure/core to refresh the file.
Root cause analysis
The bug results from two changes that are individually harmless but interact to produce the failure:
-
v3.0.2 (
e3d6c21— "perf(core): Optimize order operations"):getOrderOrThrow()was given its own minimal default relations list that includeslines.productVariantbut omitslines.productVariant.translations. Before the split-loading change below, this was safe becausefindOne()used a single query withFindOptionsUtils.joinEagerRelations(), and TypeORM's eager loading onProductVariant.translationsguaranteed the relation was loaded automatically. -
v3.3.6 (
a04b94a/ PR perf(core): Optimize relation loading strategies for orders and order lines #3652 — "perf(core): Optimize relation loading strategies for orders and order lines"):findOne()was split into two queries — one for order-level relations (relationLoadStrategy: 'query') and a separate one for line-level relations (setFindOptionswith'join'strategy). ThejoinEagerRelations()call only applies to the main order query, not the lines query. TypeORM's eager loading of transitive relations does not work reliably with the'join'strategy onsetFindOptions(see TypeORM issue #9139), soProductVariant.translationsis no longer guaranteed to be loaded.
Additionally, the auto-inject logic in findOne() only adds lines.productVariant.taxCategory when lines.productVariant is present — it does not add lines.productVariant.translations, even though findOne() unconditionally calls translator.translate() on every loaded line.productVariant.
The bug has been present since v3.3.6 (all versions v3.3.6 through v3.5.5 are affected). We first noticed it at v3.5.5, likely due to hitting a specific code path or a transitive TypeORM version change.
Additional context
- It does not appear to be bad catalog data: we verified the failing
ProductVariantandProductboth have validentranslation rows in Postgres. - In our environment, all fallback language codes resolve to
en, which is why the error showsen,en,en. - The issue appears order/path specific rather than a global translation-data problem.
- Related: TypeORM issue #9139 — eager loading of transitive relations broken with
joinstrategy.