@gqlkit-ts/cli 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/auto-type-generator/auto-type-generator.d.ts +7 -0
  2. package/dist/auto-type-generator/auto-type-generator.d.ts.map +1 -1
  3. package/dist/auto-type-generator/auto-type-generator.js +375 -55
  4. package/dist/auto-type-generator/auto-type-generator.js.map +1 -1
  5. package/dist/auto-type-generator/discriminator-field-validator.d.ts +26 -0
  6. package/dist/auto-type-generator/discriminator-field-validator.d.ts.map +1 -0
  7. package/dist/auto-type-generator/discriminator-field-validator.js +242 -0
  8. package/dist/auto-type-generator/discriminator-field-validator.js.map +1 -0
  9. package/dist/auto-type-generator/discriminator-naming.d.ts +11 -0
  10. package/dist/auto-type-generator/discriminator-naming.d.ts.map +1 -0
  11. package/dist/auto-type-generator/discriminator-naming.js +15 -0
  12. package/dist/auto-type-generator/discriminator-naming.js.map +1 -0
  13. package/dist/auto-type-generator/discriminator-resolve-type-generator.d.ts +44 -0
  14. package/dist/auto-type-generator/discriminator-resolve-type-generator.d.ts.map +1 -0
  15. package/dist/auto-type-generator/discriminator-resolve-type-generator.js +77 -0
  16. package/dist/auto-type-generator/discriminator-resolve-type-generator.js.map +1 -0
  17. package/dist/auto-type-generator/index.d.ts +3 -0
  18. package/dist/auto-type-generator/index.d.ts.map +1 -1
  19. package/dist/auto-type-generator/index.js +3 -0
  20. package/dist/auto-type-generator/index.js.map +1 -1
  21. package/dist/auto-type-generator/inline-enum-collector.d.ts.map +1 -1
  22. package/dist/auto-type-generator/inline-enum-collector.js +14 -7
  23. package/dist/auto-type-generator/inline-enum-collector.js.map +1 -1
  24. package/dist/auto-type-generator/inline-object-converter.d.ts +12 -0
  25. package/dist/auto-type-generator/inline-object-converter.d.ts.map +1 -0
  26. package/dist/auto-type-generator/inline-object-converter.js +72 -0
  27. package/dist/auto-type-generator/inline-object-converter.js.map +1 -0
  28. package/dist/auto-type-generator/inline-object-traverser.d.ts +2 -1
  29. package/dist/auto-type-generator/inline-object-traverser.d.ts.map +1 -1
  30. package/dist/auto-type-generator/inline-object-traverser.js +22 -4
  31. package/dist/auto-type-generator/inline-object-traverser.js.map +1 -1
  32. package/dist/auto-type-generator/inline-union-collector.d.ts.map +1 -1
  33. package/dist/auto-type-generator/inline-union-collector.js +20 -6
  34. package/dist/auto-type-generator/inline-union-collector.js.map +1 -1
  35. package/dist/auto-type-generator/inline-union-types.d.ts +2 -0
  36. package/dist/auto-type-generator/inline-union-types.d.ts.map +1 -1
  37. package/dist/auto-type-generator/inline-union-validator.js +3 -3
  38. package/dist/auto-type-generator/inline-union-validator.js.map +1 -1
  39. package/dist/auto-type-generator/intersection-flattener.d.ts +44 -0
  40. package/dist/auto-type-generator/intersection-flattener.d.ts.map +1 -0
  41. package/dist/auto-type-generator/intersection-flattener.js +398 -0
  42. package/dist/auto-type-generator/intersection-flattener.js.map +1 -0
  43. package/dist/auto-type-generator/naming-convention.d.ts +21 -0
  44. package/dist/auto-type-generator/naming-convention.d.ts.map +1 -1
  45. package/dist/auto-type-generator/naming-convention.js +145 -1
  46. package/dist/auto-type-generator/naming-convention.js.map +1 -1
  47. package/dist/auto-type-generator/typename-extractor.d.ts +2 -0
  48. package/dist/auto-type-generator/typename-extractor.d.ts.map +1 -1
  49. package/dist/auto-type-generator/typename-extractor.js +11 -3
  50. package/dist/auto-type-generator/typename-extractor.js.map +1 -1
  51. package/dist/auto-type-generator/typename-resolve-type-generator.d.ts +2 -0
  52. package/dist/auto-type-generator/typename-resolve-type-generator.d.ts.map +1 -1
  53. package/dist/auto-type-generator/typename-resolve-type-generator.js +12 -84
  54. package/dist/auto-type-generator/typename-resolve-type-generator.js.map +1 -1
  55. package/dist/auto-type-generator/typename-types.d.ts +4 -0
  56. package/dist/auto-type-generator/typename-types.d.ts.map +1 -1
  57. package/dist/auto-type-generator/typename-types.js +6 -0
  58. package/dist/auto-type-generator/typename-types.js.map +1 -1
  59. package/dist/auto-type-generator/typename-validator.d.ts.map +1 -1
  60. package/dist/auto-type-generator/typename-validator.js +4 -3
  61. package/dist/auto-type-generator/typename-validator.js.map +1 -1
  62. package/dist/commands/gen.d.ts.map +1 -1
  63. package/dist/commands/gen.js +2 -1
  64. package/dist/commands/gen.js.map +1 -1
  65. package/dist/config/types.d.ts +7 -0
  66. package/dist/config/types.d.ts.map +1 -1
  67. package/dist/config-loader/index.d.ts +1 -1
  68. package/dist/config-loader/index.d.ts.map +1 -1
  69. package/dist/config-loader/index.js.map +1 -1
  70. package/dist/config-loader/loader.d.ts +6 -0
  71. package/dist/config-loader/loader.d.ts.map +1 -1
  72. package/dist/config-loader/loader.js +1 -0
  73. package/dist/config-loader/loader.js.map +1 -1
  74. package/dist/config-loader/validator.d.ts.map +1 -1
  75. package/dist/config-loader/validator.js +84 -1
  76. package/dist/config-loader/validator.js.map +1 -1
  77. package/dist/gen-orchestrator/orchestrator.d.ts +2 -1
  78. package/dist/gen-orchestrator/orchestrator.d.ts.map +1 -1
  79. package/dist/gen-orchestrator/orchestrator.js +15 -2
  80. package/dist/gen-orchestrator/orchestrator.js.map +1 -1
  81. package/dist/resolver-extractor/extractor/define-api-extractor.d.ts.map +1 -1
  82. package/dist/resolver-extractor/extractor/define-api-extractor.js +4 -0
  83. package/dist/resolver-extractor/extractor/define-api-extractor.js.map +1 -1
  84. package/dist/resolver-extractor/validator/abstract-resolver-validator.d.ts +2 -0
  85. package/dist/resolver-extractor/validator/abstract-resolver-validator.d.ts.map +1 -1
  86. package/dist/resolver-extractor/validator/abstract-resolver-validator.js +16 -3
  87. package/dist/resolver-extractor/validator/abstract-resolver-validator.js.map +1 -1
  88. package/dist/schema-generator/emitter/code-emitter.d.ts.map +1 -1
  89. package/dist/schema-generator/emitter/code-emitter.js +13 -1
  90. package/dist/schema-generator/emitter/code-emitter.js.map +1 -1
  91. package/dist/schema-generator/emitter/discriminator-resolve-type-emitter.d.ts +18 -0
  92. package/dist/schema-generator/emitter/discriminator-resolve-type-emitter.d.ts.map +1 -0
  93. package/dist/schema-generator/emitter/discriminator-resolve-type-emitter.js +89 -0
  94. package/dist/schema-generator/emitter/discriminator-resolve-type-emitter.js.map +1 -0
  95. package/dist/schema-generator/generate-schema.d.ts +2 -0
  96. package/dist/schema-generator/generate-schema.d.ts.map +1 -1
  97. package/dist/schema-generator/generate-schema.js +69 -10
  98. package/dist/schema-generator/generate-schema.js.map +1 -1
  99. package/dist/schema-generator/integrator/result-integrator.d.ts +4 -0
  100. package/dist/schema-generator/integrator/result-integrator.d.ts.map +1 -1
  101. package/dist/schema-generator/integrator/result-integrator.js +18 -2
  102. package/dist/schema-generator/integrator/result-integrator.js.map +1 -1
  103. package/dist/schema-generator/resolver-collector/resolver-collector.d.ts +2 -0
  104. package/dist/schema-generator/resolver-collector/resolver-collector.d.ts.map +1 -1
  105. package/dist/schema-generator/resolver-collector/resolver-collector.js +4 -0
  106. package/dist/schema-generator/resolver-collector/resolver-collector.js.map +1 -1
  107. package/dist/shared/constants.d.ts.map +1 -1
  108. package/dist/shared/constants.js +14 -1
  109. package/dist/shared/constants.js.map +1 -1
  110. package/dist/shared/enum-prefix-detector.d.ts.map +1 -1
  111. package/dist/shared/enum-prefix-detector.js +78 -8
  112. package/dist/shared/enum-prefix-detector.js.map +1 -1
  113. package/dist/shared/inline-object-utils.js +1 -1
  114. package/dist/shared/inline-object-utils.js.map +1 -1
  115. package/dist/shared/type-converter.d.ts.map +1 -1
  116. package/dist/shared/type-converter.js +55 -0
  117. package/dist/shared/type-converter.js.map +1 -1
  118. package/dist/type-extractor/converter/graphql-converter.d.ts.map +1 -1
  119. package/dist/type-extractor/converter/graphql-converter.js +11 -1
  120. package/dist/type-extractor/converter/graphql-converter.js.map +1 -1
  121. package/dist/type-extractor/extractor/field-type-resolver.d.ts +18 -0
  122. package/dist/type-extractor/extractor/field-type-resolver.d.ts.map +1 -1
  123. package/dist/type-extractor/extractor/field-type-resolver.js +218 -16
  124. package/dist/type-extractor/extractor/field-type-resolver.js.map +1 -1
  125. package/dist/type-extractor/extractor/type-extractor.d.ts +1 -0
  126. package/dist/type-extractor/extractor/type-extractor.d.ts.map +1 -1
  127. package/dist/type-extractor/extractor/type-extractor.js +127 -14
  128. package/dist/type-extractor/extractor/type-extractor.js.map +1 -1
  129. package/dist/type-extractor/extractor/type-name-collector.d.ts.map +1 -1
  130. package/dist/type-extractor/extractor/type-name-collector.js +19 -4
  131. package/dist/type-extractor/extractor/type-name-collector.js.map +1 -1
  132. package/dist/type-extractor/types/diagnostics.d.ts +1 -1
  133. package/dist/type-extractor/types/diagnostics.d.ts.map +1 -1
  134. package/dist/type-extractor/types/index.d.ts +1 -1
  135. package/dist/type-extractor/types/index.d.ts.map +1 -1
  136. package/dist/type-extractor/types/index.js +1 -1
  137. package/dist/type-extractor/types/index.js.map +1 -1
  138. package/dist/type-extractor/types/ts-type-reference-factory.d.ts +7 -1
  139. package/dist/type-extractor/types/ts-type-reference-factory.d.ts.map +1 -1
  140. package/dist/type-extractor/types/ts-type-reference-factory.js +18 -3
  141. package/dist/type-extractor/types/ts-type-reference-factory.js.map +1 -1
  142. package/dist/type-extractor/types/typescript.d.ts +3 -1
  143. package/dist/type-extractor/types/typescript.d.ts.map +1 -1
  144. package/dist/type-extractor/validator/type-validator.d.ts.map +1 -1
  145. package/dist/type-extractor/validator/type-validator.js +6 -1
  146. package/dist/type-extractor/validator/type-validator.js.map +1 -1
  147. package/docs/configuration.md +19 -0
  148. package/docs/index.md +1 -0
  149. package/docs/integration/ai-sdk.md +189 -0
  150. package/docs/schema/unions.md +117 -0
  151. package/package.json +2 -2
  152. package/src/auto-type-generator/auto-type-generator.ts +576 -58
  153. package/src/auto-type-generator/discriminator-field-validator.ts +368 -0
  154. package/src/auto-type-generator/discriminator-naming.ts +24 -0
  155. package/src/auto-type-generator/discriminator-resolve-type-generator.ts +136 -0
  156. package/src/auto-type-generator/index.ts +17 -0
  157. package/src/auto-type-generator/inline-enum-collector.ts +19 -4
  158. package/src/auto-type-generator/inline-object-converter.ts +100 -0
  159. package/src/auto-type-generator/inline-object-traverser.ts +33 -7
  160. package/src/auto-type-generator/inline-union-collector.ts +26 -4
  161. package/src/auto-type-generator/inline-union-types.ts +2 -0
  162. package/src/auto-type-generator/inline-union-validator.ts +3 -3
  163. package/src/auto-type-generator/intersection-flattener.ts +554 -0
  164. package/src/auto-type-generator/naming-convention.ts +205 -1
  165. package/src/auto-type-generator/typename-extractor.ts +17 -3
  166. package/src/auto-type-generator/typename-resolve-type-generator.ts +19 -108
  167. package/src/auto-type-generator/typename-types.ts +7 -0
  168. package/src/auto-type-generator/typename-validator.ts +4 -3
  169. package/src/commands/gen.ts +9 -2
  170. package/src/config/types.ts +10 -0
  171. package/src/config-loader/index.ts +1 -0
  172. package/src/config-loader/loader.ts +11 -0
  173. package/src/config-loader/validator.ts +100 -1
  174. package/src/gen-orchestrator/orchestrator.ts +19 -2
  175. package/src/resolver-extractor/extractor/define-api-extractor.ts +4 -0
  176. package/src/resolver-extractor/validator/abstract-resolver-validator.ts +20 -6
  177. package/src/schema-generator/emitter/code-emitter.ts +26 -1
  178. package/src/schema-generator/emitter/discriminator-resolve-type-emitter.ts +125 -0
  179. package/src/schema-generator/generate-schema.ts +100 -13
  180. package/src/schema-generator/integrator/result-integrator.ts +25 -1
  181. package/src/schema-generator/resolver-collector/resolver-collector.ts +7 -0
  182. package/src/shared/constants.ts +15 -1
  183. package/src/shared/enum-prefix-detector.ts +96 -8
  184. package/src/shared/inline-object-utils.ts +1 -1
  185. package/src/shared/type-converter.ts +63 -0
  186. package/src/type-extractor/converter/graphql-converter.ts +17 -1
  187. package/src/type-extractor/extractor/field-type-resolver.ts +266 -16
  188. package/src/type-extractor/extractor/type-extractor.ts +148 -11
  189. package/src/type-extractor/extractor/type-name-collector.ts +19 -4
  190. package/src/type-extractor/types/diagnostics.ts +10 -1
  191. package/src/type-extractor/types/index.ts +2 -1
  192. package/src/type-extractor/types/ts-type-reference-factory.ts +24 -3
  193. package/src/type-extractor/types/typescript.ts +6 -2
  194. package/src/type-extractor/validator/type-validator.ts +6 -1
@@ -55,11 +55,36 @@ export interface ResolverPayloadContext {
55
55
  readonly fieldPath: ReadonlyArray<string>;
56
56
  }
57
57
 
58
+ const IRREGULAR_SINGULAR_FIELD_NAMES = new Map([
59
+ ["aliases", "alias"],
60
+ ["analyses", "analysis"],
61
+ ["buses", "bus"],
62
+ ["children", "child"],
63
+ ["cookies", "cookie"],
64
+ ["crises", "crisis"],
65
+ ["diagnoses", "diagnosis"],
66
+ ["feet", "foot"],
67
+ ["geese", "goose"],
68
+ ["men", "man"],
69
+ ["mice", "mouse"],
70
+ ["movies", "movie"],
71
+ ["people", "person"],
72
+ ["selfies", "selfie"],
73
+ ["statuses", "status"],
74
+ ["teeth", "tooth"],
75
+ ["theses", "thesis"],
76
+ ["women", "woman"],
77
+ ["zombies", "zombie"],
78
+ ]);
79
+
80
+ const NON_INFLECTING_FIELD_NAMES = new Set(["news", "series", "species"]);
81
+ const AMBIGUOUS_PLURAL_FIELD_NAMES = new Set(["axes"]);
82
+
58
83
  /**
59
84
  * Convert a string to PascalCase.
60
85
  * Handles camelCase, snake_case, and kebab-case inputs.
61
86
  */
62
- function toPascalCase(str: string): string {
87
+ export function toPascalCase(str: string): string {
63
88
  if (str.length === 0) return str;
64
89
 
65
90
  return str
@@ -76,6 +101,185 @@ function toPascalCase(str: string): string {
76
101
  .join("");
77
102
  }
78
103
 
104
+ function isConsonant(char: string): boolean {
105
+ return /^[bcdfghjklmnpqrstvwxyz]$/i.test(char);
106
+ }
107
+
108
+ function isUppercaseLetter(char: string): boolean {
109
+ return char.toLowerCase() !== char && char.toUpperCase() === char;
110
+ }
111
+
112
+ function applyReplacementCase(params: {
113
+ readonly replacement: string;
114
+ readonly template: string;
115
+ }): string {
116
+ const { replacement, template } = params;
117
+
118
+ if (template.toUpperCase() === template) {
119
+ return replacement.toUpperCase();
120
+ }
121
+
122
+ if (isUppercaseLetter(template.charAt(0))) {
123
+ return `${replacement.charAt(0).toUpperCase()}${replacement.slice(1)}`;
124
+ }
125
+
126
+ return replacement;
127
+ }
128
+
129
+ function singularizeIrregularFieldName(name: string): string | null {
130
+ const lowerName = name.toLowerCase();
131
+
132
+ for (const [plural, singular] of IRREGULAR_SINGULAR_FIELD_NAMES) {
133
+ if (!lowerName.endsWith(plural)) {
134
+ continue;
135
+ }
136
+
137
+ const suffixStart = name.length - plural.length;
138
+ if (suffixStart > 0) {
139
+ const previousChar = name.charAt(suffixStart - 1);
140
+ const suffixFirstChar = name.charAt(suffixStart);
141
+ const hasWordBoundary =
142
+ previousChar === "_" ||
143
+ previousChar === "-" ||
144
+ isUppercaseLetter(suffixFirstChar);
145
+
146
+ if (!hasWordBoundary) {
147
+ continue;
148
+ }
149
+ }
150
+
151
+ return `${name.slice(0, suffixStart)}${applyReplacementCase({
152
+ replacement: singular,
153
+ template: name.slice(suffixStart),
154
+ })}`;
155
+ }
156
+
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Singularize a plural field name conservatively for array element type naming.
162
+ * Falls back to the original name when the plural form is ambiguous.
163
+ */
164
+ export function singularizeFieldName(name: string): string {
165
+ const irregularSingular = singularizeIrregularFieldName(name);
166
+ if (irregularSingular) {
167
+ return irregularSingular;
168
+ }
169
+
170
+ const lowerName = name.toLowerCase();
171
+
172
+ if (name.length <= 3 || NON_INFLECTING_FIELD_NAMES.has(lowerName)) {
173
+ return name;
174
+ }
175
+
176
+ if (AMBIGUOUS_PLURAL_FIELD_NAMES.has(lowerName)) {
177
+ return name;
178
+ }
179
+
180
+ if (lowerName.endsWith("ies")) {
181
+ if (name.length <= 4) {
182
+ return name.slice(0, -1);
183
+ }
184
+
185
+ if (isConsonant(lowerName.at(-4) ?? "")) {
186
+ return `${name.slice(0, -3)}y`;
187
+ }
188
+ }
189
+
190
+ if (
191
+ lowerName.endsWith("sses") ||
192
+ lowerName.endsWith("shes") ||
193
+ lowerName.endsWith("ches") ||
194
+ lowerName.endsWith("xes") ||
195
+ lowerName.endsWith("zes")
196
+ ) {
197
+ return name.slice(0, -2);
198
+ }
199
+
200
+ if (
201
+ lowerName.endsWith("s") &&
202
+ !lowerName.endsWith("ss") &&
203
+ !lowerName.endsWith("is") &&
204
+ !lowerName.endsWith("us")
205
+ ) {
206
+ return name.slice(0, -1);
207
+ }
208
+
209
+ return name;
210
+ }
211
+
212
+ interface AppendFieldPathParams {
213
+ readonly parentPath: ReadonlyArray<string>;
214
+ readonly fieldName: string;
215
+ readonly singularize: boolean;
216
+ readonly siblingFieldNames: ReadonlySet<string> | null;
217
+ }
218
+
219
+ function hasSiblingFieldPathCollision(params: {
220
+ readonly fieldName: string;
221
+ readonly singularFieldName: string;
222
+ readonly siblingFieldNames: ReadonlySet<string> | null;
223
+ }): boolean {
224
+ const { fieldName, singularFieldName, siblingFieldNames } = params;
225
+
226
+ if (!siblingFieldNames) {
227
+ return false;
228
+ }
229
+
230
+ for (const siblingFieldName of siblingFieldNames) {
231
+ if (siblingFieldName === fieldName) {
232
+ continue;
233
+ }
234
+
235
+ if (
236
+ siblingFieldName === singularFieldName ||
237
+ singularizeFieldName(siblingFieldName) === singularFieldName
238
+ ) {
239
+ return true;
240
+ }
241
+ }
242
+
243
+ return false;
244
+ }
245
+
246
+ function resolveFieldPathSegment(params: {
247
+ readonly fieldName: string;
248
+ readonly singularize: boolean;
249
+ readonly siblingFieldNames: ReadonlySet<string> | null;
250
+ }): string {
251
+ const { fieldName, singularize, siblingFieldNames } = params;
252
+
253
+ if (!singularize) {
254
+ return fieldName;
255
+ }
256
+
257
+ const singularFieldName = singularizeFieldName(fieldName);
258
+ if (
259
+ singularFieldName !== fieldName &&
260
+ hasSiblingFieldPathCollision({
261
+ fieldName,
262
+ singularFieldName,
263
+ siblingFieldNames,
264
+ })
265
+ ) {
266
+ return fieldName;
267
+ }
268
+
269
+ return singularFieldName;
270
+ }
271
+
272
+ /**
273
+ * Append a field name to an auto-type field path.
274
+ */
275
+ export function appendFieldPath(params: AppendFieldPathParams): string[] {
276
+ const { parentPath, fieldName, singularize, siblingFieldNames } = params;
277
+ return [
278
+ ...parentPath,
279
+ resolveFieldPathSegment({ fieldName, singularize, siblingFieldNames }),
280
+ ];
281
+ }
282
+
79
283
  /**
80
284
  * Remove Input suffix from type name if present.
81
285
  */
@@ -45,7 +45,11 @@ function extractTypenameFromFields(
45
45
  }
46
46
 
47
47
  const { tsType } = field;
48
- if (tsType.nullable || tsType.kind !== "literal" || tsType.name === null) {
48
+ if (
49
+ tsType.nullable ||
50
+ tsType.kind !== "stringLiteral" ||
51
+ tsType.name === null
52
+ ) {
49
53
  return null;
50
54
  }
51
55
 
@@ -63,7 +67,11 @@ function extractTypenameFromInlineObjectProperties(
63
67
  const { property, fieldName } = found;
64
68
  const { propertyType: tsType } = property;
65
69
 
66
- if (tsType.nullable || tsType.kind !== "literal" || tsType.name === null) {
70
+ if (
71
+ tsType.nullable ||
72
+ tsType.kind !== "stringLiteral" ||
73
+ tsType.name === null
74
+ ) {
67
75
  return null;
68
76
  }
69
77
 
@@ -206,12 +214,14 @@ function extractTypenames(
206
214
  export interface CollectTypenameExtractionsParams {
207
215
  readonly extractedTypes: ReadonlyArray<ExtractedTypeInfo>;
208
216
  readonly typeMap: ReadonlyMap<string, ExtractedTypeInfo>;
217
+ /** Union names that have discriminatorFields configured; these are excluded from typename extraction. */
218
+ readonly discriminatorFieldUnionNames: ReadonlySet<string>;
209
219
  }
210
220
 
211
221
  export function collectTypenameExtractions(
212
222
  params: CollectTypenameExtractionsParams,
213
223
  ): ReadonlyArray<TypenameExtractionResult> {
214
- const { extractedTypes, typeMap } = params;
224
+ const { extractedTypes, typeMap, discriminatorFieldUnionNames } = params;
215
225
  const results: TypenameExtractionResult[] = [];
216
226
 
217
227
  for (const typeInfo of extractedTypes) {
@@ -219,6 +229,10 @@ export function collectTypenameExtractions(
219
229
  typeInfo.metadata.kind === "union" ||
220
230
  typeInfo.metadata.kind === "graphqlInterface"
221
231
  ) {
232
+ // Skip unions that have discriminatorFields configured; they use the discriminator pipeline instead
233
+ if (discriminatorFieldUnionNames.has(typeInfo.metadata.name)) {
234
+ continue;
235
+ }
222
236
  const result = extractTypenames({ abstractType: typeInfo, typeMap });
223
237
  if (result !== null) {
224
238
  results.push(result);
@@ -1,12 +1,6 @@
1
- import type {
2
- ExtractedTypeInfo,
3
- InlineObjectMember,
4
- InlineObjectProperty,
5
- } from "../type-extractor/types/index.js";
6
- import type {
7
- AutoGeneratedField,
8
- AutoGeneratedType,
9
- } from "./auto-type-generator.js";
1
+ import type { ExtractedTypeInfo } from "../type-extractor/types/index.js";
2
+ import type { AutoGeneratedType } from "./auto-type-generator.js";
3
+ import { generateObjectTypeFromInlineObject } from "./inline-object-converter.js";
10
4
  import type { ResolveTypeFieldPattern } from "./resolve-type-generator.js";
11
5
  import {
12
6
  collectTypenameExtractions,
@@ -39,6 +33,8 @@ export interface CollectTypenameResolveTypesParams {
39
33
  readonly extractedTypes: ReadonlyArray<ExtractedTypeInfo>;
40
34
  readonly typeMap: ReadonlyMap<string, ExtractedTypeInfo>;
41
35
  readonly manualResolveTypeNames: ReadonlySet<string>;
36
+ /** Union names that have discriminatorFields configured; these are excluded from typename processing. */
37
+ readonly discriminatorFieldUnionNames: ReadonlySet<string>;
42
38
  }
43
39
 
44
40
  export interface CollectTypenameResolveTypesResult {
@@ -83,100 +79,8 @@ function determineResolveTypePattern(
83
79
  return { usedFieldNames, memberFieldMap };
84
80
  }
85
81
 
86
- function primitiveToGraphQLScalar(primitive: string): string {
87
- switch (primitive) {
88
- case "string":
89
- return "String";
90
- case "number":
91
- return "Float";
92
- case "boolean":
93
- return "Boolean";
94
- default:
95
- return "String";
96
- }
97
- }
98
-
99
- function convertInlineObjectPropertyToField(
100
- property: InlineObjectProperty,
101
- ): AutoGeneratedField | null {
102
- if (
103
- property.propertyName === "__typename" ||
104
- property.propertyName === "$typeName"
105
- ) {
106
- return null;
107
- }
108
-
109
- const tsType = property.propertyType;
110
- let typeName: string;
111
- const nullable = tsType.nullable;
112
- let list = false;
113
- let listItemNullable: boolean | null = null;
114
-
115
- if (tsType.kind === "array" && tsType.elementType) {
116
- list = true;
117
- listItemNullable = tsType.elementType.nullable;
118
- const elementName = tsType.elementType.name ?? "String";
119
- typeName =
120
- tsType.elementType.kind === "primitive"
121
- ? primitiveToGraphQLScalar(elementName)
122
- : elementName;
123
- } else if (tsType.kind === "reference" && tsType.name) {
124
- typeName = tsType.name;
125
- } else if (tsType.kind === "primitive" && tsType.name) {
126
- typeName = primitiveToGraphQLScalar(tsType.name);
127
- } else if (tsType.kind === "scalar" && tsType.scalarInfo) {
128
- typeName = tsType.scalarInfo.scalarName;
129
- } else {
130
- typeName = "String";
131
- }
132
-
133
- return {
134
- name: property.propertyName,
135
- type: {
136
- typeName,
137
- nullable,
138
- list,
139
- listItemNullable,
140
- },
141
- description: property.description,
142
- deprecated: property.deprecated,
143
- directives: null,
144
- defaultValue: null,
145
- };
146
- }
147
-
148
- function generateObjectTypeFromInlineObject(
149
- inlineObjectMember: InlineObjectMember,
150
- typeName: string,
151
- abstractTypeName: string,
152
- sourceFile: string,
153
- ): AutoGeneratedType {
154
- const fields: AutoGeneratedField[] = [];
155
-
156
- for (const property of inlineObjectMember.properties) {
157
- const field = convertInlineObjectPropertyToField(property);
158
- if (field !== null) {
159
- fields.push(field);
160
- }
161
- }
162
-
163
- return {
164
- name: typeName,
165
- kind: "Object",
166
- fields,
167
- enumValues: null,
168
- unionMembers: null,
169
- needsStringEnumMapping: false,
170
- sourceLocation: { file: sourceFile, line: 1, column: 1 },
171
- generatedFrom: {
172
- parentTypeName: abstractTypeName,
173
- fieldPath: [],
174
- context: "typeField",
175
- },
176
- description: null,
177
- resolveTypeFieldPattern: null,
178
- };
179
- }
82
+ /** Typename-related properties that should be excluded from generated object types. */
83
+ const TYPENAME_PROPERTY_NAMES = new Set(["__typename", "$typeName"]);
180
84
 
181
85
  function collectGeneratedObjectTypes(
182
86
  extraction: TypenameExtractionResult,
@@ -209,12 +113,13 @@ function collectGeneratedObjectTypes(
209
113
 
210
114
  const typeName = member.typenameInfo.typeName;
211
115
 
212
- const objectType = generateObjectTypeFromInlineObject(
116
+ const objectType = generateObjectTypeFromInlineObject({
213
117
  inlineObjectMember,
214
118
  typeName,
215
- extraction.abstractTypeName,
216
- extractedType.metadata.sourceFile,
217
- );
119
+ abstractTypeName: extraction.abstractTypeName,
120
+ sourceFile: extractedType.metadata.sourceFile,
121
+ skipPropertyNames: TYPENAME_PROPERTY_NAMES,
122
+ });
218
123
 
219
124
  generatedObjectTypes.push(objectType);
220
125
  generatedInlineObjectTypes.push({
@@ -230,11 +135,17 @@ function collectGeneratedObjectTypes(
230
135
  export function collectTypenameResolveTypes(
231
136
  params: CollectTypenameResolveTypesParams,
232
137
  ): CollectTypenameResolveTypesResult {
233
- const { extractedTypes, typeMap, manualResolveTypeNames } = params;
138
+ const {
139
+ extractedTypes,
140
+ typeMap,
141
+ manualResolveTypeNames,
142
+ discriminatorFieldUnionNames,
143
+ } = params;
234
144
 
235
145
  const extractions = collectTypenameExtractions({
236
146
  extractedTypes,
237
147
  typeMap,
148
+ discriminatorFieldUnionNames,
238
149
  });
239
150
 
240
151
  const autoResolveTypes: TypenameAutoResolveTypeInfo[] = [];
@@ -15,6 +15,13 @@
15
15
  */
16
16
  export const TYPENAME_FIELD_NAMES = ["__typename", "$typeName"] as const;
17
17
 
18
+ /**
19
+ * Check if a field name is a typename discrimination field.
20
+ */
21
+ export function isTypenameFieldName(name: string): boolean {
22
+ return (TYPENAME_FIELD_NAMES as readonly string[]).includes(name);
23
+ }
24
+
18
25
  /**
19
26
  * The field name used for type discrimination.
20
27
  */
@@ -74,7 +74,7 @@ function analyzeInlineObjectTypename(
74
74
  return {
75
75
  exists: true,
76
76
  fieldName,
77
- isStringLiteral: tsType.kind === "literal" && tsType.name !== null,
77
+ isStringLiteral: tsType.kind === "stringLiteral" && tsType.name !== null,
78
78
  isNullable: tsType.nullable,
79
79
  };
80
80
  }
@@ -171,7 +171,8 @@ export function validateTypenames(
171
171
 
172
172
  const { typeName, fieldName } = member.typenameInfo;
173
173
  const memberTypeName =
174
- member.memberTypeName ?? `member${member.memberIndex}`;
174
+ member.memberTypeName ??
175
+ `(anonymous member at index ${member.memberIndex})`;
175
176
 
176
177
  const existing = typenameValueToMembers.get(typeName) ?? [];
177
178
  existing.push({ memberTypeName, fieldName });
@@ -238,7 +239,7 @@ function extractTypenameFromObjectType(
238
239
  if (
239
240
  field.optional ||
240
241
  tsType.nullable ||
241
- tsType.kind !== "literal" ||
242
+ tsType.kind !== "stringLiteral" ||
242
243
  tsType.name === null
243
244
  ) {
244
245
  return null;
@@ -45,8 +45,14 @@ export async function runGenCommand(
45
45
  ? dirname(configResult.configPath)
46
46
  : options.cwd;
47
47
 
48
- const { sourceDir, sourceIgnoreGlobs, output, scalars, tsconfigPath } =
49
- configResult.config;
48
+ const {
49
+ sourceDir,
50
+ sourceIgnoreGlobs,
51
+ output,
52
+ scalars,
53
+ tsconfigPath,
54
+ discriminatorFields,
55
+ } = configResult.config;
50
56
 
51
57
  const config: GenerationConfig = {
52
58
  cwd: options.cwd,
@@ -56,6 +62,7 @@ export async function runGenCommand(
56
62
  configDir,
57
63
  customScalars: scalars,
58
64
  tsconfigPath,
65
+ discriminatorFields,
59
66
  };
60
67
 
61
68
  progressReporter.startPhase("Extracting types");
@@ -100,6 +100,16 @@ export interface GqlkitConfig {
100
100
  * Hook configuration for lifecycle events.
101
101
  */
102
102
  readonly hooks?: HooksConfig;
103
+
104
+ /**
105
+ * Custom discriminator field mappings for union types.
106
+ * Maps GraphQL union type names to TypeScript discriminator field names.
107
+ * When specified, gqlkit generates __resolveType functions based on these fields
108
+ * instead of requiring $typeName or __typename.
109
+ */
110
+ readonly discriminatorFields?: Readonly<
111
+ Record<string, string | ReadonlyArray<string>>
112
+ >;
103
113
  }
104
114
 
105
115
  /**
@@ -3,6 +3,7 @@ export {
3
3
  type LoadConfigResult,
4
4
  loadConfig,
5
5
  type ResolvedConfig,
6
+ type ResolvedDiscriminatorFieldsMap,
6
7
  type ResolvedHooksConfig,
7
8
  type ResolvedOutputConfig,
8
9
  type ResolvedScalarMapping,
@@ -42,6 +42,15 @@ export interface ResolvedHooksConfig {
42
42
  readonly afterAllFileWrite: ReadonlyArray<string>;
43
43
  }
44
44
 
45
+ /**
46
+ * Normalized discriminator fields mapping.
47
+ * All values are normalized to arrays (single strings are wrapped in arrays).
48
+ */
49
+ export type ResolvedDiscriminatorFieldsMap = ReadonlyMap<
50
+ string,
51
+ ReadonlyArray<string>
52
+ >;
53
+
45
54
  export interface ResolvedConfig {
46
55
  readonly sourceDir: string;
47
56
  readonly sourceIgnoreGlobs: ReadonlyArray<string>;
@@ -49,6 +58,7 @@ export interface ResolvedConfig {
49
58
  readonly scalars: ReadonlyArray<ResolvedScalarMapping>;
50
59
  readonly tsconfigPath: string | null;
51
60
  readonly hooks: ResolvedHooksConfig;
61
+ readonly discriminatorFields: ResolvedDiscriminatorFieldsMap;
52
62
  }
53
63
 
54
64
  export interface LoadConfigResult {
@@ -82,6 +92,7 @@ const DEFAULT_RESOLVED_CONFIG: ResolvedConfig = {
82
92
  scalars: [],
83
93
  tsconfigPath: null,
84
94
  hooks: DEFAULT_HOOKS_CONFIG,
95
+ discriminatorFields: new Map(),
85
96
  };
86
97
 
87
98
  export async function loadConfig(
@@ -6,6 +6,7 @@ import {
6
6
  DEFAULT_SOURCE_DIR,
7
7
  DEFAULT_TYPEDEFS_PATH,
8
8
  type ResolvedConfig,
9
+ type ResolvedDiscriminatorFieldsMap,
9
10
  type ResolvedHooksConfig,
10
11
  type ResolvedOutputConfig,
11
12
  type ResolvedScalarMapping,
@@ -584,6 +585,96 @@ function validateHooksConfig(
584
585
  return { resolved: undefined, diagnostics };
585
586
  }
586
587
 
588
+ function validateDiscriminatorFieldsConfig(
589
+ value: unknown,
590
+ configPath: string,
591
+ ): {
592
+ resolved: ResolvedDiscriminatorFieldsMap | undefined;
593
+ diagnostics: Diagnostic[];
594
+ } {
595
+ const diagnostics: Diagnostic[] = [];
596
+
597
+ if (value === undefined) {
598
+ return { resolved: new Map(), diagnostics: [] };
599
+ }
600
+
601
+ if (!isRecord(value)) {
602
+ diagnostics.push({
603
+ code: "CONFIG_INVALID_DISCRIMINATOR_FIELDS",
604
+ message: "discriminatorFields must be an object",
605
+ severity: "error",
606
+ location: { file: configPath, line: 1, column: 1 },
607
+ });
608
+ return { resolved: undefined, diagnostics };
609
+ }
610
+
611
+ const result = new Map<string, ReadonlyArray<string>>();
612
+
613
+ for (const [key, entry] of Object.entries(value)) {
614
+ if (typeof entry === "string") {
615
+ if (entry === "") {
616
+ diagnostics.push({
617
+ code: "CONFIG_EMPTY_DISCRIMINATOR_FIELDS",
618
+ message: `discriminatorFields["${key}"] cannot be an empty string`,
619
+ severity: "error",
620
+ location: { file: configPath, line: 1, column: 1 },
621
+ });
622
+ continue;
623
+ }
624
+ result.set(key, [entry]);
625
+ } else if (Array.isArray(entry)) {
626
+ if (entry.length === 0) {
627
+ diagnostics.push({
628
+ code: "CONFIG_EMPTY_DISCRIMINATOR_FIELDS",
629
+ message: `discriminatorFields["${key}"] cannot be an empty array`,
630
+ severity: "error",
631
+ location: { file: configPath, line: 1, column: 1 },
632
+ });
633
+ continue;
634
+ }
635
+ let hasError = false;
636
+ for (const item of entry) {
637
+ if (typeof item !== "string") {
638
+ diagnostics.push({
639
+ code: "CONFIG_INVALID_DISCRIMINATOR_ENTRY",
640
+ message: `discriminatorFields["${key}"] array must contain only strings`,
641
+ severity: "error",
642
+ location: { file: configPath, line: 1, column: 1 },
643
+ });
644
+ hasError = true;
645
+ break;
646
+ }
647
+ if (item === "") {
648
+ diagnostics.push({
649
+ code: "CONFIG_EMPTY_DISCRIMINATOR_FIELDS",
650
+ message: `discriminatorFields["${key}"] array contains an empty string`,
651
+ severity: "error",
652
+ location: { file: configPath, line: 1, column: 1 },
653
+ });
654
+ hasError = true;
655
+ break;
656
+ }
657
+ }
658
+ if (!hasError) {
659
+ result.set(key, entry as string[]);
660
+ }
661
+ } else {
662
+ diagnostics.push({
663
+ code: "CONFIG_INVALID_DISCRIMINATOR_ENTRY",
664
+ message: `discriminatorFields["${key}"] must be a string or array of strings`,
665
+ severity: "error",
666
+ location: { file: configPath, line: 1, column: 1 },
667
+ });
668
+ }
669
+ }
670
+
671
+ if (diagnostics.length > 0) {
672
+ return { resolved: undefined, diagnostics };
673
+ }
674
+
675
+ return { resolved: result, diagnostics: [] };
676
+ }
677
+
587
678
  export function validateConfig(
588
679
  options: ValidateConfigOptions,
589
680
  ): ValidateConfigResult {
@@ -621,6 +712,12 @@ export function validateConfig(
621
712
  const hooksResult = validateHooksConfig(config["hooks"], configPath);
622
713
  diagnostics.push(...hooksResult.diagnostics);
623
714
 
715
+ const discriminatorFieldsResult = validateDiscriminatorFieldsConfig(
716
+ config["discriminatorFields"],
717
+ configPath,
718
+ );
719
+ diagnostics.push(...discriminatorFieldsResult.diagnostics);
720
+
624
721
  if (config["scalars"] !== undefined && !Array.isArray(config["scalars"])) {
625
722
  diagnostics.push({
626
723
  code: "CONFIG_INVALID_TYPE",
@@ -685,7 +782,8 @@ export function validateConfig(
685
782
  !sourceDirResult.resolved ||
686
783
  !sourceIgnoreGlobsResult.resolved ||
687
784
  !outputResult.resolved ||
688
- !hooksResult.resolved
785
+ !hooksResult.resolved ||
786
+ !discriminatorFieldsResult.resolved
689
787
  ) {
690
788
  return { valid: false, resolvedConfig: undefined, diagnostics };
691
789
  }
@@ -699,6 +797,7 @@ export function validateConfig(
699
797
  scalars: resolvedScalars,
700
798
  tsconfigPath: tsconfigPathResult.resolved,
701
799
  hooks: hooksResult.resolved,
800
+ discriminatorFields: discriminatorFieldsResult.resolved,
702
801
  },
703
802
  diagnostics: [],
704
803
  };