Skip to content
Open
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
72 changes: 72 additions & 0 deletions packages/graphql/.claude/follow-up-items.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# GraphQL Emitter Follow-up Items

## ~~Input Type Nullability Bug~~ (RESOLVED)

**Status**: Fixed in PR #77 (commit ea85714d2)

**Original issue**: Optional fields in input types were being made non-null.

**Resolution**: Per the GraphQL spec, "nullability directly determines whether a field is required." Optional fields (`?`) should be nullable in both input and output contexts. This also enables circular references in input types (e.g., `Author.friend?: Author` → `friend: AuthorInput` is valid because it's nullable).

**Correct behavior** (now implemented):

| TypeSpec | Output | Input |
|--------------------|----------|----------|
| a: string | String! | String! |
| b?: string | String | String |
| c: string \| null | String | String |

---

## @oneOf Input Union Bug (Priority: Medium)

**Issue**: Unions used in input context (e.g., mutation parameters) should be converted to `@oneOf` input objects per the GraphQL spec, but the emitter crashes with "Unknown GraphQL type" when attempting this.

**Test case that exposes the bug**:
```typespec
@schema
namespace TestNamespace {
model TextContent {
text: string;
}

model ImageContent {
url: string;
}

union Content {
text: TextContent,
image: ImageContent,
}

model Post {
id: string;
content: Content;
}

@query
op getPost(id: string): Post;

@mutation
op createPost(content: Content): Post;
}
```

**Error**:
```
Error: Unknown GraphQL type "ContentInput".
at resolveNamedType (src/schema/build/types.ts:100:11)
```

**Root cause**: The `GraphQLUnionMutation.mutateAsOneOfInput()` correctly creates the `@oneOf` input model (e.g., `ContentInput`), but this synthetic model is not being registered in the type registry that `buildArgsMap` uses to resolve argument types.

**Expected behavior**:
- In output context: `union Content = TextContent | ImageContent`
- In input context: `input ContentInput @oneOf { text: TextContentInput, image: ImageContentInput }`

**Files to investigate**:
- `src/mutation-engine/mutations/union.ts` - `mutateAsOneOfInput()` creates the @oneOf model
- `src/schema/build/types.ts` - where the type registry is built and `resolveNamedType` fails
- `src/mutation-engine/schema-mutator.ts` - may need to register synthetic types

**Related**: Similar bug exists for complex input types used in `@operationFields` arguments (the `FilterOptions` model isn't discovered when used only as an operation field argument type).
3 changes: 3 additions & 0 deletions packages/graphql/src/components/operations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { QueryType, type QueryTypeProps } from "./query-type.js";
export { MutationType, type MutationTypeProps } from "./mutation-type.js";
export { SubscriptionType, type SubscriptionTypeProps } from "./subscription-type.js";
27 changes: 27 additions & 0 deletions packages/graphql/src/components/operations/mutation-type.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type Operation } from "@typespec/compiler";
import * as gql from "@alloy-js/graphql";
import { OperationField } from "../fields/index.js";

export interface MutationTypeProps {
/** Mutation operations to render as fields */
operations: Operation[];
}

/**
* Renders the GraphQL Mutation root type using Alloy's Mutation component
*
* Only renders if operations exist (Mutation is optional in GraphQL)
*/
export function MutationType(props: MutationTypeProps) {
if (props.operations.length === 0) {
return null;
}

return (
<gql.Mutation>
{props.operations.map((op) => (
<OperationField operation={op} />
))}
</gql.Mutation>
);
}
27 changes: 27 additions & 0 deletions packages/graphql/src/components/operations/query-type.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type Operation } from "@typespec/compiler";
import * as gql from "@alloy-js/graphql";
import { OperationField } from "../fields/index.js";

export interface QueryTypeProps {
/** Query operations to render as fields */
operations: Operation[];
}

/**
* Renders the GraphQL Query root type using Alloy's Query component.
* Returns null if no query operations exist (the emitter will emit an
* empty-schema diagnostic in that case).
*/
export function QueryType(props: QueryTypeProps) {
if (props.operations.length === 0) {
return null;
}

return (
<gql.Query>
{props.operations.map((op) => (
<OperationField operation={op} />
))}
</gql.Query>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type Operation } from "@typespec/compiler";
import * as gql from "@alloy-js/graphql";
import { OperationField } from "../fields/index.js";

export interface SubscriptionTypeProps {
/** Subscription operations to render as fields */
operations: Operation[];
}

/**
* Renders the GraphQL Subscription root type using Alloy's Subscription component
*
* Only renders if operations exist (Subscription is optional in GraphQL)
*/
export function SubscriptionType(props: SubscriptionTypeProps) {
if (props.operations.length === 0) {
return null;
}

return (
<gql.Subscription>
{props.operations.map((op) => (
<OperationField operation={op} />
))}
</gql.Subscription>
);
}
118 changes: 118 additions & 0 deletions packages/graphql/src/components/type-collections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { For } from "@alloy-js/core";
import * as gql from "@alloy-js/graphql";
import type { Enum, Model, Scalar, Union } from "@typespec/compiler";
import type { ScalarVariant } from "../mutation-engine/index.js";
import {
ScalarType,
EnumType,
UnionType,
InterfaceType,
ObjectType,
InputType,
} from "./types/index.js";

export interface ScalarVariantTypesProps {
scalarVariants: ScalarVariant[];
scalars: Scalar[];
scalarSpecifications: Map<string, string>;
}

/**
* Renders scalar variant types for encoded scalars (e.g., bytes + base64 -> Bytes)
* AND custom user-defined scalars
*/
export function ScalarVariantTypes(props: ScalarVariantTypesProps) {
// Get set of variant names to avoid duplicates
const variantNames = new Set(props.scalarVariants.map((v) => v.graphqlName));

// Filter custom scalars to only include ones not already in variants
const customScalars = props.scalars.filter((s) => !variantNames.has(s.name));

return (
<>
<For each={props.scalarVariants}>
{(variant) => (
<gql.ScalarType
name={variant.graphqlName}
specifiedByUrl={variant.specificationUrl}
/>
)}
</For>
<For each={customScalars}>
{(scalar) => (
<ScalarType
type={scalar}
specificationUrl={props.scalarSpecifications.get(scalar.name)}
/>
)}
</For>
</>
);
}

export interface EnumTypesProps {
enums: Enum[];
}

/**
* Renders all enum types in the schema
*/
export function EnumTypes(props: EnumTypesProps) {
return (
<For each={props.enums}>{(enumType) => <EnumType type={enumType} />}</For>
);
}

export interface UnionTypesProps {
unions: Union[];
}

/**
* Renders all union types in the schema
*/
export function UnionTypes(props: UnionTypesProps) {
return (
<For each={props.unions}>{(union) => <UnionType type={union} />}</For>
);
}

export interface InterfaceTypesProps {
interfaces: Model[];
}

/**
* Renders all interface types in the schema
*/
export function InterfaceTypes(props: InterfaceTypesProps) {
return (
<For each={props.interfaces}>
{(iface) => <InterfaceType type={iface} />}
</For>
);
}

export interface ObjectTypesProps {
models: Model[];
}

/**
* Renders all object types in the schema
*/
export function ObjectTypes(props: ObjectTypesProps) {
return (
<For each={props.models}>{(model) => <ObjectType type={model} />}</For>
);
}

export interface InputTypesProps {
models: Model[];
}

/**
* Renders all input types in the schema
*/
export function InputTypes(props: InputTypesProps) {
return (
<For each={props.models}>{(model) => <InputType type={model} />}</For>
);
}
83 changes: 0 additions & 83 deletions packages/graphql/src/emitter.ts

This file was deleted.

Loading