diff --git a/CHANGELOG.md b/CHANGELOG.md index d261184616..ea14028961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - `apollo-codegen-typescript` - - `apollo-codegen-core` - - + - Add new `unionTypes` and `interfaceTypes` properties to the exported IR JSON (when using the `json-modern` target), that list all unions and their types, as well as all interfaces and their implementing types [#2050](https://github.com/apollographql/apollo-tooling/pull/2050). - `apollo-env` - - `apollo-graphql` diff --git a/packages/apollo-codegen-core/src/__tests__/__snapshots__/jsonOutput.ts.snap b/packages/apollo-codegen-core/src/__tests__/__snapshots__/jsonOutput.ts.snap index 8890634635..1d498567f2 100644 --- a/packages/apollo-codegen-core/src/__tests__/__snapshots__/jsonOutput.ts.snap +++ b/packages/apollo-codegen-core/src/__tests__/__snapshots__/jsonOutput.ts.snap @@ -151,7 +151,9 @@ exports[`JSON output should generate JSON output for a mutation with an enum and } ] } - ] + ], + \\"unionTypes\\": [], + \\"interfaceTypes\\": [] }" `; @@ -300,7 +302,17 @@ exports[`JSON output should generate JSON output for a query with a fragment spr ] } ], - \\"typesUsed\\": [] + \\"typesUsed\\": [], + \\"unionTypes\\": [], + \\"interfaceTypes\\": [ + { + \\"name\\": \\"Character\\", + \\"types\\": [ + \\"Human\\", + \\"Droid\\" + ] + } + ] }" `; @@ -377,7 +389,17 @@ exports[`JSON output should generate JSON output for a query with a nested selec } ], \\"fragments\\": [], - \\"typesUsed\\": [] + \\"typesUsed\\": [], + \\"unionTypes\\": [], + \\"interfaceTypes\\": [ + { + \\"name\\": \\"Character\\", + \\"types\\": [ + \\"Human\\", + \\"Droid\\" + ] + } + ] }" `; @@ -468,6 +490,16 @@ exports[`JSON output should generate JSON output for a query with an enum variab } ] } + ], + \\"unionTypes\\": [], + \\"interfaceTypes\\": [ + { + \\"name\\": \\"Character\\", + \\"types\\": [ + \\"Human\\", + \\"Droid\\" + ] + } ] }" `; @@ -538,7 +570,9 @@ exports[`JSON output should generate JSON output for a subscription 1`] = ` } ], \\"fragments\\": [], - \\"typesUsed\\": [] + \\"typesUsed\\": [], + \\"unionTypes\\": [], + \\"interfaceTypes\\": [] }" `; @@ -658,6 +692,265 @@ exports[`JSON output should generate JSON output for an input object type with d } ] } + ], + \\"unionTypes\\": [], + \\"interfaceTypes\\": [] +}" +`; + +exports[`JSON output should list all interfaces and their implementing types under a \`interfaceTypes\` property 1`] = ` +"{ + \\"operations\\": [ + { + \\"filePath\\": \\"GraphQL request\\", + \\"operationName\\": \\"HeroForEpisode\\", + \\"operationType\\": \\"query\\", + \\"rootType\\": \\"Query\\", + \\"variables\\": [], + \\"source\\": \\"query HeroForEpisode {\\\\n hero(episode: JEDI) {\\\\n __typename\\\\n name\\\\n ... on Droid {\\\\n primaryFunction\\\\n }\\\\n }\\\\n}\\", + \\"fields\\": [ + { + \\"responseName\\": \\"hero\\", + \\"fieldName\\": \\"hero\\", + \\"type\\": \\"Character\\", + \\"args\\": [ + { + \\"name\\": \\"episode\\", + \\"value\\": \\"JEDI\\", + \\"type\\": \\"Episode\\" + } + ], + \\"isConditional\\": false, + \\"isDeprecated\\": false, + \\"fields\\": [ + { + \\"responseName\\": \\"__typename\\", + \\"fieldName\\": \\"__typename\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"name\\", + \\"fieldName\\": \\"name\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"description\\": \\"The name of the character\\", + \\"isDeprecated\\": false + } + ], + \\"fragmentSpreads\\": [], + \\"inlineFragments\\": [ + { + \\"typeCondition\\": \\"Droid\\", + \\"possibleTypes\\": [ + \\"Droid\\" + ], + \\"fields\\": [ + { + \\"responseName\\": \\"__typename\\", + \\"fieldName\\": \\"__typename\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"name\\", + \\"fieldName\\": \\"name\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"description\\": \\"What others call this droid\\", + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"primaryFunction\\", + \\"fieldName\\": \\"primaryFunction\\", + \\"type\\": \\"String\\", + \\"isConditional\\": false, + \\"description\\": \\"This droid's primary function\\", + \\"isDeprecated\\": false + } + ], + \\"fragmentSpreads\\": [] + } + ] + } + ], + \\"fragmentSpreads\\": [], + \\"inlineFragments\\": [], + \\"fragmentsReferenced\\": [], + \\"sourceWithFragments\\": \\"query HeroForEpisode {\\\\n hero(episode: JEDI) {\\\\n __typename\\\\n name\\\\n ... on Droid {\\\\n primaryFunction\\\\n }\\\\n }\\\\n}\\", + \\"operationId\\": \\"173a7ebf41abd014f3fc54ddd007f6182fca738bd44a1e2d1bb75503c8bf79f6\\" + } + ], + \\"fragments\\": [], + \\"typesUsed\\": [], + \\"unionTypes\\": [], + \\"interfaceTypes\\": [ + { + \\"name\\": \\"Character\\", + \\"types\\": [ + \\"Human\\", + \\"Droid\\" + ] + } ] }" `; + +exports[`JSON output should list all unions and their types under a \`unionTypes\` property 1`] = ` +"{ + \\"operations\\": [ + { + \\"filePath\\": \\"GraphQL request\\", + \\"operationName\\": \\"Search\\", + \\"operationType\\": \\"query\\", + \\"rootType\\": \\"Query\\", + \\"variables\\": [], + \\"source\\": \\"query Search {\\\\n search(text: \\\\\\"an\\\\\\") {\\\\n __typename\\\\n ... on Human {\\\\n name\\\\n height\\\\n }\\\\n ... on Droid {\\\\n name\\\\n primaryFunction\\\\n }\\\\n ... on Starship {\\\\n name\\\\n length\\\\n }\\\\n }\\\\n}\\", + \\"fields\\": [ + { + \\"responseName\\": \\"search\\", + \\"fieldName\\": \\"search\\", + \\"type\\": \\"[SearchResult]\\", + \\"args\\": [ + { + \\"name\\": \\"text\\", + \\"value\\": \\"an\\", + \\"type\\": \\"String\\" + } + ], + \\"isConditional\\": false, + \\"isDeprecated\\": false, + \\"fields\\": [ + { + \\"responseName\\": \\"__typename\\", + \\"fieldName\\": \\"__typename\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"isDeprecated\\": false + } + ], + \\"fragmentSpreads\\": [], + \\"inlineFragments\\": [ + { + \\"typeCondition\\": \\"Human\\", + \\"possibleTypes\\": [ + \\"Human\\" + ], + \\"fields\\": [ + { + \\"responseName\\": \\"__typename\\", + \\"fieldName\\": \\"__typename\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"name\\", + \\"fieldName\\": \\"name\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"description\\": \\"What this human calls themselves\\", + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"height\\", + \\"fieldName\\": \\"height\\", + \\"type\\": \\"Float\\", + \\"isConditional\\": false, + \\"description\\": \\"Height in the preferred unit, default is meters\\", + \\"isDeprecated\\": false + } + ], + \\"fragmentSpreads\\": [] + }, + { + \\"typeCondition\\": \\"Droid\\", + \\"possibleTypes\\": [ + \\"Droid\\" + ], + \\"fields\\": [ + { + \\"responseName\\": \\"__typename\\", + \\"fieldName\\": \\"__typename\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"name\\", + \\"fieldName\\": \\"name\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"description\\": \\"What others call this droid\\", + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"primaryFunction\\", + \\"fieldName\\": \\"primaryFunction\\", + \\"type\\": \\"String\\", + \\"isConditional\\": false, + \\"description\\": \\"This droid's primary function\\", + \\"isDeprecated\\": false + } + ], + \\"fragmentSpreads\\": [] + }, + { + \\"typeCondition\\": \\"Starship\\", + \\"possibleTypes\\": [ + \\"Starship\\" + ], + \\"fields\\": [ + { + \\"responseName\\": \\"__typename\\", + \\"fieldName\\": \\"__typename\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"name\\", + \\"fieldName\\": \\"name\\", + \\"type\\": \\"String!\\", + \\"isConditional\\": false, + \\"description\\": \\"The name of the starship\\", + \\"isDeprecated\\": false + }, + { + \\"responseName\\": \\"length\\", + \\"fieldName\\": \\"length\\", + \\"type\\": \\"Float\\", + \\"isConditional\\": false, + \\"description\\": \\"Length of the starship, along the longest axis\\", + \\"isDeprecated\\": false + } + ], + \\"fragmentSpreads\\": [] + } + ] + } + ], + \\"fragmentSpreads\\": [], + \\"inlineFragments\\": [], + \\"fragmentsReferenced\\": [], + \\"sourceWithFragments\\": \\"query Search {\\\\n search(text: \\\\\\"an\\\\\\") {\\\\n __typename\\\\n ... on Human {\\\\n name\\\\n height\\\\n }\\\\n ... on Droid {\\\\n name\\\\n primaryFunction\\\\n }\\\\n ... on Starship {\\\\n name\\\\n length\\\\n }\\\\n }\\\\n}\\", + \\"operationId\\": \\"9887ff0652e14d678a66769b853c41293d4afb1d1338bbbdb9f84e66979605dd\\" + } + ], + \\"fragments\\": [], + \\"typesUsed\\": [], + \\"unionTypes\\": [ + { + \\"name\\": \\"SearchResult\\", + \\"types\\": [ + \\"Human\\", + \\"Droid\\", + \\"Starship\\" + ] + } + ], + \\"interfaceTypes\\": [] +}" +`; diff --git a/packages/apollo-codegen-core/src/__tests__/jsonOutput.ts b/packages/apollo-codegen-core/src/__tests__/jsonOutput.ts index 003946a273..99200baebb 100644 --- a/packages/apollo-codegen-core/src/__tests__/jsonOutput.ts +++ b/packages/apollo-codegen-core/src/__tests__/jsonOutput.ts @@ -1,4 +1,4 @@ -import { GraphQLSchema, buildSchema, parse } from "graphql"; +import { GraphQLSchema, buildSchema, parse, GraphQLUnionType } from "graphql"; import { compileToLegacyIR } from "../compiler/legacyIR"; import serializeToJSON from "../serializeToJSON"; @@ -157,4 +157,58 @@ describe("JSON output", function() { expect(output).toMatchSnapshot(); }); + + test("should list all unions and their types under a `unionTypes` property", function() { + const context = compileFromSource(` + query Search { + search(text: "an") { + __typename + ... on Human { + name + height + } + ... on Droid { + name + primaryFunction + } + ... on Starship { + name + length + } + } + } + `); + + expect(context.unionTypes.length).toBe(1); + const type = context.unionTypes[0] as GraphQLUnionType; + expect(type.name).toBe("SearchResult"); + expect(type.getTypes().length).toBe(3); + + const output = serializeToJSON(context); + expect(output).toMatchSnapshot(); + }); + + test("should list all interfaces and their implementing types under a `interfaceTypes` property", function() { + const context = compileFromSource(` + query HeroForEpisode { + hero(episode: JEDI) { + name + ... on Droid { + primaryFunction + } + } + } + `); + + expect(context.interfaceTypes.size).toBe(1); + const [ + interfaceType, + implementors + ] = context.interfaceTypes.entries().next().value; + expect(interfaceType.toString()).toBe("Character"); + expect(implementors.length).toBe(2); + + const output = serializeToJSON(context); + expect(output).toMatchSnapshot(); + }); }); diff --git a/packages/apollo-codegen-core/src/compiler/index.ts b/packages/apollo-codegen-core/src/compiler/index.ts index 690a946ca5..481b1b1326 100644 --- a/packages/apollo-codegen-core/src/compiler/index.ts +++ b/packages/apollo-codegen-core/src/compiler/index.ts @@ -12,6 +12,8 @@ import { GraphQLSchema, GraphQLType, GraphQLCompositeType, + GraphQLUnionType, + GraphQLInterfaceType, DocumentNode, OperationDefinitionNode, FragmentDefinitionNode, @@ -23,8 +25,8 @@ import { isEnumType, isInputObjectType, isScalarType, - NamedTypeNode, - ListTypeNode, + isUnionType, + isInterfaceType, TypeNode, parseType } from "graphql"; @@ -61,6 +63,11 @@ export interface CompilerContext { operations: { [operationName: string]: Operation }; fragments: { [fragmentName: string]: Fragment }; options: CompilerOptions; + unionTypes: GraphQLUnionType[]; + interfaceTypes: Map< + GraphQLInterfaceType, + (GraphQLObjectType | GraphQLInterfaceType)[] + >; } export interface Operation { @@ -206,14 +213,29 @@ export function compileToIR( } const typesUsed = compiler.typesUsed; - - return { schema, typesUsed, operations, fragments, options }; + const unionTypes = compiler.unionTypes; + const interfaceTypes = compiler.interfaceTypes; + + return { + schema, + typesUsed, + operations, + fragments, + options, + unionTypes, + interfaceTypes + }; } class Compiler { options: CompilerOptions; schema: GraphQLSchema; typesUsedSet: Set; + unionTypesSet: Set; + interfaceTypesMap: Map< + GraphQLInterfaceType, + (GraphQLObjectType | GraphQLInterfaceType)[] + >; unresolvedFragmentSpreads: FragmentSpread[] = []; @@ -222,6 +244,8 @@ class Compiler { this.options = options; this.typesUsedSet = new Set(); + this.unionTypesSet = new Set(); + this.interfaceTypesMap = new Map(); } addTypeUsed(type: GraphQLType) { @@ -245,6 +269,31 @@ class Compiler { return Array.from(this.typesUsedSet); } + addUnionType(type: GraphQLType) { + if (isUnionType(type)) { + if (this.unionTypesSet.has(type)) return; + this.unionTypesSet.add(type); + } + } + + get unionTypes(): GraphQLUnionType[] { + return Array.from(this.unionTypesSet); + } + + addInterfaceType(type: GraphQLType) { + if (isInterfaceType(type)) { + if (this.interfaceTypesMap.has(type)) return; + this.interfaceTypesMap.set(type, this.possibleTypesForType(type)); + } + } + + get interfaceTypes(): Map< + GraphQLInterfaceType, + (GraphQLObjectType | GraphQLInterfaceType)[] + > { + return this.interfaceTypesMap; + } + compileOperation(operationDefinition: OperationDefinitionNode): Operation { if (!operationDefinition.name) { throw new Error("Operations should be named"); @@ -372,6 +421,8 @@ class Compiler { const unmodifiedFieldType = getNamedType(fieldType); this.addTypeUsed(unmodifiedFieldType); + this.addUnionType(unmodifiedFieldType); + this.addInterfaceType(unmodifiedFieldType); const { description, isDeprecated, deprecationReason } = fieldDef; diff --git a/packages/apollo-codegen-core/src/compiler/legacyIR.ts b/packages/apollo-codegen-core/src/compiler/legacyIR.ts index d2dc8535f5..9685dfbd0b 100644 --- a/packages/apollo-codegen-core/src/compiler/legacyIR.ts +++ b/packages/apollo-codegen-core/src/compiler/legacyIR.ts @@ -4,6 +4,8 @@ import { GraphQLObjectType, GraphQLCompositeType, GraphQLInputType, + GraphQLUnionType, + GraphQLInterfaceType, DocumentNode, TypeNode } from "graphql"; @@ -39,6 +41,11 @@ export interface LegacyCompilerContext { fragments: { [fragmentName: string]: LegacyFragment }; typesUsed: GraphQLType[]; options: CompilerOptions; + unionTypes: GraphQLUnionType[]; + interfaceTypes: Map< + GraphQLInterfaceType, + (GraphQLObjectType | GraphQLInterfaceType)[] + >; } export interface LegacyOperation { @@ -185,7 +192,9 @@ class LegacyIRTransformer { operations, fragments, typesUsed: this.context.typesUsed, - options: this.options + options: this.options, + unionTypes: this.context.unionTypes, + interfaceTypes: this.context.interfaceTypes }; return legacyContext; diff --git a/packages/apollo-codegen-core/src/serializeToJSON.ts b/packages/apollo-codegen-core/src/serializeToJSON.ts index 90dd5a568f..75758f3182 100644 --- a/packages/apollo-codegen-core/src/serializeToJSON.ts +++ b/packages/apollo-codegen-core/src/serializeToJSON.ts @@ -4,9 +4,14 @@ import { GraphQLScalarType, GraphQLEnumType, GraphQLInputObjectType, + GraphQLUnionType, + GraphQLInterfaceType, + GraphQLObjectType, isEnumType, isInputObjectType, isScalarType, + isUnionType, + isInterfaceType, parseType } from "graphql"; @@ -25,13 +30,15 @@ interface serializeOptions { export default function serializeToJSON( context: LegacyCompilerContext | CompilerContext, - options: serializeOptions + options?: serializeOptions ) { return serializeAST( { operations: Object.values(context.operations), fragments: Object.values(context.fragments), - typesUsed: context.typesUsed.map(type => serializeType(type, options)) + typesUsed: context.typesUsed.map(type => serializeType(type, options)), + unionTypes: context.unionTypes.map(type => serializeType(type, options)), + interfaceTypes: serializeInterfaceTypes(context.interfaceTypes) }, "\t" ); @@ -51,13 +58,15 @@ export function serializeAST(ast: any, space?: string) { ); } -function serializeType(type: GraphQLType, options: serializeOptions) { +function serializeType(type: GraphQLType, options?: serializeOptions) { if (isEnumType(type)) { return serializeEnumType(type); } else if (isInputObjectType(type)) { return serializeInputObjectType(type, options); } else if (isScalarType(type)) { return serializeScalarType(type); + } else if (isUnionType(type)) { + return serializeUnionType(type); } else { throw new Error(`Unexpected GraphQL type: ${type}`); } @@ -113,3 +122,30 @@ function serializeScalarType(type: GraphQLScalarType) { description }; } + +function serializeUnionType(type: GraphQLUnionType) { + const { name } = type; + return { + name, + types: type.getTypes() + }; +} + +function serializeInterfaceTypes( + interfaceTypes: Map< + GraphQLInterfaceType, + (GraphQLObjectType | GraphQLInterfaceType)[] + > +) { + const types: { + name: string; + types: (GraphQLObjectType | GraphQLInterfaceType)[]; + }[] = []; + for (let [interfaceType, implementors] of interfaceTypes) { + types.push({ + name: interfaceType.toString(), + types: implementors + }); + } + return types; +} diff --git a/packages/apollo/src/commands/client/__tests__/__snapshots__/generate.test.ts.snap b/packages/apollo/src/commands/client/__tests__/__snapshots__/generate.test.ts.snap index 33977c163f..dc46acde3e 100644 --- a/packages/apollo/src/commands/client/__tests__/__snapshots__/generate.test.ts.snap +++ b/packages/apollo/src/commands/client/__tests__/__snapshots__/generate.test.ts.snap @@ -96,6 +96,7 @@ export type SimpleQuery = { exports[`client:codegen writes json operations 1`] = ` Object { "fragments": Array [], + "interfaceTypes": Array [], "operations": Array [ Object { "fields": Array [ @@ -125,12 +126,14 @@ Object { }, ], "typesUsed": Array [], + "unionTypes": Array [], } `; exports[`client:codegen writes json operations with typeNode (json-modern) 1`] = ` Object { "fragments": Array [], + "interfaceTypes": Array [], "operations": Array [ Object { "fields": Array [ @@ -170,6 +173,7 @@ Object { }, ], "typesUsed": Array [], + "unionTypes": Array [], } `;