Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
7 changes: 1 addition & 6 deletions packages/graphql/src/graphql-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
emitFile,
getNamespaceFullName,
interpolatePath,
type EmitContext,
} from "@typespec/compiler";
import { emitFile, interpolatePath, type EmitContext } from "@typespec/compiler";
import { printSchema } from "graphql";
import type { ResolvedGraphQLEmitterOptions } from "./emitter.js";
import type { GraphQLEmitterOptions } from "./lib.js";
Expand Down
94 changes: 84 additions & 10 deletions packages/graphql/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {
GraphQLBoolean,
GraphQLEnumType,
GraphQLObjectType,
GraphQLString,
type GraphQLFieldConfigMap,
type GraphQLNamedType,
type GraphQLOutputType,
type GraphQLSchemaConfig,
} from "graphql";

Expand All @@ -15,32 +18,30 @@ interface TSPTypeContext {
usageFlags?: Set<UsageFlags>;
// TODO: Add any other TSP-specific metadata here.
}

/**
* GraphQLTypeRegistry manages the registration and materialization of TypeSpec (TSP)
* GraphQLTypeRegistry manages the registration and materialization of TypeSpec (TSP)
* types into their corresponding GraphQL type definitions.
*
* The registry operates in a two-stage process:
* 1. Registration: TSP types (like Enums, Models, etc.) are first registered
* along with relevant metadata (e.g., name, usage flags). This stores an
* intermediate representation (`TSPTypeContext`) without immediately creating
* GraphQL types. This stage is typically performed while traversing the TSP AST.
* GraphQL types. This stage is typically performed while traversing the TSP AST.
* Register type by calling the appropriate method (e.g., `addEnum`).
*
*
* 2. Materialization: When a GraphQL type is needed (e.g., to build the final
* schema or resolve a field type), the registry can materialize the TSP type
* into its GraphQL counterpart (e.g., `GraphQLEnumType`, `GraphQLObjectType`).
* into its GraphQL counterpart (e.g., `GraphQLEnumType`, `GraphQLObjectType`).
* Materialize types by calling the appropriate method (e.g., `materializeEnum`).
*
* This approach helps in:
* - Decoupling TSP AST traversal from GraphQL object instantiation.
* - Caching materialized GraphQL types to avoid redundant work and ensure object identity.
* - Handling forward references and circular dependencies, as types can be
* registered first and materialized later when all dependencies are known or
* by using thunks for fields/arguments.
* - Handling forward references and circular dependencies through per-property lazy evaluation.
*/
export class GraphQLTypeRegistry {
// Stores intermediate TSP type information, keyed by TSP type name.
// TODO: make this more of a seen set
private TSPTypeContextRegistry: Map<string, TSPTypeContext> = new Map();

// Stores materialized GraphQL types, keyed by their GraphQL name.
Expand All @@ -49,13 +50,24 @@ export class GraphQLTypeRegistry {
addEnum(tspEnum: Enum): void {
const enumName = tspEnum.name;
if (this.TSPTypeContextRegistry.has(enumName)) {
// Optionally, log a warning or update if new information is more complete.
return;
}

this.TSPTypeContextRegistry.set(enumName, {
tspType: tspEnum,
name: enumName,
});
}

addModel(tspModel: Model): void {
const modelName = tspModel.name;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can assume that tspModel.name is the "GraphQL name". There are a number of transformations that might be applied to the name.

Some of them (like use of the @friendlyName decorator, or visibility) will be resolved by the compiler (though I think we have to e.g. call getFriendlyName), but others we will be computing in the GraphQL emitter (like changing names to be GraphQL-friendly, adding Input suffixes, etc).

(forgive me, I may have asked this before about enums)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong, but I don't think the TSPTypeContextRegistry is storing the GraphQL name. But, then I must ask what is the context registry storing and why?

if (this.TSPTypeContextRegistry.has(modelName)) {
return;
}

this.TSPTypeContextRegistry.set(modelName, {
tspType: tspModel,
name: modelName,
// TODO: Populate usageFlags based on TSP context and other decorator context.
});
}
Expand All @@ -79,7 +91,7 @@ export class GraphQLTypeRegistry {
name: context.name,
values: Object.fromEntries(
Array.from(tspEnum.members.values()).map((member) => [
member.name,
member.name,
{
value: member.value ?? member.name,
},
Expand All @@ -91,9 +103,71 @@ export class GraphQLTypeRegistry {
return gqlEnum;
}

private computeModelFields(tspModel: Model): GraphQLFieldConfigMap<any, any> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I've seen elsewhere in the TypeSpec codebase (and it makes sense to me), we should use JavaScript's #private properties over TypeScript's (pseudo-)private properties.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be navigated from the model, instead this should be navigated from modelProperty on the navigateModel.

const registry = this;

const fields: GraphQLFieldConfigMap<any, any> = {};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we come up with better types for this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. I had to refactor the entire prototype code just to get proper types for this.


// Process each property of the model
for (const [propertyName, property] of tspModel.properties) {
const fieldConfig: any = {};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do better than the any type?


// Define a getter for the type property to enable lazy evaluation per field
Object.defineProperty(fieldConfig, "type", {
get: function () {
// TODO: Add proper type resolution based on the property type, default to string for now
let fieldType: GraphQLOutputType = GraphQLString;

// If the property type is a reference to another type, try to materialize it
if (property.type.kind === "Model") {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is JavaScript, and we have switch statements! 🎉

const referencedType = registry.materializeModel(property.type.name);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are creating a recursion within an existing recursion (navigateProgram) by doing this. You want to use the navigateProgram's modelProperty to collect the fields and store them to be materialized later in exitModel

if (referencedType) {
fieldType = referencedType;
}
} else if (property.type.kind === "Enum") {
const referencedType = registry.materializeEnum(property.type.name);
if (referencedType) {
fieldType = referencedType;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect we'll want a dedicated method that is able to do this sort of routing, i.e. it won't just be limited to handling model fields.


return fieldType;
},
});
fields[propertyName] = fieldConfig;
}

return fields;
}

// Materializes a TSP Model into a GraphQLObjectType.
materializeModel(modelName: string): GraphQLObjectType | undefined {
// Check if the GraphQL type is already materialized.
if (this.materializedGraphQLTypes.has(modelName)) {
return this.materializedGraphQLTypes.get(modelName) as GraphQLObjectType;
}

const context = this.TSPTypeContextRegistry.get(modelName);
if (!context || context.tspType.kind !== "Model") {
// TODO: Handle error or warning for missing context.
return undefined;
}

const tspModel = context.tspType as Model;

const gqlObjectType = new GraphQLObjectType({
name: context.name,
fields: this.computeModelFields(tspModel),
});

this.materializedGraphQLTypes.set(modelName, gqlObjectType);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect to see a similar pattern to addModel, but for materializedGraphQLTypes.

I am starting to think we want a class (a subclass of Map, perhaps) that can be used for TSPTypeContextRegistry and materializedGraphQLTypes, which maintains their internal consistency. e.g. it would:

  • have a standard "get or add" action (like addModel)
  • identify type conflicts (e.g. you're trying to add a model but the existing value is an enum)
  • do type checking (i.e. I state that I am trying to get a model, but the value it has is not a model)
  • has some kind of encapsulation of a "reset state" behavior
  • can potentially track additional metadata about the types that isn't exposed externally (e.g. how many times were they accessed / set)?

I would also search the TSP code for something similar that already exists.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 I think the prototype does this badly by having multiple top-level maps. We could have something like:

/**
 * TypeSpec context for type mapping
 * @template T - The TypeSpec type
 */
interface TSPContext<T = any> {
  type: T;                    // The TypeSpec type
  usageFlag: UsageFlags;      // How the type is being used
  name?: string;              // Optional name override
  metadata?: Record<string, any>; // Optional additional metadata
}

/**
 * Base TypeMap for all GraphQL type mappings
 * @template T - The TypeSpec type
 * @template G - The GraphQL type
 */
abstract class TypeMap<T, G> {
  // Map of materialized GraphQL types
  protected materializedMap = new Map<string, G>();
  
  // Map of registration contexts
  protected registrationMap = new Map<string, TSPContext<T>>();
  
  /**
   * Register a TypeSpec type with context for later materialization
   * @param context - The TypeSpec context
   * @returns The name used for registration
   */
  register(context: TSPContext<T>): string {
    const name = this.getNameFromContext(context);
    this.registrationMap.set(name, context);
    return name;
  }
  
  /**
   * Get the materialized GraphQL type
   * @param name - The type name
   * @returns The materialized GraphQL type or undefined
   */
  get(name: string): G | undefined {
    // Return already materialized type if available
    if (this.materializedMap.has(name)) {
      return this.materializedMap.get(name);
    }
    
    // Attempt to materialize if registered
    const context = this.registrationMap.get(name);
    if (context) {
      const materializedType = this.materialize(context);
      if (materializedType) {
        this.materializedMap.set(name, materializedType);
        return materializedType;
      }
    }
    
    return undefined;
  }
  
  /**
   * Check if a type is registered
   */
  isRegistered(name: string): boolean {
    return this.registrationMap.has(name);
  }
  
  /**
   * Get a name from a context
   */
  protected abstract getNameFromContext(context: TSPContext<T>): string;
  
  /**
   * Materialize a type from a context
   */
  protected abstract materialize(context: TSPContext<T>): G | undefined;
}

/**
 * Model field map to store thunk field configurations
 */
class ModelFieldMap {
  private fieldMap = new Map<string, ThunkFieldConfig>();
  
  /**
   * Add a field with thunk configuration
   */
  addField(
    fieldName: string, 
    type: ThunkGraphQLType,
    isOptional: boolean, 
    isList: boolean,
    args?: ThunkGraphQLFieldConfigArgumentMap
  ): void {
    this.fieldMap.set(fieldName, {
      type,
      isOptional,
      isList,
      args
    });
  }
  
  /**
   * Get all field thunk configurations
   */
  getFieldThunks(): Map<string, ThunkFieldConfig> {
    return this.fieldMap;
  }
}

/**
 * TypeMap for GraphQL Object types (output types)
 */
class ObjectTypeMap extends TypeMap<Model, GraphQLObjectType> {
  // Maps for fields by model name
  private modelFieldMaps = new Map<string, ModelFieldMap>();
  
  // For handling interfaces
  private interfacesMap = new Map<string, GraphQLInterfaceType[]>();
  
  /**
   * Get a name from a context
   */
  protected override getNameFromContext(context: TSPContext<Model>): string {
    return context.name || context.type.name || '';
  }
  
  /**
   * Register a field for a model
   */
  registerField(
    modelName: string,
    fieldName: string,
    type: ThunkGraphQLType,
    isOptional: boolean,
    isList: boolean,
    args?: ThunkGraphQLFieldConfigArgumentMap
  ): void {
    if (!this.modelFieldMaps.has(modelName)) {
      this.modelFieldMaps.set(modelName, new ModelFieldMap());
    }
    
    this.modelFieldMaps.get(modelName)!.addField(
      fieldName,
      type,
      isOptional,
      isList,
      args
    );
  }
  
  /**
   * Add an interface to a model
   */
  addInterface(modelName: string, interfaceType: GraphQLInterfaceType): void {
    if (!this.interfacesMap.has(modelName)) {
      this.interfacesMap.set(modelName, []);
    }
    this.interfacesMap.get(modelName)!.push(interfaceType);
  }
  
  /**
   * Get interfaces for a model
   */
  getInterfaces(modelName: string): GraphQLInterfaceType[] {
    return this.interfacesMap.get(modelName) || [];
  }
  
  /**
   * Materialize a GraphQL object type
   */
  protected override materialize(context: TSPContext<Model>): GraphQLObjectType | undefined {
    const modelName = this.getNameFromContext(context);
    
    return new GraphQLObjectType({
      name: modelName,
      fields: () => this.materializeFields(modelName),
      interfaces: () => this.getInterfaces(modelName)
    });
  }
  
  /**
   * Materialize fields for a model
   */
  private materializeFields(modelName: string): GraphQLFieldConfigMap<any, any> {
    const fieldMap = this.modelFieldMaps.get(modelName);
    if (!fieldMap) {
      return {};
    }
    
    const result: GraphQLFieldConfigMap<any, any> = {};
    const fieldThunks = fieldMap.getFieldThunks();
    
    fieldThunks.forEach((config, fieldName) => {
      let fieldType = config.type() as GraphQLOutputType;
      
      if (fieldType instanceof GraphQLInputObjectType) {
        throw new Error(
          `Model "${modelName}" has a field "${fieldName}" that is an input type. It should be an output type.`
        );
      }
      
      if (!config.isOptional) {
        fieldType = new GraphQLNonNull(fieldType);
      }
      
      if (config.isList) {
        fieldType = new GraphQLNonNull(new GraphQLList(fieldType));
      }
      
      result[fieldName] = {
        type: fieldType,
        args: config.args ? config.args() : undefined
      };
    });
    
    return result;
  }
}

/**
 * TypeMap for GraphQL Input types
 */
class InputTypeMap extends TypeMap<Model, GraphQLInputObjectType> {
  // Maps for fields by model name
  private modelFieldMaps = new Map<string, ModelFieldMap>();
  
  /**
   * Get a name from a context
   */
  protected override getNameFromContext(context: TSPContext<Model>): string {
    return context.name || `${context.type.name || ''}Input`;
  }
  
  /**
   * Register a field for an input model
   */
  registerField(
    modelName: string,
    fieldName: string,
    type: ThunkGraphQLType,
    isOptional: boolean,
    isList: boolean
  ): void {
    if (!this.modelFieldMaps.has(modelName)) {
      this.modelFieldMaps.set(modelName, new ModelFieldMap());
    }
    
    this.modelFieldMaps.get(modelName)!.addField(
      fieldName,
      type,
      isOptional,
      isList
    );
  }
  
  /**
   * Materialize a GraphQL input type
   */
  protected override materialize(context: TSPContext<Model>): GraphQLInputObjectType | undefined {
    const modelName = this.getNameFromContext(context);
    
    return new GraphQLInputObjectType({
      name: modelName,
      fields: () => this.materializeFields(modelName)
    });
  }
  
  /**
   * Materialize fields for an input model
   */
  private materializeFields(modelName: string): GraphQLInputFieldConfigMap {
    const fieldMap = this.modelFieldMaps.get(modelName);
    if (!fieldMap) {
      return {};
    }
    
    const result: GraphQLInputFieldConfigMap = {};
    const fieldThunks = fieldMap.getFieldThunks();
    
    fieldThunks.forEach((config, fieldName) => {
      let fieldType = config.type() as GraphQLInputType;
      
      if (!(fieldType instanceof GraphQLInputType)) {
        throw new Error(
          `Model "${modelName}" has a field "${fieldName}" that is not an input type.`
        );
      }
      
      if (!config.isOptional) {
        fieldType = new GraphQLNonNull(fieldType);
      }
      
      if (config.isList) {
        fieldType = new GraphQLNonNull(new GraphQLList(fieldType));
      }
      
      result[fieldName] = {
        type: fieldType
      };
    });
    
    return result;
  }
}

/**
 * TypeMap for GraphQL Enum types
 */
class EnumTypeMap extends TypeMap<Enum, GraphQLEnumType> {
  private sanitizeFn: (name: string) => string;
  
  constructor(sanitizeFn: (name: string) => string) {
    super();
    this.sanitizeFn = sanitizeFn;
  }
  
  /**
   * Get a name from a context
   */
  protected override getNameFromContext(context: TSPContext<Enum>): string {
    return context.name || context.type.name || '';
  }
  
  /**
   * Materialize a GraphQL enum type
   */
  protected override materialize(context: TSPContext<Enum>): GraphQLEnumType | undefined {
    const enumType = context.type;
    const name = this.getNameFromContext(context);
    
    return new GraphQLEnumType({
      name,
      values: Array.from(enumType.members.values()).reduce<{
        [key: string]: GraphQLEnumValueConfig;
      }>((acc, member) => {
        acc[this.sanitizeFn(member.name)] = {
          value: member.name,
        };
        return acc;
      }, {})
    });
  }
}
... and more ...

Then the registry itself can be made to handle/manage these maps.

export class GraphQLTypeRegistry {
  // Type name registry
  private modelTypeNames = new ModelTypeRegistry();
  
  // Type maps for different GraphQL types
  private objectTypes: ObjectTypeMap;
  private inputTypes: InputTypeMap;
  private interfaceTypes: InterfaceTypeMap;
  private enumTypes: EnumTypeMap;
  private unionTypes: UnionTypeMap;
  
  constructor() {
    // Initialize type maps with necessary dependencies
    this.objectTypes = new ObjectTypeMap();
    this.inputTypes = new InputTypeMap();
    this.interfaceTypes = new InterfaceTypeMap(this.objectTypes);
    this.enumTypes = new EnumTypeMap(this.sanitizeEnumMemberName.bind(this));
    this.unionTypes = new UnionTypeMap(this.objectTypes);
  }
  
  /**
   * Register a model with usage context
   */
  addModelUsage(model: Model, usageFlag: UsageFlags): void {
    const modelName = model.name;
    if (!modelName) return;
    
    // Register with the type name registry
    const graphqlTypeName = this.modelTypeNames.registerTypeName(modelName, usageFlag);
    
    // Create context for registration
    const context: TSPContext<Model> = {
      type: model,
      usageFlag,
      name: graphqlTypeName
    };
    
    // Register with the appropriate type map
    if (usageFlag === UsageFlags.Output) {
      this.objectTypes.register(context);
    } else if (usageFlag === UsageFlags.Input) {
      this.inputTypes.register(context);
    }
  }
  
  /**
   * Get all GraphQL type names for a model
   */
  getModelTypeNames(modelName: string): ModelTypeNames {
    return this.modelTypeNames.getModelTypeNames(modelName);
  }
  
  /**
   * Register a model property
   */
  addModelProperty(
    parentModelName: string,
    propName: string,
    type: ThunkGraphQLType,
    isOptional: boolean,
    isList: boolean,
    args?: ThunkGraphQLFieldConfigArgumentMap
  ): void {
    // Get all GraphQL type names for the model
    const typeNames = this.getModelTypeNames(parentModelName);
    
    // Add to appropriate type maps based on usage
    const outputTypeName = typeNames[UsageFlags.Output];
    if (outputTypeName) {
      this.objectTypes.registerField(
        outputTypeName,
        propName,
        type,
        isOptional,
        isList,
        args
      );
    }
    
    const inputTypeName = typeNames[UsageFlags.Input];
    if (inputTypeName) {
      this.inputTypes.registerField(
        inputTypeName,
        propName,
        type,
        isOptional,
        isList
      );
    }
  }
  
... and more ...

You can look at TSP for more examples/patterns on how to do this as well, but that would be the general idea.

Copy link
Author

@FionaBronwen FionaBronwen Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, I like that approach better! I made updates in this PR buts it's getting too lengthy for my taste. Going to break all of this into smaller PRs starting with #30. Thanks!

return gqlObjectType;
}

materializeSchemaConfig(): GraphQLSchemaConfig {
const allMaterializedGqlTypes = Array.from(this.materializedGraphQLTypes.values());
let queryType = this.materializedGraphQLTypes.get("Query") as GraphQLObjectType | undefined;

if (!queryType) {
queryType = new GraphQLObjectType({
name: "Query",
Expand Down
14 changes: 10 additions & 4 deletions packages/graphql/src/schema-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import {
createDiagnosticCollector,
ListenerFlow,
navigateTypesInNamespace,
type Diagnostic,
type DiagnosticCollector,
type EmitContext,
type Enum,
type Model,
type Namespace,
} from "@typespec/compiler";
import { GraphQLSchema, validateSchema } from "graphql";
import { type GraphQLEmitterOptions } from "./lib.js";
import type { Schema } from "./lib/schema.js";
import { GraphQLTypeRegistry } from "./registry.js";
import { exit } from "node:process";

class GraphQLSchemaEmitter {
private tspSchema: Schema;
Expand Down Expand Up @@ -52,19 +53,24 @@ class GraphQLSchemaEmitter {
}

semanticNodeListener() {
// TODO: Add GraphQL types to registry as the TSP nodes are visited
return {
namespace: (namespace: Namespace) => {
if (namespace.name === "TypeSpec" || namespace.name === "Reflection") {
return ListenerFlow.NoRecursion;
}
return;
},
enum: (node: Enum) => {
this.registry.addEnum(node);
},
model: (node: Model) => {
// Add logic to handle the model node
this.registry.addModel(node);
},
exitEnum: (node: Enum) => {
this.registry.materializeEnum(node.name);
},
exitModel: (node: Model) => {
// Add logic to handle the exit of the model node
this.registry.materializeModel(node.name);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you describe (possibly in a code comment or in the commit message) why we want to add the model on visit, but materialize it on exit?

},
};
}
Expand Down
24 changes: 20 additions & 4 deletions packages/graphql/test/emitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@ import { strictEqual } from "node:assert";
import { describe, it } from "vitest";
import { emitSingleSchema } from "./test-host.js";

// For now, the expected output is a placeholder string.
// In the future, this should be replaced with the actual GraphQL schema output.
const expectedGraphQLSchema = `type Query {
// For now, the expected output contains a placeholder string and model property types as String for scalar types that are not yet supported by the emitter.
// In the future, this should be replaced with the correct GraphQL schema output.
const expectedGraphQLSchema = `type Author {
name: String
book: Book
coauthor: Author
}

type Book {
name: String
page_count: String
published: String
price: String
author: Author
}

type Query {
"""
A placeholder field. If you are seeing this, it means no operations were defined that could be emitted.
"""
Expand All @@ -21,10 +35,12 @@ describe("name", () => {
page_count: int32;
published: boolean;
price: float64;
author: Author;
}
model Author {
name: string;
books: Book[];
book: Book;
coauthor: Author;
}
op getBooks(): Book[];
op getAuthors(): Author[];
Expand Down
40 changes: 30 additions & 10 deletions packages/graphql/test/interface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ describe("@Interface", () => {
TestModel: Model;
}>(`
@Interface
@test model TestModel {}
@test model TestModel {
name: string;
}
Comment on lines +17 to +19

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding fields to all these test models?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to fix a bunch of schema validation errors: error GraphQLSchemaValidationError: Type TestModel must define one or more fields.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. That is a real issue though, as TypeSpec has no problem with empty models.
So I think instead of changing the TSP schema, we need to handle empty TSP models in the GraphQL emitter.

`);
expectDiagnosticEmpty(diagnostics);

Expand All @@ -29,10 +31,14 @@ describe("@compose", () => {
AnInterface: Interface;
}>(`
@Interface
@test model AnInterface {}
@test model AnInterface {
prop: string;
}

@compose(AnInterface)
@test model TestModel {}
@test model TestModel {
prop: string;
}
`);
expectDiagnosticEmpty(diagnostics);

Expand All @@ -50,12 +56,18 @@ describe("@compose", () => {
SecondInterface: Interface;
}>(`
@Interface
@test model FirstInterface {}
@test model FirstInterface {
prop: string;
}
@Interface
@test model SecondInterface {}
@test model SecondInterface {
prop: string;
}

@compose(FirstInterface, SecondInterface)
@test model TestModel {}
@test model TestModel {
prop: string;
}
`);
expectDiagnosticEmpty(diagnostics);

Expand Down Expand Up @@ -87,7 +99,9 @@ describe("@compose", () => {
}

@compose(AnInterface)
model TestModel extends AnInterface {}
model TestModel extends AnInterface {
another_prop: string;
}
`);
expectDiagnosticEmpty(diagnostics);
});
Expand All @@ -99,7 +113,9 @@ describe("@compose", () => {
}

@compose(AnInterface)
model TestModel is AnInterface {}
model TestModel is AnInterface {
another_prop: string;
}
`);
expectDiagnosticEmpty(diagnostics);
});
Expand Down Expand Up @@ -158,11 +174,15 @@ describe("@compose", () => {
AnotherInterface: Interface;
}>(`
@Interface
@test model AnotherInterface {}
@test model AnotherInterface {
prop: string;
}

@compose(AnotherInterface)
@Interface
@test model AnInterface {}
@test model AnInterface {
prop: string;
}
`);
expectDiagnosticEmpty(diagnostics);

Expand Down
20 changes: 15 additions & 5 deletions packages/graphql/test/main.tsp
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import "@typespec/graphql";
using GraphQL;

@schema(#{name: "library-schema"})
@schema(#{ name: "library-schema" })
namespace MyLibrary {
model Book {
id: string;
title: string;
publicationDate: string;
author: Author;
prequel: Book;
genre: Genre;
}

model Author {
id: string;
name: string;
bio?: string;
books: Book[];
book: Book;
friend: Author;
publisher: Publisher;
}


model Publisher {
id: string;
name: string;
book: Book;
author: Author;
}

enum Genre {
Fiction,
NonFiction,
Mystery,
Fantasy
Fantasy,
}
}
}
Loading