forked from microsoft/typespec
-
Notifications
You must be signed in to change notification settings - Fork 1
Add GraphQL mutation engine for TSP type transformations #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
FionaBronwen
wants to merge
1
commit into
fionabronwen/type-utils
Choose a base branch
from
fionabronwen/mutation-engine
base: fionabronwen/type-utils
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| export { $onEmit } from "./emitter.js"; | ||
| export { $lib } from "./lib.js"; | ||
| export { $decorators } from "./tsp-index.js"; | ||
|
|
||
| export { createGraphQLMutationEngine } from "./mutation-engine/index.js"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import { | ||
| type Enum, | ||
| type Model, | ||
| type Namespace, | ||
| type Operation, | ||
| type Program, | ||
| type Scalar, | ||
| } from "@typespec/compiler"; | ||
| import { $ } from "@typespec/compiler/typekit"; | ||
| import { | ||
| MutationEngine, | ||
| SimpleInterfaceMutation, | ||
| SimpleIntrinsicMutation, | ||
| SimpleLiteralMutation, | ||
| SimpleUnionMutation, | ||
| SimpleUnionVariantMutation, | ||
| } from "@typespec/mutator-framework"; | ||
| import { | ||
| GraphQLEnumMemberMutation, | ||
| GraphQLEnumMutation, | ||
| GraphQLModelMutation, | ||
| GraphQLModelPropertyMutation, | ||
| GraphQLOperationMutation, | ||
| GraphQLScalarMutation, | ||
| } from "./mutations/index.js"; | ||
| import { GraphQLMutationOptions } from "./options.js"; | ||
|
|
||
| /** | ||
| * Registry configuration for the GraphQL mutation engine. | ||
| * Maps TypeSpec type kinds to their corresponding GraphQL mutation classes. | ||
| */ | ||
| const graphqlMutationRegistry = { | ||
| // Custom GraphQL mutations for types we need to transform | ||
| Enum: GraphQLEnumMutation, | ||
| EnumMember: GraphQLEnumMemberMutation, | ||
| Model: GraphQLModelMutation, | ||
| ModelProperty: GraphQLModelPropertyMutation, | ||
| Operation: GraphQLOperationMutation, | ||
| Scalar: GraphQLScalarMutation, | ||
| // Use Simple* classes from mutator-framework for types we don't customize | ||
| Interface: SimpleInterfaceMutation, | ||
| Union: SimpleUnionMutation, | ||
| UnionVariant: SimpleUnionVariantMutation, | ||
| String: SimpleLiteralMutation, | ||
| Number: SimpleLiteralMutation, | ||
| Boolean: SimpleLiteralMutation, | ||
| Intrinsic: SimpleIntrinsicMutation, | ||
| }; | ||
|
|
||
| /** | ||
| * GraphQL mutation engine that applies GraphQL-specific transformations | ||
| * to TypeSpec types, such as name sanitization. | ||
| */ | ||
| export class GraphQLMutationEngine { | ||
| /** | ||
| * The underlying mutation engine configured with GraphQL-specific mutation classes. | ||
| * Type is inferred from graphqlMutationRegistry to avoid complex generic constraints. | ||
| */ | ||
| private engine; | ||
|
|
||
| constructor(program: Program, _namespace: Namespace) { | ||
| const tk = $(program); | ||
| this.engine = new MutationEngine(tk, graphqlMutationRegistry); | ||
| } | ||
|
|
||
| /** | ||
| * Mutate a model, applying GraphQL name sanitization. | ||
| */ | ||
| mutateModel(model: Model): GraphQLModelMutation { | ||
| return this.engine.mutate(model, new GraphQLMutationOptions()) as GraphQLModelMutation; | ||
| } | ||
|
|
||
| /** | ||
| * Mutate an enum, applying GraphQL name sanitization. | ||
| */ | ||
| mutateEnum(enumType: Enum): GraphQLEnumMutation { | ||
| return this.engine.mutate(enumType, new GraphQLMutationOptions()) as GraphQLEnumMutation; | ||
| } | ||
|
|
||
| /** | ||
| * Mutate an operation, applying GraphQL name sanitization. | ||
| */ | ||
| mutateOperation(operation: Operation): GraphQLOperationMutation { | ||
| return this.engine.mutate(operation, new GraphQLMutationOptions()) as GraphQLOperationMutation; | ||
| } | ||
|
|
||
| /** | ||
| * Mutate a scalar, applying GraphQL name sanitization. | ||
| */ | ||
| mutateScalar(scalar: Scalar): GraphQLScalarMutation { | ||
| return this.engine.mutate(scalar, new GraphQLMutationOptions()) as GraphQLScalarMutation; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates a GraphQL mutation engine for the given program and namespace. | ||
| */ | ||
| export function createGraphQLMutationEngine( | ||
| program: Program, | ||
| namespace: Namespace, | ||
| ): GraphQLMutationEngine { | ||
| return new GraphQLMutationEngine(program, namespace); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export { GraphQLMutationEngine, createGraphQLMutationEngine } from "./engine.js"; | ||
| export { | ||
| GraphQLEnumMemberMutation, | ||
| GraphQLEnumMutation, | ||
| GraphQLModelMutation, | ||
| GraphQLModelPropertyMutation, | ||
| GraphQLOperationMutation, | ||
| GraphQLScalarMutation, | ||
| } from "./mutations/index.js"; | ||
| export { GraphQLMutationOptions } from "./options.js"; |
56 changes: 56 additions & 0 deletions
56
packages/graphql/src/mutation-engine/mutations/enum-member.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import type { EnumMember, MemberType } from "@typespec/compiler"; | ||
| import { | ||
| EnumMemberMutation, | ||
| EnumMemberMutationNode, | ||
| MutationEngine, | ||
| type MutationInfo, | ||
| type MutationOptions, | ||
| } from "@typespec/mutator-framework"; | ||
| import { sanitizeNameForGraphQL } from "../../lib/type-utils.js"; | ||
|
|
||
| /** | ||
| * GraphQL-specific EnumMember mutation. | ||
| */ | ||
| export class GraphQLEnumMemberMutation extends EnumMemberMutation< | ||
| MutationOptions, | ||
| any, | ||
| MutationEngine<any> | ||
| > { | ||
| #mutationNode: EnumMemberMutationNode; | ||
|
|
||
| constructor( | ||
| engine: MutationEngine<any>, | ||
| sourceType: EnumMember, | ||
| referenceTypes: MemberType[], | ||
| options: MutationOptions, | ||
| info: MutationInfo, | ||
| ) { | ||
| super(engine, sourceType, referenceTypes, options, info); | ||
| this.#mutationNode = this.engine.getMutationNode(this.sourceType, { | ||
| mutationKey: info.mutationKey, | ||
| isSynthetic: info.isSynthetic, | ||
| }) as EnumMemberMutationNode; | ||
| // Register rename callback BEFORE any edge connections trigger mutation. | ||
| // whenMutated fires when the node is mutated (even via edge propagation), | ||
| // ensuring the name is sanitized before edge callbacks read it. | ||
| this.#mutationNode.whenMutated((member) => { | ||
| if (member) { | ||
| member.name = sanitizeNameForGraphQL(member.name); | ||
| } | ||
| }); | ||
|
Comment on lines
+36
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
|
||
| get mutationNode() { | ||
| return this.#mutationNode; | ||
| } | ||
|
|
||
| get mutatedType() { | ||
| return this.#mutationNode.mutatedType; | ||
| } | ||
|
|
||
| mutate() { | ||
| // Trigger mutation if not already mutated (whenMutated callback will run) | ||
| this.#mutationNode.mutate(); | ||
| super.mutate(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import type { Enum, MemberType } from "@typespec/compiler"; | ||
| import { | ||
| EnumMemberMutationNode, | ||
| EnumMutation, | ||
| EnumMutationNode, | ||
| MutationEngine, | ||
| MutationHalfEdge, | ||
| type MutationInfo, | ||
| type MutationOptions, | ||
| } from "@typespec/mutator-framework"; | ||
| import { sanitizeNameForGraphQL } from "../../lib/type-utils.js"; | ||
| import type { GraphQLEnumMemberMutation } from "./enum-member.js"; | ||
|
|
||
| /** | ||
| * GraphQL-specific Enum mutation. | ||
| */ | ||
| export class GraphQLEnumMutation extends EnumMutation<MutationOptions, any, MutationEngine<any>> { | ||
| #mutationNode: EnumMutationNode; | ||
|
|
||
| constructor( | ||
| engine: MutationEngine<any>, | ||
| sourceType: Enum, | ||
| referenceTypes: MemberType[], | ||
| options: MutationOptions, | ||
| info: MutationInfo, | ||
| ) { | ||
| super(engine, sourceType, referenceTypes, options, info); | ||
| this.#mutationNode = this.engine.getMutationNode(this.sourceType, { | ||
| mutationKey: info.mutationKey, | ||
| isSynthetic: info.isSynthetic, | ||
| }) as EnumMutationNode; | ||
| } | ||
|
|
||
| get mutationNode() { | ||
| return this.#mutationNode; | ||
| } | ||
|
|
||
| get mutatedType() { | ||
| return this.#mutationNode.mutatedType; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a MutationHalfEdge that wraps the node-level edge. | ||
| * This ensures proper bidirectional updates when members are renamed. | ||
| */ | ||
| protected startMemberEdge(): MutationHalfEdge<GraphQLEnumMutation, GraphQLEnumMemberMutation> { | ||
| return new MutationHalfEdge("member", this, (tail) => { | ||
| this.#mutationNode.connectMember(tail.mutationNode as EnumMemberMutationNode); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Override to pass half-edge for proper bidirectional updates. | ||
| */ | ||
| protected override mutateMembers() { | ||
| for (const member of this.sourceType.members.values()) { | ||
| this.members.set( | ||
| member.name, | ||
| this.engine.mutate(member, this.options, this.startMemberEdge()), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| mutate() { | ||
| // Apply GraphQL name sanitization via callback | ||
| this.#mutationNode.mutate((enumType) => { | ||
| enumType.name = sanitizeNameForGraphQL(enumType.name); | ||
| }); | ||
| // Handle member mutations with proper edges | ||
| this.mutateMembers(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export { GraphQLEnumMutation } from "./enum.js"; | ||
| export { GraphQLEnumMemberMutation } from "./enum-member.js"; | ||
| export { GraphQLModelMutation } from "./model.js"; | ||
| export { GraphQLModelPropertyMutation } from "./model-property.js"; | ||
| export { GraphQLOperationMutation } from "./operation.js"; | ||
| export { GraphQLScalarMutation } from "./scalar.js"; | ||
|
|
36 changes: 36 additions & 0 deletions
36
packages/graphql/src/mutation-engine/mutations/model-property.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import type { MemberType, ModelProperty } from "@typespec/compiler"; | ||
| import { | ||
| SimpleModelPropertyMutation, | ||
| type MutationInfo, | ||
| type SimpleMutationEngine, | ||
| type SimpleMutationOptions, | ||
| type SimpleMutations, | ||
| } from "@typespec/mutator-framework"; | ||
| import { sanitizeNameForGraphQL } from "../../lib/type-utils.js"; | ||
|
|
||
| /** GraphQL-specific ModelProperty mutation. */ | ||
| export class GraphQLModelPropertyMutation extends SimpleModelPropertyMutation<SimpleMutationOptions> { | ||
| constructor( | ||
| engine: SimpleMutationEngine<SimpleMutations<SimpleMutationOptions>>, | ||
| sourceType: ModelProperty, | ||
| referenceTypes: MemberType[], | ||
| options: SimpleMutationOptions, | ||
| info: MutationInfo, | ||
| ) { | ||
| super(engine as any, sourceType, referenceTypes, options, info); | ||
| // Register rename callback BEFORE any edge connections trigger mutation. | ||
| // whenMutated fires when the node is mutated (even via edge propagation), | ||
| // ensuring the name is sanitized before edge callbacks read it. | ||
| this.mutationNode.whenMutated((property) => { | ||
| if (property) { | ||
| property.name = sanitizeNameForGraphQL(property.name); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| mutate() { | ||
| // Trigger mutation if not already mutated (whenMutated callback will run) | ||
| this.mutationNode.mutate(); | ||
| super.mutate(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import type { MemberType, Model } from "@typespec/compiler"; | ||
| import { | ||
| SimpleModelMutation, | ||
| type MutationInfo, | ||
| type SimpleMutationEngine, | ||
| type SimpleMutationOptions, | ||
| type SimpleMutations, | ||
| } from "@typespec/mutator-framework"; | ||
| import { sanitizeNameForGraphQL } from "../../lib/type-utils.js"; | ||
|
|
||
| /** | ||
| * GraphQL-specific Model mutation. | ||
| */ | ||
| export class GraphQLModelMutation extends SimpleModelMutation<SimpleMutationOptions> { | ||
| constructor( | ||
| engine: SimpleMutationEngine<SimpleMutations<SimpleMutationOptions>>, | ||
| sourceType: Model, | ||
| referenceTypes: MemberType[], | ||
| options: SimpleMutationOptions, | ||
| info: MutationInfo, | ||
| ) { | ||
| super(engine as any, sourceType, referenceTypes, options, info); | ||
| } | ||
|
|
||
| mutate() { | ||
| // Apply GraphQL name sanitization | ||
| this.mutationNode.mutate((model) => { | ||
| model.name = sanitizeNameForGraphQL(model.name); | ||
| }); | ||
| super.mutate(); | ||
| } | ||
| } |
30 changes: 30 additions & 0 deletions
30
packages/graphql/src/mutation-engine/mutations/operation.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import type { MemberType, Operation } from "@typespec/compiler"; | ||
| import { | ||
| SimpleOperationMutation, | ||
| type MutationInfo, | ||
| type SimpleMutationEngine, | ||
| type SimpleMutationOptions, | ||
| type SimpleMutations, | ||
| } from "@typespec/mutator-framework"; | ||
| import { sanitizeNameForGraphQL } from "../../lib/type-utils.js"; | ||
|
|
||
| /** GraphQL-specific Operation mutation. */ | ||
| export class GraphQLOperationMutation extends SimpleOperationMutation<SimpleMutationOptions> { | ||
| constructor( | ||
| engine: SimpleMutationEngine<SimpleMutations<SimpleMutationOptions>>, | ||
| sourceType: Operation, | ||
| referenceTypes: MemberType[], | ||
| options: SimpleMutationOptions, | ||
| info: MutationInfo, | ||
| ) { | ||
| super(engine as any, sourceType, referenceTypes, options, info); | ||
| } | ||
|
|
||
| mutate() { | ||
| // Apply GraphQL name sanitization via callback | ||
| this.mutationNode.mutate((operation) => { | ||
| operation.name = sanitizeNameForGraphQL(operation.name); | ||
| }); | ||
| super.mutate(); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
MutationEngineshould capture the mutations for one type of transformation; i.e., we should have a specificGraphQLNamingMutationEngine, and other engines that capture the various different transformations we want to run, rather than a global "GraphQL" engine.I'm using the
HttpCanonicalizerengine as the reference point here — it implements a very specific type of transformation rather than "HTTP" transformations broadly.It also seems that
SimpleMutationEnginemight be sufficient for what we need here.There was a problem hiding this comment.
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:
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!
There was a problem hiding this comment.
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.