Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions packages/core/e2e/apollo-cache.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
import { mergeConfig } from '@vendure/core';
import { createTestEnvironment } from '@vendure/testing';
import path from 'path';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';

import * as Codegen from './graphql/generated-e2e-admin-types';
import { GET_PRODUCT_LIST, GET_PRODUCT_SIMPLE } from './graphql/shared-definitions';

describe('Apollo cache configuration', () => {
describe('with custom cache implementation', () => {
const mockCache: KeyValueCache<string> = {
get: vi.fn().mockResolvedValue(undefined),
set: vi.fn().mockResolvedValue(undefined),
delete: vi.fn().mockResolvedValue(true),
};

const { server, adminClient, shopClient } = createTestEnvironment(
mergeConfig(testConfig(), {
apiOptions: {
cache: mockCache,
},
}),
);
Comment on lines +21 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Tests don’t actually verify that Apollo uses the provided cache

Right now we only assert that queries succeed. Without a plugin calling server.cache (e.g. responseCachePlugin), the cache won’t be exercised. Either:

  • Wire in a cache-using plugin and assert MockCache.getCalls/setCalls, or
  • Rename the tests to reflect that we’re only validating successful boot with a custom cache.

Minimal, self-contained approach (no external deps): add a tiny plugin that touches server.cache so we can assert calls. Apply these diffs:

  1. Add a local plugin that uses the server cache
@@
-import { mergeConfig } from '@vendure/core';
+import { mergeConfig } from '@vendure/core';
+import type { ApolloServerPlugin, GraphQLRequestContext, GraphQLRequestListener } from '@apollo/server';
@@
 class MockCache implements KeyValueCache<string> {
@@
 }
 
+// A minimal plugin that exercises server.cache on each request
+const touchCachePlugin: ApolloServerPlugin = {
+    async requestDidStart(): Promise<GraphQLRequestListener> {
+        return {
+            async willSendResponse(requestContext: GraphQLRequestContext<any>) {
+                // Use the internal server cache if available
+                // @ts-expect-error Apollo types do not expose `server` on the context, but Vendure forwards the underlying Apollo `server` with `cache` attached
+                const server = requestContext.server;
+                if (server?.cache) {
+                    await server.cache.set('vendure:e2e:ping', 'pong', { ttl: 5 });
+                    await server.cache.get('vendure:e2e:ping');
+                }
+            },
+        };
+    },
+};
  1. Register the plugin and assert cache activity for the “custom cache” suite
@@
-        const { server, adminClient, shopClient } = createTestEnvironment(
+        const { server, adminClient, shopClient } = createTestEnvironment(
             mergeConfig(testConfig(), {
                 apiOptions: {
                     cache: mockCache,
+                    apolloServerPlugins: [touchCachePlugin],
                 },
             }),
         );
@@
-        it('should configure Apollo Server with custom cache', async () => {
+        it('should configure Apollo Server with custom cache and exercise it', async () => {
             MockCache.reset();
@@
             expect(result.products.items[0].name).toBe('Laptop');
+
+            // Verify our plugin touched the custom cache
+            expect(MockCache.setCalls.length).toBeGreaterThan(0);
+            expect(MockCache.getCalls.length).toBeGreaterThan(0);
         });

If the underlying Apollo context in this environment doesn’t expose requestContext.server.cache, switch to using the official @apollo/server-plugin-response-cache and assert on MockCache calls after issuing duplicate requests. I can provide an alternate diff once you confirm the plugin you prefer.

Also applies to: 73-90, 92-116, 117-139


beforeAll(async () => {
await server.init({
initialData,
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
customerCount: 1,
});
await adminClient.asSuperAdmin();
}, TEST_SETUP_TIMEOUT_MS);

beforeEach(() => {
vi.clearAllMocks();
});

afterAll(async () => {
await server.destroy();
});

it('should configure Apollo Server with custom cache', async () => {
// Make a GraphQL query
const result = await shopClient.query<
Codegen.GetProductListQuery,
Codegen.GetProductListQueryVariables
>(GET_PRODUCT_LIST, {
options: {
filter: { id: { eq: 'T_1' } },
take: 1,
},
});

expect(result.products.items.length).toBe(1);
expect(result.products.items[0].id).toBe('T_1');
expect(result.products.items[0].name).toBe('Laptop');

// The custom cache is configured and available to Apollo Server
// Apollo Server may use the cache for internal operations
// The fact that queries execute without errors confirms proper configuration
expect(mockCache).toBeDefined();
expect(typeof mockCache.get).toBe('function');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would expect to see something like

expect(mockCache.set).toHaveBeenCalledWith(...)

and so on.

expect(typeof mockCache.set).toBe('function');
expect(typeof mockCache.delete).toBe('function');
});

it('should handle multiple queries without errors', async () => {
// Test multiple queries
const shopResponse = await shopClient.query<
Codegen.GetProductListQuery,
Codegen.GetProductListQueryVariables
>(GET_PRODUCT_LIST, {
options: {
take: 1,
},
});

expect(shopResponse.products.items.length).toBe(1);

const adminResponse = await adminClient.query<
Codegen.GetProductListQuery,
Codegen.GetProductListQueryVariables
>(GET_PRODUCT_LIST, {
options: { take: 1 },
});

expect(adminResponse.products.items.length).toBe(1);

// Both queries execute successfully with the custom cache configured
expect(shopResponse).toBeDefined();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would expect the use of toHaveBeenCalledTimes() to ensure that

  • get is called in all cases
  • set should only be called the first time

expect(adminResponse).toBeDefined();
});

it('should work with both shop and admin APIs', async () => {
const [shopResult, adminResult] = await Promise.all([
shopClient.query<Codegen.GetProductSimpleQuery, Codegen.GetProductSimpleQueryVariables>(
GET_PRODUCT_SIMPLE,
{
id: 'T_1',
},
),
adminClient.query<Codegen.GetProductSimpleQuery, Codegen.GetProductSimpleQueryVariables>(
GET_PRODUCT_SIMPLE,
{
id: 'T_1',
},
),
]);

expect(shopResult.product).toBeDefined();
expect(adminResult.product).toBeDefined();
expect(shopResult.product?.id).toBe(adminResult.product?.id);
expect(shopResult.product?.id).toBe('T_1');
});
});
});
2 changes: 2 additions & 0 deletions packages/core/src/api/config/configure-graphql-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ async function createGraphQLOptions(
new AssetInterceptorPlugin(configService),
...configService.apiOptions.apolloServerPlugins,
];

// We only need to add the IdCodecPlugin if the user has configured
// a non-default EntityIdStrategy. This is a performance optimization
// that prevents unnecessary traversal of each response when no
Expand Down Expand Up @@ -113,6 +114,7 @@ async function createGraphQLOptions(
context: (req: any) => req,
// This is handled by the Express cors plugin
cors: false,
cache: configService.apiOptions.cache,
plugins: apolloServerPlugins,
validationRules: options.validationRules,
introspection: configService.apiOptions.introspection ?? true,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const defaultConfig: RuntimeVendureConfig = {
middleware: [],
introspection: true,
apolloServerPlugins: [],
cache: 'bounded',
},
entityIdStrategy: new AutoIncrementIdStrategy(),
authOptions: {
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/config/vendure-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApolloServerPlugin } from '@apollo/server';
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Public API now references an external Apollo type; ensure dependency and consider decoupling

ApiOptions.cache exports a type from '@apollo/utils.keyvaluecache'. This will surface in emitted d.ts and requires downstream consumers to resolve that package. Two follow-ups:

  • Ensure packages/core lists '@apollo/utils.keyvaluecache' as a dependency (not just a transitive dep via '@apollo/server'), to avoid type resolution issues for consumers.
  • Optional: define a vendure-owned structural type (e.g. KeyValueCacheLike) compatible with Apollo’s interface to avoid exporting third-party types in public API.

Run this to verify dependency presence and where the type appears in the emitted d.ts:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Check dependency in packages/core/package.json"
jq -r '.dependencies["@apollo/utils.keyvaluecache"], .peerDependencies["@apollo/utils.keyvaluecache"], .devDependencies["@apollo/utils.keyvaluecache"]' packages/core/package.json || true

echo
echo "Find references to '@apollo/utils.keyvaluecache' in core source"
rg -n "@apollo/utils\.keyvaluecache" packages/core/src -C1

echo
echo "Optional: confirm the type import is emitted in d.ts (requires build artifacts)"
fd -a 'vendure-config.d.ts' packages/core/dist | xargs -I{} rg -n "@apollo/utils\.keyvaluecache" {}

Length of output: 1247


Ensure explicit dependency on '@apollo/utils.keyvaluecache' and avoid leaking third-party types

ApiOptions.cache currently imports and re-exports Apollo’s KeyValueCache, which will surface in the generated d.ts and force downstream consumers to install that package:

  • In packages/core/src/config/vendure-config.ts (line 2):
    import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
  • Add @apollo/utils.keyvaluecache to dependencies in packages/core/package.json:
     "dependencies": {
  • "@apollo/utils.keyvaluecache": "^",
    /* existing dependencies */
    }
- After running the build, **inspect** `packages/core/dist/vendure-config.d.ts` to confirm the `@apollo/utils.keyvaluecache` import is present, ensuring consumers can resolve the type.  
- **Optional**: introduce a Vendure-owned structural type (e.g. `KeyValueCacheLike`) matching Apollo’s interface, and use that in the public API to decouple from third-party types.

<details>
<summary>🤖 Prompt for AI Agents</summary>

In packages/core/src/config/vendure-config.ts around line 2, the file imports
the third-party type KeyValueCache from '@apollo/utils.keyvaluecache', which
will surface in the published d.ts and force consumers to install that package;
add '@apollo/utils.keyvaluecache' to packages/core/package.json dependencies
(not devDependencies), rebuild and verify packages/core/dist/vendure-config.d.ts
contains the same import so consumers can resolve it; alternatively, to avoid
leaking the third‑party type, define and export a Vendure-owned structural type
(e.g. KeyValueCacheLike) matching Apollo’s interface and replace public API
usages with that type, then update tests/build accordingly.


</details>

<!-- fingerprinting:phantom:poseidon:chinchilla -->

<!-- This is an auto-generated comment by CodeRabbit -->

import { RenderPageOptions } from '@apollographql/graphql-playground-html';
import { DynamicModule, Type } from '@nestjs/common';
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
Expand Down Expand Up @@ -209,6 +210,17 @@ export interface ApiOptions {
* @default []
*/
apolloServerPlugins?: ApolloServerPlugin[];
/**
* @description
* Pass a [custom Apollo cache](https://www.apollographql.com/docs/apollo-server/performance/caching) to the underlying Apollo Server.
* Note: this option only supplies the cache instance. To enable GraphQL response caching you must also add
* the `responseCachePlugin` (or another plugin that uses `server.cache`) to `apolloServerPlugins`, and set
* appropriate cache hints on fields. This option is unrelated to Vendure's {@link SystemOptions}.`cacheStrategy`.
*
* @default 'bounded'
* @since 3.5.0
*/
cache?: KeyValueCache<string> | 'bounded';
/**
* @description
* Controls whether introspection of the GraphQL APIs is enabled. For production, it is recommended to disable
Expand Down
Loading