Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Commit

Permalink
include support for fragments, and fix possible issues with SSR
Browse files Browse the repository at this point in the history
  • Loading branch information
James Baxley committed Aug 12, 2016
1 parent 1070a10 commit 041f074
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 19 deletions.
27 changes: 24 additions & 3 deletions src/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
} from 'react';

// modules don't export ES6 modules
// import isObject = require('lodash.isobject');
import isEqual = require('lodash.isequal');
import flatten = require('lodash.flatten');
import shallowEqual from './shallowEqual';

import invariant = require('invariant');
Expand All @@ -23,6 +23,10 @@ import ApolloClient, {
readQueryFromStore,
} from 'apollo-client';

import {
createFragmentMap,
} from 'apollo-client/queries/getFromAST';

import {
ApolloError,
} from 'apollo-client/errors';
Expand Down Expand Up @@ -55,7 +59,7 @@ import { parser, DocumentType } from './parser';
export declare interface MutationOptions {
variables?: Object;
resultBehaviors?: MutationBehavior[];
fragments?: FragmentDefinition[];
fragments?: FragmentDefinition[] | FragmentDefinition[][];
optimisticResponse?: Object;
updateQueries?: MutationQueryReducersMap;
}
Expand All @@ -67,7 +71,7 @@ export declare interface QueryOptions {
returnPartialData?: boolean;
noFetch?: boolean;
pollInterval?: number;
fragments?: FragmentDefinition[];
fragments?: FragmentDefinition[] | FragmentDefinition[][];
skip?: boolean;
}

Expand Down Expand Up @@ -148,6 +152,16 @@ export default function graphql(

const graphQLDisplayName = `Apollo(${getDisplayName(WrappedComponent)})`;

function calculateFragments(opts) {
if (opts.fragments || operation.fragments.length) {
if (!opts.fragments) {
opts.fragments = flatten([...operation.fragments]);
} else {
opts.fragments = flatten([...opts.fragments, ...operation.fragments]);
}
}
}

function calculateVariables(props) {
const opts = mapPropsToOptions(props);
if (opts.variables || !operation.variables.length) return opts.variables;
Expand Down Expand Up @@ -179,13 +193,15 @@ export default function graphql(
if (opts.ssr === false) return false;
if (!opts.variables) opts.variables = calculateVariables(props);
if (!opts.variables) delete opts.variables;
calculateFragments(opts);

// if this query is in the store, don't block execution
try {
readQueryFromStore({
store: client.store.getState()[client.reduxRootKey].data,
query: opts.query,
variables: opts.variables,
fragmentMap: createFragmentMap(opts.fragments),
});
return false;
} catch (e) {/* tslint:disable-line */}
Expand All @@ -204,6 +220,9 @@ export default function graphql(
// for use with getData during SSR
static fetchData = operation.type === DocumentType.Query ? fetchData : false;

// start of query composition
static fragments: FragmentDefinition[] = operation.fragments;

// react / redux and react dev tools (HMR) needs
public props: any; // passed props
public version: number;
Expand Down Expand Up @@ -353,6 +372,7 @@ export default function graphql(
this.unsubscribeFromQuery();

const queryOptions: WatchQueryOptions = assign({ query: document }, opts);
calculateFragments(queryOptions);
const observableQuery = watchQuery(queryOptions);
const { queryId } = observableQuery;

Expand Down Expand Up @@ -503,6 +523,7 @@ export default function graphql(
if (typeof opts.variables === 'undefined') delete opts.variables;

(opts as any).mutation = document;
calculateFragments(opts);
return this.client.mutate((opts as any));
};

Expand Down
25 changes: 16 additions & 9 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import {
Document,
VariableDefinition,
OperationDefinition,
FragmentDefinition,
} from 'graphql';

import { createFragment } from 'apollo-client';

import invariant = require('invariant');

export enum DocumentType {
Expand All @@ -15,6 +18,7 @@ export interface IDocumentDefinition {
type: DocumentType;
name: string;
variables: VariableDefinition[];
fragments: FragmentDefinition[];
}

// the parser is mainly a safety check for the HOC
Expand All @@ -37,13 +41,10 @@ export function parser(document: Document): IDocumentDefinition {
(x: OperationDefinition) => x.kind === 'FragmentDefinition'
);

if (fragments.length) {
invariant(fragments.length === 0,
// tslint:disable-line
`Fragments should be passed to react-apollo as 'fragments' in the passed options. See http://docs.apollostack.com/apollo-client/fragments.html for more information about fragments when using apollo`
);
}

fragments = createFragment({
kind: 'Document',
definitions: [...fragments],
});

queries = document.definitions.filter(
(x: OperationDefinition) => x.kind === 'OperationDefinition' && x.operation === 'query'
Expand All @@ -53,6 +54,12 @@ export function parser(document: Document): IDocumentDefinition {
(x: OperationDefinition) => x.kind === 'OperationDefinition' && x.operation === 'mutation'
);

if (fragments.length && (!queries.length || !mutations.length)) {
invariant(true,
`Passing only a fragment to 'graphql' is not yet supported. You must include a query or mutation as well`
);
}

if (queries.length && mutations.length) {
invariant((queries.length && mutations.length),
// tslint:disable-line
Expand All @@ -73,6 +80,6 @@ export function parser(document: Document): IDocumentDefinition {
variables = definitions[0].variableDefinitions || [];
let hasName = definitions[0].name && definitions[0].name.kind === 'Name';
name = hasName ? definitions[0].name.value : 'data'; // fallback to using data if no name

return { name, type, variables };
fragments = fragments.length ? fragments : [];
return { name, type, variables, fragments };
}
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ function getQueriesFromTree(
let ComponentClass = type;
let ownProps = getPropsFromChild(component);
const Component = new ComponentClass(ownProps, context);
try { Component.props = ownProps; } catch(e) {} // tslint:disable-line
if (Component.componentWillMount) Component.componentWillMount();

let newContext = context;
if (Component.getChildContext) newContext = assign({}, context, Component.getChildContext());
Expand Down
3 changes: 1 addition & 2 deletions test/mocks/mockNetworkInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class MockNetworkInterface implements NetworkInterface {
const key = requestToKey(parsedRequest);

if (!this.mockedResponsesByKey[key]) {
throw new Error('No more mocked responses for the query: ' + request.query);
throw new Error('No more mocked responses for the query: ' + print(request.query));
}

const { result, error, delay } = this.mockedResponsesByKey[key].shift() || {} as any;
Expand All @@ -82,7 +82,6 @@ export class MockNetworkInterface implements NetworkInterface {

function requestToKey(request: ParsedRequest): string {
const queryString = request.query && print(request.query);

return JSON.stringify({
variables: request.variables,
debugName: request.debugName,
Expand Down
16 changes: 11 additions & 5 deletions test/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ describe('parser', () => {
// expect(parser('{ user { name } }')).to.throw();
// });

it('should error if fragments are included in the operation', () => {
const query = gql`fragment bookInfo on Book { name }`;
it('should dynamically create `FragmentDefinition` for included fragments', () => {
const query = gql`
fragment bookInfo on Book { name }
query getBook {
books {
...bookInfo
}
}
`;

try { parser(query); } catch (e) {
expect(e).to.match(/Invariant Violation: Fragments/);
}
const parsed = parser(query);
expect(parsed.fragments.length).to.equal(1);
});

it('should error if both a query and a mutation is present', () => {
Expand Down
170 changes: 170 additions & 0 deletions test/react-web/client/graphql/fragments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@

import * as React from 'react';
import * as chai from 'chai';
import { mount } from 'enzyme';
import gql from 'graphql-tag';

import ApolloClient, { createFragment } from 'apollo-client';

declare function require(name: string);
import chaiEnzyme = require('chai-enzyme');

chai.use(chaiEnzyme()); // Note the invocation at the end
const { expect } = chai;

import mockNetworkInterface from '../../../mocks/mockNetworkInterface';
import {
// Passthrough,
ProviderMock,
} from '../../../mocks/components';

import graphql from '../../../../src/graphql';

describe('fragments', () => {

// XXX in a later version, we should support this for composition
it('throws if you only pass a fragment', (done) => {
const query = gql`
fragment Failure on PeopleConnection { people { name } }
`;
const data = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } };
const networkInterface = mockNetworkInterface({ request: { query }, result: { data } });
const client = new ApolloClient({ networkInterface });

try {
@graphql(query)
class Container extends React.Component<any, any> {
componentWillReceiveProps(props) {
expect(props.data.loading).to.be.false;
expect(props.data.allPeople).to.deep.equal(data.allPeople);
done();
}
render() {
return null;
}
};

mount(<ProviderMock client={client}><Container /></ProviderMock>)
done(new Error('This should throw'))
} catch (e) {
// expect(e).to.match(/Invariant Violation/);
done();
}
});

it('correctly fetches a query with inline fragments', (done) => {
const query = gql`
query people { allPeople(first: 1) { ...person } }
fragment person on PeopleConnection { people { name } }
`;
const data = { allPeople: { people: [ { name: 'Luke Skywalker' } ] } };
const networkInterface = mockNetworkInterface({ request: { query }, result: { data } });
const client = new ApolloClient({ networkInterface });

@graphql(query)
class Container extends React.Component<any, any> {
componentWillReceiveProps(props) {
expect(props.data.loading).to.be.false;
expect(props.data.allPeople).to.deep.equal(data.allPeople);
done();
}
render() {
return null;
}
};

expect((Container as any).fragments.length).to.equal(1);

mount(<ProviderMock client={client}><Container /></ProviderMock>);
});

it('correctly merges a query with inline fragments and passed fragments', (done) => {
const query = gql`
query peopleAndShips {
allPeople(first: 1) { ...Person }
allShips(first: 1) { ...ships }
}
fragment Person on PeopleConnection { people { name } }
`;
const shipFragment = createFragment(gql`
fragment ships on ShipsConnection { starships { name } }
`);

const mockedQuery = gql`
query peopleAndShips {
allPeople(first: 1) { ...Person }
allShips(first: 1) { ...ships }
}
fragment Person on PeopleConnection { people { name } }
fragment ships on ShipsConnection { starships { name } }
`;

const data = {
allPeople: { people: [ { name: 'Luke Skywalker' } ] },
allShips: { starships: [ { name: 'CR90 corvette' } ] },
};
const networkInterface = mockNetworkInterface(
{ request: { query: mockedQuery }, result: { data } }
);
const client = new ApolloClient({ networkInterface });

@graphql(query, {
options: () => ({ fragments: [shipFragment]})
})
class Container extends React.Component<any, any> {
componentWillReceiveProps(props) {
expect(props.data.loading).to.be.false;
expect(props.data.allPeople).to.deep.equal(data.allPeople);
expect(props.data.allShips).to.deep.equal(data.allShips);
done();
}
render() {
return null;
}
};

expect((Container as any).fragments.length).to.equal(1);

mount(<ProviderMock client={client}><Container /></ProviderMock>);
});

it('correctly allows for passed fragments', (done) => {
const query = gql`
query ships { allShips(first: 1) { ...Ships } }
`;
const shipFragment = createFragment(gql`
fragment Ships on ShipsConnection { starships { name } }
`);

const mockedQuery = gql`
query ships { allShips(first: 1) { ...Ships } }
fragment Ships on ShipsConnection { starships { name } }
`;

const data = {
allShips: { starships: [ { name: 'CR90 corvette' } ] },
};
const networkInterface = mockNetworkInterface(
{ request: { query: mockedQuery }, result: { data } }
);
const client = new ApolloClient({ networkInterface });

@graphql(query, {
options: () => ({ fragments: [shipFragment]})
})
class Container extends React.Component<any, any> {
componentWillReceiveProps(props) {
expect(props.data.loading).to.be.false;
expect(props.data.allShips).to.deep.equal(data.allShips);
done();
}
render() {
return null;
}
};

mount(<ProviderMock client={client}><Container /></ProviderMock>);
});


});

0 comments on commit 041f074

Please sign in to comment.