├── .babelrc ├── .dockerignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE-examples ├── README.md ├── app.json ├── cache └── README.md ├── doc └── example_queries │ ├── 01_basic_query.graphql │ ├── 02_nested_fields.graphql │ ├── 03_nested_fields.graphql │ ├── 04_all_starships.graphql │ ├── 05_argument.graphql │ ├── 06_fragments.graphql │ ├── 07_fragments.graphql │ └── 08_introspection.graphql ├── handler ├── graphql.mjs ├── index.mjs └── swapi.mjs ├── jest.config.js ├── netlify.toml ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico └── index.html ├── schema.graphql ├── scripts ├── deploy-public ├── download.js ├── mocha-bootload.js ├── print-schema.js └── store-schema.js └── src ├── api ├── README.md ├── __tests__ │ └── local.spec.js ├── index.js └── local.js └── schema ├── README.md ├── __tests__ ├── apiHelper.spec.js ├── film.spec.js ├── person.spec.js ├── planet.spec.js ├── schema.spec.js ├── species.spec.js ├── starship.spec.js ├── swapi.js └── vehicle.spec.js ├── apiHelper.js ├── commonFields.js ├── connections.js ├── constants.js ├── index.js ├── relayNode.js └── types ├── film.js ├── person.js ├── planet.js ├── species.js ├── starship.js └── vehicle.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "targets": { "node": "current" }}] 4 | ], 5 | "plugins": [ 6 | "transform-flow-strip-types", 7 | "syntax-async-functions", 8 | "babel-plugin-syntax-object-rest-spread" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "plugins": [ 5 | "babel", 6 | "prettier" 7 | ], 8 | 9 | "env": { 10 | "jest": true, 11 | "browser": true, 12 | "es6": true, 13 | "node": true 14 | }, 15 | 16 | "parserOptions": { 17 | "arrowFunctions": true, 18 | "binaryLiterals": true, 19 | "blockBindings": true, 20 | "classes": true, 21 | "defaultParams": true, 22 | "destructuring": true, 23 | "experimentalObjectRestSpread": true, 24 | "forOf": true, 25 | "generators": true, 26 | "globalReturn": true, 27 | "jsx": true, 28 | "modules": true, 29 | "objectLiteralComputedProperties": true, 30 | "objectLiteralDuplicateProperties": true, 31 | "objectLiteralShorthandMethods": true, 32 | "objectLiteralShorthandProperties": true, 33 | "octalLiterals": true, 34 | "regexUFlag": true, 35 | "regexYFlag": true, 36 | "restParams": true, 37 | "spread": true, 38 | "superInFunctions": true, 39 | "templateStrings": true, 40 | "unicodeCodePointEscapes": true 41 | }, 42 | 43 | "rules": { 44 | "prettier/prettier": 2, 45 | 46 | "arrow-parens": [2, "as-needed"], 47 | "arrow-spacing": 2, 48 | "block-scoped-var": 0, 49 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 50 | "callback-return": 2, 51 | "camelcase": [2, {"properties": "always"}], 52 | "comma-dangle": 0, 53 | "comma-spacing": 0, 54 | "comma-style": [2, "last"], 55 | "complexity": 0, 56 | "computed-property-spacing": [2, "never"], 57 | "consistent-return": 0, 58 | "consistent-this": 0, 59 | "curly": [2, "all"], 60 | "default-case": 0, 61 | "dot-location": [2, "property"], 62 | "dot-notation": 0, 63 | "eol-last": 2, 64 | "eqeqeq": 2, 65 | "func-names": 0, 66 | "func-style": 0, 67 | "guard-for-in": 2, 68 | "handle-callback-err": [2, "error"], 69 | "id-length": 0, 70 | "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"], 71 | "indent": [2, 2, {"SwitchCase": 1}], 72 | "init-declarations": 0, 73 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 74 | "keyword-spacing": 2, 75 | "linebreak-style": 2, 76 | "lines-around-comment": 0, 77 | "max-depth": 0, 78 | "max-nested-callbacks": 0, 79 | "max-params": 0, 80 | "max-statements": 0, 81 | "new-cap": 0, 82 | "new-parens": 2, 83 | "newline-after-var": 0, 84 | "no-alert": 2, 85 | "no-array-constructor": 2, 86 | "no-await-in-loop": 2, 87 | "no-bitwise": 0, 88 | "no-caller": 2, 89 | "no-catch-shadow": 0, 90 | "no-class-assign": 2, 91 | "no-cond-assign": 2, 92 | "no-console": 1, 93 | "no-const-assign": 2, 94 | "no-constant-condition": 2, 95 | "no-continue": 0, 96 | "no-control-regex": 0, 97 | "no-debugger": 1, 98 | "no-delete-var": 2, 99 | "no-div-regex": 2, 100 | "no-dupe-args": 2, 101 | "no-dupe-keys": 2, 102 | "no-duplicate-case": 2, 103 | "no-else-return": 2, 104 | "no-empty": 2, 105 | "no-empty-character-class": 2, 106 | "no-eq-null": 0, 107 | "no-eval": 2, 108 | "no-ex-assign": 2, 109 | "no-extend-native": 2, 110 | "no-extra-bind": 2, 111 | "no-extra-boolean-cast": 2, 112 | "no-extra-parens": 0, 113 | "no-extra-semi": 2, 114 | "no-fallthrough": 2, 115 | "no-floating-decimal": 2, 116 | "no-func-assign": 2, 117 | "no-implicit-coercion": 2, 118 | "no-implied-eval": 2, 119 | "no-inline-comments": 0, 120 | "no-inner-declarations": [2, "functions"], 121 | "no-invalid-regexp": 2, 122 | "no-invalid-this": 0, 123 | "no-irregular-whitespace": 2, 124 | "no-iterator": 2, 125 | "no-label-var": 2, 126 | "no-labels": [2, {"allowLoop": true}], 127 | "no-lone-blocks": 2, 128 | "no-lonely-if": 2, 129 | "no-loop-func": 0, 130 | "no-mixed-requires": [2, true], 131 | "no-mixed-spaces-and-tabs": 2, 132 | "no-multi-spaces": 2, 133 | "no-multi-str": 2, 134 | "no-multiple-empty-lines": 0, 135 | "no-native-reassign": 0, 136 | "no-negated-in-lhs": 2, 137 | "no-nested-ternary": 0, 138 | "no-new": 2, 139 | "no-new-func": 0, 140 | "no-new-object": 2, 141 | "no-new-require": 2, 142 | "no-new-wrappers": 2, 143 | "no-obj-calls": 2, 144 | "no-octal": 2, 145 | "no-octal-escape": 2, 146 | "no-param-reassign": 2, 147 | "no-path-concat": 2, 148 | "no-plusplus": 0, 149 | "no-process-env": 0, 150 | "no-process-exit": 0, 151 | "no-proto": 2, 152 | "no-redeclare": 2, 153 | "no-regex-spaces": 2, 154 | "no-restricted-modules": 0, 155 | "no-return-assign": 2, 156 | "no-script-url": 2, 157 | "no-self-compare": 0, 158 | "no-sequences": 2, 159 | "no-shadow": 2, 160 | "no-shadow-restricted-names": 2, 161 | "no-spaced-func": 2, 162 | "no-sparse-arrays": 2, 163 | "no-sync": 2, 164 | "no-ternary": 0, 165 | "no-this-before-super": 2, 166 | "no-throw-literal": 2, 167 | "no-trailing-spaces": 2, 168 | "no-undef": 2, 169 | "no-undef-init": 2, 170 | "no-undefined": 0, 171 | "no-underscore-dangle": 0, 172 | "no-unexpected-multiline": 2, 173 | "no-unneeded-ternary": 2, 174 | "no-unreachable": 2, 175 | "no-unused-expressions": 2, 176 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 177 | "no-use-before-define": 0, 178 | "no-useless-call": 2, 179 | "no-useless-escape": 2, 180 | "no-useless-return": 2, 181 | "no-var": 2, 182 | "no-void": 2, 183 | "no-warning-comments": 0, 184 | "no-with": 2, 185 | "object-curly-spacing": [0, "always"], 186 | "object-shorthand": [2, "always"], 187 | "one-var": [2, "never"], 188 | "operator-assignment": [2, "always"], 189 | "padded-blocks": 0, 190 | "prefer-const": 2, 191 | "prefer-reflect": 0, 192 | "prefer-spread": 0, 193 | "quote-props": [2, "as-needed", {"numbers": true}], 194 | "radix": 2, 195 | "require-yield": 2, 196 | "semi": [2, "always"], 197 | "semi-spacing": [2, {"before": false, "after": true}], 198 | "sort-vars": 0, 199 | "space-before-blocks": [2, "always"], 200 | "space-in-parens": 0, 201 | "space-infix-ops": [2, {"int32Hint": false}], 202 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 203 | "spaced-comment": [2, "always"], 204 | "strict": 0, 205 | "use-isnan": 2, 206 | "valid-jsdoc": 0, 207 | "valid-typeof": 2, 208 | "vars-on-top": 0, 209 | "wrap-regex": 0, 210 | "yoda": [2, "never", {"exceptRange": true}] 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/coverage/.* 3 | .*/scripts/.* 4 | .*/node_modules/.* 5 | 6 | [include] 7 | 8 | [libs] 9 | 10 | [options] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /netlify-lambda-lib 3 | /public/schema.js 4 | node_modules 5 | npm-debug.log 6 | /coverage 7 | /cache/data.json 8 | 9 | # Local Netlify folder 10 | .netlify 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.5.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "flow", 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: stable 4 | 5 | cache: npm 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to swapi-graphql 2 | ============================= 3 | 4 | ## License 5 | 6 | By contributing to swapi-graphql, you agree that your contributions will be 7 | licensed under the [LICENSE-examples](LICENSE) in this repository. 8 | -------------------------------------------------------------------------------- /LICENSE-examples: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) GraphQL Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SWAPI GraphQL Wrapper 2 | ===================== 3 | 4 | A wrapper around [SWAPI](http://swapi.tech) built using GraphQL converting it into [this schema](schema.graphql). 5 | 6 | Uses: 7 | 8 | * [graphql-js](https://github.com/graphql/graphql-js) - a JavaScript GraphQL runtime. 9 | * [DataLoader](https://github.com/graphql/dataloader) - for coalescing and caching fetches. 10 | * [express-graphql](https://github.com/graphql/express-graphql) - to provide HTTP access to GraphQL. 11 | * [aws-serverless-express](https://github.com/awslabs/aws-serverless-express) - to use `express-graphql` on aws lambda. 12 | * [GraphiQL](https://github.com/graphql/graphiql) - for easy exploration of this GraphQL server. 13 | 14 | Try it out at: http://graphql.org/swapi-graphql 15 | 16 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 17 | [![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/graphql/swapi-graphql) 18 | 19 | ## Getting Started 20 | 21 | Install dependencies with 22 | 23 | ```sh 24 | npm install 25 | ``` 26 | 27 | ## SWAPI Wrapper 28 | 29 | The SWAPI wrapper is in `./swapi`. It can be tested with: 30 | 31 | ```sh 32 | yarn test 33 | ``` 34 | 35 | ## Local Server 36 | 37 | A local express server is in `./server`. It can be run with: 38 | 39 | ```sh 40 | npm start 41 | ``` 42 | 43 | A GraphiQL instance will be opened at http://localhost:8080/ (or similar; the actual port number will be printed to the console) to explore the API. 44 | 45 | # Contributing to this repo 46 | 47 | This repository is managed by EasyCLA. Project participants must sign the free ([GraphQL Specification Membership agreement](https://preview-spec-membership.graphql.org) before making a contribution. You only need to do this one time, and it can be signed by [individual contributors](http://individual-spec-membership.graphql.org/) or their [employers](http://corporate-spec-membership.graphql.org/). 48 | 49 | To initiate the signature process please open a PR against this repo. The EasyCLA bot will block the merge if we still need a membership agreement from you. 50 | 51 | You can find [detailed information here](https://github.com/graphql/graphql-wg/tree/main/membership). If you have issues, please email [operations@graphql.org](mailto:operations@graphql.org). 52 | 53 | If your company benefits from GraphQL and you would like to provide essential financial support for the systems and people that power our community, please also consider membership in the [GraphQL Foundation](https://foundation.graphql.org/join). -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SWAPI GraphQL Wrapper", 3 | "description": "A wrapper around [SWAPI](http://swapi.tech) built using GraphQL.", 4 | "repository": "https://github.com/graphql/swapi-graphql", 5 | "keywords": ["graphql", "swapi", "swapi-graphql"] 6 | } 7 | -------------------------------------------------------------------------------- /cache/README.md: -------------------------------------------------------------------------------- 1 | SWAPI Cache 2 | ============= 3 | 4 | This module contains a cache of SWAPI obtained using `npm run download`. 5 | -------------------------------------------------------------------------------- /doc/example_queries/01_basic_query.graphql: -------------------------------------------------------------------------------- 1 | { 2 | person(personID: 4) { 3 | name 4 | } 5 | } -------------------------------------------------------------------------------- /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/03_nested_fields.graphql: -------------------------------------------------------------------------------- 1 | { 2 | person(personID: 4) { 3 | name 4 | gender 5 | homeworld { 6 | name 7 | } 8 | starshipConnection { 9 | edges { 10 | node { 11 | id 12 | manufacturers 13 | } 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /doc/example_queries/04_all_starships.graphql: -------------------------------------------------------------------------------- 1 | # GraphQL server handles pagination on this example 2 | { 3 | allStarships { 4 | edges { 5 | node { 6 | id 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /doc/example_queries/05_argument.graphql: -------------------------------------------------------------------------------- 1 | { 2 | allStarships(first: 7) { 3 | edges { 4 | node { 5 | id 6 | name 7 | model 8 | costInCredits 9 | pilotConnection { 10 | edges { 11 | node { 12 | name 13 | homeworld { 14 | name 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /doc/example_queries/06_fragments.graphql: -------------------------------------------------------------------------------- 1 | { 2 | allStarships(first: 7) { 3 | edges { 4 | node { 5 | id 6 | name 7 | model 8 | costInCredits 9 | pilotConnection { 10 | edges { 11 | node { 12 | ...pilotFragment 13 | } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | 21 | fragment pilotFragment on Person { 22 | name 23 | homeworld { name } 24 | } -------------------------------------------------------------------------------- /doc/example_queries/07_fragments.graphql: -------------------------------------------------------------------------------- 1 | { 2 | allStarships(first: 7) { 3 | edges { 4 | node { 5 | ...starshipFragment 6 | } 7 | } 8 | } 9 | } 10 | 11 | fragment starshipFragment on Starship { 12 | id 13 | name 14 | model 15 | costInCredits 16 | pilotConnection { edges { node { ...pilotFragment }}} 17 | } 18 | fragment pilotFragment on Person { 19 | name 20 | homeworld { name } 21 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /handler/graphql.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2020, GraphQL Contributors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import { createHandler } from 'graphql-http/lib/use/@netlify/functions'; 11 | import schema from '../lib/schema'; 12 | 13 | const graphqlHandler = createHandler({ schema: schema.default }) 14 | 15 | // Create the GraphQL over HTTP native fetch handler 16 | export const handler = async (req, ctx) => { 17 | if (req.httpMethod === 'OPTIONS') { 18 | return { 19 | statusCode: 200, 20 | headers: { 21 | 'Access-Control-Allow-Origin': '*', 22 | 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', 23 | 'Access-Control-Allow-Headers': '*', 24 | 'Access-Control-Max-Age': '86400', 25 | } 26 | }; 27 | } 28 | const result = await graphqlHandler(req, ctx) 29 | return { 30 | ...result, 31 | headers: { 32 | ...result.headers, 33 | 'Access-Control-Allow-Origin': '*', 34 | 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', 35 | 'Access-Control-Allow-Headers': '*', 36 | 'Access-Control-Max-Age': '86400', 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /handler/index.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * 301 permanently redirect /index requests to /graphql 3 | */ 4 | export const handler = async function(event) { 5 | if (event.httpMethod === 'OPTIONS') { 6 | return { 7 | statusCode: 200, 8 | headers: { 9 | 'Access-Control-Allow-Origin': '*', 10 | 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', 11 | 'Access-Control-Allow-Headers': '*', 12 | 'Access-Control-Max-Age': '86400', 13 | } 14 | }; 15 | } 16 | let location = '/graphql'; 17 | if (event.queryStringParameters) { 18 | location += '?' + new URLSearchParams(event.queryStringParameters); 19 | } 20 | return { 21 | statusCode: 301, 22 | headers: { 23 | Location: location, 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /handler/swapi.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * 301 permanently redirect /swapi requests to /graphql 3 | */ 4 | export const handler = async function(event) { 5 | if (event.httpMethod === 'OPTIONS') { 6 | return { 7 | statusCode: 200, 8 | headers: { 9 | 'Access-Control-Allow-Origin': '*', 10 | 'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS', 11 | 'Access-Control-Allow-Headers': '*', 12 | 'Access-Control-Max-Age': '86400', 13 | } 14 | }; 15 | } 16 | let location = '/graphql'; 17 | if (event.queryStringParameters) { 18 | location += '?' + new URLSearchParams(event.queryStringParameters); 19 | } 20 | return { 21 | statusCode: 301, 22 | headers: { 23 | Location: location, 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const packageJSON = require('./package.json'); 2 | 3 | process.env.TZ = 'UTC'; 4 | 5 | module.exports = { 6 | verbose: true, 7 | name: packageJSON.name, 8 | displayName: packageJSON.name, 9 | transform: { 10 | '\\.[jt]sx?$': 'babel-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 13 | testURL: 'http://localhost', 14 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], 15 | testMatch: ['**/*.(spec|test).js'], 16 | collectCoverage: true, 17 | coverageDirectory: './coverage/', 18 | }; 19 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "handler" 3 | command = "npm run download && npm run build" 4 | publish = "public/" 5 | [[headers]] 6 | for = "/*" 7 | [headers.values] 8 | Access-Control-Allow-Origin = "*" 9 | Access-Control-Allow-Methods = "GET,HEAD,POST,OPTIONS" 10 | Access-Control-Allow-Headers = "*" 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swapi-graphql", 3 | "description": "A GraphQL wrapper for swapi.tech", 4 | "contributors": [ 5 | "Nicholas Schrock ", 6 | "Daniel Schafer " 7 | ], 8 | "license": "BSD-3-Clause", 9 | "homepage": "https://github.com/graphql/swapi-graphql", 10 | "bugs": { 11 | "url": "https://github.com/graphql/swapi-graphql/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "http://github.com/graphql/swapi-graphql.git" 16 | }, 17 | "engines": { 18 | "node": ">=20.0.0" 19 | }, 20 | "browserify": { 21 | "transform": [ 22 | "babelify" 23 | ] 24 | }, 25 | "browserify-shim": { 26 | "react": "global:React" 27 | }, 28 | "scripts": { 29 | "postinstall": "npm run download && npm run build", 30 | "watch": "netlify dev", 31 | "start": "netlify build", 32 | "test": "npm run lint && npm run check && npm run test:only", 33 | "test:only": "jest", 34 | "lint": "eslint src/** handler/**", 35 | "lint:fix": "eslint --fix src handler/**", 36 | "check": "flow check", 37 | "cover": "babel-node node_modules/.bin/isparta cover --root src --report html node_modules/.bin/_mocha -- $npm_package_options_mocha", 38 | "coveralls": "babel-node node_modules/.bin/isparta cover --root src --report lcovonly node_modules/.bin/_mocha -- $npm_package_options_mocha && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 39 | "build": "rimraf lib && babel src --ignore __tests__,public,handler --out-dir lib/", 40 | "download": "babel-node scripts/download.js cache/data.json", 41 | "serve-public": "babel-node scripts/serve-public", 42 | "prettier": "prettier --write 'src/**/*.js'", 43 | "print-schema": "babel-node scripts/print-schema.js", 44 | "store-schema": "babel-node scripts/store-schema.js" 45 | }, 46 | "dependencies": { 47 | "babel-runtime": "^6.26.0", 48 | "dataloader": "1.4.0", 49 | "graphql": "14.5.8", 50 | "graphql-http": "^1.22.4", 51 | "graphql-relay": "0.6.0" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.12.3", 55 | "babel-cli": "^6.26.0", 56 | "babel-core": "^6.26.3", 57 | "babel-eslint": "^10.0.3", 58 | "babel-jest": "^26.6.3", 59 | "babel-plugin-syntax-async-functions": "6.13.0", 60 | "babel-plugin-syntax-object-rest-spread": "^6.13.0", 61 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 62 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 63 | "babel-plugin-transform-runtime": "^6.23.0", 64 | "babel-preset-env": "^1.7.0", 65 | "babel-register": "^6.26.0", 66 | "coveralls": "^3.0.4", 67 | "eslint": "^5.16.0", 68 | "eslint-plugin-babel": "5.3.0", 69 | "eslint-plugin-prettier": "^3.1.0", 70 | "flow-bin": "^0.69.0", 71 | "isomorphic-fetch": "2.2.1", 72 | "isparta": "^4.1.1", 73 | "jest": "^26.6.3", 74 | "netlify-cli": "^15.10.0", 75 | "prettier": "^1.18.2", 76 | "sane": "^4.1.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /graphql /.netlify/functions/graphql 200 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql/swapi-graphql/8347ee93d10848e0ba22d8e417e31d22c77a65c9/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | SWAPI GraphQL API 9 | 27 | 28 | 33 | 38 | 39 | 40 |
Loading…
41 | 42 | 43 | 44 | 45 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Root 3 | } 4 | 5 | """A single film.""" 6 | type Film implements Node { 7 | """The title of this film.""" 8 | title: String 9 | 10 | """The episode number of this film.""" 11 | episodeID: Int 12 | 13 | """The opening paragraphs at the beginning of this film.""" 14 | openingCrawl: String 15 | 16 | """The name of the director of this film.""" 17 | director: String 18 | 19 | """The name(s) of the producer(s) of this film.""" 20 | producers: [String] 21 | 22 | """The ISO 8601 date format of film release at original creator country.""" 23 | releaseDate: String 24 | speciesConnection(after: String, first: Int, before: String, last: Int): FilmSpeciesConnection 25 | starshipConnection(after: String, first: Int, before: String, last: Int): FilmStarshipsConnection 26 | vehicleConnection(after: String, first: Int, before: String, last: Int): FilmVehiclesConnection 27 | characterConnection(after: String, first: Int, before: String, last: Int): FilmCharactersConnection 28 | planetConnection(after: String, first: Int, before: String, last: Int): FilmPlanetsConnection 29 | 30 | """The ISO 8601 date format of the time that this resource was created.""" 31 | created: String 32 | 33 | """The ISO 8601 date format of the time that this resource was edited.""" 34 | edited: String 35 | 36 | """The ID of an object""" 37 | id: ID! 38 | } 39 | 40 | """A connection to a list of items.""" 41 | type FilmCharactersConnection { 42 | """Information to aid in pagination.""" 43 | pageInfo: PageInfo! 44 | 45 | """A list of edges.""" 46 | edges: [FilmCharactersEdge] 47 | 48 | """ 49 | A count of the total number of objects in this connection, ignoring pagination. 50 | This allows a client to fetch the first five objects by passing "5" as the 51 | argument to "first", then fetch the total count so it could display "5 of 83", 52 | for example. 53 | """ 54 | totalCount: Int 55 | 56 | """ 57 | A list of all of the objects returned in the connection. This is a convenience 58 | field provided for quickly exploring the API; rather than querying for 59 | "{ edges { node } }" when no edge data is needed, this field can be be used 60 | instead. Note that when clients like Relay need to fetch the "cursor" field on 61 | the edge to enable efficient pagination, this shortcut cannot be used, and the 62 | full "{ edges { node } }" version should be used instead. 63 | """ 64 | characters: [Person] 65 | } 66 | 67 | """An edge in a connection.""" 68 | type FilmCharactersEdge { 69 | """The item at the end of the edge""" 70 | node: Person 71 | 72 | """A cursor for use in pagination""" 73 | cursor: String! 74 | } 75 | 76 | """A connection to a list of items.""" 77 | type FilmPlanetsConnection { 78 | """Information to aid in pagination.""" 79 | pageInfo: PageInfo! 80 | 81 | """A list of edges.""" 82 | edges: [FilmPlanetsEdge] 83 | 84 | """ 85 | A count of the total number of objects in this connection, ignoring pagination. 86 | This allows a client to fetch the first five objects by passing "5" as the 87 | argument to "first", then fetch the total count so it could display "5 of 83", 88 | for example. 89 | """ 90 | totalCount: Int 91 | 92 | """ 93 | A list of all of the objects returned in the connection. This is a convenience 94 | field provided for quickly exploring the API; rather than querying for 95 | "{ edges { node } }" when no edge data is needed, this field can be be used 96 | instead. Note that when clients like Relay need to fetch the "cursor" field on 97 | the edge to enable efficient pagination, this shortcut cannot be used, and the 98 | full "{ edges { node } }" version should be used instead. 99 | """ 100 | planets: [Planet] 101 | } 102 | 103 | """An edge in a connection.""" 104 | type FilmPlanetsEdge { 105 | """The item at the end of the edge""" 106 | node: Planet 107 | 108 | """A cursor for use in pagination""" 109 | cursor: String! 110 | } 111 | 112 | """A connection to a list of items.""" 113 | type FilmsConnection { 114 | """Information to aid in pagination.""" 115 | pageInfo: PageInfo! 116 | 117 | """A list of edges.""" 118 | edges: [FilmsEdge] 119 | 120 | """ 121 | A count of the total number of objects in this connection, ignoring pagination. 122 | This allows a client to fetch the first five objects by passing "5" as the 123 | argument to "first", then fetch the total count so it could display "5 of 83", 124 | for example. 125 | """ 126 | totalCount: Int 127 | 128 | """ 129 | A list of all of the objects returned in the connection. This is a convenience 130 | field provided for quickly exploring the API; rather than querying for 131 | "{ edges { node } }" when no edge data is needed, this field can be be used 132 | instead. Note that when clients like Relay need to fetch the "cursor" field on 133 | the edge to enable efficient pagination, this shortcut cannot be used, and the 134 | full "{ edges { node } }" version should be used instead. 135 | """ 136 | films: [Film] 137 | } 138 | 139 | """An edge in a connection.""" 140 | type FilmsEdge { 141 | """The item at the end of the edge""" 142 | node: Film 143 | 144 | """A cursor for use in pagination""" 145 | cursor: String! 146 | } 147 | 148 | """A connection to a list of items.""" 149 | type FilmSpeciesConnection { 150 | """Information to aid in pagination.""" 151 | pageInfo: PageInfo! 152 | 153 | """A list of edges.""" 154 | edges: [FilmSpeciesEdge] 155 | 156 | """ 157 | A count of the total number of objects in this connection, ignoring pagination. 158 | This allows a client to fetch the first five objects by passing "5" as the 159 | argument to "first", then fetch the total count so it could display "5 of 83", 160 | for example. 161 | """ 162 | totalCount: Int 163 | 164 | """ 165 | A list of all of the objects returned in the connection. This is a convenience 166 | field provided for quickly exploring the API; rather than querying for 167 | "{ edges { node } }" when no edge data is needed, this field can be be used 168 | instead. Note that when clients like Relay need to fetch the "cursor" field on 169 | the edge to enable efficient pagination, this shortcut cannot be used, and the 170 | full "{ edges { node } }" version should be used instead. 171 | """ 172 | species: [Species] 173 | } 174 | 175 | """An edge in a connection.""" 176 | type FilmSpeciesEdge { 177 | """The item at the end of the edge""" 178 | node: Species 179 | 180 | """A cursor for use in pagination""" 181 | cursor: String! 182 | } 183 | 184 | """A connection to a list of items.""" 185 | type FilmStarshipsConnection { 186 | """Information to aid in pagination.""" 187 | pageInfo: PageInfo! 188 | 189 | """A list of edges.""" 190 | edges: [FilmStarshipsEdge] 191 | 192 | """ 193 | A count of the total number of objects in this connection, ignoring pagination. 194 | This allows a client to fetch the first five objects by passing "5" as the 195 | argument to "first", then fetch the total count so it could display "5 of 83", 196 | for example. 197 | """ 198 | totalCount: Int 199 | 200 | """ 201 | A list of all of the objects returned in the connection. This is a convenience 202 | field provided for quickly exploring the API; rather than querying for 203 | "{ edges { node } }" when no edge data is needed, this field can be be used 204 | instead. Note that when clients like Relay need to fetch the "cursor" field on 205 | the edge to enable efficient pagination, this shortcut cannot be used, and the 206 | full "{ edges { node } }" version should be used instead. 207 | """ 208 | starships: [Starship] 209 | } 210 | 211 | """An edge in a connection.""" 212 | type FilmStarshipsEdge { 213 | """The item at the end of the edge""" 214 | node: Starship 215 | 216 | """A cursor for use in pagination""" 217 | cursor: String! 218 | } 219 | 220 | """A connection to a list of items.""" 221 | type FilmVehiclesConnection { 222 | """Information to aid in pagination.""" 223 | pageInfo: PageInfo! 224 | 225 | """A list of edges.""" 226 | edges: [FilmVehiclesEdge] 227 | 228 | """ 229 | A count of the total number of objects in this connection, ignoring pagination. 230 | This allows a client to fetch the first five objects by passing "5" as the 231 | argument to "first", then fetch the total count so it could display "5 of 83", 232 | for example. 233 | """ 234 | totalCount: Int 235 | 236 | """ 237 | A list of all of the objects returned in the connection. This is a convenience 238 | field provided for quickly exploring the API; rather than querying for 239 | "{ edges { node } }" when no edge data is needed, this field can be be used 240 | instead. Note that when clients like Relay need to fetch the "cursor" field on 241 | the edge to enable efficient pagination, this shortcut cannot be used, and the 242 | full "{ edges { node } }" version should be used instead. 243 | """ 244 | vehicles: [Vehicle] 245 | } 246 | 247 | """An edge in a connection.""" 248 | type FilmVehiclesEdge { 249 | """The item at the end of the edge""" 250 | node: Vehicle 251 | 252 | """A cursor for use in pagination""" 253 | cursor: String! 254 | } 255 | 256 | """An object with an ID""" 257 | interface Node { 258 | """The id of the object.""" 259 | id: ID! 260 | } 261 | 262 | """Information about pagination in a connection.""" 263 | type PageInfo { 264 | """When paginating forwards, are there more items?""" 265 | hasNextPage: Boolean! 266 | 267 | """When paginating backwards, are there more items?""" 268 | hasPreviousPage: Boolean! 269 | 270 | """When paginating backwards, the cursor to continue.""" 271 | startCursor: String 272 | 273 | """When paginating forwards, the cursor to continue.""" 274 | endCursor: String 275 | } 276 | 277 | """A connection to a list of items.""" 278 | type PeopleConnection { 279 | """Information to aid in pagination.""" 280 | pageInfo: PageInfo! 281 | 282 | """A list of edges.""" 283 | edges: [PeopleEdge] 284 | 285 | """ 286 | A count of the total number of objects in this connection, ignoring pagination. 287 | This allows a client to fetch the first five objects by passing "5" as the 288 | argument to "first", then fetch the total count so it could display "5 of 83", 289 | for example. 290 | """ 291 | totalCount: Int 292 | 293 | """ 294 | A list of all of the objects returned in the connection. This is a convenience 295 | field provided for quickly exploring the API; rather than querying for 296 | "{ edges { node } }" when no edge data is needed, this field can be be used 297 | instead. Note that when clients like Relay need to fetch the "cursor" field on 298 | the edge to enable efficient pagination, this shortcut cannot be used, and the 299 | full "{ edges { node } }" version should be used instead. 300 | """ 301 | people: [Person] 302 | } 303 | 304 | """An edge in a connection.""" 305 | type PeopleEdge { 306 | """The item at the end of the edge""" 307 | node: Person 308 | 309 | """A cursor for use in pagination""" 310 | cursor: String! 311 | } 312 | 313 | """An individual person or character within the Star Wars universe.""" 314 | type Person implements Node { 315 | """The name of this person.""" 316 | name: String 317 | 318 | """ 319 | The birth year of the person, using the in-universe standard of BBY or ABY - 320 | Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is 321 | a battle that occurs at the end of Star Wars episode IV: A New Hope. 322 | """ 323 | birthYear: String 324 | 325 | """ 326 | The eye color of this person. Will be "unknown" if not known or "n/a" if the 327 | person does not have an eye. 328 | """ 329 | eyeColor: String 330 | 331 | """ 332 | The gender of this person. Either "Male", "Female" or "unknown", 333 | "n/a" if the person does not have a gender. 334 | """ 335 | gender: String 336 | 337 | """ 338 | The hair color of this person. Will be "unknown" if not known or "n/a" if the 339 | person does not have hair. 340 | """ 341 | hairColor: String 342 | 343 | """The height of the person in centimeters.""" 344 | height: Int 345 | 346 | """The mass of the person in kilograms.""" 347 | mass: Float 348 | 349 | """The skin color of this person.""" 350 | skinColor: String 351 | 352 | """A planet that this person was born on or inhabits.""" 353 | homeworld: Planet 354 | filmConnection(after: String, first: Int, before: String, last: Int): PersonFilmsConnection 355 | 356 | """The species that this person belongs to, or null if unknown.""" 357 | species: Species 358 | starshipConnection(after: String, first: Int, before: String, last: Int): PersonStarshipsConnection 359 | vehicleConnection(after: String, first: Int, before: String, last: Int): PersonVehiclesConnection 360 | 361 | """The ISO 8601 date format of the time that this resource was created.""" 362 | created: String 363 | 364 | """The ISO 8601 date format of the time that this resource was edited.""" 365 | edited: String 366 | 367 | """The ID of an object""" 368 | id: ID! 369 | } 370 | 371 | """A connection to a list of items.""" 372 | type PersonFilmsConnection { 373 | """Information to aid in pagination.""" 374 | pageInfo: PageInfo! 375 | 376 | """A list of edges.""" 377 | edges: [PersonFilmsEdge] 378 | 379 | """ 380 | A count of the total number of objects in this connection, ignoring pagination. 381 | This allows a client to fetch the first five objects by passing "5" as the 382 | argument to "first", then fetch the total count so it could display "5 of 83", 383 | for example. 384 | """ 385 | totalCount: Int 386 | 387 | """ 388 | A list of all of the objects returned in the connection. This is a convenience 389 | field provided for quickly exploring the API; rather than querying for 390 | "{ edges { node } }" when no edge data is needed, this field can be be used 391 | instead. Note that when clients like Relay need to fetch the "cursor" field on 392 | the edge to enable efficient pagination, this shortcut cannot be used, and the 393 | full "{ edges { node } }" version should be used instead. 394 | """ 395 | films: [Film] 396 | } 397 | 398 | """An edge in a connection.""" 399 | type PersonFilmsEdge { 400 | """The item at the end of the edge""" 401 | node: Film 402 | 403 | """A cursor for use in pagination""" 404 | cursor: String! 405 | } 406 | 407 | """A connection to a list of items.""" 408 | type PersonStarshipsConnection { 409 | """Information to aid in pagination.""" 410 | pageInfo: PageInfo! 411 | 412 | """A list of edges.""" 413 | edges: [PersonStarshipsEdge] 414 | 415 | """ 416 | A count of the total number of objects in this connection, ignoring pagination. 417 | This allows a client to fetch the first five objects by passing "5" as the 418 | argument to "first", then fetch the total count so it could display "5 of 83", 419 | for example. 420 | """ 421 | totalCount: Int 422 | 423 | """ 424 | A list of all of the objects returned in the connection. This is a convenience 425 | field provided for quickly exploring the API; rather than querying for 426 | "{ edges { node } }" when no edge data is needed, this field can be be used 427 | instead. Note that when clients like Relay need to fetch the "cursor" field on 428 | the edge to enable efficient pagination, this shortcut cannot be used, and the 429 | full "{ edges { node } }" version should be used instead. 430 | """ 431 | starships: [Starship] 432 | } 433 | 434 | """An edge in a connection.""" 435 | type PersonStarshipsEdge { 436 | """The item at the end of the edge""" 437 | node: Starship 438 | 439 | """A cursor for use in pagination""" 440 | cursor: String! 441 | } 442 | 443 | """A connection to a list of items.""" 444 | type PersonVehiclesConnection { 445 | """Information to aid in pagination.""" 446 | pageInfo: PageInfo! 447 | 448 | """A list of edges.""" 449 | edges: [PersonVehiclesEdge] 450 | 451 | """ 452 | A count of the total number of objects in this connection, ignoring pagination. 453 | This allows a client to fetch the first five objects by passing "5" as the 454 | argument to "first", then fetch the total count so it could display "5 of 83", 455 | for example. 456 | """ 457 | totalCount: Int 458 | 459 | """ 460 | A list of all of the objects returned in the connection. This is a convenience 461 | field provided for quickly exploring the API; rather than querying for 462 | "{ edges { node } }" when no edge data is needed, this field can be be used 463 | instead. Note that when clients like Relay need to fetch the "cursor" field on 464 | the edge to enable efficient pagination, this shortcut cannot be used, and the 465 | full "{ edges { node } }" version should be used instead. 466 | """ 467 | vehicles: [Vehicle] 468 | } 469 | 470 | """An edge in a connection.""" 471 | type PersonVehiclesEdge { 472 | """The item at the end of the edge""" 473 | node: Vehicle 474 | 475 | """A cursor for use in pagination""" 476 | cursor: String! 477 | } 478 | 479 | """ 480 | A large mass, planet or planetoid in the Star Wars Universe, at the time of 481 | 0 ABY. 482 | """ 483 | type Planet implements Node { 484 | """The name of this planet.""" 485 | name: String 486 | 487 | """The diameter of this planet in kilometers.""" 488 | diameter: Int 489 | 490 | """ 491 | The number of standard hours it takes for this planet to complete a single 492 | rotation on its axis. 493 | """ 494 | rotationPeriod: Int 495 | 496 | """ 497 | The number of standard days it takes for this planet to complete a single orbit 498 | of its local star. 499 | """ 500 | orbitalPeriod: Int 501 | 502 | """ 503 | A number denoting the gravity of this planet, where "1" is normal or 1 standard 504 | G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. 505 | """ 506 | gravity: String 507 | 508 | """The average population of sentient beings inhabiting this planet.""" 509 | population: Float 510 | 511 | """The climates of this planet.""" 512 | climates: [String] 513 | 514 | """The terrains of this planet.""" 515 | terrains: [String] 516 | 517 | """ 518 | The percentage of the planet surface that is naturally occurring water or bodies 519 | of water. 520 | """ 521 | surfaceWater: Float 522 | residentConnection(after: String, first: Int, before: String, last: Int): PlanetResidentsConnection 523 | filmConnection(after: String, first: Int, before: String, last: Int): PlanetFilmsConnection 524 | 525 | """The ISO 8601 date format of the time that this resource was created.""" 526 | created: String 527 | 528 | """The ISO 8601 date format of the time that this resource was edited.""" 529 | edited: String 530 | 531 | """The ID of an object""" 532 | id: ID! 533 | } 534 | 535 | """A connection to a list of items.""" 536 | type PlanetFilmsConnection { 537 | """Information to aid in pagination.""" 538 | pageInfo: PageInfo! 539 | 540 | """A list of edges.""" 541 | edges: [PlanetFilmsEdge] 542 | 543 | """ 544 | A count of the total number of objects in this connection, ignoring pagination. 545 | This allows a client to fetch the first five objects by passing "5" as the 546 | argument to "first", then fetch the total count so it could display "5 of 83", 547 | for example. 548 | """ 549 | totalCount: Int 550 | 551 | """ 552 | A list of all of the objects returned in the connection. This is a convenience 553 | field provided for quickly exploring the API; rather than querying for 554 | "{ edges { node } }" when no edge data is needed, this field can be be used 555 | instead. Note that when clients like Relay need to fetch the "cursor" field on 556 | the edge to enable efficient pagination, this shortcut cannot be used, and the 557 | full "{ edges { node } }" version should be used instead. 558 | """ 559 | films: [Film] 560 | } 561 | 562 | """An edge in a connection.""" 563 | type PlanetFilmsEdge { 564 | """The item at the end of the edge""" 565 | node: Film 566 | 567 | """A cursor for use in pagination""" 568 | cursor: String! 569 | } 570 | 571 | """A connection to a list of items.""" 572 | type PlanetResidentsConnection { 573 | """Information to aid in pagination.""" 574 | pageInfo: PageInfo! 575 | 576 | """A list of edges.""" 577 | edges: [PlanetResidentsEdge] 578 | 579 | """ 580 | A count of the total number of objects in this connection, ignoring pagination. 581 | This allows a client to fetch the first five objects by passing "5" as the 582 | argument to "first", then fetch the total count so it could display "5 of 83", 583 | for example. 584 | """ 585 | totalCount: Int 586 | 587 | """ 588 | A list of all of the objects returned in the connection. This is a convenience 589 | field provided for quickly exploring the API; rather than querying for 590 | "{ edges { node } }" when no edge data is needed, this field can be be used 591 | instead. Note that when clients like Relay need to fetch the "cursor" field on 592 | the edge to enable efficient pagination, this shortcut cannot be used, and the 593 | full "{ edges { node } }" version should be used instead. 594 | """ 595 | residents: [Person] 596 | } 597 | 598 | """An edge in a connection.""" 599 | type PlanetResidentsEdge { 600 | """The item at the end of the edge""" 601 | node: Person 602 | 603 | """A cursor for use in pagination""" 604 | cursor: String! 605 | } 606 | 607 | """A connection to a list of items.""" 608 | type PlanetsConnection { 609 | """Information to aid in pagination.""" 610 | pageInfo: PageInfo! 611 | 612 | """A list of edges.""" 613 | edges: [PlanetsEdge] 614 | 615 | """ 616 | A count of the total number of objects in this connection, ignoring pagination. 617 | This allows a client to fetch the first five objects by passing "5" as the 618 | argument to "first", then fetch the total count so it could display "5 of 83", 619 | for example. 620 | """ 621 | totalCount: Int 622 | 623 | """ 624 | A list of all of the objects returned in the connection. This is a convenience 625 | field provided for quickly exploring the API; rather than querying for 626 | "{ edges { node } }" when no edge data is needed, this field can be be used 627 | instead. Note that when clients like Relay need to fetch the "cursor" field on 628 | the edge to enable efficient pagination, this shortcut cannot be used, and the 629 | full "{ edges { node } }" version should be used instead. 630 | """ 631 | planets: [Planet] 632 | } 633 | 634 | """An edge in a connection.""" 635 | type PlanetsEdge { 636 | """The item at the end of the edge""" 637 | node: Planet 638 | 639 | """A cursor for use in pagination""" 640 | cursor: String! 641 | } 642 | 643 | type Root { 644 | allFilms(after: String, first: Int, before: String, last: Int): FilmsConnection 645 | film(id: ID, filmID: ID): Film 646 | allPeople(after: String, first: Int, before: String, last: Int): PeopleConnection 647 | person(id: ID, personID: ID): Person 648 | allPlanets(after: String, first: Int, before: String, last: Int): PlanetsConnection 649 | planet(id: ID, planetID: ID): Planet 650 | allSpecies(after: String, first: Int, before: String, last: Int): SpeciesConnection 651 | species(id: ID, speciesID: ID): Species 652 | allStarships(after: String, first: Int, before: String, last: Int): StarshipsConnection 653 | starship(id: ID, starshipID: ID): Starship 654 | allVehicles(after: String, first: Int, before: String, last: Int): VehiclesConnection 655 | vehicle(id: ID, vehicleID: ID): Vehicle 656 | 657 | """Fetches an object given its ID""" 658 | node( 659 | """The ID of an object""" 660 | id: ID! 661 | ): Node 662 | } 663 | 664 | """A type of person or character within the Star Wars Universe.""" 665 | type Species implements Node { 666 | """The name of this species.""" 667 | name: String 668 | 669 | """The classification of this species, such as "mammal" or "reptile".""" 670 | classification: String 671 | 672 | """The designation of this species, such as "sentient".""" 673 | designation: String 674 | 675 | """The average height of this species in centimeters.""" 676 | averageHeight: Float 677 | 678 | """The average lifespan of this species in years, null if unknown.""" 679 | averageLifespan: Int 680 | 681 | """ 682 | Common eye colors for this species, null if this species does not typically 683 | have eyes. 684 | """ 685 | eyeColors: [String] 686 | 687 | """ 688 | Common hair colors for this species, null if this species does not typically 689 | have hair. 690 | """ 691 | hairColors: [String] 692 | 693 | """ 694 | Common skin colors for this species, null if this species does not typically 695 | have skin. 696 | """ 697 | skinColors: [String] 698 | 699 | """The language commonly spoken by this species.""" 700 | language: String 701 | 702 | """A planet that this species originates from.""" 703 | homeworld: Planet 704 | personConnection(after: String, first: Int, before: String, last: Int): SpeciesPeopleConnection 705 | filmConnection(after: String, first: Int, before: String, last: Int): SpeciesFilmsConnection 706 | 707 | """The ISO 8601 date format of the time that this resource was created.""" 708 | created: String 709 | 710 | """The ISO 8601 date format of the time that this resource was edited.""" 711 | edited: String 712 | 713 | """The ID of an object""" 714 | id: ID! 715 | } 716 | 717 | """A connection to a list of items.""" 718 | type SpeciesConnection { 719 | """Information to aid in pagination.""" 720 | pageInfo: PageInfo! 721 | 722 | """A list of edges.""" 723 | edges: [SpeciesEdge] 724 | 725 | """ 726 | A count of the total number of objects in this connection, ignoring pagination. 727 | This allows a client to fetch the first five objects by passing "5" as the 728 | argument to "first", then fetch the total count so it could display "5 of 83", 729 | for example. 730 | """ 731 | totalCount: Int 732 | 733 | """ 734 | A list of all of the objects returned in the connection. This is a convenience 735 | field provided for quickly exploring the API; rather than querying for 736 | "{ edges { node } }" when no edge data is needed, this field can be be used 737 | instead. Note that when clients like Relay need to fetch the "cursor" field on 738 | the edge to enable efficient pagination, this shortcut cannot be used, and the 739 | full "{ edges { node } }" version should be used instead. 740 | """ 741 | species: [Species] 742 | } 743 | 744 | """An edge in a connection.""" 745 | type SpeciesEdge { 746 | """The item at the end of the edge""" 747 | node: Species 748 | 749 | """A cursor for use in pagination""" 750 | cursor: String! 751 | } 752 | 753 | """A connection to a list of items.""" 754 | type SpeciesFilmsConnection { 755 | """Information to aid in pagination.""" 756 | pageInfo: PageInfo! 757 | 758 | """A list of edges.""" 759 | edges: [SpeciesFilmsEdge] 760 | 761 | """ 762 | A count of the total number of objects in this connection, ignoring pagination. 763 | This allows a client to fetch the first five objects by passing "5" as the 764 | argument to "first", then fetch the total count so it could display "5 of 83", 765 | for example. 766 | """ 767 | totalCount: Int 768 | 769 | """ 770 | A list of all of the objects returned in the connection. This is a convenience 771 | field provided for quickly exploring the API; rather than querying for 772 | "{ edges { node } }" when no edge data is needed, this field can be be used 773 | instead. Note that when clients like Relay need to fetch the "cursor" field on 774 | the edge to enable efficient pagination, this shortcut cannot be used, and the 775 | full "{ edges { node } }" version should be used instead. 776 | """ 777 | films: [Film] 778 | } 779 | 780 | """An edge in a connection.""" 781 | type SpeciesFilmsEdge { 782 | """The item at the end of the edge""" 783 | node: Film 784 | 785 | """A cursor for use in pagination""" 786 | cursor: String! 787 | } 788 | 789 | """A connection to a list of items.""" 790 | type SpeciesPeopleConnection { 791 | """Information to aid in pagination.""" 792 | pageInfo: PageInfo! 793 | 794 | """A list of edges.""" 795 | edges: [SpeciesPeopleEdge] 796 | 797 | """ 798 | A count of the total number of objects in this connection, ignoring pagination. 799 | This allows a client to fetch the first five objects by passing "5" as the 800 | argument to "first", then fetch the total count so it could display "5 of 83", 801 | for example. 802 | """ 803 | totalCount: Int 804 | 805 | """ 806 | A list of all of the objects returned in the connection. This is a convenience 807 | field provided for quickly exploring the API; rather than querying for 808 | "{ edges { node } }" when no edge data is needed, this field can be be used 809 | instead. Note that when clients like Relay need to fetch the "cursor" field on 810 | the edge to enable efficient pagination, this shortcut cannot be used, and the 811 | full "{ edges { node } }" version should be used instead. 812 | """ 813 | people: [Person] 814 | } 815 | 816 | """An edge in a connection.""" 817 | type SpeciesPeopleEdge { 818 | """The item at the end of the edge""" 819 | node: Person 820 | 821 | """A cursor for use in pagination""" 822 | cursor: String! 823 | } 824 | 825 | """A single transport craft that has hyperdrive capability.""" 826 | type Starship implements Node { 827 | """The name of this starship. The common name, such as "Death Star".""" 828 | name: String 829 | 830 | """ 831 | The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 832 | Orbital Battle Station". 833 | """ 834 | model: String 835 | 836 | """ 837 | The class of this starship, such as "Starfighter" or "Deep Space Mobile 838 | Battlestation" 839 | """ 840 | starshipClass: String 841 | 842 | """The manufacturers of this starship.""" 843 | manufacturers: [String] 844 | 845 | """The cost of this starship new, in galactic credits.""" 846 | costInCredits: Float 847 | 848 | """The length of this starship in meters.""" 849 | length: Float 850 | 851 | """The number of personnel needed to run or pilot this starship.""" 852 | crew: String 853 | 854 | """The number of non-essential people this starship can transport.""" 855 | passengers: String 856 | 857 | """ 858 | The maximum speed of this starship in atmosphere. null if this starship is 859 | incapable of atmosphering flight. 860 | """ 861 | maxAtmospheringSpeed: Int 862 | 863 | """The class of this starships hyperdrive.""" 864 | hyperdriveRating: Float 865 | 866 | """ 867 | The Maximum number of Megalights this starship can travel in a standard hour. 868 | A "Megalight" is a standard unit of distance and has never been defined before 869 | within the Star Wars universe. This figure is only really useful for measuring 870 | the difference in speed of starships. We can assume it is similar to AU, the 871 | distance between our Sun (Sol) and Earth. 872 | """ 873 | MGLT: Int 874 | 875 | """The maximum number of kilograms that this starship can transport.""" 876 | cargoCapacity: Float 877 | 878 | """ 879 | The maximum length of time that this starship can provide consumables for its 880 | entire crew without having to resupply. 881 | """ 882 | consumables: String 883 | pilotConnection(after: String, first: Int, before: String, last: Int): StarshipPilotsConnection 884 | filmConnection(after: String, first: Int, before: String, last: Int): StarshipFilmsConnection 885 | 886 | """The ISO 8601 date format of the time that this resource was created.""" 887 | created: String 888 | 889 | """The ISO 8601 date format of the time that this resource was edited.""" 890 | edited: String 891 | 892 | """The ID of an object""" 893 | id: ID! 894 | } 895 | 896 | """A connection to a list of items.""" 897 | type StarshipFilmsConnection { 898 | """Information to aid in pagination.""" 899 | pageInfo: PageInfo! 900 | 901 | """A list of edges.""" 902 | edges: [StarshipFilmsEdge] 903 | 904 | """ 905 | A count of the total number of objects in this connection, ignoring pagination. 906 | This allows a client to fetch the first five objects by passing "5" as the 907 | argument to "first", then fetch the total count so it could display "5 of 83", 908 | for example. 909 | """ 910 | totalCount: Int 911 | 912 | """ 913 | A list of all of the objects returned in the connection. This is a convenience 914 | field provided for quickly exploring the API; rather than querying for 915 | "{ edges { node } }" when no edge data is needed, this field can be be used 916 | instead. Note that when clients like Relay need to fetch the "cursor" field on 917 | the edge to enable efficient pagination, this shortcut cannot be used, and the 918 | full "{ edges { node } }" version should be used instead. 919 | """ 920 | films: [Film] 921 | } 922 | 923 | """An edge in a connection.""" 924 | type StarshipFilmsEdge { 925 | """The item at the end of the edge""" 926 | node: Film 927 | 928 | """A cursor for use in pagination""" 929 | cursor: String! 930 | } 931 | 932 | """A connection to a list of items.""" 933 | type StarshipPilotsConnection { 934 | """Information to aid in pagination.""" 935 | pageInfo: PageInfo! 936 | 937 | """A list of edges.""" 938 | edges: [StarshipPilotsEdge] 939 | 940 | """ 941 | A count of the total number of objects in this connection, ignoring pagination. 942 | This allows a client to fetch the first five objects by passing "5" as the 943 | argument to "first", then fetch the total count so it could display "5 of 83", 944 | for example. 945 | """ 946 | totalCount: Int 947 | 948 | """ 949 | A list of all of the objects returned in the connection. This is a convenience 950 | field provided for quickly exploring the API; rather than querying for 951 | "{ edges { node } }" when no edge data is needed, this field can be be used 952 | instead. Note that when clients like Relay need to fetch the "cursor" field on 953 | the edge to enable efficient pagination, this shortcut cannot be used, and the 954 | full "{ edges { node } }" version should be used instead. 955 | """ 956 | pilots: [Person] 957 | } 958 | 959 | """An edge in a connection.""" 960 | type StarshipPilotsEdge { 961 | """The item at the end of the edge""" 962 | node: Person 963 | 964 | """A cursor for use in pagination""" 965 | cursor: String! 966 | } 967 | 968 | """A connection to a list of items.""" 969 | type StarshipsConnection { 970 | """Information to aid in pagination.""" 971 | pageInfo: PageInfo! 972 | 973 | """A list of edges.""" 974 | edges: [StarshipsEdge] 975 | 976 | """ 977 | A count of the total number of objects in this connection, ignoring pagination. 978 | This allows a client to fetch the first five objects by passing "5" as the 979 | argument to "first", then fetch the total count so it could display "5 of 83", 980 | for example. 981 | """ 982 | totalCount: Int 983 | 984 | """ 985 | A list of all of the objects returned in the connection. This is a convenience 986 | field provided for quickly exploring the API; rather than querying for 987 | "{ edges { node } }" when no edge data is needed, this field can be be used 988 | instead. Note that when clients like Relay need to fetch the "cursor" field on 989 | the edge to enable efficient pagination, this shortcut cannot be used, and the 990 | full "{ edges { node } }" version should be used instead. 991 | """ 992 | starships: [Starship] 993 | } 994 | 995 | """An edge in a connection.""" 996 | type StarshipsEdge { 997 | """The item at the end of the edge""" 998 | node: Starship 999 | 1000 | """A cursor for use in pagination""" 1001 | cursor: String! 1002 | } 1003 | 1004 | """A single transport craft that does not have hyperdrive capability""" 1005 | type Vehicle implements Node { 1006 | """ 1007 | The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder 1008 | bike". 1009 | """ 1010 | name: String 1011 | 1012 | """ 1013 | The model or official name of this vehicle. Such as "All-Terrain Attack 1014 | Transport". 1015 | """ 1016 | model: String 1017 | 1018 | """The class of this vehicle, such as "Wheeled" or "Repulsorcraft".""" 1019 | vehicleClass: String 1020 | 1021 | """The manufacturers of this vehicle.""" 1022 | manufacturers: [String] 1023 | 1024 | """The cost of this vehicle new, in Galactic Credits.""" 1025 | costInCredits: Float 1026 | 1027 | """The length of this vehicle in meters.""" 1028 | length: Float 1029 | 1030 | """The number of personnel needed to run or pilot this vehicle.""" 1031 | crew: String 1032 | 1033 | """The number of non-essential people this vehicle can transport.""" 1034 | passengers: String 1035 | 1036 | """The maximum speed of this vehicle in atmosphere.""" 1037 | maxAtmospheringSpeed: Int 1038 | 1039 | """The maximum number of kilograms that this vehicle can transport.""" 1040 | cargoCapacity: Float 1041 | 1042 | """ 1043 | The maximum length of time that this vehicle can provide consumables for its 1044 | entire crew without having to resupply. 1045 | """ 1046 | consumables: String 1047 | pilotConnection(after: String, first: Int, before: String, last: Int): VehiclePilotsConnection 1048 | filmConnection(after: String, first: Int, before: String, last: Int): VehicleFilmsConnection 1049 | 1050 | """The ISO 8601 date format of the time that this resource was created.""" 1051 | created: String 1052 | 1053 | """The ISO 8601 date format of the time that this resource was edited.""" 1054 | edited: String 1055 | 1056 | """The ID of an object""" 1057 | id: ID! 1058 | } 1059 | 1060 | """A connection to a list of items.""" 1061 | type VehicleFilmsConnection { 1062 | """Information to aid in pagination.""" 1063 | pageInfo: PageInfo! 1064 | 1065 | """A list of edges.""" 1066 | edges: [VehicleFilmsEdge] 1067 | 1068 | """ 1069 | A count of the total number of objects in this connection, ignoring pagination. 1070 | This allows a client to fetch the first five objects by passing "5" as the 1071 | argument to "first", then fetch the total count so it could display "5 of 83", 1072 | for example. 1073 | """ 1074 | totalCount: Int 1075 | 1076 | """ 1077 | A list of all of the objects returned in the connection. This is a convenience 1078 | field provided for quickly exploring the API; rather than querying for 1079 | "{ edges { node } }" when no edge data is needed, this field can be be used 1080 | instead. Note that when clients like Relay need to fetch the "cursor" field on 1081 | the edge to enable efficient pagination, this shortcut cannot be used, and the 1082 | full "{ edges { node } }" version should be used instead. 1083 | """ 1084 | films: [Film] 1085 | } 1086 | 1087 | """An edge in a connection.""" 1088 | type VehicleFilmsEdge { 1089 | """The item at the end of the edge""" 1090 | node: Film 1091 | 1092 | """A cursor for use in pagination""" 1093 | cursor: String! 1094 | } 1095 | 1096 | """A connection to a list of items.""" 1097 | type VehiclePilotsConnection { 1098 | """Information to aid in pagination.""" 1099 | pageInfo: PageInfo! 1100 | 1101 | """A list of edges.""" 1102 | edges: [VehiclePilotsEdge] 1103 | 1104 | """ 1105 | A count of the total number of objects in this connection, ignoring pagination. 1106 | This allows a client to fetch the first five objects by passing "5" as the 1107 | argument to "first", then fetch the total count so it could display "5 of 83", 1108 | for example. 1109 | """ 1110 | totalCount: Int 1111 | 1112 | """ 1113 | A list of all of the objects returned in the connection. This is a convenience 1114 | field provided for quickly exploring the API; rather than querying for 1115 | "{ edges { node } }" when no edge data is needed, this field can be be used 1116 | instead. Note that when clients like Relay need to fetch the "cursor" field on 1117 | the edge to enable efficient pagination, this shortcut cannot be used, and the 1118 | full "{ edges { node } }" version should be used instead. 1119 | """ 1120 | pilots: [Person] 1121 | } 1122 | 1123 | """An edge in a connection.""" 1124 | type VehiclePilotsEdge { 1125 | """The item at the end of the edge""" 1126 | node: Person 1127 | 1128 | """A cursor for use in pagination""" 1129 | cursor: String! 1130 | } 1131 | 1132 | """A connection to a list of items.""" 1133 | type VehiclesConnection { 1134 | """Information to aid in pagination.""" 1135 | pageInfo: PageInfo! 1136 | 1137 | """A list of edges.""" 1138 | edges: [VehiclesEdge] 1139 | 1140 | """ 1141 | A count of the total number of objects in this connection, ignoring pagination. 1142 | This allows a client to fetch the first five objects by passing "5" as the 1143 | argument to "first", then fetch the total count so it could display "5 of 83", 1144 | for example. 1145 | """ 1146 | totalCount: Int 1147 | 1148 | """ 1149 | A list of all of the objects returned in the connection. This is a convenience 1150 | field provided for quickly exploring the API; rather than querying for 1151 | "{ edges { node } }" when no edge data is needed, this field can be be used 1152 | instead. Note that when clients like Relay need to fetch the "cursor" field on 1153 | the edge to enable efficient pagination, this shortcut cannot be used, and the 1154 | full "{ edges { node } }" version should be used instead. 1155 | """ 1156 | vehicles: [Vehicle] 1157 | } 1158 | 1159 | """An edge in a connection.""" 1160 | type VehiclesEdge { 1161 | """The item at the end of the edge""" 1162 | node: Vehicle 1163 | 1164 | """A cursor for use in pagination""" 1165 | cursor: String! 1166 | } 1167 | -------------------------------------------------------------------------------- /scripts/deploy-public: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2015-present, Facebook, Inc. 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the BSD-style license found in the 7 | # LICENSE file in the root directory of this source tree. An additional grant 8 | # of patent rights can be found in the PATENTS file in the same directory. 9 | 10 | set -e 11 | 12 | HEAD=`git rev-parse --short HEAD` 13 | PARENT=`git rev-parse gh-pages 2> /dev/null` || { 14 | echo 'No gh-pages branch found.' 15 | echo 'Fetch from upstream and then set up a local tracking branch:' 16 | echo 17 | echo ' git fetch origin' 18 | echo ' git branch --track gh-pages origin/gh-pages' 19 | echo 20 | exit 1 21 | } 22 | 23 | echo "Preparing index based on:" 24 | ls public/* 25 | git update-index --add public/* 26 | 27 | TREE=`git write-tree --prefix=public` 28 | echo "Wrote tree object: $TREE" 29 | 30 | echo "Resetting index to former state." 31 | git reset public 32 | 33 | COMMIT=`git commit-tree $TREE -p $PARENT -m "Build gh-pages from $HEAD"` 34 | echo "Wrote commit object: $COMMIT" 35 | 36 | ABBREV=`git rev-parse --short $COMMIT` 37 | echo "Updating gh-pages branch to point at $ABBREV." 38 | git update-ref refs/heads/gh-pages $COMMIT $PARENT 39 | 40 | echo 41 | echo "New build committed to gh-pages branch ($ABBREV)." 42 | echo 'Publish it with:' 43 | echo 44 | echo ' git push origin gh-pages:gh-pages' 45 | echo 46 | -------------------------------------------------------------------------------- /scripts/download.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { URL } from 'url'; 10 | import { Agent } from 'https'; 11 | import { existsSync, writeFileSync } from 'fs'; 12 | import fetch from 'isomorphic-fetch'; 13 | import { swapiPath } from '../src/schema/constants'; 14 | 15 | const resources = [ 16 | 'people', 17 | 'starships', 18 | 'vehicles', 19 | 'species', 20 | 'planets', 21 | 'films', 22 | ]; 23 | 24 | function replaceHttp(url) { 25 | let resultUrl = url; 26 | if (url.endsWith('/')) { 27 | resultUrl = url.slice(0, -1); 28 | } 29 | return ( 30 | resultUrl 31 | .replaceAll(/http:\/\//g, 'https://') 32 | // normalize irregularities in the swapi.tech API 33 | .replaceAll('https://swapi.tech', 'https://www.swapi.tech') 34 | ); 35 | } 36 | 37 | function normalizeUrl(url) { 38 | return replaceHttp(new URL(url).toString()); 39 | } 40 | 41 | /** 42 | * Iterate through the resources, fetch from the URL, convert the results into 43 | * objects, then generate and print the cache. 44 | */ 45 | async function cacheResources() { 46 | const agent = new Agent({ keepAlive: true }); 47 | const cache = {}; 48 | 49 | for (const name of resources) { 50 | let url = `${swapiPath}/${name}`; 51 | 52 | while (url != null) { 53 | const response = await fetch(url, { agent }); 54 | const text = await response.text(); 55 | 56 | const data = JSON.parse(replaceHttp(text)); 57 | 58 | cache[normalizeUrl(url)] = data; 59 | for (const obj of data.result || data.results || []) { 60 | const itemUrl = obj.url || obj.properties.url; 61 | cache[normalizeUrl(itemUrl)] = obj; 62 | } 63 | 64 | url = data.next ? data.next.replace('http:', 'https:') : null; 65 | } 66 | } 67 | 68 | return cache; 69 | } 70 | 71 | const outfile = process.argv[2]; 72 | if (!outfile) { 73 | console.error('Missing output file!'); 74 | process.exit(1); 75 | } 76 | 77 | if (!existsSync(outfile)) { 78 | console.log('Downloading cache...'); 79 | 80 | cacheResources() 81 | .then(cache => { 82 | const data = JSON.stringify(cache, null, 2); 83 | writeFileSync(outfile, data, 'utf-8'); 84 | console.log('Cached!'); 85 | }) 86 | .catch(function (err) { 87 | console.error(err); 88 | process.exit(1); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /scripts/mocha-bootload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2020, GraphQL Contributors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | */ 9 | process.env.NODE_ENV = 'test'; 10 | -------------------------------------------------------------------------------- /scripts/print-schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2020, GraphQL Contributors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import swapiSchema from '../src/schema'; 11 | import { printSchema } from 'graphql/utilities'; 12 | 13 | try { 14 | var output = printSchema(swapiSchema); 15 | console.log(output); 16 | } catch (error) { 17 | console.error(error); 18 | console.error(error.stack); 19 | } 20 | -------------------------------------------------------------------------------- /scripts/store-schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2020, GraphQL Contributors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import swapiSchema from '../src/schema'; 11 | import { printSchema } from 'graphql/utilities'; 12 | import { writeFileSync } from 'fs'; 13 | 14 | try { 15 | var output = printSchema(swapiSchema); 16 | writeFileSync('schema.graphql', output, 'utf8'); 17 | } catch (error) { 18 | console.error(error); 19 | console.error(error.stack); 20 | } 21 | -------------------------------------------------------------------------------- /src/api/README.md: -------------------------------------------------------------------------------- 1 | SWAPI Wrapper 2 | ============= 3 | 4 | The module exposes `getFromLocalUrl`, a method that takes a SWAPI URL and returns our local copy of the result. 5 | -------------------------------------------------------------------------------- /src/api/__tests__/local.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { getFromLocalUrl } from '../local'; 10 | 11 | describe('Local API Wrapper', () => { 12 | it('Gets a person', async () => { 13 | const luke = await getFromLocalUrl('https://swapi.tech/api/people/1/'); 14 | expect(luke.name).toBe('Luke Skywalker'); 15 | const threePO = await getFromLocalUrl('https://swapi.tech/api/people/2/'); 16 | expect(threePO.name).toBe('C-3PO'); 17 | }); 18 | 19 | it('Gets pages', async () => { 20 | const firstPeople = await getFromLocalUrl('https://swapi.tech/api/people/'); 21 | expect(firstPeople.results.length).toBe(10); 22 | expect(firstPeople.results[0].name).toBe('Luke Skywalker'); 23 | const secondPeople = await getFromLocalUrl( 24 | 'https://swapi.tech/api/people/?page=2', 25 | ); 26 | expect(secondPeople.results.length).toBe(10); 27 | expect(secondPeople.results[0].name).toBe('Anakin Skywalker'); 28 | }); 29 | 30 | it('Gets first page by default', async () => { 31 | const people = await getFromLocalUrl('https://swapi.tech/api/people/'); 32 | expect(people.results.length).toBe(10); 33 | expect(people.results[0].name).toBe('Luke Skywalker'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | export { getFromLocalUrl } from './local'; 12 | -------------------------------------------------------------------------------- /src/api/local.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import swapiData from '../../cache/data'; 12 | 13 | /** 14 | * Given a URL of an object in the SWAPI, return the data 15 | * from our local cache. 16 | */ 17 | export async function getFromLocalUrl( 18 | url: string, 19 | ): Promise<{ [key: string]: any }> { 20 | const text = swapiData[url]; 21 | if (!text) { 22 | throw new Error(`No entry in local cache for ${url}`); 23 | } 24 | if (process.env.NODE_ENV !== 'test') { 25 | // eslint-disable-next-line no-console 26 | console.log(`Hit the local cache for ${url}.`); 27 | } 28 | return text; 29 | } 30 | -------------------------------------------------------------------------------- /src/schema/README.md: -------------------------------------------------------------------------------- 1 | SWAPI GraphQL Schema 2 | ==================== 3 | 4 | This module exposes a `GraphQLSchema` that provides access to SWAPI. 5 | -------------------------------------------------------------------------------- /src/schema/__tests__/apiHelper.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { 10 | getObjectFromUrl, 11 | getObjectsByType, 12 | getObjectFromTypeAndId, 13 | } from '../apiHelper'; 14 | 15 | describe('API Helper', () => { 16 | it('Gets a person', async () => { 17 | const luke = await getObjectFromUrl('https://swapi.tech/api/people/1/'); 18 | expect(luke.name).toBe('Luke Skywalker'); 19 | const threePO = await getObjectFromUrl('https://swapi.tech/api/people/2/'); 20 | expect(threePO.name).toBe('C-3PO'); 21 | }); 22 | 23 | it('Gets all pages at once', async () => { 24 | const { objects, totalCount } = await getObjectsByType('people'); 25 | expect(objects.length).toBe(82); 26 | expect(totalCount).toBe(82); 27 | expect(objects[0].name).toBe('Luke Skywalker'); 28 | }); 29 | 30 | it('Gets a person by ID', async () => { 31 | const luke = await getObjectFromTypeAndId('people', 1); 32 | expect(luke.name).toBe('Luke Skywalker'); 33 | const threePO = await getObjectFromTypeAndId('people', 2); 34 | expect(threePO.name).toBe('C-3PO'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/schema/__tests__/film.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { swapi } from './swapi'; 10 | 11 | function getDocument(query) { 12 | return `${query} 13 | fragment AllFilmProperties on Film { 14 | director 15 | episodeID 16 | openingCrawl 17 | producers 18 | releaseDate 19 | title 20 | characterConnection(first:1) { edges { node { name } } } 21 | planetConnection(first:1) { edges { node { name } } } 22 | speciesConnection(first:1) { edges { node { name } } } 23 | starshipConnection(first:1) { edges { node { name } } } 24 | vehicleConnection(first:1) { edges { node { name } } } 25 | } 26 | `; 27 | } 28 | 29 | describe('Film type', () => { 30 | it('Gets an object by SWAPI ID', async () => { 31 | const query = '{ film(filmID: 1) { title } }'; 32 | const result = await swapi(query); 33 | expect(result.data.film.title).toBe('A New Hope'); 34 | }); 35 | 36 | it('Gets a different object by SWAPI ID', async () => { 37 | const query = '{ film(filmID: 2) { title } }'; 38 | const result = await swapi(query); 39 | expect(result.data.film.title).toBe('The Empire Strikes Back'); 40 | }); 41 | 42 | it('Gets an object by global ID', async () => { 43 | const query = '{ film(filmID: 1) { id, title } }'; 44 | const result = await swapi(query); 45 | const nextQuery = `{ film(id: "${result.data.film.id}") { id, title } }`; 46 | const nextResult = await swapi(nextQuery); 47 | expect(result.data.film.title).toBe('A New Hope'); 48 | expect(nextResult.data.film.title).toBe('A New Hope'); 49 | expect(result.data.film.id).toBe(nextResult.data.film.id); 50 | }); 51 | 52 | it('Gets an object by global ID with node', async () => { 53 | const query = '{ film(filmID: 1) { id, title } }'; 54 | const result = await swapi(query); 55 | const nextQuery = `{ 56 | node(id: "${result.data.film.id}") { 57 | ... on Film { 58 | id 59 | title 60 | } 61 | } 62 | }`; 63 | const nextResult = await swapi(nextQuery); 64 | expect(result.data.film.title).toBe('A New Hope'); 65 | expect(nextResult.data.node.title).toBe('A New Hope'); 66 | expect(result.data.film.id).toBe(nextResult.data.node.id); 67 | }); 68 | 69 | it('Gets all properties', async () => { 70 | const query = getDocument( 71 | `{ 72 | film(filmID: 1) { 73 | ...AllFilmProperties 74 | } 75 | }`, 76 | ); 77 | const result = await swapi(query); 78 | const expected = { 79 | title: 'A New Hope', 80 | episodeID: 4, 81 | openingCrawl: 82 | "It is a period of civil war.\r\nRebel spaceships, striking\r\nfrom a hidden base, have won\r\ntheir first victory against\r\nthe evil Galactic Empire.\r\n\r\nDuring the battle, Rebel\r\nspies managed to steal secret\r\nplans to the Empire's\r\nultimate weapon, the DEATH\r\nSTAR, an armored space\r\nstation with enough power\r\nto destroy an entire planet.\r\n\r\nPursued by the Empire's\r\nsinister agents, Princess\r\nLeia races home aboard her\r\nstarship, custodian of the\r\nstolen plans that can save her\r\npeople and restore\r\nfreedom to the galaxy....", 83 | director: 'George Lucas', 84 | producers: ['Gary Kurtz', 'Rick McCallum'], 85 | releaseDate: '1977-05-25', 86 | speciesConnection: { edges: [{ node: { name: 'Human' } }] }, 87 | starshipConnection: { edges: [{ node: { name: 'CR90 corvette' } }] }, 88 | vehicleConnection: { edges: [{ node: { name: 'Sand Crawler' } }] }, 89 | characterConnection: { edges: [{ node: { name: 'Luke Skywalker' } }] }, 90 | planetConnection: { edges: [{ node: { name: 'Tatooine' } }] }, 91 | }; 92 | expect(result.data.film).toMatchObject(expected); 93 | }); 94 | 95 | it('All objects query', async () => { 96 | const query = getDocument( 97 | '{ allFilms { edges { cursor, node { ...AllFilmProperties } } } }', 98 | ); 99 | const result = await swapi(query); 100 | expect(result.data.allFilms.edges.length).toBe(6); 101 | }); 102 | 103 | it('Pagination query', async () => { 104 | const query = `{ 105 | allFilms(first: 2) { edges { cursor, node { title } } } 106 | }`; 107 | const result = await swapi(query); 108 | expect(result.data.allFilms.edges.map(e => e.node.title)).toMatchObject([ 109 | 'A New Hope', 110 | 'The Empire Strikes Back', 111 | ]); 112 | const nextCursor = result.data.allFilms.edges[1].cursor; 113 | 114 | const nextQuery = `{ allFilms(first: 2, after:"${nextCursor}") { 115 | edges { cursor, node { title } } } 116 | }`; 117 | const nextResult = await swapi(nextQuery); 118 | expect(nextResult.data.allFilms.edges.map(e => e.node.title)).toMatchObject( 119 | ['Return of the Jedi', 'The Phantom Menace'], 120 | ); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/schema/__tests__/person.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { swapi } from './swapi'; 10 | 11 | function getDocument(query) { 12 | return `${query} 13 | fragment AllPersonProperties on Person { 14 | birthYear 15 | eyeColor 16 | gender 17 | hairColor 18 | height 19 | homeworld { name } 20 | mass 21 | name 22 | skinColor 23 | species { name } 24 | filmConnection(first:1) { edges { node { title } } } 25 | starshipConnection(first:1) { edges { node { name } } } 26 | vehicleConnection(first:1) { edges { node { name } } } 27 | } 28 | `; 29 | } 30 | 31 | describe('Person type', () => { 32 | it('Gets an object by SWAPI ID', async () => { 33 | const query = '{ person(personID: 1) { name } }'; 34 | const result = await swapi(query); 35 | expect(result.data.person.name).toBe('Luke Skywalker'); 36 | }); 37 | 38 | it('Gets a different object by SWAPI ID', async () => { 39 | const query = '{ person(personID: 2) { name } }'; 40 | const result = await swapi(query); 41 | expect(result.data.person.name).toBe('C-3PO'); 42 | }); 43 | 44 | it('Gets an object by global ID', async () => { 45 | const query = '{ person(personID: 1) { id, name } }'; 46 | const result = await swapi(query); 47 | const nextQuery = `{ person(id: "${result.data.person.id}") { id, name } }`; 48 | const nextResult = await swapi(nextQuery); 49 | expect(result.data.person.name).toBe('Luke Skywalker'); 50 | expect(nextResult.data.person.name).toBe('Luke Skywalker'); 51 | expect(result.data.person.id).toBe(nextResult.data.person.id); 52 | }); 53 | 54 | it('Gets an object by global ID with node', async () => { 55 | const query = '{ person(personID: 1) { id, name } }'; 56 | const result = await swapi(query); 57 | const nextQuery = `{ 58 | node(id: "${result.data.person.id}") { 59 | ... on Person { 60 | id 61 | name 62 | } 63 | } 64 | }`; 65 | const nextResult = await swapi(nextQuery); 66 | expect(result.data.person.name).toBe('Luke Skywalker'); 67 | expect(nextResult.data.node.name).toBe('Luke Skywalker'); 68 | expect(result.data.person.id).toBe(nextResult.data.node.id); 69 | }); 70 | 71 | it('Gets all properties', async () => { 72 | const query = getDocument( 73 | `{ 74 | person(personID: 1) { 75 | ...AllPersonProperties 76 | } 77 | }`, 78 | ); 79 | const result = await swapi(query); 80 | const expected = { 81 | name: 'Luke Skywalker', 82 | birthYear: '19BBY', 83 | eyeColor: 'blue', 84 | gender: 'male', 85 | hairColor: 'blond', 86 | height: 172, 87 | mass: 77, 88 | skinColor: 'fair', 89 | homeworld: { name: 'Tatooine' }, 90 | filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, 91 | species: null, 92 | starshipConnection: { edges: [{ node: { name: 'X-wing' } }] }, 93 | vehicleConnection: { edges: [{ node: { name: 'Snowspeeder' } }] }, 94 | }; 95 | expect(result.data.person).toMatchObject(expected); 96 | }); 97 | 98 | it('All objects query', async () => { 99 | const query = getDocument( 100 | '{ allPeople { edges { cursor, node { ...AllPersonProperties } } } }', 101 | ); 102 | const result = await swapi(query); 103 | expect(result.data.allPeople.edges.length).toBe(82); 104 | }); 105 | 106 | it('Pagination query', async () => { 107 | const query = `{ 108 | allPeople(first: 2) { edges { cursor, node { name } } } 109 | }`; 110 | const result = await swapi(query); 111 | expect(result.data.allPeople.edges.map(e => e.node.name)).toMatchObject([ 112 | 'Luke Skywalker', 113 | 'C-3PO', 114 | ]); 115 | const nextCursor = result.data.allPeople.edges[1].cursor; 116 | 117 | const nextQuery = `{ allPeople(first: 2, after:"${nextCursor}") { 118 | edges { cursor, node { name } } } 119 | }`; 120 | const nextResult = await swapi(nextQuery); 121 | expect(nextResult.data.allPeople.edges.map(e => e.node.name)).toMatchObject( 122 | ['R2-D2', 'Darth Vader'], 123 | ); 124 | }); 125 | 126 | describe('Edge cases', () => { 127 | it('Returns null if no species is set', async () => { 128 | const query = '{ person(personID: 42) { name, species { name } } }'; 129 | const result = await swapi(query); 130 | expect(result.data.person.name).toBe('Quarsh Panaka'); 131 | expect(result.data.person.species).toBe(null); 132 | }); 133 | 134 | it('Returns correctly if a species is set', async () => { 135 | const query = '{ person(personID: 67) { name, species { name } } }'; 136 | const result = await swapi(query); 137 | expect(result.data.person.name).toBe('Dooku'); 138 | expect(result.data.person.species.name).toBe('Human'); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/schema/__tests__/planet.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { swapi } from './swapi'; 10 | 11 | function getDocument(query) { 12 | return `${query} 13 | fragment AllPlanetProperties on Planet { 14 | climates 15 | diameter 16 | gravity 17 | name 18 | orbitalPeriod 19 | population 20 | rotationPeriod 21 | surfaceWater 22 | terrains 23 | filmConnection(first:1) { edges { node { title } } } 24 | residentConnection(first:1) { edges { node { name } } } 25 | } 26 | `; 27 | } 28 | 29 | describe('Planet type', () => { 30 | it('Gets an object by SWAPI ID', async () => { 31 | const query = '{ planet(planetID: 1) { name } }'; 32 | const result = await swapi(query); 33 | expect(result.data.planet.name).toBe('Tatooine'); 34 | }); 35 | 36 | it('Gets a different object by SWAPI ID', async () => { 37 | const query = '{ planet(planetID: 2) { name } }'; 38 | const result = await swapi(query); 39 | expect(result.data.planet.name).toBe('Alderaan'); 40 | }); 41 | 42 | it('Gets an object by global ID', async () => { 43 | const query = '{ planet(planetID: 1) { id, name } }'; 44 | const result = await swapi(query); 45 | const nextQuery = `{ planet(id: "${result.data.planet.id}") { id, name } }`; 46 | const nextResult = await swapi(nextQuery); 47 | expect(result.data.planet.name).toBe('Tatooine'); 48 | expect(nextResult.data.planet.name).toBe('Tatooine'); 49 | expect(result.data.planet.id).toBe(nextResult.data.planet.id); 50 | }); 51 | 52 | it('Gets an object by global ID with node', async () => { 53 | const query = '{ planet(planetID: 1) { id, name } }'; 54 | const result = await swapi(query); 55 | const nextQuery = `{ 56 | node(id: "${result.data.planet.id}") { 57 | ... on Planet { 58 | id 59 | name 60 | } 61 | } 62 | }`; 63 | const nextResult = await swapi(nextQuery); 64 | expect(result.data.planet.name).toBe('Tatooine'); 65 | expect(nextResult.data.node.name).toBe('Tatooine'); 66 | expect(result.data.planet.id).toBe(nextResult.data.node.id); 67 | }); 68 | 69 | it('Gets all properties', async () => { 70 | const query = getDocument( 71 | `{ 72 | planet(planetID: 1) { 73 | ...AllPlanetProperties 74 | } 75 | }`, 76 | ); 77 | const result = await swapi(query); 78 | const expected = { 79 | climates: ['arid'], 80 | diameter: 10465, 81 | filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, 82 | gravity: '1 standard', 83 | name: 'Tatooine', 84 | orbitalPeriod: 304, 85 | population: 200000, 86 | residentConnection: { edges: [{ node: { name: 'Luke Skywalker' } }] }, 87 | rotationPeriod: 23, 88 | surfaceWater: 1, 89 | terrains: ['desert'], 90 | }; 91 | expect(result.data.planet).toMatchObject(expected); 92 | }); 93 | 94 | it('All objects query', async () => { 95 | const query = getDocument( 96 | '{ allPlanets { edges { cursor, node { ...AllPlanetProperties } } } }', 97 | ); 98 | const result = await swapi(query); 99 | expect(result.data.allPlanets.edges.length).toBe(60); 100 | }); 101 | 102 | it('Pagination query', async () => { 103 | const query = `{ 104 | allPlanets(first: 2) { edges { cursor, node { name } } } 105 | }`; 106 | const result = await swapi(query); 107 | expect(result.data.allPlanets.edges.map(e => e.node.name)).toMatchObject([ 108 | 'Tatooine', 109 | 'Alderaan', 110 | ]); 111 | const nextCursor = result.data.allPlanets.edges[1].cursor; 112 | 113 | const nextQuery = `{ allPlanets(first: 2, after:"${nextCursor}") { 114 | edges { cursor, node { name } } } 115 | }`; 116 | const nextResult = await swapi(nextQuery); 117 | expect( 118 | nextResult.data.allPlanets.edges.map(e => e.node.name), 119 | ).toMatchObject(['Yavin IV', 'Hoth']); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /src/schema/__tests__/schema.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import swapiSchema from '../'; 10 | import { graphql } from 'graphql'; 11 | 12 | describe('Schema', () => { 13 | it('Gets an error when ID is omitted', async () => { 14 | const query = '{ species { name } }'; 15 | const result = await graphql(swapiSchema, query); 16 | expect(result.errors.length).toBe(1); 17 | expect(result.errors[0].message).toBe('must provide id or speciesID'); 18 | expect(result.data).toMatchObject({ species: null }); 19 | }); 20 | 21 | it('Gets an error when global ID is invalid', async () => { 22 | const query = '{ species(id: "notanid") { name } }'; 23 | const result = await graphql(swapiSchema, query); 24 | expect(result.errors.length).toBe(1); 25 | expect(result.errors[0].message).toEqual( 26 | expect.stringContaining('No entry in local cache for'), 27 | ); 28 | expect(result.data).toMatchObject({ species: null }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/schema/__tests__/species.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { swapi } from './swapi'; 10 | 11 | function getDocument(query) { 12 | return `${query} 13 | fragment AllSpeciesProperties on Species { 14 | averageHeight 15 | averageLifespan 16 | classification 17 | designation 18 | eyeColors 19 | hairColors 20 | homeworld { name } 21 | language 22 | name 23 | skinColors 24 | filmConnection(first:1) { edges { node { title } } } 25 | personConnection(first:1) { edges { node { name } } } 26 | } 27 | `; 28 | } 29 | 30 | describe('Species type', () => { 31 | it('Gets an object by SWAPI ID', async () => { 32 | const query = '{ species(speciesID: 4) { name } }'; 33 | const result = await swapi(query); 34 | expect(result.data.species.name).toBe('Rodian'); 35 | }); 36 | 37 | it('Gets a different object by SWAPI ID', async () => { 38 | const query = '{ species(speciesID: 6) { name } }'; 39 | const result = await swapi(query); 40 | expect(result.data.species.name).toBe("Yoda's species"); 41 | }); 42 | 43 | it('Gets an object by global ID', async () => { 44 | const query = '{ species(speciesID: 4) { id, name } }'; 45 | const result = await swapi(query); 46 | const nextQuery = ` 47 | { species(id: "${result.data.species.id}") { id, name } } 48 | `; 49 | const nextResult = await swapi(nextQuery); 50 | expect(result.data.species.name).toBe('Rodian'); 51 | expect(nextResult.data.species.name).toBe('Rodian'); 52 | expect(result.data.species.id).toBe(nextResult.data.species.id); 53 | }); 54 | 55 | it('Gets an object by global ID with node', async () => { 56 | const query = '{ species(speciesID: 4) { id, name } }'; 57 | const result = await swapi(query); 58 | const nextQuery = `{ 59 | node(id: "${result.data.species.id}") { 60 | ... on Species { 61 | id 62 | name 63 | } 64 | } 65 | }`; 66 | const nextResult = await swapi(nextQuery); 67 | expect(result.data.species.name).toBe('Rodian'); 68 | expect(nextResult.data.node.name).toBe('Rodian'); 69 | expect(result.data.species.id).toBe(nextResult.data.node.id); 70 | }); 71 | 72 | it('Gets all properties', async () => { 73 | const query = getDocument( 74 | `{ 75 | species(speciesID: 4) { 76 | ...AllSpeciesProperties 77 | } 78 | }`, 79 | ); 80 | const result = await swapi(query); 81 | const expected = { 82 | averageHeight: 170, 83 | averageLifespan: null, 84 | classification: 'sentient', 85 | designation: 'reptilian', 86 | eyeColors: ['black'], 87 | hairColors: ['n/a'], 88 | homeworld: { name: 'Rodia' }, 89 | language: 'Galatic Basic', 90 | name: 'Rodian', 91 | personConnection: { edges: [{ node: { name: 'Greedo' } }] }, 92 | filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, 93 | skinColors: ['green', 'blue'], 94 | }; 95 | expect(result.data.species).toMatchObject(expected); 96 | }); 97 | 98 | it('All objects query', async () => { 99 | const query = getDocument( 100 | '{ allSpecies { edges { cursor, node { ...AllSpeciesProperties } } } }', 101 | ); 102 | const result = await swapi(query); 103 | expect(result.data.allSpecies.edges.length).toBe(37); 104 | }); 105 | 106 | it('Pagination query', async () => { 107 | const query = `{ 108 | allSpecies(first: 2) { edges { cursor, node { name } } } 109 | }`; 110 | const result = await swapi(query); 111 | expect(result.data.allSpecies.edges.map(e => e.node.name)).toMatchObject([ 112 | 'Human', 113 | 'Droid', 114 | ]); 115 | const nextCursor = result.data.allSpecies.edges[1].cursor; 116 | 117 | const nextQuery = `{ allSpecies(first: 2, after:"${nextCursor}") { 118 | edges { cursor, node { name } } } 119 | }`; 120 | const nextResult = await swapi(nextQuery); 121 | expect( 122 | nextResult.data.allSpecies.edges.map(e => e.node.name), 123 | ).toMatchObject(['Wookie', 'Rodian']); 124 | }); 125 | 126 | describe('Edge cases', () => { 127 | it('Returns empty array for hair colors listed as none', async () => { 128 | const query = ` 129 | { 130 | species(speciesID: 34) { 131 | name 132 | hairColors 133 | } 134 | }`; 135 | const result = await swapi(query); 136 | expect(result.data.species.name).toBe('Muun'); 137 | expect(result.data.species.hairColors).toMatchObject([]); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /src/schema/__tests__/starship.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { swapi } from './swapi'; 10 | 11 | function getDocument(query) { 12 | return `${query} 13 | fragment AllStarshipProperties on Starship { 14 | MGLT 15 | cargoCapacity 16 | consumables 17 | costInCredits 18 | crew 19 | hyperdriveRating 20 | length 21 | manufacturers 22 | maxAtmospheringSpeed 23 | model 24 | name 25 | passengers 26 | starshipClass 27 | filmConnection(first:1) { edges { node { title } } } 28 | pilotConnection(first:1) { edges { node { name } } } 29 | } 30 | `; 31 | } 32 | 33 | describe('Starship type', () => { 34 | it('Gets an object by SWAPI ID', async () => { 35 | const query = '{ starship(starshipID: 5) { name } }'; 36 | const result = await swapi(query); 37 | expect(result.data.starship.name).toBe('Sentinel-class landing craft'); 38 | }); 39 | 40 | it('Gets a different object by SWAPI ID', async () => { 41 | const query = '{ starship(starshipID: 9) { name } }'; 42 | const result = await swapi(query); 43 | expect(result.data.starship.name).toBe('Death Star'); 44 | }); 45 | 46 | it('Gets an object by global ID', async () => { 47 | const query = '{ starship(starshipID: 5) { id, name } }'; 48 | const result = await swapi(query); 49 | const nextQuery = ` 50 | { starship(id: "${result.data.starship.id}") { id, name } } 51 | `; 52 | const nextResult = await swapi(nextQuery); 53 | expect(result.data.starship.name).toBe('Sentinel-class landing craft'); 54 | expect(nextResult.data.starship.name).toBe('Sentinel-class landing craft'); 55 | expect(result.data.starship.id).toBe(nextResult.data.starship.id); 56 | }); 57 | 58 | it('Gets an object by global ID with node', async () => { 59 | const query = '{ starship(starshipID: 5) { id, name } }'; 60 | const result = await swapi(query); 61 | const nextQuery = `{ 62 | node(id: "${result.data.starship.id}") { 63 | ... on Starship { 64 | id 65 | name 66 | } 67 | } 68 | }`; 69 | const nextResult = await swapi(nextQuery); 70 | expect(result.data.starship.name).toBe('Sentinel-class landing craft'); 71 | expect(nextResult.data.node.name).toBe('Sentinel-class landing craft'); 72 | expect(result.data.starship.id).toBe(nextResult.data.node.id); 73 | }); 74 | 75 | it('Gets all properties', async () => { 76 | const query = getDocument( 77 | `{ 78 | starship(starshipID: 9) { 79 | ...AllStarshipProperties 80 | } 81 | }`, 82 | ); 83 | const result = await swapi(query); 84 | const expected = { 85 | MGLT: 10, 86 | cargoCapacity: 1000000000000, 87 | consumables: '3 years', 88 | costInCredits: 1000000000000, 89 | crew: '342,953', 90 | filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, 91 | hyperdriveRating: 4, 92 | length: 120000, 93 | manufacturers: [ 94 | 'Imperial Department of Military Research', 95 | 'Sienar Fleet Systems', 96 | ], 97 | maxAtmospheringSpeed: null, 98 | model: 'DS-1 Orbital Battle Station', 99 | name: 'Death Star', 100 | passengers: '843,342', 101 | pilotConnection: { edges: [] }, 102 | starshipClass: 'Deep Space Mobile Battlestation', 103 | }; 104 | expect(result.data.starship).toMatchObject(expected); 105 | }); 106 | 107 | it('All objects query', async () => { 108 | const query = getDocument( 109 | '{ allStarships { edges { cursor, node { ...AllStarshipProperties } } } }', 110 | ); 111 | const result = await swapi(query); 112 | expect(result.data.allStarships.edges.length).toBe(36); 113 | }); 114 | 115 | it('Pagination query', async () => { 116 | const query = `{ 117 | allStarships(first: 2) { edges { cursor, node { name } } } 118 | }`; 119 | const result = await swapi(query); 120 | expect(result.data.allStarships.edges.map(e => e.node.name)).toMatchObject([ 121 | 'CR90 corvette', 122 | 'Star Destroyer', 123 | ]); 124 | const nextCursor = result.data.allStarships.edges[1].cursor; 125 | 126 | const nextQuery = `{ allStarships(first: 2, after:"${nextCursor}") { 127 | edges { cursor, node { name } } } 128 | }`; 129 | const nextResult = await swapi(nextQuery); 130 | expect( 131 | nextResult.data.allStarships.edges.map(e => e.node.name), 132 | ).toMatchObject(['Sentinel-class landing craft', 'Death Star']); 133 | }); 134 | 135 | describe('Edge cases', () => { 136 | it('Returns real speed when set to not n/a', async () => { 137 | const query = 138 | '{ starship(starshipID: 5) { name, maxAtmospheringSpeed } }'; 139 | const result = await swapi(query); 140 | expect(result.data.starship.name).toBe('Sentinel-class landing craft'); 141 | expect(result.data.starship.maxAtmospheringSpeed).toBe(1000); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/schema/__tests__/swapi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import swapiSchema from '../'; 10 | import { graphql } from 'graphql'; 11 | 12 | export async function swapi(query) { 13 | const result = await graphql(swapiSchema, query); 14 | if (result.errors !== undefined) { 15 | throw new Error(JSON.stringify(result.errors, null, 2)); 16 | } 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /src/schema/__tests__/vehicle.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | */ 8 | 9 | import { swapi } from './swapi'; 10 | 11 | function getDocument(query) { 12 | return `${query} 13 | fragment AllVehicleProperties on Vehicle { 14 | cargoCapacity 15 | consumables 16 | costInCredits 17 | crew 18 | length 19 | manufacturers 20 | maxAtmospheringSpeed 21 | model 22 | name 23 | passengers 24 | vehicleClass 25 | filmConnection(first:1) { edges { node { title } } } 26 | pilotConnection(first:1) { edges { node { name } } } 27 | } 28 | `; 29 | } 30 | 31 | describe('Vehicle type', () => { 32 | it('Gets an object by SWAPI ID', async () => { 33 | const query = '{ vehicle(vehicleID: 4) { name } }'; 34 | const result = await swapi(query); 35 | expect(result.data.vehicle.name).toBe('Sand Crawler'); 36 | }); 37 | 38 | it('Gets a different object by SWAPI ID', async () => { 39 | const query = '{ vehicle(vehicleID: 6) { name } }'; 40 | const result = await swapi(query); 41 | expect(result.data.vehicle.name).toBe('T-16 skyhopper'); 42 | }); 43 | 44 | it('Gets an object by global ID', async () => { 45 | const query = '{ vehicle(vehicleID: 4) { id, name } }'; 46 | const result = await swapi(query); 47 | const nextQuery = ` 48 | { vehicle(id: "${result.data.vehicle.id}") { id, name } } 49 | `; 50 | const nextResult = await swapi(nextQuery); 51 | expect(result.data.vehicle.name).toBe('Sand Crawler'); 52 | expect(nextResult.data.vehicle.name).toBe('Sand Crawler'); 53 | expect(result.data.vehicle.id).toBe(nextResult.data.vehicle.id); 54 | }); 55 | 56 | it('Gets an object by global ID with node', async () => { 57 | const query = '{ vehicle(vehicleID: 4) { id, name } }'; 58 | const result = await swapi(query); 59 | const nextQuery = `{ 60 | node(id: "${result.data.vehicle.id}") { 61 | ... on Vehicle { 62 | id 63 | name 64 | } 65 | } 66 | }`; 67 | const nextResult = await swapi(nextQuery); 68 | expect(result.data.vehicle.name).toBe('Sand Crawler'); 69 | expect(nextResult.data.node.name).toBe('Sand Crawler'); 70 | expect(result.data.vehicle.id).toBe(nextResult.data.node.id); 71 | }); 72 | 73 | it('Gets all properties', async () => { 74 | const query = getDocument( 75 | `{ 76 | vehicle(vehicleID: 4) { 77 | ...AllVehicleProperties 78 | } 79 | }`, 80 | ); 81 | const result = await swapi(query); 82 | const expected = { 83 | cargoCapacity: 50000, 84 | consumables: '2 months', 85 | costInCredits: 150000, 86 | crew: '46', 87 | length: 36.8, 88 | manufacturers: ['Corellia Mining Corporation'], 89 | maxAtmospheringSpeed: 30, 90 | model: 'Digger Crawler', 91 | name: 'Sand Crawler', 92 | passengers: '30', 93 | pilotConnection: { edges: [] }, 94 | filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, 95 | vehicleClass: 'wheeled', 96 | }; 97 | expect(result.data.vehicle).toMatchObject(expected); 98 | }); 99 | 100 | it('All objects query', async () => { 101 | const query = getDocument( 102 | '{ allVehicles { edges { cursor, node { ...AllVehicleProperties } } } }', 103 | ); 104 | const result = await swapi(query); 105 | expect(result.data.allVehicles.edges.length).toBe(39); 106 | }); 107 | 108 | it('Pagination query', async () => { 109 | const query = `{ 110 | allVehicles(first: 2) { edges { cursor, node { name } } } 111 | }`; 112 | const result = await swapi(query); 113 | expect(result.data.allVehicles.edges.map(e => e.node.name)).toMatchObject([ 114 | 'Sand Crawler', 115 | 'T-16 skyhopper', 116 | ]); 117 | const nextCursor = result.data.allVehicles.edges[1].cursor; 118 | 119 | const nextQuery = `{ allVehicles(first: 2, after:"${nextCursor}") { 120 | edges { cursor, node { name } } } 121 | }`; 122 | const nextResult = await swapi(nextQuery); 123 | expect( 124 | nextResult.data.allVehicles.edges.map(e => e.node.name), 125 | ).toMatchObject(['X-34 landspeeder', 'TIE/LN starfighter']); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/schema/apiHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import DataLoader from 'dataloader'; 12 | 13 | import { getFromLocalUrl } from '../api'; 14 | import { swapiPath } from './constants'; 15 | 16 | const localUrlLoader = new DataLoader(urls => 17 | Promise.all(urls.map(getFromLocalUrl)), 18 | ); 19 | 20 | /** 21 | * Objects returned from SWAPI don't have an ID field, so add one. 22 | */ 23 | function objectWithId(obj: Object): Object { 24 | obj.id = parseInt(obj.url.split('/')[5], 10); 25 | return obj; 26 | } 27 | 28 | /** 29 | * Given an object URL, fetch it, append the ID to it, and return it. 30 | */ 31 | export async function getObjectFromUrl(url: ?string): Promise { 32 | if (!url) { 33 | return null; 34 | } 35 | const data = await localUrlLoader.load(url); 36 | // some objects have a 'properties' field, others simply have the data 37 | return objectWithId(data.properties || data); 38 | } 39 | 40 | /** 41 | * Given a type and ID, get the object with the ID. 42 | */ 43 | export async function getObjectFromTypeAndId( 44 | type: string, 45 | id: string, 46 | ): Promise { 47 | return await getObjectFromUrl(`${swapiPath}/${type}/${id}`); 48 | } 49 | 50 | type ObjectsByType = { 51 | objects: Array, 52 | totalCount: number, 53 | }; 54 | 55 | /** 56 | * Given a type, fetch all of the pages, and join the objects together 57 | */ 58 | export async function getObjectsByType(type: string): Promise { 59 | let objects = []; 60 | let nextUrl = `${swapiPath}/${type}`; 61 | while (nextUrl) { 62 | // eslint-disable-next-line no-await-in-loop 63 | const pageData = await localUrlLoader.load(nextUrl); 64 | const results = pageData.result || pageData.results || []; 65 | objects = objects.concat(results.map(item => objectWithId(item.properties || item))); 66 | nextUrl = pageData.next; 67 | } 68 | objects = sortObjectsById(objects); 69 | return { objects, totalCount: objects.length }; 70 | } 71 | 72 | export async function getObjectsFromUrls(urls: string[]): Promise { 73 | const array = await Promise.all(urls.map(getObjectFromUrl)); 74 | return sortObjectsById(array); 75 | } 76 | 77 | function sortObjectsById(array: { id: number }[]): Object[] { 78 | return array.sort((a, b) => a.id - b.id); 79 | } 80 | 81 | /** 82 | * Given a string, convert it to a number 83 | */ 84 | export function convertToNumber(value: string): ?number { 85 | if (value === undefined || value === null) { 86 | return null; 87 | } 88 | if (['unknown', 'n/a'].indexOf(value) !== -1) { 89 | return null; 90 | } 91 | // remove digit grouping 92 | const numberString = value.replace(/,/, ''); 93 | return Number(numberString); 94 | } 95 | -------------------------------------------------------------------------------- /src/schema/commonFields.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { GraphQLString } from 'graphql'; 12 | 13 | // These two fields appear on all types, so let's only write them once. 14 | export function createdField(): any { 15 | return { 16 | type: GraphQLString, 17 | description: 18 | 'The ISO 8601 date format of the time that this resource was created.', 19 | }; 20 | } 21 | 22 | export function editedField(): any { 23 | return { 24 | type: GraphQLString, 25 | description: 26 | 'The ISO 8601 date format of the time that this resource was edited.', 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/schema/connections.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | connectionFromArray, 13 | connectionArgs, 14 | connectionDefinitions, 15 | } from 'graphql-relay'; 16 | 17 | import { getObjectsFromUrls } from './apiHelper'; 18 | 19 | import { GraphQLInt, GraphQLList } from 'graphql'; 20 | 21 | import type { GraphQLObjectType, GraphQLFieldConfig } from 'graphql'; 22 | 23 | /** 24 | * Constructs a GraphQL connection field config; it is assumed 25 | * that the object has a property named `prop`, and that property 26 | * contains a list of URLs. 27 | */ 28 | export function connectionFromUrls( 29 | name: string, 30 | prop: string, 31 | type: GraphQLObjectType, 32 | ): GraphQLFieldConfig<*, *> { 33 | const { connectionType } = connectionDefinitions({ 34 | name, 35 | nodeType: type, 36 | resolveNode: edge => edge.node, 37 | connectionFields: () => ({ 38 | totalCount: { 39 | type: GraphQLInt, 40 | resolve: conn => conn.totalCount, 41 | description: `A count of the total number of objects in this connection, ignoring pagination. 42 | This allows a client to fetch the first five objects by passing "5" as the 43 | argument to "first", then fetch the total count so it could display "5 of 83", 44 | for example.`, 45 | }, 46 | [prop]: { 47 | type: new GraphQLList(type), 48 | resolve: conn => conn.edges.map(edge => edge.node), 49 | description: `A list of all of the objects returned in the connection. This is a convenience 50 | field provided for quickly exploring the API; rather than querying for 51 | "{ edges { node } }" when no edge data is needed, this field can be be used 52 | instead. Note that when clients like Relay need to fetch the "cursor" field on 53 | the edge to enable efficient pagination, this shortcut cannot be used, and the 54 | full "{ edges { node } }" version should be used instead.`, 55 | }, 56 | }), 57 | }); 58 | return { 59 | type: connectionType, 60 | args: connectionArgs, 61 | resolve: async (obj, args) => { 62 | const array = await getObjectsFromUrls(obj[prop] || []); 63 | return { 64 | ...connectionFromArray(array, args), 65 | totalCount: array.length, 66 | }; 67 | }, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/schema/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | swapiSourceDomain: 'https://www.swapi.tech', 3 | swapiBasePath: 'api', 4 | }; 5 | 6 | module.exports.swapiPath = `${module.exports.swapiSourceDomain}/${module.exports.swapiBasePath}`; 7 | -------------------------------------------------------------------------------- /src/schema/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | GraphQLID, 13 | GraphQLInt, 14 | GraphQLList, 15 | GraphQLObjectType, 16 | GraphQLSchema, 17 | } from 'graphql'; 18 | 19 | import { 20 | fromGlobalId, 21 | connectionFromArray, 22 | connectionArgs, 23 | connectionDefinitions, 24 | } from 'graphql-relay'; 25 | 26 | import { getObjectsByType, getObjectFromTypeAndId } from './apiHelper'; 27 | 28 | import { swapiTypeToGraphQLType, nodeField } from './relayNode'; 29 | 30 | /** 31 | * Creates a root field to get an object of a given type. 32 | * Accepts either `id`, the globally unique ID used in GraphQL, 33 | * or `idName`, the per-type ID used in SWAPI. 34 | */ 35 | function rootFieldByID(idName, swapiType) { 36 | const getter = id => getObjectFromTypeAndId(swapiType, id); 37 | const argDefs = {}; 38 | argDefs.id = { type: GraphQLID }; 39 | argDefs[idName] = { type: GraphQLID }; 40 | return { 41 | type: swapiTypeToGraphQLType(swapiType), 42 | args: argDefs, 43 | resolve: (_, args) => { 44 | if (args[idName] !== undefined && args[idName] !== null) { 45 | return getter(args[idName]); 46 | } 47 | 48 | if (args.id !== undefined && args.id !== null) { 49 | const globalId = fromGlobalId(args.id); 50 | if ( 51 | globalId.id === null || 52 | globalId.id === undefined || 53 | globalId.id === '' 54 | ) { 55 | throw new Error('No valid ID extracted from ' + args.id); 56 | } 57 | return getter(globalId.id); 58 | } 59 | throw new Error('must provide id or ' + idName); 60 | }, 61 | }; 62 | } 63 | 64 | /** 65 | * Creates a connection that will return all objects of the given 66 | * `swapiType`; the connection will be named using `name`. 67 | */ 68 | function rootConnection(name, swapiType) { 69 | const graphqlType = swapiTypeToGraphQLType(swapiType); 70 | const { connectionType } = connectionDefinitions({ 71 | name, 72 | nodeType: graphqlType, 73 | connectionFields: () => ({ 74 | totalCount: { 75 | type: GraphQLInt, 76 | resolve: conn => conn.totalCount, 77 | description: `A count of the total number of objects in this connection, ignoring pagination. 78 | This allows a client to fetch the first five objects by passing "5" as the 79 | argument to "first", then fetch the total count so it could display "5 of 83", 80 | for example.`, 81 | }, 82 | [swapiType]: { 83 | type: new GraphQLList(graphqlType), 84 | resolve: conn => conn.edges.map(edge => edge.node), 85 | description: `A list of all of the objects returned in the connection. This is a convenience 86 | field provided for quickly exploring the API; rather than querying for 87 | "{ edges { node } }" when no edge data is needed, this field can be be used 88 | instead. Note that when clients like Relay need to fetch the "cursor" field on 89 | the edge to enable efficient pagination, this shortcut cannot be used, and the 90 | full "{ edges { node } }" version should be used instead.`, 91 | }, 92 | }), 93 | }); 94 | return { 95 | type: connectionType, 96 | args: connectionArgs, 97 | resolve: async (_, args) => { 98 | const { objects, totalCount } = await getObjectsByType(swapiType); 99 | return { 100 | ...connectionFromArray(objects, args), 101 | totalCount, 102 | }; 103 | }, 104 | }; 105 | } 106 | 107 | /** 108 | * The GraphQL type equivalent of the Root resource 109 | */ 110 | const rootType = new GraphQLObjectType({ 111 | name: 'Root', 112 | fields: () => ({ 113 | allFilms: rootConnection('Films', 'films'), 114 | film: rootFieldByID('filmID', 'films'), 115 | allPeople: rootConnection('People', 'people'), 116 | person: rootFieldByID('personID', 'people'), 117 | allPlanets: rootConnection('Planets', 'planets'), 118 | planet: rootFieldByID('planetID', 'planets'), 119 | allSpecies: rootConnection('Species', 'species'), 120 | species: rootFieldByID('speciesID', 'species'), 121 | allStarships: rootConnection('Starships', 'starships'), 122 | starship: rootFieldByID('starshipID', 'starships'), 123 | allVehicles: rootConnection('Vehicles', 'vehicles'), 124 | vehicle: rootFieldByID('vehicleID', 'vehicles'), 125 | node: nodeField, 126 | }), 127 | }); 128 | 129 | export default new GraphQLSchema({ query: rootType }); 130 | -------------------------------------------------------------------------------- /src/schema/relayNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { getObjectFromTypeAndId } from './apiHelper'; 12 | 13 | import type { GraphQLObjectType } from 'graphql'; 14 | 15 | import { nodeDefinitions, fromGlobalId } from 'graphql-relay'; 16 | 17 | /** 18 | * Given a "type" in SWAPI, returns the corresponding GraphQL type. 19 | */ 20 | export function swapiTypeToGraphQLType(swapiType: string): GraphQLObjectType { 21 | const FilmType = require('./types/film').default; 22 | const PersonType = require('./types/person').default; 23 | const PlanetType = require('./types/planet').default; 24 | const SpeciesType = require('./types/species').default; 25 | const StarshipType = require('./types/starship').default; 26 | const VehicleType = require('./types/vehicle').default; 27 | 28 | switch (swapiType) { 29 | case 'films': 30 | return FilmType; 31 | case 'people': 32 | return PersonType; 33 | case 'planets': 34 | return PlanetType; 35 | case 'starships': 36 | return StarshipType; 37 | case 'vehicles': 38 | return VehicleType; 39 | case 'species': 40 | return SpeciesType; 41 | default: 42 | throw new Error('Unrecognized type `' + swapiType + '`.'); 43 | } 44 | } 45 | 46 | const { nodeInterface, nodeField } = nodeDefinitions( 47 | globalId => { 48 | const { type, id } = fromGlobalId(globalId); 49 | return getObjectFromTypeAndId(type, id); 50 | }, 51 | obj => { 52 | const parts = obj.url.split('/'); 53 | return swapiTypeToGraphQLType(parts[parts.length - 3]); 54 | }, 55 | ); 56 | 57 | export { nodeInterface, nodeField }; 58 | -------------------------------------------------------------------------------- /src/schema/types/film.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | GraphQLInt, 13 | GraphQLList, 14 | GraphQLObjectType, 15 | GraphQLString, 16 | } from 'graphql'; 17 | 18 | import { globalIdField } from 'graphql-relay'; 19 | 20 | import { nodeInterface } from '../relayNode'; 21 | import { createdField, editedField } from '../commonFields'; 22 | import { connectionFromUrls } from '../connections'; 23 | 24 | import PersonType from './person'; 25 | import PlanetType from './planet'; 26 | import SpeciesType from './species'; 27 | import StarshipType from './starship'; 28 | import VehicleType from './vehicle'; 29 | 30 | /** 31 | * The GraphQL type equivalent of the Film resource 32 | */ 33 | const FilmType = new GraphQLObjectType({ 34 | name: 'Film', 35 | description: 'A single film.', 36 | fields: () => ({ 37 | title: { 38 | type: GraphQLString, 39 | description: 'The title of this film.', 40 | }, 41 | episodeID: { 42 | type: GraphQLInt, 43 | resolve: film => film.episode_id, 44 | description: 'The episode number of this film.', 45 | }, 46 | openingCrawl: { 47 | type: GraphQLString, 48 | resolve: film => film.opening_crawl, 49 | description: 'The opening paragraphs at the beginning of this film.', 50 | }, 51 | director: { 52 | type: GraphQLString, 53 | description: 'The name of the director of this film.', 54 | }, 55 | producers: { 56 | type: new GraphQLList(GraphQLString), 57 | resolve: film => { 58 | return film.producer 59 | ? film.producer.split(',').map(s => s.trim()) 60 | : null; 61 | }, 62 | description: 'The name(s) of the producer(s) of this film.', 63 | }, 64 | releaseDate: { 65 | type: GraphQLString, 66 | resolve: film => film.release_date, 67 | description: 68 | 'The ISO 8601 date format of film release at original creator country.', 69 | }, 70 | speciesConnection: connectionFromUrls( 71 | 'FilmSpecies', 72 | 'species', 73 | SpeciesType, 74 | ), 75 | starshipConnection: connectionFromUrls( 76 | 'FilmStarships', 77 | 'starships', 78 | StarshipType, 79 | ), 80 | vehicleConnection: connectionFromUrls( 81 | 'FilmVehicles', 82 | 'vehicles', 83 | VehicleType, 84 | ), 85 | characterConnection: connectionFromUrls( 86 | 'FilmCharacters', 87 | 'characters', 88 | PersonType, 89 | ), 90 | planetConnection: connectionFromUrls('FilmPlanets', 'planets', PlanetType), 91 | created: createdField(), 92 | edited: editedField(), 93 | id: globalIdField('films'), 94 | }), 95 | interfaces: () => [nodeInterface], 96 | }); 97 | 98 | export default FilmType; 99 | -------------------------------------------------------------------------------- /src/schema/types/person.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | GraphQLFloat, 13 | GraphQLInt, 14 | GraphQLObjectType, 15 | GraphQLString, 16 | } from 'graphql'; 17 | 18 | import { globalIdField } from 'graphql-relay'; 19 | 20 | import { nodeInterface } from '../relayNode'; 21 | import { createdField, editedField } from '../commonFields'; 22 | import { connectionFromUrls } from '../connections'; 23 | import { getObjectFromUrl, convertToNumber } from '../apiHelper'; 24 | 25 | import FilmType from './film'; 26 | import PlanetType from './planet'; 27 | import SpeciesType from './species'; 28 | import StarshipType from './starship'; 29 | import VehicleType from './vehicle'; 30 | 31 | /** 32 | * The GraphQL type equivalent of the People resource 33 | */ 34 | const PersonType = new GraphQLObjectType({ 35 | name: 'Person', 36 | description: 37 | 'An individual person or character within the Star Wars universe.', 38 | fields: () => ({ 39 | name: { 40 | type: GraphQLString, 41 | description: 'The name of this person.', 42 | }, 43 | birthYear: { 44 | type: GraphQLString, 45 | resolve: person => person.birth_year, 46 | description: `The birth year of the person, using the in-universe standard of BBY or ABY - 47 | Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is 48 | a battle that occurs at the end of Star Wars episode IV: A New Hope.`, 49 | }, 50 | eyeColor: { 51 | type: GraphQLString, 52 | resolve: person => person.eye_color, 53 | description: `The eye color of this person. Will be "unknown" if not known or "n/a" if the 54 | person does not have an eye.`, 55 | }, 56 | gender: { 57 | type: GraphQLString, 58 | description: `The gender of this person. Either "Male", "Female" or "unknown", 59 | "n/a" if the person does not have a gender.`, 60 | }, 61 | hairColor: { 62 | type: GraphQLString, 63 | resolve: person => person.hair_color, 64 | description: `The hair color of this person. Will be "unknown" if not known or "n/a" if the 65 | person does not have hair.`, 66 | }, 67 | height: { 68 | type: GraphQLInt, 69 | resolve: person => convertToNumber(person.height), 70 | description: 'The height of the person in centimeters.', 71 | }, 72 | mass: { 73 | type: GraphQLFloat, 74 | resolve: person => convertToNumber(person.mass), 75 | description: 'The mass of the person in kilograms.', 76 | }, 77 | skinColor: { 78 | type: GraphQLString, 79 | resolve: person => person.skin_color, 80 | description: 'The skin color of this person.', 81 | }, 82 | homeworld: { 83 | type: PlanetType, 84 | resolve: person => 85 | person.homeworld ? getObjectFromUrl(person.homeworld) : null, 86 | description: 'A planet that this person was born on or inhabits.', 87 | }, 88 | filmConnection: connectionFromUrls('PersonFilms', 'films', FilmType), 89 | species: { 90 | type: SpeciesType, 91 | resolve: person => { 92 | if (!person.species || person.species.length === 0) { 93 | return null; 94 | } 95 | return getObjectFromUrl(person.species[0]); 96 | }, 97 | description: 98 | 'The species that this person belongs to, or null if unknown.', 99 | }, 100 | starshipConnection: connectionFromUrls( 101 | 'PersonStarships', 102 | 'starships', 103 | StarshipType, 104 | ), 105 | vehicleConnection: connectionFromUrls( 106 | 'PersonVehicles', 107 | 'vehicles', 108 | VehicleType, 109 | ), 110 | created: createdField(), 111 | edited: editedField(), 112 | id: globalIdField('people'), 113 | }), 114 | interfaces: () => [nodeInterface], 115 | }); 116 | 117 | export default PersonType; 118 | -------------------------------------------------------------------------------- /src/schema/types/planet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | GraphQLFloat, 13 | GraphQLInt, 14 | GraphQLList, 15 | GraphQLObjectType, 16 | GraphQLString, 17 | } from 'graphql'; 18 | 19 | import { globalIdField } from 'graphql-relay'; 20 | 21 | import { nodeInterface } from '../relayNode'; 22 | import { createdField, editedField } from '../commonFields'; 23 | import { connectionFromUrls } from '../connections'; 24 | import { convertToNumber } from '../apiHelper'; 25 | 26 | import FilmType from './film'; 27 | import PersonType from './person'; 28 | 29 | /** 30 | * The GraphQL type equivalent of the Planet resource 31 | */ 32 | const PlanetType = new GraphQLObjectType({ 33 | name: 'Planet', 34 | description: `A large mass, planet or planetoid in the Star Wars Universe, at the time of 35 | 0 ABY.`, 36 | fields: () => ({ 37 | name: { 38 | type: GraphQLString, 39 | description: 'The name of this planet.', 40 | }, 41 | diameter: { 42 | type: GraphQLInt, 43 | resolve: planet => convertToNumber(planet.diameter), 44 | description: 'The diameter of this planet in kilometers.', 45 | }, 46 | rotationPeriod: { 47 | type: GraphQLInt, 48 | resolve: planet => convertToNumber(planet.rotation_period), 49 | description: `The number of standard hours it takes for this planet to complete a single 50 | rotation on its axis.`, 51 | }, 52 | orbitalPeriod: { 53 | type: GraphQLInt, 54 | resolve: planet => convertToNumber(planet.orbital_period), 55 | description: `The number of standard days it takes for this planet to complete a single orbit 56 | of its local star.`, 57 | }, 58 | gravity: { 59 | type: GraphQLString, 60 | description: `A number denoting the gravity of this planet, where "1" is normal or 1 standard 61 | G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs.`, 62 | }, 63 | population: { 64 | type: GraphQLFloat, 65 | resolve: planet => convertToNumber(planet.population), 66 | description: 67 | 'The average population of sentient beings inhabiting this planet.', 68 | }, 69 | climates: { 70 | type: new GraphQLList(GraphQLString), 71 | resolve: planet => { 72 | return planet.climate 73 | ? planet.climate.split(',').map(s => s.trim()) 74 | : null; 75 | }, 76 | nullable: true, 77 | description: 'The climates of this planet.', 78 | }, 79 | terrains: { 80 | type: new GraphQLList(GraphQLString), 81 | resolve: planet => { 82 | return planet.terrain 83 | ? planet.terrain.split(',').map(s => s.trim()) 84 | : null; 85 | }, 86 | description: 'The terrains of this planet.', 87 | }, 88 | surfaceWater: { 89 | type: GraphQLFloat, 90 | resolve: planet => convertToNumber(planet.surface_water), 91 | description: `The percentage of the planet surface that is naturally occurring water or bodies 92 | of water.`, 93 | }, 94 | residentConnection: connectionFromUrls( 95 | 'PlanetResidents', 96 | 'residents', 97 | PersonType, 98 | ), 99 | filmConnection: connectionFromUrls('PlanetFilms', 'films', FilmType), 100 | created: createdField(), 101 | edited: editedField(), 102 | id: globalIdField('planets'), 103 | }), 104 | interfaces: () => [nodeInterface], 105 | }); 106 | export default PlanetType; 107 | -------------------------------------------------------------------------------- /src/schema/types/species.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | GraphQLFloat, 13 | GraphQLInt, 14 | GraphQLList, 15 | GraphQLObjectType, 16 | GraphQLString, 17 | } from 'graphql'; 18 | 19 | import { globalIdField } from 'graphql-relay'; 20 | 21 | import { nodeInterface } from '../relayNode'; 22 | import { createdField, editedField } from '../commonFields'; 23 | import { connectionFromUrls } from '../connections'; 24 | import { getObjectFromUrl, convertToNumber } from '../apiHelper'; 25 | 26 | import FilmType from './film'; 27 | import PersonType from './person'; 28 | import PlanetType from './planet'; 29 | 30 | /** 31 | * The GraphQL type equivalent of the Species resource 32 | */ 33 | const SpeciesType = new GraphQLObjectType({ 34 | name: 'Species', 35 | description: 'A type of person or character within the Star Wars Universe.', 36 | fields: () => ({ 37 | name: { 38 | type: GraphQLString, 39 | description: 'The name of this species.', 40 | }, 41 | classification: { 42 | type: GraphQLString, 43 | description: 44 | 'The classification of this species, such as "mammal" or "reptile".', 45 | }, 46 | designation: { 47 | type: GraphQLString, 48 | description: 'The designation of this species, such as "sentient".', 49 | }, 50 | averageHeight: { 51 | type: GraphQLFloat, 52 | resolve: species => convertToNumber(species.average_height), 53 | description: 'The average height of this species in centimeters.', 54 | }, 55 | averageLifespan: { 56 | type: GraphQLInt, 57 | resolve: species => convertToNumber(species.average_lifespan), 58 | description: 59 | 'The average lifespan of this species in years, null if unknown.', 60 | }, 61 | eyeColors: { 62 | type: new GraphQLList(GraphQLString), 63 | resolve: species => { 64 | return species.eye_colors 65 | ? species.eye_colors.split(',').map(s => s.trim()) 66 | : null; 67 | }, 68 | description: `Common eye colors for this species, null if this species does not typically 69 | have eyes.`, 70 | }, 71 | hairColors: { 72 | type: new GraphQLList(GraphQLString), 73 | resolve: species => { 74 | if (species.hair_colors === 'none') { 75 | return []; 76 | } 77 | return species.hair_colors 78 | ? species.hair_colors.split(',').map(s => s.trim()) 79 | : null; 80 | }, 81 | description: `Common hair colors for this species, null if this species does not typically 82 | have hair.`, 83 | }, 84 | skinColors: { 85 | type: new GraphQLList(GraphQLString), 86 | resolve: species => { 87 | return species.skin_colors 88 | ? species.skin_colors.split(',').map(s => s.trim()) 89 | : null; 90 | }, 91 | description: `Common skin colors for this species, null if this species does not typically 92 | have skin.`, 93 | }, 94 | language: { 95 | type: GraphQLString, 96 | description: 'The language commonly spoken by this species.', 97 | }, 98 | homeworld: { 99 | type: PlanetType, 100 | resolve: species => 101 | species.homeworld ? getObjectFromUrl(species.homeworld) : null, 102 | description: 'A planet that this species originates from.', 103 | }, 104 | personConnection: connectionFromUrls('SpeciesPeople', 'people', PersonType), 105 | filmConnection: connectionFromUrls('SpeciesFilms', 'films', FilmType), 106 | created: createdField(), 107 | edited: editedField(), 108 | id: globalIdField('species'), 109 | }), 110 | interfaces: () => [nodeInterface], 111 | }); 112 | 113 | export default SpeciesType; 114 | -------------------------------------------------------------------------------- /src/schema/types/starship.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | GraphQLFloat, 13 | GraphQLInt, 14 | GraphQLList, 15 | GraphQLObjectType, 16 | GraphQLString, 17 | } from 'graphql'; 18 | 19 | import { globalIdField } from 'graphql-relay'; 20 | 21 | import { nodeInterface } from '../relayNode'; 22 | import { createdField, editedField } from '../commonFields'; 23 | import { connectionFromUrls } from '../connections'; 24 | import { convertToNumber } from '../apiHelper'; 25 | 26 | import FilmType from './film'; 27 | import PersonType from './person'; 28 | 29 | /** 30 | * The GraphQL type equivalent of the Starship resource 31 | */ 32 | const StarshipType = new GraphQLObjectType({ 33 | name: 'Starship', 34 | description: 'A single transport craft that has hyperdrive capability.', 35 | fields: () => ({ 36 | name: { 37 | type: GraphQLString, 38 | description: 39 | 'The name of this starship. The common name, such as "Death Star".', 40 | }, 41 | model: { 42 | type: GraphQLString, 43 | description: `The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 44 | Orbital Battle Station".`, 45 | }, 46 | starshipClass: { 47 | type: GraphQLString, 48 | resolve: ship => ship.starship_class, 49 | description: `The class of this starship, such as "Starfighter" or "Deep Space Mobile 50 | Battlestation"`, 51 | }, 52 | manufacturers: { 53 | type: new GraphQLList(GraphQLString), 54 | resolve: ship => { 55 | return ship.manufacturer 56 | ? ship.manufacturer.split(',').map(s => s.trim()) 57 | : null; 58 | }, 59 | description: 'The manufacturers of this starship.', 60 | }, 61 | costInCredits: { 62 | type: GraphQLFloat, 63 | resolve: ship => convertToNumber(ship.cost_in_credits), 64 | description: 'The cost of this starship new, in galactic credits.', 65 | }, 66 | length: { 67 | type: GraphQLFloat, 68 | resolve: ship => convertToNumber(ship.length), 69 | description: 'The length of this starship in meters.', 70 | }, 71 | crew: { 72 | type: GraphQLString, 73 | description: 74 | 'The number of personnel needed to run or pilot this starship.', 75 | }, 76 | passengers: { 77 | type: GraphQLString, 78 | description: 79 | 'The number of non-essential people this starship can transport.', 80 | }, 81 | maxAtmospheringSpeed: { 82 | type: GraphQLInt, 83 | resolve: ship => convertToNumber(ship.max_atmosphering_speed), 84 | description: `The maximum speed of this starship in atmosphere. null if this starship is 85 | incapable of atmosphering flight.`, 86 | }, 87 | hyperdriveRating: { 88 | type: GraphQLFloat, 89 | resolve: ship => convertToNumber(ship.hyperdrive_rating), 90 | description: 'The class of this starships hyperdrive.', 91 | }, 92 | MGLT: { 93 | type: GraphQLInt, 94 | resolve: ship => convertToNumber(ship.MGLT), 95 | description: `The Maximum number of Megalights this starship can travel in a standard hour. 96 | A "Megalight" is a standard unit of distance and has never been defined before 97 | within the Star Wars universe. This figure is only really useful for measuring 98 | the difference in speed of starships. We can assume it is similar to AU, the 99 | distance between our Sun (Sol) and Earth.`, 100 | }, 101 | cargoCapacity: { 102 | type: GraphQLFloat, 103 | resolve: ship => convertToNumber(ship.cargo_capacity), 104 | description: 105 | 'The maximum number of kilograms that this starship can transport.', 106 | }, 107 | consumables: { 108 | type: GraphQLString, 109 | description: `The maximum length of time that this starship can provide consumables for its 110 | entire crew without having to resupply.`, 111 | }, 112 | pilotConnection: connectionFromUrls('StarshipPilots', 'pilots', PersonType), 113 | filmConnection: connectionFromUrls('StarshipFilms', 'films', FilmType), 114 | created: createdField(), 115 | edited: editedField(), 116 | id: globalIdField('starships'), 117 | }), 118 | interfaces: () => [nodeInterface], 119 | }); 120 | 121 | export default StarshipType; 122 | -------------------------------------------------------------------------------- /src/schema/types/vehicle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE-examples file in the root directory of this source tree. 7 | * 8 | * @flow strict 9 | */ 10 | 11 | import { 12 | GraphQLFloat, 13 | GraphQLInt, 14 | GraphQLList, 15 | GraphQLObjectType, 16 | GraphQLString, 17 | } from 'graphql'; 18 | 19 | import { globalIdField } from 'graphql-relay'; 20 | 21 | import { nodeInterface } from '../relayNode'; 22 | import { createdField, editedField } from '../commonFields'; 23 | import { connectionFromUrls } from '../connections'; 24 | import { convertToNumber } from '../apiHelper'; 25 | 26 | import FilmType from './film'; 27 | import PersonType from './person'; 28 | 29 | /** 30 | * The GraphQL type equivalent of the Vehicle resource 31 | */ 32 | const VehicleType = new GraphQLObjectType({ 33 | name: 'Vehicle', 34 | description: 35 | 'A single transport craft that does not have hyperdrive capability', 36 | fields: () => ({ 37 | name: { 38 | type: GraphQLString, 39 | description: `The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder 40 | bike".`, 41 | }, 42 | model: { 43 | type: GraphQLString, 44 | description: `The model or official name of this vehicle. Such as "All-Terrain Attack 45 | Transport".`, 46 | }, 47 | vehicleClass: { 48 | type: GraphQLString, 49 | resolve: vehicle => vehicle.vehicle_class, 50 | description: 51 | 'The class of this vehicle, such as "Wheeled" or "Repulsorcraft".', 52 | }, 53 | manufacturers: { 54 | type: new GraphQLList(GraphQLString), 55 | resolve: vehicle => { 56 | return vehicle.manufacturer 57 | ? vehicle.manufacturer.split(',').map(s => s.trim()) 58 | : null; 59 | }, 60 | description: 'The manufacturers of this vehicle.', 61 | }, 62 | costInCredits: { 63 | type: GraphQLFloat, 64 | resolve: vehicle => convertToNumber(vehicle.cost_in_credits), 65 | description: 'The cost of this vehicle new, in Galactic Credits.', 66 | }, 67 | length: { 68 | type: GraphQLFloat, 69 | resolve: vehicle => convertToNumber(vehicle.length), 70 | description: 'The length of this vehicle in meters.', 71 | }, 72 | crew: { 73 | type: GraphQLString, 74 | description: 75 | 'The number of personnel needed to run or pilot this vehicle.', 76 | }, 77 | passengers: { 78 | type: GraphQLString, 79 | description: 80 | 'The number of non-essential people this vehicle can transport.', 81 | }, 82 | maxAtmospheringSpeed: { 83 | type: GraphQLInt, 84 | resolve: vehicle => convertToNumber(vehicle.max_atmosphering_speed), 85 | description: 'The maximum speed of this vehicle in atmosphere.', 86 | }, 87 | cargoCapacity: { 88 | type: GraphQLFloat, 89 | resolve: ship => convertToNumber(ship.cargo_capacity), 90 | description: 91 | 'The maximum number of kilograms that this vehicle can transport.', 92 | }, 93 | consumables: { 94 | type: GraphQLString, 95 | description: `The maximum length of time that this vehicle can provide consumables for its 96 | entire crew without having to resupply.`, 97 | }, 98 | pilotConnection: connectionFromUrls('VehiclePilots', 'pilots', PersonType), 99 | filmConnection: connectionFromUrls('VehicleFilms', 'films', FilmType), 100 | created: createdField(), 101 | edited: editedField(), 102 | id: globalIdField('vehicles'), 103 | }), 104 | interfaces: () => [nodeInterface], 105 | }); 106 | 107 | export default VehicleType; 108 | --------------------------------------------------------------------------------