add follow

This commit is contained in:
Sergey Krylov 2025-07-14 06:57:46 +03:00
parent 4d3428e56c
commit 0c27a6f513
7 changed files with 198 additions and 1 deletions

View File

@ -40,7 +40,22 @@ model ChatMessage {
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@map("chat_messages ") @@map("chat_messages")
}
model Follow {
id String @id @default(uuid())
follower User @relation(name: "followers", fields: [followerId], references: [id], onDelete: Cascade)
followerId String @map("follower_id")
following User @relation(name: "followings", fields: [followingId], references: [id], onDelete: Cascade)
followingId String @map("following_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([followingId, followerId])
@@index([followerId])
@@index([followingId])
@@map("follows")
} }
model SocialLink { model SocialLink {
@ -74,6 +89,8 @@ model User {
totpSecret String? @map("totp_secret") totpSecret String? @map("totp_secret")
stream Stream? stream Stream?
chatMessages ChatMessage[] chatMessages ChatMessage[]
followers Follow[] @relation(name: "followers")
followings Follow[] @relation(name: "followings")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")

View File

@ -10,6 +10,7 @@ import { VerificationModule } from '@/src/module/auth/verification/verification.
import { CategoryModule } from '@/src/module/category/category.module' import { CategoryModule } from '@/src/module/category/category.module'
import { ChatModule } from '@/src/module/chat/chat.module' import { ChatModule } from '@/src/module/chat/chat.module'
import { CronModule } from '@/src/module/cron/cron.module' import { CronModule } from '@/src/module/cron/cron.module'
import { FollowModule } from '@/src/module/follow/follow.module'
import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module' import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module'
import { MailModule } from '@/src/module/libs/mail/mail.module' import { MailModule } from '@/src/module/libs/mail/mail.module'
import { StorageModule } from '@/src/module/libs/storage/storage.module' import { StorageModule } from '@/src/module/libs/storage/storage.module'
@ -58,6 +59,7 @@ import { RedisModule } from './redis/redis.module'
WebhookModule, WebhookModule,
CategoryModule, CategoryModule,
ChatModule, ChatModule,
FollowModule,
], ],
}) })
export class CoreModule {} export class CoreModule {}

View File

@ -87,6 +87,16 @@ input FilterInput {
take: Float take: Float
} }
type FollowModel {
createdAt: DateTime!
follower: UserModel!
followerId: ID!
following: UserModel!
followingId: ID!
id: ID!
updatedAt: DateTime!
}
input GenerateStreamTokenInput { input GenerateStreamTokenInput {
channelId: String! channelId: String!
userId: String! userId: String!
@ -124,6 +134,7 @@ type Mutation {
deactivateAccount(data: DeactivateAccountInput!): AuthModel! deactivateAccount(data: DeactivateAccountInput!): AuthModel!
disableTotp: Boolean! disableTotp: Boolean!
enableTotp(data: EnableTotpInput!): Boolean! enableTotp(data: EnableTotpInput!): Boolean!
followChannel(channelId: String!): FollowModel!
generateStreamToken(data: GenerateStreamTokenInput!): GenerateTokenModel! generateStreamToken(data: GenerateStreamTokenInput!): GenerateTokenModel!
loginUser(data: LoginInput!): AuthModel! loginUser(data: LoginInput!): AuthModel!
logoutUser: Boolean! logoutUser: Boolean!
@ -135,6 +146,7 @@ type Mutation {
resetPassword(data: ResetPasswordInput!): Boolean! resetPassword(data: ResetPasswordInput!): Boolean!
sendChatMessage(data: SendMessageInput!): ChatMessageModel! sendChatMessage(data: SendMessageInput!): ChatMessageModel!
setNewPassword(data: NewPasswordInput!): Boolean! setNewPassword(data: NewPasswordInput!): Boolean!
unfollowChannel(channelId: String!): FollowModel!
updateSocialLink(data: SocialLinkInput!, id: String!): SocialLinkModel! updateSocialLink(data: SocialLinkInput!, id: String!): SocialLinkModel!
verifyAccount(data: VerificationInput!): AuthModel! verifyAccount(data: VerificationInput!): AuthModel!
} }
@ -151,6 +163,8 @@ type Query {
findCategoryBySlug(slug: String!): CategoryModel! findCategoryBySlug(slug: String!): CategoryModel!
findCurrentSession: SessionModel! findCurrentSession: SessionModel!
findMessagesByStream(streamId: String!): [ChatMessageModel!]! findMessagesByStream(streamId: String!): [ChatMessageModel!]!
findMyFollowers: [FollowModel!]!
findMyFollowings: [FollowModel!]!
findProfile: UserModel! findProfile: UserModel!
findRandomCategories: [CategoryModel!]! findRandomCategories: [CategoryModel!]!
findRandomStreams: [StreamModel!]! findRandomStreams: [StreamModel!]!

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common'
import { FollowService } from './follow.service'
import { FollowResolver } from './follow.resolver'
@Module({
providers: [FollowResolver, FollowService],
})
export class FollowModule {}

View File

@ -0,0 +1,35 @@
import { FollowModel } from '@/src/module/follow/model/follow.model'
import { Authorization } from '@/src/shared/decorators/auth.decorator'
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
import { User } from '@prisma/generated'
import { FollowService } from './follow.service'
@Resolver('Follow')
export class FollowResolver {
constructor(private readonly followService: FollowService) {}
@Authorization()
@Query(() => [FollowModel], { name: 'findMyFollowers' })
public async findMyFollowers(@Authorized() user: User) {
return this.followService.findMyFollowers(user)
}
@Authorization()
@Query(() => [FollowModel], { name: 'findMyFollowings' })
public async findMyFollowings(@Authorized() user: User) {
return this.followService.findMyFollowings(user)
}
@Authorization()
@Mutation(() => FollowModel, { name: 'followChannel' })
public async follow(@Authorized() user: User, @Args('channelId') channelId: string) {
return this.followService.follow(user, channelId)
}
@Authorization()
@Mutation(() => FollowModel, { name: 'unfollowChannel' })
public async unfollow(@Authorized() user: User, @Args('channelId') channelId: string) {
return this.followService.unfollow(user, channelId)
}
}

View File

@ -0,0 +1,94 @@
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'
import { User } from '@prisma/generated'
@Injectable()
export class FollowService {
constructor(
private readonly prismaService: PrismaService,
) {
}
public async findMyFollowers(user: User) {
return this.prismaService.follow.findMany({
where: { followingId: user.id },
orderBy: { createdAt: 'desc' },
include: { follower: true, following: true },
})
}
public async findMyFollowings(user: User) {
return this.prismaService.follow.findMany({
where: { followerId: user.id },
orderBy: { createdAt: 'desc' },
include: { follower: true, following: true },
})
}
public async follow(user: User, channelId: string) {
const channel = await this.prismaService.user.findUnique({
where: { id: channelId },
})
if (!channel) {
throw new NotFoundException('Пользователь не найден')
}
if (channel.id === user.id) {
throw new ConflictException('Нельзя подписаться на себя')
}
const existingFollow = await this.prismaService.follow.findFirst({
where: {
followerId: user.id,
followingId: channel.id,
},
})
if (existingFollow) {
throw new ConflictException('Подписка уже существует')
}
return this.prismaService.follow.create({
data: {
followerId: user.id,
followingId: channel.id,
},
include: {
following: true,
follower: true,
},
})
}
public async unfollow(user: User, channelId: string) {
const channel = await this.prismaService.user.findUnique({
where: { id: channelId },
})
if (!channel) {
throw new NotFoundException('Пользователь не найден')
}
if (channel.id === user.id) {
throw new ConflictException('Нельзя отписаться от себя')
}
const existingFollow = await this.prismaService.follow.findFirst({
where: {
followerId: user.id,
followingId: channel.id,
},
})
if (!existingFollow) {
throw new ConflictException('Подписки не существует')
}
return this.prismaService.follow.delete({
where: { id: existingFollow.id },
include: {
following: true,
follower: true,
},
})
}
}

View File

@ -0,0 +1,27 @@
import { UserModel } from '@/src/module/auth/account/models/user.model'
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { Follow } from '@prisma/generated'
@ObjectType()
export class FollowModel implements Follow {
@Field(() => ID)
public id: string
@Field(() => UserModel)
public following: UserModel
@Field(() => ID)
public followingId: string
@Field(() => UserModel)
public follower: UserModel
@Field(() => ID)
public followerId: string
@Field(() => Date)
public createdAt: Date
@Field(() => Date)
public updatedAt: Date
}