@avantstay/graphql-ts-client 10.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/endpoint.d.ts +13 -0
- package/dist/endpoint.js +836 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +985 -0
- package/dist/types-4ecbabdf.d.ts +69 -0
- package/package.json +85 -0
- package/src/__snapshots__/graphqlRequest.test.ts.snap +24 -0
- package/src/__testClient.d.ts +148 -0
- package/src/__testClient.js +118 -0
- package/src/endpoint.ts +117 -0
- package/src/generateTypescriptClient.test.ts +127 -0
- package/src/generateTypescriptClient.ts +547 -0
- package/src/graphqlRequest.test.ts +103 -0
- package/src/graphqlRequest.ts +95 -0
- package/src/index.ts +3 -0
- package/src/jsonToGraphQLQuery.test.ts +66 -0
- package/src/jsonToGraphQLQuery.ts +88 -0
- package/src/logging.ts +33 -0
- package/src/testServer.ts +71 -0
- package/src/types.ts +114 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import axiosRetry from 'axios-retry'
|
|
3
|
+
import Case from 'case'
|
|
4
|
+
import * as esbuild from 'esbuild'
|
|
5
|
+
import * as fs from 'fs'
|
|
6
|
+
import { PathLike } from 'fs'
|
|
7
|
+
import {
|
|
8
|
+
getIntrospectionQuery,
|
|
9
|
+
IntrospectionEnumType,
|
|
10
|
+
IntrospectionField,
|
|
11
|
+
IntrospectionInputObjectType,
|
|
12
|
+
IntrospectionInputTypeRef,
|
|
13
|
+
IntrospectionObjectType,
|
|
14
|
+
IntrospectionOutputTypeRef,
|
|
15
|
+
IntrospectionType,
|
|
16
|
+
} from 'graphql'
|
|
17
|
+
import { kebabCase } from 'lodash'
|
|
18
|
+
import orderBy from 'lodash/orderBy'
|
|
19
|
+
import set from 'lodash/set'
|
|
20
|
+
import md5 from 'md5'
|
|
21
|
+
import os from 'os'
|
|
22
|
+
import path from 'path'
|
|
23
|
+
import * as prettier from 'prettier'
|
|
24
|
+
import pkg from '../package.json'
|
|
25
|
+
|
|
26
|
+
const tempDir = fs.realpathSync(os.tmpdir())
|
|
27
|
+
|
|
28
|
+
const graphqlTsClientPath = process.env.GQL_CLIENT_DIST_PATH || 'graphql-ts-client/dist'
|
|
29
|
+
|
|
30
|
+
function gqlScalarToTypescript(gqlType: string) {
|
|
31
|
+
if (/(int|long|double|decimal|float)/i.test(gqlType)) return 'number'
|
|
32
|
+
if (/boolean/i.test(gqlType)) return 'boolean'
|
|
33
|
+
if (/String/i.test(gqlType)) return 'string'
|
|
34
|
+
|
|
35
|
+
return gqlType
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function gqlTypeToTypescript(
|
|
39
|
+
gqlType: IntrospectionOutputTypeRef,
|
|
40
|
+
{ required = false, isInput = false, selection = false } = {}
|
|
41
|
+
): string {
|
|
42
|
+
if (!gqlType) return ''
|
|
43
|
+
|
|
44
|
+
const maybeWrapped = (it: string) => (required || selection ? it : `Maybe<${it}>`)
|
|
45
|
+
|
|
46
|
+
// noinspection SuspiciousTypeOfGuard
|
|
47
|
+
if (typeof gqlType === 'string') {
|
|
48
|
+
return maybeWrapped(gqlType)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (gqlType.kind.endsWith('OBJECT')) {
|
|
52
|
+
return maybeWrapped((gqlType as any).name + (selection ? 'Selection' : ''))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (gqlType.kind === 'NON_NULL') {
|
|
56
|
+
return `${gqlTypeToTypescript(gqlType.ofType, {
|
|
57
|
+
isInput,
|
|
58
|
+
required: true,
|
|
59
|
+
selection,
|
|
60
|
+
})}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (gqlType.kind === 'LIST') {
|
|
64
|
+
return maybeWrapped(
|
|
65
|
+
`${gqlTypeToTypescript(gqlType.ofType, {
|
|
66
|
+
isInput,
|
|
67
|
+
required: true,
|
|
68
|
+
selection,
|
|
69
|
+
})}${selection ? '' : '[]'}`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (selection) {
|
|
74
|
+
return ''
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (gqlType.kind === 'ENUM' && gqlType.name) {
|
|
78
|
+
return maybeWrapped(gqlType.name)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (gqlType.kind === 'SCALAR') {
|
|
82
|
+
return maybeWrapped(gqlScalarToTypescript(gqlType.name))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function gqlFieldToTypescript(
|
|
89
|
+
field: IntrospectionField,
|
|
90
|
+
{ isInput, selection, defaultValue }: { defaultValue?: any; isInput: boolean; selection: boolean }
|
|
91
|
+
) {
|
|
92
|
+
let fieldTypeDefinition = gqlTypeToTypescript(field.type, {
|
|
93
|
+
isInput,
|
|
94
|
+
selection,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
fieldTypeDefinition = `${fieldTypeDefinition}`
|
|
98
|
+
|
|
99
|
+
if (selection && field.args && field.args.length) {
|
|
100
|
+
let fieldsOnArgs = field.args.map(arg =>
|
|
101
|
+
gqlFieldToTypescript(arg as unknown as IntrospectionField, {
|
|
102
|
+
defaultValue: arg.defaultValue,
|
|
103
|
+
isInput: true,
|
|
104
|
+
selection: false,
|
|
105
|
+
})
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
fieldTypeDefinition = `{ __headers?: {[key: string]: string}; __retry?: boolean; __alias?: string; __args${
|
|
109
|
+
fieldsOnArgs.every(arg => arg.isOptional) ? '?' : ''
|
|
110
|
+
}: { ${fieldsOnArgs.map(arg => arg.code).join(', ')} }}${fieldTypeDefinition ? ` & ${fieldTypeDefinition}` : ''}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isOptional = defaultValue || selection || fieldTypeDefinition.startsWith('Maybe')
|
|
114
|
+
const rawType = fieldTypeDefinition || (selection && 'boolean')
|
|
115
|
+
const wrappedType = isOptional ? (rawType as string).replace(/Maybe<(.+?)>/, '$1') : rawType
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
isOptional: isOptional,
|
|
119
|
+
code: `${field.name}${isOptional ? '?:' : ':'} ${wrappedType}`,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getArgsType(endpoint: IntrospectionField) {
|
|
124
|
+
const fieldsOnArgs = endpoint.args.map(arg =>
|
|
125
|
+
gqlFieldToTypescript(arg as unknown as IntrospectionField, {
|
|
126
|
+
defaultValue: arg.defaultValue,
|
|
127
|
+
isInput: true,
|
|
128
|
+
selection: false,
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
const argsType = `{ ${fieldsOnArgs.map(arg => arg.code).join(', ')} }`
|
|
132
|
+
const argsFullyOptional = fieldsOnArgs.every(arg => arg.isOptional)
|
|
133
|
+
|
|
134
|
+
return { alias: Case.pascal(`${endpoint.name}Args`), type: argsType, optional: argsFullyOptional }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function gqlEndpointToCode(kind: 'mutation' | 'query', endpoint: IntrospectionField, codeOutputType: 'ts' | 'js'): string {
|
|
138
|
+
const selectionType = gqlTypeToTypescript(endpoint.type, {
|
|
139
|
+
isInput: false,
|
|
140
|
+
selection: true,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const argsType = endpoint.args && endpoint.args.length ? getArgsType(endpoint) : null
|
|
144
|
+
const inputType = `{
|
|
145
|
+
__headers?: {[key: string]: string};
|
|
146
|
+
__retry?: boolean;
|
|
147
|
+
__alias?: string;
|
|
148
|
+
${argsType ? `__args${argsType.optional ? '?' : ''}: ${argsType.alias}` : ''}
|
|
149
|
+
}${selectionType ? ` & ${selectionType}` : ''}`
|
|
150
|
+
|
|
151
|
+
const outputType = gqlTypeToTypescript(endpoint.type, { required: true })
|
|
152
|
+
const wrappedOutputType = /^(string|number|boolean)$/.test(outputType) ? outputType : `DeepRequired<${outputType}>`
|
|
153
|
+
|
|
154
|
+
return codeOutputType === 'ts'
|
|
155
|
+
? `${endpoint.name}: Endpoint<${inputType}, ${wrappedOutputType}, AllEnums>`
|
|
156
|
+
: `${endpoint.name}: apiEndpoint('${kind}', '${endpoint.name}')`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function gqlSchemaToCode(
|
|
160
|
+
gqlType: any | IntrospectionObjectType | IntrospectionInputObjectType | IntrospectionEnumType,
|
|
161
|
+
{ selection = false, outputType }: { selection: boolean; outputType: 'js' | 'ts' }
|
|
162
|
+
) {
|
|
163
|
+
const rawKind = gqlType.kind || gqlType.type
|
|
164
|
+
|
|
165
|
+
if (rawKind === 'SCALAR') {
|
|
166
|
+
return outputType === 'ts' ? `export declare type ${gqlType.name} = ${/date/i.test(gqlType.name) ? 'IDate' : 'string'}` : ''
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (rawKind === 'ENUM')
|
|
170
|
+
return outputType === 'ts'
|
|
171
|
+
? `
|
|
172
|
+
export declare enum ${gqlType.name} {
|
|
173
|
+
${orderBy(gqlType.enumValues, 'name')
|
|
174
|
+
.map((_: any) => `${Case.camel(_.name)} = '${_.name}'`)
|
|
175
|
+
.join(',\n ')}
|
|
176
|
+
}`
|
|
177
|
+
: `export const ${gqlType.name} = {${orderBy(gqlType.enumValues, 'name')
|
|
178
|
+
.map((_: any) => `${Case.camel(_.name)}: '${_.name}'`)
|
|
179
|
+
.join(',\n ')}}`
|
|
180
|
+
|
|
181
|
+
const fields = (gqlType.fields && gqlType.fields) || (gqlType.inputFields && gqlType.inputFields) || []
|
|
182
|
+
|
|
183
|
+
return outputType === 'ts'
|
|
184
|
+
? `
|
|
185
|
+
export interface ${gqlType.name}${selection ? 'Selection' : ''} {
|
|
186
|
+
${fields
|
|
187
|
+
.map(
|
|
188
|
+
(_: any) =>
|
|
189
|
+
gqlFieldToTypescript(_, {
|
|
190
|
+
isInput: gqlType.kind === 'INPUT_OBJECT',
|
|
191
|
+
selection,
|
|
192
|
+
}).code
|
|
193
|
+
)
|
|
194
|
+
.join(',\n ')}
|
|
195
|
+
}`
|
|
196
|
+
: ''
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getGraphQLInputType(type: IntrospectionInputTypeRef): string {
|
|
200
|
+
switch (type.kind) {
|
|
201
|
+
case 'NON_NULL':
|
|
202
|
+
return `${getGraphQLInputType(type.ofType)}!`
|
|
203
|
+
|
|
204
|
+
case 'SCALAR':
|
|
205
|
+
case 'INPUT_OBJECT':
|
|
206
|
+
case 'ENUM':
|
|
207
|
+
return type.name
|
|
208
|
+
|
|
209
|
+
case 'LIST':
|
|
210
|
+
return `[${getGraphQLInputType(type.ofType)}]`
|
|
211
|
+
|
|
212
|
+
default:
|
|
213
|
+
return ''
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getGraphQLOutputType(type: IntrospectionOutputTypeRef): string {
|
|
218
|
+
switch (type.kind) {
|
|
219
|
+
case 'LIST':
|
|
220
|
+
return `${getGraphQLOutputType(type.ofType)}[]`
|
|
221
|
+
|
|
222
|
+
case 'NON_NULL':
|
|
223
|
+
return getGraphQLOutputType(type.ofType)
|
|
224
|
+
|
|
225
|
+
case 'OBJECT':
|
|
226
|
+
return type.name
|
|
227
|
+
|
|
228
|
+
default:
|
|
229
|
+
return ''
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getTypesTreeCode(types: IntrospectionObjectType[]) {
|
|
234
|
+
const typesTree = {}
|
|
235
|
+
|
|
236
|
+
types.forEach(type =>
|
|
237
|
+
type.fields
|
|
238
|
+
.filter(_ => _.args && _.args.length)
|
|
239
|
+
.forEach(_ =>
|
|
240
|
+
_.args.forEach(a => {
|
|
241
|
+
let inputType = getGraphQLInputType(a.type)
|
|
242
|
+
if (inputType) {
|
|
243
|
+
set(typesTree, `${type.name}.${_.name}.__args.${a.name}`, inputType)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
types.forEach(t =>
|
|
250
|
+
t.fields.forEach(f => {
|
|
251
|
+
let outputType = getGraphQLOutputType(f.type)
|
|
252
|
+
if (outputType) {
|
|
253
|
+
set(typesTree, `${t.name}.${f.name}.__shape`, outputType)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return `
|
|
259
|
+
const typesTree = {
|
|
260
|
+
${Object.entries(typesTree)
|
|
261
|
+
.map(([key, value]) => {
|
|
262
|
+
let entryCode = Object.entries(value as any)
|
|
263
|
+
.map(([k, v]: any) => {
|
|
264
|
+
const cleanShapeType = v.__shape && v.__shape.replace(/[\[\]!?]/g, '')
|
|
265
|
+
const fieldsCode =
|
|
266
|
+
v.__shape && typesTree.hasOwnProperty(cleanShapeType) ? `__fields: typesTree.${cleanShapeType},` : ''
|
|
267
|
+
|
|
268
|
+
const argsCode = v.__args
|
|
269
|
+
? `__args: {
|
|
270
|
+
${Object.entries(v.__args)
|
|
271
|
+
.map(([k, v]) => `${k}: '${v}'`)
|
|
272
|
+
.join(',\n')}
|
|
273
|
+
}`
|
|
274
|
+
: ''
|
|
275
|
+
|
|
276
|
+
return fieldsCode || argsCode
|
|
277
|
+
? `get ${k}() {
|
|
278
|
+
return {
|
|
279
|
+
${fieldsCode}
|
|
280
|
+
${argsCode}
|
|
281
|
+
}
|
|
282
|
+
}`
|
|
283
|
+
: `${k}: {}`
|
|
284
|
+
})
|
|
285
|
+
.filter(Boolean)
|
|
286
|
+
.join(',\n')
|
|
287
|
+
.trim()
|
|
288
|
+
return (
|
|
289
|
+
entryCode &&
|
|
290
|
+
`
|
|
291
|
+
${key}: {
|
|
292
|
+
${entryCode}
|
|
293
|
+
}`
|
|
294
|
+
)
|
|
295
|
+
})
|
|
296
|
+
.filter(Boolean)
|
|
297
|
+
.join(',\n')}
|
|
298
|
+
}
|
|
299
|
+
`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
type IClientOptions = {
|
|
303
|
+
output?: PathLike
|
|
304
|
+
clientName?: string
|
|
305
|
+
headers?: { [key: string]: string }
|
|
306
|
+
introspectionEndpoint?: string
|
|
307
|
+
endpoint: string
|
|
308
|
+
verbose?: boolean
|
|
309
|
+
formatGraphQL?: boolean
|
|
310
|
+
skipCache?: boolean
|
|
311
|
+
errorsParser?: (errors: any[]) => any
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
type FetchIntrospectionOptions = Omit< IClientOptions, 'output' | 'introspectionEndpoint'>
|
|
315
|
+
|
|
316
|
+
function generateClientCode(types: ReadonlyArray<IntrospectionType>, options: Omit<IClientOptions, 'output'>) {
|
|
317
|
+
const typesHash = md5(`${JSON.stringify(options)}__${JSON.stringify(types)}`)
|
|
318
|
+
const clientCacheFileName = `gql-ts-client__client__${typesHash}__${pkg.version}.json`
|
|
319
|
+
const clientCacheFilePath = path.resolve(tempDir, clientCacheFileName)
|
|
320
|
+
|
|
321
|
+
if (!options.skipCache && fs.existsSync(clientCacheFilePath)) {
|
|
322
|
+
return JSON.parse(fs.readFileSync(clientCacheFilePath, { encoding: 'utf8' }))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const queries = (<IntrospectionObjectType>types.find(it => it.name === 'Query'))?.fields || []
|
|
326
|
+
const mutations = (<IntrospectionObjectType>types.find(it => it.name === 'Mutation'))?.fields || []
|
|
327
|
+
const enums = types.filter(it => it.kind === 'ENUM' && !it.name.startsWith('__')) as IntrospectionEnumType[]
|
|
328
|
+
const scalars = types.filter(
|
|
329
|
+
it => it.kind === 'SCALAR' && !/decimal|int|float|string|long|boolean/i.test(it.name)
|
|
330
|
+
) as IntrospectionEnumType[]
|
|
331
|
+
const objectTypes = types.filter(it => ['OBJECT', 'INPUT_OBJECT'].includes(it.kind) && !it.name.startsWith('__')) as (
|
|
332
|
+
| IntrospectionObjectType
|
|
333
|
+
| IntrospectionInputObjectType
|
|
334
|
+
)[]
|
|
335
|
+
|
|
336
|
+
const forInputExtraction = types.filter(
|
|
337
|
+
it => !it.name.startsWith('__') && ['OBJECT'].includes(it.kind)
|
|
338
|
+
) as IntrospectionObjectType[]
|
|
339
|
+
|
|
340
|
+
const clientName = options.clientName || 'client'
|
|
341
|
+
|
|
342
|
+
// language=JavaScript
|
|
343
|
+
const jsCode = `
|
|
344
|
+
// noinspection TypeScriptUnresolvedVariable, ES6UnusedImports, JSUnusedLocalSymbols
|
|
345
|
+
import { getApiEndpointCreator } from '${graphqlTsClientPath}/endpoint'
|
|
346
|
+
|
|
347
|
+
${
|
|
348
|
+
options.formatGraphQL || options.verbose
|
|
349
|
+
? `
|
|
350
|
+
import { format as formatCode } from "prettier/standalone"
|
|
351
|
+
import parserGraphql from "prettier/parser-graphql"
|
|
352
|
+
|
|
353
|
+
const formatGraphQL = (query) => formatCode(query, {parser: 'graphql', plugins: [parserGraphql]})`
|
|
354
|
+
: `
|
|
355
|
+
const formatGraphQL = (query) => query`
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Enums
|
|
359
|
+
${enums.map(it => gqlSchemaToCode(it, { selection: false, outputType: 'js' })).join('\n')}
|
|
360
|
+
|
|
361
|
+
// Schema Resolution Tree
|
|
362
|
+
${getTypesTreeCode(forInputExtraction)}
|
|
363
|
+
|
|
364
|
+
let verbose = ${Boolean(options.verbose)}
|
|
365
|
+
let headers = {}
|
|
366
|
+
let url = '${options.endpoint}'
|
|
367
|
+
let retryConfig = {
|
|
368
|
+
max: 0,
|
|
369
|
+
before: undefined,
|
|
370
|
+
waitBeforeRetry: 0
|
|
371
|
+
}
|
|
372
|
+
let responseListeners = []
|
|
373
|
+
let errorsParser = ${options.errorsParser}
|
|
374
|
+
// noinspection JSUnusedLocalSymbols
|
|
375
|
+
let apiEndpoint = getApiEndpointCreator({
|
|
376
|
+
getClient: () => ({ url, headers, retryConfig }),
|
|
377
|
+
responseListeners,
|
|
378
|
+
maxAge: 30000,
|
|
379
|
+
verbose,
|
|
380
|
+
typesTree,
|
|
381
|
+
formatGraphQL,
|
|
382
|
+
errorsParser
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
export const ${clientName} = {
|
|
386
|
+
addResponseListener: (listener) => responseListeners.push(
|
|
387
|
+
listener),
|
|
388
|
+
setHeader: (key, value) => {
|
|
389
|
+
headers[key] = value
|
|
390
|
+
},
|
|
391
|
+
setHeaders: (newHeaders) => {
|
|
392
|
+
headers = newHeaders
|
|
393
|
+
},
|
|
394
|
+
setRetryConfig: (options) => {
|
|
395
|
+
if (!Number.isInteger(options.max) || options.max < 0) {
|
|
396
|
+
throw new Error('retryOptions.max should be a non-negative integer')
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
retryConfig = {
|
|
400
|
+
max: options.max,
|
|
401
|
+
waitBeforeRetry: options.waitBeforeRetry,
|
|
402
|
+
before: options.before
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
setUrl: (_url) => url = _url,
|
|
406
|
+
queries: {
|
|
407
|
+
${queries.map(query => gqlEndpointToCode('query', query, 'js')).join(',\n')}
|
|
408
|
+
},
|
|
409
|
+
mutations: {
|
|
410
|
+
${mutations.map(mutation => gqlEndpointToCode('mutation', mutation, 'js')).join(',\n')}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export default ${clientName}`
|
|
415
|
+
|
|
416
|
+
// language=TypeScript
|
|
417
|
+
const typingsCode = `
|
|
418
|
+
// noinspection TypeScriptUnresolvedVariable, ES6UnusedImports, JSUnusedLocalSymbols, TypeScriptCheckImport
|
|
419
|
+
import { DeepRequired } from 'ts-essentials'
|
|
420
|
+
import { Maybe, IResponseListener, Endpoint } from '${graphqlTsClientPath}'
|
|
421
|
+
|
|
422
|
+
// Scalars
|
|
423
|
+
export type IDate = string | Date
|
|
424
|
+
${scalars.map(it => gqlSchemaToCode(it, { selection: false, outputType: 'ts' })).join('\n')}
|
|
425
|
+
|
|
426
|
+
// Enums
|
|
427
|
+
${enums.map(it => gqlSchemaToCode(it, { selection: false, outputType: 'ts' })).join('\n')}
|
|
428
|
+
|
|
429
|
+
type AllEnums = ${enums.length ? enums.map(it => it.name).join(' | ') : 'never'}
|
|
430
|
+
|
|
431
|
+
// Args
|
|
432
|
+
${[...queries, ...mutations]
|
|
433
|
+
.map(query => {
|
|
434
|
+
const argsType = getArgsType(query)
|
|
435
|
+
return `export interface ${argsType.alias} ${argsType.type}`
|
|
436
|
+
})
|
|
437
|
+
.join('\n')}
|
|
438
|
+
|
|
439
|
+
// Input/Output Types
|
|
440
|
+
${objectTypes
|
|
441
|
+
.map(
|
|
442
|
+
it => `
|
|
443
|
+
/**
|
|
444
|
+
* @deprecated Avoid directly using this interface. Instead, create a type alias based on the query/mutation return type.
|
|
445
|
+
*/
|
|
446
|
+
${gqlSchemaToCode(it, { selection: false, outputType: 'ts' })}`
|
|
447
|
+
)
|
|
448
|
+
.join('\n')}
|
|
449
|
+
|
|
450
|
+
// Selection Types
|
|
451
|
+
${objectTypes
|
|
452
|
+
.filter(it => it.name !== 'Query')
|
|
453
|
+
.map(it => gqlSchemaToCode(it, { selection: true, outputType: 'ts' }))
|
|
454
|
+
.join('\n')}
|
|
455
|
+
|
|
456
|
+
export declare const ${clientName}: {
|
|
457
|
+
addResponseListener: (listener: IResponseListener) => void
|
|
458
|
+
setHeader: (key: string, value: string) => void
|
|
459
|
+
setHeaders: (newHeaders: { [k: string]: string }) => void,
|
|
460
|
+
setUrl: (url: string) => void,
|
|
461
|
+
setRetryConfig: (options: { max: number, waitBeforeRetry?: number, before?: IResponseListener }) => void
|
|
462
|
+
queries: {
|
|
463
|
+
${queries.map(q => gqlEndpointToCode('query', q, 'ts')).join(',\n')}
|
|
464
|
+
},
|
|
465
|
+
mutations: {
|
|
466
|
+
${mutations.map(q => gqlEndpointToCode('mutation', q, 'ts')).join(',\n')}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export default ${clientName}`
|
|
471
|
+
|
|
472
|
+
const output = {
|
|
473
|
+
js: esbuild.transformSync(jsCode, { format: 'cjs', loader: 'js' }).code,
|
|
474
|
+
typings: prettier.format(typingsCode, { semi: false, parser: 'typescript' }),
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
fs.writeFileSync(clientCacheFilePath, JSON.stringify(output))
|
|
478
|
+
|
|
479
|
+
return output
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function fetchIntrospection({ endpoint, headers }: FetchIntrospectionOptions) {
|
|
483
|
+
const introspectionCacheFileName = `gql-ts-client__introspection__${kebabCase(endpoint)}.json`
|
|
484
|
+
const introspectionCacheFilePath = path.resolve(tempDir, introspectionCacheFileName)
|
|
485
|
+
|
|
486
|
+
let loadedFromCache = false
|
|
487
|
+
let types: any
|
|
488
|
+
|
|
489
|
+
const { data } = await axios
|
|
490
|
+
.post(
|
|
491
|
+
endpoint,
|
|
492
|
+
{ query: getIntrospectionQuery() },
|
|
493
|
+
{
|
|
494
|
+
headers: {
|
|
495
|
+
'Content-Type': 'application/json',
|
|
496
|
+
...headers,
|
|
497
|
+
},
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
.catch(() => {
|
|
501
|
+
const errorMessage = `The GraphQL introspection request failed (${endpoint})`
|
|
502
|
+
if (fs.existsSync(introspectionCacheFilePath)) {
|
|
503
|
+
const cachedSchema = JSON.parse(fs.readFileSync(introspectionCacheFilePath, { encoding: 'utf8' }))
|
|
504
|
+
loadedFromCache = true
|
|
505
|
+
console.warn(`Successfully restored from local cache.`)
|
|
506
|
+
return { data: cachedSchema }
|
|
507
|
+
} else {
|
|
508
|
+
return Promise.reject(errorMessage)
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
types = data.data.__schema.types
|
|
513
|
+
|
|
514
|
+
if (!loadedFromCache) {
|
|
515
|
+
console.log(`Successfully loaded GraphQL introspection from ${endpoint}`)
|
|
516
|
+
|
|
517
|
+
fs.writeFileSync(introspectionCacheFilePath, JSON.stringify(data), {
|
|
518
|
+
encoding: 'utf8',
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return types
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export async function generateTypescriptClient({ introspectionEndpoint, output, ...options }: IClientOptions): Promise<{ typings: string; js: string }> {
|
|
526
|
+
axiosRetry(axios, { retries: 5, retryDelay: retryCount => 1000 * 2 ** retryCount })
|
|
527
|
+
|
|
528
|
+
const types = await fetchIntrospection({
|
|
529
|
+
...options,
|
|
530
|
+
endpoint: introspectionEndpoint || options.endpoint,
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
const { js, typings } = generateClientCode(types, options)
|
|
534
|
+
|
|
535
|
+
if (output && typeof output === 'string') {
|
|
536
|
+
const outputDir = path.dirname(output)
|
|
537
|
+
|
|
538
|
+
if (!fs.existsSync(outputDir)) {
|
|
539
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
fs.writeFileSync(output.replace(/(\.(ts|js))?$/, '.d.ts'), typings, { encoding: 'utf8' })
|
|
543
|
+
fs.writeFileSync(output.replace(/(\.(ts|js))?$/, '.js'), js, { encoding: 'utf8' })
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return { js, typings }
|
|
547
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { graphqlRequest } from './graphqlRequest'
|
|
2
|
+
|
|
3
|
+
describe('GraphQLRequest', () => {
|
|
4
|
+
it('Should request have proper structure', async () => {
|
|
5
|
+
let request: any;
|
|
6
|
+
|
|
7
|
+
const mockedAxios = {
|
|
8
|
+
post: function() {
|
|
9
|
+
request = arguments
|
|
10
|
+
return {
|
|
11
|
+
status: 200,
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
} as any
|
|
15
|
+
|
|
16
|
+
const result = await graphqlRequest({
|
|
17
|
+
shouldRetry: false,
|
|
18
|
+
failureMode: 'loud',
|
|
19
|
+
axios: mockedAxios,
|
|
20
|
+
queryName: 'sampleQueryName',
|
|
21
|
+
query: 'sampleQuery',
|
|
22
|
+
variables: {foo:'bar',bar:'foo'},
|
|
23
|
+
client: {
|
|
24
|
+
url: 'https://whatever.com',
|
|
25
|
+
headers: {},
|
|
26
|
+
retryConfig: {
|
|
27
|
+
max: 0,
|
|
28
|
+
before: () => void [1],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}).catch(err => err)
|
|
32
|
+
|
|
33
|
+
expect(result.status).toBe(200)
|
|
34
|
+
expect(request).toMatchSnapshot()
|
|
35
|
+
expect(request).toBeDefined()
|
|
36
|
+
const [url, data, config] = request
|
|
37
|
+
expect(url).toEqual('https://whatever.com')
|
|
38
|
+
expect(config).toBeDefined()
|
|
39
|
+
expect(data.operationName).toEqual('sampleQueryName')
|
|
40
|
+
expect(data.hasOwnProperty('query')).toEqual(true)
|
|
41
|
+
expect(data.hasOwnProperty('variables')).toEqual(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('Should retry as many times as configured properly running a "before" hook', async () => {
|
|
45
|
+
let retryCount = 0
|
|
46
|
+
const maxRetrials = 2
|
|
47
|
+
const mockedAxios = {
|
|
48
|
+
post: async () => ({
|
|
49
|
+
status: retryCount < maxRetrials ? 500 : 200,
|
|
50
|
+
}),
|
|
51
|
+
} as any
|
|
52
|
+
|
|
53
|
+
const result = await graphqlRequest({
|
|
54
|
+
|
|
55
|
+
failureMode: 'loud',
|
|
56
|
+
axios: mockedAxios,
|
|
57
|
+
queryName: 'whatever',
|
|
58
|
+
query: 'whatever',
|
|
59
|
+
variables: {},
|
|
60
|
+
requestHeaders: {},
|
|
61
|
+
client: {
|
|
62
|
+
url: 'https://whatever.com',
|
|
63
|
+
headers: {},
|
|
64
|
+
retryConfig: {
|
|
65
|
+
max: maxRetrials,
|
|
66
|
+
before: () => void [retryCount++],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(retryCount).toBe(2)
|
|
72
|
+
expect(result.status).toBe(200)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('Should ignore default retrying config explicitly asking to skip retrials ', async () => {
|
|
76
|
+
let retryCount = 0
|
|
77
|
+
const maxRetrials = 2
|
|
78
|
+
const mockedAxios = {
|
|
79
|
+
post: async () => ({
|
|
80
|
+
status: retryCount < maxRetrials ? 500 : 200,
|
|
81
|
+
}),
|
|
82
|
+
} as any
|
|
83
|
+
|
|
84
|
+
const result = await graphqlRequest({
|
|
85
|
+
shouldRetry: false,
|
|
86
|
+
failureMode: 'loud',
|
|
87
|
+
axios: mockedAxios,
|
|
88
|
+
queryName: 'whatever',
|
|
89
|
+
query: 'whatever',
|
|
90
|
+
variables: {},
|
|
91
|
+
client: {
|
|
92
|
+
url: 'https://whatever.com',
|
|
93
|
+
headers: {},
|
|
94
|
+
retryConfig: {
|
|
95
|
+
max: maxRetrials,
|
|
96
|
+
before: () => void [retryCount++],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}).catch(err => err)
|
|
100
|
+
|
|
101
|
+
expect(result).toBeInstanceOf(Error)
|
|
102
|
+
})
|
|
103
|
+
})
|