Skip to content

Conversation

@FionaBronwen
Copy link

@FionaBronwen FionaBronwen commented Jan 5, 2026

Summary

This PR introduces a GraphQL mutation engine that leverages the @typespec/mutator-framework to transform TypeSpec types for GraphQL schema generation. This PR establishes the foundation with name sanitization, but the architecture will support additional transformations (e.g., input/output type splitting, visibility filtering) in future PRs.

What it does

The mutation engine wraps the mutator-framework's MutationEngine with GraphQL-specific mutation classes. These classes intercept type graph traversal to apply GraphQL-specific transformations.

Current transformation: Name sanitization ensures all type, field, and member names are valid GraphQL identifiers (e.g., $Invalid$_Invalid_, my-fieldmy_field).

Future transformations (not in this PR):

  • Input/output type splitting based on usage
  • Custom scalar mapping
  • and more :)

Architecture

Class Purpose
GraphQLMutationEngine Core engine wrapping MutationEngine
GraphQLEnumMutation Transforms enum types
GraphQLEnumMemberMutation Transforms enum members
GraphQLModelMutation Transforms model types
GraphQLModelPropertyMutation Transforms model properties
GraphQLOperationMutation Transforms operations
GraphQLScalarMutation Transforms scalar types

Changes

New Files:

  • src/mutation-engine/engine.ts - Core engine wrapping MutationEngine
  • src/mutation-engine/options.ts - GraphQL-specific mutation options
  • src/mutation-engine/mutations/*.ts - 6 mutation classes for different type kinds
  • test/mutation-engine/graphql-mutation-engine.test.ts - unit tests

Usage

import { createGraphQLMutationEngine } from "@typespec/graphql";

const engine = createGraphQLMutationEngine(program, namespace);

const enumMutation = engine.mutateEnum(myEnum);
const modelMutation = engine.mutateModel(myModel);

// Access the transformed type
console.log(enumMutation.mutatedType.name);

@FionaBronwen FionaBronwen force-pushed the fionabronwen/type-utils branch 4 times, most recently from 65f55e7 to 0bc74f1 Compare January 7, 2026 19:59
@FionaBronwen FionaBronwen force-pushed the fionabronwen/mutation-engine branch 8 times, most recently from 17e8959 to e651f96 Compare January 7, 2026 22:00
@FionaBronwen FionaBronwen marked this pull request as ready for review January 7, 2026 22:04
@FionaBronwen FionaBronwen removed the request for review from swatkatz January 7, 2026 22:04
* GraphQL mutation engine that applies GraphQL-specific transformations
* to TypeSpec types, such as name sanitization.
*/
export class GraphQLMutationEngine {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand the implementation of the mutation framework, a MutationEngine should capture the mutations for one type of transformation; i.e., we should have a specific GraphQLNamingMutationEngine, and other engines that capture the various different transformations we want to run, rather than a global "GraphQL" engine.

I'm using the HttpCanonicalizer engine as the reference point here — it implements a very specific type of transformation rather than "HTTP" transformations broadly.

It also seems that SimpleMutationEngine might be sufficient for what we need here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed a bit offline, but just to share in context-- I think that the GraphQL mutations should all share an engine for a couple of reasons:

  1. We've broken the GraphQL mutations out into Individual "transformations" which is helpful for conceptualizing all of the ways the TSP schema must be altered to match GraphQL conventions. However, individual transformations aren't useful in isolation, they all need to be applied to give us the correct resulting GraphQL schema.
  2. When we make multiple transformations in the same engine we have the benefit of reusing a shared cache and performing a single traversal of the TSP graph.
  3. Using one mutation engine will simplify the consumer API when using the GraphQL Mutation Engine from within emitters.

If we find that this approach becomes unwieldy, then we could look into splitting into multiple engines. For now, I think this is the right approach! Open to discussing further if you feel very strongly!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 👍 on moving forward and seeing how it works out. I think it will be important that we set and keep guardrails about what does and doesn't go into the "GraphQL" mutation engine — e.g. only things that are backed up by some part of the GraphQL spec.

@FionaBronwen FionaBronwen force-pushed the fionabronwen/type-utils branch 3 times, most recently from 8dbe441 to f5fca8d Compare January 15, 2026 19:49
@FionaBronwen FionaBronwen force-pushed the fionabronwen/mutation-engine branch from e651f96 to 04ae5e7 Compare January 15, 2026 20:04
@FionaBronwen FionaBronwen force-pushed the fionabronwen/mutation-engine branch from 04ae5e7 to 83f1ddc Compare January 15, 2026 21:09
Comment on lines +36 to +40
this.#mutationNode.whenMutated((member) => {
if (member) {
member.name = sanitizeNameForGraphQL(member.name);
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Help me understand why we're taking this approach for enum members and model properties, but not any of the other types.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm taking this approach so that we can hook into whenMutated at construction time of these child nodes and ensure their names are sanitized before the parent edge callbacks need them!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants