From 94af17f40998aa66f3487d59d23033952de01a69 Mon Sep 17 00:00:00 2001 From: mmahoney Date: Mon, 4 Dec 2017 22:08:49 -0500 Subject: [PATCH 1/4] Provide GraphQLSchema => IntrospectionQuery Utility + Introspection Response cleaner --- .../__tests__/introspectionFromSchema-test.js | 589 ++++++++++++++++++ src/utilities/cleanIntrospectionResponse.js | 325 ++++++++++ src/utilities/introspectionFromSchema.js | 264 ++++++++ 3 files changed, 1178 insertions(+) create mode 100644 src/utilities/__tests__/introspectionFromSchema-test.js create mode 100644 src/utilities/cleanIntrospectionResponse.js create mode 100644 src/utilities/introspectionFromSchema.js diff --git a/src/utilities/__tests__/introspectionFromSchema-test.js b/src/utilities/__tests__/introspectionFromSchema-test.js new file mode 100644 index 0000000000..92f6e74689 --- /dev/null +++ b/src/utilities/__tests__/introspectionFromSchema-test.js @@ -0,0 +1,589 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import { introspectionQuery } from '../introspectionQuery'; +import { introspectionQueryFromGraphQLSchema } from '../introspectionFromSchema'; +import { cleanIntrospectionResponse } from '../cleanIntrospectionResponse'; +import { + graphql, + GraphQLID, + GraphQLBoolean, + GraphQLFloat, + GraphQLInt, + GraphQLScalarType, + GraphQLSchema, + GraphQLObjectType, + GraphQLString, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLList, + GraphQLNonNull, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLDirective, +} from '../..'; + +// Test property: +// Given a server's schema, a client querying the introspectionQuery returns +// the exact same result as running the +// GraphQLSchema => IntrospectionQuery conversion +async function testSchema(schema) { + const serverResponse = await graphql(schema, introspectionQuery); + const serverIntrospection = cleanIntrospectionResponse(serverResponse.data); + const introspectionFromSchema = introspectionQueryFromGraphQLSchema(schema); + expect(introspectionFromSchema).to.deep.equal(serverIntrospection); +} + +describe('Type System: build introspection from schema', () => { + it('converts a simple schema', async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Simple', + description: 'This is a simple type', + fields: { + string: { + type: GraphQLString, + description: 'This is a string field', + }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a simple schema with all operation types', async () => { + const queryType = new GraphQLObjectType({ + name: 'QueryType', + description: 'This is a simple query type', + fields: { + string: { + type: GraphQLString, + description: 'This is a string field', + }, + }, + }); + + const mutationType = new GraphQLObjectType({ + name: 'MutationType', + description: 'This is a simple mutation type', + fields: { + setString: { + type: GraphQLString, + description: 'Set the string field', + args: { + value: { type: GraphQLString }, + }, + }, + }, + }); + + const subscriptionType = new GraphQLObjectType({ + name: 'SubscriptionType', + description: 'This is a simple subscription type', + fields: { + string: { + type: GraphQLString, + description: 'This is a string field', + }, + }, + }); + + const schema = new GraphQLSchema({ + query: queryType, + mutation: mutationType, + subscription: subscriptionType, + }); + + await testSchema(schema); + }); + + it('uses built-in scalars when possible', async () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + serialize: () => null, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Scalars', + fields: { + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + string: { type: GraphQLString }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + custom: { type: customScalar }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with a recursive type reference', async () => { + const recurType = new GraphQLObjectType({ + name: 'Recur', + fields: () => ({ + recur: { type: recurType }, + }), + }); + const schema = new GraphQLSchema({ + query: recurType, + }); + + await testSchema(schema); + }); + + it('converts a schema with a circular type reference', async () => { + const dogType = new GraphQLObjectType({ + name: 'Dog', + fields: () => ({ + bestFriend: { type: humanType }, + }), + }); + const humanType = new GraphQLObjectType({ + name: 'Human', + fields: () => ({ + bestFriend: { type: dogType }, + }), + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Circular', + fields: { + dog: { type: dogType }, + human: { type: humanType }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with an interface', async () => { + const friendlyType = new GraphQLInterfaceType({ + name: 'Friendly', + fields: () => ({ + bestFriend: { + type: friendlyType, + description: 'The best friend of this friendly thing', + }, + }), + }); + const dogType = new GraphQLObjectType({ + name: 'Dog', + interfaces: [friendlyType], + fields: () => ({ + bestFriend: { type: friendlyType }, + }), + }); + const humanType = new GraphQLObjectType({ + name: 'Human', + interfaces: [friendlyType], + fields: () => ({ + bestFriend: { type: friendlyType }, + }), + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'WithInterface', + fields: { + friendly: { type: friendlyType }, + }, + }), + types: [dogType, humanType], + }); + + await testSchema(schema); + }); + + it('converts a schema with an implicit interface', async () => { + const friendlyType = new GraphQLInterfaceType({ + name: 'Friendly', + fields: () => ({ + bestFriend: { + type: friendlyType, + description: 'The best friend of this friendly thing', + }, + }), + }); + const dogType = new GraphQLObjectType({ + name: 'Dog', + interfaces: [friendlyType], + fields: () => ({ + bestFriend: { type: dogType }, + }), + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'WithInterface', + fields: { + dog: { type: dogType }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with a union', async () => { + const dogType = new GraphQLObjectType({ + name: 'Dog', + fields: () => ({ + bestFriend: { type: friendlyType }, + }), + }); + const humanType = new GraphQLObjectType({ + name: 'Human', + fields: () => ({ + bestFriend: { type: friendlyType }, + }), + }); + const friendlyType = new GraphQLUnionType({ + name: 'Friendly', + types: [dogType, humanType], + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'WithUnion', + fields: { + friendly: { type: friendlyType }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with complex field values', async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'ComplexFields', + fields: { + string: { type: GraphQLString }, + listOfString: { type: new GraphQLList(GraphQLString) }, + nonNullString: { + type: new GraphQLNonNull(GraphQLString), + }, + nonNullListOfString: { + type: new GraphQLNonNull(new GraphQLList(GraphQLString)), + }, + nonNullListOfNonNullString: { + type: new GraphQLNonNull( + new GraphQLList(new GraphQLNonNull(GraphQLString)), + ), + }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with field arguments', async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'ArgFields', + fields: { + one: { + description: 'A field with a single arg', + type: GraphQLString, + args: { + intArg: { + description: 'This is an int arg', + type: GraphQLInt, + }, + }, + }, + two: { + description: 'A field with a two args', + type: GraphQLString, + args: { + listArg: { + description: 'This is an list of int arg', + type: new GraphQLList(GraphQLInt), + }, + requiredArg: { + description: 'This is a required arg', + type: new GraphQLNonNull(GraphQLBoolean), + }, + }, + }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with default value on custom scalar field', async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'ArgFields', + fields: { + testField: { + type: GraphQLString, + args: { + testArg: { + type: new GraphQLScalarType({ + name: 'CustomScalar', + serialize: value => value, + }), + defaultValue: 'default', + }, + }, + }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with an enum', async () => { + const foodEnum = new GraphQLEnumType({ + name: 'Food', + description: 'Varieties of food stuffs', + values: { + VEGETABLES: { + description: 'Foods that are vegetables.', + value: 1, + }, + FRUITS: { + description: 'Foods that are fruits.', + value: 2, + }, + OILS: { + description: 'Foods that are oils.', + value: 3, + }, + DAIRY: { + description: 'Foods that are dairy.', + value: 4, + }, + MEAT: { + description: 'Foods that are meat.', + value: 5, + }, + }, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'EnumFields', + fields: { + food: { + description: 'Repeats the arg you give it', + type: foodEnum, + args: { + kind: { + description: 'what kind of food?', + type: foodEnum, + }, + }, + }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with an input object', async () => { + const addressType = new GraphQLInputObjectType({ + name: 'Address', + description: 'An input address', + fields: { + street: { + description: 'What street is this address?', + type: new GraphQLNonNull(GraphQLString), + }, + city: { + description: 'The city the address is within?', + type: new GraphQLNonNull(GraphQLString), + }, + country: { + description: 'The country (blank will assume USA).', + type: GraphQLString, + defaultValue: 'USA', + }, + }, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'HasInputObjectFields', + fields: { + geocode: { + description: 'Get a geocode from an address', + type: GraphQLString, + args: { + address: { + description: 'The address to lookup', + type: addressType, + }, + }, + }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with field arguments with default values', async () => { + const geoType = new GraphQLInputObjectType({ + name: 'Geo', + fields: { + lat: { type: GraphQLFloat }, + lon: { type: GraphQLFloat }, + }, + }); + + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'ArgFields', + fields: { + defaultInt: { + type: GraphQLString, + args: { + intArg: { + type: GraphQLInt, + defaultValue: 10, + }, + }, + }, + defaultList: { + type: GraphQLString, + args: { + listArg: { + type: new GraphQLList(GraphQLInt), + defaultValue: [1, 2, 3], + }, + }, + }, + defaultObject: { + type: GraphQLString, + args: { + objArg: { + type: geoType, + defaultValue: { lat: 37.485, lon: -122.148 }, + }, + }, + }, + defaultNull: { + type: GraphQLString, + args: { + intArg: { + type: GraphQLInt, + defaultValue: null, + }, + }, + }, + noDefault: { + type: GraphQLString, + args: { + intArg: { + type: GraphQLInt, + }, + }, + }, + }, + }), + }); + + await testSchema(schema); + }); + + it('converts a schema with custom directives', async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Simple', + description: 'This is a simple type', + fields: { + string: { + type: GraphQLString, + description: 'This is a string field', + }, + }, + }), + directives: [ + new GraphQLDirective({ + name: 'customDirective', + description: 'This is a custom directive', + locations: ['FIELD'], + }), + ], + }); + + await testSchema(schema); + }); + + it('converts a schema aware of deprecation', async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Simple', + description: 'This is a simple type', + fields: { + shinyString: { + type: GraphQLString, + description: 'This is a shiny string field', + }, + deprecatedString: { + type: GraphQLString, + description: 'This is a deprecated string field', + deprecationReason: 'Use shinyString', + }, + color: { + type: new GraphQLEnumType({ + name: 'Color', + values: { + RED: { description: 'So rosy' }, + GREEN: { description: 'So grassy' }, + BLUE: { description: 'So calming' }, + MAUVE: { + description: 'So sickening', + deprecationReason: 'No longer in fashion', + }, + }, + }), + }, + }, + }), + }); + + await testSchema(schema); + }); + + describe('very deep decorators', () => { + it('succeeds on deep (<= 7 levels) types', async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + foo: { + // e.g., fully non-null 3D matrix + type: new GraphQLNonNull( + new GraphQLList( + new GraphQLNonNull( + new GraphQLList( + new GraphQLNonNull( + new GraphQLList(new GraphQLNonNull(GraphQLString)), + ), + ), + ), + ), + ), + }, + }, + }), + }); + + await testSchema(schema); + }); + }); +}); diff --git a/src/utilities/cleanIntrospectionResponse.js b/src/utilities/cleanIntrospectionResponse.js new file mode 100644 index 0000000000..cd4187a69b --- /dev/null +++ b/src/utilities/cleanIntrospectionResponse.js @@ -0,0 +1,325 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +import { TypeKind } from '../type/introspection'; +import type { + IntrospectionDirective, + IntrospectionField, + IntrospectionInputTypeRef, + IntrospectionInputValue, + IntrospectionOutputTypeRef, + IntrospectionQuery, + IntrospectionSchema, + IntrospectionType, +} from './introspectionQuery'; +import type { DirectiveLocationEnum } from '../language/directiveLocation'; + +import invariant from 'invariant'; + +type RawIntrospectionResponse = { + __schema: { + queryType: { name: string }, + mutationType: ?{ name: string }, + subscriptionType: ?{ name: string }, + types: Array, + directives: Array, + }, +}; +type RawIntrospectionDirective = { + name: string, + description: ?string, + locations: Array, + args: Array, +}; +type RawIntrospectionFullType = { + kind: $Keys, + name: string, + description: ?string, + fields: ?Array, + inputFields: ?Array, + interfaces: ?Array, + enumValues: ?Array, + possibleTypes: ?Array, +}; +type RawIntrospectionField = { + name: string, + description: ?string, + args: Array, + type: RawIntrospectionTypeRef, + isDeprecated: boolean, + deprecationReason: ?string, +}; +type RawIntrospectionEnumValue = { + name: string, + description: ?string, + isDeprecated: boolean, + deprecationReason: ?string, +}; +type RawIntrospectionInputValue = { + name: string, + description: ?string, + type: RawIntrospectionTypeRef, + defaultValue: ?string, +}; +type RawIntrospectionTypeRef = { + kind: $Keys, + name: ?string, + ofType: ?RawIntrospectionTypeRef, +}; + +/** + * When receiving an IntrospectionQuery response, there will be many null + * fields on types that can only have null values. For instance, + * 'IntrospectionListType' will have a 'name: null' field. + * + * Similarly, we may not request certain fields, like the 'kind' field on + * '__schema.queryType'. While not strictly necessary for the response, + * it's easier to work with a TypeRef when the reference exactly matches the + * declared. 'IntrospectionTypeRef' + * + * To make 'IntrospectionQuery' easier to work with, we provide a utility for + * cleaning the "raw response" objects, and producing a new, + * exactly-type-matching IntrospectionQuery. + */ +export function cleanIntrospectionResponse( + response: RawIntrospectionResponse, +): IntrospectionQuery { + const raw = response.__schema; + return { + __schema: { + queryType: objectTypeRef(raw.queryType.name), + mutationType: raw.mutationType && objectTypeRef(raw.mutationType.name), + subscriptionType: + raw.subscriptionType && objectTypeRef(raw.subscriptionType.name), + directives: raw.directives.map(getDirective), + types: raw.types.map(getType), + }, + }; +} + +function getType(raw: RawIntrospectionFullType): IntrospectionType { + switch (raw.kind) { + case TypeKind.SCALAR: + return { + kind: TypeKind.SCALAR, + name: raw.name, + description: raw.description, + }; + case TypeKind.ENUM: + const rawEnumValues = raw.enumValues; + invariant(rawEnumValues, `Enum ${raw.name} has no enumValues`); + return { + kind: TypeKind.ENUM, + name: raw.name, + description: raw.description, + enumValues: rawEnumValues.map(enumValue => { + return { + name: enumValue.name, + description: enumValue.description, + isDeprecated: enumValue.isDeprecated, + deprecationReason: enumValue.deprecationReason, + }; + }), + }; + case TypeKind.OBJECT: + const rawFields = raw.fields; + invariant(rawFields, `Object ${raw.name} has null fields`); + const rawInterfaces = raw.interfaces; + invariant(rawInterfaces, `Object ${raw.name} has null interfaces`); + return { + kind: TypeKind.OBJECT, + name: raw.name, + description: raw.description, + fields: rawFields.map(getField), + interfaces: rawInterfaces.map(iface => { + invariant(iface.name, 'Unnamed Interface'); + return { kind: TypeKind.INTERFACE, name: iface.name }; + }), + }; + case TypeKind.INTERFACE: + const rawInterfaceFields = raw.fields; + invariant(rawInterfaceFields, `Interface ${raw.name} has null fields`); + const rawPossibleTypes = raw.possibleTypes; + invariant( + rawPossibleTypes, + `Interface ${raw.name} has null possibleTypes`, + ); + return { + kind: TypeKind.INTERFACE, + name: raw.name, + description: raw.description, + fields: rawInterfaceFields.map(getField), + possibleTypes: rawPossibleTypes.map(obj => { + invariant(obj.name, 'Unnamed Object'); + return { kind: TypeKind.OBJECT, name: obj.name }; + }), + }; + case TypeKind.UNION: + const rawUnionTypes = raw.possibleTypes; + invariant(rawUnionTypes, `Union ${raw.name} has null possibleTypes`); + return { + kind: TypeKind.UNION, + name: raw.name, + description: raw.description, + possibleTypes: rawUnionTypes.map(obj => { + invariant(obj.name, 'Unnamed Object'); + return { kind: TypeKind.OBJECT, name: obj.name }; + }), + }; + case TypeKind.INPUT_OBJECT: + const rawInputFields = raw.inputFields; + invariant( + rawInputFields, + `Input Object ${raw.name} has null inputFields`, + ); + return { + kind: TypeKind.INPUT_OBJECT, + name: raw.name, + description: raw.description, + inputFields: rawInputFields.map(getInputValue), + }; + default: + throw new Error(`Unknown named type: ${raw.kind}`); + } +} + +function getDirective(raw: RawIntrospectionDirective): IntrospectionDirective { + return { + name: raw.name, + description: raw.description, + locations: raw.locations, + args: raw.args.map(getInputValue), + }; +} + +function getField(raw: RawIntrospectionField): IntrospectionField { + return { + name: raw.name, + description: raw.description, + args: raw.args.map(getInputValue), + type: getOutputTypeRef(raw.type), + isDeprecated: raw.isDeprecated, + deprecationReason: raw.deprecationReason, + }; +} + +function getInputValue( + raw: RawIntrospectionInputValue, +): IntrospectionInputValue { + return { + name: raw.name, + description: raw.description, + type: getInputTypeRef(raw.type), + defaultValue: raw.defaultValue, + }; +} + +function getInputTypeRef( + raw: RawIntrospectionTypeRef, +): IntrospectionInputTypeRef { + switch (raw.kind) { + case TypeKind.LIST: + const listOf = raw.ofType; + if (!listOf) { + throw new Error('Decorated type deeper than introspection query.'); + } + return { kind: TypeKind.LIST, ofType: getInputTypeRef(listOf) }; + + case TypeKind.NON_NULL: + const childTypeRef = raw.ofType; + invariant( + childTypeRef, + 'Decorated type deeper than introspection query.', + ); + const nonNullOf = getInputTypeRef(childTypeRef); + invariant( + nonNullOf.kind !== TypeKind.NON_NULL, + 'NonNull ofType is a NonNull', + ); + return { kind: TypeKind.NON_NULL, ofType: nonNullOf }; + + case TypeKind.SCALAR: + case TypeKind.ENUM: + case TypeKind.INPUT_OBJECT: + const name = raw.name; + invariant(name, `Unnamed ${raw.kind} type`); + return { kind: introspectionInputKind(raw.kind), name }; + default: + throw new Error(`Unknown input type: ${raw.kind}`); + } +} + +function getOutputTypeRef( + raw: RawIntrospectionTypeRef, +): IntrospectionOutputTypeRef { + switch (raw.kind) { + case TypeKind.LIST: + const listOf = raw.ofType; + if (!listOf) { + throw new Error('Decorated type deeper than introspection query.'); + } + return { kind: TypeKind.LIST, ofType: getOutputTypeRef(listOf) }; + + case TypeKind.NON_NULL: + const childTypeRef = raw.ofType; + invariant( + childTypeRef, + 'Decorated type deeper than introspection query.', + ); + const nonNullOf = getOutputTypeRef(childTypeRef); + invariant( + nonNullOf.kind !== TypeKind.NON_NULL, + 'NonNull ofType is a NonNull', + ); + return { kind: TypeKind.NON_NULL, ofType: nonNullOf }; + + case TypeKind.SCALAR: + case TypeKind.ENUM: + case TypeKind.OBJECT: + case TypeKind.INTERFACE: + case TypeKind.UNION: + const name = raw.name; + invariant(name, `Unnamed ${raw.kind} type`); + return { kind: introspectionOutputKind(raw.kind), name }; + default: + throw new Error(`Unknown output type: ${raw.kind}`); + } +} + +function objectTypeRef(name: string) { + return { kind: TypeKind.OBJECT, name }; +} + +function introspectionOutputKind(kind: string) { + if (kind === TypeKind.OBJECT) { + return TypeKind.OBJECT; + } else if (kind === TypeKind.INTERFACE) { + return TypeKind.INTERFACE; + } else if (kind === TypeKind.UNION) { + return TypeKind.UNION; + } else if (kind === TypeKind.SCALAR) { + return TypeKind.SCALAR; + } else if (kind === TypeKind.ENUM) { + return TypeKind.ENUM; + } + throw new Error(`No known output type for ${kind}`); +} + +function introspectionInputKind(kind: string) { + if (kind === TypeKind.SCALAR) { + return TypeKind.SCALAR; + } else if (kind === TypeKind.ENUM) { + return TypeKind.ENUM; + } else if (kind === TypeKind.INPUT_OBJECT) { + return TypeKind.INPUT_OBJECT; + } + throw new Error(`No known type for ${kind}`); +} diff --git a/src/utilities/introspectionFromSchema.js b/src/utilities/introspectionFromSchema.js new file mode 100644 index 0000000000..ce37e55d4d --- /dev/null +++ b/src/utilities/introspectionFromSchema.js @@ -0,0 +1,264 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +import { print } from '../language/printer'; +import { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLScalarType, + GraphQLUnionType, +} from '../type/definition'; +import type { + GraphQLArgument, + GraphQLField, + GraphQLNamedType, + GraphQLType, + GraphQLOutputType, +} from '../type/definition'; +import { GraphQLDirective } from '../type/directives'; +import { TypeKind } from '../type/introspection'; +import type { GraphQLSchema } from '../type/schema'; +import { astFromValue } from './astFromValue'; +import type { + IntrospectionQuery, + IntrospectionDirective, + IntrospectionField, + IntrospectionInputValue, + IntrospectionNamedTypeRef, + IntrospectionSchema, + IntrospectionType, + IntrospectionInputType, + IntrospectionOutputType, + IntrospectionInputTypeRef, + IntrospectionOutputTypeRef, +} from './introspectionQuery'; + +import invariant from 'invariant'; + +/** + * Build an IntrospectionQuery from a GraphQLSchema + * + * Useful for converting between the two Schema types, and is the inverse + * of buildClientSchema. The primary use case is outside of the server context, + * for instance when doing schema comparisons. + * + * This is a synchronous equivalent of: + * await graphql(schema, introspectionQuery) + */ +export function introspectionQueryFromGraphQLSchema( + schema: GraphQLSchema, +): IntrospectionQuery { + return { + __schema: introspectionSchemaFromGraphQLSchema(schema), + }; +} + +/** + * Build an IntrospectionSchema from a GraphQLSchema + * + * IntrospectionSchema is useful for utilities that care about type and field + * relationships, but do not need to traverse through those relationships. + */ +export function introspectionSchemaFromGraphQLSchema( + schema: GraphQLSchema, +): IntrospectionSchema { + function getType(type: GraphQLType): IntrospectionType { + if (type instanceof GraphQLObjectType) { + const fieldMap = type.getFields(); + return { + kind: TypeKind.OBJECT, + name: type.name, + description: type.description || null, + fields: Object.keys(fieldMap) + .map(k => fieldMap[k]) + .map(getField), + interfaces: type.getInterfaces().map(iface => { + return { kind: TypeKind.INTERFACE, name: iface.name }; + }), + }; + } else if (type instanceof GraphQLInterfaceType) { + const fieldMap = type.getFields(); + return { + kind: TypeKind.INTERFACE, + name: type.name, + description: type.description || null, + fields: Object.keys(fieldMap) + .map(k => fieldMap[k]) + .map(getField), + possibleTypes: schema + .getPossibleTypes(type) + .map(possibleType => objectTypeRef(possibleType.name)), + }; + } else if (type instanceof GraphQLUnionType) { + return { + kind: TypeKind.UNION, + name: type.name, + description: type.description || null, + possibleTypes: schema + .getPossibleTypes(type) + .map(possibleType => objectTypeRef(possibleType.name)), + }; + } else if (type instanceof GraphQLScalarType) { + return { + kind: TypeKind.SCALAR, + name: type.name, + description: type.description || null, + }; + } else if (type instanceof GraphQLEnumType) { + return { + kind: TypeKind.ENUM, + name: type.name, + description: type.description || null, + enumValues: type.getValues().map(enumValue => { + return { + name: enumValue.name, + description: enumValue.description || null, + isDeprecated: + enumValue.isDeprecated !== undefined && enumValue.isDeprecated, + deprecationReason: enumValue.deprecationReason || null, + }; + }), + }; + } else if (type instanceof GraphQLInputObjectType) { + const fieldMap = type.getFields(); + return { + kind: TypeKind.INPUT_OBJECT, + name: type.name, + description: type.description || null, + inputFields: Object.keys(fieldMap) + .map(k => fieldMap[k]) + .map(getInputValue), + }; + } + throw new Error(`No known type for ${type.toString()}`); + } + + function getDirective(directive: GraphQLDirective): IntrospectionDirective { + return { + name: directive.name, + description: directive.description || null, + locations: directive.locations, + args: directive.args.map(getInputValue), + }; + } + + function getField(field: GraphQLField<*, *>): IntrospectionField { + return { + name: field.name, + description: field.description || null, + args: field.args.map(getInputValue), + type: outputTypeRef(field.type), + isDeprecated: field.isDeprecated !== undefined && field.isDeprecated, + deprecationReason: field.deprecationReason || null, + }; + } + + function getInputValue(argument: GraphQLArgument): IntrospectionInputValue { + let defaultValue = null; + const argDefault = argument.defaultValue; + if (argDefault !== undefined) { + defaultValue = print(astFromValue(argDefault, argument.type)); + } + + return { + name: argument.name, + description: argument.description || null, + type: inputTypeRef(argument.type), + defaultValue, + }; + } + + const mutation = schema.getMutationType(); + const subscription = schema.getSubscriptionType(); + + const typeMap = schema.getTypeMap(); + return { + queryType: objectTypeRef(schema.getQueryType().name), + mutationType: mutation ? objectTypeRef(mutation.name) : null, + subscriptionType: subscription ? objectTypeRef(subscription.name) : null, + directives: schema.getDirectives().map(getDirective), + types: Object.keys(typeMap) + .map(k => typeMap[k]) + .map(getType), + }; +} + +function outputTypeRef(type: GraphQLOutputType): IntrospectionOutputTypeRef { + if (type instanceof GraphQLList) { + return { kind: TypeKind.LIST, ofType: outputTypeRef(type.ofType) }; + } + if (type instanceof GraphQLNonNull) { + const childTypeRef = outputTypeRef(type.ofType); + invariant( + childTypeRef.kind !== TypeKind.NON_NULL, + 'Found a NonNull type of a NonNull', + ); + return { kind: TypeKind.NON_NULL, ofType: childTypeRef }; + } + const namedRef: IntrospectionNamedTypeRef = { + kind: introspectionOutputKind(type), + name: type.name, + }; + return namedRef; +} + +function inputTypeRef(type: GraphQLType): IntrospectionInputTypeRef { + if (type instanceof GraphQLList) { + return { kind: TypeKind.LIST, ofType: inputTypeRef(type.ofType) }; + } + if (type instanceof GraphQLNonNull) { + const childTypeRef = inputTypeRef(type.ofType); + invariant( + childTypeRef.kind !== TypeKind.NON_NULL, + `Found a NonNull type of a NonNull: ${type.toString()}`, + ); + return { kind: TypeKind.NON_NULL, ofType: childTypeRef }; + } + const namedRef: IntrospectionNamedTypeRef = { + kind: introspectionInputKind(type), + name: type.name, + }; + return namedRef; +} + +function objectTypeRef(name: string) { + return { kind: TypeKind.OBJECT, name }; +} + +function introspectionOutputKind(type: GraphQLNamedType) { + if (type instanceof GraphQLObjectType) { + return TypeKind.OBJECT; + } else if (type instanceof GraphQLInterfaceType) { + return TypeKind.INTERFACE; + } else if (type instanceof GraphQLUnionType) { + return TypeKind.UNION; + } else if (type instanceof GraphQLScalarType) { + return TypeKind.SCALAR; + } else if (type instanceof GraphQLEnumType) { + return TypeKind.ENUM; + } + throw new Error(`No known output type for ${type.toString()}`); +} + +function introspectionInputKind(type: GraphQLNamedType) { + if (type instanceof GraphQLScalarType) { + return TypeKind.SCALAR; + } else if (type instanceof GraphQLEnumType) { + return TypeKind.ENUM; + } else if (type instanceof GraphQLInputObjectType) { + return TypeKind.INPUT_OBJECT; + } + throw new Error(`No known type for ${type.toString()}`); +} From 4844c466a2016cf733a9c6fd096dc601e12beb9b Mon Sep 17 00:00:00 2001 From: mmahoney Date: Mon, 4 Dec 2017 22:18:30 -0500 Subject: [PATCH 2/4] Update test description comment --- src/utilities/__tests__/introspectionFromSchema-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/__tests__/introspectionFromSchema-test.js b/src/utilities/__tests__/introspectionFromSchema-test.js index 92f6e74689..3545b27629 100644 --- a/src/utilities/__tests__/introspectionFromSchema-test.js +++ b/src/utilities/__tests__/introspectionFromSchema-test.js @@ -31,7 +31,7 @@ import { // Test property: // Given a server's schema, a client querying the introspectionQuery returns -// the exact same result as running the +// an uncleaned response that, once cleaned, exactly matches the direct // GraphQLSchema => IntrospectionQuery conversion async function testSchema(schema) { const serverResponse = await graphql(schema, introspectionQuery); From b70b9b28f6c447162c738ecbcab172f5ad12b3b2 Mon Sep 17 00:00:00 2001 From: mmahoney Date: Mon, 4 Dec 2017 22:31:22 -0500 Subject: [PATCH 3/4] Remove unused import --- src/utilities/cleanIntrospectionResponse.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utilities/cleanIntrospectionResponse.js b/src/utilities/cleanIntrospectionResponse.js index cd4187a69b..7cf93ff05a 100644 --- a/src/utilities/cleanIntrospectionResponse.js +++ b/src/utilities/cleanIntrospectionResponse.js @@ -17,7 +17,6 @@ import type { IntrospectionInputValue, IntrospectionOutputTypeRef, IntrospectionQuery, - IntrospectionSchema, IntrospectionType, } from './introspectionQuery'; import type { DirectiveLocationEnum } from '../language/directiveLocation'; From 68e26e89d518cd2ece9f2cb10b06779d13e1c497 Mon Sep 17 00:00:00 2001 From: mmahoney Date: Tue, 5 Dec 2017 12:01:51 -0500 Subject: [PATCH 4/4] Export in index, reduce conversion functions to a single introspectionFromSchema. This has the advantage of making clear the conversion is NOT meant to be used within a server Query context --- src/index.js | 4 +++ .../__tests__/introspectionFromSchema-test.js | 6 ++--- src/utilities/index.js | 6 +++++ src/utilities/introspectionFromSchema.js | 27 +++++-------------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/index.js b/src/index.js index 8bab21b217..0d9bf06966 100644 --- a/src/index.js +++ b/src/index.js @@ -273,6 +273,10 @@ export { getIntrospectionQuery, // Deprecated: use getIntrospectionQuery introspectionQuery, + // Convert a GraphQLSchema to an IntrospectionSchema + introspectionFromSchema, + // Clean a server-provided IntrospectionQuery to give exactly-typed objects + cleanIntrospectionResponse, // Gets the target Operation from a Document getOperationAST, // Build a GraphQLSchema from an introspection result. diff --git a/src/utilities/__tests__/introspectionFromSchema-test.js b/src/utilities/__tests__/introspectionFromSchema-test.js index 3545b27629..b92f45fb3d 100644 --- a/src/utilities/__tests__/introspectionFromSchema-test.js +++ b/src/utilities/__tests__/introspectionFromSchema-test.js @@ -8,7 +8,7 @@ import { describe, it } from 'mocha'; import { expect } from 'chai'; import { introspectionQuery } from '../introspectionQuery'; -import { introspectionQueryFromGraphQLSchema } from '../introspectionFromSchema'; +import { introspectionFromSchema } from '../introspectionFromSchema'; import { cleanIntrospectionResponse } from '../cleanIntrospectionResponse'; import { graphql, @@ -36,8 +36,8 @@ import { async function testSchema(schema) { const serverResponse = await graphql(schema, introspectionQuery); const serverIntrospection = cleanIntrospectionResponse(serverResponse.data); - const introspectionFromSchema = introspectionQueryFromGraphQLSchema(schema); - expect(introspectionFromSchema).to.deep.equal(serverIntrospection); + const fromSchema = introspectionFromSchema(schema); + expect(fromSchema).to.deep.equal(serverIntrospection.__schema); } describe('Type System: build introspection from schema', () => { diff --git a/src/utilities/index.js b/src/utilities/index.js index 54f24d8b6a..84dd3c7b1f 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -44,9 +44,15 @@ export { getOperationAST } from './getOperationAST'; // Build a GraphQLSchema from an introspection result. export { buildClientSchema } from './buildClientSchema'; +// Converting between GraphQLSchema and IntrospectionQuery +export { introspectionFromSchema } from './introspectionFromSchema'; + // Build a GraphQLSchema from GraphQL Schema language. export { buildASTSchema, buildSchema } from './buildASTSchema'; +// Clean a server-provided IntrospectionQuery to give exactly-typed objects +export { cleanIntrospectionResponse } from './cleanIntrospectionResponse'; + // Extends an existing GraphQLSchema from a parsed GraphQL Schema language AST. export { extendSchema } from './extendSchema'; diff --git a/src/utilities/introspectionFromSchema.js b/src/utilities/introspectionFromSchema.js index ce37e55d4d..08d11ff265 100644 --- a/src/utilities/introspectionFromSchema.js +++ b/src/utilities/introspectionFromSchema.js @@ -32,7 +32,6 @@ import { TypeKind } from '../type/introspection'; import type { GraphQLSchema } from '../type/schema'; import { astFromValue } from './astFromValue'; import type { - IntrospectionQuery, IntrospectionDirective, IntrospectionField, IntrospectionInputValue, @@ -47,31 +46,19 @@ import type { import invariant from 'invariant'; -/** - * Build an IntrospectionQuery from a GraphQLSchema - * - * Useful for converting between the two Schema types, and is the inverse - * of buildClientSchema. The primary use case is outside of the server context, - * for instance when doing schema comparisons. - * - * This is a synchronous equivalent of: - * await graphql(schema, introspectionQuery) - */ -export function introspectionQueryFromGraphQLSchema( - schema: GraphQLSchema, -): IntrospectionQuery { - return { - __schema: introspectionSchemaFromGraphQLSchema(schema), - }; -} - /** * Build an IntrospectionSchema from a GraphQLSchema * * IntrospectionSchema is useful for utilities that care about type and field * relationships, but do not need to traverse through those relationships. + * + * This is the inverse of buildClientSchema. The primary use case is outside + * of the server context, for instance when doing schema comparisons. + * + * This is a synchronous equivalent of: + * const {__schema} = await graphql(schema, introspectionQuery); */ -export function introspectionSchemaFromGraphQLSchema( +export function introspectionFromSchema( schema: GraphQLSchema, ): IntrospectionSchema { function getType(type: GraphQLType): IntrospectionType {