Infrastructure for Ranges
Setting up infrastructure for Ranges
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the Fullstack Typescript with TailwindCSS and tRPC Using Modern Features of PostgreSQL course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Fullstack Typescript with TailwindCSS and tRPC Using Modern Features of PostgreSQL, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

[00:00 - 00:23] So before we start inserting rows into our booking table, I want to talk a little bit more about the TS range type that we're using for the during column. You might recall that it is this tuple-like syntax where the ends, the brackets specify whether an end is inclusive or exclusive, so it's sort of a glorified tuple.
[00:24 - 00:38] And as it is right now, that is just being read as a string. So if we were to manipulate these booking objects in our code, so we're not doing that just yet, but if we were to as it is right now, we would have to handle those as strings.
[00:39 - 00:51] And we could do that. So we could just say that our front end or back end or wherever we're going to be either inserting or manipulating things, we would have to parse or create these strings ourselves.
[00:52 - 01:08] But this is a good opportunity to show how you can use a more complex type in this whole architecture. There's a package on npm called Postgres range, which offers a range class that is generic, so you can specify the inner type, which for our case could be date.
[01:09 - 01:23] And so this class has both serialized and deserialized functions, and it also has some helper methods if you want to do the arithmetic that we can do in the database and other things. So let's try and use that type throughout our system.
[01:24 - 01:33] Now there are a couple of problems that we need to solve for this to work. First of all, next obviously currently returns a string for this TS range type.
[01:34 - 01:47] So we'd need to configure our next to both be able to read and insert these types. We also need to update our kernel configuration so that the types that we've generated actually reflect this change.
[01:48 - 02:07] And then finally, we need to make sure that TRPC is able to send these types back and forth because if we were to just send some class per default, it's going to serialize into the string object object, which is obviously not what we want. So there are a few things that we need to change to make this work, but we can make all this work.
[02:08 - 02:19] So I hope that this is going to be useful to you. Let's install the Postgres range package in both our schema, backend and front end, because we're going to be needing it in all three places.
[02:20 - 02:31] I'll start by installing it in the schema package. Postgres range.
[02:32 - 02:45] Let me just add it to the two other packages as well. There.
[02:46 - 03:04] So, now let's configure our kernel to actually use this. So we'll go in here and there are a number of different ways you can specify specific types in kernel.
[03:05 - 03:11] One is that you could add it as a comment in the string in the database itself. So there's a specific syntax for that.
[03:12 - 03:30] If you specify at type and then the parenthesis, you can specify what type a specific column should be, which will override what it would all otherwise turn it into. We could also have created our own custom domain or something, and then we could, like you saw with the email and so on.
[03:31 - 03:39] But for this, we're going to just override the pg catalog.ts range type. And let me just paste this configuration in here.
[03:40 - 04:02] So you can see that when we're updating public.ci text, we're just saying that the type should be string, which is very simple because this is a primitive type, so there's no need for any imports. But what we need to do now is to make sure that the kernel is going to import this range type from the Postgres range package.
[04:03 - 04:15] And so since this is a package and not one of our own source files, we're saying that this is an absolute import, but it's not the default import because this is actually a named import from the Postgres range package. That's just how that was designed.
[04:16 - 04:31] And then the specific type that we're using is this, which is range, and then specify to the date type as the generic parameter. So with this in place, our booking, let's just take a look at it as it looks right now.
[04:32 - 04:39] So you can see during here is a string, but let's try and run kernel now. Let me just create a new terminal.
[04:40 - 04:51] I don't know what happened there. So we'll run generate models.
[04:52 - 04:57] And we should now have updated this. You can see during is now range of date.
[04:58 - 05:04] And because this is an import, you can see the import sitting up here. So this is the first step.
[05:05 - 05:18] Our types now say that during is going to be a range of date. Obviously, as I said, this is a little bit of a dangerous place to be because now it looks like everything is fine, but it isn't because next is actually going to return a string.
[05:19 - 05:31] So this is an area where the lacking soundness in TS comes into play because we wouldn't discover this before runtime. And there's just nothing we can do about that.
[05:32 - 05:44] But we're going to fix that just in a minute. Just before we do that, though, notice that if we scroll down to our zod types, we're in for an unfortunate surprise, which is that these are still strings.
[05:45 - 05:54] And that is because this odd plugin for kernel isn't quite intelligent enough to figure this out. And I'm trying to make that work, but as it is right now, I haven't been able to.
[05:55 - 06:08] What we can do is to update our kernel configuration. So instead of this, just this built in or default, generate zod schemas, we're going to replace that with a larger import here.
[06:09 - 06:18] And then we're going to create our own generate zod schemas. So we do that like so.
[06:19 - 06:22] Oops. I already had that.
[06:23 - 06:38] So we use this make generate zod schemas, which is what the plugin uses internally to create the default version. And then we're just giving it the default schema metadata and the default identifier metadata callbacks.
[06:39 - 06:41] And then we're adding to the type map. We want the default type map.
[06:42 - 06:50] And then we want to specify that TS range should turn into this. So z.custom and this should specify range of date.
[06:51 - 06:58] This is a little bit hacky, but it does work. And the import will work as well because we're already importing this range in the files.
[06:59 - 07:09] So this is maybe a little bit of a temporary workaround, but at least for now, that is how things are done. But keep in mind that that may change in the future.
[07:10 - 07:16] You can see now here that it has changed. So the during is a custom zod type of range of date.
[07:17 - 07:28] And the only thing it does is so if you are not familiar with the zod.custom, it basically allows you to create whichever type you want. And it can change or transform things.
[07:29 - 07:35] But we don't want that. We just want it to be passed through as it is.
[07:36 - 07:44] So how do we get next to actually work with these range types and understand them? Well, next uses the PG library internally for Postgres access.
[07:45 - 07:58] And that is actually where we need to do our modifications. So the PG library exports types object, which you can use to expand which how it should parse types that are coming out of the database.
[07:59 - 08:17] And it turns out, I found out after some digging that there is also the sort of semi-hidden feature that if you have an object and you create a .2 Postgres method on it, the PG library is going to be using that for serializing when inserting something into the database. So we are going to be using those two things.
[08:18 - 08:36] And the range library exports both a parse and a serialized function that we can use for both of these things. So let's create a file in our backend here called prepare_infra_structure.
[08:37 - 08:42] And it's going to look like this. So we're not using super JSON just yet, but we will just in a second.
[08:43 - 08:48] But we need to install, I do have the PG library installed. We don't have the types for it installed.
[08:49 - 09:06] Let's just add those. And hopefully, there we go.
[09:07 - 09:17] It's now been discovered. So this type that is exported from PG types has this function set type parser, which takes an OID as the first parameter.
[09:18 - 09:27] And that is a pretty fault, an enum in the PG library. So I have to override it by setting this TS range OID.
[09:28 - 09:31] Now this might look suspiciously magic to you. And it certainly is.
[09:32 - 09:44] But this is a fixed OID that isn't going to change, at least so far as I understand, with the Postgres versions. And there's a query that you can use to look up what these OIDs are in the PG_ type table.
[09:45 - 09:51] I don't recall it off the top of my head. But you can go and look this up yourself if you need an OID for some other built-in type.
[09:52 - 10:02] So this is the way we get the OID for the TS range type. And we're saying for those, you should parse it with the parse function that comes out of Postgres range.
[10:03 - 10:14] And now this is maybe a little bit confusing, but the Postgres range parse type takes an inner parser because obviously it's a range of some type. And in our case, it's a range of dates.
[10:15 - 10:24] And so we need to specify that it should create-- so it takes a string here and then it should create a date out of them. And so that is going to give us a range of dates.
[10:25 - 10:33] Going the other way, we are monkey patching the range prototype. So I'm casting it to any to allow this to work.
[10:34 - 10:40] So this is a little bit of a hack. And I'm hoping that the maintainers of the range library are just going to add this.
[10:41 - 10:46] I've actually created a PR on the package that I hope they will just merge. But for now, we have to do this manually.
[10:47 - 10:58] So we're adding this dot two Postgres function to it. And the way it's designed, you can look this up in the PG documentation, but it 's given a prepare value for inner serialization.
[10:59 - 11:25] So we're going to call that for our date, but then we're going to call the serialize that comes with the Postgres range and use both the value and then the inner serialization which we've been given here. So with this in place, we should be able to both read and write range types, at least for dates, and to our database.
[11:26 - 11:41] Now the last thing I want to add here is we also want to make sure that TRPC works. And as you might recall, just like next is using PG internally, our setup means that TRPC is using SuperJSON for transforming things.
[11:42 - 11:54] So what we're doing here is that we're going to make sure that SuperJSON understands these values as well. And it has this register custom function specified.
[11:55 - 12:09] So we can tell it, okay, we're going to register a custom type called range of date. And this is applicable, we're going to just check if some value is an instance of range.
[12:10 - 12:20] So this means we're actually changing that, checking that it's an instance of a date range. So if we were to expand our system with more range types, maybe we would want to do that.
[12:21 - 12:30] But for now, we're just assuming that if there's any range instance somewhere, that means that it is a date range. And so it needs to be instructed as how to serialize it.
[12:31 - 12:45] And so here we're just telling them all serialize and use just the date.to ISO string internally to create a string out of it and to parse it, we're just parsing it like we did up here. And this is going to be the name of the thing.
[12:46 - 12:57] So with this installed in SuperJSON, it is now able to send it to the front end and read it from the front end. Obviously, SuperJSON also runs on our front end.
[12:58 - 13:05] So we need to add a similar prepare infrastructure file in our front end. So let's go and do that immediately.
[13:06 - 13:17] So this is, I need to find my front end here. I'm going to create a file called prepare in for a structure here as well.
[13:18 - 13:23] And this looks remarkably similar to the other one. So we're still using the Postgres range library.
[13:24 - 13:27] And we're using SuperJSON. We don't need to do anything database-y on the front end, obviously.
[13:28 - 13:34] We just need to make the same registration here. And so the only thing left for us to do is to call these.
[13:35 - 13:46] And in the back end, we're going to do that in our main function here. And let's do it as the very first thing so that the infrastructure will be in place before anything happens.
[13:47 - 13:57] And in our front end, we're going to do that in our main.tsx here. And we're just going to do it out here because we just need it to happen as one of the first things.
[13:58 - 14:06] So now we should have the infrastructure in place everywhere to work with ranges both on the front end and the back end and to and from the database.