@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
@@ -32,6 +32,86 @@ export function buildEnumPrefixCandidate(enumName: string): string {
32
32
  return `${toUpperSnakeCase(enumName)}_`;
33
33
  }
34
34
 
35
+ const IRREGULAR_PLURAL_SEGMENTS = new Map([
36
+ ["ALIAS", "ALIASES"],
37
+ ["ANALYSIS", "ANALYSES"],
38
+ ["CHILD", "CHILDREN"],
39
+ ["COOKIE", "COOKIES"],
40
+ ["CRISIS", "CRISES"],
41
+ ["DIAGNOSIS", "DIAGNOSES"],
42
+ ["FOOT", "FEET"],
43
+ ["GOOSE", "GEESE"],
44
+ ["MAN", "MEN"],
45
+ ["MOUSE", "MICE"],
46
+ ["MOVIE", "MOVIES"],
47
+ ["PERSON", "PEOPLE"],
48
+ ["THESIS", "THESES"],
49
+ ["TOOTH", "TEETH"],
50
+ ["WOMAN", "WOMEN"],
51
+ ]);
52
+
53
+ function isConsonant(char: string): boolean {
54
+ return /^[BCDFGHJKLMNPQRSTVWXYZ]$/.test(char);
55
+ }
56
+
57
+ function pluralizeUpperSnakeSegment(segment: string): string {
58
+ const irregularPlural = IRREGULAR_PLURAL_SEGMENTS.get(segment);
59
+ if (irregularPlural) {
60
+ return irregularPlural;
61
+ }
62
+
63
+ if (
64
+ segment.endsWith("Y") &&
65
+ segment.length > 1 &&
66
+ isConsonant(segment.at(-2) ?? "")
67
+ ) {
68
+ return `${segment.slice(0, -1)}IES`;
69
+ }
70
+
71
+ if (
72
+ segment.endsWith("S") ||
73
+ segment.endsWith("SH") ||
74
+ segment.endsWith("CH") ||
75
+ segment.endsWith("X") ||
76
+ segment.endsWith("Z")
77
+ ) {
78
+ return `${segment}ES`;
79
+ }
80
+
81
+ return `${segment}S`;
82
+ }
83
+
84
+ function buildEnumPrefixCandidates(enumName: string): string[] {
85
+ const upperSnakeName = toUpperSnakeCase(enumName);
86
+ const baseCandidate = `${upperSnakeName}_`;
87
+ const segments = upperSnakeName.split("_");
88
+ const candidateSegmentSets: string[][] = [segments];
89
+
90
+ if (segments.length === 0) {
91
+ return [baseCandidate];
92
+ }
93
+
94
+ for (const [index, segment] of segments.entries()) {
95
+ const pluralizedSegment = pluralizeUpperSnakeSegment(segment);
96
+ if (pluralizedSegment === segment) {
97
+ continue;
98
+ }
99
+
100
+ const existingCandidates = [...candidateSegmentSets];
101
+ for (const candidateSegments of existingCandidates) {
102
+ const nextCandidateSegments = [...candidateSegments];
103
+ nextCandidateSegments[index] = pluralizedSegment;
104
+ candidateSegmentSets.push(nextCandidateSegments);
105
+ }
106
+ }
107
+
108
+ return [
109
+ ...new Set(
110
+ candidateSegmentSets.map((candidate) => `${candidate.join("_")}_`),
111
+ ),
112
+ ];
113
+ }
114
+
35
115
  export interface DetectEnumPrefixParams {
36
116
  readonly enumName: string;
37
117
  readonly memberValues: ReadonlyArray<string>;
@@ -71,20 +151,28 @@ export function detectEnumPrefix(
71
151
  return { shouldStrip: false, prefix: null };
72
152
  }
73
153
 
74
- const prefixCandidate = buildEnumPrefixCandidate(enumName);
154
+ for (const prefixCandidate of buildEnumPrefixCandidates(enumName)) {
155
+ let matches = true;
156
+
157
+ for (const value of memberValues) {
158
+ if (!value.startsWith(prefixCandidate)) {
159
+ matches = false;
160
+ break;
161
+ }
75
162
 
76
- for (const value of memberValues) {
77
- if (!value.startsWith(prefixCandidate)) {
78
- return { shouldStrip: false, prefix: null };
163
+ const stripped = value.slice(prefixCandidate.length);
164
+ if (stripped === "") {
165
+ matches = false;
166
+ break;
167
+ }
79
168
  }
80
169
 
81
- const stripped = value.slice(prefixCandidate.length);
82
- if (stripped === "") {
83
- return { shouldStrip: false, prefix: null };
170
+ if (matches) {
171
+ return { shouldStrip: true, prefix: prefixCandidate };
84
172
  }
85
173
  }
86
174
 
87
- return { shouldStrip: true, prefix: prefixCandidate };
175
+ return { shouldStrip: false, prefix: null };
88
176
  }
89
177
 
90
178
  /**
@@ -12,7 +12,7 @@ export function isInlineObjectType(type: ts.Type): boolean {
12
12
  return false;
13
13
  }
14
14
  const symbolName = type.symbol.getName();
15
- if (symbolName !== "__type") {
15
+ if (symbolName !== "__type" && symbolName !== "__object") {
16
16
  return false;
17
17
  }
18
18
  if (!(type.flags & ts.TypeFlags.Object)) {
@@ -4,6 +4,21 @@ import type {
4
4
  } from "../type-extractor/types/index.js";
5
5
  import { PRIMITIVE_TYPE_MAP } from "./constants.js";
6
6
 
7
+ const GRAPHQL_INT_MIN = -(2 ** 31);
8
+ const GRAPHQL_INT_MAX = 2 ** 31 - 1;
9
+
10
+ function numericLiteralToGraphQLScalar(name: string | null): string {
11
+ const num = Number(name);
12
+ if (
13
+ Number.isInteger(num) &&
14
+ num >= GRAPHQL_INT_MIN &&
15
+ num <= GRAPHQL_INT_MAX
16
+ ) {
17
+ return "Int";
18
+ }
19
+ return "Float";
20
+ }
21
+
7
22
  function convertElementTypeName(elementType: TSTypeReference): string {
8
23
  if (elementType.kind === "scalar") {
9
24
  return elementType.scalarInfo?.scalarName ?? elementType.name ?? "String";
@@ -20,6 +35,18 @@ function convertElementTypeName(elementType: TSTypeReference): string {
20
35
  if (elementType.kind === "inlineEnum") {
21
36
  return "__INLINE_ENUM__";
22
37
  }
38
+ if (elementType.kind === "union") {
39
+ return "__INLINE_UNION__";
40
+ }
41
+ if (elementType.kind === "stringLiteral") {
42
+ return "String";
43
+ }
44
+ if (elementType.kind === "numericLiteral") {
45
+ return numericLiteralToGraphQLScalar(elementType.name);
46
+ }
47
+ if (elementType.kind === "never") {
48
+ return "__NEVER__";
49
+ }
23
50
  return elementType.name ?? "String";
24
51
  }
25
52
 
@@ -90,6 +117,42 @@ export function convertTsTypeToGraphQLType(
90
117
  };
91
118
  }
92
119
 
120
+ if (tsType.kind === "union") {
121
+ return {
122
+ typeName: "__INLINE_UNION__",
123
+ nullable,
124
+ list: false,
125
+ listItemNullable: null,
126
+ };
127
+ }
128
+
129
+ if (tsType.kind === "stringLiteral") {
130
+ return {
131
+ typeName: "String",
132
+ nullable,
133
+ list: false,
134
+ listItemNullable: null,
135
+ };
136
+ }
137
+
138
+ if (tsType.kind === "numericLiteral") {
139
+ return {
140
+ typeName: numericLiteralToGraphQLScalar(tsType.name),
141
+ nullable,
142
+ list: false,
143
+ listItemNullable: null,
144
+ };
145
+ }
146
+
147
+ if (tsType.kind === "never") {
148
+ return {
149
+ typeName: "__NEVER__",
150
+ nullable,
151
+ list: false,
152
+ listItemNullable: null,
153
+ };
154
+ }
155
+
93
156
  return {
94
157
  typeName: tsType.name ?? "String",
95
158
  nullable,
@@ -1,3 +1,4 @@
1
+ import { isTypenameFieldName } from "../../auto-type-generator/typename-types.js";
1
2
  import {
2
3
  BUILT_IN_SCALARS,
3
4
  isBuiltInScalar,
@@ -152,6 +153,11 @@ function convertFields(
152
153
  const diagnostics: Diagnostic[] = [];
153
154
 
154
155
  for (const field of extracted.fields) {
156
+ // Typename discrimination fields are silently excluded from the schema
157
+ if (isTypenameFieldName(field.name)) {
158
+ continue;
159
+ }
160
+
155
161
  const eligibility = isEligibleField({
156
162
  fieldName: field.name,
157
163
  kind: isInput ? "input" : "object",
@@ -171,9 +177,19 @@ function convertFields(
171
177
  continue;
172
178
  }
173
179
 
180
+ const graphqlType = convertTsTypeToGraphQLType(
181
+ field.tsType,
182
+ field.optional,
183
+ );
184
+
185
+ // Skip fields with never type — they represent impossible values
186
+ if (graphqlType.typeName === "__NEVER__") {
187
+ continue;
188
+ }
189
+
174
190
  fields.push({
175
191
  name: field.name,
176
- type: convertTsTypeToGraphQLType(field.tsType, field.optional),
192
+ type: graphqlType,
177
193
  description: field.description,
178
194
  deprecated: field.deprecated,
179
195
  directives: field.directives,
@@ -1,3 +1,4 @@
1
+ import { relative } from "node:path";
1
2
  import ts from "typescript";
2
3
  import {
3
4
  detectBrandedType,
@@ -7,11 +8,16 @@ import { isInternalTypeSymbol } from "../../shared/constants.js";
7
8
  import { extractInlineObjectProperties as extractInlineObjectPropertiesShared } from "../../shared/inline-object-extractor.js";
8
9
  import { isInlineObjectType } from "../../shared/inline-object-utils.js";
9
10
  import { detectScalarMetadata } from "../../shared/metadata-detector.js";
11
+ import {
12
+ getSourceLocationFromNode,
13
+ type SourceLocation,
14
+ } from "../../shared/source-location.js";
10
15
  import {
11
16
  type DeprecationInfo,
12
17
  extractTsDocFromSymbol,
13
18
  } from "../../shared/tsdoc-parser.js";
14
19
  import {
20
+ filterNonNullTypeNodes,
15
21
  findEnumParentSymbol,
16
22
  findNonNullTypeNode,
17
23
  getNonNullableTypes,
@@ -25,14 +31,17 @@ import type {
25
31
  ScalarMappingContext,
26
32
  } from "../mapper/scalar-base-type-mapper.js";
27
33
  import { lookupScalarMapping } from "../mapper/scalar-base-type-mapper.js";
34
+ import type { DiagnosticCode } from "../types/diagnostics.js";
28
35
  import {
29
36
  createArrayType,
30
37
  createInlineEnumType,
31
38
  createInlineObjectType,
32
- createLiteralType,
39
+ createNeverType,
40
+ createNumericLiteralType,
33
41
  createPrimitiveType,
34
42
  createReferenceType,
35
43
  createScalarType,
44
+ createStringLiteralType,
36
45
  createUnionType,
37
46
  } from "../types/ts-type-reference-factory.js";
38
47
  import type {
@@ -41,6 +50,20 @@ import type {
41
50
  } from "../types/typescript.js";
42
51
  import type { GlobalTypeMapping } from "./type-extractor.js";
43
52
 
53
+ export interface DiscoveredTypeEntry {
54
+ readonly name: string;
55
+ readonly tsType: ts.Type;
56
+ readonly tsSymbol: ts.Symbol;
57
+ readonly sourceFile: string;
58
+ readonly sourceLocation: SourceLocation;
59
+ }
60
+
61
+ export interface FieldTypeResolverDiagnostic {
62
+ readonly code: DiagnosticCode;
63
+ readonly message: string;
64
+ readonly severity: "error" | "warning";
65
+ }
66
+
44
67
  export interface FieldTypeResolverContext {
45
68
  readonly checker: ts.TypeChecker;
46
69
  readonly knownTypeNames: ReadonlySet<string>;
@@ -52,6 +75,10 @@ export interface FieldTypeResolverContext {
52
75
  readonly scalarMappingTable: ScalarBaseTypeMappingTable | null;
53
76
  /** Current resolution context for scalar mapping (input or output) */
54
77
  readonly scalarMappingContext: ScalarMappingContext;
78
+ /** Mutable map for collecting transitively discovered types */
79
+ readonly discoveredTypes: Map<string, DiscoveredTypeEntry> | null;
80
+ /** Mutable array for collecting diagnostics during field type resolution */
81
+ readonly diagnostics: FieldTypeResolverDiagnostic[];
55
82
  }
56
83
 
57
84
  /**
@@ -141,6 +168,30 @@ function resolveFieldTypeInternal(
141
168
  }
142
169
  }
143
170
 
171
+ // Handle `T | null` where T is a known union type alias.
172
+ // TypeScript flattens `T | null` when T is itself a union, losing aliasSymbol.
173
+ // Recover the alias by inspecting the UnionTypeNode children for a TypeReferenceNode.
174
+ // Only applies when there is exactly one non-null type node (e.g., `Item | null`),
175
+ // not when there are multiple (e.g., `User | Post | null` which is an inline union).
176
+ if (nullable && typeNode && ts.isUnionTypeNode(typeNode)) {
177
+ const nonNullTypeNodes = filterNonNullTypeNodes(typeNode);
178
+ if (nonNullTypeNodes.length === 1) {
179
+ const singleTypeNode = nonNullTypeNodes[0]!;
180
+ if (ts.isTypeReferenceNode(singleTypeNode)) {
181
+ const typeName = getTypeNameFromNode(singleTypeNode);
182
+ const nodeSymbol = checker.getSymbolAtLocation(
183
+ singleTypeNode.typeName,
184
+ );
185
+ if (
186
+ typeName &&
187
+ isKnownSchemaType(typeName, nodeSymbol ?? undefined, ctx)
188
+ ) {
189
+ return createReferenceType({ name: typeName, nullable });
190
+ }
191
+ }
192
+ }
193
+ }
194
+
144
195
  const nonNullTypes = getNonNullableTypes(type);
145
196
 
146
197
  // Check if all non-null types belong to the same enum
@@ -207,11 +258,28 @@ function resolveFieldTypeInternal(
207
258
  return { ...innerResult, nullable };
208
259
  }
209
260
 
210
- const memberResults = nonNullTypes.map((t) =>
211
- resolveFieldTypeInternal(t, undefined, ctx),
212
- );
261
+ const memberResults = nonNullTypes.map((t) => {
262
+ const result = resolveFieldTypeInternal(t, undefined, ctx);
263
+ // If the result is an unresolvable reference and the original type is an
264
+ // object type with properties, try to expand it as an inline object.
265
+ // This handles external library types used as union members.
266
+ // When type discovery is active, discovered types are kept as references
267
+ // so they can be registered with their original names later.
268
+ if (
269
+ result.kind === "reference" &&
270
+ result.name !== null &&
271
+ !ctx.knownTypeNames.has(result.name) &&
272
+ t.flags & ts.TypeFlags.Object &&
273
+ t.getProperties().length > 0 &&
274
+ !ctx.discoveredTypes?.has(result.name)
275
+ ) {
276
+ return tryExtractAsInlineObject(t, ctx, result.name);
277
+ }
278
+ return result;
279
+ });
213
280
 
214
- return createUnionType({ members: memberResults, nullable });
281
+ const aliasName = type.aliasSymbol?.getName() ?? null;
282
+ return createUnionType({ members: memberResults, nullable, aliasName });
215
283
  }
216
284
 
217
285
  // Array type handling
@@ -231,6 +299,27 @@ function resolveFieldTypeInternal(
231
299
  return createArrayType(elementResult);
232
300
  }
233
301
 
302
+ // Never type — represents an impossible value, skip this field
303
+ // Also handles `undefined` which results from `field?: never` (never | undefined simplifies to undefined)
304
+ if (type.flags & ts.TypeFlags.Never || type.flags & ts.TypeFlags.Undefined) {
305
+ return createNeverType();
306
+ }
307
+
308
+ // Unknown type — represents arbitrary values, map to JSON scalar (graphql-scalars)
309
+ if (type.flags & ts.TypeFlags.Unknown) {
310
+ return createScalarType({
311
+ name: "JSON",
312
+ scalarInfo: {
313
+ scalarName: "JSON",
314
+ typeName: "unknown",
315
+ baseType: undefined,
316
+ isCustom: true,
317
+ only: null,
318
+ },
319
+ nullable: false,
320
+ });
321
+ }
322
+
234
323
  // Primitive types
235
324
  const typeString = checker.typeToString(type);
236
325
 
@@ -247,10 +336,14 @@ function resolveFieldTypeInternal(
247
336
  return createPrimitiveType({ name: "boolean", nullable: false });
248
337
  }
249
338
  if (type.flags & ts.TypeFlags.StringLiteral) {
250
- return createLiteralType(typeString.replace(/"/g, ""));
339
+ return createStringLiteralType(typeString.replace(/"/g, ""));
251
340
  }
252
341
  if (type.flags & ts.TypeFlags.NumberLiteral) {
253
- return createLiteralType(typeString);
342
+ return createNumericLiteralType(typeString);
343
+ }
344
+ // Template literal types (e.g., `prefix-${string}`) represent string subsets
345
+ if (type.flags & ts.TypeFlags.TemplateLiteral) {
346
+ return createPrimitiveType({ name: "string", nullable: false });
254
347
  }
255
348
 
256
349
  // Intersection types in field context
@@ -291,12 +384,69 @@ function resolveFieldTypeInternal(
291
384
  });
292
385
  }
293
386
 
294
- // 4. Otherwise, treat as inline object
295
- return tryExtractAsInlineObject(type, ctx);
387
+ // 4. Register in discoveredTypes if applicable (same logic as alias expansion)
388
+ // This handles non-exported recursive intersection types that would otherwise
389
+ // cause cycle detection failures in tryExtractAsInlineObject.
390
+ if (type.aliasSymbol) {
391
+ const aliasName = type.aliasSymbol.getName();
392
+ if (!knownTypeNames.has(aliasName)) {
393
+ if (ctx.discoveredTypes && !ctx.discoveredTypes.has(aliasName)) {
394
+ const resolvedAliasSymbol = resolveOriginalSymbol(
395
+ type.aliasSymbol,
396
+ checker,
397
+ );
398
+ const declarations = resolvedAliasSymbol.getDeclarations();
399
+ const decl = declarations?.[0];
400
+ if (decl && !decl.getSourceFile().isDeclarationFile) {
401
+ const properties = type.getProperties();
402
+ if (properties.length > 0) {
403
+ const declSourceFile = decl.getSourceFile();
404
+ const location = getSourceLocationFromNode(decl) ?? {
405
+ file: declSourceFile.fileName,
406
+ line: 1,
407
+ column: 1,
408
+ };
409
+ ctx.discoveredTypes.set(aliasName, {
410
+ name: aliasName,
411
+ tsType: type,
412
+ tsSymbol: resolvedAliasSymbol,
413
+ sourceFile: relative(process.cwd(), declSourceFile.fileName),
414
+ sourceLocation: location,
415
+ });
416
+ return createReferenceType({ name: aliasName, nullable: false });
417
+ }
418
+ }
419
+ }
420
+ if (ctx.discoveredTypes?.has(aliasName)) {
421
+ return createReferenceType({ name: aliasName, nullable: false });
422
+ }
423
+ }
424
+ }
425
+
426
+ // 5. Otherwise, treat as inline object
427
+ return tryExtractAsInlineObject(type, ctx, null);
296
428
  }
297
429
 
298
430
  // Inline object type handling
299
431
  if (isInlineObjectType(type)) {
432
+ // Index signature types (Record<string, T>, { [key: string]: T })
433
+ // These have no named properties, only index signatures — map to JSONObject (graphql-scalars)
434
+ const hasStringIndex =
435
+ checker.getIndexTypeOfType(type, ts.IndexKind.String) !== undefined;
436
+ if (hasStringIndex && type.getProperties().length === 0) {
437
+ return createScalarType({
438
+ name: "JSONObject",
439
+ scalarInfo: {
440
+ scalarName: "JSONObject",
441
+ typeName: "Record",
442
+ baseType: undefined,
443
+ isCustom: true,
444
+ only: null,
445
+ },
446
+ nullable: false,
447
+ });
448
+ }
449
+
300
450
  // Check if typeNode references a known type
301
451
  if (typeNode && ts.isTypeReferenceNode(typeNode)) {
302
452
  const typeName = getTypeNameFromNode(typeNode);
@@ -309,13 +459,30 @@ function resolveFieldTypeInternal(
309
459
  }
310
460
  }
311
461
 
312
- return tryExtractAsInlineObject(type, ctx);
462
+ return tryExtractAsInlineObject(type, ctx, null);
313
463
  }
314
464
 
315
465
  // Mapped types (utility types like Omit, Pick, user-defined utilities)
316
466
  if (type.flags & ts.TypeFlags.Object) {
317
467
  const objectType = type as ts.ObjectType;
318
468
  if (objectType.objectFlags & ts.ObjectFlags.Mapped) {
469
+ // Index signature mapped types (Record<string, T>) — map to JSONObject (graphql-scalars)
470
+ const hasMappedStringIndex =
471
+ checker.getIndexTypeOfType(type, ts.IndexKind.String) !== undefined;
472
+ if (hasMappedStringIndex && type.getProperties().length === 0) {
473
+ return createScalarType({
474
+ name: "JSONObject",
475
+ scalarInfo: {
476
+ scalarName: "JSONObject",
477
+ typeName: "Record",
478
+ baseType: undefined,
479
+ isCustom: true,
480
+ only: null,
481
+ },
482
+ nullable: false,
483
+ });
484
+ }
485
+
319
486
  // Check if typeNode references a known type (schema-defined type)
320
487
  if (typeNode && ts.isTypeReferenceNode(typeNode)) {
321
488
  const typeName = getTypeNameFromNode(typeNode);
@@ -328,7 +495,7 @@ function resolveFieldTypeInternal(
328
495
  }
329
496
  }
330
497
  // Not a known type - treat as inline object
331
- return tryExtractAsInlineObject(type, ctx);
498
+ return tryExtractAsInlineObject(type, ctx, null);
332
499
  }
333
500
  }
334
501
 
@@ -348,8 +515,42 @@ function resolveFieldTypeInternal(
348
515
  ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous) !== 0;
349
516
 
350
517
  if (isAnonymousObject) {
518
+ // Check if this type alias qualifies for transitive discovery.
519
+ // Type aliases to object literals used as union members should be
520
+ // discovered with their original alias name, not expanded as inline
521
+ // objects with auto-generated names. (#202)
522
+ if (ctx.discoveredTypes && !ctx.discoveredTypes.has(aliasName)) {
523
+ const resolvedAliasSymbol = resolveOriginalSymbol(
524
+ type.aliasSymbol,
525
+ checker,
526
+ );
527
+ const declarations = resolvedAliasSymbol.getDeclarations();
528
+ const decl = declarations?.[0];
529
+ if (decl && !decl.getSourceFile().isDeclarationFile) {
530
+ const properties = type.getProperties();
531
+ if (properties.length > 0) {
532
+ const declSourceFile = decl.getSourceFile();
533
+ const location = getSourceLocationFromNode(decl) ?? {
534
+ file: declSourceFile.fileName,
535
+ line: 1,
536
+ column: 1,
537
+ };
538
+ ctx.discoveredTypes.set(aliasName, {
539
+ name: aliasName,
540
+ tsType: type,
541
+ tsSymbol: resolvedAliasSymbol,
542
+ sourceFile: relative(process.cwd(), declSourceFile.fileName),
543
+ sourceLocation: location,
544
+ });
545
+ return createReferenceType({ name: aliasName, nullable: false });
546
+ }
547
+ }
548
+ }
549
+ if (ctx.discoveredTypes?.has(aliasName)) {
550
+ return createReferenceType({ name: aliasName, nullable: false });
551
+ }
351
552
  // Not a known schema type and is an anonymous object - expand to generate Payload type
352
- return tryExtractAsInlineObject(type, ctx);
553
+ return tryExtractAsInlineObject(type, ctx, null);
353
554
  }
354
555
  }
355
556
  }
@@ -411,7 +612,7 @@ function resolveFieldTypeInternal(
411
612
  return createReferenceType({ name: symbolName, nullable: false });
412
613
  }
413
614
  // Type from outside schema files - expand as inline object
414
- return tryExtractAsInlineObject(type, ctx);
615
+ return tryExtractAsInlineObject(type, ctx, null);
415
616
  }
416
617
 
417
618
  // Check for scalar base type mapping
@@ -461,6 +662,30 @@ function resolveFieldTypeInternal(
461
662
  }
462
663
  }
463
664
 
665
+ // Discover extractable named types for transitive type registration
666
+ if (ctx.discoveredTypes && !ctx.discoveredTypes.has(symbolName)) {
667
+ const declarations = resolvedSymbol.getDeclarations();
668
+ const decl = declarations?.[0];
669
+ if (decl && !decl.getSourceFile().isDeclarationFile) {
670
+ const properties = type.getProperties();
671
+ if (properties.length > 0) {
672
+ const declSourceFile = decl.getSourceFile();
673
+ const location = getSourceLocationFromNode(decl) ?? {
674
+ file: declSourceFile.fileName,
675
+ line: 1,
676
+ column: 1,
677
+ };
678
+ ctx.discoveredTypes.set(symbolName, {
679
+ name: symbolName,
680
+ tsType: type,
681
+ tsSymbol: resolvedSymbol,
682
+ sourceFile: relative(process.cwd(), declSourceFile.fileName),
683
+ sourceLocation: location,
684
+ });
685
+ }
686
+ }
687
+ }
688
+
464
689
  // Unknown type - still return reference but it will likely cause validation error later
465
690
  return createReferenceType({ name: symbolName, nullable: false });
466
691
  }
@@ -472,12 +697,28 @@ function resolveFieldTypeInternal(
472
697
  function tryExtractAsInlineObject(
473
698
  type: ts.Type,
474
699
  ctx: InternalFieldTypeContext,
700
+ hintName: string | null,
475
701
  ): TSTypeReference {
476
702
  const { visitedTypes, checker } = ctx;
477
703
  if (visitedTypes.has(type)) {
478
- // Cycle detected, return a placeholder reference
479
- const typeName = type.symbol?.getName() ?? "Unknown";
480
- return createReferenceType({ name: typeName, nullable: false });
704
+ // Prefer aliasSymbol (type alias name) over type.symbol (which may be __type for anonymous objects)
705
+ const candidateName = type.aliasSymbol?.getName() ?? type.symbol?.getName();
706
+ if (
707
+ candidateName &&
708
+ !isInternalTypeSymbol(candidateName) &&
709
+ (ctx.knownTypeNames.has(candidateName) ||
710
+ ctx.discoveredTypes?.has(candidateName))
711
+ ) {
712
+ // Valid reference target exists in the schema
713
+ return createReferenceType({ name: candidateName, nullable: false });
714
+ }
715
+ // No valid reference target — emit warning and skip field
716
+ ctx.diagnostics.push({
717
+ code: "CYCLE_DETECTED",
718
+ message: "Cycle detected in type resolution; field will be skipped",
719
+ severity: "warning",
720
+ });
721
+ return createNeverType();
481
722
  }
482
723
 
483
724
  visitedTypes.add(type);
@@ -488,6 +729,14 @@ function tryExtractAsInlineObject(
488
729
  (propType) => resolveFieldTypeInternal(propType, undefined, ctx),
489
730
  );
490
731
 
732
+ // Allow this type to be visited again in sibling union members.
733
+ // Cycle detection still works because real cycles are caught during the
734
+ // recursive extractInlineObjectPropertiesShared call above (while the
735
+ // type is still in visitedTypes). Removing it afterward prevents
736
+ // false-positive cycle detection when TypeScript shares the same type
737
+ // object across multiple union members (common with generic .d.ts types).
738
+ visitedTypes.delete(type);
739
+
491
740
  // Extract type-level TSDoc from the alias symbol if present (Requirement 7.2)
492
741
  // Only extract from user-defined types, not built-in TypeScript utility types
493
742
  let description: string | null = null;
@@ -511,6 +760,7 @@ function tryExtractAsInlineObject(
511
760
  properties: inlineProperties,
512
761
  description,
513
762
  deprecated,
763
+ hintName,
514
764
  });
515
765
  }
516
766