From 5ed07a6e4b70fddef849b6b3786d27884beabd92 Mon Sep 17 00:00:00 2001 From: Albert Alises Date: Fri, 13 Nov 2020 12:54:40 +0100 Subject: [PATCH] feat(queries): create BikeStations query --- schema.graphql | 97 +++++++- src/RootQuery.ts | 3 + src/config.ts | 1 + src/datasources/BikeDataSource.ts | 124 +++++++++++ src/datasources/MetroDataSource.ts | 1 - .../__fixtures__/BikeStationsFixtures.ts | 208 ++++++++++++++++++ .../__tests__/BikeDataSource.test.ts | 37 ++++ src/graphql.ts | 2 + src/inputs/FilterByInput.ts | 39 +++- src/outputs/BikeStation.ts | 52 +++++ src/outputs/BikeStationAvailabilityInfo.ts | 34 +++ src/outputs/enums/BikeStationStatus.ts | 18 ++ src/queries/BikeStationsQuery.ts | 69 ++++++ src/queries/MetroStationsQuery.ts | 4 +- .../__tests__/BikeStationsQuery.test.ts | 97 ++++++++ src/utils/createTestServer.ts | 6 +- types/index.d.ts | 98 ++++++++- 17 files changed, 876 insertions(+), 14 deletions(-) create mode 100644 src/datasources/BikeDataSource.ts create mode 100644 src/datasources/__fixtures__/BikeStationsFixtures.ts create mode 100644 src/datasources/__tests__/BikeDataSource.test.ts create mode 100644 src/outputs/BikeStation.ts create mode 100644 src/outputs/BikeStationAvailabilityInfo.ts create mode 100644 src/outputs/enums/BikeStationStatus.ts create mode 100644 src/queries/BikeStationsQuery.ts create mode 100644 src/queries/__tests__/BikeStationsQuery.test.ts diff --git a/schema.graphql b/schema.graphql index be92ccc..480f4f7 100644 --- a/schema.graphql +++ b/schema.graphql @@ -4,10 +4,11 @@ schema { """Root Query""" type RootQuery { - metroStations(after: String, first: Int, before: String, last: Int, filterBy: FilterByInput): MetroStations + metroStations(after: String, first: Int, before: String, last: Int, filterBy: FilterByInputMetro): MetroStations metroStation(findBy: FindByInput!): MetroStation metroLine(findBy: FindByInput!): MetroLine metroLines(after: String, first: Int, before: String, last: Int): MetroLines + bikeStations(after: String, first: Int, before: String, last: Int, filterBy: FilterByInputBike): BikeStations } """Information about the metro stations of the city of Barcelona""" @@ -75,9 +76,9 @@ type Coordinates { } """ -Input for the filterBy argument of the queries, which allows filtering a connection by some parameters (e.g. lineName or lineId) +Input for the filterBy argument of the metro queries, which allows filtering a connection by some parameters (e.g. lineName or lineId) """ -input FilterByInput { +input FilterByInputMetro { lineId: Int lineName: String } @@ -137,3 +138,93 @@ type MetroLineEdge { """A cursor for use in pagination""" cursor: String! } + +""" +Information about the public bike stations (SMOU) of the city of Barcelona +""" +type BikeStations { + """Connection with the data about bike stations""" + stations: BikeStationConnection +} + +"""A connection to a list of items.""" +type BikeStationConnection { + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """A list of edges.""" + edges: [BikeStationEdge] +} + +"""An edge in a connection.""" +type BikeStationEdge { + """The item at the end of the edge""" + node: BikeStation + + """A cursor for use in pagination""" + cursor: String! +} + +"""Bike station information""" +type BikeStation { + """Unique ID of the station""" + id: ID + + """Status of the station e.g. IN_SERVICE""" + status: BikeStationStatus + + """Last updated information timestamp (in ms since epoch)""" + lastUpdated: Int + + """Name of the station""" + name: String + + """Total number of bikes the station has""" + capacity: Int + + """Location coordinates of the station""" + location: Coordinates + + """Information about the available bikes and docks of the station""" + available: BikeStationAvailabilityInfo +} + +enum BikeStationStatus { + IN_SERVICE + MAINTENANCE + CLOSED +} + +"""Information about the available bikes and docks of the station""" +type BikeStationAvailabilityInfo { + """Number of available bikes in the station by type""" + bikes: BikeAvailabilityInfo + + """Number of available docks in the station""" + docks: Int +} + +"""Information of the bike availability of a station by type""" +type BikeAvailabilityInfo { + """Number of available electrical bikes in the station""" + electrical: Int + + """Number of available mechanical bikes in the station""" + mechanical: Int + + """Total number of available bikes in the station""" + total: Int +} + +""" +Input for the filterBy argument of the bikes queries, which allows filtering a connection by some parameters (e.g. only with available bikes) +""" +input FilterByInputBike { + only: OnlyFilterByInputBike +} + +input OnlyFilterByInputBike { + hasAvailableBikes: Boolean + hasAvailableElectricalBikes: Boolean + isInService: Boolean +} diff --git a/src/RootQuery.ts b/src/RootQuery.ts index 8605445..0655498 100644 --- a/src/RootQuery.ts +++ b/src/RootQuery.ts @@ -6,6 +6,8 @@ import metroStations from "./queries/MetroStationsQuery"; import metroLine from "./queries/MetroLineQuery"; import metroLines from "./queries/MetroLinesQuery"; +import bikeStations from "./queries/BikeStationsQuery"; + export default new GraphQLObjectType({ name: "RootQuery", description: "Root Query", @@ -14,5 +16,6 @@ export default new GraphQLObjectType({ metroStation, metroLine, metroLines, + bikeStations, }, }); diff --git a/src/config.ts b/src/config.ts index 44f9074..aa29f27 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1 +1,2 @@ export const TMB_API_BASE_URL = "https://api.tmb.cat/v1/transit"; +export const SMOU_API_BASE_URL = "https://api.bsmsa.eu/ext/api/bsm/gbfs/v2/en"; diff --git a/src/datasources/BikeDataSource.ts b/src/datasources/BikeDataSource.ts new file mode 100644 index 0000000..e0acb33 --- /dev/null +++ b/src/datasources/BikeDataSource.ts @@ -0,0 +1,124 @@ +import { RESTDataSource } from "apollo-datasource-rest"; +import { SMOU_API_BASE_URL } from "../config"; +import { ApolloError } from "apollo-server-lambda"; +import type { + FilterByInputBike, + BikeStation as BikeStationType, + BikeStationStatus as BikeStationStatusType, +} from "../../types"; + +interface StationInfo { + address: string; + altitude: number; + capacity: number; + lat: number; + lon: number; + name: string; + nearby_distance: number; + physical_configuration: "MECHANICBIKESTATION" | "ELECTRICBIKESTATION"; + post_code: string; + station_id: number; +} + +interface StationStatus { + station_id: number; + is_charging_station: boolean; + is_installed: 1 | 0; + is_renting: 1 | 0; + is_returning: 1 | 0; + last_reported: number; + num_bikes_available: number; + num_bikes_available_types: { + ebike: number; + mechanical: number; + }; + num_docks_available: number; + status: "IN_SERVICE" | "MAINTENANCE" | "CLOSED"; +} + +interface StationInfoAPIResponse { + last_updated: number; + ttl: number; + data: { + stations: ReadonlyArray; + }; +} + +interface StationStatusAPIResponse { + last_updated: number; + ttl: number; + data: { + stations: ReadonlyArray; + }; +} + +export default class BikeDataSource extends RESTDataSource { + constructor() { + super(); + this.baseURL = SMOU_API_BASE_URL; + } + + bikeStationReducer( + stationInfoData: StationInfo | null, + stationStatusData: StationStatus | null + ): BikeStationType { + const reducedStationInfo = { + id: String(stationInfoData?.station_id) ?? null, + name: stationInfoData?.name ?? null, + capacity: stationInfoData?.capacity ?? null, + location: { + latitude: stationInfoData?.lat ?? null, + longitude: stationInfoData?.lon ?? null, + altitude: stationInfoData?.altitude ?? null, + }, + }; + + const reducedStationStatus = { + status: (stationStatusData?.status as BikeStationStatusType) ?? null, + lastUpdated: stationStatusData?.last_reported ?? null, + available: { + bikes: { + electrical: + stationStatusData?.num_bikes_available_types?.ebike ?? null, + mechanical: + stationStatusData?.num_bikes_available_types?.mechanical ?? null, + total: stationStatusData?.num_bikes_available ?? null, + }, + docks: stationStatusData?.num_docks_available ?? null, + }, + }; + + return { + ...reducedStationInfo, + ...reducedStationStatus, + }; + } + + async getAllStations(): Promise { + const stationInfoResponse: StationInfoAPIResponse | null = await this.get( + "station_information" + ); + const stationStatusResponse: StationStatusAPIResponse | null = await this.get( + "station_status" + ); + + if (!stationInfoResponse && !stationStatusResponse) { + return new ApolloError( + "There was an error fetching the bike stations information" + ); + } + + return ( + stationInfoResponse?.data?.stations?.map( + (stationInfoData: StationInfo | null) => { + const stationStatusData: StationStatus | null = + stationStatusResponse?.data?.stations?.find( + ({ station_id }) => station_id === stationInfoData?.station_id + ) ?? null; + + return this.bikeStationReducer(stationInfoData, stationStatusData); + } + ) ?? [] + ); + } +} diff --git a/src/datasources/MetroDataSource.ts b/src/datasources/MetroDataSource.ts index 7c85a16..3c876c8 100644 --- a/src/datasources/MetroDataSource.ts +++ b/src/datasources/MetroDataSource.ts @@ -1,7 +1,6 @@ import TmbApiDataSource from "./TmbApiDataSource"; import type { FindByInput, - FilterByInput, MetroStation as MetroStationType, MetroLine as MetroLineType, } from "../../types"; diff --git a/src/datasources/__fixtures__/BikeStationsFixtures.ts b/src/datasources/__fixtures__/BikeStationsFixtures.ts new file mode 100644 index 0000000..ccb32b6 --- /dev/null +++ b/src/datasources/__fixtures__/BikeStationsFixtures.ts @@ -0,0 +1,208 @@ +export const mockBikeStationsInfoAPIResponse: any = { + last_updated: 1605381341, + ttl: 20, + data: { + stations: [ + { + station_id: 1, + name: "GRAN VIA CORTS CATALANES, 760", + physical_configuration: "ELECTRICBIKESTATION", + lat: 41.3979779, + lon: 2.1801069, + altitude: 16.0, + address: "GRAN VIA CORTS CATALANES, 760", + post_code: "08013", + capacity: 46, + nearby_distance: 1000.0, + }, + { + station_id: 2, + name: "C/ ROGER DE FLOR, 126", + physical_configuration: "ELECTRICBIKESTATION", + lat: 41.3954877, + lon: 2.1771985, + altitude: 17.0, + address: "C/ ROGER DE FLOR, 126", + post_code: "08013", + capacity: 27, + nearby_distance: 1000.0, + }, + { + station_id: 3, + name: "C/ NÀPOLS, 82", + physical_configuration: "ELECTRICBIKESTATION", + lat: 41.3941557, + lon: 2.1813305, + altitude: 11.0, + address: "C/ NÀPOLS, 82", + post_code: "08013", + capacity: 27, + nearby_distance: 1000.0, + }, + { + station_id: 4, + name: "C/ RIBES, 13", + physical_configuration: "ELECTRICBIKESTATION", + lat: 41.3933173, + lon: 2.1812483, + altitude: 8.0, + address: "C/ RIBES, 13", + post_code: "08013", + capacity: 21, + nearby_distance: 1000.0, + }, + ], + }, +}; + +export const mockBikeStationsStatusAPIResponse: any = { + last_updated: 1605374106, + ttl: 9, + data: { + stations: [ + { + station_id: 1, + num_bikes_available: 3, + num_bikes_available_types: { + mechanical: 2, + ebike: 1, + }, + num_docks_available: 43, + last_reported: 1605374084, + is_charging_station: true, + status: "IN_SERVICE", + is_installed: 1, + is_renting: 1, + is_returning: 1, + }, + { + station_id: 2, + num_bikes_available: 0, + num_bikes_available_types: { + mechanical: 0, + ebike: 0, + }, + num_docks_available: 24, + last_reported: 1605374023, + is_charging_station: true, + status: "IN_SERVICE", + is_installed: 1, + is_renting: 1, + is_returning: 1, + }, + { + station_id: 3, + num_bikes_available: 6, + num_bikes_available_types: { + mechanical: 6, + ebike: 0, + }, + num_docks_available: 21, + last_reported: 1605374102, + is_charging_station: true, + status: "IN_SERVICE", + is_installed: 1, + is_renting: 1, + is_returning: 1, + }, + { + station_id: 4, + num_bikes_available: 1, + num_bikes_available_types: { + mechanical: 1, + ebike: 0, + }, + num_docks_available: 18, + last_reported: 1605374039, + is_charging_station: true, + status: "MAINTENANCE", + is_installed: 1, + is_renting: 1, + is_returning: 1, + }, + ], + }, +}; + +export const mockBikeStationsResponse: any = [ + { + available: { + bikes: { + electrical: 1, + mechanical: 2, + total: 3, + }, + docks: 43, + }, + capacity: 46, + id: "1", + lastUpdated: 1605374084, + location: { + altitude: 16, + latitude: 41.3979779, + longitude: 2.1801069, + }, + name: "GRAN VIA CORTS CATALANES, 760", + status: "IN_SERVICE", + }, + { + available: { + bikes: { + electrical: 0, + mechanical: 0, + total: 0, + }, + docks: 24, + }, + capacity: 27, + id: "2", + lastUpdated: 1605374023, + location: { + altitude: 17, + latitude: 41.3954877, + longitude: 2.1771985, + }, + name: "C/ ROGER DE FLOR, 126", + status: "IN_SERVICE", + }, + { + available: { + bikes: { + electrical: 0, + mechanical: 6, + total: 6, + }, + docks: 21, + }, + capacity: 27, + id: "3", + lastUpdated: 1605374102, + location: { + altitude: 11, + latitude: 41.3941557, + longitude: 2.1813305, + }, + name: "C/ NÀPOLS, 82", + status: "IN_SERVICE", + }, + { + available: { + bikes: { + electrical: 0, + mechanical: 1, + total: 1, + }, + docks: 18, + }, + capacity: 21, + id: "4", + lastUpdated: 1605374039, + location: { + altitude: 8, + latitude: 41.3933173, + longitude: 2.1812483, + }, + name: "C/ RIBES, 13", + status: "MAINTENANCE", + }, +]; diff --git a/src/datasources/__tests__/BikeDataSource.test.ts b/src/datasources/__tests__/BikeDataSource.test.ts new file mode 100644 index 0000000..4f26fdf --- /dev/null +++ b/src/datasources/__tests__/BikeDataSource.test.ts @@ -0,0 +1,37 @@ +import DataSource from "../BikeDataSource"; +import { + mockBikeStationsInfoAPIResponse, + mockBikeStationsStatusAPIResponse, + mockBikeStationsResponse, +} from "../__fixtures__/BikeStationsFixtures"; + +const BikeDataSource = new DataSource(); + +describe("BikeDataSource", () => { + const mockGet = jest.fn(); + + (BikeDataSource as any).get = mockGet; + + it("[bikeStationReducer]: Correctly parses a bike station information and status API data to the schema format", () => { + expect( + BikeDataSource.bikeStationReducer( + mockBikeStationsInfoAPIResponse.data.stations[0], + mockBikeStationsStatusAPIResponse.data.stations[0] + ) + ).toStrictEqual(mockBikeStationsResponse[0]); + }); + + describe("[getAllStations]", () => { + it("Correctly looks up the bike stations from the API", async () => { + mockGet + .mockReturnValueOnce(mockBikeStationsInfoAPIResponse) + .mockReturnValueOnce(mockBikeStationsStatusAPIResponse); + + const res = await BikeDataSource.getAllStations(); + expect(res).toStrictEqual(mockBikeStationsResponse); + + expect(mockGet.mock.calls[0][0]).toBe("station_information"); + expect(mockGet.mock.calls[1][0]).toBe("station_status"); + }); + }); +}); diff --git a/src/graphql.ts b/src/graphql.ts index 34b5ab3..cec85f8 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -1,6 +1,7 @@ import { ApolloServer } from "apollo-server-lambda"; import schema from "./schema"; import MetroDataSource from "./datasources/MetroDataSource"; +import BikeDataSource from "./datasources/BikeDataSource"; import formatError from "./utils/formatError"; const server: ApolloServer = new ApolloServer({ @@ -14,6 +15,7 @@ const server: ApolloServer = new ApolloServer({ introspection: true, dataSources: () => ({ metro: new MetroDataSource(), + bike: new BikeDataSource(), }), }); diff --git a/src/inputs/FilterByInput.ts b/src/inputs/FilterByInput.ts index 5cf8f47..f3c710b 100644 --- a/src/inputs/FilterByInput.ts +++ b/src/inputs/FilterByInput.ts @@ -1,9 +1,14 @@ -import { GraphQLInt, GraphQLInputObjectType, GraphQLString } from "graphql"; +import { + GraphQLInt, + GraphQLInputObjectType, + GraphQLString, + GraphQLBoolean, +} from "graphql"; -export default new GraphQLInputObjectType({ - name: "FilterByInput", +const FilterByInputMetro = new GraphQLInputObjectType({ + name: "FilterByInputMetro", description: - "Input for the filterBy argument of the queries, which allows filtering a connection by some parameters (e.g. lineName or lineId)", + "Input for the filterBy argument of the metro queries, which allows filtering a connection by some parameters (e.g. lineName or lineId)", fields: { lineId: { type: GraphQLInt, @@ -13,3 +18,29 @@ export default new GraphQLInputObjectType({ }, }, }); + +const FilterByInputBike = new GraphQLInputObjectType({ + name: "FilterByInputBike", + description: + "Input for the filterBy argument of the bikes queries, which allows filtering a connection by some parameters (e.g. only with available bikes)", + fields: { + only: { + type: new GraphQLInputObjectType({ + name: "OnlyFilterByInputBike", + fields: { + hasAvailableBikes: { + type: GraphQLBoolean, + }, + hasAvailableElectricalBikes: { + type: GraphQLBoolean, + }, + isInService: { + type: GraphQLBoolean, + }, + }, + }), + }, + }, +}); + +export { FilterByInputBike, FilterByInputMetro }; diff --git a/src/outputs/BikeStation.ts b/src/outputs/BikeStation.ts new file mode 100644 index 0000000..f50c52a --- /dev/null +++ b/src/outputs/BikeStation.ts @@ -0,0 +1,52 @@ +import Coordinates from "./Coordinates"; +import { + GraphQLObjectType, + GraphQLID, + GraphQLString, + GraphQLInt, +} from "graphql"; +import { connectionDefinitions } from "graphql-relay"; +import BikeStationAvailabilityInfo from "./BikeStationAvailabilityInfo"; +import BikeStationStatus from "./enums/BikeStationStatus"; + +const BikeStation = new GraphQLObjectType({ + name: "BikeStation", + description: "Bike station information", + fields: { + id: { + description: "Unique ID of the station", + type: GraphQLID, + }, + status: { + description: "Status of the station e.g. IN_SERVICE", + type: BikeStationStatus, + }, + lastUpdated: { + description: "Last updated information timestamp (in ms since epoch)", + type: GraphQLInt, + }, + name: { + description: "Name of the station", + type: GraphQLString, + }, + capacity: { + description: "Total number of bikes the station has", + type: GraphQLInt, + }, + location: { + description: "Location coordinates of the station", + type: Coordinates, + }, + available: { + description: + "Information about the available bikes and docks of the station", + type: BikeStationAvailabilityInfo, + }, + }, +}); + +const { connectionType: BikeStationConnection } = connectionDefinitions({ + nodeType: BikeStation, +}); + +export { BikeStationConnection, BikeStation as default }; diff --git a/src/outputs/BikeStationAvailabilityInfo.ts b/src/outputs/BikeStationAvailabilityInfo.ts new file mode 100644 index 0000000..e20c1e0 --- /dev/null +++ b/src/outputs/BikeStationAvailabilityInfo.ts @@ -0,0 +1,34 @@ +import { GraphQLObjectType, GraphQLInt } from "graphql"; + +export default new GraphQLObjectType({ + name: "BikeStationAvailabilityInfo", + description: "Information about the available bikes and docks of the station", + fields: { + bikes: { + description: "Number of available bikes in the station by type", + type: new GraphQLObjectType({ + name: "BikeAvailabilityInfo", + description: + "Information of the bike availability of a station by type", + fields: { + electrical: { + description: "Number of available electrical bikes in the station", + type: GraphQLInt, + }, + mechanical: { + description: "Number of available mechanical bikes in the station", + type: GraphQLInt, + }, + total: { + description: "Total number of available bikes in the station", + type: GraphQLInt, + }, + }, + }), + }, + docks: { + description: "Number of available docks in the station", + type: GraphQLInt, + }, + }, +}); diff --git a/src/outputs/enums/BikeStationStatus.ts b/src/outputs/enums/BikeStationStatus.ts new file mode 100644 index 0000000..a4ed7c8 --- /dev/null +++ b/src/outputs/enums/BikeStationStatus.ts @@ -0,0 +1,18 @@ +// @flow strict + +import { GraphQLEnumType } from "graphql"; + +export default new GraphQLEnumType({ + name: "BikeStationStatus", + values: { + IN_SERVICE: { + value: "IN_SERVICE", + }, + MAINTENANCE: { + value: "MAINTENANCE", + }, + CLOSED: { + value: "CLOSED", + }, + }, +}); diff --git a/src/queries/BikeStationsQuery.ts b/src/queries/BikeStationsQuery.ts new file mode 100644 index 0000000..5878b30 --- /dev/null +++ b/src/queries/BikeStationsQuery.ts @@ -0,0 +1,69 @@ +import { connectionArgs, connectionFromArray } from "graphql-relay"; +import type { + BikeStations as BikeStationsType, + FilterByInputBike as FilterByInputBikeType, + BikeStation as BikeStationType, + BikeStationConnection as BikeStationConnectionType, +} from "../../types"; +import { BikeStationConnection } from "../outputs/BikeStation"; +import { GraphQLObjectType } from "graphql"; +import { FilterByInputBike } from "../inputs/FilterByInput"; + +const filterBikeStations = ( + station: BikeStationType, + filterBy: FilterByInputBikeType +): boolean => { + if (!filterBy) { + return true; + } + + const { only } = filterBy; + + if (only?.hasAvailableBikes) { + return Number(station?.available?.bikes?.total ?? null) > 0; + } + + if (only?.hasAvailableElectricalBikes) { + return Number(station?.available?.bikes?.electrical ?? null) > 0; + } + + if (only?.isInService) { + return station?.status === "IN_SERVICE"; + } + + return true; +}; + +const BikeStations = new GraphQLObjectType({ + name: "BikeStations", + description: + "Information about the public bike stations (SMOU) of the city of Barcelona", + fields: { + stations: { + type: BikeStationConnection, + description: "Connection with the data about bike stations", + }, + }, +}); + +export default { + type: BikeStations, + args: { + ...connectionArgs, + filterBy: { + type: FilterByInputBike, + }, + }, + resolve: async (_, args, { dataSources }): Promise => { + const bikeStations = await dataSources.bike.getAllStations(); + + return { + stations: connectionFromArray( + bikeStations.filter((station) => + filterBikeStations(station, args.filterBy) + ), + args + ) as BikeStationConnectionType, + }; + }, +}; diff --git a/src/queries/MetroStationsQuery.ts b/src/queries/MetroStationsQuery.ts index e74a37c..2366584 100644 --- a/src/queries/MetroStationsQuery.ts +++ b/src/queries/MetroStationsQuery.ts @@ -5,7 +5,7 @@ import type { } from "../../types"; import { MetroStationConnection } from "../outputs/MetroStation"; import { GraphQLObjectType, GraphQLInt } from "graphql"; -import filterByInput from "../inputs/FilterByInput"; +import { FilterByInputMetro } from "../inputs/FilterByInput"; const MetroStations = new GraphQLObjectType({ name: "MetroStations", @@ -27,7 +27,7 @@ export default { args: { ...connectionArgs, filterBy: { - type: filterByInput, + type: FilterByInputMetro, }, }, resolve: async (_, args, { dataSources }): Promise => { diff --git a/src/queries/__tests__/BikeStationsQuery.test.ts b/src/queries/__tests__/BikeStationsQuery.test.ts new file mode 100644 index 0000000..ff9302d --- /dev/null +++ b/src/queries/__tests__/BikeStationsQuery.test.ts @@ -0,0 +1,97 @@ +import { createTestClient } from "apollo-server-testing"; +import { gql } from "apollo-server-lambda"; +import createTestServer from "../../utils/createTestServer"; + +import { + mockBikeStationsStatusAPIResponse, + mockBikeStationsInfoAPIResponse, + mockBikeStationsResponse, +} from "../../datasources/__fixtures__/BikeStationsFixtures"; + +const GET_BIKE_STATIONS = gql` + query getBikeStations($filterBy: FilterByInputBike) { + bikeStations(filterBy: $filterBy) { + stations { + edges { + node { + available { + bikes { + total + electrical + } + } + status + } + } + } + } + } +`; + +describe("bikeStations Query", () => { + const { server, bike } = createTestServer(); + const { query } = createTestClient(server); + + beforeEach( + () => + ((bike as any).get = jest + .fn() + .mockReturnValueOnce(mockBikeStationsInfoAPIResponse) + .mockReturnValueOnce(mockBikeStationsStatusAPIResponse)) + ); + + it("Fetches all bike stations", async () => { + const res = await query({ + query: GET_BIKE_STATIONS, + }); + + expect(res?.data?.bikeStations.stations.edges).toHaveLength( + mockBikeStationsResponse.length + ); + }); + + it("Fetches list of bike stations with available bikes", async () => { + const res = await query({ + query: GET_BIKE_STATIONS, + variables: { filterBy: { only: { hasAvailableBikes: true } } }, + }); + + //There is no station returned that has no bikes + const returnedStationWithNoBikes = + res?.data?.bikeStations.stations.edges.find( + ({ node }) => node.available.bikes.total === 0 + ) ?? null; + + expect(returnedStationWithNoBikes).toBeNull(); + }); + + it("Fetches list of bike stations with available electrical bikes", async () => { + const res = await query({ + query: GET_BIKE_STATIONS, + variables: { filterBy: { only: { hasAvailableElectricalBikes: true } } }, + }); + + //There is no station returned that has no electrical bikes + const returnedStationWithNoElectricalBikes = + res?.data?.bikeStations.stations.edges.find( + ({ node }) => node.available.bikes.electrical === 0 + ) ?? null; + + expect(returnedStationWithNoElectricalBikes).toBeNull(); + }); + + it("Fetches list of bike stations that are in service ", async () => { + const res = await query({ + query: GET_BIKE_STATIONS, + variables: { filterBy: { only: { isInService: true } } }, + }); + + //There is no station returned that is not in service + const returnedStationNotInService = + res?.data?.bikeStations.stations.edges.find( + ({ node }) => node.status !== "IN_SERVICE" + ) ?? null; + + expect(returnedStationNotInService).toBeNull(); + }); +}); diff --git a/src/utils/createTestServer.ts b/src/utils/createTestServer.ts index 37dc691..3870c31 100644 --- a/src/utils/createTestServer.ts +++ b/src/utils/createTestServer.ts @@ -1,23 +1,27 @@ import { ApolloServer } from "apollo-server-lambda"; import schema from "../schema"; import MetroDataSource from "../datasources/MetroDataSource"; +import BikeDataSource from "../datasources/BikeDataSource"; interface TestServer { server: ApolloServer; metro: MetroDataSource; + bike: BikeDataSource; } const createTestServer = (): TestServer => { const metro = new MetroDataSource(); + const bike = new BikeDataSource(); const server = new ApolloServer({ schema, dataSources: () => ({ metro, + bike, }), }); - return { server, metro }; + return { server, metro, bike }; }; export default createTestServer; diff --git a/types/index.d.ts b/types/index.d.ts index 2d1656e..a0986fb 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -16,6 +16,7 @@ export type RootQuery = { metroStation?: Maybe; metroLine?: Maybe; metroLines?: Maybe; + bikeStations?: Maybe; }; @@ -25,7 +26,7 @@ export type RootQueryMetroStationsArgs = { first?: Maybe; before?: Maybe; last?: Maybe; - filterBy?: Maybe; + filterBy?: Maybe; }; @@ -49,6 +50,16 @@ export type RootQueryMetroLinesArgs = { last?: Maybe; }; + +/** Root Query */ +export type RootQueryBikeStationsArgs = { + after?: Maybe; + first?: Maybe; + before?: Maybe; + last?: Maybe; + filterBy?: Maybe; +}; + /** Information about the metro stations of the city of Barcelona */ export type MetroStations = { __typename?: 'MetroStations'; @@ -110,8 +121,8 @@ export type Coordinates = { altitude?: Maybe; }; -/** Input for the filterBy argument of the queries, which allows filtering a connection by some parameters (e.g. lineName or lineId) */ -export type FilterByInput = { +/** Input for the filterBy argument of the metro queries, which allows filtering a connection by some parameters (e.g. lineName or lineId) */ +export type FilterByInputMetro = { lineId?: Maybe; lineName?: Maybe; }; @@ -174,3 +185,84 @@ export type MetroLineEdge = { /** A cursor for use in pagination */ cursor: Scalars['String']; }; + +/** Information about the public bike stations (SMOU) of the city of Barcelona */ +export type BikeStations = { + __typename?: 'BikeStations'; + /** Connection with the data about bike stations */ + stations?: Maybe; +}; + +/** A connection to a list of items. */ +export type BikeStationConnection = { + __typename?: 'BikeStationConnection'; + /** Information to aid in pagination. */ + pageInfo: PageInfo; + /** A list of edges. */ + edges?: Maybe>>; +}; + +/** An edge in a connection. */ +export type BikeStationEdge = { + __typename?: 'BikeStationEdge'; + /** The item at the end of the edge */ + node?: Maybe; + /** A cursor for use in pagination */ + cursor: Scalars['String']; +}; + +/** Bike station information */ +export type BikeStation = { + __typename?: 'BikeStation'; + /** Unique ID of the station */ + id?: Maybe; + /** Status of the station e.g. IN_SERVICE */ + status?: Maybe; + /** Last updated information timestamp (in ms since epoch) */ + lastUpdated?: Maybe; + /** Name of the station */ + name?: Maybe; + /** Total number of bikes the station has */ + capacity?: Maybe; + /** Location coordinates of the station */ + location?: Maybe; + /** Information about the available bikes and docks of the station */ + available?: Maybe; +}; + +export enum BikeStationStatus { + InService = 'IN_SERVICE', + Maintenance = 'MAINTENANCE', + Closed = 'CLOSED' +} + +/** Information about the available bikes and docks of the station */ +export type BikeStationAvailabilityInfo = { + __typename?: 'BikeStationAvailabilityInfo'; + /** Number of available bikes in the station by type */ + bikes?: Maybe; + /** Number of available docks in the station */ + docks?: Maybe; +}; + +/** Information of the bike availability of a station by type */ +export type BikeAvailabilityInfo = { + __typename?: 'BikeAvailabilityInfo'; + /** Number of available electrical bikes in the station */ + electrical?: Maybe; + /** Number of available mechanical bikes in the station */ + mechanical?: Maybe; + /** Total number of available bikes in the station */ + total?: Maybe; +}; + +/** Input for the filterBy argument of the bikes queries, which allows filtering a connection by some parameters (e.g. only with available bikes) */ +export type FilterByInputBike = { + only?: Maybe; +}; + +export type OnlyFilterByInputBike = { + hasAvailableBikes?: Maybe; + hasAvailableElectricalBikes?: Maybe; + isInService?: Maybe; +};