@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.
- package/dist/auto-type-generator/auto-type-generator.d.ts.map +1 -1
- package/dist/auto-type-generator/auto-type-generator.js +4 -1
- package/dist/auto-type-generator/auto-type-generator.js.map +1 -1
- package/dist/auto-type-generator/naming-convention.d.ts +2 -2
- package/dist/auto-type-generator/naming-convention.d.ts.map +1 -1
- package/dist/auto-type-generator/resolver-field-iterator.d.ts +1 -1
- package/dist/auto-type-generator/resolver-field-iterator.d.ts.map +1 -1
- package/dist/auto-type-generator/resolver-field-iterator.js +3 -0
- package/dist/auto-type-generator/resolver-field-iterator.js.map +1 -1
- package/dist/commands/docs.d.ts +1 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/gen.d.ts +1 -0
- package/dist/commands/gen.d.ts.map +1 -1
- package/dist/commands/main.d.ts +1 -0
- package/dist/commands/main.d.ts.map +1 -1
- package/dist/gen-orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/gen-orchestrator/orchestrator.js +28 -1
- package/dist/gen-orchestrator/orchestrator.js.map +1 -1
- package/dist/resolver-extractor/extract-resolvers.d.ts +4 -0
- package/dist/resolver-extractor/extract-resolvers.d.ts.map +1 -1
- package/dist/resolver-extractor/extractor/define-api-extractor.d.ts +2 -1
- package/dist/resolver-extractor/extractor/define-api-extractor.d.ts.map +1 -1
- package/dist/resolver-extractor/extractor/define-api-extractor.js +31 -6
- package/dist/resolver-extractor/extractor/define-api-extractor.js.map +1 -1
- package/dist/resolver-extractor/index.d.ts +1 -1
- package/dist/resolver-extractor/index.d.ts.map +1 -1
- package/dist/schema-generator/emitter/code-emitter.d.ts.map +1 -1
- package/dist/schema-generator/emitter/code-emitter.js +11 -3
- package/dist/schema-generator/emitter/code-emitter.js.map +1 -1
- package/dist/schema-generator/integrator/result-integrator.d.ts +1 -0
- package/dist/schema-generator/integrator/result-integrator.d.ts.map +1 -1
- package/dist/schema-generator/integrator/result-integrator.js +26 -1
- package/dist/schema-generator/integrator/result-integrator.js.map +1 -1
- package/docs/getting-started.md +2 -1
- package/docs/index.md +1 -0
- package/docs/schema/conventions.md +7 -0
- package/docs/schema/fields.md +15 -0
- package/docs/schema/queries-mutations.md +21 -2
- package/docs/schema/subscriptions.md +173 -0
- package/package.json +3 -3
- package/src/auto-type-generator/auto-type-generator.ts +12 -4
- package/src/auto-type-generator/naming-convention.ts +2 -2
- package/src/auto-type-generator/resolver-field-iterator.ts +5 -1
- package/src/gen-orchestrator/orchestrator.ts +31 -1
- package/src/resolver-extractor/extract-resolvers.ts +5 -0
- package/src/resolver-extractor/extractor/define-api-extractor.ts +43 -7
- package/src/resolver-extractor/index.ts +1 -0
- package/src/schema-generator/emitter/code-emitter.ts +17 -4
- 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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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:
|
|
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:
|
|
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.
|
|
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 =
|
|
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 (
|
|
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
|
|
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 ${
|
|
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
|
|
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 '${
|
|
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,
|
|
@@ -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 (
|
|
328
|
-
entries.push(` ${field.fieldName}: ${localName},`);
|
|
329
|
-
} else {
|
|
340
|
+
if (isSubscription) {
|
|
330
341
|
entries.push(
|
|
331
|
-
` ${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
|
-
|
|
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
|
};
|