apollo-client-devtools
Version:
Developer tools for Apollo Client, with GraphiQL integration, cache inspection, and query watching.
218 lines (189 loc) • 6.43 kB
JavaScript
import PropTypes from "prop-types";
import React, { Component } from "react";
import GraphiQL from "graphiql";
import uniqBy from "lodash.uniqby";
import flatten from "lodash.flattendeep";
import { parse } from "graphql/language/parser";
import { print } from "graphql/language/printer";
import {
printIntrospectionSchema,
buildSchema,
introspectionQuery,
printSchema,
buildClientSchema,
} from "graphql/utilities";
import { mergeSchemas } from "graphql-tools";
import { execute as graphql } from "graphql/execution";
import { Observable, execute, ApolloLink } from "apollo-link";
import { withBridge } from "../bridge";
import "./graphiql.less";
import "./graphiql-overrides.less";
let id = 0;
const introAST = parse(introspectionQuery);
const intro = introspectionQuery.replace(/\s/g, "");
export const createBridgeLink = bridge =>
new ApolloLink(
operation =>
new Observable(obs => {
const key = operation.toKey();
const { query, operationName, variables } = operation;
const complete = () => obs.complete();
const error = err => obs.error(JSON.parse(err));
const next = _result => {
const result = JSON.parse(_result);
// we use gql here because it caches ast transforms
if (
!result.extensions ||
!result.extensions.schemas ||
print(query).replace(/\s/g, "") !== intro
) {
// clean up some data
if (result.extensions) {
delete result.extensions.schemas;
if (Object.keys(result.extensions).length === 0) {
delete result.extensions;
}
}
obs.next(result);
return;
}
const { schemas } = result.extensions;
const directivesOnly = schemas
.filter(x => !x.definition)
.map(x => x.directives);
const definitions = schemas.filter(x => !!x.definition);
const built = definitions.map(({ definition, directives = "" }) =>
buildSchema(`${directives} ${definition}`),
);
let directives = built.map(({ _directives }) => _directives);
let merged;
// local only app
if (result.data && Object.keys(result.data).length !== 0) {
// local and remote app
// merge schemas together
let remoteSchema = buildClientSchema(result.data);
// add add-hoc client only directives
// XXX replace this with better directives => types function
// XXX this is a bottleneck but only happens once
let remoteSchemaString = printSchema(remoteSchema);
remoteSchemaString =
remoteSchemaString + ` ${directivesOnly.join("\n")}`;
remoteSchema = buildSchema(remoteSchemaString);
directives = directives.concat(remoteSchema._directives);
merged = mergeSchemas({
schemas: [remoteSchema].concat(built),
});
} else {
merged = mergeSchemas({ schemas: built });
}
merged._directives = uniqBy(
flatten(merged._directives.concat(directives)),
"name",
);
try {
const newResult = graphql(merged, introAST);
obs.next(newResult);
} catch (e) {
obs.error(e.stack);
}
};
// subscribe to this operation's information
bridge.on(`link:next:${key}`, next);
bridge.on(`link:error:${key}`, error);
bridge.on(`link:complete:${key}`, complete);
const payload = JSON.stringify({
query: print(query),
operationName,
variables,
key,
fetchPolicy: operation.getContext().noFetch
? "cache-only"
: "no-cache",
});
// fire the event for the link on the other side of the wall
bridge.send("link:operation", payload);
return () => {
bridge.removeListener(`link:next:${key}`, next);
bridge.removeListener(`link:error:${key}`, error);
bridge.removeListener(`link:complete:${key}`, complete);
};
}),
);
export class Explorer extends Component {
constructor(props, context) {
super(props, context);
this.state = {
noFetch: false,
query: this.props.query,
variables: this.props.variables,
};
this.link = createBridgeLink(this.props.bridge);
}
componentDidMount() {
if (this.props.query) {
if (this.props.automaticallyRunQuery) {
this.graphiql.handleRunQuery();
}
}
}
clearDefaultQueryState() {
this.setState({ query: undefined, variables: undefined });
}
fetcher = ({ query, variables = {} }) =>
execute(this.link, {
query: parse(query),
variables,
context: { noFetch: this.state.noFetch },
});
handleClickPrettifyButton = event => {
const editor = this.graphiql.getQueryEditor();
const currentText = editor.getValue();
const prettyText = print(parse(currentText));
editor.setValue(prettyText);
};
render() {
const { noFetch } = this.state;
const { theme } = this.props;
const graphiql = (
<GraphiQL
fetcher={this.fetcher}
query={this.state.query}
editorTheme={theme === "dark" ? "dracula" : "graphiql"}
onEditQuery={() => {
this.clearDefaultQueryState();
}}
onEditVariables={() => {
this.clearDefaultQueryState();
}}
variables={this.state.variables}
ref={r => {
this.graphiql = r;
}}
>
<GraphiQL.Toolbar>
<GraphiQL.Button
onClick={this.handleClickPrettifyButton}
label="Prettify"
/>
<label>
<input
type="checkbox"
checked={noFetch}
style={{ verticalAlign: "middle" }}
onChange={() => {
this.setState({
noFetch: !noFetch,
query: undefined,
variables: undefined,
});
}}
/>
Load from cache
</label>
</GraphiQL.Toolbar>
</GraphiQL>
);
return <div className="body">{graphiql}</div>;
}
}
export default withBridge(Explorer);