Skip to main content

dirty

In an ORM ecosystem, it is often necessary to determine if a record has any changes to be saved or not. Dream provides methods for introspecting the changing state of your model:

const place = await Place.findOrFail(id) // name: 'Cozy Cabin'
place.name = 'Bear Den Lodge'

place.isDirty // true
place.willSaveChangeToAttribute('name') // true
place.willSaveChangeToAttribute('style') // false
place.dirtyAttributes() // { name: 'Bear Den Lodge' } — pending values
place.changedAttributes() // { name: 'Cozy Cabin' } — original values

isDirty​

The isDirty getter returns true or false, depending on whether a record has unsaved changes:

const user = await User.first()
console.log(user.isDirty) // false

user.email = 'new@email'
console.log(user.isDirty) // true

willSaveChangeToAttribute​

willSaveChangeToAttribute(column) checks a single column for a pending change:

const user = await User.first()
user.email = 'new@email'

user.willSaveChangeToAttribute('email') // true
user.willSaveChangeToAttribute('name') // false

dirtyAttributes​

dirtyAttributes() returns the columns with pending changes and the values about to be saved:

const user = await User.first() // email: 'how@yadoin'
user.email = 'new@email'

console.log(user.dirtyAttributes())
// {
// email: 'new@email',
// }

On a record that has not been saved yet, every column given a value other than undefined counts as dirty, so User.new({ name: 'Alice' }) reports name immediately.

changedAttributes​

changedAttributes() returns the same columns as dirtyAttributes(), but with the values they held before the change, as of the last load or save:

const user = await User.first() // email: 'how@yadoin'
user.email = 'new@email'

console.log(user.changedAttributes())
// {
// email: 'how@yadoin',
// }

changes​

For both sides at once, call changes, which gives the previous and new value for each changed field:

const user = await User.first()
user.email = 'new@email'

console.log(user.changes())
// {
// email: {
// was: 'how@yadoin',
// now: 'new@email',
// },
// }

changes() keeps reporting what the last save changed, beside any newer unsaved change, until the next save.

Dirty tracking in hooks and validations​

In a validation or Before* hook, the pending-change methods describe the save in progress. this.willSaveChangeToAttribute('status') tells whether this save changes status; this.status is the value being saved, this.changedAttributes().status is the value before the change, and this.changes() gives both as { was, now }. To run a Before* hook only when a column changes, gate it with ifChanging, which on a create counts only the columns that were assigned.

Save's After* hooks run once the snapshot has been refreshed, so there willSaveChangeToAttribute is false and dirtyAttributes() is empty. Use savedChangeToAttribute(column) and changes(), which report the last save:

@deco.AfterSave()
public async logRename(this: Place) {
if (this.savedChangeToAttribute('name')) {
// ...
}
}

After a create, the last save covers every column the insert returned — the primary key, timestamps, database defaults, and null for each nullable column nobody assigned — so savedChangeToAttribute is true for all of them. To run an after hook only when a column changed, gate it with ifChanged, which is checked at the save itself and on a create counts only the columns that were assigned.

Dirty tracking is against the last load or save, not the current DB row​

The comparison these methods report is against the instance's own snapshot from its last load or save — not against whatever the row currently holds in the database. A persisted instance with nothing dirty issues no UPDATE on save() or update(), and leaves updatedAt unstamped: update({}), or an update() assigning values equal to the current ones, is a no-op rather than a touch. Before-save hooks and validations still run first, so a hook that dirties the record turns it back into a real write.

Encrypted properties​

Assigning a new plaintext to an @deco.Encrypted() property marks its encrypted<Name> column dirty; assigning the plaintext it already holds leaves the record clean, so that update issues no UPDATE either. dirtyAttributes() and changedAttributes() report the persisted encrypted<Name> key, not the plaintext virtual property — getAttribute('<plaintext>') returns undefined because it isn't the decrypting accessor, while getAttribute('encrypted<Name>') returns ciphertext. Read the decrypted value via the instance property (instance.<plaintext>) instead.