S
SolidJS3mo ago
dohn

What's the correct way to wrap server functions?

I have a bunch of duplicate authentication and validation logic in my server functions that I'd like to extract to a utility like createAuthenticatedAction that authenticates the user before running the server callback passed to it, but every time I create any wrapper around "use server" the compiler stats complaining. Where should I put a "use server" directive if I only want this helper to run on the server?
4 Replies
dohn
dohnOP3mo ago
from my understanding "use server" should only make a function an RPC, and it should only do it if you're not on the server already (otherwise it's just invoked) so my reasoning is I need a "use server" both inside my helper and inside the callback I pass to it, but that makes the compiler completely hang when I actually trying using it inside a form, the page will not load at all but no errors are logged to the console
kissa
kissa3mo ago
A minimal example would be nice to figure out what's going on. I had some trouble figuring out where 'use server' belongs too and had similar experiences (weird error messages and things mysteriously just not working), but I resolved these by starting from the with-auth SolidStart template and adapting it for my needs. Basically I found out that these forms are "safe":
export const loaderFunction = cache(async () => {
'use server';
// ... can use database, getSession(), etc,
})

export const actionFunction = action(async (formData) => {
'use server';
// ... likewise, used in form actions
})
export const loaderFunction = cache(async () => {
'use server';
// ... can use database, getSession(), etc,
})

export const actionFunction = action(async (formData) => {
'use server';
// ... likewise, used in form actions
})
I have all my loaders in loaders.ts and actions in actions.ts, and use them in route tsx files like this:
export default function Index() {
const data = createAsync(() => loaderFunction(), { deferStream: true })
// data() can be used here
return <form action={actionFunction} method="post"> ... </form>
}
export default function Index() {
const data = createAsync(() => loaderFunction(), { deferStream: true })
// data() can be used here
return <form action={actionFunction} method="post"> ... </form>
}
There's probably plenty of ways to structure your project but this works for me. And route protection works with helpers like this:
export async function ensureAdminIsLoggedIn() {
try {
let user = await getLoggedInUserId();
// verify that user is admin
// ...
} catch (err) {
throw redirect('/login')
}
}
export async function ensureAdminIsLoggedIn() {
try {
let user = await getLoggedInUserId();
// verify that user is admin
// ...
} catch (err) {
throw redirect('/login')
}
}
which is then called from those loader and action functions that need to be protected.
dohn
dohnOP3mo ago
thanks for your help! a practical example in this case is basically how to craft a function like createAuthenticatedAction that wraps action from solid-router so that i dont have to always do auth checks inside each action (which i've been doing for like 50 actions already) i figured out a way which is defining the actual implementation of the action in a separate file with a top-level "use server" and using the single export inside action, so that works. only annoying thing is that i need to split two files just to create an action
kissa
kissa3mo ago
I see where the problems with createAuthenticatedAction could come from. I tried to just wrap action in a function that just calls the callback and it breaks with "Error: callback is not defined". Probably because action lives in the browser and function with use server lives on the server, and you can't call callback (which again lives in the browser) from the server. Not 100% sure what's going on but that's my theory.
// This won't work:
function wrappedAction(callback: (formData: FormData) => Promise<unknown>) {
return action(async (formData) => {
'use server'
// Would do auth checks here

return callback(formData)
})
}
export const joinEvent = wrappedAction(...)
// this works:
//export const joinEvent = action(...)
// This won't work:
function wrappedAction(callback: (formData: FormData) => Promise<unknown>) {
return action(async (formData) => {
'use server'
// Would do auth checks here

return callback(formData)
})
}
export const joinEvent = wrappedAction(...)
// this works:
//export const joinEvent = action(...)
So, I guess your top-level "use server" or some other solution is the way around this. Adding "use server" to wrappedAction won't work either since the client can't just call server functions, it needs action for that. I'm starting to think that "use server" is maybe not the best abstraction to split client and server code, since there needs to be some bundler magic to facilitate communication between those two worlds, and your usual assumptions (such as "if I have a callback, I can simply call it") won't necessarily hold in that magic land
Want results from more Discord servers?
Add your server