ampp
ampp•7mo ago

Session Wrapper with sessionId w/Clerk

I think this is another situation where i have a simple need but most examples are more complex with local storage etc . Now that i've progressed past just using the getIdentity/viewer in most use cases, i need the sessionId in a ton of UI calls to convex. I just want to have a useSessionQuery that passes in the session id from the clerk useAuth(). The whole app is already wrapped in a ConvexProviderWithClerk and that is my session provider basically? Maybe id want to modify the ConvexProviderWithClerk instead of importing <SessionProvider> in this case? I was looking at convex-helpers but that seems to be primarily for local session storage. The stack articles don't reference if you are already using a auth provider. I would also like to use the queryWithSession wrapper side of things on the convex side but I'm guessing i have to do some customization compared to the docs as I'm forcing ent ergonomics on all functions. Thanks
8 Replies
Michal Srb
Michal Srb•7mo ago
Yeah, define a useSessionQuery, and use the clerk-provided session ID. It will work like this: https://stack.convex.dev/track-sessions-without-cookies#2-utilities-for-react-hooks But you can't use the one from helpers, copy the code from helpers and define your own, which uses the Clerk context. On the server-side you need to modify your functions.ts to declare your new (or amend the existing) query and mutation constructors that require the session ID to be passed in. The stack article should point you in the right direction.
Session Tracking Without Cookies
Advice and resources for session tracking per-tab or per-browser via localStorage / sessionStorage using React Context, hooks, and some utilities to m...
ampp
amppOP•6mo ago
I've searched around and read a lot about this but I'm really struggling to structure the function. this is as close as i can get but ive tried a few versions.
export const mutation = customMutation(
baseMutation,
customCtx(async (baseCtx) => {
return await mutationCtx(baseCtx);
})
);

export const mutationWithSession = customMutation(
baseMutation, {
args: SessionIdArg,
input: async (ctx, { sessionId }) => {
const anonymousUser = await getAnonUser(ctx, sessionId);
return { ctx: { ...ctx, anonymousUser }, args: {} };
},
});
export const mutation = customMutation(
baseMutation,
customCtx(async (baseCtx) => {
return await mutationCtx(baseCtx);
})
);

export const mutationWithSession = customMutation(
baseMutation, {
args: SessionIdArg,
input: async (ctx, { sessionId }) => {
const anonymousUser = await getAnonUser(ctx, sessionId);
return { ctx: { ...ctx, anonymousUser }, args: {} };
},
});
I'm also not clear if it can be or should be part of the ent rules, custom ctx functions. I think if we have a viewer context then we should always have a sessionId with it.
async function mutationCtx(baseCtx: MutationCtx) {
const ctx = {
db: baseCtx.db as unknown as undefined,
skipRules: { table: entsTableFactory(baseCtx, entDefinitions) },
};
const entDefinitionsWithRules = getEntDefinitionsWithRules(ctx as any);
const viewerId = await getViewerId({ ...baseCtx, ...ctx });
(ctx as any).viewerId = viewerId;
const table = entsTableFactory(baseCtx, entDefinitionsWithRules);
(ctx as any).table = table;
// Example: add `viewer` and `viewerX` helpers to `ctx`:
const viewer = async () =>
viewerId !== null ? await table("users").get(viewerId) : null;
(ctx as any).viewer = viewer;
const viewerX = async () => {
const ent = await viewer();
if (ent === null) {
throw new Error("Expected authenticated viewer");
}
return ent;
};
(ctx as any).viewerX = viewerX;
return { ...ctx, table, viewer, viewerX, viewerId };
}
async function mutationCtx(baseCtx: MutationCtx) {
const ctx = {
db: baseCtx.db as unknown as undefined,
skipRules: { table: entsTableFactory(baseCtx, entDefinitions) },
};
const entDefinitionsWithRules = getEntDefinitionsWithRules(ctx as any);
const viewerId = await getViewerId({ ...baseCtx, ...ctx });
(ctx as any).viewerId = viewerId;
const table = entsTableFactory(baseCtx, entDefinitionsWithRules);
(ctx as any).table = table;
// Example: add `viewer` and `viewerX` helpers to `ctx`:
const viewer = async () =>
viewerId !== null ? await table("users").get(viewerId) : null;
(ctx as any).viewer = viewer;
const viewerX = async () => {
const ent = await viewer();
if (ent === null) {
throw new Error("Expected authenticated viewer");
}
return ent;
};
(ctx as any).viewerX = viewerX;
return { ...ctx, table, viewer, viewerX, viewerId };
}
Michal Srb
Michal Srb•6mo ago
What is the problem? You can combine the two together (use input instead of customCtx to define the custom mutation).
ampp
amppOP•6mo ago
I played with it some more. await getAnonUser(ctx, sessionId); was giving me errors with ctx as it was not compatible for ents
export const mutationWithSession = customMutation(
baseMutation, {
args: SessionIdArg,
input: async (baseCtx, { sessionId }) => ({
ctx: {
...baseCtx,
...mutationSessionCtx(baseCtx, sessionId),
},
args: { sessionId },
}),
});
export const mutationWithSession = customMutation(
baseMutation, {
args: SessionIdArg,
input: async (baseCtx, { sessionId }) => ({
ctx: {
...baseCtx,
...mutationSessionCtx(baseCtx, sessionId),
},
args: { sessionId },
}),
});
Ive tried a few versions of this without ...baseCtx etc. The problem now is I cant get the context in a function like export const testFunction = mutationWithSession({ - it tries to make me do things like const viewer = (await ctx).viewerId; its always undefined mutationSessionCtx is a copy of the standard ents mutationCtx above setup to recevie the sessionId, and that seems to work. To clarify, the session data part seems to work because its passed into mutationSessionCtx properly but i don't know how to get the context to work on the actual function. I wanted to try to build it with my already working mutationCtx as a test but that doesnt work either.
export const mutationWithSession = customMutation(
baseMutation, {
args: SessionIdArg,
input: async (baseCtx, { sessionId }) => ({
ctx: {
...baseCtx,
...mutationCtx(baseCtx)
},
args: { sessionId },
}),
});
export const mutationWithSession = customMutation(
baseMutation, {
args: SessionIdArg,
input: async (baseCtx, { sessionId }) => ({
ctx: {
...baseCtx,
...mutationCtx(baseCtx)
},
args: { sessionId },
}),
});
Michal Srb
Michal Srb•6mo ago
When you say "it doesn't work", what does that mean?
ampp
amppOP•6mo ago
ctx.viewerId is unavailable. or any other part of the context within my function like this
export const testFunction = mutationWithSession({
args: { sessionId: v.string() },
handler: async (ctx, args) => {

const viewer = await ctx.viewerId;
console.log("testFunction", viewer);
return { };
}
});
export const testFunction = mutationWithSession({
args: { sessionId: v.string() },
handler: async (ctx, args) => {

const viewer = await ctx.viewerId;
console.log("testFunction", viewer);
return { };
}
});
typescript or whatever suggests this based on shape: const viewer = (await ctx).viewerId; still undefined
Michal Srb
Michal Srb•6mo ago
Well I think for one you need to await the ctx builder helper:
export const mutationWithSession = customMutation(
baseMutation,
{
args: SessionIdArg,
input: async (baseCtx, { sessionId }) => ({
ctx: await mutationCtx(baseCtx, sessionId),
args: { sessionId },
}),
}
);
export const mutationWithSession = customMutation(
baseMutation,
{
args: SessionIdArg,
input: async (baseCtx, { sessionId }) => ({
ctx: await mutationCtx(baseCtx, sessionId),
args: { sessionId },
}),
}
);
I also passed sessionId in so you can expose it, and removed baseCtx since you probably don't need it. (btw you can do put "ts" after the first triple quotes to get highlighting in discord, which helps others read the code)
ampp
amppOP•6mo ago
oh i thought that might be a paid discord feature 😅 that worked, the one combination i didn't think of. Thanks again I feel a bit better asking about obscure stuff when i'm the first one to post code with something built into helpers like SessionIdArg in it on discord.. if we can trust their search