1
- import { parse } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
2
-
3
- // Strip insignificant whitespace
4
- // Note that this could do a lot more, such as reorder fields etc.
5
- const normalize = ( x : string ) => x . replace ( / [ \s , ] + / g, ' ' ) . trim ( )
1
+ import { parse , DocumentNode , DefinitionNode , Location } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
6
2
7
3
// A map docString -> graphql document
8
- let docCache : any = { }
4
+ const docCache = new Map < string , DocumentNode > ( )
9
5
10
6
// A map fragmentName -> [normalized source]
11
- let fragmentSourceMap : any = { }
7
+ const fragmentSourceMap = new Map < string , Set < string > > ( )
12
8
13
- function cacheKeyFromLoc ( loc : any ) {
14
- return normalize ( loc . source . body . substring ( loc . start , loc . end ) )
9
+ let printFragmentWarnings = true
10
+ let experimentalFragmentVariables = false
11
+
12
+ // Strip insignificant whitespace
13
+ // Note that this could do a lot more, such as reorder fields etc.
14
+ function normalize ( string : string ) {
15
+ return string . replace ( / [ \s , ] + / g, ' ' ) . trim ( )
15
16
}
16
17
17
- // For testing.
18
- export function resetCaches ( ) {
19
- docCache = { }
20
- fragmentSourceMap = { }
18
+ function cacheKeyFromLoc ( loc : Location ) {
19
+ return normalize ( loc . source . body . substring ( loc . start , loc . end ) )
21
20
}
22
21
23
22
// Take a unstripped parsed document (query/mutation or even fragment), and
24
23
// check all fragment definitions, checking for name->source uniqueness.
25
24
// We also want to make sure only unique fragments exist in the document.
26
- let printFragmentWarnings = true
27
- function processFragments ( ast : any ) {
28
- const astFragmentMap : any = { }
29
- const definitions : any [ ] = [ ]
30
-
31
- for ( let i = 0 ; i < ast . definitions . length ; i ++ ) {
32
- const fragmentDefinition = ast . definitions [ i ]
25
+ function processFragments ( ast : DocumentNode ) {
26
+ const seenKeys = new Set < string > ( )
27
+ const definitions : DefinitionNode [ ] = [ ]
33
28
29
+ ast . definitions . forEach ( ( fragmentDefinition ) => {
34
30
if ( fragmentDefinition . kind === 'FragmentDefinition' ) {
35
31
const fragmentName = fragmentDefinition . name . value
36
- const sourceKey = cacheKeyFromLoc ( fragmentDefinition . loc )
32
+ const sourceKey = cacheKeyFromLoc ( fragmentDefinition . loc ! )
37
33
38
34
// We know something about this fragment
39
- if ( fragmentSourceMap . hasOwnProperty ( fragmentName ) && ! fragmentSourceMap [ fragmentName ] [ sourceKey ] ) {
35
+ let sourceKeySet = fragmentSourceMap . get ( fragmentName ) !
36
+ if ( sourceKeySet && ! sourceKeySet . has ( sourceKey ) ) {
40
37
// this is a problem because the app developer is trying to register another fragment with
41
38
// the same name as one previously registered. So, we tell them about it.
42
39
if ( printFragmentWarnings ) {
@@ -48,122 +45,123 @@ function processFragments(ast: any) {
48
45
'this in the docs: http://dev.apollodata.com/core/fragments.html#unique-names'
49
46
)
50
47
}
51
-
52
- fragmentSourceMap [ fragmentName ] [ sourceKey ] = true
53
- } else if ( ! fragmentSourceMap . hasOwnProperty ( fragmentName ) ) {
54
- fragmentSourceMap [ fragmentName ] = { }
55
- fragmentSourceMap [ fragmentName ] [ sourceKey ] = true
48
+ } else if ( ! sourceKeySet ) {
49
+ fragmentSourceMap . set ( fragmentName , ( sourceKeySet = new Set ( ) ) )
56
50
}
57
51
58
- if ( ! astFragmentMap [ sourceKey ] ) {
59
- astFragmentMap [ sourceKey ] = true
52
+ sourceKeySet . add ( sourceKey )
53
+
54
+ if ( ! seenKeys . has ( sourceKey ) ) {
55
+ seenKeys . add ( sourceKey )
60
56
definitions . push ( fragmentDefinition )
61
57
}
62
58
} else {
63
59
definitions . push ( fragmentDefinition )
64
60
}
65
- }
66
-
67
- ast . definitions = definitions
68
- return ast
69
- }
61
+ } )
70
62
71
- export function disableFragmentWarnings ( ) {
72
- printFragmentWarnings = false
63
+ return {
64
+ ...ast ,
65
+ definitions
66
+ }
73
67
}
74
68
75
- function stripLoc ( doc : any , removeLocAtThisLevel : any ) {
76
- let docType = Object . prototype . toString . call ( doc )
69
+ function stripLoc ( doc : DocumentNode ) {
70
+ const workSet = new Set < Record < string , any > > ( doc . definitions )
77
71
78
- if ( docType === '[object Array]' ) {
79
- return doc . map ( function ( d : any ) {
80
- return stripLoc ( d , removeLocAtThisLevel )
72
+ workSet . forEach ( ( node ) => {
73
+ if ( node . loc ) delete node . loc
74
+ Object . keys ( node ) . forEach ( ( key ) => {
75
+ const value = node [ key ]
76
+ if ( value && typeof value === 'object' ) {
77
+ workSet . add ( value )
78
+ }
81
79
} )
82
- }
83
-
84
- if ( docType !== '[object Object]' ) {
85
- throw new Error ( 'Unexpected input.' )
86
- }
87
-
88
- // We don't want to remove the root loc field so we can use it
89
- // for fragment substitution (see below)
90
- if ( removeLocAtThisLevel && doc . loc ) {
91
- delete doc . loc
92
- }
80
+ } )
93
81
94
- // https://github.com/apollographql/graphql-tag/issues/40
95
- if ( doc . loc ) {
96
- delete doc . loc . startToken
97
- delete doc . loc . endToken
82
+ const loc = doc . loc as Record < string , any >
83
+ if ( loc ) {
84
+ delete loc . startToken
85
+ delete loc . endToken
98
86
}
99
87
100
- const keys = Object . keys ( doc )
101
- let key
102
- let value
103
- let valueType
104
-
105
- for ( key in keys ) {
106
- if ( keys . hasOwnProperty ( key ) ) {
107
- value = doc [ keys [ key ] ]
108
- valueType = Object . prototype . toString . call ( value )
88
+ return doc
89
+ }
109
90
110
- if ( valueType === '[object Object]' || valueType === '[object Array]' ) {
111
- doc [ keys [ key ] ] = stripLoc ( value , true )
112
- }
91
+ function parseDocument ( source : string ) {
92
+ var cacheKey = normalize ( source )
93
+ if ( ! docCache . has ( cacheKey ) ) {
94
+ const parsed = parse ( source , {
95
+ experimentalFragmentVariables
96
+ } )
97
+ if ( ! parsed || parsed . kind !== 'Document' ) {
98
+ throw new Error ( 'Not a valid GraphQL document.' )
113
99
}
100
+ docCache . set (
101
+ cacheKey ,
102
+ // check that all "new" fragments inside the documents are consistent with
103
+ // existing fragments of the same name
104
+ stripLoc ( processFragments ( parsed ) )
105
+ )
114
106
}
115
-
116
- return doc
107
+ return docCache . get ( cacheKey ) !
117
108
}
118
109
119
- let experimentalFragmentVariables = false
120
-
121
- function parseDocument ( doc : string ) {
122
- const cacheKey = normalize ( doc )
123
-
124
- if ( docCache [ cacheKey ] ) {
125
- return docCache [ cacheKey ]
110
+ /**
111
+ * Create a GraphQL AST from template literal
112
+ * @param literals
113
+ * @param args
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * import { buildASTSchema, graphql } from 'https://deno.land/x/graphql_deno@v15.0.0/mod.ts'
118
+ * import { gql } from 'https://deno.land/x/graphql_tag/mod.ts'
119
+ *
120
+ * const typeDefs = gql`
121
+ * type Query {
122
+ * hello: String
123
+ * }
124
+ *`
125
+ *
126
+ * const query = `{ hello }`
127
+ *
128
+ * const resolvers = { hello: () => 'world' }
129
+ *
130
+ * console.log(await graphql(buildASTSchema(typeDefs), query, resolvers))
131
+ * ```
132
+ */
133
+ export function gql ( literals : string | readonly string [ ] , ...args : any [ ] ) {
134
+ if ( typeof literals === 'string' ) {
135
+ literals = [ literals ]
126
136
}
127
137
128
- let parsed = parse ( doc , {
129
- experimentalFragmentVariables
138
+ let result = literals [ 0 ]
139
+
140
+ args . forEach ( ( arg , i ) => {
141
+ if ( arg && arg . kind === 'Document' ) {
142
+ result += arg . loc . source . body
143
+ } else {
144
+ result += arg
145
+ }
146
+ result += literals [ i + 1 ]
130
147
} )
131
- if ( ! parsed || parsed . kind !== 'Document' ) {
132
- throw new Error ( 'Not a valid GraphQL document.' )
133
- }
134
148
135
- // check that all "new" fragments inside the documents are consistent with
136
- // existing fragments of the same name
137
- parsed = processFragments ( parsed )
138
- parsed = stripLoc ( parsed , false )
139
- docCache [ cacheKey ] = parsed
149
+ return parseDocument ( result )
150
+ }
140
151
141
- return parsed
152
+ export function resetCaches ( ) {
153
+ docCache . clear ( )
154
+ fragmentSourceMap . clear ( )
142
155
}
143
156
144
- export function enableExperimentalFragmentletiables ( ) {
157
+ export function disableFragmentWarnings ( ) {
158
+ printFragmentWarnings = false
159
+ }
160
+
161
+ export function enableExperimentalFragmentVariables ( ) {
145
162
experimentalFragmentVariables = true
146
163
}
147
164
148
165
export function disableExperimentalFragmentVariables ( ) {
149
166
experimentalFragmentVariables = false
150
167
}
151
-
152
- // XXX This should eventually disallow arbitrary string interpolation, like Relay does
153
- export function gql ( ...args : any [ ] ) {
154
- // We always get literals[0] and then matching post literals for each arg given
155
- const literals = args [ 0 ]
156
- let result = typeof literals === 'string' ? literals : literals [ 0 ]
157
-
158
- for ( let i = 1 ; i < args . length ; i ++ ) {
159
- if ( args [ i ] && args [ i ] . kind && args [ i ] . kind === 'Document' ) {
160
- result += args [ i ] . loc . source . body
161
- } else {
162
- result += args [ i ]
163
- }
164
-
165
- result += literals [ i ]
166
- }
167
-
168
- return parseDocument ( result )
169
- }
0 commit comments