Database Package
The @repo/db package contains TypeORM entity definitions and database configuration shared across the application.
Overview
This package provides:
- TypeORM entity definitions for all database tables
- Shared database configuration
- Relationships between entities
- Database naming strategies
Structure
packages/db/
├── src/
│ ├── entities/ # Entity definitions
│ │ ├── User.entity.ts
│ │ ├── Course.entity.ts
│ │ ├── Module.entity.ts
│ │ ├── Quiz.entity.ts
│ │ └── ...
│ │
│ ├── index.ts # Main exports
│ └── data-source.ts # DataSource configuration (if applicable)
│
├── package.json
└── tsconfig.jsonEntity Definitions
Entities use TypeORM decorators to define database schema and relationships.
Example Entity
// src/entities/Course.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
import { Module } from "./Module.entity";
import { Membership } from "./Membership.entity";
import { CourseFile } from "./CourseFile.entity";
@Entity()
export class Course {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
code: string;
@Column({ type: "text", nullable: true })
description: string | null;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
// Relationships
@OneToMany(() => Module, (module) => module.course)
modules: Module[];
@OneToMany(() => Membership, (membership) => membership.course)
memberships: Membership[];
@OneToMany(() => CourseFile, (file) => file.course)
files: CourseFile[];
}Key Entities
User Management
User: Core user entity
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column({ nullable: true })
passwordHash: string | null;
@OneToMany(() => OAuthCredential, (credential) => credential.user)
oauthCredentials: OAuthCredential[];
@OneToMany(() => Membership, (membership) => membership.user)
memberships: Membership[];
}OAuthCredential: OAuth provider credentials
@Entity()
export class OAuthCredential {
@PrimaryGeneratedColumn()
id: number;
@Column()
provider: string; // 'google', 'github', etc.
@Column()
providerUserId: string;
@ManyToOne(() => User, (user) => user.oauthCredentials)
user: User;
@Column()
userId: number;
}Course Structure
Course: Main course entity (shown above)
Module: Course modules/sections
@Entity()
export class Module {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ default: 0 })
order: number;
@ManyToOne(() => Course, (course) => course.modules)
course: Course;
@Column()
courseId: number;
@OneToMany(() => Chapter, (chapter) => chapter.module)
chapters: Chapter[];
}Chapter: Module chapters
@Entity()
export class Chapter {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ type: "text", nullable: true })
content: string | null;
@Column({ default: 0 })
order: number;
@ManyToOne(() => Module, (module) => module.chapters)
module: Module;
@Column()
moduleId: number;
}Course Content
CourseFile: File attachments
@Entity()
export class CourseFile {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
storagePath: string;
@ManyToOne(() => Course, (course) => course.files)
course: Course;
@Column()
courseId: number;
@OneToOne(() => CourseFileMetadata, (metadata) => metadata.file)
metadata: CourseFileMetadata;
}CourseFileMetadata: File metadata
@Entity()
export class CourseFileMetadata {
@PrimaryGeneratedColumn()
id: number;
@Column()
mimeType: string;
@Column({ type: "bigint" })
size: number;
@OneToOne(() => CourseFile, (file) => file.metadata)
@JoinColumn()
file: CourseFile;
@Column()
fileId: number;
}Quizzes
Quiz: Quiz definition
@Entity()
export class Quiz {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ type: "text", nullable: true })
description: string | null;
@Column({ nullable: true })
timeLimit: number | null; // in minutes
@Column({ default: 1 })
maxAttempts: number;
@ManyToOne(() => Course, (course) => course.quizzes)
course: Course;
@Column()
courseId: number;
@OneToMany(() => QuizQuestion, (question) => question.quiz)
questions: QuizQuestion[];
@OneToMany(() => QuizAttempt, (attempt) => attempt.quiz)
attempts: QuizAttempt[];
}QuizQuestion: Individual questions
@Entity()
export class QuizQuestion {
@PrimaryGeneratedColumn()
id: number;
@Column()
type: "MULTIPLE_CHOICE" | "TRUE_FALSE" | "SHORT_ANSWER" | "ESSAY";
@Column({ type: "text" })
questionText: string;
@Column({ type: "jsonb", nullable: true })
options: string[] | null; // For multiple choice
@Column({ type: "jsonb" })
correctAnswer: unknown;
@Column({ default: 1 })
points: number;
@Column({ default: 0 })
order: number;
@ManyToOne(() => Quiz, (quiz) => quiz.questions)
quiz: Quiz;
@Column()
quizId: number;
}QuizAttempt: Student quiz attempts
@Entity()
export class QuizAttempt {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User)
user: User;
@Column()
userId: number;
@ManyToOne(() => Quiz, (quiz) => quiz.attempts)
quiz: Quiz;
@Column()
quizId: number;
@CreateDateColumn()
startedAt: Date;
@Column({ nullable: true })
submittedAt: Date | null;
@Column({ type: "decimal", precision: 5, scale: 2, nullable: true })
score: number | null;
@OneToMany(() => QuizResponse, (response) => response.attempt)
responses: QuizResponse[];
}Discussions
Discussion: Discussion threads
@Entity()
export class Discussion {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ type: "text" })
content: string;
@ManyToOne(() => User)
author: User;
@Column()
authorId: number;
@ManyToOne(() => Course)
course: Course;
@Column()
courseId: number;
@CreateDateColumn()
createdAt: Date;
@OneToMany(() => DiscussionComment, (comment) => comment.discussion)
comments: DiscussionComment[];
}DiscussionComment: Comments on discussions
@Entity()
export class DiscussionComment {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: "text" })
content: string;
@ManyToOne(() => User)
author: User;
@Column()
authorId: number;
@ManyToOne(() => Discussion, (discussion) => discussion.comments)
discussion: Discussion;
@Column()
discussionId: number;
@CreateDateColumn()
createdAt: Date;
}Memberships and Permissions
Membership: User-course relationship
@Entity()
export class Membership {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, (user) => user.memberships)
user: User;
@Column()
userId: number;
@ManyToOne(() => Course)
course: Course;
@Column()
courseId: number;
@ManyToOne(() => Role, (role) => role.memberships)
role: Role;
@Column()
roleId: number;
@CreateDateColumn()
joinedAt: Date;
}Role: User roles
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string;
@Column({ type: "text", nullable: true })
description: string | null;
@ManyToMany(() => Permission, (permission) => permission.roles)
@JoinTable()
permissions: Permission[];
@OneToMany(() => Membership, (membership) => membership.role)
memberships: Membership[];
}Permission: Granular permissions
@Entity()
export class Permission {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
type: string;
@Column({ type: "text", nullable: true })
description: string | null;
@ManyToMany(() => Role, (role) => role.permissions)
roles: Role[];
}Analytics
ContentActivity: Content engagement tracking
@Entity()
export class ContentActivity {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User)
user: User;
@Column()
userId: number;
@Column()
contentType: "MODULE" | "CHAPTER" | "FILE" | "QUIZ";
@Column()
contentId: number;
@Column()
activityType: "VIEW" | "DOWNLOAD" | "COMPLETE";
@Column({ type: "int", nullable: true })
duration: number | null; // seconds
@CreateDateColumn()
timestamp: Date;
}ChatActivity: Chat interaction tracking
@Entity()
export class ChatActivity {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User)
user: User;
@Column()
userId: number;
@Column({ type: "text" })
userMessage: string;
@Column({ type: "text" })
aiResponse: string;
@Column()
tokenCount: number;
@CreateDateColumn()
timestamp: Date;
}Calendar
Calendar: Course calendar
@Entity()
export class Calendar {
@PrimaryGeneratedColumn()
id: number;
@OneToOne(() => Course)
@JoinColumn()
course: Course;
@Column()
courseId: number;
@OneToMany(() => CalendarEvent, (event) => event.calendar)
events: CalendarEvent[];
}CalendarEvent: Calendar events
@Entity()
export class CalendarEvent {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ type: "text", nullable: true })
description: string | null;
@Column()
startTime: Date;
@Column()
endTime: Date;
@ManyToOne(() => Calendar, (calendar) => calendar.events)
calendar: Calendar;
@Column()
calendarId: number;
}Usage in Backend
Repository Pattern
import { DataSource } from "typeorm";
import { Course, Module } from "@repo/db";
export class CourseService {
constructor(private dataSource: DataSource) {}
async getCourse(id: number): Promise<Course> {
const courseRepo = this.dataSource.getRepository(Course);
return courseRepo.findOne({
where: { id },
relations: ["modules", "memberships"],
});
}
async createCourse(data: CreateCourseDto): Promise<Course> {
const courseRepo = this.dataSource.getRepository(Course);
const course = courseRepo.create(data);
return courseRepo.save(course);
}
}Query Builder
const courses = await this.dataSource
.getRepository(Course)
.createQueryBuilder("course")
.leftJoinAndSelect("course.modules", "module")
.leftJoinAndSelect("course.memberships", "membership")
.where("membership.userId = :userId", { userId })
.orderBy("course.createdAt", "DESC")
.getMany();Naming Strategy
The package uses snake_case for database columns:
import { SnakeNamingStrategy } from "typeorm-naming-strategies";
export const dataSource = new DataSource({
// ... other config
namingStrategy: new SnakeNamingStrategy(),
});TypeScript:
class User {
createdAt: Date;
passwordHash: string;
}Database:
created_at TIMESTAMP
password_hash VARCHARMigrations
Generate migrations when entities change:
# Generate migration
typeorm migration:generate -n AddQuizTables
# Run migrations
typeorm migration:run
# Revert migration
typeorm migration:revertBest Practices
- Always specify nullable explicitly
- Use appropriate column types (text vs varchar, int vs bigint)
- Define relationships from both sides
- Use indexes on frequently queried columns
- Keep entities focused - one entity per table
- Document complex relationships
- Use transactions for multi-entity operations
Related Documentation
- Backend Components - Using entities in services
- Type Safety Flow - How entity types flow to frontend
- Architecture Overview - Database in system architecture