Adding A Saved Properties Feature
Building A GRANDstack Real Estate Search App: Part 9
Continuing to work with user authentication and authorization using Auth0 and GraphQL we add functionality to save properties in our application. Along the way, we show how to use JWT Middleware, create custom GraphQL mutations using Cypher, and use the useMutation Apollo React Hook.
Links And Resources#
- Code on Github
- GRANDstack docs: Cypher parameters from context
- express-jwt middleware
- Apollo Client useMutation hook
- Hey folks. Welcome to the Neo4j stream. My name's Will, and we're gonna be picking up where we left off last time working with some authorization features in our real estate search application. So if you don't have the code for this I'm gonna drop a link in the chat. There we go. So all the code for this up on GitHub, and we've been working on this for a few sessions now. The GitHub readme also includes links to all the recordings of the previous streams that we've done working on this so you can see what we've been doing. Basically we've been working with importing data in Neo4j, building a GraphQL API around that, and then building a React application to allow us to search for properties. And in the last session we took a look at adding some authorization features with Auth0, so specifically being able to sign in to the application, and then we restricted a couple of types in our GraphQL schema. So you can see here in this dashboard view, we took these two sort of aggregations, sorry there seems to be some construction noise going outside. Hopefully that doesn't go on too much longer, I think we'll be okay. But let me know if the noise gets too bad, maybe we can try again. Right so I have a Neo4j database running. Let's go ahead and start our application. So we'll do npm run start, that's gonna start both the React application and the GraphQL API. Dj in the chat says hello, hi, Dj! How is the noise level with this construction that's going on outside? Is it bearable at least? "Wouldn't have been noticed if you hadn't said." Okay great, we'll keep going then. Okay so here's the application up and running. So we have that dashboard view. And then we can search for some properties that get plotted on the map here. We have some photos from Mapillary. In the dashboard view I can see this list of property results, but I need to sign in to view this aggregate information, so let's sign in. And this is powered by Auth0, which is what we added last time. There we go, now that we're signed in I can see these charts with my aggregations. Cool! So what we wanna do today is, oh and the other thing I see now that I'm logged in is this sort of profile information up here with my avatar and name. So what we wanna do today is add some functionality so that as I'm searching for these properties, that I can star them, or save them so that I can then sort of keep this list of properties that I'm interested in and come back to 'em. Right? And this information should be private only to me. I don't want anyone else to see my list of starred properties, so we're gonna use our authorization feature to be able to do that. Cool! So that's the goal for today. Let's take a look at the GRANDstack docs to see how we might accomplish this. So if I go into the docs and if I look under Authorization and Middleware, I can see there's a few different options here. The first option is GraphQL Authorization Schema Directives. And this is what we used last time, we used this isAuthenticated schema directive to add this rule that says, "okay for any of these aggregation fields on the query type," let's take a look at the schema, make that a little bigger, so for any of these aggregation fields, so propertyCount or cityValues, we add the isAuthenticated schema directive that says, "Hey, you need to have a signed token to be able to view that." That token has to be valid. If you're not signed into the application, then you're not able to get the data. And in the front end we say, "Okay, you have to sign in to do this." So that's what we did last time. What we're gonna do today is look at a slightly different method for working with some of these authorization related features. And that is working with, what it says here, Cypher Parameters From Context. Let me drop a link to this in the chat too. So what's going on here? Well if we look first at this example, Cypher query that is in a Cypher schema directive in our GraphQL type definitions. This is the query type. So we're creating a new entry point into the GraphQL API. The currentUser field, this is gonna return a User object, and it's going to find that User object by running this Cypher query. So MATCH User where the user id is, and now some parameters. That dollar sign, that's the syntax for a parameter in Cypher. So cypherParams.currentUserId, that's the value of the user id that we're gonna match on in the return. But where's that coming from? Well if we look at the next bit here, this is where we're spinning up the Apollo Server instance, and one of the things we can do when creating Apollo Server instance is specify what populates this context object. So last time we looked at this we were adding the authorization header to be able to get our token that came from Auth0. In this case we're grabbing a User object on the request object and grabbing the id value and assigning that to cypherParams.currentUserId right here. So by convention, anything in the GraphQL context object, so here, under the cypherParams gets injected into these Cypher queries that are defined with Cypher schema directives as parameters. So this is really nice because this can then work with our authorization middleware where we're say, hydrating like a user object in the request and then assign that into this cypherParams object, and then our Cypher queries that we define in our GraphQL schema can take advantage of those values from our authorization middleware. And that's exactly what we're going to do today. So what we're gonna do today is figure out how to get this cypherParams object working using the Auth0 authentication method that we added. So we're gonna have still our sign in with Auth0. We need some way to be able to grab the current user from that token, and then we're going to have to say when a user clicks a button to save a property, so we'll add a button over here somewhere, like hey, I wanna save this, it's my saved property list. Then we're going to run a new mutation, so mutations in GraphQL, we haven't done any mutations yet, but mutations allow us to create and update data in the database. And that mutation is going to have some logic to check for the user id in the cypherParams, and then in the graph create like, a user saved or user starred relationship to that property. Cool! So as we're going along if you have any questions or anything, feel free to ask in the chat. Or if you have any comments or feedback also let us know in the chat. Cool! So let's jump over to index.js for the GraphQL API. And if we scroll down a bit to where we are starting up, or I should say where we are instantiating our Apollo Server instance, so that's right here. This is where we are defining the context object. And what we want is something like this over here on the right, where we're grabbing some value out of the token that's gonna somehow be a user id. Well how do we figure out what that user id is? How do we get it out of our token? Let's take a look at the token that we're working with now. So if I go to developer tools, Network. And let's refresh so we can sort of steal our token here. Oh, we'll need to login. Sign in with GitHub. Now if you remember last time, we configured Apollo Client so that if we are authenticated then we always attach the token to our GraphQL request in the request header. So if we inspect another request we're sending here, here's the Authorization header, and we can see that it adds this Bearer token. So I'm gonna go ahead and copy that token. And if we go to jwt.io, this is just a handy web tool that lets us take a look at the claims that exist in this token. Oh I don't need the Bearer piece that I copied. There we go. Cool, so here's the token that we're getting from Auth0 as a result of going through the GitHub. In my case auth flow, but we could also do different social methods for sign in, Auth0 even allows us to create email and password based usernames that Auth0 manages, and we don't have to think about, as the application developer, all we sort of care about are the claims that are in this token. So we can see there's a few claims here. Who's issuing the token? That's our Auth0 app, or are Auth0 tenant I guess is maybe the correct term. And then it has a sub claim, or the subscriber. And in the case of Auth0 this is the method that you use to authenticate. So in my case GitHub combined with, I think this is maybe some id from the system that you use to authenticate with. So maybe this maps to my GitHub username, I thought this was a hash or something like that. But anyway this is what is going to uniquely identify users in our case. So only when I authenticate using my GitHub account are we going to get this value for the sub, or the subject claim. So this is gonna be our user id, essentially, value. As we can see also like, when this token was issued, when it expires, and the scope, and so on. But this sub claim is what I'm interested in. Okay so if we go back to our GraphQL server code here, well if we look at the example it's defining a value for cypherParams out of a user object on the request object. And that's not quite what we have going on if we look at, you know, here's our request object. We have a token in the header, but we need to decode these claims that are in this token, pull out the sub claim, and inject that into the cypherParams object in the context here. And that is exactly what middleware is good for. And in our case, we are interested in JWT middleware. So there's a express-jwt middleware package, because we're using express here. Let's see. Here we're pulling an express, creating a new express app, and then the way this server is configured allows us to add any middleware, and then it applies that express app to the Apollo Server instance in this case. So we're all ready to use any express middleware. So if you're not familiar with middleware, basically, and specifically with express, basically the idea is an HTTP request comes in, it has some headers. In this case this has a token. And before we get to the piece of our server in this implementation, in this case the GraphQL server piece, before we get to that point this request object is gonna go through some middleware. And we can do things like read headers on the request, we can modify the request, we can do things like we can do some error checking. We can even reject or send back, you know, a 400 error response before we even get to, in this case, handling the GraphQL request. So that's the purpose of middleware in express, which is the Node web server that we're using. There's lots of different options and flexibility for how we can work with middleware. And in this case we're gonna use this express-jwt middleware library to essentially decode our JWT token. And what it's going to do is it's going to take the claims that are in the JWT token and add those to a user object in the request so that we can then grab those and pass those along to our Cypher query, which is pretty neat. A question from the chat. "Didn't you say there are three options, directives, middleware and something else? So do you use more than one at a time?" Yeah! So there's, I mean I'd say there's maybe more than three options, but I think maybe three options that I wanna cover in our application. And the reason I say there's more than three options is because GraphQL itself is not really opinionated about authentication and authorization. So you could argue there are sort of infinite options for how you go about that. But yeah, the three that we're gonna talk about are the sort of GraphQL schema directive approach, which is what we looked at last time. In this case we're using the Cypher parameters from the context object. And then the other approach we'll look at is using some of the scopes and roles. And the other part of your question, "Should we use more than one at a time?" And yeah often times, you know in our application we're gonna see the case where we need to make use of the authorization directives, and the Cypher parameters approach. And we're also in this case gonna be adding some middleware just so that we're able to read some of those claims from the token. So yeah. So in this case we're using, yeah, a couple different approaches here. I guess I think in, I think the blog post you may be referring to is this one. Where I think it sort of lays out the three different approaches of sort of doing auth yourself by like, inside the resolver, checking for some authorization rule. And then I think maybe talking about middleware or these authorization directives. In this case I don't think we'll do the sort of inside the resolver checking. Cool! So what we wanna do is add this express-jwt middleware to our application so that any of our requests that come in with a token, that token is decoded, and those claims are added to our request so that we can then pull those in to our context object so we can pass those values through to our Cypher query. So I'm going to cd into the api application. Do an npm install express-jwt. And then following along from the readme here, so I'll import jwt from "express-jwt". And then this example, let's see this is on the call, that app.get, passing in the call to the middleware here. I think we can also, yeah. So in this case the app, this is our express application. And then we create our Apollo Server instance, and then this server.applyMiddleware, this is gonna take any middleware that we've added to our express app and make sure that we're working with that in Apollo Server. So before the call to that we'll say app.use, jwt, and then we need to pass a secret. So this is a secret that's used to decode and verify. Well I shouldn't say decode, to verify the JWT token, so it's validating that it was signed with the private key of whatever secret we're giving it. And last time we added our secret here to this .env file, which means it will be set as an environment variable, JWT_SECRET. So our secret is going to be process.env.JWT_SECRET. Okay. And then it also wants to know what algorithms we're willing to use to verify this JWT token. And if you remember from last time, Auth0 uses the RS256 algorithm. Okay. Save that. So now we should have our claims decoded and added to your request object thanks to this middleware. So if we take a look here, where we're adding the driver instance and so on, what we wanna do is inside cypherParams we want, let's call it the userId. We want that to be request.user.sub. Okay and how do I know that is what it's gonna be? Well that's what this middleware is doing by default. So I think we can specify if we wanted to, yeah, customizing token location. So Customizing Token Location, this is saying if we want to read it from a cookie or a query parameter, I think by default it should take our bearer token. Oh here we go. "By default the decoded token is attached to req.user." Yep cool, that's what we want. We want the subject claim. Okay so let's save that and restart our application. And it takes a minute because have to rebuild our react app since I stopped it. Okay and we can see right away we get some errors. And if we look at our GraphQL requests, these returned 401 Unauthorized. That says unauthorized, no authorization token was found. Right so I guess by default this JWT middleware is, if there's no token, if it can't verify it, then it returns a 401 error. So we want to make the token optional. Which I think we can do here. Let's see. Revoked tokens. Error handling. Ah here we go, credentialsRequired: false. I think that's what we want. "You might want to use this module to identify registered users while still providing access to unregistered users. You can do this by using credentialsRequired." Yes! That's exactly what we wanna do. So in this configuration object we'll say credentialsRequired: false. And let's refresh our application and try this again. And we get a 500 error. So in this case an error on the server. Context creation failed: Cannot read property 'sub' of undefined. Oh right. Okay so in that case, so here userId is req.user.sub, but if I'm not logged in to the application, then I'm not going to have a token, so then the middleware is not going to hydrate this user object. So in that case I think we can use the sort of optional chaining syntax. Which sort of short circuits to null if one of these is not found. So this is sort of equivalent to like if, the sort of thing that we used to have to check to make sure that the user object actually exists before grabbing the nested value. Okay. So let's restart application again. And okay. So we're seeing our table view. That's good. Let's login and see what happens. So okay, so now we're signed in and we can see the aggregate information. Okay cool, so that's good. So now we can take advantage of this userId field in the cypherParams in our GraphQL schema. So let's jump over to our GraphQL schema. And first of all I guess let's just verify that this is working. So let's create a new field on the query type, let's just call it me. And it's, I don't know, just gonna return a string. It's just gonna return that userId field since we haven't added a user type to our schema yet. But we can get to that. Okay so cypher, this is gonna be a Cypher directive field. And we're just gonna return cypherParams.userId. Okay and we can even check and see. Since we're in debug mode, we've been logging the generated Cypher query, so this is the query that ran previously to populate this search result table. And we can see the parameters passed to it included the cypherParams object and our subject field, which is now the userId field, and the cypherParams object from our token. So that seems to be working. Let's double check this in our GraphQL API. So if we go to localhost:4001/graphql. From the chat, "Hello." Hi, thanks for joining. Okay so. This looks like our token from last time. Let's take that out of there and see what happens. So now if we look at our docs, we have me, which returns a string. So this should tell us our userId. And it's null because there's no token, so I'm not sending any authorization header with a token in it. If we add the token and send it, then now, okay we get, yep, this is our userId. Cool! So that is a good start. The next thing that we wanna do is now add a mutation field for actually indicating that this user has starred or saved this property. So remember, if we're searching for properties here in our search results and I see one that I like, I wanna be able to star it or save it and have that show up in a list somewhere and I don't want anyone else to see that. I only want to see that when I'm signed in. So to do that let's add a custom mutation. So on the Mutation type, and let's call this starProperty maybe. And so starProperty is going to take some id for the property that we wanna save. So if we look at the property type, well it has a PropertyID fields. So let's use that. It also has an id field, but I think this id field, oh no, well that's, I was gonna say I don't think we have this for every property, but this is non-nullable. So yeah, let's just use the id field then. And that id field is a String. I feel like that should be really an ID type. Let's change that and see if anything breaks. Something broke. But that could be just because we haven't finished saying here this is gonna be of type ID, and this is going to give me a list of properties. Yeah? Okay. Did that break our app? No, I don't think so. Cool. This is going to be a Cypher directive field, so the logic for this mutation we're gonna define with a Cypher query. And what do we wanna do here? Well we want to I guess figure out the node to represent the user. We haven't added any user nodes to the database yet. So in this case we'll use a merge statement. So MERGE, this is like a GET or CREATE. So what are we gonna merge on? Let's say userId the $cypherParams.userId. So merge if there's a user node in the graph with this value for userId, then we just match on it, if not then we create the node. Then the next thing we wanna do is match on the property where the id value of the property is this value, so whatever value we're passing in to the mutation. And then we wanna create a relationship connecting the user and the property and say hey, this user has starred this property, let's use a merge so we don't create a bunch of duplicate relationships. So we'll say user stars, or STARRED, the property. And then we need to return, we don't want a list of properties returned, just the single property. And that's gonna be the property that we passed in. And we'll leave this as nullable I guess, because this id might not actually match to a property, so in the case where we pass in an invalid id, or the case that we aren't able, I guess, to merge on a user, it might not always return, it might return null. Okay so let's give that a try. So in GraphQL playground we'll refresh to pickup our schema changes. And then we're going to execute Mutation starProperty:id. We're going to need the id for a property, so let's jump to Neo4j browser. And let's take a look at a property. Sure this one, I don't know, this looks fine. Let's copy the ID here. Another question from the chat, "Is there a good way to write Cypher with syntax highlighting, or is the browser best for that?" Yeah so I mean there's some syntax highlighting in Neo4j browser. So like this is the query we wanna run here essentially, something like that where we have some syntax highlighting. But you'll notice that we sort of lose Cypher syntax highlighting when we're doing that inside our GraphQL schema in these Cypher directive fields. Yeah so there's, I should be using the cypher tag. So not sure if this is actually in the docs or not, but there is a cypher template tag that we can use here that is exported by neo4j-graphql-js. Will it pick up a special character like that? No. Yeah so like in this example, so here in the Cypher directive we're just using strings. But if we imported this cypher template, cypher tag template I guess is the right terminology for that. It's kind of like the gql tag. Then we would have syntax highlighting I think within our Cypher directives here. So this won't quite work because I'm in a schema.graphql file, not a JavaScript file. But if you did something like import {cypher} from "neo4j-graphql-js". And then here if you used cypher like that. Yeah. That seems to work. We'd have to tweak a couple things because this, the way that were reading our schema.graphql file I think would break with this import. But yeah that's probably the way we should be doing it. If you're not writing cypher in one of these, like in a graphql schema file, you could also pull these out into like a cypher snippet file. And there, I believe, are some VSCode extensions for working with, preferences, extensions, for working with Cypher. Yes, there's a few. So there's Trinity. There's a VSCode extension for working let's say for Neo4j. And then there's a couple specific to Cypher as well. So a few options there. But yeah that's a good point about using the cypher literal tag. We should tweak the way we import this graphql file, we'll do it next time so we get nice syntax highlighting. Because it is kind of awkward to write the Cypher queries inside our GraphQL schema without syntax highlighting, I agree. So I think we want to, let's see, we were looking at here. We were just trying to find a property. So here's the property, pass in that ID. Let me zoom in a bit on this GraphQL playground here. There we go. That's a bit more. The chat, "So you'd change it from schema.graphql to schema.js." Yeah something like that. So if we look at what's going on here, so this schema.graphql file gets loaded by graphql-schema.js. And basically all it's doing is reading this file and exporting that as typedefs so that then in index.js, where we import our typedefs here. Nope, first line. Here. This is just reading this file directly. Yeah, so we'd have to modify that somehow to I think strip out our import of the Cypher template tag. So okay. Here's the property we wanna star, and then I don't know, not all have an address, they all have a legal description, so we can bring that back. Oh and yeah, let me bring back the id. So let's run this. And we have our token. That's important because if we didn't attach the token here, it wouldn't be able to find a user. And it says Cypher error, need a WITH between a MERGE and a MATCH. Oh yeah, so let's tweak this a bit. So let's match first. So first we match on the property, then we merge on the user, then merge on the relationship. That should fix our syntax error there. There we go. So okay, it says we've starred this property with this ID, and this legal description. So now if we go back to Neo4j browser and if we, let's match User nodes. And now we have a user, it has userId with my sub value, that value from the sub claim on the JWT token. And it's connected to this property, so I've starred this property. Cool! Let's try another one. Let's just grab another one at random, and we'll copy the ID. Switch that ID. Run the mutation. And then if we run this Cypher query again, now there's two properties that I've starred! Okay cool. So I can create starred properties in the database. In order to show those in my application though I'm gonna need to read that from the GraphQL API. So I'm gonna need another field on the query type. Maybe let's call this getStarredProperties, and this is going to give me now a list of property objects. And this is going to be a Cypher directive field. Okay so what do we wanna do here? Well here we're searching for where the user has starred properties. RETURN p, but not just any user, specifically where the userId is cypherParams.userId. Right? So the currently authenticated user, find them in the graph, find any properties that they have starred and return those. So if we jump to GraphQL playground, refresh to update our schema, and we should be able to do getStarredProperties. And give me the id and LegalDescription for the properties I starred. Cool. Here's the ones I want. And of course you know, in our application we might want to plot those, so like give me the latitude and longitude as well as the address, if I have it. Cool! Okay so now we've got our API working for saving starred properties, for reading starred properties for the currently authenticated user. I think the next step is to switch to the UI in the React application to be able to display those somewhere, and also I guess to be able to save those from the UI. So let's jump, well first let's take a look at the way we're gonna do this in the app. So I think on the search view, so as we're searching here, maybe let's just go ahead and add another component that's just like a list of any starred properties. Maybe that's useful. So let's just go ahead and now in the api, in web-react let's go ahead and create a new component. And let's call it, I don't know, StarredProperties. StarredProperties Okay so this is gonna be a component that is going to make a query to the GraphQL API to find any starred properties for the authenticated user and maybe just like show them in a list initially? So kinda simple. So we'll start off, import React. We'll need gql from graphql-tag. We'll need our useQuery hook from apollo/react-hooks. What else? Oh I think there's this title component that we created previously. I don't know, this just allows us to add these sort of styled titles. That might be nice so we don't have to mess around with any CSS. Okay and then we're going to define the GraphQL query to get our starred properties. So we wrap that in the gql template tag. That's going to, whoops, parse our GraphQL query. And what's our query going to be? Well it's gonna be get, what did we call it? getStarredProperties I think is what we called it? Yeah getStarredProperties. It doesn't take any arguments. What can we return? We can return the id, and the address, and the LegalDescription. Maybe it'll just show one of those or something in a list. Okay so then our actual component is going to be a functional component. StarredProperties. So the first thing we'll do is setup our useQuery hook. So we get a loading state, an error state, and data back from the call to useQuery. Pass it to the GraphQL query we wanna run. If we're in the loading state, then we'll just return Loading. If there's an error, then we'll return Error. But if we have some data, then we'll return, let's wrap this in a React fragment so we can use our Title component. Starred Properties. And yeah, let's just keep this simple for now. So we'll create an unordered list, and we need to map over this array. So map over data.getStarredProperties, that's gonna be an array, and we wanna map over that, and for each entry pull out the, since we don't always have address, maybe just the LegalDescription of the property. Later on we'll make that like a link or something that we can click into and view the property details, but for now that should be good. So data.getStarredProperties.map. And then that takes, map takes a function that is passed each one of those property objects. And then we're going to return a list item and render the LegalDescription. Okay. I think that should be good. So that's the component. Now we need to show it in our, in this component, which is the MapResults component that gets rendered here, I think. Okay and the first thing, let's see, we'll import StarredProperties from './StarredProperties'. And then let's see. We have this grid setup here. So this like a 12 column grid. So that this is, I guess this responsive layout, so if we're in small, each one of these, the map component and our property results takes up 12 columns if we're in small. When we're in this sort of medium, they each take up like roughly half. There's a weird semicolon. What's that? This. Don't know how that got in there, let's delete that. Question from the chat. "Apart from queries and mutations is there any other additional use cases for using Apollo in this app?" Yeah that's a good question. Yes there is. So Apollo Client is what we've been using for fetching data from our GraphQL application, or GraphQL API. And Apollo can do a lot more than that, than just send queries and mutations. It can also manage our local state's data. So all of these queries and mutations that we run, those go into an Apollo Client cache so that, you know, by default the performance is really fast. And we can verify this if we, say for example, keep switching back and forth between the Search view and the Dashboard view. And we can see that the second time we do this it loads very fast, that's because it's reading it from the cache. So there's lots of things we can do with caching our application data. But I think the more interesting thing here we can also do is work with local state data. So it may be that we have some state of our application that could be something, we haven't implemented this in the UI yet, but maybe that's like all of the filters, the filtering criteria that we've applied. We can maybe manage the state of that data using Apollo. And then that means in other components throughout our application, we can use the same GraphQL Client hooks. So like useQuery to fetch and work with local data. Which is really nice, because then that's sort of shared across our application. We don't have to think about how to pass that state data throughout our application, or use the React context API or something, or Redux or something like that. Instead we can manage all of that data in Apollo Client. The other thing is that Apollo Client has some nice features for things that give us optimistic UI. So I think this is new in Apollo Client 3. If we look at, where's this in the docs? Maybe under LOCAL STATE? Yeah so Local-only fields, this allows us to basically add fields to our GraphQL schema that actually aren't on the server. But it allows us to then define on the client where that data comes from. And if we hook that up to what are called reactive variables, then this gives us a really nice optimistic UI or way to update different UI states based on changes in that client data. So yeah, to answer your question about additional use cases for using Apollo other than sort of running these crazy mutations, I think managing local state is probably the most interesting additional use case. "Would pagination then be used from the Apollo side or from Neo4j?" Yeah so that's a good question. I guess it depends on like, how intensive, and how much data you're getting back in the first place. So the way I think of it is it is typically probably good if you have lot of data to bring back to just sort of worry about pulling just what you need to render a view. And then let the backend data fetching logic be responsible for that pagination. Of course you can also take advantage of things like pagination from data that's in the cache. So if we pull back a bunch of data and that sort of goes into the Apollo cache, then we can sort of paginate through that on the client without having to back to the server again to fetch data. So that's definitely, when we get to properly wiring up our map here, right now we just have like a dumb limit, so we're bringing back, I don't know, like 100 results or something. But that's something we'll have to address when we actually add in our filtering criteria, and then actually as we zoom out on the map, we'll need to think about how we're going to paginate when we get back a whole bunch of results. So yeah, something we'll need to think about. Okay so we wanna add in this starred properties component that we created. I think what I'll do is I will trim off a couple of grids, or a couple of columns, rather, from these grid items. So this one. Medium, we've got seven columns. When it's large this gets six. And then this one in medium gets three columns. And then large gets four columns. So then that gives us two columns to work with for our starredResults. So it's gonna be a Grid item. What's extra small? 12. Medium, two. Large, we'll give it two as well. Okay so Grid item. And then it needs a Paper component to give it that nice sort of shadowy background with the drop shadow. And we have some CSS styles we can give to it so we don't have to think about that either. And then it is the StarredProperties component. So if we save that, let's see what happens. Okay so now StarredProperties is blank, but I'm not signed in, so let's log in and see if that changes. So we should see the two starred properties that we added just sort of manually in GraphQL playground. Yeah here we go, so now those show up. Cool! So okay, so I'm showing my starred properties. For this to be useful, I don't know, you'd need to be able to click on this and then sort of zoom in the map to the property and show the details. Something like that, or I don't know, maybe it should go on a separate page or something like that. But this is good enough for now. The thing we are missing though is now I wanna add some more properties to this list. So what I wanna do is add a star property button now in my property results, and then have that call the mutation in the database and update the UI here to show me the new entry in my starred property list. Okay so to do that we need to, we need a button, and we need to wire that button up to executing a mutation against the GraphQL API. So I guess that can happen in our MapResults component. There's a lot going on this component, maybe we'll put the logic in here and then refactor it later on to pull out some of that logic. It's getting a little busy, but that's okay for now. So let's see. We need, let's import the material-ui button component. And then we'll need the useMutation hook from @apollo/react-hooks. And we'll need graphql-tag. It's worth pointing out that the newer version of Apollo Client, so Apollo Client 3, not sure if we're using, but we're using the react. Yeah, so we're not using Apollo Client 3, because all of these are bundled in the same package. So that I think would be another good session to look at is updating this. Now that Apollo Client 3 is out, all of these, the things like the useMutation, useQuery hooks, the graphql-tag, those are bundled in the same package. And the API, for a few things have changed slightly, but the same basic idea. Anyway okay. So now we can use our mutation hook to get a function that's gonna be sort of the handler around that. So this is gonna be like the onStarHandler. And it's also gonna give us a data, loading, and error state objects, well I don't know if we need those. So I call the useMutation, and then we can just put our mutation query right here inline. It's fine. So this is gonna be a mutation. What is this going to look like? Well we're going to say mutation, and then we need to declare some GraphQL variables here. So mutation is the type of operation, but then let's call this starPropertyMutation, and declare our GraphQL variables. We're gonna pass an ID, this is going to be a required ID field. And if we look back to GraphQL playground, we can make sure we get the naming right here. So we've called it starProperty, and it passes an ID. And then we can bring back some fields, although I don't think we'll really need these since we're just gonna hook this up to a button. Okay so that's this onStarHandler. So now what we wanna do is add a button. Syntax error. Fix that and save that. There we go. So what we wanna do is wire up this onStarHandler function to a button so that when we click a button here, it's going to call this mutation. So way down here we've got where we're rendering our currentProperty. What we wanna do is, where should we put this? Maybe after the address we'll add a button that says Star Property. And then the onClick handler for this button is going to be a call to our, what did we call it? onStarHandler. So we're gonna execute our mutation, and we can pass the variables. So the variables that we're gonna pass here to our onStarHandler if we look at the GraphQL query, that's going to be the id of the property we wanna star. And if we look at what we are rendering, that is the currentProperty.id. Syntax error. What's goin' on there? So Button onClick, onStarHandler, variables. Call the onStarHandler. Variables. Oh right, variables id. So this is an object. So variables is an object where the id value is equal to currentProperty.id. Okay. But we wanna make sure that we actually have a value for this id field on currentProperty since I don't think we actually are currently bringing that back. So if we go to the Search component, here's the query that we're running. So filter for properties that are within a radius of this hard coded, so this should be the center of our map. Let's add id fields, so that we can be sure we have that. Okay so now we've got this button, it's wired up to any time we click it execute the mutation passing in the id value of the currentProperty. So let's login and see if this works. Okay so we sign in, go to the Search view, and it's showing us here's our previously saved properties that we added just sort of manually. And here's one I wanna save, we click the STAR PROPERTY button. If we go to, let's open up developer tools, and take a look at the Network tab. Let's try that again. So here's another property. Let's star this one. And we can see our GraphQL query executes. It says starProperty where the id is this value, so this is the id of the currently selected property on 15th Avenue. And the response is returning that, cool. So that looks good. But there's kind of a weird problem here. Why doesn't our starred properties component, why isn't it showing the results as soon as we click that? Well do we have to refresh to see that? What's going on? So we sign in. By the way, also if you notice, each time we refresh we lose our authenticated state, so that's something else to investigate. I think that might require some other configuration in Auth0. Anyway, okay so now when I refresh, now I see those saved properties. So okay, that's nice. So I can save. Or I meant to say I can star these properties using the starProperty mutation. And you know, if we query those for our GraphQL endpoint, we can see those are showing up, that's good. However we're not sort of updating those automatically in the UI. We have to actually do a refresh to see those. And this sort of gets back to what we were talking about earlier with this idea of working with the Apollo Client cache and working with local state and optimistic UI. So if we look at our options here for mutations, when we execute a mutation we can do things like updating the cache after a mutation explicitly. Which is one way to go about this, but I think it would be much more interesting to look at how we might be able to do this using some of the new features in Apollo Client. Like, does it make sense to use reactive variables here? How else can we sort of wire up this more optimistic UI to update this component when a mutation changes the server value, or the server code? Cool! So maybe we'll leave that for next time since that seems like another chunk to bite off. And we did get the sort of auth related features added that we wanted to here. So just to sort of recap what we accomplished, so we added this express-jwt middleware to our GraphQL server. We were able to use that to grab a userId from the JWT token in each request that comes from Auth0. And then we were able to use that in some Cypher queries that we added to our GraphQL schema here, where we did things like find the currently authenticated user, save properties for the currently authenticated user, and show any starred properties for the currently authenticated user. So now only when we're logged in will we see that. And it's important to point out we'll only see our saved property. If someone else logs in as someone else, they'll see their own saved properties. They won't be able to see my saved properties. So we did that. And then we wired that up in the UI, adding the StarredProperties component to show our starred properties, and then this STAR PROPERTY button to trigger the GraphQL mutation to actually send that to the server. Cool! So let's call it good for today. I'll go ahead and push this code up to GitHub so you can grab it there. And next week, we will pick up from here. I don't know, maybe we'll look at some of the Apollo Client features. Or maybe we will look at protecting mutations in general with some of the roles and scopes. We'll see. But either way, thanks for joining today, and we hope to see you next week. Cheers!
Subscribe To Will's Newsletter
Want to know when the next blog post or video is published? Subscribe now!