diff --git a/site/docs/Learn-Queries.md b/site/docs/Learn-Queries.md index 88eb6e24e3..399f03d845 100644 --- a/site/docs/Learn-Queries.md +++ b/site/docs/Learn-Queries.md @@ -181,36 +181,27 @@ Most discussions of GraphQL focus on data fetching, but any complete data platfo In REST, any request might end up causing some side-effects on the server, but by convention it's suggested that one doesn't use `GET` requests to modify data. GraphQL is similar - technically any query could be implemented to cause a data write. However, it's useful to establish a convention that any operations that cause writes should be sent explicitly via a mutation. -Here's an example of a simple mutation: +Just like in queries, if the mutation field returns an object type, you can ask for nested fields. This can be useful for fetching the new state of an object after an update. Let's look at a simple example mutation: ```graphql -mutation CreateCharacterInEpisode($name: String!, $appearsIn: Episode!) { - createCharacter(name: $name) - addCharacterToEpisode(name: $name, episode: $appearsIn) +# { "graphiql": true, "variables": { "ep": "JEDI", "review": { "stars": 5, "commentary": "This is a great movie!" } } } +mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { + createReview(episode: $ep, review: $review) { + stars + commentary + } } ``` -You can see that a mutation can contain multiple fields, just like a query. There's one important distinction between queries, and mutations, other than the name: - -**While query fields are executed in parallel, mutation fields run in series, one after the other.** - -This means that even though we sent `createCharacter` and `addCharacterToEpisode` in one request, the first is guaranteed to finish before the second begins, ensuring that we create the character before trying to add it an episode. +In this case, the `incrementCredits` mutation field returns a `Human` object, so we can query the new value of `totalCredits` after giving that character some more credits. Otherwise, we would have needed to send two requests - one to update the credits, and another to get the new value - or guess at the new amount based on outdated data. -#### Returning data from mutations +#### Multiple fields in mutations -Just like in queries, you can ask for nested fields in the mutation result. This can be useful for fetching the new state of an object after an update: +A mutation can contain multiple fields, just like a query. There's one important distinction between queries and mutations, other than the name: -```graphql -mutation IncrementCredits($characterId: ID!) { - incrementCredits(characterId: $characterId) { - totalCredits - } -} -``` - -In this case, the `incrementCredits` mutation field returns a `Character` object, so we can query the new value of `totalCredits` after giving that character some more credits. Otherwise, we would have needed to send two requests - one to update the credits, and another to get the new value - or guess at the new amount based on outdated data. +**While query fields are executed in parallel, mutation fields run in series, one after the other.** -That's all! Now you know everything you need to know about GraphQL queries and mutations to build a pretty good application. For more advanced features and tips, check out the advanced section. +This means that if we send two `incrementCredits` mutations in one request, the first is guaranteed to finish before the second begins, ensuring that we don't end up with a race condition with ourselves. ### Fragments and type conditions @@ -226,8 +217,8 @@ query HeroForEpisode($ep: Episode!) { ... on Droid { primaryFunction } - ... on Droid { - homePlanet + ... on Human { + height } } } @@ -237,4 +228,4 @@ In this query, the `hero` field returns the type `Character`, which might be eit To ask for a field on the concrete type, you need to use an _inline fragment_ with a type condition. Because the first fragment is labeled as `... on Droid`, the `primaryFunction` field will only be executed if the `Character` returned from `hero` is of the `Droid` type. Similarly for the `homePlanet` field for the `Human` type. -Named fragments can also be used in the same way, since a named fragment always has a type condition attached. +Named fragments can also be used in the same way, since a named fragment always has a type attached. diff --git a/site/docs/Learn-Schema.md b/site/docs/Learn-Schema.md index 8db514c87a..a71de70c26 100644 --- a/site/docs/Learn-Schema.md +++ b/site/docs/Learn-Schema.md @@ -41,7 +41,7 @@ The most basic components of a GraphQL schema are object types, which just repre ```graphql type Character { name: String! - appearsIn: [Episode] + appearsIn: [Episode]! } ``` @@ -55,6 +55,11 @@ The language is pretty readable, but let's go over it so that we can have a shar Now you know what a GraphQL object type looks like, and how to read the basics of the GraphQL type language. +### Arguments + +TODO +- Include the idea of default arguments + ### The Query and Mutation types Most types in your schema will just be normal object types, but there are two types that are special within a schema: @@ -74,7 +79,7 @@ query { hero { name } - droid(id: "2001") { + droid(id: "2000") { name } } @@ -155,7 +160,7 @@ Object types, scalars, and enums are the only kinds of types you can define in G ```graphql type Character { name: String! - appearsIn: [Episode] + appearsIn: [Episode]! } ``` @@ -165,7 +170,7 @@ The Non-Null type modifier can also be used when defining arguments for a field, ```graphql # { "graphiql": true, "variables": { "id": null } } -query DroidById($id: String!) { +query DroidById($id: ID!) { droid(id: $id) { name } @@ -212,10 +217,10 @@ For example, you could have an interface `Character` that represents any charact ```graphql interface Character { - id: String! - name: String + id: ID! + name: String! friends: [Character] - appearsIn: [Episode] + appearsIn: [Episode]! } ``` @@ -225,25 +230,28 @@ For example, here are some types that might implement `Character`: ```graphql type Human implements Character { - id: String! - name: String + id: ID! + name: String! friends: [Character] - appearsIn: [Episode] - homePlanet: String + appearsIn: [Episode]! + starships: [Starship] + totalCredits: Int } type Droid implements Character { - id: String! - name: String + id: ID! + name: String! friends: [Character] - appearsIn: [Episode] + appearsIn: [Episode]! primaryFunction: String } ``` You can see that both of these types have all of the fields from the `Character` interface, but also bring in extra fields, `homePlanet` and `primaryFunction`, that are specific to that particular type of character. -Interfaces are useful when you want to return an object or set of objects, but those might be of several different types. For example, in the following query: +Interfaces are useful when you want to return an object or set of objects, but those might be of several different types. + +For example, note that the following query produces an error: ```graphql # { "graphiql": true, "variables": { "ep": "JEDI" } } @@ -255,7 +263,9 @@ query HeroForEpisode($ep: Episode!) { } ``` -The `hero` field returns the type `Character`, which means it might be either a `Human` or a `Droid` depending on the `episode` argument. In the query above, you can only ask for fields that exist on the `Character` interface, and to ask for a field on a specific object type, you need to use an inline fragment: +The `hero` field returns the type `Character`, which means it might be either a `Human` or a `Droid` depending on the `episode` argument. In the query above, you can only ask for fields that exist on the `Character` interface, which doesn't include `primaryFunction`. + +To ask for a field on a specific object type, you need to use an inline fragment: ```graphql # { "graphiql": true, "variables": { "ep": "JEDI" } } @@ -275,20 +285,30 @@ Learn more about this in the [inline fragments](XXX) section in the query guide. Union types are very similar to interfaces, but they don't get to specify any common fields between the types. -XXX no example in SWAPI - ```graphql -union SearchResult = Photo | Person +union SearchResult = Human | Droid | Starship +``` -type Person { - name: String - age: Int -} +Wherever we return a `SearchResult` type in our schema, we might get a `Human`, a `Droid`, or a `Starship`. Note that members of a union type need to be concrete object types; you can't create a union type out of interfaces or other unions. -type Photo { - height: Int - width: Int +In this case, if you query a field that returns the `SearchResult` union type, you need to use a conditional fragment to be able to query any fields at all: + +```graphql +# { "graphiql": true} +{ + search(text: "an") { + ... on Human { + name + height + } + ... on Droid { + name + primaryFunction + } + ... on Starship { + name + length + } + } } ``` - -In this case, if you query a field that returns the `SearchResult` union type, you need to use a conditional fragment to be able to query any fields at all. diff --git a/site/docs/_swapiSchema.js b/site/docs/_swapiSchema.js index c6b7a8988f..82af8dd9d8 100644 --- a/site/docs/_swapiSchema.js +++ b/site/docs/_swapiSchema.js @@ -16,17 +16,138 @@ import { GraphQLString } from 'graphql'; +import { makeExecutableSchema } from 'graphql-tools'; -/** - * 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. - */ +const schemaString = ` +schema { + query: Query + mutation: Mutation +} + +# The query type, represents all of the entry points into our object graph +type Query { + hero(episode: Episode): Character + reviews(episode: Episode!): [Review] + search(text: String): [SearchResult] + droid(id: ID!): Droid + human(id: ID!): Human + starship(id: ID!): Starship +} + +# The mutation type, represents all updates we can make to our data +type Mutation { + createReview(episode: Episode, review: ReviewInput!): Review +} + +# The episodes in the Star Wars trilogy +enum Episode { + # Star Wars Episode IV: A New Hope, released in 1977. + NEWHOPE + + # Star Wars Episode V: The Empire Strikes Back, released in 1980. + EMPIRE + + # Star Wars Episode VI: Return of the Jedi, released in 1983. + JEDI +} + +# A character from the Star Wars universe +interface Character { + # The ID of the character + id: ID! + + # The name of the character + name: String! + + # The friends of the character, or an empty list if they have none + friends: [Character] + + # The movies this character appears in + appearsIn: [Episode]! +} + +# Units of height +enum LengthUnit { + # The standard unit around the world + METER + + # Primarily used in the United States + FOOT +} + +# A humanoid creature from the Star Wars universe +type Human implements Character { + # The ID of the human + id: ID! + + # What this human calls themselves + name: String! + + # Height in the preferred unit, default is meters + height(unit: LengthUnit = METER): Float + + # Mass in kilograms, or null if unknown + mass: Float + + # This human's friends, or an empty list if they have none + friends: [Character] + + # The movies this human appears in + appearsIn: [Episode]! + + # A list of starships this person has piloted, or an empty list if none + starships: [Starship] +} + +# An autonomous mechanical character in the Star Wars universe +type Droid implements Character { + # The ID of the droid + id: ID! + + # What others call this droid + name: String! + + # This droid's friends, or an empty list if they have none + friends: [Character] + + # The movies this droid appears in + appearsIn: [Episode]! + + # This droid's primary function + primaryFunction: String +} + +# Represents a review for a movie +type Review { + # The number of stars this review gave, 1-5 + stars: Int! + + # Comment about the movie + commentary: String +} + +# The input object sent when someone is creating a new review +input ReviewInput { + # 0-5 stars + stars: Int! + + # Comment about the movie, optional + commentary: String +} + +type Starship { + # The ID of the starship + id: ID! + + # The name of the starship + name: String! + + # Length of the starship, along the longest axis + length(unit: LengthUnit = METER): Float +} + +union SearchResult = Human | Droid | Starship +`; /** * This defines a basic set of data for our Star Wars Schema. @@ -36,72 +157,108 @@ import { * JSON objects in a more complex demo. */ -var luke = { - id: '1000', - name: 'Luke Skywalker', - friends: [ '1002', '1003', '2000', '2001' ], - appearsIn: [ 4, 5, 6 ], - homePlanet: 'Tatooine', -}; - -var vader = { - id: '1001', - name: 'Darth Vader', - friends: [ '1004' ], - appearsIn: [ 4, 5, 6 ], - homePlanet: 'Tatooine', -}; - -var han = { - id: '1002', - name: 'Han Solo', - friends: [ '1000', '1003', '2001' ], - appearsIn: [ 4, 5, 6 ], -}; - -var leia = { - id: '1003', - name: 'Leia Organa', - friends: [ '1000', '1002', '2000', '2001' ], - appearsIn: [ 4, 5, 6 ], - homePlanet: 'Alderaan', -}; - -var tarkin = { - id: '1004', - name: 'Wilhuff Tarkin', - friends: [ '1001' ], - appearsIn: [ 4 ], -}; - -var humanData = { - 1000: luke, - 1001: vader, - 1002: han, - 1003: leia, - 1004: tarkin, -}; - -var threepio = { - id: '2000', - name: 'C-3PO', - friends: [ '1000', '1002', '1003', '2001' ], - appearsIn: [ 4, 5, 6 ], - primaryFunction: 'Protocol', -}; - -var artoo = { - id: '2001', - name: 'R2-D2', - friends: [ '1000', '1002', '1003' ], - appearsIn: [ 4, 5, 6 ], - primaryFunction: 'Astromech', -}; - -var droidData = { - 2000: threepio, - 2001: artoo, -}; +const humans = [ + { + id: '1000', + name: 'Luke Skywalker', + friends: [ '1002', '1003', '2000', '2001' ], + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + height: 1.72, + mass: 77, + starships: [ '3001', '3003' ], + }, + { + id: '1001', + name: 'Darth Vader', + friends: [ '1004' ], + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + height: 2.02, + mass: 136, + starships: [ '3002' ], + }, + { + id: '1002', + name: 'Han Solo', + friends: [ '1000', '1003', '2001' ], + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + height: 1.8, + mass: 80, + starships: [ '3000', '3003' ], + }, + { + id: '1003', + name: 'Leia Organa', + friends: [ '1000', '1002', '2000', '2001' ], + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + height: 1.5, + mass: 49, + starships: [], + }, + { + id: '1004', + name: 'Wilhuff Tarkin', + friends: [ '1001' ], + appearsIn: [ 'NEWHOPE' ], + height: 1.8, + mass: null, + starships: [], + }, +]; + +const humanData = {}; +humans.forEach((ship) => { + humanData[ship.id] = ship; +}); + +const droids = [ + { + id: '2000', + name: 'C-3PO', + friends: [ '1000', '1002', '1003', '2001' ], + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + primaryFunction: 'Protocol', + }, + { + id: '2001', + name: 'R2-D2', + friends: [ '1000', '1002', '1003' ], + appearsIn: [ 'NEWHOPE', 'EMPIRE', 'JEDI' ], + primaryFunction: 'Astromech', + }, +]; + +const droidData = {}; +droids.forEach((ship) => { + droidData[ship.id] = ship; +}); + +const starships = [ + { + id: '3000', + name: 'Millenium Falcon', + length: 34.37, + }, + { + id: '3001', + name: 'X-Wing', + length: 12.5, + }, + { + id: '3002', + name: 'TIE Advanced x1', + length: 9.2, + }, + { + id: '3003', + name: 'Imperial shuttle', + length: 20, + }, +]; + +const starshipData = {}; +starships.forEach((ship) => { + starshipData[ship.id] = ship; +}); /** * Helper function to get a character by ID. @@ -122,12 +279,12 @@ function getFriends(character) { * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2. */ function getHero(episode) { - if (episode === 5) { + if (episode === 'EMPIRE') { // Luke is the hero of Episode V. - return luke; + return humanData['1000']; } // Artoo is the hero otherwise. - return artoo; + return droidData['2001']; } /** @@ -144,246 +301,78 @@ function getDroid(id) { return droidData[id]; } +function getStarship(id) { + return starshipData[id]; +} -/** - * 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 } - */ -var 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] - * } - */ -var 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] - * } - */ -var 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 ] -}); +const resolvers = { + Query: { + hero: (root, { episode }) => getHero(episode), + human: (root, { id }) => getHuman(id), + droid: (root, { id }) => getDroid(id), + starship: (root, { id }) => getStarship(id), + reviews: () => null, + search: (root, { text }) => { + const re = new RegExp(text, 'i'); -/** - * 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 - * } - */ -var 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 ] -}); + const allData = [ + ...humans, + ...droids, + ...starships, + ]; -/** - * 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 - * } - * - */ -var 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), + return allData.filter((obj) => re.test(obj.name)); }, - human: { - type: humanType, - args: { - id: { - description: 'id of the human', - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve: (root, { id }) => getHuman(id), + }, + Mutation: { + createReview: (root, { episode, review }) => review, + }, + Character: { + __resolveType(data, context, info){ + if(humanData[data.id]){ + return info.schema.getType('Human'); + } + if(droidData[data.id]){ + return info.schema.getType('Droid'); + } + return null; }, - droid: { - type: droidType, - args: { - id: { - description: 'id of the droid', - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve: (root, { id }) => getDroid(id), + }, + Human: { + height: ({ height }) => height, // XXX add units + friends: ({ friends }) => friends.map(getCharacter), + starships: () => null, + appearsIn: ({ appearsIn }) => appearsIn, + }, + Droid: { + friends: ({ friends }) => friends.map(getCharacter), + appearsIn: ({ appearsIn }) => appearsIn, + }, + Starship: { + length: ({ length }) => length, // XXX add units + }, + SearchResult: { + __resolveType(data, context, info){ + if(humanData[data.id]){ + return info.schema.getType('Human'); + } + if(droidData[data.id]){ + return info.schema.getType('Droid'); + } + if(starshipData[data.id]){ + return info.schema.getType('Starship'); + } + return null; }, - }) -}); + }, +} /** * Finally, we construct our schema (whose starting query type is the query * type we defined above) and export it. */ -export var StarWarsSchema = new GraphQLSchema({ - query: queryType +console.log('hi') +export const StarWarsSchema = makeExecutableSchema({ + typeDefs: [schemaString], + resolvers });