Skip to content

$setAuth() on hook client breaks no-hook-trigger guarantee, causing infinite recursion #2493

@docloulou

Description

@docloulou

Describe the bug
The documentation for entity mutation hooks states:

mutation operations initiated with this client will not trigger the entity mutation hooks again
However, calling client.$setAuth(...) inside afterEntityMutation creates a new ZenStackClient instance (due to immutability) that loses the no-hook-trigger flag. Any mutation performed through this derived client re-triggers the entity mutation hooks, leading to infinite recursion.
To Reproduce

import { definePlugin } from '@zenstackhq/orm'

export const myPlugin = definePlugin({
  id: 'my-plugin',
  onEntityMutation: {
    runAfterMutationWithinTransaction: true,

    async beforeEntityMutation({ model, action, queryId, loadBeforeMutationEntities }) {
      if (model !== 'Order') return
      if (action !== 'update') return
      const entities = (await loadBeforeMutationEntities()) || []
      cache.set(queryId, entities)
    },

    async afterEntityMutation({ model, action, queryId, loadAfterMutationEntities, client }) {
      if (model !== 'Order') return
      if (action !== 'update') return

      const afterEntities = (await loadAfterMutationEntities()) || []
      const beforeEntities = cache.get(queryId) || []

      for (const after of afterEntities) {
        const before = beforeEntities.find(b => b.id === after.id)
        if (!before || after.amount === before.amount) continue

        // WORKS: using client directly (no-hook-trigger preserved)
        await client.order.update({
          where: { id: after.id },
          data: { status: after.amount === 0 ? 'completed' : 'active' },
        })

        // BUG: using $setAuth creates a NEW client that re-triggers hooks
        const adminClient = client.$setAuth({ role: 'admin' })
        await adminClient.order.update({
          where: { id: after.id },
          data: { status: after.amount === 0 ? 'completed' : 'active' },
        })
        // ^ This triggers beforeEntityMutation + afterEntityMutation again
        // causing infinite recursion
      }
    },
  },
})

const cache = new Map<any, any[]>()

Expected behavior
client.$setAuth(...) should return a derived client that preserves the no-hook-trigger behavior of the original hook client. The auth context should change, but the hook suppression should be inherited.
Actual behavior
client.$setAuth(...) returns a new ZenStackClient instance that re-triggers entity mutation hooks, causing infinite recursion when the hook performs mutations on the same model it listens to.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions