-
Notifications
You must be signed in to change notification settings - Fork 3
SWAPI, Paging, DataLoader, Request ID #242
base: master
Are you sure you want to change the base?
Changes from 12 commits
171cdfc
fb5c8c9
5375e3d
6c54662
d3c8a0b
5200db1
2e96c9c
5a266de
da863e5
6303b2f
cc73c50
e2b5551
0948466
4f2ea13
e17c60d
972f32a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import DataLoader from 'dataloader'; | ||
| import { createClient } from 'flashheart'; | ||
| import logger from '../logger'; | ||
|
|
||
| const http = createClient({ logger, timeout: 5000 }); | ||
|
|
||
| async function getFromUrl(url) { | ||
| const response = await http.getAsync(url); | ||
| return response; | ||
| } | ||
|
|
||
| export const dataLoader = new DataLoader(urls => | ||
| Promise.all(urls.map(getFromUrl)), | ||
| ); | ||
|
|
||
| /** | ||
| * Given an object URL, fetch it, append the ID to it, and return it. | ||
| */ | ||
| export const getObjectFromUrl = async (url: string): Promise<any> => { | ||
| return await dataLoader.load(url); | ||
| }; | ||
|
|
||
| /** | ||
| * Given a type, get the object with the ID. | ||
|
||
| */ | ||
| export const getObjectsFromType = async (type: string): Promise<any> => { | ||
| return await getObjectFromUrl(`${process.env.SWAPI_SERVICE_URL}/${type}/`); | ||
| }; | ||
|
|
||
| /** | ||
| * Given a type and ID, get the object with the ID. | ||
| */ | ||
| export const getObjectFromTypeAndId = async (type: string, id: string): Promise<any> => { | ||
| const data = await getObjectFromUrl(`${process.env.SWAPI_SERVICE_URL}/${type}/${id}/`); | ||
| return objectWithId(data); | ||
| }; | ||
|
|
||
| /** | ||
| * Given an objects URLs, fetch it, append the ID to it, sort it, and return it. | ||
|
||
| */ | ||
| export const getObjectsFromUrls = async (urls: string[]): Promise<any[]> => { | ||
| const array = await Promise.all(urls.map(getObjectFromUrl)); | ||
| return array.map(objectWithId); | ||
| }; | ||
|
|
||
| /** | ||
| * Objects returned from SWAPI don't have an ID field, so add one. | ||
| */ | ||
| export const objectWithId = (obj: {id: number, url: string}): Object => { | ||
| obj.id = parseInt(obj.url.split('/')[5], 10); | ||
| return obj; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { connectionDefinitions } from 'graphql-relay-tools'; | ||
|
|
||
| /** | ||
| * Constructs a GraphQL connection field config; it is assumed | ||
| * that the object has a property named `prop`, and that property | ||
| * contains a list of types. | ||
| */ | ||
| export function connectTypes(name: string, prop: string, type: string) { | ||
| const { connectionType } = connectionDefinitions({ | ||
| name, | ||
| nodeType: type, | ||
| connectionFields: ` | ||
| totalCount: Int | ||
| ${prop}: [${type}] | ||
| `, | ||
| }); | ||
|
|
||
| return connectionType; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { | ||
| getObjectFromUrl, | ||
| getObjectsFromUrls, | ||
| getObjectFromTypeAndId, | ||
| getObjectsFromType, | ||
| objectWithId, | ||
| } from '../../connectors/swapi'; | ||
|
||
|
|
||
| type ObjectsByType = { | ||
| objects: Object[], | ||
| totalCount: number, | ||
| }; | ||
|
|
||
| /** | ||
| * Given a type, fetch all of the pages, and join the objects together | ||
| */ | ||
| const byType = async (type: string): Promise<ObjectsByType> => { | ||
| const typeData = await getObjectsFromType(type); | ||
| let objects: Object[] = []; | ||
| let nextUrl = typeData.next; | ||
|
|
||
| objects = objects.concat(typeData.results.map(objectWithId)); | ||
| while (nextUrl) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
|
||
| const pageData = await getObjectFromUrl(nextUrl); | ||
| objects = objects.concat(pageData.results.map(objectWithId)); | ||
| nextUrl = pageData.next; | ||
| } | ||
|
|
||
| objects = sortObjectsById(objects); | ||
| return { objects, totalCount: objects.length }; | ||
| }; | ||
|
|
||
| /** | ||
| * Given a type and ID, get the object with the ID. | ||
| */ | ||
| const byTypeAndId = async (type: string, id: string): Promise<Object> => { | ||
|
||
| return await getObjectFromTypeAndId(type, id); | ||
| }; | ||
|
|
||
| /** | ||
| * Given an object URL, fetch it, append the ID to it, and return it. | ||
| */ | ||
| const byUrl = async (url: string): Promise<any> => { | ||
| return await getObjectFromUrl(url); | ||
| }; | ||
|
|
||
| /** | ||
| * Given an objects URLs, fetch it, append the ID to it, sort it, and return it. | ||
| */ | ||
| const byUrls = async (urls: string[]): Promise<any[]> => { | ||
| const array = await getObjectsFromUrls(urls); | ||
| return sortObjectsById(array); | ||
| }; | ||
|
|
||
| const sortObjectsById = (array: any[]): Object[] => { | ||
| return array.sort((a, b) => a.id - b.id); | ||
| }; | ||
|
|
||
| const convertToNumber = (value: string): number | null => { | ||
| if (['unknown', 'n/a'].indexOf(value) !== -1) { | ||
| return null; | ||
| } | ||
|
|
||
| // remove digit grouping | ||
| const numberString = value.replace(/,/, ''); | ||
| return Number(numberString); | ||
| }; | ||
|
|
||
| export { | ||
| byTypeAndId, | ||
| byType, | ||
| byUrl, | ||
| byUrls, | ||
| convertToNumber, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const node = { | ||
| __resolveType: () => null, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,11 +4,15 @@ import { Resolvers } from '../_generated/types'; | |
| import { query as Query } from './Query'; | ||
| import { jokes as Jokes } from './Jokes'; | ||
| import { joke as Joke } from './Joke'; | ||
| import { node as Node } from './Node'; | ||
|
|
||
| /** SWAPI resolvers */ | ||
|
||
| import swapiResolvers from './swapi'; | ||
|
|
||
| const resolvers: Resolvers = { | ||
| Query, | ||
| Jokes, | ||
| Joke, | ||
| }; | ||
|
|
||
| export default merge(resolvers); | ||
| export default merge(resolvers, swapiResolvers, { Node }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not spread over const resolvers: Resolvers = {
Query,
Jokes,
Joke,
...swapiResolvers,
{ Node }
};
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generated TypeScript interface |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { connectionFromArray } from 'graphql-relay-tools'; | ||
| import { byUrls, byType } from '../../models/swapi'; | ||
|
|
||
| function connection(prop: string) { | ||
| return async (obj, args) => { | ||
| const array = await byUrls(obj[prop] || []); | ||
| const connObj = connectionFromArray(array, args); | ||
| return { | ||
| ...connObj, | ||
| totalCount: array.length, | ||
| [prop]: _ => connObj.edges.map(edge => edge.node), | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| function rootConnection(swapiType) { | ||
| return async (_, args) => { | ||
| const { objects, totalCount } = await byType(swapiType); | ||
| const connObj = connectionFromArray(objects, args); | ||
| return { | ||
| ...connObj, | ||
| totalCount, | ||
| [swapiType]: _ => connObj.edges.map(edge => edge.node), | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| export { | ||
| rootConnection, | ||
| connection, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { globalIdResolver } from 'graphql-relay-tools'; | ||
| import { connection } from './Connection'; | ||
|
|
||
| export const film = { | ||
| episodeID: film => film.episode_id, | ||
| openingCrawl: film => film.opening_crawl, | ||
| producers: film => film.producer.split(',').map(s => s.trim()), | ||
| releaseDate: film => film.release_date, | ||
| speciesConnection: connection('species'), | ||
| starshipConnection: connection('starships'), | ||
| vehicleConnection: connection('vehicles'), | ||
| characterConnection: connection('characters'), | ||
| planetConnection: connection('planets'), | ||
| id: globalIdResolver(), | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { nodeDefinitions, fromGlobalId } from 'graphql-relay-tools'; | ||
| import { byTypeAndId } from '../../models/swapi'; | ||
|
|
||
| const { nodeResolver, nodesResolver } = nodeDefinitions((globalId) => { | ||
| const { type, id } = fromGlobalId(globalId); | ||
| return byTypeAndId(type, id); | ||
| }); | ||
|
|
||
| export const node = { | ||
| node: nodeResolver, | ||
| nodes: nodesResolver, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { globalIdResolver } from 'graphql-relay-tools'; | ||
| import { convertToNumber, byUrl } from '../../models/swapi'; | ||
| import { connection } from './Connection'; | ||
|
|
||
| export const person = { | ||
| birthYear: person => person.birth_year, | ||
| eyeColor: person => person.eye_color, | ||
| hairColor: person => person.hair_color, | ||
| height: person => convertToNumber(person.height), | ||
| mass: person => convertToNumber(person.mass), | ||
| skinColor: person => person.skin_color, | ||
| homeworld: person => person.homeworld ? byUrl(person.homeworld) : null, | ||
| species: (person) => { | ||
| if (!person.species || person.species.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| return byUrl(person.species[0]); | ||
| }, | ||
| filmConnection: connection('films'), | ||
| starshipConnection: connection('starships'), | ||
| vehicleConnection: connection('vehicles'), | ||
| id: globalIdResolver(), | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { globalIdResolver } from 'graphql-relay-tools'; | ||
| import { convertToNumber } from '../../models/swapi'; | ||
| import { connection } from './Connection'; | ||
|
|
||
| export const planet = { | ||
| diameter: planet => convertToNumber(planet.diameter), | ||
| rotationPeriod: planet => convertToNumber(planet.rotation_period), | ||
| orbitalPeriod: planet => convertToNumber(planet.orbital_period), | ||
| population: planet => convertToNumber(planet.population), | ||
| climates: planet => planet.climate.split(',').map(s => s.trim()), | ||
| terrains: planet => planet.terrain.split(',').map(s => s.trim()), | ||
| surfaceWater: planet => convertToNumber(planet.surface_water), | ||
| residentConnection: connection('residents'), | ||
| filmConnection: connection('films'), | ||
| id: globalIdResolver(), | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { fromGlobalId } from 'graphql-relay-tools'; | ||
| import { isEmpty } from 'lodash'; | ||
| import { byTypeAndId } from '../../models/swapi'; | ||
| import { rootConnection } from './Connection'; | ||
|
|
||
| function rootField(idName, swapiType) { | ||
| return (_, args) => { | ||
| if (!isEmpty(args[idName])) { | ||
| return byTypeAndId(swapiType, args[idName]); | ||
| } | ||
|
|
||
| if (!isEmpty(args.id)) { | ||
| const globalId = fromGlobalId(args.id); | ||
|
|
||
| if (isEmpty(globalId.id)) { | ||
| throw new Error(`No valid ID extracted from ${args.id}`); | ||
| } | ||
|
|
||
| return byTypeAndId(swapiType, globalId.id); | ||
| } | ||
|
|
||
| throw new Error(`must provide id or ${idName}`); | ||
| }; | ||
| } | ||
|
|
||
| export const query = { | ||
| allFilms: rootConnection('films'), | ||
| allPeople: rootConnection('people'), | ||
| allPlanets: rootConnection('planets'), | ||
| allSpecies: rootConnection('species'), | ||
| allStarships: rootConnection('starships'), | ||
| allVehicles: rootConnection('vehicles'), | ||
|
|
||
| film: rootField('filmID', 'films'), | ||
| person: rootField('personID', 'people'), | ||
| planet: rootField('planetID', 'planets'), | ||
| species: rootField('speciesID', 'species'), | ||
| starship: rootField('starshipID', 'starships'), | ||
| vehicle: rootField('vehicleID', 'vehicles'), | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { globalIdResolver } from 'graphql-relay-tools'; | ||
| import { convertToNumber, byUrl } from '../../models/swapi'; | ||
| import { connection } from './Connection'; | ||
|
|
||
| export const species = { | ||
| averageHeight: species => convertToNumber(species.average_height), | ||
| averageLifespan: species => convertToNumber(species.average_lifespan), | ||
| eyeColors: species => species.eye_colors.split(',').map(s => s.trim()), | ||
| hairColors: (species) => { | ||
| if (species.hair_colors === 'none') { | ||
| return []; | ||
| } | ||
|
|
||
| return species.hair_colors.split(',').map(s => s.trim()); | ||
| }, | ||
| skinColors: species => species.skin_colors.split(',').map(s => s.trim()), | ||
| homeworld: species => species.homeworld ? byUrl(species.homeworld) : null, | ||
| personConnection: connection('people'), | ||
| filmConnection: connection('films'), | ||
| id: globalIdResolver(), | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { globalIdResolver } from 'graphql-relay-tools'; | ||
| import { convertToNumber } from '../../models/swapi'; | ||
| import { connection } from './Connection'; | ||
|
|
||
| export const starship = { | ||
| starshipClass: ship => ship.starship_class, | ||
| manufacturers: ship => ship.manufacturer.split(',').map(s => s.trim()), | ||
| costInCredits: ship => convertToNumber(ship.cost_in_credits), | ||
| length: ship => convertToNumber(ship.length), | ||
| maxAtmospheringSpeed: ship => convertToNumber(ship.max_atmosphering_speed), | ||
| hyperdriveRating: ship => convertToNumber(ship.hyperdrive_rating), | ||
| MGLT: ship => convertToNumber(ship.MGLT), | ||
| cargoCapacity: ship => convertToNumber(ship.cargo_capacity), | ||
| pilotConnection: connection('pilots'), | ||
| filmConnection: connection('films'), | ||
| id: globalIdResolver(), | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.