@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.
- package/README.md +143 -0
- package/dist/auto-type-generator/auto-type-generator.d.ts.map +1 -1
- package/dist/auto-type-generator/auto-type-generator.js +16 -13
- package/dist/auto-type-generator/auto-type-generator.js.map +1 -1
- package/dist/config/types.d.ts +13 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config-loader/loader.d.ts +3 -0
- package/dist/config-loader/loader.d.ts.map +1 -1
- package/dist/config-loader/loader.js +1 -0
- package/dist/config-loader/loader.js.map +1 -1
- package/dist/config-loader/validator.d.ts.map +1 -1
- package/dist/config-loader/validator.js +23 -0
- package/dist/config-loader/validator.js.map +1 -1
- package/dist/gen-orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/gen-orchestrator/orchestrator.js +19 -6
- package/dist/gen-orchestrator/orchestrator.js.map +1 -1
- package/dist/resolver-extractor/extractor/define-api-extractor.js +4 -4
- package/dist/resolver-extractor/extractor/define-api-extractor.js.map +1 -1
- package/dist/schema-generator/builder/ast-builder.d.ts +2 -2
- package/dist/schema-generator/builder/ast-builder.d.ts.map +1 -1
- package/dist/schema-generator/builder/ast-builder.js +12 -34
- package/dist/schema-generator/builder/ast-builder.js.map +1 -1
- package/dist/schema-generator/emitter/code-emitter.d.ts +3 -1
- package/dist/schema-generator/emitter/code-emitter.d.ts.map +1 -1
- package/dist/schema-generator/emitter/code-emitter.js +22 -12
- package/dist/schema-generator/emitter/code-emitter.js.map +1 -1
- package/dist/schema-generator/emitter/sdl-emitter.d.ts +0 -4
- package/dist/schema-generator/emitter/sdl-emitter.d.ts.map +1 -1
- package/dist/schema-generator/emitter/sdl-emitter.js +0 -4
- package/dist/schema-generator/emitter/sdl-emitter.js.map +1 -1
- package/dist/schema-generator/generate-schema.d.ts +2 -0
- package/dist/schema-generator/generate-schema.d.ts.map +1 -1
- package/dist/schema-generator/generate-schema.js +13 -4
- package/dist/schema-generator/generate-schema.js.map +1 -1
- package/dist/schema-generator/integrator/result-integrator.d.ts +10 -12
- package/dist/schema-generator/integrator/result-integrator.d.ts.map +1 -1
- package/dist/schema-generator/integrator/result-integrator.js +18 -55
- package/dist/schema-generator/integrator/result-integrator.js.map +1 -1
- package/dist/shared/branded-type-detector.d.ts +43 -0
- package/dist/shared/branded-type-detector.d.ts.map +1 -0
- package/dist/shared/branded-type-detector.js +146 -0
- package/dist/shared/branded-type-detector.js.map +1 -0
- package/dist/shared/string-utils.d.ts +2 -0
- package/dist/shared/string-utils.d.ts.map +1 -0
- package/dist/shared/string-utils.js +8 -0
- package/dist/shared/string-utils.js.map +1 -0
- package/dist/type-extractor/converter/field-eligibility.d.ts +8 -6
- package/dist/type-extractor/converter/field-eligibility.d.ts.map +1 -1
- package/dist/type-extractor/converter/field-eligibility.js +7 -28
- package/dist/type-extractor/converter/field-eligibility.js.map +1 -1
- package/dist/type-extractor/converter/graphql-converter.d.ts.map +1 -1
- package/dist/type-extractor/converter/graphql-converter.js +6 -11
- package/dist/type-extractor/converter/graphql-converter.js.map +1 -1
- package/dist/type-extractor/extractor/field-type-resolver.d.ts.map +1 -1
- package/dist/type-extractor/extractor/field-type-resolver.js +39 -4
- package/dist/type-extractor/extractor/field-type-resolver.js.map +1 -1
- package/dist/type-extractor/types/diagnostics.d.ts +1 -1
- package/dist/type-extractor/types/diagnostics.d.ts.map +1 -1
- package/dist/type-extractor/validator/type-validator.d.ts +1 -1
- package/dist/type-extractor/validator/type-validator.d.ts.map +1 -1
- package/dist/type-extractor/validator/type-validator.js +2 -10
- package/dist/type-extractor/validator/type-validator.js.map +1 -1
- package/docs/configuration.md +9 -0
- package/package.json +1 -1
- package/src/auto-type-generator/auto-type-generator.ts +23 -26
- package/src/config/types.ts +15 -0
- package/src/config-loader/loader.ts +4 -0
- package/src/config-loader/validator.ts +33 -0
- package/src/gen-orchestrator/orchestrator.ts +30 -11
- package/src/resolver-extractor/extractor/define-api-extractor.ts +5 -5
- package/src/schema-generator/builder/ast-builder.ts +52 -81
- package/src/schema-generator/emitter/code-emitter.ts +48 -11
- package/src/schema-generator/emitter/sdl-emitter.ts +0 -4
- package/src/schema-generator/generate-schema.ts +13 -15
- package/src/schema-generator/integrator/result-integrator.ts +37 -78
- package/src/shared/branded-type-detector.ts +182 -0
- package/src/shared/string-utils.ts +7 -0
- package/src/type-extractor/converter/field-eligibility.ts +13 -29
- package/src/type-extractor/converter/graphql-converter.ts +6 -16
- package/src/type-extractor/extractor/field-type-resolver.ts +49 -4
- package/src/type-extractor/types/diagnostics.ts +1 -0
- 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
|
|
271
|
-
typesResult: ExtractTypesResult
|
|
272
|
-
resolversResult: ExtractResolversResult
|
|
273
|
-
customScalarNames: ReadonlyArray<string> | null
|
|
274
|
-
collectedScalars
|
|
275
|
-
directiveDefinitions
|
|
276
|
-
autoGeneratedTypes
|
|
277
|
-
typenameAutoResolveTypes
|
|
278
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
58
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
167
|
-
|
|
168
|
-
:
|
|
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
|
|
239
|
-
// GraphQL doesn't have intersection types, so we must
|
|
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
|
|