@gqlkit-ts/cli 0.6.0 → 0.7.0

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 (190) 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 +198 -15
  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 +100 -9
  128. package/dist/type-extractor/extractor/type-extractor.js.map +1 -1
  129. package/dist/type-extractor/types/diagnostics.d.ts +1 -1
  130. package/dist/type-extractor/types/diagnostics.d.ts.map +1 -1
  131. package/dist/type-extractor/types/index.d.ts +1 -1
  132. package/dist/type-extractor/types/index.d.ts.map +1 -1
  133. package/dist/type-extractor/types/index.js +1 -1
  134. package/dist/type-extractor/types/index.js.map +1 -1
  135. package/dist/type-extractor/types/ts-type-reference-factory.d.ts +7 -1
  136. package/dist/type-extractor/types/ts-type-reference-factory.d.ts.map +1 -1
  137. package/dist/type-extractor/types/ts-type-reference-factory.js +18 -3
  138. package/dist/type-extractor/types/ts-type-reference-factory.js.map +1 -1
  139. package/dist/type-extractor/types/typescript.d.ts +3 -1
  140. package/dist/type-extractor/types/typescript.d.ts.map +1 -1
  141. package/dist/type-extractor/validator/type-validator.d.ts.map +1 -1
  142. package/dist/type-extractor/validator/type-validator.js +6 -1
  143. package/dist/type-extractor/validator/type-validator.js.map +1 -1
  144. package/docs/configuration.md +19 -0
  145. package/docs/index.md +1 -0
  146. package/docs/integration/ai-sdk.md +189 -0
  147. package/docs/schema/unions.md +117 -0
  148. package/package.json +2 -2
  149. package/src/auto-type-generator/auto-type-generator.ts +576 -58
  150. package/src/auto-type-generator/discriminator-field-validator.ts +368 -0
  151. package/src/auto-type-generator/discriminator-naming.ts +24 -0
  152. package/src/auto-type-generator/discriminator-resolve-type-generator.ts +136 -0
  153. package/src/auto-type-generator/index.ts +17 -0
  154. package/src/auto-type-generator/inline-enum-collector.ts +19 -4
  155. package/src/auto-type-generator/inline-object-converter.ts +100 -0
  156. package/src/auto-type-generator/inline-object-traverser.ts +33 -7
  157. package/src/auto-type-generator/inline-union-collector.ts +26 -4
  158. package/src/auto-type-generator/inline-union-types.ts +2 -0
  159. package/src/auto-type-generator/inline-union-validator.ts +3 -3
  160. package/src/auto-type-generator/intersection-flattener.ts +554 -0
  161. package/src/auto-type-generator/naming-convention.ts +205 -1
  162. package/src/auto-type-generator/typename-extractor.ts +17 -3
  163. package/src/auto-type-generator/typename-resolve-type-generator.ts +19 -108
  164. package/src/auto-type-generator/typename-types.ts +7 -0
  165. package/src/auto-type-generator/typename-validator.ts +4 -3
  166. package/src/commands/gen.ts +9 -2
  167. package/src/config/types.ts +10 -0
  168. package/src/config-loader/index.ts +1 -0
  169. package/src/config-loader/loader.ts +11 -0
  170. package/src/config-loader/validator.ts +100 -1
  171. package/src/gen-orchestrator/orchestrator.ts +19 -2
  172. package/src/resolver-extractor/extractor/define-api-extractor.ts +4 -0
  173. package/src/resolver-extractor/validator/abstract-resolver-validator.ts +20 -6
  174. package/src/schema-generator/emitter/code-emitter.ts +26 -1
  175. package/src/schema-generator/emitter/discriminator-resolve-type-emitter.ts +125 -0
  176. package/src/schema-generator/generate-schema.ts +100 -13
  177. package/src/schema-generator/integrator/result-integrator.ts +25 -1
  178. package/src/schema-generator/resolver-collector/resolver-collector.ts +7 -0
  179. package/src/shared/constants.ts +15 -1
  180. package/src/shared/enum-prefix-detector.ts +96 -8
  181. package/src/shared/inline-object-utils.ts +1 -1
  182. package/src/shared/type-converter.ts +63 -0
  183. package/src/type-extractor/converter/graphql-converter.ts +17 -1
  184. package/src/type-extractor/extractor/field-type-resolver.ts +241 -16
  185. package/src/type-extractor/extractor/type-extractor.ts +119 -5
  186. package/src/type-extractor/types/diagnostics.ts +10 -1
  187. package/src/type-extractor/types/index.ts +2 -1
  188. package/src/type-extractor/types/ts-type-reference-factory.ts +24 -3
  189. package/src/type-extractor/types/typescript.ts +6 -2
  190. 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,6 +8,10 @@ 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,
@@ -25,14 +30,17 @@ import type {
25
30
  ScalarMappingContext,
26
31
  } from "../mapper/scalar-base-type-mapper.js";
27
32
  import { lookupScalarMapping } from "../mapper/scalar-base-type-mapper.js";
33
+ import type { DiagnosticCode } from "../types/diagnostics.js";
28
34
  import {
29
35
  createArrayType,
30
36
  createInlineEnumType,
31
37
  createInlineObjectType,
32
- createLiteralType,
38
+ createNeverType,
39
+ createNumericLiteralType,
33
40
  createPrimitiveType,
34
41
  createReferenceType,
35
42
  createScalarType,
43
+ createStringLiteralType,
36
44
  createUnionType,
37
45
  } from "../types/ts-type-reference-factory.js";
38
46
  import type {
@@ -41,6 +49,20 @@ import type {
41
49
  } from "../types/typescript.js";
42
50
  import type { GlobalTypeMapping } from "./type-extractor.js";
43
51
 
52
+ export interface DiscoveredTypeEntry {
53
+ readonly name: string;
54
+ readonly tsType: ts.Type;
55
+ readonly tsSymbol: ts.Symbol;
56
+ readonly sourceFile: string;
57
+ readonly sourceLocation: SourceLocation;
58
+ }
59
+
60
+ export interface FieldTypeResolverDiagnostic {
61
+ readonly code: DiagnosticCode;
62
+ readonly message: string;
63
+ readonly severity: "error" | "warning";
64
+ }
65
+
44
66
  export interface FieldTypeResolverContext {
45
67
  readonly checker: ts.TypeChecker;
46
68
  readonly knownTypeNames: ReadonlySet<string>;
@@ -52,6 +74,10 @@ export interface FieldTypeResolverContext {
52
74
  readonly scalarMappingTable: ScalarBaseTypeMappingTable | null;
53
75
  /** Current resolution context for scalar mapping (input or output) */
54
76
  readonly scalarMappingContext: ScalarMappingContext;
77
+ /** Mutable map for collecting transitively discovered types */
78
+ readonly discoveredTypes: Map<string, DiscoveredTypeEntry> | null;
79
+ /** Mutable array for collecting diagnostics during field type resolution */
80
+ readonly diagnostics: FieldTypeResolverDiagnostic[];
55
81
  }
56
82
 
57
83
  /**
@@ -207,11 +233,28 @@ function resolveFieldTypeInternal(
207
233
  return { ...innerResult, nullable };
208
234
  }
209
235
 
210
- const memberResults = nonNullTypes.map((t) =>
211
- resolveFieldTypeInternal(t, undefined, ctx),
212
- );
236
+ const memberResults = nonNullTypes.map((t) => {
237
+ const result = resolveFieldTypeInternal(t, undefined, ctx);
238
+ // If the result is an unresolvable reference and the original type is an
239
+ // object type with properties, try to expand it as an inline object.
240
+ // This handles external library types used as union members.
241
+ // When type discovery is active, discovered types are kept as references
242
+ // so they can be registered with their original names later.
243
+ if (
244
+ result.kind === "reference" &&
245
+ result.name !== null &&
246
+ !ctx.knownTypeNames.has(result.name) &&
247
+ t.flags & ts.TypeFlags.Object &&
248
+ t.getProperties().length > 0 &&
249
+ !ctx.discoveredTypes?.has(result.name)
250
+ ) {
251
+ return tryExtractAsInlineObject(t, ctx, result.name);
252
+ }
253
+ return result;
254
+ });
213
255
 
214
- return createUnionType({ members: memberResults, nullable });
256
+ const aliasName = type.aliasSymbol?.getName() ?? null;
257
+ return createUnionType({ members: memberResults, nullable, aliasName });
215
258
  }
216
259
 
217
260
  // Array type handling
@@ -231,6 +274,27 @@ function resolveFieldTypeInternal(
231
274
  return createArrayType(elementResult);
232
275
  }
233
276
 
277
+ // Never type — represents an impossible value, skip this field
278
+ // Also handles `undefined` which results from `field?: never` (never | undefined simplifies to undefined)
279
+ if (type.flags & ts.TypeFlags.Never || type.flags & ts.TypeFlags.Undefined) {
280
+ return createNeverType();
281
+ }
282
+
283
+ // Unknown type — represents arbitrary values, map to JSON scalar (graphql-scalars)
284
+ if (type.flags & ts.TypeFlags.Unknown) {
285
+ return createScalarType({
286
+ name: "JSON",
287
+ scalarInfo: {
288
+ scalarName: "JSON",
289
+ typeName: "unknown",
290
+ baseType: undefined,
291
+ isCustom: true,
292
+ only: null,
293
+ },
294
+ nullable: false,
295
+ });
296
+ }
297
+
234
298
  // Primitive types
235
299
  const typeString = checker.typeToString(type);
236
300
 
@@ -247,10 +311,14 @@ function resolveFieldTypeInternal(
247
311
  return createPrimitiveType({ name: "boolean", nullable: false });
248
312
  }
249
313
  if (type.flags & ts.TypeFlags.StringLiteral) {
250
- return createLiteralType(typeString.replace(/"/g, ""));
314
+ return createStringLiteralType(typeString.replace(/"/g, ""));
251
315
  }
252
316
  if (type.flags & ts.TypeFlags.NumberLiteral) {
253
- return createLiteralType(typeString);
317
+ return createNumericLiteralType(typeString);
318
+ }
319
+ // Template literal types (e.g., `prefix-${string}`) represent string subsets
320
+ if (type.flags & ts.TypeFlags.TemplateLiteral) {
321
+ return createPrimitiveType({ name: "string", nullable: false });
254
322
  }
255
323
 
256
324
  // Intersection types in field context
@@ -291,12 +359,69 @@ function resolveFieldTypeInternal(
291
359
  });
292
360
  }
293
361
 
294
- // 4. Otherwise, treat as inline object
295
- return tryExtractAsInlineObject(type, ctx);
362
+ // 4. Register in discoveredTypes if applicable (same logic as alias expansion)
363
+ // This handles non-exported recursive intersection types that would otherwise
364
+ // cause cycle detection failures in tryExtractAsInlineObject.
365
+ if (type.aliasSymbol) {
366
+ const aliasName = type.aliasSymbol.getName();
367
+ if (!knownTypeNames.has(aliasName)) {
368
+ if (ctx.discoveredTypes && !ctx.discoveredTypes.has(aliasName)) {
369
+ const resolvedAliasSymbol = resolveOriginalSymbol(
370
+ type.aliasSymbol,
371
+ checker,
372
+ );
373
+ const declarations = resolvedAliasSymbol.getDeclarations();
374
+ const decl = declarations?.[0];
375
+ if (decl && !decl.getSourceFile().isDeclarationFile) {
376
+ const properties = type.getProperties();
377
+ if (properties.length > 0) {
378
+ const declSourceFile = decl.getSourceFile();
379
+ const location = getSourceLocationFromNode(decl) ?? {
380
+ file: declSourceFile.fileName,
381
+ line: 1,
382
+ column: 1,
383
+ };
384
+ ctx.discoveredTypes.set(aliasName, {
385
+ name: aliasName,
386
+ tsType: type,
387
+ tsSymbol: resolvedAliasSymbol,
388
+ sourceFile: relative(process.cwd(), declSourceFile.fileName),
389
+ sourceLocation: location,
390
+ });
391
+ return createReferenceType({ name: aliasName, nullable: false });
392
+ }
393
+ }
394
+ }
395
+ if (ctx.discoveredTypes?.has(aliasName)) {
396
+ return createReferenceType({ name: aliasName, nullable: false });
397
+ }
398
+ }
399
+ }
400
+
401
+ // 5. Otherwise, treat as inline object
402
+ return tryExtractAsInlineObject(type, ctx, null);
296
403
  }
297
404
 
298
405
  // Inline object type handling
299
406
  if (isInlineObjectType(type)) {
407
+ // Index signature types (Record<string, T>, { [key: string]: T })
408
+ // These have no named properties, only index signatures — map to JSONObject (graphql-scalars)
409
+ const hasStringIndex =
410
+ checker.getIndexTypeOfType(type, ts.IndexKind.String) !== undefined;
411
+ if (hasStringIndex && type.getProperties().length === 0) {
412
+ return createScalarType({
413
+ name: "JSONObject",
414
+ scalarInfo: {
415
+ scalarName: "JSONObject",
416
+ typeName: "Record",
417
+ baseType: undefined,
418
+ isCustom: true,
419
+ only: null,
420
+ },
421
+ nullable: false,
422
+ });
423
+ }
424
+
300
425
  // Check if typeNode references a known type
301
426
  if (typeNode && ts.isTypeReferenceNode(typeNode)) {
302
427
  const typeName = getTypeNameFromNode(typeNode);
@@ -309,13 +434,30 @@ function resolveFieldTypeInternal(
309
434
  }
310
435
  }
311
436
 
312
- return tryExtractAsInlineObject(type, ctx);
437
+ return tryExtractAsInlineObject(type, ctx, null);
313
438
  }
314
439
 
315
440
  // Mapped types (utility types like Omit, Pick, user-defined utilities)
316
441
  if (type.flags & ts.TypeFlags.Object) {
317
442
  const objectType = type as ts.ObjectType;
318
443
  if (objectType.objectFlags & ts.ObjectFlags.Mapped) {
444
+ // Index signature mapped types (Record<string, T>) — map to JSONObject (graphql-scalars)
445
+ const hasMappedStringIndex =
446
+ checker.getIndexTypeOfType(type, ts.IndexKind.String) !== undefined;
447
+ if (hasMappedStringIndex && type.getProperties().length === 0) {
448
+ return createScalarType({
449
+ name: "JSONObject",
450
+ scalarInfo: {
451
+ scalarName: "JSONObject",
452
+ typeName: "Record",
453
+ baseType: undefined,
454
+ isCustom: true,
455
+ only: null,
456
+ },
457
+ nullable: false,
458
+ });
459
+ }
460
+
319
461
  // Check if typeNode references a known type (schema-defined type)
320
462
  if (typeNode && ts.isTypeReferenceNode(typeNode)) {
321
463
  const typeName = getTypeNameFromNode(typeNode);
@@ -328,7 +470,7 @@ function resolveFieldTypeInternal(
328
470
  }
329
471
  }
330
472
  // Not a known type - treat as inline object
331
- return tryExtractAsInlineObject(type, ctx);
473
+ return tryExtractAsInlineObject(type, ctx, null);
332
474
  }
333
475
  }
334
476
 
@@ -348,8 +490,42 @@ function resolveFieldTypeInternal(
348
490
  ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous) !== 0;
349
491
 
350
492
  if (isAnonymousObject) {
493
+ // Check if this type alias qualifies for transitive discovery.
494
+ // Type aliases to object literals used as union members should be
495
+ // discovered with their original alias name, not expanded as inline
496
+ // objects with auto-generated names. (#202)
497
+ if (ctx.discoveredTypes && !ctx.discoveredTypes.has(aliasName)) {
498
+ const resolvedAliasSymbol = resolveOriginalSymbol(
499
+ type.aliasSymbol,
500
+ checker,
501
+ );
502
+ const declarations = resolvedAliasSymbol.getDeclarations();
503
+ const decl = declarations?.[0];
504
+ if (decl && !decl.getSourceFile().isDeclarationFile) {
505
+ const properties = type.getProperties();
506
+ if (properties.length > 0) {
507
+ const declSourceFile = decl.getSourceFile();
508
+ const location = getSourceLocationFromNode(decl) ?? {
509
+ file: declSourceFile.fileName,
510
+ line: 1,
511
+ column: 1,
512
+ };
513
+ ctx.discoveredTypes.set(aliasName, {
514
+ name: aliasName,
515
+ tsType: type,
516
+ tsSymbol: resolvedAliasSymbol,
517
+ sourceFile: relative(process.cwd(), declSourceFile.fileName),
518
+ sourceLocation: location,
519
+ });
520
+ return createReferenceType({ name: aliasName, nullable: false });
521
+ }
522
+ }
523
+ }
524
+ if (ctx.discoveredTypes?.has(aliasName)) {
525
+ return createReferenceType({ name: aliasName, nullable: false });
526
+ }
351
527
  // Not a known schema type and is an anonymous object - expand to generate Payload type
352
- return tryExtractAsInlineObject(type, ctx);
528
+ return tryExtractAsInlineObject(type, ctx, null);
353
529
  }
354
530
  }
355
531
  }
@@ -411,7 +587,7 @@ function resolveFieldTypeInternal(
411
587
  return createReferenceType({ name: symbolName, nullable: false });
412
588
  }
413
589
  // Type from outside schema files - expand as inline object
414
- return tryExtractAsInlineObject(type, ctx);
590
+ return tryExtractAsInlineObject(type, ctx, null);
415
591
  }
416
592
 
417
593
  // Check for scalar base type mapping
@@ -461,6 +637,30 @@ function resolveFieldTypeInternal(
461
637
  }
462
638
  }
463
639
 
640
+ // Discover extractable named types for transitive type registration
641
+ if (ctx.discoveredTypes && !ctx.discoveredTypes.has(symbolName)) {
642
+ const declarations = resolvedSymbol.getDeclarations();
643
+ const decl = declarations?.[0];
644
+ if (decl && !decl.getSourceFile().isDeclarationFile) {
645
+ const properties = type.getProperties();
646
+ if (properties.length > 0) {
647
+ const declSourceFile = decl.getSourceFile();
648
+ const location = getSourceLocationFromNode(decl) ?? {
649
+ file: declSourceFile.fileName,
650
+ line: 1,
651
+ column: 1,
652
+ };
653
+ ctx.discoveredTypes.set(symbolName, {
654
+ name: symbolName,
655
+ tsType: type,
656
+ tsSymbol: resolvedSymbol,
657
+ sourceFile: relative(process.cwd(), declSourceFile.fileName),
658
+ sourceLocation: location,
659
+ });
660
+ }
661
+ }
662
+ }
663
+
464
664
  // Unknown type - still return reference but it will likely cause validation error later
465
665
  return createReferenceType({ name: symbolName, nullable: false });
466
666
  }
@@ -472,12 +672,28 @@ function resolveFieldTypeInternal(
472
672
  function tryExtractAsInlineObject(
473
673
  type: ts.Type,
474
674
  ctx: InternalFieldTypeContext,
675
+ hintName: string | null,
475
676
  ): TSTypeReference {
476
677
  const { visitedTypes, checker } = ctx;
477
678
  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 });
679
+ // Prefer aliasSymbol (type alias name) over type.symbol (which may be __type for anonymous objects)
680
+ const candidateName = type.aliasSymbol?.getName() ?? type.symbol?.getName();
681
+ if (
682
+ candidateName &&
683
+ !isInternalTypeSymbol(candidateName) &&
684
+ (ctx.knownTypeNames.has(candidateName) ||
685
+ ctx.discoveredTypes?.has(candidateName))
686
+ ) {
687
+ // Valid reference target exists in the schema
688
+ return createReferenceType({ name: candidateName, nullable: false });
689
+ }
690
+ // No valid reference target — emit warning and skip field
691
+ ctx.diagnostics.push({
692
+ code: "CYCLE_DETECTED",
693
+ message: "Cycle detected in type resolution; field will be skipped",
694
+ severity: "warning",
695
+ });
696
+ return createNeverType();
481
697
  }
482
698
 
483
699
  visitedTypes.add(type);
@@ -488,6 +704,14 @@ function tryExtractAsInlineObject(
488
704
  (propType) => resolveFieldTypeInternal(propType, undefined, ctx),
489
705
  );
490
706
 
707
+ // Allow this type to be visited again in sibling union members.
708
+ // Cycle detection still works because real cycles are caught during the
709
+ // recursive extractInlineObjectPropertiesShared call above (while the
710
+ // type is still in visitedTypes). Removing it afterward prevents
711
+ // false-positive cycle detection when TypeScript shares the same type
712
+ // object across multiple union members (common with generic .d.ts types).
713
+ visitedTypes.delete(type);
714
+
491
715
  // Extract type-level TSDoc from the alias symbol if present (Requirement 7.2)
492
716
  // Only extract from user-defined types, not built-in TypeScript utility types
493
717
  let description: string | null = null;
@@ -511,6 +735,7 @@ function tryExtractAsInlineObject(
511
735
  properties: inlineProperties,
512
736
  description,
513
737
  deprecated,
738
+ hintName,
514
739
  });
515
740
  }
516
741