Skip to content

Commit 4ae922d

Browse files
committed
feat: validate GraphQL documents
1 parent 21494de commit 4ae922d

19 files changed

Lines changed: 159 additions & 178 deletions

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,20 @@ Configure this in your `package.json`:
469469
}
470470
```
471471
472+
## `validate` (default: `true`)
473+
474+
Whether to validate each GraphQL document before processing it.
475+
Excludes `NoUnusedFragmentsRule` from validation, in case you put fragment definitions
476+
in separate template literals.
477+
478+
Right now this is only configurable in your `package.json`:
479+
480+
```
481+
"graphql-typegen": {
482+
"validate": false
483+
}
484+
```
485+
472486
## `addTypename` (default: `true`)
473487
474488
Places this may be configured, in order of increasing precendence:

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@
8585
"devDependencies": {
8686
"@babel/cli": "^7.1.5",
8787
"@babel/core": "^7.1.6",
88-
"@babel/node": "^7.8.4",
8988
"@babel/plugin-proposal-class-properties": "^7.1.0",
9089
"@babel/plugin-proposal-export-default-from": "^7.0.0",
9190
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
@@ -136,9 +135,8 @@
136135
},
137136
"dependencies": {
138137
"@babel/runtime": "^7.1.5",
139-
"flatted": "^2.0.1",
140138
"fs-extra": "^8.1.0",
141-
"graphql": "^14.5.8",
139+
"graphql": "^14.6.0",
142140
"graphql-tag": "^2.10.1",
143141
"jscodeshift-add-imports": "^1.0.3",
144142
"jscodeshift-choose-parser": "^1.0.0",

src/graphql-typegen-async.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ module.exports = async function graphqlTypegenAsync(
2323
}
2424
const config = applyConfigDefaults(Object.assign(packageConf, options))
2525
const { schemaFile, server } = config
26-
const schema = await analyzeSchema({ schemaFile, server })
27-
return graphqlTypegenCore(fileInfo, api, options, { schema })
26+
const { analyzed, schema } = await analyzeSchema({
27+
schemaFile,
28+
server,
29+
})
30+
return graphqlTypegenCore(fileInfo, api, options, {
31+
analyzedSchema: analyzed,
32+
schema,
33+
})
2834
}

src/graphql-typegen.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ module.exports = function graphqlTypegen(
2323
}
2424
const config = applyConfigDefaults(Object.assign(packageConf, options))
2525
const { schemaFile, server } = config
26-
const schema = analyzeSchemaSync({ schemaFile, server })
27-
return graphqlTypegenCore(fileInfo, api, options, { schema })
26+
const { analyzed, schema } = analyzeSchemaSync({
27+
schemaFile,
28+
server,
29+
})
30+
return graphqlTypegenCore(fileInfo, api, options, {
31+
analyzedSchema: analyzed,
32+
schema,
33+
})
2834
}

src/internal/analyzeSchema.ts

Lines changed: 53 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,13 @@
11
#! /usr/bin/env babel-node --extensions .js,.ts
22
/* eslint-disable @typescript-eslint/no-use-before-define */
33

4-
import gql from 'graphql-tag'
54
import * as graphql from 'graphql'
65
import superagent from 'superagent'
76
import getConfigDirectives, { ConfigDirectives } from './getConfigDirectives'
87
import { execFileSync } from 'child_process'
8+
import { getIntrospectionQuery } from 'graphql/utilities/introspectionQuery'
99
import * as fs from 'fs'
1010
import * as path from 'path'
11-
import flatted from 'flatted'
12-
13-
const typesQuery = gql`
14-
fragment typeInfo on __Type {
15-
name
16-
kind
17-
# Fetch really deep just in case someone is doing something like [[[Float!]!]!]!...
18-
# Haven't devised a plan to deal with arbitrarily deep types yet
19-
ofType {
20-
name
21-
kind
22-
ofType {
23-
name
24-
kind
25-
ofType {
26-
name
27-
kind
28-
ofType {
29-
name
30-
kind
31-
ofType {
32-
name
33-
kind
34-
ofType {
35-
name
36-
kind
37-
ofType {
38-
name
39-
kind
40-
ofType {
41-
name
42-
}
43-
}
44-
}
45-
}
46-
}
47-
}
48-
}
49-
}
50-
}
51-
query getTypes {
52-
__schema {
53-
types {
54-
kind
55-
name
56-
description
57-
enumValues {
58-
name
59-
description
60-
}
61-
interfaces {
62-
name
63-
description
64-
}
65-
possibleTypes {
66-
...typeInfo
67-
}
68-
fields {
69-
name
70-
description
71-
args {
72-
name
73-
description
74-
type {
75-
...typeInfo
76-
}
77-
}
78-
type {
79-
...typeInfo
80-
}
81-
}
82-
inputFields {
83-
name
84-
description
85-
type {
86-
...typeInfo
87-
}
88-
}
89-
}
90-
}
91-
}
92-
`
9311

9412
export type TypeKind =
9513
| 'SCALAR'
@@ -176,13 +94,14 @@ export type AnalyzedType = {
17694
}
17795

17896
function analyzeTypes(
179-
introspectionTypes: Array<IntrospectionType>,
97+
data: graphql.IntrospectionQuery,
18098
{
18199
cwd,
182100
}: {
183101
cwd: string
184102
}
185103
): Record<string, AnalyzedType> {
104+
const introspectionTypes: IntrospectionType[] = data.__schema.types as any
186105
function getDescriptionDirectives(
187106
description: string | undefined
188107
): ConfigDirectives {
@@ -340,55 +259,73 @@ function analyzeTypes(
340259
}
341260

342261
export type AnalyzedSchema = Record<string, AnalyzedType>
262+
export type AnalyzeResult = {
263+
analyzed: AnalyzedSchema
264+
schema: graphql.GraphQLSchema
265+
}
343266

344-
export default async function analyzeSchema({
267+
export async function getIntrospectionData({
345268
schema,
346269
schemaFile,
347270
server,
348271
}: {
349272
schemaFile?: string
350273
schema?: graphql.GraphQLSchema
351274
server?: string
352-
}): Promise<AnalyzedSchema> {
353-
let result: graphql.ExecutionResult<{
354-
__schema: { types: IntrospectionType[] }
355-
}>
275+
}): Promise<graphql.IntrospectionQuery> {
356276
if (schemaFile)
357277
schema = graphql.buildSchema(fs.readFileSync(schemaFile, 'utf8'))
358-
if (schema) result = await graphql.execute(schema, typesQuery)
278+
const introspectionQuery = graphql.parse(getIntrospectionQuery())
279+
let introspection
280+
if (schema) introspection = await graphql.execute(schema, introspectionQuery)
359281
else if (server) {
360-
result = (
282+
introspection = (
361283
await superagent
362284
.post(server)
363285
.type('json')
364286
.accept('json')
365287
.send({
366-
query: typesQuery,
288+
query: introspectionQuery,
367289
})
368290
).body
369291
} else {
370292
throw new Error('schemaFile or server must be configured')
371293
}
372-
const { data } = result
373-
if (!data) throw new Error('failed to get introspection query data')
374-
const {
375-
__schema: { types },
376-
} = data
294+
if (introspection.errors) {
295+
throw new Error(
296+
`failed to get introspection data:\n${introspection.errors.join('\n')}`
297+
)
298+
}
299+
return introspection.data
300+
}
301+
302+
export default async function analyzeSchema(options: {
303+
schemaFile?: string
304+
schema?: graphql.GraphQLSchema
305+
server?: string
306+
}): Promise<AnalyzeResult> {
307+
const data = await getIntrospectionData(options)
308+
const { schemaFile } = options
309+
const schema = options.schema || graphql.buildClientSchema(data)
310+
377311
const cwd = schemaFile ? path.dirname(schemaFile) : process.cwd()
378-
return analyzeTypes(types, { cwd })
312+
return {
313+
analyzed: analyzeTypes(data, { cwd }),
314+
schema,
315+
}
379316
}
380317

381318
const schemaFileTimestamps: Map<string, Date> = new Map()
382-
const schemaCache: Map<string, AnalyzedSchema> = new Map()
319+
const schemaCache: Map<string, AnalyzeResult> = new Map()
383320

384321
/**
385-
* Uses execFileSync to run analyzeSchema synchronously,
322+
* Uses execFileSync to analyze the schema synchronously,
386323
* since jscodeshift transforms unfortunately have to be sync right now
387324
*/
388325
export function analyzeSchemaSync(options: {
389326
schemaFile?: string
390327
server?: string
391-
}): AnalyzedSchema {
328+
}): AnalyzeResult {
392329
const file = options.schemaFile
393330
if (file) {
394331
const timestamp = schemaFileTimestamps.get(file)
@@ -403,20 +340,30 @@ export function analyzeSchemaSync(options: {
403340
}
404341
}
405342

406-
const schema = flatted.parse(
343+
const data = JSON.parse(
407344
execFileSync(
408-
require.resolve('./runAnalyzeSchemaSync'),
409-
[JSON.stringify({ ...options, target: __filename })],
345+
require.resolve('./runSync'),
346+
[
347+
JSON.stringify({
348+
...options,
349+
target: __filename,
350+
method: 'getIntrospectionData',
351+
}),
352+
],
410353
{
411354
encoding: 'utf8',
412355
maxBuffer: 256 * 1024 * 1024,
413356
}
414357
)
415358
)
359+
const cwd = file ? path.dirname(file) : process.cwd()
360+
const schema = graphql.buildClientSchema(data)
361+
const analyzed = analyzeTypes(data, { cwd })
362+
const result = { analyzed, schema }
416363
if (file) {
417364
const latest = fs.statSync(file).mtime
418365
schemaFileTimestamps.set(file, latest)
419-
schemaCache.set(file, schema)
366+
schemaCache.set(file, result)
420367
}
421-
return schema
368+
return result
422369
}

src/internal/config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export type Config = {
3838
* const data: QueryRenderProps<QueryData, QueryVariables> = useQuery(query, {variables: {id}})
3939
*/
4040
useFunctionTypeArguments?: boolean
41+
/**
42+
* Whether to validate GraphQL queries
43+
*/
44+
validate?: boolean
4145
}
4246

4347
export type DefaultedConfig = {
@@ -77,7 +81,11 @@ export type DefaultedConfig = {
7781
*
7882
* const data: QueryRenderProps<QueryData, QueryVariables> = useQuery(query, {variables: {id}})
7983
*/
80-
useFunctionTypeArguments?: boolean
84+
useFunctionTypeArguments: boolean
85+
/**
86+
* Whether to validate GraphQL queries
87+
*/
88+
validate: boolean
8189
}
8290

8391
export function applyConfigDefaults(config: Config): DefaultedConfig {
@@ -87,6 +95,7 @@ export function applyConfigDefaults(config: Config): DefaultedConfig {
8795
const useReadOnlyTypes = config.useReadOnlyTypes ?? false
8896
const objectType = config.objectType || 'ambiguous'
8997
const useFunctionTypeArguments = config.useFunctionTypeArguments ?? true
98+
const validate = config.validate ?? true
9099

91100
const result = {
92101
schemaFile,
@@ -96,6 +105,7 @@ export function applyConfigDefaults(config: Config): DefaultedConfig {
96105
useReadOnlyTypes,
97106
objectType,
98107
useFunctionTypeArguments,
108+
validate,
99109
}
100110

101111
for (const key in config) {

src/internal/generateFlowTypesFromDocument.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ type GeneratedTypes = {
4545
fragment: Record<string, TypeAlias>
4646
}
4747

48-
export default function graphqlToFlow({
49-
query,
48+
export default function generateFlowTypesFromDocument({
49+
document,
5050
file,
5151
types,
5252
config: _config,
5353
getMutationFunction,
5454
j,
5555
}: {
56-
query: string | graphql.DocumentNode
56+
document: graphql.DocumentNode
5757
file: string
5858
types: Record<string, AnalyzedType>
5959
config: Config
@@ -217,9 +217,6 @@ export default function graphqlToFlow({
217217
.replace(/\..+$/, '')
218218
: null
219219

220-
const document: graphql.DocumentNode =
221-
typeof query === 'string' ? (query = graphql.parse(query)) : query
222-
223220
const fragments: Map<string, TypeAlias> = new Map()
224221
const statements: TypeAlias[] = []
225222
const generatedTypes: GeneratedTypes = {

src/internal/getConfigDirectives.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,11 @@ export default function getConfigDirectives(
9595
}
9696
}
9797
}
98-
return { external, extract, objectType, useReadOnlyTypes, addTypename }
98+
return {
99+
external,
100+
extract,
101+
objectType,
102+
useReadOnlyTypes,
103+
addTypename,
104+
}
99105
}

0 commit comments

Comments
 (0)