@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.
@@ -0,0 +1,95 @@
1
+ import _axios, { AxiosStatic } from 'axios'
2
+ import { ClientConfig, GraphQLClientError, ResponseData } from './types'
3
+
4
+ const sleep = (ms = 0) =>
5
+ new Promise<void>(resolve => {
6
+ setTimeout(() => resolve(), ms)
7
+ })
8
+
9
+ export async function graphqlRequest({
10
+ shouldRetry = true,
11
+ axios = _axios,
12
+ queryName,
13
+ client,
14
+ query,
15
+ requestHeaders = {},
16
+ variables,
17
+ failureMode,
18
+ errorsParser,
19
+ }: {
20
+ shouldRetry?: boolean
21
+ failureMode: 'loud' | 'silent'
22
+ axios?: AxiosStatic
23
+ client: ClientConfig
24
+ queryName: string
25
+ query: string
26
+ requestHeaders?: { [_key: string]: any }
27
+ variables: { [_key: string]: any }
28
+ errorsParser?: (errors: any[]) => any
29
+ }) {
30
+ let lastResponse: ResponseData
31
+
32
+ const maxRetrials = shouldRetry ? client.retryConfig.max : 0
33
+
34
+ for (let trial = 0; true; trial++) {
35
+ const infoParams = {
36
+ _q: queryName,
37
+ ...(trial > 0 ? { _retrial: trial + 1 } : {}),
38
+ }
39
+
40
+ const {
41
+ data: responseData = {} as any,
42
+ headers,
43
+ status,
44
+ } = await axios.post(
45
+ client.url,
46
+ { query, variables, operationName: queryName },
47
+ {
48
+ params: infoParams,
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ ...client.headers,
52
+ ...requestHeaders,
53
+ },
54
+ validateStatus: () => true,
55
+ }
56
+ )
57
+
58
+ let { data, errors, warnings } = responseData
59
+
60
+ if (status >= 400 && !errors?.length) {
61
+ errors = [{ message: `Request "${queryName}" failed with status ${status}` }]
62
+ }
63
+
64
+ lastResponse = {
65
+ errors: errorsParser ? errorsParser(errors) : errors,
66
+ data,
67
+ warnings,
68
+ headers,
69
+ status,
70
+ }
71
+
72
+ if (!errors?.length) {
73
+ break
74
+ } else if (trial < maxRetrials && typeof client.retryConfig?.before === 'function') {
75
+ await client.retryConfig?.before({
76
+ queryName,
77
+ query,
78
+ variables,
79
+ response: lastResponse,
80
+ })
81
+
82
+ if (client.retryConfig.waitBeforeRetry) {
83
+ await sleep(client.retryConfig.waitBeforeRetry)
84
+ }
85
+ } else if (trial >= maxRetrials) {
86
+ break
87
+ }
88
+ }
89
+
90
+ if (failureMode === 'loud' && lastResponse!.errors && lastResponse!.errors?.length) {
91
+ throw new GraphQLClientError(lastResponse)
92
+ }
93
+
94
+ return lastResponse!
95
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { generateTypescriptClient } from './generateTypescriptClient'
2
+
3
+ export * from './types'
@@ -0,0 +1,66 @@
1
+ import { jsonToGraphQLQuery } from './jsonToGraphQLQuery'
2
+
3
+ describe('jsonToGraphQLQuery', () => {
4
+ it('use args without mutating', () => {
5
+ const jsonQuery = {
6
+ __args: {
7
+ foo: 'Bar',
8
+ },
9
+
10
+ something: true,
11
+ }
12
+
13
+ const originalArgs = JSON.stringify(jsonQuery)
14
+
15
+ const response = jsonToGraphQLQuery({
16
+ kind: 'query',
17
+ queryName: 'testing',
18
+ jsonQuery,
19
+ typesTree: {
20
+ Query: {
21
+ get testing(): any {
22
+ return {
23
+ __args: {
24
+ foo: 'UUID!',
25
+ },
26
+ }
27
+ },
28
+ },
29
+ },
30
+ })
31
+
32
+ expect(response).not.toBeNull()
33
+ expect(originalArgs).toBe(JSON.stringify(jsonQuery))
34
+ })
35
+
36
+ it('variables are correctly named', () => {
37
+ const jsonQuery = {
38
+ __args: {
39
+ foo: 'Bar',
40
+ },
41
+
42
+ something: true,
43
+ }
44
+
45
+ const response = jsonToGraphQLQuery({
46
+ kind: 'query',
47
+ queryName: 'testing',
48
+ jsonQuery,
49
+ typesTree: {
50
+ Query: {
51
+ get testing(): any {
52
+ return {
53
+ __args: {
54
+ foo: 'UUID!',
55
+ },
56
+ }
57
+ },
58
+ },
59
+ },
60
+ })
61
+
62
+ Object.keys(response.variables).forEach(varName => {
63
+ expect(response.query.split(varName).length - 1).toBeGreaterThanOrEqual(2) //every variable name should be present at least 2 times
64
+ })
65
+ })
66
+ })
@@ -0,0 +1,88 @@
1
+ import omit from 'lodash/omit'
2
+
3
+ const VAR_PREFIX = '@@VAR@@'
4
+ const VAR_PREFIX_LENGTH = VAR_PREFIX.length
5
+
6
+ const fromEntries: (arr: [string, any][]) => { [key: string]: any } = require('lodash/fromPairs')
7
+ const entries: (obj: { [key: string]: any }) => [string, any][] = require('lodash/toPairs')
8
+ const cloneDeep = require('lodash/cloneDeep')
9
+
10
+ export function jsonToGraphQLQuery({
11
+ kind,
12
+ queryName,
13
+ jsonQuery = {},
14
+ typesTree,
15
+ }: {
16
+ kind: 'query' | 'mutation'
17
+ queryName: string
18
+ jsonQuery: any
19
+ typesTree: any
20
+ }) {
21
+ const variablesData = {} as any
22
+ const alias = jsonQuery.__alias
23
+ const newJsonQuery = cloneDeep(omit(jsonQuery, ['__alias', '__headers']))
24
+
25
+ extractVariables({
26
+ jsonQuery: { [queryName]: newJsonQuery },
27
+ variables: variablesData,
28
+ parentType: kind === 'query' ? typesTree.Query : typesTree.Mutation,
29
+ })
30
+
31
+ const variablesQuery = Object.keys(variablesData).length
32
+ ? `(${entries(variablesData)
33
+ .map(([queryName, { type }]: any) => `$${queryName}: ${type}`)
34
+ .join(', ')})`
35
+ : ''
36
+
37
+ const query = `${kind} ${alias || queryName}${variablesQuery} { ${alias ? `${alias}:` : ''}${queryName}${toGraphql(
38
+ newJsonQuery
39
+ )} }`
40
+ const variables = fromEntries(entries(variablesData).map(([k, v]: any) => [k, v.value]))
41
+
42
+ return {
43
+ query,
44
+ variables,
45
+ }
46
+ }
47
+
48
+ function extractVariables({ jsonQuery, variables, parentType }: { jsonQuery: any; variables: any; parentType: any }) {
49
+ if (!parentType) return
50
+
51
+ if (jsonQuery.__args) {
52
+ Object.keys(jsonQuery.__args).forEach(k => {
53
+ if (typeof jsonQuery.__args[k] === 'string' && jsonQuery.__args[k].startsWith(VAR_PREFIX)) return
54
+
55
+ const variableName = `${k}_${Math.random().toString(36).substr(2, 4)}`
56
+
57
+ if (jsonQuery.__args[k] !== undefined) {
58
+ variables[variableName] = {
59
+ type: parentType.__args[k],
60
+ value: jsonQuery.__args[k],
61
+ }
62
+ jsonQuery.__args[k] = `${VAR_PREFIX}$${variableName}`
63
+ }
64
+ })
65
+ }
66
+
67
+ Object.keys(jsonQuery)
68
+ .filter(k => k !== '__args' && typeof jsonQuery[k] === 'object')
69
+ .forEach(k =>
70
+ extractVariables({
71
+ jsonQuery: jsonQuery[k],
72
+ variables,
73
+ parentType: parentType.hasOwnProperty(k) ? parentType[k] : parentType.__fields ? parentType.__fields[k] : undefined,
74
+ })
75
+ )
76
+ }
77
+
78
+ function toGraphql(jsonQuery: any) {
79
+ const fields = entries(jsonQuery)
80
+ .filter(([k, v]) => k !== '__args' && v !== false && v !== undefined)
81
+ .map(([k, v]) => (typeof v === 'object' ? `${k}${toGraphql(v)}` : k))
82
+ .join(' ') as any
83
+
84
+ const validArgs = jsonQuery.__args ? entries(jsonQuery.__args).filter(([_, v]) => v !== undefined) : []
85
+ const argsQuery = validArgs.length ? `(${validArgs.map(([k, v]: any) => `${k}:${v.substr(VAR_PREFIX_LENGTH)}`).join(',')})` : ''
86
+
87
+ return `${argsQuery} ${fields ? `{ ${fields} }` : ''}`
88
+ }
package/src/logging.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { LogInfo } from './types'
2
+
3
+ export function logRequest(logInfo: LogInfo) {
4
+ let identifier = `%c#graphql-ts-client ${logInfo.kind} ${logInfo.queryName}`
5
+ let identifierStyles = 'color: transparent; font-size: 0px'
6
+
7
+ console.groupCollapsed(
8
+ `%c#graphql-ts-client %c${logInfo.kind} %c${logInfo.queryName} %c(${logInfo.duration.toFixed(2)}ms)`,
9
+ 'color: #f90',
10
+ 'color: #999',
11
+ `color: ${logInfo.response ? 'unset' : '#f00'}; font-weight: bold`,
12
+ 'color: #999'
13
+ )
14
+
15
+ console.groupCollapsed(`%cQuery ${identifier}`, 'color: #999', identifierStyles)
16
+ console.log(logInfo.formatGraphQL(logInfo.query) + identifier, identifierStyles)
17
+ console.groupEnd()
18
+ console.groupCollapsed(`%cVariables ${identifier}`, 'color: #999', identifierStyles)
19
+ console.log(JSON.stringify(logInfo.variables, null, ' ') + identifier, identifierStyles)
20
+ console.groupEnd()
21
+ console.groupCollapsed(`%cTrace ${identifier}`, 'color: #999', identifierStyles)
22
+ console.trace(identifier, identifierStyles)
23
+ console.groupEnd()
24
+
25
+ if (logInfo.response) {
26
+ console.log('%cResponse'.padEnd(15, ' ') + identifier, 'color: #999', identifierStyles, logInfo.response)
27
+ }
28
+ if (logInfo.error) {
29
+ console.log('%cError'.padEnd(15, ' ') + identifier, 'color: #999', identifierStyles, logInfo.error)
30
+ }
31
+
32
+ console.groupEnd()
33
+ }
@@ -0,0 +1,71 @@
1
+ import { ApolloServer, gql } from 'apollo-server'
2
+
3
+ const typeDefs = gql`
4
+ scalar ISODate
5
+
6
+ enum BookType {
7
+ IPSUM
8
+ DOLOR
9
+ SIT
10
+ }
11
+
12
+ type Book {
13
+ title: String
14
+ author: String
15
+ type: BookType
16
+ dateCreated: ISODate
17
+ }
18
+
19
+ input BookSearchParamsAllOptional {
20
+ title: String
21
+ author: String
22
+ createdAfter: ISODate
23
+ }
24
+
25
+ input BookSearchParamsSomeRequired {
26
+ title: String!
27
+ author: String
28
+ }
29
+
30
+ type Query {
31
+ booksWithoutParams: [Book]
32
+ booksWithOptionalParams(params: BookSearchParamsAllOptional! = {}): [Book]
33
+ booksWithRequiredParams(params: BookSearchParamsSomeRequired!): [Book]
34
+ failingQuery(id: String!): String
35
+ }
36
+ `
37
+
38
+ const books = [
39
+ {
40
+ title: 'The Awakening',
41
+ author: 'Kate Chopin',
42
+ },
43
+ {
44
+ title: 'City of Glass',
45
+ author: 'Paul Auster',
46
+ },
47
+ ]
48
+
49
+ function filterBooks(params: { title?: string; author?: string }) {
50
+ return books.filter(
51
+ book => (!params.title || book.title.includes(params.title)) && (!params.author || book.author.includes(params.author))
52
+ )
53
+ }
54
+
55
+ const resolvers = {
56
+ Query: {
57
+ booksWithoutParams: () => books,
58
+ booksWithOptionalParams: (_: any, { params = {} }: { params: { title?: string; author?: string } }) => filterBooks(params),
59
+ booksWithRequiredParams: (_: any, { params }: { params: { title: string; author?: string } }) => filterBooks(params),
60
+ failingQuery: () => {
61
+ throw new Error('Failed lorem ipsum dolor')
62
+ },
63
+ },
64
+ }
65
+
66
+ const testServer = new ApolloServer({
67
+ typeDefs,
68
+ resolvers,
69
+ })
70
+
71
+ export const startServer = () => testServer.listen(4123).then(({ url }) => ({ url, server: testServer }))
package/src/types.ts ADDED
@@ -0,0 +1,114 @@
1
+ export type Maybe<T> = null | undefined | T
2
+ export type Defined<T> = Exclude<T, undefined>
3
+
4
+ export type ResponseData = {
5
+ data: any
6
+ warnings: any
7
+ headers: any
8
+ status?: number
9
+ errors: {
10
+ message: string
11
+ }[]
12
+ }
13
+
14
+ export type ResponseListenerInfo = {
15
+ queryName: string
16
+ query: string
17
+ variables: any
18
+ response: ResponseData
19
+ }
20
+ export type IResponseListener = (info: ResponseListenerInfo) => void | Promise<void>
21
+
22
+ type ArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never
23
+
24
+ export type Projection<Selection, Base, E = never> = Base extends Array<any>
25
+ ? ArrayElement<Base> extends Date | string | number | boolean | null | undefined | E
26
+ ? ArrayElement<Base>[]
27
+ : Projection<Defined<Selection>, ArrayElement<Base>, E>[]
28
+ : Base extends Date | string | number | boolean | null | E
29
+ ? // Is primitive and extends undefined
30
+ Selection extends undefined
31
+ ? Base | undefined
32
+ : Base
33
+ : {
34
+ [k in keyof Selection & keyof Base]: Selection[k] extends boolean
35
+ ? Base[k]
36
+ : Base[k] extends Array<infer A>
37
+ ? Projection<Defined<Selection[k]>, A, E>[]
38
+ : Projection<Defined<Selection[k]>, Base[k], E>
39
+ }
40
+
41
+ export type Unpacked<T> = T extends (infer U)[]
42
+ ? U
43
+ : T extends (...args: any[]) => infer U
44
+ ? U
45
+ : T extends Promise<infer U>
46
+ ? U
47
+ : T
48
+
49
+ export type Replacement<M extends [any, any], T> = M extends any ? ([T] extends [M[0]] ? M[1] : never) : never
50
+
51
+ export type DeepReplace<T, Ignore, M extends [any, any]> = {
52
+ [P in keyof T]: T[P] extends M[0]
53
+ ? T[P] extends Ignore
54
+ ? T[P] extends object
55
+ ? DeepReplace<T[P], Ignore, M>
56
+ : T[P]
57
+ : Replacement<M, T[P]>
58
+ : T[P] extends object
59
+ ? DeepReplace<T[P], Ignore, M>
60
+ : T[P]
61
+ }
62
+
63
+ export type RawEndpoint<I, O, E> = <S extends I>(
64
+ jsonQuery?: S
65
+ ) => Promise<{ data: Projection<S, O, E>; errors: any[]; warnings: any[]; headers: any; status: any }>
66
+
67
+ export type JsonOutput<O, ToBeIgnored> = DeepReplace<O, ToBeIgnored, [string | Date, string]>
68
+
69
+ export type Endpoint<I, O, E> = (<S extends I>(jsonQuery?: S) => Promise<Projection<S, JsonOutput<O, E>, E>>) & {
70
+ memo: <S extends I>(jsonQuery?: S) => Promise<Projection<S, JsonOutput<O, E>, E>>
71
+ memoRaw: RawEndpoint<I, JsonOutput<O, E>, E>
72
+ raw: RawEndpoint<I, JsonOutput<O, E>, E>
73
+ }
74
+
75
+ export type ClientConfig = {
76
+ url: string
77
+ headers: {
78
+ [key: string]: string
79
+ }
80
+ retryConfig: {
81
+ max: number
82
+ waitBeforeRetry?: number
83
+ before: IResponseListener
84
+ }
85
+ }
86
+
87
+ export type LogInfo = {
88
+ query: string
89
+ variables: any
90
+ formatGraphQL: any
91
+ kind: string
92
+ queryName: string
93
+ response?: any
94
+ error?: Error
95
+ duration: number
96
+ }
97
+
98
+ export class GraphQLClientError extends Error {
99
+ responseData: ResponseData
100
+
101
+ constructor(responseData: ResponseData) {
102
+ super()
103
+ this.responseData = responseData
104
+ Object.setPrototypeOf(this, GraphQLClientError.prototype)
105
+ }
106
+
107
+ get message(): string {
108
+ return this.response.errors.map(it => it.message).join(';\n')
109
+ }
110
+
111
+ get response(): ResponseData {
112
+ return this.responseData
113
+ }
114
+ }