Skip to main content

Entity Basics

Entity ID is enforced as CamelCase, and various names are auto-generated based on it.Input: ProductAuto-generated names:
  1. Entity ID (id)
    • Product (same as input)
    • Validation: /^[A-Z][a-zA-Z0-9]*$/ (CamelCase required)
  2. Table name (table)
    • products (auto-pluralized)
    • Customizable (direct input in Sonamu UI)
  3. File/directory name (names.fs)
    • product (lowercase + dash)
    • Used for: directory names, file names
  4. Module name (names.module)
    • Product (same as input)
    • Used for: class names, type names
Generated files:
api/src/application/product/product.entity.json
api/src/application/product/product.types.ts
api/src/application/product/product.model.ts (via Scaffolding)
Generated type/class names:
ProductBaseSchema
ProductBaseListParams
ProductSubsetMapping
ProductModel (Model class)
ProductService (frontend)
Conversion rules:
  • ID → table: inflection.underscore(inflection.pluralize(id))
    • Productproducts
    • OrderItemorder_items
  • ID → fs: inflection.dasherize(inflection.underscore(id)).toLowerCase()
    • Productproduct
    • OrderItemorder-item
Note:
  • Entity ID must start with uppercase (CamelCase)
  • Table name is auto-generated but customizable
When Entity is modified, migrations are not auto-generated. You must manually Generate in the Sonamu UI’s DB Migration tab.Migration generation targets (shown in Prepared Migration Codes):Props changes:
  • Prop add/delete
  • Prop type change (stringnumeric, etc.)
  • StringProp length change (length)
  • nullable change
  • dbDefault change
  • precision, scale change when NumberProp has numberType: "numeric"
Relation changes:
  • BelongsToOne add/delete (FK column create/delete)
  • OneToOne add/delete (when hasJoinColumn: true)
  • ManyToMany add/delete (join table create/delete)
  • onUpdate, onDelete changes
Index changes:
  • Index add/delete
  • unique, index, fulltext type changes
  • Index column changes
Table changes:
  • Table name change
Changes that don’t generate migrations:
  • Subset add/modify/delete
  • Enum labels change
  • Virtual prop add/modify/delete
  • HasMany relation add/modify (FK is on the other side)
  • Title, Description changes
Note:
  • Migration files are not auto-generated after Entity modification
  • Check DB Migration tab → Prepared Migration Codes
  • Manual generation required by clicking Generate button
1. Entity JSON file created
  • {entity}.entity.json
  • Location: api/src/application/{entity}/{entity}.entity.json
2. Type file auto-generated
  • {entity}.types.ts
  • Location: api/src/application/{entity}/{entity}.types.ts
3. Generated files updated
  • sonamu.generated.sso.ts
  • sonamu.generated.ts
  • Location: api/src/application/
4. Frontend files copied
  • user.types.tsweb/src/services/user/user.types.ts
  • sonamu.generated.tsweb/src/services/sonamu.generated.ts
5. sonamu.lock updated
  • Stores checksums of newly created files
Note:
  • Migration files are not auto-generated
  • Separate Generate click required in DB Migration tab
  • model, model_test, view are generated separately in Scaffolding tab

Entity Properties

Define in the enums object with enum ID as key and value-label pairs as object:
{
  "enums": {
    "UserRole": {
      "normal": "Normal",
      "admin": "Admin"
    },
    "UserOrderBy": {
      "id-desc": "ID Newest"
    }
  }
}
  • Key (normal, admin): Actual value stored in DB
  • Value (Normal, Admin): Label displayed in UI
  • Enum ID (UserRole): Used as TypeScript type name
Best Practices:
  • DB values in English (for internationalization and migration ease)
  • UI labels in user’s language
A calculated field that is included in query results but has no DB column.How to create (Sonamu UI):
  1. Entity detail → Click Add a prop
  2. Type: Select virtual
  3. Name: Enter field name (e.g., company_stats)
  4. Check Nullable: true
  5. CustomType ID: Select type (e.g., CompanyStatsType)
Type definition ({entity}.types.ts):
export const CompanyStatsType = z.object({
  totalDepartments: z.number(),
  totalEmployees: z.number(),
  avgSalary: z.number(),
  departments: z.array(z.object({
    id: z.number(),
    name: z.string(),
    employeeCount: z.number(),
  })),
});

export type CompanyStatsType = z.infer<typeof CompanyStatsType>;
Implementation (Enhancer approach):
// company.model.ts
const enhancers = this.createEnhancers({
  WithStats: async (row) => {
    const departments = await DepartmentModel.findMany('P', {
      company_id: row.id,
    });

    return {
      ...row,
      company_stats: {
        totalDepartments: departments.rows.length,
        totalEmployees: departments.rows.reduce((sum, d) => sum + d.employee_count, 0),
        avgSalary: calculateAvgSalary(departments.rows),
        departments: departments.rows.map(d => ({
          id: d.id,
          name: d.name,
          employeeCount: d.employee_count,
        })),
      },
    };
  },
});
Main use cases:
  • Relation data aggregation (employee count, comment count)
  • Complex statistics objects (department stats, company dashboard)
  • User-specific status (like status, permission info)
  • Nested JSON structures (metadata, config objects)
Note:
  • .omit({ virtual_field: true }) required in SaveParams
  • Implement calculation logic in Enhancer
  • Consider performance for complex calculations

Entity Relations

Define with relation type fields:OneToOne: 1:1 relation
  • Specify FK owner with hasJoinColumn
BelongsToOne: N:1 relation (Many-to-One)
  • Defined in Entity that has the FK
HasMany: 1:N relation (One-to-Many)
  • virtual relation (FK is on the other side)
ManyToMany: N:M relation
  • Requires joinTable
Each relation can set referential integrity with onUpdate, onDelete options in the Entity that has the FK.
Represents an N:1 (Many-to-One) relation.How to configure (Sonamu UI):
  1. Entity detail → Click Add a prop
  2. Type: Select relation
  3. Relation Type: Select BelongsToOne
  4. Name: Relation field name (e.g., department)
  5. With: Entity ID to connect (e.g., Department)
  6. Nullable: true for optional relations
  7. ON UPDATE/ON DELETE: Select CASCADE, SET NULL, RESTRICT, NO ACTION, etc.
Result:
  • Foreign key column auto-created (e.g., department_id)
  • Type includes department?: Department
{
  "type": "relation",
  "name": "department",
  "with": "Department",
  "nullable": true,
  "relationType": "BelongsToOne",
  "onUpdate": "CASCADE",
  "onDelete": "SET NULL"
}

Model Classes

There are two ways to create independent APIs without an Entity.Method 1: Add @api decorator to existing Model
// api/src/application/user/user.model.ts
class UserModelClass extends BaseModelClass {
  // Existing CRUD methods...

  @api({ httpMethod: "GET" })
  async getMyIP(): Promise<{ ip: string }> {
    const context = Sonamu.getContext();
    return { ip: context.ip };
  }

  @api({ httpMethod: "POST" })
  async sendEmail(params: { to: string; subject: string }): Promise<boolean> {
    // External API call (no DB query)
    return true;
  }
}
Method 2: Use FrameFrame is a class for creating independent APIs without an Entity.
// api/src/application/util/util.frame.ts
import { BaseFrameClass, api } from "sonamu";

class UtilFrameClass extends BaseFrameClass {
  @api({ httpMethod: "GET" })
  async healthCheck(): Promise<{ status: string; timestamp: Date }> {
    return { status: "ok", timestamp: new Date() };
  }
}

export const UtilFrame = new UtilFrameClass();
Frame characteristics:
  • Extends BaseFrameClass
  • Provides getDB(), getUpsertBuilder() methods
  • No Entity, Subset, Model methods
  • Filename: {name}.frame.ts
Selection criteria:
  • Related to existing Entity: Add method to that Model
  • Completely independent: Create Frame class
When Model class grows, you can separate independent logic into Frame classes.Problem: user.model.ts becomes large
class UserModelClass extends BaseModelClass {
  async findById() { ... }
  async save() { ... }
  // Logic unrelated to Entity
  async sendEmail() { ... }
  async uploadAvatar() { ... }
  async generateReport() { ... }
}
Improvement: Separate into user-util.frame.ts
// user-util.frame.ts
class UserUtilFrameClass extends BaseFrameClass {
  @api({ httpMethod: "POST" })
  async sendEmail(params: EmailParams): Promise<boolean> {
    // Email sending logic
    return true;
  }

  @api({ httpMethod: "POST" })
  async uploadAvatar(file: File): Promise<string> {
    // File upload logic
    return "url";
  }
}
Separation criteria:
  • Directly related to User Entity: Keep in user.model.ts
  • Utility-type features: Separate to user-util.frame.ts
  • Completely independent: Create separate Frame class
A singleton class that centrally manages all Entities.Main features:
  • Entity lookup
  • Auto-generate names (table, fs, module)
  • Schema validation
  • Subset lookup
  • Subset field expansion
Used in:
  • Model classes
  • API decorators
  • Syncer
  • Migration generator
Sonamu UI:
  • Real-time error display when editing Entity
EntityManager:
const errors = EntityManager.schemaValidate(entity);
Syncer:
  • Auto-validates and outputs errors when running pnpm dev
Validation items:
  • Required fields exist
  • Type validity
  • Subset references
  • Enum validity