graphql-http 1.2.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -26
- package/lib/audits/common.d.ts +58 -0
- package/lib/audits/common.js +7 -0
- package/lib/audits/common.mjs +6 -0
- package/lib/audits/index.d.ts +2 -0
- package/lib/audits/index.js +18 -0
- package/lib/audits/index.mjs +2 -0
- package/lib/audits/server.d.ts +39 -0
- package/lib/audits/server.js +400 -0
- package/lib/audits/server.mjs +395 -0
- package/lib/audits/utils.d.ts +27 -0
- package/lib/audits/utils.js +100 -0
- package/lib/audits/utils.mjs +81 -0
- package/lib/client.js +2 -2
- package/lib/client.mjs +2 -2
- package/lib/handler.d.ts +9 -3
- package/lib/handler.js +18 -13
- package/lib/handler.mjs +18 -13
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.mjs +1 -0
- package/lib/utils.d.ts +3 -1
- package/lib/utils.js +12 -1
- package/lib/utils.mjs +10 -0
- package/package.json +15 -15
- package/umd/graphql-http.js +2 -2
- package/umd/graphql-http.min.js +1 -1
- package/umd/graphql-http.min.js.gz +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* audit/server
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import { assert, audit, extendedTypeof } from './utils.mjs';
|
|
7
|
+
/**
|
|
8
|
+
* List of server audits required to check GraphQL over HTTP spec conformance.
|
|
9
|
+
*
|
|
10
|
+
* @category Audits
|
|
11
|
+
*/
|
|
12
|
+
export function serverAudits(opts) {
|
|
13
|
+
const fetchFn = (opts.fetchFn || fetch);
|
|
14
|
+
return [
|
|
15
|
+
// Media Types
|
|
16
|
+
audit('MUST accept application/graphql-response+json and match the content-type', async () => {
|
|
17
|
+
const url = new URL(opts.url);
|
|
18
|
+
url.searchParams.set('query', '{ __typename }');
|
|
19
|
+
const res = await fetchFn(url.toString(), {
|
|
20
|
+
headers: {
|
|
21
|
+
accept: 'application/graphql-response+json',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
assert('Status code', res.status).toBe(200);
|
|
25
|
+
assert('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
|
|
26
|
+
}),
|
|
27
|
+
audit('MUST accept application/json and match the content-type', async () => {
|
|
28
|
+
const url = new URL(opts.url);
|
|
29
|
+
url.searchParams.set('query', '{ __typename }');
|
|
30
|
+
const res = await fetchFn(url.toString(), {
|
|
31
|
+
headers: {
|
|
32
|
+
accept: 'application/json',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
assert('Status code', res.status).toBe(200);
|
|
36
|
+
assert('Content-Type header', res.headers.get('content-type')).toContain('application/json');
|
|
37
|
+
}),
|
|
38
|
+
audit('MUST accept */* and use application/graphql-response+json for the content-type', async () => {
|
|
39
|
+
const url = new URL(opts.url);
|
|
40
|
+
url.searchParams.set('query', '{ __typename }');
|
|
41
|
+
const res = await fetchFn(url.toString(), {
|
|
42
|
+
headers: {
|
|
43
|
+
accept: '*/*',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
assert('Status code', res.status).toBe(200);
|
|
47
|
+
assert('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
|
|
48
|
+
}),
|
|
49
|
+
audit('MUST assume application/graphql-response+json content-type when accept is missing', async () => {
|
|
50
|
+
const url = new URL(opts.url);
|
|
51
|
+
url.searchParams.set('query', '{ __typename }');
|
|
52
|
+
const res = await fetchFn(url.toString());
|
|
53
|
+
assert('Status code', res.status).toBe(200);
|
|
54
|
+
assert('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
|
|
55
|
+
}),
|
|
56
|
+
audit('MUST use utf-8 charset in response', async () => {
|
|
57
|
+
const url = new URL(opts.url);
|
|
58
|
+
url.searchParams.set('query', '{ __typename }');
|
|
59
|
+
const res = await fetchFn(url.toString());
|
|
60
|
+
assert('Status code', res.status).toBe(200);
|
|
61
|
+
assert('Content-Type header', res.headers.get('content-type')).toContain('charset=utf-8');
|
|
62
|
+
}),
|
|
63
|
+
audit('MUST accept only utf-8 charset', async () => {
|
|
64
|
+
const url = new URL(opts.url);
|
|
65
|
+
url.searchParams.set('query', '{ __typename }');
|
|
66
|
+
const res = await fetchFn(url.toString(), {
|
|
67
|
+
headers: {
|
|
68
|
+
accept: 'application/graphql-response+json; charset=iso-8859-1',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
assert('Status code', res.status).toBe(406);
|
|
72
|
+
assert('Accept header', res.headers.get('accept')).toContain('charset=utf-8');
|
|
73
|
+
}),
|
|
74
|
+
// Request
|
|
75
|
+
audit('MUST accept POST requests', async () => {
|
|
76
|
+
const res = await fetchFn(opts.url, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
80
|
+
});
|
|
81
|
+
assert('Status code', res.status).toBe(200);
|
|
82
|
+
}),
|
|
83
|
+
audit('MAY accept application/x-www-form-urlencoded formatted GET requests', async () => {
|
|
84
|
+
const url = new URL(opts.url);
|
|
85
|
+
url.searchParams.set('query', '{ __typename }');
|
|
86
|
+
const res = await fetchFn(url.toString());
|
|
87
|
+
assert('Status code', res.status).toBe(200);
|
|
88
|
+
}),
|
|
89
|
+
// Request GET
|
|
90
|
+
audit('MUST NOT allow executing mutations on GET requests', async () => {
|
|
91
|
+
const url = new URL(opts.url);
|
|
92
|
+
url.searchParams.set('query', 'mutation { __typename }');
|
|
93
|
+
const res = await fetchFn(url.toString(), {
|
|
94
|
+
headers: {
|
|
95
|
+
accept: 'application/graphql-response+json',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
assert('Status code', res.status).toBe(405);
|
|
99
|
+
}),
|
|
100
|
+
// Request POST
|
|
101
|
+
audit('SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
|
|
102
|
+
const res = await fetchFn(opts.url, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
});
|
|
105
|
+
assert('Status code', res.status).toBeGreaterThanOrEqual(400);
|
|
106
|
+
assert('Status code', res.status).toBeLessThanOrEqual(599);
|
|
107
|
+
}),
|
|
108
|
+
audit('MUST accept application/json POST requests', async () => {
|
|
109
|
+
const res = await fetchFn(opts.url, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'content-type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
113
|
+
});
|
|
114
|
+
assert('Status code', res.status).toBe(200);
|
|
115
|
+
}),
|
|
116
|
+
audit('MUST require a request body on POST', async () => {
|
|
117
|
+
const res = await fetchFn(opts.url, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'content-type': 'application/json' },
|
|
120
|
+
});
|
|
121
|
+
assert('Status code', res.status).toBe(400);
|
|
122
|
+
}),
|
|
123
|
+
// Request Parameters
|
|
124
|
+
audit('MUST require the {query} parameter', async () => {
|
|
125
|
+
const res = await fetchFn(opts.url, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'content-type': 'application/json',
|
|
129
|
+
accept: 'application/graphql-response+json',
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({ notquery: '{ __typename }' }),
|
|
132
|
+
});
|
|
133
|
+
assert('Status code', res.status).toBe(400);
|
|
134
|
+
}),
|
|
135
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => audit(`MUST NOT allow ${extendedTypeof(invalid)} for the {query} parameter`, async () => {
|
|
136
|
+
const res = await fetchFn(opts.url, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: {
|
|
139
|
+
'content-type': 'application/json',
|
|
140
|
+
accept: 'application/graphql-response+json',
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
query: invalid,
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
assert('Status code', res.status).toBe(400);
|
|
147
|
+
})),
|
|
148
|
+
audit('MUST accept a string for the {query} parameter', async () => {
|
|
149
|
+
const res = await fetchFn(opts.url, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'content-type': 'application/json',
|
|
153
|
+
accept: 'application/graphql-response+json',
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
query: '{ __typename }',
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
assert('Status code', res.status).toBe(200);
|
|
160
|
+
}),
|
|
161
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => audit(`MUST NOT allow ${extendedTypeof(invalid)} for the {operationName} parameter`, async () => {
|
|
162
|
+
const res = await fetchFn(opts.url, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: {
|
|
165
|
+
'content-type': 'application/json',
|
|
166
|
+
accept: 'application/graphql-response+json',
|
|
167
|
+
},
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
operationName: invalid,
|
|
170
|
+
query: '{ __typename }',
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
assert('Status code', res.status).toBe(400);
|
|
174
|
+
})),
|
|
175
|
+
audit('MUST accept a string for the {operationName} parameter', async () => {
|
|
176
|
+
const res = await fetchFn(opts.url, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: {
|
|
179
|
+
'content-type': 'application/json',
|
|
180
|
+
accept: 'application/graphql-response+json',
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
operationName: 'Query',
|
|
184
|
+
query: 'query Query { __typename }',
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
assert('Status code', res.status).toBe(200);
|
|
188
|
+
}),
|
|
189
|
+
...['string', 0, false, ['array']].map((invalid) => audit(`MUST NOT allow ${extendedTypeof(invalid)} for the {variables} parameter`, async () => {
|
|
190
|
+
const res = await fetchFn(opts.url, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: {
|
|
193
|
+
'content-type': 'application/json',
|
|
194
|
+
accept: 'application/graphql-response+json',
|
|
195
|
+
},
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
query: '{ __typename }',
|
|
198
|
+
variables: invalid,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
assert('Status code', res.status).toBe(400);
|
|
202
|
+
})),
|
|
203
|
+
audit('MUST accept a map for the {variables} parameter', async () => {
|
|
204
|
+
const res = await fetchFn(opts.url, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers: {
|
|
207
|
+
'content-type': 'application/json',
|
|
208
|
+
accept: 'application/graphql-response+json',
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
query: 'query Type($name: String!) { __type(name: $name) { name } }',
|
|
212
|
+
variables: { name: 'sometype' },
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
assert('Status code', res.status).toBe(200);
|
|
216
|
+
const result = (await res.json());
|
|
217
|
+
assert('Execution result', result).notToHaveProperty('errors');
|
|
218
|
+
}),
|
|
219
|
+
audit('MUST accept a URL-encoded JSON string for the {variables} parameter in GETs', async () => {
|
|
220
|
+
const url = new URL(opts.url);
|
|
221
|
+
url.searchParams.set('query', 'query Type($name: String!) { __type(name: $name) { name } }');
|
|
222
|
+
url.searchParams.set('variables', JSON.stringify({ name: 'sometype' }));
|
|
223
|
+
const res = await fetchFn(url.toString(), {
|
|
224
|
+
method: 'GET',
|
|
225
|
+
});
|
|
226
|
+
assert('Status code', res.status).toBe(200);
|
|
227
|
+
const result = (await res.json());
|
|
228
|
+
assert('Execution result', result).notToHaveProperty('errors');
|
|
229
|
+
}),
|
|
230
|
+
...['string', 0, false, ['array']].map((invalid) => audit(`MUST NOT allow ${extendedTypeof(invalid)} for the {extensions} parameter`, async () => {
|
|
231
|
+
const res = await fetchFn(opts.url, {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: {
|
|
234
|
+
'content-type': 'application/json',
|
|
235
|
+
accept: 'application/graphql-response+json',
|
|
236
|
+
},
|
|
237
|
+
body: JSON.stringify({
|
|
238
|
+
query: '{ __typename }',
|
|
239
|
+
extensions: invalid,
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
assert('Status code', res.status).toBe(400);
|
|
243
|
+
})),
|
|
244
|
+
audit('MUST accept a map for the {extensions} parameter', async () => {
|
|
245
|
+
const res = await fetchFn(opts.url, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: {
|
|
248
|
+
'content-type': 'application/json',
|
|
249
|
+
accept: 'application/graphql-response+json',
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
query: '{ __typename }',
|
|
253
|
+
extensions: { some: 'value' },
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
assert('Status code', res.status).toBe(200);
|
|
257
|
+
}),
|
|
258
|
+
// TODO: audit('MUST accept a map for the {extensions} parameter'),
|
|
259
|
+
// Response application/json
|
|
260
|
+
audit('SHOULD use 200 status code on JSON parsing failure when accepting application/json', async () => {
|
|
261
|
+
const res = await fetchFn(opts.url, {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: {
|
|
264
|
+
'content-type': 'application/json',
|
|
265
|
+
accept: 'application/json',
|
|
266
|
+
},
|
|
267
|
+
body: '{ "not a JSON',
|
|
268
|
+
});
|
|
269
|
+
assert('Status code', res.status).toBe(200);
|
|
270
|
+
}),
|
|
271
|
+
audit('SHOULD use 200 status code if parameters are invalid when accepting application/json', async () => {
|
|
272
|
+
const url = new URL(opts.url);
|
|
273
|
+
url.searchParams.set('qeury' /* typo */, '{ __typename }');
|
|
274
|
+
const res = await fetchFn(url.toString(), {
|
|
275
|
+
method: 'GET',
|
|
276
|
+
headers: { accept: 'application/json' },
|
|
277
|
+
});
|
|
278
|
+
assert('Status code', res.status).toBe(200);
|
|
279
|
+
}),
|
|
280
|
+
audit('SHOULD use 200 status code on document parsing failure when accepting application/json', async () => {
|
|
281
|
+
const url = new URL(opts.url);
|
|
282
|
+
url.searchParams.set('query', '{');
|
|
283
|
+
const res = await fetchFn(url.toString(), {
|
|
284
|
+
method: 'GET',
|
|
285
|
+
headers: { accept: 'application/json' },
|
|
286
|
+
});
|
|
287
|
+
assert('Status code', res.status).toBe(200);
|
|
288
|
+
}),
|
|
289
|
+
audit('SHOULD use 200 status code on document validation failure when accepting application/json', async () => {
|
|
290
|
+
const url = new URL(opts.url);
|
|
291
|
+
url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
|
|
292
|
+
const res = await fetchFn(url.toString(), {
|
|
293
|
+
method: 'GET',
|
|
294
|
+
headers: { accept: 'application/json' },
|
|
295
|
+
});
|
|
296
|
+
assert('Status code', res.status).toBe(200);
|
|
297
|
+
}),
|
|
298
|
+
// Response application/graphql-response+json
|
|
299
|
+
audit('MUST use 4xx or 5xx status codes on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
300
|
+
const res = await fetchFn(opts.url, {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: {
|
|
303
|
+
'content-type': 'application/json',
|
|
304
|
+
accept: 'application/graphql-response+json',
|
|
305
|
+
},
|
|
306
|
+
body: '{ "not a JSON',
|
|
307
|
+
});
|
|
308
|
+
assert('Status code', res.status).toBeGreaterThanOrEqual(400);
|
|
309
|
+
assert('Status code', res.status).toBeLessThanOrEqual(599);
|
|
310
|
+
}),
|
|
311
|
+
audit('SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
312
|
+
const res = await fetchFn(opts.url, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: {
|
|
315
|
+
'content-type': 'application/json',
|
|
316
|
+
accept: 'application/graphql-response+json',
|
|
317
|
+
},
|
|
318
|
+
body: '{ "not a JSON',
|
|
319
|
+
});
|
|
320
|
+
assert('Status code', res.status).toBe(400);
|
|
321
|
+
}),
|
|
322
|
+
audit('MUST use 4xx or 5xx status codes if parameters are invalid when accepting application/graphql-response+json', async () => {
|
|
323
|
+
const url = new URL(opts.url);
|
|
324
|
+
url.searchParams.set('qeury' /* typo */, '{ __typename }');
|
|
325
|
+
const res = await fetchFn(url.toString(), {
|
|
326
|
+
method: 'GET',
|
|
327
|
+
headers: { accept: 'application/graphql-response+json' },
|
|
328
|
+
});
|
|
329
|
+
assert('Status code', res.status).toBeGreaterThanOrEqual(400);
|
|
330
|
+
assert('Status code', res.status).toBeLessThanOrEqual(599);
|
|
331
|
+
}),
|
|
332
|
+
audit('SHOULD use 400 status code if parameters are invalid when accepting application/graphql-response+json', async () => {
|
|
333
|
+
const url = new URL(opts.url);
|
|
334
|
+
url.searchParams.set('qeury' /* typo */, '{ __typename }');
|
|
335
|
+
const res = await fetchFn(url.toString(), {
|
|
336
|
+
method: 'GET',
|
|
337
|
+
headers: { accept: 'application/graphql-response+json' },
|
|
338
|
+
});
|
|
339
|
+
assert('Status code', res.status).toBe(400);
|
|
340
|
+
}),
|
|
341
|
+
audit('MUST use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json', async () => {
|
|
342
|
+
const url = new URL(opts.url);
|
|
343
|
+
url.searchParams.set('query', '{');
|
|
344
|
+
const res = await fetchFn(url.toString(), {
|
|
345
|
+
method: 'GET',
|
|
346
|
+
headers: { accept: 'application/graphql-response+json' },
|
|
347
|
+
});
|
|
348
|
+
assert('Status code', res.status).toBeGreaterThanOrEqual(400);
|
|
349
|
+
assert('Status code', res.status).toBeLessThanOrEqual(599);
|
|
350
|
+
}),
|
|
351
|
+
audit('SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json', async () => {
|
|
352
|
+
const url = new URL(opts.url);
|
|
353
|
+
url.searchParams.set('query', '{');
|
|
354
|
+
const res = await fetchFn(url.toString(), {
|
|
355
|
+
method: 'GET',
|
|
356
|
+
headers: { accept: 'application/graphql-response+json' },
|
|
357
|
+
});
|
|
358
|
+
assert('Status code', res.status).toBe(400);
|
|
359
|
+
}),
|
|
360
|
+
audit('MUST use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json', async () => {
|
|
361
|
+
const url = new URL(opts.url);
|
|
362
|
+
url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
|
|
363
|
+
const res = await fetchFn(url.toString(), {
|
|
364
|
+
method: 'GET',
|
|
365
|
+
headers: { accept: 'application/graphql-response+json' },
|
|
366
|
+
});
|
|
367
|
+
assert('Status code', res.status).toBeGreaterThanOrEqual(400);
|
|
368
|
+
assert('Status code', res.status).toBeLessThanOrEqual(599);
|
|
369
|
+
}),
|
|
370
|
+
audit('SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json', async () => {
|
|
371
|
+
const url = new URL(opts.url);
|
|
372
|
+
url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
|
|
373
|
+
const res = await fetchFn(url.toString(), {
|
|
374
|
+
method: 'GET',
|
|
375
|
+
headers: { accept: 'application/graphql-response+json' },
|
|
376
|
+
});
|
|
377
|
+
assert('Status code', res.status).toBe(400);
|
|
378
|
+
}),
|
|
379
|
+
// TODO: how to make an unauthorized request?
|
|
380
|
+
// https://graphql.github.io/graphql-over-http/draft/#sel-EANNNDTAAEVBAAqqc
|
|
381
|
+
// audit('SHOULD use 401 or 403 status codes when the request is not permitted')
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Performs the full list of server audits required for GraphQL over HTTP spec conformance.
|
|
386
|
+
*
|
|
387
|
+
* Please consult the `AuditResult` for more information.
|
|
388
|
+
*
|
|
389
|
+
* @category Audits
|
|
390
|
+
*/
|
|
391
|
+
export async function auditServer(opts) {
|
|
392
|
+
const audits = serverAudits(opts);
|
|
393
|
+
// audit tests will throw only on fatal errors, tests are contained within the AuditResult
|
|
394
|
+
return await Promise.all(audits.map(({ fn }) => fn()));
|
|
395
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* audit/utils
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import { Audit, AuditName } from './common';
|
|
7
|
+
export * from '../utils';
|
|
8
|
+
/**
|
|
9
|
+
* Wrap and prepare an audit for testing.
|
|
10
|
+
*
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
13
|
+
export declare function audit(name: AuditName, fn: () => Promise<void>): Audit;
|
|
14
|
+
/**
|
|
15
|
+
* Will throw a string if the assertion fails.
|
|
16
|
+
*
|
|
17
|
+
* All fatal problems will throw an instance of Error.
|
|
18
|
+
*
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
export declare function assert<T = unknown>(name: string, actual: T): {
|
|
22
|
+
toBe: (expected: T) => void;
|
|
23
|
+
toBeLessThanOrEqual: (expected: T extends number ? T : never) => void;
|
|
24
|
+
toBeGreaterThanOrEqual: (expected: T extends number ? T : never) => void;
|
|
25
|
+
toContain: (expected: T extends (infer U)[] ? U : T extends string ? T : never) => void;
|
|
26
|
+
notToHaveProperty: (prop: T extends Record<any, any> ? PropertyKey : never) => void;
|
|
27
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* audit/utils
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
19
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.assert = exports.audit = void 0;
|
|
23
|
+
__exportStar(require("../utils"), exports);
|
|
24
|
+
/**
|
|
25
|
+
* Wrap and prepare an audit for testing.
|
|
26
|
+
*
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
function audit(name, fn) {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
fn: async () => {
|
|
33
|
+
try {
|
|
34
|
+
await fn();
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
status: 'ok',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (errOrReason) {
|
|
41
|
+
if (typeof errOrReason !== 'string') {
|
|
42
|
+
// anything thrown that is not an assertion string is considered fatal
|
|
43
|
+
throw errOrReason;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
status: name.startsWith('MUST')
|
|
48
|
+
? // only failing MUSTs are considered errors
|
|
49
|
+
'error'
|
|
50
|
+
: // everything else is optional and considered a warning
|
|
51
|
+
'warn',
|
|
52
|
+
reason: errOrReason,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
exports.audit = audit;
|
|
59
|
+
/**
|
|
60
|
+
* Will throw a string if the assertion fails.
|
|
61
|
+
*
|
|
62
|
+
* All fatal problems will throw an instance of Error.
|
|
63
|
+
*
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
function assert(name, actual) {
|
|
67
|
+
return {
|
|
68
|
+
toBe: (expected) => {
|
|
69
|
+
if (actual !== expected) {
|
|
70
|
+
throw `${name} ${actual} is not ${expected}`;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
toBeLessThanOrEqual: (expected) => {
|
|
74
|
+
if (!(actual <= expected)) {
|
|
75
|
+
throw `${name} ${actual} is not less than or equal to ${expected}`;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
toBeGreaterThanOrEqual: (expected) => {
|
|
79
|
+
if (!(actual >= expected)) {
|
|
80
|
+
throw `${name} ${actual} is not greater than or equal to ${expected}`;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
toContain: (expected) => {
|
|
84
|
+
// @ts-expect-error types will match, otherwise never
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
if (!actual.includes(expected)) {
|
|
87
|
+
throw `${name} ${JSON.stringify(actual)} does not contain ${JSON.stringify(expected)}`;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
notToHaveProperty: (
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
prop) => {
|
|
93
|
+
// @ts-expect-error types will match, otherwise never
|
|
94
|
+
if (prop in actual) {
|
|
95
|
+
throw `${name} ${JSON.stringify(actual)} does have a property '${String(prop)}'`;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
exports.assert = assert;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* audit/utils
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
export * from '../utils.mjs';
|
|
7
|
+
/**
|
|
8
|
+
* Wrap and prepare an audit for testing.
|
|
9
|
+
*
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
export function audit(name, fn) {
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
fn: async () => {
|
|
16
|
+
try {
|
|
17
|
+
await fn();
|
|
18
|
+
return {
|
|
19
|
+
name,
|
|
20
|
+
status: 'ok',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (errOrReason) {
|
|
24
|
+
if (typeof errOrReason !== 'string') {
|
|
25
|
+
// anything thrown that is not an assertion string is considered fatal
|
|
26
|
+
throw errOrReason;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
status: name.startsWith('MUST')
|
|
31
|
+
? // only failing MUSTs are considered errors
|
|
32
|
+
'error'
|
|
33
|
+
: // everything else is optional and considered a warning
|
|
34
|
+
'warn',
|
|
35
|
+
reason: errOrReason,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Will throw a string if the assertion fails.
|
|
43
|
+
*
|
|
44
|
+
* All fatal problems will throw an instance of Error.
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
export function assert(name, actual) {
|
|
49
|
+
return {
|
|
50
|
+
toBe: (expected) => {
|
|
51
|
+
if (actual !== expected) {
|
|
52
|
+
throw `${name} ${actual} is not ${expected}`;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
toBeLessThanOrEqual: (expected) => {
|
|
56
|
+
if (!(actual <= expected)) {
|
|
57
|
+
throw `${name} ${actual} is not less than or equal to ${expected}`;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
toBeGreaterThanOrEqual: (expected) => {
|
|
61
|
+
if (!(actual >= expected)) {
|
|
62
|
+
throw `${name} ${actual} is not greater than or equal to ${expected}`;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
toContain: (expected) => {
|
|
66
|
+
// @ts-expect-error types will match, otherwise never
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
if (!actual.includes(expected)) {
|
|
69
|
+
throw `${name} ${JSON.stringify(actual)} does not contain ${JSON.stringify(expected)}`;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
notToHaveProperty: (
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
prop) => {
|
|
75
|
+
// @ts-expect-error types will match, otherwise never
|
|
76
|
+
if (prop in actual) {
|
|
77
|
+
throw `${name} ${JSON.stringify(actual)} does have a property '${String(prop)}'`;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -105,7 +105,7 @@ function createClient(options) {
|
|
|
105
105
|
res = await fetchFn(url, {
|
|
106
106
|
signal: control.signal,
|
|
107
107
|
method: 'POST',
|
|
108
|
-
headers: Object.assign(Object.assign({}, headers), { 'content-type': 'application/json; charset=utf-8', accept: 'application/graphql+json, application/json' }),
|
|
108
|
+
headers: Object.assign(Object.assign({}, headers), { 'content-type': 'application/json; charset=utf-8', accept: 'application/graphql-response+json, application/json' }),
|
|
109
109
|
credentials,
|
|
110
110
|
referrer,
|
|
111
111
|
referrerPolicy,
|
|
@@ -122,7 +122,7 @@ function createClient(options) {
|
|
|
122
122
|
const contentType = res.headers.get('content-type');
|
|
123
123
|
if (!contentType)
|
|
124
124
|
throw new Error('Missing response content-type');
|
|
125
|
-
if (!contentType.includes('application/graphql+json') &&
|
|
125
|
+
if (!contentType.includes('application/graphql-response+json') &&
|
|
126
126
|
!contentType.includes('application/json')) {
|
|
127
127
|
throw new Error(`Unsupported response content-type ${contentType}`);
|
|
128
128
|
}
|
package/lib/client.mjs
CHANGED
|
@@ -88,7 +88,7 @@ export function createClient(options) {
|
|
|
88
88
|
res = await fetchFn(url, {
|
|
89
89
|
signal: control.signal,
|
|
90
90
|
method: 'POST',
|
|
91
|
-
headers: Object.assign(Object.assign({}, headers), { 'content-type': 'application/json; charset=utf-8', accept: 'application/graphql+json, application/json' }),
|
|
91
|
+
headers: Object.assign(Object.assign({}, headers), { 'content-type': 'application/json; charset=utf-8', accept: 'application/graphql-response+json, application/json' }),
|
|
92
92
|
credentials,
|
|
93
93
|
referrer,
|
|
94
94
|
referrerPolicy,
|
|
@@ -105,7 +105,7 @@ export function createClient(options) {
|
|
|
105
105
|
const contentType = res.headers.get('content-type');
|
|
106
106
|
if (!contentType)
|
|
107
107
|
throw new Error('Missing response content-type');
|
|
108
|
-
if (!contentType.includes('application/graphql+json') &&
|
|
108
|
+
if (!contentType.includes('application/graphql-response+json') &&
|
|
109
109
|
!contentType.includes('application/json')) {
|
|
110
110
|
throw new Error(`Unsupported response content-type ${contentType}`);
|
|
111
111
|
}
|
package/lib/handler.d.ts
CHANGED
|
@@ -32,7 +32,13 @@ export interface Request<RawRequest, Context> {
|
|
|
32
32
|
readonly method: string;
|
|
33
33
|
readonly url: string;
|
|
34
34
|
readonly headers: RequestHeaders;
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Parsed request body or a parser function.
|
|
37
|
+
*
|
|
38
|
+
* If the provided function throws, the error message "Unparsable JSON body" will
|
|
39
|
+
* be in the erroneous response.
|
|
40
|
+
*/
|
|
41
|
+
readonly body: string | Record<string, unknown> | null | (() => string | Record<string, unknown> | null | Promise<string | Record<string, unknown> | null>);
|
|
36
42
|
/**
|
|
37
43
|
* The raw request itself from the implementing server.
|
|
38
44
|
*
|
|
@@ -232,7 +238,7 @@ export declare type Handler<RawRequest = unknown, Context = unknown> = (req: Req
|
|
|
232
238
|
* url: req.url,
|
|
233
239
|
* method: req.method,
|
|
234
240
|
* headers: req.headers,
|
|
235
|
-
* body:
|
|
241
|
+
* body: () => new Promise((resolve) => {
|
|
236
242
|
* let body = '';
|
|
237
243
|
* req.on('data', (chunk) => (body += chunk));
|
|
238
244
|
* req.on('end', () => resolve(body));
|
|
@@ -258,7 +264,7 @@ export declare function createHandler<RawRequest = unknown, Context = unknown>(o
|
|
|
258
264
|
*
|
|
259
265
|
* @category Server
|
|
260
266
|
*/
|
|
261
|
-
export declare type AcceptableMediaType = 'application/graphql+json' | 'application/json';
|
|
267
|
+
export declare type AcceptableMediaType = 'application/graphql-response+json' | 'application/json';
|
|
262
268
|
/**
|
|
263
269
|
* Inspects the request and detects the appropriate/acceptable Media-Type
|
|
264
270
|
* looking at the `Accept` header while complying with the GraphQL over HTTP Protocol.
|