Skip to content

Add mutation engine for TypeSpec-to-GraphQL type transformation#62

Open
FionaBronwen wants to merge 16 commits intofeature/graphqlfrom
fionabronwen/graphql-mutation-engine-v2
Open

Add mutation engine for TypeSpec-to-GraphQL type transformation#62
FionaBronwen wants to merge 16 commits intofeature/graphqlfrom
fionabronwen/graphql-mutation-engine-v2

Conversation

@FionaBronwen
Copy link

@FionaBronwen FionaBronwen commented Feb 23, 2026

This PR introduces a mutation engine that bridges the gap between TypeSpec's type system and
GraphQL's naming/structural constraints. It builds on @typespec/mutator-framework to apply
GraphQL-specific transformations to TypeSpec types before they're emitted as SDL.

What this does

TypeSpec and GraphQL have different conventions and rules around type naming and structure. For
example:

  • TypeSpec uses camelCase for properties, GraphQL enum values use SCREAMING_SNAKE_CASE
  • TypeSpec allows unions of scalars, but GraphQL unions can only contain object types
  • TypeSpec template instantiations like List<string> need readable names like ListOfString

The mutation engine handles these transformations by defining per-type mutation classes that hook
into the mutator framework's traversal:

  • Enum/EnumMember — Sanitizes names to PascalCase (types) and SCREAMING_SNAKE_CASE (values)
  • Model/ModelProperty — Sanitizes model and property names for GraphQL compatibility
  • Operation — Normalizes operation names; automatically propagates input context to parameters and
    output context to return types
  • Scalar — Maps TypeSpec scalars to their GraphQL equivalents via a scalar mapping table
  • Union — Detects nullable unions (T | null) and flattens them; wraps scalar variants in synthetic
    object types (since GraphQL unions only allow object members)

Input/output type context splitting

GraphQL distinguishes between input types (used in operation arguments) and output types (used in
return values). The mutation engine handles this via GraphQLMutationOptions, which overrides the
framework's mutationKey to produce separate cached mutations per context ("input" vs "output").

When an operation is mutated, GraphQLOperationMutation overrides mutateParameters() and
mutateReturnType() to inject the appropriate context. The framework's built-in options propagation
carries that context to all nested types automatically.

Also included

  • src/lib/type-utils.ts — Shared utilities for name sanitization (toTypeName, toFieldName,
    sanitizeNameForGraphQL), nullable union detection, template name generation, and splitWithAcronyms
    for handling acronyms in casing conversions
  • src/lib/scalar-mappings.ts — Complete mapping table from TypeSpec scalars to GraphQL scalar types
    (including int64BigInt, float32Float, custom scalar specs, etc.)
  • 56 tests across type-utils (35) and mutation engine (21)
  • Package hygiene: tspMain, node engine bump to >=20, api-extractor.json, consistent casing in test
    helpers

Testing

npx vitest run

@FionaBronwen FionaBronwen force-pushed the fionabronwen/graphql-mutation-engine-v2 branch from 0dda7d3 to 4964137 Compare February 23, 2026 21:45
@FionaBronwen FionaBronwen force-pushed the fionabronwen/graphql-package-hygiene branch from 0f7b9e1 to 139f961 Compare February 23, 2026 21:45
@FionaBronwen FionaBronwen changed the base branch from fionabronwen/graphql-package-hygiene to feature/graphql February 25, 2026 21:27
@FionaBronwen FionaBronwen force-pushed the fionabronwen/graphql-mutation-engine-v2 branch 5 times, most recently from a05a236 to 79ea773 Compare February 26, 2026 16:15
@FionaBronwen FionaBronwen marked this pull request as ready for review February 26, 2026 16:17
@FionaBronwen FionaBronwen removed the request for review from swatkatz February 26, 2026 16:17
Comment on lines +47 to +52
/**
* Get the wrapper models that were created for scalar variants
*/
get wrapperModels() {
return this.#wrapperModels;
}

Choose a reason for hiding this comment

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

What's this used for?

Copy link
Author

@FionaBronwen FionaBronwen Mar 12, 2026

Choose a reason for hiding this comment

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

The emitter uses wrapperModels to register the synthetic wrapper types (e.g. TextUnionVariant { value: String! }) in the schema alongside the union. Without this, the wrapper objects would be
created but never emitted. But like I mentioned below, it sounds like the GraphQL Spec doesn't want us to do this 😅 I'll come up with a better appraoch.

const isScalar = variant.type.kind === "Scalar" || variant.type.kind === "Intrinsic";

if (isScalar) {
// Create a synthetic wrapper model for this scalar variant

Choose a reason for hiding this comment

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

Did we say in the design doc we'd support this this way? I can see it being very surprising that this be the result (and not in a good way). Maybe it needs to be configurable.

Copy link
Author

Choose a reason for hiding this comment

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

yep! In he union section, it proposes putting wrapper types on scalars since all union member in GraphQL must be models. Although, after looking at the GraphQL spec I see that "The member types of a Union type must all be Object base types; Scalar, Interface and Union types must not be member types of a Union. Similarly, wrapping types must not be member types of a Union." Soooo, maybe we need to rethink this.

FionaBronwen and others added 14 commits March 13, 2026 14:28
## Summary

Adds utility functions for transforming TypeSpec names into valid GraphQL identifiers. These utilities form the foundation for name handling throughout the GraphQL emitter.

## Changes

- **`src/lib/type-utils.ts`** - Core utility functions for GraphQL name transformations
- **`test/lib/type-utils.test.ts`** - Unit tests for `sanitizeNameForGraphQL`

## Utilities Added

| Function | Purpose |
|----------|---------|
| `sanitizeNameForGraphQL` | Sanitize names to be valid GraphQL identifiers |
| `toTypeName` | Convert to PascalCase for type names |
| `toFieldName` | Convert to camelCase for field names |
| `toEnumMemberName` | Convert to CONSTANT_CASE for enum members |
| `getUnionName` | Generate names for anonymous unions |
| `getTemplatedModelName` | Generate names for templated models (e.g., `ListOfString`) |
| `isArray`, `isRecordType` | Type guards for array/record models |
| `unwrapModel`, `unwrapType` | Extract element types from arrays |
| `isTrueModel` | Check if a model should emit as GraphQL object type |
| `getGraphQLDoc` | Extract doc comments for GraphQL descriptions |
Introduce a mutation engine that transforms TypeSpec types into
GraphQL-compatible forms using the mutator framework. Includes mutations
for enums, models, scalars, unions, and operations.

Also includes package hygiene: add tspMain, update node engine to >=20,
add api-extractor.json, CHANGELOG.md, fix testing casing, and clean up
dead code.
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.
Co-authored-by: Steve Rice <srice@pinterest.com>
Co-authored-by: Steve Rice <srice@pinterest.com>
Co-authored-by: Steve Rice <srice@pinterest.com>
Co-authored-by: Steve Rice <srice@pinterest.com>
Co-authored-by: Steve Rice <srice@pinterest.com>
Use scalars.graphql.org hosted specs per review feedback:
- PlainDate → andimarek/local-date
- PlainTime → apollographql/localtime-v0.1
- BigDecimal (decimal, decimal128) → chillicream/decimal
@FionaBronwen FionaBronwen force-pushed the fionabronwen/graphql-mutation-engine-v2 branch from 70e7589 to 84b7764 Compare March 13, 2026 18:53
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.

2 participants