Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .chronus/changes/auto-decorator-setter-2026-7-14-11-0-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Add `setAutoDecorator` API to programmatically apply an `auto` decorator to a target, mirroring what the synthesized `auto dec` implementation does when the decorator is written in source. This lets emitters and mutators mark synthetic types without reaching into the program state map directly.

```ts
import { setAutoDecorator } from "@typespec/compiler";

setAutoDecorator(program, "MyLib.myFlag", target);
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

`createTester` now mounts each discovered library's `tspconfig.yaml` into the virtual file system, so experimental features a library opts into (e.g. `auto-decorators`) are honored when compiling against the tester.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/tspd"
---

`tspd gen-extern-signature` now also generates a typed setter (e.g. `setMyFlag`, `setMyLabel`) for each `auto` decorator, alongside the existing `is*`/`get*` readers.
220 changes: 220 additions & 0 deletions packages/graphql/generated-defs/TypeSpec.GraphQL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {
type DecoratorContext,
type DecoratorValidatorCallbacks,
hasAutoDecorator,
type Interface,
type Model,
type ModelProperty,
type Namespace,
type Operation,
type Program,
type Scalar,
setAutoDecorator,
type Union,
} from "@typespec/compiler";

export interface SchemaOptions {
readonly name?: string;
}

/**
* Mark this model as a GraphQL Interface. Interfaces can be implemented by other models
* using the `@compose` decorator.
*
* @param options .interfaceOnly When true, the model will only be emitted as an interface
* (no "Interface" suffix is added to the name). Use this for abstract interfaces that
* will never be used directly as output/input types (e.g., Node, Connection). Defaults to false.
* @example
* ```typespec
* @graphqlInterface(#{ interfaceOnly: true })
* model Node {
* id: string;
* }
*
* @compose(Node)
* model User {
* ...Node;
* name: string;
* }
* // Emits: interface Node { id: String! }
* // type User implements Node { id: String!; name: String! }
* ```
*/
export type GraphqlInterfaceDecorator = (
context: DecoratorContext,
target: Model,
options?: {
readonly interfaceOnly?: boolean;
},
) => DecoratorValidatorCallbacks | void;

/**
* Specify the GraphQL interfaces that should be implemented by a model.
* The interfaces must be decorated with the `@graphqlInterface` decorator,
* and all of the interfaces' properties must be present and compatible.
*
* @example
* ```typespec
* @graphqlInterface(#{ interfaceOnly: true })
* model Node {
* id: string;
* }
*
* @compose(Node)
* model User {
* ...Node;
* name: string;
* }
* ```
*/
export type ComposeDecorator = (
context: DecoratorContext,
target: Model,
...interfaces: Model[]
) => DecoratorValidatorCallbacks | void;

/**
* Assign one or more operations or interfaces to act as fields with arguments on a model.
* The operations become fields on the GraphQL type with their parameters as arguments.
*
* @example
* ```typespec
* op followers(query: string): Person[];
*
* @operationFields(followers)
* model Person {
* name: string;
* }
* // Emits: type Person { name: String!; followers(query: String!): [Person!]! }
* ```
*/
export type OperationFieldsDecorator = (
context: DecoratorContext,
target: Model,
...operations: (Operation | Interface)[]
) => DecoratorValidatorCallbacks | void;

/**
* Specify the GraphQL Operation kind for the target operation to be `MUTATION`.
*
* @example
* ```typespec
* @mutation op createUser(name: string): User;
* ```
*/
export type MutationDecorator = (
context: DecoratorContext,
target: Operation,
) => DecoratorValidatorCallbacks | void;

/**
* Specify the GraphQL Operation kind for the target operation to be `QUERY`.
*
* @example
* ```typespec
* @query op getUser(id: string): User;
* ```
*/
export type QueryDecorator = (
context: DecoratorContext,
target: Operation,
) => DecoratorValidatorCallbacks | void;

/**
* Specify the GraphQL Operation kind for the target operation to be `SUBSCRIPTION`.
*
* @example
* ```typespec
* @subscription op onUserCreated(): User;
* ```
*/
export type SubscriptionDecorator = (
context: DecoratorContext,
target: Operation,
) => DecoratorValidatorCallbacks | void;

/**
* Mark this namespace as describing a GraphQL schema and configure schema properties.
* All types and operations within the namespace will be emitted to a single GraphQL schema file.
*
* @example
* ```typespec
* @schema(#{ name: "MyAPI" })
* namespace MyAPI {
* model User { id: string; name: string; }
* @query op getUser(id: string): User;
* }
* // Emits: MyAPI.graphql
* ```
*/
export type SchemaDecorator = (
context: DecoratorContext,
target: Namespace,
options?: SchemaOptions,
) => DecoratorValidatorCallbacks | void;

/**
* Provide a specification URL for a custom GraphQL scalar type.
* This maps to the `@specifiedBy` directive in the emitted GraphQL schema.
*
* @param url URL to the scalar type specification
* @example
* ```typespec
* @specifiedBy("https://scalars.graphql.org/andimarek/date-time")
* scalar DateTime extends utcDateTime;
* ```
*/
export type SpecifiedByDecorator = (
context: DecoratorContext,
target: Scalar,
url: string,
) => DecoratorValidatorCallbacks | void;

export type TypeSpecGraphQLDecorators = {
graphqlInterface: GraphqlInterfaceDecorator;
compose: ComposeDecorator;
operationFields: OperationFieldsDecorator;
mutation: MutationDecorator;
query: QueryDecorator;
subscription: SubscriptionDecorator;
schema: SchemaDecorator;
specifiedBy: SpecifiedByDecorator;
};

export function isInputType(program: Program, target: Model): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target);
}

export function setInputType(program: Program, target: Model): void {
setAutoDecorator(program, "TypeSpec.GraphQL.inputType", target);
}

export function isNullable(
program: Program,
target: ModelProperty | Operation | Union | Model,
): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.nullable", target);
}

export function setNullable(
program: Program,
target: ModelProperty | Operation | Union | Model,
): void {
setAutoDecorator(program, "TypeSpec.GraphQL.nullable", target);
}

export function isNullableElements(program: Program, target: ModelProperty | Operation): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target);
}

export function setNullableElements(program: Program, target: ModelProperty | Operation): void {
setAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target);
}

export function isOneOf(program: Program, target: Model): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target);
}

export function setOneOf(program: Program, target: Model): void {
setAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target);
}
10 changes: 10 additions & 0 deletions packages/graphql/generated-defs/TypeSpec.GraphQL.ts-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// An error in the imports would mean that the decorator is not exported or
// doesn't have the right name.

import { $decorators } from "@typespec/graphql";
import type { TypeSpecGraphQLDecorators } from "./TypeSpec.GraphQL.js";

/**
* An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ...
*/
const _decs: TypeSpecGraphQLDecorators = $decorators["TypeSpec.GraphQL"];
12 changes: 12 additions & 0 deletions packages/graphql/lib/input-type.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using TypeSpec.Reflection;

namespace TypeSpec.GraphQL;

/**
* Mark a model as a GraphQL input type in the emitted schema.
*
* This decorator is applied automatically by the mutation engine when it produces
* a model that is used in input position. The emitter uses this to emit the model
* as an `input` type rather than an object `type`.
*/
internal auto dec inputType(target: Model);
1 change: 1 addition & 0 deletions packages/graphql/lib/main.tsp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "../dist/src/tsp-index.js";
import "./interface.tsp";
import "./input-type.tsp";
import "./nullable.tsp";
import "./one-of.tsp";
import "./operation-fields.tsp";
Expand Down
7 changes: 3 additions & 4 deletions packages/graphql/lib/nullable.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ namespace TypeSpec.GraphQL;
* Mark a field, operation, or type as nullable in the emitted GraphQL schema.
*
* Applied automatically by the mutation engine when it strips `| null` from
* union types. The decorator's presence on the type's `decorators` array is
* the signal — the implementation is a no-op.
* union types, and can also be applied directly in TypeSpec source.
*/
internal extern dec nullable(target: ModelProperty | Operation | Union | Model);
internal auto dec nullable(target: ModelProperty | Operation | Union | Model);

/**
* Mark a field or operation as having nullable array elements in the emitted GraphQL schema.
*
* Applied automatically by the mutation engine when it detects `Array<T | null>`
* patterns. Causes the emitter to emit `[T]` instead of `[T!]`.
*/
internal extern dec nullableElements(target: ModelProperty | Operation);
internal auto dec nullableElements(target: ModelProperty | Operation);
2 changes: 1 addition & 1 deletion packages/graphql/lib/one-of.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ namespace TypeSpec.GraphQL;
*
* @see https://spec.graphql.org/September2025/#sec-OneOf-Input-Objects
*/
internal extern dec oneOf(target: Model);
internal auto dec oneOf(target: Model);
4 changes: 3 additions & 1 deletion packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
},
"scripts": {
"clean": "rimraf ./dist ./temp",
"build": "alloy build",
"build": "pnpm gen-extern-signature && alloy build",
"watch": "alloy build --watch",
"gen-extern-signature": "tspd --enable-experimental gen-extern-signature .",
"test": "vitest run",
"test:watch": "vitest -w",
"lint": "oxlint . --deny-warnings",
Expand All @@ -48,6 +49,7 @@
},
"files": [
"lib/*.tsp",
"tspconfig.yaml",
"dist/**",
"!dist/test/**"
],
Expand Down
6 changes: 3 additions & 3 deletions packages/graphql/src/components/fields/field.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as gql from "@pinterest/alloy-graphql";
import { type ModelProperty, getDeprecationDetails, isArrayModelType } from "@typespec/compiler";
import { useTsp } from "@typespec/emitter-framework";
import { isNullable, isNullableElements } from "../../../generated-defs/TypeSpec.GraphQL.js";
import { resolveGraphQLTypeName } from "../../lib/graphql-type-name.js";
import { hasNullableElements, isNullable } from "../../lib/nullable.js";

export interface FieldProps {
property: ModelProperty;
Expand All @@ -14,11 +14,11 @@ export function Field(props: FieldProps) {

const doc = $.type.getDoc(props.property);
const deprecation = getDeprecationDetails(program, props.property);
const nullable = isNullable(props.property) || props.property.optional;
const nullable = isNullable(program, props.property) || props.property.optional;
const type = props.property.type;

if (type.kind === "Model" && isArrayModelType(type)) {
const elemNullable = hasNullableElements(props.property);
const elemNullable = isNullableElements(program, props.property);
const typeName = resolveGraphQLTypeName(type.indexer.value, program);

if (props.isInput) {
Expand Down
10 changes: 5 additions & 5 deletions packages/graphql/src/components/fields/operation-field.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as gql from "@pinterest/alloy-graphql";
import { type Operation, getDeprecationDetails, isArrayModelType } from "@typespec/compiler";
import { useTsp } from "@typespec/emitter-framework";
import { isNullable, isNullableElements } from "../../../generated-defs/TypeSpec.GraphQL.js";
import { resolveGraphQLTypeName } from "../../lib/graphql-type-name.js";
import { hasNullableElements, isNullable } from "../../lib/nullable.js";

export interface OperationFieldProps {
operation: Operation;
Expand All @@ -14,14 +14,14 @@ export function OperationField(props: OperationFieldProps) {
const doc = $.type.getDoc(props.operation);
const deprecation = getDeprecationDetails(program, props.operation);
const returnType = props.operation.returnType;
const nullable = isNullable(props.operation);
const nullable = isNullable(program, props.operation);
const params = Array.from(props.operation.parameters.properties.values());

const isList = returnType.kind === "Model" && isArrayModelType(returnType);
const typeName = isList
? resolveGraphQLTypeName(returnType.indexer.value, program)
: resolveGraphQLTypeName(returnType, program);
const elemNullable = isList && hasNullableElements(props.operation);
const elemNullable = isList && isNullableElements(program, props.operation);

return (
<gql.Field
Expand All @@ -33,10 +33,10 @@ export function OperationField(props: OperationFieldProps) {
>
{isList ? <gql.Field.List nonNull={!nullable} /> : undefined}
{params.map((param) => {
const paramNullable = isNullable(param) || param.optional;
const paramNullable = isNullable(program, param) || param.optional;
const paramType = param.type;
const paramIsList = paramType.kind === "Model" && isArrayModelType(paramType);
const paramElemNullable = paramIsList && hasNullableElements(param);
const paramElemNullable = paramIsList && isNullableElements(program, param);
const paramTypeName = paramIsList
? resolveGraphQLTypeName(paramType.indexer.value, program)
: resolveGraphQLTypeName(paramType, program);
Expand Down
Loading