diff --git a/src/__tests__/starWarsData.ts b/src/__tests__/starWarsData.ts index 60c4331bb6..aeebcd012e 100644 --- a/src/__tests__/starWarsData.ts +++ b/src/__tests__/starWarsData.ts @@ -27,6 +27,22 @@ export interface Droid { primaryFunction: string; } +export interface Jedi { + type: 'Jedi'; + id: string; + human: string; + lightsaberColor: string; + enemies: ReadonlyArray; +} + +export interface Sith { + type: 'Sith'; + id: string; + human: string; + darkSidePower: string; + enemies: ReadonlyArray; +} + /** * This defines a basic set of data for our Star Wars Schema. * @@ -109,6 +125,30 @@ const droidData: { [id: string]: Droid } = { [artoo.id]: artoo, }; +const jediLuke: Jedi = { + type: 'Jedi', + id: '3000', + human: '1000', + lightsaberColor: 'Blue', + enemies: ['4000'], +}; + +const jediData: { [id: string]: Jedi } = { + [jediLuke.id]: jediLuke, +}; + +const sithVader: Sith = { + type: 'Sith', + id: '4000', + human: '1001', + darkSidePower: 'Force Choke', + enemies: ['3000'], +}; + +const sithData: { [id: string]: Sith } = { + [sithVader.id]: sithVader, +}; + /** * Helper function to get a character by ID. */ @@ -152,3 +192,24 @@ export function getHuman(id: string): Human | null { export function getDroid(id: string): Droid | null { return droidData[id]; } + +/** + * Allows us to query for the jedi with the given id + */ +export function getJedi(id: string): Jedi | null { + return jediData[id]; +} + +/** + * Allows us to query for the enimies. + */ +export function getEnemies(character: Jedi | Sith): Array { + return character.enemies.map((id) => getJedi(id) ?? getSith(id)); +} + +/** + * Allows us to query for the sith with the given id + */ +export function getSith(id: string): Sith | null { + return sithData[id]; +} diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index 224c506c19..eb08f0d005 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -35,6 +35,8 @@ describe('Star Wars Introspection Tests', () => { { name: 'String' }, { name: 'Episode' }, { name: 'Droid' }, + { name: 'Jedi' }, + { name: 'Sith' }, { name: 'Query' }, { name: 'Boolean' }, { name: '__Schema' }, @@ -339,6 +341,42 @@ describe('Star Wars Introspection Tests', () => { }, ], }, + { + name: 'jedi', + args: [ + { + name: 'id', + description: 'id of the jedi', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'String', + }, + }, + defaultValue: null, + }, + ], + }, + { + name: 'sith', + args: [ + { + name: 'id', + description: 'id of the sith', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'String', + }, + }, + defaultValue: null, + }, + ], + }, ], }, }, diff --git a/src/__tests__/starWarsQuery-test.ts b/src/__tests__/starWarsQuery-test.ts index ecc69b2b02..7900cd8fbb 100644 --- a/src/__tests__/starWarsQuery-test.ts +++ b/src/__tests__/starWarsQuery-test.ts @@ -494,4 +494,60 @@ describe('Star Wars Query Tests', () => { }); }); }); + + describe('Using types with Ref types', () => { + it('Allows us to query jedi enemies', async () => { + const source = ` + query FetchJediEnemies { + jedi(id: "3000") { + enemies { + id + darkSidePower + } + } + } + `; + + const result = await graphql({ schema, source }); + expect(result).to.deep.equal({ + data: { + jedi: { + enemies: [ + { + id: '4000', + darkSidePower: 'Force Choke', + }, + ], + }, + }, + }); + }); + + it('Allows us to query sith enemies', async () => { + const source = ` + query FetchSithEnemies { + sith(id: "4000") { + enemies { + id + lightsaberColor + } + } + } + `; + + const result = await graphql({ schema, source }); + expect(result).to.deep.equal({ + data: { + sith: { + enemies: [ + { + id: '3000', + lightsaberColor: 'Blue', + }, + ], + }, + }, + }); + }); + }); }); diff --git a/src/__tests__/starWarsSchema.ts b/src/__tests__/starWarsSchema.ts index f42203c37d..ad9e7ef563 100644 --- a/src/__tests__/starWarsSchema.ts +++ b/src/__tests__/starWarsSchema.ts @@ -8,7 +8,15 @@ import { import { GraphQLString } from '../type/scalars.js'; import { GraphQLSchema } from '../type/schema.js'; -import { getDroid, getFriends, getHero, getHuman } from './starWarsData.js'; +import { + getDroid, + getEnemies, + getFriends, + getHero, + getHuman, + getJedi, + getSith, +} from './starWarsData.js'; /** * This is designed to be an end-to-end test, demonstrating @@ -241,6 +249,86 @@ const droidType = new GraphQLObjectType({ interfaces: [characterInterface], }); +/** + * We introduce two new types to represent characters in the Star Wars universe: + * Jedi and Sith. Both types have a non-null array field 'enemies' that references + * characters of the opposite type, creating a bidirectional relationship. + * + * This implements the following type system shorthand: + * ```graphql + * type Jedi { + * id: String! + * human: Human! + * lightsaberColor: String + * enemies: [Sith!]! + * } + * + * type Sith { + * id: String! + * human: Human! + * darkSidePower: String + * enemies: [Jedi!]! + * } + * ``` + */ +const jediType = new GraphQLObjectType({ + name: 'Jedi', + description: 'A Jedi character in the Star Wars universe.', + fields: () => ({ + id: { + type: new GraphQLNonNull(GraphQLString), + description: 'The id of the Jedi.', + }, + name: { + type: GraphQLString, + description: 'The name of the Jedi.', + }, + human: { + type: new GraphQLNonNull(humanType), + description: 'The human counterpart of the Jedi.', + resolve: (jedi) => getHuman(jedi.human), + }, + lightsaberColor: { + type: GraphQLString, + description: "The color of the Jedi's lightsaber.", + }, + enemies: { + type: new GraphQLList('Sith'), + description: 'The enemies of the Jedi.', + resolve: (sith) => getEnemies(sith), + }, + }), +}); + +const sithType = new GraphQLObjectType({ + name: 'Sith', + description: 'A Sith character in the Star Wars universe.', + fields: () => ({ + id: { + type: new GraphQLNonNull(GraphQLString), + description: 'The id of the Sith.', + }, + name: { + type: GraphQLString, + description: 'The name of the Sith.', + }, + human: { + type: new GraphQLNonNull(humanType), + description: 'The human counterpart of the Jedi.', + resolve: (sith) => getHuman(sith.human), + }, + darkSidePower: { + type: GraphQLString, + description: 'The dark side power of the Sith.', + }, + enemies: { + type: new GraphQLList('Jedi'), + description: 'The enemies of the Sith.', + resolve: (sith) => getEnemies(sith), + }, + }), +}); + /** * This is the type that will be the root of our query, and the * entry point into our schema. It gives us the ability to fetch @@ -253,6 +341,8 @@ const droidType = new GraphQLObjectType({ * hero(episode: Episode): Character * human(id: String!): Human * droid(id: String!): Droid + * jedi(id: String!): Jedi + * sith(id: String!): Sith * } * ``` */ @@ -290,6 +380,26 @@ const queryType = new GraphQLObjectType({ }, resolve: (_source, { id }) => getDroid(id), }, + jedi: { + type: jediType, + args: { + id: { + description: 'id of the jedi', + type: new GraphQLNonNull(GraphQLString), + }, + }, + resolve: (_source, { id }) => getJedi(id), + }, + sith: { + type: sithType, + args: { + id: { + description: 'id of the sith', + type: new GraphQLNonNull(GraphQLString), + }, + }, + resolve: (_source, { id }) => getSith(id), + }, }), }); @@ -299,5 +409,5 @@ const queryType = new GraphQLObjectType({ */ export const StarWarsSchema: GraphQLSchema = new GraphQLSchema({ query: queryType, - types: [humanType, droidType], + types: [humanType, droidType, jediType, sithType], }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index a19a51a217..8eb2140ac6 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -42,6 +42,8 @@ import { isListType, isNonNullType, isObjectType, + isOutputType, + isRefType, } from '../type/definition.js'; import { GraphQLStreamDirective } from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; @@ -839,6 +841,21 @@ function completeValue( deferMap, ); } + + // If field is a ref, get the original type and complete all sub-selections + if (isRefType(returnType)) { + return completeRefValue( + exeContext, + returnType, + fieldGroup, + info, + path, + result, + incrementalDataRecord, + deferMap + ); + } + /* c8 ignore next 6 */ // Not reachable, all possible output types have been considered. invariant( @@ -847,6 +864,36 @@ function completeValue( ); } +function completeRefValue( + exeContext: ExecutionContext, + returnType: string, + fieldNodes: FieldGroup, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + incrementalDataRecord: IncrementalDataRecord, + deferMap: ReadonlyMap, +) { + const type = exeContext.schema.getType(returnType); + + if (type == null || !isOutputType(type)) { + throw new Error( + `"${returnType}" is not a valid type`, + ); + } + + return completeValue( + exeContext, + type, + fieldNodes, + info, + path, + result, + incrementalDataRecord, + deferMap + ); +} + async function completePromisedValue( exeContext: ExecutionContext, returnType: GraphQLOutputType, diff --git a/src/type/definition.ts b/src/type/definition.ts index 0ca4152bd2..2ddb2738fb 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -60,7 +60,8 @@ export function isType(type: unknown): type is GraphQLType { isEnumType(type) || isInputObjectType(type) || isListType(type) || - isNonNullType(type) + isNonNullType(type) || + isRefType(type) ); } @@ -233,7 +234,8 @@ export function isOutputType(type: unknown): type is GraphQLOutputType { isInterfaceType(type) || isUnionType(type) || isEnumType(type) || - (isWrappingType(type) && isOutputType(type.ofType)) + (isWrappingType(type) && isOutputType(type.ofType)) || + isRefType(type) ); } @@ -442,7 +444,8 @@ export type GraphQLNamedOutputType = | GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType - | GraphQLEnumType; + | GraphQLEnumType + | string; export function isNamedType(type: unknown): type is GraphQLNamedType { return ( @@ -481,6 +484,10 @@ export function getNamedType( } } +export function isRefType(type: unknown): type is string { + return typeof type === 'string'; +} + /** * Used while defining GraphQL types to allow for circular references in * otherwise immutable type definitions. diff --git a/src/type/schema.ts b/src/type/schema.ts index 694454fae5..8b4b540d2b 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -26,6 +26,7 @@ import { isInputObjectType, isInterfaceType, isObjectType, + isRefType, isUnionType, } from './definition.js'; import type { GraphQLDirective } from './directives.js'; @@ -215,6 +216,10 @@ export class GraphQLSchema { continue; } + if (isRefType(namedType)) { + continue; + } + const typeName = namedType.name; if (this._typeMap[typeName] !== undefined) { throw new Error(