#5359·vendure

Admin API ProductVariant.stockLevels runs one query per variant

Author: ivanvasyliukCreated Sep 11, 2026Updated Sep 11, 2026
Labelstype: bug 🐛

Describe the bug The Admin API ProductVariant.stockLevels field resolver (packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts, stockLevels) calls StockLevelService.getStockLevelsForVariant, which builds a query joining stockLocation.channels and filtering on ctx.channelId, once per variant. Nothing batches it, so any list of variants that selects stockLevels costs one extra query per row.

#5224 removed the same per-variant stampede for stockOnHand / stockAllocated (the getAvailableStock path), but stockLevels still has it. The dashboard product-variants list and the variants table on the product detail page both select stockLevels for every row.

To Reproduce

  1. Enable query logging (dbConnectionOptions.logging: ['query']) or attach a counting TypeORM logger.
  2. Run this Admin API query against a catalog with a few dozen variants:
    graphql
    query {
        productVariants(options: { take: 40 }) {
            items {
                id
                stockLevels { id stockOnHand stockAllocated stockLocation { id name } }
            }
        }
    }
  3. Count the SELECT ... FROM "stock_level" statements.

On the core e2e fixture (34 variants) this issues 34 stock_level selects, one per variant, 48 queries for the request in total.

Expected behavior A page of variants costs a bounded number of stock queries, as it does for stockOnHand / stockAllocated since #5224.

Actual behavior One stock_level query per variant in the page. An ERP or WMS sync paging 100 variants pays 100 extra queries per page.

Environment

  • @vendure/core version: 3.7.2 (master, includes #5224)
  • Nodejs version:
  • Database: reproduced on sqljs, Postgres 16, MySQL 8, MariaDB 11.5
  • Operating System:
  • Package manager:

Additional context Same root cause as #5224, same fix shape: a per-request DataLoader with cache: false, so a write earlier in the request is not masked. PR to follow.