npm-packages/private-demos/middleware/README.md
Writing middleware in JavaScript is pretty easy, but writing middleware that preserves types is tricky. We implement some middleware in the monorepo to better understand the effect of changes in client APIs, in particular types.
Implementing custom behavior in JavaScript is pretty easy, it's preserving the types that makes this difficult.
Now that useQuery is not generated code custom hooks should be reusable.
The tricky bit is dealing with the variadicity of these hooks in TypeScript.
let x = useQuery("listMessages");
let x = useQuery("listMessages", {}); // allowed
let x = useQuery("listMessages", undefined); // allowed
let x = useQuery("listMessagesForChannel", { channel: 17 });
let x = useQuery("listMessagesForChannel"); // type error
We have two options for implementing these:
useQuery<QueryReference>
for queries with empty and non-empty argsBoth of these work poorly. We have these type tests here for comparing them.
We could encourage custom hooks to wrap the non-variadic useQueries instead,
applying -- although this breaks composition.
We could demonstrate how to cheat the types; it's ok if types break when writing middleware as long as the experience of using the hooks is good.
Our code-generated wrappers like query, mutation, and action should be
able to be extended and these extensions should be composeable. Middleware
should be able to:
Maybe we want other things
Complicated things like
import { mutation } from "./_generated/server";
const myMutWrapper = withSession(withUser(withCustomerCtx(mutation)))
export myMut = myMutWrapper({
input: { a: v.string() },
openAPIexample: "Run the function like this."
customContext: { foo: 123 },
handler: ({ user, session, foo }, { a, addedByAWrapper }) => { ... }
}]
There are a few ways to wrap Convex functions:
wrapTheImpl = mutation(modifyTheFunction((ctx, { a: number }) => {}));
wrapTheImpl2 = mutation({
args: { a: v.number() },
handler: modifyTheFunction((ctx, { a: number }) => {})
}
wrapTheMutation = modifyTheMutation(mutation((ctx, { a: number }) => {}));
wrapTheMutation2 = modifyTheMutation(mutation({
args: { a: v.number() },
handler: (ctx, { a: number }) => {}
}
wrapTheWrapper = modifyTheMutation(mutation)((ctx, { a: number }) => {});
We should probably choose one of these to endorse.
Ian say's it's been convenient to run wrappers at definition site instead of
using the wrapTheWrapper approach so he can mix and match middleware.
wrapTheImpl2 isn't generally as powerful: you can't modify args and other
metadata with it.
I wonder if with the wrapTheMutation approach it's even possible to influence
the inferred signature of the function. It looks like it wouldn't be? But you
could annotate the return type of mutation() and that could do it.
Ian reports that the wrapTheMutation1 approach fails to infer the context
type, but wrapTheMutation2 is fine.
That leaves wrapTheMutation1 and wraptheImpl1.