diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..5f21825e2a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +# don't commit compiled files +lib diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..0808d688325 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Ben Newman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/package.json b/package.json new file mode 100644 index 00000000000..c6ab165ec0b --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "relay-runtime-query", + "version": "0.7.1", + "description": "Compile your Relay GraphQL queries at runtime, no need for Babel. Only for use in development.", + "main": "./lib/index.js", + "jsnext:main": "./src/index.js", + "scripts": { + "test": "mocha --reporter spec --full-trace test/index.js", + "compile": "babel --presets es2015,stage-0 -d lib/ src/", + "prepublish": "npm run compile" + }, + "repository": { + "type": "git", + "url": "meteor/relay-runtime-query" + }, + "keywords": [ + "ecmascript", + "es2015", + "jsnext", + "javascript", + "relay", + "npm", + "react" + ], + "author": "Sashko Stubailo ", + "license": "MIT", + "dependencies": { + "babel-polyfill": "^6.5.0", + "babel-relay-plugin": "0.7.1", + "graphql": "^0.4.17", + "inherits": "^2.0.1", + "react": "^0.14.7", + "react-dom": "^0.14.7", + "react-relay": "0.7.1", + "util": "^0.10.3" + }, + "devDependencies": { + "babel-core": "6.3.21", + "babel-loader": "6.2.0", + "babel-plugin-transform-flow-strip-types": "^6.5.0", + "babel-preset-es2015": "^6.5.0", + "babel-preset-stage-0": "^6.5.0", + "graphql": "^0.4.17", + "graphql-relay": "^0.3.6", + "lodash": "^4.5.1", + "mocha": "^2.3.3" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000000..31b2cbbed86 --- /dev/null +++ b/src/index.js @@ -0,0 +1,218 @@ +import RelayQLTransformer from 'babel-relay-plugin/lib/RelayQLTransformer'; +const {utilities_buildClientSchema: {buildClientSchema}} = require('babel-relay-plugin/lib/GraphQL'); +import invariant from 'babel-relay-plugin/lib/invariant'; +import RelayQLPrinter from 'babel-relay-plugin/lib/RelayQLPrinter'; +import { introspectionQuery } from 'graphql/utilities/introspectionQuery'; +import Relay from 'react-relay'; +import generateHash from 'babel-relay-plugin/lib/generateHash'; + +function getSchema(schemaProvider: GraphQLSchemaProvider): GraphQLSchema { + const introspection = typeof schemaProvider === 'function' ? + schemaProvider() : + schemaProvider; + invariant( + typeof introspection === 'object' && introspection && + typeof introspection.__schema === 'object' && introspection.__schema, + 'Invalid introspection data supplied to `getBabelRelayPlugin()`. The ' + + 'resulting schema is not an object with a `__schema` property.' + ); + return buildClientSchema(introspection); +} + +let fragmentIndex = 0; +const fragmentCache = {}; + +function encodeFragmentIndex(index) { + return '$$$' + index + '$$$'; +} + +const t = { + arrayExpression(array) { + return array; + }, + nullLiteral() { + return null; + }, + valueToNode(value) { + return value; + }, + objectExpression(propertyArray) { + const obj = {}; + + propertyArray.forEach((property) => { + if (property.value.__identifier) { + throw new Error("Don't support identifiers yet"); + } + + obj[property.key] = property.value; + }); + + return obj; + }, + identifier(identifierName) { + return { + __identifier: identifierName + }; + }, + objectProperty(nameIdentifier, value) { + return { + key: nameIdentifier.__identifier, + value: value + }; + }, + + // Only used once, to return a definition object in `print` + returnStatement(expressionToReturn) { + return { + __fakeReturnStatement: expressionToReturn + }; + }, + + // Used twice - for runtime errors, and to return a definition object in `print` + blockStatement(arrayOfStatements) { + return { + __fakeBlockStatement: arrayOfStatements + }; + }, + + functionExpression(name, substitutionIdentifiers, printedDocumentReturnBlockStatement) { + const query = printedDocumentReturnBlockStatement.__fakeBlockStatement[0].__fakeReturnStatement; + + const querySubstitutionFunction = function () { + return query; + } + + return querySubstitutionFunction; + }, + + callExpression(func, args) { + // Try to hackily identify shallowFlatten + if (args && args.length === 2) { + return [].concat.apply([], args[1]); + } + + if (args && args.length > 0) { throw new Error("Args not implemented lol") } + + return func(); + }, + + memberExpression(members) { + return { + __fakeMemberExpression: members + }; + } +}; + +export function initTemplateStringTransformer(schemaJson) { + const schema = getSchema(schemaJson); + const transformer = new RelayQLTransformer(schema, {}); + + function templateStringTag(quasis, ...expressions) { + const processedTemplateLiteral = processTemplateLiteral(quasis, expressions, 'queryName'); + + const processedTemplateText = transformer.processTemplateText(processedTemplateLiteral.templateText, { + documentName: 'queryName', + propName: 'propName' + }); + + const definition = transformer.processDocumentText(processedTemplateText, { + documentName: 'queryName', + propName: 'propName', + fragmentLocationID: generateHash(JSON.stringify(processedTemplateText)).substring(0, 12) + }); + + const options = {}; + const Printer = RelayQLPrinter(t, options); + + modifyPrinterClass(Printer); + + const printed = new Printer('wtf??', {}) + .print(definition, []); + + return printed; + } + + return templateStringTag; +} + +// Attempted lift from https://github.com/facebook/relay/blob/0be965c3c92c48499b452e953d823837838df962/scripts/babel-relay-plugin/src/RelayQLTransformer.js#L114-L148 +// Returns { substitutions, templateText, variableNames } +// Who knows why they are called quasis?? +function processTemplateLiteral(quasis, expressions, documentName) { + const chunks = []; + const variableNames = {}; + const substitutions = []; + + quasis.forEach((chunk, ii) => { + chunks.push(chunk); + + if (ii !== quasis.length - 1) { + const name = 'RQL_' + ii; + const value = expressions[ii]; + + runtime.fragments[name] = value; + + substitutions.push({name, value}); + + if (/:\s*$/.test(chunk)) { + invariant( + false, // this.options.substituteVariables, + 'You supplied a GraphQL document named `%s` that uses template ' + + 'substitution for an argument value, but variable substitution ' + + 'has not been enabled.', + documentName + ); + chunks.push('$' + name); + variableNames[name] = undefined; + } else { + chunks.push('...' + name); + } + } + }); + + return {substitutions, templateText: chunks.join('').trim(), variableNames}; +} + +// Override certain functions on the printer +function modifyPrinterClass(printer) { + printer.prototype.printFragmentReference = function (fragmentReference) { + return [].concat.apply([], [Relay.QL.__frag(runtime.getFragment(fragmentReference.getName()))]); + } +} + +const runtime = { + fragments: {}, + getFragment(name) { + const frag = this.fragments[name]; + delete this.fragments[name]; + return frag; + } +} + +// Eventually improve this to support old services like GraphiQL does. +export function initTemplateStringTransformerFromUrl(url, callback) { + graphQLFetcher(url, { query: introspectionQuery }).then(result => { + const schemaJson = result.data; + callback(initTemplateStringTransformer(schemaJson)); + }); +} + +function graphQLFetcher(url, graphQLParams) { + return fetch(url, { + method: 'post', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(graphQLParams), + credentials: 'include', + }).then(function (response) { + return response.text(); + }).then(function (responseBody) { + try { + return JSON.parse(responseBody); + } catch (error) { + return responseBody; + } + }); +} diff --git a/test/babelRelayPlugin.js b/test/babelRelayPlugin.js new file mode 100644 index 00000000000..cc27f4d035c --- /dev/null +++ b/test/babelRelayPlugin.js @@ -0,0 +1,4 @@ +var getbabelRelayPlugin = require('babel-relay-plugin'); +var schema = require('./starwars.json'); + +module.exports = getbabelRelayPlugin(schema.data); diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000000..2c718ef6dca --- /dev/null +++ b/test/index.js @@ -0,0 +1,7 @@ +// This file cannot be written with ECMAScript 2015 because it has to load +// the Babel require hook to enable ECMAScript 2015 features! +require("babel-core/register"); +require("babel-polyfill"); + +// The tests, however, can and should be written with ECMAScript 2015. +require("./tests.js"); diff --git a/test/introspectStarWars.js b/test/introspectStarWars.js new file mode 100644 index 00000000000..d53a4b7a553 --- /dev/null +++ b/test/introspectStarWars.js @@ -0,0 +1,7 @@ +import { StarWarsSchema } from './starWarsSchema.js'; +import { graphql } from 'graphql'; +import { introspectionQuery } from 'graphql/utilities/introspectionQuery'; + +export async function introspectStarwars() { + return await graphql(StarWarsSchema, introspectionQuery); +} diff --git a/test/starWarsData.js b/test/starWarsData.js new file mode 100644 index 00000000000..4767aa0b118 --- /dev/null +++ b/test/starWarsData.js @@ -0,0 +1,149 @@ +// Copied from https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsData.js + +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * This defines a basic set of data for our Star Wars Schema. + * + * This data is hard coded for the sake of the demo, but you could imagine + * fetching this data from a backend service rather than from hardcoded + * JSON objects in a more complex demo. + */ + +const luke = { + id: '1000', + name: 'Luke Skywalker', + friends: [ '1002', '1003', '2000', '2001' ], + appearsIn: [ 4, 5, 6 ], + homePlanet: 'Tatooine', +}; + +const vader = { + id: '1001', + name: 'Darth Vader', + friends: [ '1004' ], + appearsIn: [ 4, 5, 6 ], + homePlanet: 'Tatooine', +}; + +const han = { + id: '1002', + name: 'Han Solo', + friends: [ '1000', '1003', '2001' ], + appearsIn: [ 4, 5, 6 ], +}; + +const leia = { + id: '1003', + name: 'Leia Organa', + friends: [ '1000', '1002', '2000', '2001' ], + appearsIn: [ 4, 5, 6 ], + homePlanet: 'Alderaan', +}; + +const tarkin = { + id: '1004', + name: 'Wilhuff Tarkin', + friends: [ '1001' ], + appearsIn: [ 4 ], +}; + +const humanData = { + 1000: luke, + 1001: vader, + 1002: han, + 1003: leia, + 1004: tarkin, +}; + +const threepio = { + id: '2000', + name: 'C-3PO', + friends: [ '1000', '1002', '1003', '2001' ], + appearsIn: [ 4, 5, 6 ], + primaryFunction: 'Protocol', +}; + +const artoo = { + id: '2001', + name: 'R2-D2', + friends: [ '1000', '1002', '1003' ], + appearsIn: [ 4, 5, 6 ], + primaryFunction: 'Astromech', +}; + +const droidData = { + 2000: threepio, + 2001: artoo, +}; + +/** + * Helper function to get a character by ID. + */ +function getCharacter(id) { + // Returning a promise just to illustrate GraphQL.js's support. + return Promise.resolve(humanData[id] || droidData[id]); +} + +/** + * Allows us to query for a character's friends. + */ +export function getFriends(character) { + return character.friends.map(id => getCharacter(id)); +} + +/** + * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2. + */ +export function getHero(episode) { + if (episode === 5) { + // Luke is the hero of Episode V. + return luke; + } + // Artoo is the hero otherwise. + return artoo; +} + +/** + * Allows us to query for the human with the given id. + */ +export function getHuman(id) { + return humanData[id]; +} + +/** + * Allows us to query for the droid with the given id. + */ +export function getDroid(id) { + return droidData[id]; +} + +// FROM RELAY EXAMPLE APP +var rebels = { + id: '1', + name: 'Alliance to Restore the Republic', + ships: ['1', '2', '3', '4', '5'], +}; + +var empire = { + id: '2', + name: 'Galactic Empire', + ships: ['6', '7', '8'], +}; + +export function getFactions(names) { + return names.map(name => { + if (name === 'empire') { + return empire; + } + if (name === 'rebels') { + return rebels; + } + return null; + }); +} diff --git a/test/starWarsSchema.js b/test/starWarsSchema.js new file mode 100644 index 00000000000..44c8327dc9d --- /dev/null +++ b/test/starWarsSchema.js @@ -0,0 +1,444 @@ +// Copied from https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsSchema.js + +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { + GraphQLEnumType, + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLList, + GraphQLNonNull, + GraphQLSchema, + GraphQLString, +} from 'graphql/type'; + +import { getFriends, getHero, getHuman, getDroid } from './starWarsData.js'; + +import { + connectionArgs, + connectionDefinitions, + connectionFromArray, + fromGlobalId, + globalIdField, + mutationWithClientMutationId, + nodeDefinitions, +} from 'graphql-relay'; + +/** + * This is designed to be an end-to-end test, demonstrating + * the full GraphQL stack. + * + * We will create a GraphQL schema that describes the major + * characters in the original Star Wars trilogy. + * + * NOTE: This may contain spoilers for the original Star + * Wars trilogy. + */ + +/** + * Using our shorthand to describe type systems, the type system for our + * Star Wars example is: + * + * enum Episode { NEWHOPE, EMPIRE, JEDI } + * + * interface Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * } + * + * type Human : Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * homePlanet: String + * } + * + * type Droid : Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * primaryFunction: String + * } + * + * type Query { + * hero(episode: Episode): Character + * human(id: String!): Human + * droid(id: String!): Droid + * } + * + * We begin by setting up our schema. + */ + +/** + * The original trilogy consists of three movies. + * + * This implements the following type system shorthand: + * enum Episode { NEWHOPE, EMPIRE, JEDI } + */ +const episodeEnum = new GraphQLEnumType({ + name: 'Episode', + description: 'One of the films in the Star Wars Trilogy', + values: { + NEWHOPE: { + value: 4, + description: 'Released in 1977.', + }, + EMPIRE: { + value: 5, + description: 'Released in 1980.', + }, + JEDI: { + value: 6, + description: 'Released in 1983.', + }, + } +}); + +/** + * Characters in the Star Wars trilogy are either humans or droids. + * + * This implements the following type system shorthand: + * interface Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * } + */ +const characterInterface = new GraphQLInterfaceType({ + name: 'Character', + description: 'A character in the Star Wars Trilogy', + fields: () => ({ + id: { + type: new GraphQLNonNull(GraphQLString), + description: 'The id of the character.', + }, + name: { + type: GraphQLString, + description: 'The name of the character.', + }, + friends: { + type: new GraphQLList(characterInterface), + description: 'The friends of the character, or an empty list if they ' + + 'have none.', + }, + appearsIn: { + type: new GraphQLList(episodeEnum), + description: 'Which movies they appear in.', + }, + }), + resolveType: character => { + return getHuman(character.id) ? humanType : droidType; + } +}); + +/** + * We define our human type, which implements the character interface. + * + * This implements the following type system shorthand: + * type Human : Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * } + */ +const humanType = new GraphQLObjectType({ + name: 'Human', + description: 'A humanoid creature in the Star Wars universe.', + fields: () => ({ + id: { + type: new GraphQLNonNull(GraphQLString), + description: 'The id of the human.', + }, + name: { + type: GraphQLString, + description: 'The name of the human.', + }, + friends: { + type: new GraphQLList(characterInterface), + description: 'The friends of the human, or an empty list if they ' + + 'have none.', + resolve: human => getFriends(human), + }, + appearsIn: { + type: new GraphQLList(episodeEnum), + description: 'Which movies they appear in.', + }, + homePlanet: { + type: GraphQLString, + description: 'The home planet of the human, or null if unknown.', + }, + }), + interfaces: [ characterInterface ] +}); + +/** + * The other type of character in Star Wars is a droid. + * + * This implements the following type system shorthand: + * type Droid : Character { + * id: String! + * name: String + * friends: [Character] + * appearsIn: [Episode] + * primaryFunction: String + * } + */ +const droidType = new GraphQLObjectType({ + name: 'Droid', + description: 'A mechanical creature in the Star Wars universe.', + fields: () => ({ + id: { + type: new GraphQLNonNull(GraphQLString), + description: 'The id of the droid.', + }, + name: { + type: GraphQLString, + description: 'The name of the droid.', + }, + friends: { + type: new GraphQLList(characterInterface), + description: 'The friends of the droid, or an empty list if they ' + + 'have none.', + resolve: droid => getFriends(droid), + }, + appearsIn: { + type: new GraphQLList(episodeEnum), + description: 'Which movies they appear in.', + }, + primaryFunction: { + type: GraphQLString, + description: 'The primary function of the droid.', + }, + }), + interfaces: [ characterInterface ] +}); + +/** + * We get the node interface and field from the Relay library. + * + * The first method defines the way we resolve an ID to its object. + * The second defines the way we resolve a node object to its GraphQL type. + */ +var {nodeInterface, nodeField} = nodeDefinitions( + (globalId) => { + var {type, id} = fromGlobalId(globalId); + if (type === 'Faction') { + return getFaction(id); + } else if (type === 'Ship') { + return getShip(id); + } else { + return null; + } + }, + (obj) => { + return obj.ships ? factionType : shipType; + } +); + +/** + * We define our basic ship type. + * + * This implements the following type system shorthand: + * type Ship : Node { + * id: String! + * name: String + * } + */ +var shipType = new GraphQLObjectType({ + name: 'Ship', + description: 'A ship in the Star Wars saga', + fields: () => ({ + id: globalIdField('Ship'), + name: { + type: GraphQLString, + description: 'The name of the ship.', + }, + }), + interfaces: [nodeInterface], +}); + +/** + * We define a connection between a faction and its ships. + * + * connectionType implements the following type system shorthand: + * type ShipConnection { + * edges: [ShipEdge] + * pageInfo: PageInfo! + * } + * + * connectionType has an edges field - a list of edgeTypes that implement the + * following type system shorthand: + * type ShipEdge { + * cursor: String! + * node: Ship + * } + */ +var {connectionType: shipConnection} = + connectionDefinitions({name: 'Ship', nodeType: shipType}); + +/** + * We define our faction type, which implements the node interface. + * + * This implements the following type system shorthand: + * type Faction : Node { + * id: String! + * name: String + * ships: ShipConnection + * } + */ +var factionType = new GraphQLObjectType({ + name: 'Faction', + description: 'A faction in the Star Wars saga', + fields: () => ({ + id: globalIdField('Faction'), + name: { + type: GraphQLString, + description: 'The name of the faction.', + }, + ships: { + type: shipConnection, + description: 'The ships used by the faction.', + args: connectionArgs, + resolve: (faction, args) => connectionFromArray( + faction.ships.map((id) => getShip(id)), + args + ), + }, + }), + interfaces: [nodeInterface], +}); + +/** + * This is the type that will be the root of our query, and the + * entry point into our schema. It gives us the ability to fetch + * objects by their IDs, as well as to fetch the undisputed hero + * of the Star Wars trilogy, R2-D2, directly. + * + * This implements the following type system shorthand: + * type Query { + * hero(episode: Episode): Character + * human(id: String!): Human + * droid(id: String!): Droid + * } + * + */ +const queryType = new GraphQLObjectType({ + name: 'Query', + fields: () => ({ + hero: { + type: characterInterface, + args: { + episode: { + description: 'If omitted, returns the hero of the whole saga. If ' + + 'provided, returns the hero of that particular episode.', + type: episodeEnum + } + }, + resolve: (root, { episode }) => getHero(episode), + }, + human: { + type: humanType, + args: { + id: { + description: 'id of the human', + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve: (root, { id }) => getHuman(id), + }, + droid: { + type: droidType, + args: { + id: { + description: 'id of the droid', + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve: (root, { id }) => getDroid(id), + }, + factions: { + type: new GraphQLList(factionType), + args: { + names: { + type: new GraphQLList(GraphQLString), + }, + }, + resolve: (root, {names}) => getFactions(names), + }, + }) +}); + +// NOT FROM STAR WARS - FOR TESTING MUTATIONS +import { + GraphQLID, +} from 'graphql'; + +const STORY = { + comments: [], + id: '42', +}; + +var CommentType = new GraphQLObjectType({ + name: 'Comment', + fields: () => ({ + id: {type: GraphQLID}, + text: {type: GraphQLString}, + }), +}); + +var StoryType = new GraphQLObjectType({ + name: 'Story', + fields: () => ({ + comments: { type: new GraphQLList(CommentType) }, + id: { type: GraphQLString }, + }), +}); + +var CreateCommentMutation = mutationWithClientMutationId({ + name: 'CreateComment', + inputFields: { + text: { type: new GraphQLNonNull(GraphQLString) }, + }, + outputFields: { + story: { + type: StoryType, + resolve: () => STORY, + }, + }, + mutateAndGetPayload: ({text}) => { + var newComment = { + id: STORY.comments.length, + text, + }; + STORY.comments.push(newComment); + return newComment; + }, +}); + + +/** + * Finally, we construct our schema (whose starting query type is the query + * type we defined above) and export it. + */ +export const StarWarsSchema = new GraphQLSchema({ + query: queryType, + mutation: new GraphQLObjectType({ + name: 'Mutation', + fields: () => ({ + createComment: CreateCommentMutation, + }), + }), +}); diff --git a/test/starwars.json b/test/starwars.json new file mode 100644 index 00000000000..2852d8bed78 --- /dev/null +++ b/test/starwars.json @@ -0,0 +1,1722 @@ +{ + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "hero", + "description": null, + "args": [ + { + "name": "episode", + "description": "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.", + "type": { + "kind": "ENUM", + "name": "Episode", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "INTERFACE", + "name": "Character", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "human", + "description": null, + "args": [ + { + "name": "id", + "description": "id of the human", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Human", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "droid", + "description": null, + "args": [ + { + "name": "id", + "description": "id of the droid", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Droid", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "factions", + "description": null, + "args": [ + { + "name": "names", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Faction", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "Episode", + "description": "One of the films in the Star Wars Trilogy", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "NEWHOPE", + "description": "Released in 1977.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EMPIRE", + "description": "Released in 1980.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "JEDI", + "description": "Released in 1983.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "Character", + "description": "A character in the Star Wars Trilogy", + "fields": [ + { + "name": "id", + "description": "The id of the character.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the character.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friends", + "description": "The friends of the character, or an empty list if they have none.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "Character", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "appearsIn", + "description": "Which movies they appear in.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "Episode", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Human", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Droid", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "Human", + "description": "A humanoid creature in the Star Wars universe.", + "fields": [ + { + "name": "id", + "description": "The id of the human.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the human.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friends", + "description": "The friends of the human, or an empty list if they have none.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "Character", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "appearsIn", + "description": "Which movies they appear in.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "Episode", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "homePlanet", + "description": "The home planet of the human, or null if unknown.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Character", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Droid", + "description": "A mechanical creature in the Star Wars universe.", + "fields": [ + { + "name": "id", + "description": "The id of the droid.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the droid.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "friends", + "description": "The friends of the droid, or an empty list if they have none.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INTERFACE", + "name": "Character", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "appearsIn", + "description": "Which movies they appear in.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "Episode", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "primaryFunction", + "description": "The primary function of the droid.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Character", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Faction", + "description": "A faction in the Star Wars saga", + "fields": [ + { + "name": "id", + "description": "The ID of an object", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the faction.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ships", + "description": "The ships used by the faction.", + "args": [ + { + "name": "after", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ShipConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "Node", + "description": "An object with an ID", + "fields": [ + { + "name": "id", + "description": "The id of the object.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Ship", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Faction", + "ofType": null + } + ] + }, + { + "kind": "OBJECT", + "name": "Ship", + "description": "A ship in the Star Wars saga", + "fields": [ + { + "name": "id", + "description": "The ID of an object", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the ship.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Node", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ShipConnection", + "description": "A connection to a list of items.", + "fields": [ + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "edges", + "description": "Information to aid in pagination.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ShipEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PageInfo", + "description": "Information about pagination in a connection.", + "fields": [ + { + "name": "hasNextPage", + "description": "When paginating forwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPreviousPage", + "description": "When paginating backwards, are there more items?", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startCursor", + "description": "When paginating backwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endCursor", + "description": "When paginating forwards, the cursor to continue.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "ShipEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "node", + "description": "The item at the end of the edge", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Ship", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "A cursor for use in pagination", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": null, + "fields": [ + { + "name": "createComment", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateCommentInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateCommentPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "CreateCommentInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "text", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateCommentPayload", + "description": null, + "fields": [ + { + "name": "story", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Story", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "clientMutationId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Story", + "description": null, + "fields": [ + { + "name": "comments", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Comment", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Comment", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "text", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type" + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive" + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue" + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL’s execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue" + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": true, + "onField": true + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ], + "onOperation": false, + "onFragment": true, + "onField": true + } + ] + } + } +} \ No newline at end of file diff --git a/test/tests.js b/test/tests.js new file mode 100644 index 00000000000..506714181ab --- /dev/null +++ b/test/tests.js @@ -0,0 +1,269 @@ +import assert from "assert"; +import { introspectStarwars } from './introspectStarwars'; +import { initTemplateStringTransformer } from '../src/index'; +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; + +// Needed because some of the compiled stuff refers to Relay.QL.__frag +import Relay from 'react-relay'; + + +// Uncomment the below to generate a new schema JSON +describe("graphql", () => { + it("can introspect star wars", async () => { + const result = await introspectStarwars(); + + fs.writeFileSync(path.join(__dirname, "starwars.json"), + JSON.stringify(result, null, 2)); + + assert.ok(result.data); + assert.ok(result.data.__schema); + }); +}); + + +describe("runtime query transformer", async () => { + let transform; + + before(async () => { + const result = await introspectStarwars(); + + transform = initTemplateStringTransformer(result.data); + }); + + it("can be initialized with an introspected query", async () => { + const result = await introspectStarwars(); + const transformer = initTemplateStringTransformer(result.data); + }); + + it("can compile a Relay.QL query", () => { + Relay.QL` + query HeroNameQuery { + hero { + name + } + } + `; + }); + + it("can transform a simple query", async () => { + const transformed = transform` + query HeroNameAndFriendsQuery { + hero { + id + name + friends { + name + } + } + } + `; + + const expected = Relay.QL` + query HeroNameAndFriendsQuery { + hero { + id + name + friends { + name + } + } + } + `; + + assert.deepEqual(transformed, expected); + }); + + it("can transform a query with arguments", async () => { + const transformed = transform` + query FetchLukeQuery { + human(id: "1000") { + name + } + } + `; + + const expected = Relay.QL` + query FetchLukeQuery { + human(id: "1000") { + name + } + } + `; + + assert.deepEqual(transformed, expected); + }); + + it("can transform a query with variables", async () => { + const transformed = transform` + query FetchSomeIDQuery($someId: String!) { + human(id: $someId) { + name + } + } + `; + + const expected = Relay.QL` + query FetchSomeIDQuery($someId: String!) { + human(id: $someId) { + name + } + } + `; + + assert.deepEqual(transformed, expected); + }); + + it("can transform a query fragment", async () => { + const transformed = transform` + fragment HumanFragment on Human { + name + homePlanet + } + `; + + const expected = Relay.QL` + fragment HumanFragment on Human { + name + homePlanet + } + `; + + assertEqualSansNameAndId(transformed, expected); + }); + + it("can transform a query with fragment substitution", async () => { + function getFragmentRuntime() { + return transform` + fragment on Human { + name + homePlanet + } + `; + } + + const transformed = transform` + query FetchSomeIDQuery { + human(id: $someId) { + ${getFragmentRuntime()} + } + } + `; + + function getFragmentRelayQL() { + return Relay.QL` + fragment on Human { + name + homePlanet + } + `; + } + + const expected = Relay.QL` + query FetchSomeIDQuery { + human(id: $someId) { + ${getFragmentRelayQL()} + } + } + `; + + assertEqualSansNameAndId(transformed, expected); + }); + + it("can transform a mutation", async () => { + const transformed = transform` + mutation { createComment } + `; + + const expected = Relay.QL` + mutation { createComment } + `; + + assertEqualSansNameAndId(transformed, expected); + }); + + it("can transform a query from star wars example with an array argument", () => { + const transformed = transform` + query { + factions(names: $factionNames) + } + `; + + const expected = Relay.QL` + query { + factions(names: $factionNames) + } + `; + + assertEqualSansNameAndId(transformed, expected); + }); + + it("can transform a query with a Relay annotation from the star wars example", () => { + function getTransformedFragment() { + return transform` + fragment on Ship { + name + } + `; + } + + const transformed = transform` + fragment on Faction @relay(plural: true) { + name, + ships(first: 10) { + edges { + node { + ${getTransformedFragment()} + } + } + } + } + `; + + function f() { + function getRelayQLFragment() { + return Relay.QL` + fragment on Ship { + name + } + `; + } + + const expected = Relay.QL` + fragment on Faction @relay(plural: true) { + name, + ships(first: 10) { + edges { + node { + ${getRelayQLFragment()} + } + } + } + } + `; + } + console.log(f.toString()); + + assertEqualSansNameAndId(transformed, expected); + }); +}); + +function assertEqualSansNameAndId(a, b) { + const filteredA = omitNameAndIdFields(a); + const filteredB = omitNameAndIdFields(b); + + assert.deepEqual(filteredA, filteredB); +} + +function omitNameAndIdFields(obj) { + if (! _.isObject(obj)) { + return obj; + } + + const omitted = _.omit(obj, ['id', 'name']); + + return _.mapValues(omitted, (value) => { + return omitNameAndIdFields(value); + }); +}