diff --git a/final/server/package-lock.json b/final/server/package-lock.json index 45a16ef50..076980ebe 100644 --- a/final/server/package-lock.json +++ b/final/server/package-lock.json @@ -2020,21 +2020,73 @@ } }, "apollo-datasource-rest": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/apollo-datasource-rest/-/apollo-datasource-rest-0.1.5.tgz", - "integrity": "sha512-sKDzsPCfDlLXxrgQtrxnvyMLbpRRfxcNiWJFeSdYR9in2zY37KUHgmyQOum7NylCikdkrhxzj51ny1YGWrOLrA==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/apollo-datasource-rest/-/apollo-datasource-rest-0.9.3.tgz", + "integrity": "sha512-DqSmcziPpSSz79zS+bmixFhlf5fVDKbAKqU5Hqd77jRjrrzpYN0pgT7NJTd4IhC1jVLTE+/9bZ6Q7Cr8fcID/A==", + "dev": true, "requires": { - "apollo-datasource": "0.1.3", - "apollo-server-caching": "0.1.2", - "apollo-server-env": "2.0.3", - "apollo-server-errors": "2.0.2", + "apollo-datasource": "^0.7.2", + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5", + "apollo-server-errors": "^2.4.2", "http-cache-semantics": "^4.0.0" }, "dependencies": { + "apollo-datasource": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.2.tgz", + "integrity": "sha512-ibnW+s4BMp4K2AgzLEtvzkjg7dJgCaw9M5b5N0YKNmeRZRnl/I/qBTQae648FsRKgMwTbRQIvBhQ0URUFAqFOw==", + "dev": true, + "requires": { + "apollo-server-caching": "^0.5.2", + "apollo-server-env": "^2.4.5" + } + }, + "apollo-server-caching": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.2.tgz", + "integrity": "sha512-HUcP3TlgRsuGgeTOn8QMbkdx0hLPXyEJehZIPrcof0ATz7j7aTPA4at7gaiFHCo8gk07DaWYGB3PFgjboXRcWQ==", + "dev": true, + "requires": { + "lru-cache": "^5.0.0" + } + }, + "apollo-server-env": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.5.tgz", + "integrity": "sha512-nfNhmGPzbq3xCEWT8eRpoHXIPNcNy3QcEoBlzVMjeglrBGryLG2LXwBSPnVmTRRrzUYugX0ULBtgE3rBFNoUgA==", + "dev": true, + "requires": { + "node-fetch": "^2.1.2", + "util.promisify": "^1.0.0" + } + }, + "apollo-server-errors": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz", + "integrity": "sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ==", + "dev": true + }, "http-cache-semantics": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", - "integrity": "sha512-NtexGRtaV5z3ZUX78W9UDTOJPBdpqms6RmwQXmOhHws7CuQK3cqIoQtnmeqi1VvVD6u6eMMRL0sKE9BCZXTDWQ==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -2489,7 +2541,8 @@ "apollo-server-errors": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-2.0.2.tgz", - "integrity": "sha512-zyWDqAVDCkj9espVsoUpZr9PwDznM8UW6fBfhV+i1br//s2AQb07N6ektZ9pRIEvkhykDZW+8tQbDwAO0vUROg==" + "integrity": "sha512-zyWDqAVDCkj9espVsoUpZr9PwDznM8UW6fBfhV+i1br//s2AQb07N6ektZ9pRIEvkhykDZW+8tQbDwAO0vUROg==", + "dev": true }, "apollo-server-express": { "version": "2.15.0", diff --git a/final/server/package.json b/final/server/package.json index 50c404805..893cf6c9d 100644 --- a/final/server/package.json +++ b/final/server/package.json @@ -12,7 +12,6 @@ "license": "ISC", "dependencies": { "apollo-datasource": "^0.1.3", - "apollo-datasource-rest": "^0.1.5", "apollo-server": "^2.15.0", "apollo-server-testing": "^2.15.0", "aws-sdk": "^2.585.0", @@ -27,6 +26,7 @@ }, "devDependencies": { "apollo": "^2.1.8", + "apollo-datasource-rest": "0.9.3", "apollo-link": "^1.2.3", "apollo-link-http": "^1.5.5", "jest": "^25.0.0", diff --git a/final/server/src/__tests__/__snapshots__/e2e.js.snap b/final/server/src/__tests__/__snapshots__/e2e.js.snap index 1af1ae58f..9c55a6f05 100644 --- a/final/server/src/__tests__/__snapshots__/e2e.js.snap +++ b/final/server/src/__tests__/__snapshots__/e2e.js.snap @@ -17,6 +17,37 @@ Object { } `; +exports[`Server - e2e gets a single launch 2`] = ` +Object { + "data": Object { + "launch": null, + }, + "errors": Array [ + Object { + "extensions": Object { + "code": "Not Found", + "response": Object { + "body": "Not Found", + "status": 404, + "statusText": "Not Found", + "url": "https://api.spacexdata.com/v2/launch?flight_number=30", + }, + }, + "locations": Array [ + Object { + "column": 3, + "line": 2, + }, + ], + "message": "NOT_FOUND", + "path": Array [ + "launch", + ], + }, + ], +} +`; + exports[`Server - e2e gets list of launches 1`] = ` Object { "data": Object { diff --git a/final/server/src/__tests__/__snapshots__/integration.js.snap b/final/server/src/__tests__/__snapshots__/integration.js.snap index ba7ba0ac7..5cdccf878 100644 --- a/final/server/src/__tests__/__snapshots__/integration.js.snap +++ b/final/server/src/__tests__/__snapshots__/integration.js.snap @@ -82,3 +82,20 @@ Object { }, } `; + +exports[`Queries returns error for single launch 1`] = ` +Object { + "data": Object { + "launch": null, + }, + "errors": Array [ + [GraphQLError: Unexpected error value: "Not Found"], + ], + "extensions": undefined, + "http": Object { + "headers": Headers { + Symbol(map): Object {}, + }, + }, +} +`; diff --git a/final/server/src/__tests__/__utils.js b/final/server/src/__tests__/__utils.js index 5ebaadcac..24c5230ac 100644 --- a/final/server/src/__tests__/__utils.js +++ b/final/server/src/__tests__/__utils.js @@ -1,6 +1,6 @@ -const { HttpLink } = require('apollo-link-http'); -const fetch = require('node-fetch'); -const { execute, toPromise } = require('apollo-link'); +const { HttpLink } = require("apollo-link-http"); +const fetch = require("node-fetch"); +const { execute, toPromise } = require("apollo-link"); module.exports.toPromise = toPromise; @@ -12,8 +12,8 @@ const { ApolloServer, LaunchAPI, UserAPI, - store, -} = require('../'); + store +} = require("../"); /** * Integration testing utils @@ -26,7 +26,7 @@ const constructTestServer = ({ context = defaultContext } = {}) => { typeDefs, resolvers, dataSources: () => ({ userAPI, launchAPI }), - context, + context }); return { server, userAPI, launchAPI }; @@ -48,16 +48,15 @@ const startTestServer = async server => { const link = new HttpLink({ uri: `http://localhost:${httpServer.port}`, - fetch, + fetch }); - const executeOperation = ({ query, variables = {} }) => - execute(link, { query, variables }); + const executeOperation = ({ query, variables = {} }) => execute(link, { query, variables }); return { link, stop: () => httpServer.server.close(), - graphql: executeOperation, + graphql: executeOperation }; }; diff --git a/final/server/src/__tests__/integration.js b/final/server/src/__tests__/integration.js index a9b42398a..2738ee978 100644 --- a/final/server/src/__tests__/integration.js +++ b/final/server/src/__tests__/integration.js @@ -1,13 +1,13 @@ -const {createTestClient} = require('apollo-server-testing'); -const gql = require('graphql-tag'); -const nock = require('nock'); +const { createTestClient } = require("apollo-server-testing"); +const gql = require("graphql-tag"); +// const nock = require("nock"); -const {constructTestServer} = require('./__utils'); +const { constructTestServer } = require("./__utils"); // the mocked REST API data -const {mockLaunchResponse} = require('../datasources/__tests__/launch'); +const { mockLaunchResponse } = require("../datasources/__tests__/launch"); // the mocked SQL DataSource store -const {mockStore} = require('../datasources/__tests__/user'); +const { mockStore } = require("../datasources/__tests__/user"); const GET_LAUNCHES = gql` query launchList($after: String) { @@ -63,71 +63,142 @@ const BOOK_TRIPS = gql` } `; -describe('Queries', () => { - it('fetches list of launches', async () => { +describe("Queries", () => { + it("fetches list of launches", async () => { // create an instance of ApolloServer that mocks out context, while reusing // existing dataSources, resolvers, and typeDefs. // This function returns the server instance as well as our dataSource // instances, so we can overwrite the underlying fetchers - const {server, launchAPI, userAPI} = constructTestServer({ - context: () => ({user: {id: 1, email: 'a@a.a'}}), + const { server, launchAPI, userAPI } = constructTestServer({ + context: () => ({ user: { id: 1, email: "a@a.a" } }), }); - // mock the datasources' underlying fetch methods, whether that's a REST // lookup in the RESTDataSource or the store query in the Sequelize datasource launchAPI.get = jest.fn(() => [mockLaunchResponse]); userAPI.store = mockStore; - userAPI.store.trips.findAll.mockReturnValueOnce([ - {dataValues: {launchId: 1}}, - ]); + userAPI.store.trips.findAll.mockReturnValueOnce([{ dataValues: { launchId: 1 } }]); // use our test server as input to the createTestClient fn // This will give us an interface, similar to apolloClient.query // to run queries against our instance of ApolloServer - const {query} = createTestClient(server); - const res = await query({query: GET_LAUNCHES}); + const { query } = createTestClient(server); + const res = await query({ query: GET_LAUNCHES }); expect(res).toMatchSnapshot(); }); - it('fetches single launch', async () => { - const {server, launchAPI, userAPI} = constructTestServer({ - context: () => ({user: {id: 1, email: 'a@a.a'}}), + it("fetches single launch", async () => { + const { server, launchAPI, userAPI } = constructTestServer({ + context: () => ({ user: { id: 1, email: "a@a.a" } }), }); launchAPI.get = jest.fn(() => [mockLaunchResponse]); userAPI.store = mockStore; - userAPI.store.trips.findAll.mockReturnValueOnce([ - {dataValues: {launchId: 1}}, - ]); + userAPI.store.trips.findAll.mockReturnValueOnce([{ dataValues: { launchId: 1 } }]); + + const { query } = createTestClient(server); + const res = await query({ query: GET_LAUNCH, variables: { id: 1 } }); + expect(res).toMatchSnapshot(); + }); - const {query} = createTestClient(server); - const res = await query({query: GET_LAUNCH, variables: {id: 1}}); + it("returns error for single launch", async () => { + const { server, launchAPI, userAPI } = constructTestServer({ + context: () => ({ user: { id: 1, email: "a@a.a" } }), + }); + + launchAPI.get = jest.fn().mockRejectedValueOnce("Not Found"); + userAPI.store = mockStore; + userAPI.store.trips.findAll.mockReturnValueOnce([{ dataValues: { launchId: 1 } }]); + + const { query } = createTestClient(server); + const res = await query({ query: GET_LAUNCH, variables: { id: 1 } }); + // The below is the value of res that we got by exwecuting our quer form this test + // res = { + // "http": { + // "headers": {} + // }, + // "errors": [ + // { + // "message": "Unexpected error value: \"Not Found\"", + // "locations": [ + // { + // "line": 2, + // "column": 3 + // } + // ], + // "path": [ + // "launch" + // ], + // "extensions": { + // "code": "INTERNAL_SERVER_ERROR" + // } + // } + // ], + // "data": { + // "launch": null + // } + // } + + // below is the actual error you get on playground + const errorFromPlayground = { + errors: [ + { + message: "NOT_FOUND", + locations: [ + { + line: 13, + column: 3, + }, + ], + path: ["launch"], + extensions: { + code: "Not Found", + response: { + url: "https://api.spacexdata.com/v2/launch?flight_number=123", + status: 404, + statusText: "Not Found", + body: "Not Found", + }, + exception: { + stacktrace: [ + "Error: NOT_FOUND", + " at LaunchAPI.errorFromResponse (/Users/kamlesh/razorpay/fullstack-tutorial/final/server/src/datasources/launch.js:53:15)", + " at processTicksAndRejections (internal/process/task_queues.js:93:5)", + ], + }, + }, + }, + ], + data: { + launch: null, + }, + }; + + // the below equality check fails since the both the objects are not identical + expect(res).toEqual(errorFromPlayground); expect(res).toMatchSnapshot(); }); }); -describe('Mutations', () => { - it('returns login token', async () => { - const {server, launchAPI, userAPI} = constructTestServer({ +describe("Mutations", () => { + it("returns login token", async () => { + const { server, launchAPI, userAPI } = constructTestServer({ context: () => {}, }); userAPI.store = mockStore; - userAPI.store.users.findOrCreate.mockReturnValueOnce([ - {id: 1, email: 'a@a.a'}, - ]); + userAPI.store.users.findOrCreate.mockReturnValueOnce([{ id: 1, email: "a@a.a" }]); - const {mutate} = createTestClient(server); + const { mutate } = createTestClient(server); const res = await mutate({ mutation: LOGIN, - variables: {email: 'a@a.a'}, + variables: { email: "a@a.a" }, }); - expect(res.data.login).toEqual('YUBhLmE='); + expect(res.data.login).toEqual("YUBhLmE="); }); - it('books trips', async () => { - const {server, launchAPI, userAPI} = constructTestServer({ - context: () => ({user: {id: 1, email: 'a@a.a'}}), + it("books trips", async () => { + const { server, launchAPI, userAPI } = constructTestServer({ + context: () => ({ user: { id: 1, email: "a@a.a" } }), }); // mock the underlying fetches @@ -136,21 +207,21 @@ describe('Mutations', () => { // look up the launches from the launch API launchAPI.get .mockReturnValueOnce([mockLaunchResponse]) - .mockReturnValueOnce([{...mockLaunchResponse, flight_number: 2}]); + .mockReturnValueOnce([{ ...mockLaunchResponse, flight_number: 2 }]); // book the trip in the store userAPI.store = mockStore; userAPI.store.trips.findOrCreate - .mockReturnValueOnce([{get: () => ({launchId: 1})}]) - .mockReturnValueOnce([{get: () => ({launchId: 2})}]); + .mockReturnValueOnce([{ get: () => ({ launchId: 1 }) }]) + .mockReturnValueOnce([{ get: () => ({ launchId: 2 }) }]); // check if user is booked userAPI.store.trips.findAll.mockReturnValue([{}]); - const {mutate} = createTestClient(server); + const { mutate } = createTestClient(server); const res = await mutate({ mutation: BOOK_TRIPS, - variables: {launchIds: ['1', '2']}, + variables: { launchIds: ["1", "2"] }, }); expect(res).toMatchSnapshot(); }); diff --git a/final/server/src/datasources/launch.js b/final/server/src/datasources/launch.js index 407567e0d..c0855f798 100644 --- a/final/server/src/datasources/launch.js +++ b/final/server/src/datasources/launch.js @@ -1,9 +1,10 @@ -const { RESTDataSource } = require('apollo-datasource-rest'); +const { RESTDataSource } = require("apollo-datasource-rest"); +const { AuthenticationError, ForbiddenError, ApolloError } = require("apollo-server-express"); class LaunchAPI extends RESTDataSource { constructor() { super(); - this.baseURL = 'https://api.spacexdata.com/v2/'; + this.baseURL = "https://api.spacexdata.com/v2/"; } // leaving this inside the class to make the class easier to test @@ -26,22 +27,42 @@ class LaunchAPI extends RESTDataSource { } async getAllLaunches() { - const response = await this.get('launches'); + const response = await this.get("launches"); // transform the raw launches to a more friendly - return Array.isArray(response) - ? response.map(launch => this.launchReducer(launch)) : []; + return Array.isArray(response) ? response.map((launch) => this.launchReducer(launch)) : []; } async getLaunchById({ launchId }) { - const res = await this.get('launches', { flight_number: launchId }); + const res = await this.get("launch", { flight_number: launchId }); return this.launchReducer(res[0]); } async getLaunchesByIds({ launchIds }) { - return Promise.all( - launchIds.map(launchId => this.getLaunchById({ launchId })), - ); + return Promise.all(launchIds.map((launchId) => this.getLaunchById({ launchId }))); + } + async errorFromResponse(response) { + const body = await this.parseBody(response); + const message = body; + let error; + if (response.status === 401) { + error = new AuthenticationError(message); + } else if (response.status === 403) { + error = new ForbiddenError(message); + } else { + error = new ApolloError("NOT_FOUND", message); + } + + Object.assign(error.extensions, { + response: { + url: response.url, + status: response.status, + statusText: response.statusText, + body, + }, + }); + + return error; } } diff --git a/final/server/store.sqlite b/final/server/store.sqlite index 5b706846b..ba0a81a03 100644 Binary files a/final/server/store.sqlite and b/final/server/store.sqlite differ