-
I'm writing an application that has a number of services (facades for business logic) and I was looking at middleware to try to compose those services, ideally with something like: const feature1Client =
baseClient
.use(middleware1) << adds user id to context
.use(middleware3) << adds orgId to context
.use(middleware4) << adds user roles to contexts, requires userId, orgId;
export const feature1Action1 = feature1Client
.metadata({ actionName: "feature1Action1" })
.schema(feature1Action1Input)
.action(async ({ parsedInput: { foo, bar, baz }, ctx: { userId, orgId, roles, facadeService1 } }) => {
if(orgId==1 && roles.includes("admin") {
await facadeService1.doSomething({ foo, bar, baz, userId });
}
}); Then in a different feature, I want to: const feature2Client =
baseClient
.use(middleware1) << adds user id to context
.use(middleware5) << facadeService2 to context, requires userId;
export const feature2Action1 = feature2Client
.metadata({ actionName: "feature2Action1" })
.schema(feature2Action1Input)
.action(async ({ parsedInput: { bat }, ctx: { userId, facadeService2 } }) => {
await facadeService2.doSomethingDifferent({ bat, userId });
}); Does anyone have an example where there are a number of composable middlewares and the resulting context is typed correctly? No matter my experiments, I haven't been able to nail this one. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
Hi, this does not work as you may expect because you currently need to extend the context by spreading the previous value into a new object inside the export const actionClient = createSafeActionClient()
.use(async ({ next }) => {
return next({ ctx: { userId: crypto.randomUUID() } });
})
.use(async ({ next, ctx }) => {
return next({ ctx: { ...ctx, sessionId: crypto.randomUUID() } });
}); I initially thought this would be the better solution, but I now feel like it would be much smarter and straight forward to only allow context objects and automatically extend the previous context with the property set in the new defined middleware, just like tRPC does. This change would make the middleware system much more composable. |
Beta Was this translation helpful? Give feedback.
-
I kind of ended up in the same place without seeing the tRPC implementation, but it still feels kind of verbose to me. type Context = {
octokit: Octokit;
onboardingService: OnboardingService;
orgId: string;
posthog: IPostHogService;
userId: string;
workspaceId: string;
};
export const onboardingActionClient = authActionClient
.use(withTenant<Context>())
.use(withUserOctokit<Context>())
.use(withOnboardingService<Context>()) And then in an individual middleware, I end up having to explicitly specify which inputs/outputs I expect in the context... import { auth } from "@clerk/nextjs/server";
import { MiddlewareFn } from "next-safe-action";
import { ApiError } from "@/lib/errors";
import { DrizzleWorkspaceRepo } from "@/repos/workspace";
type WithTenantContext = {
orgId: string;
workspaceId: string;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type WithTenant = <TMyContext extends WithTenantContext>() => MiddlewareFn<string, object, any, TMyContext>;
export const withTenant: WithTenant =
() =>
async ({ ctx, next }) => {
const { orgId } = auth();
if (!orgId) {
throw new ApiError({ code: "UNAUTHORIZED", message: "orgId is missing" });
}
const workspaceRepo = new DrizzleWorkspaceRepo();
const workspace = await workspaceRepo.getByTenantId(orgId);
if (!workspace) {
throw new ApiError({ code: "PRECONDITION_FAILED", message: `workspace missing for orgId` });
}
return next({ ctx: { ...ctx, orgId, workspaceId: workspace.id } });
}; It would definitely be nice if there were a way to make this more composable out of the box. |
Beta Was this translation helpful? Give feedback.
v7.6.0 will include support for defining standalone middleware functions. See #229