Skip to content

Commit

Permalink
feat: add model.lockForUpdate method to lock the model row for updates
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Feb 27, 2024
1 parent 018c4ce commit 703e6b5
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 0 deletions.
24 changes: 24 additions & 0 deletions src/orm/base_model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,30 @@ class BaseModelImpl implements LucidRow {
return this
}

/**
* The lockForUpdate method re-fetches the model instance from
* the database and locks the row to perform an update. The
* provided callback receives a fresh user instance and should
* use that to perform an update.
*/
async lockForUpdate<T>(callback: (user: this) => Promise<T> | T): Promise<T> {
const Model = this.constructor as LucidModel
const queryClient = Model.$adapter.modelClient(this)

return managedTransaction<T>(queryClient, async (trx) => {
const user = await Model.query({ client: trx })
.forUpdate()
.where(Model.primaryKey, this.$primaryKeyValue)
.first()

if (!user) {
throw new errors.E_MODEL_DELETED()
}

return callback(user as this)
})
}

/**
* Perform delete by issuing a delete request on the adapter
*/
Expand Down
16 changes: 16 additions & 0 deletions src/types/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,23 @@ export interface LucidRow {
* Actions to perform on the instance
*/
save(): Promise<this>

/**
* The lockForUpdate method re-fetches the model instance from
* the database and locks the row to perform an update. The
* provided callback receives a fresh user instance and should
* use that to perform an update.
*/
lockForUpdate<T>(callback: (user: this) => Promise<T> | T): Promise<T>

/**
* Perform delete by issuing a delete request on the adapter
*/
delete(): Promise<void>

/**
* Reload/Refresh the model instance
*/
refresh(): Promise<this>

/**
Expand Down
124 changes: 124 additions & 0 deletions test/orm/base_model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7926,3 +7926,127 @@ test.group('Base model | inheritance', (group) => {
assert.isFalse(MyBaseModel.$hooks.has('before:create', hook2))
})
})

test.group('Base Model | lockForUpdate', (group) => {
group.setup(async () => {
await setup()
})

group.teardown(async () => {
await cleanupTables()
})

test('lock model row for update', async ({ fs, assert }) => {
const app = new AppFactory().create(fs.baseUrl, () => {})
await app.init()
const db = getDb()
const adapter = ormAdapter(db)

const BaseModel = getBaseModel(adapter)

class User extends BaseModel {
@column({ isPrimary: true })
declare id: number

@column()
declare username: string

@column()
declare email: string

@column()
declare points: number
}

const user = await User.create({ email: '[email protected]', username: 'virk', points: 0 })
await Promise.all([
user.lockForUpdate(async (freshUser) => {
freshUser.points = freshUser.points + 1
await freshUser.save()
}),
user.lockForUpdate(async (freshUser) => {
freshUser.points = freshUser.points + 1
await freshUser.save()
}),
])

await user.refresh()
assert.equal(user.points, 2)
})

test('re-use initial model transaction', async ({ fs, assert }) => {
const app = new AppFactory().create(fs.baseUrl, () => {})
await app.init()
const db = getDb()
const adapter = ormAdapter(db)

const BaseModel = getBaseModel(adapter)

class User extends BaseModel {
@column({ isPrimary: true })
declare id: number

@column()
declare username: string

@column()
declare email: string

@column()
declare points: number
}

const user = await User.create({ email: '[email protected]', username: 'virk', points: 0 })
const trx = await db.transaction()
user.useTransaction(trx)

await Promise.all([
user.lockForUpdate(async (freshUser) => {
freshUser.points = freshUser.points + 1
await freshUser.save()
}),
user.lockForUpdate(async (freshUser) => {
freshUser.points = freshUser.points + 1
await freshUser.save()
}),
])

assert.isFalse(trx.isCompleted)
await trx.rollback()

await user.refresh()
assert.equal(user.points, 0)
})

test('throw error when row is missing', async ({ fs, assert }) => {
const app = new AppFactory().create(fs.baseUrl, () => {})
await app.init()
const db = getDb()
const adapter = ormAdapter(db)

const BaseModel = getBaseModel(adapter)

class User extends BaseModel {
@column({ isPrimary: true })
declare id: number

@column()
declare username: string

@column()
declare email: string

@column()
declare points: number
}

const user = await User.create({ email: '[email protected]', username: 'virk', points: 0 })
await user.delete()
await assert.rejects(() =>
user.lockForUpdate(async (freshUser) => {
freshUser.points = freshUser.points + 1
await freshUser.save()
})
)
})
})

0 comments on commit 703e6b5

Please sign in to comment.