adonisjs / lucid

AdonisJS SQL ORM. Supports PostgreSQL, MySQL, MSSQL, Redshift, SQLite and many more
https://lucid.adonisjs.com/
MIT License
1.02k stars 189 forks source link

firstOrCreate() causes infinite loop in beforeUpdate() hook #1021

Open FrenchMajesty opened 3 months ago

FrenchMajesty commented 3 months ago

Package version

18.4.2

Describe the bug

I have noticed that trying to create a related model from an update hook causes an infinite loop which blows up the stack.

See the following code:

class User extends Model {
  @hasOne(() => PromoterOnboarding)
  public promoter: HasOne<typeof PromoterOnboarding>

  @beforeUpdate()
  public static async beforeUpdateHook(user: User) {
    if (user.$dirty.billingPlan === 'promoter') {
      console.log(user.$dirty)
      await user.related('promoter').query().firstOrCreate({ id: user.id })
    }
  }
}

When attempting to update the user via the following code:

    await user
      .merge({
        stripeCustomerId: customer.id,
        billingPlan: 'promoter',
        onboardedAt: DateTime.now(),
      })
      .save()

The server crashes out with the following error:

{ "message": "Maximum call stack size exceeded", "stack": "RangeError: Maximum call stack size exceeded\n at formatRaw (node:internal/util/inspect:1060:50)\n at formatValue (node:internal/util/inspect:839:10)\n at inspect (node:internal/util/inspect:363:10)\n at formatWithOptionsInternal (node:internal/util/inspect:2297:40)\n at formatWithOptions (node:internal/util/inspect:2159:10)\n at console.value (node:internal/console/constructor:342:14)\n at console.log (node:internal/console/constructor:379:61)\n at Function.beforeUpdateHook (/my-app-source/Models/User.ts:271:15)\n at Hooks.exec (/my-app-source/node_modules/@adonisjs/lucid/node_modules/@poppinss/hooks/build/src/Hooks/index.js:129:23)\n at Proxy.save (/my-app-source/node_modules/@adonisjs/lucid/build/src/Orm/BaseModel/index.js:1379:28)" }

I can observe that this beforeUpdate() hook is getting called recursively non-stop unless I kill the server. See screenshot:

Screenshot 2024-03-26 at 7 57 28 PM

Interestingly, the following snippet does not fail:

if (user.$dirty.billingPlan === 'promoter') {
      const exists = await user.related('promoter').query().first()
      if (!exists) {
        await user.related('promoter').create({ id: user.id })
      }
    }

It seems to be an issue with the firstOrCreate() method.

Will put together a repro repo if I have time

Reproduction repo

No response