From 551a73001e1373dd332db97b2d2850b5bd2a61c2 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sun, 13 Jul 2025 10:43:23 +0300 Subject: [PATCH] add categories --- backend/prisma/schema.prisma | 35 ++++++--- backend/src/core/core.module.ts | 2 + backend/src/core/graphql/schema.gql | 16 +++++ .../src/module/category/category.module.ts | 8 +++ .../src/module/category/category.resolver.ts | 23 ++++++ .../src/module/category/category.service.ts | 71 +++++++++++++++++++ .../module/category/models/category.model.ts | 32 +++++++++ .../src/module/stream/models/stream.model.ts | 7 ++ backend/src/module/stream/stream.service.ts | 13 +++- 9 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 backend/src/module/category/category.module.ts create mode 100644 backend/src/module/category/category.resolver.ts create mode 100644 backend/src/module/category/category.service.ts create mode 100644 backend/src/module/category/models/category.model.ts diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index ea047a0..a737599 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -9,17 +9,19 @@ datasource db { } model Stream { - id String @id @default(uuid()) + id String @id @default(uuid()) title String - thumbnailUrl String? @map("thumbnail_url") - ingressId String? @unique @map("ingress_id") - serverUrl String? @map("server_url") - key String? @map("key") - isLive Boolean @default(false) @map("is_live") - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String? @unique @map("user_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + thumbnailUrl String? @map("thumbnail_url") + ingressId String? @unique @map("ingress_id") + serverUrl String? @map("server_url") + key String? @map("key") + isLive Boolean @default(false) @map("is_live") + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String? @unique @map("user_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade) + categoryId String? @map("category_id") @@map("stream") } @@ -60,6 +62,19 @@ model User { @@map("users") } +model Category { + id String @id @default(uuid()) + title String + slug String @unique + description String? + thumbnailUrl String @map("thumbnail_url") + streams Stream[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("categories") +} + model Token { id String @id @default(uuid()) token String @unique diff --git a/backend/src/core/core.module.ts b/backend/src/core/core.module.ts index 43c054f..f6736ff 100644 --- a/backend/src/core/core.module.ts +++ b/backend/src/core/core.module.ts @@ -7,6 +7,7 @@ import { ProfileModule } from '@/src/module/auth/profile/profile.module' import { SessionModule } from '@/src/module/auth/session/session.module' import { TotpModule } from '@/src/module/auth/totp/totp.module' import { VerificationModule } from '@/src/module/auth/verification/verification.module' +import { CategoryModule } from '@/src/module/category/category.module' import { CronModule } from '@/src/module/cron/cron.module' import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module' import { MailModule } from '@/src/module/libs/mail/mail.module' @@ -54,6 +55,7 @@ import { RedisModule } from './redis/redis.module' StreamModule, IngressModule, WebhookModule, + CategoryModule, ], }) export class CoreModule {} diff --git a/backend/src/core/graphql/schema.gql b/backend/src/core/graphql/schema.gql index 2ba3118..fb20c7f 100644 --- a/backend/src/core/graphql/schema.gql +++ b/backend/src/core/graphql/schema.gql @@ -7,6 +7,17 @@ type AuthModel { user: UserModel } +type CategoryModel { + createdAt: DateTime! + description: String + id: ID! + slug: String! + streams: [StreamModel!]! + thumbnailUrl: String! + title: String! + updatedAt: DateTime! +} + input ChangeEmailInput { email: String! } @@ -118,9 +129,12 @@ input NewPasswordInput { } type Query { + findAllCategories: [CategoryModel!]! findAllStreams(filters: FilterInput!): [StreamModel!]! + findCategoryBySlug(slug: String!): CategoryModel! findCurrentSession: SessionModel! findProfile: UserModel! + findRandomCategories: [CategoryModel!]! findRandomStreams: [StreamModel!]! findSessionsByUser: [SessionModel!]! findSocialLinks: [SocialLinkModel!]! @@ -165,6 +179,8 @@ input SocialLinkOrderInput { } type StreamModel { + category: CategoryModel! + categoryId: ID! createdAt: DateTime! id: ID! ingressId: String diff --git a/backend/src/module/category/category.module.ts b/backend/src/module/category/category.module.ts new file mode 100644 index 0000000..90e4778 --- /dev/null +++ b/backend/src/module/category/category.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { CategoryService } from './category.service'; +import { CategoryResolver } from './category.resolver'; + +@Module({ + providers: [CategoryResolver, CategoryService], +}) +export class CategoryModule {} diff --git a/backend/src/module/category/category.resolver.ts b/backend/src/module/category/category.resolver.ts new file mode 100644 index 0000000..a127994 --- /dev/null +++ b/backend/src/module/category/category.resolver.ts @@ -0,0 +1,23 @@ +import { CategoryModel } from '@/src/module/category/models/category.model' +import { Args, Query, Resolver } from '@nestjs/graphql' +import { CategoryService } from './category.service' + +@Resolver('Category') +export class CategoryResolver { + constructor(private readonly categoryService: CategoryService) {} + + @Query(() => [CategoryModel], { name: 'findAllCategories' }) + public async findAll() { + return this.categoryService.findAll() + } + + @Query(() => [CategoryModel], { name: 'findRandomCategories' }) + public async findRandom() { + return this.categoryService.findRandom() + } + + @Query(() => CategoryModel, { name: 'findCategoryBySlug' }) + public async findByhSlug(@Args('slug') slug: string) { + return this.categoryService.findBySlug(slug) + } +} diff --git a/backend/src/module/category/category.service.ts b/backend/src/module/category/category.service.ts new file mode 100644 index 0000000..7870003 --- /dev/null +++ b/backend/src/module/category/category.service.ts @@ -0,0 +1,71 @@ +import { PrismaService } from '@/src/core/prisma/prisma.service' +import { Injectable, NotFoundException } from '@nestjs/common' + +@Injectable() +export class CategoryService { + constructor( + private readonly prismaService: PrismaService, + ) { + } + + public async findAll() { + return this.prismaService.category.findMany({ + orderBy: { createdAt: 'desc' }, + include: { + streams: { + include: { + user: true, + category: true, + }, + }, + }, + }) + } + + public async findRandom() { + const total = await this.prismaService.category.count({}) + + const randomIndexes = new Set() + while (randomIndexes.size < 7) { + const randomIndex = Math.floor(Math.random() * total) + randomIndexes.add(randomIndex) + } + + const categories = await this.prismaService.category.findMany({ + take: total, + skip: 0, + include: { + streams: { + include: { + user: true, + category: true, + }, + }, + }, + }) + + return Array.from(randomIndexes).map(index => categories[index]) + } + + public async findBySlug(slug: string) { + const category = await this.prismaService.category.findUnique({ + where: { + slug, + }, + include: { + streams: { + include: { + user: true, + category: true, + }, + }, + }, + }) + + if (!category) { + throw new NotFoundException('Категория не найдена') + } + + return category + } +} diff --git a/backend/src/module/category/models/category.model.ts b/backend/src/module/category/models/category.model.ts new file mode 100644 index 0000000..9918ad6 --- /dev/null +++ b/backend/src/module/category/models/category.model.ts @@ -0,0 +1,32 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql' + +import type { Category } from '@/prisma/generated' + +import { StreamModel } from '../../stream/models/stream.model' + +@ObjectType() +export class CategoryModel implements Category { + @Field(() => ID) + public id: string + + @Field(() => String) + public title: string + + @Field(() => String) + public slug: string + + @Field(() => String, { nullable: true }) + public description: string + + @Field(() => String) + public thumbnailUrl: string + + @Field(() => [StreamModel]) + public streams: StreamModel[] + + @Field(() => Date) + public createdAt: Date + + @Field(() => Date) + public updatedAt: Date +} diff --git a/backend/src/module/stream/models/stream.model.ts b/backend/src/module/stream/models/stream.model.ts index 3356a04..da2aa98 100644 --- a/backend/src/module/stream/models/stream.model.ts +++ b/backend/src/module/stream/models/stream.model.ts @@ -1,4 +1,5 @@ import { UserModel } from '@/src/module/auth/account/models/user.model' +import { CategoryModel } from '@/src/module/category/models/category.model'; import { Field, ID, ObjectType } from '@nestjs/graphql' import { Stream } from '@prisma/generated' @@ -13,6 +14,12 @@ export class StreamModel implements Stream { @Field(() => UserModel) user: UserModel + @Field(() => ID) + categoryId: CategoryModel['id'] + + @Field(() => CategoryModel) + category: CategoryModel + @Field(() => String) title: string diff --git a/backend/src/module/stream/stream.service.ts b/backend/src/module/stream/stream.service.ts index d690f92..b8a6562 100644 --- a/backend/src/module/stream/stream.service.ts +++ b/backend/src/module/stream/stream.service.ts @@ -32,7 +32,7 @@ export class StreamService { user: { isDeactivated: false }, ...whereClause, }, - include: { user: true }, + include: { user: true, category: true }, orderBy: { createdAt: 'desc', }, @@ -62,7 +62,7 @@ export class StreamService { }, take: total, skip: 0, - include: { user: true }, + include: { user: true, category: true }, }) return Array.from(randomIndexes).map(index => streams[index]) @@ -93,7 +93,14 @@ export class StreamService { const { categoryId, title } = input return this.prismaService.stream.update({ where: { userId: user.id }, - data: { title }, + data: { + title, + category: { + connect: { + id: categoryId, + }, + }, + }, }) }