Entity
TypeORM entity definice a relace
Turtor používá TypeORM entity pro definici databázového schématu. Každá entita mapuje jednu PostgreSQL tabulku.
Umístění entit
apps/api/src/modules/*/entities/*.entity.tsEntity jsou organizovány podle feature modulů -- např. modules/courses/entities/course.entity.ts, modules/users/entities/user.entity.ts.
BaseEntity
Nové entity by měly dědit z BaseEntity, která poskytuje společné sloupce:
// apps/api/src/common/entities/base.entity.ts
import { BaseEntity } from '../../common/entities/base.entity';
@Entity('table_name')
export class MyEntity extends BaseEntity {
// Zděděné: id (UUID), createdAt, updatedAt
@Column({ name: 'my_field' })
myField: string;
}BaseEntity obsahuje:
id-- UUID primární klíč (@PrimaryGeneratedColumn('uuid'))createdAt-- datum vytvoření (@CreateDateColumn)updatedAt-- datum poslední aktualizace (@UpdateDateColumn)
Starší entity ještě nemusí dědit z BaseEntity -- definují si tyto sloupce přímo. Probíhá postupná migrace.
Konvence pojmenování
@Entity('table_name') // Explicitní název tabulky (snake_case)
export class EntityName {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'snake_case_name' }) // Explicitní název sloupce
camelCaseName: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
@Index() // Index pro často dotazované sloupce
@Column()
frequentlyQueried: string;
}Vždy používejte @Column({ name: 'snake_case' }) pro explicitní kontrolu názvů sloupců. TypeORM jinak automaticky převede camelCase na camelcase (bez podtržítek).
Relace
ManyToOne / OneToMany
// Course -> Region (mnoho kurzů patří jednomu kraji)
@ManyToOne(() => Region, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'region_id' })
region: Region;
// Region -> Course (opačná strana)
@OneToMany(() => Course, (course) => course.region)
courses: Course[];OneToOne
// User <-> Instructor (1:1)
@OneToOne(() => Instructor, (instructor) => instructor.user)
instructor: Instructor;ManyToMany (přes join entitu)
V Turtoru jsou M2M relace řešeny přes explicitní join entity (např. CourseInstructor, CompanyMember, InstructorSpecialization), ne přes @ManyToMany dekorátor. To umožňuje přidání extra sloupců na join tabulku.
// CourseInstructor -- M2M mezi Course a Instructor s extra sloupci
@Entity('course_instructors')
export class CourseInstructor extends BaseEntity {
@ManyToOne(() => Course, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'course_id' })
course: Course;
@ManyToOne(() => Instructor, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'instructor_id' })
instructor: Instructor;
@Column({ name: 'role', type: 'varchar' })
role: string; // hlavni_lektor, masker, figurant
@Column({ name: 'confirmation_status', type: 'varchar' })
confirmationStatus: string;
}Typy sloupců
// String
@Column({ type: 'varchar', length: 255 })
name: string;
// Nullable
@Column({ type: 'varchar', nullable: true })
bio: string | null;
// Enum jako varchar
@Column({ type: 'varchar', default: 'draft' })
status: string;
// PostgreSQL array
@Column({ type: 'varchar', array: true, default: '{}' })
roles: string[];
// JSONB (preferováno před json)
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, unknown> | null;
// Decimal pro ceny
@Column({ type: 'decimal', precision: 10, scale: 2 })
price: number;
// Boolean s výchozí hodnotou
@Column({ type: 'boolean', default: false })
isVerified: boolean;
// Date
@Column({ type: 'date' })
courseDate: string;
// Timestamp
@Column({ type: 'timestamp', nullable: true })
expiresAt: Date | null;Přehled entit
Core doména
| Entita | Tabulka | Modul | Popis |
|---|---|---|---|
User | users | users | Uživatelské účty |
Instructor | instructors | instructors | Profily instruktorů (1:1 s User) |
Company | companies | companies | Firemní zákazníci |
Course | courses | courses | Školení první pomoci |
Booking | bookings | bookings | Rezervace kurzů |
Payment | payments | payments | Platební záznamy |
Instruktor doména
| Entita | Tabulka | Popis |
|---|---|---|
Specialization | specializations | Typy rolí (hlavni_lektor, masker, figurant) |
InstructorSpecialization | instructor_specializations | M2M instruktor-specializace |
Certification | certifications | Certifikace instruktorů |
InstructorDayAvailability | instructor_day_availability | Dostupnost (bitmask sloty) |
Lokace a regiony
| Entita | Tabulka | Popis |
|---|---|---|
Region | regions | 14 českých krajů (ISO 3166-2:CZ) |
LocalGroup | local_groups | 62 oblastních spolků ČČK |
Location | locations | Fyzická místa konání kurzů |
City | cities | Města s H3 indexy |
Indexy
Důležité indexy pro výkon dotazů:
-- Kompozitní indexy pro časté dotazy
CREATE INDEX idx_courses_status_date ON courses(status, date);
CREATE INDEX idx_courses_region_status_date ON courses(region_id, status, date);
CREATE INDEX idx_bookings_course_status ON bookings(course_id, status);
CREATE INDEX idx_availability_instructor_date
ON instructor_day_availability(instructor_id, date);Indexy definujte přes @Index() dekorátor na entitě. Pro kompozitní indexy použijte @Index(['column1', 'column2']) na úrovni entity třídy.
ER diagram (zjednodušený)
User (1) ---- (1) Instructor
| |
|--- CompanyMember--+-- InstructorSpecialization -- Specialization
| | |-- Certification
| v |-- InstructorDayAvailability
| Company
| |
| +-- Booking ------- Payment
| |
+-- Notification +------- Course
|
+-- CourseInstructor
+-- CourseAuditLog
Region -- LocalGroup