User-defined Functions in Fauna
with Rob Sutter
What if you could bring compute to your data? In this episode, Rob Sutter teaches us how to create user-defined functions in Fauna to implement custom business logic right in your database.
Resources & Links
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
Jason: Hello, everyone. Welcome to another episode of Learn With Jason. Today on the show we have Rob Sutter. Rob, thank you so much for being here.
Rob: Thanks for having me, Jason. Hey, everybody. Happy to be here.
Jason: I'm super excited to have you on the show. We're going to be doing some really, really fun stuff. Before we talk about what we're doing, let's talk about you. So for folks who aren't familiar with your work, do you want to give us a little bit of a background?
Rob: Sure. So I am a big fan of serverless. I'm currently the head of developer advocacy here at Fauna. The serverless database, don't call it that, it's the data API for modern applications. (Whispering) it's a serverless database. I'm going to get in trouble if the CEO hears that. I'm pretty confident he's not on Twitch. Let's hope I'm right. Prior to that, I was with the serverless team at AWS. So all the classics, AWS Lambda, event bridge, all those things. I love to build in public. I've done all manner of stuff outside of tech as well. I worked for the government. I worked -- I was in the military. So you know, here and there, all over the world. Just an all-around all-around guy.
Jason: All-around, all-around guy. I like it. Yeah, so Fauna, I think, is kind of -- well, let's set some context before we talk about Fauna specifically. So I think that a lot of the folks who watch the show are front-end developers. I'm a front-end developer. Over the course of my career, we come in contact with the need for storing data. What I found consistently is that a lot of times I'm more willing to talk myself out of an idea than deal with the process of starting up and hosting and figuring out how to deploy a database, right? So I feel like as front-end developers, we get kind of sold this idea that we should be full stack. Being a puck-stack developer is completely infeasible. Like, there's so many things you got to know. You can't really become an expert in all the things that you need. So then Fauna kind of comes in and it's offering something. You called it a serverless database, but it's part of this kind of new class of database systems that is really interesting to me as a front-end developer because I don't have to think about how to stand it up, how to deploy it, how to get it out there. So do you want to talk about kind of what is Fauna, how does it work for us?
Rob: Yeah, so I'll get to Fauna. I want to hit on a couple things you said as well. First off, like I'm glad you came out with the opinion of being a full-stack developer. There's just so much to learn. I'm a back-end developer. So everything that all of y'all do on the front end just mystifies and amazes me. I don't get it. We all make the joke about not being able to center a div. That's not a joke. I can't do it. Much less make something look feasible. So everything we do on the back end, y'all, A, have been doing on the front end, and B, making it look good at the same time. So I think it's fantastic what front-end developers do, and it mystifies and amazes me. I also think a lot of the points you said about Fauna, I think Fauna is a great match for front-end developers because you're already consuming APIs elsewhere. You want to process a payment in your app? You're going to Stripe. You want to send an SMS, you're going to Twilio. You know, you want to embed search, you look at something like Algolia. So you already know how to do all these things. You know how to get an API key and store it securely in your application. Like, all of these concepts that you're doing, you're just doing the same thing with Fauna. So you create a query, just like with GraphQL. It's either a query or mutation. You send it to Fauna. And that's it. Like stop thinking about the rest. You hit on it's so difficult to manage, even if you do it cloud first. You have to create a virtual private cloud. What's that? That's a whole category of problems we can talk about for a day. You have to get the security groups right. That's wide open. Now all my stuff is out there for everybody. There's all these things you have to go through just to get it functional. That doesn't even begin to get at what do you have to do to get it correct and performant and fault tolerant. All of these other back-end things that now I start to become in my element and nerd out on so you don't have to. It's just another service. But your API and your key authenticates your user, sends your call, store and retrieve your data.
Jason: Yeah, and that's really the magic of this stuff. We start talking about, like, as front-end developers, the thing that attracts me to this jamstack decoupled paradigm is that we're getting to the point where everything we're doing is building a front end and sending API calls to do everything else. So if I can form a JSON object and send a fetch, I'm able to do almost everything I need to do to interact with different back-end platforms. That feels like -- you know, when we start looking at Fauna, that is kind of what's happening here. We're no longer, like, I don't have to figure out how do I do a special connection. I don't have to sect up connects or open up a raw MySQL connection or any of those things. I don't have to think about what that actually means. I have a database, and I'm going to send a request to the GraphQL end point or the other Fauna database end point, and I'm going to get a response back. That's my data. I'm done. I've built a custom database as far as I need to care. I think that's what really makes this such a powerful idea. I as a front-end developer want to be able to build things. I don't want to have to think about deploying and scaling. There are companies that do this. As you said, Stripe just handles payments so I don't have to think about the security and the compliance and all of the infinite things that can go wrong with payment. And with database, it's the same thing. A database for me on my local machine, cool, whatever. It takes me ten seconds. A database that needs to work here and in Australia, that's a whole different set of problems, right. And Fauna is just making that go away. I think that's what's exciting.
Rob: It is exciting. It's hard to talk about Fauna because there's so much -- and like I'm a back-end centric person, but our engineers are just on a whole other level from me. There's all this world-class engineering that's going into Fauna. There's all these firsts in there. You really want to talk about them all the time, but quite frankly, nobody cares. What you want is a place to put your data and to get your data when it's there. So it's an interesting thing to try and not talk about so often when we're working with it. But yeah, just like you said, the fact is you put data into Fauna, you define your GraphQL schema, perform a mutation, and that is available for you in multiple locations either to read it or for resiliency. Really, that's all you need to know about it, that it's there and working.
Jason: Yeah. I mean, and I think when you start dreaming about this idea of full-stack developers, that's what unlocks that promise. If you're a front-end developer and you can do the website stuff and then you have services like Fauna and Stripe and all the headless CMSs, all these other myriad services you can pull up and use, you are now a full-stack developer because you've got these platforms filling in all of those scalability and dependability and reliability gaps that you would have to build a whole team around. But because these services are there and providing you with APIs, you as a front-end developer can now kind of build incredibly complex apps without having to build that whole extra set of skills. And it really is like gaining superpowers when you really start to put together what serverless and these platforms unlock for you.
Rob: Yeah, it's -- I think front-end development is the most valuable part of the stack. Don't get mad at me, everybody else. It touches the customer. It defines the customer experience for the customer. It's the most differentiating. It's always going to be the most differentiating, which means all the work you're doing there is creating some sort of value. Value for the customer, value for the company, differentiating you from your competition. You know, I like talking about do you understand power systems theory, do you understand supply chain economics and diversification of fuel sources? Because these are things that are still happening underneath your app, but you're not considering them right now. Your app is running somewhere in a data center, and that day center has to have power from multiple times, and those have to be -- these are all things that have been commoditized and stripped away.
Rob: We do have the smartest rocks.
Jason: We do. Smartest rocks in the galaxy here on planet earth.
Rob: You don't even have to get out a software for that. Name every register in your CPU. Oh, you just upgrade. You don't have to go down to that level either. It's folly to suggest.
Jason: So where we have to be as developers is -- honestly, just as humans. We're here to solve problems. We're not here to know everything. So we should choose the abstractions that enable us to solve problems. I can't -- I don't have the time to learn every single thing, but I do have enough time to learn this one common abstraction, which is an API. Because everybody's really starting to coalesce around this idea of you have an API end point, you have GraphQL or Rest or however you want to send these queries, you're able to send and receive data through a common interface. That means I only have to learn that. That's the boundary I have to learn up to. Everything else up to that is an abstraction. I don't care how Stripe processes payments. I just care when I send it data, I get a response back, and that response is what I need.
Rob: Right. Exactly right. And the differentiation in there is, you know, Stripe handles fraud detection and alerting so you don't have to run massive machine learning models and learn machine learning and learn theory behind fraud detection and all this. It's not only storage, right. There's so much specialization that they do. Similarly, you don't have to worry about learning, you know, distributed consensus algorithms and multiphase commit versus single-phase locks.
Jason: I don't even want to learn what those words mean.
Rob: In fact, I made one of those up.
Rob: Some savvy user out there can tell me what it is. My favorited distributed consensus algorithms. Raft and Sesame Street.
Jason: I'm actually worried it's not Sesame Street that's the lie.
Rob: That term was used without authentication from the Sesame Street broadcast. I regret the error. That was my mistake, not the show's mistake.
Jason: All right, y'all. So knowing all of that, talking about that, let's talk a little bit about specifically what we're going to try to accomplish today. We've got some content of how to get up and running with Fauna on Learn With Jason already. But we're not talking about that today. We're instead talking about something that's even a little more -- is advanced the right word? I guess just customized, which is user-defined functions. Can you talk about what this means?
Rob: Yeah, let's not call it advanced or customized. Let's call this a growth session. You've gotten logged in, you have a basic GraphQL schema. You want to enforce something at the data layer. A user-defined function is exactly what it sounds like. It's user defined, so you create the definition. It's a function, so it's compute.
Jason: Yes. Thank you, indifferent ghost.
Rob: Yes, thank you. On Jason's behalf. We don't have any boops yet. Kind of disappointed.
Jason: I know, I know. We haven't gotten the chat going. Sometimes you just got to put the quarter in. Got a new subscriber, you have to abuse that new boop you've got. There it is.
Rob: There we go. Thank you. Appreciate it, y'all. And the last point is it's a function. That means that it's compute that's running. The thing about this, you talked about duality of data or something earlier that I thought was really appropriate where this is happening on your data, in the Fauna data centers. You've got to load that data into your function, do some processing on it, store the result, and send it back. They're co-located. Your compute and your data are co-located on the same instances. It's right there. If you want to think about it conceptually on the same machine. So it's really performant. There's several use cases for it. Some are just simple, quote/unquote, custom business logic. We'll walk through some simple examples. You can also do really advanced stuff here. It's maybe beyond what we'll show today, but you can do fault injection. So chaos engineering and fault tolerance, where you intentionally insert errors on a certain frequency in order to determine if your overall system handles that error. You can do unit testing of your functions with that. You can do very advanced attribute-based access control for your functions to ensure that, you know, all of these conditions are met before a write or read occurs. And user-defined functions are just really powerful ways of incapsulating that logic and hiding it from the user. So you can do that with guards. They may not even know they're calling all of this logic. You create a create boop function, and all the user knows is I want to boop. They request it, and if all the right conditions are met, they get a boop.
Jason: Got it.
Rob: If those conditions aren't met, including intentionally injected failures, they don't get a boop. And that's all they need to worry about. So it gets reused among different components of your application as well.
Jason: That's slick. So I feel like at this point, we're starting to talk about things in more concrete terms. So let's switch over into paraprogramming view here. Let's talk for a minute about the live captioning. So this show, like all of our shows, is being live captioned. We've got Rachel here with us today. Thank you so much, Rachel. That is being done by White Coat Captioning. The captioning is made possible by our sponsors, Netlify, Fauna -- thank you, Rob -- Auth0, and Hasura, all of who are kicking in to make this show more accessible to more people. It means a lot to us. I should call out this is not like a sponsored episode. Fauna is a sponsor of the show, and this is a show about Fauna, but it's not -- I don't know how to differentiate. Like this episode is not -- Fauna wasn't like, you have to do this or else. (Laughter)
Rob: No, I can be kicked off at any time if I underperform. It's important you know I'm skating on the razor's edge right now.
Jason: Yes. So now make sure you go and follow Rob on Twitter. And we are, today, talking about Fauna. So if you want to go check out Fauna, that's where you want to start. So I want to do some data. I'm ready. How do I get started with this?
Rob: Hey, can we side bar? I want to show -- 15 seconds.
Rob: You said this is not a getting started with Fauna episode, but if you click that signup button, can you show them how easy it is to get started with Fauna? We won't go through the whole thing. You got a GitHub account? You can get started with Fauna. You have a Netlify account? You can get started with Fauna. Boom, free tier. No payment information. Drop you right in, ready to go. In the time I've read my mouth, you all could have created Fauna accounts.
Jason: And the free tier is good, too. You get like 100k reads per month, 50k writes per month, 500k compute. That's what we're building today, right?
Rob: Yeah, and we're going to talk about those, too.
Jason: And 100 megabytes of storage. This is something where I as a developer am able to run a handful of things on Fauna, and I don't even get close to these free limits. So this is very much like you can start here and you'll be able to use it to get your business up to the point where it makes sense to pay for things. That's a model that's pretty near and dear to my heart. At Netlify, we do the same thing. We want you to be able to use it for free until you make money from it. When you start to make money from it, then it makes sense to pay for your services, right? So it's kind of like we want to start with you and grow with you and make all that work.
Rob: Yeah, side note, before we go off of that, hard limits enforced. That's a decision we made so you can play in there safely.
Jason: Hard limits means when I hit my -- I do 100,001, it says, nah, you're done.
Rob: Right, rather than starting to bill you and you getting a surprise bill. The other plans allow you to manage overages and things like that.
Jason: Yeah. Yeah, yeah. Cool, cool. So, let me log in. I already have an account. I'm going to log in with my Netlify account here.
Rob: While you're doing that, we have a question in the chat. How does a company provide such a generous free tier? Always baffles me.
Rob: There's a whole lot of pricing theory around this. It's customer acquisition costs. The hope here is that you'll do like what Jason and I have done, go out and build with Fauna. When it comes time at your work that you'll say, hey, I've built this with database. It's really awesome. All you got to do is put your data in, get it out. It handles everything. We should use it for this project. Then that becomes -- like, we grow that way. And your company will need to pay for it because of the scale and size of data that it has. But you as an individual can learn for free. And side note, I pay for Netlify as well because the analytics are so valuable for me.
Jason: And I think that's the other thing, too, when you start thinking about software as a service. If you're trying to build out something and considering a free tier, the way that the pricing is shaped, it is significantly more expensive to try and support like tens of thousands of $5 a month customers than it is to do a free tier with like forum-based support that doesn't require active head count and have like paid enterprise customers. If you look at what a company like IBM or Amazon is paying for their monthly contracts, it's in the like tens or hundreds of thousands of dollars a month for a given service. So enterprise contracts totally make up the difference. That's why companies are able to do free tiers. When you as an individual developer go sell this to your boss and you work at Facebook, it absolutely pays for all the free services that you got. Like many, many times over.
Rob: That's right.
Jason: And you know, again, we don't want to monetize individuals. We want you to build things and like learn and have fun. We want companies that make money from services to pay for services. That's where it makes sense.
Rob: That's right.
Jason: Cool. Okay. So I'm here. I'm in. This is my -- I've done 1,829 read ops over the last seven days on Fauna.
Rob: You got 998,881 to go. Let's not use them all on this episode.
Jason: Hopefully the chat doesn't get too aggressive. All right. New database?
Rob: Yeah, new database.
Jason: We're going to call this one user-defined functions. And do I want to pre-populate?
Rob: Yeah, let's do it. It's not a ton of data. It gives you something to work against, query against. It gives everybody the same pre-populated data so you can take the functions we write today, put them in your own database, and off we go.
Jason: So I've got some data in here. Let's pop one of these open and look at it. So we've got, you know, customer, first name, last name, address, phone number, credit card. All these things, good.
Rob: Not a real credit card, I hope. Just in case anybody is not super familiar with Fauna, collection is a collection of documents. You probably think about that like a table, if you're coming from a relational database background. A document is an item or a row if you're coming from other platforms. We call them collections and documents. And fun fact that informs a lot of theory, everything is a document in Fauna.
Jason: And these are unstructured, right? I can put whatever I want in here. I guess the term that you would use is NoSQL? Is that how you would describe Fauna?
Rob: Fauna is sort of -- it's a next-generation SQL database. If you want to pigeonhole it that way, but it works with both schema oriented and schemaless. You'll have that defined schema in there because GraphQL requires it. But it's not forced by the underlying database. So you could in theory migrate away from that and start doing naughty things. But then you'll get drift off of your GraphQL schema. But gives you the flexibility to do it.
Jason: I think that's all a trade-off about going with something structured versus unstructured. You can do whatever you want with unstructured, which makes it flexible. You get a lot of freedom. Also, you can do whatever you want with it, and that's huge.
Rob: It is. But one foot gun that it isn't is single table. You still model everything the way you think about it. Single table design is fascinating if your access patterns are fixed. You've got to break your brain to get to it. Fauna allows you to think about things the way you've always thought about them. You do an ERD or entity-relationship diagram to define the structure and relationships of your data, that's exactly what your collections are going to look like inside Fauna as well. So that helps smooth the learning curve a little bit.
Jason: Yeah, definitely does. I feel like just looking at tables in general, if you're not super familiar with data, can be a little, okay, so I've got -- and I can just create a new one whenever I want. You sure can. You can do whatever you want. So having to figure how you would fit all of that into a single mental model where it's like, okay, I'm putting it into data. Then I got to make it into a data customer? Or a data product? It's like, okay, I can get there, but that's more than I want to do. So we've got this data, this kind of general setup of different places you can go and buy things, products, the orders for things, and the customers who made those orders. What should we do next?
Rob: Well, not go to functions, but let's go to functions anyway. You'll see there's one that's been created for you. That's not going to help you learn today. It's a more realistic but far more complex than how we want to start. So let's go over to the shell instead.
Jason: Oh, I've never been to this tab.
Rob: You've never been to this shell? The shell is my friend.
Jason: I pretty much only use the GraphQL tab. This is where I live in Fauna. I've never used FQL either.
Rob: Well, boy, is today going to be a treat for you. User-defined functions are written exclusively in FQL. Although, they can be consumed in GraphQL. We can do what I call stupid GraphQL tricks later. You don't even have to define types. You can only define queries with resolvers that are functions. Then in GraphQL you are consuming your underlying FQL structure. That's a little advanced. We won't start from there. The first thing we should do is do hello world. Everybody loves a good hello world.
Jason: Go easy on me.
Rob: We got to do hello world. What you see here is the code editor. Everybody can figure that part out. So the bottom is where you're going to write your code. The top is where you're going to get some results. So we can maybe adjust that balance a little bit.
Jason: We would need to add code. Adjusting -- oh, cool. Look at that.
Rob: A little drag up like that. Just real click -- real click -- real quick, you can download it, save it for later. You can upload files. Maybe you've got a repository with some sample user to defined functions that have been written by some cunning Fauna DAs and you wanted to upload those yourself. Then this would be a good place to do that.
Jason: Okay, all right.
Rob: So you can go ahead and click clear.
Jason: If you want to jump ahead and see what he's hinting at, this is a repo full of examples that we may fall back on if I can't figure this out myself.
Rob: We may. We may because there's always syntax. And the syntax is going to be new because it's FQL, but it is learnable. I have learned it. That is how I know it as learnable. I have learned parts of it. I never claim to master anything. All right. So we'll get rid of all those paginate calls. We want to create a function. Me, I like typing stuff out. Type create, see what happens.
Jason: Hey, look at that. Create function.
Rob: Create function. There we go. We use parentheses. Go ahead and hit enter to split those. And now if we go to the docs, it's going to give us a very complex example, but let's just -- functions. Everything in Fauna takes objects. Those objects aren't strictly JSON objects, but they're similar. Go ahead and give us some curly braces here.
Jason: Curly braces.
Rob: Yeah, and split those. A function has two components, a name and a body. So, name. Colon is the separator for key values. Then quotes and hello world. Here we go. Now, naming conventions, you can't have spaces in there.
Jason: Oh, right. This is the actual function name.
Rob: Yeah, this is going to be the actual name of the function.
Jason: Okay. And the body can be whatever I want.
Jason: Okay. So we got a query.
Rob: Yep. And all calls. They're functions, so put some parentheses there. Then we're going to use what's called a lambda. It's exactly what it sounds like. It's an anonymous function defined right there. Lambdas take two parameters. If you want, you can split these out. If you don't, that's okay too. I always split mine out so that I know where I am on the line. So then we know that it's going to take two parameters, so you can put a little comma there. Hit return and you got like parameter one and parameter two. Parameter one is an array of arguments. Yes, that is the array designator in FQL. So let's call that first argument name because it's hello world, so we want to pass it some function or some name to say hello to. Arguments need quotes.
Rob: Yep. And it is functions all the way down from the chat. That is true. It is functions all the way down.
Jason: Functional turtles.
Rob: Functional turtles. So here what we're going to do is use the concat function.
Rob: It does exactly what you think it does. Takes arguments --
Jason: Like that? Or do I need to put it in as separate?
Rob: So if you do that, it might actually work, but it wouldn't concatenate anything with anything. So I guess we're going to step 1B by not just doing a static string. We're taking the string parameter. You're running off ahead of me, but it's okay. The concat takes one or optionally two arguments. The first is an array of strings to concatenate.
Jason: Oh, I understand. Okay. So we got our array, and I'm going to do one of these. Actually, let's talk to the chat. How about that? What's up, chat?
Rob: Hello, chat.
Jason: And this is the glue?
Rob: That's right. That's the separator.
Jason: Okay. So this is like -- you know, any time you're learning something new, there's no way that I would have just intuited this, but it is kind of nice that the auto complete starts solving a lot of the initial problems. I'm pretty fond of that. So is this -- we're done?
Rob: I got to count my parentheses, but that looks good to me. Do run query, and let's see. Hey, it created it. Before we go off this -- oh, sorry, you can go back up if you want. What it shows you is the command is just ran then the output of that command. It's essentially the same as if you use the SDK and create a query and send it. That's the response object that you get back. What you see there is the ref, which is a unique identifier, the time stamp, and some other information about the function.
Jason: The name and the body.
Rob: It just collapsed.
Jason: It did not execute this function, though.
Rob: That's right. What it did was it created that function. Again, if you want to get really technical, it created a document in the functions category that you can now invoke, right.
Jason: Is that going to be in my collections? I guess it would be in the functions.
Rob: It's going to be in functions. We can go over there now and see.
Jason: Here's my hello world.
Rob: You didn't give it a role, so of course it doesn't have one. Then there's the body of your function. That's what we did here. We ran create function.
Jason: Yeah, yeah, yeah. I got you.
Rob: Now when you want to call your function, you run call.
Rob: Yep. Call takes two arguments.
Jason: First one is the name of the function.
Rob: You would expect. The first one is a reference to the function because that function name can actually change. So you dereference. This is like a pointer. Use function. You can use ref. That's a separate thing for another time. Function, right there. Then the name. A little bit of sugar to give you the reference. Then a comma. Then an array of parameters. If you only have one, you can simplify this to a single parameter. They are ordered, not named. So it's not like keyword args in Python. You just give it the name you want it to run. It's really not going to matter for us here because we didn't use that parameter.
Rob: So anything you give it, it should send back hello chat.
Jason: Okay. So let's modify this then to use that parameter. So can I edit right in the functions tab if I want to change my function?
Rob: You can. In fact, I recommend that you do.
Jason: Okay. So I've defined a parameter, but we don't use it. So instead of this, I want to use the name. How would one do so?
Rob: Right. So the way you get a parameter is with the var keyword. You can take out chat there. Var, like everything else, is a function. It's all functions all the way down. Functions, functions, functions. Var, name. There you go. That's that.
Jason: Okay. So then I need to do a little bit of sugar here to make that work. Save it.
Jason: Come back out here, run it again.
Rob: Now you're going to have a little -- hey!
Jason: Nice. Okay. All right. So I saw a question that I want to answer before we move forward. Ricardo asked, do you know of any CMS using Fauna out of the box?
Rob: I was hoping that was going to scroll right off the chat because I do know of one and can't remember the name off the top of my head.
Jason: Oh, no. So they do exist, but we got to Google for them.
Rob: Yeah, we got to search. Let me see if I can get somebody to help me out. I've asked for the reference. I'm extremely embarrassed that I cannot remember it.
Jason: And it looks like databrecht in the chat did link to a forum post. Let me grab that link.
Rob: Okay. Sorry. I should answer this in two ways. I was thinking of a customer who has built a CMS powered by Fauna. That is not -- if you're asking for a CMS for your data in Fauna, that is a different thing. And the dashboard gives you some -- yeah. There you go. This is more of that, that you're showing on the screen now. This is more of a CMS to access your data in Fauna. It's pretty intuitive. I like it. There is also a CMS for building static site generator. Like putting the content into your Hugo or Gatsby site generator, right. Built on top of Fauna. There is that as a customer-use case as well. That's the one that was slipping my mind.
Jason: For sure. I think there's even like -- if you look at Gatsby and Fauna, there's a source to pull in your Fauna data and stuff like that. There's definitely ways to kind of make it a first-class sort of deal. It looks like we have ajcwebdev in the chat sharing more links. So lots and lots of options there for getting stuff done. Okay.
Rob: Sorry, if you want to show real quick just on collections, you can do basic editing in the console in the dashboard the way you just edited the function. You can edit those documents the same way with your little pencil icon over there.
Jason: Oh, so I can just come in here.
Rob: For one-offs. That's not as robust as the other solutions we just pointed out. But to get you started, you make quick edits there. Thanks for the tips, everybody.
Jason: Cool. So we've got ourselves a basic function. It's running, it's working. I guess the question that I have when we start looking at something like this is how would somebody like use this? So if I'm actually storing data, right, like I'm probably going to be working in some other application and I'm going to be sending data in. How do we add these into our work flow? What's the practical use case of a function like this?
Rob: That's one path. There's also the GraphQL path where you use user-defined functions as resolvers. You specify the resolver and the name hello world. Then in the query definition for the types, you would have to list name in this case. The names don't have to match, but you would have to list name as a string. It would accept one string, return another string, using this user-defined function. That way you can get it straight through GraphQL. And we can show that, if you want to create another database that's just completely empty.
Jason: Yeah, let's do it. I'm going to create a new database. I'm not going to -- user-defined functions GraphQL. No database. So it's an empty collection now.
Rob: Which is actually just a document of typed database.
Jason: Oh, look at that. Do I need to define a schema to do this?
Rob: You will, but first you need to get that function in. Actually, you don't. Let's open a file and define a GraphQL schema really quickly.
Jason: Okay. So let me -- there's nothing in this. So let's open this up, and we can mess with some things. So I have an empty repo here. I'm going to create schema.GraphQL.
Rob: I will say that for data research purposes, I observed to see whether you would type GQL or type out GraphQL all the way.
Jason: I don't know where I developed this happen, but I picked it up from some framework. So let's see if I remember. We get a type query. Then in here we would want, like, get --
Rob: Say hello.
Jason: And in here like name string. If I can type it out properly. And that would return back a string. That's is a simple GraphQL schema.
Rob: Yes. But what we haven't done is wire up the resolver. It doesn't know we have a UDF name hello world that's going to implement it.
Jason: Okay. Yep, you're right.
Rob: To do that, all you need to do is on the same line, just add a space @resolver. Name, hello world.
Jason: That's it?
Rob: That's it.
Jason: Oh, okay. I understand. That's nice. So now what we're able to do then is I am not actually going to implement any data. This is effectively me saying, hey, Fauna, this one is basically an echo loop. I don't need to store anything for this to function. I just need you to call this function and spit back a processed piece of data.
Rob: Correct. So you can upload this schema without creating any types, any other mutations, anything. Just that as is. What's going to happen here is going to show us a couple concepts. One --
Jason: Yeah, I need to get back into GraphQL. Then I'm going to import this schema. And we're going to drag this one on to this one. There it is. And let's open it. Oh, I screwed something up. What did I screw up? Expected field definition. What did I do, chat? What do y'all see?
Rob: String, string, @resolver, hello world.
Jason: This is correct.
Jason: Invalid input string, string.
Rob: It's weird that it didn't highlight them the same. Do you have some kind of weird space in there?
Jason: What am I doing wrong here?
Rob: Oh, recover name hello world. That's what it is.
Jason: Like that?
Rob: Yes. Sorry.
Jason: All good. All good. We're going to try this one more time. Hey, look at it go! All right. Now if I go into the docs, I've got this say hello. It takes a string. Perfect. Okay. That's everything that I expect.
Rob: It's not exploding, but it's not going to give you what you want or what you expect. But go ahead and run it. Like I said, it'll be instructive for a couple reasons.
Jason: Function hello world was not implemented yet. This is a good error message. I was really worried that it was going to give us one of those kind of generic, like, unable to determine, you know, invocation at line whatever instead of just doing what I want, which is to tell me that I haven't done my job yet. This is really slick, though.
Rob: I'd be lying if I said we didn't have some of those. But this isn't one of them.
Jason: We all end up with those at some point in the code base. But it is nice this one at least, you know, when I'm coming straight out of the gate. I feel like as a beginner, that's when instructive errors are the most important. Otherwise, I'm just going to throw my hands up and be like, I don't have time for this. So this is wonderful. Now I need to implement this function, which I will do by --
Rob: Well, let's see what happened here. Let's go over to functions. Let's see how it managed to get that response to us.
Jason: Wow! Look at that. Holy crap.
Rob: So when you imported a GraphQL schema with a custom resolver, it realized that resolver isn't there yet. So what it did was it went out and created that function for you with this error message in it. So when you call it, it will say, hey, you didn't do that yet.
Jason: You froze up there for a second. Audio is all good. Okay. So now we've got a query that's a lambda. So then I can just start implementing things, right? I can go back and do our thing. It's going to be a concat.
Jason: And it's going to concat hello, name. Okay. I think I've done that. I'm going to save it. Let's go try this again. I'm going to go back to GraphQL. I'm going to run it again. String expected, array provided.
Rob: Yeah, this one -- if you go back to the function definition, it's one of our --
Jason: Is it because I did this?
Rob: It's hard for me to see. Is that a regular parentheses followed by a bracket? My screen is like gone. Yeah, that's what it was. The lambda expects an array of arguments. Then the location function to apply it. Concat. Yeah, it does that. Then you remember we said that second argument in concat is optional? You've specified the optional, the default value.
Jason: Oh, so if I leave that out, it'll just work.
Jason: Okay. Let's go back and try that again. Hey!
Rob: There we go.
Jason: Look at that, everyone.
Rob: So another point we sort of glossed over here that I think is pretty important. Hey. Thanks, that Adrian guy. Everybody when you create a child database, that's now the scope that you're working in. This gives you isolation guarantees. So if you want, you can create a new database for each customer. That maybe makes sense if you're doing some B to B SaaS kind of thing. Or you can isolate environment production staging, testing, et cetera. Then you can modify and have different versions of those functions in each environment. Downside, you have to manage them and keep them in sync. We have tools for doing that, but that's probably outside of the scope of today's lesson. It's just something to be aware of. You did define hello world, but you defined it in the database user define functions. Now we're playing in user-defined functions GraphQL. So the GraphQL layer defined it for you. And now we've defined it. And now everybody is happy.
Jason: Yeah, yeah. And this all makes -- I mean, this makes sense. So what I'm kind of imagining from here is that I would probably want this to be, like, I could do some kind of data pre-processing on query or a pre-processing before saving so if I'm going to -- I think saving a customer order one was a good example where it looked like -- I didn't actually look at what the function did, but my guess is we're doing some kind of validation, like the order comes in and we're checking to make sure that required fields are there. Or we're breaking a full name into a first and last name or something like that. So how would we do that where -- let's maybe add another function here that would let me pre-process some data so we could send a mutation here and save some data but mutate in a way. Do you have --
Rob: It's like you're reading my mind. I know you haven't actually seen this function in the repo, which is why it's even more impressive. Example number two, for those of you following at home in the repo, is a limited sum or a limited adder. Basically, this is just like a contrived example, but it works for what you're describing. It lets you add two variables with a limit. If it exceeds the limit, it aborts and throws an error. If it doesn't, it returns the sum. And this is an example of that pre-processing. Say you have an account limit where somebody can -- they want to order 500 of X but they only have a limit of 50. So you want to fail the order. You don't trust the input from whatever client. Even if you control that client, you can still treat all unput as untrusted and unvalidated. Do that validation in your database layer. By the way, you can make that a function so that your create order function calls the validater, which calls something else, et cetera. So it's all a composition now. It just asks for validation without knowing what the validate rules are, and your validation team can begin applying increasingly complex logic. Feature flags I didn't mention, but that's also an excellent use case for this. Feature flags, where if you want to compare releases or turn things on for other customers or not. So let's create a new function here. Let's go back. If we want to do it -- I'll leave it up to you. We can either add it again in the GraphQL schema and show updating the schema, which will merge those changes and create --
Jason: Yeah, let's do that. I feel like in terms of my standard work flow, I'm probably going to use GraphQL for my day-to-day. So showing how to set this up for GraphQL seems like it's immediately practically applicable for me. So let's follow my preference, everyone.
Rob: It's your show. I'm the guest. It's your house. I'm drinking out of your cups, man. Yeah, just copy and paste that inside the query block there. Create a new one.
Jason: You said this was called limited sum.
Rob: If you want to make it match the repo -- yeah, I think it calls it limit adder. But it doesn't matter. Now we're going to give it two arguments. Yeah, num1, num2. Int for simplicity.
Jason: And we'll make a function called limited sum.
Rob: How much easier could it get, really?
Jason: So I'm going to merge schema by uploading this again. What this should do is just doesn't break anything that was already there. Had I done something destructive, it would have failed. Nah, you can't do that because it's not mergeable.
Rob: And there's the two question mark helpers, like tool tips there next to merge schema and override schema that show you the different behaviors and what's going to happen on each. So it aborts if the indexes are not compatible with the current schema. For my SQL people, that's drop database, re-create, everything. Including your data. Your data is going away if you use that button. Don't use that button. Unless you know what it does. Then you'll know it's okay to use it. So again, if we call limited adder and give it two arguments -- sorry, limited sum.
Jason: What we want back is six, but what we're going to get back is you have not finished doing this thing.
Rob: Exactly right.
Jason: So then I'm going to go here.
Rob: The bobby tables button, yes.
Jason: Num1 and num2. Down here, we're out of my depth on FQL. I don't know what happens here. What if I type add? Is that the right thing? Oh, wait.
Rob: You know, let's start there. Well, let's start there. Let's iterate. Let's just return the sum without any limits. Look at you. You got it. Yes. This is the FQL way.
Jason: You're doing it. You're doing it, Peter! (Laughter) Let's try this again. I'm going to go back out here. Let's run this. Hey! Look at that. I just FQL'ed.
Rob: Just drop some trailing numbers. It's basically the same.
Jason: Yeah, that's how I do math too. We call it Y2K math. Just roll it over.
Rob: That's the 386. That was the Pentium that had that bug. That probably made sense to five people. Once upon a time, there was a thing that didn't work.
Jason: Once upon a time, there was a thing that didn't work. The never-ending story of web development. Okay. So now -- wait, I'm looking at the wrong thing.
Rob: But I appreciate your attention to formatting.
Jason: I mean, this is a self-preservation instinct, honestly.
Rob: Which is fair.
Jason: All right. So here's our formatting. We're now adding, but we want to make sure that these numbers are below a -- I guess we want to make sure this is below a certain value.
Rob: Yeah. Let's do this in two phases. I'm going to walk you through a simple way that works, and then we're going to expand that to something that does the same thing but is more realistic. So first, let's just make it work. So really our condition is if the sum of these numbers is less than or equal to some limit, return the sum. Otherwise, throw an error. That sounds very much like an if statement. Conveniently, FQL has an if statement. Equally conveniently --
Jason: Equally conveniently, Rob can freeze right before we get -- we lost you right as you started saying equally conveniently about an if statement.
Rob: Okay. I still hear you. So hopefully -- like I said, I'll just get in the. I'm still here.
Jason: Oh, we can hear you. We're good.
Rob: Wasn't that the Joaquin Phoenix autobiography? Anyway.
Jason: Was that autobiographical? Anyway, this is not film criticism with Jason.
Rob: All of my references are tangential and off topic. So if takes three arguments. If takes a condition, and it takes a statement to perform, if that condition evaluates to true. And it takes a statement to perform if that condition evaluates to false. Right. So we're going to repeat ourselves. We're going to violate the dry principle here and repeat ourselves, but our condition is if that sum that you have there is greater than or equal to some number. Or is greater than some number. Let's make it less than or equal to the limit. So in this case, we have LTE is the less than or equal to function. All this stuff is in the docs, of course. That's one that you kind of like need to know it's there, otherwise you won't find it. Less than or equal takes two arguments. A number and a number. If the first number is less than or equal to the second number, it returns true.
Jason: So let's do an easy one. Less than 100. So if this is less than or equal to this -- or is it the other way around?
Rob: If that is less than or equal to 100, it evaluates to true.
Jason: Okay, all right. Good, good, good.
Rob: If it's strictly greater than, it evaluates to false. If it's less than or equal to 100, we return that value you already got there. We know what to do. If not, we want to throw an error. Abort is the keyword for doing this. Abort will break you out of the transaction you're in. All the way out. With an error message you specify. That's a little bit of an aggressive error message, but that's fine. We can keep that.
Jason: So let's give this a shot then. It didn't fail. That's always good news. So let's make sure it still does that math. Didn't hard code numbers. Now let's make it way too big.
Rob: There you go.
Jason: And I've successfully entertained myself. Thanks for coming, everybody.
Rob: That's what it's all about.
Jason: I mean, this is great. So we now have an actual limited sum. You said this can be better.
Rob: Right. If we go back to that function definition and look at it, you know, I'm not crazy about seeing that add var num1, num2 in there twice. Here we can talk about compute ops. Every 50 FQL verbs is one compute op. When we do add var var, that's three. When we do it twice, that's six. Three and six are both less than 50, so that's one compute op. In this case, it's sort of trivial. When you have realistic workloads with much bigger queries, much bigger guards and gates, you don't want to do things more than once. You don't want to recalculate the same result. So what will be great would be if we could like calculate that result, sort of store it somewhere, and then do the comparison against the stored value. If so, return the stored value. If not, return the error.
Jason: Yeah, I like that.
Rob: That's what the let keyword does for us in Fauna.
Jason: Okay. So I'm going to make a guess that I wrap everything with a let so that it's defined in that scope.
Rob: That is correct.
Rob: Let takes two arguments. Let takes an object or a map, key value pairs. Then it takes an expression that we call in but it's just the thing to do with those variables defined. So in this case, that entire if -- now, you don't call it in there.
Jason: Oh, okay.
Rob: It's that entire if statement is the second argument.
Jason: The entire if statement is the second argument.
Jason: Okay. Then the first one is going to be -- I assume I need to pull this out.
Jason: Where does this go?
Rob: It goes inside that map of variables that you're calculating for this context.
Jason: So I can just do this?
Jason: So when I use this, I am going to -- I assume there's a keyword for this. Or do I just write it out?
Rob: It's another variable so you need to wrap it with the variable call. Less than or equal to var sum.
Jason: Okay. So basically we're creating local scope.
Rob: Right. So there are three variables here. Two are parameters that are passed in and one that is created by the let statement. So this simple function here is actually showing a lot of different pieces of functionality of UDFs.
Jason: Oh, I missed something. I missed it where?
Rob: You're missing it on the sum line. You're missing a closing parentheses. Line six, yeah.
Jason: Got it, okay. Great. So then if I come back out here, same thing. Works.
Rob: You should see the same results here.
Rob: Right. Yep. It would just refuse to return it. And you can get really dialed in on that with a-back too. Not only are you logged in and authenticated and authorized to access the data, but is it working hours? Can you access only certain sensitive data from Monday to Friday, 9:00 to 5:00 or whatever? Or is some other person logged in, like a two-person control on it. Anything you can calculate inside a query you can use as an attribute.
Jason: So let's go one step further here, which I want to actually --
Rob: Let me add one thing before we go on.
Jason: Please do.
Rob: That let statement in there, any query can go in there too. So some could be some value that we retrieve from the database. This is where the feature flag example we talked about, right, you can look up based on the user ID. Are they enrolled for this feature? Did they subscribe for this feature? Are they paying for it? However you want to use feature flags for dark deploys or whatever, that's one aspect of it. Reads are pretty good here. Be careful about writes there, in particular if they're going to cause contention. All of these are transactions. They're all or none. It either is going to happen or it isn't. Reads are much less problematic. Any query you do can go in there. This is also where you would do whether you're determining to intentionally fail or whatever.
Jason: Okay. So what I want to do then is go one step further and save some data. So I'm thinking about something like this. What I want to return back is a user. So let's create our type user. Our type user is going to have a name, which will be a string, and a status, which will be a status. Then an enum. Do enums work in Fauna?
Rob: I believe so, yes. Did I lose you?
Jason: I'm here.
Rob: No, okay. Cool, not cool.
Jason: Yeah, this is my thinking. Did I do that right? I think that's correct.
Rob: Front end, man. You're outside of my comfort zone with GraphQL. It goes very quickly.
Jason: Let's do this. Let's not use an enum so we don't introduce any kind of chaos. But here's what I want to do. I want to have save user call a function, and that function is going to be determine coolness. So we're going to pass in a name. The function is going to determine let's call it coolness. All right.
Rob: You're going to have to pass the user, though, right? Oh, no. I see what you're saying. The function will randomly calculate.
Jason: Yeah, yeah. Because we would need to go further with this if we were going to make it like more professional, but we've also only got like ten minutes left before we need to start tearing down. So I want to be realistic about what we're trying to accomplish here. So now what I want to be able to do is in here I want to be able to sit -- wait, did it just create a whole Burch of stuff for me? Wow! Look at that.
Jason: That's super cool. Now we've got -- wait, which is the one that I wanted? Here's mine.
Rob: Yours, because you defined it, is going to be overridden. The others are implicit.
Jason: So I'm going to do this. What we'll get back is a name and a coolness. I'm going to run it right now. It's going to say you don't have a function, but we can go over here, and now I'm going to get a name, and then we want to do a let. In my let, I need some data. Then I have more stuff that we're going to do. So I want to -- let's see what I can figure out here. I'm going to get -- well, actually, I don't need this because we're not going to calculate anything.
Rob: You have to calculate the coolness, right? This is the place where you would generate the coolness.
Jason: That's true. I was actually just going to do a hard string comparison and say anybody who's not Jason is cool.
Jason: But I guess we can do that.
Rob: Jasons of the world, you don't have to put up with this.
Jason: Let's do that. Let's set coolness here, and we'll calculate it once. That would be an if, name. Or wait, I need to do -- what's a string comparison here? Equals, yes? Then cool, otherwise not -- oh, wait, I did that backwards. Then I don't need an if here anymore. Instead, I want to do it right now. We haven't done this yet, but I want to write to my users' collection, which has not been created. Do I need do create that first?
Rob: Oh, it has been created because you created it in the GraphQL schema. So there's a users collection over there for you.
Jason: Okay. So then I need to write to that collection, and I don't know how to do that.
Rob: I am here for you, man. Create.
Rob: Create. The power of create -- create takes two arguments. One is a reference.
Jason: Like this?
Rob: Well, don't try to type on this one yet. The second is the data. You're going to create -- oh, no.
Jason: Okay. We're back. Users that were created.
Rob: Am I back? Yeah. Create collection. Then users, I think, was what you named it. It's case sensitive, too. So whatever you named it in the schema.
Jason: Excellent. What did I name it in the schema? I called it user.
Rob: So create user. Then an object. So name, var, name. Oh, no, sorry, not on that line. On the next line. So the first line is the reference, which is just collection user. That's where you're going to go to create a document. You're going to create a document in that collection. Then you give the body of the document, which in this case --
Jason: I understand. Okay. So my coolness. Cool. So I kind of think I maybe slightly overcomplicated this one given the straightforwardness of this. But still.
Rob: Yeah, it's a place holder for you to go out and calculate all the things you need to do inside your function.
Jason: Yeah. So let's try this, I think. No, I screwed it up. What did I do?
Rob: Ecwyne caught it in the chat. It needs to be data because the top-level object contains the ref and the data. So we have to give it -- yeah. That should work.
Jason: Try it again.
Rob: I always use couching language. It should work, yes.
Jason: So saved me. Not cool. Rob, cool.
Rob: Oh, yeah!
Jason: If I get another one out here, let's query users. Oh, no. Is there like a get all users? There's not, but we can look at our collections.
Rob: Is there not a list users? I don't remember what it generates.
Rob: That's exactly what it is.
Jason: Okay, perfect. So this is -- I mean, this is great. Now without having to become a DBA, without having to figure out how to manage this stuff, I'm able to do these more -- you know, this is pretty powerful stuff that's not going to happen client side. Or in a serverless function. It's going to happen right during the function. And how does this affect speed?
Rob: So this is one thing I'm working on getting hard numbers around. The soft answer is minimally. I talked about the wrappers for write a gated create or guarded create versus a primitive create. Minimal, meaning like 5% is what I'm observing. When you think about that compared to round trip on the network, it's essentially not different.
Jason: And there's also the fact that -- like the way we've configured this one, this work will happen exactly one time when you store the thing. If you were doing it like ways I have implemented things like this, ways I've seen things like this implemented would be I would store the name in the database and every time I query the database, I would run that coolness calculation as part of my client-side code. Again, I would do that because it's flexible. I can change it in the future, and I know how to do that. But we can do that here on like a query -- if we needed to, we could do some kind of a data migration thing where we can run this function with a different set, right. And recalculate these values.
Rob: Functioning can be routers where you can check the version sent in from a client. If it's an old version, you mangle the data to the new format, call the new function, mangle the results, send that back. Say you have like embedded code, embedded devices that are notoriously difficult to update. You have to assume that those updates will fail when they're out there the way they are. You have to support that API version. You can do that in a function. You can't do that if you're calling a primitive. Create is going to take what you give it and put it in there. A guarded create type function is going to allow you to modify that and modify it and modify it. So it's really, really powerful for that migration use case as well.
Jason: That's super cool. Okay. So if we are -- I think this is probably about all we have time for to build today. So for somebody who's interested in going deeper on this, what are next steps? Where can we go?
Rob: Yeah, so check out the repo. There's two more examples in there that add roles, as well as versions of what you and I have built today. Those roles allow you to do more with permissions where okay, look, nobody can call, create, update, delete. Only this function can. You have to go through this function, which itself enforces the a-back. You can see how that becomes a powerful pattern. Do not use that function -- oh, that's the role. Sorry. That role just gives access to do those operations on the stores, collection from the sample data. Everything in the repo is built to work with the sample data, the pre-populate with demo data. The next step would be go back to Fauna Labs and click around. One of our DAs, who's been in the chat here answering some questions --
Jason: Where is labs?
Rob: Sorry, the organization where that repo was.
Jason: Oh, oh. I understand. Sorry. Got it.
Jason: So if we want to do functions, we're going to need this FQL setup, right? This is where we're going to find all of those functions and all the pieces that we did there.
Rob: That's correct. But if you go back to the repo, at the bottom of it -- hopefully I'm not making a liar out of myself -- I'm pretty sure I added a next steps. Hey, look at that. This series of tutorials for FQL that was done by Pierre, who I don't feel like I'm doxing him by saying this, but he said on Twitter today very publicly that it's his birthday. So happy birthday, Pierre. Really appreciate everything you do for Fauna and all the help you provide to builders. It's awesome, and I hope you have a great day. He's built this wonderful series of tutorials that really take you through a deep dive of FQL, including user-defined functions, but also including other concepts like indexes and roles and things that you'll need to really build effectively with Fauna on a big scale.
Jason: Very cool. Well, that sounds super exciting. So this is -- like, I can see a huge amount of application for this. I can see this being something that really lets us make good decisions about our data without adding a lot of complexity to the way that we are actually managing it. You know, I'm really excited to see what folks are going to build with this. I feel like I'm seeing more and more advanced applications. The fact you've got a Twitter clone built is very, very cool. And you know, I'm seeing more and more people who would typically call themselves front-end developers building these very advanced, like, serverless applications with custom databases and all these API integrations. All stuff that, you know, 10, 15 years ago there's no chance that you would have been able to build all of the things that you're able to build now. Especially if you were to call yourself, like, I'm a front-end developer, but the front has extended quite a ways deeper into the stack than you would have maybe -- jamstack. But there's a whole world of possibilities out there. So even if you think you are only a front-end developer -- oh, I couldn't do this data stuff, I couldn't do that -- dig into this a little bit. Look at what serverless unlocks. Look at what these platforms unlock. See how far you can get. I bet you can get further than you think.
Jason: So Rob, I'm going to kick everybody over to your Twitter again. Where else should people go if they want to keep up with you?
Rob: Um, I once upon a time streamed a lot and hope to start doing so again. I'm on Twitch.TV/RobSutter. My GitHub is far less interesting than the Fauna Labs repo. So go there. If you want to email me, if that's how you roll, firstname.lastname@example.org. That's a pretty straightforward -- here, I'll dox myself in the chat there. But I'm always happy to hear from users, help answer questions where I can. There's the Fauna forums, a great place to look for help. I'm not trying to push you away from me. I'm saying maybe your question is already answered, and the Fauna forums are a great place to find that. But yeah, I'm very active on Twitter. My DMs are open. If you hit me up there, I'll get back to you.
Jason: Great. Forums.fauna.com. That's what we're talking about. Actually, I'm just going to link to this community page. Looks like there's also a Slack channel.
Rob: There is. There is.
Jason: So get in there and check that out. All right. Well, with that, I think we're going to call this one a success. As always, we've had Rachel here from White Coat Captioning live captioning this whole process. Thank you so much for being here. And that's made possible by Netlify, Fauna, Auth0, and Hasura, all who are kicking in to make this show more accessible to more people. While you're on the website, check out the schedule. We've got a lot of really fun stuff coming up. Apparently my styles didn't kick in. There we go. There they are.
Rob: Very large picture of my head.
Jason: We've got Mark Erikson coming in later this week. We're going to learn modern Redux. He's been working hard on this, to make it more approachable, a little less boilerplate, more understandable. I'm super excited to see how that's been going. I have a bunch of episodes I haven't added yet. I'm going to get on that and make it happen. We have all sorts of fun things coming up. Make sure you check out that schedule. And click this add on Google Calendar button so you can see what's coming and get a little heads up when we're about to go live. With that, Rob, thank you so much for hanging out with us today.
Rob: Thank you for having me. Thank you for joining, everybody.
Jason: It's been an absolute blast. Chat, stay tuned. We're going to find somebody to raid. We'll see you next time.