Skip to main content
The pnpm scaffold command auto-generates boilerplate code based on Entity definitions. It generates Model classes and test files in seconds, significantly reducing development time.

Basic Concept

Scaffold is an automation tool that converts Entities to code:
  • Entity-based: Understand types and structure from Entity definitions
  • Type-safe: TypeScript types are auto-generated
  • Consistent: All code follows the same pattern
  • Customizable: Freely customize after generation

Commands

model - Generate Model Class

Generate a Model class for an Entity.
pnpm scaffold model
An interactive prompt appears:
? Please select entity: (Use arrow keys)
❯ User
  Post
  Comment
Generated files:
📁src/models/
📄TSUser.model.ts - User Model class
Generated code:
src/models/User.model.ts
import { BaseModelClass } from "sonamu";
import type { InferSelectModel } from "sonamu";
import { UserEntity } from "../entities/User.entity";
import type { UserSubsetKey, UserSubsetMapping } from "../sonamu.generated";
import { userLoaderQueries, userSubsetQueries } from "../sonamu.generated.sso";

export type User = InferSelectModel<typeof UserEntity>;

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

  // TODO: Add Model methods
}

export const UserModel = new UserModelClass();

model_test - Generate Test File

Generate a test file for a Model.
pnpm scaffold model_test
Generated files:
📁src/models/
📄TSUser.model.test.ts - User Model tests
Generated code:
src/models/User.model.test.ts
import { beforeAll, describe, test } from "bun:test";
import { expect } from "@jest/globals";
import { FixtureManager } from "sonamu/test";
import { UserModel } from "./User.model";

describe("User Model", () => {
  beforeAll(async () => {
    await FixtureManager.sync();
  });

  test("findById", async () => {
    const user = await UserModel.findById(1);
    expect(user).toBeDefined();
    expect(user?.id).toBe(1);
  });

  test("findMany", async () => {
    const users = await UserModel.findMany({
      num: 10,
      page: 1,
    });
    expect(users.rows.length).toBeGreaterThan(0);
  });

  // TODO: Add more tests
});

View Scaffolding (In Development)

View component auto-generation is currently in development. You can generate React components through Sonamu UI.
# Run Sonamu UI
pnpm sonamu ui

# Generate View in Scaffolding tab

Generated Code Structure

Model Class

Generated Model classes have the following structure:
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // Entity type
  entity = UserEntity;

  // Add methods here
  async findByEmail(email: string): Promise<User | null> {
    return this.getPuri("r")
      .where("email", email)
      .first();
  }
}
Auto-generated features:
  • ✅ TypeScript types
  • ✅ Entity connection
  • ✅ BaseModel inheritance
  • ✅ Basic CRUD methods
You need to add:
  • Business logic methods
  • Complex queries
  • Data validation
  • Relation loading

Test File

Generated test files include basic tests:
describe("User Model", () => {
  beforeAll(async () => {
    await FixtureManager.sync();  // Load fixtures
  });

  test("findById", async () => {
    // Basic CRUD test
  });

  test("findMany", async () => {
    // List query test
  });
});
Extensible:
  • Add business logic tests
  • Edge case tests
  • Performance tests
  • Integration tests

Development Workflow

1. Define Entity

src/entities/Product.entity.ts
export const ProductEntity = {
  properties: [
    { name: "id", type: "int", primaryKey: true, autoIncrement: true },
    { name: "title", type: "string", length: 255 },
    { name: "price", type: "decimal", precision: 10, scale: 2 },
    { name: "category_id", type: "int" },
    { name: "created_at", type: "datetime" },
  ],
  belongsTo: [
    { entityId: "Category", as: "category" },
  ],
} satisfies EntityType;

2. Migration

# Create and apply migration
pnpm migrate run

3. Generate Model

# Generate Model class
pnpm scaffold model

# Select Entity: Product

4. Add Business Logic

src/models/Product.model.ts
class ProductModelClass extends BaseModelClass<
  ProductSubsetKey,
  ProductSubsetMapping,
  typeof productSubsetQueries,
  typeof productLoaderQueries
> {
  constructor() {
    super("Product", productSubsetQueries, productLoaderQueries);
  }

  // Find products by category
  async findByCategory(categoryId: number) {
    return this.getPuri("r")
      .where("category_id", categoryId)
      .where("deleted_at", null);
  }

  // Find products by price range
  async findByPriceRange(min: number, max: number) {
    return this.getPuri("r")
      .whereBetween("price", [min, max]);
  }
}

5. Generate Tests

# Generate test file
pnpm scaffold model_test

# Select Entity: Product

6. Write Tests

src/models/Product.model.test.ts
describe("Product Model", () => {
  beforeAll(async () => {
    await FixtureManager.sync();
  });

  test("findByCategory", async () => {
    const products = await ProductModel.findByCategory(1);
    expect(products.length).toBeGreaterThan(0);
    expect(products[0].category_id).toBe(1);
  });

  test("findByPriceRange", async () => {
    const products = await ProductModel.findByPriceRange(10000, 50000);
    products.forEach(product => {
      expect(product.price).toBeGreaterThanOrEqual(10000);
      expect(product.price).toBeLessThanOrEqual(50000);
    });
  });
});

7. Add API

src/models/Product.model.ts
class ProductModelClass extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async findByCategory(categoryId: number) {
    const { qb } = this.getSubsetQueries("A");
    qb.where("category_id", categoryId);

    return this.executeSubsetQuery({
      subset: "A",
      qb,
      params: { num: 20, page: 1 },
    });
  }
}

Customization

Adding Model Methods

Add business logic to generated Models:
class UserModelClass extends BaseModelClass<
  UserSubsetKey,
  UserSubsetMapping,
  typeof userSubsetQueries,
  typeof userLoaderQueries
> {
  constructor() {
    super("User", userSubsetQueries, userLoaderQueries);
  }

  // Find user by email
  async findByEmail(email: string): Promise<User | null> {
    return this.getPuri("r")
      .where("email", email)
      .first();
  }

  // Find only active users
  async findActiveUsers() {
    return this.getPuri("r")
      .where("is_active", true)
      .where("deleted_at", null);
  }

  // User statistics
  async getUserStats(userId: number) {
    const [stats] = await this.getPuri("r")
      .select({
        postCount: this.raw("COUNT(posts.id)"),
        commentCount: this.raw("COUNT(comments.id)"),
      })
      .leftJoin("posts", "posts.user_id", "users.id")
      .leftJoin("comments", "comments.user_id", "users.id")
      .where("users.id", userId)
      .groupBy("users.id");

    return stats;
  }
}

Extending Tests

Add more test cases:
describe("User Model - Extended", () => {
  test("findByEmail - existing email", async () => {
    const user = await UserModel.findByEmail("user1@example.com");
    expect(user).toBeDefined();
  });

  test("findByEmail - non-existing email", async () => {
    const user = await UserModel.findByEmail("notexist@example.com");
    expect(user).toBeNull();
  });

  test("findActiveUsers", async () => {
    const users = await UserModel.findActiveUsers();
    users.forEach(user => {
      expect(user.is_active).toBe(true);
      expect(user.deleted_at).toBeNull();
    });
  });

  test("getUserStats", async () => {
    const stats = await UserModel.getUserStats(1);
    expect(stats.postCount).toBeGreaterThanOrEqual(0);
    expect(stats.commentCount).toBeGreaterThanOrEqual(0);
  });
});

Batch Generation for Multiple Entities

Using Scripts

# Generate Model for all Entities
for entity in User Post Comment Category; do
  echo $entity | pnpm scaffold model
done

Batch Test Generation

# Generate tests for all Models
for entity in User Post Comment Category; do
  echo $entity | pnpm scaffold model_test
done

Practical Tips

1. Complete Entity First

// ✅ Scaffold after Entity is fully defined
export const UserEntity = {
  properties: [
    // Define all fields
  ],
  indexes: [
    // Define indexes
  ],
  belongsTo: [
    // Define relations
  ],
} satisfies EntityType;

2. Incremental Extension

// 1. Generate basic Model
pnpm scaffold model

// 2. Add simple methods first
async findById(id: number) { ... }

// 3. Add complex logic
async complexQuery() { ... }

// 4. Write tests
pnpm scaffold model_test

3. Follow Conventions

// ✅ Consistent naming
async findByEmail(email: string) { }     // find + By + Field
async findActiveUsers() { }               // find + Adjective + Entities
async getUserStats(userId: number) { }   // get + Noun + Type

// ✅ Consistent return types
Promise<User | null>      // Single record (nullable)
Promise<User[]>           // Multiple records
Promise<ListResult<User>> // Pagination

4. Git Management

# Commit generated files
git add src/models/User.model.ts
git add src/models/User.model.test.ts
git commit -m "Add User model and tests"

Troubleshooting

Regenerating Model

Problem: Want to regenerate Model Solution:
# Backup existing file
cp src/models/User.model.ts src/models/User.model.ts.bak

# Regenerate
pnpm scaffold model

# Restore necessary code

Syncing After Entity Change

Problem: Update Model after Entity change Solution:
# 1. Apply migration
pnpm migrate run

# 2. Update Model manually
# (Types sync automatically)

Next Steps