GraphQL Mutations & Authorization With Auth0
Building A GRANDstack Real Estate Search App: Part 10
Adding authorization to GraphQL mutations in our GRANDstack app. Specifically we look at the @hasScope GraphQL authorization schema directive, scopes & API permissions with Auth0, and using the Apollo Client useMutation React Hook with forms.
Links And Resources#
- Code on Github
- GRANDstack docs: Cypher parameters from context
- GRANDstack docs: Generated mutations
- GRANDstack docs: hasScope schema directive
- Apollo Client useMutation hook
- Hey folks, welcome to the Neo4j stream. Sounds like some folks are excited about authentication and authorization today, and that's what we're gonna talk about it, talk about it today. So thanks everyone for joining as always feel free to chime in in the chat, as we have questions. This session today is kind of the third installment of talking about authorization and GraphQL and the GRANDstack. Let's jump to the GitHub README and do a quick overview of where we are. So as always this...drop this in the chat. As always, this is the GitHub repo for the zillow-clone and the real estate search app we've been building. This is now the 10th session that we've done working on this on the stream. So I say, this is sort of the third installment in authorization related features. So we had kind of an overview, if you look in the list here, this is the list of the streams that link to the YouTube recording. So if we go back a few of these, we had kind of an overview of our options for working with authorization but in GraphQl and GRANDstack. And if we look in the docs here, we go to the Authorization/Middleware page...there's a fly. It's also another construction day. So if you hear loud banging and beeping, the city has been tearing up the water main on my street for... It feels like a couple of months now, but they're now sealing the pavement above it. So hopefully that means they're almost done. So anyway, that's what the banging and beeping is anyway, hopefully it's not too bad. Yeah, so we had kind of an overview of our options for adding authorization to a GRANDstack App and specifically for our GraphQL API, how to sort of secure that and protect types and fields. And we saw that there was this GraphQL Authorization Schema Directives that we could use where we're sort of adding our authorization rules in the GraphQL type definitions...we need a fly swatter. Then the other option was, and this is what we used last time, was the Cypher Parameters From Context where we added some middleware that was, in this case, hooking into Auth0 to grab an authorization token and from that, we could figure out what the user ID is. So then we could, in the cypher queries that we were binding to fields in our GraphQL schema, we could reference that user ID. And what we did last time was add this saved properties feature so that a user could select a house they were looking at and save it. What else, let's fire this up so we can take a look at it. (indistinct) and start. From the chat off topic question about using view with Neo4j GRANDstack, of course it can be done. Yeah, so if you look at the GRANDstack Starter Project, which is what we started with... drop the link in the chat here. That's what we started this project with, was using the GRANDstack Starter Project so gives us sort of a lot of the boilerplates code that we need for not only the API but also the front end. And in this case we're using react. In the starter there are a few different implementations of the front ends, so if you scroll down a bit, we see there's the web-react version, which is what we're using. There's an angular implementation and then just a few days ago, we got a PR for a Flutter version of the front end. And then we also... there's a TypeScript flavor of the React front-end. So it's definitely possible to use lots of different front-end frameworks with GraphQL and as part of GRANDstack. The nice thing about GRANDstack is that, once you have your GraphQL API, you can then build multiple clients that consume and interact with that GraphQL API. So we've been using React, you can certainly use Vue. I'm not sure what the state of GraphQL Vue client says. I think there's a Apollo Clients integration, Vue Apollo. So maybe that would be a good one to add to our GRANDstack Starter Project implementations. But yeah, essentially, if you wanted to do a Vue version of GRANDstack, you would essentially just replace the react front-end using a Vue implementation, some sort of Vue GraphQL client, I think there is an Apollo Clients integration for that. So it's really can be done, although we don't have a version of it implemented in the starter yet, but yeah good question. My colleague Adam is a big Vue fan, so maybe we can guilt him into doing some things with a GraphQL with you. Cool, so last time we added this login functionality with Auth0 or I guess we did that a couple of times ago. And now when we go to the SearchView, I'll see my saved searches here once this fire is up. I think the reason this is so slow is it's hitting the Mapillary API for every property that we bring back and generating photo URLs from the Mapillary API. So maybe we...let's see if we look in resolvers, we bring back... (indistinct) so it's cool so the Neo4j settings should be the same. Yeah so in regards to sort of adding a Vue version, yeah everything here that we do with Neo4j doesn't really change the only sort of front-end specific things, are how you interact with the GraphQL end points. And so that doesn't really change with, I should say, the API piece of this doesn't really change if you're using Vue or React. There we go, okay that was a bit faster that time so let's login. And when I'm signed in I can see this aggregate data so we added the isAuthenticated schema directive if you remember that last time, if we look at our schema, we added these isAuthenticated schema directives two sessions ago to restrict the sort of aggregate information to say well, you have to be signed in to the application to be able to see those. So I sign in and I also see my sort of user profile information up here. Okay, and now when I sign in and look at the search results, I see my saved results. And that was the star property button that we added last time that allows us to trigger this mutation that we run. So here starred property, which is using the userId that is injected into our GraphQL context object, thanks to the JWT Middleware that we're using, so we take a look at index.js. This JWT Middleware is what we added last time that is grabbing the token that comes in on the GraphQL request header and it adds the claims to a user object, which means that here, when we spin up Apollo server and we're setting the context object so this context object is past every resolver in our graft old server implementation. But what that means is that we can use this request, at user.sub value so that's our userId, which allows us to inject that into our cypher query that we define here. Cool, so what we wanna do today is to take a look at GraphQL mutations. So mutations, we've done a little bit with where we, for example, we added our starred property mutation to be able to connect properties that users have starred. But what I wanna do today is look at the generated mutations so in (indistinct) GraphQL.js when we creates our augmented schema, mutations are added for every type for creating, updating and deleting both nodes and relationships. And so what I wanna do today is see how we can add that functionality into our react applications. How can we create data from forms basically in our react application? How would we add a new property for example. But we don't want anyone to just be able to add data into our application, we only want certain users. So we wanna see how we can add permissions and protect our GraphQL mutations so that only users with the correct permissions are able to create and update and delete data. So that's our goal for today. Let's take a look at the documentation for mutations, look at the generated mutation. Cool, so using this example, we have it here so in simple one movies and genre. We've seen the generated queries that we get. And what about the mutations? Well we generate, for every type, create, update, delete and merge mutations. And then for relationships, the naming convention there is Add and Remove. So if it's create, that's creating a node, if it's add, that's creating a relationship. Delete is deleting node, remove is removing a relationship. So let's take a look at the generated mutations for our API, which is running at 4,001 GraphQL. And if we'd go to the docs figure, you're interested in these mutations. So here's all the mutations there's quite a few of them if you think, essentially there are several operations that are generated for each type. So for city, for example, there's CreateCity, DeleteCity and MergeCity and then also UpdateCity. In this case I guess we don't have an UpdateCity since city just has a single field which is the name. Is that why we don't have an UpdateCity, yeah. For example, we have UpdateProperty so that the property, we can find an existing property and update any of the existing values. Alright, okay let's start with a simpler example here. So let's create a city, so we'll say mutation and the mutation I wanna run is CreateCity. And in this case, since city has just a single field, then it just takes a name argument. So let's create, and then San Mateo and this returns the city object that it creates and then shows us that's created that in the database. From the chat any plans using cypher template strings for syntax highlighting. Yeah, so we saw last time that we can actually use this (typing) cypher template literal ops out there. Yeah, that would have worked, but yeah that we can do something like this to get syntax highlighting first cypher statements that we embed. But yeah we said we needed to tweak the way that we import our schema to use those which we just definitely do but I have not done yet. But yeah I agree syntax highlighting is nice. It's kind of interesting when you have sort of your cypher statements kind of embedded here as arguments in your GraphQL schema. I think a lot of examples I've seen the cypher statements are sort of moved elsewhere into other files and then sort of imported and exported. That can be kind of easy to create a very large type definition file, especially when you're adding in lots of different types and lots of different cypher directives. Okay so we've created a city, let's jump to the database, so here's Neo4j browser MATCH (city) so here's all of our cities, let's find the one with the name San Mateo. I'm sure we actually created something, yep there it is so we created this database as a single property name. Okay let's look at one that's a bit more complex. So let's create a property. So if we look at property, there's a lot of inputs here, a lot of arguments. If you look at our type definition for property, you can see that we have a lot of optional fields. Originally we had inferred and generated this schema after we did our data import. And if you remember the data that we originally got, we downloaded that from the state GIS websites and a lot of these fields were missing for some. And so when we generated this type definition, any fields that existed for all property nodes in the database those get the exclamation that bang, and it means that this is a required field. So any of those in the mutation, we're gonna have to specify those in the mutation, any of the other fields, the notable fields, those are optional. So if we wanna create a new property it's gonna have quite a few fields, let's see what we're gonna have. We're gonna have to have a County CD that's an integer, not sure what County CD is actually. what else are we gonna need? Continuous, that's like number of continuous acres I think, that's afloat. Fallow acre also afloat. Increase a bit, more readable, fallow acre, farm site acre is required. Let's say we have a property with, I don't know, one acre. Forest acres, I guess these should be zeros. If we have one acre for the whole property, we shouldn't have some of them fallow and some of them forested but anyway, just acres. So this I think is like the total number of acres in the property grazing. I don't have any grazing, property irrigated. We don't have any of that so this is for things like farmland, I guess, non-qualifying, not sure what non-qualifying acres are. What else is required? PropertyID is required here is an ID which we can think of as a string. We have the PropertyID and then we have shape area which is afloat. So again, I think that's maybe in square meters or something like that based on the shape file. Length tax year, there's an integer total acres (typing) is afloat. Total bill, so this is the total value of the building (typing) total land value. Let's say that's 50,000. And then the total value is gonna be these two so 150,000 And wild pay acres (typing) is also required (indistinct) of those. And then we have this ID field. So the ID fields in our create mutations are special fields because even though in our type definition, we said that these are required. These are actually optional fields in the mutation because what happens is if we don't specify them, then a random DUID is generated for us. Let's also add an address here. We'll say that this is 111 East 5th Avenue in San Mateo. Okay and I'm not gonna specify an ID, let that be automatically generated for us, we have ID and address. Okay, it's complaining about a few things. Let me see what field to create property argument total value of type Int is required. And a total land value, the total value. And then this one, and we see the error there...this is real nuts expected type Int, propertyID is an Int not an ID, okay. There we go. Okay cool so let's run this, the other thing I wanna add, that's not required, is location. And location takes an input type that takes a latitude and longitude, let's see (typing) Let's see if we get a latitude and longitude for this that I'm looking for. 111 5th Avenue, San Mateo. Like if I just ask for longitude and latitude will they give it to me? No, that's okay, we'll get it from Google Maps. So I think if we right click and say what's here, then yeah, here we go. Then I get a latitude and longitude that I can copy. Okay, so our property has an address, a latitude and longitude, let's create it. And it's a says okay, here's your ID. Let's copy this because we might need this later and your address. Okay so now if we go to the database and I wanna match on a property address, let's say, where p.address contains 111 East, 5th Avenue RETURN p we get back this node that we just created through the GraphQL API. Okay, cool. Now it's not connected to any other nodes so the next thing we wanna do is create a relationship. And specifically what I wanna do is connect this property to the city node that I just created for San Mateo. And so to do that, what we'll do is we're looking in docs. Well we said that the add mutations are for creating relationships. So we have this AddCityProperties mutation and this takes a "from" and a "to" input type. So it takes a property input, there's a property input here, which is gonna be the ID field of the property. So that ID field on our GraphQL types, I said had some important meaning. And that's how we reference these objects, in this case, that's a node, but essentially that's how we referenced these nodes as input types when we want to do other operations with them. So when we want to, say like, create a relationship, when we want to update the values on them, we refer to them by that ID field. I think of it as kind of like a primary key. If we don't have a field that is of type ID well, then we can use...city input is only one in that case then there's sort of an algorithm. So we look for any fields that are of type ID. If there's not, then we'll use sort of the first available non-nullable field, which in this case for a city, it's just name because that's the only field on the type. You can override that sort of primary key selection algorithm. If we look at our GraphQL Schema Directives, there is an ID unique and index directive, which we can add to indicate that a certain field should be used as essentially the primary key or that a unique constraint should be created. This is a fairly recent addition to (indistinct) GraphQL.js. Especially the unique and index directives are especially useful because we can then generate the database constraints and indexes automatically just from those directives, if we take a look at the assertSchema function. So assertSchema, if we run that passing in our schema and a driver instance that will look at all of the ID and unique directives and generate the database constraints and indexes based on those directives. Anyway okay, what we wanna do now is create the connection between the city and the property indicating that this property is in San Mateo. So we're gonna run another mutation, AddCityProperties and it takes a "from" which is the input for our property so the property input. So it wants the ID of our property, and then the "to" argument is a city input. And in this case, that is just the name of the city. And then this returns a "from" object, which is the property and a "to" object (indistinct), which is the city. Of course so we run that and says yep, I created this relationship from this property node to the city node and if we go to the database now we can see that this property node is now connected to San Mateo. Okay so we saw how we can use these mutations to create data, how we use the ID fields and the input types to refer to these nodes. There's lots of other things we can do with these mutations, like updating and deleting, but let's skip ahead and talk about how we can add some security to our GraphQL API to ensure that not just anyone is able to create and update data in our application. So if we look at the documentation for our Authorization Schema Directives, we saw that we could add this isAuthenticated schema directive to our types. And that's what we've used here to protect a couple of fields we can add it to the type as well. We have the hasRole directive which we can also add and then that requires that the existence of certain role claims in our access token. We took a look at this one I think a few sessions ago, if we go back to the overview video that was this one, we took a look at using that. But what we didn't see is how to protect the generated mutations right? Because these are only things, these directives that I'm adding hasRole and isAuthenticated. Those essentially we can only apply to queries or our custom mutations, right? So I could easily add a isAuthenticated directive to our custom mutation that I created, but how do we protect our mutations? Because those are generated for us automatically. We don't have a way to sort of manually add these directives in the schema. And so that's where hasScope comes in. So hasScope, this is similar to the hasRole directive however, it's looking for scopes that... here's an example so in this case, the hasScope directive is added to the createMovie mutation saying that well you must have this scope so your JWT token must have a claim that says you have the scope among possible other scopes to be able to execute this operation. But because these mutations are generated, we just have to toggle a configuration here to enable those. Here we go, attaching directives to auto-generated queries and mutations. So last time we...if you look at index.js and our schema configuration here, we enabled the isAuthenticated schema directive. And that just sort of added it as a declaration saying that, yep, we can use that in our type definitions, but if we say hasScope: true and if we enable the hasScope directive in our configuration, what this will do is it will add the appropriate hasScope directive to now all of the generated queries and mutations. So now in order to be able to, in this example, CreateMovie I need to have this CreateMovie scope attached to a token. So for example, if I try to run now this mutation again, creating this relationship, connecting the city and the property I just added, instead I'll get this error that says no authorization token. So now all of these mutations are protected and now I need to have a token that grants me the permission specifically for this operation to be able to do that. From the chat there is a role authentication answer as well that have no recent GRANDstack. So we will see specifically how... we kind of skipped over roles here, but we'll take a look at roles and specifically how we can map Auth0 roles to scopes, to enable our mutations. Cool okay, so now our API is now protected, no one is able to sort of just come to our API and start executing mutations to create data. But now what we need to do is attach tokens with the appropriate claims for our scope. So how are we gonna do that? And to do that we're going jump to Auth0. So we've been using Auth0 for a couple of sessions now I think and Auth0 you can think of as, the way I think of it is a access token provider. If I think of, really it's taking care of a lot of things, it's taking care of authentication where I'm sort of saying, you can use these social sign-ons, you can allow the user to create a username and password authentication and figure out how to authenticate them. But then also it handles a lot of the authorization aspects. It allows me to define custom rules. So for example, here's a rule that... I originally created this for a different application, but it's useful to look at now. So if we remember the hasRole example in the documentation, so here, so hasRole. So here we're saying if we access to the movie type, you have to have the role "ADMIN." Well how do you get the "ADMIN" role in the access token? Well, there's so many ways to do that. The way that I've done that in the past is by using rules in Auth0 and it's a rule in sort of like middleware. So a function that runs after the user has authenticated with Auth0 but before the token is generated and in this case, what we do is we check to see if the email address ends with either GRANDstack.io or neotechnology.com. So if they work for Neo4j or if they have a GRANDstack email address, then we assign an "ADMIN" role. If not then they're just a regular user. And we namespace it in this grandstack.io/roles claim and then that gets attached to the token. Okay so that's one way and if you wanna see that one being used that is let's see (typing) here we go, the GRANDstack Starter Auth0 Demo. So this shows how to make use of that hasRole schema directive, where it's protected certain types and it uses that Auth0 rule to sort of set users as admins if they have certain domains in their email address. Okay, what we wanna do is to protect our API. So last time we were using this as the API audience. So just the default API that comes with Auth0, but really we wanna create our own API because this will allow us to add some custom logic for how we wanna handle the permissions and such. So let's say this is willow.GRANDstack.io doesn't have to be an actual URL, just some way to logically identify our API. Okay and then some code samples, that's nice. In our settings, let's see anything that we wanna tweak here. I think that we can enable RBAC, role-based authentication. So this means that roles and permission assignments will be added to the token and then we want to or will be evaluated and we wanna make sure that those are added to the access token because that's what we're going to check in our GraphQL Auth directive. So what we're saying here is we wanna use Auth0 to define some permissions for our users, so that we can then attach those based on roles that we define so that certain users will have certain permissions and certain users will have others. Okay changes are saved and then permission. So here we need to define all of the possible scopes. So the convention that the GraphQL Auth directives package users and looking into GraphQL.js is the name of the type and then the operation, which is not quite consistent I guess with some other ways that you may see this you may see this as something like lowercase, CreateCity, something like that, but that's the convention that's used. Maybe we should change that. Anyway, city create to create city node. What else? Property creates (typing) creates property node. And then we also want to add the read permissions because here we enable that hasScope directive. We even add the hasScope directive to the generated query fields as well. So we wanna be able to read property nodes or property:read. From the chat that URL could be something random that doesn't exist, yeah, exactly. So this willow.grandstack.io is not the actual end point for the API, just some way for us to identify it. Okay so we've defined all of the permissions that can exist, now we want to define users and roles. So we can look at all of the users in our user database. So that's one service that AUTH0 has that is quite nice as it maintains for us a database of users. So we can see who's logged into our application, we can give them certain permissions and so on directly, but we can also create roles and a role can be like a group of permissions then we can assign those to users. We can even use the rules. It's that sort of middleware function example that were looking at earlier to assign roles to users and based on something programmatically. The example I looked at was their email address had a certain domain but we could also call out to an external system, maybe I have another database somewhere, maybe I have a list of... and in this example, we're dealing with real estate data. Maybe I had a list of realtors and I wanna verify their email address or their user ID against some external system to verify that they are a realtor. And if they are, then I'll add them to this role, which will then give them the permissions they need to create properties in our system. So we're gonna create a new role, let's call this a realtor. So if you're a realtor then you should have permissions to create listings in our application. A realtor is the name of the role description is real estate agent permissions. Okay, now we need to add some permissions to this role so we want use the Willow API, and we're gonna add all three of these permissions. So city create, property create and property read, I'm gonna add those permissions to this role. So anyone, any user who is assigned the role realtor now will have these permissions and when the token's generated what this means is that these will translate into scopes that are attached to that Auth token, which we can then verify using the GraphQL Auth directives, package and our GraphQL server. Okay and we could define some rule that when the user first authenticates that it checks this against some list of real estate agents or something, or we can just manually add users. So let's add me, this isn't me this my get hub user, yup. So we can manually add users to this role. So now when I authenticate, I should have the realtor role, which maps to these permissions, which then get added as claims to... as a claim rather to my JWT token. Okay, so there's one thing we need to change in the front end of our application. So web react, I think index, yeah. So right here where we created our Auth0 provider previously, we were lazy and we just pointed this at the existing API that we get when we create an Auth0 tenant, but we wanna change the API audience now to point to our Willow API, so we'll change the audience prop here, and let's jump back to our application here and we get an errored, why do we get an error and what is it? Well, if you remember, we said that enabling the hasScope directive, so here in the API, and we say hasScope is true. That automatically attaches, that hasScope directive to every query and mutation, which means now that you have to be signed in at least to use the application. We could define some custom mutations and those will not have the hasScope directive added to them. So that might be something that we could do in the case where we wanna have some functionality in our application enabled for users who are not signed in. Okay, so we sign in, go through the Auth flow. I opened the network tab here because I wanna be able to hijack that Auth token. So this is Willow GRANDstack and it's requesting access to your GRANDstack tenant, yup, sounds good. Okay, and now that I'm signed in, I can see this data, the aggregate data and now I can see the list of most expensive properties. Okay so that's good. Let's take a look here at the GraphQL request and if we scroll down to the headers, here's the token, what I wanna do is grab this token and go to JWT.io, which allows us to decode tokens and see the claims on them. And if we take a look here, here are the claims in that token. So you can see the API audience is now our Willow API. And the thing we're interested in here is now we have this permissions claim, which has the permissions that we added to the realtor role. And the only reason that I have these is because I explicitly assigned the realtor role to my Auth0 user, if I sign in with someone else that user will not have these permissions, because we haven't created a rule to assign those to that user. So they won't be able to do those operations against the API. And so it's specifically this property read permission, which is allowing us to see...make this smaller, allowing us to see this table, because that's now assigned to the property read hasScope. And if we go to the search We can see our starred properties and our search (indistinct) and the property details, cool. So that all works. So now there's just some chat about custom mutations. Yeah you can write custom mutations with cypher or exclude types and write your own as well. Yeah so at the beginning we were talking last time about this mutation that we added last time to starred properties, so you could consider this a custom mutation, I guess, since we're sort of defining the logic there, but we can also, just like how we added this custom resolver that goes out to the Mapillary API and finds the photos for our properties using the Mapillary API, we can also define custom mutation operations that create an update data rather than just go out and fetch it. So yeah, lots of flexible options there. I guess what would be a good example of a more custom mutation operation that we could add here. So something that is beyond what we get to define in a cypher custom mutation where we actually have to write some code. Anyone who has any ideas, drop those in the chat, we can take a look at those. Okay, so what I wanna do now is take this token that we hijacked, and we were getting this no authorization token error when we tried to run a mutation. So let's try, let's do another CreateCity. So we don't, (indistinct) or anything. So let's create a new city node. What should we call it? Well, we did send this to San Mateo let's do San Francisco. Okay so I tried to run this without attaching any Auth token and I get this no authorization token error so what I wanna do is, in the header, add an authorization header that has a bearer token. And there's the token that I copied from our application that we got from Auth0, just adding that authorization header. And now when I run this, it lets me do it. And it gives me back the city node that I created. And that is because this CreateCity mutation, this is protected with the hasScope city create permission, which we added to our realtor claim or our realtor role here. So we have that permission and the token so it lets us do it. We can try to do... let's try to do one that we don't have permission to do. So we have City Create and Property Create. Let's try to create a neighborhood, neighborhood we don't have permission to do. So if we try to do CreateNeighborhood (indistinct) name, that's a good name for a neighborhood. About Sudden Valley, if we try to run this, it says we're not authorized for this resource. And that's because we don't have the Neighborhood Create permission in our token, because we didn't add that to specifically the realtor role. And, I don't know, that seems right, like a realtor, they can create properties, like they can create listings, but we probably don't want them to able to tweak our larger data model. So assuming we've already added all of the neighborhoods that should exist, then we don't want a realtor to be able to create new ones. Okay so that's fine. So I can create data in GraphQL Playgrounds, what I wanna do is add this to our react application so that I can actually, fill out a form type in some data and actually then creates that in our application. So let me go to web react source components, do a new file and what should we call this? How about let's create a CreateProperty.js So we're gonna create a CreateProperty components (typing) we do react. Oops, there we go. Need the gql, GraphQL template tag and we're gonna run a mutation so we need the use mutation apollo/react-hook. And I know I've been using Apollo Client at 3 and some other projects and it has a bit different API. We still haven't upgraded this project to Apollo Client 3 yet. So this is using Apollo Client to that 5 I think, it was the last version before 3. So in Apollo Client 3 gql and the react hooks, those are all available from one package by default. Okay and then we need to create, CreateProperty mutation. So let's start off with a simpler mutation. We saw there were a lot of fields to add for creating a property. Let's start off with something simple and then we'll switch over to creating a property. So initially our CreateProperty component is gonna create like we've been doing city so let's do the create a city mutation first just to show this works. So we'll say mutation, CreateCity mutation. So, because we're gonna use a form here and so what we're gonna do is define some GraphQL variables that we're then gonna grab from the form. And so what I wanna do is declare some GraphQL variables in the mutation and then the actual values for those we'll grab from the form when we execute the mutation. Okay so we'll say CreateCity mutation, and then declare a GraphQL variable city, which is of type non-nullable string. And then, so that's just sort of our declaration. And then we want to run the mutation operation, CreateCity passing in the city GraphQL variable and then RETURN (indistinct) name. Okay so that's the mutation we want to run and then (typing) our component, functional react component we'll call CreateProperty, let's see so I'm gonna use a "ref" here just cause it's a fairly... it's the lazy way, I guess, to deal with forms and react. So there's a few different ways of dealing with forms in react. So one thing that we can do is have a controlled form component where I've sort of bound the states in my react application to the state of each of the inputs. So like an HTML input likes to maintain its own state. So it has, for example, like the value, right? So as I'm typing something in, the state of that element is changing. So if I bind that to the state of my components, that's sort of the more react way to do it, but I can also use a "ref" and a "ref" is basically just a way that allows me to sort of still have that HTML element, maintain its own state and it sort of gives me a reference to that HTLM element sort of outside reacts Virtual DOM, essentially. So I'm gonna just declare this input variable, which I'm gonna use to hold the value from our farm input, which initially is just gonna be the name of the city. And then we want to use our use mutation hook. So that looks something like this, we'll say, CreateProperty and then we also have the data that (typing) use mutation function returns. So we'll say use mutation, that's our react hook passing in CreateProperty mutation that we defined above. And then in our component that we're going to render a form and we'll have an unSubmit handler for the form, that handler takes an event. And we wanna do a default prevented, a preventDefault. So when by default, when you submit a form, we do a post request, which triggers a reload of the page, but we don't want that to happen here. So we're gonna say e.preventDefault to prevent that post request and relay to the page from happening. What we want to do is (indistinct) this should be a lowercase see this is the name of a function CreateProperty. So we're gonna run or execute our CreateProperty function when we submit the form and we're now going to pass in the value of our GraphQL variables. So we defined city as a GraphQL variable. So we're gonna pass in input.value. So input is what we defined here, we'll see when we create our input elements using a "ref," we'll see how we're gonna bind those too. A question from the chat, what about loading an error along with data? Yeah so here, when we call the use mutation hook, we get back first a function that we can use to execute this mutation and then here we get back an object that has the data and then it also has like loading and error. If we have either one of those states after we execute our mutation, yeah we can bring those along. I'm not sure what we're gonna do yet with the result of the mutation. We'll sort of submit our form right to the database and then yeah, I guess then something has to happen. So sure we'll bring those springers along and figure out what to do with them. Okay so when the form is submitted, we prevent defaults, we execute our mutation, we pass for variables. City is the value of our input which we have not defined yet. And then let's just reset the form after we submit it, after we execute our mutation. Okay so that's the form, our form, it needs an input. And here's where we're gonna create our "ref." So you say "ref" and then this allows us to kind of bind something to this element and we'll say, input equals this element. So now we can use this variable inputs to refer to our input elements using a react "ref." But also you need a button (typing) type "submit" Add City. Okay I think that is all we need here. So we should have a form with a button which is a single input fields. You can input the name of the city and it should execute this mutation and create a new database. Okay, cool, so next thing we need to do is make sure this shows up somewhere. So if we jump to App.js, I think it is, so App.js this is what has, if we look at our application, App.js is what has kind of the header and this toolbar drawer over here. So we want to import, what did we call this? CreateProperty I think, from components CreateProperty input that component and then some where we have... well first let's add it to our routes. So we're using react router here to keep track of the possible routes. And basically what this means is that when we're at a certain path in the URL, so when we're at the root just slash, then we render a component Dashboard, we've got a /search then we render the search component. For businesses we render a UserList that I think is a remnant from the GRANDstack Starter that we're not using. So let's create a new path here, let's call it "create" and the component we'll render in this case is CreateProperty. Okay and then we need to add that up here where we've got...so these are the, in this drawer, these are the links and the icons here. So let's just copy our /search link and add that to the list. So this will be /create, and the text will be CreateProperty. Now we have the people icon, we can do better than that, (typing) material ui icons. What do we want? we want like a house. Yeah, this one called house let's use that. Import house as HouseIcon, it's good. This is a convention I kind of like, of aliasing. So when you import the icon you have like people from icons, but it's nice to kind of let you know that it's not the people component it's the PeopleIcon. So we're gonna follow a convention of aliasing that to HouseIcon. And then instead of PeopleIcon, it is HouseIcon. Okay is that all we need to do? I think so. So create property. Let's add a city. What's another good city to add, how about Burlingame? That's right next to the San Mateo. So I have the network tab open here let's see what happens. Notice that we're not logged in. So when we restarted the application on that, that logged us out. So we're not authenticated, so we should not be able to create any data here, you click Add City, what happened? Oh, we got an error. No authorization token, yup that's right. We needed to log in. Okay, now we're logged in. Let's create Burlingame, Add City. Okay, it clears out our form field. If you take a look at that GraphQL request, see it had our token attached to it, did a CreateCity mutation passing along City Burlingame as a GraphQL variables. If we look at the response and the data, let's give it back the name and the type name so, okay. It says it created that we can verify if we go and in the database now MATCH (city) turn all of our cities. We should have a Burlingame in here somewhere. Here it is Burlingame, yup, cool. So it created our data in the database from our form, even though we had it...even though we had that mutation protected using the hasScope directive... these flies are killing me. Okay, cool so that I think is what we wanted to accomplish to actually switch this for actually creating a property. That's just gonna be a matter of filling out, the rest of the form here so that we have all of the proper fields and then grabbing those variables when we execute the mutation. There's a library that I've wanted to try called frontier-forms, and this looks really neat because this takes away a lot of the boilerplates of adding these form fields and basically just binding the values of the form field inputs to variables in your mutation. And the way frontier forms works is you just define a GraphQL mutation and then you pass that mutation to the frontier component and it figures out what form fields to generate and then takes care of all of that sort of boilerplate for you. Which I like, I think that is pretty promising. So essentially you're just like defining your forms, using a GraphQL mutation. I haven't actually tried this, but this would be nice in this case so that we don't have to define all of these form fields for all of the different arguments they wanna pass to create our property node in the database. So cool, I think that's probably enough for today though. So maybe next time or maybe some time in the future, we can take a look at getting this to work with the frontier-forms package. I'll drop a link to it in the chat here though. It looks promising. Cool, so we'll call it good for today. So just refresher of what we did, we saw a bit on how to use the generated mutations with Neo4j GraphQL.js. We saw how to protect the generated mutations using the hasScope directive here, by adding that to all the generated mutations. Then we saw how to use Auth0 to create roles and permissions and assign those to users in Auth0 so that we could grant those permissions that then get picked up by the authorization directives that we've defined in our schema. And then we saw how to add a form in react to execute that mutation using the token from Auth0 so that it passes the authorization rules that we defined. Cool so I will push this code up to GitHub. I will not be around next week. I'm on vacation next week and I'll be in the woods. So we'll skip next week stream, but then we will definitely pick it up the week after. So the second Thursday in September is when I will see you again. Cool, so thanks for joining today and I will see you next time, cheers.
Subscribe To Will's Newsletter
Want to know when the next blog post or video is published? Subscribe now!