[graphql] Add @typespec/graphql emitter#11000
Conversation
Add CODEOWNERS for graphql pull request reviews --------- Co-authored-by: swatikumar <swatikumar@pinterest.com>
…#5033) ### Description This PR sets up the flow to use the GraphQL emitter by providing an interface for the various options that the GraphQL emitter will use eventually. It also sets up test-hosts to work with these options. The actual schema emitter doesn't really do anything other than emit "Hello World" as it did previously, but the options get pass through as confirmed by the test case. Going forward, we can change the code in schema-emitter.ts to setup it up for GraphQL using `navigateProgram`. We need to add diagnostics in the emitter lib definition, but that can be done in a separate PR. The next PR will have the outer layer of the GraphQL emitter setup to deal with multiple schemas similar to multiple services in the OAI emitter. ### Testing Run the tests and see that they pass. --------- Co-authored-by: swatikumar <swatikumar@pinterest.com>
These are just some basic updates to the metadata in the `@typespec/graphql` `package.json`. This brings it in line with other packages like `@typespec/openapi3`.
The `@compose` decorator is used to indicate that a GraphQL `type` or `interface` implements one or more interfaces. As [defined by the GraphQL spec](https://spec.graphql.org/October2021/#sec-Interfaces), a `type` or `interface` implementing an interface must contain all the properties defined by that interface, and so we require that the TypeSpec model implement the interface's properties as well. There is no restriction on how this is accomplished (via spread, via composition, via manual copying of properties, etc). To reduce confusion, we do not allow a TypeSpec model to define both a `type` and an `interface`. In order to define an `interface`, the model must be decorated with the `@Interface` decorator.
The `@operationFields` decorator is used to specify one or more operations that should be placed onto a GraphQL type as fields with arguments. This is our solution for representing [GraphQL field arguments](https://spec.graphql.org/October2021/#sec-Field-Arguments) in TypeSpec, as TypeSpec does not support arguments on model properties.
Implement `@Interface` and `@compose` decorators
Implement `@operationFields` decorator
Import useStateMap from compiler utils (after merge)
A few updates to make the project buildable again: - `implements` became a reserved keyword in 9a4463b, so we switch to `interfaces` - `expectIdenticalTypes` was renamed to `expectTypeEquals` in 32ca22f - `validateDecoratorTarget` was removed in 32ca22f, as the TypeSpec type system handles this - `SyntaxKind` was moved to `compiler/ast` in 32ca22f - we also bump version of `devDependencies` to match those in other projects
Add main.tsp and remove strict from tspconfig.yml
…ation Fix interface prefix, @compose diagnostics, and @operationFields input warning
Implements TypeSpec lifecycle visibility for the GraphQL emitter: - Query params accept Read/Query-visible fields - Mutation params accept Create/Update-visible fields - Output types include Read-visible fields only - @invisible properties excluded from all contexts - @compose stripped from input variants (prevents spurious validation) When a model is used by both @query and @mutation operations and the visibility filters produce different property sets, two input types are emitted (e.g., UserQueryInput + UserMutationInput). Otherwise a single UserInput suffices. Uses the compiler's isVisible() + VisibilityFilter API. Pre-computes inputOperationVariance in type-usage to drive naming decisions cleanly through the naming pipeline (no post-hoc renames). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add visibility filtering with operation-kind-aware input splitting
…nput Three crash categories fixed: 1. Record<T> fields — the mutation engine replaces Record models with custom scalars (e.g., RecordOfString), but the replacement was never added to the type graph. Fix: pushMutatedModel helper detects replaced mutation nodes and pushes the replacement type. 2. Nested generics — template instantiations like PagedResponse<Post> referenced transitively (as array element types in BatchResult<Post>) were never registered. Fix: buildTypeGraph now recursively discovers all types referenced by model properties and operation signatures, producing a self-contained graph. 3. Union-as-input — unions used as mutation parameters need @OneOf input object conversion. The union handler now also mutates in Input context when type-usage indicates input usage, registering the resulting model. buildTypeGraph rewrite: instead of a flat "put these in a bag" function, it now transitively walks property types, return types, and parameters to register everything the renderer will need to resolve. Skips std scalars and GraphQL built-ins (String, Int, Float, Boolean, ID). Sets isFinished directly (required by navigateTypesInNamespace) without calling finishType (which would re-invoke decorators). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix emitter crashes for Record types, nested generics, and union-as-input
9 schemas testing 50+ TypeSpec/GraphQL patterns from the test matrix. Emits SDL output files for visual inspection alongside automated assertions. Schemas: - 01-core: operations, models, scalars, enums, interfaces, unions, nullability, spread, extends, deprecation, circular refs, input/output split, Records - 02-generics: template models, nested, recursive, generic input - 03-visibility: read-only exclusion, create-only, query/mutation split - 04-records: Record<string>, Record<Model>, Record<unknown>, Record<never> - 05-union-input: union as mutation param → @OneOf - 06-descriptions: @doc on types/fields/params, #deprecated - 07-opfields: @operationFields with visibility and input exclusion - 08-gaps: optional+nullable, constrained generic - 09-nested-empty: known crash edge case (API-5280) Known bugs documented in TEST_COVERAGE.md: - API-5278: Record scalars duplicated per context - API-5279: extends doesn't flatten base model fields - API-5280: crash on nested empty visibility-filtered model Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add comprehensive e2e manual validation test suite
GraphQL has no type inheritance for object/input types, so models using `extends` must have all inherited fields flattened into the child type. Fix: after super.mutate() completes (which traverses baseModel), copy the base model's mutated properties into the child and clear baseModel. Own properties take precedence over inherited ones. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- graphql-emitter: Architecture guide for the GraphQL emitter covering two-phase design, key files, TypeGraph contract, mutation patterns, renderer decisions. Includes reference docs (pipeline design, renderer architecture, transformation inventory, e2e testing strategy). - fix-graphql-bug: End-to-end workflow for fixing bugs from Jira backlog (typespec_graphql_emitter label). Fetches open bugs, loads context via graphql-emitter skill, implements with TDD, runs tests, creates PR, links back to Jira. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts: # pnpm-lock.yaml
… into feature/graphql
The link path was `../../alloy/packages/graphql` which resolved relative to node_modules/@alloy-js/ (4 levels up), landing in typespec/alloy/ which doesn't exist. The alloy repo is a sibling of typespec, so the correct path is `../../../alloy/packages/graphql`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Flatten base model fields into child during GraphQL mutation
When all properties of a model are invisible under a visibility filter (e.g., all @visibility(Lifecycle.Read) used in input context), replace the model with a scalar instead of producing an empty type that crashes the renderer. The pre-check runs before super.mutate(), same timing as the Record replacement, ensuring the scalar propagates through half-edges to any parent that references the type. Fixes API-5280. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…scalar Replace visibility-filtered empty models with scalars
…103) Record<T> types were incorrectly getting separate scalar names in input vs output contexts (e.g., RecordOfString and RecordOfStringInput). Since Records are opaque map types with no structural difference between input/output usage, they should share a single scalar name. Fix: When a Record is replaced with a scalar in model.ts, skip the Input suffix since Records are context-independent. Visibility-filtered scalars still get the suffix as they represent genuinely different variants. TypeScript 6.0 compat: Iterator.next().done now returns boolean | undefined, fixed with ?? false coercion.
- Remove unimplemented 'strict' emitter option - Add CHANGELOG.md with initial release notes - Add tsconfig.json project references for monorepo builds - Expand README with comprehensive decorator documentation
- Add composite: true to emitter-framework tsconfig for project references - Add type narrowing in visibility test to fix TypeScript error
| /** Generate a GraphQL type name for a templated model (e.g., `ListOfString`). */ | ||
| export function getTemplatedModelName(model: Model): string { | ||
| const name = getTypeName(model, {}); | ||
| const baseName = toTypeName(name.replace(/<[^>]*>/g, "")); |
| "strict": true, | ||
| "skipLibCheck": true, | ||
| "isolatedModules": true, | ||
| "composite": true, |
There was a problem hiding this comment.
I think because this needs alloy to build it can't really be composite right?
| ###################### | ||
| # GraphQL | ||
| ###################### | ||
| /packages/graphql/ @steverice @swatkatz @fionabronwen @bterlson @markcowl @allenjzhang @timotheeguerin |
There was a problem hiding this comment.
Last time we talked I think codeowners couldn't be from outside the organization so we'll have to figure out what we want to do here.
| * } | ||
| * ``` | ||
| */ | ||
| extern dec Interface(target: Model, options?: valueof {interfaceOnly?: boolean}); |
There was a problem hiding this comment.
was this meant to be captialized?
| * union types. The decorator's presence on the type's `decorators` array is | ||
| * the signal — the implementation is a no-op. | ||
| */ | ||
| extern dec nullable(target: ModelProperty | Operation | Union | Model); |
There was a problem hiding this comment.
we have the concept of auto decorators comming soon, is that something that would help simplify the implementation of some of your decorators? #10197
| * - **Operation**: return type `T | null` | ||
| * - **Union**: named unions like `Cat | Dog | null` (safe — new unique object) | ||
| */ | ||
| export function isNullable(type: Type): boolean { |
There was a problem hiding this comment.
The way those decorators are implemented kinda goes against the philosphy of those decorators(shouldn't really have to check the .decorators list unless there is a feature gap in the compiler)
Any reason to do that over using the stateMap/stateSet(and potentially as mentioned in another comment the upcomming auto decorators?)
| @@ -0,0 +1,188 @@ | |||
| import { createTypeSpecLibrary, paramMessage, type JSONSchemaType } from "@typespec/compiler"; | |||
|
|
|||
| export const NAMESPACE = "TypeSpec.GraphQL"; | |||
There was a problem hiding this comment.
you don't need to export(and I would recommend against now) the namespace, the $decorators just takes care of it.
| @@ -0,0 +1,5 @@ | |||
| export { $onEmit } from "./emitter.js"; | |||
| export { $lib } from "./lib.js"; | |||
There was a problem hiding this comment.
I see the $lib contains a few error diagnostics but you don't have any onValidate. Ideally we'd like to have emitters try to not emit erors. In your care this is of course both a library providing protocol bindings and an emitter but it would be ideal if the protocol validation was able to be done in the $onValidate. This would ensure that the spec is checked regardless of if the emitter is run
| schema: { description: "State for the @schema decorator." }, | ||
| specifiedBy: { description: "State for the @specifiedBy decorator." }, | ||
| }, | ||
| } as const; |
There was a problem hiding this comment.
we also have the concept of a dry-run compatible emitter, this is a flag you can set in the config to say that if --dry-run is passed your emitter can be run and it will respect this flag(not write anything but still report the diagnostics)
If that is possible with the architecture of the emitter we should do that. (Doesn't also need to be done right now)
There was a problem hiding this comment.
we should also add the emitter to the website
- Revert ci.yml change that added feature/* to PR triggers - Add documentation to SchemaOptions model and name property - Add clarifying comment for regex in type-utils.ts (addresses false-positive HTML injection warning from security bot)
Remove mcp-server-typespec-docs package and .mcp.json config file as they are unrelated to the GraphQL emitter.
a1fe28e to
8ab9cf6
Compare
Summary
This PR introduces
@typespec/graphql, a new emitter that generates GraphQL SDL (Schema Definition Language) from TypeSpec definitions.Features
@query,@mutation, and@subscriptiondecorators@Interfaceand@composefor GraphQL interface inheritancepatterns
@visibilitydecorators@specifiedBydecorator for custom scalar URL specifications@schemadecorator for multi-schema scenariosArchitecture
The emitter uses a two-phase approach:
@alloy-js/graphqlcomponents to emit the final SDL outputExample
Generates:
Test plan
Notes