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:

models.tsTypeScript
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:

creating-refs.tsTypeScript
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:

resolving-refs.tsTypeScript
// 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 await

Why 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:

lazy-loading.tsTypeScript
// 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).email

Eager Loading

For lists, lazy loading causes N+1 queries. Use the load option to batch-fetch references:

eager-loading.tsTypeScript
// 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:

has-with-load.tsTypeScript
// 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