Skip to content
Draft
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
11 changes: 7 additions & 4 deletions packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@
"node": ">=20.0.0"
},
"dependencies": {
"@alloy-js/core": "^0.11.0",
"@alloy-js/typescript": "^0.11.0",
"@alloy-js/core": "^0.22.0",
"@alloy-js/graphql": "^0.1.0",
"@alloy-js/typescript": "^0.22.0",
"change-case": "^5.4.4",
"graphql": "^16.9.0"
},
"scripts": {
"clean": "rimraf ./dist ./temp",
"build": "tsc -p .",
"watch": "tsc --watch",
"build": "alloy build",
"watch": "alloy build --watch",
"test": "vitest run",
"test:watch": "vitest -w",
"lint": "eslint . --max-warnings=0",
Expand All @@ -56,6 +57,8 @@
"@typespec/mutator-framework": "workspace:~"
},
"devDependencies": {
"@alloy-js/cli": "^0.22.0",
"@alloy-js/rollup-plugin": "^0.1.0",
"@types/node": "~22.13.13",
"@typespec/compiler": "workspace:~",
"@typespec/emitter-framework": "workspace:~",
Expand Down
76 changes: 76 additions & 0 deletions packages/graphql/src/components/fields/field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { type ModelProperty, getDoc, getDeprecationDetails } from "@typespec/compiler";
import * as gql from "@alloy-js/graphql";
import { useTsp } from "@typespec/emitter-framework";
import { GraphQLTypeResolutionContext } from "../../context/index.js";
import { isNullable, hasNullableElements } from "../../lib/nullable.js";
import { GraphQLTypeExpression } from "./type-expression.js";

export interface FieldProps {
/** The model property to render as a field */
property: ModelProperty;
/** Whether this field is in an input type context */
isInput: boolean;
}

/**
* Renders a GraphQL field (property on a type or input type)
*
* Automatically handles:
* - Description from doc comments
* - Type resolution with input/output awareness
* - Nullability based on optional flag and nullable unions
* - Array types using Field.List
* - Directives like @deprecated
*
* Uses gql.Field for output fields and gql.InputField for input fields
*/
export function Field(props: FieldProps) {
const { program } = useTsp();
const mode = props.isInput ? "input" : "output";

const doc = getDoc(program, props.property);
const deprecation = getDeprecationDetails(program, props.property);

return (
<GraphQLTypeResolutionContext.Provider value={{ mode }}>
<GraphQLTypeExpression
type={props.property.type}
isOptional={props.property.optional}
isNullable={isNullable(program, props.property)}
hasNullableElements={hasNullableElements(program, props.property)}
targetType={props.property}
>
{(typeInfo) => {
if (props.isInput) {
return (
<gql.InputField
name={props.property.name}
type={typeInfo.typeName}
nonNull={typeInfo.isList ? typeInfo.itemNonNull : typeInfo.isNonNull}
description={doc}
>
{typeInfo.isList ? (
<gql.InputField.List nonNull={typeInfo.isNonNull} />
) : undefined}
</gql.InputField>
);
}

return (
<gql.Field
name={props.property.name}
type={typeInfo.typeName}
nonNull={typeInfo.isList ? typeInfo.itemNonNull : typeInfo.isNonNull}
description={doc}
deprecated={deprecation ? deprecation.message : undefined}
>
{typeInfo.isList ? (
<gql.Field.List nonNull={typeInfo.isNonNull} />
) : undefined}
</gql.Field>
);
}}
</GraphQLTypeExpression>
</GraphQLTypeResolutionContext.Provider>
);
}
7 changes: 7 additions & 0 deletions packages/graphql/src/components/fields/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { Field, type FieldProps } from "./field.js";
export { OperationField, type OperationFieldProps } from "./operation-field.js";
export {
GraphQLTypeExpression,
type GraphQLTypeExpressionProps,
type GraphQLTypeInfo,
} from "./type-expression.js";
91 changes: 91 additions & 0 deletions packages/graphql/src/components/fields/operation-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { type Operation, getDoc, getDeprecationDetails } from "@typespec/compiler";
import * as gql from "@alloy-js/graphql";
import { useTsp } from "@typespec/emitter-framework";
import { GraphQLTypeResolutionContext } from "../../context/index.js";
import { isNullable, hasNullableElements } from "../../lib/nullable.js";
import { GraphQLTypeExpression } from "./type-expression.js";

export interface OperationFieldProps {
/** The operation to render as a field */
operation: Operation;
}

/**
* Renders an operation as a field with arguments.
*
* Used for @operationFields decorator where operations become
* fields on a type with parameters as arguments.
*
* Nullability for parameters is tracked by the mutation engine on each
* mutated ModelProperty (see nullable.ts). This component queries the
* state maps and passes the results to GraphQLTypeExpression.
*/
export function OperationField(props: OperationFieldProps) {
const { program } = useTsp();
const params = Array.from(props.operation.parameters.properties.values());
const doc = getDoc(program, props.operation);
const deprecation = getDeprecationDetails(program, props.operation);

return (
<GraphQLTypeResolutionContext.Provider value={{ mode: "output" }}>
<GraphQLTypeExpression
type={props.operation.returnType}
isOptional={false}
isNullable={isNullable(program, props.operation)}
targetType={props.operation}
>
{(returnTypeInfo) => (
<gql.Field
name={props.operation.name}
type={returnTypeInfo.typeName}
nonNull={
returnTypeInfo.isList
? returnTypeInfo.itemNonNull
: returnTypeInfo.isNonNull
}
description={doc}
deprecated={deprecation ? deprecation.message : undefined}
>
{returnTypeInfo.isList ? (
<gql.Field.List nonNull={returnTypeInfo.isNonNull} />
) : undefined}
<GraphQLTypeResolutionContext.Provider value={{ mode: "input" }}>
{params.map((param) => (
<GraphQLTypeExpression
key={param.name}
type={param.type}
isOptional={param.optional}
isNullable={isNullable(program, param)}
hasNullableElements={hasNullableElements(program, param)}
targetType={param}
>
{(paramTypeInfo) => (
<gql.InputValue
name={param.name}
type={paramTypeInfo.typeName}
nonNull={
paramTypeInfo.isList
? paramTypeInfo.itemNonNull
: paramTypeInfo.isNonNull
}
description={getDoc(program, param)}
deprecated={
getDeprecationDetails(program, param)?.message
}
>
{paramTypeInfo.isList ? (
<gql.InputValue.List
nonNull={paramTypeInfo.isNonNull}
/>
) : undefined}
</gql.InputValue>
)}
</GraphQLTypeExpression>
))}
</GraphQLTypeResolutionContext.Provider>
</gql.Field>
)}
</GraphQLTypeExpression>
</GraphQLTypeResolutionContext.Provider>
);
}
Loading