graphql-http 1.10.0 → 1.12.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.
@@ -19,7 +19,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
19
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
20
  };
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.assertBodyAsExecutionResult = exports.assert = exports.audit = void 0;
22
+ exports.ressert = exports.AuditError = exports.audit = void 0;
23
23
  __exportStar(require("../utils"), exports);
24
24
  /**
25
25
  * Wrap and prepare an audit for testing.
@@ -37,10 +37,10 @@ function audit(name, fn) {
37
37
  status: 'ok',
38
38
  };
39
39
  }
40
- catch (errOrReason) {
41
- if (typeof errOrReason !== 'string') {
42
- // anything thrown that is not an assertion string is considered fatal
43
- throw errOrReason;
40
+ catch (err) {
41
+ if (!(err instanceof AuditError)) {
42
+ // anything thrown that is not an assertion error is considered fatal
43
+ throw err;
44
44
  }
45
45
  return {
46
46
  name,
@@ -49,7 +49,8 @@ function audit(name, fn) {
49
49
  'error'
50
50
  : // everything else is optional and considered a warning
51
51
  'warn',
52
- reason: errOrReason,
52
+ reason: err.reason,
53
+ response: err.response,
53
54
  };
54
55
  }
55
56
  },
@@ -57,71 +58,98 @@ function audit(name, fn) {
57
58
  }
58
59
  exports.audit = audit;
59
60
  /**
60
- * Will throw a string if the assertion fails.
61
+ * Error thrown when an assertion test fails.
61
62
  *
62
- * All fatal problems will throw an instance of Error.
63
+ * @private
64
+ */
65
+ class AuditError {
66
+ constructor(response, reason) {
67
+ this.response = response;
68
+ this.reason = reason;
69
+ }
70
+ }
71
+ exports.AuditError = AuditError;
72
+ /**
73
+ * Will throw an AuditError if the assertion on Response fails.
74
+ *
75
+ * All fatal problems will throw an instance of an Error.
76
+ *
77
+ * The name "ressert" is a wordplay combining "response" and "assert".
63
78
  *
64
79
  * @private
65
80
  */
66
- function assert(name, actual) {
81
+ function ressert(res) {
67
82
  return {
68
- toBe: (expected) => {
69
- if (actual !== expected) {
70
- throw `${name} ${actual} is not ${expected}`;
71
- }
72
- },
73
- toBeBetween: (min, max) => {
74
- if (!(min <= actual && actual <= max)) {
75
- throw `${name} ${actual} is not between ${min} and ${max}`;
76
- }
77
- },
78
- toContain: (expected) => {
79
- // @ts-expect-error types will match, otherwise never
80
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
- if (!actual.includes(expected)) {
82
- throw `${name} ${JSON.stringify(actual)} does not contain ${JSON.stringify(expected)}`;
83
- }
84
- },
85
- notToContain: (expected) => {
86
- // @ts-expect-error types will match, otherwise never
87
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
- if (actual.includes(expected)) {
89
- throw `${name} ${JSON.stringify(actual)} contains ${JSON.stringify(expected)}`;
90
- }
83
+ status: {
84
+ toBe(code) {
85
+ if (res.status !== code) {
86
+ throw new AuditError(res, `Response status code is not ${code}`);
87
+ }
88
+ },
89
+ toBeBetween: (min, max) => {
90
+ if (!(min <= res.status && res.status <= max)) {
91
+ throw new AuditError(res, `Response status is not between ${min} and ${max}`);
92
+ }
93
+ },
91
94
  },
92
- toHaveProperty: (
93
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
- prop) => {
95
- // @ts-expect-error types will match, otherwise never
96
- if (!(prop in actual)) {
97
- throw `${name} ${JSON.stringify(actual)} does not have a property '${String(prop)}'`;
98
- }
95
+ header(key) {
96
+ return {
97
+ toContain(part) {
98
+ var _a;
99
+ if (!((_a = res.headers.get(key)) === null || _a === void 0 ? void 0 : _a.includes(part))) {
100
+ throw new AuditError(res, `Response header ${key} does not contain ${part}`);
101
+ }
102
+ },
103
+ notToContain(part) {
104
+ var _a;
105
+ if ((_a = res.headers.get(key)) === null || _a === void 0 ? void 0 : _a.includes(part)) {
106
+ throw new AuditError(res, `Response header ${key} contains ${part}`);
107
+ }
108
+ },
109
+ };
99
110
  },
100
- notToHaveProperty: (
101
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
- prop) => {
103
- // @ts-expect-error types will match, otherwise never
104
- if (prop in actual) {
105
- throw `${name} ${JSON.stringify(actual)} does have a property '${String(prop)}'`;
106
- }
111
+ bodyAsExecutionResult: {
112
+ data: {
113
+ async toBe(val) {
114
+ const body = await assertBodyAsExecutionResult(res);
115
+ if (body.data !== val) {
116
+ throw new AuditError(res, `Response body execution result data is not "${val}"`);
117
+ }
118
+ },
119
+ },
120
+ async toHaveProperty(key) {
121
+ const body = await assertBodyAsExecutionResult(res);
122
+ if (!(key in body)) {
123
+ throw new AuditError(res, `Response body execution result does not have a property "${key}"`);
124
+ }
125
+ },
126
+ async notToHaveProperty(key) {
127
+ const body = await assertBodyAsExecutionResult(res);
128
+ if (key in body) {
129
+ throw new AuditError(res, `Response body execution result has a property "${key}"`);
130
+ }
131
+ },
107
132
  },
108
133
  };
109
134
  }
110
- exports.assert = assert;
111
- /**
112
- * Parses the string as JSON and safely reports parsing issues for audits.
113
- *
114
- * Assumes the parsed JSON will be an `ExecutionResult`.
115
- *
116
- * @private
117
- * */
135
+ exports.ressert = ressert;
136
+ /** @private */
118
137
  async function assertBodyAsExecutionResult(res) {
119
- const str = await res.text();
138
+ let decoded;
139
+ try {
140
+ const decoder = new TextDecoder('utf-8');
141
+ const buff = await res.arrayBuffer();
142
+ decoded = decoder.decode(buff);
143
+ }
144
+ catch (err) {
145
+ throw new AuditError(res, 'Response body is not UTF-8 encoded');
146
+ }
147
+ let body;
120
148
  try {
121
- return JSON.parse(str);
149
+ body = JSON.parse(decoded);
122
150
  }
123
151
  catch (err) {
124
- throw `Response body is not valid JSON. Got ${JSON.stringify(str)}`;
152
+ throw new AuditError(res, 'Response body is not valid JSON');
125
153
  }
154
+ return body;
126
155
  }
127
- exports.assertBodyAsExecutionResult = assertBodyAsExecutionResult;
@@ -20,10 +20,10 @@ export function audit(name, fn) {
20
20
  status: 'ok',
21
21
  };
22
22
  }
23
- catch (errOrReason) {
24
- if (typeof errOrReason !== 'string') {
25
- // anything thrown that is not an assertion string is considered fatal
26
- throw errOrReason;
23
+ catch (err) {
24
+ if (!(err instanceof AuditError)) {
25
+ // anything thrown that is not an assertion error is considered fatal
26
+ throw err;
27
27
  }
28
28
  return {
29
29
  name,
@@ -32,76 +32,104 @@ export function audit(name, fn) {
32
32
  'error'
33
33
  : // everything else is optional and considered a warning
34
34
  'warn',
35
- reason: errOrReason,
35
+ reason: err.reason,
36
+ response: err.response,
36
37
  };
37
38
  }
38
39
  },
39
40
  };
40
41
  }
41
42
  /**
42
- * Will throw a string if the assertion fails.
43
+ * Error thrown when an assertion test fails.
43
44
  *
44
- * All fatal problems will throw an instance of Error.
45
+ * @private
46
+ */
47
+ export class AuditError {
48
+ constructor(response, reason) {
49
+ this.response = response;
50
+ this.reason = reason;
51
+ }
52
+ }
53
+ /**
54
+ * Will throw an AuditError if the assertion on Response fails.
55
+ *
56
+ * All fatal problems will throw an instance of an Error.
57
+ *
58
+ * The name "ressert" is a wordplay combining "response" and "assert".
45
59
  *
46
60
  * @private
47
61
  */
48
- export function assert(name, actual) {
62
+ export function ressert(res) {
49
63
  return {
50
- toBe: (expected) => {
51
- if (actual !== expected) {
52
- throw `${name} ${actual} is not ${expected}`;
53
- }
54
- },
55
- toBeBetween: (min, max) => {
56
- if (!(min <= actual && actual <= max)) {
57
- throw `${name} ${actual} is not between ${min} and ${max}`;
58
- }
59
- },
60
- toContain: (expected) => {
61
- // @ts-expect-error types will match, otherwise never
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
- if (!actual.includes(expected)) {
64
- throw `${name} ${JSON.stringify(actual)} does not contain ${JSON.stringify(expected)}`;
65
- }
66
- },
67
- notToContain: (expected) => {
68
- // @ts-expect-error types will match, otherwise never
69
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
- if (actual.includes(expected)) {
71
- throw `${name} ${JSON.stringify(actual)} contains ${JSON.stringify(expected)}`;
72
- }
64
+ status: {
65
+ toBe(code) {
66
+ if (res.status !== code) {
67
+ throw new AuditError(res, `Response status code is not ${code}`);
68
+ }
69
+ },
70
+ toBeBetween: (min, max) => {
71
+ if (!(min <= res.status && res.status <= max)) {
72
+ throw new AuditError(res, `Response status is not between ${min} and ${max}`);
73
+ }
74
+ },
73
75
  },
74
- toHaveProperty: (
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- prop) => {
77
- // @ts-expect-error types will match, otherwise never
78
- if (!(prop in actual)) {
79
- throw `${name} ${JSON.stringify(actual)} does not have a property '${String(prop)}'`;
80
- }
76
+ header(key) {
77
+ return {
78
+ toContain(part) {
79
+ var _a;
80
+ if (!((_a = res.headers.get(key)) === null || _a === void 0 ? void 0 : _a.includes(part))) {
81
+ throw new AuditError(res, `Response header ${key} does not contain ${part}`);
82
+ }
83
+ },
84
+ notToContain(part) {
85
+ var _a;
86
+ if ((_a = res.headers.get(key)) === null || _a === void 0 ? void 0 : _a.includes(part)) {
87
+ throw new AuditError(res, `Response header ${key} contains ${part}`);
88
+ }
89
+ },
90
+ };
81
91
  },
82
- notToHaveProperty: (
83
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
- prop) => {
85
- // @ts-expect-error types will match, otherwise never
86
- if (prop in actual) {
87
- throw `${name} ${JSON.stringify(actual)} does have a property '${String(prop)}'`;
88
- }
92
+ bodyAsExecutionResult: {
93
+ data: {
94
+ async toBe(val) {
95
+ const body = await assertBodyAsExecutionResult(res);
96
+ if (body.data !== val) {
97
+ throw new AuditError(res, `Response body execution result data is not "${val}"`);
98
+ }
99
+ },
100
+ },
101
+ async toHaveProperty(key) {
102
+ const body = await assertBodyAsExecutionResult(res);
103
+ if (!(key in body)) {
104
+ throw new AuditError(res, `Response body execution result does not have a property "${key}"`);
105
+ }
106
+ },
107
+ async notToHaveProperty(key) {
108
+ const body = await assertBodyAsExecutionResult(res);
109
+ if (key in body) {
110
+ throw new AuditError(res, `Response body execution result has a property "${key}"`);
111
+ }
112
+ },
89
113
  },
90
114
  };
91
115
  }
92
- /**
93
- * Parses the string as JSON and safely reports parsing issues for audits.
94
- *
95
- * Assumes the parsed JSON will be an `ExecutionResult`.
96
- *
97
- * @private
98
- * */
99
- export async function assertBodyAsExecutionResult(res) {
100
- const str = await res.text();
116
+ /** @private */
117
+ async function assertBodyAsExecutionResult(res) {
118
+ let decoded;
119
+ try {
120
+ const decoder = new TextDecoder('utf-8');
121
+ const buff = await res.arrayBuffer();
122
+ decoded = decoder.decode(buff);
123
+ }
124
+ catch (err) {
125
+ throw new AuditError(res, 'Response body is not UTF-8 encoded');
126
+ }
127
+ let body;
101
128
  try {
102
- return JSON.parse(str);
129
+ body = JSON.parse(decoded);
103
130
  }
104
131
  catch (err) {
105
- throw `Response body is not valid JSON. Got ${JSON.stringify(str)}`;
132
+ throw new AuditError(res, 'Response body is not valid JSON');
106
133
  }
134
+ return body;
107
135
  }
package/lib/handler.js CHANGED
@@ -326,41 +326,40 @@ exports.getAcceptableMediaType = getAcceptableMediaType;
326
326
  * @category Server
327
327
  */
328
328
  function makeResponse(resultOrErrors, acceptedMediaType) {
329
- if ((0, utils_1.isExecutionResult)(resultOrErrors)) {
329
+ const errors = (0, utils_1.isGraphQLError)(resultOrErrors)
330
+ ? [resultOrErrors]
331
+ : (0, utils_1.areGraphQLErrors)(resultOrErrors)
332
+ ? resultOrErrors
333
+ : null;
334
+ if (errors) {
330
335
  return [
331
- JSON.stringify(resultOrErrors),
332
- {
333
- status: 200,
334
- statusText: 'OK',
335
- headers: {
336
+ JSON.stringify({ errors }),
337
+ Object.assign(Object.assign({}, (acceptedMediaType === 'application/json'
338
+ ? {
339
+ status: 200,
340
+ statusText: 'OK',
341
+ }
342
+ : {
343
+ status: 400,
344
+ statusText: 'Bad Request',
345
+ })), { headers: {
336
346
  'content-type': acceptedMediaType === 'application/json'
337
347
  ? 'application/json; charset=utf-8'
338
348
  : 'application/graphql-response+json; charset=utf-8',
339
- },
340
- },
349
+ } }),
341
350
  ];
342
351
  }
343
352
  return [
344
- JSON.stringify({
345
- errors: Array.isArray(resultOrErrors)
346
- ? (0, utils_1.isObject)(resultOrErrors)
347
- ? resultOrErrors
348
- : new graphql_1.GraphQLError(String(resultOrErrors))
349
- : [resultOrErrors],
350
- }),
351
- Object.assign(Object.assign({}, (acceptedMediaType === 'application/json'
352
- ? {
353
- status: 200,
354
- statusText: 'OK',
355
- }
356
- : {
357
- status: 400,
358
- statusText: 'Bad Request',
359
- })), { headers: {
353
+ JSON.stringify(resultOrErrors),
354
+ {
355
+ status: 200,
356
+ statusText: 'OK',
357
+ headers: {
360
358
  'content-type': acceptedMediaType === 'application/json'
361
359
  ? 'application/json; charset=utf-8'
362
360
  : 'application/graphql-response+json; charset=utf-8',
363
- } }),
361
+ },
362
+ },
364
363
  ];
365
364
  }
366
365
  exports.makeResponse = makeResponse;
package/lib/handler.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  */
6
6
  import { validate as graphqlValidate, execute as graphqlExecute, parse as graphqlParse, getOperationAST as graphqlGetOperationAST, GraphQLError, } from 'graphql';
7
- import { areGraphQLErrors, isAsyncIterable, isExecutionResult, isObject, } from './utils.mjs';
7
+ import { areGraphQLErrors, isAsyncIterable, isExecutionResult, isGraphQLError, isObject, } from './utils.mjs';
8
8
  /**
9
9
  * Checks whether the passed value is the `graphql-http` server agnostic response.
10
10
  *
@@ -320,41 +320,40 @@ export function getAcceptableMediaType(acceptHeader) {
320
320
  * @category Server
321
321
  */
322
322
  export function makeResponse(resultOrErrors, acceptedMediaType) {
323
- if (isExecutionResult(resultOrErrors)) {
323
+ const errors = isGraphQLError(resultOrErrors)
324
+ ? [resultOrErrors]
325
+ : areGraphQLErrors(resultOrErrors)
326
+ ? resultOrErrors
327
+ : null;
328
+ if (errors) {
324
329
  return [
325
- JSON.stringify(resultOrErrors),
326
- {
327
- status: 200,
328
- statusText: 'OK',
329
- headers: {
330
+ JSON.stringify({ errors }),
331
+ Object.assign(Object.assign({}, (acceptedMediaType === 'application/json'
332
+ ? {
333
+ status: 200,
334
+ statusText: 'OK',
335
+ }
336
+ : {
337
+ status: 400,
338
+ statusText: 'Bad Request',
339
+ })), { headers: {
330
340
  'content-type': acceptedMediaType === 'application/json'
331
341
  ? 'application/json; charset=utf-8'
332
342
  : 'application/graphql-response+json; charset=utf-8',
333
- },
334
- },
343
+ } }),
335
344
  ];
336
345
  }
337
346
  return [
338
- JSON.stringify({
339
- errors: Array.isArray(resultOrErrors)
340
- ? isObject(resultOrErrors)
341
- ? resultOrErrors
342
- : new GraphQLError(String(resultOrErrors))
343
- : [resultOrErrors],
344
- }),
345
- Object.assign(Object.assign({}, (acceptedMediaType === 'application/json'
346
- ? {
347
- status: 200,
348
- statusText: 'OK',
349
- }
350
- : {
351
- status: 400,
352
- statusText: 'Bad Request',
353
- })), { headers: {
347
+ JSON.stringify(resultOrErrors),
348
+ {
349
+ status: 200,
350
+ statusText: 'OK',
351
+ headers: {
354
352
  'content-type': acceptedMediaType === 'application/json'
355
353
  ? 'application/json; charset=utf-8'
356
354
  : 'application/graphql-response+json; charset=utf-8',
357
- } }),
355
+ },
356
+ },
358
357
  ];
359
358
  }
360
359
  function getHeader(req, key) {
@@ -0,0 +1,24 @@
1
+ /// <reference types="node" />
2
+ import type { Middleware } from 'koa';
3
+ import type { IncomingMessage } from 'http';
4
+ import { HandlerOptions, OperationContext } from '../handler';
5
+ /**
6
+ * Create a GraphQL over HTTP Protocol compliant request handler for
7
+ * the Koa framework.
8
+ *
9
+ * ```js
10
+ * import Koa from 'koa'; // yarn add koa
11
+ * import mount from 'koa-mount'; // yarn add koa-mount
12
+ * import { createHandler } from 'graphql-http/lib/use/koa';
13
+ * import { schema } from './my-graphql-schema';
14
+ *
15
+ * const app = new Koa();
16
+ * app.use(mount('/', createHandler({ schema })));
17
+ *
18
+ * app.listen({ port: 4000 });
19
+ * console.log('Listening to port 4000');
20
+ * ```
21
+ *
22
+ * @category Server/koa
23
+ */
24
+ export declare function createHandler<Context extends OperationContext = undefined>(options: HandlerOptions<IncomingMessage, undefined, Context>): Middleware;
@@ -0,0 +1,24 @@
1
+ /// <reference types="node" />
2
+ import type { Middleware } from 'koa';
3
+ import type { IncomingMessage } from 'http';
4
+ import { HandlerOptions, OperationContext } from '../handler';
5
+ /**
6
+ * Create a GraphQL over HTTP Protocol compliant request handler for
7
+ * the Koa framework.
8
+ *
9
+ * ```js
10
+ * import Koa from 'koa'; // yarn add koa
11
+ * import mount from 'koa-mount'; // yarn add koa-mount
12
+ * import { createHandler } from 'graphql-http/lib/use/koa';
13
+ * import { schema } from './my-graphql-schema';
14
+ *
15
+ * const app = new Koa();
16
+ * app.use(mount('/', createHandler({ schema })));
17
+ *
18
+ * app.listen({ port: 4000 });
19
+ * console.log('Listening to port 4000');
20
+ * ```
21
+ *
22
+ * @category Server/koa
23
+ */
24
+ export declare function createHandler<Context extends OperationContext = undefined>(options: HandlerOptions<IncomingMessage, undefined, Context>): Middleware;
package/lib/use/koa.js ADDED
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createHandler = void 0;
4
+ const handler_1 = require("../handler");
5
+ /**
6
+ * Create a GraphQL over HTTP Protocol compliant request handler for
7
+ * the Koa framework.
8
+ *
9
+ * ```js
10
+ * import Koa from 'koa'; // yarn add koa
11
+ * import mount from 'koa-mount'; // yarn add koa-mount
12
+ * import { createHandler } from 'graphql-http/lib/use/koa';
13
+ * import { schema } from './my-graphql-schema';
14
+ *
15
+ * const app = new Koa();
16
+ * app.use(mount('/', createHandler({ schema })));
17
+ *
18
+ * app.listen({ port: 4000 });
19
+ * console.log('Listening to port 4000');
20
+ * ```
21
+ *
22
+ * @category Server/koa
23
+ */
24
+ function createHandler(options) {
25
+ const isProd = process.env.NODE_ENV === 'production';
26
+ const handle = (0, handler_1.createHandler)(options);
27
+ return async function requestListener(ctx) {
28
+ try {
29
+ const [body, init] = await handle({
30
+ url: ctx.url,
31
+ method: ctx.method,
32
+ headers: ctx.headers,
33
+ body: () => {
34
+ if (ctx.body) {
35
+ // in case koa has a body parser
36
+ return ctx.body;
37
+ }
38
+ return new Promise((resolve) => {
39
+ let body = '';
40
+ ctx.req.on('data', (chunk) => (body += chunk));
41
+ ctx.req.on('end', () => resolve(body));
42
+ });
43
+ },
44
+ raw: ctx.req,
45
+ context: undefined,
46
+ });
47
+ ctx.body = body;
48
+ ctx.response.status = init.status;
49
+ ctx.response.message = init.statusText;
50
+ if (init.headers) {
51
+ for (const [name, value] of Object.entries(init.headers)) {
52
+ ctx.response.set(name, value);
53
+ }
54
+ }
55
+ }
56
+ catch (err) {
57
+ // The handler shouldnt throw errors.
58
+ // If you wish to handle them differently, consider implementing your own request handler.
59
+ console.error('Internal error occurred during request handling. ' +
60
+ 'Please check your implementation.', err);
61
+ ctx.response.status = 500;
62
+ if (!isProd) {
63
+ ctx.response.set('content-type', 'application/json; charset=utf-8');
64
+ ctx.body = {
65
+ errors: [
66
+ err instanceof Error
67
+ ? {
68
+ message: err.message,
69
+ stack: err.stack,
70
+ }
71
+ : err,
72
+ ],
73
+ };
74
+ }
75
+ }
76
+ };
77
+ }
78
+ exports.createHandler = createHandler;