apollo-codegen
Version:
Generate API code or type annotations based on a GraphQL schema and query documents
1,117 lines (1,002 loc) • 26.5 kB
JavaScript
import chai, { expect } from 'chai'
import chaiSubset from 'chai-subset'
chai.use(chaiSubset);
import { stripIndent } from 'common-tags'
import {
parse,
isType,
GraphQLID,
GraphQLString,
GraphQLList,
GraphQLNonNull
} from 'graphql';
import { loadSchema } from '../src/loading'
import { compileToIR } from '../src/compilation'
import { serializeAST } from '../src/serializeToJSON'
const schema = loadSchema(require.resolve('./starwars/schema.json'));
describe('Compiling query documents', () => {
it(`should include variables defined in operations`, () => {
const document = parse(`
query HeroName($episode: Episode) {
hero(episode: $episode) {
name
}
}
query Search($text: String!) {
search(text: $text) {
... on Character {
name
}
}
}
mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
createReview(episode: $episode, review: $review) {
stars
commentary
}
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['HeroName']).variables).to.deep.equal(
[
{ name: 'episode', type: 'Episode' }
]
);
expect(filteredIR(operations['Search']).variables).to.deep.equal(
[
{ name: 'text', type: 'String!' }
]
);
expect(filteredIR(operations['CreateReviewForEpisode']).variables).to.deep.equal(
[
{ name: 'episode', type: 'Episode!' },
{ name: 'review', type: 'ReviewInput!' }
]
);
});
it(`should keep track of enums and input object types used in variables`, () => {
const document = parse(`
query HeroName($episode: Episode) {
hero(episode: $episode) {
name
}
}
query Search($text: String) {
search(text: $text) {
... on Character {
name
}
}
}
mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
createReview(episode: $episode, review: $review) {
stars
commentary
}
}
`);
const { typesUsed } = compileToIR(schema, document);
expect(filteredIR(typesUsed)).to.deep.equal(['Episode', 'ReviewInput', 'ColorInput']);
});
it(`should keep track of enums used in fields`, () => {
const document = parse(`
query Hero {
hero {
name
appearsIn
}
droid(id: "2001") {
appearsIn
}
}
`);
const { typesUsed } = compileToIR(schema, document);
expect(filteredIR(typesUsed)).to.deep.equal(['Episode']);
});
it(`should keep track of types used in fields of input objects`, () => {
const bookstore_schema = loadSchema(require.resolve('./bookstore/schema.json'));
const document = parse(`
query ListBooks {
books {
id name writtenBy { author { id name } }
}
}
mutation CreateBook($book: BookInput!) {
createBook(book: $book) {
id, name, writtenBy { author { id name } }
}
}
query ListPublishers {
publishers {
id name
}
}
query ListAuthors($publishedBy: PublishedByInput!) {
authors(publishedBy: $publishedBy) {
id name publishedBy { publisher { id name } }
}
}
`)
const { typesUsed } = compileToIR(bookstore_schema, document);
expect(filteredIR(typesUsed)).to.deep.include('IdInput');
expect(filteredIR(typesUsed)).to.deep.include('WrittenByInput');
});
it(`should include the original field name for an aliased field`, () => {
const document = parse(`
query HeroName {
r2: hero {
name
}
luke: hero(episode: EMPIRE) {
name
}
}
`);
const { operations } = compileToIR(schema, document);
expect(operations['HeroName'].fields[0].fieldName).to.equal("hero");
});
it(`should include field arguments`, () => {
const document = parse(`
query HeroName {
hero(episode: EMPIRE) {
name
}
}
`);
const { operations } = compileToIR(schema, document);
expect(operations['HeroName'].fields[0].args)
.to.deep.equal([{ name: "episode", value: "EMPIRE" }]);
});
it(`should include isOptional if a field has skip or include directives`, () => {
const document = parse(`
query HeroNameConditionalInclusion {
hero {
name @include(if: false)
}
}
query HeroNameConditionalExclusion {
hero {
name @skip(if: true)
}
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['HeroNameConditionalInclusion'])).to.deep.equal({
operationName: 'HeroNameConditionalInclusion',
operationType: 'query',
variables: [],
fragmentsReferenced: [],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!',
isConditional: true
},
],
fragmentSpreads: [],
inlineFragments: []
}
]
});
expect(filteredIR(operations['HeroNameConditionalExclusion'])).to.deep.equal({
operationName: 'HeroNameConditionalExclusion',
operationType: 'query',
variables: [],
fragmentsReferenced: [],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!',
isConditional: true
},
],
fragmentSpreads: [],
inlineFragments: []
}
]
});
});
it(`should recursively flatten inline fragments with type conditions that match the parent type`, () => {
const document = parse(`
query Hero {
hero {
id
... on Character {
name
... on Character {
id
appearsIn
}
id
}
}
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['Hero'])).to.deep.equal({
operationName: 'Hero',
operationType: 'query',
variables: [],
fragmentsReferenced: [],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [
{
responseName: 'id',
fieldName: 'id',
type: 'ID!'
},
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
},
{
responseName: 'appearsIn',
fieldName: 'appearsIn',
type: '[Episode]!'
}
],
fragmentSpreads: [],
inlineFragments: []
}
]
});
});
it(`should recursively include fragment spreads with type conditions that match the parent type`, () => {
const document = parse(`
query Hero {
hero {
id
...HeroDetails
}
}
fragment HeroDetails on Character {
id
...MoreHeroDetails
name
}
fragment MoreHeroDetails on Character {
appearsIn
id
}
`);
const { operations, fragments } = compileToIR(schema, document);
expect(filteredIR(operations['Hero'])).to.deep.equal({
operationName: 'Hero',
operationType: 'query',
variables: [],
fragmentsReferenced: ['HeroDetails', 'MoreHeroDetails'],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [
{
responseName: 'id',
fieldName: 'id',
type: 'ID!'
}
],
fragmentSpreads: ['HeroDetails', 'MoreHeroDetails'],
inlineFragments: [],
}
],
});
expect(filteredIR(fragments['HeroDetails'])).to.deep.equal({
fragmentName: 'HeroDetails',
typeCondition: 'Character',
fragmentsReferenced: ['MoreHeroDetails'],
fields: [
{
responseName: 'id',
fieldName: 'id',
type: 'ID!'
},
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
}
],
fragmentSpreads: ['MoreHeroDetails'],
inlineFragments: []
});
expect(filteredIR(fragments['MoreHeroDetails'])).to.deep.equal({
fragmentName: 'MoreHeroDetails',
typeCondition: 'Character',
fragmentsReferenced: [],
fields: [
{ responseName: 'appearsIn',
fieldName: 'appearsIn',
type: '[Episode]!'
},
{
responseName: 'id',
fieldName: 'id',
type: 'ID!'
}
],
fragmentSpreads: [],
inlineFragments: []
});
});
it(`should include fragment spreads from subselections`, () => {
const document = parse(`
query HeroAndFriends {
hero {
...HeroDetails
appearsIn
id
friends {
id
...HeroDetails
}
}
}
fragment HeroDetails on Character {
name
id
}
`);
const { operations, fragments } = compileToIR(schema, document);
expect(filteredIR(operations['HeroAndFriends'])).to.deep.equal({
operationName: 'HeroAndFriends',
operationType: 'query',
variables: [],
fragmentsReferenced: ['HeroDetails'],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [
{ responseName: 'appearsIn',
fieldName: 'appearsIn',
type: '[Episode]!'
},
{
responseName: 'id',
fieldName: 'id',
type: 'ID!'
},
{
responseName: 'friends',
fieldName: 'friends',
type: '[Character]',
fields: [
{
responseName: 'id',
fieldName: 'id',
type: 'ID!'
}
],
fragmentSpreads: ['HeroDetails'],
inlineFragments: []
}
],
fragmentSpreads: ['HeroDetails'],
inlineFragments: []
}
]
});
expect(filteredIR(fragments['HeroDetails'])).to.deep.equal({
fragmentName: 'HeroDetails',
typeCondition: 'Character',
fragmentsReferenced: [],
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
},
{
responseName: 'id',
fieldName: 'id',
type: 'ID!'
}
],
fragmentSpreads: [],
inlineFragments: []
});
});
it(`should include type conditions with merged fields for inline fragments`, () => {
const document = parse(`
query Hero {
hero {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['Hero'])).to.deep.equal({
operationName: 'Hero',
operationType: 'query',
variables: [],
fragmentsReferenced: [],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
}
],
fragmentSpreads: [],
inlineFragments: [
{
typeCondition: 'Droid',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
},
{
responseName: 'primaryFunction',
fieldName: 'primaryFunction',
type: 'String'
},
],
fragmentSpreads: []
},
{
typeCondition: 'Human',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
},
{
responseName: 'height',
fieldName: 'height',
type: 'Float'
},
],
fragmentSpreads: []
}
]
}
]
});
});
it(`should include fragment spreads with type conditions`, () => {
const document = parse(`
query Hero {
hero {
name
...DroidDetails
...HumanDetails
}
}
fragment DroidDetails on Droid {
primaryFunction
}
fragment HumanDetails on Human {
height
}
`);
const { operations, fragments } = compileToIR(schema, document);
expect(filteredIR(operations['Hero'])).to.deep.equal({
operationName: 'Hero',
operationType: 'query',
variables: [],
fragmentsReferenced: ['DroidDetails', 'HumanDetails'],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fragmentSpreads: ['DroidDetails', 'HumanDetails'],
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
}
],
inlineFragments: []
}
]
});
expect(filteredIR(fragments['DroidDetails'])).to.deep.equal({
fragmentName: 'DroidDetails',
typeCondition: 'Droid',
fragmentsReferenced: [],
fields: [
{
responseName: 'primaryFunction',
fieldName: 'primaryFunction',
type: 'String'
}
],
fragmentSpreads: [],
inlineFragments: []
});
expect(filteredIR(fragments['HumanDetails'])).to.deep.equal({
fragmentName: 'HumanDetails',
typeCondition: 'Human',
fragmentsReferenced: [],
fields: [
{
responseName: 'height',
fieldName: 'height',
type: 'Float'
}
],
fragmentSpreads: [],
inlineFragments: []
});
});
it(`should not include type conditions for fragment spreads with type conditions that match the parent type`, () => {
const document = parse(`
query Hero {
hero {
name
...HeroDetails
}
}
fragment HeroDetails on Character {
name
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['Hero'])).to.deep.equal({
operationName: 'Hero',
operationType: 'query',
variables: [],
fragmentsReferenced: ['HeroDetails'],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fragmentSpreads: ['HeroDetails'],
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
}
],
inlineFragments: []
}
],
});
});
it(`should include type conditions for inline fragments in fragments`, () => {
const document = parse(`
query Hero {
hero {
...HeroDetails
}
}
fragment HeroDetails on Character {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
`);
const { operations, fragments } = compileToIR(schema, document);
expect(filteredIR(operations['Hero'])).to.deep.equal({
operationName: 'Hero',
operationType: 'query',
variables: [],
fragmentsReferenced: ['HeroDetails'],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [],
fragmentSpreads: ['HeroDetails'],
inlineFragments: []
}
]
});
expect(filteredIR(fragments['HeroDetails'])).to.deep.equal({
fragmentName: 'HeroDetails',
typeCondition: 'Character',
fragmentsReferenced: [],
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
}
],
fragmentSpreads: [],
inlineFragments: [
{
typeCondition: 'Droid',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
},
{
responseName: 'primaryFunction',
fieldName: 'primaryFunction',
type: 'String'
},
],
fragmentSpreads: []
},
{
typeCondition: 'Human',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
},
{
responseName: 'height',
fieldName: 'height',
type: 'Float'
},
],
fragmentSpreads: []
}
]
});
});
it(`should inherit type condition when nesting an inline fragment in an inline fragment with a more specific type condition`, () => {
const document = parse(`
query HeroName {
hero {
... on Droid {
... on Character {
name
}
}
}
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['HeroName'])).to.deep.equal({
operationName: 'HeroName',
operationType: 'query',
variables: [],
fragmentsReferenced: [],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [],
fragmentSpreads: [],
inlineFragments: [
{
typeCondition: 'Droid',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
}
],
fragmentSpreads: []
}
]
}
]
});
});
it(`should not inherit type condition when nesting an inline fragment in an inline fragment with a less specific type condition`, () => {
const document = parse(`
query HeroName {
hero {
... on Character {
... on Droid {
name
}
}
}
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['HeroName'])).to.deep.equal({
operationName: 'HeroName',
operationType: 'query',
variables: [],
fragmentsReferenced: [],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [],
fragmentSpreads: [],
inlineFragments: [
{
typeCondition: 'Droid',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
}
],
fragmentSpreads: [],
}
]
}
]
});
});
it(`should inherit type condition when nesting a fragment spread in an inline fragment with a more specific type condition`, () => {
const document = parse(`
query HeroName {
hero {
... on Droid {
...HeroName
}
}
}
fragment HeroName on Character {
name
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['HeroName'])).to.deep.equal({
operationName: 'HeroName',
operationType: 'query',
variables: [],
fragmentsReferenced: ['HeroName'],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [],
fragmentSpreads: [],
inlineFragments: [
{
typeCondition: 'Droid',
fragmentSpreads: ['HeroName'],
fields: [],
}
]
}
]
});
});
it(`should not inherit type condition when nesting a fragment spread in an inline fragment with a less specific type condition`, () => {
const document = parse(`
query HeroName {
hero {
... on Character {
...DroidName
}
}
}
fragment DroidName on Droid {
name
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['HeroName'])).to.deep.equal({
operationName: 'HeroName',
operationType: 'query',
variables: [],
fragmentsReferenced: ['DroidName'],
fields: [
{
responseName: 'hero',
fieldName: 'hero',
type: 'Character',
fields: [],
fragmentSpreads: ['DroidName'],
inlineFragments: []
}
]
});
});
it(`should include type conditions for inline fragments on a union type`, () => {
const document = parse(`
query Search {
search(text: "an") {
... on Character {
name
}
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
`);
const { operations } = compileToIR(schema, document);
expect(filteredIR(operations['Search']).fields[0].inlineFragments).to.deep.equal([
{
typeCondition: 'Droid',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
},
{
responseName: 'primaryFunction',
fieldName: 'primaryFunction',
type: 'String'
},
],
fragmentSpreads: [],
},
{
typeCondition: 'Human',
fields: [
{
responseName: 'name',
fieldName: 'name',
type: 'String!'
},
{
responseName: 'height',
fieldName: 'height',
type: 'Float'
},
],
fragmentSpreads: [],
}
]);
});
it(`should keep track of fragments referenced in a subselection`, () => {
const document = parse(`
query HeroAndFriends {
hero {
name
friends {
...HeroDetails
}
}
}
fragment HeroDetails on Character {
name
}
`);
const { operations } = compileToIR(schema, document);
expect(operations['HeroAndFriends'].fragmentsReferenced).to.deep.equal(['HeroDetails']);
});
it(`should keep track of fragments referenced in a fragment within a subselection`, () => {
const document = parse(`
query HeroAndFriends {
hero {
...HeroDetails
}
}
fragment HeroDetails on Character {
friends {
...HeroName
}
}
fragment HeroName on Character {
name
}
`);
const { operations } = compileToIR(schema, document);
expect(operations['HeroAndFriends'].fragmentsReferenced).to.deep.equal(['HeroDetails', 'HeroName']);
});
it(`should keep track of fragments referenced in a subselection nested in an inline fragment`, () => {
const document = parse(`
query HeroAndFriends {
hero {
name
... on Droid {
friends {
...HeroDetails
}
}
}
}
fragment HeroDetails on Character {
name
}
`);
const { operations } = compileToIR(schema, document);
expect(operations['HeroAndFriends'].fragmentsReferenced).to.deep.equal(['HeroDetails']);
});
it(`should include the source of operations with __typename added for abstract types`, () => {
const source = stripIndent`
query HeroName {
hero {
name
}
}
`
const document = parse(source);
const { operations } = compileToIR(schema, document);
expect(operations['HeroName'].source).to.equal(stripIndent`
query HeroName {
hero {
__typename
name
}
}
`);
});
it(`should include the source of fragments with __typename added for abstract types`, () => {
const source = stripIndent`
fragment HeroDetails on Character {
name
}
`
const document = parse(source);
const { fragments } = compileToIR(schema, document);
expect(fragments['HeroDetails'].source).to.equal(stripIndent`
fragment HeroDetails on Character {
__typename
name
}
`);
});
it(`should include the operationType for a query`, () => {
const source = stripIndent`
query HeroName {
hero {
name
}
}
`
const document = parse(source);
const { operations } = compileToIR(schema, document);
expect(operations['HeroName'].operationType).to.equal('query');
});
it(`should include the operationType for a mutation`, () => {
const source = stripIndent`
mutation CreateReview {
createReview {
stars
commentary
}
}
`
const document = parse(source);
const { operations } = compileToIR(schema, document);
expect(operations['CreateReview'].operationType).to.equal('mutation');
});
});
function filteredIR(ir) {
return JSON.parse(serializeAST(ir), function(key, value) {
if (key === 'source') {
return undefined;
}
return value;
});
}