Skip to content

OrderService translates ProductVariant without guaranteed translations relation #4566

@yasserlens

Description

@yasserlens

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:

  1. Create an active order with at least one existing order line.
  2. Ensure the order line's ProductVariant has a valid translation row.
  3. Trigger an order mutation that goes through OrderService.addItemToOrder() or another path that reloads the active order via getOrderOrThrow().
  4. Vendure reloads the order with lines.productVariant but without lines.productVariant.translations.
  5. findOne() then attempts to translate line.productVariant.
  6. Observe error.entity-has-no-translation-in-language.

Narrowed core-level reproduction target:

  1. Call OrderService.findOne(ctx, orderId, ['lines', 'lines.productVariant', 'lines.productVariant.productVariantPrices', 'shippingLines', 'surcharges', 'customer']) on an active order with lines.
  2. Observe that findOne() later translates each line.productVariant.
  3. If translations was 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-language
  • entityName: ProductVariant
  • languageCode: 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/core version: 3.5.5
  • Nodejs version: 20.18.0 in 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-package with "postinstall": "patch-package" in backend/package.json, so the patch reapplies after every npm install / CI install.
  • Patch file: backend/patches/@vendure+core+3.5.5.patch (must be regenerated if @vendure/core is 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):

  1. findOne relation injection — Where core only auto-added lines.productVariant.taxCategory when lines.productVariant was present, the patch also ensures:

    • lines.productVariant when any lines.productVariant.* relation is requested
    • lines.productVariant.taxCategory (unchanged intent, slightly broader detection)
    • lines.productVariant.translations
    • lines.productVariant.product and lines.productVariant.product.translations when product relations under the line variant are used

    So the split line query loads the data translator.translate() needs.

  2. getOrderOrThrow defaults — Adds lines.productVariant.translations to the default relation list alongside lines.productVariant and lines.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:

  1. v3.0.2 (e3d6c21 — "perf(core): Optimize order operations"): getOrderOrThrow() was given its own minimal default relations list that includes lines.productVariant but omits lines.productVariant.translations. Before the split-loading change below, this was safe because findOne() used a single query with FindOptionsUtils.joinEagerRelations(), and TypeORM's eager loading on ProductVariant.translations guaranteed the relation was loaded automatically.

  2. 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 (setFindOptions with 'join' strategy). The joinEagerRelations() 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 on setFindOptions (see TypeORM issue #9139), so ProductVariant.translations is 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 ProductVariant and Product both have valid en translation rows in Postgres.
  • In our environment, all fallback language codes resolve to en, which is why the error shows en,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 join strategy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions