Skip to content

Commit 79ea773

Browse files
committed
Add input/output type context splitting to mutation engine
Operations automatically propagate input context to parameters and output context to return types via GraphQLMutationOptions. The framework's cache and options propagation handle nested types, so the same source model produces separate input and output mutations without any custom type-graph walking.
1 parent bc8a2a1 commit 79ea773

7 files changed

Lines changed: 346 additions & 6 deletions

File tree

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
GraphQLScalarMutation,
2525
GraphQLUnionMutation,
2626
} from "./mutations/index.js";
27+
import { GraphQLMutationOptions, GraphQLTypeContext } from "./options.js";
2728

2829
/**
2930
* Registry configuration for the GraphQL mutation engine.
@@ -49,7 +50,12 @@ const graphqlMutationRegistry = {
4950

5051
/**
5152
* GraphQL mutation engine that applies GraphQL-specific transformations
52-
* to TypeSpec types, such as name sanitization.
53+
* to TypeSpec types, such as name sanitization, scalar mapping, and
54+
* input/output type splitting via mutation keys.
55+
*
56+
* When an operation is mutated, parameters are automatically mutated with
57+
* input context and return types with output context. The mutation framework's
58+
* cache ensures each (type, context) pair produces a separate mutation.
5359
*/
5460
export class GraphQLMutationEngine {
5561
/**
@@ -70,6 +76,15 @@ export class GraphQLMutationEngine {
7076
return this.engine.mutate(model, new SimpleMutationOptions()) as GraphQLModelMutation;
7177
}
7278

79+
/**
80+
* Mutate a model with explicit input/output context.
81+
* Models mutated with different contexts produce separate cached mutations,
82+
* allowing the same source model to have both an input and output variant.
83+
*/
84+
mutateModelAs(model: Model, context: GraphQLTypeContext): GraphQLModelMutation {
85+
return this.engine.mutate(model, new GraphQLMutationOptions(context)) as GraphQLModelMutation;
86+
}
87+
7388
/**
7489
* Mutate an enum, applying GraphQL name sanitization.
7590
*/
@@ -79,6 +94,8 @@ export class GraphQLMutationEngine {
7994

8095
/**
8196
* Mutate an operation, applying GraphQL name sanitization.
97+
* Parameters are automatically mutated with input context,
98+
* return types with output context.
8299
*/
83100
mutateOperation(operation: Operation): GraphQLOperationMutation {
84101
return this.engine.mutate(operation, new SimpleMutationOptions()) as GraphQLOperationMutation;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { GraphQLMutationEngine, createGraphQLMutationEngine } from "./engine.js";
2+
export { GraphQLMutationOptions, GraphQLTypeContext } from "./options.js";
23
export {
34
GraphQLEnumMemberMutation,
45
GraphQLEnumMutation,

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type SimpleMutations,
88
} from "@typespec/mutator-framework";
99
import { sanitizeNameForGraphQL } from "../../lib/type-utils.js";
10+
import { GraphQLMutationOptions, GraphQLTypeContext } from "../options.js";
1011

1112
/**
1213
* GraphQL-specific Model mutation.
@@ -22,6 +23,16 @@ export class GraphQLModelMutation extends SimpleModelMutation<SimpleMutationOpti
2223
super(engine, sourceType, referenceTypes, options, info);
2324
}
2425

26+
/**
27+
* The input/output context this model was mutated with, if any.
28+
* Undefined when the model was mutated directly (not through an operation).
29+
*/
30+
get typeContext(): GraphQLTypeContext | undefined {
31+
return this.options instanceof GraphQLMutationOptions
32+
? this.options.typeContext
33+
: undefined;
34+
}
35+
2536
mutate() {
2637
// Apply GraphQL name sanitization
2738
this.mutationNode.mutate((model) => {

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type SimpleMutations,
88
} from "@typespec/mutator-framework";
99
import { sanitizeNameForGraphQL } from "../../lib/type-utils.js";
10+
import { GraphQLMutationOptions, GraphQLTypeContext } from "../options.js";
1011

1112
/** GraphQL-specific Operation mutation. */
1213
export class GraphQLOperationMutation extends SimpleOperationMutation<SimpleMutationOptions> {
@@ -20,6 +21,32 @@ export class GraphQLOperationMutation extends SimpleOperationMutation<SimpleMuta
2021
super(engine, sourceType, referenceTypes, options, info);
2122
}
2223

24+
/**
25+
* Override to mutate parameters with input context.
26+
* Types reachable from operation parameters become GraphQL input types.
27+
*/
28+
protected override mutateParameters() {
29+
const inputOptions = new GraphQLMutationOptions(GraphQLTypeContext.Input);
30+
this.parameters = this.engine.mutate(
31+
this.sourceType.parameters,
32+
inputOptions,
33+
this.startParametersEdge(),
34+
);
35+
}
36+
37+
/**
38+
* Override to mutate return type with output context.
39+
* Types reachable from operation return types become GraphQL object types.
40+
*/
41+
protected override mutateReturnType() {
42+
const outputOptions = new GraphQLMutationOptions(GraphQLTypeContext.Output);
43+
this.returnType = this.engine.mutate(
44+
this.sourceType.returnType,
45+
outputOptions,
46+
this.startReturnTypeEdge(),
47+
);
48+
}
49+
2350
mutate() {
2451
// Apply GraphQL name sanitization via callback
2552
this.mutationNode.mutate((operation) => {

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,22 @@ export class GraphQLUnionMutation extends UnionMutation<MutationOptions, any, Mu
140140
* GraphQL doesn't support nested unions, so union Pet { cat: Cat, animal: Animal }
141141
* where Animal is itself a union becomes union Pet { Cat | Bear | Lion }
142142
*/
143-
private flattenUnionVariants(union: Union): Array<{ name: string | symbol; type: Type }> {
143+
private flattenUnionVariants(
144+
union: Union,
145+
seen: Set<Union> = new Set(),
146+
): Array<{ name: string | symbol; type: Type }> {
147+
if (seen.has(union)) {
148+
return [];
149+
}
150+
seen.add(union);
151+
144152
const flattened: Array<{ name: string | symbol; type: Type }> = [];
145153

146154
for (const variant of union.variants.values()) {
147155
if (variant.type.kind === "Union") {
148-
// Recursively flatten nested union
149-
const nestedVariants = this.flattenUnionVariants(variant.type as Union);
156+
const nestedVariants = this.flattenUnionVariants(variant.type as Union, seen);
150157
flattened.push(...nestedVariants);
151158
} else {
152-
// Regular variant (not a union)
153159
flattened.push({ name: variant.name, type: variant.type });
154160
}
155161
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { SimpleMutationOptions } from "@typespec/mutator-framework";
2+
3+
/**
4+
* Context for how a type is used in GraphQL operations.
5+
* Determines whether a model becomes an object type (output) or input type (input).
6+
*/
7+
export enum GraphQLTypeContext {
8+
/** Type reachable from operation parameters */
9+
Input = "input",
10+
/** Type reachable from operation return types */
11+
Output = "output",
12+
}
13+
14+
/**
15+
* Mutation options that carry input/output context through the type graph.
16+
* The mutationKey ensures the framework caches input and output variants
17+
* separately for the same source type.
18+
*/
19+
export class GraphQLMutationOptions extends SimpleMutationOptions {
20+
readonly typeContext: GraphQLTypeContext;
21+
22+
constructor(typeContext: GraphQLTypeContext) {
23+
super();
24+
this.typeContext = typeContext;
25+
}
26+
27+
override get mutationKey(): string {
28+
return this.typeContext;
29+
}
30+
}

0 commit comments

Comments
 (0)