Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add union type #134

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
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 @@ -31,6 +31,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 @@ -12,6 +12,7 @@ import {
GraphQLInt,
GraphQLList,
GraphQLObjectType,
GraphQLUnionType,
GraphQLSchema,
} from 'graphql';

Expand All @@ -24,26 +25,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 @@ -53,7 +65,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 @@ -94,7 +106,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 @@ -121,6 +157,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