@@ -38,25 +38,120 @@ const enforcedRules = specifiedRules.filter(
38
38
( rule ) => ! rulesToIgnore . includes ( rule )
39
39
) ;
40
40
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
+ */
41
47
const isSingle = < T > ( item : T | readonly T [ ] ) : item is T => ! Array . isArray ( item ) ;
42
48
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
+ */
43
55
const getLeafType = ( typeNode : TypeNode ) : NamedTypeNode => {
44
56
return typeNode . kind === Kind . NAMED_TYPE ?
45
57
typeNode
46
58
: getLeafType ( typeNode . type ) ;
47
59
} ;
48
60
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
+ */
49
67
const ucFirst = ( str : string ) => {
50
68
if ( ! str ) {
51
69
return "" ;
52
70
}
53
71
return str . charAt ( 0 ) . toUpperCase ( ) + str . slice ( 1 ) ;
54
72
} ;
55
73
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
+ */
56
112
function isFloat ( num : number ) {
57
113
return typeof num === "number" && ! Number . isInteger ( num ) ;
58
114
}
59
115
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
+
60
155
const ScalarTypes = [ "String" , "Int" , "Float" , "Boolean" , "ID" ] ;
61
156
62
157
export type OperationVariableDefinitions = Record < string , TypeNode > ;
@@ -183,7 +278,7 @@ export class GrowingSchema {
183
278
// Create all input objects from the operation's variable definitions.
184
279
// By doing this here, we _may_ create unused input objects, but this
185
280
// helps us avoid complexity in tying input objects to field definitions.
186
- const inputObjects =
281
+ const inputObjects = this . mergeRepeatedInputObjects (
187
282
variableDefinitions . reduce (
188
283
( acc , variableDefinition ) => {
189
284
const leafType = getLeafType ( variableDefinition . type ) ;
@@ -208,7 +303,8 @@ export class GrowingSchema {
208
303
return acc ;
209
304
} ,
210
305
[ ] as ( InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode ) [ ]
211
- ) || [ ] ;
306
+ ) || [ ]
307
+ ) ;
212
308
213
309
accumulatedExtensions . definitions . push ( ...inputObjects ) ;
214
310
@@ -538,7 +634,15 @@ export class GrowingSchema {
538
634
inputObjects : InputObjectsList ;
539
635
} {
540
636
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 )
542
646
. map ( ( [ fieldName , fieldVariableValue ] ) => {
543
647
let valueType : TypeNode ;
544
648
switch ( typeof fieldVariableValue ) {
@@ -549,19 +653,40 @@ export class GrowingSchema {
549
653
name : { kind : Kind . NAME , value : "String" } ,
550
654
} ;
551
655
} 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.
561
661
valueType = {
562
662
kind : Kind . NAMED_TYPE ,
563
663
name : { kind : Kind . NAME , value : inputObjectName } ,
564
664
} ;
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 ) ;
565
690
}
566
691
break ;
567
692
case "string" :
@@ -608,6 +733,29 @@ export class GrowingSchema {
608
733
return { fields, inputObjects } ;
609
734
}
610
735
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
+
611
759
public toString ( ) {
612
760
return printSchema ( this . schema ) ;
613
761
}
0 commit comments