[ ] Add your API key to your .env file
[ ] We now will be making our way into bringing a REST endpoint into our flow. We're going to use the OMDB api to search their database for different types of media, that we can store in the DB and add to playlists.
How are going to deal with REST in our Apollo app. We could easily call our fetch inside of whichever resolver needs the data and pipe it through. For arguments sake, the OMDB endpoint we're using is only a singular endpoint, so that would be the most efficient. BUT what if we needed to wrap multiple different parameters to an endpoint. We'd be making a bunch of fetch calls throughout a bunch of resolvers, basically as we would if we were doing this inside of a React app.
In comes RESTDatasource —
<aside> 💡 Data sources are classes that encapsulate fetching data from a particular service, with built-in support for caching, deduplication, and error handling. You write the code that is specific to interacting with your backend, and Apollo Server takes care of the rest.
</aside>
Inside of server lets create a directory called datasource
where we will create a new file named omdb.js
const { RESTDataSource } = require("apollo-datasource-rest");
// First we want to extend RESTDatasource so we can set our baseURL and attach
// methods that will act as our getters and setters
class OmdbAPI extends RESTDataSource {
constructor() {
super();
// our base endpoint url
this.baseURL = "<http://www.omdbapi.com/?">;
}
// This "reducer" is going to allow us to organize the data we are pulling
// in from the endpoint and plug it in how we want to graphql
mediaReducer(media) {
const {
Title,
Year,
Rated,
Released,
Runtime,
Genre,
Director,
Writer,
Actors,
Plot,
Language,
Country,
Awards,
Poster,
Ratings,
MetaScore,
imdbRating,
imdbVotes,
imdbID,
Type,
totalSeasons,
Response
} = media;
return {
title: Title,
year: Year,
rated: Rated,
released: Released,
runtime: Runtime,
genre: Genre,
director: Director,
writer: Writer,
actors: Actors,
plot: Plot,
language: Language,
country: Country,
awards: Awards,
poster: Poster,
source: Ratings.source,
value: Ratings.value,
metascore: MetaScore,
imdbRating,
imdbVotes,
imdbID,
type: Type,
totalSeasons,
response: Response
};
}
// here we acces .get off of this, which is being brought in from extending
// the RESTDatasource. We plug in a title argument and our API key
async getMediaDetails(title) {
const response = await this.get(
`?t=${title.replace(" ", "+")}&apikey=${process.env.OMDB_KEY}`
);
// we now run the response through our reducer
return this.mediaReducer(response);
}
}
module.exports = OmdbAPI;
server/index.js
Now we're going to need to add the datasource into our ApolloServer so we can utilize it inside of our resolvers.
const OmdbAPI = require("./datasources/omdb");
const server = new ApolloServer({
...
dataSources: () => ({
omdbAPI: new OmdbAPI()
}),
...
});
Lastly we need to add a new query and some mutations to our schema and resolvers
// Media Mutations
createMedia: async (_, args, { prisma }) => {
// first we will loop the current media in the DB to see if one exists with the same title
const mediaExists = await prisma
.medias()
.then(data => data.find(media => media.title === args.mediaTitle));
// if the media does exist, we want to just pass that object so addMediaToPlaylist can consume it
if (mediaExists instanceof Object) {
return mediaExists;
}
const newMedia = prisma.createMedia({
...args.media
});
return newMedia;
},
addMediaToPlaylist: async (_, args, { prisma, user }) => {
if (!user) {
throw new Error("Log in first!");
}
// Let's check to make sure we're not duplicating media inside of the playlist
const mediaExistsInPlaylist = await prisma
.playlist({
title: args.playlistTitle
})
.media()
.then(data => data.some(media => media.title === args.mediaTitle));
if (mediaExistsInPlaylist) {
throw new Error("This media already exists in this playlist!");
}
// Update the playlist to add a media to the currently chosen playlist
return prisma.updatePlaylist({
data: {
media: {
connect: {
id: args.mediaId
}
}
},
where: {
title: args.playlistTitle
}
});
}
},
// We also need to add some Type resolvers as well. These are so we can help
// Graphql understand what a relationship field is supposed to return
User: {
playlists: (parent, args, { prisma }) => {
return prisma.user({ id: parent.id }).playlists();
}
},
Playlist: {
owner: (parent, args, { prisma }) => {
return prisma.playlist({ id: parent.id }).owner();
},
media: (parent, args, { prisma }) => {
return prisma.playlist({ id: parent.id }).media();
}
}
Now we're going to need some new components to handle searching for the media data, storing them in the DB, and adding them to playlists.
Landing
First we have to make a stop in the landing page component again. We're gong to add state trackers to pass the search media input into mutation on enter, with the proper value.
const Landing = ({ currentUser }) => {
// The first tracks the on change of the input
const [search, setSearch] = React.useState("");
// The second takes the search state and allots us a variable to pass through
// to our mutation
const [value, setValue] = React.useState("");
return (
...
<div>
<div>
<h1>SEARCH YOUR MEDIA</h1>
// Now we pass the values we need over to the SearchMedia component
<SearchMedia value={value} setSearch={setSearch} setValue={setValue} />
</div>
// We also use the search vairable through to render the Media details
{search && <MediaCard search={search} />}
</div>
...
);
};
SearchMedia
Now we need the form to track the title input that we're going to use to feed to rest data source and hit the endpoint
import React from "react";
const SearchMedia = ({ setSearch, value, setValue }) => {
return (
<div>
<form
onSubmit={e => {
e.preventDefault();
// The setSearch function allows us to grab the current state from the
// input and set off the OMDB search process, feeding the data to our
// MediaCard and Dropdown components
return setSearch(value);
}}
>
<div>
<input onChange={e => setValue(e.target.value)} />
<button>Search</button>
</InputContainer>
</form>
</div>
);
};
export default SearchMedia;
MediaCard
When Search has a value, which is the title, we can pass it through to the media card and send out our query.
import React from "react";
import { useQuery } from "@apollo/react-hooks";
import gql from "graphql-tag";
import Dropdown from '../Dropdown';
// Query constant
const SEARCH_MEDIA = gql`
query SearchMedia($title: String) {
showOrMovieData(title: $title) {
title
plot
poster
imdbRating
totalSeasons
}
}
`;
const MediaCard = ({ search }) => {
// Here we define the media query, pass the it search variable (title) and
// hit the OMDB endpoint, returning us the mediaData
const { data: mediaData, loading } = useQuery(SEARCH_MEDIA, {
variables: {
title: search
}
});
if (loading) return <p>Loading...</p>;
// We need to do some fact checking to see if it's a series or movie by
// checking if theres a season count
const renderSeasonCount = mediaData.showOrMovieData.totalSeasons !== null && <p><span>{mediaData.showOrMovieData.totalSeasons}</span> seasons</p>
// Render the output from the query
return (
<div>
<div>
<img src={mediaData.showOrMovieData.poster} alt="Media poster" />
</div>
<div>
<h1>{mediaData.showOrMovieData.title}</h1>
<div>
<div>
<p><span>{mediaData.showOrMovieData.imdbRating}</span> of 10</p>
{renderSeasonCount}
</div>
<div>
<p>{mediaData.showOrMovieData.plot}</p>
// pass that mediaData into our final component, the Dropdown
// which will allow us to view our playlists and add some media
// to them
<Dropdown mediaData={mediaData} />
</div>
</div>
</div>
</div>
);
};
export default MediaCard;
Dropdown
Our final component is our biggest component. This component needs to be able to control some state on a select node and pull some data out to send to our mutation for adding media to a playlist.
import React from 'react';
import { useMutation } from '@apollo/react-hooks';
import gql from "graphql-tag";
import CURRENT_USER from "../Auth/CurrentUser";
// First we need to be able to create the media, so we have something to store.
// Currently all of the media data is just coming from an endpoint, now we
// will be dealing with the data from our own DB
const CREATE_MEDIA = gql`
mutation CreateMedia($media: MediaInput!, $mediaTitle: String!) {
createMedia(media: $media, mediaTitle: $mediaTitle) {
id
title
}
}
`;
// After we've added the media to our DB, we can use that media and put it into
// any playlist we create
const ADD_MEDIA_TO_PLAYLIST = gql`
mutation AddMediaToPlaylist(
$playlistTitle: String!
$mediaId: ID!
$mediaTitle: String!
) {
addMediaToPlaylist(
playlistTitle: $playlistTitle
mediaId: $mediaId
mediaTitle: $mediaTitle
) {
media {
title
}
}
}
`;
const Dropdown = ({ mediaData }) => {
// First we're going to want to give the dropDown's state a dropdown value
// that is dependent on if a user has any playlists
const defaultDropdownValue = userData.currentUser.playlists.length > 0 ? userData.currentUser.playlists[0].title : "";
// The state here is how we will track the dropdown selections
const [dropdownValue, setDropDownValue] = React.useState(defaultDropdownValue);
// We will be using this data to feed a variable to our mutation
const { data: userData } = useQuery(CURRENT_USER);
// We need to define our addMediaToPlaylist method so we can expose it
// to the createMedia mutation
const [addMediaToPlaylist] = useMutation(ADD_MEDIA_TO_PLAYLIST, {
refetchQueries: () => [{ query: CURRENT_USER, variables: {} }]
});
// After we succesfully create our media (or it already exists) we want to
// pass that media into our playlist using our addMediaToPlaylist mutation
const [createMedia] = useMutation(CREATE_MEDIA, {
onCompleted: data => {
const mediaId = data.createMedia.id;
const mediaTitle = data.createMedia.title;
return addMediaToPlaylist({
variables: {
playlistTitle: dropdownValue,
mediaId,
mediaTitle
}
});
}
});
// We use this function has a way to sanitize the data before we send it
// into the DB or a playlist
const createAndAddMedia = media => {
/*
Since we're going to pull the media in from our RESTDatasource, the type created in our
source is going to attach a __typename. We need to strip that key off so Prisma can tack it on
when it runs createMedia()
*/
media = Object.keys(media).reduce((obj, key) => {
if (key !== "__typename") {
obj[key] = media[key];
}
return obj;
}, {});
return createMedia({
variables: {
media: {
...media
},
mediaTitle: media.title
}
});
};
return (
<div>
<select value={dropdownValue} onChange={e => setDropDownValue(e.target.value)}>
{userData &&
userData.currentUser.playlists.map(playlist => {
return (
<option key={playlist.id} value={playlist.title}>
{playlist.title}
</option>
);
})}
</select>
<button
onClick={() => createAndAddMedia(mediaData.showOrMovieData)}
>
+
</button>
</div>
)
};
export default Dropdown;