From 873685136fb3bc3152b62720bdd2d4a15eb1a3dc Mon Sep 17 00:00:00 2001 From: Fiona Date: Wed, 4 Jun 2025 16:42:57 -0400 Subject: [PATCH 1/5] Add TypeMap abstract class --- packages/graphql/src/type-maps.ts | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 packages/graphql/src/type-maps.ts diff --git a/packages/graphql/src/type-maps.ts b/packages/graphql/src/type-maps.ts new file mode 100644 index 00000000000..a8783380657 --- /dev/null +++ b/packages/graphql/src/type-maps.ts @@ -0,0 +1,92 @@ +import { UsageFlags } from "@typespec/compiler"; + +/** + * TypeSpec context for type mapping + * @template T - The TypeSpec type + */ +export interface TSPContext { + type: T; // The TypeSpec type + usageFlag: UsageFlags; // How the type is being used (input, output, etc.) + name?: string; // Optional name override + metadata?: Record; // Optional additional metadata +} + +/** + * Base TypeMap for all GraphQL type mappings + * @template T - The TypeSpec type + * @template G - The GraphQL type + */ +export abstract class TypeMap { + // Map of materialized GraphQL types + protected materializedMap = new Map(); + + // Map of registration contexts + protected registrationMap = new Map>(); + + /** + * Register a TypeSpec type with context for later materialization + * @param context - The TypeSpec context + * @returns The name used for registration + */ + register(context: TSPContext): 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 all materialized types + */ + getAllMaterialized(): G[] { + return Array.from(this.materializedMap.values()); + } + + /** + * Reset the type map + */ + reset(): void { + this.materializedMap.clear(); + this.registrationMap.clear(); + } + + /** + * Get a name from a context + */ + protected abstract getNameFromContext(context: TSPContext): string; + + /** + * Materialize a type from a context + */ + protected abstract materialize(context: TSPContext): G | undefined; +} From 5361f3968440e73ab5de23c31e4c4d1f261afb6e Mon Sep 17 00:00:00 2001 From: Fiona Date: Thu, 5 Jun 2025 12:07:16 -0400 Subject: [PATCH 2/5] Add type constraints and nominal type for map key --- packages/graphql/src/type-maps.ts | 37 ++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/graphql/src/type-maps.ts b/packages/graphql/src/type-maps.ts index a8783380657..adff6465635 100644 --- a/packages/graphql/src/type-maps.ts +++ b/packages/graphql/src/type-maps.ts @@ -1,34 +1,41 @@ -import { UsageFlags } from "@typespec/compiler"; +import { UsageFlags, type Type } from "@typespec/compiler"; +import type { GraphQLType } from "graphql"; /** * TypeSpec context for type mapping * @template T - The TypeSpec type */ -export interface TSPContext { +export interface TSPContext { type: T; // The TypeSpec type usageFlag: UsageFlags; // How the type is being used (input, output, etc.) name?: string; // Optional name override - metadata?: Record; // Optional additional metadata + metadata: Record; // Additional metadata } +/** + * Nominal type for keys in the TypeMap + */ +type TypeKey = string & { __typeKey: any }; + /** * Base TypeMap for all GraphQL type mappings - * @template T - The TypeSpec type - * @template G - The GraphQL type + * @template T - The TypeSpec type constrained to TSP's Type + * @template G - The GraphQL type constrained to GraphQL's GraphQLType */ -export abstract class TypeMap { +export abstract class TypeMap { + // Map of materialized GraphQL types - protected materializedMap = new Map(); + protected materializedMap = new Map(); // Map of registration contexts - protected registrationMap = new Map>(); + protected registrationMap = new Map>(); /** * Register a TypeSpec type with context for later materialization * @param context - The TypeSpec context - * @returns The name used for registration + * @returns The name used for registration as a TypeKey */ - register(context: TSPContext): string { + register(context: TSPContext): TypeKey { const name = this.getNameFromContext(context); this.registrationMap.set(name, context); return name; @@ -36,10 +43,10 @@ export abstract class TypeMap { /** * Get the materialized GraphQL type - * @param name - The type name + * @param name - The type name as a TypeKey * @returns The materialized GraphQL type or undefined */ - get(name: string): G | undefined { + get(name: TypeKey): G | undefined { // Return already materialized type if available if (this.materializedMap.has(name)) { return this.materializedMap.get(name); @@ -62,7 +69,7 @@ export abstract class TypeMap { * Check if a type is registered */ isRegistered(name: string): boolean { - return this.registrationMap.has(name); + return this.registrationMap.has(name as TypeKey); } /** @@ -83,10 +90,10 @@ export abstract class TypeMap { /** * Get a name from a context */ - protected abstract getNameFromContext(context: TSPContext): string; + protected abstract getNameFromContext(context: TSPContext): TypeKey; /** * Materialize a type from a context */ - protected abstract materialize(context: TSPContext): G | undefined; + protected abstract materialize(context: TSPContext): G; } From 69715a96e7f7d7e98400735d2397db5515e8edc1 Mon Sep 17 00:00:00 2001 From: Fiona Date: Thu, 5 Jun 2025 16:01:26 -0400 Subject: [PATCH 3/5] Fix naming, use MapIterator --- packages/graphql/src/type-maps.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/graphql/src/type-maps.ts b/packages/graphql/src/type-maps.ts index adff6465635..14382080d63 100644 --- a/packages/graphql/src/type-maps.ts +++ b/packages/graphql/src/type-maps.ts @@ -8,7 +8,7 @@ import type { GraphQLType } from "graphql"; export interface TSPContext { type: T; // The TypeSpec type usageFlag: UsageFlags; // How the type is being used (input, output, etc.) - name?: string; // Optional name override + graphqlName?: string; // Optional GraphQL type name override (e.g., "ModelInput" for input types) metadata: Record; // Additional metadata } @@ -75,8 +75,8 @@ export abstract class TypeMap { /** * Get all materialized types */ - getAllMaterialized(): G[] { - return Array.from(this.materializedMap.values()); + getAllMaterialized(): MapIterator { + return this.materializedMap.values(); } /** From 2cc3738ac1a3342fd1935fe38b971141fdef0279 Mon Sep 17 00:00:00 2001 From: Fiona Date: Thu, 5 Jun 2025 17:44:23 -0400 Subject: [PATCH 4/5] remove unnecessary null check --- packages/graphql/src/type-maps.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/graphql/src/type-maps.ts b/packages/graphql/src/type-maps.ts index 14382080d63..e78c4edb7b5 100644 --- a/packages/graphql/src/type-maps.ts +++ b/packages/graphql/src/type-maps.ts @@ -56,10 +56,8 @@ export abstract class TypeMap { const context = this.registrationMap.get(name); if (context) { const materializedType = this.materialize(context); - if (materializedType) { - this.materializedMap.set(name, materializedType); - return materializedType; - } + this.materializedMap.set(name, materializedType); + return materializedType; } return undefined; From 7a061748932d0c98781fe43f338da643501d9804 Mon Sep 17 00:00:00 2001 From: Fiona Date: Thu, 5 Jun 2025 18:02:40 -0400 Subject: [PATCH 5/5] Check if a type is already registered before registering --- packages/graphql/src/type-maps.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/graphql/src/type-maps.ts b/packages/graphql/src/type-maps.ts index e78c4edb7b5..a1555f7dfb1 100644 --- a/packages/graphql/src/type-maps.ts +++ b/packages/graphql/src/type-maps.ts @@ -36,7 +36,13 @@ export abstract class TypeMap { * @returns The name used for registration as a TypeKey */ register(context: TSPContext): TypeKey { + // Check if the type is already registered const name = this.getNameFromContext(context); + if (this.isRegistered(name)) { + throw new Error(`Type ${name} is already registered`); + } + + // Register the type this.registrationMap.set(name, context); return name; }