Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { HealthModule } from './modules/health/health.module';
import { RedisModule } from './redis/redis.module';
import { AuthModule } from './modules/auth/auth.module';
import { UserModule } from './modules/user/user.module';
import { SessionsModule } from './modules/sessions/sessions.module';
import { ShutdownModule } from './common/services/shutdown.module';

@Module({
Expand All @@ -25,6 +26,7 @@ import { ShutdownModule } from './common/services/shutdown.module';
RedisModule.forRoot(),
AuthModule,
UserModule,
SessionsModule,
HealthModule,
ShutdownModule,
],
Expand Down
175 changes: 175 additions & 0 deletions src/database/migrations/1745800000000-CreateSessionsTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from 'typeorm';

export class CreateSessionsTable1745800000000 implements MigrationInterface {
name = 'CreateSessionsTable1745800000000';

public async up(queryRunner: QueryRunner): Promise<void> {
// Create sessions table
await queryRunner.createTable(
new Table({
name: 'sessions',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{
name: 'mentorId',
type: 'uuid',
},
{
name: 'menteeId',
type: 'uuid',
},
{
name: 'startTime',
type: 'timestamp',
},
{
name: 'endTime',
type: 'timestamp',
},
{
name: 'status',
type: 'enum',
enum: ['pending', 'confirmed', 'completed', 'cancelled', 'no_show'],
default: "'pending'",
},
{
name: 'meetingUrl',
type: 'varchar',
isNullable: true,
},
{
name: 'notes',
type: 'text',
isNullable: true,
},
{
name: 'rating',
type: 'int',
isNullable: true,
},
{
name: 'review',
type: 'text',
isNullable: true,
},
{
name: 'cancelledAt',
type: 'timestamp',
isNullable: true,
},
{
name: 'cancellationReason',
type: 'varchar',
isNullable: true,
},
{
name: 'createdAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
},
],
}),
true,
);

// Create indexes
await queryRunner.createIndex(
'sessions',
new TableIndex({
name: 'IDX_SESSIONS_MENTOR_ID',
columnNames: ['mentorId'],
}),
);

await queryRunner.createIndex(
'sessions',
new TableIndex({
name: 'IDX_SESSIONS_MENTEE_ID',
columnNames: ['menteeId'],
}),
);

await queryRunner.createIndex(
'sessions',
new TableIndex({
name: 'IDX_SESSIONS_STATUS',
columnNames: ['status'],
}),
);

await queryRunner.createIndex(
'sessions',
new TableIndex({
name: 'IDX_SESSIONS_TIME_RANGE',
columnNames: ['startTime', 'endTime'],
}),
);

await queryRunner.createIndex(
'sessions',
new TableIndex({
name: 'IDX_SESSIONS_MENTOR_TIME',
columnNames: ['mentorId', 'startTime', 'endTime'],
}),
);

// Create foreign keys
await queryRunner.createForeignKey(
'sessions',
new TableForeignKey({
name: 'FK_SESSIONS_MENTOR',
columnNames: ['mentorId'],
referencedTableName: 'users',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);

await queryRunner.createForeignKey(
'sessions',
new TableForeignKey({
name: 'FK_SESSIONS_MENTEE',
columnNames: ['menteeId'],
referencedTableName: 'users',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Drop foreign keys
const table = await queryRunner.getTable('sessions');
const mentorForeignKey = table?.foreignKeys.find((fk) => fk.name === 'FK_SESSIONS_MENTOR');
const menteeForeignKey = table?.foreignKeys.find((fk) => fk.name === 'FK_SESSIONS_MENTEE');

if (mentorForeignKey) {
await queryRunner.dropForeignKey('sessions', mentorForeignKey);
}
if (menteeForeignKey) {
await queryRunner.dropForeignKey('sessions', menteeForeignKey);
}

// Drop indexes
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_MENTOR_TIME');
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_TIME_RANGE');
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_STATUS');
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_MENTEE_ID');
await queryRunner.dropIndex('sessions', 'IDX_SESSIONS_MENTOR_ID');

// Drop table
await queryRunner.dropTable('sessions');
}
}
8 changes: 8 additions & 0 deletions src/modules/auth/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
JoinTable,
Index,
OneToOne,
OneToMany,
} from 'typeorm';
import { Role } from './role.entity';
import { MentorProfile } from '../../user/entities/mentor-profile.entity';
import { MenteeProfile } from '../../user/entities/mentee-profile.entity';
import { Session } from '../../sessions/entities/session.entity';

export enum UserRole {
USER = 'user',
Expand Down Expand Up @@ -86,6 +88,12 @@ export class User {
@OneToOne(() => MenteeProfile, (menteeProfile) => menteeProfile.user, { nullable: true, cascade: true })
menteeProfile: MenteeProfile;

@OneToMany(() => Session, (session) => session.mentor)
mentorSessions: Session[];

@OneToMany(() => Session, (session) => session.mentee)
menteeSessions: Session[];

@CreateDateColumn()
createdAt: Date;

Expand Down
77 changes: 77 additions & 0 deletions src/modules/sessions/dto/session.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { IsString, IsDateString, IsOptional, IsEnum, Min, Max, IsNumber } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class CreateSessionDto {
@ApiProperty({ description: 'Mentor ID to book session with' })
@IsString()
mentorId: string;

@ApiProperty({ description: 'Session start time in ISO format' })
@IsDateString()
startTime: string;

@ApiProperty({ description: 'Session end time in ISO format' })
@IsDateString()
endTime: string;

@ApiPropertyOptional({ description: 'Session notes or agenda' })
@IsString()
@IsOptional()
notes?: string;
}

export class UpdateSessionStatusDto {
@ApiProperty({
enum: ['pending', 'confirmed', 'completed', 'cancelled', 'no_show'],
description: 'New session status'
})
@IsEnum(['pending', 'confirmed', 'completed', 'cancelled', 'no_show'])
status: string;
}

export class CancelSessionDto {
@ApiPropertyOptional({ description: 'Reason for cancellation' })
@IsString()
@IsOptional()
reason?: string;
}

export class RescheduleSessionDto {
@ApiProperty({ description: 'New session start time in ISO format' })
@IsDateString()
startTime: string;

@ApiProperty({ description: 'New session end time in ISO format' })
@IsDateString()
endTime: string;
}

export class RateSessionDto {
@ApiProperty({ description: 'Rating from 1 to 5', minimum: 1, maximum: 5 })
@IsNumber()
@Min(1)
@Max(5)
rating: number;

@ApiPropertyOptional({ description: 'Review text' })
@IsString()
@IsOptional()
review?: string;
}

export class SessionQueryDto {
@ApiPropertyOptional({ description: 'Filter by status' })
@IsEnum(['pending', 'confirmed', 'completed', 'cancelled', 'no_show'])
@IsOptional()
status?: string;

@ApiPropertyOptional({ description: 'Page number', default: 1 })
@IsNumber()
@IsOptional()
page?: number;

@ApiPropertyOptional({ description: 'Items per page', default: 20 })
@IsNumber()
@IsOptional()
limit?: number;
}
85 changes: 85 additions & 0 deletions src/modules/sessions/entities/session.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { User } from '../../auth/entities/user.entity';

export enum SessionStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
COMPLETED = 'completed',
CANCELLED = 'cancelled',
NO_SHOW = 'no_show',
}

@Entity('sessions')
@Index(['mentorId'])
@Index(['menteeId'])
@Index(['status'])
@Index(['startTime', 'endTime'])
@Index(['mentorId', 'startTime', 'endTime'])
export class Session {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
@Index()
mentorId: string;

@Column()
@Index()
menteeId: string;

@Column({ type: 'timestamp' })
@Index()
startTime: Date;

@Column({ type: 'timestamp' })
@Index()
endTime: Date;

@Column({
type: 'enum',
enum: SessionStatus,
default: SessionStatus.PENDING,
})
status: SessionStatus;

@Column({ nullable: true })
meetingUrl: string;

@Column({ type: 'text', nullable: true })
notes: string;

@Column({ type: 'int', nullable: true })
rating: number; // 1-5 rating

@Column({ type: 'text', nullable: true })
review: string;

@Column({ type: 'timestamp', nullable: true })
cancelledAt: Date;

@Column({ nullable: true })
cancellationReason: string;

@ManyToOne(() => User, { eager: false })
@JoinColumn({ name: 'mentorId' })
mentor: User;

@ManyToOne(() => User, { eager: false })
@JoinColumn({ name: 'menteeId' })
mentee: User;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
12 changes: 12 additions & 0 deletions src/modules/sessions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export { Session, SessionStatus } from './entities/session.entity';
export { SessionsModule } from './sessions.module';
export { SessionsService } from './sessions.service';
export { SessionsController } from './sessions.controller';
export {
CreateSessionDto,
UpdateSessionStatusDto,
CancelSessionDto,
RescheduleSessionDto,
RateSessionDto,
SessionQueryDto,
} from './dto/session.dto';
Loading