@avantstay/graphql-ts-client 11.0.0-beta.1 → 11.0.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@@";
@@ -603,7 +618,8 @@ function jsonToGraphQLQuery(param) {
603
618
  var alias = jsonQuery.__alias;
604
619
  var newJsonQuery = cloneDeep((0, import_omit.default)(jsonQuery, [
605
620
  "__alias",
606
- "__headers"
621
+ "__headers",
622
+ "__url"
607
623
  ]));
608
624
  extractVariables({
609
625
  jsonQuery: _defineProperty({}, queryName, newJsonQuery),
@@ -723,11 +739,13 @@ var getApiEndpointCreator = function(apiConfig) {
723
739
  return function(kind, queryName) {
724
740
  var rawEndpoint = function() {
725
741
  var _ref = _asyncToGenerator(function(failureMode, jsonQuery) {
726
- var _jsonQuery___alias, alias, _jsonQuery___retry, shouldRetry, _jsonQuery___headers, requestHeaders, _jsonToGraphQLQuery, query, variables, start, logOptions, responseListenerData, _ref, data, errors, warnings, headers, status, response, error;
742
+ var clientConfig, _jsonQuery___alias, alias, _jsonQuery___url, url, _jsonQuery___retry, shouldRetry, _jsonQuery___headers, requestHeaders, _jsonToGraphQLQuery, query, variables, start, logOptions, responseListenerData, _ref, data, errors, warnings, headers, status, response, error;
727
743
  return __generator(this, function(_state) {
728
744
  switch(_state.label){
729
745
  case 0:
746
+ clientConfig = apiConfig.getClient();
730
747
  alias = (_jsonQuery___alias = jsonQuery === null || jsonQuery === void 0 ? void 0 : jsonQuery.__alias) !== null && _jsonQuery___alias !== void 0 ? _jsonQuery___alias : queryName;
748
+ url = (_jsonQuery___url = jsonQuery === null || jsonQuery === void 0 ? void 0 : jsonQuery.__url) !== null && _jsonQuery___url !== void 0 ? _jsonQuery___url : clientConfig.url;
731
749
  shouldRetry = (_jsonQuery___retry = jsonQuery === null || jsonQuery === void 0 ? void 0 : jsonQuery.__retry) !== null && _jsonQuery___retry !== void 0 ? _jsonQuery___retry : true;
732
750
  requestHeaders = (_jsonQuery___headers = jsonQuery === null || jsonQuery === void 0 ? void 0 : jsonQuery.__headers) !== null && _jsonQuery___headers !== void 0 ? _jsonQuery___headers : {};
733
751
  _jsonToGraphQLQuery = jsonToGraphQLQuery({
@@ -764,7 +782,9 @@ var getApiEndpointCreator = function(apiConfig) {
764
782
  shouldRetry: shouldRetry,
765
783
  failureMode: failureMode,
766
784
  queryName: alias,
767
- client: apiConfig.getClient(),
785
+ client: _objectSpreadProps(_objectSpread({}, clientConfig), {
786
+ url: url
787
+ }),
768
788
  requestHeaders: requestHeaders,
769
789
  query: query,
770
790
  variables: variables,
package/dist/index.js CHANGED
@@ -477,7 +477,7 @@ var import_case = __toESM(require("case"));
477
477
  var esbuild = __toESM(require("esbuild"));
478
478
  var fs = __toESM(require("fs"));
479
479
  var import_graphql = require("graphql");
480
- var import_lodash = require("lodash");
480
+ var import_kebabCase = __toESM(require("lodash/kebabCase"));
481
481
  var import_orderBy = __toESM(require("lodash/orderBy"));
482
482
  var import_set = __toESM(require("lodash/set"));
483
483
  var import_md5 = __toESM(require("md5"));
@@ -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: "11.0.0-beta.1",
490
+ version: "11.0.1",
491
491
  description: "GraphQL Typescript Client Generator",
492
492
  author: "Wellington Guimaraes",
493
493
  license: "MIT",
@@ -639,7 +639,7 @@ function gqlFieldToTypescript(field, param) {
639
639
  selection: false
640
640
  });
641
641
  });
642
- fieldTypeDefinition = "{ __headers?: {[key: string]: string}; __retry?: boolean; __alias?: string; __args".concat(fieldsOnArgs.every(function(arg) {
642
+ fieldTypeDefinition = "{ __headers?: {[key: string]: string}; __retry?: boolean; __alias?: string; __url?: string; __args".concat(fieldsOnArgs.every(function(arg) {
643
643
  return arg.isOptional;
644
644
  }) ? "?" : "", ": { ").concat(fieldsOnArgs.map(function(arg) {
645
645
  return arg.code;
@@ -679,7 +679,7 @@ function gqlEndpointToCode(kind, endpoint, codeOutputType) {
679
679
  selection: true
680
680
  });
681
681
  var argsType = endpoint.args && endpoint.args.length ? getArgsType(endpoint) : null;
682
- var inputType = "{\n __headers?: {[key: string]: string};\n __retry?: boolean;\n __alias?: string;\n ".concat(argsType ? "__args".concat(argsType.optional ? "?" : "", ": ").concat(argsType.alias) : "", "\n }").concat(selectionType ? " & ".concat(selectionType) : "");
682
+ var inputType = "{\n __headers?: {[key: string]: string};\n __retry?: boolean;\n __alias?: string;\n __url?: string;\n ".concat(argsType ? "__args".concat(argsType.optional ? "?" : "", ": ").concat(argsType.alias) : "", "\n }").concat(selectionType ? " & ".concat(selectionType) : "");
683
683
  var outputType = gqlTypeToTypescript(endpoint.type, {
684
684
  required: true
685
685
  });
@@ -866,7 +866,7 @@ function _fetchIntrospection() {
866
866
  switch(_state.label){
867
867
  case 0:
868
868
  endpoint = param.endpoint, headers = param.headers;
869
- introspectionCacheFileName = "gql-ts-client__introspection__".concat((0, import_lodash.kebabCase)(endpoint), ".json");
869
+ introspectionCacheFileName = "gql-ts-client__introspection__".concat((0, import_kebabCase.default)(endpoint), ".json");
870
870
  introspectionCacheFilePath = import_path.default.resolve(tempDir, introspectionCacheFileName);
871
871
  loadedFromCache = false;
872
872
  return [
@@ -877,7 +877,7 @@ function _fetchIntrospection() {
877
877
  headers: _objectSpread({
878
878
  "Content-Type": "application/json"
879
879
  }, headers)
880
- }).catch(function() {
880
+ }).catch(function(e) {
881
881
  var errorMessage = "The GraphQL introspection request failed (".concat(endpoint, ")");
882
882
  if (fs.existsSync(introspectionCacheFilePath)) {
883
883
  var cachedSchema = JSON.parse(fs.readFileSync(introspectionCacheFilePath, {
@@ -889,6 +889,7 @@ function _fetchIntrospection() {
889
889
  data: cachedSchema
890
890
  };
891
891
  } else {
892
+ console.error(e);
892
893
  return Promise.reject(errorMessage);
893
894
  }
894
895
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avantstay/graphql-ts-client",
3
- "version": "11.0.0-beta.1",
3
+ "version": "11.0.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
@@ -119,6 +119,7 @@ export declare const client: {
119
119
  __headers?: { [key: string]: string }
120
120
  __retry?: boolean
121
121
  __alias?: string
122
+ __url?: string
122
123
  },
123
124
  string,
124
125
  AllEnums
@@ -30,15 +30,6 @@ export interface FailingQueryArgs {
30
30
 
31
31
  // Input/Output Types
32
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 Image {
38
- url?: string
39
- alt?: string
40
- }
41
-
42
33
  /**
43
34
  * @deprecated Avoid directly using this interface. Instead, create a type alias based on the query/mutation return type.
44
35
  */
@@ -48,7 +39,6 @@ export interface Book {
48
39
  author?: string
49
40
  type?: BookType
50
41
  dateCreated?: ISODate
51
- images?: Image[]
52
42
  }
53
43
 
54
44
  /**
@@ -83,17 +73,11 @@ export interface Query {
83
73
 
84
74
  // Selection Types
85
75
 
86
- export interface ImageSelection {
87
- url?: boolean
88
- alt?: boolean
89
- }
90
-
91
76
  export interface BookSelection {
92
77
  title?: boolean
93
78
  author?: boolean
94
79
  type?: boolean
95
80
  dateCreated?: boolean
96
- images?: ImageSelection
97
81
  }
98
82
 
99
83
  export interface BookSearchParamsAllOptionalSelection {
@@ -123,6 +107,7 @@ export declare const myApiClient: {
123
107
  __headers?: { [key: string]: string }
124
108
  __retry?: boolean
125
109
  __alias?: string
110
+ __url?: string
126
111
  } & BookSelection,
127
112
  Book[],
128
113
  AllEnums
@@ -132,6 +117,7 @@ export declare const myApiClient: {
132
117
  __headers?: { [key: string]: string }
133
118
  __retry?: boolean
134
119
  __alias?: string
120
+ __url?: string
135
121
  __args?: BooksWithOptionalParamsArgs
136
122
  } & BookSelection,
137
123
  Book[],
@@ -142,6 +128,7 @@ export declare const myApiClient: {
142
128
  __headers?: { [key: string]: string }
143
129
  __retry?: boolean
144
130
  __alias?: string
131
+ __url?: string
145
132
  __args: BooksWithRequiredParamsArgs
146
133
  } & BookSelection,
147
134
  Book[],
@@ -152,6 +139,7 @@ export declare const myApiClient: {
152
139
  __headers?: { [key: string]: string }
153
140
  __retry?: boolean
154
141
  __alias?: string
142
+ __url?: string
155
143
  __args: FailingQueryArgs
156
144
  },
157
145
  string,
@@ -39,7 +39,6 @@ const typesTree = {
39
39
  Query: {
40
40
  get booksWithOptionalParams() {
41
41
  return {
42
- __fields: typesTree.Book,
43
42
  __args: {
44
43
  params: "BookSearchParamsAllOptional!"
45
44
  }
@@ -47,7 +46,6 @@ const typesTree = {
47
46
  },
48
47
  get booksWithRequiredParams() {
49
48
  return {
50
- __fields: typesTree.Book,
51
49
  __args: {
52
50
  params: "BookSearchParamsSomeRequired!"
53
51
  }
@@ -60,14 +58,7 @@ const typesTree = {
60
58
  }
61
59
  };
62
60
  },
63
- get booksWithoutParams() {
64
- return {
65
- __fields: typesTree.Book
66
- };
67
- }
68
- },
69
- Book: {
70
- images: {}
61
+ booksWithoutParams: {}
71
62
  }
72
63
  };
73
64
  let verbose = false;
package/src/endpoint.ts CHANGED
@@ -32,7 +32,10 @@ export const getApiEndpointCreator =
32
32
  headers: any
33
33
  status: any
34
34
  }> => {
35
+ const clientConfig = apiConfig.getClient()
36
+
35
37
  const alias = (jsonQuery as any)?.__alias ?? queryName
38
+ const url = (jsonQuery as any)?.__url ?? clientConfig.url
36
39
  const shouldRetry = (jsonQuery as any)?.__retry ?? true
37
40
  const requestHeaders = (jsonQuery as any)?.__headers ?? {}
38
41
  const { query, variables } = jsonToGraphQLQuery({ kind, queryName, jsonQuery, typesTree: apiConfig.typesTree })
@@ -58,7 +61,7 @@ export const getApiEndpointCreator =
58
61
  shouldRetry,
59
62
  failureMode,
60
63
  queryName: alias,
61
- client: apiConfig.getClient(),
64
+ client: {...clientConfig, url },
62
65
  requestHeaders,
63
66
  query,
64
67
  variables,
@@ -17,7 +17,7 @@ import {
17
17
  IntrospectionType,
18
18
  Source,
19
19
  } from 'graphql'
20
- import { kebabCase } from 'lodash'
20
+ import kebabCase from 'lodash/kebabCase'
21
21
  import orderBy from 'lodash/orderBy'
22
22
  import set from 'lodash/set'
23
23
  import md5 from 'md5'
@@ -108,7 +108,7 @@ function gqlFieldToTypescript(
108
108
  })
109
109
  )
110
110
 
111
- fieldTypeDefinition = `{ __headers?: {[key: string]: string}; __retry?: boolean; __alias?: string; __args${
111
+ fieldTypeDefinition = `{ __headers?: {[key: string]: string}; __retry?: boolean; __alias?: string; __url?: string; __args${
112
112
  fieldsOnArgs.every(arg => arg.isOptional) ? '?' : ''
113
113
  }: { ${fieldsOnArgs.map(arg => arg.code).join(', ')} }}${fieldTypeDefinition ? ` & ${fieldTypeDefinition}` : ''}`
114
114
  }
@@ -148,6 +148,7 @@ function gqlEndpointToCode(kind: 'mutation' | 'query', endpoint: IntrospectionFi
148
148
  __headers?: {[key: string]: string};
149
149
  __retry?: boolean;
150
150
  __alias?: string;
151
+ __url?: string;
151
152
  ${argsType ? `__args${argsType.optional ? '?' : ''}: ${argsType.alias}` : ''}
152
153
  }${selectionType ? ` & ${selectionType}` : ''}`
153
154
 
@@ -499,7 +500,7 @@ async function fetchIntrospection({ endpoint, headers }: FetchIntrospectionOptio
499
500
  },
500
501
  }
501
502
  )
502
- .catch(() => {
503
+ .catch((e) => {
503
504
  const errorMessage = `The GraphQL introspection request failed (${endpoint})`
504
505
  if (fs.existsSync(introspectionCacheFilePath)) {
505
506
  const cachedSchema = JSON.parse(fs.readFileSync(introspectionCacheFilePath, { encoding: 'utf8' }))
@@ -507,6 +508,7 @@ async function fetchIntrospection({ endpoint, headers }: FetchIntrospectionOptio
507
508
  console.warn(`Successfully restored from local cache.`)
508
509
  return { data: cachedSchema }
509
510
  } else {
511
+ console.error(e)
510
512
  return Promise.reject(errorMessage)
511
513
  }
512
514
  })
@@ -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
+ })
@@ -23,7 +23,7 @@ export function jsonToGraphQLQuery({
23
23
  }) {
24
24
  const variablesData = {} as ExtractedVariables
25
25
  const alias = jsonQuery.__alias
26
- const newJsonQuery = cloneDeep(omit(jsonQuery, ['__alias', '__headers']))
26
+ const newJsonQuery = cloneDeep(omit(jsonQuery, ['__alias', '__headers', '__url']))
27
27
 
28
28
  extractVariables({
29
29
  jsonQuery: { [queryName]: newJsonQuery },
@@ -1,69 +0,0 @@
1
- type Maybe<T> = null | undefined | T;
2
- type Defined<T> = Exclude<T, undefined>;
3
- type ResponseData = {
4
- data: any;
5
- warnings: any;
6
- headers: any;
7
- status?: number;
8
- errors: {
9
- message: string;
10
- }[];
11
- };
12
- type ResponseListenerInfo = {
13
- queryName: string;
14
- query: string;
15
- variables: any;
16
- response: ResponseData;
17
- };
18
- type IResponseListener = (info: ResponseListenerInfo) => void | Promise<void>;
19
- type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
20
- type Projection<Selection, Base, E = never> = Base extends Array<any> ? ArrayElement<Base> extends Date | string | number | boolean | null | undefined | E ? ArrayElement<Base>[] : Projection<Defined<Selection>, ArrayElement<Base>, E>[] : Base extends Date | string | number | boolean | null | E ? Selection extends undefined ? Base | undefined : Base : {
21
- [k in keyof Selection & keyof Base]: Selection[k] extends boolean ? Base[k] : Base[k] extends Array<infer A> ? Projection<Defined<Selection[k]>, A, E>[] : Projection<Defined<Selection[k]>, Base[k], E>;
22
- };
23
- type Unpacked<T> = T extends (infer U)[] ? U : T extends (...args: any[]) => infer U ? U : T extends Promise<infer U> ? U : T;
24
- type Replacement<M extends [any, any], T> = M extends any ? ([T] extends [M[0]] ? M[1] : never) : never;
25
- type DeepReplace<T, Ignore, M extends [any, any]> = {
26
- [P in keyof T]: T[P] extends M[0] ? T[P] extends Ignore ? T[P] extends object ? DeepReplace<T[P], Ignore, M> : T[P] : Replacement<M, T[P]> : T[P] extends object ? DeepReplace<T[P], Ignore, M> : T[P];
27
- };
28
- type RawEndpoint<I, O, E> = <S extends I>(jsonQuery?: S) => Promise<{
29
- data: Projection<S, O, E>;
30
- errors: any[];
31
- warnings: any[];
32
- headers: any;
33
- status: any;
34
- }>;
35
- type JsonOutput<O, ToBeIgnored> = DeepReplace<O, ToBeIgnored, [string | Date, string]>;
36
- type Endpoint<I, O, E> = (<S extends I>(jsonQuery?: S) => Promise<Projection<S, JsonOutput<O, E>, E>>) & {
37
- memo: <S extends I>(jsonQuery?: S) => Promise<Projection<S, JsonOutput<O, E>, E>>;
38
- memoRaw: RawEndpoint<I, JsonOutput<O, E>, E>;
39
- raw: RawEndpoint<I, JsonOutput<O, E>, E>;
40
- };
41
- type ClientConfig = {
42
- url: string;
43
- headers: {
44
- [key: string]: string;
45
- };
46
- retryConfig: {
47
- max: number;
48
- waitBeforeRetry?: number;
49
- before: IResponseListener;
50
- };
51
- };
52
- type LogInfo = {
53
- query: string;
54
- variables: any;
55
- formatGraphQL: any;
56
- kind: string;
57
- queryName: string;
58
- response?: any;
59
- error?: Error;
60
- duration: number;
61
- };
62
- declare class GraphQLClientError extends Error {
63
- responseData: ResponseData;
64
- constructor(responseData: ResponseData);
65
- get message(): string;
66
- get response(): ResponseData;
67
- }
68
-
69
- export { ClientConfig as C, Defined as D, Endpoint as E, GraphQLClientError as G, IResponseListener as I, JsonOutput as J, LogInfo as L, Maybe as M, Projection as P, ResponseData as R, Unpacked as U, ResponseListenerInfo as a, Replacement as b, DeepReplace as c, RawEndpoint as d };