Open KaviiSuri opened 1 year ago
Came up with a solution based on ExpressJS middleware: https://github.com/notadamking/exzact
Open to feedback (@KaviiSuri @t3dotgg)
Here's a simple example using exzact
:
import { z } from "zod"
import { exzact, Middleware } from "exzact"
interface Context {
thing?: string
something?: string
}
const app = exzact<Context>({
thing: "things",
})
const logMiddleware: Middleware = async (context, next) => {
console.log("Log Middleware (Pre): ", context)
await next()
console.log("Log Middleware (Post): ", context)
}
export const hello = app.zact(z.object({ stuff: z.string().min(1) }), {
something: "stuff",
})(async ({ stuff }, { thing, something }) => {
console.log(`Hello ${stuff}, you injected ${thing} and ${something}`)
})
app.use(logMiddleware)
hello({ stuff: "world" })
In the above example, the logging middleware will run before the action has executed.
The simple example above will log the following:
Log Middleware (Pre): { input: { stuff: 'world' }, thing: 'things', something: 'stuff' }
Log Middleware (Post): { input: { stuff: 'world' }, thing: 'things', something: 'stuff' }
Hello world, you injected things and stuff
There's also an example using Upstash (or local memory) for rate-limiting: https://github.com/notadamking/exzact/blob/master/examples/upstash.ts
As well as an example for using JWT for authentication (just an example, not secure, needs secret verification): https://github.com/notadamking/exzact/blob/master/examples/auth.ts
There are also multiple examples of adding middleware and context only to specific routes.
Wow, looks amazing, I was thinking of hacking around on it when I get time, but this looks perfect.
Do you think it'd be better if we didn't call it an app? But rather a better name protectedAction
, loggedAction
, adminAction
, ratelimitedAction
etc, this would allow us to share the middleware stack across the application in a clean, obvious way. Thoughts?
Wow, looks amazing, I was thinking of hacking around on it when I get time, but this looks perfect.
Do you think it'd be better if we didn't call it an app? But rather a better name
protectedAction
,loggedAction
,adminAction
,ratelimitedAction
etc, this would allow us to share the middleware stack across the application in a clean, obvious way. Thoughts?
Perhaps, though it really depends on how you intend to use the library. It was originally written to be used like an express
app, where an app is a collection of actions. You can combine multiple middleware across actions however needed, or add middleware to the top-level app to add them to all actions. However, nothing stops you from using the library as you mention, and in fact, you may get better type support for it depending on the use case. For example:
import { z } from "zod"
import { ZactAction } from "zact/server"
import { ActionType, Middleware, exzact } from "exzact"
import { AuthenticatedMiddlewareContext } from "./middleware/context"
import { authMiddleware } from "./middleware"
export const protectedApp = exzact<AuthenticatedMiddlewareContext>()
const authInput = z.object({ userId: z.string().min(1) })
type AuthInput = typeof authInput
export function protectedAction<
InputType extends z.ZodTypeAny,
ContextType extends AuthenticatedMiddlewareContext
>(validator?: InputType, defaultContext: ContextType = {} as ContextType) {
return function <RType = void>(
action: ActionType<InputType, AuthenticatedMiddlewareContext, RType>,
...additional: Middleware<AuthenticatedMiddlewareContext>[]
): ZactAction<InputType & AuthInput, RType> {
return protectedApp.zact(validator, defaultContext)(
action,
authMiddleware,
...additional
)
}
}
Then you can simply create a new protectedAction
with:
export const authorize = protectedAction(
z.object({ someAdditionalParam: z.string().min(1) })
)(async (input, { user }) => {
.... input.userId is available here ...
})
The above protectedAction
would work as you mention, and would be a nice DX with full type support. Perhaps I could add first-class support for this sort of thing to the library.
I'm really excited about what this package does, but middlwares/auth etc are something we don't have patterns for yet as it's very early.
What do you think of defining "middleware stacks" once and reusing them, something like "protectedProcedure" in tRPC.
Imagine zact taking in a list of functions to execute before the action as a stack, and the stack is reusable.
It'll just chain predefined function calls together to give you a typesafe middleware stacks