Skip to content
16 changes: 16 additions & 0 deletions doc/example_queries/09_union.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
allMachines {
machines {
... on Vehicle {
id,
name,
vehicleClass
},
... on Starship {
id,
name,
starshipClass
}
}
}
}
178 changes: 178 additions & 0 deletions src/schema/__tests__/machine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* 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.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please copy updated header from master


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 */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both lines not needed after recent changes in master


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 } } }
}

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 } } }
}
`;
}

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 },
... on Person { 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
}
... on Person {
...AllPersonProperties
}
}
}`,
);
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 },
... on Person { ... AllPersonProperties }
}
}
}
}`,
);
const result = await swapi(query);
expect(result.data.allMachines.edges.length).to.equal(81);
});

it('Pagination query', async () => {
const query = `{
allMachines(first: 2) {
edges {
cursor,
node {
... on Vehicle { name },
... on Starship { name },
... on Person { 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 }, ... on Person { 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']);
});
});
8 changes: 8 additions & 0 deletions src/schema/apiHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export async function getObjectFromUrl(url: string): Promise<Object> {
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.
*/
Expand Down
26 changes: 26 additions & 0 deletions src/schema/graphQLFilteredUnionType.js
Original file line number Diff line number Diff line change
@@ -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 } 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;
}
}
}
58 changes: 48 additions & 10 deletions src/schema/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
GraphQLInt,
GraphQLList,
GraphQLObjectType,
GraphQLUnionType,
GraphQLSchema,
} from 'graphql';

Expand All @@ -25,26 +26,37 @@ import {

import { getObjectsByType, getObjectFromTypeAndId } from './apiHelper';

import { swapiTypeToGraphQLType, nodeField } from './relayNode';
import {
swapiTypeToGraphQLType,
graphQLTypeToSwapiType,
nodeField,
} from './relayNode';
import GraphQLFilteredUnionType from './graphQLFilteredUnionType';

/**
* 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 graphQLType = swapiTypeToGraphQLType(swapiType);
const argDefs = {};
argDefs.id = { type: GraphQLID };
argDefs[idName] = { type: GraphQLID };
if (!(graphQLType instanceof GraphQLUnionType)) {
argDefs[idName] = { type: GraphQLID };
}
return {
type: swapiTypeToGraphQLType(swapiType),
type: graphQLType,
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 (
Expand All @@ -54,7 +66,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);
},
Expand Down Expand Up @@ -95,7 +107,31 @@ 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),
);
if (graphQLType instanceof GraphQLFilteredUnionType) {
objectsByType.objects = graphQLType.filter(
type,
objectsByType.objects,
);
objectsByType.totalCount = objectsByType.objects.length;
}
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,
Expand All @@ -122,6 +158,8 @@ const rootType = new GraphQLObjectType({
starship: rootFieldByID('starshipID', 'starships'),
allVehicles: rootConnection('Vehicles', 'vehicles'),
vehicle: rootFieldByID('vehicleID', 'vehicles'),
machine: rootFieldByID('machineID', 'machines'),
allMachines: rootConnection('Machines', 'machines'),
node: nodeField,
}),
});
Expand Down
Loading