Skip to content

Instantly share code, notes, and snippets.

@fokot
Last active June 29, 2017 11:51
Show Gist options
  • Save fokot/50656eceba00f5803472607dc40396d5 to your computer and use it in GitHub Desktop.
Save fokot/50656eceba00f5803472607dc40396d5 to your computer and use it in GitHub Desktop.
GraphQL schema compare

GraphQL schema compare by Sangria

  • Compares two graphQL schemas
  • If schema is not set it takes actual schema from server
  • Html can be used with any server as it returns data in format {"error":null,"data":[{"breaking":true,"description":"`Mutation` type was removed"}]}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
div, input, button, li {
font-size: large;
}
.CompareForm {
display: flex;
flex-direction: column;
padding: 5px;
}
.CompareForm > * {
margin-bottom: 10px;
}
.breaking {
font-weight: bold;
color: red;
}
</style>
</head>
<body>
<h1>Compare two GraphQL schemas</h1>
<ul>
<li>input graphql endpoint e.g. https://dev.merlonintelligence.com/graphql</li>
<li>press `Compare`</li>
<li>if not set, current server graphQL will be used</li>
<li>breaking changes are in bold</li>
</ul>
<br/>
<div id="react-app"></div>
<script src="https://cdn.jsdelivr.net/react/15.5.4/react.js"></script>
<script src="https://cdn.jsdelivr.net/react/15.5.4/react-dom.js"></script>
<script>
var Change = function(props){
return React.createElement('li', {className: props.change.breaking ? 'breaking' : 'non-breaking'}, props.change.description)
};
var CompareForm = (props) =>
React.createElement('form', {className: 'CompareForm'},
React.createElement('div', {}, 'Old schema'),
React.createElement('input', {
type: 'text',
value: props.oldSchema,
onChange: e => props.setOldSchema(e.target.value)
}),
React.createElement('div', {}, 'New schema'),
React.createElement('input', {
type: 'text',
value: props.newSchema,
onChange: e => props.setNewSchema(e.target.value)
}),
React.createElement('button', { type: 'submit', onClick: e => { e.preventDefault(); props.compare()} }, "Compare")
);
var CompareView = (props) => {
if(props.comparing) {
return React.createElement('h1', {}, 'Comparing...');
}
if(props.result.error) {
return React.createElement('h1', {}, props.result.error);
}
if(props.result.data.length === 0) {
return React.createElement('h1', {}, 'Schemas are identical');
}
return React.createElement('div', {},
React.createElement('h1', {}, `${props.result.data.length} changes`),
React.createElement('ul', {}, props.result.data.map((c, i) =>
React.createElement(Change, {key: i, change: c})
)
)
);
}
var App = (props) =>
React.createElement('div', {},
React.createElement(CompareForm, props),
React.createElement(CompareView, props)
);
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
result: {
data: [],
error: 'No comparison yet'
},
comparing: false,
oldSchema: '',
newSchema: '',
};
this.compare = this.compare.bind(this);
}
compare() {
this.setState({comparing: true});
const url = "/schemaCompareResult";
const xhr = new XMLHttpRequest();
window.w = this;
var params = "oldSchema=" + this.state.oldSchema + "&newSchema=" + this.state.newSchema;
xhr.open("GET", url+"?"+params, true);
xhr.send(null);
xhr.onreadystatechange = function() {//Call a function when the state changes.
if(xhr.readyState === 4 && xhr.status === 200) {
w.setState({comparing: false, result: JSON.parse(xhr.responseText)});
} else if(xhr.readyState === 4 && xhr.status !== 200) {
w.setState({comparing: false, result: { data: [], error: 'Calling server failed'}});
}
}
}
render() {
return React.createElement('div', {},
React.createElement(CompareForm, {
oldSchema: this.state.oldSchema,
newSchema: this.state.newSchema,
setOldSchema: (s) => this.setState({oldSchema: s}),
setNewSchema: (s) => this.setState({newSchema: s}),
compare: this.compare,
}),
React.createElement(CompareView, this.state)
);
}
}
ReactDOM.render(React.createElement(MyComponent), document.getElementById('react-app'));
</script>
</body>
</html>
package raptorclientapi
import io.circe.Encoder
import io.circe.parser.parse
import sangria.schema.{Schema, SchemaChange}
import scalaj.http.{Http => SJHttp}
import sangria.marshalling.circe.CirceInputUnmarshaller
import io.circe._
import io.circe.generic.semiauto._
import cats.implicits._
import scala.util.Try
object SchemaCompare {
val introspectionQuery = s"""{"query":"${sangria.introspection.introspectionQuery.source.get.lines.mkString(" ")}"}"""
type ParsedSchema = Either[String, Schema[_, _]]
def downloadAndParseSchema(url: String): ParsedSchema =
Try(Schema.buildFromIntrospection(parse( SJHttp(url)
.postData(introspectionQuery)
.header("content-type", "application/json")
.asString.body ).right.get)).toOption.toRight(s"Can't parse schema from $url")
case class ComparisonResult(error: Option[String], data: Vector[SchemaChange] = Vector.empty)
implicit val SchemaChangeEncoder: Encoder[SchemaChange] = new Encoder[SchemaChange] {
final def apply(c: SchemaChange): Json = Json.fromFields(
"breaking" -> Json.fromBoolean(c.breakingChange) :: "description" -> Json.fromString(c.description) :: Nil
)
}
implicit val ComparisonResultEncoder: Encoder[ComparisonResult] = deriveEncoder
def schemaCompare(oldSchema: Option[String], newSchema: Option[String]): ComparisonResult = (oldSchema, newSchema) match {
case (None, None) => ComparisonResult("No schema selected".some)
case (Some(o), None) => compare(downloadAndParseSchema(o), SchemaDefinition.RaptorSchema.asRight[String])
case (None, Some(n)) => compare(SchemaDefinition.RaptorSchema.asRight[String], downloadAndParseSchema(n))
case (Some(o), Some(n)) => compare(downloadAndParseSchema(o), downloadAndParseSchema(n))
}
def compare(oldSchema: ParsedSchema, newSchema: ParsedSchema): ComparisonResult = {
val res = for(
o <- oldSchema;
n <- newSchema
) yield ComparisonResult(None, n compare o)
res.left.map(error => ComparisonResult(error.some)).merge
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment