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:
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:
// 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') // OKEasy Testing
Swap PostgreSQL for in-memory storage in tests without changing your services:
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:
// 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:
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.