@gqlkit-ts/cli 0.5.0 → 0.6.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 (58) hide show
  1. package/README.md +3 -1
  2. package/dist/auto-type-generator/auto-type-generator.d.ts.map +1 -1
  3. package/dist/auto-type-generator/auto-type-generator.js +4 -1
  4. package/dist/auto-type-generator/auto-type-generator.js.map +1 -1
  5. package/dist/auto-type-generator/naming-convention.d.ts +2 -2
  6. package/dist/auto-type-generator/naming-convention.d.ts.map +1 -1
  7. package/dist/auto-type-generator/resolver-field-iterator.d.ts +1 -1
  8. package/dist/auto-type-generator/resolver-field-iterator.d.ts.map +1 -1
  9. package/dist/auto-type-generator/resolver-field-iterator.js +3 -0
  10. package/dist/auto-type-generator/resolver-field-iterator.js.map +1 -1
  11. package/dist/commands/docs.d.ts +1 -0
  12. package/dist/commands/docs.d.ts.map +1 -1
  13. package/dist/commands/gen.d.ts +1 -0
  14. package/dist/commands/gen.d.ts.map +1 -1
  15. package/dist/commands/main.d.ts +1 -0
  16. package/dist/commands/main.d.ts.map +1 -1
  17. package/dist/gen-orchestrator/orchestrator.d.ts.map +1 -1
  18. package/dist/gen-orchestrator/orchestrator.js +32 -1
  19. package/dist/gen-orchestrator/orchestrator.js.map +1 -1
  20. package/dist/resolver-extractor/extract-resolvers.d.ts +4 -0
  21. package/dist/resolver-extractor/extract-resolvers.d.ts.map +1 -1
  22. package/dist/resolver-extractor/extractor/define-api-extractor.d.ts +2 -1
  23. package/dist/resolver-extractor/extractor/define-api-extractor.d.ts.map +1 -1
  24. package/dist/resolver-extractor/extractor/define-api-extractor.js +31 -6
  25. package/dist/resolver-extractor/extractor/define-api-extractor.js.map +1 -1
  26. package/dist/resolver-extractor/index.d.ts +1 -1
  27. package/dist/resolver-extractor/index.d.ts.map +1 -1
  28. package/dist/schema-generator/emitter/code-emitter.d.ts.map +1 -1
  29. package/dist/schema-generator/emitter/code-emitter.js +11 -3
  30. package/dist/schema-generator/emitter/code-emitter.js.map +1 -1
  31. package/dist/schema-generator/integrator/result-integrator.d.ts +1 -0
  32. package/dist/schema-generator/integrator/result-integrator.d.ts.map +1 -1
  33. package/dist/schema-generator/integrator/result-integrator.js +26 -1
  34. package/dist/schema-generator/integrator/result-integrator.js.map +1 -1
  35. package/dist/type-extractor/extractor/type-name-collector.d.ts +6 -0
  36. package/dist/type-extractor/extractor/type-name-collector.d.ts.map +1 -1
  37. package/dist/type-extractor/extractor/type-name-collector.js +54 -18
  38. package/dist/type-extractor/extractor/type-name-collector.js.map +1 -1
  39. package/dist/type-extractor/types/diagnostics.d.ts +1 -1
  40. package/dist/type-extractor/types/diagnostics.d.ts.map +1 -1
  41. package/docs/getting-started.md +2 -1
  42. package/docs/index.md +1 -0
  43. package/docs/schema/conventions.md +7 -0
  44. package/docs/schema/fields.md +15 -0
  45. package/docs/schema/queries-mutations.md +21 -2
  46. package/docs/schema/subscriptions.md +173 -0
  47. package/package.json +3 -3
  48. package/src/auto-type-generator/auto-type-generator.ts +12 -4
  49. package/src/auto-type-generator/naming-convention.ts +2 -2
  50. package/src/auto-type-generator/resolver-field-iterator.ts +5 -1
  51. package/src/gen-orchestrator/orchestrator.ts +38 -1
  52. package/src/resolver-extractor/extract-resolvers.ts +5 -0
  53. package/src/resolver-extractor/extractor/define-api-extractor.ts +43 -7
  54. package/src/resolver-extractor/index.ts +1 -0
  55. package/src/schema-generator/emitter/code-emitter.ts +17 -4
  56. package/src/schema-generator/integrator/result-integrator.ts +30 -1
  57. package/src/type-extractor/extractor/type-name-collector.ts +80 -18
  58. package/src/type-extractor/types/diagnostics.ts +2 -1
@@ -35,7 +35,11 @@ import type {
35
35
  TSTypeReference,
36
36
  } from "../../type-extractor/types/index.js";
37
37
 
38
- export type DefineApiResolverType = "query" | "mutation" | "field";
38
+ export type DefineApiResolverType =
39
+ | "query"
40
+ | "mutation"
41
+ | "field"
42
+ | "subscription";
39
43
 
40
44
  export type AbstractResolverKind = "resolveType" | "isTypeOf";
41
45
 
@@ -56,6 +60,7 @@ export interface ArgumentDefinition {
56
60
 
57
61
  export interface DefineApiResolverInfo {
58
62
  readonly fieldName: string;
63
+ readonly resolverExportName: string;
59
64
  readonly resolverType: DefineApiResolverType;
60
65
  readonly parentTypeName: string | null;
61
66
  readonly argsType: TSTypeReference | null;
@@ -222,7 +227,12 @@ function detectResolverFromMetadataType(
222
227
  const kindType = checker.getTypeOfSymbol(kindProp);
223
228
  if (kindType.isStringLiteral()) {
224
229
  const kind = kindType.value;
225
- if (kind === "query" || kind === "mutation" || kind === "field") {
230
+ if (
231
+ kind === "query" ||
232
+ kind === "mutation" ||
233
+ kind === "field" ||
234
+ kind === "subscription"
235
+ ) {
226
236
  return kind;
227
237
  }
228
238
  }
@@ -230,6 +240,20 @@ function detectResolverFromMetadataType(
230
240
  return null;
231
241
  }
232
242
 
243
+ function resolveFieldNameFromExportName(exportName: string): string | null {
244
+ const delimiterIndex = exportName.lastIndexOf("$");
245
+ if (delimiterIndex === -1) {
246
+ return exportName;
247
+ }
248
+
249
+ const fieldName = exportName.slice(delimiterIndex + 1);
250
+ if (fieldName.length === 0) {
251
+ return null;
252
+ }
253
+
254
+ return fieldName;
255
+ }
256
+
233
257
  function isInlineTypeLiteralDeclaration(declaration: ts.Declaration): boolean {
234
258
  if (!ts.isPropertySignature(declaration)) {
235
259
  return false;
@@ -633,7 +657,7 @@ export function extractDefineApiResolvers(
633
657
  continue;
634
658
  }
635
659
 
636
- const fieldName = declaration.name.getText(sourceFile);
660
+ const exportName = declaration.name.getText(sourceFile);
637
661
  const initializer = declaration.initializer;
638
662
 
639
663
  if (!initializer) {
@@ -647,11 +671,11 @@ export function extractDefineApiResolvers(
647
671
  ) {
648
672
  const hasDefineCall = initializer
649
673
  .getText(sourceFile)
650
- .match(/define(Query|Mutation|Field)/);
674
+ .match(/define(Query|Mutation|Field|Subscription)/);
651
675
  if (hasDefineCall) {
652
676
  diagnostics.push({
653
677
  code: "INVALID_DEFINE_CALL",
654
- message: `Complex expressions with define* functions are not supported. Use a simple 'export const ${fieldName} = defineXxx(...)' pattern.`,
678
+ message: `Complex expressions with define* functions are not supported. Use a simple 'export const ${exportName} = defineXxx(...)' pattern.`,
655
679
  severity: "error",
656
680
  location: getSourceLocationFromNode(declaration.name),
657
681
  });
@@ -671,7 +695,7 @@ export function extractDefineApiResolvers(
671
695
  abstractTypeResolvers.push({
672
696
  kind: abstractResolverInfo.kind,
673
697
  targetTypeName: abstractResolverInfo.targetTypeName,
674
- exportName: fieldName,
698
+ exportName,
675
699
  sourceFile: filePath,
676
700
  sourceLocation,
677
701
  });
@@ -688,6 +712,17 @@ export function extractDefineApiResolvers(
688
712
  continue;
689
713
  }
690
714
 
715
+ const fieldName = resolveFieldNameFromExportName(exportName);
716
+ if (fieldName === null) {
717
+ diagnostics.push({
718
+ code: "INVALID_DEFINE_CALL",
719
+ message: `Resolver export '${exportName}' must have a non-empty field name after '$'.`,
720
+ severity: "error",
721
+ location: getSourceLocationFromNode(declaration.name),
722
+ });
723
+ continue;
724
+ }
725
+
691
726
  const funcName = ts.isIdentifier(initializer.expression)
692
727
  ? initializer.expression.text
693
728
  : undefined;
@@ -702,7 +737,7 @@ export function extractDefineApiResolvers(
702
737
  if (!typeInfo) {
703
738
  diagnostics.push({
704
739
  code: "INVALID_DEFINE_CALL",
705
- message: `Failed to extract type arguments from ${funcName ?? "define*"} call for '${fieldName}'`,
740
+ message: `Failed to extract type arguments from ${funcName ?? "define*"} call for '${exportName}'`,
706
741
  severity: "error",
707
742
  location: getSourceLocationFromNode(declaration.name),
708
743
  });
@@ -720,6 +755,7 @@ export function extractDefineApiResolvers(
720
755
 
721
756
  resolvers.push({
722
757
  fieldName,
758
+ resolverExportName: exportName,
723
759
  resolverType,
724
760
  parentTypeName: typeInfo.parentTypeName,
725
761
  argsType: typeInfo.argsType,
@@ -5,6 +5,7 @@ export type {
5
5
  GraphQLInputValue,
6
6
  MutationFieldDefinitions,
7
7
  QueryFieldDefinitions,
8
+ SubscriptionFieldDefinitions,
8
9
  TypeExtension,
9
10
  } from "./extract-resolvers.js";
10
11
  export type {
@@ -15,6 +15,7 @@ import type {
15
15
  } from "../integrator/result-integrator.js";
16
16
  import type {
17
17
  AbstractTypeResolverInfo,
18
+ FieldResolver,
18
19
  ResolverInfo,
19
20
  TypeResolvers,
20
21
  } from "../resolver-collector/resolver-collector.js";
@@ -315,21 +316,33 @@ function buildStringEnumResolvers(
315
316
  return stringEnumMappings.map(buildStringEnumResolver);
316
317
  }
317
318
 
319
+ function buildFieldResolverValue(
320
+ localName: string,
321
+ field: FieldResolver,
322
+ ): string {
323
+ if (field.isDirectExport) {
324
+ return localName;
325
+ }
326
+ return `${localName}.${field.fieldName}`;
327
+ }
328
+
318
329
  function buildTypeResolverEntry(
319
330
  type: TypeResolvers,
320
331
  abstractResolverForType: AbstractTypeResolverInfo | null,
321
332
  ): string {
322
333
  const entries: string[] = [];
334
+ const isSubscription = type.typeName === "Subscription";
323
335
 
324
336
  for (const field of type.fields) {
325
337
  const localName = makeResolverLocalName(type.typeName, field.fieldName);
338
+ const resolverValue = buildFieldResolverValue(localName, field);
326
339
 
327
- if (field.isDirectExport) {
328
- entries.push(` ${field.fieldName}: ${localName},`);
329
- } else {
340
+ if (isSubscription) {
330
341
  entries.push(
331
- ` ${field.fieldName}: ${localName}.${field.fieldName},`,
342
+ ` ${field.fieldName}: { subscribe: ${resolverValue}, resolve: (event: unknown) => event },`,
332
343
  );
344
+ } else {
345
+ entries.push(` ${field.fieldName}: ${resolverValue},`);
333
346
  }
334
347
  }
335
348
 
@@ -145,6 +145,7 @@ export interface IntegratedResult {
145
145
  readonly stringEnumMappings: ReadonlyArray<StringEnumMappingInfo>;
146
146
  readonly hasQuery: boolean;
147
147
  readonly hasMutation: boolean;
148
+ readonly hasSubscription: boolean;
148
149
  readonly hasErrors: boolean;
149
150
  readonly diagnostics: ReadonlyArray<Diagnostic>;
150
151
  }
@@ -469,8 +470,10 @@ export function integrate(params: IntegrateParams): IntegratedResult {
469
470
 
470
471
  const hasQuery = resolversResult.queryFields.fields.length > 0;
471
472
  const hasMutation = resolversResult.mutationFields.fields.length > 0;
473
+ const hasSubscription = resolversResult.subscriptionFields.fields.length > 0;
472
474
 
473
- if (hasQuery) {
475
+ // GraphQL spec requires Query root type even when only Subscription/Mutation are defined
476
+ if (hasQuery || hasMutation || hasSubscription) {
474
477
  baseTypes.push({
475
478
  name: "Query",
476
479
  kind: "Object",
@@ -502,6 +505,22 @@ export function integrate(params: IntegrateParams): IntegratedResult {
502
505
  directives: null,
503
506
  });
504
507
  }
508
+ if (hasSubscription) {
509
+ baseTypes.push({
510
+ name: "Subscription",
511
+ kind: "Object",
512
+ fields: [],
513
+ unionMembers: null,
514
+ enumValues: null,
515
+ isNumericEnum: false,
516
+ needsStringEnumMapping: false,
517
+ implementedInterfaces: null,
518
+ description: null,
519
+ deprecated: null,
520
+ sourceFile: null,
521
+ directives: null,
522
+ });
523
+ }
505
524
 
506
525
  const typenameAutoResolveTypeNames = new Set([
507
526
  ...(typenameAutoResolveTypes?.map((t) => t.abstractTypeName) ?? []),
@@ -540,6 +559,15 @@ export function integrate(params: IntegrateParams): IntegratedResult {
540
559
  });
541
560
  }
542
561
 
562
+ if (hasSubscription) {
563
+ typeExtensions.push({
564
+ targetTypeName: "Subscription",
565
+ fields: resolversResult.subscriptionFields.fields.map(
566
+ convertToExtensionField,
567
+ ),
568
+ });
569
+ }
570
+
543
571
  for (const ext of resolversResult.typeExtensions) {
544
572
  if (!knownTypeNames.has(ext.targetTypeName)) {
545
573
  const firstField = ext.fields[0];
@@ -712,6 +740,7 @@ export function integrate(params: IntegrateParams): IntegratedResult {
712
740
  stringEnumMappings,
713
741
  hasQuery,
714
742
  hasMutation,
743
+ hasSubscription,
715
744
  hasErrors,
716
745
  diagnostics,
717
746
  };
@@ -1,8 +1,28 @@
1
1
  import ts from "typescript";
2
+ import {
3
+ getSourceLocationFromNode,
4
+ type SourceLocation,
5
+ } from "../../shared/source-location.js";
2
6
  import {
3
7
  isExported,
4
8
  resolveOriginalSymbol,
5
9
  } from "../../shared/typescript-utils.js";
10
+ import type { Diagnostic } from "../types/index.js";
11
+
12
+ /**
13
+ * Tracks location and symbol of a type declaration for duplicate detection.
14
+ */
15
+ interface TypeDeclarationLocation {
16
+ readonly location: SourceLocation;
17
+ readonly symbol: ts.Symbol | null;
18
+ }
19
+
20
+ /**
21
+ * Formats a source location as a human-readable string.
22
+ */
23
+ function formatLocation(location: SourceLocation): string {
24
+ return `${location.file}:${location.line}`;
25
+ }
6
26
 
7
27
  export interface TypeNameCollectionResult {
8
28
  readonly typeNames: ReadonlySet<string>;
@@ -13,6 +33,11 @@ export interface TypeNameCollectionResult {
13
33
  * This allows recognizing `ExternalUser` as `User` in field types.
14
34
  */
15
35
  readonly underlyingSymbolToTypeName: ReadonlyMap<ts.Symbol, string>;
36
+ /**
37
+ * Diagnostics collected during type name collection.
38
+ * Contains errors for duplicate type exports.
39
+ */
40
+ readonly diagnostics: ReadonlyArray<Diagnostic>;
16
41
  }
17
42
 
18
43
  /**
@@ -33,8 +58,51 @@ export function collectDeclaredTypeNames(
33
58
  const typeNames = new Set<string>();
34
59
  const typeSymbols = new Map<string, ts.Symbol>();
35
60
  const underlyingSymbolToTypeName = new Map<ts.Symbol, string>();
61
+ const diagnostics: Diagnostic[] = [];
36
62
  const checker = program.getTypeChecker();
37
63
 
64
+ const typeLocations = new Map<string, TypeDeclarationLocation>();
65
+
66
+ /**
67
+ * Registers a type name and checks for duplicates.
68
+ * Returns true if this is a new type, false if it's a duplicate.
69
+ */
70
+ function registerTypeName(
71
+ name: string,
72
+ location: SourceLocation,
73
+ symbol: ts.Symbol | undefined,
74
+ ): boolean {
75
+ const resolvedSymbol = symbol
76
+ ? resolveOriginalSymbol(symbol, checker)
77
+ : null;
78
+ const existing = typeLocations.get(name);
79
+ if (existing) {
80
+ // Check if both symbols resolve to the same underlying type
81
+ // (e.g., re-exports of the same type from different files)
82
+ if (
83
+ resolvedSymbol &&
84
+ existing.symbol &&
85
+ resolvedSymbol === existing.symbol
86
+ ) {
87
+ // Same underlying type - not a true duplicate, skip silently
88
+ return false;
89
+ }
90
+ diagnostics.push({
91
+ code: "DUPLICATE_TYPE_EXPORT",
92
+ message: `Type '${name}' is exported from multiple files. First defined at ${formatLocation(existing.location)}.`,
93
+ severity: "error",
94
+ location,
95
+ });
96
+ return false;
97
+ }
98
+ typeLocations.set(name, { location, symbol: resolvedSymbol });
99
+ typeNames.add(name);
100
+ if (resolvedSymbol) {
101
+ typeSymbols.set(name, resolvedSymbol);
102
+ }
103
+ return true;
104
+ }
105
+
38
106
  for (const filePath of sourceFiles) {
39
107
  const sourceFile = program.getSourceFile(filePath);
40
108
  if (!sourceFile) continue;
@@ -43,11 +111,11 @@ export function collectDeclaredTypeNames(
43
111
  // Direct type declarations
44
112
  if (ts.isTypeAliasDeclaration(node) && isExported(node)) {
45
113
  const name = node.name.getText(sourceFile);
46
- typeNames.add(name);
114
+ const location = getSourceLocationFromNode(node)!;
47
115
  const symbol = checker.getSymbolAtLocation(node.name);
48
- if (symbol) {
49
- typeSymbols.set(name, resolveOriginalSymbol(symbol, checker));
116
+ const isNew = registerTypeName(name, location, symbol);
50
117
 
118
+ if (isNew && symbol) {
51
119
  // For type aliases like `type User = ExternalUser;`,
52
120
  // also track the underlying type's symbol
53
121
  const type = checker.getTypeAtLocation(node.type);
@@ -62,19 +130,15 @@ export function collectDeclaredTypeNames(
62
130
  }
63
131
  if (ts.isInterfaceDeclaration(node) && isExported(node)) {
64
132
  const name = node.name.getText(sourceFile);
65
- typeNames.add(name);
133
+ const location = getSourceLocationFromNode(node)!;
66
134
  const symbol = checker.getSymbolAtLocation(node.name);
67
- if (symbol) {
68
- typeSymbols.set(name, resolveOriginalSymbol(symbol, checker));
69
- }
135
+ registerTypeName(name, location, symbol);
70
136
  }
71
137
  if (ts.isEnumDeclaration(node) && isExported(node)) {
72
138
  const name = node.name.getText(sourceFile);
73
- typeNames.add(name);
139
+ const location = getSourceLocationFromNode(node)!;
74
140
  const symbol = checker.getSymbolAtLocation(node.name);
75
- if (symbol) {
76
- typeSymbols.set(name, resolveOriginalSymbol(symbol, checker));
77
- }
141
+ registerTypeName(name, location, symbol);
78
142
  }
79
143
 
80
144
  // Re-exports: `export type { ... } from "..."` or `export type * from "..."`
@@ -85,11 +149,9 @@ export function collectDeclaredTypeNames(
85
149
  for (const element of node.exportClause.elements) {
86
150
  // Use the exported name (element.name), not the property name
87
151
  const name = element.name.getText(sourceFile);
88
- typeNames.add(name);
152
+ const location = getSourceLocationFromNode(element)!;
89
153
  const symbol = checker.getSymbolAtLocation(element.name);
90
- if (symbol) {
91
- typeSymbols.set(name, resolveOriginalSymbol(symbol, checker));
92
- }
154
+ registerTypeName(name, location, symbol);
93
155
  }
94
156
  }
95
157
  } else if (node.moduleSpecifier) {
@@ -103,8 +165,8 @@ export function collectDeclaredTypeNames(
103
165
  const name = exp.getName();
104
166
  // Check if the export is a type (not a value)
105
167
  if (isTypeExport(exp)) {
106
- typeNames.add(name);
107
- typeSymbols.set(name, resolveOriginalSymbol(exp, checker));
168
+ const location = getSourceLocationFromNode(node)!;
169
+ registerTypeName(name, location, exp);
108
170
  }
109
171
  }
110
172
  }
@@ -113,7 +175,7 @@ export function collectDeclaredTypeNames(
113
175
  });
114
176
  }
115
177
 
116
- return { typeNames, typeSymbols, underlyingSymbolToTypeName };
178
+ return { typeNames, typeSymbols, underlyingSymbolToTypeName, diagnostics };
117
179
  }
118
180
 
119
181
  /**
@@ -96,7 +96,8 @@ export type DiagnosticCode =
96
96
  | "TYPENAME_FIELD_STRUCTURE_MISMATCH"
97
97
  | "DUPLICATE_TYPENAME_VALUE"
98
98
  | "IGNORE_FIELD_NOT_FOUND"
99
- | "IGNORE_ALL_FIELDS";
99
+ | "IGNORE_ALL_FIELDS"
100
+ | "DUPLICATE_TYPE_EXPORT";
100
101
 
101
102
  export interface Diagnostic {
102
103
  readonly code: DiagnosticCode;