Repositories

Type-safe data access with the repository pattern

Repositories provide a clean abstraction for data access in JustScale. They implement the repository pattern, separating your domain logic from storage details. This allows you to write business logic that works with any storage backend - PostgreSQL for production, in-memory for testing.

The Modern Approach

JustScale uses a layered approach: define your domain model once with defineModel, then wrap it with storage-specific configuration using createPgModel and createPgRepository:

user.model.tsTypeScript
import { defineModel, field } from '@justscale/models'
import { createPgModel, createPgRepository } from '@justscale/postgres'

// 1. Define domain model (pure, no storage details)
export const User = defineModel({
  email: field.string().max(255).unique(),
  name: field.string().max(100),
  status: field.enum('UserStatus', ['active', 'inactive', 'banned'] as const)
    .default('active'),
  createdAt: field.createdAt(),
  updatedAt: field.updatedAt(),
})

// 2. Create PostgreSQL storage model
export const PgUser = createPgModel(User, {
  table: 'users',  // Optional: defaults to snake_case pluralized
})

// 3. Create repository service for DI
export const UserRepository = createPgRepository(PgUser)

Why This Pattern?

Clean Separation

Your domain model (User) is pure TypeScript with no database dependencies. Storage configuration (PgUser) lives separately. Services only depend on the abstract repository interface.

Type-Safe Queries

Field expressions like User.fields.email.eq(value) are fully typed. TypeScript catches typos in field names and invalid comparison values at compile time:

type-safe-queries.tsTypeScript
// TypeScript catches these errors:
User.fields.email.eq(123)           // Error: expected string
User.fields.status.eq('unknown')    // Error: not a valid status
User.fields.emial.eq('test')        // Error: 'emial' doesn't exist

// Correct usage - fully typed:
User.fields.email.eq('test@example.com')  // OK
User.fields.status.eq('active')           // OK

Easy Testing

Swap PostgreSQL for in-memory storage in tests without changing your services:

user.service.test.tsTypeScript
import { createInMemoryRepository } from '@justscale/models'
import { User, UserRepository } from './user.model'
import { UserService } from './user.service'
import { TestContainer } from '@justscale/testing'

test('find by email', async () => {
  const container = new TestContainer()

  // Use in-memory repository for fast, isolated tests
  container.bind(UserRepository, createInMemoryRepository(User))

  const service = container.resolve(UserService)
  await service.create({ email: 'test@example.com', name: 'Test' })

  const user = await service.findByEmail('test@example.com')
  expect(user?.name).toBe('Test')
})

Repository Methods

All repositories implement the same interface, whether backed by PostgreSQL, in-memory storage, or custom implementations:

repository-api.tsTypeScript
// Query methods
await users.find({ where, orderBy, limit, offset })  // Find multiple
await users.findById(id)                              // Find by primary key
await users.findOne(where)                            // Find first match
await users.count(where?)                             // Count matching
await users.exists(where)                             // Check existence

// Mutation methods
await users.insert(data)                              // Insert one
await users.insertMany([data1, data2])                // Bulk insert
await users.update(id, data)                          // Update by ID
await users.save(entity)                              // Smart insert/update
await users.delete(id)                                // Delete by ID
await users.deleteWhere(where)                        // Delete matching

// Streaming (for large datasets)
for await (const user of users.stream({ where })) {
  // Process one at a time
}

for await (const batch of users.streamBatches({ batchSize: 100 })) {
  // Process in batches
}

Wiring It Together

Register your repository and services with the cluster builder. The PostgreSQL client connection is shared across all repositories:

main.tsTypeScript
import { createClusterBuilder } from '@justscale/core'
import { createPostgresClient } from '@justscale/postgres'
import { UserRepository } from './user.model'
import { UserService } from './user.service'
import { UserController } from './user.controller'

// Create PostgreSQL client
const pgClient = createPostgresClient({
  connectionString: process.env.DATABASE_URL,
})

// Build and run
const cluster = createClusterBuilder()
  .add(pgClient)
  .add(UserRepository)
  .add(UserService)
  .add(UserController)
  .build()

await cluster.compile()
await cluster.start()

Available Adapters

PostgreSQL

Production-ready adapter with full query support, transactions, migrations, and advanced features like advisory locks and LISTEN/NOTIFY.

Learn about PostgreSQL adapter

In-Memory

Fast, zero-dependency storage for testing and prototyping. Implements the full repository interface with JavaScript Maps.

Learn about In-Memory Repository