@avantstay/graphql-ts-client 10.6.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 +43 -10
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/__snapshots__/generateTypescriptClient.test.ts.snap +2 -2
- package/src/__testClient.d.ts +1 -1
- package/src/__testClient.js +1 -1
- package/src/graphqlRequest.ts +29 -1
- package/src/graphqlResponseShape.test.ts +246 -0
- package/src/jsonToGraphQLQuery.test.ts +19 -3
- package/src/jsonToGraphQLQuery.ts +43 -12
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@@";
|
|
@@ -610,12 +625,22 @@ function jsonToGraphQLQuery(param) {
|
|
|
610
625
|
variables: variablesData,
|
|
611
626
|
parentType: kind === "query" ? typesTree.Query : typesTree.Mutation
|
|
612
627
|
});
|
|
613
|
-
var
|
|
628
|
+
var variableItems = Object.values(variablesData).reduce(function(variablesObj, variables2) {
|
|
629
|
+
variables2.forEach(function(variable, index) {
|
|
630
|
+
var name = variable.update(variables2.length > 1 ? index : void 0);
|
|
631
|
+
variablesObj[name] = {
|
|
632
|
+
type: variable.type,
|
|
633
|
+
value: variable.value
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
return variablesObj;
|
|
637
|
+
}, {});
|
|
638
|
+
var variablesQuery = Object.keys(variableItems).length ? "(".concat(entries(variableItems).map(function(param) {
|
|
614
639
|
var _param = _slicedToArray(param, 2), queryName2 = _param[0], type = _param[1].type;
|
|
615
640
|
return "$".concat(queryName2, ": ").concat(type);
|
|
616
641
|
}).join(", "), ")") : "";
|
|
617
642
|
var query = "".concat(kind, " ").concat(alias || queryName).concat(variablesQuery, " { ").concat(alias ? "".concat(alias, ":") : "").concat(queryName).concat(toGraphql(newJsonQuery), " }");
|
|
618
|
-
var variables = fromEntries(entries(
|
|
643
|
+
var variables = fromEntries(entries(variableItems).map(function(param) {
|
|
619
644
|
var _param = _slicedToArray(param, 2), k = _param[0], v = _param[1];
|
|
620
645
|
return [
|
|
621
646
|
k,
|
|
@@ -633,14 +658,22 @@ function extractVariables(param) {
|
|
|
633
658
|
if (jsonQuery.__args) {
|
|
634
659
|
Object.keys(jsonQuery.__args).forEach(function(k) {
|
|
635
660
|
if (typeof jsonQuery.__args[k] === "string" && jsonQuery.__args[k].startsWith(VAR_PREFIX)) return;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
value: jsonQuery.__args[k]
|
|
641
|
-
};
|
|
642
|
-
jsonQuery.__args[k] = "".concat(VAR_PREFIX, "$").concat(variableName);
|
|
661
|
+
if (jsonQuery.__args[k] === void 0) return;
|
|
662
|
+
var variableName = k;
|
|
663
|
+
if (!variables[variableName]) {
|
|
664
|
+
variables[variableName] = [];
|
|
643
665
|
}
|
|
666
|
+
variables[variableName].push({
|
|
667
|
+
name: variableName,
|
|
668
|
+
type: parentType.__args[k],
|
|
669
|
+
value: jsonQuery.__args[k],
|
|
670
|
+
update: function(index) {
|
|
671
|
+
var name = "".concat(variableName).concat(index !== void 0 ? "_".concat(index) : "");
|
|
672
|
+
jsonQuery.__args[k] = "".concat(VAR_PREFIX, "$").concat(name);
|
|
673
|
+
return name;
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
jsonQuery.__args[k] = VAR_PREFIX;
|
|
644
677
|
});
|
|
645
678
|
}
|
|
646
679
|
Object.keys(jsonQuery).filter(function(k) {
|
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.
|
|
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.
|
|
3
|
+
"version": "10.7.1",
|
|
4
4
|
"description": "GraphQL Typescript Client Generator",
|
|
5
5
|
"author": "Wellington Guimaraes",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"typings": "dist/index.d.ts",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsup src/index.ts src/endpoint.ts --dts",
|
|
11
|
-
"test": "cross-env GQL_CLIENT_DIST_PATH='.' jest --watch"
|
|
12
|
-
|
|
11
|
+
"test": "cross-env GQL_CLIENT_DIST_PATH='.' jest --watch",
|
|
12
|
+
"prepublishOnly": "yarn build"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
@@ -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
|
package/src/__testClient.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// noinspection TypeScriptUnresolvedVariable, ES6UnusedImports, JSUnusedLocalSymbols, TypeScriptCheckImport
|
|
2
2
|
import { DeepRequired } from "ts-essentials"
|
|
3
|
-
import { Maybe, IResponseListener, Endpoint } from "
|
|
3
|
+
import { Maybe, IResponseListener, Endpoint } from "."
|
|
4
4
|
|
|
5
5
|
// Scalars
|
|
6
6
|
export type IDate = string | Date
|
package/src/__testClient.js
CHANGED
|
@@ -26,7 +26,7 @@ __export(exports, {
|
|
|
26
26
|
default: () => stdin_default,
|
|
27
27
|
myApiClient: () => myApiClient
|
|
28
28
|
});
|
|
29
|
-
var import_endpoint = __toModule(require("
|
|
29
|
+
var import_endpoint = __toModule(require("./endpoint"));
|
|
30
30
|
var import_standalone = __toModule(require("prettier/standalone"));
|
|
31
31
|
var import_parser_graphql = __toModule(require("prettier/parser-graphql"));
|
|
32
32
|
const formatGraphQL = (query) => (0, import_standalone.format)(query, { parser: "graphql", plugins: [import_parser_graphql.default] });
|
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
|
+
})
|
|
@@ -38,8 +38,14 @@ describe('jsonToGraphQLQuery', () => {
|
|
|
38
38
|
__args: {
|
|
39
39
|
foo: 'Bar',
|
|
40
40
|
},
|
|
41
|
-
|
|
42
41
|
something: true,
|
|
42
|
+
another: {
|
|
43
|
+
__args: {
|
|
44
|
+
foo: 'Lorem ipsum',
|
|
45
|
+
bar: 'Dolor sit amet',
|
|
46
|
+
},
|
|
47
|
+
zeta: true,
|
|
48
|
+
},
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
const response = jsonToGraphQLQuery({
|
|
@@ -53,14 +59,24 @@ describe('jsonToGraphQLQuery', () => {
|
|
|
53
59
|
__args: {
|
|
54
60
|
foo: 'UUID!',
|
|
55
61
|
},
|
|
62
|
+
something: 'Boolean!',
|
|
63
|
+
another: {
|
|
64
|
+
__args: {
|
|
65
|
+
foo: 'String!',
|
|
66
|
+
bar: 'String!',
|
|
67
|
+
},
|
|
68
|
+
zeta: 'Int!',
|
|
69
|
+
},
|
|
56
70
|
}
|
|
57
71
|
},
|
|
58
72
|
},
|
|
59
73
|
},
|
|
60
74
|
})
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
expect(response.variables).toEqual({
|
|
77
|
+
foo_0: 'Bar',
|
|
78
|
+
foo_1: 'Lorem ipsum',
|
|
79
|
+
bar: 'Dolor sit amet',
|
|
64
80
|
})
|
|
65
81
|
})
|
|
66
82
|
})
|
|
@@ -7,6 +7,9 @@ const fromEntries: (arr: [string, any][]) => { [key: string]: any } = require('l
|
|
|
7
7
|
const entries: (obj: { [key: string]: any }) => [string, any][] = require('lodash/toPairs')
|
|
8
8
|
const cloneDeep = require('lodash/cloneDeep')
|
|
9
9
|
|
|
10
|
+
type ExtractedVariables = Record<string, (Variable & { name: string; update: (index?: number) => string })[]>
|
|
11
|
+
type Variable = { type: any; value: any }
|
|
12
|
+
|
|
10
13
|
export function jsonToGraphQLQuery({
|
|
11
14
|
kind,
|
|
12
15
|
queryName,
|
|
@@ -18,7 +21,7 @@ export function jsonToGraphQLQuery({
|
|
|
18
21
|
jsonQuery: any
|
|
19
22
|
typesTree: any
|
|
20
23
|
}) {
|
|
21
|
-
const variablesData = {} as
|
|
24
|
+
const variablesData = {} as ExtractedVariables
|
|
22
25
|
const alias = jsonQuery.__alias
|
|
23
26
|
const newJsonQuery = cloneDeep(omit(jsonQuery, ['__alias', '__headers']))
|
|
24
27
|
|
|
@@ -28,8 +31,17 @@ export function jsonToGraphQLQuery({
|
|
|
28
31
|
parentType: kind === 'query' ? typesTree.Query : typesTree.Mutation,
|
|
29
32
|
})
|
|
30
33
|
|
|
31
|
-
const
|
|
32
|
-
|
|
34
|
+
const variableItems = Object.values(variablesData).reduce((variablesObj, variables) => {
|
|
35
|
+
variables.forEach((variable, index) => {
|
|
36
|
+
const name = variable.update(variables.length > 1 ? index : undefined)
|
|
37
|
+
variablesObj[name] = { type: variable.type, value: variable.value }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return variablesObj
|
|
41
|
+
}, {} as Record<string, Variable>)
|
|
42
|
+
|
|
43
|
+
const variablesQuery = Object.keys(variableItems).length
|
|
44
|
+
? `(${entries(variableItems)
|
|
33
45
|
.map(([queryName, { type }]: any) => `$${queryName}: ${type}`)
|
|
34
46
|
.join(', ')})`
|
|
35
47
|
: ''
|
|
@@ -37,7 +49,7 @@ export function jsonToGraphQLQuery({
|
|
|
37
49
|
const query = `${kind} ${alias || queryName}${variablesQuery} { ${alias ? `${alias}:` : ''}${queryName}${toGraphql(
|
|
38
50
|
newJsonQuery
|
|
39
51
|
)} }`
|
|
40
|
-
const variables = fromEntries(entries(
|
|
52
|
+
const variables = fromEntries(entries(variableItems).map(([k, v]: any) => [k, v.value]))
|
|
41
53
|
|
|
42
54
|
return {
|
|
43
55
|
query,
|
|
@@ -45,22 +57,41 @@ export function jsonToGraphQLQuery({
|
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
function extractVariables({
|
|
60
|
+
function extractVariables({
|
|
61
|
+
jsonQuery,
|
|
62
|
+
variables,
|
|
63
|
+
parentType,
|
|
64
|
+
}: {
|
|
65
|
+
jsonQuery: any
|
|
66
|
+
variables: ExtractedVariables
|
|
67
|
+
parentType: any
|
|
68
|
+
}) {
|
|
49
69
|
if (!parentType) return
|
|
50
70
|
|
|
51
71
|
if (jsonQuery.__args) {
|
|
52
72
|
Object.keys(jsonQuery.__args).forEach(k => {
|
|
53
73
|
if (typeof jsonQuery.__args[k] === 'string' && jsonQuery.__args[k].startsWith(VAR_PREFIX)) return
|
|
74
|
+
if (jsonQuery.__args[k] === undefined) return
|
|
54
75
|
|
|
55
|
-
const variableName =
|
|
76
|
+
const variableName = k
|
|
56
77
|
|
|
57
|
-
if (
|
|
58
|
-
variables[variableName] =
|
|
59
|
-
type: parentType.__args[k],
|
|
60
|
-
value: jsonQuery.__args[k],
|
|
61
|
-
}
|
|
62
|
-
jsonQuery.__args[k] = `${VAR_PREFIX}$${variableName}`
|
|
78
|
+
if (!variables[variableName]) {
|
|
79
|
+
variables[variableName] = []
|
|
63
80
|
}
|
|
81
|
+
|
|
82
|
+
variables[variableName].push({
|
|
83
|
+
name: variableName,
|
|
84
|
+
type: parentType.__args[k],
|
|
85
|
+
value: jsonQuery.__args[k],
|
|
86
|
+
update: (index?: number) => {
|
|
87
|
+
const name = `${variableName}${index !== undefined ? `_${index}` : ''}`
|
|
88
|
+
jsonQuery.__args[k] = `${VAR_PREFIX}$${name}`
|
|
89
|
+
|
|
90
|
+
return name
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
jsonQuery.__args[k] = VAR_PREFIX
|
|
64
95
|
})
|
|
65
96
|
}
|
|
66
97
|
|