S
SolidJS6d ago
Anshu

How to redirect from sub query

Hello there, I'm trying to call a query within another query to avoid duplicate database calls. For example:
// This query is called in multiple places but will only run once and return data to all of them
const getRedirectOrUserQuery = query(async () => {
"use server";
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw redirect("/login");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw redirect("/register");
return { id: user.id, username: user.username };
}, "get-redirect-or-user");

// Same logic, but as a server function instead of a query
const getRedirectOrUser = async () => {
"use server";
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw redirect("/login");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw redirect("/register");
return { id: user.id, username: user.username };
};

// Main query used in createAsync
export const getUser = query(async () => {
"use server";
const user = await getRedirectOrUserQuery();
return user;
}, "user");
// This query is called in multiple places but will only run once and return data to all of them
const getRedirectOrUserQuery = query(async () => {
"use server";
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw redirect("/login");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw redirect("/register");
return { id: user.id, username: user.username };
}, "get-redirect-or-user");

// Same logic, but as a server function instead of a query
const getRedirectOrUser = async () => {
"use server";
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw redirect("/login");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw redirect("/register");
return { id: user.id, username: user.username };
};

// Main query used in createAsync
export const getUser = query(async () => {
"use server";
const user = await getRedirectOrUserQuery();
return user;
}, "user");
The issue is that when the user is not found in the session or database, the getRedirectOrUserQuery function throws a redirect, but that exception is never received by getUser. Instead, getRedirectOrUserQuery returns undefined, and then getUser returns undefined. If I look at the type of const user, it's inferred as non-nullable, but at runtime, it's actually undefined. What I expect is for getRedirectOrUserQuery to throw a redirect, which should redirect the user and stop the execution of getUser. Interestingly, if I replace the inner query call with a server function like this:
export const getUser = query(async () => {
"use server";
const user = await getRedirectOrUser();
return user;
}, "user");
export const getUser = query(async () => {
"use server";
const user = await getRedirectOrUser();
return user;
}, "user");
Then it works as expected.
15 Replies
Anshu
AnshuOP6d ago
The only problem with using the function method is that it calls the database every time the function is invoked. On the other hand, when using a query, it only calls the database once and returns the data to every query that calls it.
ryansolid
ryansolid5d ago
Discussion for this can be found here: https://github.com/solidjs/solid-start/issues/1624
GitHub
[Bug?]: Redirects from nested cache server functions get ignored ...
Duplicates I have searched the existing issues Latest version I have tested the latest version Current behavior 😯 When a cache server function calls another cache server function, any redirects thr...
ryansolid
ryansolid5d ago
Basically query is the layer that handles the redirect,.. its the try catch. So nesting them can't work the way you want.
Madaxen86
Madaxen865d ago
A simplified version of your example:
// Same logic, but as a server function instead of a query
const getRedirectOrUser = async () => {
"use server";
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw redirect("/login");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw redirect("/register");
return { id: user.id, username: user.username };
};
// This query is called in multiple places but will only run once and return data to all of them
const getRedirectOrUserQuery = query(getRedirectOrUser, "get-redirect-or-user");

// Main query used in createAsync
export const getUser = getRedirectOrUserQuery; //inherits the query-key of the query so both are revalidated and don't get out of sync.
// Same logic, but as a server function instead of a query
const getRedirectOrUser = async () => {
"use server";
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw redirect("/login");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw redirect("/register");
return { id: user.id, username: user.username };
};
// This query is called in multiple places but will only run once and return data to all of them
const getRedirectOrUserQuery = query(getRedirectOrUser, "get-redirect-or-user");

// Main query used in createAsync
export const getUser = getRedirectOrUserQuery; //inherits the query-key of the query so both are revalidated and don't get out of sync.
Anshu
AnshuOP5d ago
@Madaxen86 that was just an example of nested query
Madaxen86
Madaxen865d ago
Note: that with query is not a server-side cache. What you can achieve is storing data in the requestEvent to share it within chain of a single request.E.g.
const getRedirectOrUser = async () => {
"use server";
let user
const evt = getRequestEvent();
if (evt) user = evt.locals.user;
if (user) return user;

const session = await getSession();
const userId = session.data.userId;
user = await db.user.findUnique({ where: { id:userId } });

if (!user) throw redirect("/register");

evt?.locals.user = user
return user
}
const getRedirectOrUser = async () => {
"use server";
let user
const evt = getRequestEvent();
if (evt) user = evt.locals.user;
if (user) return user;

const session = await getSession();
const userId = session.data.userId;
user = await db.user.findUnique({ where: { id:userId } });

if (!user) throw redirect("/register");

evt?.locals.user = user
return user
}
Thinking about it, you could also simply just add more data to the session instead of the request event. That would just mean that as long as the session is valid, you could not lock a user.
Anshu
AnshuOP5d ago
Hmm, but I don't think that will work either. What I ultimately want is to deduplicate the database call, which is why I wrap it in a query. When we hit a page, it runs all the required queries in parallel. So, if one query is reading from the database while another tries to read from event.locals, it will be undefined.
Madaxen86
Madaxen865d ago
That's true. That's when putting the relevant data in session would help assuming that it is an encrypted cookie (not a db call)
Anshu
AnshuOP5d ago
That can work for the user details case, but I have other cases where I can't store data in the session and have to make a database call I think I should consider other ways to dedup the calls instead of relying on the query. As Ryan said, throwing a redirect in a sub-query won't work.
Madaxen86
Madaxen865d ago
For other things you might need server side cache like Redis
Brendonovich
Brendonovich4d ago
Caching the call on the request is what I'd do:
async function getSessionUser() {
const session = await getSession();
const userId = session.data.userId;
return await db.user.findUnique({ where: { id:userId } });
}

async function getSessionUserDeduped() {
const { locals } = getRequestEvent();

locals.getSessionUser ??= getSessionUser();

return await locals.getSessionUser;
}
async function getSessionUser() {
const session = await getSession();
const userId = session.data.userId;
return await db.user.findUnique({ where: { id:userId } });
}

async function getSessionUserDeduped() {
const { locals } = getRequestEvent();

locals.getSessionUser ??= getSessionUser();

return await locals.getSessionUser;
}
Madaxen86
Madaxen864d ago
Nice, but that still doesn’t dedupe on parallel request by the client.
Brendonovich
Brendonovich4d ago
Ah good point, it’ll at least dedupe during SSR
ryansolid
ryansolid4d ago
To be fair no independent API call would dedupe in that case either. Like if you had API routes etc.. pretty much at that point you are looking for Redis or something.

Did you find this page helpful?