@gqlkit-ts/cli 0.5.1 → 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 (49) hide show
  1. package/dist/auto-type-generator/auto-type-generator.d.ts.map +1 -1
  2. package/dist/auto-type-generator/auto-type-generator.js +4 -1
  3. package/dist/auto-type-generator/auto-type-generator.js.map +1 -1
  4. package/dist/auto-type-generator/naming-convention.d.ts +2 -2
  5. package/dist/auto-type-generator/naming-convention.d.ts.map +1 -1
  6. package/dist/auto-type-generator/resolver-field-iterator.d.ts +1 -1
  7. package/dist/auto-type-generator/resolver-field-iterator.d.ts.map +1 -1
  8. package/dist/auto-type-generator/resolver-field-iterator.js +3 -0
  9. package/dist/auto-type-generator/resolver-field-iterator.js.map +1 -1
  10. package/dist/commands/docs.d.ts +1 -0
  11. package/dist/commands/docs.d.ts.map +1 -1
  12. package/dist/commands/gen.d.ts +1 -0
  13. package/dist/commands/gen.d.ts.map +1 -1
  14. package/dist/commands/main.d.ts +1 -0
  15. package/dist/commands/main.d.ts.map +1 -1
  16. package/dist/gen-orchestrator/orchestrator.d.ts.map +1 -1
  17. package/dist/gen-orchestrator/orchestrator.js +28 -1
  18. package/dist/gen-orchestrator/orchestrator.js.map +1 -1
  19. package/dist/resolver-extractor/extract-resolvers.d.ts +4 -0
  20. package/dist/resolver-extractor/extract-resolvers.d.ts.map +1 -1
  21. package/dist/resolver-extractor/extractor/define-api-extractor.d.ts +2 -1
  22. package/dist/resolver-extractor/extractor/define-api-extractor.d.ts.map +1 -1
  23. package/dist/resolver-extractor/extractor/define-api-extractor.js +31 -6
  24. package/dist/resolver-extractor/extractor/define-api-extractor.js.map +1 -1
  25. package/dist/resolver-extractor/index.d.ts +1 -1
  26. package/dist/resolver-extractor/index.d.ts.map +1 -1
  27. package/dist/schema-generator/emitter/code-emitter.d.ts.map +1 -1
  28. package/dist/schema-generator/emitter/code-emitter.js +11 -3
  29. package/dist/schema-generator/emitter/code-emitter.js.map +1 -1
  30. package/dist/schema-generator/integrator/result-integrator.d.ts +1 -0
  31. package/dist/schema-generator/integrator/result-integrator.d.ts.map +1 -1
  32. package/dist/schema-generator/integrator/result-integrator.js +26 -1
  33. package/dist/schema-generator/integrator/result-integrator.js.map +1 -1
  34. package/docs/getting-started.md +2 -1
  35. package/docs/index.md +1 -0
  36. package/docs/schema/conventions.md +7 -0
  37. package/docs/schema/fields.md +15 -0
  38. package/docs/schema/queries-mutations.md +21 -2
  39. package/docs/schema/subscriptions.md +173 -0
  40. package/package.json +3 -3
  41. package/src/auto-type-generator/auto-type-generator.ts +12 -4
  42. package/src/auto-type-generator/naming-convention.ts +2 -2
  43. package/src/auto-type-generator/resolver-field-iterator.ts +5 -1
  44. package/src/gen-orchestrator/orchestrator.ts +31 -1
  45. package/src/resolver-extractor/extract-resolvers.ts +5 -0
  46. package/src/resolver-extractor/extractor/define-api-extractor.ts +43 -7
  47. package/src/resolver-extractor/index.ts +1 -0
  48. package/src/schema-generator/emitter/code-emitter.ts +17 -4
  49. package/src/schema-generator/integrator/result-integrator.ts +30 -1
@@ -0,0 +1,173 @@
1
+ ---
2
+ title: Defining Subscriptions
3
+ description: Define Subscription fields using the @gqlkit-ts/runtime API.
4
+ ---
5
+
6
+ # Subscriptions
7
+
8
+ Define Subscription fields using the `@gqlkit-ts/runtime` API.
9
+
10
+ > **Prerequisites**: This guide assumes you have completed the [basic setup](../getting-started.md#set-up-context-and-resolver-factories).
11
+
12
+ ## Setup
13
+
14
+ Export `defineSubscription` from your `gqlkit.ts`:
15
+
16
+ ```typescript
17
+ import { createGqlkitApis } from "@gqlkit-ts/runtime";
18
+ import type { Context } from "./context";
19
+
20
+ export const { defineQuery, defineMutation, defineSubscription } =
21
+ createGqlkitApis<Context>();
22
+ ```
23
+
24
+ ## Subscription Resolvers
25
+
26
+ Use `defineSubscription` to define Subscription fields. The resolver is typically an async generator function:
27
+
28
+ ```typescript
29
+ import { defineSubscription } from "../gqlkit";
30
+ import type { Message } from "./message";
31
+
32
+ // Subscription.messageAdded(channelId: String!)
33
+ export const messageAdded = defineSubscription<
34
+ { channelId: string },
35
+ Message
36
+ >(async function* (_root, args, ctx) {
37
+ yield* ctx.pubsub.subscribe<Message>("MESSAGE_ADDED", args.channelId);
38
+ });
39
+ ```
40
+
41
+ Generates:
42
+
43
+ ```graphql
44
+ type Subscription {
45
+ messageAdded(channelId: String!): Message!
46
+ }
47
+ ```
48
+
49
+ The same export name conventions apply as with [Queries & Mutations](./queries-mutations.md):
50
+
51
+ ```typescript
52
+ // GraphQL field name: messageAdded
53
+ export const Subscription$messageAdded = defineSubscription<
54
+ { channelId: string },
55
+ Message
56
+ >(async function* (_root, args, ctx) {
57
+ yield* ctx.pubsub.subscribe<Message>("MESSAGE_ADDED", args.channelId);
58
+ });
59
+ ```
60
+
61
+ ## NoArgs Subscriptions
62
+
63
+ For subscriptions without arguments, use `NoArgs` as the first type parameter — same as with `defineQuery` and `defineMutation`. See [Queries & Mutations](./queries-mutations.md#noargs-queries) for details.
64
+
65
+ ## Resolver Function Signature
66
+
67
+ Subscription resolvers receive the same four arguments as Query/Mutation resolvers, but return an `AsyncIterable` instead of a direct value:
68
+
69
+ ```typescript
70
+ (root, args, ctx, info) => AsyncIterable<T> | Promise<AsyncIterable<T>>
71
+ ```
72
+
73
+ | Argument | Description |
74
+ |----------|-------------|
75
+ | `root` | The root value (always undefined) |
76
+ | `args` | The arguments passed to the field |
77
+ | `ctx` | The context object (typed via `createGqlkitApis<Context>()`) |
78
+ | `info` | GraphQL resolve info |
79
+
80
+ ## Inline Object Arguments
81
+
82
+ Subscription arguments support the same inline object types as queries and mutations:
83
+
84
+ ```typescript
85
+ export const orderUpdated = defineSubscription<
86
+ {
87
+ filter: {
88
+ orderId: string | null;
89
+ status: string | null;
90
+ };
91
+ },
92
+ Order
93
+ >(async function* (_root, args, ctx) {
94
+ yield* ctx.pubsub.subscribe<Order>("ORDER_UPDATED", args.filter);
95
+ });
96
+ ```
97
+
98
+ Generates:
99
+
100
+ ```graphql
101
+ type Subscription {
102
+ orderUpdated(filter: OrderUpdatedFilterInput!): Order!
103
+ }
104
+
105
+ input OrderUpdatedFilterInput {
106
+ orderId: String
107
+ status: String
108
+ }
109
+ ```
110
+
111
+ ## Attaching Directives
112
+
113
+ Add a third type parameter to attach directives:
114
+
115
+ ```typescript
116
+ import { defineSubscription } from "../gqlkit";
117
+ import type { AuthDirective } from "./directives";
118
+ import type { Message } from "./message";
119
+
120
+ export const messageAdded = defineSubscription<
121
+ { channelId: string },
122
+ Message,
123
+ [AuthDirective<{ role: ["USER"] }>]
124
+ >(async function* (_root, args, ctx) {
125
+ yield* ctx.pubsub.subscribe<Message>("MESSAGE_ADDED", args.channelId);
126
+ });
127
+ ```
128
+
129
+ Generates:
130
+
131
+ ```graphql
132
+ type Subscription {
133
+ messageAdded(channelId: String!): Message! @auth(role: [USER])
134
+ }
135
+ ```
136
+
137
+ See [Directives](./directives.md) for more details on defining and using custom directives.
138
+
139
+ ## Documentation
140
+
141
+ TSDoc comments on subscription exports are extracted as GraphQL descriptions:
142
+
143
+ ```typescript
144
+ /** Subscribe to new messages in a channel. */
145
+ export const messageAdded = defineSubscription<
146
+ { channelId: string },
147
+ Message
148
+ >(async function* (_root, args, ctx) {
149
+ yield* ctx.pubsub.subscribe<Message>("MESSAGE_ADDED", args.channelId);
150
+ });
151
+
152
+ /**
153
+ * @deprecated Use messageAdded instead.
154
+ */
155
+ export const onMessage = defineSubscription<
156
+ { channelId: string },
157
+ Message
158
+ >(async function* (_root, args, ctx) {
159
+ yield* ctx.pubsub.subscribe<Message>("MESSAGE_ADDED", args.channelId);
160
+ });
161
+ ```
162
+
163
+ Generates:
164
+
165
+ ```graphql
166
+ type Subscription {
167
+ """Subscribe to new messages in a channel."""
168
+ messageAdded(channelId: String!): Message!
169
+ onMessage(channelId: String!): Message! @deprecated(reason: "Use messageAdded instead.")
170
+ }
171
+ ```
172
+
173
+ See [Documentation](./documentation.md) for more details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gqlkit-ts/cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Just types and functions — write TypeScript, generate GraphQL.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@graphql-tools/utils": "^11.0.0",
44
- "gunshi": "^0.27.5",
44
+ "gunshi": "^0.29.0",
45
45
  "jiti": "^2.4.2",
46
46
  "shell-quote": "^1.8.3"
47
47
  },
@@ -61,7 +61,7 @@
61
61
  "@types/shell-quote": "1.7.5",
62
62
  "memfs": "4.56.10",
63
63
  "@gqlkit-ts/docs": "0.0.1",
64
- "@gqlkit-ts/runtime": "0.2.0"
64
+ "@gqlkit-ts/runtime": "0.3.0"
65
65
  },
66
66
  "scripts": {
67
67
  "build": "tsc --build && bundle-docs --target ./docs",
@@ -52,7 +52,10 @@ import {
52
52
  isInputTypeName,
53
53
  } from "./naming-convention.js";
54
54
  import type { ResolveTypeFieldPattern } from "./resolve-type-generator.js";
55
- import { forEachResolverField } from "./resolver-field-iterator.js";
55
+ import {
56
+ forEachResolverField,
57
+ type ResolverType,
58
+ } from "./resolver-field-iterator.js";
56
59
  import {
57
60
  createFieldNameSet,
58
61
  findTypenameProperty,
@@ -353,7 +356,7 @@ function collectInlineObjectsFromResolvers(
353
356
 
354
357
  function collectInlineObjectsFromResolverArgs(
355
358
  field: GraphQLFieldDefinition,
356
- resolverType: "query" | "mutation" | "field",
359
+ resolverType: ResolverType,
357
360
  parentTypeName: string | null,
358
361
  results: InlineObjectWithContext[],
359
362
  ): void {
@@ -420,7 +423,7 @@ function collectInlinePayloadsFromResolvers(
420
423
 
421
424
  function collectInlinePayloadFromReturnType(
422
425
  field: GraphQLFieldDefinition,
423
- resolverType: "query" | "mutation" | "field",
426
+ resolverType: ResolverType,
424
427
  parentTypeName: string | null,
425
428
  results: InlineObjectWithContext[],
426
429
  ): void {
@@ -716,6 +719,11 @@ function updateResolversResult(
716
719
  updateResolverField(field, params, "mutation", null),
717
720
  ),
718
721
  },
722
+ subscriptionFields: {
723
+ fields: resolversResult.subscriptionFields.fields.map((field) =>
724
+ updateResolverField(field, params, "subscription", null),
725
+ ),
726
+ },
719
727
  typeExtensions: resolversResult.typeExtensions.map((ext) => ({
720
728
  ...ext,
721
729
  fields: ext.fields.map((field) =>
@@ -728,7 +736,7 @@ function updateResolversResult(
728
736
  function updateResolverField(
729
737
  field: GraphQLFieldDefinition,
730
738
  params: UpdateTypeNamesParams,
731
- resolverType: "query" | "mutation" | "field",
739
+ resolverType: "query" | "mutation" | "subscription" | "field",
732
740
  parentTypeName: string | null,
733
741
  ): GraphQLFieldDefinition {
734
742
  const { generatedTypeNames, enumTypeNames, unionTypeNames } = params;
@@ -34,7 +34,7 @@ export interface InputFieldContext {
34
34
  */
35
35
  export interface ResolverArgContext {
36
36
  readonly kind: "resolverArg";
37
- readonly resolverType: "query" | "mutation" | "field";
37
+ readonly resolverType: "query" | "mutation" | "subscription" | "field";
38
38
  readonly fieldName: string;
39
39
  readonly argName: string;
40
40
  readonly parentTypeName: string | null;
@@ -49,7 +49,7 @@ export interface ResolverArgContext {
49
49
  */
50
50
  export interface ResolverPayloadContext {
51
51
  readonly kind: "resolverPayload";
52
- readonly resolverType: "query" | "mutation" | "field";
52
+ readonly resolverType: "query" | "mutation" | "subscription" | "field";
53
53
  readonly fieldName: string;
54
54
  readonly parentTypeName: string | null;
55
55
  readonly fieldPath: ReadonlyArray<string>;
@@ -3,7 +3,7 @@ import type {
3
3
  GraphQLFieldDefinition,
4
4
  } from "../resolver-extractor/index.js";
5
5
 
6
- export type ResolverType = "query" | "mutation" | "field";
6
+ export type ResolverType = "query" | "mutation" | "field" | "subscription";
7
7
 
8
8
  export interface ResolverFieldInfo {
9
9
  readonly field: GraphQLFieldDefinition;
@@ -27,6 +27,10 @@ export function forEachResolverField(
27
27
  visitor({ field, resolverType: "mutation", parentTypeName: null });
28
28
  }
29
29
 
30
+ for (const field of resolversResult.subscriptionFields.fields) {
31
+ visitor({ field, resolverType: "subscription", parentTypeName: null });
32
+ }
33
+
30
34
  for (const ext of resolversResult.typeExtensions) {
31
35
  for (const field of ext.fields) {
32
36
  visitor({
@@ -34,6 +34,7 @@ import {
34
34
  collectScalars,
35
35
  type ScalarMetadataInfo,
36
36
  } from "../type-extractor/collector/scalar-collector.js";
37
+ import { isEligibleField } from "../type-extractor/converter/field-eligibility.js";
37
38
  import { convertToGraphQL } from "../type-extractor/converter/graphql-converter.js";
38
39
  import {
39
40
  extractTypesFromProgram,
@@ -86,6 +87,7 @@ interface TypesResult {
86
87
  interface ResolversResult {
87
88
  queryFields: { fields: ReadonlyArray<GraphQLFieldDefinition> };
88
89
  mutationFields: { fields: ReadonlyArray<GraphQLFieldDefinition> };
90
+ subscriptionFields: { fields: ReadonlyArray<GraphQLFieldDefinition> };
89
91
  typeExtensions: ReadonlyArray<TypeExtension>;
90
92
  abstractTypeResolvers: ReadonlyArray<AbstractResolverInfo>;
91
93
  diagnostics: Diagnostics;
@@ -279,20 +281,42 @@ function convertDefineApiToFields(
279
281
  ): {
280
282
  queryFields: { fields: ReadonlyArray<GraphQLFieldDefinition> };
281
283
  mutationFields: { fields: ReadonlyArray<GraphQLFieldDefinition> };
284
+ subscriptionFields: { fields: ReadonlyArray<GraphQLFieldDefinition> };
282
285
  typeExtensions: ReadonlyArray<TypeExtension>;
286
+ diagnostics: ReadonlyArray<Diagnostic>;
283
287
  } {
284
288
  const queryFields: GraphQLFieldDefinition[] = [];
285
289
  const mutationFields: GraphQLFieldDefinition[] = [];
290
+ const subscriptionFields: GraphQLFieldDefinition[] = [];
286
291
  const typeExtensionMap = new Map<string, GraphQLFieldDefinition[]>();
292
+ const diagnostics: Diagnostic[] = [];
287
293
 
288
294
  for (const resolver of resolvers) {
295
+ const eligibility = isEligibleField({
296
+ fieldName: resolver.fieldName,
297
+ kind: "object",
298
+ });
299
+ if (!eligibility.eligible) {
300
+ diagnostics.push({
301
+ code: "SKIPPED_FIELD",
302
+ message: eligibility.skipReason.message,
303
+ severity: "warning",
304
+ location: {
305
+ file: resolver.sourceLocation.file,
306
+ line: resolver.sourceLocation.line,
307
+ column: resolver.sourceLocation.column,
308
+ },
309
+ });
310
+ continue;
311
+ }
312
+
289
313
  const returnType = resolver.returnType;
290
314
  const fieldDef: GraphQLFieldDefinition = {
291
315
  name: resolver.fieldName,
292
316
  type: convertTsTypeToGraphQLType(returnType),
293
317
  args: resolver.args ? convertArgsToInputValues(resolver.args) : null,
294
318
  sourceLocation: resolver.sourceLocation,
295
- resolverExportName: resolver.fieldName,
319
+ resolverExportName: resolver.resolverExportName,
296
320
  description: resolver.description,
297
321
  deprecated: resolver.deprecated,
298
322
  directives: resolver.directives,
@@ -316,6 +340,8 @@ function convertDefineApiToFields(
316
340
  queryFields.push(fieldDef);
317
341
  } else if (resolver.resolverType === "mutation") {
318
342
  mutationFields.push(fieldDef);
343
+ } else if (resolver.resolverType === "subscription") {
344
+ subscriptionFields.push(fieldDef);
319
345
  } else if (resolver.resolverType === "field" && resolver.parentTypeName) {
320
346
  const existing = typeExtensionMap.get(resolver.parentTypeName) ?? [];
321
347
  existing.push(fieldDef);
@@ -331,7 +357,9 @@ function convertDefineApiToFields(
331
357
  return {
332
358
  queryFields: { fields: queryFields },
333
359
  mutationFields: { fields: mutationFields },
360
+ subscriptionFields: { fields: subscriptionFields },
334
361
  typeExtensions,
362
+ diagnostics,
335
363
  };
336
364
  }
337
365
 
@@ -411,9 +439,11 @@ function extractResolversCore(
411
439
  allDiagnostics.push(...defineApiExtractionResult.diagnostics);
412
440
 
413
441
  const result = convertDefineApiToFields(defineApiExtractionResult.resolvers);
442
+ allDiagnostics.push(...result.diagnostics);
414
443
  return {
415
444
  queryFields: result.queryFields,
416
445
  mutationFields: result.mutationFields,
446
+ subscriptionFields: result.subscriptionFields,
417
447
  typeExtensions: result.typeExtensions,
418
448
  abstractTypeResolvers: defineApiExtractionResult.abstractTypeResolvers,
419
449
  diagnostics: collectDiagnostics(allDiagnostics),
@@ -68,6 +68,10 @@ export interface MutationFieldDefinitions {
68
68
  readonly fields: ReadonlyArray<GraphQLFieldDefinition>;
69
69
  }
70
70
 
71
+ export interface SubscriptionFieldDefinitions {
72
+ readonly fields: ReadonlyArray<GraphQLFieldDefinition>;
73
+ }
74
+
71
75
  export interface TypeExtension {
72
76
  readonly targetTypeName: string;
73
77
  readonly fields: ReadonlyArray<GraphQLFieldDefinition>;
@@ -76,6 +80,7 @@ export interface TypeExtension {
76
80
  export interface ExtractResolversResult {
77
81
  readonly queryFields: QueryFieldDefinitions;
78
82
  readonly mutationFields: MutationFieldDefinitions;
83
+ readonly subscriptionFields: SubscriptionFieldDefinitions;
79
84
  readonly typeExtensions: ReadonlyArray<TypeExtension>;
80
85
  readonly abstractTypeResolvers: ReadonlyArray<AbstractResolverInfo>;
81
86
  readonly diagnostics: Diagnostics;
@@ -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
  };