External fields
Read this page in the documentation
External fields An external field is a model attribute whose contents live in an object or key-value store while the row lives in the database. The table column holds only the object key; the ORM keeps the two in step, so the link between a row and its blob is not something you maintain by hand. Use it for the payloads that do not belong in a row — avatars, attachments, rendered documents, large JSON blobs — while keeping them addressable through the model that owns them. Setting one up @ExternalField registers a companion column for you — avatarKey by default — so the CREATE TABLE gains a string column holding the key, and nothing else changes about the row. Using one Every field x gets four methods: Method | Does | --- | --- | loadX() | Read from the store into x. Returns null when the key column is empty. | saveX(value?) | Write to the store and persist the key. With no argument, writes whatever was assigned to x. | deleteX() | Delete the object and null the key column. | hasX() | Whether the object exists in the store. | How it behaves The value is a non-enumerable accessor. user.avatar is defined with Object.defineProperty, deliberately non-enumerable, so save()'s sweep over the instance's own keys never tries to write a Buffer into a column that does not exist. Assigning to it records a pending write; nothing goes over the wire until you save. save() is wrapped. Pending payloads are uploaded first, then the row is written with the key column already set — one row write, not an insert followed by an update. destroy() is wrapped. Each field's object is deleted before the row is. If the store is unreachable, the delete is swallowed and the row still goes: an orphaned object is better than a half-deleted record. Sweep orphans with a periodic job if that matters to you. Keys are generated as ${keyPrefix}${tableName}-${id} — for example avatar/users-42. A row with no primary key yet (saving a payload before the insert) falls back to a base-36 timestamp. An existing key is reused, so re-saving overwrites the same object rather than leaking a new one. Options Option | Default | Purpose | --- | --- | --- | store | — | The name the store was registered under. Required. | bucket | the store's defaultBucket | Bucket/container; becomes a key prefix on key-value stores. | keyColumn | <field>Key | The column holding the object key. | keyPrefix | <field>/ | Prefix on generated keys. | contentType | — | Recorded on write where the store supports it. | encoding | buffer | buffer, utf8, or json — parsed on read, stringified on write. | Stores Any shipped store works without an adapter — S3, MinIO, R2, GCS, Azure Blob, Redis and friends all name their methods differently (putObject vs uploadObject vs uploadBlob), and registerStore() normalises them: Key-value stores have no bucket concept, so a configured bucket becomes a key prefix and binary payloads round-trip through base64. For anything unrecognised, pass an object implementing ExternalStoreAdapter: toExternalStoreAdapter(store) is the normaliser, exported if you want to wrap a store yourself. It throws ExternalStoreError for a store it cannot adapt, and for an operation that needs a container on a store that has none. Introspection Things worth knowing There is no transaction across the two systems. The object write and the row write are separate. A crash between them leaves an object with no row (harmless, sweepable) or — if you write the key yourself — a row pointing at an object that is not there. The wrapped save() orders them so the harmless case is the one you get. loadX() is explicit. Reading a row does not fetch its objects; nothing is downloaded until you ask. That keeps findAll() over a thousand rows from becoming a thousand GETs. Bulk paths do not participate. These accessors are attached to instances, so Model.update({…}, { where }) and other static writes do not touch the store. Related reading Store adapters — the 100+ shipped stores Decorators Defining models