From eafe3b3a10f260eeaf6833cc53929a5b412e81f5 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 8 May 2026 16:05:40 -0700 Subject: [PATCH 1/4] Generate per-interface class maps in schema.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For each GraphQL interface with exported class implementors, Grats now generates an exported typename-to-class map in schema.ts. This enables type-safe dispatch patterns like the Node spec's `node()` resolver without manual switch statements — TypeScript enforces that all implementors provide the required static methods. Also updates the production-app example to use the generated `nodeClassMap` and the resolveTypeViaClass integration test to demonstrate the full pattern including functional @gqlField on interfaces. --- examples/apollo-server/schema.ts | 3 + examples/express-graphql-http/schema.ts | 3 + examples/next-js/schema.ts | 3 + examples/production-app/graphql/Node.ts | 18 +--- examples/production-app/models/Like.ts | 4 + examples/production-app/models/Post.ts | 4 + examples/production-app/models/User.ts | 4 + examples/production-app/schema.ts | 11 ++- .../strict-semantic-nullability/schema.ts | 3 + examples/yoga/schema.ts | 3 + src/Extractor.ts | 24 +++-- src/codegen/schemaCodegen.ts | 82 ++++++++++++++--- ...nticNonNullMatchesInterface.ts.expected.md | 4 + ...finitionImplementsInterface.ts.expected.md | 4 + ...mplementsMultipleInterfaces.ts.expected.md | 7 ++ .../PropertySignatureTypename.ts.expected.md | 4 + .../resolveTypeViaClass/index.ts | 77 +++------------- .../resolveTypeViaClass/index.ts.expected.md | 92 +++++-------------- .../resolveTypeViaClass/models.ts | 60 ++++++++++++ .../resolveTypeViaClass/schema.ts | 53 ++++++----- src/tests/test.ts | 13 ++- 21 files changed, 269 insertions(+), 207 deletions(-) create mode 100644 src/tests/integrationFixtures/resolveTypeViaClass/models.ts diff --git a/examples/apollo-server/schema.ts b/examples/apollo-server/schema.ts index 02f6597b..bf343493 100644 --- a/examples/apollo-server/schema.ts +++ b/examples/apollo-server/schema.ts @@ -107,3 +107,6 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const iPersonClassMap = { + User: UserClass +}; diff --git a/examples/express-graphql-http/schema.ts b/examples/express-graphql-http/schema.ts index ea09bc5e..4ebce2af 100644 --- a/examples/express-graphql-http/schema.ts +++ b/examples/express-graphql-http/schema.ts @@ -107,3 +107,6 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const iPersonClassMap = { + User: UserClass +}; diff --git a/examples/next-js/schema.ts b/examples/next-js/schema.ts index 7d607739..71ec5e51 100644 --- a/examples/next-js/schema.ts +++ b/examples/next-js/schema.ts @@ -106,3 +106,6 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const iPersonClassMap = { + User: UserClass +}; diff --git a/examples/production-app/graphql/Node.ts b/examples/production-app/graphql/Node.ts index 602dccf4..5f5aae1e 100644 --- a/examples/production-app/graphql/Node.ts +++ b/examples/production-app/graphql/Node.ts @@ -1,6 +1,7 @@ import { fromGlobalId, toGlobalId } from "graphql-relay"; import { ID } from "grats"; import { VC } from "../ViewerContext.js"; +import { nodeClassMap } from "../schema.js"; /** * Converts a globally unique ID into a local ID asserting @@ -42,20 +43,11 @@ export async function node( vc: VC, ): Promise { const { type, id } = fromGlobalId(args.id); - - // Note: Every type which implements `Node` must be represented here, and - // there's not currently any static way to enforce that. This is a potential - // source of bugs. - switch (type) { - case "User": - return vc.getUserById(id); - case "Post": - return vc.getPostById(id); - case "Like": - return vc.getLikeById(id); - default: - throw new Error(`Unknown typename: ${type}`); + const cls = nodeClassMap[type as keyof typeof nodeClassMap]; + if (cls == null) { + throw new Error(`Type "${type}" does not implement Node`); } + return cls.fetchById(vc, id); } /** diff --git a/examples/production-app/models/Like.ts b/examples/production-app/models/Like.ts index 4f7ae6cd..d1272337 100644 --- a/examples/production-app/models/Like.ts +++ b/examples/production-app/models/Like.ts @@ -13,6 +13,10 @@ import { Post } from "./Post.js"; export class Like extends Model implements GraphQLNode { __typename = "Like" as const; + static async fetchById(vc: VC, id: string): Promise { + return vc.getLikeById(id); + } + /** * The date and time at which the post was liked. * @gqlField */ diff --git a/examples/production-app/models/Post.ts b/examples/production-app/models/Post.ts index 50d1711b..ae479dbc 100644 --- a/examples/production-app/models/Post.ts +++ b/examples/production-app/models/Post.ts @@ -14,6 +14,10 @@ import { connectionFromSelectOrCount } from "../graphql/gqlUtils.js"; export class Post extends Model implements GraphQLNode { __typename = "Post" as const; + static async fetchById(vc: VC, id: string): Promise { + return vc.getPostById(id); + } + /** * The editor-approved title of the post. * @gqlField */ diff --git a/examples/production-app/models/User.ts b/examples/production-app/models/User.ts index eee087c3..8bdcb46a 100644 --- a/examples/production-app/models/User.ts +++ b/examples/production-app/models/User.ts @@ -11,6 +11,10 @@ import { connectionFromSelectOrCount } from "../graphql/gqlUtils.js"; export class User extends Model implements GraphQLNode { __typename = "User" as const; + static async fetchById(vc: VC, id: string): Promise { + return vc.getUserById(id); + } + /** * User's name. **Note:** This field is not guaranteed to be unique. * @gqlField */ diff --git a/examples/production-app/schema.ts b/examples/production-app/schema.ts index b77fe4fb..a9548ebc 100644 --- a/examples/production-app/schema.ts +++ b/examples/production-app/schema.ts @@ -12,9 +12,9 @@ import { nodes as likeConnectionNodesResolver, likes as queryLikesResolver, post import { getVc } from "./ViewerContext.js"; import { nodes as userConnectionNodesResolver, users as queryUsersResolver } from "./models/UserConnection.js"; import { Viewer as queryViewerResolver } from "./models/Viewer.js"; -import { createLike as mutationCreateLikeResolver } from "./models/Like.js"; -import { createPost as mutationCreatePostResolver } from "./models/Post.js"; -import { createUser as mutationCreateUserResolver } from "./models/User.js"; +import { createLike as mutationCreateLikeResolver, Like as LikeClass } from "./models/Like.js"; +import { createPost as mutationCreatePostResolver, Post as PostClass } from "./models/Post.js"; +import { createUser as mutationCreateUserResolver, User as UserClass } from "./models/User.js"; export type SchemaConfig = { scalars: { Date: GqlScalar; @@ -687,3 +687,8 @@ export function getSchema(config: SchemaConfig): GraphQLSchema { types: [DateType, NodeType, CreateLikeInputType, CreatePostInputType, CreateUserInputType, MarkdownNodeType, PostContentInputType, CreateLikePayloadType, CreatePostPayloadType, CreateUserPayloadType, LikeType, LikeConnectionType, LikeEdgeType, MutationType, PageInfoType, PostType, PostConnectionType, PostEdgeType, QueryType, SubscriptionType, UserType, UserConnectionType, UserEdgeType, ViewerType] }); } +export const nodeClassMap = { + Like: LikeClass, + Post: PostClass, + User: UserClass +}; diff --git a/examples/strict-semantic-nullability/schema.ts b/examples/strict-semantic-nullability/schema.ts index e5948402..f29d6f80 100644 --- a/examples/strict-semantic-nullability/schema.ts +++ b/examples/strict-semantic-nullability/schema.ts @@ -183,3 +183,6 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const iPersonClassMap = { + User: UserClass +}; diff --git a/examples/yoga/schema.ts b/examples/yoga/schema.ts index ede983c3..79c8f23b 100644 --- a/examples/yoga/schema.ts +++ b/examples/yoga/schema.ts @@ -131,3 +131,6 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const iPersonClassMap = { + User: UserClass +}; diff --git a/src/Extractor.ts b/src/Extractor.ts index 0df41b8a..02aa3349 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -1351,19 +1351,17 @@ class Extractor { let exported: { tsModulePath: string; exportName: string | null } | null = null; - if (!hasTypeName) { - const isExported = node.modifiers?.find( - (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword, - ); - const isDefault = node.modifiers?.find( - (modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword, - ); - if (isExported) { - exported = { - tsModulePath: relativePath(node.getSourceFile().fileName), - exportName: isDefault ? null : node.name.text, - }; - } + const isExported = node.modifiers?.find( + (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword, + ); + const isDefault = node.modifiers?.find( + (modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword, + ); + if (isExported) { + exported = { + tsModulePath: relativePath(node.getSourceFile().fileName), + exportName: isDefault ? null : node.name.text, + }; } const directives = this.collectDirectives(node); diff --git a/src/codegen/schemaCodegen.ts b/src/codegen/schemaCodegen.ts index ffb9f3bd..de2eede3 100644 --- a/src/codegen/schemaCodegen.ts +++ b/src/codegen/schemaCodegen.ts @@ -427,6 +427,21 @@ class Codegen { return F.createIdentifier(varName); } + ensureClassImported(t: GraphQLObjectType, exported: ExportDefinition): string { + let localName = this._typeNameMappings.get(t.name); + if (localName == null) { + localName = `${t.name}Class`; + this.ts.importUserConstruct( + exported.tsModulePath, + exported.exportName, + localName, + false, + ); + this._typeNameMappings.set(t.name, localName); + } + return localName; + } + resolveType(obj: GraphQLAbstractType): ts.ShorthandPropertyAssignment | null { let needsResolveType = false; for (const t of this._schema.getPossibleTypes(obj)) { @@ -434,20 +449,8 @@ class Codegen { if (ast.hasTypeNameField) { continue; } - - const exportedMetadata = ast.exported; - if (exportedMetadata != null) { - if (!this._typeNameMappings.has(t.name)) { - const localName = `${t.name}Class`; - this.ts.importUserConstruct( - exportedMetadata.tsModulePath, - exportedMetadata.exportName, - localName, - false, - ); - - this._typeNameMappings.set(t.name, localName); - } + if (ast.exported != null) { + this.ensureClassImported(t, ast.exported); needsResolveType = true; } } @@ -974,6 +977,57 @@ class Codegen { this.ts.addStatement(this.resolveTypeFunctionDeclaration()); } + this.interfaceClassMaps(); + return this.ts.print(); } + + interfaceClassMaps(): void { + const interfaceTypes = Object.values(this._schema.getTypeMap()) + .filter(isInterfaceType) + .sort((a, b) => naturalCompare(a.name, b.name)); + + for (const interfaceType of interfaceTypes) { + const possibleTypes = this._schema.getPossibleTypes(interfaceType); + const classEntries: Array<[string, string]> = []; + + for (const t of possibleTypes) { + const exported = nullThrows(t.astNode).exported; + if (exported == null) continue; + const localName = this.ensureClassImported(t, exported); + classEntries.push([t.name, localName]); + } + + if (classEntries.length === 0) continue; + + classEntries.sort(([a], [b]) => naturalCompare(a, b)); + + const mapName = `${lowercaseFirst(interfaceType.name)}ClassMap`; + + const properties = classEntries.map(([typeName, localName]) => + F.createPropertyAssignment(typeName, F.createIdentifier(localName)), + ); + + this.ts.addStatement( + F.createVariableStatement( + [F.createModifier(ts.SyntaxKind.ExportKeyword)], + F.createVariableDeclarationList( + [ + F.createVariableDeclaration( + F.createIdentifier(mapName), + undefined, + undefined, + this.ts.objectLiteral(properties), + ), + ], + ts.NodeFlags.Const, + ), + ), + ); + } + } +} + +function lowercaseFirst(str: string): string { + return str.charAt(0).toLowerCase() + str.slice(1); } diff --git a/src/tests/fixtures/semantic_nullability/semanticNonNullMatchesInterface.ts.expected.md b/src/tests/fixtures/semantic_nullability/semanticNonNullMatchesInterface.ts.expected.md index 34b0cd13..ba040480 100644 --- a/src/tests/fixtures/semantic_nullability/semanticNonNullMatchesInterface.ts.expected.md +++ b/src/tests/fixtures/semantic_nullability/semanticNonNullMatchesInterface.ts.expected.md @@ -75,6 +75,7 @@ type User implements IPerson { ```ts import { GraphQLSchema, GraphQLDirective, DirectiveLocation, GraphQLList, GraphQLInt, specifiedDirectives, GraphQLInterfaceType, GraphQLString, GraphQLObjectType, defaultFieldResolver } from "graphql"; +import { User as UserClass } from "./semanticNonNullMatchesInterface"; async function assertNonNull(value: T | Promise): Promise { const awaited = await value; if (awaited == null) @@ -125,4 +126,7 @@ export function getSchema(): GraphQLSchema { types: [IPersonType, UserType] }); } +export const iPersonClassMap = { + User: UserClass +}; ``` \ No newline at end of file diff --git a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterface.ts.expected.md b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterface.ts.expected.md index 65fbe924..0f79af16 100644 --- a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterface.ts.expected.md +++ b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsInterface.ts.expected.md @@ -34,6 +34,7 @@ type User implements Person { ### TypeScript ```ts +import UserClass from "./TypeFromClassDefinitionImplementsInterface"; import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; export function getSchema(): GraphQLSchema { const PersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ @@ -65,4 +66,7 @@ export function getSchema(): GraphQLSchema { types: [PersonType, UserType] }); } +export const personClassMap = { + User: UserClass +}; ``` \ No newline at end of file diff --git a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsMultipleInterfaces.ts.expected.md b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsMultipleInterfaces.ts.expected.md index 6ac502a6..b32dad75 100644 --- a/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsMultipleInterfaces.ts.expected.md +++ b/src/tests/fixtures/type_definitions/TypeFromClassDefinitionImplementsMultipleInterfaces.ts.expected.md @@ -48,6 +48,7 @@ type User implements Node & Person { ### TypeScript ```ts +import UserClass from "./TypeFromClassDefinitionImplementsMultipleInterfaces"; import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; export function getSchema(): GraphQLSchema { const NodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ @@ -94,4 +95,10 @@ export function getSchema(): GraphQLSchema { types: [NodeType, PersonType, UserType] }); } +export const nodeClassMap = { + User: UserClass +}; +export const personClassMap = { + User: UserClass +}; ``` \ No newline at end of file diff --git a/src/tests/fixtures/typename/PropertySignatureTypename.ts.expected.md b/src/tests/fixtures/typename/PropertySignatureTypename.ts.expected.md index 778d76cc..c978853c 100644 --- a/src/tests/fixtures/typename/PropertySignatureTypename.ts.expected.md +++ b/src/tests/fixtures/typename/PropertySignatureTypename.ts.expected.md @@ -35,6 +35,7 @@ type User implements IPerson { ```ts import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; +import { User as UserClass } from "./PropertySignatureTypename"; export function getSchema(): GraphQLSchema { const IPersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ name: "IPerson", @@ -65,4 +66,7 @@ export function getSchema(): GraphQLSchema { types: [IPersonType, UserType] }); } +export const iPersonClassMap = { + User: UserClass +}; ``` \ No newline at end of file diff --git a/src/tests/integrationFixtures/resolveTypeViaClass/index.ts b/src/tests/integrationFixtures/resolveTypeViaClass/index.ts index 62325986..588ddf63 100644 --- a/src/tests/integrationFixtures/resolveTypeViaClass/index.ts +++ b/src/tests/integrationFixtures/resolveTypeViaClass/index.ts @@ -1,83 +1,34 @@ import { ID } from "../../../Types.js"; - -/** @gqlInterface */ -interface GqlNode { - /** @gqlField */ - id: ID; -} - -/** @gqlType */ -export default class DefaultNode implements GqlNode { - constructor( - /** @gqlField */ - public id: ID, - ) {} -} - -/** @gqlType */ -export class User implements GqlNode { - constructor( - /** @gqlField */ - public id: ID, - ) {} -} - -/** @gqlType RenamedNode */ -export class ThisNameGetsIgnored implements GqlNode { - constructor( - /** @gqlField */ - public id: ID, - ) {} -} - -/** @gqlType */ -export class Guest implements GqlNode { - constructor( - /** @gqlField */ - public id: ID, - ) {} -} - -class AlsoUser extends User { - constructor(id: ID) { - super(id); - } -} +import { gqlNodeClassMap } from "./schema.js"; +import type { GqlNode } from "./models.js"; /** @gqlQueryField */ export function node(args: { id: ID }): GqlNode { - const { id } = args; - if (id.startsWith("User:")) { - return new User(id); - } else if (id.startsWith("Guest:")) { - return new Guest(id); - } else if (id.startsWith("DefaultNode:")) { - return new DefaultNode(id); - } else if (id.startsWith("RenamedNode:")) { - return new ThisNameGetsIgnored(id); - } else if (id.startsWith("AlsoUser:")) { - return new AlsoUser(id); - } else { - return new Guest(id); + const [type, localId] = (args.id as string).split(":"); + const cls = gqlNodeClassMap[type as keyof typeof gqlNodeClassMap]; + if (cls == null) { + throw new Error(`Type "${type}" does not implement GqlNode`); } + return cls.fromId(localId); } export const query = /* GraphQL */ ` query { user: node(id: "User:1") { __typename + id } - alsoUser: node(id: "AlsoUser:1") { - __typename - } - guest: node(id: "Guest:1") { + guest: node(id: "Guest:2") { __typename + id } - defaultNode: node(id: "DefaultNode:1") { + defaultNode: node(id: "DefaultNode:3") { __typename + id } - renamedNode: node(id: "RenamedNode:1") { + renamedNode: node(id: "RenamedNode:4") { __typename + id } } `; diff --git a/src/tests/integrationFixtures/resolveTypeViaClass/index.ts.expected.md b/src/tests/integrationFixtures/resolveTypeViaClass/index.ts.expected.md index 040cb61f..160759e4 100644 --- a/src/tests/integrationFixtures/resolveTypeViaClass/index.ts.expected.md +++ b/src/tests/integrationFixtures/resolveTypeViaClass/index.ts.expected.md @@ -4,85 +4,36 @@ ```ts title="resolveTypeViaClass/index.ts" import { ID } from "../../../Types.js"; - -/** @gqlInterface */ -interface GqlNode { - /** @gqlField */ - id: ID; -} - -/** @gqlType */ -export default class DefaultNode implements GqlNode { - constructor( - /** @gqlField */ - public id: ID, - ) {} -} - -/** @gqlType */ -export class User implements GqlNode { - constructor( - /** @gqlField */ - public id: ID, - ) {} -} - -/** @gqlType RenamedNode */ -export class ThisNameGetsIgnored implements GqlNode { - constructor( - /** @gqlField */ - public id: ID, - ) {} -} - -/** @gqlType */ -export class Guest implements GqlNode { - constructor( - /** @gqlField */ - public id: ID, - ) {} -} - -class AlsoUser extends User { - constructor(id: ID) { - super(id); - } -} +import { gqlNodeClassMap } from "./schema.js"; +import type { GqlNode } from "./models.js"; /** @gqlQueryField */ export function node(args: { id: ID }): GqlNode { - const { id } = args; - if (id.startsWith("User:")) { - return new User(id); - } else if (id.startsWith("Guest:")) { - return new Guest(id); - } else if (id.startsWith("DefaultNode:")) { - return new DefaultNode(id); - } else if (id.startsWith("RenamedNode:")) { - return new ThisNameGetsIgnored(id); - } else if (id.startsWith("AlsoUser:")) { - return new AlsoUser(id); - } else { - return new Guest(id); + const [type, localId] = (args.id as string).split(":"); + const cls = gqlNodeClassMap[type as keyof typeof gqlNodeClassMap]; + if (cls == null) { + throw new Error(`Type "${type}" does not implement GqlNode`); } + return cls.fromId(localId); } export const query = /* GraphQL */ ` query { user: node(id: "User:1") { __typename + id } - alsoUser: node(id: "AlsoUser:1") { - __typename - } - guest: node(id: "Guest:1") { + guest: node(id: "Guest:2") { __typename + id } - defaultNode: node(id: "DefaultNode:1") { + defaultNode: node(id: "DefaultNode:3") { __typename + id } - renamedNode: node(id: "RenamedNode:1") { + renamedNode: node(id: "RenamedNode:4") { __typename + id } } `; @@ -96,19 +47,20 @@ export const query = /* GraphQL */ ` { "data": { "user": { - "__typename": "User" - }, - "alsoUser": { - "__typename": "User" + "__typename": "User", + "id": "User:1" }, "guest": { - "__typename": "Guest" + "__typename": "Guest", + "id": "Guest:2" }, "defaultNode": { - "__typename": "DefaultNode" + "__typename": "DefaultNode", + "id": "DefaultNode:3" }, "renamedNode": { - "__typename": "RenamedNode" + "__typename": "RenamedNode", + "id": "RenamedNode:4" } } } diff --git a/src/tests/integrationFixtures/resolveTypeViaClass/models.ts b/src/tests/integrationFixtures/resolveTypeViaClass/models.ts new file mode 100644 index 00000000..a51870ae --- /dev/null +++ b/src/tests/integrationFixtures/resolveTypeViaClass/models.ts @@ -0,0 +1,60 @@ +import { ID } from "../../../Types.js"; + +/** @gqlInterface */ +export interface GqlNode { + __typename: string; + localID(): string; +} + +/** @gqlField */ +export function id(node: GqlNode): ID { + return `${node.__typename}:${node.localID()}`; +} + +/** @gqlType */ +export default class DefaultNode implements GqlNode { + __typename = "DefaultNode" as const; + constructor(private _id: string) {} + localID() { + return this._id; + } + static fromId(id: string): DefaultNode { + return new DefaultNode(id); + } +} + +/** @gqlType */ +export class User implements GqlNode { + __typename = "User" as const; + constructor(private _id: string) {} + localID() { + return this._id; + } + static fromId(id: string): User { + return new User(id); + } +} + +/** @gqlType RenamedNode */ +export class ThisNameGetsIgnored implements GqlNode { + __typename = "RenamedNode" as const; + constructor(private _id: string) {} + localID() { + return this._id; + } + static fromId(id: string): ThisNameGetsIgnored { + return new ThisNameGetsIgnored(id); + } +} + +/** @gqlType */ +export class Guest implements GqlNode { + __typename = "Guest" as const; + constructor(private _id: string) {} + localID() { + return this._id; + } + static fromId(id: string): Guest { + return new Guest(id); + } +} diff --git a/src/tests/integrationFixtures/resolveTypeViaClass/schema.ts b/src/tests/integrationFixtures/resolveTypeViaClass/schema.ts index 0e486ce2..9ee1e033 100644 --- a/src/tests/integrationFixtures/resolveTypeViaClass/schema.ts +++ b/src/tests/integrationFixtures/resolveTypeViaClass/schema.ts @@ -1,6 +1,7 @@ -import DefaultNodeClass from "./index.js"; +import DefaultNodeClass from "./models.js"; import { GraphQLSchema, GraphQLObjectType, GraphQLInterfaceType, GraphQLID, GraphQLNonNull } from "graphql"; -import { Guest as GuestClass, ThisNameGetsIgnored as RenamedNodeClass, User as UserClass, node as queryNodeResolver } from "./index.js"; +import { node as queryNodeResolver } from "./index.js"; +import { id as defaultNodeIdResolver, id as guestIdResolver, id as renamedNodeIdResolver, id as userIdResolver, Guest as GuestClass, ThisNameGetsIgnored as RenamedNodeClass, User as UserClass } from "./models.js"; export function getSchema(): GraphQLSchema { const GqlNodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ name: "GqlNode", @@ -11,8 +12,7 @@ export function getSchema(): GraphQLSchema { type: GraphQLID } }; - }, - resolveType + } }); const QueryType: GraphQLObjectType = new GraphQLObjectType({ name: "Query", @@ -39,7 +39,10 @@ export function getSchema(): GraphQLSchema { return { id: { name: "id", - type: GraphQLID + type: GraphQLID, + resolve(source) { + return defaultNodeIdResolver(source); + } } }; }, @@ -53,7 +56,10 @@ export function getSchema(): GraphQLSchema { return { id: { name: "id", - type: GraphQLID + type: GraphQLID, + resolve(source) { + return guestIdResolver(source); + } } }; }, @@ -67,7 +73,10 @@ export function getSchema(): GraphQLSchema { return { id: { name: "id", - type: GraphQLID + type: GraphQLID, + resolve(source) { + return renamedNodeIdResolver(source); + } } }; }, @@ -81,7 +90,10 @@ export function getSchema(): GraphQLSchema { return { id: { name: "id", - type: GraphQLID + type: GraphQLID, + resolve(source) { + return userIdResolver(source); + } } }; }, @@ -94,22 +106,9 @@ export function getSchema(): GraphQLSchema { types: [GqlNodeType, DefaultNodeType, GuestType, QueryType, RenamedNodeType, UserType] }); } -const typeNameMap = new Map(); -typeNameMap.set(DefaultNodeClass, "DefaultNode"); -typeNameMap.set(GuestClass, "Guest"); -typeNameMap.set(RenamedNodeClass, "RenamedNode"); -typeNameMap.set(UserClass, "User"); -function resolveType(obj: any): string { - if (typeof obj.__typename === "string") { - return obj.__typename; - } - let prototype = Object.getPrototypeOf(obj); - while (prototype) { - const name = typeNameMap.get(prototype.constructor); - if (name != null) { - return name; - } - prototype = Object.getPrototypeOf(prototype); - } - throw new Error("Cannot find type name."); -} +export const gqlNodeClassMap = { + DefaultNode: DefaultNodeClass, + Guest: GuestClass, + RenamedNode: RenamedNodeClass, + User: UserClass +}; diff --git a/src/tests/test.ts b/src/tests/test.ts index e13d0143..e34352c1 100644 --- a/src/tests/test.ts +++ b/src/tests/test.ts @@ -17,7 +17,7 @@ import { import { Command } from "commander"; import { locate } from "../Locate.js"; import { gqlErr, ReportableDiagnostics } from "../utils/DiagnosticError.js"; -import { readFileSync, writeFileSync } from "fs"; +import { readdirSync, readFileSync, writeFileSync } from "fs"; import { codegen } from "../codegen/schemaCodegen.js"; import { printEnumsModule } from "../printSchema.js"; import { diff } from "jest-diff"; @@ -277,7 +277,7 @@ const testDirs: TestDir[] = [ { fixturesDir: integrationFixturesDir, testFilePattern: /index.ts$/, - ignoreFilePattern: /(schema)|(enums).ts$/, + ignoreFilePattern: /(? f.endsWith(".ts") && f !== "schema.ts" && f !== "enums.ts") + .map((f) => path.join(fixtureDir, f)); + + const files = [...siblingFiles, path.join(__dirname, `../Types.ts`)]; const parsedOptionsResult = validateGratsOptions({ options: { // Required to enable ts-node to locate function exports From 448e1e5cad3c8485586d6df32dfdbc3ff7f989cc Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 8 May 2026 16:54:46 -0700 Subject: [PATCH 2/4] Export getTypeName to enable dropping __typename from classes When resolveType is generated for class-based types, also export a getTypeName alias. This lets users derive the GraphQL typename from a class instance without defining __typename, which is useful for encoding global IDs in the Node spec pattern. Updates the production-app example and resolveTypeViaClass integration test to use getTypeName instead of __typename. --- examples/production-app/graphql/Node.ts | 5 ++-- examples/production-app/models/Like.ts | 2 -- examples/production-app/models/Post.ts | 2 -- examples/production-app/models/User.ts | 2 -- examples/production-app/schema.ts | 28 ++++++++++++++++--- src/codegen/schemaCodegen.ts | 16 +++++++++++ .../resolveTypeViaClass/models.ts | 8 ++---- .../resolveTypeViaClass/schema.ts | 25 +++++++++++++++-- 8 files changed, 67 insertions(+), 21 deletions(-) diff --git a/examples/production-app/graphql/Node.ts b/examples/production-app/graphql/Node.ts index 5f5aae1e..b130a37f 100644 --- a/examples/production-app/graphql/Node.ts +++ b/examples/production-app/graphql/Node.ts @@ -1,7 +1,7 @@ import { fromGlobalId, toGlobalId } from "graphql-relay"; import { ID } from "grats"; import { VC } from "../ViewerContext.js"; -import { nodeClassMap } from "../schema.js"; +import { nodeClassMap, getTypeName } from "../schema.js"; /** * Converts a globally unique ID into a local ID asserting @@ -19,7 +19,6 @@ export function getLocalTypeAssert(id: ID, typename: string): string { * Indicates a stable refetchable object in the system. * @gqlInterface Node */ export interface GraphQLNode { - __typename: string; localID(): string; } @@ -32,7 +31,7 @@ export interface GraphQLNode { * @gqlField * @killsParentOnException */ export function id(node: GraphQLNode): ID { - return toGlobalId(node.__typename, node.localID()); + return toGlobalId(getTypeName(node), node.localID()); } /** diff --git a/examples/production-app/models/Like.ts b/examples/production-app/models/Like.ts index d1272337..b20e8c1c 100644 --- a/examples/production-app/models/Like.ts +++ b/examples/production-app/models/Like.ts @@ -11,8 +11,6 @@ import { Post } from "./Post.js"; * A reaction from a user indicating that they like a post. * @gqlType */ export class Like extends Model implements GraphQLNode { - __typename = "Like" as const; - static async fetchById(vc: VC, id: string): Promise { return vc.getLikeById(id); } diff --git a/examples/production-app/models/Post.ts b/examples/production-app/models/Post.ts index ae479dbc..bea3f065 100644 --- a/examples/production-app/models/Post.ts +++ b/examples/production-app/models/Post.ts @@ -12,8 +12,6 @@ import { connectionFromSelectOrCount } from "../graphql/gqlUtils.js"; * A blog post. * @gqlType */ export class Post extends Model implements GraphQLNode { - __typename = "Post" as const; - static async fetchById(vc: VC, id: string): Promise { return vc.getPostById(id); } diff --git a/examples/production-app/models/User.ts b/examples/production-app/models/User.ts index 8bdcb46a..b323fcbf 100644 --- a/examples/production-app/models/User.ts +++ b/examples/production-app/models/User.ts @@ -9,8 +9,6 @@ import { connectionFromSelectOrCount } from "../graphql/gqlUtils.js"; /** @gqlType */ export class User extends Model implements GraphQLNode { - __typename = "User" as const; - static async fetchById(vc: VC, id: string): Promise { return vc.getUserById(id); } diff --git a/examples/production-app/schema.ts b/examples/production-app/schema.ts index a9548ebc..ac85fe30 100644 --- a/examples/production-app/schema.ts +++ b/examples/production-app/schema.ts @@ -7,14 +7,14 @@ import type { GqlScalar } from "grats"; import type { GqlDate as DateInternal } from "./graphql/CustomScalars.js"; import { GraphQLSchema, GraphQLDirective, DirectiveLocation, GraphQLNonNull, GraphQLInt, specifiedDirectives, GraphQLObjectType, GraphQLList, GraphQLString, GraphQLScalarType, GraphQLID, GraphQLInterfaceType, GraphQLBoolean, GraphQLInputObjectType } from "graphql"; import { id as likeIdResolver, id as userIdResolver, id as postIdResolver, node as queryNodeResolver, nodes as queryNodesResolver } from "./graphql/Node.js"; +import { Like as LikeClass, createLike as mutationCreateLikeResolver } from "./models/Like.js"; +import { Post as PostClass, createPost as mutationCreatePostResolver } from "./models/Post.js"; +import { User as UserClass, createUser as mutationCreateUserResolver } from "./models/User.js"; import { nodes as postConnectionNodesResolver, posts as queryPostsResolver } from "./models/PostConnection.js"; import { nodes as likeConnectionNodesResolver, likes as queryLikesResolver, postLikes as subscriptionPostLikesResolver } from "./models/LikeConnection.js"; import { getVc } from "./ViewerContext.js"; import { nodes as userConnectionNodesResolver, users as queryUsersResolver } from "./models/UserConnection.js"; import { Viewer as queryViewerResolver } from "./models/Viewer.js"; -import { createLike as mutationCreateLikeResolver, Like as LikeClass } from "./models/Like.js"; -import { createPost as mutationCreatePostResolver, Post as PostClass } from "./models/Post.js"; -import { createUser as mutationCreateUserResolver, User as UserClass } from "./models/User.js"; export type SchemaConfig = { scalars: { Date: GqlScalar; @@ -38,7 +38,8 @@ export function getSchema(config: SchemaConfig): GraphQLSchema { type: new GraphQLNonNull(GraphQLID) } }; - } + }, + resolveType }); const PostType: GraphQLObjectType = new GraphQLObjectType({ name: "Post", @@ -687,6 +688,25 @@ export function getSchema(config: SchemaConfig): GraphQLSchema { types: [DateType, NodeType, CreateLikeInputType, CreatePostInputType, CreateUserInputType, MarkdownNodeType, PostContentInputType, CreateLikePayloadType, CreatePostPayloadType, CreateUserPayloadType, LikeType, LikeConnectionType, LikeEdgeType, MutationType, PageInfoType, PostType, PostConnectionType, PostEdgeType, QueryType, SubscriptionType, UserType, UserConnectionType, UserEdgeType, ViewerType] }); } +const typeNameMap = new Map(); +typeNameMap.set(LikeClass, "Like"); +typeNameMap.set(PostClass, "Post"); +typeNameMap.set(UserClass, "User"); +function resolveType(obj: any): string { + if (typeof obj.__typename === "string") { + return obj.__typename; + } + let prototype = Object.getPrototypeOf(obj); + while (prototype) { + const name = typeNameMap.get(prototype.constructor); + if (name != null) { + return name; + } + prototype = Object.getPrototypeOf(prototype); + } + throw new Error("Cannot find type name."); +} +export const getTypeName = resolveType; export const nodeClassMap = { Like: LikeClass, Post: PostClass, diff --git a/src/codegen/schemaCodegen.ts b/src/codegen/schemaCodegen.ts index de2eede3..3ba5a0b4 100644 --- a/src/codegen/schemaCodegen.ts +++ b/src/codegen/schemaCodegen.ts @@ -975,6 +975,22 @@ class Codegen { ); } this.ts.addStatement(this.resolveTypeFunctionDeclaration()); + this.ts.addStatement( + F.createVariableStatement( + [F.createModifier(ts.SyntaxKind.ExportKeyword)], + F.createVariableDeclarationList( + [ + F.createVariableDeclaration( + "getTypeName", + undefined, + undefined, + F.createIdentifier("resolveType"), + ), + ], + ts.NodeFlags.Const, + ), + ), + ); } this.interfaceClassMaps(); diff --git a/src/tests/integrationFixtures/resolveTypeViaClass/models.ts b/src/tests/integrationFixtures/resolveTypeViaClass/models.ts index a51870ae..7afd2c93 100644 --- a/src/tests/integrationFixtures/resolveTypeViaClass/models.ts +++ b/src/tests/integrationFixtures/resolveTypeViaClass/models.ts @@ -1,19 +1,18 @@ import { ID } from "../../../Types.js"; +import { getTypeName } from "./schema.js"; /** @gqlInterface */ export interface GqlNode { - __typename: string; localID(): string; } /** @gqlField */ export function id(node: GqlNode): ID { - return `${node.__typename}:${node.localID()}`; + return `${getTypeName(node)}:${node.localID()}`; } /** @gqlType */ export default class DefaultNode implements GqlNode { - __typename = "DefaultNode" as const; constructor(private _id: string) {} localID() { return this._id; @@ -25,7 +24,6 @@ export default class DefaultNode implements GqlNode { /** @gqlType */ export class User implements GqlNode { - __typename = "User" as const; constructor(private _id: string) {} localID() { return this._id; @@ -37,7 +35,6 @@ export class User implements GqlNode { /** @gqlType RenamedNode */ export class ThisNameGetsIgnored implements GqlNode { - __typename = "RenamedNode" as const; constructor(private _id: string) {} localID() { return this._id; @@ -49,7 +46,6 @@ export class ThisNameGetsIgnored implements GqlNode { /** @gqlType */ export class Guest implements GqlNode { - __typename = "Guest" as const; constructor(private _id: string) {} localID() { return this._id; diff --git a/src/tests/integrationFixtures/resolveTypeViaClass/schema.ts b/src/tests/integrationFixtures/resolveTypeViaClass/schema.ts index 9ee1e033..91ca6075 100644 --- a/src/tests/integrationFixtures/resolveTypeViaClass/schema.ts +++ b/src/tests/integrationFixtures/resolveTypeViaClass/schema.ts @@ -1,7 +1,7 @@ import DefaultNodeClass from "./models.js"; import { GraphQLSchema, GraphQLObjectType, GraphQLInterfaceType, GraphQLID, GraphQLNonNull } from "graphql"; +import { Guest as GuestClass, ThisNameGetsIgnored as RenamedNodeClass, User as UserClass, id as defaultNodeIdResolver, id as guestIdResolver, id as renamedNodeIdResolver, id as userIdResolver } from "./models.js"; import { node as queryNodeResolver } from "./index.js"; -import { id as defaultNodeIdResolver, id as guestIdResolver, id as renamedNodeIdResolver, id as userIdResolver, Guest as GuestClass, ThisNameGetsIgnored as RenamedNodeClass, User as UserClass } from "./models.js"; export function getSchema(): GraphQLSchema { const GqlNodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ name: "GqlNode", @@ -12,7 +12,8 @@ export function getSchema(): GraphQLSchema { type: GraphQLID } }; - } + }, + resolveType }); const QueryType: GraphQLObjectType = new GraphQLObjectType({ name: "Query", @@ -106,6 +107,26 @@ export function getSchema(): GraphQLSchema { types: [GqlNodeType, DefaultNodeType, GuestType, QueryType, RenamedNodeType, UserType] }); } +const typeNameMap = new Map(); +typeNameMap.set(DefaultNodeClass, "DefaultNode"); +typeNameMap.set(GuestClass, "Guest"); +typeNameMap.set(RenamedNodeClass, "RenamedNode"); +typeNameMap.set(UserClass, "User"); +function resolveType(obj: any): string { + if (typeof obj.__typename === "string") { + return obj.__typename; + } + let prototype = Object.getPrototypeOf(obj); + while (prototype) { + const name = typeNameMap.get(prototype.constructor); + if (name != null) { + return name; + } + prototype = Object.getPrototypeOf(prototype); + } + throw new Error("Cannot find type name."); +} +export const getTypeName = resolveType; export const gqlNodeClassMap = { DefaultNode: DefaultNodeClass, Guest: GuestClass, From c35bb1d3533b6cd4276ec31a80ed8f93a44b107a Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 8 May 2026 16:58:06 -0700 Subject: [PATCH 3/4] Add Node spec guide documenting the class map pattern Documents how to implement GraphQL's Global Object Identification spec using Grats' generated nodeClassMap and getTypeName exports, with copy-pasteable code examples. --- llm-docs/guides/node-spec.md | 98 +++++++++++++++++++++++++ website/docs/05-guides/11-node-spec.md | 99 ++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 llm-docs/guides/node-spec.md create mode 100644 website/docs/05-guides/11-node-spec.md diff --git a/llm-docs/guides/node-spec.md b/llm-docs/guides/node-spec.md new file mode 100644 index 00000000..5c000e71 --- /dev/null +++ b/llm-docs/guides/node-spec.md @@ -0,0 +1,98 @@ +# Node Interface (Global Object Identification) + +The [Global Object Identification](https://graphql.org/learn/global-object-identification/) spec defines a pattern for fetching any object by a globally unique ID. It is used by clients like [Relay](https://relay.dev) to efficiently refetch individual objects and normalize cached data. + +The spec requires: + +- A `Node` interface with a single `id: ID!` field +- A root `node(id: ID!): Node` query field that can fetch any `Node` by its global ID +- A root `nodes(ids: [ID!]!): [Node]!` query field for batch fetching + +Grats provides several features that make implementing this spec straightforward, with full static type safety. + +> **INFO:** +> For a full working example of the Node interface in action, see our [Production App](../examples/production-app.md) example app. + +## Implementation + +### Step 1: Define the Node interface + +Define a TypeScript interface for `Node`. Since TypeScript has a built-in `Node` type (for DOM nodes), use a different TypeScript name and rename it with `@gqlInterface Node`: + +```ts +import { ID } from "grats"; +import { getTypeName } from "./schema"; +import { toGlobalId } from "graphql-relay"; + +/** @gqlInterface Node */ +export interface GraphQLNode { + localID(): string; +} + +/** + * @gqlField + * @killsParentOnException */ +export function id(node: GraphQLNode): ID { + return toGlobalId(getTypeName(node), node.localID()); +} +``` + +`localID()` is a TypeScript-only contract (not a GraphQL field) that each implementor must provide. It returns the type's local (unprefixed) identifier. + +The functional `@gqlField` automatically adds the `id` field to every type that implements `Node`. `getTypeName` is exported by Grats' generated `schema.ts` — it returns the GraphQL typename for any class instance, so you don't need to define `__typename` on your classes. `toGlobalId` from `graphql-relay` encodes `typename:localId` as a base64 string. + +### Step 2: Implement Node on your types + +Each type that should be a `Node` implements the interface and provides a static `fetchById` method: + +```ts +/** @gqlType */ +export class User implements GraphQLNode { + constructor(private _id: string) {} + + localID() { + return this._id; + } + + static async fetchById(id: string): Promise { + return db.users.get(id); + } +} +``` + +### Step 3: Implement the `node` and `nodes` query fields + +Grats generates a `nodeClassMap` in `schema.ts` that maps every `Node` implementor's typename to its class. Use it to dispatch to the correct `fetchById`: + +```ts +import { fromGlobalId } from "graphql-relay"; +import { nodeClassMap } from "./schema"; + +/** @gqlQueryField */ +export async function node(args: { id: ID }): Promise { + const { type, id } = fromGlobalId(args.id); + const cls = nodeClassMap[type as keyof typeof nodeClassMap]; + if (cls == null) { + throw new Error(`Type "${type}" does not implement Node`); + } + return cls.fetchById(id); +} + +/** @gqlQueryField */ +export async function nodes(ids: ID[]): Promise> { + return Promise.all(ids.map((id) => node({ id }))); +} +``` + +## Static type safety + +If you add a new type that implements `GraphQLNode` but forget to add a `fetchById` static method, TypeScript will report an error at the `cls.fetchById(...)` call — because the union of all classes in `nodeClassMap` now includes a class without that method. + +This eliminates the common bug of adding a `Node` implementor but forgetting to register it in the `node()` resolver. + +## How it works + +Grats generates two exports in `schema.ts` that power this pattern: + +- **`nodeClassMap`** — An object mapping GraphQL typenames to their class constructors for every type that implements the `Node` interface. Grats generates one of these maps per interface in your schema. +- **`getTypeName`** — A function that returns the GraphQL typename for any class instance, using the same prototype-chain resolution that GraphQL uses internally. This lets you encode global IDs without defining `__typename` on your classes. diff --git a/website/docs/05-guides/11-node-spec.md b/website/docs/05-guides/11-node-spec.md new file mode 100644 index 00000000..92554931 --- /dev/null +++ b/website/docs/05-guides/11-node-spec.md @@ -0,0 +1,99 @@ +# Node Interface (Global Object Identification) + +The [Global Object Identification](https://graphql.org/learn/global-object-identification/) spec defines a pattern for fetching any object by a globally unique ID. It is used by clients like [Relay](https://relay.dev) to efficiently refetch individual objects and normalize cached data. + +The spec requires: + +- A `Node` interface with a single `id: ID!` field +- A root `node(id: ID!): Node` query field that can fetch any `Node` by its global ID +- A root `nodes(ids: [ID!]!): [Node]!` query field for batch fetching + +Grats provides several features that make implementing this spec straightforward, with full static type safety. + +:::info +For a full working example of the Node interface in action, see our [Production App](../05-examples/10-production-app.md) example app. +::: + +## Implementation + +### Step 1: Define the Node interface + +Define a TypeScript interface for `Node`. Since TypeScript has a built-in `Node` type (for DOM nodes), use a different TypeScript name and rename it with `@gqlInterface Node`: + +```ts +import { ID } from "grats"; +import { getTypeName } from "./schema"; +import { toGlobalId } from "graphql-relay"; + +/** @gqlInterface Node */ +export interface GraphQLNode { + localID(): string; +} + +/** + * @gqlField + * @killsParentOnException */ +export function id(node: GraphQLNode): ID { + return toGlobalId(getTypeName(node), node.localID()); +} +``` + +`localID()` is a TypeScript-only contract (not a GraphQL field) that each implementor must provide. It returns the type's local (unprefixed) identifier. + +The functional `@gqlField` automatically adds the `id` field to every type that implements `Node`. `getTypeName` is exported by Grats' generated `schema.ts` — it returns the GraphQL typename for any class instance, so you don't need to define `__typename` on your classes. `toGlobalId` from `graphql-relay` encodes `typename:localId` as a base64 string. + +### Step 2: Implement Node on your types + +Each type that should be a `Node` implements the interface and provides a static `fetchById` method: + +```ts +/** @gqlType */ +export class User implements GraphQLNode { + constructor(private _id: string) {} + + localID() { + return this._id; + } + + static async fetchById(id: string): Promise { + return db.users.get(id); + } +} +``` + +### Step 3: Implement the `node` and `nodes` query fields + +Grats generates a `nodeClassMap` in `schema.ts` that maps every `Node` implementor's typename to its class. Use it to dispatch to the correct `fetchById`: + +```ts +import { fromGlobalId } from "graphql-relay"; +import { nodeClassMap } from "./schema"; + +/** @gqlQueryField */ +export async function node(args: { id: ID }): Promise { + const { type, id } = fromGlobalId(args.id); + const cls = nodeClassMap[type as keyof typeof nodeClassMap]; + if (cls == null) { + throw new Error(`Type "${type}" does not implement Node`); + } + return cls.fetchById(id); +} + +/** @gqlQueryField */ +export async function nodes(ids: ID[]): Promise> { + return Promise.all(ids.map((id) => node({ id }))); +} +``` + +## Static type safety + +If you add a new type that implements `GraphQLNode` but forget to add a `fetchById` static method, TypeScript will report an error at the `cls.fetchById(...)` call — because the union of all classes in `nodeClassMap` now includes a class without that method. + +This eliminates the common bug of adding a `Node` implementor but forgetting to register it in the `node()` resolver. + +## How it works + +Grats generates two exports in `schema.ts` that power this pattern: + +- **`nodeClassMap`** — An object mapping GraphQL typenames to their class constructors for every type that implements the `Node` interface. Grats generates one of these maps per interface in your schema. +- **`getTypeName`** — A function that returns the GraphQL typename for any class instance, using the same prototype-chain resolution that GraphQL uses internally. This lets you encode global IDs without defining `__typename` on your classes. From 6dedaa6f2387508551a3a57a010ac8cd594281f1 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 8 May 2026 17:08:32 -0700 Subject: [PATCH 4/4] Fix formatting and regenerate example schemas Fixes prettier formatting in schemaCodegen.ts and test.ts, and regenerates all example project schemas to include the new getTypeName export. --- examples/apollo-server/schema.ts | 1 + examples/express-graphql-http/schema.ts | 1 + examples/next-js/schema.ts | 1 + examples/strict-semantic-nullability/schema.ts | 1 + examples/yoga/schema.ts | 1 + src/codegen/schemaCodegen.ts | 5 ++++- src/tests/test.ts | 4 +++- 7 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/apollo-server/schema.ts b/examples/apollo-server/schema.ts index bf343493..6b3ceb6b 100644 --- a/examples/apollo-server/schema.ts +++ b/examples/apollo-server/schema.ts @@ -107,6 +107,7 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const getTypeName = resolveType; export const iPersonClassMap = { User: UserClass }; diff --git a/examples/express-graphql-http/schema.ts b/examples/express-graphql-http/schema.ts index 4ebce2af..50d0f77e 100644 --- a/examples/express-graphql-http/schema.ts +++ b/examples/express-graphql-http/schema.ts @@ -107,6 +107,7 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const getTypeName = resolveType; export const iPersonClassMap = { User: UserClass }; diff --git a/examples/next-js/schema.ts b/examples/next-js/schema.ts index 71ec5e51..92dfd93f 100644 --- a/examples/next-js/schema.ts +++ b/examples/next-js/schema.ts @@ -106,6 +106,7 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const getTypeName = resolveType; export const iPersonClassMap = { User: UserClass }; diff --git a/examples/strict-semantic-nullability/schema.ts b/examples/strict-semantic-nullability/schema.ts index f29d6f80..ce44a632 100644 --- a/examples/strict-semantic-nullability/schema.ts +++ b/examples/strict-semantic-nullability/schema.ts @@ -183,6 +183,7 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const getTypeName = resolveType; export const iPersonClassMap = { User: UserClass }; diff --git a/examples/yoga/schema.ts b/examples/yoga/schema.ts index 79c8f23b..39bf7ca1 100644 --- a/examples/yoga/schema.ts +++ b/examples/yoga/schema.ts @@ -131,6 +131,7 @@ function resolveType(obj: any): string { } throw new Error("Cannot find type name."); } +export const getTypeName = resolveType; export const iPersonClassMap = { User: UserClass }; diff --git a/src/codegen/schemaCodegen.ts b/src/codegen/schemaCodegen.ts index 3ba5a0b4..0acdbe33 100644 --- a/src/codegen/schemaCodegen.ts +++ b/src/codegen/schemaCodegen.ts @@ -427,7 +427,10 @@ class Codegen { return F.createIdentifier(varName); } - ensureClassImported(t: GraphQLObjectType, exported: ExportDefinition): string { + ensureClassImported( + t: GraphQLObjectType, + exported: ExportDefinition, + ): string { let localName = this._typeNameMappings.get(t.name); if (localName == null) { localName = `${t.name}Class`; diff --git a/src/tests/test.ts b/src/tests/test.ts index e34352c1..7bd18a38 100644 --- a/src/tests/test.ts +++ b/src/tests/test.ts @@ -297,7 +297,9 @@ const testDirs: TestDir[] = [ const schemaPath = path.join(fixtureDir, "schema.ts"); const siblingFiles = readdirSync(fixtureDir) - .filter((f) => f.endsWith(".ts") && f !== "schema.ts" && f !== "enums.ts") + .filter( + (f) => f.endsWith(".ts") && f !== "schema.ts" && f !== "enums.ts", + ) .map((f) => path.join(fixtureDir, f)); const files = [...siblingFiles, path.join(__dirname, `../Types.ts`)];