Turtor Docs
Databáze

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.ts

Entity 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

EntitaTabulkaModulPopis
UserusersusersUživatelské účty
InstructorinstructorsinstructorsProfily instruktorů (1:1 s User)
CompanycompaniescompaniesFiremní zákazníci
CoursecoursescoursesŠkolení první pomoci
BookingbookingsbookingsRezervace kurzů
PaymentpaymentspaymentsPlatební záznamy

Instruktor doména

EntitaTabulkaPopis
SpecializationspecializationsTypy rolí (hlavni_lektor, masker, figurant)
InstructorSpecializationinstructor_specializationsM2M instruktor-specializace
CertificationcertificationsCertifikace instruktorů
InstructorDayAvailabilityinstructor_day_availabilityDostupnost (bitmask sloty)

Lokace a regiony

EntitaTabulkaPopis
Regionregions14 českých krajů (ISO 3166-2:CZ)
LocalGrouplocal_groups62 oblastních spolků ČČK
LocationlocationsFyzická místa konání kurzů
CitycitiesMě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