Unleashing Object Proxies: Building Type-Safe Wrappers for Anything
From Author:
You must or must not have heard of object proxies before, but have you ever understood how to use them? What the actual usecase of an object proxy is? In this talk, we will do a deep dive into object proxies, understanding how they work, and the kinds of things you can build using them. We will build higher-order wrappers for existing libraries, and learn how to wrap over them in a type-safe manner.
This talk has been presented at TypeScript Congress 2023, check out the latest edition of this JavaScript Conference.
FAQ
Object proxies serve as middleware for objects, acting as a wrapper to control both the input received by the object and the output returned when a user accesses any method or property of the object.
Object proxies can be used for input validation and implementing an access control layer. For instance, they can manage and restrict access to sensitive data within an object based on the user's or developer's permissions.
Object proxies are commonly used for controlling data access, adding logging to track and monitor access to sensitive information, and handling responses dynamically, such as lazy loading data only when it is accessed.
Prisma uses object proxies to manage database interactions. It generates types for database tables and applies them to an object proxy, which then handles real-time data fetching and operations transparently as if accessing local properties.
Object proxies can automatically log access attempts to objects, especially unauthorized ones. This helps in monitoring and auditing access to sensitive data or operations within an application.
In response handling, object proxies can manipulate the return values based on the input or conditions, such as performing API calls for data fetching when a property is accessed, thereby enabling dynamic data loading and caching mechanisms.
TypeScript can be used to define and ensure the type safety of the properties handled by object proxies. It helps in generating types that match the expected structure of the data managed by the proxies, enhancing reliability and developer experience.
Object proxies enhance security and data integrity by controlling access and validation, improve performance through lazy loading and caching, and offer flexibility in handling data interactions and transformations within applications.
Video Transcription
1. Introduction to Object Proxies
I'm going to talk about unleashing object proxies, building type-safe wrappers for anything. Object proxies are middleware for your objects that allow you to control input and output. They have various use cases such as controlling data access, adding logging, and handling responses. Prisma and PRPC are examples of frameworks that use object proxies to access databases and enable transparent method calls.
Hey guys, my name is Akash Joshi. I'm a software engineer at SIGTECH and today I'm going to talk about unleashing object proxies, building type-safe wrappers for anything. Object proxies are an amazing tool that are severely under-explored but are quite widely used in all of the libraries that we use in our day-to-day. So I'm going to explain how they work, what they are and how you can use them as well.
So what are object proxies? Object proxies are basically middleware for your objects. So think of them as a wrapper around your object with which you can control what input comes into the object and what you return from the object whenever a user tries to access a method or a property. So what are some of the use cases that we see with object proxies in the real life? So one of the first use cases that one can think of is controlling data access. So doing input validation or implementing an access control layer on any sort of object which can contain privileged properties. So for example, on a bank, if you want to create an object which contains the user's data then based on whether a user has, one of your developers have, access to that property or not, you can actually do input validation on it. So if the user or the developer has access to that property, then you return it, otherwise you can return an error or whatever you want to do instead.
Similarly, you can add logging to your objects via object proxies. So if a user tries to repeatedly access an unauthorized data, or you just want to have a log of all of the activities that your developers are doing on any privileged object, then you can use, you can add logging to it by object proxies. Any accesses are logged to one of your access logs. But most importantly, what we use object proxies for is response handling. So this can involve returning something else based on what the user provides or lazy loading certain objects. So for example, just continuing with the user case. Let's say we have an object which contains a user's banking details, like their address history, their transaction history, anything. But you don't want to fetch all of it at runtime. So whenever someone tries to access their address history, then we make a network call to the address API, get all of that data, and then return it via the output. So on the on the developer side, it just looks like you made a property call, like a .call to an object. So that would be user.address. But in the backend, it will actually make an API call. And you can also implement caching and other things on top of it to actually make your object calls faster.
In the wild, object proxies are used by Prisma. So have you ever thought about how Prisma knows what all of your tables are and how it is able to access all of them at runtime, even though it actually isn't present in their codebase? What they do is they generate the types for all of your tables and then they apply them to an object proxy. So whenever you are accessing your property by using the dot method, so like prisma.analytics, prisma.user, prisma.posts, what it actually does is it makes a network call via object proxies to the actual database and then fetches those results and then returns them to your client. Of course, it isn't all hard-coded into the Prisma's codebase, but they generate it at runtime via TypeScript and then object proxies take care of the rest. Similarly for PRPC, PRPC works on a similar concept where they allow you to make object calls, make method calls, from your front end to your back end quite transparently. They use object proxies as well. They expose a router object on the back end and expose its types and then use those types on the front end.
2. Implementing Object Proxies
The actual proxying happens on the front end where any call to one of their queries is transpiled by the object proxy into a network call. We are going to implement a simple version of the TRPC library and explain how object proxies work. On the server, we have a Fastify server with a route that accepts a procedure name and parameters, applies them to the procedure, and returns the response. The API object provided by the developer contains the exposed APIs. On the client side, we define an object proxy to access properties and make method calls. We use the get, has, and apply properties of the proxy to handle property access and method calls.
So the actual proxying happens on the front end where any call to one of their queries is actually transpiled by the object proxy into a network call and then the network call executes on the back end to actually return you the result on the front end. So what we are going to do now is we are going to implement a very simple version of the TRPC library or just try to implement a little bit of RPC to the back end ourselves using object proxies. And I'm going to also explain how object proxies work.
This just involves a little bit of code. Firstly, let's look at what our server looks like. So this is a very simple Fastify server. If you go to Fastify's documentation page, this is similar to what you will see on the main page. In fact, this is what I've copied from there. The only route that I've added here is slash RPC. It accepts a procedure name and a set of parameters for that procedure. And it then parses this to this data to string and then it applies those parameters on those procedure and then returns the response to the front end.
Where are we getting the actual procedure from? Let's say we have an API object which the developer provides to the server and this API object contains all of the APIs that the developer actually wants to expose to the client. In this case, we are exposing hello, which just returns world and then another method called sum, which returns the summation of two numbers. And see how we have actually defined the types correctly here. This is because we are going to use the properties of TypeScript to actually pass that type to the client as well.
On the client side now what we are going to do is that we are going to define a very simple object proxy. Have a look at the main method here where I will define a few lines of code which try to access some properties of the object proxy and try to get the results of it. So the first one is that we are trying to destruct a property called hello on an object that doesn't exist yet. Next we are checking for two properties whether they exist on the object and lastly we are trying to make method calls on the object proxy and then console.logging the result of it. So let's quickly define our object proxy here. The syntax for defining object proxies is to use a new proxy to define them. The first parameter it takes is the target object that you want to build it upon. So the standard way would be to use a wrapper over an existing object. In this case we are just going to use the properties of the wrapper so we don't use any existing object so I pass in an empty object.
Next we actually look at all of the properties that a proxy accepts. So they are primarily get, has and apply. These are the properties that you will be mostly looking at while building object proxies. The get property is the dot method so when you are destructing hello or you are doing client proxy dot some or client proxy dot anything else then the get method is called and you can return whatever you want from it. Has method is sort of like has own property which actually checks for whether a property exists in object proxy or not and in this case we are just returning true so these two methods which check whether these two parameters exist in the object property should actually return true even though they don't really exist there and lastly we have apply I don't think we have enough time to go into the depth of it but it is a trap for any method calls that you make on your object proxy. In this case get should be enough we will go into the depth of it as we actually define it.
Comments