Models
Define your domain models with type-safe field builders
Models in JustScale define your domain entities using a declarative field builder API. They provide compile-time type safety, automatic TypeScript type inference, and separation between your domain schema and storage implementation.
Creating a Model
Use defineModel to create a model class. The model extends the returned class, and TypeScript automatically infers the types from your field definitions:
import { defineModel, field } from '@justscale/models';
// Define a User model with field builders
class User extends defineModel({
email: field.string().max(255).unique(),
name: field.string().max(100),
balance: field.decimal(10, 2).default('0.00'),
isActive: field.boolean().default(true),
}) {}
// TypeScript infers the type automatically:
// {
// email: string
// name: string
// balance: string // Decimals are strings for precision
// isActive: boolean
// }
export { User };Creating Instances
Models can be instantiated directly with new. The constructor accepts partial data for all fields. This creates a transient instance (not yet persisted):
import { User } from './user.model';
// Create a new transient user (no id yet)
const user = new User({
email: 'alice@example.com',
name: 'Alice',
// balance defaults to '0.00'
// isActive defaults to true
});
console.log(user.email); // 'alice@example.com'
console.log(user.balance); // '0.00'Field Builders
Field builders provide a fluent API for defining field types and constraints. Each field type has specific modifiers and options:
import { defineModel, field } from '@justscale/models';
class Product extends defineModel({
// String fields
sku: field.string().max(50).unique().index(),
name: field.string().max(200),
description: field.text(), // For long content
// Number fields
quantity: field.int().default(0),
price: field.decimal(10, 2), // Precision for money
weight: field.float().optional(),
// Boolean
inStock: field.boolean().default(true),
// Date/Time
manufacturedAt: field.timestamp().optional(),
// JSON
metadata: field.jsonb<{ tags: string[] }>().optional(),
}) {}
export { Product };See Field Builders Reference for a complete list of available field types and modifiers.
System Fields
When a model instance is persisted via a repository, the repository automatically adds system fields for tracking lifecycle and versioning:
id- Unique identifier (string, UUID by default)createdAt- Timestamp when the record was createdupdatedAt- Timestamp when the record was last updatedversion- Optimistic concurrency version number
Use the Persistent<T> type to represent a persisted model instance with these system fields:
import { User } from './user.model';
import type { Persistent } from '@justscale/models';
import { createService } from '@justscale/core';
import { UserRepository } from './user.repository';
export const UserService = createService({
inject: { users: UserRepository },
factory: ({ users }) => ({
create: async (email: string, name: string): Promise<Persistent<typeof User>> => {
const user = new User({ email, name });
const saved = await users.save(user);
// saved now has: id, createdAt, updatedAt, version
console.log(saved.id); // 'uuid-string'
console.log(saved.createdAt); // Date
console.log(saved.version); // 1
return saved;
},
}),
});Type Inference
JustScale models use TypeScript's advanced type inference to extract types directly from your field definitions. You rarely need to write explicit types:
import { defineModel, field, type ModelData } from '@justscale/models';
class User extends defineModel({
email: field.string(),
age: field.int().optional(),
balance: field.decimal(10, 2),
active: field.boolean().default(true),
}) {}
// Extract clean type reference
type UserData = ModelData<typeof User>;
// {
// email: string
// age: number | undefined
// balance: string
// active: boolean
// }
// TypeScript infers parameter types
function greetUser(user: UserData) {
console.log(`Hello, ${user.email}`);
}
const user = new User({ email: 'test@example.com', balance: '100.00' });
greetUser(user); // Type-safe!Special Field Types
Timestamps
Use semantic timestamp fields for common patterns like created/updated tracking:
import { defineModel, field } from '@justscale/models';
class Post extends defineModel({
title: field.string(),
content: field.text(),
publishedAt: field.timestamp().optional(),
createdAt: field.createdAt(), // Auto-set on insert
updatedAt: field.updatedAt(), // Auto-updated on save
deletedAt: field.deletedAt(), // Soft delete support (optional by default)
}) {}
export { Post };Enums
Define PostgreSQL enums with type-safe values:
import { defineModel, field } from '@justscale/models';
const ORDER_STATUSES = ['pending', 'processing', 'completed', 'cancelled'] as const;
class Order extends defineModel({
orderNumber: field.string().max(50).unique(),
status: field.enum('order_status', ORDER_STATUSES).default('pending'),
total: field.decimal(10, 2),
}) {}
// TypeScript knows status must be one of the enum values
const order = new Order({
orderNumber: 'ORD-001',
status: 'pending', // ✓ Type-safe
// status: 'invalid', // ✗ TypeScript error
total: '99.99',
});
export { Order };Arrays
Store arrays of primitive types using PostgreSQL array columns:
import { defineModel, field } from '@justscale/models';
class Article extends defineModel({
title: field.string(),
tags: field.array(field.string()),
ratings: field.array(field.int()).optional(),
}) {}
const article = new Article({
title: 'TypeScript Tips',
tags: ['typescript', 'programming', 'web'],
ratings: [4, 5, 5, 3],
});
export { Article };Best Practices
- Use semantic field types - Prefer
field.createdAt()overfield.timestamp()for timestamps - Set appropriate constraints - Use
.max(),.unique(),.index()to express domain rules - Use decimal for money - Never use
floatordoublefor currency; usefield.decimal(10, 2) - Provide defaults - Set sensible defaults with
.default()to reduce required constructor parameters - Let TypeScript infer - Avoid manual type annotations; let the field builders do the work
- Export the class, not the instance - Export
Userclass, notnew User()