Skip to content

Commit edc0752

Browse files
fix(cli/http): update introspection correctly on schema change via polling (ardatan#6797)
* fix(cli/http): update introspection correctly on schema change via polling * chore(dependencies): updated changesets for modified dependencies * Add removed Mesh config file --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 1e5172a commit edc0752

File tree

10 files changed

+280
-56
lines changed

10 files changed

+280
-56
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-mesh/http": patch
3+
---
4+
dependencies updates:
5+
- Added dependency [`@graphql-tools/utils@^9.2.1 || ^10.0.0` ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/9.2.1) (to `peerDependencies`)

.changeset/fair-dolphins-cheat.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@graphql-mesh/http": patch
3+
"@graphql-mesh/cli": patch
4+
---
5+
6+
Update introspection on schema updates via polling
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
sources:
2+
- name: Supergraph
3+
handler:
4+
supergraph:
5+
source: '{env.SUPERGRAPH_SOURCE}'
6+
7+
serve:
8+
fork: 0
9+
10+
pollingInterval: 2_000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
schema
2+
@core(feature: "https://specs.apollo.dev/core/v0.2")
3+
@core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) {
4+
query: Query
5+
}
6+
7+
directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA
8+
9+
directive @join__field(
10+
graph: join__Graph
11+
provides: join__FieldSet
12+
requires: join__FieldSet
13+
) on FIELD_DEFINITION
14+
15+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
16+
17+
directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT
18+
19+
directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT
20+
21+
type Product
22+
@join__owner(graph: PRODUCTS)
23+
@join__type(graph: PRODUCTS, key: "upc")
24+
@join__type(graph: INVENTORY, key: "upc")
25+
@join__type(graph: REVIEWS, key: "upc") {
26+
inStock: Boolean @join__field(graph: INVENTORY)
27+
name: String @join__field(graph: PRODUCTS)
28+
price: Int @join__field(graph: PRODUCTS)
29+
reviews: [Review] @join__field(graph: REVIEWS)
30+
shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight")
31+
upc: String!
32+
weight: Int @join__field(graph: PRODUCTS)
33+
}
34+
35+
type Query {
36+
me: User @join__field(graph: ACCOUNTS)
37+
topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS)
38+
user(id: ID!): User @join__field(graph: ACCOUNTS)
39+
users: [User] @join__field(graph: ACCOUNTS)
40+
}
41+
42+
type Review @join__owner(graph: REVIEWS) @join__type(graph: REVIEWS, key: "id") {
43+
author: User @join__field(graph: REVIEWS, provides: "username")
44+
body: String @join__field(graph: REVIEWS)
45+
id: ID! @join__field(graph: REVIEWS)
46+
product: Product @join__field(graph: REVIEWS)
47+
}
48+
49+
type User
50+
@join__owner(graph: ACCOUNTS)
51+
@join__type(graph: ACCOUNTS, key: "id")
52+
@join__type(graph: REVIEWS, key: "id") {
53+
id: ID!
54+
name: String @join__field(graph: ACCOUNTS)
55+
reviews: [Review] @join__field(graph: REVIEWS)
56+
username: String @join__field(graph: ACCOUNTS)
57+
}
58+
59+
enum core__Purpose {
60+
"""
61+
`EXECUTION` features provide metadata necessary to for operation execution.
62+
"""
63+
EXECUTION
64+
65+
"""
66+
`SECURITY` features provide metadata necessary to securely resolve fields.
67+
"""
68+
SECURITY
69+
}
70+
71+
scalar join__FieldSet
72+
73+
enum join__Graph {
74+
ACCOUNTS @join__graph(name: "accounts", url: "http://localhost:9880/graphql")
75+
INVENTORY @join__graph(name: "inventory", url: "http://localhost:9872/graphql")
76+
PRODUCTS @join__graph(name: "products", url: "http://localhost:9873/graphql")
77+
REVIEWS @join__graph(name: "reviews", url: "http://localhost:9874/graphql")
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { exec, execSync } from 'child_process';
2+
import { createReadStream, readFile, readFileSync, write, writeFileSync } from 'fs';
3+
import { createServer } from 'http';
4+
import { join } from 'path';
5+
6+
jest.setTimeout(30000);
7+
describe('Polling Test', () => {
8+
let cleanupCallbacks: (() => void)[] = [];
9+
afterAll(() => {
10+
cleanupCallbacks.forEach(cb => cb());
11+
});
12+
it('should pass', async () => {
13+
const cwd = join(__dirname, 'fixtures/polling');
14+
const supergraphSdlPath = join(cwd, 'supergraph.graphql');
15+
const supergraphSdl = readFileSync(supergraphSdlPath, 'utf-8');
16+
let changedSupergraph = false;
17+
const supergraphSdlServer = createServer((req, res) => {
18+
res.statusCode = 200;
19+
res.setHeader('Content-Type', 'text/plain');
20+
console.log('Serving supergraph SDL');
21+
if (changedSupergraph) {
22+
res.end(supergraphSdl.replace('topProducts', 'topProductsNew'));
23+
} else {
24+
res.end(supergraphSdl);
25+
}
26+
});
27+
await new Promise<void>(resolve => supergraphSdlServer.listen(0, resolve));
28+
cleanupCallbacks.push(() => supergraphSdlServer.close());
29+
const SUPERGRAPH_SOURCE = `http://localhost:${(supergraphSdlServer.address() as any).port}`;
30+
console.info('Supergraph SDL server is running on ' + SUPERGRAPH_SOURCE);
31+
const buildCmd = exec(`${join(__dirname, '../node_modules/.bin/mesh')} build`, {
32+
cwd,
33+
env: {
34+
...process.env,
35+
SUPERGRAPH_SOURCE,
36+
},
37+
});
38+
await new Promise<void>(resolve => {
39+
buildCmd.stdout?.on('data', function stdoutListener(data: string) {
40+
if (data.includes('Done!')) {
41+
buildCmd.stdout?.off('data', stdoutListener);
42+
resolve();
43+
}
44+
});
45+
});
46+
const serveCmd = exec(`${join(__dirname, '../node_modules/.bin/mesh')} start`, {
47+
cwd,
48+
env: {
49+
...process.env,
50+
SUPERGRAPH_SOURCE,
51+
},
52+
});
53+
cleanupCallbacks.push(() => serveCmd.kill());
54+
await new Promise<void>(resolve => {
55+
serveCmd.stdout?.on('data', function stdoutListener(data: string) {
56+
console.log(data);
57+
if (data.includes('Serving GraphQL Mesh')) {
58+
serveCmd.stdout?.off('data', stdoutListener);
59+
resolve();
60+
}
61+
});
62+
});
63+
const resp = await fetch('http://127.0.0.1:4000/graphql', {
64+
method: 'POST',
65+
headers: {
66+
'Content-Type': 'application/json',
67+
},
68+
body: JSON.stringify({
69+
query: `
70+
{
71+
__type(name:"Query") {
72+
fields {
73+
name
74+
}
75+
}
76+
}
77+
`,
78+
}),
79+
});
80+
const data = await resp.json();
81+
expect(data).toEqual({
82+
data: {
83+
__type: {
84+
fields: [
85+
{
86+
name: 'me',
87+
},
88+
{
89+
name: 'user',
90+
},
91+
{
92+
name: 'users',
93+
},
94+
{
95+
name: 'topProducts',
96+
},
97+
],
98+
},
99+
},
100+
});
101+
changedSupergraph = true;
102+
await new Promise(resolve => setTimeout(resolve, 3000));
103+
const resp2 = await fetch('http://127.0.0.1:4000/graphql', {
104+
method: 'POST',
105+
headers: {
106+
'Content-Type': 'application/json',
107+
},
108+
body: JSON.stringify({
109+
query: `
110+
{
111+
__type(name:"Query") {
112+
fields {
113+
name
114+
}
115+
}
116+
}
117+
`,
118+
}),
119+
});
120+
const data2 = await resp2.json();
121+
expect(data2).toEqual({
122+
data: {
123+
__type: {
124+
fields: [
125+
{
126+
name: 'me',
127+
},
128+
{
129+
name: 'user',
130+
},
131+
{
132+
name: 'users',
133+
},
134+
{
135+
name: 'topProductsNew',
136+
},
137+
],
138+
},
139+
},
140+
});
141+
});
142+
});

jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ if (process.env.LEAK_TEST) {
2020
testMatch.push('!**/examples/v1-next/grpc-*/**');
2121
testMatch.push('!**/examples/v1-next/sqlite-*/**');
2222
testMatch.push('!**/examples/v1-next/mysql-*/**');
23+
testMatch.push('!**/examples/federation-example/tests/polling.test.ts');
2324
}
2425

2526
testMatch.push(process.env.INTEGRATION_TEST ? '!**/packages/**' : '!**/examples/**');

packages/legacy/cli/src/commands/serve/serve.ts

+10-22
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,20 @@ export async function serveMesh(
125125
}
126126
logger.info(`Starting GraphQL Mesh...`);
127127

128-
const mesh$: Promise<MeshInstance> = getBuiltMesh()
129-
.then(async mesh => {
130-
if (mesh.schema.getType('BigInt')) {
131-
await import('json-bigint-patch');
132-
}
133-
logger.info(`${cliParams.serveMessage}: ${serverUrl}`);
134-
registerTerminateHandler(eventName => {
135-
const eventLogger = logger.child(`${eventName} 💀`);
136-
eventLogger.info(`Destroying GraphQL Mesh...`);
137-
mesh.destroy();
138-
});
139-
return mesh;
140-
})
141-
.catch(e => handleFatalError(e, logger));
128+
logger.info(`${cliParams.serveMessage}: ${serverUrl}`);
129+
registerTerminateHandler(eventName => {
130+
const eventLogger = logger.child(`${eventName} 💀`);
131+
eventLogger.info(`Destroying GraphQL Mesh...`);
132+
getBuiltMesh()
133+
.then(mesh => mesh.destroy())
134+
.catch(e => eventLogger.error(e));
135+
});
142136

143137
let uWebSocketsApp: TemplatedApp;
144138

145139
const meshHTTPHandler = createMeshHTTPHandler({
146140
baseDir,
147-
getBuiltMesh: () => mesh$,
141+
getBuiltMesh,
148142
rawServeConfig,
149143
playgroundTitle,
150144
});
@@ -175,7 +169,7 @@ export async function serveMesh(
175169
execute: args => (args as EnvelopedExecutionArgs).rootValue.execute(args),
176170
subscribe: args => (args as EnvelopedExecutionArgs).rootValue.subscribe(args),
177171
onSubscribe: async (ctx, msg) => {
178-
const { getEnveloped } = await mesh$;
172+
const { getEnveloped } = await getBuiltMesh();
179173
const { schema, execute, subscribe, contextFactory, parse, validate } = getEnveloped(ctx);
180174

181175
const args: EnvelopedExecutionArgs = {
@@ -216,12 +210,6 @@ export async function serveMesh(
216210
).catch(() => {});
217211
}
218212
});
219-
220-
return mesh$.then(mesh => ({
221-
mesh,
222-
httpServer: uWebSocketsApp,
223-
logger,
224-
}));
225213
}
226214
return null;
227215
}

packages/legacy/http/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@graphql-mesh/runtime": "^0.98.7",
3737
"@graphql-mesh/types": "^0.97.5",
3838
"@graphql-mesh/utils": "^0.97.5",
39+
"@graphql-tools/utils": "^9.2.1 || ^10.0.0",
3940
"graphql": "*",
4041
"tslib": "^2.4.0"
4142
},
+26-34
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CORSOptions, createYoga, useLogger } from 'graphql-yoga';
22
import { MeshInstance } from '@graphql-mesh/runtime';
3+
import { memoize1 } from '@graphql-tools/utils';
34

45
export const graphqlHandler = ({
56
getBuiltMesh,
@@ -16,39 +17,30 @@ export const graphqlHandler = ({
1617
corsConfig: CORSOptions;
1718
batchingLimit?: number;
1819
}) => {
19-
let yoga: ReturnType<typeof createYoga>;
20-
let yoga$: Promise<ReturnType<typeof createYoga>>;
21-
return (request: Request, ctx: any) => {
22-
if (yoga) {
23-
return yoga.handleRequest(request, ctx);
24-
}
25-
if (!yoga$) {
26-
yoga$ = getBuiltMesh().then(mesh => {
27-
yoga = createYoga({
28-
plugins: [
29-
...mesh.plugins,
30-
useLogger({
31-
skipIntrospection: true,
32-
logFn: (eventName, { args }) => {
33-
if (eventName.endsWith('-start')) {
34-
mesh.logger.debug(`\t headers: `, args.contextValue.headers);
35-
}
36-
},
37-
}),
38-
],
39-
logging: mesh.logger,
40-
maskedErrors: false,
41-
graphiql: playgroundEnabled && {
42-
title: playgroundTitle,
20+
const getYogaForMesh = memoize1(function getYogaForMesh(mesh: MeshInstance) {
21+
return createYoga({
22+
plugins: [
23+
...mesh.plugins,
24+
useLogger({
25+
skipIntrospection: true,
26+
logFn: (eventName, { args }) => {
27+
if (eventName.endsWith('-start')) {
28+
mesh.logger.debug(`\t headers: `, args.contextValue.headers);
29+
}
4330
},
44-
cors: corsConfig,
45-
graphqlEndpoint,
46-
landingPage: false,
47-
batching: batchingLimit ? { limit: batchingLimit } : false,
48-
});
49-
return yoga;
50-
});
51-
}
52-
return yoga$.then(yoga => yoga.handleRequest(request, ctx));
53-
};
31+
}),
32+
],
33+
logging: mesh.logger,
34+
maskedErrors: false,
35+
graphiql: playgroundEnabled && {
36+
title: playgroundTitle,
37+
},
38+
cors: corsConfig,
39+
graphqlEndpoint,
40+
landingPage: false,
41+
batching: batchingLimit ? { limit: batchingLimit } : false,
42+
});
43+
});
44+
return (request: Request, ctx: any) =>
45+
getBuiltMesh().then(mesh => getYogaForMesh(mesh).handleRequest(request, ctx));
5446
};

yarn.lock

+1
Original file line numberDiff line numberDiff line change
@@ -4978,6 +4978,7 @@ __metadata:
49784978
"@graphql-mesh/runtime": ^0.98.7
49794979
"@graphql-mesh/types": ^0.97.5
49804980
"@graphql-mesh/utils": ^0.97.5
4981+
"@graphql-tools/utils": ^9.2.1 || ^10.0.0
49814982
graphql: "*"
49824983
tslib: ^2.4.0
49834984
languageName: unknown

0 commit comments

Comments
 (0)