Skip to Content
🚀 APSO is now in public beta. Get started →
GuidesSchemaEntity Definition

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:

.apsorc
{ "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 project database table (entity name lowercased)
  • REST endpoints: GET /project, POST /project, GET /project/:id, PATCH /project/:id, DELETE /project/:id
  • A TypeORM entity class Project with decorators and validation
  • Create and Update DTO classes with class-validator rules

Entity Properties Reference

PropertyTypeRequiredDefaultDescription
namestringYes—The entity name. Used as the class name and (lowercased) as the table name.
fieldsarrayYes—Array of field definitions (at least one required).
created_atbooleanNofalseAdds an auto-managed created_at timestamp column.
updated_atbooleanNofalseAdds an auto-managed updated_at timestamp column.
primaryKeyType"serial" or "uuid"No"serial"Type of primary key. serial = auto-incrementing integer, uuid = UUID v4.
uniquesarrayNo[]Composite unique constraints spanning multiple fields.
scopeBystring or string[]No—Fields for multi-tenant data isolation. See Multi-Tenancy.
scopeOptionsobjectNo—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

PropertyTypeApplies ToDescription
namestringAllField name. Used as the column name and TypeScript property.
typestringAllData type. See Field Types for the complete list.
nullablebooleanAllWhether the field accepts null. Generates @IsOptional() when true.
uniquebooleanAllAdds a unique constraint on the column.
defaultstring, number, booleanAllDefault value for the column.
lengthintegertextMaximum character length. Generates @MaxLength(n).
is_emailbooleantextAdds @IsEmail() validation.
valuesstring[]enumRequired for enum fields. The list of allowed values.
precisionintegerdecimal, numericTotal number of digits (1 to 131072).
scaleintegerdecimal, numericDigits 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 ConfigurationGenerated ValidatorEffect
"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, not Projects
  • Entity class names are used exactly as defined in .apsorc
  • Table names are lowercased: Project becomes the project table

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:

PropertyTypeRequiredDescription
namestringYesThe constraint name in the database.
fieldsstring[]YesThe 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:

.apsorc (entities array excerpt)
{ "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:

  1. Create an extensions directory for the entity: src/extensions/{EntityName}/
  2. Subclass the generated service to add custom methods
  3. 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 here

Custom 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); } }

Next Steps

Last updated on