1+ import { query } from 'express' ;
12import {
23 ArgumentNode ,
34 GraphQLArgument ,
@@ -14,6 +15,7 @@ import {
1415 isObjectType ,
1516 isScalarType ,
1617 isUnionType ,
18+ Kind ,
1719 ValueNode ,
1820} from 'graphql' ;
1921import { Maybe } from 'graphql/jsutils/Maybe' ;
@@ -47,53 +49,111 @@ export const defaultTypeWeightsConfig: TypeWeightConfig = {
4749 connection : DEFAULT_CONNECTION_WEIGHT ,
4850} ;
4951
50- /**
51- * The default typeWeightsConfig object is based off of Shopifys implementation of query
52- * cost analysis. Our function should input a users configuration of type weights or fall
53- * back on shopifys settings. We can change this later.
54- *
55- * This function should
56- * - iterate through the schema object and create the typeWeightObject as described in the tests
57- * - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
58- *
59- * @param schema
60- * @param typeWeightsConfig Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
61- */
62- function buildTypeWeightsFromSchema (
52+ function parseQuery (
6353 schema : GraphQLSchema ,
64- typeWeightsConfig : TypeWeightConfig = defaultTypeWeightsConfig
54+ typeWeightObject : TypeWeightObject ,
55+ typeWeights : TypeWeightConfig
6556) : TypeWeightObject {
66- if ( ! schema ) throw new Error ( 'Must provide schema' ) ;
57+ // Get any Query fields (these are the queries that the API exposes)
58+ const queryType : Maybe < GraphQLObjectType > = schema . getQueryType ( ) ;
6759
68- // Merge the provided type weights with the default to account for missing values
69- const typeWeights : TypeWeightConfig = {
70- ...defaultTypeWeightsConfig ,
71- ...typeWeightsConfig ,
60+ if ( ! queryType ) return typeWeightObject ;
61+
62+ const result : TypeWeightObject = { ...typeWeightObject } ;
63+
64+ result . query = {
65+ weight : typeWeights . query || DEFAULT_QUERY_WEIGHT ,
66+ // fields gets populated with the query fields and associated weights.
67+ fields : { } ,
7268 } ;
7369
74- // Confirm that any custom weights are positive
75- Object . entries ( typeWeights ) . forEach ( ( value : [ string , number ] ) => {
76- if ( value [ 1 ] < 0 ) {
77- throw new Error ( `Type weights cannot be negative. Received: ${ value [ 0 ] } : ${ value [ 1 ] } ` ) ;
70+ const queryFields : GraphQLFieldMap < any , any > = queryType . getFields ( ) ;
71+
72+ Object . keys ( queryFields ) . forEach ( ( field ) => {
73+ // this is the type the query resolves to
74+ const resolveType : GraphQLOutputType = queryFields [ field ] . type ;
75+
76+ // check if any of our keywords 'first', 'last', 'limit' exist in the arg list
77+ queryFields [ field ] . args . forEach ( ( arg : GraphQLArgument ) => {
78+ // If query has an argument matching one of the limiting keywords and resolves to a list then the weight of the query
79+ // should be dependent on both the weight of the resolved type and the limiting argument.
80+ if ( KEYWORDS . includes ( arg . name ) && isListType ( resolveType ) ) {
81+ // Get the type that comprises the list
82+ const listType = resolveType . ofType ;
83+
84+ // Composite Types are Objects, Interfaces and Unions.
85+ if ( isCompositeType ( listType ) ) {
86+ // Set the field weight to a function that accepts
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+ result . query . fields [ field ] = ( args : ArgumentNode [ ] ) : number => {
92+ // TODO: Test this function
93+ const limitArg : ArgumentNode | undefined = args . find (
94+ ( cur ) => cur . name . value === arg . name
95+ ) ;
96+
97+ if ( limitArg ) {
98+ const node : ValueNode = limitArg . value ;
99+
100+ if ( Kind . INT === node . kind ) {
101+ const multiplier = Number ( node . value || arg . defaultValue ) ;
102+
103+ return result [ listType . name . toLowerCase ( ) ] . weight * multiplier ;
104+ }
105+
106+ if ( Kind . VARIABLE === node . kind ) {
107+ // TODO: Get variable value and return
108+ // const multiplier: number =
109+ // return result[listType.name.toLowerCase()].weight * multiplier;
110+ throw new Error (
111+ 'ERROR: buildTypeWeights Variable arge values not supported;'
112+ ) ;
113+ }
114+ }
115+
116+ // FIXME: The list is unbounded. Return the object weight for
117+ throw new Error (
118+ `ERROR: buildTypeWeights: Unbouned list complexity not supported. Query results should be limited with ${ KEYWORDS } `
119+ ) ;
120+ } ;
121+ } else {
122+ // TODO: determine the type of the list and use the appropriate weight
123+ // TODO: This should multiply as well
124+ result . query . fields [ field ] = typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ;
125+ }
126+ }
127+ } ) ;
128+
129+ // if the field is a scalar set weight accordingly
130+ // TODO: Allow config for enum weights
131+ if ( isScalarType ( resolveType ) || isEnumType ( resolveType ) ) {
132+ result . query . fields [ field ] = typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ;
78133 }
79134 } ) ;
135+ return result ;
136+ }
80137
81- const result : TypeWeightObject = { } ;
82-
138+ function parseTypes (
139+ schema : GraphQLSchema ,
140+ typeWeightObject : TypeWeightObject ,
141+ typeWeights : TypeWeightConfig
142+ ) : TypeWeightObject {
83143 const typeMap : ObjMap < GraphQLNamedType > = schema . getTypeMap ( ) ;
84144
145+ const result : TypeWeightObject = { ...typeWeightObject } ;
146+
85147 // Handle Object, Interface, Enum and Union types
86148 Object . keys ( typeMap ) . forEach ( ( type ) => {
149+ const typeName = type . toLowerCase ( ) ;
150+
87151 const currentType : GraphQLNamedType = typeMap [ type ] ;
88152 // Get all types that aren't Query or Mutation or a built in type that starts with '__'
89- if (
90- currentType . name !== 'Query' &&
91- currentType . name !== 'Mutation' &&
92- ! currentType . name . startsWith ( '__' )
93- ) {
153+ if ( type !== 'Query' && type !== 'Mutation' && ! type . startsWith ( '__' ) ) {
94154 if ( isObjectType ( currentType ) || isInterfaceType ( currentType ) ) {
95- // Add the type to the result
96- result [ type . toLowerCase ( ) ] = {
155+ // Add the type and it's associated fields to the result
156+ result [ typeName ] = {
97157 fields : { } ,
98158 weight : typeWeights . object || DEFAULT_OBJECT_WEIGHT ,
99159 } ;
@@ -102,108 +162,66 @@ function buildTypeWeightsFromSchema(
102162
103163 Object . keys ( fields ) . forEach ( ( field : string ) => {
104164 const fieldType : GraphQLOutputType = fields [ field ] . type ;
165+
166+ // Only scalars are considered here any other types should be references from the top level of the type weight object.
105167 if (
106168 isScalarType ( fieldType ) ||
107169 ( isNonNullType ( fieldType ) && isScalarType ( fieldType . ofType ) )
108170 ) {
109- result [ type . toLowerCase ( ) ] . fields [ field ] =
171+ result [ typeName ] . fields [ field ] =
110172 typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ;
111173 }
112174 } ) ;
113175 } else if ( isEnumType ( currentType ) ) {
114- result [ currentType . name . toLowerCase ( ) ] = {
176+ result [ typeName ] = {
115177 fields : { } ,
116178 weight : typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ,
117179 } ;
118180 } else if ( isUnionType ( currentType ) ) {
119- result [ currentType . name . toLowerCase ( ) ] = {
181+ result [ typeName ] = {
120182 fields : { } ,
121183 weight : typeWeights . object || DEFAULT_OBJECT_WEIGHT ,
122184 } ;
123185 }
124186 }
125187 } ) ;
126188
127- // Get any Query fields (these are the queries that the API exposes)
128- const queryType : Maybe < GraphQLObjectType > = schema . getQueryType ( ) ;
189+ return result ;
190+ }
129191
130- if ( queryType ) {
131- result . query = {
132- weight : typeWeights . query || DEFAULT_QUERY_WEIGHT ,
133- // fields gets populated with the query fields and associated weights.
134- fields : { } ,
135- } ;
136-
137- const queryFields : GraphQLFieldMap < any , any > = queryType . getFields ( ) ;
138-
139- Object . keys ( queryFields ) . forEach ( ( field ) => {
140- // this is the type the query resolves to
141- const resolveType : GraphQLOutputType = queryFields [ field ] . type ;
142-
143- // check if any of our keywords 'first', 'last', 'limit' exist in the arg list
144- queryFields [ field ] . args . forEach ( ( arg : GraphQLArgument ) => {
145- // If query has an argument matching one of the limiting keywords and resolves to a list then the weight of the query
146- // should be dependent on both the weight of the resolved type and the limiting argument.
147- if ( KEYWORDS . includes ( arg . name ) && isListType ( resolveType ) ) {
148- const defaultVal : number = < number > arg . defaultValue ;
149-
150- // Get the type that comprises the list
151- const listType = resolveType . ofType ;
152-
153- // Composite Types are Objects, Interfaces and Unions.
154- if ( isCompositeType ( listType ) ) {
155- // Set the field weight to a function that accepts
156- // TODO: Accept ArgumentNode[] and look for the arg we need.
157- // TODO: Test this function
158- result . query . fields [ field ] = ( args : ArgumentNode [ ] ) : number => {
159- // Function should receive object with arg, value as k, v pairs
160- // function iterate on this object looking for a keyword then returns
161- const limitArg : ArgumentNode | undefined = args . find (
162- ( cur ) => cur . name . value === arg . name
163- ) ;
164-
165- // FIXME: Need to use the value of this variable
166- // const isVariable = (node: any): node is VariableNode => {
167- // if (node as VariableNode) return true;
168- // return false;
169- // };
170-
171- const isIntNode = ( node : any ) : node is IntValueNode => {
172- if ( node as IntValueNode ) return true ;
173- return false ;
174- } ;
175-
176- if ( limitArg ) {
177- const node : ValueNode = limitArg . value ;
178-
179- // FIXME: Is there a better way to check for the type here?
180- if ( isIntNode ( node ) ) {
181- const multiplier = Number ( node . value || arg . defaultValue ) ;
182-
183- return result [ listType . name . toLowerCase ( ) ] . weight * multiplier ;
184- }
185- }
192+ /**
193+ * The default typeWeightsConfig object is based off of Shopifys implementation of query
194+ * cost analysis. Our function should input a users configuration of type weights or fall
195+ * back on shopifys settings. We can change this later.
196+ *
197+ * This function should
198+ * - iterate through the schema object and create the typeWeightObject as described in the tests
199+ * - validate that the typeWeightsConfig parameter has no negative values (throw an error if it does)
200+ *
201+ * @param schema
202+ * @param typeWeightsConfig Defaults to {mutation: 10, object: 1, field: 0, connection: 2}
203+ */
204+ function buildTypeWeightsFromSchema (
205+ schema : GraphQLSchema ,
206+ typeWeightsConfig : TypeWeightConfig = defaultTypeWeightsConfig
207+ ) : TypeWeightObject {
208+ if ( ! schema ) throw new Error ( 'Missing Argument: schema is required' ) ;
186209
187- // FIXME: The list is unbounded. Return the object weight
188- return result [ listType . name . toLowerCase ( ) ] . weight ;
189- } ;
190- } else {
191- // TODO: determine the type of the list and use the appropriate weight
192- // TODO: This should multiply as well
193- result . query . fields [ field ] = typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ;
194- }
195- }
196- } ) ;
210+ // Merge the provided type weights with the default to account for missing values
211+ const typeWeights : TypeWeightConfig = {
212+ ...defaultTypeWeightsConfig ,
213+ ...typeWeightsConfig ,
214+ } ;
197215
198- // if the field is a scalar set weight accordingly
199- // FIXME: Enums shouldn't be here???
200- if ( isScalarType ( resolveType ) || isEnumType ( resolveType ) ) {
201- result . query . fields [ field ] = typeWeights . scalar || DEFAULT_SCALAR_WEIGHT ;
202- }
203- } ) ;
204- }
216+ // Confirm that any custom weights are positive
217+ Object . entries ( typeWeights ) . forEach ( ( value : [ string , number ] ) => {
218+ if ( value [ 1 ] < 0 ) {
219+ throw new Error ( `Type weights cannot be negative. Received: ${ value [ 0 ] } : ${ value [ 1 ] } ` ) ;
220+ }
221+ } ) ;
205222
206- return result ;
223+ const objectTypeWeights = parseTypes ( schema , { } , typeWeights ) ;
224+ return parseQuery ( schema , objectTypeWeights , typeWeights ) ;
207225}
208226
209227export default buildTypeWeightsFromSchema ;
0 commit comments