Skip to main content
Model is a core layer of Sonamu that handles business logic and data access. Based on Entity definitions, it provides database queries, API endpoints, and type-safe data processing.

Role of Model

Data Access

Database CRUD operationsType-safe queries through Puri query builder

Business Logic

Implementing domain rulesData validation, transformation, calculation

API Endpoints

Automatic HTTP API generationREST API provided via @api decorator

Type Safety

Compile-time type checkingAuto-generated types based on Entity

Position in Sonamu Architecture

Layer Structure:
  1. Client - Frontend (React, Vue, etc.)
  2. API Layer - HTTP endpoints (@api decorator)
  3. Model Layer - Business logic ⭐
  4. Database - PostgreSQL
Model is the business logic layer between API Layer and Database. All data access and business rules are handled in Model.

Model Class Structure

Basic Structure

user.model.ts
import { BaseModelClass, api } from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";

class UserModelClass extends BaseModelClass<
  UserSubsetKey,              // "A" | "P" | "SS"
  UserSubsetMapping,          // Type for each subset
  typeof userSubsetQueries,   // Subset query functions
  typeof userLoaderQueries    // Loader query functions
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET" })
  async findById(id: number): Promise<UserSubsetMapping["A"]> {
    // Business logic implementation
  }
}

export const UserModel = new UserModelClass();

Generic Type Parameters

ParameterDescriptionExample
TSubsetKeyUnion type of Subset keys"A" | "P" | "SS"
TSubsetMappingResult type mapping per Subset{ A: UserA, P: UserP }
TSubsetQueriesSubset query function typeAuto-generated
TLoaderQueriesLoader query typeAuto-generated
These generic types are automatically generated in sonamu.generated.ts and sonamu.generated.sso.ts, so you don’t need to write them manually.

Model vs Entity vs Types

Let’s clarify commonly confused concepts in Sonamu:
CategoryEntityTypesModel
File{entity}.entity.json{entity}.types.ts{entity}.model.ts
RoleData structure definitionType definitionBusiness logic
CreationManual (Sonamu UI)Auto-generatedManual (Scaffold)
ContentTables, columns, relationsTypeScript types, ZodMethods, API, queries
Change frequencyLowAuto-regeneratedHigh
Examples:
{
  "id": "User",
  "table": "users",
  "props": [
    { "name": "id", "type": "integer" },
    { "name": "email", "type": "string" }
  ]
}

Features Provided by Model

1. Type-Safe Data Access

// ✅ Type safe: type inference based on subset
const user = await UserModel.findById("A", 1);
// user type: UserSubsetMapping["A"]

// ✅ Compile error: invalid subset
const user = await UserModel.findById("INVALID", 1);
//                                      ^^^^^^^^^ 
// Error: Argument of type '"INVALID"' is not assignable

2. Subset-Based Queries

async findMany<T extends UserSubsetKey>(
  subset: T,
  params: UserListParams
): Promise<ListResult<UserSubsetMapping[T]>> {
  const { qb } = this.getSubsetQueries(subset);
  
  // Automatically SELECT only fields matching the subset
  qb.where("id", params.id);
  
  return this.executeSubsetQuery({ subset, qb, params });
}
Subsets reduce network traffic and improve performance by selecting only the fields needed for API responses.

3. Automatic API Generation

@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
async findById(id: number): Promise<User> {
  // Method implementation
}
What gets auto-generated:
  • HTTP endpoint: GET /user/findById?id=1
  • TypeScript client code
  • TanStack Query hooks
  • API documentation

4. Transaction Management

@transactional()
async updateUserAndProfile(
  userId: number,
  userData: UserData,
  profileData: ProfileData
): Promise<void> {
  // All operations execute in a single transaction
  await this.save([userData]);
  await ProfileModel.save([profileData]);
  // Auto commit or rollback
}

5. Data Validation and Transformation

async save(params: UserSaveParams[]): Promise<number[]> {
  // Business rule validation
  for (const param of params) {
    if (param.age && param.age < 18) {
      throw new BadRequestException("Users under 18 cannot register");
    }
  }
  
  // Data transformation
  const hashedParams = params.map(p => ({
    ...p,
    password: bcrypt.hashSync(p.password, 10)
  }));
  
  // Save
  return this.getPuri("w").ubUpsert("users", hashedParams);
}

Model Writing Patterns

CRUD Methods

A typical Model provides the following CRUD methods:
class UserModelClass extends BaseModelClass {
  // Create
  @api({ httpMethod: "POST" })
  async save(params: UserSaveParams[]): Promise<number[]> {
    // ...
  }
  
  // Read
  @api({ httpMethod: "GET" })
  async findById(id: number): Promise<User> {
    // ...
  }
  
  @api({ httpMethod: "GET" })
  async findMany(params: UserListParams): Promise<ListResult<User>> {
    // ...
  }
  
  // Update
  @api({ httpMethod: "PUT" })
  async update(id: number, params: UserUpdateParams): Promise<User> {
    // ...
  }
  
  // Delete
  @api({ httpMethod: "DELETE" })
  async del(ids: number[]): Promise<number> {
    // ...
  }
}

Domain-Specific Methods

You can add methods specialized for business domains:
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async login(params: LoginParams): Promise<{ user: User }> {
    // Login logic
  }
  
  @api({ httpMethod: "POST" })
  async logout(): Promise<{ message: string }> {
    // Logout logic
  }
  
  @api({ httpMethod: "GET" })
  async me(): Promise<User | null> {
    // Current user info
  }
  
  @api({ httpMethod: "POST" })
  async changePassword(params: ChangePasswordParams): Promise<void> {
    // Password change
  }
}

BaseModel Methods

BaseModelClass provides the following utility methods:
MethodDescriptionReturn Type
getDB(preset)Get database connectionKnex
getPuri(preset)Get Puri query builderPuriWrapper
getSubsetQueries(subset)Get Subset query builder{ qb, onSubset }
executeSubsetQuery(params)Execute Subset queryListResult
createEnhancers(enhancers)Enhancer creation helperEnhancerMap
Learn More

Advantages of Model

TypeScript types are auto-generated based on Entity definitions, allowing errors to be caught at compile time.
// ✅ Type check
const user: UserSubsetMapping["A"] = await UserModel.findById("A", 1);

// ❌ Compile error
user.nonExistentField;
Write common logic as Model methods and reuse across multiple APIs.
// Model method
async findActiveUsers(): Promise<User[]> {
  return this.puri().where("is_active", true).many();
}

// Reuse in multiple APIs
@api()
async api1() {
  return this.findActiveUsers();
}

@api()
async api2() {
  const users = await this.findActiveUsers();
  // Additional logic...
}
Models can be tested independently.
describe("UserModel", () => {
  it("should find user by id", async () => {
    const user = await UserModel.findById("A", 1);
    expect(user.id).toBe(1);
  });
});
Clearly separates business logic (Model) from HTTP handling (Controller/API).
  • Model: Data processing, business rules
  • API: HTTP request/response, authentication, authorization

Next Steps

After understanding the basic concepts of Model, learn the following topics: