Skip to main content
Models can be auto-generated through Scaffold after Entity definition or written manually. Using Scaffold generates a Model template with basic structure and CRUD methods included.

Model Creation Process

1

Define Entity

First, you need to define an Entity.
user.entity.json
{
  "id": "User",
  "table": "users",
  "props": [
    { "name": "id", "type": "integer" },
    { "name": "email", "type": "string", "length": 255 },
    { "name": "username", "type": "string", "length": 255 }
  ],
  "subsets": {
    "A": ["id", "email", "username"],
    "SS": ["id", "email"]
  }
}
2

Auto Type Generation

Type files are automatically generated when you save the Entity.Generated files:
  • user.types.ts - TypeScript types and Zod schemas
  • sonamu.generated.ts - Base schemas and Enums
  • sonamu.generated.sso.ts - Subset query functions
3

Generate Model Scaffold

Use Sonamu CLI to generate a Model template.
pnpm sonamu generate model --entity User
Or generate from the Scaffolding tab in Sonamu UI.
4

Customize Model

Add business logic to the generated Model.
// Add methods to generated template
@api({ httpMethod: "POST" })
async login(params: LoginParams) {
  // Implement login logic
}

Creating Models with Scaffold

Using CLI

# Specify Entity name
pnpm sonamu generate model --entity User

# Generate multiple Entities at once
pnpm sonamu generate model --entity User,Post,Comment

Using Sonamu UI

1

Open Scaffolding Tab

Click the Scaffolding tab in Sonamu UI (http://localhost:1028/sonamu-ui).
2

Select Model Template

Select the β€œModel” template and specify the Entity.
3

Configure Options

  • Default Search Field: Default search field (e.g., email)
  • Default Order By: Default sorting (e.g., id-desc)
  • Overwrite: Whether to overwrite existing files
4

Execute Generation

Click the β€œGenerate” button to create the Model file.

Generated Model Structure

Basic structure of a Model generated by Scaffold:
user.model.ts
import {
  api,
  asArray,
  BadRequestException,
  BaseModelClass,
  exhaustive,
  type ListResult,
  NotFoundException,
} from "sonamu";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";
import type { UserListParams, UserSaveParams } from "./user.types";

/*
  User Model
*/

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "User" })
  async findById<T extends UserSubsetKey>(
    subset: T,
    id: number
  ): Promise<UserSubsetMapping[T]> {
    const { rows } = await this.findMany(subset, {
      id,
      num: 1,
      page: 1,
    });
    if (!rows[0]) {
      throw new NotFoundException(`User ID ${id} does not exist`);
    }
    return rows[0];
  }

  async findOne<T extends UserSubsetKey>(
    subset: T,
    listParams: UserListParams
  ): Promise<UserSubsetMapping[T] | null> {
    const { rows } = await this.findMany(subset, {
      ...listParams,
      num: 1,
      page: 1,
    });

    return rows[0] ?? null;
  }

  @api({
    httpMethod: "GET",
    clients: ["axios", "tanstack-query"],
    resourceName: "Users",
  })
  async findMany<T extends UserSubsetKey, LP extends UserListParams>(
    subset: T,
    rawParams?: LP
  ): Promise<ListResult<LP, UserSubsetMapping[T]>> {
    // params with defaults
    const params = {
      num: 24,
      page: 1,
      search: "id" as const,
      orderBy: "id-desc" as const,
      ...rawParams,
    };

    const { qb, onSubset } = this.getSubsetQueries(subset);

    // id
    if (params.id) {
      qb.whereIn("users.id", asArray(params.id));
    }

    // search-keyword
    if (params.search && params.keyword && params.keyword.length > 0) {
      if (params.search === "id") {
        qb.where("users.id", Number(params.keyword));
      } else {
        exhaustive(params.search);
      }
    }

    // orderBy
    if (params.orderBy) {
      if (params.orderBy === "id-desc") {
        qb.orderBy("users.id", "desc");
      } else {
        exhaustive(params.orderBy);
      }
    }

    const enhancers = this.createEnhancers({
      A: (row) => row,
      SS: (row) => row,
    });

    return this.executeSubsetQuery({
      subset,
      qb,
      params,
      enhancers,
    });
  }

  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
  async save(spa: UserSaveParams[]): Promise<number[]> {
    const wdb = this.getPuri("w");

    // register
    spa.forEach((sp) => {
      wdb.ubRegister("users", sp);
    });

    // transaction
    return wdb.transaction(async (trx) => {
      const ids = await trx.ubUpsert("users");
      return ids;
    });
  }

  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"], guards: ["admin"] })
  async del(ids: number[]): Promise<number> {
    const wdb = this.getPuri("w");

    await wdb.transaction(async (trx) => {
      return trx.table("users").whereIn("users.id", ids).delete();
    });

    return ids.length;
  }
}

export const UserModel = new UserModelClass();

Generated Methods Explanation

Default methods generated by Scaffold:

1. findById

Retrieves a single record by ID.
async findById<T extends UserSubsetKey>(
  subset: T,
  id: number
): Promise<UserSubsetMapping[T]>
Features:
  • Receives Subset as parameter to query only needed fields
  • Throws NotFoundException if record doesn’t exist
  • Creates HTTP endpoint with @api decorator

2. findOne

Retrieves the first record matching conditions.
async findOne<T extends UserSubsetKey>(
  subset: T,
  listParams: UserListParams
): Promise<UserSubsetMapping[T] | null>
Features:
  • Returns null if record doesn’t exist (no exception thrown)
  • Internally calls findMany with num: 1, page: 1

3. findMany

Retrieves multiple records based on conditions.
async findMany<T extends UserSubsetKey, LP extends UserListParams>(
  subset: T,
  rawParams?: LP
): Promise<ListResult<LP, UserSubsetMapping[T]>>
Features:
  • Supports pagination (num, page)
  • Search/filtering (search, keyword)
  • Sorting (orderBy)
  • Returns { rows, total } with ListResult type

4. save

Creates or updates records.
async save(spa: UserSaveParams[]): Promise<number[]>
Features:
  • Uses Upsert Builder (Insert or Update)
  • Processes multiple records at once with array
  • Executes wrapped in transaction
  • Returns array of created/updated IDs

5. del

Deletes records.
async del(ids: number[]): Promise<number>
Features:
  • Receives multiple IDs as array
  • Executes in transaction
  • Only admins can delete with guards: ["admin"]
  • Returns number of deleted records

Creating Models Manually

You can also write manually without using Scaffold.

Minimum Structure

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

class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // Add methods...
}

export const UserModel = new UserModelClass();

Naming Conventions

CategoryPatternExample
Class name{Entity}ModelClassUserModelClass
Export name{Entity}ModelUserModel
File name{entity}.model.tsuser.model.ts
Class name must end with ModelClass. Export must end with Model.
// βœ… Correct naming
class UserModelClass extends BaseModelClass { }
export const UserModel = new UserModelClass();

// ❌ Wrong naming
class UserModel extends BaseModelClass { }  // X
export const User = new UserModelClass();   // X

Model File Location

Model files are located in the same directory as the Entity:

Writing Types File

Define parameter types to use with Model:
user.types.ts
import { z } from "zod";
import { UserOrderBy, UserSearchField } from "../sonamu.generated";

// List parameters
export const UserListParams = z.object({
  num: z.number().optional(),
  page: z.number().optional(),
  search: UserSearchField.optional(),
  keyword: z.string().optional(),
  orderBy: UserOrderBy.optional(),
  id: z.union([z.number(), z.array(z.number())]).optional(),
});
export type UserListParams = z.infer<typeof UserListParams>;

// Save parameters
export const UserSaveParams = z.object({
  id: z.number().optional(),
  email: z.string().email(),
  username: z.string().min(2).max(50),
  password: z.string().min(8),
});
export type UserSaveParams = z.infer<typeof UserSaveParams>;

// Login parameters
export const UserLoginParams = z.object({
  email: z.string().email(),
  password: z.string(),
});
export type UserLoginParams = z.infer<typeof UserLoginParams>;
Types file patterns:
  • {Entity}ListParams: For list queries
  • {Entity}SaveParams: For create/update
  • {Entity}{Action}Params: For specific actions (e.g., LoginParams)

Verifying Model Registration

Verify that the generated Model loads properly:
api/src/application/sonamu.generated.ts
// Model imports (auto-generated)
export * from "./user/user.model";

// All Models are exported here
sonamu.generated.ts is automatically updated when you save Entities. When you add a Model, it’s automatically added to this file too.

Next Steps

After creating a Model, learn the following topics: