y_nk
y_nk2mo ago

creating middleware with context type as param

i'm looking into build a "post operation" function into separate piece of code. i know it may be done differently but i'm looking into it for code structure purpose. the goal of the middleware is to accept a function which takes the current context as input, as well as the result from the handler, and execute code.
import type { MiddlewareResult } from '@trpc/server'
import { initTRPC } from '@trpc/server'

import type { Context } from '../context'

type Action<C, R> = (opts: {
ctx: C
result: MiddlewareResult<R>
}) => unknown | Promise<unknown>

export function post<C, R>(action: Action<C, R>) {
const t = initTRPC.context<Pick<Context, 'get'>>().create()

return t.procedure
.use(async ({ ctx, next }) => {
const result = await next()

action({ ctx, result })

return result
})
}
import type { MiddlewareResult } from '@trpc/server'
import { initTRPC } from '@trpc/server'

import type { Context } from '../context'

type Action<C, R> = (opts: {
ctx: C
result: MiddlewareResult<R>
}) => unknown | Promise<unknown>

export function post<C, R>(action: Action<C, R>) {
const t = initTRPC.context<Pick<Context, 'get'>>().create()

return t.procedure
.use(async ({ ctx, next }) => {
const result = await next()

action({ ctx, result })

return result
})
}
my problems are: 1. i have no idea how to get the C (context) type right 2. the MiddlewareResult<> type is not exposed from @trpc/server so i can't pull it. my questions are: 1. can it be done? 2. how difficult it should be? 3. somebody has pointer on it? help please thanks
8 Replies
Nick
Nick2mo ago
Would “standalone middleware” fit your needs here? Sounds like you’re trying to reproduce that
y_nk
y_nkOP2mo ago
@Nick Lucas yes and no. what i'm doing is the same as standalone middleware but with reusable procedure instead as standalone middlewares are deprecated (as per docs)
y_nk
y_nkOP2mo ago
No description
y_nk
y_nkOP2mo ago
the difficulty that i'm having is that i would like to have the result type of the operation carried as well and it seems not possible when using, at the time of building the procedure, it's impossible to get the returned type of the operation since .concat() is only before .query(), .mutation() and .subscription() but not available after. so i'm 90% sure it's impossible :/ @Alex / KATT 🐱 would maybe confirm this easily
someProc: asPublic
.concat(
post(
async ({ result }) => {
console.log(result.foo) // <- pretty sure we can't grab the type of result before .query
}
)
)
.query(
async () => ({ foo: 'foo' })
)
someProc: asPublic
.concat(
post(
async ({ result }) => {
console.log(result.foo) // <- pretty sure we can't grab the type of result before .query
}
)
)
.query(
async () => ({ foo: 'foo' })
)
and this also not possible
someProc: asPublic
.mutation(
async () => ({ foo: 'foo' })
)
.concat( // <- can't chain after an op
post(
async ({ result }) => {
console.log(result.foo) // <- insert to audit log op, dispatch mutation to event bus
}
)
)
someProc: asPublic
.mutation(
async () => ({ foo: 'foo' })
)
.concat( // <- can't chain after an op
post(
async ({ result }) => {
console.log(result.foo) // <- insert to audit log op, dispatch mutation to event bus
}
)
)
only way i can think of is wrapping the whole procedure just to capture the return type in a generic but i can't even start to understand how to type it correctly
import type { ProcedureBuilder } from '@trpc/server/unstable-core-do-not-import'

function post<P extends ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
TCaller
>>(
proc: P,
handler: (opts: { ctx: ???, result: ??? }) => Promise<void> | void,
) {
return proc.use(async ({ ctx, next }) => {
const result = await next();
setTimeout(() => handler({ ctx, result }), 0); // run after
return result;
});
}

// usage
const router = {
someProc: post(
asPublic
.mutation(
() => { foo: 'foo' }
),
async ({ result }) => {
if (result.ok) {
console.log(result.data.foo) // typed
}
},
),
});
import type { ProcedureBuilder } from '@trpc/server/unstable-core-do-not-import'

function post<P extends ProcedureBuilder<
TContext,
TMeta,
TContextOverrides,
TInputIn,
TInputOut,
TOutputIn,
TOutputOut,
TCaller
>>(
proc: P,
handler: (opts: { ctx: ???, result: ??? }) => Promise<void> | void,
) {
return proc.use(async ({ ctx, next }) => {
const result = await next();
setTimeout(() => handler({ ctx, result }), 0); // run after
return result;
});
}

// usage
const router = {
someProc: post(
asPublic
.mutation(
() => { foo: 'foo' }
),
async ({ result }) => {
if (result.ok) {
console.log(result.data.foo) // typed
}
},
),
});
and even like this, it's kinda ugly i've tried to read source code of trpc to know if that could be implemented but it's unlikely. we'd need to have createResolver not to create a resolver but another kind of builder which returns either the created resolver or a post action which returns the resolver. since the builder pattern is well made and atomic, it's equally impossible to "patch" anything because it's neat and tight. otherwise i'd have to patch every return of createNewBuilder and implement my own query , mutation and subscription i'm pretty sure i'd loose typing in the process so i guess the "wrapper" is the least ugly of all
Alex / KATT 🐱
here's a quick and dirty version: https://github.com/trpc/trpc/compare/09-22-post-proc?expand=1 there's probably a way to wrap the whole procedure and not just the resolver at the end, but i don't have time to figure out how to do that actually it wasn't that hard, i added that too
y_nk
y_nkOP2mo ago
thanks a lot @Alex / KATT 🐱 , indeed the solution is damn simple i don't know why i didn't figure it out myself i guess i was stuck into trying to explicitly pass ctx and result types but didn't think to just carry them all with the function itself @Alex / KATT 🐱 would you see this as a useful chain after queries and mutations? we have "before-ish" middlewares, but how about post ones? as a lib feature, i meant. i wouldn't mind trying if you thought it's worth it.
Alex / KATT 🐱
Not sure, I still don't understand why you need it
y_nk
y_nkOP2mo ago
i want to handle my api and return the payload asap, but at the same time have the mutation advertised in a message queue after the response is delivered (doesn't need to be awaited) i could very much put it inside the handler directly but having to handle success/error case on top makes the handler "clogged with non handler primary logic"

Did you find this page helpful?