@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 +24 -4
- package/dist/index.js +7 -6
- package/package.json +1 -1
- package/src/__snapshots__/generateTypescriptClient.test.ts.snap +3 -2
- package/src/__testClient.d.ts +4 -16
- package/src/__testClient.js +1 -10
- package/src/endpoint.ts +4 -1
- package/src/generateTypescriptClient.ts +5 -3
- package/src/graphqlRequest.ts +29 -1
- package/src/graphqlResponseShape.test.ts +246 -0
- package/src/jsonToGraphQLQuery.ts +1 -1
- package/dist/types-4ecbabdf.d.ts +0 -69
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
|
|
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:
|
|
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
|
|
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.
|
|
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,
|
|
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
|
@@ -29,7 +29,7 @@ __export(exports, {
|
|
|
29
29
|
client: () => client,
|
|
30
30
|
default: () => stdin_default
|
|
31
31
|
});
|
|
32
|
-
var import_endpoint = __toModule(require("
|
|
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 "
|
|
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
|
package/src/__testClient.d.ts
CHANGED
|
@@ -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,
|
package/src/__testClient.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
})
|
package/src/graphqlRequest.ts
CHANGED
|
@@ -55,7 +55,9 @@ export async function graphqlRequest({
|
|
|
55
55
|
}
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
-
|
|
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 },
|
package/dist/types-4ecbabdf.d.ts
DELETED
|
@@ -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 };
|