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:

user.model.tsTypeScript
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):

create-user.tsTypeScript
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:

product.model.tsTypeScript
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 created
  • updatedAt - Timestamp when the record was last updated
  • version - Optimistic concurrency version number

Use the Persistent<T> type to represent a persisted model instance with these system fields:

user-service.tsTypeScript
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:

type-inference.tsTypeScript
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:

post.model.tsTypeScript
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:

order.model.tsTypeScript
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:

tag.model.tsTypeScript
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() over field.timestamp() for timestamps
  • Set appropriate constraints - Use .max(), .unique(), .index() to express domain rules
  • Use decimal for money - Never use float or double for currency; use field.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 User class, not new User()