References
Working with entity relationships in a DDD-friendly way
References in JustScale allow you to work with entity relationships without exposing raw IDs. The repository resolves references — how it fetches the entity is an implementation detail.
Defining References
Use field.ref() to define a reference to another model:
import { defineModel, field } from '@justscale/core/models'
class Author extends defineModel({
name: field.string().max(100),
email: field.string().max(255).unique(),
}) {}
class Post extends defineModel({
title: field.string().max(200),
status: field.enum('PostStatus', ['draft', 'published'] as const),
author: field.ref(Author), // Reference<Author>, not a string ID
}) {}Creating References at Boundaries
Raw strings only enter the system at boundaries — controllers, process handlers, external APIs. Convert them to typed references with tagged templates:
import { Author, Post } from './models'
// At a boundary: convert string to typed reference
const authorRef = Author.ref`${authorId}`
// Use in relationships
const post = await postRepo.insert({
title: 'My Post',
status: 'draft',
author: authorRef, // Reference<Author>, not a raw string
})
// A persistent entity IS a valid reference — just pass it directly
const author = await authorRepo.findOne(Author.fields.email.eq('alice@example.com'))
const post2 = await postRepo.insert({
title: 'Another Post',
status: 'draft',
author: author!, // Persistent<Author> works as Ref<Author>
})Resolving References
The repository is the abstraction for resolving references. Use get() with a typed reference:
// Get a single entity by reference
const authorRef = Author.ref`${someId}`
const author = await authorRepo.get(authorRef)
// Batch get multiple references (single query)
const refs = [Author.ref`${id1}`, Author.ref`${id2}`, Author.ref`${id3}`]
const authors = await authorRepo.getMany(refs)
// Resolve a reference from a loaded entity's field
const post = await postRepo.findOne(Post.fields.title.eq('My Post'))
const author = await post!.author // Reference is PromiseLike — just awaitWhy on the Repository?
- Repository is the contract for entity access
- Implementation is hidden (Postgres today, Redis tomorrow)
- You ask "give me this Author" and the repository figures out how
- IDs stay as an implementation detail of the storage layer
Lazy Loading
Reference fields loaded from the database are automatically awaitable. Just await them to fetch the related entity:
// Load a post
const post = await postRepo.findOne(Post.fields.status.eq('published'))
// The author field is a Reference — just await it
const author = await post!.author
console.log(author.name)
// Or chain it
const email = (await post!.author).emailEager Loading
For lists, lazy loading causes N+1 queries. Use the load option to batch-fetch references:
// Without eager loading — N+1 queries!
const posts = await postRepo.find({
where: Post.fields.status.eq('published'),
})
for (const post of posts) {
const author = await post.author // One query per post
}
// With eager loading — 2 queries total
const posts = await postRepo.find({
where: Post.fields.status.eq('published'),
load: ['author'], // Batch fetch all authors
})
for (const post of posts) {
const author = await post.author // Already loaded, no query
}Filtering on References
Use .has() to filter on related entity fields, and combine with load for eager loading:
// Find posts by premium authors AND eager load those authors
const posts = await postRepo.find({
where: Post.fields.author.has(
Author.fields.tier.eq('premium')
),
load: ['author'], // Authors already filtered, now also loaded
})
// All loaded — no additional queries
for (const post of posts) {
const author = await post.author
console.log(`Premium author: ${author.name}`)
}How It Works
Under the hood, eager loading:
- Main query fetches the primary entities
- Collects unique references from results
- Single batch query resolves all references
- Pre-populates Reference objects from the result