Skip to content

Commit 06daaa6

Browse files
committed
Replace state maps with decorators for mutation engine metadata
1 parent 8b51ca4 commit 06daaa6

13 files changed

Lines changed: 166 additions & 67 deletions

File tree

packages/graphql/lib/main.tsp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import "./interface.tsp";
2+
import "./nullable.tsp";
3+
import "./one-of.tsp";
24
import "./operation-fields.tsp";
35
import "./operation-kind.tsp";
46
import "./scalars.tsp";

packages/graphql/lib/nullable.tsp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import "../dist/src/lib/nullable.js";
2+
3+
using TypeSpec.Reflection;
4+
5+
namespace TypeSpec.GraphQL;
6+
7+
/**
8+
* Mark a field, operation, or type as nullable in the emitted GraphQL schema.
9+
*
10+
* Applied automatically by the mutation engine when it strips `| null` from
11+
* union types. The decorator's presence on the type's `decorators` array is
12+
* the signal — the implementation is a no-op.
13+
*/
14+
extern dec nullable(target: ModelProperty | Operation | Union | Model);
15+
16+
/**
17+
* Mark a field as having nullable array elements in the emitted GraphQL schema.
18+
*
19+
* Applied automatically by the mutation engine when it detects `Array<T | null>`
20+
* patterns. Causes the emitter to emit `[T]` instead of `[T!]`.
21+
*/
22+
extern dec nullableElements(target: ModelProperty);

packages/graphql/lib/one-of.tsp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import "../dist/src/lib/one-of.js";
2+
3+
using TypeSpec.Reflection;
4+
5+
namespace TypeSpec.GraphQL;
6+
7+
/**
8+
* Mark a model as a `@oneOf` input object in the emitted GraphQL schema.
9+
*
10+
* This decorator is applied automatically by the mutation engine when it converts
11+
* a union type in input context to a synthetic input object (since GraphQL unions
12+
* are output-only). The emitter uses this to emit the `@oneOf` directive.
13+
*
14+
* @see https://spec.graphql.org/September2025/#sec-OneOf-Input-Objects
15+
*/
16+
extern dec oneOf(target: Model);

packages/graphql/src/components/fields/field.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export function Field(props: FieldProps) {
2222
type={props.property.type}
2323
isOptional={props.property.optional}
2424
isInput={props.isInput}
25-
isNullable={isNullable(program, props.property)}
26-
hasNullableElements={hasNullableElements(program, props.property)}
25+
isNullable={isNullable(props.property)}
26+
hasNullableElements={hasNullableElements(props.property)}
2727
targetType={props.property}
2828
>
2929
{(typeInfo) => {

packages/graphql/src/components/fields/operation-field.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function OperationField(props: OperationFieldProps) {
2323
type={props.operation.returnType}
2424
isOptional={false}
2525
isInput={false}
26-
isNullable={isNullable(program, props.operation)}
26+
isNullable={isNullable(props.operation)}
2727
targetType={props.operation}
2828
>
2929
{(returnTypeInfo) => (
@@ -46,8 +46,8 @@ export function OperationField(props: OperationFieldProps) {
4646
type={param.type}
4747
isOptional={param.optional}
4848
isInput={true}
49-
isNullable={isNullable(program, param)}
50-
hasNullableElements={hasNullableElements(program, param)}
49+
isNullable={isNullable(param)}
50+
hasNullableElements={hasNullableElements(param)}
5151
targetType={param}
5252
>
5353
{(paramTypeInfo) => (

packages/graphql/src/components/fields/type-expression.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function GraphQLTypeExpression(props: GraphQLTypeExpressionProps) {
4444
const { $, program } = useTsp();
4545
const { modelVariants } = useGraphQLSchema();
4646

47-
const nullable = props.isNullable || isNullable(program, props.type);
47+
const nullable = props.isNullable || isNullable(props.type);
4848

4949
// Input fields are non-null unless nullable; optionality is expressed via
5050
// default values. Output fields are non-null unless optional or nullable.
@@ -80,7 +80,7 @@ export function GraphQLTypeExpression(props: GraphQLTypeExpressionProps) {
8080
// element type's own state map, or from an inline T | null union still present.
8181
const elementIsNullable =
8282
props.hasNullableElements ||
83-
isNullable(program, elementType) ||
83+
isNullable(elementType) ||
8484
($.union.is(elementType) && unwrapNullableUnion(elementType) !== undefined);
8585

8686
return (

packages/graphql/src/lib.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,6 @@ export const libDef = {
174174
interface: { description: "State for the @Interface decorator." },
175175
schema: { description: "State for the @schema decorator." },
176176
specifiedBy: { description: "State for the @specifiedBy decorator." },
177-
oneOf: { description: "State for tracking @oneOf input objects created from input unions." },
178-
nullable: {
179-
description:
180-
"State for tracking types and properties marked nullable after null-variant stripping by the mutation engine.",
181-
},
182-
nullableElements: {
183-
description:
184-
"State for tracking properties whose array element type was originally T | null before mutation.",
185-
},
186177
},
187178
} as const;
188179

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,38 @@
1-
import type { Program, Type } from "@typespec/compiler";
2-
import { useStateSet } from "@typespec/compiler/utils";
3-
import { GraphQLKeys } from "../lib.js";
1+
import type {
2+
DecoratedType,
3+
DecoratorContext,
4+
DecoratorFunction,
5+
Model,
6+
ModelProperty,
7+
Operation,
8+
Type,
9+
Union,
10+
} from "@typespec/compiler";
11+
import { NAMESPACE } from "../lib.js";
412

5-
const [getNullableState, setNullableState] = useStateSet<Type>(GraphQLKeys.nullable);
13+
// This will set the namespace for decorators implemented in this file
14+
export const namespace = NAMESPACE;
15+
16+
/**
17+
* Decorator implementation for `@nullable`.
18+
*
19+
* No-op — the decorator's presence on the type's `decorators` array is the
20+
* signal. No additional state storage is needed.
21+
*/
22+
export const $nullable: DecoratorFunction = (
23+
_context: DecoratorContext,
24+
_target: ModelProperty | Operation | Union | Model,
25+
) => {};
26+
27+
/**
28+
* Decorator implementation for `@nullableElements`.
29+
*
30+
* No-op — presence on the decorators array is the signal.
31+
*/
32+
export const $nullableElements: DecoratorFunction = (
33+
_context: DecoratorContext,
34+
_target: ModelProperty,
35+
) => {};
636

737
/**
838
* Check whether a type was marked nullable after null-variant stripping.
@@ -12,30 +42,39 @@ const [getNullableState, setNullableState] = useStateSet<Type>(GraphQLKeys.nulla
1242
* - **Operation**: return type `T | null`
1343
* - **Union**: named unions like `Cat | Dog | null` (safe — new unique object)
1444
*/
15-
export function isNullable(program: Program, type: Type): boolean {
16-
return getNullableState(program, type);
45+
export function isNullable(type: Type): boolean {
46+
if (!isDecoratedType(type)) return false;
47+
return type.decorators.some((d) => d.decorator === $nullable);
1748
}
1849

19-
/** Mark a type, property, or operation as nullable. */
20-
export function setNullable(program: Program, type: Type): void {
21-
setNullableState(program, type);
50+
/**
51+
* Mark a type, property, or operation as nullable.
52+
* Called by the mutation engine when null variants are stripped.
53+
*/
54+
export function setNullable(type: Type): void {
55+
if (!isDecoratedType(type)) return;
56+
if (type.decorators.some((d) => d.decorator === $nullable)) return;
57+
type.decorators.push({ decorator: $nullable, args: [] });
2258
}
2359

24-
const [getNullableElementsState, setNullableElementsState] = useStateSet<Type>(
25-
GraphQLKeys.nullableElements,
26-
);
27-
2860
/**
2961
* Check whether a property's array elements were originally `T | null`.
3062
*
3163
* For `(string | null)[]`, marks the ModelProperty so components emit
3264
* `[String]` instead of `[String!]`.
3365
*/
34-
export function hasNullableElements(program: Program, type: Type): boolean {
35-
return getNullableElementsState(program, type);
66+
export function hasNullableElements(type: Type): boolean {
67+
if (!isDecoratedType(type)) return false;
68+
return type.decorators.some((d) => d.decorator === $nullableElements);
3669
}
3770

3871
/** Mark a property as having nullable array elements. */
39-
export function setNullableElements(program: Program, type: Type): void {
40-
setNullableElementsState(program, type);
72+
export function setNullableElements(type: Type): void {
73+
if (!isDecoratedType(type)) return;
74+
if (type.decorators.some((d) => d.decorator === $nullableElements)) return;
75+
type.decorators.push({ decorator: $nullableElements, args: [] });
76+
}
77+
78+
function isDecoratedType(type: Type): type is Type & DecoratedType {
79+
return "decorators" in type;
4180
}

packages/graphql/src/lib/one-of.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
1-
import type { Model, Program } from "@typespec/compiler";
2-
import { useStateSet } from "@typespec/compiler/utils";
3-
import { GraphQLKeys } from "../lib.js";
1+
import type { DecoratorContext, DecoratorFunction, Model } from "@typespec/compiler";
2+
import { NAMESPACE } from "../lib.js";
43

5-
const [getOneOfState, setOneOfState] = useStateSet<Model>(GraphQLKeys.oneOf);
4+
// This will set the namespace for decorators implemented in this file
5+
export const namespace = NAMESPACE;
6+
7+
/**
8+
* Decorator implementation for `@oneOf`.
9+
*
10+
* No-op — the decorator's presence on the type's `decorators` array is the
11+
* signal. No additional state storage is needed.
12+
*/
13+
export const $oneOf: DecoratorFunction = (
14+
_context: DecoratorContext,
15+
_target: Model,
16+
) => {};
617

718
/**
819
* Check if a model has been marked as a @oneOf input object.
920
* These are synthetic models created by the union mutation when a union
1021
* is used in input context — GraphQL unions are output-only, so input
1122
* unions become @oneOf input objects.
1223
*/
13-
export function isOneOf(program: Program, model: Model): boolean {
14-
return getOneOfState(program, model);
24+
export function isOneOf(model: Model): boolean {
25+
return model.decorators.some((d) => d.decorator === $oneOf);
1526
}
1627

1728
/**
1829
* Mark a model as a @oneOf input object.
1930
*/
20-
export function setOneOf(program: Program, model: Model): void {
21-
setOneOfState(program, model);
31+
export function setOneOf(model: Model): void {
32+
if (model.decorators.some((d) => d.decorator === $oneOf)) return;
33+
model.decorators.push({ decorator: $oneOf, args: [] });
2234
}

packages/graphql/src/mutation-engine/mutations/model-property.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ export class GraphQLModelPropertyMutation extends SimpleModelPropertyMutation<Si
5252
super.mutate();
5353

5454
if (isInlineNullable) {
55-
setNullable(this.engine.$.program, this.mutatedType);
55+
setNullable(this.mutatedType);
5656
}
5757
if (isArrayWithNullableElements) {
58-
setNullableElements(this.engine.$.program, this.mutatedType);
58+
setNullableElements(this.mutatedType);
5959
}
6060
}
6161
}

0 commit comments

Comments
 (0)