├── schema ├── schema.gql ├── planet.gql ├── people.gql ├── species.gql ├── vehicle.gql ├── film.gql ├── starship.gql └── root.gql ├── .dockerignore ├── .gitignore ├── doc └── example_queries │ ├── 01_basic_query.graphql │ ├── 04_all_starships.graphql │ ├── 02_nested_fields.graphql │ ├── 08_introspection.graphql │ ├── 03_nested_fields.graphql │ ├── 05_argument.graphql │ ├── 06_fragments.graphql │ └── 07_fragments.graphql ├── Dockerfile ├── .vscode ├── tasks.json └── launch.json ├── tsconfig.json ├── src ├── express.ts ├── resolvers │ ├── planet.ts │ ├── index.ts │ ├── vehicle.ts │ ├── starship.ts │ ├── species.ts │ ├── people.ts │ └── film.ts ├── hapi.ts ├── index.ts └── connectors │ └── swapi.ts ├── LICENSE ├── package.json ├── README.md └── tslint.json /schema/schema.gql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: RootQuery 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | typings 2 | node_modules 3 | README* 4 | LICENSE 5 | Dockerfile 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | typings 4 | dist 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /doc/example_queries/01_basic_query.graphql: -------------------------------------------------------------------------------- 1 | { 2 | person(personID: 4) { 3 | name 4 | } 5 | } -------------------------------------------------------------------------------- /doc/example_queries/04_all_starships.graphql: -------------------------------------------------------------------------------- 1 | # GraphQL server handles pagination on this example 2 | { 3 | allStarships { 4 | id 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /doc/example_queries/02_nested_fields.graphql: -------------------------------------------------------------------------------- 1 | { 2 | person(personID: 4) { 3 | name 4 | gender 5 | homeworld { 6 | name 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /doc/example_queries/08_introspection.graphql: -------------------------------------------------------------------------------- 1 | { 2 | __type(name: "Person") { 3 | name 4 | fields { 5 | name 6 | description 7 | type { name } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /doc/example_queries/03_nested_fields.graphql: -------------------------------------------------------------------------------- 1 | { 2 | person(personID: 4) { 3 | name 4 | gender 5 | homeworld { 6 | name 7 | } 8 | starships { 9 | id 10 | manufacturers 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /doc/example_queries/05_argument.graphql: -------------------------------------------------------------------------------- 1 | { 2 | allStarships(first: 7) { 3 | id 4 | name 5 | model 6 | costInCredits 7 | pilots { 8 | name 9 | homeworld { 10 | name 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk add --update --no-cache nodejs tini 4 | WORKDIR /app 5 | COPY . /app 6 | 7 | RUN npm --unsafe-perm install && npm cache clear 8 | 9 | EXPOSE 3000 10 | ENTRYPOINT ["/sbin/tini", "--"] 11 | CMD ["npm", "start"] 12 | -------------------------------------------------------------------------------- /doc/example_queries/06_fragments.graphql: -------------------------------------------------------------------------------- 1 | { 2 | allStarships(first: 7) { 3 | id 4 | name 5 | model 6 | costInCredits 7 | pilots { 8 | ...pilotFragment 9 | } 10 | } 11 | } 12 | 13 | fragment pilotFragment on Person { 14 | name 15 | homeworld { name } 16 | } 17 | -------------------------------------------------------------------------------- /doc/example_queries/07_fragments.graphql: -------------------------------------------------------------------------------- 1 | { 2 | allStarships { 3 | ...starshipFragment 4 | } 5 | } 6 | 7 | fragment starshipFragment on Starship { 8 | name,model,costInCredits, pilots { ...pilotFragment } 9 | } 10 | 11 | fragment pilotFragment on Person { 12 | name,homeworld { name } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "tsc", 6 | "isShellCommand": true, 7 | "args": ["-w", "-p", "."], 8 | "showOutput": "silent", 9 | "isBackground": true, 10 | "problemMatcher": "$tsc-watch" 11 | } -------------------------------------------------------------------------------- /schema/planet.gql: -------------------------------------------------------------------------------- 1 | type Planet implements Node { 2 | name: String 3 | diameter: Int 4 | rotationPeriod: Int 5 | orbitalPeriod: Int 6 | gravity: String 7 | population: Int 8 | climates: [String] 9 | terrains: [String] 10 | surfaceWater: Float 11 | residents: [Person] 12 | films: [Film] 13 | created: String 14 | edited: String 15 | id: ID! 16 | } 17 | 18 | -------------------------------------------------------------------------------- /schema/people.gql: -------------------------------------------------------------------------------- 1 | type Person implements Node { 2 | name: String 3 | birthYear: String 4 | eyeColor: String 5 | gender: String 6 | hairColor: String 7 | height: Int 8 | mass: Int 9 | skinColor: String 10 | homeworld: Planet 11 | films: [Film] 12 | species: [Species] 13 | starships: [Starship] 14 | vehicles: [Vehicle] 15 | created: String 16 | edited: String 17 | id: ID! 18 | } 19 | -------------------------------------------------------------------------------- /schema/species.gql: -------------------------------------------------------------------------------- 1 | type Species implements Node { 2 | name: String 3 | classification: String 4 | designation: String 5 | averageHeight: Float 6 | averageLifespan: Int 7 | eyeColors: [String] 8 | hairColors: [String] 9 | skinColors: [String] 10 | language: String 11 | homeworld: Planet 12 | people: [Person] 13 | films: [Film] 14 | created: String 15 | edited: String 16 | id: ID! 17 | } 18 | -------------------------------------------------------------------------------- /schema/vehicle.gql: -------------------------------------------------------------------------------- 1 | type Vehicle implements Node { 2 | name: String 3 | model: String 4 | vehicleClass: String 5 | manufacturers: [String] 6 | costInCredits: Int 7 | length: Float 8 | crew: String 9 | passengers: String 10 | maxAtmospheringSpeed: Int 11 | cargoCapacity: Int 12 | consumables: String 13 | pilots: [Person] 14 | films: [Film] 15 | created: String 16 | edited: String 17 | id: ID! 18 | } 19 | -------------------------------------------------------------------------------- /schema/film.gql: -------------------------------------------------------------------------------- 1 | type FilmDetails { 2 | species: [Species] 3 | starships: [Starship] 4 | vehicles: [Vehicle] 5 | characters: [Person] 6 | planets: [Planet] 7 | } 8 | 9 | type Film implements Node { 10 | title: String 11 | episodeID: Int 12 | openingCrawl: String 13 | director: String 14 | producers: [String] 15 | releaseDate: String 16 | created: String 17 | edited: String 18 | details: FilmDetails 19 | id: ID! 20 | } 21 | -------------------------------------------------------------------------------- /schema/starship.gql: -------------------------------------------------------------------------------- 1 | type Starship implements Node { 2 | name: String 3 | model: String 4 | starshipClass: String 5 | manufacturers: [String] 6 | costInCredits: Float 7 | length: Float 8 | crew: String 9 | passengers: String 10 | maxAtmospheringSpeed: Int 11 | hyperdriveRating: Float 12 | MGLT: Int 13 | cargoCapacity: Float 14 | consumables: String 15 | pilots: [Person] 16 | films: [Film] 17 | created: String 18 | edited: String 19 | id: ID! 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "noImplicitAny": false, 9 | "rootDir": "./src", 10 | "outDir": "./dist", 11 | "allowSyntheticDefaultImports": true, 12 | "pretty": true, 13 | "removeComments": true, 14 | "lib":[ 15 | "esnext" 16 | ] 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | "dist" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/express.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express' 2 | import * as bodyParser from 'body-parser' 3 | import * as apollo from 'graphql-server-express' 4 | 5 | 6 | const expressPort = process.env.EXPRESS_PORT || 3000 7 | const app = express() 8 | 9 | export function startExpress(graphqlOptions) { 10 | app.use(bodyParser.json()) 11 | app.use('/graphql', apollo.graphqlExpress(graphqlOptions)) 12 | app.use('/', apollo.graphiqlExpress({endpointURL: '/graphql'})) 13 | 14 | app.listen(expressPort, () => { 15 | console.log(`Express server is listen on ${expressPort}`) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/dist/index.js", 12 | "outFiles": [ 13 | "${workspaceRoot}/dist/**/*.js" 14 | ], 15 | "sourceMaps": true 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /schema/root.gql: -------------------------------------------------------------------------------- 1 | interface Node { 2 | id: ID! 3 | } 4 | 5 | type RootQuery { 6 | allFilms(offset: Int, limit: Int): [Film] 7 | film(id: ID, filmID: ID): Film 8 | allPeople(offset: Int, limit: Int): [Person] 9 | person(id: ID, personID: ID): Person 10 | allPlanets(offset: Int, limit: Int): [Planet] 11 | planet(id: ID, planetID: ID): Planet 12 | allSpecies(offset: Int, limit: Int): [Species] 13 | species(id: ID, speciesID: ID): Species 14 | allStarships(offset: Int, limit: Int): [Starship] 15 | starship(id: ID, starshipID: ID): Starship 16 | allVehicles(offset: Int, limit: Int): [Vehicle] 17 | vehicle(id: ID, vehicleID: ID): Vehicle 18 | node(id: ID!): Node 19 | } 20 | -------------------------------------------------------------------------------- /src/resolvers/planet.ts: -------------------------------------------------------------------------------- 1 | import { getPageFetcher } from '../connectors/swapi' 2 | 3 | const path = '/planets/' 4 | 5 | export default (fetch) => ({ 6 | RootQuery: { 7 | allPlanets: (_, params) => getPageFetcher(fetch)(path, params.offset, params.limit), 8 | planet: (_, params) => fetch(params.id || `${path}${params.planetID}/`), 9 | }, 10 | Planet: { 11 | id: (planet) => planet.url, 12 | rotationPeriod: (planet) => planet.rotation_period, 13 | orbitalPeriod: (planet) => planet.orbital_period, 14 | surfaceWater: (planet) => planet.surface_water, 15 | residents: (planet, _, context) => context.loader.loadMany(planet.residents), 16 | films: (planet, _, context) => context.loader.loadMany(planet.films), 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Nick Nance 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { IFetcher } from '../connectors/swapi' 2 | import film from './film' 3 | import people from './people' 4 | import planet from './planet' 5 | import species from './species' 6 | import starship from './starship' 7 | import vehicle from './vehicle' 8 | 9 | export default (fetch: IFetcher) => Object.assign( 10 | {}, 11 | film(fetch), 12 | people(fetch), 13 | planet(fetch), 14 | species(fetch), 15 | starship(fetch), 16 | vehicle(fetch), 17 | { 18 | RootQuery: Object.assign( 19 | {}, 20 | film(fetch).RootQuery, 21 | people(fetch).RootQuery, 22 | planet(fetch).RootQuery, 23 | species(fetch).RootQuery, 24 | starship(fetch).RootQuery, 25 | vehicle(fetch).RootQuery, 26 | ), 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /src/resolvers/vehicle.ts: -------------------------------------------------------------------------------- 1 | import { getPageFetcher } from '../connectors/swapi' 2 | 3 | const path = '/vehicles/' 4 | 5 | export default (fetch) => ({ 6 | RootQuery: { 7 | allVehicles: (_, params) => getPageFetcher(fetch)(path, params.offset, params.limit), 8 | vehicle: (_, params) => fetch(params.id || `${path}${params.vehicleID}/`), 9 | }, 10 | Vehicle: { 11 | id: (vehicle) => vehicle.url, 12 | costInCredits: (vehicle) => vehicle.cost_in_credits, 13 | maxAtmospheringSpeed: (vehicle) => vehicle.max_atmosphering_speed, 14 | cargoCapacity: (vehicle) => vehicle.cargo_capacity, 15 | vehicleClass: (vehicle) => vehicle.vehicle_class, 16 | pilots: (vehicle, _, context) => context.loader.loadMany(vehicle.pilots), 17 | films: (vehicle, _, context) => context.loader.loadMany(vehicle.films), 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /src/hapi.ts: -------------------------------------------------------------------------------- 1 | import * as hapi from 'hapi' 2 | import * as apollo from 'graphql-server-hapi' 3 | 4 | const hapiPort = process.env.HAPI_PORT || 8000 5 | 6 | export async function startHapi(graphqlOptions) { 7 | const server = new hapi.Server({ 8 | host: 'localhost', 9 | port: hapiPort, 10 | }) 11 | 12 | await server.register({ 13 | options: { 14 | graphqlOptions, 15 | path: '/graphql', 16 | }, 17 | plugin: apollo.graphqlHapi, 18 | }) 19 | 20 | await server.register({ 21 | options: { 22 | graphiqlOptions: { 23 | endpointURL: '/graphql', 24 | }, 25 | path: '/', 26 | }, 27 | plugin: apollo.graphiqlHapi, 28 | }) 29 | 30 | await server.start() 31 | console.log(`HAPI server is listen on ${hapiPort}`) 32 | console.log(`open browser to http://localhost:${hapiPort}`) 33 | } 34 | -------------------------------------------------------------------------------- /src/resolvers/starship.ts: -------------------------------------------------------------------------------- 1 | import { getPageFetcher } from '../connectors/swapi' 2 | 3 | const path = '/starships/' 4 | 5 | export default (fetch) => ({ 6 | RootQuery: { 7 | allStarships: (_, params) => getPageFetcher(fetch)(path, params.offset, params.limit), 8 | starship: (_, params) => fetch(params.id || `${path}${params.starshipID}/`), 9 | }, 10 | Starship: { 11 | id: (starship) => starship.url, 12 | costInCredits: (starship) => starship.cost_in_credits, 13 | maxAtmospheringSpeed: (starship) => starship.max_atmosphering_speed, 14 | cargoCapacity: (starship) => starship.cargo_capacity, 15 | hyperdriveRating: (starship) => starship.hyperdrive_rating, 16 | starshipClass: (starship) => starship.starship_class, 17 | pilots: (starship, _, context) => context.loader.loadMany(starship.pilots), 18 | films: (starship, _, context) => context.loader.loadMany(starship.films), 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /src/resolvers/species.ts: -------------------------------------------------------------------------------- 1 | import { getPageFetcher } from '../connectors/swapi' 2 | 3 | const path = '/species/' 4 | 5 | export default (fetch) => ({ 6 | RootQuery: { 7 | allSpecies: (_, params) => getPageFetcher(fetch)(path, params.offset, params.limit), 8 | species: (_, params) => fetch(params.id || `${path}${params.speciesID}/`), 9 | }, 10 | Species: { 11 | id: (species) => species.url, 12 | averageHeight: (species) => species.average_height, 13 | skinColors: (species) => species.skin_colors.split(','), 14 | hairColors: (species) => species.hair_colors.split(','), 15 | eyeColors: (species) => species.eye_colors.split(','), 16 | averageLifespan: (species) => species.average_lifespan, 17 | homeworld: (species, _, context) => context.loader.loadMany(species.homeworld), 18 | people: (species, _, context) => context.loader.loadMany(species.people), 19 | films: (species, _, context) => context.loader.loadMany(species.films), 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/resolvers/people.ts: -------------------------------------------------------------------------------- 1 | import { getPageFetcher } from '../connectors/swapi' 2 | 3 | const path = '/people/' 4 | 5 | export default (fetch) => ({ 6 | RootQuery: { 7 | allPeople: (_, params) => getPageFetcher(fetch)(path, params.offset, params.limit), 8 | person: (_, params) => fetch(params.id || `${path}${params.personID}/`), 9 | }, 10 | Person: { 11 | id: (person) => person.url, 12 | hairColor: (person) => person.hair_color, 13 | skinColor: (person) => person.skin_color, 14 | eyeColor: (person) => person.eye_color, 15 | birthYear: (person) => person.birth_year, 16 | homeworld: (person, _, context) => context.loader.loadMany(person.homeworld), 17 | films: (person, _, context) => context.loader.loadMany(person.films), 18 | species: (person, _, context) => context.loader.loadMany(person.species), 19 | starships: (person, _, context) => context.loader.loadMany(person.starships), 20 | vehicles: (person, _, context) => context.loader.loadMany(person.vehicles), 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { loadSchema } from '@creditkarma/graphql-loader' 2 | import { addResolveFunctionsToSchema } from 'graphql-tools' 3 | import { GraphQLSchema } from 'graphql' 4 | import getResolversWithFetchers from './resolvers/index' 5 | import { getFetcher, getLoader } from './connectors/swapi' 6 | import { startExpress } from './express' 7 | import { startHapi } from './hapi' 8 | 9 | 10 | const apiHost = process.env.API_HOST ? `${process.env.API_HOST}/api` : 'http://swapi.co/api' 11 | 12 | const fetcher = getFetcher(apiHost) 13 | 14 | const graphqlOptions = (schema: GraphQLSchema) => { 15 | return () => ({ 16 | pretty: true, 17 | schema, 18 | context: { 19 | loader: getLoader(fetcher), 20 | }, 21 | }) 22 | } 23 | 24 | loadSchema('./schema/*.gql') 25 | .then(schema => { 26 | const resolvers = getResolversWithFetchers(fetcher) 27 | 28 | addResolveFunctionsToSchema(schema, resolvers) 29 | startExpress(graphqlOptions(schema)) 30 | startHapi(graphqlOptions(schema)) 31 | }) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swapi-apollo", 3 | "version": "0.0.1", 4 | "description": "An Apollo server wrapping SWAPI", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "lint": "tslint src/**/*", 9 | "start": "npm run build && node ./dist/index.js", 10 | "dev": "nodemon ./dist/index.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "Apollo-Stack", 15 | "GraphQL" 16 | ], 17 | "author": "Nick Nance", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@creditkarma/graphql-loader": "^0.7.0", 21 | "body-parser": "^1.18.2", 22 | "dataloader": "^1.3.0", 23 | "express": "^4.15.3", 24 | "graphql": "^0.11.0", 25 | "graphql-server-express": "^1.3.2", 26 | "graphql-server-hapi": "^1.3.2", 27 | "graphql-tools": "^2.18.0", 28 | "hapi": "^17.2.0", 29 | "request": "^2.83.0" 30 | }, 31 | "devDependencies": { 32 | "@types/body-parser": "^1.16.3", 33 | "@types/express": "^4.11.0", 34 | "@types/graphql": "^0.11.8", 35 | "nodemon": "^1.14.11", 36 | "rimraf": "^2.6.2", 37 | "tslint": "^5.9.1", 38 | "typescript": "^2.6.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/resolvers/film.ts: -------------------------------------------------------------------------------- 1 | import { getPageFetcher } from '../connectors/swapi' 2 | 3 | const path = '/films/' 4 | 5 | export default (fetch) => ({ 6 | RootQuery: { 7 | allFilms: (_, params) => getPageFetcher(fetch)(path, params.offset, params.limit), 8 | film: (_, params) => fetch(params.id || `${path}${params.filmID}/`), 9 | }, 10 | Film: { 11 | id: (film) => film.url, 12 | episodeID: (film) => film.episode_id, 13 | openingCrawl: (film) => film.opening_crawl, 14 | releaseDate: (film) => film.release_date, 15 | details: (film) => ({ 16 | species: film.species, 17 | starships: film.starships, 18 | vehicles: film.vehicles, 19 | characters: film.characters, 20 | planets: film.planets, 21 | }), 22 | }, 23 | FilmDetails: { 24 | species: (details, _, context) => context.loader.loadMany(details.species), 25 | starships: (details, _, context) => context.loader.loadMany(details.starships), 26 | vehicles: (details, _, context) => context.loader.loadMany(details.vehicles), 27 | characters: (details, _, context) => context.loader.loadMany(details.characters), 28 | planets: (details, _, context) => context.loader.loadMany(details.planets), 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #SWAPI Apollo Server Wrapper 2 | 3 | A wrapper around [SWAPI](http://swapi.co) built using Apollo Server. This is intended to be a POC of an express and HAPI GraphQL server. 4 | 5 | Uses: 6 | 7 | * [graphql-server-express](https://github.com/apollographql/graphql-server) - Apollo server GraphQL middleware for express. 8 | * [graphql-server-hapi](https://github.com/apollographql/graphql-server) - Apollo server GraphQL plugin for Hapi. 9 | * [graphql-js](https://github.com/graphql/graphql-js) - a JavaScript GraphQL runtime. 10 | * [DataLoader](https://github.com/facebook/dataloader) - for coalescing and caching fetches. 11 | * [GraphiQL](https://github.com/graphql/graphiql) - for easy exploration of this GraphQL server. 12 | 13 | ## Getting Started 14 | 15 | Install dependencies with 16 | 17 | ```sh 18 | npm install 19 | ``` 20 | 21 | ## Local Server 22 | 23 | A local server is in `./src`. It can be run with: 24 | 25 | ```sh 26 | npm start 27 | ``` 28 | 29 | This is will start both an express server on port 3000 and a HAPI server on port 8000. 30 | 31 | You can explore the API at http://localhost:3000 or http://localhost:8000 32 | 33 | ## Development Server 34 | 35 | A development server that watches for changes can be ran with: 36 | 37 | ```sh 38 | npm run dev 39 | ``` 40 | -------------------------------------------------------------------------------- /src/connectors/swapi.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'request' 2 | const DataLoader = require('dataloader') 3 | 4 | export interface IFetcher { 5 | (resource: string): Promise 6 | } 7 | 8 | export const getFetcher = (rootURL?: string): IFetcher => { 9 | const apiRoot = rootURL || 'http://swapi.co/api' 10 | 11 | return (resource: string): Promise => { 12 | const url = resource.indexOf(apiRoot) === 0 ? resource : apiRoot + resource 13 | 14 | return new Promise((resolve, reject) => { 15 | console.log(`fetch: ${url}`) 16 | request.get(url, (err, resp, body) => { 17 | console.log(`fetch: ${url} completed`) 18 | err ? reject(err) : resolve(JSON.parse(body)) 19 | }) 20 | }) 21 | } 22 | } 23 | 24 | export const getLoader = (fetch: IFetcher) => { 25 | return new DataLoader((urls) => { 26 | const promises = urls.map((url) => { 27 | return fetch(url) 28 | }) 29 | return Promise.all(promises) 30 | }, {batch: false}) 31 | } 32 | 33 | export const getPageFetcher = (fetch: IFetcher) => (resource: string, offset?: number, limit?: number) => { 34 | let results = [] 35 | let index = 0 36 | const size = limit || 0 37 | 38 | function pagination(pageURL: string) { 39 | return new Promise((resolve, reject) => { 40 | fetch(pageURL).then((data) => { 41 | // fast forward until offset is reached 42 | if (offset && results.length === 0) { 43 | if (index + data.results.length > offset) { 44 | results = data.results.slice(offset - index) 45 | } 46 | if (data.next) { 47 | index = index + data.results.length 48 | pagination(data.next).then(resolve) 49 | } else { 50 | resolve(results) 51 | } 52 | } else { 53 | if (size > 0 && size - results.length - data.results.length < 0) { 54 | results = results.concat(data.results.slice(0, size - results.length)) 55 | } else { 56 | results = results.concat(data.results) 57 | } 58 | if (data.next && (size === 0 || size - results.length > 0)) { 59 | pagination(data.next).then(resolve) 60 | } else { 61 | resolve(results) 62 | } 63 | } 64 | }) 65 | }) 66 | } 67 | 68 | return pagination(resource) 69 | } 70 | 71 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "interface-name": false, 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "label-undefined": true, 22 | "max-line-length": [ 23 | true, 24 | 140 25 | ], 26 | "member-access": true, 27 | "member-ordering": [ 28 | true, 29 | "public-before-private", 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-any": false, 34 | "no-arg": true, 35 | "no-bitwise": true, 36 | "no-conditional-assignment": true, 37 | "no-consecutive-blank-lines": false, 38 | "no-console": [ 39 | false, 40 | "log", 41 | "debug", 42 | "info", 43 | "time", 44 | "timeEnd", 45 | "trace" 46 | ], 47 | "no-construct": true, 48 | "no-constructor-vars": true, 49 | "no-debugger": true, 50 | "no-duplicate-key": true, 51 | "no-duplicate-variable": true, 52 | "no-empty": true, 53 | "no-eval": true, 54 | "no-inferrable-types": false, 55 | "no-internal-module": true, 56 | "no-null-keyword": false, 57 | "no-require-imports": false, 58 | "no-shadowed-variable": true, 59 | "no-switch-case-fall-through": true, 60 | "no-trailing-whitespace": true, 61 | "no-unreachable": true, 62 | "no-unused-expression": true, 63 | "no-unused-variable": true, 64 | "no-use-before-declare": true, 65 | "no-var-keyword": true, 66 | "no-var-requires": false, 67 | "object-literal-sort-keys": false, 68 | "one-line": [ 69 | true, 70 | "check-open-brace", 71 | "check-catch", 72 | "check-else", 73 | "check-finally", 74 | "check-whitespace" 75 | ], 76 | "quotemark": [ 77 | true, 78 | "single", 79 | "avoid-escape" 80 | ], 81 | "radix": true, 82 | "semicolon": [ 83 | true, 84 | "never" 85 | ], 86 | "switch-default": true, 87 | "trailing-comma": [ 88 | true, 89 | { 90 | "multiline": "always", 91 | "singleline": "never" 92 | } 93 | ], 94 | "triple-equals": [ 95 | true, 96 | "allow-null-check" 97 | ], 98 | "typedef": [ 99 | false, 100 | "call-signature", 101 | "parameter", 102 | "arrow-parameter", 103 | "property-declaration", 104 | "variable-declaration", 105 | "member-variable-declaration" 106 | ], 107 | "typedef-whitespace": [ 108 | true, 109 | { 110 | "call-signature": "nospace", 111 | "index-signature": "nospace", 112 | "parameter": "nospace", 113 | "property-declaration": "nospace", 114 | "variable-declaration": "nospace" 115 | }, 116 | { 117 | "call-signature": "space", 118 | "index-signature": "space", 119 | "parameter": "space", 120 | "property-declaration": "space", 121 | "variable-declaration": "space" 122 | } 123 | ], 124 | "use-strict": [ 125 | false 126 | ], 127 | "variable-name": [ 128 | true, 129 | "check-format", 130 | "allow-leading-underscore", 131 | "ban-keywords", 132 | "allow-pascal-case" 133 | ], 134 | "whitespace": [ 135 | true, 136 | "check-branch", 137 | "check-decl", 138 | "check-operator", 139 | "check-separator", 140 | "check-type" 141 | ] 142 | } 143 | } 144 | --------------------------------------------------------------------------------