1+ import {
2+ ArgumentNode ,
3+ GraphQLArgument ,
4+ GraphQLFieldMap ,
5+ GraphQLNamedType ,
6+ GraphQLObjectType ,
7+ GraphQLOutputType ,
8+ isCompositeType ,
9+ isEnumType ,
10+ isInterfaceType ,
11+ isListType ,
12+ isNonNullType ,
13+ isObjectType ,
14+ isScalarType ,
15+ isUnionType ,
16+ Kind ,
17+ ValueNode ,
18+ } from 'graphql' ;
19+ import { Maybe } from 'graphql/jsutils/Maybe' ;
20+ import { ObjMap } from 'graphql/jsutils/ObjMap' ;
121import { GraphQLSchema } from 'graphql/type/schema' ;
222
23+ export const KEYWORDS = [ 'first' , 'last' , 'limit' ] ;
24+
25+ // These variables exist to provide a default value for typescript when accessing a weight
26+ // since all props are optioal in TypeWeightConfig
27+ const DEFAULT_MUTATION_WEIGHT = 10 ;
28+ const DEFAULT_OBJECT_WEIGHT = 1 ;
29+ const DEFAULT_SCALAR_WEIGHT = 0 ;
30+ const DEFAULT_CONNECTION_WEIGHT = 2 ;
31+ const DEFAULT_QUERY_WEIGHT = 1 ;
32+
33+ // FIXME: What about Interface defaults
34+
335/**
436 * Default TypeWeight Configuration:
537 * mutation: 10
@@ -8,19 +40,168 @@ import { GraphQLSchema } from 'graphql/type/schema';
840 * connection: 2
941 */
1042export const defaultTypeWeightsConfig : TypeWeightConfig = {
11- mutation : 10 ,
12- object : 1 ,
13- scalar : 0 ,
14- connection : 2 ,
43+ mutation : DEFAULT_MUTATION_WEIGHT ,
44+ object : DEFAULT_OBJECT_WEIGHT ,
45+ scalar : DEFAULT_SCALAR_WEIGHT ,
46+ connection : DEFAULT_CONNECTION_WEIGHT ,
1547} ;
1648
49+ /**
50+ * Parses the Query type in the provided schema object and outputs a new TypeWeightObject
51+ * @param schema
52+ * @param typeWeightObject
53+ * @param typeWeights
54+ * @returns
55+ */
56+ function parseQuery (
57+ schema : GraphQLSchema ,
58+ typeWeightObject : TypeWeightObject ,
59+ typeWeights : TypeWeightConfig
60+ ) : TypeWeightObject {
61+ // Get any Query fields (these are the queries that the API exposes)
62+ const queryType : Maybe < GraphQLObjectType > = schema . getQueryType ( ) ;
63+
64+ if ( ! queryType ) return typeWeightObject ;
65+
66+ const result : TypeWeightObject = { ...typeWeightObject } ;
67+
68+ result . query = {
69+ weight : typeWeights . query || DEFAULT_QUERY_WEIGHT ,
70+ // fields gets populated with the query fields and associated weights.
71+ fields : { } ,
72+ } ;
73+
74+ const queryFields : GraphQLFieldMap < any , any > = queryType . getFields ( ) ;
75+
76+ Object . keys ( queryFields ) . forEach ( ( field ) => {
77+ // this is the type the query resolves to
78+ const resolveType : GraphQLOutputType = queryFields [ field ] . type ;
79+
80+ // check if any of our keywords 'first', 'last', 'limit' exist in the arg list
81+ queryFields [ field ] . args . forEach ( ( arg : GraphQLArgument ) => {
82+ // If query has an argument matching one of the limiting keywords and resolves to a list then the weight of the query
83+ // should be dependent on both the weight of the resolved type and the limiting argument.
84+ if ( KEYWORDS . includes ( arg . name ) && isListType ( resolveType ) ) {
85+ // Get the type that comprises the list
86+ const listType = resolveType . ofType ;
87+
88+ // FIXME: This function can only handle integer arguments for one of the keyword params.
89+ // In order to handle variable arguments, we may need to accept a second parameter so that the complexity aglorithm
90+ // can pass in the variables as well.
91+ // FIXME: If the weight of the resolveType is 0 the weight can be set to 0 rather than a function.
92+ result . query . fields [ field ] = ( args : ArgumentNode [ ] ) : number => {
93+ // TODO: Test this function
94+ const limitArg : ArgumentNode | undefined = args . find (
95+ ( cur ) => cur . name . value === arg . name
96+ ) ;
97+
98+ if ( limitArg ) {
99+ const node : ValueNode = limitArg . value ;
100+
101+ if ( Kind . INT === node . kind ) {
102+ const multiplier = Number ( node . value || arg . defaultValue ) ;
103+ const weight = isCompositeType ( listType )
104+ ? result [ listType . name . toLowerCase ( ) ] . weight
105+ : typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ; // Note this includes enums
106+
107+ return weight * multiplier ;
108+ }
109+
110+ if ( Kind . VARIABLE === node . kind ) {
111+ // TODO: Get variable value and return
112+ // const multiplier: number =
113+ // return result[listType.name.toLowerCase()].weight * multiplier;
114+ throw new Error (
115+ 'ERROR: buildTypeWeights Variable arge values not supported;'
116+ ) ;
117+ }
118+ }
119+
120+ // FIXME: The list is unbounded. Return the object weight for
121+ throw new Error (
122+ `ERROR: buildTypeWeights: Unbouned list complexity not supported. Query results should be limited with ${ KEYWORDS } `
123+ ) ;
124+ } ;
125+ }
126+ } ) ;
127+
128+ // if the field is a scalar or an enum set weight accordingly
129+ if ( isScalarType ( resolveType ) || isEnumType ( resolveType ) ) {
130+ result . query . fields [ field ] = typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ;
131+ }
132+ } ) ;
133+ return result ;
134+ }
135+
136+ /**
137+ * Parses all types in the provided schema object excempt for Query, Mutation
138+ * and built in types that begin with '__' and outputs a new TypeWeightObject
139+ * @param schema
140+ * @param typeWeightObject
141+ * @param typeWeights
142+ * @returns
143+ */
144+ function parseTypes (
145+ schema : GraphQLSchema ,
146+ typeWeightObject : TypeWeightObject ,
147+ typeWeights : TypeWeightConfig
148+ ) : TypeWeightObject {
149+ const typeMap : ObjMap < GraphQLNamedType > = schema . getTypeMap ( ) ;
150+
151+ const result : TypeWeightObject = { ...typeWeightObject } ;
152+
153+ // Handle Object, Interface, Enum and Union types
154+ Object . keys ( typeMap ) . forEach ( ( type ) => {
155+ const typeName = type . toLowerCase ( ) ;
156+
157+ const currentType : GraphQLNamedType = typeMap [ type ] ;
158+ // Get all types that aren't Query or Mutation or a built in type that starts with '__'
159+ if ( type !== 'Query' && type !== 'Mutation' && ! type . startsWith ( '__' ) ) {
160+ if ( isObjectType ( currentType ) || isInterfaceType ( currentType ) ) {
161+ // Add the type and it's associated fields to the result
162+ result [ typeName ] = {
163+ fields : { } ,
164+ weight : typeWeights . object || DEFAULT_OBJECT_WEIGHT ,
165+ } ;
166+
167+ const fields = currentType . getFields ( ) ;
168+
169+ Object . keys ( fields ) . forEach ( ( field : string ) => {
170+ const fieldType : GraphQLOutputType = fields [ field ] . type ;
171+
172+ // Only scalars are considered here any other types should be references from the top level of the type weight object.
173+ if (
174+ isScalarType ( fieldType ) ||
175+ ( isNonNullType ( fieldType ) && isScalarType ( fieldType . ofType ) )
176+ ) {
177+ result [ typeName ] . fields [ field ] =
178+ typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ;
179+ }
180+ } ) ;
181+ } else if ( isEnumType ( currentType ) ) {
182+ result [ typeName ] = {
183+ fields : { } ,
184+ weight : typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ,
185+ } ;
186+ } else if ( isUnionType ( currentType ) ) {
187+ result [ typeName ] = {
188+ fields : { } ,
189+ weight : typeWeights . object || DEFAULT_OBJECT_WEIGHT ,
190+ } ;
191+ }
192+ }
193+ } ) ;
194+
195+ return result ;
196+ }
197+
17198/**
18199 * The default typeWeightsConfig object is based off of Shopifys implementation of query
19200 * cost analysis. Our function should input a users configuration of type weights or fall
20201 * back on shopifys settings. We can change this later.
21202 *
22203 * This function should
23- * - itreate through the schema object and create the typeWeightObject as described in the tests
204+ * - iterate through the schema object and create the typeWeightObject as described in the tests
24205 * - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
25206 *
26207 * @param schema
@@ -30,6 +211,23 @@ function buildTypeWeightsFromSchema(
30211 schema : GraphQLSchema ,
31212 typeWeightsConfig : TypeWeightConfig = defaultTypeWeightsConfig
32213) : TypeWeightObject {
33- throw Error ( `getTypeWeightsFromSchema is not implemented.` ) ;
214+ if ( ! schema ) throw new Error ( 'Missing Argument: schema is required' ) ;
215+
216+ // Merge the provided type weights with the default to account for missing values
217+ const typeWeights : TypeWeightConfig = {
218+ ...defaultTypeWeightsConfig ,
219+ ...typeWeightsConfig ,
220+ } ;
221+
222+ // Confirm that any custom weights are positive
223+ Object . entries ( typeWeights ) . forEach ( ( value : [ string , number ] ) => {
224+ if ( value [ 1 ] < 0 ) {
225+ throw new Error ( `Type weights cannot be negative. Received: ${ value [ 0 ] } : ${ value [ 1 ] } ` ) ;
226+ }
227+ } ) ;
228+
229+ const objectTypeWeights = parseTypes ( schema , { } , typeWeights ) ;
230+ return parseQuery ( schema , objectTypeWeights , typeWeights ) ;
34231}
232+
35233export default buildTypeWeightsFromSchema ;
0 commit comments