-
Notifications
You must be signed in to change notification settings - Fork 145
Expand file tree
/
Copy pathmock-graphql.ts
More file actions
150 lines (127 loc) · 4.77 KB
/
Copy pathmock-graphql.ts
File metadata and controls
150 lines (127 loc) · 4.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* Mock GraphQL endpoint for developing the laboratory UI.
*
* Mounted onto the Vite dev server at /graphql by vite.config.ts, so `pnpm dev`
* gives the lab a same-origin endpoint with no extra process and no CORS.
*
* Serves Hive's own API schema (the repo root schema.graphql) with generated
* resolvers, so the builder has a large, deeply nested, described schema to
* render. Values are deterministic so response sizes stay stable between runs.
*
* Send `x-query-plan: <fixture>` with a request to get an extensions.queryPlan back;
* see dev/query-plan-fixtures.ts for the names. Omit the header for the empty state.
*
* Note: everything here goes through graphql-yoga's own exports on purpose. The
* workspace has two copies of `graphql` (16.9.0 hoisted at the root, 16.14.2 for
* this package) and importing it directly builds a schema that fails yoga's
* internal instanceof checks with "Duplicate graphql modules cannot be used".
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { createSchema, createYoga, type Plugin } from 'graphql-yoga';
import { resolveQueryPlanFixture } from './query-plan-fixtures';
/** Structural shapes, so this file never needs graphql's own type predicates. */
type MockType = {
ofType?: MockType;
name?: string;
getValues?: () => Array<{ value: unknown }>;
serialize?: unknown;
toString: () => string;
};
type MockField = { type: MockType; resolve?: (source: unknown) => unknown };
type MockNamedType = {
name: string;
getFields?: () => Record<string, MockField>;
isTypeOf?: unknown;
resolveType?: unknown;
};
const SCALAR_VALUES: Record<string, unknown> = {
ID: 'mock-id',
Int: 42,
Float: 3.14,
Boolean: true,
SafeInt: 42,
Date: '2026-07-25',
DateTime: '2026-07-25T09:00:00.000Z',
DateTime64: '2026-07-25T09:00:00.000Z',
JSON: { mock: true },
JSONObject: { mock: true },
JSONSchemaObject: { type: 'object' },
};
const mockValue = (type: MockType, fieldName: string): unknown => {
const signature = type.toString();
if (signature.endsWith('!') && type.ofType) {
return mockValue(type.ofType, fieldName);
}
if (signature.startsWith('[') && type.ofType) {
return [0, 1, 2].map(() => mockValue(type.ofType as MockType, fieldName));
}
// Enums expose getValues; check before scalars, since enums serialize too.
if (typeof type.getValues === 'function') {
return type.getValues()[0]?.value ?? null;
}
if (typeof type.serialize === 'function') {
if (type.name === 'String') {
return `${fieldName} value`;
}
return type.name && type.name in SCALAR_VALUES
? SCALAR_VALUES[type.name]
: `${fieldName} value`;
}
// Object, interface and union types resolve to an empty shell; their own
// field resolvers fill in the next level down.
return {};
};
const queryPlanPlugin: Plugin = {
onExecute({ args }) {
const request = (args.contextValue as { request?: Request } | undefined)?.request;
const queryPlan = resolveQueryPlanFixture(request?.headers.get('x-query-plan') ?? null);
if (!queryPlan) {
return;
}
return {
onExecuteDone({ result, setResult }) {
// Streamed results (defer/stream) arrive as async iterables; skip those.
if (Symbol.asyncIterator in Object(result)) {
return;
}
const single = result as { errors?: unknown[]; extensions?: Record<string, unknown> };
if (single.errors?.length) {
return;
}
setResult({ ...single, extensions: { ...single.extensions, queryPlan } });
},
};
},
};
export const createMockYoga = ({ graphqlEndpoint = '/graphql' } = {}) => {
const schemaPath = fileURLToPath(new URL('../../../../schema.graphql', import.meta.url));
const schema = createSchema({ typeDefs: readFileSync(schemaPath, 'utf-8') });
for (const type of Object.values(schema.getTypeMap()) as unknown as MockNamedType[]) {
if (type.name.startsWith('__')) {
continue;
}
// Object types own isTypeOf; interfaces and unions own resolveType.
if ('isTypeOf' in type && typeof type.getFields === 'function') {
for (const [fieldName, field] of Object.entries(type.getFields())) {
field.resolve = (source: unknown) => {
// Honour anything an ancestor explicitly provided.
if (source && typeof source === 'object' && fieldName in source) {
return (source as Record<string, unknown>)[fieldName];
}
return mockValue(field.type, fieldName);
};
}
}
if ('resolveType' in type) {
const possibleTypes = schema.getPossibleTypes(type as never);
type.resolveType = () => possibleTypes[0]?.name ?? null;
}
}
return createYoga({
schema,
graphqlEndpoint,
graphiql: false,
plugins: [queryPlanPlugin],
});
};