@@ -38,25 +38,120 @@ const enforcedRules = specifiedRules.filter(
3838 ( rule ) => ! rulesToIgnore . includes ( rule )
3939) ;
4040
41+ /**
42+ * Type guard for checking if an item is a single item.
43+ *
44+ * @param item - The item to check.
45+ * @returns True if the item is a single item, false otherwise.
46+ */
4147const isSingle = < T > ( item : T | readonly T [ ] ) : item is T => ! Array . isArray ( item ) ;
4248
49+ /**
50+ * Get the leaf type of a type node.
51+ *
52+ * @param typeNode - The type node to get the leaf type of.
53+ * @returns The leaf type of the type node.
54+ */
4355const getLeafType = ( typeNode : TypeNode ) : NamedTypeNode => {
4456 return typeNode . kind === Kind . NAMED_TYPE ?
4557 typeNode
4658 : getLeafType ( typeNode . type ) ;
4759} ;
4860
61+ /**
62+ * Convert the first letter of a string to uppercase.
63+ *
64+ * @param str - The string to convert.
65+ * @returns The string with the first letter capitalized.
66+ */
4967const ucFirst = ( str : string ) => {
5068 if ( ! str ) {
5169 return "" ;
5270 }
5371 return str . charAt ( 0 ) . toUpperCase ( ) + str . slice ( 1 ) ;
5472} ;
5573
74+ /**
75+ * Convert a plural word to its singular form.
76+ *
77+ * @param str - The plural word to convert.
78+ * @returns The singular form of the word.
79+ */
80+ const singularize = ( str : string ) => {
81+ if ( ! str ) {
82+ return "" ;
83+ }
84+
85+ // Handle common pluralization patterns
86+ if ( str . endsWith ( "ies" ) ) {
87+ return str . slice ( 0 , - 3 ) + "y" ;
88+ } else if ( str . endsWith ( "ves" ) ) {
89+ return str . slice ( 0 , - 3 ) + "f" ;
90+ } else if ( str . endsWith ( "es" ) ) {
91+ // Special cases for -es endings
92+ if ( str . endsWith ( "ches" ) || str . endsWith ( "shes" ) || str . endsWith ( "xes" ) ) {
93+ return str . slice ( 0 , - 2 ) ;
94+ } else if ( str . endsWith ( "ses" ) ) {
95+ return str . slice ( 0 , - 2 ) ;
96+ } else {
97+ return str . slice ( 0 , - 1 ) ;
98+ }
99+ } else if ( str . endsWith ( "s" ) && str . length > 1 ) {
100+ return str . slice ( 0 , - 1 ) ;
101+ }
102+
103+ return str ;
104+ } ;
105+
106+ /**
107+ * Check if a number is a float (i.e. 9.5).
108+ *
109+ * @param num - The number to check.
110+ * @returns True if the number is a float, false otherwise.
111+ */
56112function isFloat ( num : number ) {
57113 return typeof num === "number" && ! Number . isInteger ( num ) ;
58114}
59115
116+ /**
117+ * Deep merge utility function to preserve nested properties.
118+ *
119+ * @param target - The target object to merge into.
120+ * @param source - The source object to merge from.
121+ * @returns The merged object.
122+ */
123+ function deepMerge ( target : any , source : any ) : any {
124+ if ( source === null || typeof source !== "object" ) {
125+ return source ;
126+ }
127+
128+ if ( Array . isArray ( source ) ) {
129+ return source ;
130+ }
131+
132+ if ( target === null || typeof target !== "object" || Array . isArray ( target ) ) {
133+ target = { } ;
134+ }
135+
136+ const result = { ...target } ;
137+
138+ for ( const key in source ) {
139+ if ( source . hasOwnProperty ( key ) ) {
140+ if (
141+ typeof source [ key ] === "object" &&
142+ source [ key ] !== null &&
143+ ! Array . isArray ( source [ key ] )
144+ ) {
145+ result [ key ] = deepMerge ( result [ key ] , source [ key ] ) ;
146+ } else {
147+ result [ key ] = source [ key ] ;
148+ }
149+ }
150+ }
151+
152+ return result ;
153+ }
154+
60155const ScalarTypes = [ "String" , "Int" , "Float" , "Boolean" , "ID" ] ;
61156
62157export type OperationVariableDefinitions = Record < string , TypeNode > ;
@@ -183,7 +278,7 @@ export class GrowingSchema {
183278 // Create all input objects from the operation's variable definitions.
184279 // By doing this here, we _may_ create unused input objects, but this
185280 // helps us avoid complexity in tying input objects to field definitions.
186- const inputObjects =
281+ const inputObjects = this . mergeRepeatedInputObjects (
187282 variableDefinitions . reduce (
188283 ( acc , variableDefinition ) => {
189284 const leafType = getLeafType ( variableDefinition . type ) ;
@@ -208,7 +303,8 @@ export class GrowingSchema {
208303 return acc ;
209304 } ,
210305 [ ] as ( InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode ) [ ]
211- ) || [ ] ;
306+ ) || [ ]
307+ ) ;
212308
213309 accumulatedExtensions . definitions . push ( ...inputObjects ) ;
214310
@@ -538,7 +634,15 @@ export class GrowingSchema {
538634 inputObjects : InputObjectsList ;
539635 } {
540636 const inputObjects : InputObjectsList = [ ] ;
541- const fields = Object . entries ( valuesInScope )
637+
638+ let valuesToHandle = valuesInScope ;
639+ if ( Array . isArray ( valuesInScope ) ) {
640+ valuesToHandle = valuesInScope . reduce ( ( acc , item ) => {
641+ return deepMerge ( acc , item ) ;
642+ } , { } ) ;
643+ }
644+
645+ const fields = Object . entries ( valuesToHandle )
542646 . map ( ( [ fieldName , fieldVariableValue ] ) => {
543647 let valueType : TypeNode ;
544648 switch ( typeof fieldVariableValue ) {
@@ -549,19 +653,40 @@ export class GrowingSchema {
549653 name : { kind : Kind . NAME , value : "String" } ,
550654 } ;
551655 } else {
552- // If a variable field is a key/value object, then it is
553- // an input object and we need to create it and any other
554- // input objects from its fields.
555- const inputObjectName = `${ ucFirst ( fieldName ) } Input` ;
556- const inputObject = this . getInputObjectsForVariableValue (
557- inputObjectName ,
558- fieldVariableValue
559- ) ;
560- inputObjects . push ( ...inputObject ) ;
656+ // Create a name for the input object based on the singular
657+ // form of the field name + "Input".
658+ const inputObjectName = `${ ucFirst ( singularize ( fieldName ) ) } Input` ;
659+
660+ // Create a type node for the input object.
561661 valueType = {
562662 kind : Kind . NAMED_TYPE ,
563663 name : { kind : Kind . NAME , value : inputObjectName } ,
564664 } ;
665+
666+ // If the field value is an array, then we need to create a list
667+ // type node for the input object and merge the array items
668+ // into a single object for creating the input object.
669+ let variableValueToHandle = fieldVariableValue ;
670+ if ( Array . isArray ( fieldVariableValue ) ) {
671+ valueType = {
672+ kind : Kind . LIST_TYPE ,
673+ type : valueType ,
674+ } ;
675+ variableValueToHandle = fieldVariableValue . reduce (
676+ ( acc , item ) => {
677+ return deepMerge ( acc , item ) ;
678+ } ,
679+ { }
680+ ) ;
681+ }
682+
683+ // Create the input object and any other input objects from its
684+ // fields.
685+ const inputObject = this . getInputObjectsForVariableValue (
686+ inputObjectName ,
687+ variableValueToHandle
688+ ) ;
689+ inputObjects . push ( ...inputObject ) ;
565690 }
566691 break ;
567692 case "string" :
@@ -608,6 +733,29 @@ export class GrowingSchema {
608733 return { fields, inputObjects } ;
609734 }
610735
736+ mergeRepeatedInputObjects ( inputObjects : InputObjectsList ) : InputObjectsList {
737+ return Object . values (
738+ inputObjects . reduce (
739+ ( acc , inputObject ) => {
740+ const existingInputObject = acc [ inputObject . name . value ] ;
741+ if ( existingInputObject ) {
742+ acc [ inputObject . name . value ] = {
743+ ...existingInputObject ,
744+ fields : [
745+ ...( existingInputObject ?. fields || [ ] ) ,
746+ ...( inputObject . fields || [ ] ) ,
747+ ] ,
748+ } ;
749+ } else {
750+ acc [ inputObject . name . value ] = inputObject ;
751+ }
752+ return acc ;
753+ } ,
754+ { } as Record < string , InputObject >
755+ )
756+ ) ;
757+ }
758+
611759 public toString ( ) {
612760 return printSchema ( this . schema ) ;
613761 }
0 commit comments