@gqlkit-ts/cli 0.3.0 → 0.4.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 (82) hide show
  1. package/README.md +143 -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 +16 -13
  4. package/dist/auto-type-generator/auto-type-generator.js.map +1 -1
  5. package/dist/config/types.d.ts +13 -0
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/config-loader/loader.d.ts +3 -0
  8. package/dist/config-loader/loader.d.ts.map +1 -1
  9. package/dist/config-loader/loader.js +1 -0
  10. package/dist/config-loader/loader.js.map +1 -1
  11. package/dist/config-loader/validator.d.ts.map +1 -1
  12. package/dist/config-loader/validator.js +23 -0
  13. package/dist/config-loader/validator.js.map +1 -1
  14. package/dist/gen-orchestrator/orchestrator.d.ts.map +1 -1
  15. package/dist/gen-orchestrator/orchestrator.js +19 -6
  16. package/dist/gen-orchestrator/orchestrator.js.map +1 -1
  17. package/dist/resolver-extractor/extractor/define-api-extractor.js +4 -4
  18. package/dist/resolver-extractor/extractor/define-api-extractor.js.map +1 -1
  19. package/dist/schema-generator/builder/ast-builder.d.ts +2 -2
  20. package/dist/schema-generator/builder/ast-builder.d.ts.map +1 -1
  21. package/dist/schema-generator/builder/ast-builder.js +12 -34
  22. package/dist/schema-generator/builder/ast-builder.js.map +1 -1
  23. package/dist/schema-generator/emitter/code-emitter.d.ts +3 -1
  24. package/dist/schema-generator/emitter/code-emitter.d.ts.map +1 -1
  25. package/dist/schema-generator/emitter/code-emitter.js +22 -12
  26. package/dist/schema-generator/emitter/code-emitter.js.map +1 -1
  27. package/dist/schema-generator/emitter/sdl-emitter.d.ts +0 -4
  28. package/dist/schema-generator/emitter/sdl-emitter.d.ts.map +1 -1
  29. package/dist/schema-generator/emitter/sdl-emitter.js +0 -4
  30. package/dist/schema-generator/emitter/sdl-emitter.js.map +1 -1
  31. package/dist/schema-generator/generate-schema.d.ts +2 -0
  32. package/dist/schema-generator/generate-schema.d.ts.map +1 -1
  33. package/dist/schema-generator/generate-schema.js +13 -4
  34. package/dist/schema-generator/generate-schema.js.map +1 -1
  35. package/dist/schema-generator/integrator/result-integrator.d.ts +10 -12
  36. package/dist/schema-generator/integrator/result-integrator.d.ts.map +1 -1
  37. package/dist/schema-generator/integrator/result-integrator.js +18 -55
  38. package/dist/schema-generator/integrator/result-integrator.js.map +1 -1
  39. package/dist/shared/branded-type-detector.d.ts +43 -0
  40. package/dist/shared/branded-type-detector.d.ts.map +1 -0
  41. package/dist/shared/branded-type-detector.js +146 -0
  42. package/dist/shared/branded-type-detector.js.map +1 -0
  43. package/dist/shared/string-utils.d.ts +2 -0
  44. package/dist/shared/string-utils.d.ts.map +1 -0
  45. package/dist/shared/string-utils.js +8 -0
  46. package/dist/shared/string-utils.js.map +1 -0
  47. package/dist/type-extractor/converter/field-eligibility.d.ts +8 -6
  48. package/dist/type-extractor/converter/field-eligibility.d.ts.map +1 -1
  49. package/dist/type-extractor/converter/field-eligibility.js +7 -28
  50. package/dist/type-extractor/converter/field-eligibility.js.map +1 -1
  51. package/dist/type-extractor/converter/graphql-converter.d.ts.map +1 -1
  52. package/dist/type-extractor/converter/graphql-converter.js +6 -11
  53. package/dist/type-extractor/converter/graphql-converter.js.map +1 -1
  54. package/dist/type-extractor/extractor/field-type-resolver.d.ts.map +1 -1
  55. package/dist/type-extractor/extractor/field-type-resolver.js +39 -4
  56. package/dist/type-extractor/extractor/field-type-resolver.js.map +1 -1
  57. package/dist/type-extractor/types/diagnostics.d.ts +1 -1
  58. package/dist/type-extractor/types/diagnostics.d.ts.map +1 -1
  59. package/dist/type-extractor/validator/type-validator.d.ts +1 -1
  60. package/dist/type-extractor/validator/type-validator.d.ts.map +1 -1
  61. package/dist/type-extractor/validator/type-validator.js +2 -10
  62. package/dist/type-extractor/validator/type-validator.js.map +1 -1
  63. package/docs/configuration.md +9 -0
  64. package/package.json +1 -1
  65. package/src/auto-type-generator/auto-type-generator.ts +23 -26
  66. package/src/config/types.ts +15 -0
  67. package/src/config-loader/loader.ts +4 -0
  68. package/src/config-loader/validator.ts +33 -0
  69. package/src/gen-orchestrator/orchestrator.ts +30 -11
  70. package/src/resolver-extractor/extractor/define-api-extractor.ts +5 -5
  71. package/src/schema-generator/builder/ast-builder.ts +52 -81
  72. package/src/schema-generator/emitter/code-emitter.ts +48 -11
  73. package/src/schema-generator/emitter/sdl-emitter.ts +0 -4
  74. package/src/schema-generator/generate-schema.ts +13 -15
  75. package/src/schema-generator/integrator/result-integrator.ts +37 -78
  76. package/src/shared/branded-type-detector.ts +182 -0
  77. package/src/shared/string-utils.ts +7 -0
  78. package/src/type-extractor/converter/field-eligibility.ts +13 -29
  79. package/src/type-extractor/converter/graphql-converter.ts +6 -16
  80. package/src/type-extractor/extractor/field-type-resolver.ts +49 -4
  81. package/src/type-extractor/types/diagnostics.ts +1 -0
  82. package/src/type-extractor/validator/type-validator.ts +2 -15
@@ -6,6 +6,7 @@ import {
6
6
  validateSchemaTypenames,
7
7
  validateTypenames,
8
8
  } from "../auto-type-generator/index.js";
9
+ import type { ImportExtension } from "../config/types.js";
9
10
  import type { ExtractResolversResult } from "../resolver-extractor/index.js";
10
11
  import type { DirectiveDefinitionInfo } from "../shared/directive-definition-extractor.js";
11
12
  import type { CollectedScalarType } from "../type-extractor/collector/scalar-collector.js";
@@ -33,6 +34,7 @@ export interface GenerateSchemaInput {
33
34
  readonly enablePruning: boolean | null;
34
35
  readonly sourceRoot: string | null;
35
36
  readonly knownTypeNames: ReadonlySet<string> | null;
37
+ readonly importExtension: ImportExtension;
36
38
  }
37
39
 
38
40
  export interface GenerateSchemaResult {
@@ -58,6 +60,7 @@ export function generateSchema(
58
60
  enablePruning,
59
61
  sourceRoot,
60
62
  knownTypeNames,
63
+ importExtension,
61
64
  } = input;
62
65
 
63
66
  const autoTypeResult = generateAutoTypes({
@@ -216,20 +219,17 @@ export function generateSchema(
216
219
  ...typenameResolveTypesResult.generatedObjectTypes,
217
220
  ];
218
221
 
219
- const integratedResult = integrate(
220
- updatedTypesForIntegration,
221
- autoTypeResult.updatedResolversResult,
222
+ const integratedResult = integrate({
223
+ typesResult: updatedTypesForIntegration,
224
+ resolversResult: autoTypeResult.updatedResolversResult,
222
225
  customScalarNames,
223
- customScalars,
226
+ collectedScalars: customScalars,
224
227
  directiveDefinitions,
225
- allAutoGeneratedTypes,
226
- typenameResolveTypesResult.autoResolveTypes,
227
- );
228
+ autoGeneratedTypes: allAutoGeneratedTypes,
229
+ typenameAutoResolveTypes: typenameResolveTypesResult.autoResolveTypes,
230
+ });
228
231
 
229
- let documentNode = buildDocumentNode(
230
- integratedResult,
231
- sourceRoot !== null ? { sourceRoot } : undefined,
232
- );
232
+ let documentNode = buildDocumentNode(integratedResult, { sourceRoot });
233
233
  let prunedTypes: ReadonlyArray<string> | null = null;
234
234
 
235
235
  if (enablePruning) {
@@ -241,10 +241,7 @@ export function generateSchema(
241
241
  prunedTypes = pruneResult.removedTypes;
242
242
  }
243
243
 
244
- const typeDefsCode = emitTypeDefsCode(
245
- integratedResult,
246
- sourceRoot !== null ? { sourceRoot } : undefined,
247
- );
244
+ const typeDefsCode = emitTypeDefsCode(integratedResult, { sourceRoot });
248
245
  const sdlContent = emitSdlContent(documentNode);
249
246
 
250
247
  const resolverInfo = collectResolverInfo(integratedResult);
@@ -254,6 +251,7 @@ export function generateSchema(
254
251
  customScalars: customScalars ?? [],
255
252
  numericEnums: integratedResult.numericEnums,
256
253
  stringEnumMappings: integratedResult.stringEnumMappings,
254
+ importExtension,
257
255
  });
258
256
 
259
257
  return {
@@ -136,23 +136,12 @@ export interface IntegratedResult {
136
136
  readonly baseTypes: ReadonlyArray<BaseType>;
137
137
  readonly inputTypes: ReadonlyArray<InputType>;
138
138
  readonly typeExtensions: ReadonlyArray<TypeExtension>;
139
- /** @deprecated Use customScalars instead */
140
- readonly customScalarNames: ReadonlyArray<string> | null;
141
- /** Custom scalars with description information */
142
139
  readonly customScalars: ReadonlyArray<CustomScalarInfo> | null;
143
- /** Directive definitions extracted from type aliases */
144
140
  readonly directiveDefinitions: ReadonlyArray<DirectiveDefinitionInfo> | null;
145
- /** Abstract type resolvers (resolveType and isTypeOf) */
146
141
  readonly abstractTypeResolvers: ReadonlyArray<AbstractResolverInfo>;
147
- /** @deprecated Use autoGeneratedUnions instead */
148
- readonly autoGeneratedUnionNames: ReadonlyArray<string>;
149
- /** Auto-generated Union types that need __resolveType with field pattern info */
150
142
  readonly autoGeneratedUnions: ReadonlyArray<AutoGeneratedUnionInfo>;
151
- /** Union/Interface types that have typename-based auto resolveType */
152
143
  readonly typenameAutoResolveTypes: ReadonlyArray<TypenameAutoResolveTypeInfo>;
153
- /** Numeric enum information for resolver generation */
154
144
  readonly numericEnums: ReadonlyArray<NumericEnumInfo>;
155
- /** String enum mappings for resolver generation */
156
145
  readonly stringEnumMappings: ReadonlyArray<StringEnumMappingInfo>;
157
146
  readonly hasQuery: boolean;
158
147
  readonly hasMutation: boolean;
@@ -160,6 +149,17 @@ export interface IntegratedResult {
160
149
  readonly diagnostics: ReadonlyArray<Diagnostic>;
161
150
  }
162
151
 
152
+ function toBaseField(field: BaseField): BaseField {
153
+ return {
154
+ name: field.name,
155
+ type: field.type,
156
+ description: field.description,
157
+ deprecated: field.deprecated,
158
+ directives: field.directives,
159
+ defaultValue: field.defaultValue,
160
+ };
161
+ }
162
+
163
163
  function convertToExtensionField(
164
164
  field: GraphQLFieldDefinition,
165
165
  ): ExtensionField {
@@ -267,15 +267,26 @@ function getCompatibleLocations(
267
267
  }
268
268
  }
269
269
 
270
- export function integrate(
271
- typesResult: ExtractTypesResult,
272
- resolversResult: ExtractResolversResult,
273
- customScalarNames: ReadonlyArray<string> | null,
274
- collectedScalars?: ReadonlyArray<CollectedScalarType> | null,
275
- directiveDefinitions?: ReadonlyArray<DirectiveDefinitionInfo> | null,
276
- autoGeneratedTypes?: ReadonlyArray<AutoGeneratedType> | null,
277
- typenameAutoResolveTypes?: ReadonlyArray<TypenameAutoResolveTypeInfo>,
278
- ): IntegratedResult {
270
+ export interface IntegrateParams {
271
+ readonly typesResult: ExtractTypesResult;
272
+ readonly resolversResult: ExtractResolversResult;
273
+ readonly customScalarNames: ReadonlyArray<string> | null;
274
+ readonly collectedScalars: ReadonlyArray<CollectedScalarType> | null;
275
+ readonly directiveDefinitions: ReadonlyArray<DirectiveDefinitionInfo> | null;
276
+ readonly autoGeneratedTypes: ReadonlyArray<AutoGeneratedType> | null;
277
+ readonly typenameAutoResolveTypes: ReadonlyArray<TypenameAutoResolveTypeInfo> | null;
278
+ }
279
+
280
+ export function integrate(params: IntegrateParams): IntegratedResult {
281
+ const {
282
+ typesResult,
283
+ resolversResult,
284
+ customScalarNames,
285
+ collectedScalars,
286
+ directiveDefinitions,
287
+ autoGeneratedTypes,
288
+ typenameAutoResolveTypes,
289
+ } = params;
279
290
  const directiveTypeAliasNames = new Set(
280
291
  directiveDefinitions?.map((d) => d.typeAliasName) ?? [],
281
292
  );
@@ -299,7 +310,6 @@ export function integrate(
299
310
 
300
311
  const baseTypes: BaseType[] = [];
301
312
  const inputTypes: InputType[] = [];
302
- const autoGeneratedUnionNames: string[] = [];
303
313
  const autoGeneratedUnions: AutoGeneratedUnionInfo[] = [];
304
314
 
305
315
  for (const autoType of autoGeneratedTypes ?? []) {
@@ -307,14 +317,7 @@ export function integrate(
307
317
  baseTypes.push({
308
318
  name: autoType.name,
309
319
  kind: "Object",
310
- fields: autoType.fields!.map((f) => ({
311
- name: f.name,
312
- type: f.type,
313
- description: f.description,
314
- deprecated: f.deprecated,
315
- directives: f.directives,
316
- defaultValue: f.defaultValue,
317
- })),
320
+ fields: autoType.fields!.map(toBaseField),
318
321
  unionMembers: null,
319
322
  enumValues: null,
320
323
  isNumericEnum: false,
@@ -362,7 +365,6 @@ export function integrate(
362
365
  sourceFile: autoType.sourceLocation.file,
363
366
  directives: null,
364
367
  });
365
- autoGeneratedUnionNames.push(autoType.name);
366
368
  autoGeneratedUnions.push({
367
369
  name: autoType.name,
368
370
  fieldPattern: autoType.resolveTypeFieldPattern ?? {
@@ -373,14 +375,7 @@ export function integrate(
373
375
  } else if (autoType.kind === "OneOfInputObject") {
374
376
  inputTypes.push({
375
377
  name: autoType.name,
376
- fields: autoType.fields!.map((f) => ({
377
- name: f.name,
378
- type: f.type,
379
- description: f.description,
380
- deprecated: f.deprecated,
381
- directives: f.directives,
382
- defaultValue: f.defaultValue,
383
- })),
378
+ fields: autoType.fields!.map(toBaseField),
384
379
  sourceFile: autoType.sourceLocation.file,
385
380
  description: autoType.description,
386
381
  isOneOf: true,
@@ -388,14 +383,7 @@ export function integrate(
388
383
  } else {
389
384
  inputTypes.push({
390
385
  name: autoType.name,
391
- fields: autoType.fields!.map((f) => ({
392
- name: f.name,
393
- type: f.type,
394
- description: f.description,
395
- deprecated: f.deprecated,
396
- directives: f.directives,
397
- defaultValue: f.defaultValue,
398
- })),
386
+ fields: autoType.fields!.map(toBaseField),
399
387
  sourceFile: autoType.sourceLocation.file,
400
388
  description: autoType.description,
401
389
  isOneOf: false,
@@ -411,15 +399,7 @@ export function integrate(
411
399
  if (type.kind === "InputObject" || type.kind === "OneOfInputObject") {
412
400
  inputTypes.push({
413
401
  name: type.name,
414
- fields:
415
- type.fields?.map((field) => ({
416
- name: field.name,
417
- type: field.type,
418
- description: field.description,
419
- deprecated: field.deprecated,
420
- directives: field.directives,
421
- defaultValue: field.defaultValue,
422
- })) ?? [],
402
+ fields: type.fields?.map(toBaseField) ?? [],
423
403
  sourceFile: type.sourceFile,
424
404
  description: type.description,
425
405
  isOneOf: type.kind === "OneOfInputObject",
@@ -443,15 +423,7 @@ export function integrate(
443
423
  baseTypes.push({
444
424
  name: type.name,
445
425
  kind: type.kind,
446
- fields:
447
- type.fields?.map((field) => ({
448
- name: field.name,
449
- type: field.type,
450
- description: field.description,
451
- deprecated: field.deprecated,
452
- directives: field.directives,
453
- defaultValue: field.defaultValue,
454
- })) ?? null,
426
+ fields: type.fields?.map(toBaseField) ?? null,
455
427
  unionMembers: null,
456
428
  enumValues: null,
457
429
  isNumericEnum: false,
@@ -466,15 +438,7 @@ export function integrate(
466
438
  baseTypes.push({
467
439
  name: type.name,
468
440
  kind: type.kind,
469
- fields:
470
- type.fields?.map((field) => ({
471
- name: field.name,
472
- type: field.type,
473
- description: field.description,
474
- deprecated: field.deprecated,
475
- directives: field.directives,
476
- defaultValue: field.defaultValue,
477
- })) ?? null,
441
+ fields: type.fields?.map(toBaseField) ?? null,
478
442
  unionMembers: null,
479
443
  enumValues: null,
480
444
  isNumericEnum: false,
@@ -736,17 +700,12 @@ export function integrate(
736
700
  baseTypes,
737
701
  inputTypes,
738
702
  typeExtensions,
739
- customScalarNames:
740
- customScalarNames && customScalarNames.length > 0
741
- ? customScalarNames
742
- : null,
743
703
  customScalars,
744
704
  directiveDefinitions:
745
705
  directiveDefinitions && directiveDefinitions.length > 0
746
706
  ? directiveDefinitions
747
707
  : null,
748
708
  abstractTypeResolvers: resolversResult.abstractTypeResolvers,
749
- autoGeneratedUnionNames,
750
709
  autoGeneratedUnions,
751
710
  typenameAutoResolveTypes: typenameAutoResolveTypes ?? [],
752
711
  numericEnums,
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Branded type detector.
3
+ *
4
+ * This module provides functions to detect branded type patterns in TypeScript
5
+ * intersection types and extract the underlying primitive type.
6
+ *
7
+ * Branded types are intersection types combining a primitive with a marker object:
8
+ * - `string & { __brand: 'UserId' }`
9
+ * - `number & { readonly __brand: unique symbol }`
10
+ * - `boolean & { __nominal: true }`
11
+ */
12
+
13
+ import ts from "typescript";
14
+
15
+ /**
16
+ * Result of branded type detection.
17
+ */
18
+ export interface BrandedTypeResult {
19
+ /** Whether the type is a branded primitive type */
20
+ readonly isBranded: boolean;
21
+ /** The base primitive type if branded, null otherwise */
22
+ readonly baseType: "string" | "number" | "boolean" | null;
23
+ }
24
+
25
+ /**
26
+ * Property names commonly used as brand markers.
27
+ * These indicate the type is a branded type, not an actual object.
28
+ */
29
+ const BRAND_PROPERTY_NAMES: ReadonlySet<string> = new Set([
30
+ "__brand",
31
+ "_brand",
32
+ "brand",
33
+ "__nominal",
34
+ "_nominal",
35
+ "__tag",
36
+ "_tag",
37
+ "__type",
38
+ ]);
39
+
40
+ /**
41
+ * Property names to exclude from brand detection.
42
+ * These are used by gqlkit for scalar metadata, not branding.
43
+ */
44
+ const EXCLUDED_BRAND_PROPERTIES: ReadonlySet<string> = new Set([
45
+ " $gqlkitScalar",
46
+ ]);
47
+
48
+ /**
49
+ * Detects if a type is a branded primitive type.
50
+ *
51
+ * A branded type is an intersection type where:
52
+ * 1. One member is a primitive type (string, number, or boolean)
53
+ * 2. Other members are pure brand markers (objects with only brand properties)
54
+ *
55
+ * @param type - The TypeScript type to analyze
56
+ * @returns Detection result with isBranded flag and baseType
57
+ */
58
+ export function detectBrandedType(type: ts.Type): BrandedTypeResult {
59
+ const notBranded: BrandedTypeResult = { isBranded: false, baseType: null };
60
+
61
+ if (!type.isIntersection()) {
62
+ return notBranded;
63
+ }
64
+
65
+ let primitiveBase: "string" | "number" | "boolean" | null = null;
66
+ let hasNonBrandMember = false;
67
+
68
+ for (const member of type.types) {
69
+ const primitiveType = getPrimitiveType(member);
70
+ if (primitiveType !== null) {
71
+ if (primitiveBase !== null && primitiveBase !== primitiveType) {
72
+ // Multiple different primitive types - not a valid branded type
73
+ return notBranded;
74
+ }
75
+ primitiveBase = primitiveType;
76
+ continue;
77
+ }
78
+
79
+ // Check if this member is a pure brand marker
80
+ if (!isPureBrandMarker(member)) {
81
+ hasNonBrandMember = true;
82
+ break;
83
+ }
84
+ }
85
+
86
+ if (hasNonBrandMember || primitiveBase === null) {
87
+ return notBranded;
88
+ }
89
+
90
+ return { isBranded: true, baseType: primitiveBase };
91
+ }
92
+
93
+ /**
94
+ * Detects if all types in the array are branded with the same base type.
95
+ *
96
+ * This is useful for union types where branded boolean expands to:
97
+ * `(true & { __nominal: true }) | (false & { __nominal: true })`
98
+ *
99
+ * @param types - Array of TypeScript types to analyze
100
+ * @returns Common branded result if all types are branded with the same base, otherwise non-branded
101
+ */
102
+ export function detectUniformBrandedType(
103
+ types: ReadonlyArray<ts.Type>,
104
+ ): BrandedTypeResult {
105
+ if (types.length === 0) {
106
+ return { isBranded: false, baseType: null };
107
+ }
108
+
109
+ const first = detectBrandedType(types[0]!);
110
+ if (!first.isBranded) {
111
+ return { isBranded: false, baseType: null };
112
+ }
113
+
114
+ for (let i = 1; i < types.length; i++) {
115
+ const result = detectBrandedType(types[i]!);
116
+ if (!result.isBranded || result.baseType !== first.baseType) {
117
+ return { isBranded: false, baseType: null };
118
+ }
119
+ }
120
+
121
+ return first;
122
+ }
123
+
124
+ /**
125
+ * Gets the primitive type from a TypeScript type.
126
+ *
127
+ * @returns "string", "number", "boolean", or null if not a primitive
128
+ */
129
+ function getPrimitiveType(
130
+ type: ts.Type,
131
+ ): "string" | "number" | "boolean" | null {
132
+ if (type.flags & ts.TypeFlags.String) {
133
+ return "string";
134
+ }
135
+ if (type.flags & ts.TypeFlags.Number) {
136
+ return "number";
137
+ }
138
+ if (
139
+ type.flags & ts.TypeFlags.Boolean ||
140
+ type.flags & ts.TypeFlags.BooleanLiteral
141
+ ) {
142
+ return "boolean";
143
+ }
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Checks if a type is a pure brand marker.
149
+ *
150
+ * A pure brand marker is an object type where all properties are
151
+ * brand-related (e.g., __brand, __nominal, __tag).
152
+ *
153
+ * @param type - The type to check
154
+ * @returns true if the type is a pure brand marker
155
+ */
156
+ function isPureBrandMarker(type: ts.Type): boolean {
157
+ if (!(type.flags & ts.TypeFlags.Object)) {
158
+ return false;
159
+ }
160
+
161
+ const properties = type.getProperties();
162
+ if (properties.length === 0) {
163
+ // Empty object is not a brand marker
164
+ return false;
165
+ }
166
+
167
+ for (const prop of properties) {
168
+ const propName = prop.getName();
169
+
170
+ // Excluded properties (like gqlkit metadata) disqualify as brand marker
171
+ if (EXCLUDED_BRAND_PROPERTIES.has(propName)) {
172
+ return false;
173
+ }
174
+
175
+ // If any property is not a brand property, this is not a pure brand marker
176
+ if (!BRAND_PROPERTY_NAMES.has(propName)) {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ return true;
182
+ }
@@ -0,0 +1,7 @@
1
+ export function toScreamingSnakeCase(value: string): string {
2
+ return value
3
+ .replace(/[-\s]+/g, "_")
4
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
5
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
6
+ .toUpperCase();
7
+ }
@@ -24,45 +24,29 @@ function isReservedName(name: string): boolean {
24
24
  return name.startsWith("__");
25
25
  }
26
26
 
27
- /**
28
- * Check if a field name is eligible to be included as a GraphQL object field.
29
- */
30
- export function isEligibleAsObjectField(fieldName: string): EligibilityResult {
31
- if (isReservedName(fieldName)) {
32
- return {
33
- eligible: false,
34
- skipReason: {
35
- code: "RESERVED_NAME",
36
- message: `Field '${fieldName}' starts with '__' which is reserved for GraphQL introspection`,
37
- },
38
- };
39
- }
27
+ export type FieldEligibilityKind = "object" | "input";
40
28
 
41
- if (!isValidGraphQLName(fieldName)) {
42
- return {
43
- eligible: false,
44
- skipReason: {
45
- code: "INVALID_NAME",
46
- message: `Field '${fieldName}' is not a valid GraphQL identifier (must match /^[_A-Za-z][_0-9A-Za-z]*$/)`,
47
- },
48
- };
49
- }
50
-
51
- return { eligible: true, skipReason: null };
29
+ export interface IsEligibleFieldParams {
30
+ readonly fieldName: string;
31
+ readonly kind: FieldEligibilityKind;
52
32
  }
53
33
 
54
34
  /**
55
- * Check if a field name is eligible to be included as a GraphQL input object field.
35
+ * Check if a field name is eligible to be included as a GraphQL field.
36
+ * Uses the kind parameter to determine the error message prefix.
56
37
  */
57
- export function isEligibleAsInputObjectField(
58
- fieldName: string,
38
+ export function isEligibleField(
39
+ params: IsEligibleFieldParams,
59
40
  ): EligibilityResult {
41
+ const { fieldName, kind } = params;
42
+ const prefix = kind === "input" ? "Input field" : "Field";
43
+
60
44
  if (isReservedName(fieldName)) {
61
45
  return {
62
46
  eligible: false,
63
47
  skipReason: {
64
48
  code: "RESERVED_NAME",
65
- message: `Input field '${fieldName}' starts with '__' which is reserved for GraphQL introspection`,
49
+ message: `${prefix} '${fieldName}' starts with '__' which is reserved for GraphQL introspection`,
66
50
  },
67
51
  };
68
52
  }
@@ -72,7 +56,7 @@ export function isEligibleAsInputObjectField(
72
56
  eligible: false,
73
57
  skipReason: {
74
58
  code: "INVALID_NAME",
75
- message: `Input field '${fieldName}' is not a valid GraphQL identifier (must match /^[_A-Za-z][_0-9A-Za-z]*$/)`,
59
+ message: `${prefix} '${fieldName}' is not a valid GraphQL identifier (must match /^[_A-Za-z][_0-9A-Za-z]*$/)`,
76
60
  },
77
61
  };
78
62
  }
@@ -7,6 +7,7 @@ import {
7
7
  detectEnumPrefix,
8
8
  stripEnumPrefix,
9
9
  } from "../../shared/enum-prefix-detector.js";
10
+ import { toScreamingSnakeCase } from "../../shared/string-utils.js";
10
11
  import { convertTsTypeToGraphQLType } from "../../shared/type-converter.js";
11
12
  import type {
12
13
  Diagnostic,
@@ -19,11 +20,7 @@ import type {
19
20
  InlineObjectProperty,
20
21
  SourceLocation,
21
22
  } from "../types/index.js";
22
- import {
23
- isEligibleAsEnumValue,
24
- isEligibleAsInputObjectField,
25
- isEligibleAsObjectField,
26
- } from "./field-eligibility.js";
23
+ import { isEligibleAsEnumValue, isEligibleField } from "./field-eligibility.js";
27
24
 
28
25
  export interface ConversionResult {
29
26
  readonly types: ReadonlyArray<GraphQLTypeInfo>;
@@ -41,14 +38,6 @@ function isInputTypeName(name: string): boolean {
41
38
  return name.endsWith("Input");
42
39
  }
43
40
 
44
- function toScreamingSnakeCase(value: string): string {
45
- return value
46
- .replace(/[-\s]+/g, "_")
47
- .replace(/([a-z])([A-Z])/g, "$1_$2")
48
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
49
- .toUpperCase();
50
- }
51
-
52
41
  interface ConvertEnumMembersParams {
53
42
  readonly members: ReadonlyArray<EnumMemberInfo>;
54
43
  readonly enumName: string;
@@ -163,9 +152,10 @@ function convertFields(
163
152
  const diagnostics: Diagnostic[] = [];
164
153
 
165
154
  for (const field of extracted.fields) {
166
- const eligibility = isInput
167
- ? isEligibleAsInputObjectField(field.name)
168
- : isEligibleAsObjectField(field.name);
155
+ const eligibility = isEligibleField({
156
+ fieldName: field.name,
157
+ kind: isInput ? "input" : "object",
158
+ });
169
159
 
170
160
  if (!eligibility.eligible) {
171
161
  diagnostics.push({
@@ -1,4 +1,8 @@
1
1
  import ts from "typescript";
2
+ import {
3
+ detectBrandedType,
4
+ detectUniformBrandedType,
5
+ } from "../../shared/branded-type-detector.js";
2
6
  import { isInternalTypeSymbol } from "../../shared/constants.js";
3
7
  import { extractInlineObjectProperties as extractInlineObjectPropertiesShared } from "../../shared/inline-object-extractor.js";
4
8
  import { isInlineObjectType } from "../../shared/inline-object-utils.js";
@@ -175,6 +179,20 @@ function resolveFieldTypeInternal(
175
179
  });
176
180
  }
177
181
 
182
+ // Check if all non-null types are branded primitives with the same base type
183
+ // This handles cases like: boolean & { __nominal: true }
184
+ // which expands to: (true & { __nominal: true }) | (false & { __nominal: true })
185
+ const uniformBrandedResult = detectUniformBrandedType(nonNullTypes);
186
+ if (
187
+ uniformBrandedResult.isBranded &&
188
+ uniformBrandedResult.baseType !== null
189
+ ) {
190
+ return createPrimitiveType({
191
+ name: uniformBrandedResult.baseType,
192
+ nullable,
193
+ });
194
+ }
195
+
178
196
  if (nonNullTypes.length === 1) {
179
197
  const nonNullTypeNode =
180
198
  typeNode && ts.isUnionTypeNode(typeNode)
@@ -235,18 +253,45 @@ function resolveFieldTypeInternal(
235
253
  return createLiteralType(typeString);
236
254
  }
237
255
 
238
- // Intersection types in field context are ALWAYS treated as inline objects
239
- // GraphQL doesn't have intersection types, so we must expand them
256
+ // Intersection types in field context
257
+ // GraphQL doesn't have intersection types, so we must resolve them appropriately
240
258
  if (type.isIntersection()) {
241
- // If the intersection has an alias that's in knownTypeNames, use it
259
+ // 1. If the intersection has an alias that's in knownTypeNames, use it as reference
242
260
  if (type.aliasSymbol) {
243
261
  const aliasName = type.aliasSymbol.getName();
244
262
  if (isKnownSchemaType(aliasName, type.aliasSymbol, ctx)) {
245
263
  return createReferenceType({ name: aliasName, nullable: false });
246
264
  }
265
+
266
+ // 2. Check if aliasSymbol has a globalTypeMapping (custom scalar)
267
+ const globalMapping = globalTypeMappings.find(
268
+ (m) => m.typeName === aliasName,
269
+ );
270
+ if (globalMapping) {
271
+ return createScalarType({
272
+ name: globalMapping.scalarName,
273
+ scalarInfo: {
274
+ scalarName: globalMapping.scalarName,
275
+ typeName: globalMapping.typeName,
276
+ baseType: undefined,
277
+ isCustom: true,
278
+ only: globalMapping.only,
279
+ },
280
+ nullable: false,
281
+ });
282
+ }
283
+ }
284
+
285
+ // 3. Check if this is a branded primitive type pattern
286
+ const brandedResult = detectBrandedType(type);
287
+ if (brandedResult.isBranded && brandedResult.baseType !== null) {
288
+ return createPrimitiveType({
289
+ name: brandedResult.baseType,
290
+ nullable: false,
291
+ });
247
292
  }
248
293
 
249
- // Otherwise, treat as inline object
294
+ // 4. Otherwise, treat as inline object
250
295
  return tryExtractAsInlineObject(type, ctx);
251
296
  }
252
297
 
@@ -41,6 +41,7 @@ export type DiagnosticCode =
41
41
  | "CONFIG_INVALID_HOOK_TYPE"
42
42
  | "CONFIG_INVALID_HOOK_COMMAND"
43
43
  | "CONFIG_INVALID_ONLY_VALUE"
44
+ | "CONFIG_INVALID_IMPORT_EXTENSION"
44
45
  | "CUSTOM_SCALAR_TYPE_NOT_FOUND"
45
46
  | "TSCONFIG_NOT_FOUND"
46
47
  | "TSCONFIG_PARSE_ERROR"