IdoPesok / zsa

https://zsa.vercel.app
MIT License
761 stars 23 forks source link

Merge Multiple Procedures into One in ZSA #122

Closed tusheer closed 3 months ago

tusheer commented 3 months ago

I am new to using ZSA and would like to know if there is a way to merge multiple procedures into a single procedure when creating an action.

IdoPesok commented 3 months ago

Hi, you can chain procedures. This is a sequential merge of procedures. If you are looking for something else, please provide a code example of what you want the code to be able to do with regard to merging. Happy to help out.

tusheer commented 3 months ago

Let's say we have four procedures: authProcedure, logProcedure, rateLimitProcedure, and another procedure. Each procedure is independent of the others. How can we use more than two procedures in a single action?

IdoPesok commented 3 months ago

Hi, I will go over a few things I would do:

(1) if these procedures can just be standalone functions, call functions from onStart event

const myAction = protectedAction
  .onStart(async () => {
    // call functions here
    await Promise.all([checkRateLimit(), log()])
  })
  .handler(async ({ ctx }) => {
    return `hello world ${ctx.auth.name}`
  })

note: you can even just call these in the handler itself and not use onStart

(2) Use a procedure builder

const buildAction = (run: ("rate-limit" | "log")[]) => {
  const procedure = createServerActionProcedure().handler(async () => {
    const session = await auth()

    if (run.includes("rate-limit")) {
      await checkRateLimit(session.id)
    }

    if (run.includes("log")) {
      await log(session.id)
    }

    return session
  })

  return procedure.createServerAction()
}

const myAction = buildAction(["rate-limit", "log"]).handler(async ({ ctx }) => {
  return `hello world ${ctx.name}`
})

I have been personally using these light weight procedure builder functions in my code base and have found them quite useful. I'm sure there are many ways to do this.

Would something like this work for you? The real magic of ZSA is the chaining of dependent procedures as we take care of context flows and input merging. However, if the procedures are independent (which I assume means no input and no context), I believe just calling them as normal functions -- within procedures/actions -- is the easiest path.

Hopefully that makes sense. If I misunderstood your situation, lmk and will discuss further.

drewhamlett commented 3 months ago

We're using a similar approach @IdoPesok. Something like this so we can get Newrelic tracing on things too

export function createAuthAction(
  name: string,
  { role }: { role?: 'user' | 'superuser' } = {}
) {
  const authAction = baseAction.handler(async () => {
    try {
      const user = await currentUser()

      if (role === 'superuser' && !(await getIsSuperUser(user.uid))) {
        throw new Error('User is not a superuser')
      }

      return { user }
    } catch {
      throw new Error('User not authenticated')
    }
  })

  return authAction.createServerAction().onStart((props) => {
    newrelic.setTransactionName(`serverAction: ${name}`)
    logger.debug({ props, module: 'server-action' }, 'createAction start')
  })
}
const createTodoAction = createAuthAction('createTodo', { role: 'superuser' })
IdoPesok commented 3 months ago

Awesome @drewhamlett! I will probably add something about this pattern in the docs soon, so more people are aware.