Skip to content

Commit 1edba84

Browse files
committed
feat(graphql): add input/output type splitting based on usage
- Update GraphQLMutationOptions with usageFlag and mutationKey - GraphQLModelMutation adds 'Input' suffix for input types - GraphQLMutationEngine.mutateModel() returns ModelMutationResult with separate input/output mutations based on resolveUsages() - ModelTypeMap supports GraphQLInputObjectType for input types - Schema emitter handles both input and output model variants - Add 5 new tests for input/output splitting behavior
1 parent bcfae7d commit 1edba84

8 files changed

Lines changed: 269 additions & 56 deletions

File tree

packages/graphql/src/mutation-engine/engine.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import {
2+
resolveUsages,
3+
UsageFlags,
24
type Enum,
35
type Model,
46
type Namespace,
57
type Operation,
68
type Program,
79
type Scalar,
10+
type UsageTracker,
811
} from "@typespec/compiler";
912
import { $ } from "@typespec/compiler/typekit";
1013
import {
@@ -47,9 +50,20 @@ const graphqlMutationRegistry = {
4750
Intrinsic: SimpleIntrinsicMutation,
4851
};
4952

53+
/**
54+
* Result of mutating a model with usage awareness.
55+
* Contains separate mutations for input and output variants when applicable.
56+
*/
57+
export interface ModelMutationResult {
58+
/** The input variant mutation (with "Input" suffix), if the model is used as input */
59+
input?: GraphQLModelMutation;
60+
/** The output variant mutation (no suffix), if the model is used as output */
61+
output?: GraphQLModelMutation;
62+
}
63+
5064
/**
5165
* GraphQL mutation engine that applies GraphQL-specific transformations
52-
* to TypeSpec types, such as name sanitization.
66+
* to TypeSpec types, such as name sanitization and input/output splitting.
5367
*/
5468
export class GraphQLMutationEngine {
5569
/**
@@ -58,16 +72,55 @@ export class GraphQLMutationEngine {
5872
*/
5973
private engine;
6074

61-
constructor(program: Program, _namespace: Namespace) {
75+
/** Usage tracker for types in the namespace */
76+
private usageTracker: UsageTracker;
77+
78+
constructor(program: Program, namespace: Namespace) {
6279
const tk = $(program);
6380
this.engine = new MutationEngine(tk, graphqlMutationRegistry);
81+
82+
// Resolve usages once at construction time
83+
this.usageTracker = resolveUsages(namespace);
84+
}
85+
86+
/**
87+
* Get the usage flags for a model.
88+
*/
89+
getUsage(model: Model): UsageFlags {
90+
const isInput = this.usageTracker.isUsedAs(model, UsageFlags.Input);
91+
const isOutput = this.usageTracker.isUsedAs(model, UsageFlags.Output);
92+
93+
if (isInput && isOutput) {
94+
return UsageFlags.Input | UsageFlags.Output;
95+
} else if (isInput) {
96+
return UsageFlags.Input;
97+
} else if (isOutput) {
98+
return UsageFlags.Output;
99+
}
100+
return UsageFlags.None;
64101
}
65102

66103
/**
67-
* Mutate a model, applying GraphQL name sanitization.
104+
* Mutate a model with usage awareness.
105+
* Returns separate input/output mutations based on how the model is used.
68106
*/
69-
mutateModel(model: Model): GraphQLModelMutation {
70-
return this.engine.mutate(model, new GraphQLMutationOptions()) as GraphQLModelMutation;
107+
mutateModel(model: Model): ModelMutationResult {
108+
const usage = this.getUsage(model);
109+
const result: ModelMutationResult = {};
110+
111+
// Create output mutation if used as output (or no usage info)
112+
if (usage & UsageFlags.Output || usage === UsageFlags.None) {
113+
const outputOptions = new GraphQLMutationOptions(UsageFlags.Output);
114+
result.output = this.engine.mutate(model, outputOptions) as GraphQLModelMutation;
115+
}
116+
117+
// Create input mutation if used as input
118+
if (usage & UsageFlags.Input) {
119+
const inputOptions = new GraphQLMutationOptions(UsageFlags.Input);
120+
result.input = this.engine.mutate(model, inputOptions) as GraphQLModelMutation;
121+
}
122+
123+
return result;
71124
}
72125

73126
/**

packages/graphql/src/mutation-engine/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export { GraphQLMutationEngine, createGraphQLMutationEngine } from "./engine.js";
1+
export {
2+
GraphQLMutationEngine,
3+
createGraphQLMutationEngine,
4+
type ModelMutationResult,
5+
} from "./engine.js";
26
export {
37
GraphQLEnumMemberMutation,
48
GraphQLEnumMutation,

packages/graphql/src/mutation-engine/mutations/model.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MemberType, Model } from "@typespec/compiler";
1+
import { UsageFlags, type MemberType, type Model } from "@typespec/compiler";
22
import {
33
SimpleModelMutation,
44
type MutationInfo,
@@ -7,11 +7,15 @@ import {
77
type SimpleMutations,
88
} from "@typespec/mutator-framework";
99
import { sanitizeNameForGraphQL } from "../../lib/type-utils.js";
10+
import type { GraphQLMutationOptions } from "../options.js";
1011

1112
/**
12-
* GraphQL-specific Model mutation.
13+
* GraphQL-specific Model mutation that sanitizes names for GraphQL compatibility.
14+
* Adds "Input" suffix when the model is used as an input type.
1315
*/
1416
export class GraphQLModelMutation extends SimpleModelMutation<SimpleMutationOptions> {
17+
private graphqlOptions: GraphQLMutationOptions;
18+
1519
constructor(
1620
engine: SimpleMutationEngine<SimpleMutations<SimpleMutationOptions>>,
1721
sourceType: Model,
@@ -20,12 +24,20 @@ export class GraphQLModelMutation extends SimpleModelMutation<SimpleMutationOpti
2024
info: MutationInfo,
2125
) {
2226
super(engine as any, sourceType, referenceTypes, options, info);
27+
this.graphqlOptions = options as GraphQLMutationOptions;
2328
}
2429

2530
mutate() {
2631
// Apply GraphQL name sanitization
2732
this.mutationNode.mutate((model) => {
28-
model.name = sanitizeNameForGraphQL(model.name);
33+
let name = sanitizeNameForGraphQL(model.name);
34+
35+
// Add "Input" suffix for input types
36+
if (this.graphqlOptions.usageFlag === UsageFlags.Input) {
37+
name = `${name}Input`;
38+
}
39+
40+
model.name = name;
2941
});
3042
super.mutate();
3143
}
Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
1+
import { UsageFlags } from "@typespec/compiler";
12
import { SimpleMutationOptions } from "@typespec/mutator-framework";
23

34
/**
45
* GraphQL-specific mutation options.
56
*
6-
* Currently a simple wrapper around SimpleMutationOptions.
7-
* Can be extended in the future to support additional GraphQL-specific options.
7+
* Extends SimpleMutationOptions with usage-aware mutation key support,
8+
* enabling separate mutations for input vs output type variants.
89
*/
9-
export class GraphQLMutationOptions extends SimpleMutationOptions {}
10+
export class GraphQLMutationOptions extends SimpleMutationOptions {
11+
/**
12+
* The usage flag indicating whether this mutation is for input or output usage.
13+
* Used to generate separate mutations for the same type when used in both contexts.
14+
*/
15+
readonly usageFlag: UsageFlags;
16+
17+
constructor(usageFlag: UsageFlags = UsageFlags.None) {
18+
super();
19+
this.usageFlag = usageFlag;
20+
}
21+
22+
/**
23+
* Override mutationKey to include usage flag.
24+
* This ensures the mutation engine caches separate mutations for input vs output variants.
25+
*/
26+
override get mutationKey(): string {
27+
const baseKey = super.mutationKey;
28+
if (this.usageFlag === UsageFlags.Input) {
29+
return `${baseKey}:input`;
30+
} else if (this.usageFlag === UsageFlags.Output) {
31+
return `${baseKey}:output`;
32+
}
33+
return baseKey;
34+
}
35+
}

packages/graphql/src/schema-emitter.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,34 @@ class GraphQLSchemaEmitter {
6565
this.registry.addEnum(mutation.mutatedType);
6666
},
6767
model: (node: Model) => {
68-
const mutation = this.engine.mutateModel(node);
69-
// TODO: Handle input/output variants
70-
this.registry.addModel(mutation.mutatedType, UsageFlags.Output);
68+
// Mutate the model - returns input/output variants
69+
const result = this.engine.mutateModel(node);
70+
71+
// Register output variant if present
72+
if (result.output) {
73+
this.registry.addModel(result.output.mutatedType, UsageFlags.Output);
74+
}
75+
76+
// Register input variant if present
77+
if (result.input) {
78+
this.registry.addModel(result.input.mutatedType, UsageFlags.Input);
79+
}
7180
},
7281
exitEnum: (node: Enum) => {
7382
const mutation = this.engine.mutateEnum(node);
7483
this.registry.materializeEnum(mutation.mutatedType.name);
7584
},
7685
exitModel: (node: Model) => {
77-
const mutation = this.engine.mutateModel(node);
78-
this.registry.materializeModel(mutation.mutatedType.name);
86+
// Materialize both input and output variants
87+
const result = this.engine.mutateModel(node);
88+
89+
if (result.output) {
90+
this.registry.materializeModel(result.output.mutatedType.name);
91+
}
92+
93+
if (result.input) {
94+
this.registry.materializeModel(result.input.mutatedType.name);
95+
}
7996
},
8097
};
8198
}

packages/graphql/src/type-maps/model.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
GraphQLString,
66
type GraphQLFieldConfigMap,
77
type GraphQLInputFieldConfigMap,
8+
type GraphQLInputType,
9+
type GraphQLOutputType,
810
} from "graphql";
911
import { TypeMap, type TSPContext, type TypeKey } from "../type-maps.js";
1012

@@ -32,42 +34,55 @@ export class ModelTypeMap extends TypeMap<Model, GraphQLObjectType | GraphQLInpu
3234

3335
// Create InputObjectType for input usage, ObjectType for output
3436
if (context.usageFlag === UsageFlags.Input) {
35-
return this.materializeInputType(name, tspModel);
37+
return this.materializeInputType(tspModel, name);
3638
}
37-
return this.materializeOutputType(name, tspModel);
39+
return this.materializeOutputType(tspModel, name);
3840
}
3941

40-
private materializeOutputType(name: string, tspModel: Model): GraphQLObjectType {
42+
/**
43+
* Materialize as a GraphQLObjectType (output type).
44+
*/
45+
private materializeOutputType(tspModel: Model, name: string): GraphQLObjectType {
4146
const fields: GraphQLFieldConfigMap<unknown, unknown> = {};
4247

4348
for (const [propName, prop] of tspModel.properties) {
4449
fields[propName] = {
45-
type: this.mapPropertyType(prop.type),
46-
// TODO: Add description from doc comments
50+
type: this.mapOutputType(prop.type),
4751
};
4852
}
4953

5054
return new GraphQLObjectType({ name, fields });
5155
}
5256

53-
private materializeInputType(name: string, tspModel: Model): GraphQLInputObjectType {
57+
/**
58+
* Materialize as a GraphQLInputObjectType (input type).
59+
*/
60+
private materializeInputType(tspModel: Model, name: string): GraphQLInputObjectType {
5461
const fields: GraphQLInputFieldConfigMap = {};
5562

5663
for (const [propName, prop] of tspModel.properties) {
5764
fields[propName] = {
58-
type: this.mapPropertyType(prop.type),
59-
// TODO: Add description from doc comments
65+
type: this.mapInputType(prop.type),
6066
};
6167
}
6268

6369
return new GraphQLInputObjectType({ name, fields });
6470
}
6571

6672
/**
67-
* Map a TypeSpec property type to a GraphQL String type.
73+
* Map a TypeSpec property type to a GraphQL output type.
74+
* TODO: Implement full type mapping with references to other registered types.
75+
*/
76+
private mapOutputType(_type: unknown): GraphQLOutputType {
77+
// Placeholder - will need to resolve references to other types
78+
return GraphQLString;
79+
}
80+
81+
/**
82+
* Map a TypeSpec property type to a GraphQL input type.
6883
* TODO: Implement full type mapping with references to other registered types.
6984
*/
70-
private mapPropertyType(_type: unknown): typeof GraphQLString {
85+
private mapInputType(_type: unknown): GraphQLInputType {
7186
// Placeholder - will need to resolve references to other types
7287
return GraphQLString;
7388
}

packages/graphql/test/main.tsp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ namespace MyLibrary {
2424
Mystery,
2525
Fantasy,
2626
}
27+
28+
op createBook(book: Book): Book;
2729
}

0 commit comments

Comments
 (0)