Entity Definition
Entities are the core building blocks of your Apso schema. Each entity maps to a database table and generates a complete NestJS module with CRUD endpoints, a TypeORM entity class, DTOs with validation, and a service layer.
Basic Entity Structure
Entities are defined as objects in the top-level entities array. At minimum, every entity needs a name and at least one fields entry:
{
"version": 2,
"rootFolder": "src",
"entities": [
{
"name": "Project",
"created_at": true,
"updated_at": true,
"fields": [
{ "name": "name", "type": "text" },
{ "name": "description", "type": "text", "nullable": true }
]
}
],
"relationships": []
}This generates:
- A
projectdatabase table (entity name lowercased) - REST endpoints:
GET /project,POST /project,GET /project/:id,PATCH /project/:id,DELETE /project/:id - A TypeORM entity class
Projectwith decorators and validation - Create and Update DTO classes with class-validator rules
Entity Properties Reference
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | The entity name. Used as the class name and (lowercased) as the table name. |
fields | array | Yes | — | Array of field definitions (at least one required). |
created_at | boolean | No | false | Adds an auto-managed created_at timestamp column. |
updated_at | boolean | No | false | Adds an auto-managed updated_at timestamp column. |
primaryKeyType | "serial" or "uuid" | No | "serial" | Type of primary key. serial = auto-incrementing integer, uuid = UUID v4. |
uniques | array | No | [] | Composite unique constraints spanning multiple fields. |
scopeBy | string or string[] | No | — | Fields for multi-tenant data isolation. See Multi-Tenancy. |
scopeOptions | object | No | — | Fine-tune scoping behavior. See Multi-Tenancy. |
Auto-Generated Fields
Apso automatically generates several fields on every entity. You do not need to define them in your fields array.
Primary Key
Every entity receives an id field:
// With primaryKeyType: "serial" (default)
@PrimaryGeneratedColumn()
id: number;
// With primaryKeyType: "uuid"
@PrimaryGeneratedColumn("uuid")
id: string;The default is serial (auto-incrementing integer). Set "primaryKeyType": "uuid" on the entity if you need UUID primary keys:
{
"name": "ApiToken",
"primaryKeyType": "uuid",
"fields": [
{ "name": "token", "type": "text", "unique": true }
]
}Timestamps
If created_at and/or updated_at are set to true on the entity, Apso generates timestamp columns:
// "created_at": true
@CreateDateColumn()
created_at: Date;
// "updated_at": true
@UpdateDateColumn()
updated_at: Date;These are managed automatically by TypeORM — created_at is set on insert, and updated_at is set on every update. You do not need to set them in your application code.
{
"name": "Task",
"created_at": true,
"updated_at": true,
"fields": [
{ "name": "title", "type": "text" }
]
}Foreign Key Fields
For each relationship where the entity holds the foreign key (ManyToOne, or the owning side of OneToOne), Apso generates a foreign key column and the relationship property. You do not define these as fields — they come from the relationships array. See Relationships.
// Generated from: { "from": "Task", "to": "Project", "type": "ManyToOne" }
@ManyToOne(() => Project)
@JoinColumn({ name: 'projectId' })
project: Project;
@Column({ type: 'integer' })
projectId: number;Field Definition
Each field is an object in the entity’s fields array with a required name and type:
{
"name": "email",
"type": "text",
"length": 255,
"unique": true,
"is_email": true
}Field Properties
| Property | Type | Applies To | Description |
|---|---|---|---|
name | string | All | Field name. Used as the column name and TypeScript property. |
type | string | All | Data type. See Field Types for the complete list. |
nullable | boolean | All | Whether the field accepts null. Generates @IsOptional() when true. |
unique | boolean | All | Adds a unique constraint on the column. |
default | string, number, boolean | All | Default value for the column. |
length | integer | text | Maximum character length. Generates @MaxLength(n). |
is_email | boolean | text | Adds @IsEmail() validation. |
values | string[] | enum | Required for enum fields. The list of allowed values. |
precision | integer | decimal, numeric | Total number of digits (1 to 131072). |
scale | integer | decimal, numeric | Digits after the decimal point (0 to 16383). |
For the full field type reference with TypeORM mappings and auto-applied validation, see Field Types.
Validation Rules
Apso auto-generates class-validator decorators based on your field configuration:
| Field Configuration | Generated Validator | Effect |
|---|---|---|
"type": "text" | @IsString(), @IsNotEmpty() | String required by default |
"type": "integer" | @IsNumber() | Must be a valid number |
"type": "boolean" | @IsBoolean() | Must be true or false |
"type": "decimal" / "numeric" | @IsNumber() | Must be a valid number |
"nullable": true | @IsOptional() | Field can be omitted or null |
"is_email": true | @IsEmail() | Must be a valid email format |
"length": 255 | @MaxLength(255) | String cannot exceed length |
"unique": true | @Column({ unique: true }) | Database-level unique constraint |
Naming Conventions
Entity Names
- Use PascalCase:
WorkspaceUser,ApplicationService,InfrastructureStack - Use singular form:
Project, notProjects - Entity class names are used exactly as defined in
.apsorc - Table names are lowercased:
Projectbecomes theprojecttable
Field Names
- Use camelCase:
fullName,inviteCode,activeAt - Foreign key columns use the pattern
{relationName}Id:projectId,workspaceId,ownerId - Join column names match the foreign key column name
Enum Naming
When a field has "type": "enum", Apso generates a TypeScript enum with the naming pattern {PascalCaseEntityName}{PascalCaseFieldName}Enum:
{
"name": "WorkspaceUser",
"fields": [
{ "name": "role", "type": "enum", "values": ["User", "Admin"] }
]
}Generates:
// src/autogen/enums.ts
export enum WorkspaceUserRoleEnum {
User = "User",
Admin = "Admin",
}Composite Unique Constraints
For unique constraints spanning multiple fields, use the uniques property:
{
"name": "WorkspaceUser",
"fields": [
{ "name": "email", "type": "text" },
{ "name": "role", "type": "enum", "values": ["User", "Admin"] }
],
"uniques": [
{
"name": "UQ_workspace_user_email",
"fields": ["email", "workspaceId"]
}
]
}Each unique constraint requires:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The constraint name in the database. |
fields | string[] | Yes | The fields included in the constraint (at least one). |
This generates a @Unique decorator on the entity class ensuring no two rows share the same combination of values across the specified columns.
Complete Entity Example
Here is a realistic entity definition for a SaaS workspace member:
{
"name": "WorkspaceUser",
"created_at": true,
"updated_at": true,
"fields": [
{ "name": "email", "type": "text", "length": 255, "is_email": true },
{ "name": "invite_code", "type": "text", "length": 64 },
{ "name": "role", "type": "enum", "values": ["User", "Admin"], "default": "Admin" },
{ "name": "status", "type": "enum", "values": ["Active", "Invited", "Inactive", "Deleted"] },
{ "name": "activeAt", "type": "date", "nullable": true }
],
"uniques": [
{
"name": "UQ_workspace_user_workspace_email",
"fields": ["email", "workspaceId"]
}
]
}This generates a TypeORM entity roughly equivalent to:
@Entity('workspaceuser')
export class WorkspaceUser {
@PrimaryGeneratedColumn()
id: number;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
@Column({ type: 'text', length: 255 })
@IsString()
@IsNotEmpty()
@IsEmail()
@MaxLength(255)
email: string;
@Column({ type: 'text', length: 64 })
@IsString()
@IsNotEmpty()
@MaxLength(64)
invite_code: string;
@Column({ type: 'enum', enum: WorkspaceUserRoleEnum, default: 'Admin' })
role: WorkspaceUserRoleEnum;
@Column({ type: 'enum', enum: WorkspaceUserStatusEnum })
status: WorkspaceUserStatusEnum;
@Column({ type: 'date', nullable: true })
@IsOptional()
activeAt: Date;
// Foreign key fields generated from relationships
@ManyToOne(() => User)
@JoinColumn({ name: 'userId' })
user: User;
@Column({ type: 'integer' })
userId: number;
@ManyToOne(() => Workspace)
@JoinColumn({ name: 'workspaceId' })
workspace: Workspace;
@Column({ type: 'integer' })
workspaceId: number;
}Extending Generated Code
All generated code lives in src/autogen/ and is overwritten every time you run apso server scaffold. To add custom business logic:
- Create an extensions directory for the entity:
src/extensions/{EntityName}/ - Subclass the generated service to add custom methods
- Create a custom controller for additional endpoints
src/
autogen/
WorkspaceUser/
WorkspaceUser.entity.ts # DO NOT MODIFY
WorkspaceUser.service.ts # DO NOT MODIFY
WorkspaceUser.controller.ts # DO NOT MODIFY
WorkspaceUser.module.ts # DO NOT MODIFY
extensions/
WorkspaceUser/
WorkspaceUser.service.ts # Your custom logic here
WorkspaceUser.controller.ts # Your custom endpoints hereCustom service example:
// src/extensions/WorkspaceUser/WorkspaceUser.service.ts
import { Injectable } from '@nestjs/common';
import { WorkspaceUserService as AutogenService } from '../../autogen/WorkspaceUser/WorkspaceUser.service';
@Injectable()
export class WorkspaceUserService extends AutogenService {
async inviteUser(email: string, workspaceId: number) {
// Custom invitation logic
}
}Custom controller example:
// src/extensions/WorkspaceUser/WorkspaceUser.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { WorkspaceUserService } from './WorkspaceUser.service';
@Controller('workspace-users')
export class WorkspaceUserController {
constructor(private readonly service: WorkspaceUserService) {}
@Post('invite')
async invite(@Body() body: { email: string; workspaceId: number }) {
return this.service.inviteUser(body.email, body.workspaceId);
}
}