Generate a TypeORM entity (extending BaseEntity), Create DTO, and Update DTO for the Lety 2.0 Backend following official TypeORM and NestJS best practices. Triggered when the user needs to create or update an entity/DTO pair.
You are generating a TypeORM entity + DTOs for the Lety 2.0 Backend (NestJS + TypeORM 0.3.x + PostgreSQL).
Priority rule: Always follow official documentation and industry best practices. If existing code in the project does not follow best practices, generate the correct version anyway and note the discrepancy so it can be improved.
Fetch the relevant page whenever there is any doubt about a decorator, option, or behavior:
Ask the user for missing information. Required:
Invoice, Lead, Conversationstring | number | boolean | Date | Decimal | enum:EnumName | jsonb | uuidEncryptionTransformer + @ApiHideProperty() + @Exclude())agencyId)ManyToOne | OneToMany | OneToOne | ManyToMany@JoinColumn here)tenant | platform | auth (default: tenant)['id'] index — e.g. ['agencyId', 'status'] for frequent filtersDo NOT invent fields. If the description is vague, ask for clarification.
From domain name (e.g. Invoice):
domainPlural → invoicestableName → invoice (snake_case singular)libs/common/src/entities/tenant/<domainPlural>/<tableName>.entity.tslibs/common/src/dto/tenant/<domainPlural>/create-<tableName>.dto.tslibs/common/src/dto/tenant/<domainPlural>/update-<tableName>.dto.tsStructure:
extends BaseEntity (from @app/common/database) — never redefine id, createdAt, updatedAt, deletedAt@Entity('table_name') — singular snake_case@Index(['id']) as minimum — add composite indices for fields used together in WHERE clauses@Unique([...]) for unique constraints across multiple columns (not just unique: true on column)Columns:
{ name: 'snake_case_name' } explicitly — TypeORM does NOT auto-convert camelCasestring → @Column({ name: 'field_name', nullable: false }) (default type is varchar)string with length → @Column({ name: 'field_name', length: 255 })text (no length limit) → @Column({ name: 'field_name', type: 'text' })number integer → @Column({ name: 'field_name', type: 'int' })number float → @Column({ name: 'field_name', type: 'float' })Decimal (money/precision) → @Column({ name: 'field_name', type: 'decimal', precision: 12, scale: 6, transformer: new DecimalColumnTransformer() })boolean → @Column({ name: 'field_name', type: 'boolean', default: false })Date timestamp → @Column({ name: 'field_name', type: 'timestamp', nullable: true })enum → @Column({ name: 'field_name', type: 'enum', enum: SomeEnum, default: SomeEnum.VALUE })jsonb → @Column({ name: 'field_name', type: 'jsonb', nullable: true, default: {} })uuid FK column → @Column({ name: 'related_id', type: 'uuid', nullable: false })transformer: new EncryptionTransformer() + @ApiHideProperty() + @Exclude()Swagger:
@ApiProperty({ description: '...', example: ..., required: true/false, nullable: true/false }) on every public column@ApiHideProperty() on sensitive/internal fields (tokens, keys, internal IDs not exposed)Relations:
@JoinColumn({ name: 'fk_name', referencedColumnName: 'id' })@JoinColumn([{ name: 'fk1', referencedColumnName: 'field1' }, ...])onDelete: 'CASCADE' only when parent deletion should cascadeOneToMany: no @JoinColumn, the FK is on the other sideagentId: string) alongside the relation objecttoResponseDto():
<Domain>Datathis.relation ? this.relation.toResponseDto() : undefinedDate fields as-is (not .toISOString())@Exclude() fields must not appear in toResponseDto).map() with null guard: this.items ? this.items.map(i => i.toResponseDto()) : []import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { BaseEntity } from '@app/common/database';
import { EncryptionTransformer } from '@app/common/database/transformers/encryption.transformer';
import DecimalColumnTransformer from '@app/common/utils/decimal.util';
import { <Domain>Data } from '@app/common/types/proto/tenant/<domainPlural>/<tableName>-interface';
// import related entities...
@Entity('<tableName>')
@Index(['id'])
// Add composite indices for frequently filtered combinations:
// @Index(['agencyId', 'status'])
export class <Domain>Entity extends BaseEntity {
// --- Public fields ---
@ApiProperty({ description: 'Field description', example: 'example value', nullable: false })
@Column({ name: 'field_name', nullable: false })
fieldName: string;
// --- Enum field ---
@ApiProperty({ description: 'Status', enum: StatusEnum, example: StatusEnum.ACTIVE })
@Column({ name: 'status', type: 'enum', enum: StatusEnum, default: StatusEnum.ACTIVE })
status: StatusEnum;
// --- Decimal (money/precision) field ---
@ApiProperty({ description: 'Amount', example: 100.00 })
@Column({ name: 'amount', type: 'decimal', precision: 12, scale: 6, default: 0,
transformer: new DecimalColumnTransformer() })
amount: Decimal;
// --- Sensitive field (never in API response) ---
@ApiHideProperty()
@Exclude()
@Column({ name: 'secret_token', nullable: true, transformer: new EncryptionTransformer() })
secretToken: string | null;
// --- FK column + relation ---
@ApiProperty({ description: 'Parent agency ID' })
@Column({ name: 'agency_id', type: 'uuid', nullable: false })
agencyId: string;
@ManyToOne(() => AgencyEntity, agency => agency.<domainPlural>, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'agency_id', referencedColumnName: 'id' })
agency: AgencyEntity;
// --- OneToMany (no @JoinColumn here, FK is on child) ---
@OneToMany(() => Child<Domain>Entity, child => child.<domainSingular>)
children: Child<Domain>Entity[];
toResponseDto(): <Domain>Data {
return {
id: this.id,
fieldName: this.fieldName,
status: this.status,
agencyId: this.agencyId,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
// Relations (null-safe):
agency: this.agency ? this.agency.toResponseDto() : undefined,
children: this.children ? this.children.map(c => c.toResponseDto()) : [],
// NEVER include secretToken or other @Exclude() fields
};
}
}
Imports:
ApiProperty / ApiPropertyOptional from @nestjs/swaggerPer field type:
string → @ApiProperty() + @IsString() + @IsNotEmpty()string with max length → add @MaxLength(N, { message: '...' })string → @ApiPropertyOptional() + @IsOptional() + @IsString()uuid → @IsUUID() + @IsNotEmpty() (or @IsOptional())number integer → @IsInt(), float → @IsNumber()number with range → add @Min(N) + @Max(N) with messagesboolean → @IsBoolean()enum → @IsEnum(EnumType, { message: '...' })@IsUrl()@ValidateNested() + @Type(() => NestedDto)@IsArray() + @ValidateNested({ each: true }) + @Type(() => ItemDto)@Transform() to parse JSON string → objectRules:
@IsOptional() must come BEFORE type validators — order matters@IsNotEmpty() on a field that also has @IsOptional()agencyId, subAccountId, etc.) never go in the Create DTO — they come from context@IsString() + @IsNotEmpty() + @IsUUID() — UUID already implies string)import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID,
Max, MaxLength, Min, ValidateNested,
} from 'class-validator';
export class Create<Domain>Dto {
@ApiProperty({ description: 'Required string field', example: 'value' })
@IsString()
@IsNotEmpty()
@MaxLength(100, { message: 'fieldName cannot exceed 100 characters' })
fieldName: string;
@ApiPropertyOptional({ description: 'Optional UUID reference' })
@IsOptional()
@IsUUID()
relatedId?: string;
@ApiPropertyOptional({ description: 'Optional enum', enum: StatusEnum })
@IsOptional()
@IsEnum(StatusEnum, { message: 'status must be a valid StatusEnum value' })
status?: StatusEnum;
@ApiProperty({ description: 'Integer in range', example: 10 })
@IsInt({ message: 'limit must be an integer' })
@Min(1, { message: 'limit must be at least 1' })
@Max(100, { message: 'limit must be at most 100' })
limit: number;
}
Always use PartialType — never copy-paste validators from Create DTO:
import { PartialType } from '@nestjs/swagger';
import { Create<Domain>Dto } from './create-<tableName>.dto';
export class Update<Domain>Dto extends PartialType(Create<Domain>Dto) {}
After generating, if the user's specification (or existing code they shared) contains any of these issues, flag them explicitly:
| Issue | What to tell the user |
|---|---|
Missing { name: 'snake_case' } on column | "Column name not explicit — TypeORM may use camelCase in DB which diverges from PostgreSQL conventions" |
Sensitive field without @Exclude() | "This field may leak in serialized responses — add @ApiHideProperty() and @Exclude()" |
Missing toResponseDto() | "Entity needs toResponseDto() — without it, the entity is serialized directly which exposes all columns" |
@IsNotEmpty() on optional field | "@IsNotEmpty() conflicts with @IsOptional() — remove it" |
FK column missing explicit { name: 'snake_case' } | "FK column name will default to camelCase — add { name: 'related_id' } explicitly" |
synchronize: true in config | "Remove from non-development environments — use migrations instead" |
| Raw SQL in migration (manually written) | "Use typeorm migration:generate to generate migrations from entity changes" |
any type on a column or DTO | "Avoid any — use a proper TypeScript type or interface" |
Present all 3 files with full paths and content. Ask:
"¿Todo correcto? ¿Quieres cambiar algo antes de crear los archivos?"
Wait for confirmation. Apply changes if requested.
On confirmation, write the 3 files. Then show:
✅ Entity + DTOs created: <Domain>
libs/common/src/entities/tenant/<domainPlural>/<tableName>.entity.ts
libs/common/src/dto/tenant/<domainPlural>/create-<tableName>.dto.ts
libs/common/src/dto/tenant/<domainPlural>/update-<tableName>.dto.ts
Next steps:
- Register entity in the module: TypeOrmModule.forFeature([<Domain>Entity])
- Add to database config if using a new schema
- Generate migration: pnpm migration:generate:tenant Add<Domain>Table
- Define proto type <Domain>Data in proto/tenant/<domainPlural>.proto
BaseEntity provides id, createdAt, updatedAt, deletedAt — never redeclare these{ name: 'snake_case' } — no exceptionstoResponseDto() is required — serialization must be explicit@ApiHideProperty() + @Exclude() + EncryptionTransformerPartialType — never duplicate validatorsagencyId, subAccountId, userId) never in Create DTO@IsOptional() must be first among validatorsany type — use proper TypeScript interfaces or types@IsNotEmpty() on an optional field