Skip to main content

attributes

Attributes are usually set as part of create or update calls, or directly using dot notation (balloon.color = 'red' and then calling balloon.save); however, there are times (e.g. when adding custom getters and setters that transform the data prior to persisting) when it is helpful to bypass the getters and setters.

Decimal columns​

PostgreSQL numeric/decimal columns return JavaScript number — not string or a Decimal wrapper. Dream's type-sync converts these to TypeScript number, so DreamColumn<Place, 'latitude'> resolves to number | null (or number if NOT NULL). No Number(value) coercion is needed when reading.

To control how many decimal places appear in a response, use the precision option on the serializer attribute rather than rounding in application code. It rounds the rendered number to that many decimal places:

// In your serializer
.attribute('latitude', { precision: 7 })
.attribute('price', { precision: 2 })
// Also works on delegatedAttribute
.delegatedAttribute('place', 'latitude', { precision: 7 })

This handles the common floating-point quirk where 1.1 + 2.2 becomes 3.3000000000000003: at precision: 2, it renders as 3.3. precision changes only the rendered value; the OpenAPI shape is unchanged.

Branching on enum values​

When branching on a database enum column, always use an exhaustive switch with a const _never: never default — never an if/else if chain. Database enums regenerated into @src/types/db.js are closed sets: an if/else if chain silently no-ops when a new enum value is added, while the _never default turns a missing case into a compile error.

const style = place.style // e.g. PlaceStylesEnum
switch (style) {
case 'cabin':
return handleCabin(place)
case 'cottage':
return handleCottage(place)
case 'apartment':
return handleApartment(place)
default: {
const _never: never = style
throw new Error(`Unhandled style: ${String(_never)}`)
}
}

This rule applies to any closed union of string literals: database enums, STI type discriminators, or any other union declared in code.

Bypassing methods​

getAttributes​

Returns an object containing all the columns on this Dream class, as well as their values, bypassing getters:

const user = User.new({ email: 'how@yadoin' })
user.getAttributes()
// {
// email: 'how@yadoin',
// ...
// }

getAttribute​

Returns the value for a columnName provided, bypassing the getters:

const user = User.new({ email: 'how@yadoin' })
user.getAttribute('email')
// 'how@yadoin'

setAttributes​

Takes the attributes passed in and sets their values internally, bypassing custom setters defined for these attributes:

const user = new User()
user.setAttributes({ email: 'my@email', password: 'my password' })

setAttribute​

Changes the attribute value for a single attribute internally, bypassing custom-defined setters:

const user = new User()
user.setAttribute('email', 'sally@gmail.com')

Non-bypassing methods​

assignAttributes​

Takes the attributes passed in and sets their values, leveraging custom setters defined for these attributes:

const user = new User()
user.assignAttributes({ email: 'my@email', password: 'my password' })

assignAttribute​

Changes the attribute value for a single attribute, leveraging custom-defined setters. assignAttribute is literally this[attr] = value.

const user = new User()
user.assignAttribute('email', 'sally@gmail.com')

Which writes run the setter​

Dream splits attribute assignment into two paths:

  • create(), update(), assignAttribute/assignAttributes, and a plain this.email = ... run your custom setter.
  • setAttribute/setAttributes bypass custom setters and write the attribute bag directly — use these when you deliberately want the untransformed value, and setAttribute inside a setter's own body (to avoid re-entering the setter and recursing infinitely). setAttributes still runs the setter of a key that is not a column, and of a json/jsonb column.

So input arriving through create/update/extracted params is transformed, while internal hydration (loading a row from the database) writes raw and is never double-transformed, json/jsonb columns aside.

An @deco.Encrypted property is still encrypted by setAttributes, and its backing encrypted<Name> column accepts only decryptable ciphertext or null from setAttribute/setAttributes, throwing DoNotSetEncryptedFieldsDirectly otherwise.

A { skipHooks: true } query update — Booking.where(...).update(attrs, { skipHooks: true }) — never instantiates a model, so no setter runs at all: the attribute names go into the UPDATE as given and any transform is silently skipped. Naming an @deco.Encrypted column's backing column this way throws unless the value is null. To update an encrypted column, use a path that instantiates the model — an instance update, a query update without skipHooks, or a query update with { lock: true } (with or without skipHooks) — and write through the plaintext property. The instance form, instance.update(attrs, { skipHooks: true }), is unaffected by this distinction — it still assigns through assignAttributes, so custom setters still run; skipHooks there means lifecycle hooks, not setters.

Transforming a column on write​

To normalize or clamp an input as it's written to a real column (lowercase an email, bound a number, coerce null), define a custom getter/setter pair on the model over the column's own name. Read through getAttribute and write through setAttribute — setAttribute writes the attribute bag directly and does not re-enter your setter, which avoids the infinite recursion that this.email = ... inside the setter would cause:

class User extends ApplicationModel {
public get email(): DreamColumn<User, 'email'> {
return this.getAttribute('email')
}

public set email(value: DreamColumn<User, 'email'>) {
this.setAttribute('email', value === null ? null : value.toLowerCase())
}
}

This is distinct from @deco.Virtual: a virtual backs a property that isn't a column (or maps to a differently-named one), whereas here the accessors wrap the column under its own name. No decorator is needed — the model's accessor simply overrides the one Dream installs.