Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@cacheControl support tests, fixes... #351

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/tricky-paws-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/federation': patch
---

Keep the custom directives(using @composeDirective) from the supergraph, in the unified schema served by the gateway should keep it.
30 changes: 30 additions & 0 deletions internal/testing/src/composeLocalSchemasWithApollo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { IntrospectAndCompose, LocalGraphQLDataSource } from '@apollo/gateway';
import { GraphQLSchema } from 'graphql';

export interface ComposeLocalSchemaWithApolloSubgraphOpts {
name: string;
schema: GraphQLSchema;
url?: string;
}

export async function composeLocalSchemasWithApollo(
subgraphs: ComposeLocalSchemaWithApolloSubgraphOpts[],
) {
let { supergraphSdl, cleanup } = await new IntrospectAndCompose({
subgraphs,
}).initialize({
update(updatedSupergraphSdl: string) {
supergraphSdl = updatedSupergraphSdl;
},
healthCheck: async () => {},
getDataSource({ name }) {
const subgraph = subgraphs.find((subgraph) => subgraph.name === name);
if (!subgraph) {
throw new Error(`Subgraph ${name} not found`);
}
return new LocalGraphQLDataSource(subgraph.schema);
},
});
await cleanup();
return supergraphSdl;
}
1 change: 1 addition & 0 deletions internal/testing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './getLocalhost';
export * from './opts';
export * from './assertions';
export * from './benchConfig';
export * from './composeLocalSchemasWithApollo';
6 changes: 6 additions & 0 deletions packages/federation/src/supergraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,12 @@ export function getStitchingOptionsFromSupergraphSdl(
}
}
}
} else if (
definition.kind === Kind.DIRECTIVE_DEFINITION &&
!definition.name.value.startsWith('join__') &&
!definition.name.value.startsWith('core')
) {
extraDefinitions.push(definition);
}
}
const additionalTypeDefs: DocumentNode = {
Expand Down
8 changes: 7 additions & 1 deletion packages/federation/tests/supergraphs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ describe('Supergraphs', () => {
fieldFilter: (_, __, fieldConfig) =>
!getDirective(sortedInputSchema, fieldConfig, 'inaccessible')
?.length,
directiveFilter: () => false,
directiveFilter: (typeName) =>
!typeName.startsWith('link__') &&
!typeName.startsWith('join__') &&
!typeName.startsWith('core__') &&
typeName !== 'core' &&
typeName !== 'link' &&
typeName !== 'inaccessible',
enumValueFilter: (_, __, enumValueConfig) =>
!getDirective(sortedInputSchema, enumValueConfig, 'inaccessible')
?.length,
Expand Down
223 changes: 223 additions & 0 deletions packages/runtime/tests/cache-control.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { setTimeout } from 'timers/promises';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { createGatewayRuntime } from '@graphql-hive/gateway-runtime';
import InmemoryLRUCache from '@graphql-mesh/cache-inmemory-lru';
import useHttpCache from '@graphql-mesh/plugin-http-cache';
import { composeLocalSchemasWithApollo } from '@internal/testing';
import { parse } from 'graphql';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';

// ApolloServer is not playing nice with Leak Tests
describe.skipIf(process.env['LEAK_TEST'])(
'Cache Control directives w/ Apollo Server subgraph',
() => {
vi.useFakeTimers?.();
const advanceTimersByTimeAsync = vi.advanceTimersByTimeAsync || setTimeout;
const products = [
{ id: '1', name: 'Product 1', price: 100 },
{ id: '2', name: 'Product 2', price: 200 },
{ id: '3', name: 'Product 3', price: 300 },
];
const productsSchema = buildSubgraphSchema({
typeDefs: parse(/* GraphQL */ `
enum CacheControlScope {
PUBLIC
PRIVATE
}

directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION

type Product @key(fields: "id") @cacheControl(maxAge: 3) {
id: ID!
name: String!
price: Int!
}

extend type Query {
products: [Product!]!
product(id: ID!): Product
}

extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.6"
import: ["@key", "@composeDirective"]
)
@link(
url: "https://the-guild.dev/mesh/v1.0"
import: ["@cacheControl"]
)
@composeDirective(name: "@cacheControl") {
query: Query
}
`),
resolvers: {
Query: {
product(_root, { id }) {
return products.find((product) => product.id === id);
},
products() {
return products;
},
},
},
});
let supergraph: string;
let apolloServer: ApolloServer;
let requestDidStart: Mock;
beforeEach(async () => {
requestDidStart = vi.fn();
apolloServer = new ApolloServer({
schema: productsSchema,
plugins: [
{
requestDidStart,
},
],
});
const { url } = await startStandaloneServer(apolloServer, {
listen: { port: 0 },
});
supergraph = await composeLocalSchemasWithApollo([
{
schema: productsSchema,
name: 'products',
url,
},
]);
});
it('response caching plugin respect @cacheControl(maxAge:) w/ @composeDirective', async () => {
await using cache = new InmemoryLRUCache();
await using gw = createGatewayRuntime({
supergraph,
cache,
responseCaching: {
session: () => null,
includeExtensionMetadata: true,
},
});
async function makeRequest() {
const res = await gw.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: /* GraphQL */ `
query {
products {
id
name
price
}
}
`,
}),
});
return res.json();
}
await expect(makeRequest()).resolves.toEqual({
data: {
products,
},
extensions: {
responseCache: {
didCache: true,
hit: false,
ttl: 3_000,
},
},
});
// 15 seconds later
await advanceTimersByTimeAsync(1_000);
await expect(makeRequest()).resolves.toEqual({
data: {
products,
},
extensions: {
responseCache: {
hit: true,
},
},
});
// 15 seconds later but the cache is expired
await advanceTimersByTimeAsync(2_000);
await expect(makeRequest()).resolves.toEqual({
data: {
products,
},
extensions: {
responseCache: {
didCache: true,
hit: false,
ttl: 3_000,
},
},
});
// GW received 3 requests but only 2 were forwarded to the subgraph
expect(requestDidStart).toHaveBeenCalledTimes(2);
});
// TODO: HTTP Cache plugin has issues with Bun
it.skipIf(globalThis.Bun)(
'http caching plugin should respect cache control headers',
async () => {
await using cache = new InmemoryLRUCache();
await using gw = createGatewayRuntime({
supergraph,
cache,
plugins: (ctx) => [
// @ts-expect-error - we need to fix the types
useHttpCache(ctx),
],
});
async function makeRequest() {
const res = await gw.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: /* GraphQL */ `
query {
products {
id
name
price
}
}
`,
}),
});
return res.json();
}
await expect(makeRequest()).resolves.toEqual({
data: {
products,
},
});
// 15 seconds later
await advanceTimersByTimeAsync(1_000);
await expect(makeRequest()).resolves.toEqual({
data: {
products,
},
});
// 15 seconds later but the cache is expired
await advanceTimersByTimeAsync(2_000);
await expect(makeRequest()).resolves.toEqual({
data: {
products,
},
});
// GW received 3 requests but only 2 were forwarded to the subgraph
expect(requestDidStart).toHaveBeenCalledTimes(2);
},
);
},
);
21 changes: 21 additions & 0 deletions vitest-jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,29 @@ module.exports = new Proxy(require('@jest/globals'), {
describeFn.only = function describeOnly(name, ...args) {
return jestGlobals.describe.only(name, ...args);
};
describeFn.each = function describeEach(table) {
return jestGlobals.describe.each(table);
};
return describeFn;
}
if (prop === 'it') {
const itFn = function it(name, ...args) {
return jestGlobals.it(name, ...args);
};
itFn.skipIf = function itSkipIf(condition) {
return condition ? itFn.skip : itFn;
};
itFn.skip = function itSkip(name, ...args) {
return jestGlobals.it.skip(name, ...args);
};
itFn.only = function itOnly(name, ...args) {
return jestGlobals.it.only(name, ...args);
};
itFn.each = function itEach(table) {
return jestGlobals.it.each(table);
};
return itFn;
}
return Reflect.get(jestGlobals, prop, receiver);
},
});
9 changes: 1 addition & 8 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,7 @@ import { defineConfig } from 'vitest/config';
// packages as per the Node resolution spec.
//
// Vite will process inlined modules.
const inline = [
/@graphql-mesh\/utils/,
/@graphql-mesh\/runtime/,
/@graphql-mesh\/fusion-composition/,
/@graphql-mesh\/plugin-hive/,
/@graphql-mesh\/transport-rest/,
/@omnigraph\/.*/,
];
const inline = [/@graphql-mesh\/.*/, /@omnigraph\/.*/];
ardatan marked this conversation as resolved.
Show resolved Hide resolved

export default defineConfig({
plugins: [tsconfigPaths()],
Expand Down
Loading