Skip to content

Shopware cache rework: route catalog reads through cacheable GET requests#19

Open
dm-heinze wants to merge 11 commits into
mainfrom
8-shopware-cache-rework
Open

Shopware cache rework: route catalog reads through cacheable GET requests#19
dm-heinze wants to merge 11 commits into
mainfrom
8-shopware-cache-rework

Conversation

@dm-heinze

Copy link
Copy Markdown
Contributor

Summary

  • Rework Shopware store-api catalog reads (navigation, SEO-URL, product-detail, category, listing, landing) to run as cacheable GET requests through the framework's method-choice engine, so Nitro/CDN caching applies instead of uncacheable POSTs.
  • Retire the bespoke server-side cache layer: remove the clear-cache.get, navigation.post, seo-url.post, and seo-url-cache-warmup.get routes plus the shopware-get-cached-navigation / shopware-get-cached-seo-url / useNavigationCached helpers, in favour of useCategorySearch, useCategoryListing, useLandingSearch composables and shared _criteria / isCacheableListingQuery utils.
  • Upgrade the Shopware dependency stack to the cache-rework versions and fix the resulting TypeScript schema/typecheck errors via storeApiTypes.overrides.ts.
  • Add Vitest test infrastructure with unit coverage for the new cacheable-read paths, real-API integration scripts (excluded from CI), and a testing guide.

Test plan

  • npm run lint passes
  • npx nuxt typecheck clean (only the 2 known upstream node_modules errors remain)
  • npx vitest run passes
  • Manually verify catalog pages (category, listing, landing, product detail) render and that their store-api reads are issued as cacheable GET requests

dm-heinze and others added 11 commits July 14, 2026 13:51
Upgrade the Shopware frontend stack to the versions that expose the
cacheable GET read variants, foundation for the store-api catalog
caching rework (epic #8).

- @shopware/composables    1.11.1 -> 1.12.0
- @shopware/nuxt-module     1.4.4 -> 1.5.0
- @shopware/api-client      1.5.0 (pinned explicitly)
- @shopware/helpers         1.7.1 (pinned explicitly; version the 1.5/1.12
                                   stack resolves to)
- @shopware/api-gen         1.5.0 (added as devDependency for type regen)

composables, api-client and helpers are now explicit direct dependencies
since the layer imports from them directly.

cacheableReads stays unset (default off) - no read changes in this
commit; behaviour is unchanged and backwards-compatible.

npm lint passes (0 errors). api-gen type regeneration against a
CACHE_REWORK backend is deferred - it needs live OPENAPI_JSON_URL /
OPENAPI_ACCESS_KEY credentials not available in this environment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stand up automated unit testing for the repo, which previously had none.

- Add Vitest with @nuxt/test-utils/@vue/test-utils/happy-dom devDependencies
- Add `npm test` / `npm run test:watch` scripts; `pretest` runs `nuxt prepare`
  so the generated .playground/.nuxt/tsconfig.json exists in a fresh checkout
- vitest.config.ts aliases Nuxt virtual modules (#imports, #app, #shopware,
  #nuxtware) to lightweight test stubs so composables load without a full Nuxt
  runtime
- test/setup.ts stubs Nuxt/Vue auto-import globals (useCookie, ref, ...)
- Demonstrate the core apiClient.invoke seam with a smoke test on useCurrency
  that asserts the chosen operation string + HTTP method
  ("readCurrency post /currency")
- Document the seam and layout in docs/testing-guide.md

Gates: `npm test` and `npm lint` are green. `npx nuxt typecheck` is red on the
base commit already (pre-existing errors in app/ and server/); the new files
contribute zero type errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Store API types are now fetched per project (potentially from a
customer instance with custom endpoints/entities), so the generated
schema files must not be versioned. Only the overrides file is committed.

- Un-versioned generated schema: gitignore api-types/storeApiSchema.json
  and api-types/storeApiTypes.d.ts; keep storeApiTypes.overrides.ts tracked
- Resolve all app/server typecheck errors without using `any`:
  - Fix wrong property access, operation names and runtime-config paths
  - Add undefined/null guards, type guards and narrow casts where values
    are known (no `any`)
  - Type provide/inject navigation as a ref (fixes runtime
    `navigationItems.find is not a function` in mega-menu and sidenav)
  - Build the change-profile body as a discriminated union instead of
    mutating properties
  - Base the contact/newsletter form slot types on CmsElementForm
- Base the CheckoutShipping price on currencyPrice
- Add @types/js-cookie and vue-tsc dev deps; install missing vitest
- Document type-declaration locations and the two known upstream
  node_modules typecheck errors in CLAUDE.md (consolidated TypeScript section)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enable `shopware: { cacheableReads: true }` in the layer nuxt.config so
anonymous catalog reads (SEO-URL, navigation, product-detail) route through
their cacheable GET variants — the Store-API Criteria is compressed into the
`?_criteria` query param, letting a CACHE_REWORK Shopware backend serve them
from its http_cache. SSR still renders the full requested result; only
cacheability changes, never the rendered DOM.

Add `test/helpers/cacheableRead.ts`, a reusable verification helper the
downstream read tickets (#4-#7) reuse: it parses the mocked
`apiClient.invoke` seam and asserts a given read issued a GET (correct
operation + method) carrying its Criteria in `query._criteria`, then returns
the response so the caller can assert the app renders from it.

`test/cacheableReads.test.ts` self-tests the helper and drives the real
flag-covered `useProductSearch` read to prove product-detail issues
`readProductDetailGet` when the flag is on and falls back to POST when off.

Out of scope (per ticket): asserting `s-maxage`/`Cache-Control` headers and
assigning the Shopware `store_api` caching policy (Shopware-side/ops); the
live "routes respond 200" check is an out-of-CI integration concern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retire the redundant custom Nitro cache layer for SEO-URL and navigation
and let those reads flow through the GET-capable framework composables.

- [...dynamic].vue resolves paths via useNavigationSearch().resolvePath(),
  handling both the raw SeoUrl and the resolved {routeName, foreignKey}
  shapes (technical URLs covered as a superset); 404 behaviour preserved.
- LayoutHeader switches from the custom useNavigationCached to the
  framework useNavigation().
- Remove the dead Nitro cache layer: the seo-url/navigation POST routes,
  their cached server utils, the shared api client, useNavigationCached,
  apiCacheLifetime, and the clear-cache + seo-url warmup routes.
- Add unit tests asserting the invoke seam issues the correct operation
  and GET method for SEO-URL and navigation reads, and that both
  resolvePath return shapes drive useNavigationContext.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With cacheableReads on, useProductSearch().search issues the GET variant
(readProductDetailGet) and folds its associations into ?_criteria, so the
product-detail page renders identically whether the framework GETs (flag on)
or POSTs (flag off) — the method only affects cacheability, never the DOM.

- Extract the manufacturer/media/cover associations into a shared, auto-imported
  productDetailAssociations constant so the component and the test assert the
  same payload; typed via `satisfies Schemas['Association']` so it stays a
  concrete, assignable type in both the app and stubbed test contexts.
- Add decodeCriteria() to the cacheable-read helper (reverses encodeForQuery)
  so tests can inspect what a GET carried in query._criteria.
- Unit tests: product-detail issues readProductDetailGet with all associations
  in _criteria (flag on), falls back to readProductDetail POST (flag off), and
  sends byte-identical Criteria either way (the DOM-parity guarantee).
- Add an out-of-CI integration script that hits a real CACHE_REWORK backend and
  asserts the anonymous product-detail GET returns a render-ready cmsPage,
  building its Criteria exactly as the composable does (defu(cmsAssociations,…)).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`nuxt typecheck` only covered `test/nuxt/**`, so our root-level `test/**` specs
were invisible to the CLI gate, while the IDE checked them against the minimal
Vitest runtime stubs in test/tsconfig.json — surfacing false type errors the
gate never saw.

- .playground/nuxt.config.ts: add `test/**` to the generated tsconfig include so
  `nuxt typecheck` checks the specs with the real Nuxt types.
- test/tsconfig.json: extend the generated Nuxt tsconfig instead of aliasing
  #imports/#shopware/#app to stubs. The stubs stay as runtime-only aliases in
  vitest.config.ts — types and runtime are now cleanly separated.

Verified: nuxt typecheck now flags real errors inside test files and otherwise
stays at the known 2-error node_modules baseline; vitest stays 24/24.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e engine (#15)

Override useCategorySearch and useCategoryListing so anonymous catalog reads
issue the cacheable GET variant of their route for allow-listed listing queries
and fall back to POST for everything else. SSR sends the full Criteria and
renders the complete result either way — the method only affects cacheability,
never the DOM.

- Add isCacheableListingQuery + the central CACHEABLE_LISTING_PARAMS allow-list
  ('p', 'limit', 'order'); a single non-allow-listed key forces POST.
- useCategorySearch: wrap-and-spread override, only search replaced (rest via
  ...core). GET readCategoryGet with Criteria in ?_criteria for cacheable
  queries; POST readCategory otherwise. cmsAssociations rebuilt locally.
- useCategoryListing: rebuilt around the framework's createListingComposable so
  only searchMethod changes; interactive pagination/limiter/sorter reads GET
  readProductListingGet for allow-listed queries, POST readProductListing
  otherwise. Shared state stays SDK-compatible (listingKey categoryListing).
- Narrow _criteria cast bridges the typed GET operations that omit the
  compressed _criteria query param they accept at runtime.
- Vitest matrices for both overrides (GET/POST choice, flag off, byte-identical
  criteria) plus out-of-CI integration scripts against a real backend.
- Revise ADR decision 6: the interactive listing now rides the same engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gine (#16)

Override useLandingSearch so anonymous landing reads issue the cacheable GET
variant of the route for allow-listed listing queries and fall back to POST for
everything else. SSR sends the full Criteria and renders the complete result
(incl. cmsPage) either way — the method only affects cacheability, never the DOM.

- useLandingSearch: wrap-and-spread override, only search replaced (rest via
  ...core). GET readLandingPageGet with Criteria in ?_criteria for cacheable
  queries; POST readLandingPage otherwise. Reuses #15's isCacheableListingQuery
  / CACHEABLE_LISTING_PARAMS allow-list (no duplicate predicate).
- The typing gap is one step wider than category: readLandingPageGet is absent
  from the SDK operations union and operationPaths entirely (the route exists and
  accepts _criteria at runtime), so a locally typed invoke bridges just this one
  call. Self-contained and trivially removable once shopware-frontends ships the
  typed GET landing variant.
- Associations mirror the framework landing composable exactly (single-level
  cmsAssociations.associations), so the GET/POST payload is byte-identical to the
  upstream read and cmsPage resolves unchanged.
- Vitest matrix (GET/POST choice, flag off, byte-identical criteria) plus an
  out-of-CI integration script against a real backend.
- Extend ADR consequences with the wider landing bridge; translate the ADR to
  English.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant