In this chapter, we're going to go ahead and develop the agent's form. This will include creating a list header component, which is going to render the My Agents text and the New Agent button. Once we press on that button we are going to use previously created responsive dialog and render the agent form inside. In order to achieve all of that we are going to have to implement a protected procedure, agents.create procedure, the list header component, the new agent dialogue, and the actual agent form. And this agent form will be reusable for both create and update actions.
So let's go ahead and start with protected procedure. So right now, if you go inside of source, drpc, init, all you have is a base procedure. And for example, in your modules, agents, server, procedures, for getMany, we use a base procedure. This is the equivalent of an API request, which has absolutely no security at all in the sense of protecting the authorized route. So that's what we're going to do in this chapter.
We're going to create a protected procedure. So before you start doing that, as always, Ensure that you're on your default branch, and feel free to click Synchronize Changes just in case something was left over. Go ahead and npm run dev your app. And now we're going to borrow the way we protected our dashboard page.tsx using the session, like this. So you can copy this right here and go inside of trpc, init.
Now in here, we're going to export const protected procedure and we are going to extend on top of base procedure. Technically, you could also do t.procedure, because that's exactly what base procedure is, but just semantically, it makes more sense that our protected procedure is based on top of a base procedure. Now obviously if later on you modify your base procedure to do something specific for you know public routes you wouldn't want to build the protected procedure on top of that right but for this kind of arrangement, it makes sense. So base procedure dot use async and extract context and Next. So this is a kind of middleware.
And now we're just going to paste the session await from out, which we have to import from lib out. So make sure you've added this import and import the headers from next headers. And now we have our session using the server side out adapter from better out. And what we're going to do is check if there is no session, throw new TRPC error here. Make sure you import the TRPC error from at TRPC server, code, unauthorized, message, unauthorized, or whatever you want to throw.
And the important, throw back next and extend the context. So we're going to spread the existing context and under key out, we are going to store the current session. So now, after this validation passes, we will have access to the currently logged in user information in all of our future procedures. So now we have the protected procedure here. So, let's go ahead now and do the following.
Let's get inside of modules, agents, server, procedures. Leave this as base procedure for now. So I'm going to add a little to do change, get many, to use protected procedure, you know, just in case we forget. But for now, just leave it like this. Instead, let's create the create procedure using the protected procedure.
So make sure you import this. Great. Now, inside of here, we're going to add an input. And inside of the input, we are going to add the schema for create. So let's go ahead and do this.
Inside of the agents module, create a new file called schemas.ts. So it's like this, it's not specifically in the server folder nor the UI folder, it's schemas for itself in the agents folder. In here, import z from Zod, export const agents insert schema, z object, name, string with a minimum value of one, and a message, name is required, and the same thing for instructions. Like this. And now let's pass in the agents insert schema here.
So the reason I have done it in a separate file is very simple, so I can later reuse it for the form. Great. So protected procedure.input, and then let's change .mutation because this is not a query, this is a mutation. So in here, we grab async again, and we can export... My apologies.
We can destruct input and the context from here so what is what in the input we can grab things like our name and instructions, basically the things that will be added through a form. But from the context, we can now destructure auth. And auth will hold our session, our user ID, and our user information. You can see even some more useful things here. So that's what these two hold.
So let's go ahead and do the following. Const data or we can do this, you can immediately destructure the array and go ahead and write created agent to be await database. You already have it imported because we used it here. So database.insert into agents schema, values, and in here, spread the input, and attach user ID from context out user ID. And make sure to chain dot returning.
There we go. So now when you hover over created agent, you will see that that will be a new created agent. If you forgot to add returning, then it's just a type of any because it's not returning anything. If you're wondering why do we need to do it inside of an array like this? Why not just like this?
That's because by default, Drizzle always returns an array because that's the way SQL natively works, right? So that's why we have to destructure the first item in this array because we know that even though this is an array, we are only creating a single record. So it's safe for us to simply access the first item in this array immediately in here, calling it createdAgent. And once you've obtained the createdAgent, you can simply return the createdAgent. And what's cool is that this is a fully validated and out protected procedure now, even though we didn't explicitly write any checks here.
So if the user forgets to pass instructions or the name for the agent, this will cause the procedure to fail. And if the user is not logged in, this, the protected procedure, will cause it to fail. So that's how we have abstracted that into our safe API, safe procedure. Perfect. Now let's go ahead and create our list header component.
So let me just go to our app here, make sure you have your app running, of course, and let's go inside of agents. So we are on the same page here. And in here, let's go inside of source app folder, dashboard agents page. And now what we're going to do is the following. So you might think that we will put the ListHeader component, which is this, the TextMyAgents and the NewAgentButton inside of the AgentsView, right?
Maybe add it above this. We are not going to do that because it makes no sense for that part to be shown an error or for that part to be loading. That part never needs to load, it's static, right? We are not loading the agent name, It's just a hard-coded text, my agents. So what we can do is inside of this server component, we can simply render some things outside of this hydration boundary here.
For example, list header, like this. And now we are going to go ahead inside of modules, agents, UI and create a new folder called components just so we semantically differentiate between the two and let's call this one list header dot TSX. If you want you can call it agents list header if you want to be more specific. Let's export const AgentsListHeader like this. Let's just call it ListHeader and in here import AgentsListHeader from modules AgentsUIComponents AgentsListHeader.
So now you will see that this list header is automatically displayed regardless of this being in the loading state and that's exactly how I wanted it to behave right There's no point in hiding this while it's loading. Perfect. So now let's work inside of the agents list header here. And let's give this div a class name of py4px4. And let's do mdpx8 flex flex column and gap y of 4.
Now inside of here, add another div with a class name, flex-item-center and justify between. And in the first element here, which is the heading 5, write my agents. And then below it, add a button from components UI button with the text new agent. So make sure you've added this import here. And also, yeah, mark this as use client.
Because by default, if you just put a new component inside of a server component, it will be a server component until either the parent has used client or the component itself has used client. And I have to explain this just in case you got confused now. So if you were to mark this as useClient, then yes, every single child inside of here would automatically become a client component as well. But that can be overridden if you used children, for example. So if you inject children into a useClient component, then everything will become a client component besides the children.
Children act like a portal outside of it. So I just needed to explain that just in case you jumped to some conclusions because I often get comments asking about that. I will try to find a better example because this wasn't exactly the best way to show it. So yeah, just make sure you didn't change anything in this agents page besides adding the agents list header here. And now let's see how this looks like.
There we go. The text my agents and the new agent button. So now what I want to do is I want to give this h5 element a class name of font medium and text extra large. There we go. My agents, new agent.
And for this button, let's add a plus icon here. From Lucid React. And give the plus icon a class name. Actually, I don't think you need to do anything, it will automatically have the proper class name. There we go.
Great. So now, what we have to do is we have to enable a dialog to open when we click on the new agent. So for that, we're going to go inside of this Agents, Modulus Agents UI Components and we're going to create a new agent Dialogue.tsx and in here, import our previously created responsive dialog from components responsive dialog. Create an interface, new dialog, new agent, dialog props, open Boolean on open change. A simple toggle for that.
And export const new agent dialog. Let's go ahead and assign the props here and destructure them. Open on open change and in here return the responsive dialog like this. Let's give it a title, new agent, a description, create a new agent. Open and on open change.
And inside of here you can just write new agent form. There we go. So we now have the new agent dialog. So we can now go back and set up the agents list header, encapsulate this entire component in a fragment. So we are semantically correct with the positioning of our new agent dialog, even though it uses a portal, right?
So it doesn't really matter where you put it, but I like to be semantically correct. Now in here, let's add is dialogue open and set is dialogue open to be used state from React with default false. Let me just align these imports like I like them to. Let's set the open here to be isDialogOpen and onOpenChange to be setIsDialogOpen. And modify the button on click here to modify set is dialogue open to true there we go so now when you click new agent you get the new agent dialogue and in case you are on mobile you will get a drawer so exactly as we've developed in the previous chapter.
Perfect. So what we have to do now is develop the actual new agent form, which will be compatible both for updating and for creating the agents. So let's go inside of the components here. So ModulusAgentsUIComponents, Agent-Form. It's unspecified whether it's a new agent form or update agent form because it's one form to rule them all.
So in here, let's create an interface agent form props. And inside of here, we are going to get on success to be an optional type of callback, right? Same as on cancel. So these won't be required, but it's good to have them in case you want to redirect the user after a success or if they cancel, right? And then we need the optional initial values, right?
But right now we have no idea what the type of the initial values is. So how about we create this? And the way we can do this, the way I like to do this is not by reading what's inside of my schema. That's one way of doing it. We could infer this from Drizzle.
But what I like to do is I like to know exactly what my procedure returns back. So that's the way I like to do it. So this is what we're going to do. Let's quickly create a get one procedure. So get one.
And the reason that I'm doing this is simply because when the user goes about updating the agent, they will first have to fetch that individual agent. So that will be done using the getOne procedure. So let's also keep this as base procedure for now. It really doesn't matter. Let's just add two comments here.
So change getOne to use protected procedure. So let's just add base procedure here. It's okay. And let's add the input here to be z.object. And I think we need to import z here.
So import z from Zod like this. And in here, just set the ID to be a type of string, We are basically fetching the individual agent using ID. So select from agents, where, and then add equals from drizzle ORM, like this. And all you're going to check is whether agents.id equals input.id. And you have to destructure the input from here.
And then you can use it here. And then in here you can destructure the individual existing agent like this and return it. So when I hover over this, you will see that it's the correct type. So this is the initial values that the update form will be populated with. That's why I wanted to create this right here.
So make sure that you have added this. It's just a very simple temporary form. We are going to change it to protected procedure later. We will do the whole proper thing. Let's just leave it like this for now.
And let's go inside of agents, and let's create types.ts. And We are not going to type any types, we are just going to infer them here. So import inferRouterOutputs from at trpc slash server and go ahead and import type app router from at forward slash trpc routers underscore app so it's basically this export right here in our trpc routers underscore app folder. So this app router now has access to the entire agents router including the get one procedure. So what I can do now is export type agent get one and make it infer router outputs passing the app router so it has the context target the agents specifically the get one procedure.
So now when I hover over this, this is the type that I'm returning in this getOne procedure. So now we have this reusable getOne type, which is correctly inferred as our API will return it, right? Because you might think, oh, it would be much shorter if I just inferred it from the database schema. Well, true, but what if we change our get one base procedure and we only select the name, for example, and we don't select anything else? Then your schema will not be correct.
So that's why I like to know exactly what my API returns. So right now, for example, let's try and do this. I'm not sure if I'm doing it correctly. But let's say I use name agents.name. Is this how I do it?
And now when I hover over this, you can see that it only returns the name, right? So that's why I think it's important to use the return of your API rather than your schema here, because this is what defines what will the initial values be. So now we have that in here. I just remove that select. You do it as well.
Okay, so we just did a small detour here. Now let's go back inside of UI components agent form. And we can now set the agent get one from our types here. And now we know what our initial values will be. Now let's export const agent form.
And in here, let's add agent form props. And let's this structure on success on cancel and initial values let's prepare the RPC from use the RPC here let's prepare router from use a router from next navigation And let's import query client from use query client from 10 stack react query. My apologies. Oops, no, no, no, this is incorrect. Use query client.
There we go. No need for this. So use router from next navigation use query client from 10 stack react query, use the RPC from add slash the RPC client and agent get one from the dice. Great. Now, let's go ahead and create our create agent method using use mutation from 10 stack react query.
And in here, simply pass the TRPC agents create and pass in the mutation options here. And in the mutation options, define onSuccessMethod here for now to be empty and onError to be empty as well. We're gonna come back to this later. Great. So now that we have this, let's go ahead and create our form here.
So const form will be use form from react hook form. And we're going to z.info and in order to do this we need to import z from zod. So z.info and we don't have to write any new schema we can just do a type of agents insert schema. Do you remember this? We've created this previously, just a moment ago, when we used it inside of our create procedure agents insert schema.
It's located inside of our modules agents schemas. It's right here. So I just import that again and then I can use it here. I don't have to write any new schemas. I can then add my resolvers, ZodResolver, from HookFormResolvers.Zod and FastEAgent's insertSchema and add my default values, which are name and instructions.
So make sure that you have imported the Zod resolver here and then make sure that you properly assign default values, because this is true for a new form. But if we actually have initial values, we should fill them. So go ahead and check if you have initial values, and then depending on if we are in .name or an empty string or if we are in .instructions or an empty string. So this way it's going to populate or fall back to an empty string. Great, so we now have the form.
Now let's create a useful isEdit method here, which will simply be a double exclamation point, initial values, question mark dot ID. So this is what we're going to use to determine whether this is editing form or creating form. And let's define const isPending here to be createAgent isPending. The reason I'm adding it in a separate variable is because later we're going to have updateAgent isPending. So right now, this does not exist, so that's why it's an error.
But let's already be prepared for this to be extended later on. Perfect. So now let's do const on submit method here, which gets the value z infer type of insert agent schema, Agents insert schema, my apologies. And if is edit, we're just gonna do console log to do update agent. Else, we're going to do create agent.mutate and pass in the values like this there we go perfect so Now what we have to do is we have to import everything from our form control.
I mean from our add slash components UI form. So form, form control, field, item, label, and message. Besides that, we're also going to use our generated avatar component, which we have already used in our dashboard user button in case the user doesn't have an image. And we're going to add text area. I think this is the first time using this component.
It's just a text area in your source components UI text area added from Shazzy and UI. And besides that, we're also going to need the usual input from components UI input and button from components UI button. And I think for now, that's all the imports we need. Let's go ahead and develop this form now. So after onSubmit, let's finally do a return here and open up a form.
Go ahead and spread the form from useForm here. Inside of here, add a native HTML form element. Give it a class name of space y4 and give it an onSubmit of form handleSubmit and pass in the onSubmit function. In case you're wondering, why am I doing a function inside of a function? Form.handleSubmit basically ensures that we didn't do anything wrong in regards to the schema, right?
So this part will prevent this from being called, which is the actual submit method, if the form is not correct. If it's too short, if there are any HTML errors being thrown. So now inside of here, let's add a generated avatar component here. And we're going to give the seed to be formed dot watch name. So every time you change the name of this agent, a new avatar will be generated according to that name.
The variant will be bots neutral. Let me just select it. There we go. And class name will be border and size 16. There we go.
Now let's add a form field which is a self-closing tag. Let's give it a name of name and let's give it a control of form dot control. And then finally a render method. In here we can immediately destructure the field. Let's add form item here.
Form label which will be the name. Let's add form control here. And let's render the input. And let's simply spread the field here. So let's go ahead and check it out.
Let me just say, okay, these errors are fine. These are just unused values. I think what I have to do now is go inside of my new agent dialog right here and actually render the agent form. From dot slash agent form. Now in here, what I want to pass is on success, on open change, false, and on cancel too.
So basically, once we complete the form, and once we, or if we cancel the form, just close the responsive dialog. So that's what we want in the callback. So now if you click here you should see this right How you name your agent will depend on the avatar that will be generated. And I think that's pretty cool because this way we have consistent avatars for an agent, right, if you name it John, it will have this avatar everywhere because we're always gonna use the agent name as the seed. Perfect.
So now let's go ahead and let me just see one thing. Yeah, this doesn't have a placeholder. So if you want to, you can add a placeholder here. For example, john, right, or, I don't know, maybe something like math tutor. Right.
Now, what we're going to do is we're going to copy this entire form field and we're going to use a instructions. Here, this will be instructions. And instead of using the input, we will use a text area, right. And in the placeholder here, you can add the no whatever you want to explain to your users what it is. So for example, it can be, let me just paste it.
You are a helpful, let's do math assistant that can answer questions and help with assignments. Like this, right? Whatever you want to add inside. There we go. Perfect.
And then what we need is after still inside of the form right but after the form control here add a div and inside check if we have on cancel if we do render a button and the text cancel. Give it a variant of ghost, disabled of isPending, and type of button. This is very important, otherwise it will submit the form because it's still inside of the form here. And on click here will be a very simple on cancel. And then you can add another button here.
Loading, actually disabled here will be spending as well. And give it a type of submit. Check if it's edit and then render update, otherwise render create. There we go. Perfect.
So now you have cancel and create here and let's just use this div to give it a flex justify between and gap x of two. There we go. So now when I click cancel here, I can close this. And the reason I can close it because in here, I have added a callback here to close it, right? Perfect.
So now, Yeah, I think that I forgot to do one thing here for the name also add form message here outside of the form control and do the same thing here. Great, right. So now if you try to submit this, you will see the errors. And yeah, it looks like inside of my agent schemas, I need to change this to be instructions are required. There we go.
Perfect. So now, let's just see what's being unused here. So this is what we're going to do now. In the onSuccess, let's do the following. Let's use query client to invalidate queries, PRPC dot agents dot get many and pass in the query options like this.
And later on, I'm going to add a to-do here. Invalidate individual... Oh, actually we can do it. Yes, my apologies. We can already do if initial values as the ID, we can also invalidate another thing called PRPC agents, what we just created, get one.
Because in here, we need to pass the ID. And in this case, that is the initial values ID. So what's going on here? Basically, when onSuccess happens, when we create the agent and we successfully do it, we are going to invalidate the getMany so that this right here refreshes. So we are gonna add a new agent and we want to immediately show that new agent in the table.
So that's why we need to invalidate those queries, right? And this is where you actually see the power of using prefetching in server components and handling the rest in the client components. Because the fact that in the page.tsx of our... Oops. Page.tsx in our agents, we do the prefetching.
It does not prevent us from continuing to refetch this through invalidation or any other method, right? So that's why I prefer using this way. You could have done the same thing of course if you used just normal use query here but if you actually load the data in the server component and then pass it as a prop you would not be able to revalidate it like this. That's why I really like this way of doing it. And also, this invalidated queries, I think that they are a promise.
So if you want to, you can mark this as async, and then you can await them like this. I'm not sure if you have to wait. Not exactly sure. I mean, they definitely still work, but you know. And let's also add on success here, but since it's optional, do it like this.
And two S's. There we go. So this will then close it, right? Why do I say it will close it? Well, because we added that to happen here.
Great. So now let's go ahead and go inside of the onError here. And what we have to do here is the following. So in here, we have to first go inside of our root layout. So in the app folder layout, and add a toaster from components UI sonar.
So it's not a wrapper, it is just a component to be added like this and then instead of the agent form you're going to get the error here and you're going to do toast dot error here error dot message And you can import toast from sonar like this. And I will just add a to do comment here. Let me just go here. To do, check if error code is forbidden, redirect to forward slash upgrade. So we don't yet have this, but we will have this in the future.
Great. So the only thing that is unused at the moment is useRouter. And there is something that, yeah, okay. Let's just remove use router then because we can't really redirect anywhere at the moment. Great, so I think that this is now ready to create.
Let's try it actually. So make sure that you are logged in and let's try a test and test instruction here. Instruction. Let's click create and let's see. I think that it works.
Test instruction, it's right here. There we go. Test instruction and test. Perfect, so it's exactly what we wanted. Let's try another one.
Let's click create. And there we go, it immediately adds that here. Perfect. Now let's go ahead and let's try an error. So how do we achieve an error?
Well, I think there is one way of achieving the error without changing anything. So if you go inside of source app folder dashboard agents page, you can see that this page is not protected. So unauthorized users can actually see this page. And I purposely told you that in the getMany you leave it as the base procedure, the same as getOne. The reason I told you is so I can demonstrate this.
So I'm going to copy this URL and I'm going to log out. And then I will go back here in the agents. As you can see, I can access this page even though I'm logged out. So this would be a very bad thing, right? But what happens when a logged out user actually tries to create something?
You get back what I now notice is a typo, unauthorized. Let me just quickly fix that inside of my trpc init, unauthorized. So even if you forget to do a redirect in the server page, right? Because in my dashboard page, I do this session and I do the redirect. So even if you forget to do that somewhere, You don't have to worry because that is our last line of defense.
Our first line of defense are going to be the protected APIs. So now what I want you to do is the following. I want you to go inside of your agents procedures, so modules, agents, server procedures, and now change this to protected procedure and you can remove this comment. And same thing for get one protected procedure and you can remove the base procedure input. And Now when you do a refresh here, you will see that logged out users cannot even see the data.
So even if you forget to protect your page and redirect the user away, you don't have to be afraid because our APIs are completely protected. As you can see here, we have this unfortunate maximum update depth, which I talked about previously, but you can see that eventually it stops, and it will just show the user an error. And if they even try to create something, they also get the unauthorized error. So there's absolutely nothing they can do. But let's just wrap it up with doing proper redirect here as well.
So we can just copy this from the dashboard, this entire thing here. There we go. And we can do it before we even add these, like that. So import out from lib out. Let me just move it here.
Import headers from next headers and import redirect from next navigation. So now, even if you refresh, you can no longer access that. So I just told you the worst case scenario, which is you forget to redirect from the server component, nothing dangerous will happen. That's because this is our last line of defense, right? This is not what's actually protecting our app.
TRPC and the proper use of procedures are protecting our app. That's what's handling the errors, and that's what's protecting the actual data. Perfect, so I think we've learned a lot in this agents form. So we have successfully created the protected procedure, agents create procedure, list header, new agent dialog, and the agent form which is reusable for both create which we demonstrated but we can't yet demonstrate the update. And now let's go ahead and merge these changes.
So I'm gonna go ahead and click on my branch here, create a new branch. This is 11 agents form. So 11 agents form. Let's stage all of these changes by clicking on the plus. So they are now all staged 11 agents form, omit and publish the branch.
Now let's go ahead and open this pull request. And let's wait for the review from CodeRabbit. And here we have the summary. So we have added a global TOS notification for improved user feedback, which is our toaster. We introduced a new My Agents page header with a new agent button.
We added a model dialog for creating new agents with form validation and avatar preview. We implemented an agent creation form supporting both creation and editing workflows. Perfect. So in here, of course, we have a more in-depth walkthrough. As you can see, it noticed that we added a protected TRPC middleware for securing the endpoints, right?
And in here, we have a whole sequence diagram explaining the state of our app right now. But this part right here, we've already covered this in the previous pull requests. What we are interested in now is in this part, when the user clicks on New Agent, and in here, it clicks it here in the agent list header, which opens a dialog, a new agent dialog, which finally renders the agent form. So the user then submits the form in the agent form, which then calls the create, which is the protected procedure. So that's calling the TRPC server, right?
The TRPC server is a protected middleware in this case, because it's a protected procedure. So it calls the session. If it has no session, it fails. But if it has a session, it inserts the new agent right here in the database and the database returns back the created agent to the TRPC server which then on success handles the rest of the events. So as I said these sequence diagrams will become more and more useful as we go on.
In here we have some comments, of course. We left a to-do here, so it recommended adding the update agent in case we forgot, but we did not forget. We actually don't have it yet. We just developed ahead of time. In here, it gave us the proper advice of handling non-existent agents.
So yes, when we actually go and develop this getOne method, which we are going to have to advance a bit further than this, we will also have to handle this, throwing an error if the agent is not found. So yes, this is a good suggestion and we will add that later when we develop the actual agent get one page. So we can resolve this and in here in the get many it tells us to actually protect this procedure even further by only loading the agents from that user. That is exactly what we are going to do in the next chapter when we load this in the actual table, right? Because right now we're just JSON stringify the agents.
So in the next chapter, we're going to develop the actual table for that. And that will include this fix as well. So All great suggestions, exactly what I plan to do in the following chapters. So let's go ahead and merge this now. Perfect.
And once you've merged it, go back to main and click synchronize changes and click OK here. And you should be all good. Let's go ahead and check our graph here. We just merged 11 agents form, which marks the end of this chapter. Amazing, amazing job and see you in the next chapter.