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 is the abstraction for resolving references - how it fetches the entity is an implementation detail.

Creating References

Use the Model.ref tagged template literal to create references:

creating-refs.tsTypeScript
import { Author, Post } from './models';

// Create a reference from an entity
const authorRef = Author.ref`${author.id}`;

// Use in relationships
const post = await postRepo.insert({
  title: 'My Post',
  status: 'draft',
  author: authorRef,  // Reference, not raw ID
});

Resolving References

The repository is the abstraction for resolving references. This is the DDD-friendly way - you work with references, not IDs:

resolving-refs.tsTypeScript
// Resolve a single reference
const authorRef = Author.ref`${someId}`;
const author = await authorRepo.resolve(authorRef);

// Batch resolve multiple references (single query)
const refs = [
  Author.ref`${id1}`,
  Author.ref`${id2}`,
  Author.ref`${id3}`,
];
const authors = await authorRepo.resolveMany(refs);

// Resolve a reference from a loaded entity
const article = await articleRepo.findById('...');
const author = await authorRepo.resolve(article.author);

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
  • Keeps IDs as an implementation detail of the storage layer

Lazy Loading

References loaded from the database are automatically awaitable. Just await them to fetch the related entity:

lazy-loading.tsTypeScript
// Load an article
const article = await articleRepo.findById('...');

// The author field is a Reference - just await it
const author = await article.author;
console.log(author.name);

// Or chain it
const email = (await article.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: 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: status.eq('published'),
  load: ['author'],  // Batch fetch all authors
});

for (const post of posts) {
  const author = await post.author;  // Already loaded, no query
}

Object Form

Use object form for selective loading:

TypeScript
await postRepo.find({
  where: status.eq('published'),
  load: { author: true },  // Only load author
});

Combining with has()

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 reference IDs from results
  • Single batch query: SELECT * FROM authors WHERE id = ANY($1)
  • Stores in local cache
  • Pre-populates Reference objects from cache