passthrough
Associations in Dream can be defined with a passthrough constraint. This means that the only way these associations can be loaded is if data is passed through using a passthrough method. This is useful for localization patterns where the locale changes per request and is driven by headers:
export default class Post extends ApplicationModel {
@deco.HasOne('LocalizedText', {
polymorphic: true,
on: 'localizableId',
and: { locale: DreamConst.passthrough },
})
public currentLocalizedText: LocalizedText
}
const reloadedUser = await user
.passthrough({ locale: 'es-ES' })
.load('posts', 'currentLocalizedText')
.execute()
The DreamConst.passthrough value means locale is resolved from the query's passthrough context at query time rather than from a value fixed on the association itself. If you call .load('currentLocalizedText') (or preloadFor it) without first calling .passthrough({ locale }), Dream has no value to satisfy the and: clause and the association will not load.
Pairing a passthrough condition with a fixed fallback
A model can declare more than one association to the same target with different and: conditions — one driven by passthrough, one fixed to a constant value — and both resolve independently in the same preload tree. This is the shape behind a localization fallback: a LocalizedText row for the requested locale might not exist yet, so a second association pinned to your default locale gives you something to fall back to.
export default class Place extends ApplicationModel {
// HasMany for all localized texts (used by hosts for CRUD)
@deco.HasMany('LocalizedText', { polymorphic: true, on: 'localizableId', dependent: 'destroy' })
public localizedTexts: LocalizedText[]
// HasOne with a passthrough condition for the current request's locale
@deco.HasOne('LocalizedText', {
polymorphic: true,
on: 'localizableId',
and: { locale: DreamConst.passthrough },
})
public currentLocalizedText: LocalizedText
// HasOne fixed to the default locale, used as a fallback
// when the requested locale has no LocalizedText row
@deco.HasOne('LocalizedText', {
polymorphic: true,
on: 'localizableId',
and: { locale: 'en-US' },
})
public fallbackCurrentLocalizedText: LocalizedText
}
Guarantee the fallback row exists by creating it whenever the parent record is created. Binding the create to the hook's txn keeps it inside the parent's transaction when there is one:
@deco.AfterCreate()
public async createDefaultLocalizedText(this: Place, txn: DreamTransaction<Dream> | null) {
await this.txn(txn).createAssociation('localizedTexts', { locale: 'en-US', title: `My ${this.style}` })
}
On the serializer side, layer a delegatedAttribute('fallbackCurrentLocalizedText', ...) under a delegatedAttribute('currentLocalizedText', ...) so a record with no translation for the requested locale still renders the default-locale value instead of going blank. See serializers — layering two delegatedAttributes onto the same key for that pattern, and preloadFor for how both associations get loaded automatically.
A unique index on the polymorphic + locale columns keeps this one-row-per-locale invariant enforced at the database level. LocalizedText carries @SoftDelete() because it sits in its parent's dependent: 'destroy' chain, so the index covers only live rows — otherwise a deleted translation could never be re-added:
import { sql, SqlBool } from 'kysely'
await db.schema
.createIndex('localized_texts_localizable_for_locale')
.on('localized_texts')
.columns(['localizable_type', 'localizable_id', 'locale'])
.unique()
.where(sql<SqlBool>`deleted_at IS NULL`)
.execute()
Passing the same value through both channels
A passthrough association is satisfied by .passthrough(...) on the query, which is a separate channel from serializerPassthrough, which feeds serializer callbacks like customAttribute. A locale-aware endpoint typically needs to set both from the same resolved value:
const place = await Place
.passthrough({ locale: this.locale }) // association constraints
.preloadFor('forGuests')
.findOrFail(this.castParam('id', 'uuid'))
this.serializerPassthrough({ locale: this.locale }) // serializer callbacks
this.ok(place)
this.locale here is a getter that reads the Accept-Language header — see i18n — usage for the full pattern, including validating the header against your supported locales before trusting it.