Derived Fields
Derived fields are field values (i.e. primitives) that can be calculated from other data within your domain model.
There are two axis by which to categorize derived fields:
Synchronous vs. Asynchronous
Synchronous derived fields only rely on other fields/columns within the same entity (i.e. an
Author
s initials only require knowing the sameAuthor
'sfirstName
andlastName
), so do not need to load any other entities to calculate their value.Async derived values do rely on fields from other entities (i.e. an
Author
's number of books requires loading theauthor.books
one-to-many collection), and so need to load references/collections before calculating their value.Persisted vs. Unpersisted
Persisted derived fields have their latest value stored in the database, typically for quick access on summary screens, filtering, or exposing the values to external systems like a data warehouse.
Unpersisted derived fields are not stored in the database, and have their value recalculated every time they're accessed.
And so there are 4 combinations (async vs. sync x
persisted vs. unpersisted), and Joist has an approach for modeling each combination.
Synchronous, Unpersisted Fields
For synchronous, unpersisted fields, you can just implement these as getters in your domain objects:
export class Author {
/** Implements the business logic for an unpersisted derived value. */
get fullName(): string {
return this.firstName + (this.lastName ? ` ${this.lastName}` : "");
}
}
Synchronous, Persisted Fields
For synchronous, persisted fields, there will be a column in the database to hold the value, i.e. authors.initials
, which you can mark as sync
in joist-config.json
:
{
"entities": {
"Author": {
"fields": {
"initials": { "derived": "sync" }
}
}
}
}
This will cause the Author.initials
field to not have a setter, only an abstract
getter than you must implement:
export class Author {
/** Implements the business logic for a (synchronous) persisted derived value. */
get initials(): string {
return (this.firstName || "")[0] + (this.lastName !== undefined ? this.lastName[0] : "");
}
}
This getter will be automatically called by Joist during any INSERT
or UPDATE
of Author
, to determine the latest value.
Because of this, synchronous persisted derived fields should be cheap to calculate.
Asynchronous, Unpersisted Fields
For async, unpersisted fields, you can use hasAsyncProperty
:
export class Author {
/** Example of an async property that can be loaded via a populate hint. */
readonly numberOfBooks: AsyncProperty<Author, number> = hasAsyncProperty("books", (a) => {
return a.books.get.length;
});
}
Because it's async, the property must be loaded with a populate hint:
const a = await em.load(Author, "a:1", "numberOfBooks");
console.log(a.numberOfBooks.get);
Asynchronous, Persisted Fields
For async, persisted fields, there will be a column in the database to hold the value, i.e. authors.number_of_books
, which you can mark as async
in joist-config.json
:
{
"entities": {
"Author": {
"fields": {
"numberOfBooks": { "derived": "async" }
}
}
}
}
And then implement it in the Author
domain model:
import { PersistedAsyncProperty, hasPersistedAsyncProperty } from "joist-orm";
class Author extends AuthorCodegen {
readonly numberOfBooks: PersistedAsyncProperty<Author, number> = hasPersistedAsyncProperty(
"numberOfBooks",
"books",
(author) => author.books.get.length,
);
}
Joist will call this lambda:
- When the
Author
is initially created - When the
Author
is updated - Whenever one of the
Author
's books changes
For example, in this scenario:
const a1 = await em.load(Author, "a:1");
const a2 = await em.load(Author, "a:2");