[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
…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, "")); |
| * - **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?)
There was a problem hiding this comment.
The @nullable decorator is applied programmatically by the mutation engine after stripping | null variants from unions. (In GraphQL, unions don't have null variants, they just become null themselves.) We check .decorators rather than stateMap because we need the state to travel with cloned types. stateMap entries point to the original type objects and would be lost after cloning during mutation. Keeping this information alongside the cloned type via a decorator, enables mutation engine chaining where each mutation phase can work on the transformed output of the previous one and still contain the nullability context.
Since auto decorators #10197 use stateMap I think we'd run into the same issue. Happy to discuss if there's a better pattern that still allows us to chain together mutation stages without auxiliary data to pass around!
| @@ -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
There was a problem hiding this comment.
Added $onValidate with the validations that can run before the mutation engine:
empty-schema- no@query/@mutation/@subscriptionoperationsempty-enum- enum with no valuesempty-union- union with no non-null variantsreserved-name- names starting with__
The remaining diagnostics (type-name-collision, ,
duplicate-union-variant, unrecognized-union) have to stay in $onEmit because they depend on transformations the mutation engine performs (name resolution, union flattening, etc.) which I think would be too heavyweight to run $onValidate.
- 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
Replace the local alloy/packages/graphql dependency with the new @pinterest/alloy-graphql package. Update @alloy-js/core to 0.23.1 in the workspace catalog to match alloy-graphql's peer dependency.
TypeSpec uses backtick escaping for decorators that conflict with reserved keywords (e.g., @`package`, @`scenario`). The @interface decorator was incorrectly using PascalCase to avoid the `interface` keyword conflict. This change renames @interface to @`interface` to follow the established TypeSpec convention for keyword-escaping. Changes: - lib/interface.tsp: extern dec `interface` - src/lib/interface.ts: $interface - src/tsp-index.ts: interface: $interface - Updated all tests, docs, and error messages
Remove NAMESPACE export from lib.ts and individual namespace exports from decorator files. Register all decorators via the $decorators export in tsp-index.ts. Update .tsp files to import from tsp-index.js instead of individual decorator JS files.
Demonstrates queries, mutations, object types, enums, and nullable returns for the @typespec/graphql emitter.
- Add guide.md with usage documentation for the GraphQL emitter - Generate reference docs using tspd doc tool - Add GraphQL to sidebar under Emitters section with preview badge - Add regen-docs script and tspd dev dependency to graphql package
70b7061 to
fc4d2e7
Compare
External contributors cannot be codeowners in the Microsoft org.
Allows running validation without writing output files when using --dry-run flag.
Implement $onValidate to report the empty-schema diagnostic before the emitter runs, providing immediate IDE feedback when a schema has no @query, @mutation, or @subscription operations. - Only validates namespaces with @Schema decorator - Removes duplicate diagnostic from emitter.tsx (keeps skip-generation) - Adds @query operations to test namespaces to avoid spurious warnings
21d5a98 to
e9dad03
Compare
Add early validations per GraphQL spec:
- empty-enum: Enums must define at least one value
- empty-union: Unions must have at least one non-null member
- reserved-name: Names must not begin with "__" (reserved for introspection)
The reserved-name check validates:
- Type names (models, enums, unions)
- Field/property names
- Operation parameter names
- Enum member names
The empty-union check catches unions that will be empty after null-stripping
(e.g., `union Foo { none: null }`) before the mutation engine runs.
Spec references:
- https://spec.graphql.org/September2025/#sec-Enums
- https://spec.graphql.org/September2025/#sec-Unions
- https://spec.graphql.org/September2025/#sec-Names.Reserved-Names
e9dad03 to
954e220
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