This commit is contained in:
2026-02-22 12:17:12 +03:30
parent e8eda432dc
commit 6fb561fb7f
17 changed files with 479 additions and 2 deletions
+11
View File
@@ -0,0 +1,11 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
import { LearningMessage } from "../../../common/enums/message.enum";
export class CreateLearningCategoryDto {
@ApiProperty({ description: "name of the learning category", maxLength: 200 })
@IsString({ message: LearningMessage.NAME_STRING })
@IsNotEmpty({ message: LearningMessage.NAME_REQUIRED })
name: string;
}
+38
View File
@@ -0,0 +1,38 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsNumberString, IsOptional, IsString, IsUrl } from "class-validator";
import { LearningMessage } from "../../../common/enums/message.enum";
export class CreateLearningDto {
@IsNotEmpty({ message: LearningMessage.CATEGORY_MUST_BE_ID })
@IsString({ message: LearningMessage.CATEGORY_MUST_BE_UUID })
@ApiProperty({ description: "Category ID", example: "01ARZ3NDEKTSV4RRFFQ69G5FAV" })
categoryId: string;
@ApiProperty({ description: "Title of the learning", maxLength: 200 })
@IsString({ message: LearningMessage.TITLE_STRING })
@IsNotEmpty({ message: LearningMessage.TITLE_REQUIRED })
title: string;
@IsNotEmpty({ message: LearningMessage.DESCRIPTION_REQUIRED })
@IsString({ message: LearningMessage.DESCRIPTION_STRING })
@ApiProperty({ description: "Description of the learning" })
description: string;
@IsNotEmpty({ message: LearningMessage.COVER_URL_REQUIRED })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: LearningMessage.COVER_URL_INVALID })
@IsString({ message: LearningMessage.COVER_URL_INVALID })
@ApiProperty({ description: "Cover image URL" })
coverUrl: string;
@IsOptional()
@IsString({ message: LearningMessage.VIDEO_URL_INVALID })
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: LearningMessage.VIDEO_URL_INVALID })
@ApiProperty({ description: "Video URL", required: false })
videoUrl?: string;
@IsOptional()
@IsNumberString({ no_symbols: true }, { message: LearningMessage.VIDEO_DURATION_INVALID })
@ApiProperty({ description: "Video Duration", required: false })
videoDuration?: string;
}
@@ -0,0 +1,5 @@
import { PartialType } from "@nestjs/swagger";
import { CreateLearningCategoryDto } from "./create-learning-category.dto";
export class UpdateLearningCategoryDto extends PartialType(CreateLearningCategoryDto) {}
+5
View File
@@ -0,0 +1,5 @@
import { PartialType } from "@nestjs/swagger";
import { CreateLearningDto } from "./create-learning.dto";
export class UpdateLearningDto extends PartialType(CreateLearningDto) {}
+14
View File
@@ -0,0 +1,14 @@
import { Collection, Entity, OneToMany, Property, OptionalProps } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Learning } from './learning.entity';
@Entity({ tableName: 'learning_categories' })
export class LearningCategory extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'deletedAt';
@Property({ type: 'varchar', length: 100, unique: true })
name: string;
@OneToMany(() => Learning, (learning) => learning.category)
learnings = new Collection<Learning>(this);
}
+18
View File
@@ -0,0 +1,18 @@
import { Entity, ManyToOne, Property, OptionalProps } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Learning } from './learning.entity';
import { User } from '../../user/entities/user.entity';
@Entity({ tableName: 'learning_progress' })
export class LearningProgress extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'deletedAt';
@ManyToOne(() => User)
user: User;
@ManyToOne(() => Learning)
learning: Learning;
@Property({ type: 'timestamptz', defaultRaw: 'CURRENT_TIMESTAMP' })
watchedAt: Date;
}
+30
View File
@@ -0,0 +1,30 @@
import { Collection, Entity, ManyToOne, OneToMany, Property, OptionalProps } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { LearningCategory } from './learning-category.entity';
import { LearningProgress } from './learning-progress.entity';
@Entity({ tableName: 'learnings' })
export class Learning extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'deletedAt';
@Property({ type: 'varchar', length: 200 })
title: string;
@Property({ type: 'text' })
description: string;
@Property({ type: 'text' })
coverUrl: string;
@Property({ type: 'text', nullable: true })
videoUrl?: string;
@Property({ type: 'varchar', length: 200, nullable: true })
videoDuration?: string;
@ManyToOne(() => LearningCategory)
category: LearningCategory;
@OneToMany(() => LearningProgress, (learningProgress) => learningProgress.learning)
learningProgress = new Collection<LearningProgress>(this);
}
+114
View File
@@ -0,0 +1,114 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateLearningCategoryDto } from './DTO/create-learning-category.dto';
import { CreateLearningDto } from './DTO/create-learning.dto';
import { UpdateLearningCategoryDto } from './DTO/update-learning-category.dto';
import { UpdateLearningDto } from './DTO/update-learning.dto';
import { LearningService } from './providers/learning.service';
import { AuthGuard } from '../auth/guards/auth.guard';
import { AdminAuthGuard } from '../auth/guards/adminAuth.guard';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { UserId } from 'src/common/decorators';
import { PermissionEnum } from 'src/common/enums/permission.enum';
@ApiTags('Learning')
@ApiBearerAuth()
@Controller('learnings')
export class LearningController {
constructor(private readonly learningService: LearningService) {}
// ----------------- Admin routes -----------------
@Post()
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.MANAGE_LEARNINGS)
@ApiOperation({ summary: 'Create learning video' })
createLearningVideo(@Body() createDto: CreateLearningDto) {
return this.learningService.createLearningVideo(createDto);
}
@Delete(':id')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.MANAGE_LEARNINGS)
@ApiOperation({ summary: 'Delete learning video by id' })
deleteLearningVideoById(@Param('id') id: string) {
return this.learningService.deleteLearningVideoById(id);
}
@Patch(':id')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.MANAGE_LEARNINGS)
@ApiOperation({ summary: 'Update learning video by id' })
updateLearningVideoById(@Param('id') id: string, @Body() updateDto: UpdateLearningDto) {
return this.learningService.updateLearningVideo(id, updateDto);
}
@Post('category')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.MANAGE_LEARNINGS)
@ApiOperation({ summary: 'Create learning category' })
createCategory(@Body() createDto: CreateLearningCategoryDto) {
return this.learningService.createLearningCategory(createDto);
}
@Patch('category/:id')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.MANAGE_LEARNINGS)
@ApiOperation({ summary: 'Update learning category' })
updateLearningCategory(@Param('id') id: string, @Body() updateDto: UpdateLearningCategoryDto) {
return this.learningService.updateLearningCategory(id, updateDto);
}
@Delete('category/:id')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.MANAGE_LEARNINGS)
@ApiOperation({ summary: 'Delete category by id' })
deleteCategoryById(@Param('id') id: string) {
return this.learningService.deleteCategoryById(id);
}
// ----------------- Public / User routes -----------------
@Get()
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Find learnings video' })
findAllLearnings() {
return this.learningService.findLearningsVideos();
}
@Get('category')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Find learning categories' })
findCategories() {
return this.learningService.findCategories();
}
@Get('category/:id')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Find one learning category by id' })
findCategoryById(@Param('id') id: string) {
return this.learningService.findCategoryById(id);
}
@Get('progress')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Get current user learning progress' })
getUserProgress(@UserId() userId: string) {
return this.learningService.getUserProgress(userId);
}
@Post(':learningId/progress')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Track learning progress' })
trackProgress(@UserId() userId: string, @Param('learningId') learningId: string) {
return this.learningService.trackProgress(userId, learningId);
}
@Get(':id')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Find learning video by id' })
findLearningVideoById(@Param('id') id: string) {
return this.learningService.findLearningVideoById(id);
}
}
+28
View File
@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Learning } from './entities/learning.entity';
import { LearningCategory } from './entities/learning-category.entity';
import { LearningProgress } from './entities/learning-progress.entity';
import { LearningController } from './learning.controller';
import { LearningService } from './providers/learning.service';
import { LearningCategoryRepository } from './repositories/learning-category.repository';
import { LearningProgressRepository } from './repositories/learning-progress.repository';
import { LearningRepository } from './repositories/learning.repository';
import { UserModule } from '../user/user.module';
@Module({
imports: [
MikroOrmModule.forFeature([Learning, LearningCategory, LearningProgress]),
UserModule,
],
providers: [
LearningService,
LearningRepository,
LearningCategoryRepository,
LearningProgressRepository,
],
controllers: [LearningController],
exports: [LearningService],
})
export class LearningModule {}
+158
View File
@@ -0,0 +1,158 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { CommonMessage, LearningMessage } from '../../../common/enums/message.enum';
import { UserService } from '../../user/providers/user.service';
import { CreateLearningCategoryDto } from '../DTO/create-learning-category.dto';
import { CreateLearningDto } from '../DTO/create-learning.dto';
import { UpdateLearningCategoryDto } from '../DTO/update-learning-category.dto';
import { UpdateLearningDto } from '../DTO/update-learning.dto';
import { LearningCategoryRepository } from '../repositories/learning-category.repository';
import { LearningProgressRepository } from '../repositories/learning-progress.repository';
import { LearningRepository } from '../repositories/learning.repository';
@Injectable()
export class LearningService {
constructor(
private readonly learningRepository: LearningRepository,
private readonly learningProgressRepository: LearningProgressRepository,
private readonly learningCategoryRepository: LearningCategoryRepository,
private readonly userService: UserService,
private readonly em: EntityManager,
) {}
async createLearningVideo(createDto: CreateLearningDto) {
const { category } = await this.findCategoryById(createDto.categoryId);
const learning = this.learningRepository.create({ ...createDto, category });
await this.em.persistAndFlush(learning);
return {
message: CommonMessage.CREATED,
learning,
};
}
async updateLearningVideo(learningId: string, updateDto: UpdateLearningDto) {
const { learning } = await this.findLearningVideoById(learningId);
if (updateDto.categoryId) {
const { category } = await this.findCategoryById(updateDto.categoryId);
learning.category = category;
}
this.em.assign(learning, updateDto);
await this.em.flush();
return {
message: CommonMessage.UPDATE_SUCCESS,
learning,
};
}
async deleteLearningVideoById(learningId: string) {
const { learning } = await this.findLearningVideoById(learningId);
learning.deletedAt = new Date();
await this.em.flush();
return {
message: CommonMessage.DELETED,
};
}
async findLearningsVideos() {
const learnings = await this.learningRepository.findAll();
return { learnings };
}
async findLearningVideoById(id: string) {
const learning = await this.learningRepository.findOne({ id });
if (!learning) throw new BadRequestException(LearningMessage.NOT_FOUND);
return { learning };
}
async createLearningCategory(createDto: CreateLearningCategoryDto) {
const existCategory = await this.learningCategoryRepository.findOne({
name: createDto.name,
});
if (existCategory) throw new BadRequestException(LearningMessage.CATEGORY_NAME_EXIST);
const category = this.learningCategoryRepository.create(createDto);
await this.em.persistAndFlush(category);
return { category };
}
async updateLearningCategory(categoryId: string, updateDto: UpdateLearningCategoryDto) {
const category = await this.learningCategoryRepository.findOne({ id: categoryId });
if (!category) throw new BadRequestException(LearningMessage.CATEGORY_NOT_FOUND);
if (updateDto.name) {
const existCategory = await this.learningCategoryRepository.findOne({
name: updateDto.name,
id: { $ne: categoryId },
});
if (existCategory) throw new BadRequestException(LearningMessage.CATEGORY_NAME_EXIST);
category.name = updateDto.name;
}
await this.em.flush();
return {
message: CommonMessage.UPDATE_SUCCESS,
category,
};
}
async deleteCategoryById(categoryId: string) {
await this.findCategoryById(categoryId);
const category = await this.learningCategoryRepository.findOneOrFail({ id: categoryId });
category.deletedAt = new Date();
await this.em.flush();
return {
message: CommonMessage.DELETED,
};
}
async findCategories() {
const categories = await this.learningCategoryRepository.findAll({
filters: ['notDeleted'],
});
return { categories };
}
async findCategoryById(id: string) {
const category = await this.learningCategoryRepository.findOne(
{ id },
{ filters: ['notDeleted'] },
);
if (!category) throw new BadRequestException(LearningMessage.CATEGORY_NOT_FOUND);
return { category };
}
async trackProgress(userId: string, learningId: string) {
const user = await this.userService.findOrFail(userId);
const { learning } = await this.findLearningVideoById(learningId);
const progress = this.learningProgressRepository.create({
user,
learning,
watchedAt: new Date(),
});
await this.em.persistAndFlush(progress);
return progress;
}
async getUserProgress(userId: string) {
return this.learningProgressRepository.find(
{ user: { id: userId } },
{ populate: ['learning'] },
);
}
}
@@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { LearningCategory } from '../entities/learning-category.entity';
@Injectable()
export class LearningCategoryRepository extends EntityRepository<LearningCategory> {
constructor(readonly em: EntityManager) {
super(em, LearningCategory);
}
}
@@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { LearningProgress } from '../entities/learning-progress.entity';
@Injectable()
export class LearningProgressRepository extends EntityRepository<LearningProgress> {
constructor(readonly em: EntityManager) {
super(em, LearningProgress);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { Learning } from '../entities/learning.entity';
@Injectable()
export class LearningRepository extends EntityRepository<Learning> {
constructor(readonly em: EntityManager) {
super(em, Learning);
}
}
+4
View File
@@ -1,5 +1,6 @@
import { Entity, Index, Property, OneToMany, Collection, Cascade, PrimaryKey, OptionalProps } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { LearningProgress } from '../../learnings/entities/learning-progress.entity';
// import { Order } from 'src/modules/order/entities/order.entity';
import { normalizePhone } from '../../util/phone.util';
import { ulid } from 'ulid';
@@ -8,6 +9,9 @@ import { ulid } from 'ulid';
export class User extends BaseEntity {
[OptionalProps]?: 'createdAt' | 'deletedAt'
@OneToMany(() => LearningProgress, (lp) => lp.user)
learningProgress = new Collection<LearningProgress>(this);
// @OneToMany(() => Order, order => order.user)
// orders = new Collection<Order>(this);