Skip to content

Commit 574a5d8

Browse files
authored
enhancement: lab to fetch schema if no introspection provided in inte… (#7888)
1 parent d3e0ef5 commit 574a5d8

6 files changed

Lines changed: 152 additions & 51 deletions

File tree

.changeset/loud-mammals-clean.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-hive/laboratory': patch
3+
'@graphql-hive/render-laboratory': patch
4+
---
5+
6+
If schema introspection isn't provided as property to Laboratory, lab will start interval to fetch
7+
schema every second.

packages/libraries/laboratory/src/components/laboratory/builder.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,6 @@ export const Builder = (props: {
791791
});
792792
}, [schema, deferredSearchValue, isSearchActive, tabValue]);
793793

794-
console.log(searchResult);
795-
796794
const visiblePaths = isSearchActive ? (searchResult?.visiblePaths ?? null) : null;
797795
const forcedOpenPaths =
798796
isSearchActive && deferredSearchValue.includes('.')

packages/libraries/laboratory/src/components/laboratory/editor.tsx

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
22
import * as monaco from 'monaco-editor';
3+
import { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js';
34
import { initializeMode } from 'monaco-graphql/initializeMode';
45
import MonacoEditor, { loader } from '@monaco-editor/react';
56
import { useLaboratory } from './context';
@@ -128,34 +129,44 @@ const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
128129
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
129130
const { introspection, endpoint, theme } = useLaboratory();
130131
const [typescriptReady, setTypescriptReady] = useState(!!monaco.languages.typescript);
132+
const apiRef = useRef<MonacoGraphQLAPI | null>(null);
131133

132134
useEffect(() => {
133135
if (introspection) {
134-
const api = initializeMode({
135-
schemas: [
136+
if (apiRef.current) {
137+
apiRef.current.setSchemaConfig([
136138
{
137139
introspectionJSON: introspection,
138140
uri: `schema_${endpoint}.graphql`,
139141
},
140-
],
141-
diagnosticSettings:
142-
props.uri && props.variablesUri
143-
? {
144-
validateVariablesJSON: {
145-
[props.uri.toString()]: [props.variablesUri.toString()],
146-
},
147-
jsonDiagnosticSettings: {
148-
allowComments: true, // allow json, parse with a jsonc parser to make requests
149-
},
150-
}
151-
: undefined,
152-
});
153-
154-
api.setCompletionSettings({
155-
__experimental__fillLeafsOnComplete: true,
156-
});
142+
]);
143+
} else {
144+
apiRef.current = initializeMode({
145+
schemas: [
146+
{
147+
introspectionJSON: introspection,
148+
uri: `schema_${endpoint}.graphql`,
149+
},
150+
],
151+
diagnosticSettings:
152+
props.uri && props.variablesUri
153+
? {
154+
validateVariablesJSON: {
155+
[props.uri.toString()]: [props.variablesUri.toString()],
156+
},
157+
jsonDiagnosticSettings: {
158+
allowComments: true, // allow json, parse with a jsonc parser to make requests
159+
},
160+
}
161+
: undefined,
162+
});
163+
164+
apiRef.current.setCompletionSettings({
165+
__experimental__fillLeafsOnComplete: true,
166+
});
167+
}
157168
}
158-
}, [introspection, props.uri?.toString(), props.variablesUri?.toString()]);
169+
}, [endpoint, introspection, props.uri?.toString(), props.variablesUri?.toString()]);
159170

160171
useEffect(() => {
161172
void (async function () {

packages/libraries/laboratory/src/lib/endpoint.ts

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
type IntrospectionQuery,
77
} from 'graphql';
88
import { toast } from 'sonner';
9+
import z from 'zod';
10+
import { asyncInterval } from '@/lib/utils';
911

1012
export interface LaboratoryEndpointState {
1113
endpoint: string | null;
@@ -20,6 +22,16 @@ export interface LaboratoryEndpointActions {
2022
restoreDefaultEndpoint: () => void;
2123
}
2224

25+
const GraphQLResponseErrorSchema = z
26+
.object({
27+
errors: z.array(
28+
z.object({
29+
message: z.string(),
30+
}),
31+
),
32+
})
33+
.strict();
34+
2335
export const useEndpoint = (props: {
2436
defaultEndpoint?: string | null;
2537
onEndpointChange?: (endpoint: string | null) => void;
@@ -40,35 +52,87 @@ export const useEndpoint = (props: {
4052
return introspection ? buildClientSchema(introspection) : null;
4153
}, [introspection]);
4254

43-
const fetchSchema = useCallback(async () => {
44-
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
45-
setIntrospection(props.defaultSchemaIntrospection);
46-
return;
47-
}
55+
const fetchSchema = useCallback(
56+
async (signal?: AbortSignal) => {
57+
if (endpoint === props.defaultEndpoint && props.defaultSchemaIntrospection) {
58+
setIntrospection(props.defaultSchemaIntrospection);
59+
return;
60+
}
4861

49-
if (!endpoint) {
50-
setIntrospection(null);
51-
return;
52-
}
62+
if (!endpoint) {
63+
setIntrospection(null);
64+
return;
65+
}
66+
67+
try {
68+
const response = await fetch(endpoint, {
69+
signal,
70+
method: 'POST',
71+
body: JSON.stringify({
72+
query: getIntrospectionQuery(),
73+
}),
74+
headers: {
75+
'Content-Type': 'application/json',
76+
},
77+
}).then(r => r.json());
78+
79+
const parsedResponse = GraphQLResponseErrorSchema.safeParse(response);
80+
81+
if (parsedResponse.success) {
82+
throw new Error(parsedResponse.data.errors.map(e => e.message).join('\n'));
83+
}
84+
85+
if (response.error && typeof response.error === 'string') {
86+
throw new Error(response.error);
87+
}
5388

54-
try {
55-
const response = await fetch(endpoint, {
56-
method: 'POST',
57-
body: JSON.stringify({
58-
query: getIntrospectionQuery(),
59-
}),
60-
headers: {
61-
'Content-Type': 'application/json',
62-
},
63-
}).then(r => r.json());
64-
65-
setIntrospection(response.data as IntrospectionQuery);
66-
} catch {
67-
toast.error('Failed to fetch schema');
68-
setIntrospection(null);
89+
setIntrospection(response.data as IntrospectionQuery);
90+
} catch (error: unknown) {
91+
if (
92+
error &&
93+
typeof error === 'object' &&
94+
'message' in error &&
95+
typeof error.message === 'string'
96+
) {
97+
toast.error(error.message);
98+
} else {
99+
toast.error('Failed to fetch schema');
100+
}
101+
102+
setIntrospection(null);
103+
104+
throw error;
105+
}
106+
},
107+
[endpoint],
108+
);
109+
110+
const shouldPollSchema = useMemo(() => {
111+
return endpoint !== props.defaultEndpoint || !props.defaultSchemaIntrospection;
112+
}, [endpoint, props.defaultEndpoint, props.defaultSchemaIntrospection]);
113+
114+
useEffect(() => {
115+
if (!shouldPollSchema || !endpoint) {
69116
return;
70117
}
71-
}, [endpoint]);
118+
119+
const intervalController = new AbortController();
120+
121+
void asyncInterval(
122+
async () => {
123+
try {
124+
await fetchSchema(intervalController.signal);
125+
} catch {
126+
intervalController.abort();
127+
}
128+
},
129+
5000,
130+
intervalController.signal,
131+
);
132+
return () => {
133+
intervalController.abort();
134+
};
135+
}, [shouldPollSchema, fetchSchema]);
72136

73137
const restoreDefaultEndpoint = useCallback(() => {
74138
if (props.defaultEndpoint) {
@@ -77,10 +141,10 @@ export const useEndpoint = (props: {
77141
}, [props.defaultEndpoint]);
78142

79143
useEffect(() => {
80-
if (endpoint) {
144+
if (endpoint && !shouldPollSchema) {
81145
void fetchSchema();
82146
}
83-
}, [endpoint, fetchSchema]);
147+
}, [endpoint, fetchSchema, shouldPollSchema]);
84148

85149
return {
86150
endpoint,

packages/libraries/laboratory/src/lib/operations.utils.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,6 @@ export async function getOperationHash(
348348
operation: Pick<LaboratoryOperation, 'query' | 'variables'>,
349349
) {
350350
try {
351-
console.log(operation.query, operation.variables);
352-
353351
const canonicalQuery = print(parse(operation.query));
354352
const canonicalVariables = '';
355353
const canonical = `${canonicalQuery}\n${canonicalVariables}`;

packages/libraries/laboratory/src/lib/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,26 @@ export function splitIdentifier(input: string): string[] {
1717
.split(/\s+/)
1818
.map(w => w.toLowerCase());
1919
}
20+
21+
export async function asyncInterval(
22+
fn: () => Promise<void>,
23+
delay: number,
24+
signal?: AbortSignal,
25+
): Promise<void> {
26+
while (!signal?.aborted) {
27+
await fn();
28+
29+
await new Promise<void>((resolve, reject) => {
30+
const timer = setTimeout(resolve, delay);
31+
32+
signal?.addEventListener(
33+
'abort',
34+
() => {
35+
clearTimeout(timer);
36+
reject(new DOMException('Aborted', 'AbortError'));
37+
},
38+
{ once: true },
39+
);
40+
});
41+
}
42+
}

0 commit comments

Comments
 (0)