@avantstay/graphql-ts-client 10.7.0 → 10.7.1

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/dist/endpoint.js CHANGED
@@ -508,7 +508,9 @@ function _graphqlRequest() {
508
508
  ];
509
509
  case 2:
510
510
  _ref = _state.sent(), tmp = _ref.data, responseData = tmp === void 0 ? {} : tmp, headers = _ref.headers, status = _ref.status;
511
- data = responseData.data, errors = responseData.errors, warnings = responseData.warnings;
511
+ data = responseData.data;
512
+ errors = liftErrorExtensions(responseData.errors);
513
+ warnings = readWarnings(responseData);
512
514
  if (status >= 400 && !(errors === null || errors === void 0 ? void 0 : errors.length)) {
513
515
  errors = [
514
516
  {
@@ -590,6 +592,19 @@ function _graphqlRequest() {
590
592
  });
591
593
  return _graphqlRequest.apply(this, arguments);
592
594
  }
595
+ function readWarnings(responseData) {
596
+ var _responseData_extensions;
597
+ var _responseData_extensions_warnings;
598
+ return (_responseData_extensions_warnings = (_responseData_extensions = responseData.extensions) === null || _responseData_extensions === void 0 ? void 0 : _responseData_extensions.warnings) !== null && _responseData_extensions_warnings !== void 0 ? _responseData_extensions_warnings : responseData.warnings;
599
+ }
600
+ function liftErrorExtensions(errors) {
601
+ if (!Array.isArray(errors)) return errors;
602
+ return errors.map(function(error) {
603
+ return (error === null || error === void 0 ? void 0 : error.extensions) ? Object.assign({}, error.extensions.details, error.extensions.code != null ? {
604
+ code: error.extensions.code
605
+ } : {}, error) : error;
606
+ });
607
+ }
593
608
  // src/jsonToGraphQLQuery.ts
594
609
  var import_omit = __toESM(require("lodash/omit"));
595
610
  var VAR_PREFIX = "@@VAR@@";
package/dist/index.js CHANGED
@@ -487,7 +487,7 @@ var prettier = __toESM(require("prettier"));
487
487
  // package.json
488
488
  var package_default = {
489
489
  name: "@avantstay/graphql-ts-client",
490
- version: "10.7.0",
490
+ version: "10.7.1",
491
491
  description: "GraphQL Typescript Client Generator",
492
492
  author: "Wellington Guimaraes",
493
493
  license: "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avantstay/graphql-ts-client",
3
- "version": "10.7.0",
3
+ "version": "10.7.1",
4
4
  "description": "GraphQL Typescript Client Generator",
5
5
  "author": "Wellington Guimaraes",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@ __export(exports, {
29
29
  client: () => client,
30
30
  default: () => stdin_default
31
31
  });
32
- var import_endpoint = __toModule(require("../dist/endpoint"));
32
+ var import_endpoint = __toModule(require("./endpoint"));
33
33
  const formatGraphQL = (query) => query;
34
34
  const typesTree = {};
35
35
  let verbose = false;
@@ -79,7 +79,7 @@ var stdin_default = client;
79
79
  ",
80
80
  "typings": "// noinspection TypeScriptUnresolvedVariable, ES6UnusedImports, JSUnusedLocalSymbols, TypeScriptCheckImport
81
81
  import { DeepRequired } from "ts-essentials"
82
- import { Maybe, IResponseListener, Endpoint } from "../dist"
82
+ import { Maybe, IResponseListener, Endpoint } from "."
83
83
 
84
84
  // Scalars
85
85
  export type IDate = string | Date
@@ -0,0 +1,148 @@
1
+ // noinspection TypeScriptUnresolvedVariable, ES6UnusedImports, JSUnusedLocalSymbols, TypeScriptCheckImport
2
+ import { DeepRequired } from "ts-essentials"
3
+ import { Maybe, IResponseListener, Endpoint } from "."
4
+
5
+ // Scalars
6
+ export type IDate = string | Date
7
+ export declare type ISODate = IDate
8
+
9
+ // Enums
10
+
11
+ export declare enum BookType {
12
+ dolor = "DOLOR",
13
+ ipsum = "IPSUM",
14
+ sit = "SIT",
15
+ }
16
+
17
+ type AllEnums = BookType
18
+
19
+ // Args
20
+ export interface BooksWithoutParamsArgs {}
21
+ export interface BooksWithOptionalParamsArgs {
22
+ params?: BookSearchParamsAllOptional
23
+ }
24
+ export interface BooksWithRequiredParamsArgs {
25
+ params: BookSearchParamsSomeRequired
26
+ }
27
+ export interface FailingQueryArgs {
28
+ id: string
29
+ }
30
+
31
+ // Input/Output Types
32
+
33
+ /**
34
+ * @deprecated Avoid directly using this interface. Instead, create a type alias based on the query/mutation return type.
35
+ */
36
+
37
+ export interface Book {
38
+ title?: string
39
+ author?: string
40
+ type?: BookType
41
+ dateCreated?: ISODate
42
+ }
43
+
44
+ /**
45
+ * @deprecated Avoid directly using this interface. Instead, create a type alias based on the query/mutation return type.
46
+ */
47
+
48
+ export interface BookSearchParamsAllOptional {
49
+ title?: string
50
+ author?: string
51
+ createdAfter?: ISODate
52
+ }
53
+
54
+ /**
55
+ * @deprecated Avoid directly using this interface. Instead, create a type alias based on the query/mutation return type.
56
+ */
57
+
58
+ export interface BookSearchParamsSomeRequired {
59
+ title: string
60
+ author?: string
61
+ }
62
+
63
+ /**
64
+ * @deprecated Avoid directly using this interface. Instead, create a type alias based on the query/mutation return type.
65
+ */
66
+
67
+ export interface Query {
68
+ booksWithoutParams?: Book[]
69
+ booksWithOptionalParams?: Book[]
70
+ booksWithRequiredParams?: Book[]
71
+ failingQuery?: string
72
+ }
73
+
74
+ // Selection Types
75
+
76
+ export interface BookSelection {
77
+ title?: boolean
78
+ author?: boolean
79
+ type?: boolean
80
+ dateCreated?: boolean
81
+ }
82
+
83
+ export interface BookSearchParamsAllOptionalSelection {
84
+ title?: boolean
85
+ author?: boolean
86
+ createdAfter?: boolean
87
+ }
88
+
89
+ export interface BookSearchParamsSomeRequiredSelection {
90
+ title?: boolean
91
+ author?: boolean
92
+ }
93
+
94
+ export declare const myApiClient: {
95
+ addResponseListener: (listener: IResponseListener) => void
96
+ setHeader: (key: string, value: string) => void
97
+ setHeaders: (newHeaders: { [k: string]: string }) => void
98
+ setUrl: (url: string) => void
99
+ setRetryConfig: (options: {
100
+ max: number
101
+ waitBeforeRetry?: number
102
+ before?: IResponseListener
103
+ }) => void
104
+ queries: {
105
+ booksWithoutParams: Endpoint<
106
+ {
107
+ __headers?: { [key: string]: string }
108
+ __retry?: boolean
109
+ __alias?: string
110
+ } & BookSelection,
111
+ DeepRequired<Book[]>,
112
+ AllEnums
113
+ >
114
+ booksWithOptionalParams: Endpoint<
115
+ {
116
+ __headers?: { [key: string]: string }
117
+ __retry?: boolean
118
+ __alias?: string
119
+ __args?: BooksWithOptionalParamsArgs
120
+ } & BookSelection,
121
+ DeepRequired<Book[]>,
122
+ AllEnums
123
+ >
124
+ booksWithRequiredParams: Endpoint<
125
+ {
126
+ __headers?: { [key: string]: string }
127
+ __retry?: boolean
128
+ __alias?: string
129
+ __args: BooksWithRequiredParamsArgs
130
+ } & BookSelection,
131
+ DeepRequired<Book[]>,
132
+ AllEnums
133
+ >
134
+ failingQuery: Endpoint<
135
+ {
136
+ __headers?: { [key: string]: string }
137
+ __retry?: boolean
138
+ __alias?: string
139
+ __args: FailingQueryArgs
140
+ },
141
+ string,
142
+ AllEnums
143
+ >
144
+ }
145
+ mutations: {}
146
+ }
147
+
148
+ export default myApiClient
@@ -0,0 +1,110 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
8
+ var __export = (target, all) => {
9
+ __markAsModule(target);
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __reExport = (target, module2, desc) => {
14
+ if (module2 && typeof module2 === "object" || typeof module2 === "function") {
15
+ for (let key of __getOwnPropNames(module2))
16
+ if (!__hasOwnProp.call(target, key) && key !== "default")
17
+ __defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
18
+ }
19
+ return target;
20
+ };
21
+ var __toModule = (module2) => {
22
+ return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2);
23
+ };
24
+ __export(exports, {
25
+ BookType: () => BookType,
26
+ default: () => stdin_default,
27
+ myApiClient: () => myApiClient
28
+ });
29
+ var import_endpoint = __toModule(require("./endpoint"));
30
+ var import_standalone = __toModule(require("prettier/standalone"));
31
+ var import_parser_graphql = __toModule(require("prettier/parser-graphql"));
32
+ const formatGraphQL = (query) => (0, import_standalone.format)(query, { parser: "graphql", plugins: [import_parser_graphql.default] });
33
+ const BookType = {
34
+ dolor: "DOLOR",
35
+ ipsum: "IPSUM",
36
+ sit: "SIT"
37
+ };
38
+ const typesTree = {
39
+ Query: {
40
+ get booksWithOptionalParams() {
41
+ return {
42
+ __args: {
43
+ params: "BookSearchParamsAllOptional!"
44
+ }
45
+ };
46
+ },
47
+ get booksWithRequiredParams() {
48
+ return {
49
+ __args: {
50
+ params: "BookSearchParamsSomeRequired!"
51
+ }
52
+ };
53
+ },
54
+ get failingQuery() {
55
+ return {
56
+ __args: {
57
+ id: "String!"
58
+ }
59
+ };
60
+ },
61
+ booksWithoutParams: {}
62
+ }
63
+ };
64
+ let verbose = false;
65
+ let headers = {};
66
+ let url = "http://localhost:4123/graphql";
67
+ let retryConfig = {
68
+ max: 0,
69
+ before: void 0,
70
+ waitBeforeRetry: 0
71
+ };
72
+ let responseListeners = [];
73
+ let errorsParser = void 0;
74
+ let apiEndpoint = (0, import_endpoint.getApiEndpointCreator)({
75
+ getClient: () => ({ url, headers, retryConfig }),
76
+ responseListeners,
77
+ maxAge: 3e4,
78
+ verbose,
79
+ typesTree,
80
+ formatGraphQL,
81
+ errorsParser
82
+ });
83
+ const myApiClient = {
84
+ addResponseListener: (listener) => responseListeners.push(listener),
85
+ setHeader: (key, value) => {
86
+ headers[key] = value;
87
+ },
88
+ setHeaders: (newHeaders) => {
89
+ headers = newHeaders;
90
+ },
91
+ setRetryConfig: (options) => {
92
+ if (!Number.isInteger(options.max) || options.max < 0) {
93
+ throw new Error("retryOptions.max should be a non-negative integer");
94
+ }
95
+ retryConfig = {
96
+ max: options.max,
97
+ waitBeforeRetry: options.waitBeforeRetry,
98
+ before: options.before
99
+ };
100
+ },
101
+ setUrl: (_url) => url = _url,
102
+ queries: {
103
+ booksWithoutParams: apiEndpoint("query", "booksWithoutParams"),
104
+ booksWithOptionalParams: apiEndpoint("query", "booksWithOptionalParams"),
105
+ booksWithRequiredParams: apiEndpoint("query", "booksWithRequiredParams"),
106
+ failingQuery: apiEndpoint("query", "failingQuery")
107
+ },
108
+ mutations: {}
109
+ };
110
+ var stdin_default = myApiClient;
@@ -55,7 +55,9 @@ export async function graphqlRequest({
55
55
  }
56
56
  )
57
57
 
58
- let { data, errors, warnings } = responseData
58
+ const { data } = responseData
59
+ let errors = liftErrorExtensions(responseData.errors)
60
+ const warnings = readWarnings(responseData)
59
61
 
60
62
  if (status >= 400 && !errors?.length) {
61
63
  errors = [{ message: `Request "${queryName}" failed with status ${status}` }]
@@ -93,3 +95,29 @@ export async function graphqlRequest({
93
95
 
94
96
  return lastResponse!
95
97
  }
98
+
99
+ // The Arriere monolith (arriere@16aefc85) moved request warnings from the top level to
100
+ // `extensions.warnings`, and each error's `code` and public fields to `errors[].extensions.code`
101
+ // and `errors[].extensions.details`. These helpers restore the legacy top-level shape so existing
102
+ // readers of `warnings` and `errors[].code` keep working against both old and new servers.
103
+
104
+ // Extension warnings win whenever they are present, an empty array included.
105
+ function readWarnings(responseData: any) {
106
+ return responseData.extensions?.warnings ?? responseData.warnings
107
+ }
108
+
109
+ // Fields already on the error win over lifted ones, and `extensions` stays untouched.
110
+ function liftErrorExtensions(errors: any) {
111
+ if (!Array.isArray(errors)) return errors
112
+
113
+ return errors.map((error: any) =>
114
+ error?.extensions
115
+ ? Object.assign(
116
+ {},
117
+ error.extensions.details,
118
+ error.extensions.code != null ? { code: error.extensions.code } : {},
119
+ error
120
+ )
121
+ : error
122
+ )
123
+ }
@@ -0,0 +1,246 @@
1
+ import axios from 'axios'
2
+ import { getApiEndpointCreator } from './endpoint'
3
+ import { graphqlRequest } from './graphqlRequest'
4
+ import { GraphQLClientError } from './types'
5
+
6
+ // The Arriere monolith moved `warnings` to `extensions.warnings` and each error's `code` and
7
+ // public fields to `errors[].extensions`. graphqlRequest restores the legacy top-level shape
8
+ // before anything else (errorsParser, retry hooks, response listeners, callers) sees it.
9
+
10
+ const legacyWarnings = [{ code: 'LEGACY', message: 'Legacy warning' }]
11
+ const extensionWarnings = [{ code: 'COUPON_DISABLED', message: 'Coupon is disabled' }]
12
+
13
+ const client = (retryConfig: { max: number; before: (info: any) => any }) => ({
14
+ url: 'https://example.invalid/graphql',
15
+ headers: {},
16
+ retryConfig,
17
+ })
18
+
19
+ const requestOnce = (response: { status: number; data?: any }, options: Partial<Parameters<typeof graphqlRequest>[0]> = {}) =>
20
+ graphqlRequest({
21
+ shouldRetry: false,
22
+ failureMode: 'silent',
23
+ queryName: 'booking',
24
+ query: 'query booking { booking }',
25
+ variables: {},
26
+ client: client({ max: 0, before: () => undefined }),
27
+ axios: { post: jest.fn().mockResolvedValue({ headers: {}, ...response }) } as any,
28
+ ...options,
29
+ })
30
+
31
+ describe('warnings', () => {
32
+ it.each([
33
+ { name: 'legacy top-level warnings are kept', body: { warnings: legacyWarnings }, expected: legacyWarnings },
34
+ { name: 'extension warnings are read', body: { extensions: { warnings: extensionWarnings } }, expected: extensionWarnings },
35
+ {
36
+ name: 'extension warnings win over legacy ones',
37
+ body: { warnings: legacyWarnings, extensions: { warnings: extensionWarnings } },
38
+ expected: extensionWarnings,
39
+ },
40
+ {
41
+ name: 'an empty extension warnings array wins',
42
+ body: { warnings: legacyWarnings, extensions: { warnings: [] } },
43
+ expected: [],
44
+ },
45
+ { name: 'null extensions fall back to legacy', body: { warnings: legacyWarnings, extensions: null }, expected: legacyWarnings },
46
+ {
47
+ name: 'null extension warnings fall back to legacy',
48
+ body: { warnings: legacyWarnings, extensions: { warnings: null } },
49
+ expected: legacyWarnings,
50
+ },
51
+ {
52
+ name: 'unrelated extensions fall back to legacy',
53
+ body: { warnings: legacyWarnings, extensions: { tracing: {} } },
54
+ expected: legacyWarnings,
55
+ },
56
+ { name: 'absent warnings stay absent', body: {}, expected: undefined },
57
+ ])('$name', async ({ body, expected }) => {
58
+ const response = await requestOnce({ status: 200, data: { data: { booking: null }, ...body } })
59
+
60
+ expect(response.warnings).toEqual(expected)
61
+ expect(response.data).toEqual({ booking: null })
62
+ })
63
+ })
64
+
65
+ describe('errors', () => {
66
+ it('lifts extensions.code and extensions.details to the top level and keeps extensions', async () => {
67
+ const error = {
68
+ message: 'Calendar is blocked',
69
+ path: ['createBooking'],
70
+ extensions: { code: 'CALENDAR_BLOCKED', details: { homeId: 'home-1', nights: 3 } },
71
+ }
72
+
73
+ const response = await requestOnce({ status: 200, data: { data: null, errors: [error] } })
74
+
75
+ expect(response.errors).toEqual([
76
+ {
77
+ homeId: 'home-1',
78
+ nights: 3,
79
+ code: 'CALENDAR_BLOCKED',
80
+ message: 'Calendar is blocked',
81
+ path: ['createBooking'],
82
+ extensions: { code: 'CALENDAR_BLOCKED', details: { homeId: 'home-1', nights: 3 } },
83
+ },
84
+ ])
85
+ expect((response.errors[0] as any).extensions).toBe(error.extensions)
86
+ })
87
+
88
+ it('keeps fields already on the error over lifted ones', async () => {
89
+ const error = {
90
+ message: 'Top-level message',
91
+ code: 'LEGACY_CODE',
92
+ homeId: 'legacy-home',
93
+ extensions: { code: 'CART_EXPIRED', details: { message: 'Details message', homeId: 'details-home', cartId: 'cart-1' } },
94
+ }
95
+
96
+ const response = await requestOnce({ status: 200, data: { data: null, errors: [error] } })
97
+
98
+ expect(response.errors[0]).toEqual({ ...error, cartId: 'cart-1' })
99
+ })
100
+
101
+ it('lifts details without inventing a code and leaves errors without extensions alone', async () => {
102
+ const plain = { message: 'No extensions' }
103
+ const detailsOnly = { message: 'Details only', extensions: { details: { field: 'email' } } }
104
+
105
+ const response = await requestOnce({ status: 200, data: { data: null, errors: [plain, detailsOnly, null] } })
106
+
107
+ expect(response.errors[0]).toBe(plain)
108
+ expect(response.errors[1]).toEqual({ field: 'email', ...detailsOnly })
109
+ expect(response.errors[1]).not.toHaveProperty('code')
110
+ expect(response.errors[2]).toBeNull()
111
+ })
112
+
113
+ it('does not mutate the response body', async () => {
114
+ const error = { message: 'Cart expired', extensions: { code: 'CART_EXPIRED' } }
115
+
116
+ await requestOnce({ status: 200, data: { data: null, errors: [error] } })
117
+
118
+ expect(error).toEqual({ message: 'Cart expired', extensions: { code: 'CART_EXPIRED' } })
119
+ })
120
+
121
+ it('normalizes errors on a 422 that carries errors', async () => {
122
+ const response = await requestOnce({
123
+ status: 422,
124
+ data: { errors: [{ message: 'Cart expired', extensions: { code: 'CART_EXPIRED' } }] },
125
+ })
126
+
127
+ expect(response.status).toBe(422)
128
+ expect(response.errors).toEqual([{ code: 'CART_EXPIRED', message: 'Cart expired', extensions: { code: 'CART_EXPIRED' } }])
129
+ })
130
+
131
+ it('falls back to the synthetic message on a 500 without a body', async () => {
132
+ const response = await requestOnce({ status: 500 })
133
+
134
+ expect(response.errors).toEqual([{ message: 'Request "booking" failed with status 500' }])
135
+ expect(response.warnings).toBeUndefined()
136
+ })
137
+
138
+ it('throws a GraphQLClientError carrying the normalized errors in loud mode', async () => {
139
+ const error = await requestOnce(
140
+ { status: 200, data: { data: null, errors: [{ message: 'Calendar is blocked', extensions: { code: 'CALENDAR_BLOCKED' } }] } },
141
+ { failureMode: 'loud' }
142
+ ).catch(e => e)
143
+
144
+ expect(error).toBeInstanceOf(GraphQLClientError)
145
+ expect(error.response.errors[0].code).toBe('CALENDAR_BLOCKED')
146
+ })
147
+
148
+ it('passes the normalized errors to errorsParser', async () => {
149
+ const errorsParser = jest.fn((errors: any[]) => errors.map(e => e.code))
150
+
151
+ const response = await requestOnce(
152
+ { status: 200, data: { data: null, errors: [{ message: 'Cart expired', extensions: { code: 'CART_EXPIRED' } }] } },
153
+ { errorsParser }
154
+ )
155
+
156
+ expect(errorsParser).toHaveBeenCalledWith([
157
+ { code: 'CART_EXPIRED', message: 'Cart expired', extensions: { code: 'CART_EXPIRED' } },
158
+ ])
159
+ expect(response.errors).toEqual(['CART_EXPIRED'])
160
+ })
161
+ })
162
+
163
+ describe('retries', () => {
164
+ it('shows the normalized shape to the retry hook', async () => {
165
+ const before = jest.fn()
166
+ const post = jest
167
+ .fn()
168
+ .mockResolvedValueOnce({
169
+ status: 200,
170
+ headers: {},
171
+ data: {
172
+ data: null,
173
+ errors: [{ message: 'Cart expired', extensions: { code: 'CART_EXPIRED', details: { cartId: 'cart-1' } } }],
174
+ extensions: { warnings: extensionWarnings },
175
+ },
176
+ })
177
+ .mockResolvedValueOnce({ status: 200, headers: {}, data: { data: { booking: { id: 'booking-1' } } } })
178
+
179
+ const response = await graphqlRequest({
180
+ failureMode: 'silent',
181
+ axios: { post } as any,
182
+ queryName: 'booking',
183
+ query: 'query booking { booking }',
184
+ variables: {},
185
+ client: client({ max: 1, before }),
186
+ })
187
+
188
+ expect(post).toHaveBeenCalledTimes(2)
189
+ expect(before).toHaveBeenCalledTimes(1)
190
+ expect(before.mock.calls[0][0].response).toMatchObject({
191
+ errors: [{ code: 'CART_EXPIRED', cartId: 'cart-1', message: 'Cart expired' }],
192
+ warnings: extensionWarnings,
193
+ })
194
+ expect(response.errors).toBeUndefined()
195
+ expect(response.data).toEqual({ booking: { id: 'booking-1' } })
196
+ })
197
+ })
198
+
199
+ describe('response listeners', () => {
200
+ afterEach(() => jest.restoreAllMocks())
201
+
202
+ const createEndpoint = (listener: jest.Mock) =>
203
+ getApiEndpointCreator({
204
+ getClient: () => client({ max: 0, before: () => undefined }),
205
+ responseListeners: [listener],
206
+ typesTree: {},
207
+ maxAge: 0,
208
+ verbose: false,
209
+ formatGraphQL: (query: string) => query,
210
+ })('mutation', 'booking')
211
+
212
+ const mockServer = (body: any) => jest.spyOn(axios, 'post').mockResolvedValue({ status: 200, headers: {}, data: body })
213
+
214
+ const flushListeners = () => new Promise(resolve => setTimeout(resolve, 0))
215
+
216
+ it('sees normalized warnings and errors on raw requests', async () => {
217
+ const listener = jest.fn()
218
+ mockServer({
219
+ data: { booking: null },
220
+ errors: [{ message: 'Coupon rejected', extensions: { code: 'COUPON_INVALID' } }],
221
+ extensions: { warnings: extensionWarnings },
222
+ })
223
+
224
+ const response = await createEndpoint(listener).raw({ __alias: 'booking' })
225
+ await flushListeners()
226
+
227
+ const expected = {
228
+ warnings: extensionWarnings,
229
+ errors: [{ code: 'COUPON_INVALID', message: 'Coupon rejected', extensions: { code: 'COUPON_INVALID' } }],
230
+ }
231
+ expect(response).toMatchObject(expected)
232
+ expect(listener).toHaveBeenCalledWith(expect.objectContaining({ response: expect.objectContaining(expected) }))
233
+ })
234
+
235
+ it('sees normalized errors when a loud request throws', async () => {
236
+ const listener = jest.fn()
237
+ mockServer({ data: null, errors: [{ message: 'Calendar is blocked', extensions: { code: 'CALENDAR_BLOCKED' } }] })
238
+
239
+ const error = await createEndpoint(listener)({ __alias: 'booking' }).catch((e: any) => e)
240
+ await flushListeners()
241
+
242
+ expect(error).toBeInstanceOf(GraphQLClientError)
243
+ expect(error.response.errors[0].code).toBe('CALENDAR_BLOCKED')
244
+ expect(listener.mock.calls[0][0].response.errors[0].code).toBe('CALENDAR_BLOCKED')
245
+ })
246
+ })