From 04d3114d5f93fab75e29fee35581a3345ddfb9ee Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 11:16:39 +0100 Subject: [PATCH 01/12] Add an Union type --- src/schema/apiHelper.js | 8 +++++++ src/schema/index.js | 18 +++++++++------- src/schema/relayNode.js | 3 +++ src/schema/types/machine.js | 42 +++++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 src/schema/types/machine.js diff --git a/src/schema/apiHelper.js b/src/schema/apiHelper.js index b4854e4..1bb904f 100644 --- a/src/schema/apiHelper.js +++ b/src/schema/apiHelper.js @@ -31,6 +31,14 @@ export async function getObjectFromUrl(url: string): Promise { return objectWithId(data); } +/** + * Given an object URL, return the Swapi type from it + * @param url + */ +export function getSwapiTypeFromUrl(url: string): string { + return url.split('/')[4]; +} + /** * Given a type and ID, get the object with the ID. */ diff --git a/src/schema/index.js b/src/schema/index.js index 7947bc5..9ceb330 100644 --- a/src/schema/index.js +++ b/src/schema/index.js @@ -12,6 +12,7 @@ import { GraphQLInt, GraphQLList, GraphQLObjectType, + GraphQLUnionType, GraphQLSchema, } from 'graphql'; @@ -29,21 +30,23 @@ import { swapiTypeToGraphQLType, nodeField } from './relayNode'; /** * Creates a root field to get an object of a given type. * Accepts either `id`, the globally unique ID used in GraphQL, - * or `idName`, the per-type ID used in SWAPI. + * or `idName`, the per-type ID used in SWAPI (idName is only + * usable on non-union elements). */ function rootFieldByID(idName, swapiType) { - const getter = id => getObjectFromTypeAndId(swapiType, id); + const type = swapiTypeToGraphQLType(swapiType); const argDefs = {}; argDefs.id = { type: GraphQLID }; - argDefs[idName] = { type: GraphQLID }; + if (!(type instanceof GraphQLUnionType)) { + argDefs[idName] = { type: GraphQLID }; + } return { type: swapiTypeToGraphQLType(swapiType), args: argDefs, resolve: (_, args) => { - if (args[idName] !== undefined && args[idName] !== null) { - return getter(args[idName]); + if (!(swapiType instanceof GraphQLUnionType) && args[idName] !== undefined && args[idName] !== null) { + return getObjectFromTypeAndId(swapiType, args[idName]); } - if (args.id !== undefined && args.id !== null) { const globalId = fromGlobalId(args.id); if ( @@ -53,7 +56,7 @@ function rootFieldByID(idName, swapiType) { ) { throw new Error('No valid ID extracted from ' + args.id); } - return getter(globalId.id); + return getObjectFromTypeAndId(globalId.type, globalId.id); } throw new Error('must provide id or ' + idName); }, @@ -121,6 +124,7 @@ const rootType = new GraphQLObjectType({ starship: rootFieldByID('starshipID', 'starships'), allVehicles: rootConnection('Vehicles', 'vehicles'), vehicle: rootFieldByID('vehicleID', 'vehicles'), + machine: rootFieldByID('machineID', 'machines'), node: nodeField, }), }); diff --git a/src/schema/relayNode.js b/src/schema/relayNode.js index 7ea8edb..be72955 100644 --- a/src/schema/relayNode.js +++ b/src/schema/relayNode.js @@ -23,6 +23,7 @@ export function swapiTypeToGraphQLType(swapiType: string): GraphQLObjectType { const SpeciesType = require('./types/species').default; const StarshipType = require('./types/starship').default; const VehicleType = require('./types/vehicle').default; + const MachineType = require('./types/machine').default; switch (swapiType) { case 'films': @@ -37,6 +38,8 @@ export function swapiTypeToGraphQLType(swapiType: string): GraphQLObjectType { return VehicleType; case 'species': return SpeciesType; + case 'machines': + return MachineType; default: throw new Error('Unrecognized type `' + swapiType + '`.'); } diff --git a/src/schema/types/machine.js b/src/schema/types/machine.js new file mode 100644 index 0000000..f318db8 --- /dev/null +++ b/src/schema/types/machine.js @@ -0,0 +1,42 @@ +/* @flow */ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { + GraphQLUnionType +} from 'graphql'; + +import { getSwapiTypeFromUrl } from '../apiHelper'; + +import VehicleType from './vehicle'; +import StarshipType from './starship'; + +/** + * GraphQL equivalent of every "machine" in the SW univers (from SWAPI) + */ +const MachineType = new GraphQLUnionType({ + name: 'Machine', + types: [VehicleType, StarshipType], + resolveType(value) { + + const swapiType = getSwapiTypeFromUrl(value.url); + + switch (swapiType) { + case 'vehicles': + return VehicleType; + case 'starships': + return StarshipType; + default: + throw new Error('Type `' + swapiType + '` not in Machine type.'); + } + + }, + description: 'Union of Vehicle and Starship : every available machine' +}); + +export default MachineType; From 85be99f5c77e1b722842faafb3dca5fb28de0528 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 11:23:17 +0100 Subject: [PATCH 02/12] Small fix : re-use the constant --- src/schema/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/schema/index.js b/src/schema/index.js index 9ceb330..ba1f12f 100644 --- a/src/schema/index.js +++ b/src/schema/index.js @@ -34,14 +34,14 @@ import { swapiTypeToGraphQLType, nodeField } from './relayNode'; * usable on non-union elements). */ function rootFieldByID(idName, swapiType) { - const type = swapiTypeToGraphQLType(swapiType); + const graphQLType = swapiTypeToGraphQLType(swapiType); const argDefs = {}; argDefs.id = { type: GraphQLID }; - if (!(type instanceof GraphQLUnionType)) { + if (!(graphQLType instanceof GraphQLUnionType)) { argDefs[idName] = { type: GraphQLID }; } return { - type: swapiTypeToGraphQLType(swapiType), + type: graphQLType, args: argDefs, resolve: (_, args) => { if (!(swapiType instanceof GraphQLUnionType) && args[idName] !== undefined && args[idName] !== null) { From 5eec7229ababca86dc463541745ddb4d7812d6c1 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 13:36:20 +0100 Subject: [PATCH 03/12] Add union support to query all vehicles and ships --- doc/example_queries/09_union.graphql | 16 ++++++++++++++++ src/schema/index.js | 20 ++++++++++++++++++-- src/schema/relayNode.js | 21 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 doc/example_queries/09_union.graphql diff --git a/doc/example_queries/09_union.graphql b/doc/example_queries/09_union.graphql new file mode 100644 index 0000000..a07dcca --- /dev/null +++ b/doc/example_queries/09_union.graphql @@ -0,0 +1,16 @@ +{ + allMachines { + machines { + ... on Vehicle { + id, + name, + vehicleClass + }, + ... on Starship { + id, + name, + starshipClass + } + } + } +} diff --git a/src/schema/index.js b/src/schema/index.js index ba1f12f..7f40cb1 100644 --- a/src/schema/index.js +++ b/src/schema/index.js @@ -25,7 +25,7 @@ import { import { getObjectsByType, getObjectFromTypeAndId } from './apiHelper'; -import { swapiTypeToGraphQLType, nodeField } from './relayNode'; +import { swapiTypeToGraphQLType, graphQLTypeToSwapiType, nodeField } from './relayNode'; /** * Creates a root field to get an object of a given type. @@ -97,7 +97,22 @@ full "{ edges { node } }" version should be used instead.`, type: connectionType, args: connectionArgs, resolve: async (_, args) => { - const { objects, totalCount } = await getObjectsByType(swapiType); + const graphQLType = swapiTypeToGraphQLType(swapiType); + let objects = []; + let totalCount = 0; + if (graphQLType instanceof GraphQLUnionType) { + for (const type of graphQLType.getTypes()) { + // eslint-disable-next-line no-await-in-loop + const objectsByType = await getObjectsByType(graphQLTypeToSwapiType(type)); + objects = objects.concat(objectsByType.objects); + totalCount += objectsByType.totalCount; + } + } else { + const objectsByType = await getObjectsByType(swapiType); + objects = objects.concat(objectsByType.objects); + totalCount = objectsByType.totalCount; + } + return { ...connectionFromArray(objects, args), totalCount, @@ -125,6 +140,7 @@ const rootType = new GraphQLObjectType({ allVehicles: rootConnection('Vehicles', 'vehicles'), vehicle: rootFieldByID('vehicleID', 'vehicles'), machine: rootFieldByID('machineID', 'machines'), + allMachines: rootConnection('Machines', 'machines'), node: nodeField, }), }); diff --git a/src/schema/relayNode.js b/src/schema/relayNode.js index be72955..0f2b478 100644 --- a/src/schema/relayNode.js +++ b/src/schema/relayNode.js @@ -45,6 +45,27 @@ export function swapiTypeToGraphQLType(swapiType: string): GraphQLObjectType { } } +/** + * Given a GraphQL type, return the corresponding SWAPI type + */ +export function graphQLTypeToSwapiType(graphQLType: GraphQLObjectType): string { + const typeMap = { + 'Film': 'films', + 'Person': 'people', + 'Planet': 'planets', + 'Starship': 'starships', + 'Vehicle': 'vehicles', + 'Species': 'species', + 'Machine': 'machines', + }; + + if (graphQLType.name in typeMap) { + return typeMap[graphQLType.name]; + } else { + throw new Error('Unrecognized type `' + graphQLType.name + '`.'); + } +} + const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const { type, id } = fromGlobalId(globalId); From 4325d05aca2c3da7198cca6ee0f897bc386a1772 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 14:10:07 +0100 Subject: [PATCH 04/12] Fix unnecessarily quoted properties --- src/schema/relayNode.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/schema/relayNode.js b/src/schema/relayNode.js index 0f2b478..8f0d381 100644 --- a/src/schema/relayNode.js +++ b/src/schema/relayNode.js @@ -50,13 +50,13 @@ export function swapiTypeToGraphQLType(swapiType: string): GraphQLObjectType { */ export function graphQLTypeToSwapiType(graphQLType: GraphQLObjectType): string { const typeMap = { - 'Film': 'films', - 'Person': 'people', - 'Planet': 'planets', - 'Starship': 'starships', - 'Vehicle': 'vehicles', - 'Species': 'species', - 'Machine': 'machines', + Film: 'films', + Person: 'people', + Planet: 'planets', + Starship: 'starships', + Vehicle: 'vehicles', + Species: 'species', + Machine: 'machines', }; if (graphQLType.name in typeMap) { From 71031da4b35e5ec2be67f6e3110c4beade784ad4 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 14:16:48 +0100 Subject: [PATCH 05/12] Remove useless else... --- src/schema/relayNode.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/schema/relayNode.js b/src/schema/relayNode.js index 8f0d381..7d4a304 100644 --- a/src/schema/relayNode.js +++ b/src/schema/relayNode.js @@ -61,9 +61,9 @@ export function graphQLTypeToSwapiType(graphQLType: GraphQLObjectType): string { if (graphQLType.name in typeMap) { return typeMap[graphQLType.name]; - } else { - throw new Error('Unrecognized type `' + graphQLType.name + '`.'); } + + throw new Error('Unrecognized type `' + graphQLType.name + '`.'); } const { nodeInterface, nodeField } = nodeDefinitions( From 6fd8041c1439c391d6fb387ff5fb606fd3fe8058 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 14:21:40 +0100 Subject: [PATCH 06/12] Prettify --- src/schema/index.js | 16 +++++++++++++--- src/schema/types/machine.js | 8 ++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/schema/index.js b/src/schema/index.js index 7f40cb1..887de79 100644 --- a/src/schema/index.js +++ b/src/schema/index.js @@ -25,7 +25,11 @@ import { import { getObjectsByType, getObjectFromTypeAndId } from './apiHelper'; -import { swapiTypeToGraphQLType, graphQLTypeToSwapiType, nodeField } from './relayNode'; +import { + swapiTypeToGraphQLType, + graphQLTypeToSwapiType, + nodeField, +} from './relayNode'; /** * Creates a root field to get an object of a given type. @@ -44,7 +48,11 @@ function rootFieldByID(idName, swapiType) { type: graphQLType, args: argDefs, resolve: (_, args) => { - if (!(swapiType instanceof GraphQLUnionType) && args[idName] !== undefined && args[idName] !== null) { + if ( + !(swapiType instanceof GraphQLUnionType) && + args[idName] !== undefined && + args[idName] !== null + ) { return getObjectFromTypeAndId(swapiType, args[idName]); } if (args.id !== undefined && args.id !== null) { @@ -103,7 +111,9 @@ full "{ edges { node } }" version should be used instead.`, if (graphQLType instanceof GraphQLUnionType) { for (const type of graphQLType.getTypes()) { // eslint-disable-next-line no-await-in-loop - const objectsByType = await getObjectsByType(graphQLTypeToSwapiType(type)); + const objectsByType = await getObjectsByType( + graphQLTypeToSwapiType(type), + ); objects = objects.concat(objectsByType.objects); totalCount += objectsByType.totalCount; } diff --git a/src/schema/types/machine.js b/src/schema/types/machine.js index f318db8..1a5cafc 100644 --- a/src/schema/types/machine.js +++ b/src/schema/types/machine.js @@ -7,9 +7,7 @@ * LICENSE-examples file in the root directory of this source tree. */ -import { - GraphQLUnionType -} from 'graphql'; +import { GraphQLUnionType } from 'graphql'; import { getSwapiTypeFromUrl } from '../apiHelper'; @@ -23,7 +21,6 @@ const MachineType = new GraphQLUnionType({ name: 'Machine', types: [VehicleType, StarshipType], resolveType(value) { - const swapiType = getSwapiTypeFromUrl(value.url); switch (swapiType) { @@ -34,9 +31,8 @@ const MachineType = new GraphQLUnionType({ default: throw new Error('Type `' + swapiType + '` not in Machine type.'); } - }, - description: 'Union of Vehicle and Starship : every available machine' + description: 'Union of Vehicle and Starship : every available machine', }); export default MachineType; From 6048896ad8ed2ae774252af503421f865554da05 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 14:37:45 +0100 Subject: [PATCH 07/12] Fix code style and flow --- src/schema/relayNode.js | 6 ++++-- src/schema/types/machine.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/schema/relayNode.js b/src/schema/relayNode.js index 7d4a304..421fd82 100644 --- a/src/schema/relayNode.js +++ b/src/schema/relayNode.js @@ -9,14 +9,16 @@ import { getObjectFromTypeAndId } from './apiHelper'; -import type { GraphQLObjectType } from 'graphql'; +import type { GraphQLObjectType, GraphQLUnionType } from 'graphql'; import { nodeDefinitions, fromGlobalId } from 'graphql-relay'; /** * Given a "type" in SWAPI, returns the corresponding GraphQL type. */ -export function swapiTypeToGraphQLType(swapiType: string): GraphQLObjectType { +export function swapiTypeToGraphQLType( + swapiType: string, +): GraphQLObjectType | GraphQLUnionType { const FilmType = require('./types/film').default; const PersonType = require('./types/person').default; const PlanetType = require('./types/planet').default; diff --git a/src/schema/types/machine.js b/src/schema/types/machine.js index 1a5cafc..caa480a 100644 --- a/src/schema/types/machine.js +++ b/src/schema/types/machine.js @@ -20,7 +20,7 @@ import StarshipType from './starship'; const MachineType = new GraphQLUnionType({ name: 'Machine', types: [VehicleType, StarshipType], - resolveType(value) { + resolveType: value => { const swapiType = getSwapiTypeFromUrl(value.url); switch (swapiType) { From 294c488c6b6731962ba5c08cae8811098aa79c60 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 15:08:33 +0100 Subject: [PATCH 08/12] Add unit tests --- src/schema/__tests__/machine.js | 138 ++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/schema/__tests__/machine.js diff --git a/src/schema/__tests__/machine.js b/src/schema/__tests__/machine.js new file mode 100644 index 0000000..e8415db --- /dev/null +++ b/src/schema/__tests__/machine.js @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { swapi } from './swapi'; + +// 80+ char lines are useful in describe/it, so ignore in this file. +/* eslint-disable max-len */ + +function getDocument(query) { + return `${query} + fragment AllStarshipProperties on Starship { + MGLT + starshipCargoCapacity: cargoCapacity + consumables + starshipCostInCredits: costInCredits + crew + hyperdriveRating + length + manufacturers + maxAtmospheringSpeed + model + name + passengers + starshipClass + filmConnection(first:1) { edges { node { title } } } + pilotConnection(first:1) { edges { node { name } } } + } + + fragment AllVehicleProperties on Vehicle { + vehicleCargoCapacity: cargoCapacity + consumables + vehicleCostInCredits: costInCredits + crew + length + manufacturers + maxAtmospheringSpeed + model + name + passengers + vehicleClass + filmConnection(first:1) { edges { node { title } } } + pilotConnection(first:1) { edges { node { name } } } + } + `; +} + +describe('Machine type', async () => { + + it('Gets an object by global ID', async () => { + const query = '{ starship(starshipID: 5) { id, name } }'; + const result = await swapi(query); + const nextQuery = ` + { machine(id: "${result.data.starship.id}") { ... on Vehicle { id, name }, ... on Starship { id, name } } } + `; + const nextResult = await swapi(nextQuery); + expect(result.data.starship.name).to.equal('Sentinel-class landing craft'); + expect(nextResult.data.machine.name).to.equal( + 'Sentinel-class landing craft', + ); + expect(result.data.starship.id).to.equal(nextResult.data.machine.id); + }); + + it('Gets all properties', async () => { + + const query = '{ starship(starshipID: 5) { id, name } }'; + const idResult = await swapi(query); + + const nextQuery = getDocument( + `{ + machine(id: "${idResult.data.starship.id}") { + ... on Starship { + ...AllStarshipProperties + } + ... on Vehicle { + ...AllVehicleProperties + } + } + }`, + ); + const result = await swapi(nextQuery); + const expected = { + MGLT: 70, + starshipCargoCapacity: 180000, + consumables: '1 month', + starshipCostInCredits: 240000, + crew: '5', + filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, + hyperdriveRating: 1, + length: 38, + manufacturers: [ + 'Sienar Fleet Systems', + 'Cyngus Spaceworks' + ], + maxAtmospheringSpeed: 1000, + model: 'Sentinel-class landing craft', + name: 'Sentinel-class landing craft', + passengers: '75', + pilotConnection: { edges: [] }, + starshipClass: 'landing craft', + }; + expect(result.data.machine).to.deep.equal(expected); + }); + + it('All objects query', async () => { + const query = getDocument( + '{ allMachines { edges { cursor, node { ... on Starship { ...AllStarshipProperties }, ... on Vehicle { ...AllVehicleProperties } } } } }', + ); + const result = await swapi(query); + expect(result.data.allMachines.edges.length).to.equal(76); + }); + + it('Pagination query', async () => { + const query = `{ + allMachines(first: 2) { edges { cursor, node { ... on Vehicle { name }, ... on Starship { name } } } } + }`; + const result = await swapi(query); + expect(result.data.allMachines.edges.map(e => e.node.name)).to.deep.equal([ + 'Sand Crawler', + 'T-16 skyhopper', + ]); + const nextCursor = result.data.allMachines.edges[1].cursor; + + const nextQuery = `{ allMachines(first: 2, after:"${nextCursor}") { + edges { cursor, node { ... on Vehicle { name }, ... on Starship { name } } } } + }`; + const nextResult = await swapi(nextQuery); + expect( + nextResult.data.allMachines.edges.map(e => e.node.name), + ).to.deep.equal(['X-34 landspeeder', 'TIE/LN starfighter']); + }); +}); From 3e1f0597b6228fe808bf5f1ba6f9981a51659c57 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 15:40:35 +0100 Subject: [PATCH 09/12] Prettify machine test cases --- src/schema/__tests__/machine.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/schema/__tests__/machine.js b/src/schema/__tests__/machine.js index e8415db..4d80edf 100644 --- a/src/schema/__tests__/machine.js +++ b/src/schema/__tests__/machine.js @@ -52,12 +52,13 @@ function getDocument(query) { } describe('Machine type', async () => { - it('Gets an object by global ID', async () => { const query = '{ starship(starshipID: 5) { id, name } }'; const result = await swapi(query); const nextQuery = ` - { machine(id: "${result.data.starship.id}") { ... on Vehicle { id, name }, ... on Starship { id, name } } } + { machine(id: "${ + result.data.starship.id + }") { ... on Vehicle { id, name }, ... on Starship { id, name } } } `; const nextResult = await swapi(nextQuery); expect(result.data.starship.name).to.equal('Sentinel-class landing craft'); @@ -68,7 +69,6 @@ describe('Machine type', async () => { }); it('Gets all properties', async () => { - const query = '{ starship(starshipID: 5) { id, name } }'; const idResult = await swapi(query); @@ -94,10 +94,7 @@ describe('Machine type', async () => { filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, hyperdriveRating: 1, length: 38, - manufacturers: [ - 'Sienar Fleet Systems', - 'Cyngus Spaceworks' - ], + manufacturers: ['Sienar Fleet Systems', 'Cyngus Spaceworks'], maxAtmospheringSpeed: 1000, model: 'Sentinel-class landing craft', name: 'Sentinel-class landing craft', From 50ab10fa597b2263a3e853c7f303d423df6f49d5 Mon Sep 17 00:00:00 2001 From: Elendev Date: Thu, 15 Mar 2018 15:51:42 +0100 Subject: [PATCH 10/12] prettify.... --- src/schema/__tests__/machine.js | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/schema/__tests__/machine.js b/src/schema/__tests__/machine.js index 4d80edf..5c0255a 100644 --- a/src/schema/__tests__/machine.js +++ b/src/schema/__tests__/machine.js @@ -56,9 +56,12 @@ describe('Machine type', async () => { const query = '{ starship(starshipID: 5) { id, name } }'; const result = await swapi(query); const nextQuery = ` - { machine(id: "${ - result.data.starship.id - }") { ... on Vehicle { id, name }, ... on Starship { id, name } } } + { + machine(id: "${result.data.starship.id}") { + ... on Vehicle { id, name }, + ... on Starship { id, name } + } + } `; const nextResult = await swapi(nextQuery); expect(result.data.starship.name).to.equal('Sentinel-class landing craft'); @@ -107,7 +110,17 @@ describe('Machine type', async () => { it('All objects query', async () => { const query = getDocument( - '{ allMachines { edges { cursor, node { ... on Starship { ...AllStarshipProperties }, ... on Vehicle { ...AllVehicleProperties } } } } }', + `{ + allMachines { + edges { + cursor, + node { + ... on Starship { ...AllStarshipProperties }, + ... on Vehicle { ...AllVehicleProperties } + } + } + } + }`, ); const result = await swapi(query); expect(result.data.allMachines.edges.length).to.equal(76); @@ -115,7 +128,15 @@ describe('Machine type', async () => { it('Pagination query', async () => { const query = `{ - allMachines(first: 2) { edges { cursor, node { ... on Vehicle { name }, ... on Starship { name } } } } + allMachines(first: 2) { + edges { + cursor, + node { + ... on Vehicle { name }, + ... on Starship { name } + } + } + } }`; const result = await swapi(query); expect(result.data.allMachines.edges.map(e => e.node.name)).to.deep.equal([ From 73041b37d1c8a14de2944431a647dbe378ade819 Mon Sep 17 00:00:00 2001 From: Elendev Date: Mon, 19 Mar 2018 10:31:59 +0100 Subject: [PATCH 11/12] Add 'Person' in the 'Machines' type to represent all machines in the universe (vehicle, starship and Droid) --- src/schema/__tests__/machine.js | 32 ++++++++++++++++++++++---- src/schema/graphQLFilteredUnionType.js | 26 +++++++++++++++++++++ src/schema/index.js | 8 +++++++ src/schema/types/machine.js | 20 ++++++++++++---- 4 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 src/schema/graphQLFilteredUnionType.js diff --git a/src/schema/__tests__/machine.js b/src/schema/__tests__/machine.js index 5c0255a..dc5e248 100644 --- a/src/schema/__tests__/machine.js +++ b/src/schema/__tests__/machine.js @@ -48,6 +48,22 @@ function getDocument(query) { filmConnection(first:1) { edges { node { title } } } pilotConnection(first:1) { edges { node { name } } } } + + fragment AllPersonProperties on Person { + birthYear + eyeColor + gender + hairColor + height + homeworld { name } + mass + name + skinColor + species { name } + filmConnection(first:1) { edges { node { title } } } + starshipConnection(first:1) { edges { node { name } } } + vehicleConnection(first:1) { edges { node { name } } } + } `; } @@ -59,7 +75,8 @@ describe('Machine type', async () => { { machine(id: "${result.data.starship.id}") { ... on Vehicle { id, name }, - ... on Starship { id, name } + ... on Starship { id, name }, + ... on Person { id, name } } } `; @@ -84,6 +101,9 @@ describe('Machine type', async () => { ... on Vehicle { ...AllVehicleProperties } + ... on Person { + ...AllPersonProperties + } } }`, ); @@ -116,14 +136,15 @@ describe('Machine type', async () => { cursor, node { ... on Starship { ...AllStarshipProperties }, - ... on Vehicle { ...AllVehicleProperties } + ... on Vehicle { ...AllVehicleProperties }, + ... on Person { ... AllPersonProperties } } } } }`, ); const result = await swapi(query); - expect(result.data.allMachines.edges.length).to.equal(76); + expect(result.data.allMachines.edges.length).to.equal(81); }); it('Pagination query', async () => { @@ -133,7 +154,8 @@ describe('Machine type', async () => { cursor, node { ... on Vehicle { name }, - ... on Starship { name } + ... on Starship { name }, + ... on Person { name } } } } @@ -146,7 +168,7 @@ describe('Machine type', async () => { const nextCursor = result.data.allMachines.edges[1].cursor; const nextQuery = `{ allMachines(first: 2, after:"${nextCursor}") { - edges { cursor, node { ... on Vehicle { name }, ... on Starship { name } } } } + edges { cursor, node { ... on Vehicle { name }, ... on Starship { name }, ... on Person { name } } } } }`; const nextResult = await swapi(nextQuery); expect( diff --git a/src/schema/graphQLFilteredUnionType.js b/src/schema/graphQLFilteredUnionType.js new file mode 100644 index 0000000..b6a7ce6 --- /dev/null +++ b/src/schema/graphQLFilteredUnionType.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { GraphQLUnionType, GraphQLObjectType } from 'graphql'; +import { GraphQLUnionTypeConfig } from 'graphql/type/definition'; + +/** + * GraphQLUnionType with a 'filter' method that allow to filter the + * elements of the union + */ +export default class GraphQLFilteredUnionType extends GraphQLUnionType { + constructor(config: GraphQLUnionTypeConfig<*, *>): void { + super(config); + + if ('filter' in config) { + this.filter = config.filter; + } else { + this.filter = (type, objects) => objects; + } + } +} diff --git a/src/schema/index.js b/src/schema/index.js index 887de79..ef93f3d 100644 --- a/src/schema/index.js +++ b/src/schema/index.js @@ -30,6 +30,7 @@ import { graphQLTypeToSwapiType, nodeField, } from './relayNode'; +import GraphQLFilteredUnionType from './graphQLFilteredUnionType'; /** * Creates a root field to get an object of a given type. @@ -114,6 +115,13 @@ full "{ edges { node } }" version should be used instead.`, const objectsByType = await getObjectsByType( graphQLTypeToSwapiType(type), ); + if (graphQLType instanceof GraphQLFilteredUnionType) { + objectsByType.objects = graphQLType.filter( + type, + objectsByType.objects, + ); + objectsByType.totalCount = objectsByType.objects.length; + } objects = objects.concat(objectsByType.objects); totalCount += objectsByType.totalCount; } diff --git a/src/schema/types/machine.js b/src/schema/types/machine.js index caa480a..1c9baa7 100644 --- a/src/schema/types/machine.js +++ b/src/schema/types/machine.js @@ -7,19 +7,20 @@ * LICENSE-examples file in the root directory of this source tree. */ -import { GraphQLUnionType } from 'graphql'; +import GraphQLFilteredUnionType from '../graphQLFilteredUnionType'; import { getSwapiTypeFromUrl } from '../apiHelper'; import VehicleType from './vehicle'; import StarshipType from './starship'; +import PersonType from './person'; /** * GraphQL equivalent of every "machine" in the SW univers (from SWAPI) */ -const MachineType = new GraphQLUnionType({ +const MachineType = new GraphQLFilteredUnionType({ name: 'Machine', - types: [VehicleType, StarshipType], + types: [VehicleType, StarshipType, PersonType], resolveType: value => { const swapiType = getSwapiTypeFromUrl(value.url); @@ -28,11 +29,22 @@ const MachineType = new GraphQLUnionType({ return VehicleType; case 'starships': return StarshipType; + case 'people': + return PersonType; default: throw new Error('Type `' + swapiType + '` not in Machine type.'); } }, - description: 'Union of Vehicle and Starship : every available machine', + filter: (type, objects) => { + if (type.name === PersonType.name) { + // filter Person to return only droid (species ID : 2) + return objects.filter(person => + person.species.includes('https://swapi.co/api/species/2/'), + ); + } + return objects; + }, + description: 'Union of Vehicle, Starship and Droid : every available machine', }); export default MachineType; From 56b27b5a155365cd4a61bed208509fdc31e48487 Mon Sep 17 00:00:00 2001 From: Elendev Date: Mon, 19 Mar 2018 10:36:25 +0100 Subject: [PATCH 12/12] Remove unused import ^^" --- src/schema/graphQLFilteredUnionType.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema/graphQLFilteredUnionType.js b/src/schema/graphQLFilteredUnionType.js index b6a7ce6..8af5cb1 100644 --- a/src/schema/graphQLFilteredUnionType.js +++ b/src/schema/graphQLFilteredUnionType.js @@ -6,7 +6,7 @@ * LICENSE-examples file in the root directory of this source tree. */ -import { GraphQLUnionType, GraphQLObjectType } from 'graphql'; +import { GraphQLUnionType } from 'graphql'; import { GraphQLUnionTypeConfig } from 'graphql/type/definition'; /**