diff --git a/jest.config.js b/jest.config.js index 12f02d6..1910f74 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,16 +3,20 @@ const packageJSON = require('./package.json'); process.env.TZ = 'UTC'; module.exports = { - verbose: true, name: packageJSON.name, - displayName: packageJSON.name, + globals: { + 'ts-jest': { + disableSourceMapSupport: true, + }, + }, + verbose: true, transform: { - '\\.[jt]sx?$': 'babel-jest', + '^.+\\.tsx?$': 'ts-jest', }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], testURL: 'http://localhost', transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], - testMatch: ['**/*.(spec|test).js'], + testMatch: ['**/*.(spec|test).ts'], collectCoverage: true, coverageDirectory: './coverage/', }; diff --git a/package-lock.json b/package-lock.json index aba6439..6c12e61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2243,6 +2243,15 @@ "@types/node": "*" } }, + "@types/graphql-relay": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/graphql-relay/-/graphql-relay-0.6.0.tgz", + "integrity": "sha512-/o2GDW22mdggQ9PUSNMbYFJMrCMwwiX8uu1/xS1rbDEdcC6+OVBy9Uz5ePW2zLDzhOznh3Ozm5t09jHqaY8cuw==", + "dev": true, + "requires": { + "graphql": "^14.5.3 || ^15.0.0" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -2267,6 +2276,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "26.0.15", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz", + "integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, "@types/json-schema": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", @@ -3951,6 +3970,15 @@ "electron-to-chromium": "^1.3.47" } }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, "bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -8871,6 +8899,12 @@ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -8911,6 +8945,12 @@ "semver": "^5.6.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -12059,6 +12099,48 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "ts-jest": { + "version": "26.4.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.4.tgz", + "integrity": "sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg==", + "dev": true, + "requires": { + "@types/jest": "26.x", + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^26.1.0", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "dependencies": { + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", @@ -12131,6 +12213,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz", + "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==", + "dev": true + }, "uglify-js": { "version": "3.10.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.3.tgz", @@ -13449,6 +13537,12 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true } } } diff --git a/package.json b/package.json index 67d7418..d74040f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "postinstall": "npm run download && npm run build", "start": "node lib/server", "watch": "babel scripts/watch.js | node", - "test": "npm run lint && npm run check && npm run test:only", + "test": "npm run lint && npm run test:only", "test:only": "jest", "lint": "eslint src handler", "lint:fix": "eslint --fix src handler", @@ -56,6 +56,7 @@ }, "devDependencies": { "@babel/core": "^7.12.3", + "@types/graphql-relay": "^0.6.0", "babel-cli": "^6.26.0", "babel-core": "^6.26.3", "babel-eslint": "^10.0.3", @@ -77,6 +78,8 @@ "jest": "^26.6.3", "netlify-lambda": "^1.6.3", "prettier": "^1.18.2", - "sane": "^4.1.0" + "sane": "^4.1.0", + "ts-jest": "^26.4.4", + "typescript": "^4.1.2" } } diff --git a/src/api/__tests__/local.spec.js b/src/api/__tests__/local.spec.ts similarity index 100% rename from src/api/__tests__/local.spec.js rename to src/api/__tests__/local.spec.ts diff --git a/src/api/index.js b/src/api/index.ts similarity index 100% rename from src/api/index.js rename to src/api/index.ts diff --git a/src/api/local.js b/src/api/local.ts similarity index 62% rename from src/api/local.js rename to src/api/local.ts index 4351680..ff85a49 100644 --- a/src/api/local.js +++ b/src/api/local.ts @@ -8,16 +8,23 @@ * @flow strict */ -import swapiData from '../../cache/data'; +import swapiData from '../../cache/data.json'; /** * Given a URL of an object in the SWAPI, return the data * from our local cache. */ +// casting json file to a Record with Index. +const typedSwapiData = swapiData as typeof swapiData & { [key: string]: any } export async function getFromLocalUrl( - url: string, -): Promise<{ [key: string]: any }> { - const text = swapiData[url]; + url: unknown, +): Promise> { + + if (!(typeof url === 'string')) { + throw new Error('Url provided is not a string'); + } + + const text = typedSwapiData[url]; if (!text) { throw new Error(`No entry in local cache for ${url}`); } diff --git a/src/schema/__tests__/apiHelper.spec.ts b/src/schema/__tests__/apiHelper.spec.ts new file mode 100644 index 0000000..976b92d --- /dev/null +++ b/src/schema/__tests__/apiHelper.spec.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { + getObjectFromUrl, + getObjectsByType, + getObjectFromTypeAndId, +} from '../apiHelper'; + +describe('API Helper', () => { + it('Gets a person', async () => { + const luke = await getObjectFromUrl('https://swapi.dev/api/people/1/'); + expect(luke.name).toBe('Luke Skywalker'); + const threePO = await getObjectFromUrl('https://swapi.dev/api/people/2/'); + expect(threePO.name).toBe('C-3PO'); + }); + + it('Gets all pages at once', async () => { + const { objects, totalCount } = await getObjectsByType('people'); + expect(objects.length).toBe(82); + expect(totalCount).toBe(82); + expect(objects[0].name).toBe('Luke Skywalker'); + }); + + it('Gets a person by ID', async () => { + const luke = await getObjectFromTypeAndId('people', 1); + expect(luke.name).toBe('Luke Skywalker'); + const threePO = await getObjectFromTypeAndId('people', 2); + expect(threePO.name).toBe('C-3PO'); + }); +}); diff --git a/src/schema/__tests__/film.spec.ts b/src/schema/__tests__/film.spec.ts new file mode 100644 index 0000000..81faca5 --- /dev/null +++ b/src/schema/__tests__/film.spec.ts @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { swapi } from './swapi'; + +function getDocument(query: string): string { + return `${query} + fragment AllFilmProperties on Film { + director + episodeID + openingCrawl + producers + releaseDate + title + characterConnection(first:1) { edges { node { name } } } + planetConnection(first:1) { edges { node { name } } } + speciesConnection(first:1) { edges { node { name } } } + starshipConnection(first:1) { edges { node { name } } } + vehicleConnection(first:1) { edges { node { name } } } + } + `; +} + +describe('Film type', () => { + it('Gets an object by SWAPI ID', async () => { + const query = '{ film(filmID: 1) { title } }'; + const result = await swapi(query); + expect(result.data?.film.title).toBe('A New Hope'); + }); + + it('Gets a different object by SWAPI ID', async () => { + const query = '{ film(filmID: 2) { title } }'; + const result = await swapi(query); + expect(result.data?.film.title).toBe('The Empire Strikes Back'); + }); + + it('Gets an object by global ID', async () => { + const query = '{ film(filmID: 1) { id, title } }'; + const result = await swapi(query); + const nextQuery = `{ film(id: "${result.data?.film.id}") { id, title } }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.film.title).toBe('A New Hope'); + expect(nextResult.data?.film.title).toBe('A New Hope'); + expect(result.data?.film.id).toBe(nextResult.data?.film.id); + }); + + it('Gets an object by global ID with node', async () => { + const query = '{ film(filmID: 1) { id, title } }'; + const result = await swapi(query); + const nextQuery = `{ + node(id: "${result.data?.film.id}") { + ... on Film { + id + title + } + } + }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.film.title).toBe('A New Hope'); + expect(nextResult.data?.node.title).toBe('A New Hope'); + expect(result.data?.film.id).toBe(nextResult.data?.node.id); + }); + + it('Gets all properties', async () => { + const query = getDocument( + `{ + film(filmID: 1) { + ...AllFilmProperties + } + }`, + ); + const result = await swapi(query); + const expected = { + title: 'A New Hope', + episodeID: 4, + openingCrawl: + "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....", + director: 'George Lucas', + producers: ['Gary Kurtz', 'Rick McCallum'], + releaseDate: '1977-05-25', + speciesConnection: { edges: [{ node: { name: 'Human' } }] }, + starshipConnection: { edges: [{ node: { name: 'CR90 corvette' } }] }, + vehicleConnection: { edges: [{ node: { name: 'Sand Crawler' } }] }, + characterConnection: { edges: [{ node: { name: 'Luke Skywalker' } }] }, + planetConnection: { edges: [{ node: { name: 'Tatooine' } }] }, + }; + expect(result.data?.film).toMatchObject(expected); + }); + + it('All objects query', async () => { + const query = getDocument( + '{ allFilms { edges { cursor, node { ...AllFilmProperties } } } }', + ); + const result = await swapi(query); + expect(result.data?.allFilms.edges.length).toBe(6); + }); + + it('Pagination query', async () => { + const query = `{ + allFilms(first: 2) { edges { cursor, node { title } } } + }`; + const result = await swapi(query); + expect( + result.data?.allFilms.edges.map((e: any) => e.node.title), + ).toMatchObject(['A New Hope', 'The Empire Strikes Back']); + const nextCursor = result.data?.allFilms.edges[1].cursor; + const nextQuery = `{ allFilms(first: 2, after:"${nextCursor}") { + edges { cursor, node { title } } } + }`; + const nextResult = await swapi(nextQuery); + expect( + nextResult.data?.allFilms.edges.map((e: any) => e.node.title), + ).toMatchObject(['Return of the Jedi', 'The Phantom Menace']); + }); +}); diff --git a/src/schema/__tests__/person.spec.ts b/src/schema/__tests__/person.spec.ts new file mode 100644 index 0000000..2a79889 --- /dev/null +++ b/src/schema/__tests__/person.spec.ts @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { swapi } from './swapi'; + +function getDocument(query: string): string { + return `${query} + fragment AllPersonProperties on Person { + birthYear + eyeColor + gender + hairColor + height + homeworld { name } + mass + name + skinColor + species { name } + filmConnection(first:1) { edges { node { title } } } + starshipConnection(first:1) { edges { node { name } } } + vehicleConnection(first:1) { edges { node { name } } } + } + `; +} + +describe('Person type', () => { + it('Gets an object by SWAPI ID', async () => { + const query = '{ person(personID: 1) { name } }'; + const result = await swapi(query); + expect(result.data?.person.name).toBe('Luke Skywalker'); + }); + + it('Gets a different object by SWAPI ID', async () => { + const query = '{ person(personID: 2) { name } }'; + const result = await swapi(query); + expect(result.data?.person.name).toBe('C-3PO'); + }); + + it('Gets an object by global ID', async () => { + const query = '{ person(personID: 1) { id, name } }'; + const result = await swapi(query); + const nextQuery = `{ person(id: "${result.data?.person.id}") { id, name } }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.person.name).toBe('Luke Skywalker'); + expect(nextResult.data?.person.name).toBe('Luke Skywalker'); + expect(result.data?.person.id).toBe(nextResult.data?.person.id); + }); + + it('Gets an object by global ID with node', async () => { + const query = '{ person(personID: 1) { id, name } }'; + const result = await swapi(query); + const nextQuery = `{ + node(id: "${result.data?.person.id}") { + ... on Person { + id + name + } + } + }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.person.name).toBe('Luke Skywalker'); + expect(nextResult.data?.node.name).toBe('Luke Skywalker'); + expect(result.data?.person.id).toBe(nextResult.data?.node.id); + }); + + it('Gets all properties', async () => { + const query = getDocument( + `{ + person(personID: 1) { + ...AllPersonProperties + } + }`, + ); + const result = await swapi(query); + const expected = { + name: 'Luke Skywalker', + birthYear: '19BBY', + eyeColor: 'blue', + gender: 'male', + hairColor: 'blond', + height: 172, + mass: 77, + skinColor: 'fair', + homeworld: { name: 'Tatooine' }, + filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, + species: null, + starshipConnection: { edges: [{ node: { name: 'X-wing' } }] }, + vehicleConnection: { edges: [{ node: { name: 'Snowspeeder' } }] }, + }; + expect(result.data?.person).toMatchObject(expected); + }); + + it('All objects query', async () => { + const query = getDocument( + '{ allPeople { edges { cursor, node { ...AllPersonProperties } } } }', + ); + const result = await swapi(query); + expect(result.data?.allPeople.edges.length).toBe(82); + }); + + it('Pagination query', async () => { + const query = `{ + allPeople(first: 2) { edges { cursor, node { name } } } + }`; + const result = await swapi(query); + expect( + result.data?.allPeople.edges.map((e: any) => e.node.name), + ).toMatchObject(['Luke Skywalker', 'C-3PO']); + const nextCursor = result.data?.allPeople.edges[1].cursor; + const nextQuery = `{ allPeople(first: 2, after:"${nextCursor}") { + edges { cursor, node { name } } } + }`; + const nextResult = await swapi(nextQuery); + expect( + nextResult.data?.allPeople.edges.map((e: any) => e.node.name), + ).toMatchObject(['R2-D2', 'Darth Vader']); + }); + + describe('Edge cases', () => { + it('Returns null if no species is set', async () => { + const query = '{ person(personID: 42) { name, species { name } } }'; + const result = await swapi(query); + expect(result.data?.person.name).toBe('Quarsh Panaka'); + expect(result.data?.person.species).toBe(null); + }); + + it('Returns correctly if a species is set', async () => { + const query = '{ person(personID: 67) { name, species { name } } }'; + const result = await swapi(query); + expect(result.data?.person.name).toBe('Dooku'); + expect(result.data?.person.species.name).toBe('Human'); + }); + }); +}); diff --git a/src/schema/__tests__/planet.spec.ts b/src/schema/__tests__/planet.spec.ts new file mode 100644 index 0000000..f0993f5 --- /dev/null +++ b/src/schema/__tests__/planet.spec.ts @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { swapi } from './swapi'; + +function getDocument(query: string) { + return `${query} + fragment AllPlanetProperties on Planet { + climates + diameter + gravity + name + orbitalPeriod + population + rotationPeriod + surfaceWater + terrains + filmConnection(first:1) { edges { node { title } } } + residentConnection(first:1) { edges { node { name } } } + } + `; +} + +describe('Planet type', () => { + it('Gets an object by SWAPI ID', async () => { + const query = '{ planet(planetID: 1) { name } }'; + const result = await swapi(query); + expect(result.data?.planet.name).toBe('Tatooine'); + }); + + it('Gets a different object by SWAPI ID', async () => { + const query = '{ planet(planetID: 2) { name } }'; + const result = await swapi(query); + expect(result.data?.planet.name).toBe('Alderaan'); + }); + + it('Gets an object by global ID', async () => { + const query = '{ planet(planetID: 1) { id, name } }'; + const result = await swapi(query); + const nextQuery = `{ planet(id: "${result.data?.planet.id}") { id, name } }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.planet.name).toBe('Tatooine'); + expect(nextResult.data?.planet.name).toBe('Tatooine'); + expect(result.data?.planet.id).toBe(nextResult.data?.planet.id); + }); + + it('Gets an object by global ID with node', async () => { + const query = '{ planet(planetID: 1) { id, name } }'; + const result = await swapi(query); + const nextQuery = `{ + node(id: "${result.data?.planet.id}") { + ... on Planet { + id + name + } + } + }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.planet.name).toBe('Tatooine'); + expect(nextResult.data?.node.name).toBe('Tatooine'); + expect(result.data?.planet.id).toBe(nextResult.data?.node.id); + }); + + it('Gets all properties', async () => { + const query = getDocument( + `{ + planet(planetID: 1) { + ...AllPlanetProperties + } + }`, + ); + const result = await swapi(query); + const expected = { + climates: ['arid'], + diameter: 10465, + filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, + gravity: '1 standard', + name: 'Tatooine', + orbitalPeriod: 304, + population: 200000, + residentConnection: { edges: [{ node: { name: 'Luke Skywalker' } }] }, + rotationPeriod: 23, + surfaceWater: 1, + terrains: ['desert'], + }; + expect(result.data?.planet).toMatchObject(expected); + }); + + it('All objects query', async () => { + const query = getDocument( + '{ allPlanets { edges { cursor, node { ...AllPlanetProperties } } } }', + ); + debugger; + const result = await swapi(query); + expect(result.data?.allPlanets.edges.length).toBe(60); + }); + + it('Pagination query', async () => { + const query = `{ + allPlanets(first: 2) { edges { cursor, node { name } } } + }`; + const result = await swapi(query); + expect( + result.data?.allPlanets.edges.map( + (e: Record) => e.node.name, + ), + ).toMatchObject(['Tatooine', 'Alderaan']); + const nextCursor = result.data?.allPlanets.edges[1].cursor; + const nextQuery = `{ allPlanets(first: 2, after:"${nextCursor}") { + edges { cursor, node { name } } } + }`; + const nextResult = await swapi(nextQuery); + expect( + nextResult.data?.allPlanets.edges.map( + (e: Record) => e.node.name, + ), + ).toMatchObject(['Yavin IV', 'Hoth']); + }); +}); diff --git a/src/schema/__tests__/schema.spec.ts b/src/schema/__tests__/schema.spec.ts new file mode 100644 index 0000000..8c728da --- /dev/null +++ b/src/schema/__tests__/schema.spec.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import swapiSchema from '..'; +import { graphql } from 'graphql'; + +describe('Schema', () => { + it('Gets an error when ID is omitted', async () => { + const query = '{ species { name } }'; + const result = await graphql(swapiSchema, query); + expect(result?.errors?.length).toBe(1); + expect(result.errors && result.errors[0].message).toBe( + 'must provide id or speciesID', + ); + expect(result.data).toMatchObject({ species: null }); + }); + + it('Gets an error when global ID is invalid', async () => { + const query = '{ species(id: "notanid") { name } }'; + const result = await graphql(swapiSchema, query); + expect(result.errors?.length).toBe(1); + expect(result.errors && result.errors[0].message).toEqual( + expect.stringContaining('No entry in local cache for'), + ); + expect(result.data).toMatchObject({ species: null }); + }); +}); diff --git a/src/schema/__tests__/species.spec.ts b/src/schema/__tests__/species.spec.ts new file mode 100644 index 0000000..9ef1c5d --- /dev/null +++ b/src/schema/__tests__/species.spec.ts @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { swapi } from './swapi'; + +function getDocument(query: string): string { + return `${query} + fragment AllSpeciesProperties on Species { + averageHeight + averageLifespan + classification + designation + eyeColors + hairColors + homeworld { name } + language + name + skinColors + filmConnection(first:1) { edges { node { title } } } + personConnection(first:1) { edges { node { name } } } + } + `; +} + +describe('Species type', () => { + it('Gets an object by SWAPI ID', async () => { + const query = '{ species(speciesID: 4) { name } }'; + const result = await swapi(query); + expect(result.data?.species.name).toBe('Rodian'); + }); + + it('Gets a different object by SWAPI ID', async () => { + const query = '{ species(speciesID: 6) { name } }'; + const result = await swapi(query); + expect(result.data?.species.name).toBe("Yoda's species"); + }); + + it('Gets an object by global ID', async () => { + const query = '{ species(speciesID: 4) { id, name } }'; + const result = await swapi(query); + const nextQuery = ` + { species(id: "${result.data?.species.id}") { id, name } } + `; + const nextResult = await swapi(nextQuery); + expect(result.data?.species.name).toBe('Rodian'); + expect(nextResult.data?.species.name).toBe('Rodian'); + expect(result.data?.species.id).toBe(nextResult.data?.species.id); + }); + + it('Gets an object by global ID with node', async () => { + const query = '{ species(speciesID: 4) { id, name } }'; + const result = await swapi(query); + const nextQuery = `{ + node(id: "${result.data?.species.id}") { + ... on Species { + id + name + } + } + }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.species.name).toBe('Rodian'); + expect(nextResult.data?.node.name).toBe('Rodian'); + expect(result.data?.species.id).toBe(nextResult.data?.node.id); + }); + + it('Gets all properties', async () => { + const query = getDocument( + `{ + species(speciesID: 4) { + ...AllSpeciesProperties + } + }`, + ); + const result = await swapi(query); + const expected = { + averageHeight: 170, + averageLifespan: null, + classification: 'sentient', + designation: 'reptilian', + eyeColors: ['black'], + hairColors: ['n/a'], + homeworld: { name: 'Rodia' }, + language: 'Galatic Basic', + name: 'Rodian', + personConnection: { edges: [{ node: { name: 'Greedo' } }] }, + filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, + skinColors: ['green', 'blue'], + }; + expect(result.data?.species).toMatchObject(expected); + }); + + it('All objects query', async () => { + const query = getDocument( + '{ allSpecies { edges { cursor, node { ...AllSpeciesProperties } } } }', + ); + const result = await swapi(query); + expect(result.data?.allSpecies.edges.length).toBe(37); + }); + + it('Pagination query', async () => { + const query = `{ + allSpecies(first: 2) { edges { cursor, node { name } } } + }`; + const result = await swapi(query); + expect( + result.data?.allSpecies.edges.map((e: any) => e.node.name), + ).toMatchObject(['Human', 'Droid']); + const nextCursor = result.data?.allSpecies.edges[1].cursor; + const nextQuery = `{ allSpecies(first: 2, after:"${nextCursor}") { + edges { cursor, node { name } } } + }`; + const nextResult = await swapi(nextQuery); + expect( + nextResult.data?.allSpecies.edges.map((e: any) => e.node.name), + ).toMatchObject(['Wookie', 'Rodian']); + }); + + describe('Edge cases', () => { + it('Returns empty array for hair colors listed as none', async () => { + const query = ` + { + species(speciesID: 34) { + name + hairColors + } + }`; + const result = await swapi(query); + expect(result.data?.species.name).toBe('Muun'); + expect(result.data?.species.hairColors).toMatchObject([]); + }); + }); +}); diff --git a/src/schema/__tests__/starship.spec.ts b/src/schema/__tests__/starship.spec.ts new file mode 100644 index 0000000..88a8dfc --- /dev/null +++ b/src/schema/__tests__/starship.spec.ts @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { swapi } from './swapi'; + +function getDocument(query: string): string { + return `${query} + fragment AllStarshipProperties on Starship { + MGLT + cargoCapacity + consumables + costInCredits + crew + hyperdriveRating + length + manufacturers + maxAtmospheringSpeed + model + name + passengers + starshipClass + filmConnection(first:1) { edges { node { title } } } + pilotConnection(first:1) { edges { node { name } } } + } + `; +} + +describe('Starship type', () => { + it('Gets an object by SWAPI ID', async () => { + const query = '{ starship(starshipID: 5) { name } }'; + const result = await swapi(query); + expect(result.data?.starship.name).toBe('Sentinel-class landing craft'); + }); + + it('Gets a different object by SWAPI ID', async () => { + const query = '{ starship(starshipID: 9) { name } }'; + const result = await swapi(query); + expect(result.data?.starship.name).toBe('Death Star'); + }); + + it('Gets an object by global ID', async () => { + const query = '{ starship(starshipID: 5) { id, name } }'; + const result = await swapi(query); + const nextQuery = ` + { starship(id: "${result.data?.starship.id}") { id, name } } + `; + const nextResult = await swapi(nextQuery); + expect(result.data?.starship.name).toBe('Sentinel-class landing craft'); + expect(nextResult.data?.starship.name).toBe('Sentinel-class landing craft'); + expect(result.data?.starship.id).toBe(nextResult.data?.starship.id); + }); + + it('Gets an object by global ID with node', async () => { + const query = '{ starship(starshipID: 5) { id, name } }'; + const result = await swapi(query); + const nextQuery = `{ + node(id: "${result.data?.starship.id}") { + ... on Starship { + id + name + } + } + }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.starship.name).toBe('Sentinel-class landing craft'); + expect(nextResult.data?.node.name).toBe('Sentinel-class landing craft'); + expect(result.data?.starship.id).toBe(nextResult.data?.node.id); + }); + + it('Gets all properties', async () => { + const query = getDocument( + `{ + starship(starshipID: 9) { + ...AllStarshipProperties + } + }`, + ); + const result = await swapi(query); + const expected = { + MGLT: 10, + cargoCapacity: 1000000000000, + consumables: '3 years', + costInCredits: 1000000000000, + crew: '342,953', + filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, + hyperdriveRating: 4, + length: 120000, + manufacturers: [ + 'Imperial Department of Military Research', + 'Sienar Fleet Systems', + ], + maxAtmospheringSpeed: null, + model: 'DS-1 Orbital Battle Station', + name: 'Death Star', + passengers: '843,342', + pilotConnection: { edges: [] }, + starshipClass: 'Deep Space Mobile Battlestation', + }; + expect(result.data?.starship).toMatchObject(expected); + }); + + it('All objects query', async () => { + const query = getDocument( + '{ allStarships { edges { cursor, node { ...AllStarshipProperties } } } }', + ); + const result = await swapi(query); + expect(result.data?.allStarships.edges.length).toBe(36); + }); + + it('Pagination query', async () => { + const query = `{ + allStarships(first: 2) { edges { cursor, node { name } } } + }`; + const result = await swapi(query); + expect( + result.data?.allStarships.edges.map((e: any) => e.node.name), + ).toMatchObject(['CR90 corvette', 'Star Destroyer']); + const nextCursor = result.data?.allStarships.edges[1].cursor; + const nextQuery = `{ allStarships(first: 2, after:"${nextCursor}") { + edges { cursor, node { name } } } + }`; + const nextResult = await swapi(nextQuery); + expect( + nextResult.data?.allStarships.edges.map((e: any) => e.node.name), + + ).toMatchObject(['Sentinel-class landing craft', 'Death Star']); + }); + + describe('Edge cases', () => { + it('Returns real speed when set to not n/a', async () => { + const query = + '{ starship(starshipID: 5) { name, maxAtmospheringSpeed } }'; + const result = await swapi(query); + expect(result.data?.starship.name).toBe('Sentinel-class landing craft'); + expect(result.data?.starship.maxAtmospheringSpeed).toBe(1000); + }); + }); +}); diff --git a/src/schema/__tests__/swapi.ts b/src/schema/__tests__/swapi.ts new file mode 100644 index 0000000..250736f --- /dev/null +++ b/src/schema/__tests__/swapi.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import swapiSchema from '../index'; +import { graphql } from 'graphql'; + +export async function swapi(query: string) { + const result = await graphql(swapiSchema, query); + if (result.errors !== undefined) { + throw new Error(JSON.stringify(result.errors, null, 2)); + } + return result; +} diff --git a/src/schema/__tests__/vehicle.spec.ts b/src/schema/__tests__/vehicle.spec.ts new file mode 100644 index 0000000..a2c54e1 --- /dev/null +++ b/src/schema/__tests__/vehicle.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE-examples file in the root directory of this source tree. + */ + +import { swapi } from './swapi'; + +function getDocument(query: string): string { + return `${query} + fragment AllVehicleProperties on Vehicle { + cargoCapacity + consumables + costInCredits + crew + length + manufacturers + maxAtmospheringSpeed + model + name + passengers + vehicleClass + filmConnection(first:1) { edges { node { title } } } + pilotConnection(first:1) { edges { node { name } } } + } + `; +} + +describe('Vehicle type', () => { + it('Gets an object by SWAPI ID', async () => { + const query = '{ vehicle(vehicleID: 4) { name } }'; + const result = await swapi(query); + expect(result.data?.vehicle.name).toBe('Sand Crawler'); + }); + + it('Gets a different object by SWAPI ID', async () => { + const query = '{ vehicle(vehicleID: 6) { name } }'; + const result = await swapi(query); + expect(result.data?.vehicle.name).toBe('T-16 skyhopper'); + }); + + it('Gets an object by global ID', async () => { + const query = '{ vehicle(vehicleID: 4) { id, name } }'; + const result = await swapi(query); + const nextQuery = ` + { vehicle(id: "${result.data?.vehicle.id}") { id, name } } + `; + const nextResult = await swapi(nextQuery); + expect(result.data?.vehicle.name).toBe('Sand Crawler'); + expect(nextResult.data?.vehicle.name).toBe('Sand Crawler'); + expect(result.data?.vehicle.id).toBe(nextResult.data?.vehicle.id); + }); + + it('Gets an object by global ID with node', async () => { + const query = '{ vehicle(vehicleID: 4) { id, name } }'; + const result = await swapi(query); + const nextQuery = `{ + node(id: "${result.data?.vehicle.id}") { + ... on Vehicle { + id + name + } + } + }`; + const nextResult = await swapi(nextQuery); + expect(result.data?.vehicle.name).toBe('Sand Crawler'); + expect(nextResult.data?.node.name).toBe('Sand Crawler'); + expect(result.data?.vehicle.id).toBe(nextResult.data?.node.id); + }); + + it('Gets all properties', async () => { + const query = getDocument( + `{ + vehicle(vehicleID: 4) { + ...AllVehicleProperties + } + }`, + ); + const result = await swapi(query); + const expected = { + cargoCapacity: 50000, + consumables: '2 months', + costInCredits: 150000, + crew: '46', + length: 36.8, + manufacturers: ['Corellia Mining Corporation'], + maxAtmospheringSpeed: 30, + model: 'Digger Crawler', + name: 'Sand Crawler', + passengers: '30', + pilotConnection: { edges: [] }, + filmConnection: { edges: [{ node: { title: 'A New Hope' } }] }, + vehicleClass: 'wheeled', + }; + expect(result.data?.vehicle).toMatchObject(expected); + }); + + it('All objects query', async () => { + const query = getDocument( + '{ allVehicles { edges { cursor, node { ...AllVehicleProperties } } } }', + ); + const result = await swapi(query); + expect(result.data?.allVehicles.edges.length).toBe(39); + }); + + it('Pagination query', async () => { + const query = `{ + allVehicles(first: 2) { edges { cursor, node { name } } } + }`; + const result = await swapi(query); + expect( + result.data?.allVehicles.edges.map((e: any) => e.node.name), + ).toMatchObject(['Sand Crawler', 'T-16 skyhopper']); + const nextCursor = result.data?.allVehicles.edges[1].cursor; + + const nextQuery = `{ allVehicles(first: 2, after:"${nextCursor}") { + edges { cursor, node { name } } } + }`; + const nextResult = await swapi(nextQuery); + expect( + nextResult.data?.allVehicles.edges.map((e: any) => e.node.name), + ).toMatchObject(['X-34 landspeeder', 'TIE/LN starfighter']); + }); +}); diff --git a/src/schema/apiHelper.js b/src/schema/apiHelper.ts similarity index 75% rename from src/schema/apiHelper.js rename to src/schema/apiHelper.ts index dd44cca..dae24a9 100644 --- a/src/schema/apiHelper.js +++ b/src/schema/apiHelper.ts @@ -12,22 +12,23 @@ import DataLoader from 'dataloader'; import { getFromLocalUrl } from '../api'; -const localUrlLoader = new DataLoader(urls => - Promise.all(urls.map(getFromLocalUrl)), +type ObjectWithId = Record & { id: number }; +const localUrlLoader = new DataLoader>(urls => + Promise.all(urls.map((url) => getFromLocalUrl(url))), ); /** * Objects returned from SWAPI don't have an ID field, so add one. */ -function objectWithId(obj: Object): Object { - obj.id = parseInt(obj.url.split('/')[5], 10); - return obj; +function objectWithId(obj: Record): ObjectWithId { + const id = parseInt(obj.url.split('/')[5], 10); + return { ...obj, id }; } /** * Given an object URL, fetch it, append the ID to it, and return it. */ -export async function getObjectFromUrl(url: string): Promise { +export async function getObjectFromUrl(url: string): Promise { const data = await localUrlLoader.load(url); return objectWithId(data); } @@ -37,13 +38,13 @@ export async function getObjectFromUrl(url: string): Promise { */ export async function getObjectFromTypeAndId( type: string, - id: string, -): Promise { + id: number, +): Promise { return await getObjectFromUrl(`https://swapi.dev/api/${type}/${id}/`); } type ObjectsByType = { - objects: Array, + objects: Array, totalCount: number, }; @@ -51,7 +52,7 @@ type ObjectsByType = { * Given a type, fetch all of the pages, and join the objects together */ export async function getObjectsByType(type: string): Promise { - let objects = []; + let objects = [] as ObjectWithId[]; let nextUrl = `https://swapi.dev/api/${type}/`; while (nextUrl) { // eslint-disable-next-line no-await-in-loop @@ -63,19 +64,19 @@ export async function getObjectsByType(type: string): Promise { return { objects, totalCount: objects.length }; } -export async function getObjectsFromUrls(urls: string[]): Promise { +export async function getObjectsFromUrls(urls: string[]): Promise { const array = await Promise.all(urls.map(getObjectFromUrl)); return sortObjectsById(array); } -function sortObjectsById(array: { id: number }[]): Object[] { +function sortObjectsById(array: { id: number }[]): ObjectWithId[] { return array.sort((a, b) => a.id - b.id); } /** * Given a string, convert it to a number */ -export function convertToNumber(value: string): ?number { +export function convertToNumber(value: string): number | null { if (['unknown', 'n/a'].indexOf(value) !== -1) { return null; } diff --git a/src/schema/commonFields.js b/src/schema/commonFields.ts similarity index 97% rename from src/schema/commonFields.js rename to src/schema/commonFields.ts index ff7f1e5..44e1456 100644 --- a/src/schema/commonFields.js +++ b/src/schema/commonFields.ts @@ -5,7 +5,6 @@ * This source code is licensed under the license found in the * LICENSE-examples file in the root directory of this source tree. * - * @flow strict */ import { GraphQLString } from 'graphql'; diff --git a/src/schema/connections.js b/src/schema/connections.ts similarity index 94% rename from src/schema/connections.js rename to src/schema/connections.ts index f7340e5..87cbab6 100644 --- a/src/schema/connections.js +++ b/src/schema/connections.ts @@ -29,7 +29,7 @@ export function connectionFromUrls( name: string, prop: string, type: GraphQLObjectType, -): GraphQLFieldConfig<*, *> { +): GraphQLFieldConfig> { const { connectionType } = connectionDefinitions({ name, nodeType: type, @@ -45,7 +45,7 @@ for example.`, }, [prop]: { type: new GraphQLList(type), - resolve: conn => conn.edges.map(edge => edge.node), + resolve: conn => conn.edges.map((edge: any) => edge.node), description: `A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used diff --git a/src/schema/index.js b/src/schema/index.ts similarity index 98% rename from src/schema/index.js rename to src/schema/index.ts index 5f61c2c..c14bf3a 100644 --- a/src/schema/index.js +++ b/src/schema/index.ts @@ -34,7 +34,7 @@ import { swapiTypeToGraphQLType, nodeField } from './relayNode'; */ function rootFieldByID(idName, swapiType) { const getter = id => getObjectFromTypeAndId(swapiType, id); - const argDefs = {}; + const argDefs = {} as Record; argDefs.id = { type: GraphQLID }; argDefs[idName] = { type: GraphQLID }; return { diff --git a/src/schema/relayNode.js b/src/schema/relayNode.ts similarity index 96% rename from src/schema/relayNode.js rename to src/schema/relayNode.ts index dfa7ac6..48b3020 100644 --- a/src/schema/relayNode.js +++ b/src/schema/relayNode.ts @@ -5,7 +5,6 @@ * This source code is licensed under the license found in the * LICENSE-examples file in the root directory of this source tree. * - * @flow strict */ import { getObjectFromTypeAndId } from './apiHelper'; @@ -46,7 +45,7 @@ export function swapiTypeToGraphQLType(swapiType: string): GraphQLObjectType { const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const { type, id } = fromGlobalId(globalId); - return getObjectFromTypeAndId(type, id); + return getObjectFromTypeAndId(type, Number(id)); }, obj => { const parts = obj.url.split('/'); diff --git a/src/schema/types/film.js b/src/schema/types/film.ts similarity index 94% rename from src/schema/types/film.js rename to src/schema/types/film.ts index 01b0620..c042e84 100644 --- a/src/schema/types/film.js +++ b/src/schema/types/film.ts @@ -30,7 +30,7 @@ import VehicleType from './vehicle'; /** * The GraphQL type equivalent of the Film resource */ -const FilmType = new GraphQLObjectType({ +const FilmType: GraphQLObjectType = new GraphQLObjectType({ name: 'Film', description: 'A single film.', fields: () => ({ @@ -55,7 +55,7 @@ const FilmType = new GraphQLObjectType({ producers: { type: new GraphQLList(GraphQLString), resolve: film => { - return film.producer.split(',').map(s => s.trim()); + return film.producer.split(',').map((s: string) => s.trim()); }, description: 'The name(s) of the producer(s) of this film.', }, diff --git a/src/schema/types/person.js b/src/schema/types/person.ts similarity index 98% rename from src/schema/types/person.js rename to src/schema/types/person.ts index a3bcb73..f6309e5 100644 --- a/src/schema/types/person.js +++ b/src/schema/types/person.ts @@ -31,7 +31,7 @@ import VehicleType from './vehicle'; /** * The GraphQL type equivalent of the People resource */ -const PersonType = new GraphQLObjectType({ +const PersonType: GraphQLObjectType = new GraphQLObjectType({ name: 'Person', description: 'An individual person or character within the Star Wars universe.', diff --git a/src/schema/types/planet.js b/src/schema/types/planet.ts similarity index 93% rename from src/schema/types/planet.js rename to src/schema/types/planet.ts index 4fc27bf..237a7f5 100644 --- a/src/schema/types/planet.js +++ b/src/schema/types/planet.ts @@ -29,7 +29,7 @@ import PersonType from './person'; /** * The GraphQL type equivalent of the Planet resource */ -const PlanetType = new GraphQLObjectType({ +const PlanetType: GraphQLObjectType = new GraphQLObjectType({ name: 'Planet', description: `A large mass, planet or planetoid in the Star Wars Universe, at the time of 0 ABY.`, @@ -69,14 +69,14 @@ G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs.`, climates: { type: new GraphQLList(GraphQLString), resolve: planet => { - return planet.climate.split(',').map(s => s.trim()); + return planet.climate.split(',').map((s: string) => s.trim()); }, description: 'The climates of this planet.', }, terrains: { type: new GraphQLList(GraphQLString), resolve: planet => { - return planet.terrain.split(',').map(s => s.trim()); + return planet.terrain.split(',').map((s: string) => s.trim()); }, description: 'The terrains of this planet.', }, diff --git a/src/schema/types/species.js b/src/schema/types/species.ts similarity index 93% rename from src/schema/types/species.js rename to src/schema/types/species.ts index 0ee7d5c..aabdaab 100644 --- a/src/schema/types/species.js +++ b/src/schema/types/species.ts @@ -61,7 +61,7 @@ const SpeciesType = new GraphQLObjectType({ eyeColors: { type: new GraphQLList(GraphQLString), resolve: species => { - return species.eye_colors.split(',').map(s => s.trim()); + return species.eye_colors.split(',').map((s: string) => s.trim()); }, description: `Common eye colors for this species, null if this species does not typically have eyes.`, @@ -72,7 +72,7 @@ have eyes.`, if (species.hair_colors === 'none') { return []; } - return species.hair_colors.split(',').map(s => s.trim()); + return species.hair_colors.split(',').map((s: string) => s.trim()); }, description: `Common hair colors for this species, null if this species does not typically have hair.`, @@ -80,7 +80,7 @@ have hair.`, skinColors: { type: new GraphQLList(GraphQLString), resolve: species => { - return species.skin_colors.split(',').map(s => s.trim()); + return species.skin_colors.split(',').map((s: string) => s.trim()); }, description: `Common skin colors for this species, null if this species does not typically have skin.`, diff --git a/src/schema/types/starship.js b/src/schema/types/starship.ts similarity index 96% rename from src/schema/types/starship.js rename to src/schema/types/starship.ts index ac1e6b8..2316f9e 100644 --- a/src/schema/types/starship.js +++ b/src/schema/types/starship.ts @@ -29,7 +29,7 @@ import PersonType from './person'; /** * The GraphQL type equivalent of the Starship resource */ -const StarshipType = new GraphQLObjectType({ +const StarshipType: GraphQLObjectType = new GraphQLObjectType({ name: 'Starship', description: 'A single transport craft that has hyperdrive capability.', fields: () => ({ @@ -52,7 +52,7 @@ Battlestation"`, manufacturers: { type: new GraphQLList(GraphQLString), resolve: ship => { - return ship.manufacturer.split(',').map(s => s.trim()); + return ship.manufacturer.split(',').map((s: string) => s.trim()); }, description: 'The manufacturers of this starship.', }, diff --git a/src/schema/types/vehicle.js b/src/schema/types/vehicle.ts similarity index 97% rename from src/schema/types/vehicle.js rename to src/schema/types/vehicle.ts index d97bdf7..db5b1ce 100644 --- a/src/schema/types/vehicle.js +++ b/src/schema/types/vehicle.ts @@ -53,7 +53,7 @@ Transport".`, manufacturers: { type: new GraphQLList(GraphQLString), resolve: vehicle => { - return vehicle.manufacturer.split(',').map(s => s.trim()); + return vehicle.manufacturer.split(',').map((s: string) => s.trim()); }, description: 'The manufacturers of this vehicle.', }, diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7aafdda --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "include": ["src", "test", "index.ts"], + "compilerOptions": { + "types": ["node", "jest"], + "target": "es2017", + "module": "es6", + "lib": ["dom", "esnext"], + "emitDecoratorMetadata": true, + "importHelpers": true, + "declaration": true, + "sourceMap": true, + "strict": true, + "noImplicitAny": true, + "isolatedModules": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "experimentalDecorators": true, + "allowJs": true, + "baseUrl": "./", + "jsx": "react", + "esModuleInterop": true + } +}